From 96921da618e44efc53d173e420b9e2a6b2d6ca5d Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 18 Apr 2024 09:37:58 +0200 Subject: [PATCH 1/4] feat: removed old and introduced new structure --- .git_hooks/post-commit | 3 + .git_hooks/pre-commit | 5 +- .gitignore | 3 + README.md | 69 +- .../bec_client/plugins/LamNI/LamNI_logo.png | Bin 49460 -> 0 bytes .../bec_client/plugins/LamNI/__init__.py | 2 - .../bec_client/plugins/LamNI/bl_show_all.mac | 269 ---- .../plugins/LamNI/lamni_optics_mixin.py | 161 -- .../LamNI/load_additional_correction.py | 23 - .../plugins/LamNI/x_ray_eye_align.py | 1332 ----------------- .../bec_client/plugins/cSAXS/__init__.py | 1 - .../bec_client/plugins/cSAXS/beamline_info.py | 108 -- .../plugins/cSAXS/cSAXS_beamline.py | 28 - bin/open_tunnel.sh | 5 - bin/setup_bec.sh | 47 - bin/setup_bec_widgets.sh | 16 - csaxs_bec/__init__.py | 0 csaxs_bec/bec_ipython_client/__init__.py | 0 .../high_level_interface/__init__.py | 0 .../bec_ipython_client/plugins/__init__.py | 0 .../bec_ipython_client/startup/__init__.py | 0 .../startup/post_startup.py | 76 + .../bec_ipython_client/startup/pre_startup.py | 25 + csaxs_bec/bec_widgets/__init__.py | 0 csaxs_bec/dap_services/__init__.py | 0 csaxs_bec/device_configs/__init__.py | 0 csaxs_bec/devices/__init__.py | 0 csaxs_bec/scans/__init__.py | 0 deployment/autodeploy_versions | 11 - deployment/bec-server-config.yaml | 18 - deployment/deploy.sh | 27 - pyproject.toml | 55 + setup.cfg | 21 - setup.py | 7 - 34 files changed, 174 insertions(+), 2138 deletions(-) create mode 100755 .git_hooks/post-commit delete mode 100644 bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png delete mode 100644 bec_plugins/bec_client/plugins/LamNI/__init__.py delete mode 100644 bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac delete mode 100644 bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py delete mode 100755 bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py delete mode 100644 bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/__init__.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/beamline_info.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py delete mode 100755 bin/open_tunnel.sh delete mode 100755 bin/setup_bec.sh delete mode 100755 bin/setup_bec_widgets.sh create mode 100644 csaxs_bec/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/high_level_interface/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/startup/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/startup/post_startup.py create mode 100644 csaxs_bec/bec_ipython_client/startup/pre_startup.py create mode 100644 csaxs_bec/bec_widgets/__init__.py create mode 100644 csaxs_bec/dap_services/__init__.py create mode 100644 csaxs_bec/device_configs/__init__.py create mode 100644 csaxs_bec/devices/__init__.py create mode 100644 csaxs_bec/scans/__init__.py delete mode 100644 deployment/autodeploy_versions delete mode 100644 deployment/bec-server-config.yaml delete mode 100755 deployment/deploy.sh create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.git_hooks/post-commit b/.git_hooks/post-commit new file mode 100755 index 0000000..3fe80fe --- /dev/null +++ b/.git_hooks/post-commit @@ -0,0 +1,3 @@ +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +semantic-release changelog -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__ +semantic-release version -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__ \ No newline at end of file diff --git a/.git_hooks/pre-commit b/.git_hooks/pre-commit index 4dde641..392493b 100644 --- a/.git_hooks/pre-commit +++ b/.git_hooks/pre-commit @@ -1,2 +1,3 @@ -black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM) -git add $(git diff --cached --name-only --diff-filter=ACM) +black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py') +isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py') +git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py') diff --git a/.gitignore b/.gitignore index 74235c6..f4c73aa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ **/.pytest_cache **/*.egg* +# recovery_config files +recovery_config_* + # file writer data **.h5 diff --git a/README.md b/README.md index 00f0a12..757f5be 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,12 @@ You might want to run cSAXS copy scripts before in case you want to have the for ## Overview -1. Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.) -2. Install BEC -3. Start Epics iocs -4. Start BEC, BEC server and load/modify the device config with relevant hardware -5. BEC commands -6. Start BEC widgets (GUI for motor control, eiger live plot) -7. Troubleshooting and common problems +- Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.) +- Start Epics iocs +- Start BEC, BEC server and load/modify the device config with relevant hardware +- BEC commands -## 1. Clone cSAXS BEC repository +## Clone cSAXS BEC repository Clone the current cSAXS BEC repository from GIT into the new e-account. Create directory @@ -24,19 +21,9 @@ cd ~/Data10/software ``` Clone repository ```bash -git clone https://gitlab.psi.ch/bec/csaxs-bec.git +git clone https://gitlab.psi.ch/bec/csaxs_bec.git ``` - -## 2. Install BEC - -There is a bash sript in the followin directory. -Go to the directory and run the script on pc15543 logged in as the e-account (BEC server): -```bash -ssh pc15543 -cd ~/Data10/software/csaxs-bec/bin/ -./setup_bec.sh -``` -## 3. Start epics iocs +## Start epics iocs You can start up the iocs while the *./setup_bec.sh* script is running. Be aware though that the scripts requires you to interact with it. @@ -94,7 +81,7 @@ iocsh -7.0.6 startup.script ``` Be aware -7.0.6 is referring to the current epics version and might change in future (SLS 2.0) -## 4. Start BEC, BEC server and load device config +## Start BEC, BEC server and load device config Step 1 needs to have finished for continuing with these steps. What remains now is to start the bec server. Connect to pc15543 and open a new terminal to run: @@ -127,7 +114,7 @@ bec.config.save_current_session('~/Data10/software/current_config.yaml') ``` The second command is helpful if you adjust limits of motors, which will then be stored in the config and loaded if a reload of the configuration is needed. -## 5. BEC commands +## BEC commands A number of commands that are useful: @@ -147,41 +134,3 @@ scans.line_scan(dev.samx, -1, 1, dev.samy, -1, 1, steps=20, exp_time=0.5, readou scans.sgalil_grid(start_y = , end_y = , interval_y = , start_x=, end_x=, interval_x =, exp_time=0.5, readout_time=3e-3, relative=True) ``` -## 6. Start BEC widgets (GUI for motor control, eiger live plot) - -To start the BEC widgets, the first step is to make the bec_widgets_venv using the start startup script. -Follow the commands below: -``` bash -cd ~/Data10/software/csaxs-bec/bin -./setup_bec_widgets.sh -``` -Afterwards, activate the environment on either cons-01 comp-1/2 -``` bash -cd ~/Data10/software/ -source activate bec_widgets_venv/bin/activate -``` -Each Plot needs their own shell with activate environment - -1. Eiger Plot -``` bash -cd ~/Data10/software/bec-widgets/bec_widgets/examples/eiger_plot -python eiger_plot.py -``` -2. Motor Controller -``` bash -cd ~/Data10/software/bec-widgets/bec_widgets/examples/motor_movement -python motor_example.py --config csaxs_config.yaml -``` - -## 7. Troubleshooting and common problems - -Sometimes the data backend for the Eiger gets stuck or misses frames, this will result in an error -``` python -raise EigerTimeoutError( -ophyd_devices.epics.devices.eiger9m_csaxs.EigerTimeoutError: Reached timeout with detector state 1, std_daq state FINISHED and received frames of 100 for the file writer) -``` -This happens more likely after CTRL C of a scan. To recover from this more reliably, perform the an acquisition in burst mode with 100 frames, little exposure until no error message is raised after. This can be up to 3 times from former experience. -``` bash -scans.acquire(exp_time=0.02, frames_per_trigger=100, readout_time= 3e-3) -``` -Afterwards, you should be good to continue with 2D gridscans. diff --git a/bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png b/bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png deleted file mode 100644 index 965195cffb7ce55bced133089bc0f9ebaf0906ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49460 zcmeFaby!u~);|tgP*O@jKtgHh?ohg=OG>)Cb1M=Oo9+weg&L z&%Nio_q>0+@AvzAzK=W)?#0?`%{k_nV~+6|F{i;V*Q-ur)EaGKPbb431U5r=i@1m!=&RA#4VIk>x*! z0KYDYobi@XwG1l?F5~M%q$ej8dEd2OV8IjBJ^k=q3r>^ngNDWfPd$1Xn$1_AOB$r$ zbxvbfrL*9Mi zEt(}{f4DprvXkE4)b=;rxUVeD$X{!8??#>wE40sZ4~uj2q=aJ8lZF)0AfgA}GL!=0GU&%-JMambG5I3!& zFZfdlj3z9&7nP67drC*fy`@}0%AcW6qx;qN3=6~(_oXnE$vBxXDa4-@p_Gg0y1cCY zV)9aviq2v}oIW_V-S)mcz1~A?vegchp|;QED3?()4Ga6Pv{@0iXb|Vp(+NnQ_G65- zFLy2m8h&Qnf7|14K;kp!AGA=ucLsiTP;S`hW$--Z^=c!H5SiK>er}M%s|P2wv_jOP z1zc|@C`-xY2k{kp0&$f3Ah{t2hKtMgi(;B7We?r?&z@q!Ymi|w`yogZCfu#?dtcd{ zPukR_{TlvW(6hVW?|8m7sg;N-5<^P2e~)y9j77Q>c7z&Heeeu^#Ft>OJN<-AXH1K1 zGZH;bI0f~~v-@80ogT;%4V14tUXY&!y54`l7|M0;UDfscW?=O^yKG+Y3p0vV5(10X zxUXcd$XH??%2s_ow%3~>g7_);()88d+u`94w>|Nv_GkPe@`d9$*TR^}=jZzvisG+r zI!TK<#imX_wJ6$WkK=JPmYrqFKx+=4(FXhz7w8e-A6|!KF>jg zBDFljslBRiUN2&7wFd1Bo2olfE`mPYj~7I;i+}NkP7dQCJexjx#}Bj_Kd!xd&&be3 z-?&Af<+QA^E%4*NA<*a4fP43hRs(SiEL}!mb*H{Xw~Uk>O#YpM`wpX@%y+`tJEjU4 zZujrDXzw9E!^D0rW)?*9DpZ5aC5UYieD>s-Bm#B_1$n;Y-6x_HomNIzO7CJG$9Ae^ z;hp31grQe%r5Uqpj!u|&5?IAMWSKpC?id;jYM{i02bX`+KvoMM)j!riz3Njhvne9I z^uw_y>}%OxK)c_DW5*~+oF2l|LAS5tMm`sy_nGwjoilY%tiEs=xf|AmKe9jC7qsUr z87XCN?1JnFOR`D6Xg`URq}IZkUC@%HGQl#%ipSx?CJLes()lX!^%-_>mP7^ldBg{) zM@oYE302^#K&KR^#1RpX(ySt*qNSn-MKK>JRJ8I`N~*Ni6NZelbEVTeH;hm#@OhGY z$onZuDQu~0BAB8)JJV?hAJDzSpiGMr(-h8sKcXzJyrZN`M@N_Z)blAs9gn78z2wDQ zu9w<5_~0I{9IiHQTsH-7a5qY9>QibJoBW0xrTjP*UkenD^t#ttG6G_a3f`sC%FZv0 z3lCIQ^VRY>3v|`IGsM*@Uluai2XIVbRXr5f`d^Ts>R zt%naCk1ub=t#PjD4qxYW44@8u8`CZx&vI2D9j_TBEfo^+jlMCzzERZ5Y>=>*hz*?* z+V}`>p>K(4$^1<}`B~uS_Zg7}K^dKPZx(10w0VTMgt*43#Yx4<&?nP-7CRMBsEMgLtGO2EkNJ!Z7h4vUPGMT} zTUA(dPQ9!Uv(~X@vo5q^n#ypoaRJqbx#(mfX9`H5N{oj#8I->2uVN}It@Qma>>6aJ zu{N-@_bT)}A%>-}qY84qv9^)$qjq*bJUT}?VfcA2UaorI$G(@VPge^K^9|p0^bt+- zpJa!mqRtyff0I*Bvdr4V9#iJD3ThCpq-PyUuA4To^9oD3$ogE{AKPpFar{forH|pDJru)A_eyIOA z9hmP)Uu|xM(hdhCw#~G9VSRr{cAxvfUiflbQY$Z35MlsA9D>VT^ShJ?s&{8GIMMr% zN>E9V?9mf(9iGHs5|Y&M9^IXe=rta%xc6nIb;ZB>ZJ~e1TlD~;(6TU0i7L?#kp_{L zPj4gd{~&U&e9x49S$MTwf=b+ofI*Kx4-O;6CN*kQva%#?FfE_bUFy4XFS*K1qc*b^ zrN*hXs$J@z`-p6S8F} zh3vL6{ScBnm12_F?cM9rh#rhzkJn6_WYR}|ipW&?ypq$j@mqM`T%tfsbj)x>VN7z8 zr$!!a58g*Pwj?#~Vi_aqr+Cf^r%6pV7k=C?8^|%bl2gW0N)a&*h_NlZ6r%1SHX*7a z?wV5calgn@q@mwRFz0ie?8z1TEY{rf#nj%kx{oulQUBx(PY;jw>J!J|m5+UF$u(}e z{Z|p^^>{DvjF`^6Gk$!jcx#JC$zY*rRqJ-MC?WLrN=3a$i=MHm{HtfzO7@x3=b=7h z!+2|rlWKo;ho+J8s)nOOhn<5%A-?OE`-cPXj&M`yX4P!8#9mUqboQ>E-_S88G!`?4 zjER<*zx<}5U2)P|m%rH;TOVVqtDwoGIj@7IIN0#ntF-_!66U^1$V<~eizt~k?j@!A~+Dm(NeQ(&bA0eG*w9iV&XKZyV(ouKye)=i* zywm6H=Z1ln9U1dFzCt7GW0P)!*RKvK`(+q(e$<8QoYZ?ANPmxIQ=e34dKpqT<}9#q z(MuF<<21iCyVV%$xVgOjdOCflr&*>}+d<)4_uTu|TxOewg)7ljB6uQ$1g^IVGcLav;XI$4OznTj4i}ec~<3scJO^;DG@$W$opoNC| zUxfQ*_P=GQf79D+oI7t{mfO_t`F>sV$yTV5&b{>1XZ#)nW1BQuQ1F`l2iv-18-6kq zgI;U%9C0@>A-4V3N0W+hZ(ir*-r2LDh8r=0Gu!hKPo!yvcA-3JuI$D!Ulfb-h+OB8j1b=o!_rP!oYWM z&y+=^q<~LlLkDAH8%Hx+C)z^t8Q=!8ousBC92_1c>=#~2iDD0Ef81O}!%0I_LVWCo3$OR9XNhBUf|N&*vWv*&DzSwk=IS&$?X%oz%}eP(-X4WN1QALo@mIu zAQQ25Fec+?R` zA3s0oG2h!|M1nnzpCnJ>>y%m4fN?G_|N(J?d3my`P+;9Ot7K< zffYabe0vvQv>*yU(?2Fn5G6Huj{%s+N9LjmD!?ZoWw0N3GTLskp&!&3+Cs(oQ2iF}sTpoFV)|YvhYomT>jM?WD*CNjtW^GPNo0z}GfrKa7#b zukK(}tl#xC6~ey4y!+;JL@N%V$zzkqJMD`E&%%^XlrRW~bYt+eVral-uF6l6H|t8D z>W+0K+p|3(-w~L{*{5|gO7$dPyxCbj6C?-IEAS_+=T-F#qeOe(MFy4NpY4sP%#DKXrTiY)imhg1;pYVG+T~EXXqW z_1|_3bZhiCqXoJR!YRvmTtbQRf0D2NS0oQC7Js$6QdfU<@onemEbnrl00MoK>crH1kyG#C%BHuxklKGjtzDf*%aUFO&T$H4=&J?|g z6w!DUMeV#GDIU3e%It97;D@>#Z6d_Rwo3dL1xgl*5+>qDl$A`zs+M66T7f$TG8HP_v{M`kiNSJ0~Waj8nu%^{m zoy?qbmdLZio1jwFkF-d%Yv*IUvPL4=O`~MBIx7e_ktmkgv*TiTHz;1YjXuC|j%k$I zUn`W(7b8w27>w^}icu{VK$#ub%cY&`ZU%2BiWNY!FV@m4a`R&3es|I)= zm9VI&W-RI_Ob3#(l$2(mb*_ra5GiDX4f^O>pZ9_|=@GB-F{t=u_U?4>%b zpR^n;PmEW`fhlURH3Gk@OymE`3=oAq0cN1tYf^w^~@RCZxOen+fpH0cFzq@{WuzkMfO69%$Q-* zRtP3EPsP3%lVVE31}LDe#DG!Nzm5XBm3iA|uu-W5Wy^3OY_B+1!(_<+nTbvg zea^-1Rr}U;-o=z!LEYAZ+h;IUuW@!sm-JN2nZK{M{&uam7{X+&`hP{%EFr*ZKdP~F zoYnEUHg@&!a;h(1Sj+M}ALW`I&6@AA>rfze6-Gmb!KW5~P+C2u%{vLrSS)x@F%m`b zW7s+xln%IG+y`6XgQr6IaU$OgQBRYjAThjAE3#2isG4eq#Ne!yoRKIciBwUv(xkO68b@{6d+_=b<}}>GnUt7)%O%AgjU_^ZZZW{5y>CdH~o; z?Em9H;{c&4MyGURbct;c24jvE!33gckKgAM1xC3QkpsZwO7wWMeB6Rhmt<>kb z9PIOLj>=6?3ZJC%!6UR(AU(HDN&; zhB#ltikLk%O=0=KSp(p%e@lWN4_nTUNAMsFWDOA#Y=^X#OhhX)jrhLjRV6XwSb)nb z{0)@m%HV2yF|};yVW>{V{1#|aS^2zJw=$A+m6;Vo*Qkm5Ok<>dLl@{noiXoTudp2w^xZajp^L5m^c(3op~eTR!f z(rH)607tKpMg}=T9V0_-_4tV(j%;^ghtEFot2$v`H(-HPISiBTS$wL$9F zT|I{%-hM+gskZpZ5~4LlJTnzlGImnc@yHj^1w8ilAVUz?v7L~0jCmEwiZbF;#==Dd zpQer0!|U@RzDBo7+?FZZl24eF9!2P@Z((_!G9@y5@6!hD$&yKh&W;HFK_iW9@<7Ij zgyAGh&-8m#%SDn$G;}GhF>8vBeH0!?!TPMb4Q2}#!J5U1!%e2L-Ahy+F}Q@I4mm*I*QbfnoTyjltr5#klIa*l zBS|b<-5S9X4D>d`< z984t5rz9?<*Tco`T=-C zno#k3%NKRHLvxR&e<3oeyr#99sF9Fa&|*SeLC@aNE;T+{Jsy0 zE(Mee#8$qA4_Hk8U2*~OPW~<)@J<9-UV{Enh-Umgos^I&j-mo-GnkVlabSkqmrp&Uk`^E@C&lqDd_jB8U;|E;un zOCAET;o|&9K4*!Gji2!J+W7RK>tGu1B3;jEb$plf+8*Lz7}b$!(~^A4GhJ}j@$BKw z9GI?Y9o6WHq=?@Z zn|n+w0VPt_GPwb<6q{6MFdB?Q>yU@+(A}O@kxAu%0 zYH&Wpgtt2G`{``B`H(B_-WA1dtZ*9@;QV|m?XIw3&dBj3LJkhIYv4!NB+a07b`-ZA z*viIX`%s~Fu9xw_?qWW~VK!}dN+B2i{uth}Q{;Icj$4%w30Upr z9XY*vFIn3y6oed1pv{6`ZT@%p*&zI{IYYZZQYa7ax~|W$oa^z9$z1omo(ogDSHZ%i zRuixw=KHlpqB#SnwqJK zQ4(KLqz(X+memI+@&YMMxz3-eDjG1rq1XsR2a#prS&0-&qUZpCi&_Dszeu?cYeHJT ze6&8->+t82tRW(ZBO;6wg7^%jHu~@xBQBec>jn++OA`YrOaSl|2~S<@xSvkQ%O47! zKdj41M5A-^7aq@Et~!~TGkq<^!#2d8V zo#?JxW%sG2NUirLOYR7CgTe|>{8utJc(E;%IH(!r^<~NI`ZPq^30EG0ixF74*k7i* zQh$>}%@j&^&c(O=2-3Lh<-q?S3aMUbxXpd3p-_|ns)=rz;}ZG&VH9-|TA|a@03x=d zmZSCVVM|sDZZL`Q+rw~d0gJvwQHUUx>`E#=!%GI1LJr5N(@?3z8Bs{Z^UNer{(0V~ z@T{bfo?uIyhd7ccY-17S;XD%hbl^^O^j#)B7X@)HR(e5i&(`RjrK|d4xG^8cwzo7x zhSuCnuoVwh1w{gIW46Gh*XN~Rz)~btD=RUN9k??^?d9|(H#04@SeAt5Vo|0fGtCaK813&GJ~z~lkurI2U^ z4a@+;-1vJd&aNj{YbWFSj~dq)=jMtocZzlKKQY@nOZ-eXHwrHS@_~;dfBx{}lOZFG z9P)dNn2E#hEImm9(2*5%L?e^p{9s+tzEAWPbZi2$`YKYhIN_%r6!(asBvgIyyP1C_ z|1t>x9ZN9KQT;pU2)cUraVN*q)p;(JiXbt?Hk2 zMDzTw&@qi<^m}5`rWxH}v@{(ircKG#+MhZ5w435P;{4|1fth&$zv(&okc3|vydWtK zsM;lDK}O6$=1;E3)~m znMcf1T;Kt8bkc;MC|IX4f8h+k;m0oX7v&c|ZbcvM1Aq_)1FFXSx1Ig(iYRCjtbpf4 zp#b<$0tQM+d%&$2@^c!23#{l2sV`XG1TJ7H^DjWVPZ5_gkJcpqVFjd#NwNb6b&+8P zG12zcAYL6=lHieYas6%)d4p_CYNI(wex@7Cs6k_=6_NaIF`(ASP&BzmT{scL1_}TU zfZbY_6qJVed^IwQvJ=${(9s7-p%Dm`hX0L}-#kgBK#6sODlU-F#;(x<#($%NNfM}F zdT&DW_D^A(f6q0Z(ddi3q4>-A=)XK_58`D1S}pnvrdn=I=znzqSVh+V_`oemUy&yU zFvqbGVJ^vkrje7q^FKcTBLtHgNTt4%c_8{XjUgaS43ORVuSow#Kf)=%DC`*&G5?m+ z0Q1oPh9N+!znJ%zQuB*>f0D0XlJ_T;+^))B%l1#2?U&B`6H9Jw$^R9DktU1}UCr@4 zy@;Rf8vLM5e~Y+gXTOf~8-w|8T}-4$AN-uFw|F5#t$7b3AVZm?ECfjuj|Sr_Q2Aj9 zAd7wBbLNAG;YPrPqDNNfk?{mFRW>qYh}<}fsa2ktAc_0AzHCbhtuqV)U6PdrH3uh; z&rTj@Vk8q>0LbTt$74{ZLEt+BI{zYhp)d%65T~pp4*iFs$HQ3AfuhF|Ih9DXKlz1{ z3s_=(5b_)T03ZnfM5G}xkxgiPpa0mm2#y5cU?;F;-v>xGV?sE9mGW)r3Z7RbE?ml? z-+K9{AgChO=O2nzbAaUq$AZR2qXGb=qJq+S+Xb2&cKDjiBq9k6#Qg+W0PeblI7;Fd ztXl}tL;e!$k*q`U2Gj`vGJ4ERrQa9B%P)ShHPg+%Gf59S3NFLDOe&+LR0Y?kS+nqJsP?Ku7d`mDYl>ZT?8D%p{f?x zXzveHyCQD}ab(3-hsP}f zE}M>Glh`&-HzV{k`3fD9(gg`i8@B+B_j5i zie_UoS2(tr*!vUSSLo)Hv>R3$0DA{l8rK=+(~IYN%4SCFt_tc?Hj@c0n0hjG<*~_x z-#-HvY!q!@?I78O~6t18f~ zv=PR?8*p+vtAT^k;0?=yqLQsJ9H2rguP$6eL@PL$cN(uO!=T7_e08?VVff2hzv=9H zTXMO=)a<7M)WaQ{R%!qxlZn4y0XU9>bLVXl?F+}IBg!N;OU~|Z1vk8pd92C(IL6rs zXxU&=M4ZTGY--^V6FWkbSTEvvt3z7Ns)e@!o~xg>6M%}5MqWx3Juc1?O$|ex7+@Z1 zIgMwkr_LlIz)NpxT##~gxS8#w-riH`ed``rvychH*W@qk0fq-lfuE1z=5gy?VD~0E z6D>P)aItrqlb;vXrgcdsVgYiy)tN%8tvr}XpEcJxL;U-MlY?T%RWym$!<1peu7wR7 zIha=)Kd&rfp=S2Dt-t<9H75!^7aIS`>hR9esGid}7sPUyA61c$1fqPz2zaYK3z}$W-EzHyOR`q#5gPPL`5pq5JM_>d-AwC0*hc%-i=l1x z#|qxn*~7y#RoC+!d%Zh@aS}hw)dk@}_Z|<70mFC1%s^}E`nV#4Vcv((VLmi8)m@Mx zPZ)qp%1BO$28k2sp2{WclBAuElhE{Rj9i@^NW1l7xl4Jj9o~>JHx3zTBDK*;-oPkVEfasyi&K(aw6$0%6!0frXlxA;N}$ z^!Mgl z$;meekctm~Aj^RLfuyFgvGW9x1X9(Lh%%ULN`>$e;lj*MJ0*QjaU!6{;ej~A*yybn z{>!%gvTeU?+b`Spcebr_e!%Xqj?dG_>C%!kT-OQT*4D%x@-pbDN6+h>CxGTk(0;#o zYtbroY%Rg>4zo*VQzq-)1RlcU0%#)atE_mp_6$6?0hv7+fF=p0CAChyFSy1RPJ*jl*FHP)UT2a>a0OI;NtB$2R{ z*P{V0_6qJ^3T-2uN6g!YVkjzofxcWJvSKERu$~hmAF5E2QzAniLMkHO0|2f!4@kBv z*)F+rRhj+_Pl_WvEzPgs9U{sFbmj|t;_`qhY;0#v#=Xp@o3}!A2sgcp{WnXb^gbu6 zP?xrIg+PcN10%E)D12#~zk0Jj&LeZ{x}m`LC%$g=Yk31&>3Jini%CY73onPn^h~__ z?mmEb13*BC_Fn?OpFkB!(?qdYX3ts(hn~leOf&&;I*jAJc_?X!-c=^lVZYG#e0Rrp zc|WA>oD2G6Q31NPe!WUedFvP&g3{OwdHb*)q9m{Z&dmYB5_>7NIWE9l0J#JXz7Ph& z&&U#h--Y-SGtsCEt2ugRvVs_p0Hd%LMMwl}mNiJddvgQTyc&fbHPhd$LT`LEuFj#C zu(Bau&4#Ppo8&g%YrRy)n@%paqRcf8Sgf&3y2a5XHuML=4nDK#j;lSpjIey=MWjYp zXp9cv>petAH2SyruLGhHM-A6g@`2ZW99eG}^UDnYa7<{EHgvmvLJ-0f5GMTihq|8oA}zcQ54~OD5psZ< zUs=jcM-0^#E_SO1qGhP|RiWP1YDhD*5PJE8FFl>1Q{#4+M!U!k!gwU~jINT56H#2A{Sx^m;tt&(;E6ORmZ#YpBys3G+$P>rC)8Q zGu&KV#uGE7rC;xc_+Ed5TCq%=maQK*rok+K4pv!{!jBE*Q{@(~$^NFg!gZkYbt`XH zcu*j%zK%doQ>o|pLHTXnFMvFFE2P`)K?q}5WdTK*OB@KC*Z_zjFKJsmg>K*jRnM2r zT2~jBrvln3x>xBTz9+-bqrTjkN$={j{l%?~={6?ln$V|1UJ;lklmxoYb$U+BfRw<* z;3O3$1ti~p9MIT()fe`M$XsPq?lbin@w`_Qao{3WUyDq@rM*92MWMw&i97b|9Oyp&eKu^hb zxXt3P$~BHRsk&#?74U_4p!gxsZ3SqW9#?sA^*9jC)0cAM&_2DpUhh zQP0 z+?wj21zfaL-#jd!ONuUj5+#AFvXeyQKp-)zs1S%GVQ2Q8AmHs&&QFq+Igo!I;W{|^ zz<2GV+}44PUdZ`$=4GmLe|$0tUVq(i2v>bEeVbv8>+&!XCO11ctGeexH-{;o%NVG5 zq9vhq&gzL5M5mdMBfmvRnB|<12sze0r?KUXRnLSTRST&s;6==3zY%K|_ zZfY|qmMQU2{Q9M#fV%@Quw^;IJz6H-q)zlXTyMHJ9!c&8<&m2;9hnC)BNt@$p>;J{ zhzh=5k@c6bd0C*A!$m0sq>juV3=(ZxC{euATFU%>=7VX#N?$mIMM5b6d4bWNSivHW zK!_crp9p*a755EgTk(fQK94Ga$Je(DBb=8yG+oZaX5vT8Q-JMMUG!PL= zR*~`8slg8=54NJBzQ_jfafhck0 z#_|#>$`3_iuzHsmeb%CaIl%1_fXKoKVG>8s!Uh6)#g7>%#fxEt?CB5h*r>pk@6J{e zFD$b~Y~$&+=YY{jW?WG_PaE5MTG8ab@SPc;?1_muey#<4g7!B*iqf6xUS z%K;-sZC7R@C4VQ9D0U-uU2YNto1i=o+oD1#`-dY>Nbf`?_^K1DsjW&LhmL&Oj#Xb) z91?YN>x+?Nc`}e7d<@j(4r#oKnSKv*H95Bd^_A3Y^P#l*;>#6T0zP2vzZ-yq01@%A z&3xkFXlv|zKgUg_l-vrKnLL{V+Nj?oxp*$Vj7NEvoa^Z*-EeXRB9L;+Qaqm1P>)_@ z&Jz>7OThvy;e~N?gCO5=ny$1vGb?xx@X?cBiN0q6Z@`iy?E_i4x9htBBvm8^qe)jOM_&Gk4x2dr+%!!P|DFnHO!GTy@08>FEO8gWoJ`R zmnnk_IGyBmhv148{wcdEz{L7B`Ro1&hyg|pD}io)7&$UYn(HUcL{`!?wol&b1YjK$ z*=iuB>M}{0E(VnAy#c7dIWRsoI_X7A=Txvd+@#6TN~n1k$psunqAvnQ9%tRW3}~#q zJV5qzJXJakC%q7Q5Pn-|>A9;-&7(1G=&p=2I=JPvu0zGD8C7~+=|XJoXkuL(tjDdW zmz2te<1RbX)_Qg})hMMcV&VBne1pU_9_P5d?xNP^Yom-{07GJ@kz0m%_{hm#!?*k0 z{^xrsT{evyNL#Y{A7Dk;mG{rdK-#E}wgLinGJGRbtt#KxnoaOl3j!I=6GWTMM93Kd@QhagabRaOz=>atsM!GXn9 zT3@+Z)BNLd?dU`DQAimbV-H<^3gB_I+;|lQaKEYal!N?q-7b4;Ne2okP_Eq2N0F`w>%a5>qpKGrv# zxw|%u`GR>)^h`?}cUntb-H!2Rk9)~^j{y@WP}kUA5Qeh3-9%XLLr!8?^BvfMg-{nT zR`EuAetNx^A1b2!4mjNfgI=$)H@+$SL39i+$y#sV@L^A4;AX-qZrG9WN1oxJ4*mtC zdTGAKL{oJ04sv=)wwPf_fU#^w3r#Ye9WS!6q74>sh|N1%4{bgmh5A_Wov-nmiu0{g zfIUEAML;gc1IvE-4NiiuNNXx`Q-YUat6@{z#Y$`Qw}!QFpFHbP)0uN;oI=2)(5uP0 zh0gjp0$A>hY@kQW#c$J-t?@h}lcx!Vy$v|%Xb{n={d{OV@>P^lhncNlYKmcBk#fhQ zQu7Z?Y<-DT{3}S}-#N54dLLOz%8-EVld7_)0CSwme#elMkyIp=N;be)!6l0lqrblE zRoTIt*iWKh$TJ z>ph!R7WzKa0T8OaWF(+tf|y$ZSa|i#w#?4fyL`@Wo1WVJiG0`R28Ii%vc(>Zr7Q%> z{7M@Ssq2R!sylpVrx`yP-R@b*@0lxs)04OyYXP;&WOBV=Sx7N&&2+KYc-dS$H`BF+ zU@56h;>@bNSzvv%A}?NCSlK2f7%*a;fVP>o0es~;hTUJE|7yQ=^uq2|q!K0PC7ckrTbg3v}_3K@aL-%Q_bhdQ8 zqLq1~wQog^Iq;Aq`+Se>b`Q35!l49-(XAfu{kt&s^F2t{Q4kbtJv$n|D;prD;FiS# z%m8ww-Mj=>6kuO3lU5&sF~N6X8AwUB7<(F+cw0$c-fmetgt8v~o^k}VT1W_?=FHCeu6hF>P$j#TtjWA|J$}EuN>RJGcTO> zz-CftLw)Vd_6c&Lx4~SKQ92L^1%q4qU<#$>to)wKTyh5<_CXT>kjKjJ9cDv12e4GvG4!yrTPtQ5lmi_uJe*WQn0zAu?p1dw0> zep6|Ea?m}pvSH^@LI=2D`w(=LM-u@7yLE@r)HTrR)q?p%&aQpx*{*ZfX$hlT)WehI zGX%iV;g{B51$cR_=v})j$G6+-jc>lD@vcLDbK=eUkc3$-m9`OUb~uBL0n>claMRZ3 zl*#-KU8Ou4(7yq zodr6N-`Co|^YXK1c7-lNtH0}^p&cIc%h}PuoLJC>>@!bsQEWM z00M8s5l)`z_W?6jfe~zvaf>-Mhr7d2ZoQQBRGpjSTaQ5KvKm)tr?14PlAq_gp9g@qt8G!nxBoI>A3Jk?51D9OEq zkJ~nyw$ef{^l8=(;~ZhL2_Q=TNrOgi`z_@B-8p-C8-N3IK!i1c3MLp=wd~ZTXqRIL z>a$xhXxnXA1)MIfV9A&cE zD+#PlldK+1h2F7v@s%Dcl1dkMA;!3Yw&$5IV zYL79SG;(7H5b4}Ybx{JhwzogKXUn>G?f1HysyW~i6^-?WkUlSS-#rAqBQcyRoK{iv z<-N8$cZi*}2EgWbOK2-~a(7kp<3FmX=4Ms)PYn$ZNM4Ra_VM;>ndYyYBxQpoD`8IozL!VBpa;9dZzWWi{S%nl6d^+>!CtgA~}HqkTE z>%O??J#jqQcXT(+Ti7mL{xL3|-dyQ%r9VOu9$pd;&I#_q;Nd=U8so;+{YiCHe0WJB z6-jZl7ga|x2N$wuXAz015qzO+XH}V4tt#Fi!$??o(+I%YaJ_)r5cRtC4lNw z;b<7uIfLwaj3ZJcM|fyQ20M9| zIYYQmH>Vk*@TzcTD|o90(j1fi<@=YuR00y;6J>Yk0;%Vm!D~4-5C2MYED442Z+^pH zC@29XBRNV2twj87{`#3Shi*m^I8g^Sj+GZbK};&7^X2*B3iW!nu`HPUK3 z)aZ*m_nMwHfUUNg{NiH;f?j?6QJ&AgZy{(MVF@g4b+;oQQ%3l{H&`t}3W@gZZQZec zH5WU&T_d0=y9W@h679Lg;|$w-X=eeZYLxZ$Rh>#&GVwK{g^CP^k~}j%;O4E@y_?b{ z_x$dCCX-O|We!J;f^7w=ei_s8o+T3NVNW%QZ&Ed-FO86t#ve%ohOy!0doiZrdA&3U>8Vv@ zGAZP=^)R#slzMdxBXHv)#c>vZa%~*hCTyZlIDFNh3vZs z#uwi!?h(2)tFHoGH<(>)fC@Wk0gXsj=y#LG!NwG_dEoq$<`Ud^m>2f^u zSSViG)x<4vA))allVO#^&SsK%GXA)%wyi|6E->=B39K}{0rm8gPex zzUgm+UdKZX_Vu>Su6&{6(1tTx=q1#6zkhdnoA3Ny6Gf4$0 z>kj<5%snaWS5M)`Wec2;0^q=Gm;>tlWWhLTuP+iX$F*^4G2y+Jo?~ybBg;~}50pjN zv_=knP&=U*zDw<+ZX+}kQ`2f)lzt(&6No(5e0|YGkN@r9WTkYuvUIy?tHS;S0m+H% zeY0x>#p4_-yT&Z3x5B(wcD+tfGzQpY17UtmQ*s3+$SU4FUv}Oj^hpbo@)SDp5`cOU zk0MrO0n^zY>X%+FhdTaoDNfsprK zSIqz8eQQ8ZSJ}Hf_`4Q`_5J<80azL?KMO1V@%~?2Od&lqGyAbc^A}(JE7dJXfbu0T z+mikp8WE7Hu$Tu1+9wtN_5FVzHEfAu0W1>NpN##B@BWo*X5{+sGr}q}|05r7xknDL zD72-5?qA>k3(gLRw+M>>YyR)gU>65i^bl;U{}3;`@M zMAH(#^1{q?TM_~3Wq@Ck2Z6iKwq^ljyxU#J626+S&0Q2?(3Ztn{#FMOg?72>~dVf-;;cGb84Y#b1Kj7MZ3Al1-WD>9dDBq2}gVP8qm#`h)1W_j32Sd z_E_NsmW<`&>*6nJM|${dfn+!KfO>9}00eL~RwFxPxKXPKD|^U2(CDbDgBcJ!FzvQa zA}Co-C;{DM)wClNJ387rOK|hhjGH^p4)F4TKbm%H=+z4q?(HlwbHZK1jy!Ln09{;C z6wSr7v>~lM#*f}mPtChcpQdI~4fb9n}>s7I!2WG}K_pm$Q02@W#)w zCXhb3oD0y+nXDh#^2%V&oxnU-Ih%Ji_gVFW%=GhORcE zRY*&74q@lYZ{ZH8pH@tq1^QjQKxstE8%?17a}Pp54~7nSP2#T2_6IFz|y{h+)fwd(doW?IAWS9D9^BQjf7e21n+%3ME8CDP@Poa^30qB z5Cq51f0`N>8URAKN!9zF5ijP@clgYH&8X*YIIDHJ6*F^S=FPnu`M{3&10sC_tsSZ9 z7uVIFj&y+`L6`n^U4mLaRPT1_bwc|7WXgM_Fz5+8nl&O!j{w_qq$HL z4+jC8*(<&tI4sE*(ZY+&0FzpE{c-LL$j!ro&v zq?@(t{JDnCOFQy2+!}f+%o}!?6^UJ|47~FQ4`Iy5b;<96Gq2fSO-G|lz=T_=q7`u9r(x<*!6uF<|xK7AZJw7>Mx zX4$Z3ct5NCZ;pdgNpZd|7-?-yf89Oi(Yr21bEar=AVw}J!uVC_PoyHuX`m+J^EK2D zyxBx=(L+RQYVo+Y`uxv08KE5*Mg=a7CgP>hdzrj6-4u~)np<90IA{iQ7mL3%7o=>J z-o@E$xj&J6RonTxqiZQVAsa}ROssN6>e8&+#cNxs&1LzgN0wg2iNrx#>8EC|UO4^| zrgK}+-H7*N$_A83mIcY6+mQ5oWaxUAzqT4DV~ya42Vw|*Dt#_{99mN;3VSwbQ&Z1Y z2hp1pp={l`i*xFqO|GW!1aBbSfhLu-9xJ9g4h@T3yr&tl$J*PFqO9cgc<^E)Ii73M z1p@%3(NsG8~*vC6u+5M6}K))LJ~WTbLP~^r6j6(jL8PCY$aUbY)y)#Tl6c?MCgC*obLn z(Qbt?zIT2(8P`f|iLb%qDqxH*8za+UB`H(Vb@^R!DLd0BR<%5&w8^B13h`;GOzbuA z>EFIIyq)6y^A3y-ljOz3UOK@N%`UqjqvTZArfa9JBgMgtC!7)_v3LQwrCVPlK{|`! zqG6SVjFZeZQRc{wTps1V$ASNPi5@2u+UmjTJ=Cu*LL;5sBrL6KI-L`F?Yy#I^AWsE zlmVkB)^X$yZJmBByT$&*%+d-rxm%{9`*$Vf8IplSlfF@U3z(Ax5!>!C!fsL;TB;Jw z*?~lF_53uYKHdAk1KXm<4P8^IVADaT?V)oTdDpR#+W{u(e#yp_8KnkCYVE)j172C) zyEjQD!rDFh#+jMdwDc04cXV@d*UxG*`&dLZE;v~{-+N}q3X3xzXBT?lf}n7jC-z!EOgAJY<;KuC?&^(&)t_Xt*z~i?~YXDXp!G zW%B-b1Z|=Ok?^=XTsEX^tA*FZ)CRO0O3GY+E@2Ft}J<=hV znVtHJsgyN60YuggaJi$HID}fV$6#6YD$1lWs7^`5(NU|t&Qkt9Z@{N7ws7sk`sN8b z>XAM&tXC{~5bHxWSG+nt@5MitPEO$)qZ6(C%;IdtmGx~pd`|+)^!=&N-llPl%fcq2 zwB|x{?X(;1pg<;p8=x~P)2D@)JO)5`cK;Q^PLd?@jaTUaozo{HTg(#{!zGc|E1VsY z;X8gq-RFyEJQ*i%HO9(6B21C|+=;$jy}n{E{r#}kUN zl+WS~nEPS_S})cTY+z4%$FQE=GI-!)na*f->>A&-wa$;`j#8ccA`XSBMC7n3#VazY zA}y1>*t;rZDa(LGXyecuH$i2qKVav?mM~Cw9)KsjXH*yhOtZZYYvQfjDLrQIRmKWc z2xcCnRXW=3;^aE@1-_YaGN*Q6M7bl)6V3bgE_{5rFxgCTJO7DJ)dxH5w3+GJcWq?@ z+x$U0^y#}{k&i{gx$qmCN}GeX00kgzdMcbw$=)*L&=7Ad0@w@4xXq^HtseRgH!%+T zcr1x91;C!4fzankbWF?E_8elO2OSCeacU3jh3?gT*~nKua*yNj1Crn6u!dpPJx>(N zDQZ0I4LwT*_f?mNKk?bCS8n!Xh9flG@*coZ4jxWlvqo;C{AvCsHC|hXxw15h-6cKB zr`l2+l!@2#Z zBuVQw+kGO=jxVL;9RI+&S$v5{*1Jc(?IZncm8xoFUIqbw8UjWT6 zSKmGDug~A~DH%7BnF-Mu2PyO;fNN*HRM<_wGi=)uBneO(t!>zU35fguVjsEZ%xQVaoV~gFeP5RkM)-fb9xt#7k#j ztCm!Mlk8nSIQj~CnZBo9=-kLTkGrLsQwZE+NtKSjfPGuP*f-R(JU>rRI->O2dO4-C zOUr5Zv%)EEj>k>2YrEnjUiW0xw7xyLg-*Y}9(&~wqQl#SBK4n0e_%2J@w?@JXCo$( z2(@U)8bbl>w^an~v?%UY(2@H6iProR1)XMS|F)ITq2B3q)=0FbPcu%*kKC$J7xTr` z63TtSnW)Y*v6s0+K6M;L@C)P3&zn)cGU_yB{?OgfAirm~YV49OJ{ovSh_y`?9i>fS*0j^;%ffN|ZZ0gk^{2UixZ{+y`)bMdbFPnYT5)9gW%6%GDv0H(smc z$l(nL$o;&_rQaqhJ=j2ckGAp-ACwby?z=XiW7IQJF!l`MCH|g|Er;cj^aDLr!P7YG zIv|SJtM?(2QH0wLVQ;m|-ilQ%#G~ilVcE|>)09wf<}op z_UFyRR~Vz${6>ZNIP^mNf&djCk_Q{vBSFHOSjH1*ll7*DsxNDjRdOT_v;0;=A2GzL zk6>T_=ZM3IO6eO&nh-Tz8Z4G_8;;2_IlG#dZTcGS)rb_Y+6>A-Jbk!)bw zbqLd|+}dc8s0k}8dXRRM`6$0`P5@CU84$)6sWzWpkL2065aXVRh|tW>c^pX8q)+xu zCBM>E4ef+4ldYrFw-qK^0w%j~y#8Ev_tFU!ADvG~*x_wzX}dZjN=Up$$b}V^uaRQO z5xJkCKkR&jhTUZBUXDKjk<5AYxS`7j1>gl|%=P$N!v`5pARIRusbayd$I2!Rrkdn46=uALn(bO9XbX*b|AG z;Ma%ac8?JS!bG5wE_x3t6rELqZ@-Mv45Ei$aZeOIx5v?{ma)z6Si1pL-ItN3awd@c6xJ zwYwnN_;?ZkuD+*5goZum5m4IV(Nt@((Q}F?$q+B&|7CmAp??M@nVDfdmu%5co?&5g zvO^7RuC|=y;RQzEkUvPctomz1Ad5-S6`b>g@VL;GIrorCc1T2}PY}$u=v>s5I>OH# zHkx(VAoGvuKXU@Ml8(I)KbYYk%1^s;DK;mIE_VdVX75Zv)l2Uni*|8l7IHLzEZXSq zRKF->(TAw(SvVd$LKgjO-J7UpL;i-JW}SE;i`M^h79C$wY7J7552fyQc8q(|bn7g4 zpe$t2ggx-hd(t?!k!}G`Q}bIg%Rz4>I>3Oq;ET06_conQr39QR9;s$(u@@TnQ~oSP zKspaScRa1TF|c?oau>Kl6_sdGmjBcOGKd<L-%kqj>eiEO}pUkgB@&TdZJ_epj+c=1K@F9P3X_)9^DC_C^BW%~cAiyQ($ zNU#8v?CN*bl6DX*+BMt}gY%!i^##1B5xSQ2h1&e@JAh<}%`BAfbP*h$7hXKAx0U02 zqamCENyi-D8~mLv!l4#;ajVXD#_y{tXJNNKE2Q*&9qlx{xUvlYjWG1B)kw(KZd@gP zcQgWjJLPw5^tV%fXEpxql;44&{{^IU|9RV%#ezTSE|w)yW~z&Ki_9cz`bKnS9li41 zvR&G%-ty%P@vY%#tflgv(u-;T<&7BBDYW$dj$d0I27w_Yl-Ic%iAkH-ak`17X~7-k z;h9(F+E1`HxT~{^Jo1-djOT)Xb5Mrhw%yXCgvOGv&ep<7>41s(o{VD!U5ZWPckgHS zD2?V9nMO?Uoqp4Fc;r_|t;#T-M(4_qd{e5A$%;xX^CkXF-T|B%&|5rGi4=8NoKg~@ zSPu`9i}5Pj!$&;v5dUb~vm38b5`}+n#Wj_s+md`%uY{ccy#Ukb(t)!WZ1TWdJA9?o zZB_AoNk+Pi6VK`RTzv~_#3T(m55b-LDsQ3ai8oj#Y8Wp_BCpn_QZg&XuM?+F>3M-& z_QJo{LLUrHuDc-H=!kf~ce}Pf*7{MW1 zw&5>Y)QzOFn)g|ohIkFceB|Z>su7o$$N2}Ll3&TQWV9SiD3BP@>RS32oADyC!Nl?V zBov6%J~Z3oG?c~Em2#apBX$7*S82_Vwd*Enq)Lt7x^hN@Iu3KvcMu|S$cX+}H-vAx z4puyp-J)^MqSk=meOAKHRw;=&w4-bzrh#P$uS+IQKTu#sO*};`V@^A(93St|_Sq)^ zYCwE?zZk7;JA7GeEF#eVcvj8xxdhp+#jSyr!@|WOuy5bBqFT=t{wma%LI{V1zyCfz z4GV$@{(v=bYlktQoHTUB9=oGLEwYkH!{-Y762j*i^@r{0izktJmG>?iu@!|x2BjmW zuEw|a40tXM@I=f#Qe@x?RP)_H+i|#1II!|`Lro!MTBk8lAWZn2ZXLYTuwOt6@=j-i zTXSTO>OSn996>^&C`lZsj`Q`1NHWcdYzcl9vQI$EdnnCy7u`mn+&$&Zy+!Zgn{A|i zhV_N{`#lho@G*8g-?`9pX5l2?$D+xKn3ah8v0~Y~F}hY${AnQNl}vEu=|fe8P_Y+` zsYV?MJ_bKv%dOD@3IElKWEBfNPLJ|ZSd~Ku1au)Gi+O;u-&^V;u8w(il0m5|(=AVU zBU?oBI5eaZv;X*6c#=#q!pa{Mm?f0epVQ4U82ISZF#FW8!wwXnr(zXSvH?n_lD3Co zqCcz=8U}jzkdHXav_McQkQ*%P4tSR*3(>yEf zN-(=2cCa7IkSpkTi!e&^lTLkr(%!rAw(W7dqd|yS-N#x|XEiCG{-G%HCsN?Xs+YiN z1WFP`d(sH^(Dd#Qq+wk`s__RPy#rk>3Qg}NG`+D_JJasuTAwFr({87{Y^8fV;Ub#e zZK1^Lbw~gUfMPY~4WMC`*Z}F$9qr)rhTS(X`QHwyV95sivwO&deau_1_lEoW1tp zofMu}P=6RXAt7VkUI*)uVd6tcozT?hk=1U{Xor#2N)TL<1#22=;8 zucN>)bWZzh)Gvu`Ul+?MHfS?_hKg-?hOGGYkDNz*~m8Jj%eW&wO=BX@6LZtzI;;t@(afjKJ_ zZ9d^3$HZPOBeRlMY}@(fqRI1ztz92odhLMf(22TAQ+=;|qBN-!tP%XE)N-xRsP&we zcGw2eZy2DFaUsbTEktc~JqizEz|xpP5W%a7yfr|>UXR(nRve=_{MB1;A#c^9zYX3x zxnAxyo$LFM_^kHcjrg2d!`Y%G7ms;&QSyikw`k8t7mwxP%VG&*3BL}OHbi^y$v;KE zBO!l{&>H{x;azY_FGR>0E$EYF{r{p8wkZv90ctjF^34=E~?y#?-9VW)JT+JA3Cl+LvXzq5T_oPvmPp9QW*>ls;*)i=?SqU zdoF6{rb=%9GPl$hy0p_}HLfxc=_*gSfZe{_85^YJcLr{%caWyzv)PtW|iRH=+6!%{MaOAkp26P&N zHDi6^S6H(XNz6V#l^rLGLKVa|(gB9`w$hpxR@z9-nX&*^RePh2Hg?>=khsDI_`W26 z?c-ild|&)ZXBnf{#qI-NRz$wcJHx6?4b2&_!q^`93Mg`1jfQyIiY)*;?~wd#Es9oT zTSa;C?6!gP4O*iPh6B`zEH=7&H#+lSPxU>p@RasdWp^c)wLaQFNxsm6x!te>YB7}I zIDyquknftFGE;aS0<^qh^hEOAAabwHnNxbib6Pt{xNkN>ae$UOb#|PL1+s54j~&@} zEuBt{51m8jQE>jajbB1=u5Tryt9U;tub41$xxO?yLgxrUM;(8z6#n})$O(sK)xFxT`1bH7Qv3rr<(5K(@lmlZkQs%Vxv zPpj3I!W}*{D+ZBI>jgAE*p?c5vhNe2~wMGh(?h(t&J&09z1mvJh;mXV%Pk; z*9cPIIPhu|+}!4-@W8#XcNlGOp8@l~3MPE}C+Y%Y7IroVSIU~eq?QV=yMSUUElGpH zc!R_{;9PY__EYgmZV;B`3laow5Qs>rV};1We)=W^ap$5;67PJ)=~F1Cx|fKj@I3z9 zY!UMAp|Rm(_`{Pf$Cj@^r?FBMu-R0It)YR$nTUr%Bx^9s|4X0>;+FD zDmd32s=1|DLVw-#(XW$PCjpT$>vTUBpAVr&>Gf|zbnSm+$M7UQp)V%LR19Jl&PXXt zwTFtKa=!6Kakr-jw|O}5>)k8aVS=e z!=>SxObT%2≶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 8) { - text_bf - } - printf("%.3f mm\n",gap) - text_non_bf - - if (!_id_loss_rate_ok()) { - id_loss_rate - } - - bl_shutter_status - - if (_bl_cvd_filter_open()) { - text_bf - printf("CVD diamond filter : open / out\n") - text_non_bf - } - - if (!_bl_xbox_valve_es1_open()) { - bl_xbox_valve_es1 _show - } - - if (_bl_ln2_non_standard()) { - text_bf - printf("\nNon standard liquid nitrogen cooling-warning parameters occur. Please report this to your local contact.\n") - text_non_bf - printf("The macro bl_ln2_warn can be used to control this e-mail warning feature.\n") - bl_ln2_warn "show" - printf("\n") - } - - printf("\n") - bl_flight_tube_pressure - printf("\n") - - bl_attended _show - - _bl_check_alarm_records(1,1) - - printf("\n") - bl_op_msg -}' - - -def _bl_hall_temperature_ok() '{ - local temp_ok - local stat - - temp_ok = 1 - - # EH T02 average temperature - stat = epics_get("ILUUL-02AV:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - # EH T02 temperature at T0204 axis 16 - stat = epics_get("ILUUL-0200-EB104:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - # EH T02 temperature at T0205 axis 18 - stat = epics_get("ILUUL-0200-EB105:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - return (temp_ok) -}' - - -# ---------------------------------------------------------------------- -def bl_hall_temperature '{ - local stat - - stat = epics_get("ILUUL-02AV:TEMP") - printf("hall T02 average temperature : ") - if ((stat < 23.0) || (stat > 25.0)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - - stat = epics_get("ILUUL-0200-EB104:TEMP") - printf("hall temperature at T0204 axis 16 : ") - if ((stat < 23) || (stat > 25)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - - stat = epics_get("ILUUL-0200-EB105:TEMP") - printf("hall temperature at T0205 axis 18 : ") - if ((stat < 23) || (stat > 25)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - -# stat = epics_get("ILUUL-0300-EB102:TEMP") -# printf("EH T03 temperature at T0302 axis 21: ") -# if ((stat < 23) || (stat > 25)) { -# text_bf -# } -# printf("%.2f deg.C\n",stat) -# text_non_bf - -}' - -def _bl_sls_status_unusual() '{ - local unusual - local stat - - unusual = 0 - - stat = epics_get("X12SA-SR-VAC:SETPOINT") - if (stat != "OK") { - unusual = 1 - } - - stat = epics_get("ACOAU-ACCU:OP-MODE.VAL") - if ((stat != "Light Available") && (stat != "Light-Available")) { - unusual = 1 - } - - stat = epics_get("ALIRF-GUN:INJ-MODE") - if (stat != "TOP-UP") { - unusual = 1 - } - - # current threshold - stat = epics_get("ALIRF-GUN:CUR-LOWLIM") - if (stat < 350) { - unusual = 1 - } - - # current deadband - stat = epics_get("ALIRF-GUN:CUR-DBAND") - if (stat > 2) { - unusual = 1 - } - - # orbit feedback mode - stat = epics_get("ARIDI-BPM:OFB-MODE") - if (stat != "fast") { - unusual = 1 - } - - # fast orbit feedback - stat = epics_get("ARIDI-BPM:FOFBSTATUS-G") - if (stat != "running") { - unusual = 1 - } - - return(unusual) -}' - -def bl_sls_status '{ - local stat - - stat = epics_get("ACOAU-ACCU:OP-MODE.VAL") - printf("SLS status : ") - if ((stat != "Light Available") && (stat != "Light-Available")) { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:INJ-MODE") - printf("SLS injection mode : ") - if (stat != "TOP-UP") { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:CUR-LOWLIM") - printf("SLS current threshold : ") - if (stat < 350) { - text_bf - } - printf("%7.3f\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:CUR-DBAND") - printf("SLS current deadband : ") - if (stat > 2) { - text_bf - } - printf("%7.3f\n",stat) - text_non_bf - - stat = epics_get("ACORF-FILL:PAT-SELECT") - printf("SLS filling pattern : ") - printf("%s\n",stat) - - bl_ring_current - - stat = epics_get("ARIDI-PCT:TAU-HOUR") - printf("SLS filling life time : ") - printf("%.2f h\n",stat) - - stat = epics_get("ARIDI-BPM:OFB-MODE") - printf("orbit feedback mode : ") - if (stat != "fast") { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ARIDI-BPM:FOFBSTATUS-G") - printf("fast orbit feedback : ") - if (stat != "running") { - text_bf - } - printf("%s\n",stat) - text_non_bf - -}' - -def _bl_get_ring_current() '{ - return epics_get("ARIDI-PCT:CURRENT") -}' - - -# ---------------------------------------------------------------------- -def _bl_no_ring_current() '{ - # set an arbitrary current limit of 100mA as no-beam limit - if (_bl_get_ring_current() < 100) { - return 1 - } else { - return 0 - } -}' - - -# ---------------------------------------------------------------------- -def bl_ring_current '{ - local curr - - curr = _bl_get_ring_current() - - if (curr < 300) { - text_bf - } - printf("SLS ring current : %.3f mA\n",curr) - text_non_bf -}' \ No newline at end of file diff --git a/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py b/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py deleted file mode 100644 index ccf6d95..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py +++ /dev/null @@ -1,161 +0,0 @@ -import builtins -import time - -from rich import box -from rich.console import Console -from rich.table import Table - -from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose - -# import builtins to avoid linter errors -dev = builtins.__dict__.get("dev") -umv = builtins.__dict__.get("umv") -bec = builtins.__dict__.get("bec") - - -class LamNIOpticsMixin: - @staticmethod - def _get_user_param_safe(device, var): - param = dev[device].user_parameter - if not param or param.get(var) is None: - raise ValueError(f"Device {device} has no user parameter definition for {var}.") - return param.get(var) - - def leye_out(self): - self.loptics_in() - fshclose() - leyey_out = self._get_user_param_safe("leyey", "out") - umv(dev.leyey, leyey_out) - - epics_put("XOMNYI-XEYE-ACQ:0", 2) - # move rotation stage to zero to avoid problems with wires - umv(dev.lsamrot, 0) - umv(dev.dttrz, 5854, dev.fttrz, 2395) - - def leye_in(self): - bec.queue.next_dataset_number += 1 - # move rotation stage to zero to avoid problems with wires - umv(dev.lsamrot, 0) - umv(dev.dttrz, 6419.677, dev.fttrz, 2959.979) - while True: - moved_out = (input("Did the flight tube move out? (Y/n)") or "y").lower() - if moved_out == "y": - break - if moved_out == "n": - return - leyex_in = self._get_user_param_safe("leyex", "in") - leyey_in = self._get_user_param_safe("leyey", "in") - umv(dev.leyex, leyex_in, dev.leyey, leyey_in) - self.align.update_frame() - - def _lfzp_in(self): - loptx_in = self._get_user_param_safe("loptx", "in") - lopty_in = self._get_user_param_safe("lopty", "in") - umv( - dev.loptx, loptx_in, dev.lopty, lopty_in - ) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm - - def lfzp_in(self): - """ - move in the lamni zone plate. - This will disable rt feedback, move the FZP and re-enabled the feedback. - """ - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_disable() - - self._lfzp_in() - - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_enable_with_reset() - - def loptics_in(self): - """ - Move in the lamni optics, including the FZP and the OSA. - """ - self.lfzp_in() - self.losa_in() - - def loptics_out(self): - """Move out the lamni optics""" - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_disable() - - # self.lcs_out() - self.losa_out() - loptx_out = self._get_user_param_safe("loptx", "out") - lopty_out = self._get_user_param_safe("lopty", "out") - umv(dev.loptx, loptx_out, dev.lopty, lopty_out) - - if "rtx" in dev and dev.rtx.enabled: - time.sleep(1) - dev.rtx.controller.feedback_enable_with_reset() - - def lcs_in(self): - # umv lcsx -1.852 lcsy -0.095 - pass - - def lcs_out(self): - umv(dev.lcsy, 3) - - def losa_in(self): - # 6.2 keV, 170 um FZP - # umv(dev.losax, -1.4450000, dev.losay, -0.1800) - # umv(dev.losaz, -1) - # 6.7, 170 - # umv(dev.losax, -1.4850, dev.losay, -0.1930) - # umv(dev.losaz, 1.0000) - # 7.2, 150 - losax_in = self._get_user_param_safe("losax", "in") - losay_in = self._get_user_param_safe("losay", "in") - losaz_in = self._get_user_param_safe("losaz", "in") - umv(dev.losax, losax_in, dev.losay, losay_in) - umv(dev.losaz, losaz_in) - # 11 kev - # umv(dev.losax, -1.161000, dev.losay, -0.196) - # umv(dev.losaz, 1.0000) - - def losa_out(self): - losay_out = self._get_user_param_safe("losay", "out") - losaz_out = self._get_user_param_safe("losaz", "out") - umv(dev.losaz, losaz_out) - umv(dev.losay, losay_out) - - def lfzp_info(self): - loptz_val = dev.loptz.read()["loptz"]["value"] - distance = -loptz_val + 85.6 + 52 - print(f"The sample is in a distance of {distance:.1f} mm from the FZP.") - - diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6] - - mokev_val = dev.mokev.read()["mokev"]["value"] - console = Console() - table = Table( - title=f"At the current energy of {mokev_val:.4f} keV we have following options:", - box=box.SQUARE, - ) - table.add_column("Diameter", justify="center") - table.add_column("Focal distance", justify="center") - table.add_column("Current beam size", justify="center") - - wavelength = 1.2398e-9 / mokev_val - - for diameter in diameters: - outermost_zonewidth = 60e-9 - focal_distance = diameter * outermost_zonewidth / wavelength - beam_size = ( - -diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6 - ) - table.add_row( - f"{diameter*1e6:.2f} microns", - f"{focal_distance:.2f} mm", - f"{beam_size:.2f} microns", - ) - - console.print(table) - - print("OSA Information:") - # print(f"Current losaz %.1f\n", A[losaz]) - # print("The OSA will collide with the sample plane at %.1f\n\n", 89.3-A[loptz]) - print( - "The numbers presented here are for a sample in the plane of the lamni sample holder.\n" - ) diff --git a/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py b/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py deleted file mode 100755 index 3fd1281..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py +++ /dev/null @@ -1,23 +0,0 @@ -def lamni_read_additional_correction(): - # "additional_correction_shift" - # [0][] x , [1][] y, [2][] angle, [3][0] number of elements - import numpy as np - - with open("correction_lamni_um_S01405_.txt", "r") as f: - num_elements = f.readline() - int_num_elements = int(num_elements.split(" ")[2]) - print(int_num_elements) - corr_pos_x = [] - corr_pos_y = [] - corr_angle = [] - for j in range(0, int_num_elements * 3): - line = f.readline() - value = line.split(" ")[2] - name = line.split(" ")[0].split("[")[0] - if name == "corr_pos_x": - corr_pos_x.append(value) - elif name == "corr_pos_y": - corr_pos_y.append(value) - elif name == "corr_angle": - corr_angle.append(value) - return (corr_pos_x, corr_pos_y, corr_angle, num_elements) diff --git a/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py b/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py deleted file mode 100644 index 17a6f25..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py +++ /dev/null @@ -1,1332 +0,0 @@ -import builtins -import datetime -import os -import subprocess -import threading -import time -from collections import defaultdict -from pathlib import Path - -import h5py -import numpy as np -from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen -from bec_lib import bec_logger -from bec_lib.alarm_handler import AlarmBase -from bec_lib.pdf_writer import PDFWriter -from typeguard import typechecked - -from .lamni_optics_mixin import LamNIOpticsMixin - -logger = bec_logger.logger -bec = builtins.__dict__.get("bec") - - -class XrayEyeAlign: - # pixel calibration, multiply to get mm - # PIXEL_CALIBRATION = 0.2/209 #.2 with binning - PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning - - def __init__(self, client, lamni) -> None: - self.client = client - self.lamni = lamni - self.device_manager = client.device_manager - self.scans = client.scans - self.xeye = self.device_manager.devices.xeye - self.alignment_values = defaultdict(list) - self._reset_init_values() - self.corr_pos_x = [] - self.corr_pos_y = [] - self.corr_angle = [] - self.corr_pos_x_2 = [] - self.corr_pos_y_2 = [] - self.corr_angle_2 = [] - - def reset_correction(self): - self.corr_pos_x = [] - self.corr_pos_y = [] - self.corr_angle = [] - - def reset_correction_2(self): - self.corr_pos_x_2 = [] - self.corr_pos_y_2 = [] - self.corr_angle_2 = [] - - def reset_xray_eye_correction(self): - self.client.delete_global_var("tomo_fit_xray_eye") - - @property - def tomo_fovx_offset(self): - val = self.client.get_global_var("tomo_fov_offset") - if val is None: - return 0.0 - return val[0] / 1000 - - @tomo_fovx_offset.setter - @typechecked - def tomo_fovx_offset(self, val: float): - val_old = self.client.get_global_var("tomo_fov_offset") - if val_old is None: - val_old = [0.0, 0.0] - self.client.set_global_var("tomo_fov_offset", [val * 1000, val_old[1]]) - - @property - def tomo_fovy_offset(self): - val = self.client.get_global_var("tomo_fov_offset") - if val is None: - return 0.0 - return val[1] / 1000 - - @tomo_fovy_offset.setter - @typechecked - def tomo_fovy_offset(self, val: float): - val_old = self.client.get_global_var("tomo_fov_offset") - if val_old is None: - val_old = [0.0, 0.0] - self.client.set_global_var("tomo_fov_offset", [val_old[0], val * 1000]) - - def _reset_init_values(self): - self.shift_xy = [0, 0] - self._xray_fov_xy = [0, 0] - - def save_frame(self): - epics_put("XOMNYI-XEYE-SAVFRAME:0", 1) - - def update_frame(self): - epics_put("XOMNYI-XEYE-ACQDONE:0", 0) - # start live - epics_put("XOMNYI-XEYE-ACQ:0", 1) - # wait for start live - while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: - time.sleep(0.5) - print("waiting for live view to start...") - fshopen() - - epics_put("XOMNYI-XEYE-ACQDONE:0", 0) - - while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: - print("waiting for new frame...") - time.sleep(0.5) - - time.sleep(0.5) - # stop live view - epics_put("XOMNYI-XEYE-ACQ:0", 0) - time.sleep(1) - # fshclose - print("got new frame") - - def _disable_rt_feedback(self): - self.device_manager.devices.rtx.controller.feedback_disable() - - def _enable_rt_feedback(self): - self.device_manager.devices.rtx.controller.feedback_enable_with_reset() - - def tomo_rotate(self, val: float): - # pylint: disable=undefined-variable - umv(self.device_manager.devices.lsamrot, val) - - def get_tomo_angle(self): - return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"] - - def update_fov(self, k: int): - self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0]) - self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0]) - - @property - def movement_buttons_enabled(self): - return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")] - - @movement_buttons_enabled.setter - def movement_buttons_enabled(self, enabled: bool): - enabled = int(enabled) - epics_put("XOMNYI-XEYE-ENAMVX:0", enabled) - epics_put("XOMNYI-XEYE-ENAMVY:0", enabled) - - def send_message(self, msg: str): - epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg) - - def align(self): - # reset shift xy and fov params - self._reset_init_values() - self.reset_correction() - self.reset_correction_2() - - # this makes sure we are in a defined state - self._disable_rt_feedback() - - epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION) - - self._enable_rt_feedback() - - # initialize - # disable movement buttons - self.movement_buttons_enabled = False - - epics_put("XOMNYI-XEYE-ACQ:0", 0) - self.send_message("please wait...") - - # put sample name - epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", "Let us LAMNI...") - - # first step - self._disable_rt_feedback() - k = 0 - - # move zone plate in, eye in to get beam position - self.lamni.lfzp_in() - - self.update_frame() - - # enable submit buttons - self.movement_buttons_enabled = False - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - epics_put("XOMNYI-XEYE-STEP:0", 0) - self.send_message("Submit center value of FZP.") - - while True: - if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1: - val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") * self.PIXEL_CALIBRATION # in mm - val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm - self.alignment_values[k] = [val_x, val_y] - print( - f"Clicked position {k}: x {self.alignment_values[k][0]}, y {self.alignment_values[k][1]}" - ) - - if k == 0: # received center value of FZP - self.send_message("please wait ...") - # perform movement: FZP out, Sample in - self.lamni.loptics_out() - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - print("Moving sample in, FZP out") - - self._disable_rt_feedback() - time.sleep(0.3) - self._enable_rt_feedback() - time.sleep(0.3) - - # zero is now at the center - self.update_frame() - self.send_message("Go and find the sample") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - self.movement_buttons_enabled = True - - elif ( - k == 1 - ): # received sample center value at samroy 0 ie the final base shift values - msg = f"Base shift values from movement are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - print(msg) - logger.info(msg) - self.shift_xy[0] += ( - self.alignment_values[0][0] - self.alignment_values[1][0] - ) * 1000 - self.shift_xy[1] += ( - self.alignment_values[1][1] - self.alignment_values[0][1] - ) * 1000 - print( - f"Base shift values from movement and clicked position are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - ) - - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - self.send_message("please wait ...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - time.sleep(1) - - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) - self.update_frame() - self.send_message("Submit sample center and FOV (0 deg)") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - self.update_fov(k) - - elif 1 < k < 10: # received sample center value at samroy 0 ... 315 - self.send_message("please wait ...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - - # we swtich feedback off before rotating to not have it on and off again later for smooth operation - self._disable_rt_feedback() - self.tomo_rotate((k - 1) * 45 - 45 / 2) - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - self._disable_rt_feedback() - self.tomo_rotate((k - 1) * 45) - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) - self.update_frame() - self.send_message("Submit sample center") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - epics_put("XOMNYI-XEYE-ENAMVX:0", 1) - self.update_fov(k) - - elif k == 10: # received sample center value at samroy 270 and done - self.send_message("done...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - self.update_fov(k) - break - - k += 1 - epics_put("XOMNYI-XEYE-STEP:0", k) - if k < 2: - # allow movements, store movements to calculate center - _xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0") - _xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0") - if _xrayeyalignmvx != 0 or _xrayeyalignmvy != 0: - self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx - self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - print( - f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}" - ) - epics_put("XOMNYI-XEYE-MVY:0", 0) - epics_put("XOMNYI-XEYE-MVX:0", 0) - self.update_frame() - - time.sleep(0.2) - - self.write_output() - fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2 - fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2 - print( - f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy = {fovy:.0f} microns" - ) - print("Use matlab routine to fit the current alignment...") - - print( - f"This additional shift is applied to the base shift values\n which are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - ) - - self._disable_rt_feedback() - - self.tomo_rotate(0) - - print( - "\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running lamni.align.read_xray_eye_correction()\n" - ) - - self.client.set_global_var("tomo_fov_offset", self.shift_xy) - - def write_output(self): - with open( - os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w" - ) as alignment_values_file: - alignment_values_file.write(f"angle\thorizontal\tvertical\n") - for k in range(2, 11): - fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000 - fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000 - print( - f"Writing to file new alignment: number {k}, value x {fovx_offset}, y {fovy_offset}" - ) - alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n") - - def read_xray_eye_correction(self, dir_path=os.path.expanduser("~/Data10/specES1/internal/")): - tomo_fit_xray_eye = np.zeros((2, 3)) - with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file: - tomo_fit_xray_eye[0][0] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file: - tomo_fit_xray_eye[0][1] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file: - tomo_fit_xray_eye[0][2] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file: - tomo_fit_xray_eye[1][0] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file: - tomo_fit_xray_eye[1][1] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file: - tomo_fit_xray_eye[1][2] = file.readline() - - self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist()) - # x amp, phase, offset, y amp, phase, offset - # 0 0 0 1 0 2 1 0 1 1 1 2 - - print("New alignment parameters loaded from X-ray eye") - print( - f"X Amplitude {tomo_fit_xray_eye[0][0]}," - f"X Phase {tomo_fit_xray_eye[0][1]}, " - f"X Offset {tomo_fit_xray_eye[0][2]}," - f"Y Amplitude {tomo_fit_xray_eye[1][0]}," - f"Y Phase {tomo_fit_xray_eye[1][1]}," - f"Y Offset {tomo_fit_xray_eye[1][2]}" - ) - - def lamni_compute_additional_correction_xeye_mu(self, angle): - tomo_fit_xray_eye = self.client.get_global_var("tomo_fit_xray_eye") - if tomo_fit_xray_eye is None: - print("Not applying any additional correction. No x-ray eye data available.\n") - return (0, 0) - - # x amp, phase, offset, y amp, phase, offset - # 0 0 0 1 0 2 1 0 1 1 1 2 - correction_x = ( - tomo_fit_xray_eye[0][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[0][1]) - + tomo_fit_xray_eye[0][2] - ) / 1000 - correction_y = ( - tomo_fit_xray_eye[1][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[1][1]) - + tomo_fit_xray_eye[1][2] - ) / 1000 - - print(f"Xeye correction x {correction_x}, y {correction_y} for angle {angle}\n") - return (correction_x, correction_y) - - def compute_additional_correction(self, angle): - if not self.corr_pos_x: - print("Not applying any additional correction. No data available.\n") - return (0, 0) - - # find index of closest angle - for j, _ in enumerate(self.corr_pos_x): - newangledelta = np.fabs(self.corr_angle[j] - angle) - if j == 0: - angledelta = newangledelta - additional_correction_shift_x = self.corr_pos_x[j] - additional_correction_shift_y = self.corr_pos_y[j] - continue - - if newangledelta < angledelta: - additional_correction_shift_x = self.corr_pos_x[j] - additional_correction_shift_y = self.corr_pos_y[j] - angledelta = newangledelta - - if additional_correction_shift_x == 0 and angle < self.corr_angle[0]: - additional_correction_shift_x = self.corr_pos_x[0] - additional_correction_shift_y = self.corr_pos_y[0] - - if additional_correction_shift_x == 0 and angle > self.corr_angle[-1]: - additional_correction_shift_x = self.corr_pos_x[-1] - additional_correction_shift_y = self.corr_pos_y[-1] - print( - f"Additional correction shifts: {additional_correction_shift_x} {additional_correction_shift_y}" - ) - return (additional_correction_shift_x, additional_correction_shift_y) - - def read_additional_correction(self, correction_file: str): - with open(correction_file, "r") as f: - num_elements = f.readline() - int_num_elements = int(num_elements.split(" ")[2]) - print(int_num_elements) - corr_pos_x = [] - corr_pos_y = [] - corr_angle = [] - for j in range(0, int_num_elements * 3): - line = f.readline() - value = line.split(" ")[2] - name = line.split(" ")[0].split("[")[0] - if name == "corr_pos_x": - corr_pos_x.append(float(value) / 1000) - elif name == "corr_pos_y": - corr_pos_y.append(float(value) / 1000) - elif name == "corr_angle": - corr_angle.append(float(value)) - self.corr_pos_x = corr_pos_x - self.corr_pos_y = corr_pos_y - self.corr_angle = corr_angle - return - - def compute_additional_correction_2(self, angle): - if not self.corr_pos_x_2: - print("Not applying any additional secondary correction. No data available.\n") - return (0, 0) - - # find index of closest angle - for j, _ in enumerate(self.corr_pos_x_2): - newangledelta = np.fabs(self.corr_angle_2[j] - angle) - if j == 0: - angledelta = newangledelta - additional_correction_shift_x = self.corr_pos_x_2[j] - additional_correction_shift_y = self.corr_pos_y_2[j] - continue - - if newangledelta < angledelta: - additional_correction_shift_x = self.corr_pos_x_2[j] - additional_correction_shift_y = self.corr_pos_y_2[j] - angledelta = newangledelta - - if additional_correction_shift_x == 0 and angle < self.corr_angle_2[0]: - additional_correction_shift_x = self.corr_pos_x_2[0] - additional_correction_shift_y = self.corr_pos_y_2[0] - - if additional_correction_shift_x == 0 and angle > self.corr_angle_2[-1]: - additional_correction_shift_x = self.corr_pos_x_2[-1] - additional_correction_shift_y = self.corr_pos_y_2[-1] - print( - f"Additional correction shifts 2: {additional_correction_shift_x} {additional_correction_shift_y}" - ) - return (additional_correction_shift_x, additional_correction_shift_y) - - def read_additional_correction_2(self, correction_file: str): - with open(correction_file, "r") as f: - num_elements = f.readline() - int_num_elements = int(num_elements.split(" ")[2]) - print(int_num_elements) - corr_pos_x = [] - corr_pos_y = [] - corr_angle = [] - for j in range(0, int_num_elements * 3): - line = f.readline() - value = line.split(" ")[2] - name = line.split(" ")[0].split("[")[0] - if name == "corr_pos_x": - corr_pos_x.append(float(value) / 1000) - elif name == "corr_pos_y": - corr_pos_y.append(float(value) / 1000) - elif name == "corr_angle": - corr_angle.append(float(value)) - self.corr_pos_x_2 = corr_pos_x - self.corr_pos_y_2 = corr_pos_y - self.corr_angle_2 = corr_angle - return - - -class LamNI(LamNIOpticsMixin): - def __init__(self, client): - super().__init__() - self.client = client - self.align = XrayEyeAlign(client, self) - - self.check_shutter = True - self.check_light_available = True - self.check_fofb = True - self._check_msgs = [] - self.tomo_id = -1 - self.special_angles = [] - self.special_angle_repeats = 20 - self.special_angle_tolerance = 20 - self._current_special_angles = [] - self._beam_is_okay = True - self._stop_beam_check_event = None - self.beam_check_thread = None - - def get_beamline_checks_enabled(self): - print( - f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available: {self.check_light_available}" - ) - - @property - def beamline_checks_enabled(self): - return { - "shutter": self.check_shutter, - "fofb": self.check_fofb, - "light available": self.check_light_available, - } - - @beamline_checks_enabled.setter - def beamline_checks_enabled(self, val: bool): - self.check_shutter = val - self.check_light_available = val - self.check_fofb = val - self.get_beamline_checks_enabled() - - def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5): - """Set the special angles for a tomo - - Args: - angles (list): List of special angles - repeats (int, optional): Number of repeats at a special angle. Defaults to 20. - tolerance (float, optional): Number of repeats at a special angle. Defaults to 0.5. - - """ - self.special_angles = angles - self.special_angle_repeats = repeats - self.special_angle_tolerance = tolerance - - def remove_special_angles(self): - """Remove the special angles and set the number of repeats to 1""" - self.special_angles = [] - self.special_angle_repeats = 1 - - @property - def tomo_shellstep(self): - val = self.client.get_global_var("tomo_shellstep") - if val is None: - return 1 - return val - - @tomo_shellstep.setter - def tomo_shellstep(self, val: float): - self.client.set_global_var("tomo_shellstep", val) - - @property - def tomo_circfov(self): - val = self.client.get_global_var("tomo_circfov") - if val is None: - return 0.0 - return val - - @tomo_circfov.setter - def tomo_circfov(self, val: float): - self.client.set_global_var("tomo_circfov", val) - - @property - def tomo_countingtime(self): - val = self.client.get_global_var("tomo_countingtime") - if val is None: - return 0.1 - return val - - @tomo_countingtime.setter - def tomo_countingtime(self, val: float): - self.client.set_global_var("tomo_countingtime", val) - - @property - def manual_shift_x(self): - val = self.client.get_global_var("manual_shift_x") - if val is None: - return 0.0 - return val - - @manual_shift_x.setter - def manual_shift_x(self, val: float): - self.client.set_global_var("manual_shift_x", val) - - @property - def manual_shift_y(self): - val = self.client.get_global_var("manual_shift_y") - if val is None: - return 0.0 - return val - - @manual_shift_y.setter - def manual_shift_y(self, val: float): - self.client.set_global_var("manual_shift_y", val) - - @property - def lamni_piezo_range_x(self): - val = self.client.get_global_var("lamni_piezo_range_x") - if val is None: - return 20 - return val - - @lamni_piezo_range_x.setter - def lamni_piezo_range_x(self, val: float): - if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): - self.client.set_global_var("lamni_piezo_range_x", val) - return - if val > 80: - raise ValueError("Piezo range cannot be larger than 80 um.") - self.client.set_global_var("lamni_piezo_range_x", val) - - @property - def lamni_piezo_range_y(self): - val = self.client.get_global_var("lamni_piezo_range_y") - if val is None: - return 20 - return val - - @lamni_piezo_range_y.setter - def lamni_piezo_range_y(self, val: float): - if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): - self.client.set_global_var("lamni_piezo_range_y", val) - return - if val > 80: - raise ValueError("Piezo range cannot be larger than 80 um.") - self.client.set_global_var("lamni_piezo_range_y", val) - - @property - def corridor_size(self): - val = self.client.get_global_var("corridor_size") - if val is None: - val = -1 - return val - - @corridor_size.setter - def corridor_size(self, val: float): - self.client.set_global_var("corridor_size", val) - - @property - def lamni_stitch_x(self): - val = self.client.get_global_var("lamni_stitch_x") - if val is None: - return 0 - return val - - @lamni_stitch_x.setter - @typechecked - def lamni_stitch_x(self, val: int): - self.client.set_global_var("lamni_stitch_x", val) - - @property - def lamni_stitch_y(self): - val = self.client.get_global_var("lamni_stitch_y") - if val is None: - return 0 - return val - - @lamni_stitch_y.setter - @typechecked - def lamni_stitch_y(self, val: int): - self.client.set_global_var("lamni_stitch_y", val) - - @property - def ptycho_reconstruct_foldername(self): - val = self.client.get_global_var("ptycho_reconstruct_foldername") - if val is None: - return "ptycho_reconstruct" - return val - - @ptycho_reconstruct_foldername.setter - def ptycho_reconstruct_foldername(self, val: str): - self.client.set_global_var("ptycho_reconstruct_foldername", val) - - @property - def tomo_angle_stepsize(self): - val = self.client.get_global_var("tomo_angle_stepsize") - if val is None: - return 10.0 - return val - - @tomo_angle_stepsize.setter - def tomo_angle_stepsize(self, val: float): - self.client.set_global_var("tomo_angle_stepsize", val) - - @property - def tomo_stitch_overlap(self): - val = self.client.get_global_var("tomo_stitch_overlap") - if val is None: - return 0.2 - return val - - @tomo_stitch_overlap.setter - def tomo_stitch_overlap(self, val: float): - self.client.set_global_var("tomo_stitch_overlap", val) - - @property - def sample_name(self): - val = self.client.get_global_var("sample_name") - if val is None: - return "bec_test_sample" - return val - - @sample_name.setter - @typechecked - def sample_name(self, val: str): - self.client.set_global_var("sample_name", val) - - def write_to_spec_log(self, content): - try: - with open( - os.path.expanduser( - "~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log" - ), - "a", - ) as log_file: - log_file.write(content) - except Exception: - logger.warning("Failed to write to spec log file (omny web page).") - - def write_to_scilog(self, content, tags: list = None): - try: - if tags is not None: - tags.append("BEC") - else: - tags = ["BEC"] - msg = bec.logbook.LogbookMessage() - msg.add_text(content).add_tag(tags) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to write to scilog.") - - def tomo_scan_projection(self, angle: float): - scans = builtins.__dict__.get("scans") - - additional_correction = self.align.compute_additional_correction(angle) - additional_correction_2 = self.align.compute_additional_correction_2(angle) - correction_xeye_mu = self.align.lamni_compute_additional_correction_xeye_mu(angle) - - self._current_scan_list = [] - - for stitch_x in range(-self.lamni_stitch_x, self.lamni_stitch_x + 1): - for stitch_y in range(-self.lamni_stitch_y, self.lamni_stitch_y + 1): - # pylint: disable=undefined-variable - self._current_scan_list.append(bec.queue.next_scan_number) - logger.info( - f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}], step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0}, stitch_overlap={1}," - f"center_x={self.align.tomo_fovx_offset}, center_y={self.align.tomo_fovy_offset}, " - f"shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]}, " - f"shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]}, " - f"fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')" - ) - log_message = f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan number {bec.queue.next_scan_number}.\n" - self.write_to_spec_log(log_message) - # self.write_to_scilog(log_message, ["BEC_scans", self.sample_name]) - corridor_size = self.corridor_size if self.corridor_size > 0 else None - scans.lamni_fermat_scan( - fov_size=[self.lamni_piezo_range_x, self.lamni_piezo_range_y], - step=self.tomo_shellstep, - stitch_x=stitch_x, - stitch_y=stitch_y, - stitch_overlap=self.tomo_stitch_overlap, - center_x=self.align.tomo_fovx_offset, - center_y=self.align.tomo_fovy_offset, - shift_x=( - self.manual_shift_x - + correction_xeye_mu[0] - - additional_correction[0] - - additional_correction_2[0] - ), - shift_y=( - self.manual_shift_y - + correction_xeye_mu[1] - - additional_correction[1] - - additional_correction_2[1] - ), - fov_circular=self.tomo_circfov, - angle=angle, - scan_type="fly", - exp_time=self.tomo_countingtime, - optim_trajectory_corridor=corridor_size, - ) - - def _run_beamline_checks(self): - msgs = [] - dev = builtins.__dict__.get("dev") - try: - if self.check_shutter: - shutter_val = dev.x12sa_es1_shutter_status.read(cached=True) - if shutter_val["value"].lower() != "open": - self._beam_is_okay = False - msgs.append("Check beam failed: Shutter is closed.") - if self.check_light_available: - machine_status = dev.sls_machine_status.read(cached=True) - if machine_status["value"] not in [ - "Light Available", - "Light-Available", - ]: - self._beam_is_okay = False - msgs.append("Check beam failed: Light not available.") - if self.check_fofb: - fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True) - if fast_orbit_feedback["value"] != "running": - self._beam_is_okay = False - msgs.append("Check beam failed: Fast orbit feedback is not running.") - except Exception: - logger.warning("Failed to check beam.") - return msgs - - def _check_beam(self): - while not self._stop_beam_check_event.is_set(): - self._check_msgs = self._run_beamline_checks() - - if not self._beam_is_okay: - self._stop_beam_check_event.set() - time.sleep(1) - - def _start_beam_check(self): - self._beam_is_okay = True - self._stop_beam_check_event = threading.Event() - - self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True) - self.beam_check_thread.start() - - def _was_beam_okay(self): - self._stop_beam_check_event.set() - self.beam_check_thread.join() - return self._beam_is_okay - - def _print_beamline_checks(self): - for msg in self._check_msgs: - logger.warning(msg) - - def _wait_for_beamline_checks(self): - self._print_beamline_checks() - try: - msg = bec.logbook.LogbookMessage() - msg.add_text( - f"

Beamline checks failed at {str(datetime.datetime.now())}: {''.join(self._check_msgs)}

" - ).add_tag(["BEC", "beam_check"]) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to send update to SciLog.") - - while True: - self._beam_is_okay = True - self._check_msgs = self._run_beamline_checks() - if self._beam_is_okay: - break - self._print_beamline_checks() - time.sleep(1) - - try: - msg = bec.logbook.LogbookMessage() - msg.add_text( - f"

Operation resumed at {str(datetime.datetime.now())}.

" - ).add_tag(["BEC", "beam_check"]) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to send update to SciLog.") - - def add_sample_database( - self, - samplename, - date, - eaccount, - scan_number, - setup, - sample_additional_info, - user, - ): - """Add a sample to the omny sample database. This also retrieves the tomo id.""" - subprocess.run( - f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'", - shell=True, - ) - with open("/tmp/currsamplesnr.txt") as tomo_number_file: - tomo_number = int(tomo_number_file.read()) - return tomo_number - - def _at_each_angle(self, angle: float) -> None: - self.tomo_scan_projection(angle) - self.tomo_reconstruct() - - ### XMCD ### - # 2 projections, 1 for each polarization state - # cp() - # self.tomo_scan_projection(angle) - # self.tomo_reconstruct() - # cm() - # self.tomo_scan_projection(angle) - # self.tomo_reconstruct() - - def sub_tomo_scan(self, subtomo_number, start_angle=None): - """start a subtomo""" - dev = builtins.__dict__.get("dev") - bec = builtins.__dict__.get("bec") - if self.tomo_id > 0: - tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] - else: - tags = ["BEC_subtomo", self.sample_name] - self.write_to_scilog( - f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", - tags, - ) - - if start_angle is None: - if subtomo_number == 1: - start_angle = 0 - elif subtomo_number == 2: - start_angle = self.tomo_angle_stepsize / 8.0 * 4 - elif subtomo_number == 3: - start_angle = self.tomo_angle_stepsize / 8.0 * 2 - elif subtomo_number == 4: - start_angle = self.tomo_angle_stepsize / 8.0 * 6 - elif subtomo_number == 5: - start_angle = self.tomo_angle_stepsize / 8.0 * 1 - elif subtomo_number == 6: - start_angle = self.tomo_angle_stepsize / 8.0 * 5 - elif subtomo_number == 7: - start_angle = self.tomo_angle_stepsize / 8.0 * 3 - elif subtomo_number == 8: - start_angle = self.tomo_angle_stepsize / 8.0 * 7 - - # _tomo_shift_angles (potential global variable) - _tomo_shift_angles = 0 - angle_end = start_angle + 360 - for angle in np.linspace( - start_angle + _tomo_shift_angles, - angle_end, - num=int(360 / self.tomo_angle_stepsize) + 1, - endpoint=True, - ): - successful = False - error_caught = False - if 0 <= angle < 360.05: - print(f"Starting LamNI scan for angle {angle}") - while not successful: - self._start_beam_check() - if not self.special_angles: - self._current_special_angles = [] - if self._current_special_angles: - next_special_angle = self._current_special_angles[0] - if np.isclose(angle, next_special_angle, atol=0.5): - self._current_special_angles.pop(0) - num_repeats = self.special_angle_repeats - else: - num_repeats = 1 - try: - start_scan_number = bec.queue.next_scan_number - for i in range(num_repeats): - self._at_each_angle(angle) - error_caught = False - except AlarmBase as exc: - if exc.alarm_type == "TimeoutError": - bec.queue.request_queue_reset() - time.sleep(2) - error_caught = True - else: - raise exc - - if self._was_beam_okay() and not error_caught: - successful = True - else: - self._wait_for_beamline_checks() - end_scan_number = bec.queue.next_scan_number - for scan_nr in range(start_scan_number, end_scan_number): - self._write_tomo_scan_number(scan_nr, angle, subtomo_number) - - def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None: - tomo_scan_numbers_file = os.path.expanduser( - "~/Data10/specES1/dat-files/tomography_scannumbers.txt" - ) - with open(tomo_scan_numbers_file, "a+") as out_file: - # pylint: disable=undefined-variable - out_file.write( - f"{scan_number} {angle} {dev.lsamrot.read()['lsamrot']['value']:.3f} {self.tomo_id} {subtomo_number} {0} {'lamni'}\n" - ) - - def tomo_scan(self, subtomo_start=1, start_angle=None): - """start a tomo scan""" - bec = builtins.__dict__.get("bec") - scans = builtins.__dict__.get("scans") - self._current_special_angles = self.special_angles.copy() - - if subtomo_start == 1 and start_angle is None: - # pylint: disable=undefined-variable - self.tomo_id = self.add_sample_database( - self.sample_name, - str(datetime.date.today()), - bec.active_account.decode(), - bec.queue.next_scan_number, - "lamni", - "test additional info", - "BEC", - ) - self.write_pdf_report() - with scans.dataset_id_on_hold: - for ii in range(subtomo_start, 9): - self.sub_tomo_scan(ii, start_angle=start_angle) - start_angle = None - - def tomo_parameters(self): - """print and update the tomo parameters""" - print("Current settings:") - print(f"Counting time = {self.tomo_countingtime} s") - print(f"Stepsize microns = {self.tomo_shellstep}") - print( - f"Piezo range (max 80) = {self.lamni_piezo_range_x}, {self.lamni_piezo_range_y}" - ) - print(f"Stitching number x,y = {self.lamni_stitch_x}, {self.lamni_stitch_y}") - print(f"Stitching overlap = {self.tomo_stitch_overlap}") - print(f"Circuilar FOV diam = {self.tomo_circfov}") - print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}") - print( - "For information, fov offset is rotating and finding the ROI, manual shift moves rotation center" - ) - print(f" _tomo_fovx_offset = {self.align.tomo_fovx_offset}") - print(f" _tomo_fovy_offset = {self.align.tomo_fovy_offset}") - print(f" _manual_shift_x = {self.manual_shift_x}") - print(f" _manual_shift_y = {self.manual_shift_y}") - print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") - print(f"Resulting in number of projections: {360/self.tomo_angle_stepsize*8}") - print(f"Sample name: {self.sample_name}\n") - - user_input = input("Are these parameters correctly set for your scan? ") - if user_input == "y": - print("good then") - else: - self.tomo_countingtime = self._get_val(" s", self.tomo_countingtime, float) - self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) - self.lamni_piezo_range_x = self._get_val( - " um", self.lamni_piezo_range_x, float - ) - self.lamni_piezo_range_y = self._get_val( - " um", self.lamni_piezo_range_y, float - ) - self.lamni_stitch_x = self._get_val("", self.lamni_stitch_x, int) - self.lamni_stitch_y = self._get_val("", self.lamni_stitch_y, int) - self.tomo_circfov = self._get_val(" um", self.tomo_circfov, float) - self.ptycho_reconstruct_foldername = self._get_val( - "Reconstruction queue ", self.ptycho_reconstruct_foldername, str - ) - tomo_numberofprojections = self._get_val( - "Number of projections", 360 / self.tomo_angle_stepsize * 8, int - ) - - print(f"The angular step will be {360/tomo_numberofprojections}") - self.tomo_angle_stepsize = 360 / tomo_numberofprojections * 8 - print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}") - self.sample_name = self._get_val("sample name", self.sample_name, str) - - @staticmethod - def _get_val(msg: str, default_value, data_type): - return data_type(input(f"{msg} ({default_value}): ") or default_value) - - def tomo_reconstruct(self, base_path="~/Data10/specES1"): - """write the tomo reconstruct file for the reconstruction queue""" - bec = builtins.__dict__.get("bec") - base_path = os.path.expanduser(base_path) - ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername)) - ptycho_queue_path.mkdir(parents=True, exist_ok=True) - - # pylint: disable=undefined-variable - last_scan_number = bec.queue.next_scan_number - 1 - ptycho_queue_file = os.path.abspath( - os.path.join(ptycho_queue_path, f"scan_{last_scan_number:05d}.dat") - ) - with open(ptycho_queue_file, "w") as queue_file: - scans = " ".join([str(scan) for scan in self._current_scan_list]) - queue_file.write(f"p.scan_number {scans}\n") - queue_file.write(f"p.check_nextscan_started 1\n") - - def write_pdf_report(self): - """create and write the pdf report with the current LamNI settings""" - dev = builtins.__dict__.get("dev") - header = ( - " \n" * 3 - + " ::: ::: ::: ::: :::: ::: ::::::::::: \n" - + " :+: :+: :+: :+:+: :+:+: :+:+: :+: :+: \n" - + " +:+ +:+ +:+ +:+ +:+:+ +:+ :+:+:+ +:+ +:+ \n" - + " +#+ +#++:++#++: +#+ +:+ +#+ +#+ +:+ +#+ +#+ \n" - + " +#+ +#+ +#+ +#+ +#+ +#+ +#+#+# +#+ \n" - + " #+# #+# #+# #+# #+# #+# #+#+# #+# \n" - + " ########## ### ### ### ### ### #### ########### \n" - ) - padding = 20 - piezo_range = f"{self.lamni_piezo_range_x:.2f}/{self.lamni_piezo_range_y:.2f}" - stitching = f"{self.lamni_stitch_x:.2f}/{self.lamni_stitch_y:.2f}" - dataset_id = str(self.client.queue.next_dataset_number) - content = [ - f"{'Sample Name:':<{padding}}{self.sample_name:>{padding}}\n", - f"{'Measurement ID:':<{padding}}{str(self.tomo_id):>{padding}}\n", - f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", - f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", - f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n", - f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n", - f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", - f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n", - f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n", - f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", - f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", - f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range:>{padding}}\n", - f"{'Restriction to circular FOV:':<{padding}}{self.tomo_circfov:>{padding}.2f}\n", - f"{'Stitching:':<{padding}}{stitching:>{padding}}\n", - f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n", - f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n", - ] - content = "".join(content) - user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf") - with PDFWriter(user_target) as file: - file.write(header) - file.write(content) - subprocess.run( - "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", - shell=True, - ) - # status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True) - msg = bec.logbook.LogbookMessage() - logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") - msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).add_tag( - [ - "BEC", - "tomo_parameters", - f"dataset_id_{dataset_id}", - "LamNI", - self.sample_name, - ] - ) - self.client.logbook.send_logbook_message(msg) - - -class MagLamNI(LamNI): - def sub_tomo_scan(self, subtomo_number, start_angle=None): - super().sub_tomo_scan(subtomo_number, start_angle) - # self.rotate_slowly(0) - - def rotate_slowly(self, angle, step_size=20): - current_angle = dev.lsamrot.read(cached=True)["value"] - steps = int(np.ceil(np.abs(current_angle - angle) / step_size)) + 1 - for target_angle in np.linspace(current_angle, angle, steps, endpoint=True): - umv(dev.lsamrot, target_angle) - scans.lamni_move_to_scan_center( - self.align.tomo_fovx_offset, self.align.tomo_fovy_offset, target_angle - ) - - def _at_each_angle(self, angle: float) -> None: - if "lamni_at_each_angle" in builtins.__dict__: - lamni_at_each_angle(self, angle) - return - - self.tomo_scan_projection(angle) - self.tomo_reconstruct() - - # # cm() - # # umv(dev.ppth,15.1762) #11.567 keV - # for ii in range(2): - # self.tomo_scan_projection(angle) - # self.tomo_reconstruct() - # # cp() - # # umv(dev.ppth,15.1827) #11.567 keV - # for ii in range(2): - # self.tomo_scan_projection(angle) - # self.tomo_reconstruct() - - -class DataDrivenLamNI(LamNI): - def __init__(self, client): - super().__init__(client) - self.tomo_data = {} - - def tomo_scan( - self, - subtomo_start=1, - start_index=None, - fname="~/Data10/data_driven_config/datadriven_params.h5", - ): - """start a tomo scan""" - bec = builtins.__dict__.get("bec") - scans = builtins.__dict__.get("scans") - - fname = os.path.expanduser(fname) - - if not os.path.exists(fname): - raise FileNotFoundError(f"Could not find datadriven params file in {fname}.") - content = f"Loading tomo parameters from {fname}." - logger.warning(content) - tags = ["Data_driven_file", "BEC"] - msg = bec.logbook.LogbookMessage() - msg.add_text(content).add_tag(tags) - self.client.logbook.send_logbook_message(msg) - self._update_tomo_data_from_file(fname) - - self._current_special_angles = self.special_angles.copy() - - if subtomo_start == 1 and start_index is None: - # pylint: disable=undefined-variable - self.tomo_id = self.add_sample_database( - self.sample_name, - str(datetime.date.today()), - bec.active_account.decode(), - bec.queue.next_scan_number, - "lamni", - "test additional info", - "BEC", - ) - self.write_pdf_report() - with scans.dataset_id_on_hold: - self.sub_tomo_data_driven(start_index) - - def sub_tomo_scan(self): - raise NotImplementedError( - "Cannot run sub_tomo_scan with data-driven LamNI. Please use lamni.tomo_scan(subtomo_start=) instead." - ) - - def _at_each_angle( - self, - angle=None, - stepsize=None, - loptz_pos=None, - manual_shift_x=0, - manual_shift_y=0, - ): - # Do something... - # self.tomo_parameters - self.manual_shift_x = manual_shift_x - self.manual_shift_y = manual_shift_y - self.tomo_shellstep = stepsize # in microns - if loptz_pos is not None: - dev.rtx.controller.feedback_disable() - umv(dev.loptz, loptz_pos) - super()._at_each_angle(angle=angle) - - def sub_tomo_data_driven(self, start_index=None): - # for theta, stepsize, sample_to_focus, probe_diameter, subtomo_id in zip(*self.tomo_data.values()): - - for scan_index, scan_data in enumerate(zip(*self.tomo_data.values())): - if start_index and scan_index < start_index: - continue - ( - angle, - stepsize, - loptz_pos, - propagation_distance, - manual_shift_x, - manual_shift_y, - subtomo_number, - ) = scan_data - bec.metadata.update( - {key: float(val) for key, val in zip(self.tomo_data.keys(), scan_data)} - ) - successful = False - error_caught = False - if 0 <= angle < 360.05: - print(f"Starting LamNI scan for angle {angle}") - while not successful: - self._start_beam_check() - if not self.special_angles: - self._current_special_angles = [] - if self._current_special_angles: - next_special_angle = self._current_special_angles[0] - if np.isclose(angle, next_special_angle, atol=0.5): - self._current_special_angles.pop(0) - num_repeats = self.special_angle_repeats - else: - num_repeats = 1 - try: - start_scan_number = bec.queue.next_scan_number - for i in range(num_repeats): - self._at_each_angle( - float(angle), - stepsize=float(stepsize), - loptz_pos=float(loptz_pos), - manual_shift_x=float(manual_shift_x), - manual_shift_y=float(manual_shift_y), - ) - error_caught = False - except AlarmBase as exc: - if exc.alarm_type == "TimeoutError": - bec.queue.request_queue_reset() - time.sleep(2) - error_caught = True - else: - raise exc - - if self._was_beam_okay() and not error_caught: - successful = True - else: - self._wait_for_beamline_checks() - end_scan_number = bec.queue.next_scan_number - for scan_nr in range(start_scan_number, end_scan_number): - self._write_tomo_scan_number(scan_nr, angle, subtomo_number) - - def _update_tomo_data_from_file(self, fname: str) -> None: - with h5py.File(fname, "r") as file: - self.tomo_data["theta"] = np.array([*file["theta"]]).flatten() - self.tomo_data["stepsize"] = np.array([*file["stepsize"]]).flatten() - self.tomo_data["loptz"] = np.array([*file["loptz"]]).flatten() - self.tomo_data["propagation_distance"] = np.array( - [*file["relative_propagation_distance"]] - ).flatten() - self.tomo_data["manual_shift_x"] = np.array([*file["manual_shift_x"]]).flatten() - self.tomo_data["manual_shift_y"] = np.array([*file["manual_shift_y"]]).flatten() - self.tomo_data["subtomo_id"] = np.array([*file["subtomo_id"]]).flatten() - - shapes = [] - for data in self.tomo_data.values(): - shapes.append(data.shape) - if len(set(shapes)) > 1: - raise ValueError(f"Tomo data file has entries of inconsistent lengths: {shapes}.") diff --git a/bec_plugins/bec_client/plugins/cSAXS/__init__.py b/bec_plugins/bec_client/plugins/cSAXS/__init__.py deleted file mode 100644 index aff5e6b..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .cSAXS_beamline import fshopen, fshclose, fshstatus, epics_get, epics_put diff --git a/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py b/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py deleted file mode 100644 index 49e6647..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py +++ /dev/null @@ -1,108 +0,0 @@ -import builtins - -from rich import box -from rich.table import Table - -from bec_client.beamline_mixin import BeamlineShowInfo - - -class BeamlineInfo(BeamlineShowInfo): - def show(self): - """Display information about the current beamline status""" - console = self._get_console() - - table = Table(title="X12SA Info", box=box.SQUARE) - table.add_column("Key", justify="left") - table.add_column("Value", justify="left") - - info = self._get_beamline_info_messages() - self._add_op_status(table, info) - self._add_id_gap(table, info) - self._add_storage_ring_vac(table, info) - self._add_shutter_status(table, info) - self._add_mokev(table, info) - self._add_fe_status(table, info) - self._add_es1_valve(table, info) - self._add_xbox1_pressure(table, info) - self._add_xbox2_pressure(table, info) - - console.print(table) - - def _add_op_status(self, table, info): - val = self._get_info_val(info, "x12sa_op_status") - if val not in ["attended"]: - return table.add_row("Beamline operation", val, style=self.ALARM_STYLE) - return table.add_row("Beamline operation", val, style=self.DEFAULT_STYLE) - - def _add_shutter_status(self, table, info): - val = self._get_info_val(info, "x12sa_es1_shutter_status") - if val.lower() not in ["open"]: - return table.add_row("Shutter", val, style=self.ALARM_STYLE) - return table.add_row("Shutter", val, style=self.DEFAULT_STYLE) - - def _add_storage_ring_vac(self, table, info): - val = self._get_info_val(info, "x12sa_storage_ring_vac") - if val.lower() not in ["ok"]: - return table.add_row("Storage ring vacuum", val, style=self.ALARM_STYLE) - return table.add_row("Storage ring vacuum", val, style=self.DEFAULT_STYLE) - - def _add_es1_valve(self, table, info): - val = self._get_info_val(info, "x12sa_es1_valve") - if val.lower() not in ["open"]: - return table.add_row("ES1 valve", val, style=self.ALARM_STYLE) - return table.add_row("ES1 valve", val, style=self.DEFAULT_STYLE) - - def _add_xbox1_pressure(self, table, info): - MAX_PRESSURE = 2e-6 - val = info["x12sa_exposure_box1_pressure"]["value"] - if val > MAX_PRESSURE: - return table.add_row( - f"Exposure box 1 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", - f"{val:.1e} mbar", - style=self.ALARM_STYLE, - ) - return table.add_row("Exposure box 1 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) - - def _add_xbox2_pressure(self, table, info): - MAX_PRESSURE = 2e-6 - val = info["x12sa_exposure_box2_pressure"]["value"] - if val > MAX_PRESSURE: - return table.add_row( - f"Exposure box 2 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", - f"{val:.1e} mbar", - style=self.ALARM_STYLE, - ) - return table.add_row("Exposure box 2 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) - - def _add_fe_status(self, table, info): - val = self._get_info_val(info, "x12sa_fe_status") - return table.add_row("Front end shutter", val, style=self.DEFAULT_STYLE) - - def _add_id_gap(self, table, info): - val = info["x12sa_id_gap"]["value"] - if val > 8: - return table.add_row("ID gap", f"{val:.3f} mm", style=self.ALARM_STYLE) - return table.add_row("ID gap", f"{val:.3f} mm", style=self.DEFAULT_STYLE) - - def _add_mokev(self, table, info): - val = info["x12sa_mokev"]["value"] - return table.add_row("Selected energy (mokev)", f"{val:.3f} keV", style=self.DEFAULT_STYLE) - - def _get_beamline_info_messages(self) -> dict: - dev = builtins.__dict__.get("dev") - - def _get_bl_msg(info, device_name): - info[device_name] = dev[device_name].read(cached=True) - - info = {} - _get_bl_msg(info, "x12sa_op_status") - _get_bl_msg(info, "x12sa_storage_ring_vac") - _get_bl_msg(info, "x12sa_es1_shutter_status") - _get_bl_msg(info, "x12sa_id_gap") - _get_bl_msg(info, "x12sa_mokev") - _get_bl_msg(info, "x12sa_fe_status") - _get_bl_msg(info, "x12sa_es1_valve") - _get_bl_msg(info, "x12sa_exposure_box1_pressure") - _get_bl_msg(info, "x12sa_exposure_box2_pressure") - - return info diff --git a/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py b/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py deleted file mode 100644 index 6863e46..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py +++ /dev/null @@ -1,28 +0,0 @@ -import epics - - -def epics_put(channel, value): - epics.caput(channel, value) - - -def epics_get(channel): - return epics.caget(channel) - - -def fshon(): - pass - - -def fshopen(): - """open the fast shutter""" - epics_put("X12SA-ES1-TTL:OUT_01", 1) - - -def fshclose(): - """close the fast shutter""" - epics_put("X12SA-ES1-TTL:OUT_01", 0) - - -def fshstatus(): - """show the fast shutter status""" - return epics_get("X12SA-ES1-TTL:OUT_01") diff --git a/bin/open_tunnel.sh b/bin/open_tunnel.sh deleted file mode 100755 index 966ae40..0000000 --- a/bin/open_tunnel.sh +++ /dev/null @@ -1,5 +0,0 @@ - -for i in `seq 1 8` -do - ssh -N -R 6379:localhost:6379 x12sa-cn-$i & -done diff --git a/bin/setup_bec.sh b/bin/setup_bec.sh deleted file mode 100755 index 4982e12..0000000 --- a/bin/setup_bec.sh +++ /dev/null @@ -1,47 +0,0 @@ - -if [ "pc15543.psi.ch" != "$(hostname)" ]; then - echo "Please run this script on pc15543" - exit 1 -fi - -module add psi-python311/2024.02 -echo module add tmux/3.2 >> ~/.bashrc -echo module add redis/7.0.12 >> ~/.bashrc - -source ~/.bashrc - -cd ~/Data10 -mkdir -p software/ -mkdir -p ~/bec/scripts -cd software - -git clone https://gitlab.psi.ch/bec/bec.git -git clone https://gitlab.psi.ch/bec/ophyd_devices.git -git clone https://gitlab.psi.ch/bec/bec-widgets.git - -python -m venv ./bec_venv -source ./bec_venv/bin/activate - -cd bec -git checkout sastt-online-changes -pip install -e ./bec_server[dev] - -cd ../csaxs-bec -pip install -e .[dev] - -#redis-server --protected-mode no & - -read -p "Do you want to set the current BEC account to $(whoami)? (yes/no) " yn - -val=$(whoami) - -case $yn in - yes ) echo ok, setting account to $val; - redis-cli SET internal/account:val $val;; - no ) echo ;; - * ) echo invalid response; - exit 1;; -esac - - -$(pwd)/open_tunnel.sh diff --git a/bin/setup_bec_widgets.sh b/bin/setup_bec_widgets.sh deleted file mode 100755 index d142791..0000000 --- a/bin/setup_bec_widgets.sh +++ /dev/null @@ -1,16 +0,0 @@ -module add psi-python311/2024.02 - -cd ~/Data10/software -python -m venv ./bec_widgets_venv -source ./bec_widgets_venv/bin/activate -pip install --upgrade pip -cd ~/Data10/software/bec/bec_lib -pip install -e . - -cd ~/Data10/software/csaxs-bec -pip install -e . - -cd ~/Data10/software/bec-widgets -pip install -e . - -echo "For the moment widgets only run on beamline consoles comp1/2 and cons1" diff --git a/csaxs_bec/__init__.py b/csaxs_bec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/__init__.py b/csaxs_bec/bec_ipython_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/high_level_interface/__init__.py b/csaxs_bec/bec_ipython_client/high_level_interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/plugins/__init__.py b/csaxs_bec/bec_ipython_client/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/startup/__init__.py b/csaxs_bec/bec_ipython_client/startup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/startup/post_startup.py b/csaxs_bec/bec_ipython_client/startup/post_startup.py new file mode 100644 index 0000000..ca2ade5 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/startup/post_startup.py @@ -0,0 +1,76 @@ +""" +Post startup script for the BEC client. This script is executed after the +IPython shell is started. It is used to load the beamline specific +information and to setup the prompts. + +The script is executed in the global namespace of the IPython shell. This +means that all variables defined here are available in the shell. + +If needed, bec command-line arguments can be parsed here. For example, to +parse the --session argument, add the following lines to the script: + + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--session", help="Session name", type=str, default="my_default_session") + args = parser.parse_args() + + if args.session == "my_session": + print("Loading my_session session") + from bec_plugins.bec_ipython_client.plugins.my_session import * + else: + print("Loading default session") + from bec_plugins.bec_ipython_client.plugins.default_session import * +""" + +# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module +import argparse + +from bec_lib import bec_logger + +logger = bec_logger.logger + +logger.info("Using the cSAXS startup script.") + +parser = argparse.ArgumentParser() +parser.add_argument("--session", help="Session name", type=str, default="cSAXS") +args = parser.parse_args() + +if args.session == "LamNI": + print("Loading LamNI session") + from csaxs_bec.bec_ipython_client.plugins.cSAXS import * + from csaxs_bec.bec_ipython_client.plugins.LamNI import * + + lamni = LamNI(bec) + +elif args.session == "cSAXS": + print("Loading cSAXS session") + from csaxs_bec.bec_ipython_client.plugins.cSAXS import * + + +# SETUP BEAMLINE INFO +from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo + +from csaxs_bec.bec_ipython_client.plugins.cSAXS.beamline_info import BeamlineInfo + +bec._beamline_mixin._bl_info_register(BeamlineInfo) +bec._beamline_mixin._bl_info_register(SLSInfo) +bec._beamline_mixin._bl_info_register(OperatorInfo) + +# SETUP PROMPTS +bec._ip.prompts.username = args.session +bec._ip.prompts.status = 1 + + +# REGISTER BEAMLINE CHECKS +from bec_lib.bl_conditions import ( + FastOrbitFeedbackCondition, + LightAvailableCondition, + ShutterCondition, +) + +# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback) +_light_available_condition = LightAvailableCondition(dev.sls_machine_status) +_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status) +# bec.bl_checks.register(_fast_orbit_feedback_condition) +bec.bl_checks.register(_light_available_condition) +bec.bl_checks.register(_shutter_condition) diff --git a/csaxs_bec/bec_ipython_client/startup/pre_startup.py b/csaxs_bec/bec_ipython_client/startup/pre_startup.py new file mode 100644 index 0000000..dcfa194 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/startup/pre_startup.py @@ -0,0 +1,25 @@ +""" +Pre-startup script for BEC client. This script is executed before the BEC client +is started. It can be used to set up the BEC client configuration. The script is +executed in the global namespace of the BEC client. This means that all +variables defined here are available in the BEC client. + +To set up the BEC client configuration, use the ServiceConfig class. For example, +to set the configuration file path, add the following lines to the script: + + import pathlib + from bec_lib.core import ServiceConfig + + current_path = pathlib.Path(__file__).parent.resolve() + CONFIG_PATH = f"{current_path}/" + + config = ServiceConfig(CONFIG_PATH) + +If this startup script defined a ServiceConfig object, the BEC client will use +it to configure itself. Otherwise, the BEC client will use the default config. +""" + +# example: +# current_path = pathlib.Path(__file__).parent.resolve() +# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml" +# config = ServiceConfig(CONFIG_PATH) diff --git a/csaxs_bec/bec_widgets/__init__.py b/csaxs_bec/bec_widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/dap_services/__init__.py b/csaxs_bec/dap_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/device_configs/__init__.py b/csaxs_bec/device_configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/devices/__init__.py b/csaxs_bec/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/scans/__init__.py b/csaxs_bec/scans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deployment/autodeploy_versions b/deployment/autodeploy_versions deleted file mode 100644 index 44c018c..0000000 --- a/deployment/autodeploy_versions +++ /dev/null @@ -1,11 +0,0 @@ -# This file is used to select the BEC and Ophyd Devices version for the auto deployment process. -# Do not edit this file unless you know what you are doing! - -# The version can be a git tag, branch or commit hash. - -# BEC version to use -BEC_AUTODEPLOY_VERSION="master" - -# ophyd_devices version to use -OPHYD_DEVICES_AUTODEPLOY_VERSION="master" - diff --git a/deployment/bec-server-config.yaml b/deployment/bec-server-config.yaml deleted file mode 100644 index 6484ecb..0000000 --- a/deployment/bec-server-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -redis: - host: localhost - port: 6379 -mongodb: - host: localhost - port: 27017 -scibec: - host: http://[::1] - port: 3030 - beamline: "CSAXS" -service_config: - general: - reset_queue_on_cancel: True - enforce_ACLs: False - file_writer: - plugin: default_NeXus_format - base_path: ./ - diff --git a/deployment/deploy.sh b/deployment/deploy.sh deleted file mode 100755 index 799ff9f..0000000 --- a/deployment/deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ -# deployment script to be translated to Ansible - -# NOT NEEDED since the beamline repo will be autodeployed -# BEAMLINE_REPO=gitlab.psi.ch:bec/csaxs-bec.git -# git clone git@$BEAMLINE_REPO - -module add psi-python311/2024.02 - -# start redis -docker run --network=host --name redis-bec -d redis -# alternative: -# conda install -y redis; redis-server & - - -# get the target versions for ophyd_devices and BEC -source ./csaxs-bec/deployment/autodeploy_versions - -git clone -b $OPHYD_DEVICES_AUTODEPLOY_VERSION https://gitlab.psi.ch/bec/ophyd_devices.git -git clone -b $BEC_AUTODEPLOY_VERSION https://gitlab.psi.ch/bec/bec.git - -# install BEC -cd bec -source ./bin/install_bec_dev.sh - -cd ../ -# start the BEC server -bec-server start --config ./csaxs-bec/deployment/bec-server-config.yaml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..017a063 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "csaxs_bec" +version = "0.0.0" +description = "Custom device implementations based on the ophyd hardware abstraction layer" +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", +] +dependencies = ["bec_ipython_client", "bec_lib", "bec_server", "rich", "pyepics"] + +[project.optional-dependencies] +dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order"] + +[project.entry-points."bec"] +plugin_bec = "csaxs_bec" + +[project.entry-points."bec.scans"] +plugin_scans = "csaxs_bec.scans" + +[project.entry-points."bec.ipython_client"] +plugin_ipython_client = "csaxs_bec.bec_ipython_client" + +[project.entry-points."bec.widgets"] +plugin_widgets = "csaxs_bec.bec_widgets" + +[tool.hatch.build.targets.wheel] +include = ["*"] + +[tool.isort] +profile = "black" +line_length = 100 +multi_line_output = 3 +include_trailing_comma = true + +[tool.black] +line-length = 100 +skip-magic-trailing-comma = true + +[tool.pylint.basic] +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs = [ + ".*scanID.*", + ".*RID.*", + ".*pointID.*", + ".*ID.*", + ".*_2D.*", + ".*_1D.*", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a5ae6c0..0000000 --- a/setup.cfg +++ /dev/null @@ -1,21 +0,0 @@ -[metadata] -name = bec_plugins -description = BEC plugins to modify the behaviour of services within the BEC framework -long_description = file: README.md -long_description_content_type = text/markdown -url = https://gitlab.psi.ch/bec/bec -project_urls = - Bug Tracker = https://gitlab.psi.ch/bec/bec/issues -classifiers = - Programming Language :: Python :: 3 - Development Status :: 3 - Alpha - Topic :: Scientific/Engineering - -[options] -package_dir = - = . -packages = find: -python_requires = >=3.10 - -[options.packages.find] -where = . diff --git a/setup.py b/setup.py deleted file mode 100644 index c7bb35c..0000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup - -if __name__ == "__main__": - setup( - install_requires=["pyyaml", "pyepics"], - extras_require={"dev": ["pytest", "pytest-random-order", "coverage"]}, - ) From c109abce12ba8d24a4a55b6289df72b0b7e8fe26 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 19 Apr 2024 12:09:03 +0200 Subject: [PATCH 2/4] feat: populated csaxs_bec from ophyd and bec --- bec_plugins/__init__.py | 1 - bec_plugins/bec_client/__init__.py | 1 - bec_plugins/bec_client/hli/spec_hli.py | 245 -- bec_plugins/bec_client/plugins/__init__.py | 3 - .../bec_client/startup/post_startup.py | 76 - bec_plugins/bec_client/startup/pre_startup.py | 25 - .../configs/config_session_start_e20632.yaml | 3027 ----------------- bec_plugins/scibec/lamni_config.py | 152 - bec_plugins/scibec/test_config_cSAXS.yaml | 2326 ------------- bec_plugins/utils/csaxs_post_archive.py | 182 - bec_plugins/utils/saxs_params.py | 88 - bec_plugins/utils/service_config.py | 13 - {bec_plugins => bin}/utils/__init__.py | 0 .../utils/saxs_params.py | 4 + .../plugins/LamNI/LamNI_logo.png | Bin 0 -> 49460 bytes .../plugins/LamNI/__init__.py | 2 + .../plugins/LamNI/lamni_optics_mixin.py | 161 + .../LamNI/load_additional_correction.py | 22 + .../plugins/LamNI/x_ray_eye_align.py | 1332 ++++++++ .../plugins/cSAXS/__init__.py | 2 + .../plugins/cSAXS/beamline_info.py | 107 + .../plugins/cSAXS/cSAXS_beamline.py | 28 + .../plugins/cSAXS/csaxs_bl_checks.py | 122 + .../plugins/flomni/__init__.py | 1 + .../plugins/flomni/flomni.py | 2044 +++++++++++ .../plugins/flomni/flomni_optics_mixin.py | 177 + .../plugins/flomni/flomni_test_config.yaml | 340 ++ .../plugins/flomni/x_ray_eye_align.py | 244 ++ .../hli => csaxs_bec/deployment}/__init__.py | 0 .../deployment/device_server}/__init__.py | 0 .../deployment}/device_server/startup.py | 0 .../bec_device_config_sastt.yaml | 0 .../device_configs}/e21125_lamni_config.yaml | 0 csaxs_bec/device_configs/flomni_config.yaml | 344 ++ csaxs_bec/device_configs/x12sa_database.yml | 1463 ++++++++ .../devices/eiger1p5m_csaxs}/__init__.py | 0 .../devices/eiger1p5m_csaxs/eiger1p5m.py | 173 + csaxs_bec/devices/epics/__init__.py | 8 + .../devices/epics/devices/InsertionDevice.py | 28 + csaxs_bec/devices/epics/devices/XbpmBase.py | 136 + csaxs_bec/devices/epics/devices/__init__.py | 14 + .../epics/devices/cSaxsVirtualMotors.py | 32 + .../epics/devices/delay_generator_csaxs.py | 345 ++ .../devices/epics/devices/eiger9m_csaxs.py | 427 +++ .../devices/epics/devices/falcon_csaxs.py | 356 ++ .../epics/devices/flomni_sample_storage.py | 116 + csaxs_bec/devices/epics/devices/mcs_csaxs.py | 310 ++ .../epics/devices/omny_sample_storage.py | 267 ++ .../devices/epics/devices/pilatus_csaxs.py | 416 +++ csaxs_bec/devices/galil/__init__.py | 4 + .../galil/csaxs_sgalil_triggering.drawio | 201 ++ .../devices/galil/csaxs_sgalil_triggering.png | Bin 0 -> 160848 bytes csaxs_bec/devices/galil/fgalil_ophyd.py | 370 ++ csaxs_bec/devices/galil/fupr_ophyd.py | 318 ++ csaxs_bec/devices/galil/galil_ophyd.py | 604 ++++ csaxs_bec/devices/galil/sgalil.dmc | 415 +++ csaxs_bec/devices/galil/sgalil_ophyd.py | 713 ++++ csaxs_bec/devices/galil/sgalil_readme.md | 79 + csaxs_bec/devices/npoint/__init__.py | 1 + csaxs_bec/devices/npoint/npoint.py | 545 +++ csaxs_bec/devices/rt_lamni/__init__.py | 2 + csaxs_bec/devices/rt_lamni/rt_flomni_ophyd.py | 811 +++++ csaxs_bec/devices/rt_lamni/rt_lamni_ophyd.py | 847 +++++ csaxs_bec/devices/rt_lamni/rt_ophyd.py | 817 +++++ .../devices/sls_devices}/__init__.py | 0 .../devices/sls_devices/cSAXS/__init__.py | 0 csaxs_bec/devices/sls_devices/cSAXS/xeye.py | 9 + csaxs_bec/devices/smaract/__init__.py | 2 + csaxs_bec/devices/smaract/serializer.py | 44 + .../devices/smaract/smaract_controller.py | 507 +++ csaxs_bec/devices/smaract/smaract_errors.py | 47 + csaxs_bec/devices/smaract/smaract_ophyd.py | 321 ++ .../devices/smaract/smaract_sensors.json | 303 ++ .../scans}/LamNIFermatScan.py | 50 +- csaxs_bec/scans/flomni_fermat_scan.py | 328 ++ csaxs_bec/scans/owis_grid.py | 309 ++ csaxs_bec/scans/scan_plugin_template.py | 32 + csaxs_bec/scans/sgalil_grid.py | 216 ++ pyproject.toml | 4 +- tests/tests_bec_ipython_client/README.md | 31 + .../test_beamline_info.py | 61 + .../test_x_ray_eye_align.py | 105 + tests/tests_bec_widgets/README.md | 31 + tests/tests_dap_services/README.md | 31 + tests/tests_devices/README.md | 31 + .../test_delay_generator_csaxs.py | 298 ++ tests/tests_devices/test_eiger9m_csaxs.py | 456 +++ tests/tests_devices/test_falcon_csaxs.py | 313 ++ tests/tests_devices/test_fupr_ophyd.py | 60 + tests/tests_devices/test_galil.py | 149 + tests/tests_devices/test_galil_flomni.py | 172 + tests/tests_devices/test_mcs_card.py | 332 ++ tests/tests_devices/test_npoint_piezo.py | 141 + tests/tests_devices/test_pilatus_csaxs.py | 469 +++ tests/tests_devices/test_rt_flomni.py | 89 + tests/tests_devices/test_smaract.py | 217 ++ tests/tests_scans/README.md | 31 + tests/tests_scans/test_flomni_fermat_scan.py | 61 + tests/tests_scans/test_lamni_fermat_scan.py | 426 +++ tests/tests_scans/test_owis_grid.py | 62 + 100 files changed, 20464 insertions(+), 6161 deletions(-) delete mode 100644 bec_plugins/__init__.py delete mode 100644 bec_plugins/bec_client/__init__.py delete mode 100644 bec_plugins/bec_client/hli/spec_hli.py delete mode 100644 bec_plugins/bec_client/plugins/__init__.py delete mode 100644 bec_plugins/bec_client/startup/post_startup.py delete mode 100644 bec_plugins/bec_client/startup/pre_startup.py delete mode 100644 bec_plugins/configs/config_session_start_e20632.yaml delete mode 100644 bec_plugins/scibec/lamni_config.py delete mode 100644 bec_plugins/scibec/test_config_cSAXS.yaml delete mode 100755 bec_plugins/utils/csaxs_post_archive.py delete mode 100755 bec_plugins/utils/saxs_params.py delete mode 100755 bec_plugins/utils/service_config.py rename {bec_plugins => bin}/utils/__init__.py (100%) rename bec_plugins/utils/saxs_params_start_stop.py => bin/utils/saxs_params.py (95%) create mode 100644 csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png create mode 100644 csaxs_bec/bec_ipython_client/plugins/LamNI/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/LamNI/lamni_optics_mixin.py create mode 100755 csaxs_bec/bec_ipython_client/plugins/LamNI/load_additional_correction.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/LamNI/x_ray_eye_align.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/cSAXS/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/cSAXS/beamline_info.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS_beamline.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/cSAXS/csaxs_bl_checks.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/flomni/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml create mode 100644 csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py rename {bec_plugins/bec_client/hli => csaxs_bec/deployment}/__init__.py (100%) rename {bec_plugins/bec_client/startup => csaxs_bec/deployment/device_server}/__init__.py (100%) rename {bec_plugins => csaxs_bec/deployment}/device_server/startup.py (100%) rename {bec_plugins/configs => csaxs_bec/device_configs}/bec_device_config_sastt.yaml (100%) rename {bec_plugins/configs => csaxs_bec/device_configs}/e21125_lamni_config.yaml (100%) create mode 100644 csaxs_bec/device_configs/flomni_config.yaml create mode 100644 csaxs_bec/device_configs/x12sa_database.yml rename {bec_plugins/device_server => csaxs_bec/devices/eiger1p5m_csaxs}/__init__.py (100%) create mode 100644 csaxs_bec/devices/eiger1p5m_csaxs/eiger1p5m.py create mode 100644 csaxs_bec/devices/epics/__init__.py create mode 100644 csaxs_bec/devices/epics/devices/InsertionDevice.py create mode 100644 csaxs_bec/devices/epics/devices/XbpmBase.py create mode 100644 csaxs_bec/devices/epics/devices/__init__.py create mode 100644 csaxs_bec/devices/epics/devices/cSaxsVirtualMotors.py create mode 100644 csaxs_bec/devices/epics/devices/delay_generator_csaxs.py create mode 100644 csaxs_bec/devices/epics/devices/eiger9m_csaxs.py create mode 100644 csaxs_bec/devices/epics/devices/falcon_csaxs.py create mode 100644 csaxs_bec/devices/epics/devices/flomni_sample_storage.py create mode 100644 csaxs_bec/devices/epics/devices/mcs_csaxs.py create mode 100644 csaxs_bec/devices/epics/devices/omny_sample_storage.py create mode 100644 csaxs_bec/devices/epics/devices/pilatus_csaxs.py create mode 100644 csaxs_bec/devices/galil/__init__.py create mode 100644 csaxs_bec/devices/galil/csaxs_sgalil_triggering.drawio create mode 100644 csaxs_bec/devices/galil/csaxs_sgalil_triggering.png create mode 100644 csaxs_bec/devices/galil/fgalil_ophyd.py create mode 100644 csaxs_bec/devices/galil/fupr_ophyd.py create mode 100644 csaxs_bec/devices/galil/galil_ophyd.py create mode 100644 csaxs_bec/devices/galil/sgalil.dmc create mode 100644 csaxs_bec/devices/galil/sgalil_ophyd.py create mode 100644 csaxs_bec/devices/galil/sgalil_readme.md create mode 100644 csaxs_bec/devices/npoint/__init__.py create mode 100644 csaxs_bec/devices/npoint/npoint.py create mode 100644 csaxs_bec/devices/rt_lamni/__init__.py create mode 100644 csaxs_bec/devices/rt_lamni/rt_flomni_ophyd.py create mode 100644 csaxs_bec/devices/rt_lamni/rt_lamni_ophyd.py create mode 100644 csaxs_bec/devices/rt_lamni/rt_ophyd.py rename {bec_plugins/scan_server => csaxs_bec/devices/sls_devices}/__init__.py (100%) create mode 100644 csaxs_bec/devices/sls_devices/cSAXS/__init__.py create mode 100644 csaxs_bec/devices/sls_devices/cSAXS/xeye.py create mode 100644 csaxs_bec/devices/smaract/__init__.py create mode 100644 csaxs_bec/devices/smaract/serializer.py create mode 100644 csaxs_bec/devices/smaract/smaract_controller.py create mode 100644 csaxs_bec/devices/smaract/smaract_errors.py create mode 100644 csaxs_bec/devices/smaract/smaract_ophyd.py create mode 100644 csaxs_bec/devices/smaract/smaract_sensors.json rename {bec_plugins/scan_server/scan_plugins => csaxs_bec/scans}/LamNIFermatScan.py (93%) create mode 100644 csaxs_bec/scans/flomni_fermat_scan.py create mode 100644 csaxs_bec/scans/owis_grid.py create mode 100644 csaxs_bec/scans/scan_plugin_template.py create mode 100644 csaxs_bec/scans/sgalil_grid.py create mode 100644 tests/tests_bec_ipython_client/README.md create mode 100644 tests/tests_bec_ipython_client/test_beamline_info.py create mode 100644 tests/tests_bec_ipython_client/test_x_ray_eye_align.py create mode 100644 tests/tests_bec_widgets/README.md create mode 100644 tests/tests_dap_services/README.md create mode 100644 tests/tests_devices/README.md create mode 100644 tests/tests_devices/test_delay_generator_csaxs.py create mode 100644 tests/tests_devices/test_eiger9m_csaxs.py create mode 100644 tests/tests_devices/test_falcon_csaxs.py create mode 100644 tests/tests_devices/test_fupr_ophyd.py create mode 100644 tests/tests_devices/test_galil.py create mode 100644 tests/tests_devices/test_galil_flomni.py create mode 100644 tests/tests_devices/test_mcs_card.py create mode 100644 tests/tests_devices/test_npoint_piezo.py create mode 100644 tests/tests_devices/test_pilatus_csaxs.py create mode 100644 tests/tests_devices/test_rt_flomni.py create mode 100644 tests/tests_devices/test_smaract.py create mode 100644 tests/tests_scans/README.md create mode 100644 tests/tests_scans/test_flomni_fermat_scan.py create mode 100644 tests/tests_scans/test_lamni_fermat_scan.py create mode 100644 tests/tests_scans/test_owis_grid.py diff --git a/bec_plugins/__init__.py b/bec_plugins/__init__.py deleted file mode 100644 index c8ba5d1..0000000 --- a/bec_plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .bec_client import * diff --git a/bec_plugins/bec_client/__init__.py b/bec_plugins/bec_client/__init__.py deleted file mode 100644 index ba24808..0000000 --- a/bec_plugins/bec_client/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .plugins import * diff --git a/bec_plugins/bec_client/hli/spec_hli.py b/bec_plugins/bec_client/hli/spec_hli.py deleted file mode 100644 index 79700b2..0000000 --- a/bec_plugins/bec_client/hli/spec_hli.py +++ /dev/null @@ -1,245 +0,0 @@ -from bec_lib.devicemanager import Device -from bec_lib.scan_report import ScanReport - -# pylint:disable=undefined-variable -# pylint: disable=too-many-arguments - - -def dscan( - motor1: Device, m1_from: float, m1_to: float, steps: int, exp_time: float, **kwargs -) -> ScanReport: - """Relative line scan with one device. - - Args: - motor1 (Device): Device that should be scanned. - m1_from (float): Start position relative to the current position. - m1_to (float): End position relative to the current position. - steps (int): Number of steps. - exp_time (float): Exposure time. - - Returns: - ScanReport: Status object. - - Examples: - >>> dscan(dev.motor1, -5, 5, 10, 0.1) - """ - return scans.line_scan( - motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=True, **kwargs - ) - - -def d2scan( - motor1: Device, - m1_from: float, - m1_to: float, - motor2: Device, - m2_from: float, - m2_to: float, - steps: int, - exp_time: float, - **kwargs -) -> ScanReport: - """Relative line scan with two devices. - - Args: - motor1 (Device): First device that should be scanned. - m1_from (float): Start position of the first device relative to its current position. - m1_to (float): End position of the first device relative to its current position. - motor2 (Device): Second device that should be scanned. - m2_from (float): Start position of the second device relative to its current position. - m2_to (float): End position of the second device relative to its current position. - steps (int): Number of steps. - exp_time (float): Exposure time - - Returns: - ScanReport: Status object. - - Examples: - >>> d2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1) - """ - return scans.line_scan( - motor1, - m1_from, - m1_to, - motor2, - m2_from, - m2_to, - steps=steps, - exp_time=exp_time, - relative=True, - **kwargs - ) - - -def ascan(motor1, m1_from, m1_to, steps, exp_time, **kwargs): - """Absolute line scan with one device. - - Args: - motor1 (Device): Device that should be scanned. - m1_from (float): Start position. - m1_to (float): End position. - steps (int): Number of steps. - exp_time (float): Exposure time. - - Returns: - ScanReport: Status object. - - Examples: - >>> ascan(dev.motor1, -5, 5, 10, 0.1) - """ - return scans.line_scan( - motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=False, **kwargs - ) - - -def a2scan(motor1, m1_from, m1_to, motor2, m2_from, m2_to, steps, exp_time, **kwargs): - """Absolute line scan with two devices. - - Args: - motor1 (Device): First device that should be scanned. - m1_from (float): Start position of the first device. - m1_to (float): End position of the first device. - motor2 (Device): Second device that should be scanned. - m2_from (float): Start position of the second device. - m2_to (float): End position of the second device. - steps (int): Number of steps. - exp_time (float): Exposure time - - Returns: - ScanReport: Status object. - - Examples: - >>> a2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1) - """ - return scans.line_scan( - motor1, - m1_from, - m1_to, - motor2, - m2_from, - m2_to, - steps=steps, - exp_time=exp_time, - relative=False, - **kwargs - ) - - -def dmesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs): - """Relative mesh scan (grid scan) with two devices. - - Args: - motor1 (Device): First device that should be scanned. - m1_from (float): Start position of the first device relative to its current position. - m1_to (float): End position of the first device relative to its current position. - m1_steps (int): Number of steps for motor1. - motor2 (Device): Second device that should be scanned. - m2_from (float): Start position of the second device relative to its current position. - m2_to (float): End position of the second device relative to its current position. - m2_steps (int): Number of steps for motor2. - exp_time (float): Exposure time - - Returns: - ScanReport: Status object. - - Examples: - >>> dmesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1) - """ - return scans.grid_scan( - motor1, - m1_from, - m1_to, - m1_steps, - motor2, - m2_from, - m2_to, - m2_steps, - exp_time=exp_time, - relative=True, - ) - - -def amesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs): - """Absolute mesh scan (grid scan) with two devices. - - Args: - motor1 (Device): First device that should be scanned. - m1_from (float): Start position of the first device. - m1_to (float): End position of the first device. - m1_steps (int): Number of steps for motor1. - motor2 (Device): Second device that should be scanned. - m2_from (float): Start position of the second device. - m2_to (float): End position of the second device. - m2_steps (int): Number of steps for motor2. - exp_time (float): Exposure time - - Returns: - ScanReport: Status object. - - Examples: - >>> amesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1) - """ - return scans.grid_scan( - motor1, - m1_from, - m1_to, - m1_steps, - motor2, - m2_from, - m2_to, - m2_steps, - exp_time=exp_time, - relative=False, - ) - - -def umv(*args) -> ScanReport: - """Updated absolute move (i.e. blocking) for one or more devices. - - Returns: - ScanReport: Status object. - - Examples: - >>> umv(dev.samx, 1) - >>> umv(dev.samx, 1, dev.samy, 2) - """ - return scans.umv(*args, relative=False) - - -def umvr(*args) -> ScanReport: - """Updated relative move (i.e. blocking) for one or more devices. - - Returns: - ScanReport: Status object. - - Examples: - >>> umvr(dev.samx, 1) - >>> umvr(dev.samx, 1, dev.samy, 2) - """ - return scans.umv(*args, relative=True) - - -def mv(*args) -> ScanReport: - """Absolute move for one or more devices. - - Returns: - ScanReport: Status object. - - Examples: - >>> mv(dev.samx, 1) - >>> mv(dev.samx, 1, dev.samy, 2) - """ - return scans.mv(*args, relative=False) - - -def mvr(*args) -> ScanReport: - """Relative move for one or more devices. - - Returns: - ScanReport: Status object. - - Examples: - >>> mvr(dev.samx, 1) - >>> mvr(dev.samx, 1, dev.samy, 2) - """ - return scans.mv(*args, relative=True) diff --git a/bec_plugins/bec_client/plugins/__init__.py b/bec_plugins/bec_client/plugins/__init__.py deleted file mode 100644 index 9558172..0000000 --- a/bec_plugins/bec_client/plugins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .cSAXS import * - -# from .LamNI import * diff --git a/bec_plugins/bec_client/startup/post_startup.py b/bec_plugins/bec_client/startup/post_startup.py deleted file mode 100644 index bbcf0eb..0000000 --- a/bec_plugins/bec_client/startup/post_startup.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Post startup script for the BEC client. This script is executed after the -IPython shell is started. It is used to load the beamline specific -information and to setup the prompts. - -The script is executed in the global namespace of the IPython shell. This -means that all variables defined here are available in the shell. - -If needed, bec command-line arguments can be parsed here. For example, to -parse the --session argument, add the following lines to the script: - - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("--session", help="Session name", type=str, default="my_default_session") - args = parser.parse_args() - - if args.session == "my_session": - print("Loading my_session session") - from bec_plugins.bec_client.plugins.my_session import * - else: - print("Loading default session") - from bec_plugins.bec_client.plugins.default_session import * -""" - -# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module -import argparse - -from bec_lib import bec_logger - -logger = bec_logger.logger - -logger.info("Using the cSAXS startup script.") - -parser = argparse.ArgumentParser() -parser.add_argument("--session", help="Session name", type=str, default="cSAXS") -args = parser.parse_args() - -if args.session == "LamNI": - print("Loading LamNI session") - from bec_plugins.bec_client.plugins.cSAXS import * - from bec_plugins.bec_client.plugins.LamNI import * - - lamni = LamNI(bec) - -elif args.session == "cSAXS": - print("Loading cSAXS session") - from bec_plugins.bec_client.plugins.cSAXS import * - - -# SETUP BEAMLINE INFO -from bec_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo - -from bec_plugins.bec_client.plugins.cSAXS.beamline_info import BeamlineInfo - -bec._beamline_mixin._bl_info_register(BeamlineInfo) -bec._beamline_mixin._bl_info_register(SLSInfo) -bec._beamline_mixin._bl_info_register(OperatorInfo) - -# SETUP PROMPTS -bec._ip.prompts.username = args.session -bec._ip.prompts.status = 1 - - -# REGISTER BEAMLINE CHECKS -from bec_lib.bl_conditions import ( - FastOrbitFeedbackCondition, - LightAvailableCondition, - ShutterCondition, -) - -# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback) -_light_available_condition = LightAvailableCondition(dev.sls_machine_status) -_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status) -# bec.bl_checks.register(_fast_orbit_feedback_condition) -bec.bl_checks.register(_light_available_condition) -bec.bl_checks.register(_shutter_condition) diff --git a/bec_plugins/bec_client/startup/pre_startup.py b/bec_plugins/bec_client/startup/pre_startup.py deleted file mode 100644 index b39c6eb..0000000 --- a/bec_plugins/bec_client/startup/pre_startup.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Pre-startup script for BEC client. This script is executed before the BEC client -is started. It can be used to set up the BEC client configuration. The script is -executed in the global namespace of the BEC client. This means that all -variables defined here are available in the BEC client. - -To set up the BEC client configuration, use the ServiceConfig class. For example, -to set the configuration file path, add the following lines to the script: - - import pathlib - from bec_lib import ServiceConfig - - current_path = pathlib.Path(__file__).parent.resolve() - CONFIG_PATH = f"{current_path}/" - - config = ServiceConfig(CONFIG_PATH) - -If this startup script defined a ServiceConfig object, the BEC client will use -it to configure itself. Otherwise, the BEC client will use the default config. -""" - -# example: -# current_path = pathlib.Path(__file__).parent.resolve() -# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml" -# config = ServiceConfig(CONFIG_PATH) diff --git a/bec_plugins/configs/config_session_start_e20632.yaml b/bec_plugins/configs/config_session_start_e20632.yaml deleted file mode 100644 index 11d4775..0000000 --- a/bec_plugins/configs/config_session_start_e20632.yaml +++ /dev/null @@ -1,3027 +0,0 @@ -FBPMDX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMDX - read_pv: X12SA-ID-FBPMD:X - deviceTags: - - cSAXS - - fofb - name: FBPMDX - onFailure: buffer - status: - enabled: true -FBPMDY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMDY - read_pv: X12SA-ID-FBPMD:Y - deviceTags: - - cSAXS - - fofb - name: FBPMDY - onFailure: buffer - status: - enabled: true -FBPMUX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMUX - read_pv: X12SA-ID-FBPMU:X - deviceTags: - - cSAXS - - fofb - name: FBPMUX - onFailure: buffer - status: - enabled: true -FBPMUY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMUY - read_pv: X12SA-ID-FBPMU:Y - deviceTags: - - cSAXS - - fofb - name: FBPMUY - onFailure: buffer - status: - enabled: true -XASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: XASYM - read_pv: X12SA-LBB:X-ASYM - deviceTags: - - cSAXS - - fofb - name: XASYM - onFailure: buffer - status: - enabled: true -XSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: XSYM - read_pv: X12SA-LBB:X-SYM - deviceTags: - - cSAXS - - fofb - name: XSYM - onFailure: buffer - status: - enabled: true -YASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: YASYM - read_pv: X12SA-LBB:Y-ASYM - deviceTags: - - cSAXS - - fofb - name: YASYM - onFailure: buffer - status: - enabled: true -YSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: YSYM - read_pv: X12SA-LBB:Y-SYM - deviceTags: - - cSAXS - - fofb - name: YSYM - onFailure: buffer - status: - enabled: true -aptrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: ES aperture horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: aptrx - prefix: X12SA-ES1-PIN1:TRX1 - deviceTags: - - cSAXS - name: aptrx - onFailure: buffer - status: - enabled: true -aptry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: ES aperture vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: aptry - prefix: X12SA-ES1-PIN1:TRY1 - deviceTags: - - cSAXS - name: aptry - onFailure: buffer - status: - enabled: true -bm1trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 1 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm1trx - prefix: X12SA-FE-BM1:TRH - deviceTags: - - cSAXS - - bm1 - name: bm1trx - onFailure: buffer - status: - enabled: true -bm1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 1 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm1try - prefix: X12SA-FE-BM1:TRV - deviceTags: - - cSAXS - - bm1 - name: bm1try - onFailure: buffer - status: - enabled: true -bm2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm2trx - prefix: X12SA-FE-BM2:TRH - deviceTags: - - cSAXS - - bm2 - name: bm2trx - onFailure: buffer - status: - enabled: true -bm2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm2try - prefix: X12SA-FE-BM2:TRV - deviceTags: - - cSAXS - - bm2 - name: bm2try - onFailure: buffer - status: - enabled: true -bm3trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 1 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm3trx - prefix: X12SA-OP-BM1:TRX1 - deviceTags: - - cSAXS - - bm3 - name: bm3trx - onFailure: buffer - status: - enabled: true -bm3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 1 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm3try - prefix: X12SA-OP-BM1:TRY1 - deviceTags: - - cSAXS - - bm3 - name: bm3try - onFailure: buffer - status: - enabled: true -bm4trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm4trx - prefix: X12SA-OP-BM2:TRX1 - deviceTags: - - cSAXS - - bm4 - name: bm4trx - onFailure: buffer - status: - enabled: true -bm4try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm4try - prefix: X12SA-OP-BM2:TRY1 - deviceTags: - - cSAXS - - bm4 - name: bm4try - onFailure: buffer - status: - enabled: true -bm5trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 3 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm5trx - prefix: X12SA-OP-BM3:TRX1 - deviceTags: - - cSAXS - - bm5 - name: bm5trx - onFailure: buffer - status: - enabled: true -bm5try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 3 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm5try - prefix: X12SA-OP-BM3:TRY1 - deviceTags: - - cSAXS - - bm5 - name: bm5try - onFailure: buffer - status: - enabled: true -bpm1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 1: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp - deviceConfig: - name: bpm1 - prefix: 'X12SA-OP-BPM2:' - deviceTags: - - cSAXS - - bpm1 - name: bpm1 - onFailure: buffer - status: - enabled: true -bpm1i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some VME XBPM... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm1i - read_pv: X12SA-OP-BPM1:SUM - deviceTags: - - cSAXS - - bpm1 - name: bpm1i - onFailure: buffer - status: - enabled: true -bpm2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 2: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp - deviceConfig: - name: bpm2 - prefix: 'X12SA-OP-BPM2:' - deviceTags: - - cSAXS - - bpm2 - name: bpm2 - onFailure: buffer - status: - enabled: true -bpm2i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some VME XBPM... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm2i - read_pv: X12SA-OP-BPM2:SUM - deviceTags: - - cSAXS - - bpm2 - name: bpm2i - onFailure: buffer - status: - enabled: true -bpm3a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3a - read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - name: bpm3a - onFailure: buffer - status: - enabled: true -bpm3b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3b - read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - name: bpm3b - onFailure: buffer - status: - enabled: true -bpm3c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3c - read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - name: bpm3c - onFailure: buffer - status: - enabled: true -bpm3d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3d - read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - name: bpm3d - onFailure: buffer - status: - enabled: true -bpm4a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4a - read_pv: X12SA-OP1-SCALER.S2 - deviceTags: - - cSAXS - - bpm4 - name: bpm4a - onFailure: buffer - status: - enabled: true -bpm4b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4b - read_pv: X12SA-OP1-SCALER.S3 - deviceTags: - - cSAXS - - bpm4 - name: bpm4b - onFailure: buffer - status: - enabled: true -bpm4c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4c - read_pv: X12SA-OP1-SCALER.S4 - deviceTags: - - cSAXS - - bpm4 - name: bpm4c - onFailure: buffer - status: - enabled: true -bpm4d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4d - read_pv: X12SA-OP1-SCALER.S5 - deviceTags: - - cSAXS - - bpm4 - name: bpm4d - onFailure: buffer - status: - enabled: true -bpm4i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 4: integrated counts' - deviceClass: Bpm4i - deviceConfig: - name: bpm4i - prefix: X12SA-OP1-SCALER. - deviceTags: - - cSAXS - - bpm4 - name: bpm4i - onFailure: buffer - status: - enabled: true -bpm5a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5a - read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - name: bpm5a - onFailure: buffer - status: - enabled: true -bpm5b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5b - read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - name: bpm5b - onFailure: buffer - status: - enabled: true -bpm5c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5c - read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - name: bpm5c - onFailure: buffer - status: - enabled: true -bpm5d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5d - read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - name: bpm5d - onFailure: buffer - status: - enabled: true -bpm6a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6a - read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - name: bpm6a - onFailure: buffer - status: - enabled: true -bpm6b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6b - read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - name: bpm6b - onFailure: buffer - status: - enabled: true -bpm6c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6c - read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - name: bpm6c - onFailure: buffer - status: - enabled: true -bpm6d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6d - read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - name: bpm6d - onFailure: buffer - status: - enabled: true -bs1x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 x - deviceClass: EpicsMotor - deviceConfig: - name: bs1x - prefix: X12SA-ES1-BS1:TRX1 - deviceTags: - - cSAXS - - beam stop - name: bs1x - onFailure: buffer - status: - enabled: true -bs1y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 y - deviceClass: EpicsMotor - deviceConfig: - name: bs1y - prefix: X12SA-ES1-BS1:TRY1 - deviceTags: - - cSAXS - - beam stop - name: bs1y - onFailure: buffer - status: - enabled: true -bs2x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 x - deviceClass: EpicsMotor - deviceConfig: - name: bs2x - prefix: X12SA-ES1-BS2:TRX1 - deviceTags: - - cSAXS - - beam stop - name: bs2x - onFailure: buffer - status: - enabled: true -bs2y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 y - deviceClass: EpicsMotor - deviceConfig: - name: bs2y - prefix: X12SA-ES1-BS2:TRY1 - deviceTags: - - cSAXS - - beam stop - name: bs2y - onFailure: buffer - status: - enabled: true -curr: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: SLS ring current - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: curr - read_pv: ARIDI-PCT:CURRENT - deviceTags: - - cSAXS - name: curr - onFailure: buffer - status: - enabled: true -cyb: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: cyb - read_pv: X12SA-ES1-SCALER.S2 - deviceTags: - - cSAXS - name: cyb - onFailure: buffer - status: - enabled: true -dettrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dettrx - prefix: X12SA-ES1-DET1:TRX1 - deviceTags: - - cSAXS - - detector table - name: dettrx - onFailure: buffer - status: - enabled: true -di2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd diaphragm 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: di2trx - prefix: X12SA-FE-DI2:TRX1 - deviceTags: - - cSAXS - name: di2trx - onFailure: buffer - status: - enabled: true -di2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd diaphragm 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: di2try - prefix: X12SA-FE-DI2:TRY1 - deviceTags: - - cSAXS - name: di2try - onFailure: buffer - status: - enabled: true -diode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: diode - read_pv: X12SA-ES1-SCALER.S3 - deviceTags: - - cSAXS - name: diode - onFailure: buffer - status: - enabled: true -dtpush: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower tilt pusher - deviceClass: EpicsMotor - deviceConfig: - name: dtpush - prefix: X12SA-ES1-DETT:ROX1 - deviceTags: - - cSAXS - - detector table - name: dtpush - onFailure: buffer - status: - enabled: true -dtth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower tilt rotation - deviceClass: PmDetectorRotation - deviceConfig: - name: dtth - prefix: X12SA-ES1-DETT:ROX1 - deviceTags: - - cSAXS - - detector table - name: dtth - onFailure: buffer - status: - enabled: true -dttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dttrx - prefix: X12SA-ES1-DETT:TRX1 - deviceTags: - - cSAXS - - detector table - name: dttrx - onFailure: buffer - status: - enabled: true -dttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion, no encoder - deviceClass: EpicsMotor - deviceConfig: - name: dttry - prefix: X12SA-ES1-DETT:TRY1 - deviceTags: - - cSAXS - - detector table - name: dttry - onFailure: buffer - status: - enabled: true -dttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dttrz - prefix: X12SA-ES1-DETT:TRZ1 - deviceTags: - - cSAXS - - detector table - name: dttrz - onFailure: buffer - status: - enabled: true -ebtrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtrx - prefix: X12SA-ES1-EB:TRX1 - deviceTags: - - cSAXS - - xbox - name: ebtrx - onFailure: buffer - status: - enabled: true -ebtry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtry - prefix: X12SA-ES1-EB:TRY1 - deviceTags: - - cSAXS - - xbox - name: ebtry - onFailure: buffer - status: - enabled: true -ebtrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 axial movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtrz - prefix: X12SA-ES1-EB:TRZ1 - deviceTags: - - cSAXS - - xbox - name: ebtrz - onFailure: buffer - status: - enabled: true -eiger1p5m: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: monitored - schedule: sync - description: Eiger 1.5M in vacuum detector, in-house developed, PSI - deviceClass: Eiger1p5MDetector - deviceConfig: - device_access: true - name: eiger1p5m - deviceTags: - - detector - name: eiger1p5m - onFailure: retry - status: - enabled: true - enabled_set: true -eyecenx: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyecenx - read_pv: XOMNYI-XEYE-XCEN:0 - deviceTags: - - cSAXS - - xeye - name: eyecenx - onFailure: buffer - status: - enabled: true -eyeceny: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyeceny - read_pv: XOMNYI-XEYE-YCEN:0 - deviceTags: - - cSAXS - - xeye - name: eyeceny - onFailure: buffer - status: - enabled: true -eyefoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye focusing motor - deviceClass: EpicsMotor - deviceConfig: - name: eyefoc - prefix: X12SA-ES2-ES25 - deviceTags: - - cSAXS - - xeye - name: eyefoc - onFailure: buffer - status: - enabled: true -eyeint: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyeint - read_pv: XOMNYI-XEYE-INT_MEAN:0 - deviceTags: - - cSAXS - - xeye - name: eyeint - onFailure: buffer - status: - enabled: true -eyex: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye motion - deviceClass: EpicsMotor - deviceConfig: - name: eyex - prefix: X12SA-ES2-ES01 - deviceTags: - - cSAXS - - xeye - name: eyex - onFailure: buffer - status: - enabled: true -eyey: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye motion - deviceClass: EpicsMotor - deviceConfig: - name: eyey - prefix: X12SA-ES2-ES02 - deviceTags: - - cSAXS - - xeye - name: eyey - onFailure: buffer - status: - enabled: true -fal0: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal0 - read_pv: X12SA-ES1-SCALER.S4 - deviceTags: - - cSAXS - name: fal0 - onFailure: buffer - status: - enabled: true -fal1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal1 - read_pv: X12SA-ES1-SCALER.S5 - deviceTags: - - cSAXS - name: fal1 - onFailure: buffer - status: - enabled: true -fal2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal2 - read_pv: X12SA-ES1-SCALER.S6 - deviceTags: - - cSAXS - name: fal2 - onFailure: buffer - status: - enabled: true -fi1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 1 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi1try - prefix: X12SA-OP-FI1:TRY1 - deviceTags: - - cSAXS - - filter - name: fi1try - onFailure: buffer - status: - enabled: true -fi2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 2 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi2try - prefix: X12SA-OP-FI2:TRY1 - deviceTags: - - cSAXS - - filter - name: fi2try - onFailure: buffer - status: - enabled: true -fi3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 3 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi3try - prefix: X12SA-OP-FI3:TRY1 - deviceTags: - - cSAXS - - filter - name: fi3try - onFailure: buffer - status: - enabled: true -ftp: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Flight tube pressure - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: ftp - read_pv: X12SA-ES1-FT1MT1:PRESSURE - deviceTags: - - cSAXS - - flight tube - name: ftp - onFailure: buffer - status: - enabled: true -fttrx1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrx1 - prefix: X12SA-ES1-FTS1:TRX1 - deviceTags: - - cSAXS - - flight tube - name: fttrx1 - onFailure: buffer - status: - enabled: true -fttrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrx2 - prefix: X12SA-ES1-FTS2:TRX1 - deviceTags: - - cSAXS - - flight tube - name: fttrx2 - onFailure: buffer - status: - enabled: true -fttry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttry1 - prefix: X12SA-ES1-FTS1:TRY1 - deviceTags: - - cSAXS - - flight tube - name: fttry1 - onFailure: buffer - status: - enabled: true -fttry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttry2 - prefix: X12SA-ES1-FTS2:TRY1 - deviceTags: - - cSAXS - - flight tube - name: fttry2 - onFailure: buffer - status: - enabled: true -fttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrz - prefix: X12SA-ES1-FTS1:TRZ1 - deviceTags: - - cSAXS - - flight tube - name: fttrz - onFailure: buffer - status: - enabled: true -idgap: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Undulator gap size [mm] - deviceClass: InsertionDevice - deviceConfig: - name: idgap - prefix: X12SA-ID - deviceTags: - - cSAXS - name: idgap - onFailure: buffer - status: - enabled: true -led: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: led - read_pv: X12SA-ES1-SCALER.S4 - deviceTags: - - cSAXS - name: led - onFailure: buffer - status: - enabled: true -leyex: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: G - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: leyex - limits: - - 0 - - 0 - name: leyex - port: 8081 - sign: -1 - tolerance: 0.001 - deviceTags: - - lamni - name: leyex - onFailure: retry - status: - enabled: true - enabled_set: true - userParameter: - in: 14.117 -leyey: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: H - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: leyey - limits: - - 0 - - 0 - name: leyey - port: 8081 - sign: -1 - tolerance: 0.001 - deviceTags: - - lamni - name: leyey - onFailure: retry - status: - enabled: true - enabled_set: true - userParameter: - in: 48.069 - out: 0.5 -lmagnet: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor - deviceConfig: - axis_Id: F - host: mpc2680.psi.ch - labels: lmagnet - limits: - - 0 - - 0 - name: lmagnet - port: 8085 - sign: -1 - tolerance: 0.05 - deviceTags: - - lamni - name: lmagnet - onFailure: retry - status: - enabled: true - enabled_set: true -loptx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: E - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: loptx - limits: - - 0 - - 0 - name: loptx - port: 8081 - sign: 1 - tolerance: 0.5 - deviceTags: - - lamni - name: loptx - onFailure: retry - status: - enabled: true - enabled_set: false - userParameter: - in: -0.244 - out: -0.394 -lopty: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: F - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: lopty - limits: - - 0 - - 0 - name: lopty - port: 8081 - sign: 1 - tolerance: 0.5 - deviceTags: - - lamni - name: lopty - onFailure: retry - status: - enabled: true - enabled_set: false - userParameter: - in: 3.724 - out: 3.5300000000000002 -loptz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: D - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: loptz - limits: - - 0 - - 0 - name: loptz - port: 8081 - sign: -1 - tolerance: 0.5 - deviceTags: - - lamni - name: loptz - onFailure: retry - status: - enabled: true - enabled_set: false -losax: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor - deviceConfig: - axis_Id: A - host: mpc2680.psi.ch - labels: losax - limits: - - 0 - - 0 - name: losax - port: 8085 - sign: -1 - tolerance: 0.05 - deviceTags: - - lamni - name: losax - onFailure: retry - status: - enabled: true - enabled_set: true - userParameter: - in: -1.442 -losay: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor - deviceConfig: - axis_Id: B - host: mpc2680.psi.ch - labels: losay - limits: - - 0 - - 0 - name: losay - port: 8085 - sign: -1 - tolerance: 0.05 - deviceTags: - - lamni - name: losay - onFailure: retry - status: - enabled: true - enabled_set: true - userParameter: - in: -0.171 - out: 3.8 -losaz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor - deviceConfig: - axis_Id: C - host: mpc2680.psi.ch - labels: losaz - limits: - - 0 - - 0 - name: losaz - port: 8085 - sign: 1 - tolerance: 0.05 - deviceTags: - - lamni - name: losaz - onFailure: retry - status: - enabled: true - enabled_set: true - userParameter: - in: -1 - out: -3 -lsamrot: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: C - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: lsamrot - limits: - - 0 - - 0 - name: lsamrot - port: 8081 - sign: 1 - tolerance: 0.5 - deviceTags: - - lamni - name: lsamrot - onFailure: retry - status: - enabled: true - enabled_set: true -lsamx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: A - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: lsamx - limits: - - 0 - - 0 - name: lsamx - port: 8081 - sign: -1 - tolerance: 0.5 - deviceTags: - - lamni - name: lsamx - onFailure: retry - status: - enabled: true - enabled_set: false - userParameter: - center: 8.53 -lsamy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor - deviceConfig: - axis_Id: B - device_access: true - device_mapping: - rt: rtx - host: mpc2680.psi.ch - labels: lsamy - limits: - - 0 - - 0 - name: lsamy - port: 8081 - sign: 1 - tolerance: 0.5 - deviceTags: - - lamni - name: lsamy - onFailure: retry - status: - enabled: true - enabled_set: false - userParameter: - center: 10.029 -mibd1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror bender 1 - deviceClass: EpicsMotor - deviceConfig: - name: mibd1 - prefix: X12SA-OP-MI:TRZ1 - deviceTags: - - cSAXS - - mirror - name: mibd1 - onFailure: buffer - status: - enabled: true -mibd2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror bender 2 - deviceClass: EpicsMotor - deviceConfig: - name: mibd2 - prefix: X12SA-OP-MI:TRZ2 - deviceTags: - - cSAXS - - mirror - name: mibd2 - onFailure: buffer - status: - enabled: true -micfoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Microscope focusing motor - deviceClass: EpicsMotor - deviceConfig: - name: micfoc - prefix: X12SA-ES2-ES03 - deviceTags: - - cSAXS - name: micfoc - onFailure: buffer - status: - enabled: true -mitrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: mitrx - prefix: X12SA-OP-MI:TRX1 - deviceTags: - - cSAXS - - mirror - name: mitrx - onFailure: buffer - status: - enabled: true -mitry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 1 - deviceClass: EpicsMotor - deviceConfig: - name: mitry1 - prefix: X12SA-OP-MI:TRY1 - deviceTags: - - cSAXS - - mirror - name: mitry1 - onFailure: buffer - status: - enabled: true -mitry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 2 - deviceClass: EpicsMotor - deviceConfig: - name: mitry2 - prefix: X12SA-OP-MI:TRY2 - deviceTags: - - cSAXS - - mirror - name: mitry2 - onFailure: buffer - status: - enabled: true -mitry3: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 3 - deviceClass: EpicsMotor - deviceConfig: - name: mitry3 - prefix: X12SA-OP-MI:TRY3 - deviceTags: - - cSAXS - - mirror - name: mitry3 - onFailure: buffer - status: - enabled: true -mobd: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender virtual motor - deviceClass: PmMonoBender - deviceConfig: - name: mobd - prefix: 'X12SA-OP-MO:' - deviceTags: - - cSAXS - - mono - name: mobd - onFailure: buffer - status: - enabled: true -mobdai: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender inner motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdai - prefix: X12SA-OP-MO:TRYA - deviceTags: - - cSAXS - - mono - name: mobdai - onFailure: buffer - status: - enabled: true -mobdbo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender outer motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdbo - prefix: X12SA-OP-MO:TRYB - deviceTags: - - cSAXS - - mono - name: mobdbo - onFailure: buffer - status: - enabled: true -mobdco: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender outer motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdco - prefix: X12SA-OP-MO:TRYC - deviceTags: - - cSAXS - - mono - name: mobdco - onFailure: buffer - status: - enabled: true -mobddi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender inner motor - deviceClass: EpicsMotor - deviceConfig: - name: mobddi - prefix: X12SA-OP-MO:TRYD - deviceTags: - - cSAXS - - mono - name: mobddi - onFailure: buffer - status: - enabled: true -mokev: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator energy in keV - deviceClass: EnergyKev - deviceConfig: - auto_monitor: true - name: mokev - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - name: mokev - onFailure: buffer - status: - enabled: true -mopush1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 angle - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: mopush1 - read_pv: X12SA-OP-MO:ROX1 - deviceTags: - - cSAXS - - mono - name: mopush1 - onFailure: buffer - status: - enabled: true -mopush2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 angle - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: mopush2 - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - name: mopush2 - onFailure: buffer - status: - enabled: true -moroll1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 roll - deviceClass: EpicsMotor - deviceConfig: - name: moroll1 - prefix: X12SA-OP-MO:ROZ1 - deviceTags: - - cSAXS - - mono - name: moroll1 - onFailure: buffer - status: - enabled: true -moroll2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 roll movement - deviceClass: EpicsMotor - deviceConfig: - name: moroll2 - prefix: X12SA-OP-MO:ROZ2 - deviceTags: - - cSAXS - - mono - name: moroll2 - onFailure: buffer - status: - enabled: true -moth1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator Theta 1 - deviceClass: MonoTheta1 - deviceConfig: - auto_monitor: true - name: moth1 - read_pv: X12SA-OP-MO:ROX1 - deviceTags: - - cSAXS - - mono - name: moth1 - onFailure: buffer - status: - enabled: true -moth1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 theta encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: moth1e - read_pv: X12SA-OP-MO:ECX1 - deviceTags: - - cSAXS - - mono - name: moth1e - onFailure: buffer - status: - enabled: true -moth2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator Theta 2 - deviceClass: MonoTheta2 - deviceConfig: - auto_monitor: true - name: moth2 - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - name: moth2 - onFailure: buffer - status: - enabled: true -moth2e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 theta encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: moth2e - read_pv: X12SA-OP-MO:ECX2 - deviceTags: - - cSAXS - - mono - name: moth2e - onFailure: buffer - status: - enabled: true -motrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: motrx2 - prefix: X12SA-OP-MO:TRX2 - deviceTags: - - cSAXS - - mono - name: motrx2 - onFailure: buffer - status: - enabled: true -motry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch optical table vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: motry - prefix: X12SA-OP-OT:TRY - deviceTags: - - cSAXS - - mono - name: motry - onFailure: buffer - status: - enabled: true -motry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: motry2 - prefix: X12SA-OP-MO:TRY2 - deviceTags: - - cSAXS - - mono - name: motry2 - onFailure: buffer - status: - enabled: true -motrz1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 axial movement - deviceClass: EpicsMotor - deviceConfig: - name: motrz1 - prefix: X12SA-OP-MO:TRZ1 - deviceTags: - - cSAXS - - mono - name: motrz1 - onFailure: buffer - status: - enabled: true -motrz1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 axial movement encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: motrz1e - read_pv: X12SA-OP-MO:ECZ1 - deviceTags: - - cSAXS - - mono - name: motrz1e - onFailure: buffer - status: - enabled: true -moyaw2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 yaw movement - deviceClass: EpicsMotor - deviceConfig: - name: moyaw2 - prefix: X12SA-OP-MO:ROY2 - deviceTags: - - cSAXS - - mono - name: moyaw2 - onFailure: buffer - status: - enabled: true -ppth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate angle - deviceClass: EpicsMotor - deviceConfig: - name: ppth - prefix: X12SA-ES2-ES23 - deviceTags: - - cSAXS - - lamni - - phase plates - name: ppth - onFailure: buffer - status: - enabled: true - enabled_set: true -ppthenc: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: phase plate encoder - deviceClass: EpicsSignalRO - deviceConfig: - name: ppthenc - read_pv: X12SA-ES2-EC1.VAL - deviceTags: - - cSAXS - - lamni - - phase plates - name: ppthenc - onFailure: buffer - status: - enabled: true -ppx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate - deviceClass: EpicsMotor - deviceConfig: - name: ppx - prefix: X12SA-ES2-ES01 - deviceTags: - - cSAXS - - lamni - - phase plates - name: ppx - onFailure: buffer - status: - enabled: true - enabled_set: true -rtx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: RtLamniMotor - deviceConfig: - axis_Id: A - device_access: true - host: mpc2680.psi.ch - labels: rtx - limits: - - 0 - - 0 - name: rtx - port: 3333 - sign: 1 - deviceTags: - - lamni - name: rtx - onFailure: retry - status: - enabled: true - enabled_set: true -rty: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: RtLamniMotor - deviceConfig: - axis_Id: B - device_access: true - host: mpc2680.psi.ch - labels: rty - limits: - - 0 - - 0 - name: rty - port: 3333 - sign: 1 - deviceTags: - - lamni - name: rty - onFailure: retry - status: - enabled: true - enabled_set: true -samx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Sample motion - deviceClass: EpicsMotor - deviceConfig: - name: samx - prefix: X12SA-ES2-ES04 - deviceTags: - - cSAXS - name: samx - onFailure: buffer - status: - enabled: true -samy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Sample motion - deviceClass: EpicsMotor - deviceConfig: - name: samy - prefix: X12SA-ES2-ES05 - deviceTags: - - cSAXS - name: samy - onFailure: buffer - status: - enabled: true -sec: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sec - read_pv: X12SA-ES1-SCALER.S1 - deviceTags: - - cSAXS - name: sec - onFailure: buffer - status: - enabled: true -sl0h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit virtual movement - deviceClass: SlitH - deviceConfig: - name: sl0h - prefix: 'X12SA-FE-SH1:' - deviceTags: - - cSAXS - name: sl0h - onFailure: buffer - status: - enabled: true -sl0trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl0trxi - prefix: X12SA-FE-SH1:TRX1 - deviceTags: - - cSAXS - name: sl0trxi - onFailure: buffer - status: - enabled: true -sl0trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl0trxo - prefix: X12SA-FE-SH1:TRX2 - deviceTags: - - cSAXS - name: sl0trxo - onFailure: buffer - status: - enabled: true -sl1h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit virtual movement - deviceClass: SlitH - deviceConfig: - name: sl1h - prefix: 'X12SA-OP-SH1:' - deviceTags: - - cSAXS - name: sl1h - onFailure: buffer - status: - enabled: true -sl1trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1trxi - prefix: X12SA-OP-SH1:TRX2 - deviceTags: - - cSAXS - name: sl1trxi - onFailure: buffer - status: - enabled: true -sl1trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1trxo - prefix: X12SA-OP-SH1:TRX1 - deviceTags: - - cSAXS - name: sl1trxo - onFailure: buffer - status: - enabled: true -sl1tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit bottom blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1tryb - prefix: X12SA-OP-SV1:TRY2 - deviceTags: - - cSAXS - name: sl1tryb - onFailure: buffer - status: - enabled: true -sl1tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit top blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1tryt - prefix: X12SA-OP-SV1:TRY1 - deviceTags: - - cSAXS - name: sl1tryt - onFailure: buffer - status: - enabled: true -sl1v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit virtual movement - deviceClass: SlitV - deviceConfig: - name: sl1v - prefix: 'X12SA-OP-SV1:' - deviceTags: - - cSAXS - name: sl1v - onFailure: buffer - status: - enabled: true -sl2h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 virtual movement - deviceClass: SlitH - deviceConfig: - name: sl2h - prefix: 'X12SA-OP-SH2:' - deviceTags: - - cSAXS - name: sl2h - onFailure: buffer - status: - enabled: true -sl2trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2trxi - prefix: X12SA-OP-SH2:TRX2 - deviceTags: - - cSAXS - name: sl2trxi - onFailure: buffer - status: - enabled: true -sl2trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2trxo - prefix: X12SA-OP-SH2:TRX1 - deviceTags: - - cSAXS - name: sl2trxo - onFailure: buffer - status: - enabled: true -sl2tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 bottom blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2tryb - prefix: X12SA-OP-SV2:TRY2 - deviceTags: - - cSAXS - name: sl2tryb - onFailure: buffer - status: - enabled: true -sl2tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 top blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2tryt - prefix: X12SA-OP-SV2:TRY1 - deviceTags: - - cSAXS - name: sl2tryt - onFailure: buffer - status: - enabled: true -sl2v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 virtual movement - deviceClass: SlitV - deviceConfig: - name: sl2v - prefix: 'X12SA-OP-SV2:' - deviceTags: - - cSAXS - name: sl2v - onFailure: buffer - status: - enabled: true -sls_crane_usage: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_crane_usage - read_pv: IBWKR-0101-QH10003:D01_H_D-WA - string: true - deviceTags: - - SLS status - name: sls_crane_usage - onFailure: buffer - status: - enabled: true -sls_current_deadband: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_current_deadband - read_pv: ALIRF-GUN:CUR-DBAND - string: false - deviceTags: - - SLS status - name: sls_current_deadband - onFailure: buffer - status: - enabled: true -sls_current_threshold: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_current_threshold - read_pv: ALIRF-GUN:CUR-LOWLIM - string: false - deviceTags: - - SLS status - name: sls_current_threshold - onFailure: buffer - status: - enabled: true -sls_fast_orbit_feedback: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_fast_orbit_feedback - read_pv: ARIDI-BPM:FOFBSTATUS-G - string: true - deviceTags: - - SLS status - name: sls_fast_orbit_feedback - onFailure: buffer - status: - enabled: true -sls_filling_life_time: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_filling_life_time - read_pv: ARIDI-PCT:TAU-HOUR - string: false - deviceTags: - - SLS status - name: sls_filling_life_time - onFailure: buffer - status: - enabled: true -sls_filling_pattern: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_filling_pattern - read_pv: ACORF-FILL:PAT-SELECT - string: true - deviceTags: - - SLS status - name: sls_filling_pattern - onFailure: buffer - status: - enabled: true -sls_info: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: SLSInfo - deviceConfig: - name: sls_info - deviceTags: - - SLS status - name: sls_info - onFailure: buffer - status: - enabled: true -sls_injection_mode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_injection_mode - read_pv: ALIRF-GUN:INJ-MODE - string: true - deviceTags: - - SLS status - name: sls_injection_mode - onFailure: buffer - status: - enabled: true -sls_machine_status: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_machine_status - read_pv: ACOAU-ACCU:OP-MODE - string: true - deviceTags: - - SLS status - name: sls_machine_status - onFailure: buffer - status: - enabled: true -sls_operator: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: SLSOperatorMessages - deviceConfig: - name: sls_operator - deviceTags: - - SLS status - name: sls_operator - onFailure: buffer - status: - enabled: true -sls_orbit_feedback_mode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_orbit_feedback_mode - read_pv: ARIDI-BPM:OFB-MODE - string: true - deviceTags: - - SLS status - name: sls_orbit_feedback_mode - onFailure: buffer - status: - enabled: true -sls_ring_current: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: monitored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_ring_current - read_pv: ARIDI-PCT:CURRENT - string: false - deviceTags: - - SLS status - name: sls_ring_current - onFailure: buffer - status: - enabled: true -strox: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual pitch - deviceClass: GirderMotorPITCH - deviceConfig: - name: strox - prefix: X12SA-HG - deviceTags: - - cSAXS - name: strox - onFailure: buffer - status: - enabled: true -stroy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual yaw - deviceClass: GirderMotorYAW - deviceConfig: - name: stroy - prefix: X12SA-HG - deviceTags: - - cSAXS - name: stroy - onFailure: buffer - status: - enabled: true -stroz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual roll - deviceClass: GirderMotorROLL - deviceConfig: - name: stroz - prefix: X12SA-HG - deviceTags: - - cSAXS - name: stroz - onFailure: buffer - status: - enabled: true -sttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder X translation - deviceClass: GirderMotorX1 - deviceConfig: - name: sttrx - prefix: X12SA-HG - deviceTags: - - cSAXS - name: sttrx - onFailure: buffer - status: - enabled: true -sttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder Y translation - deviceClass: GirderMotorY1 - deviceConfig: - name: sttry - prefix: X12SA-HG - deviceTags: - - cSAXS - name: sttry - onFailure: buffer - status: - enabled: true -transd: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Transmission diode - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: transd - read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV - deviceTags: - - cSAXS - name: transd - onFailure: buffer - status: - enabled: true -x12sa_es1_shutter_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_es1_shutter_status - read_pv: X12SA-OP-ST1:OPEN_EPS - string: true - deviceTags: - - X12SA status - name: x12sa_es1_shutter_status - onFailure: retry - status: - enabled: true -x12sa_es1_valve: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_es1_valve - read_pv: X12SA-ES-VW1:OPEN - string: true - deviceTags: - - X12SA status - name: x12sa_es1_valve - onFailure: retry - status: - enabled: true -x12sa_exposure_box1_pressure: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_exposure_box1_pressure - read_pv: X12SA-ES-CH1MF1:PRESSURE - string: false - deviceTags: - - X12SA status - name: x12sa_exposure_box1_pressure - onFailure: retry - status: - enabled: true -x12sa_exposure_box2_pressure: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_exposure_box2_pressure - read_pv: X12SA-ES-EB1MF1:PRESSURE - string: false - deviceTags: - - X12SA status - name: x12sa_exposure_box2_pressure - onFailure: retry - status: - enabled: true -x12sa_fe_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_fe_status - read_pv: X12SA-FE-PH1:CLOSE4BL - string: true - deviceTags: - - X12SA status - name: x12sa_fe_status - onFailure: retry - status: - enabled: true -x12sa_id_gap: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_id_gap - read_pv: X12SA-ID-GAP:READ - string: false - deviceTags: - - X12SA status - name: x12sa_id_gap - onFailure: retry - status: - enabled: true -x12sa_mokev: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_mokev - read_pv: X12SA-OP-MO:E-GET - string: false - deviceTags: - - X12SA status - name: x12sa_mokev - onFailure: retry - status: - enabled: true -x12sa_op_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_op_status - read_pv: ACOAU-ACCU:OP-X12SA - string: true - deviceTags: - - X12SA status - name: x12sa_op_status - onFailure: retry - status: - enabled: true -x12sa_storage_ring_vac: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_storage_ring_vac - read_pv: X12SA-SR-VAC:SETPOINT - string: true - deviceTags: - - X12SA status - name: x12sa_storage_ring_vac - onFailure: retry - status: - enabled: true diff --git a/bec_plugins/scibec/lamni_config.py b/bec_plugins/scibec/lamni_config.py deleted file mode 100644 index 4335e48..0000000 --- a/bec_plugins/scibec/lamni_config.py +++ /dev/null @@ -1,152 +0,0 @@ -import yaml - -# TODO: fix imports, those classes are located in .../bec/scibec/init_scibec/configs/config.py -# (see also lamni_config.py in bec repository) -from .config import DemoConfig, X12SAConfig - - -class LamNIConfig(DemoConfig, X12SAConfig): - def run(self): - # self.write_galil_motors() - # self.write_rt_motors() - # self.write_smaract_motors() - # self.write_eiger1p5m() - self.write_x12sa_status() - self.write_sls_status() - self.load_csaxs_config() - # self.write_sim_user_motors() - # self.write_sim_beamline_motors() - # self.write_sim_beamline_monitors() - - def write_galil_motors(self): - lamni_galil_motors = [ - ("lsamx", "A", -1, 0.5, {"center": 8.768000}), - ("lsamy", "B", 1, 0.5, {"center": 10.041000}), - ("lsamrot", "C", 1, 0.5, {}), - ("loptz", "D", -1, 0.5, {}), - ("loptx", "E", 1, 0.5, {"in": -0.8380, "out": -0.699}), - ("lopty", "F", 1, 0.5, {"in": 3.3540, "out": 3.53}), - ("leyex", "G", -1, 0.001, {"in": 14.117000}), - ("leyey", "H", -1, 0.001, {"in": 48.069000, "out": 0.5}), - ] - out = {} - for m in lamni_galil_motors: - out[m[0]] = dict( - { - "status": {"enabled": True, "enabled_set": True}, - "deviceClass": "GalilMotor", - "deviceConfig": { - "axis_Id": m[1], - "name": m[0], - "labels": m[0], - "host": "mpc2680.psi.ch", - "port": 8081, - "sign": m[2], - "limits": [0, 0], - "tolerance": m[3], - "device_access": True, - "device_mapping": {"rt": "rtx"}, - }, - "acquisitionConfig": { - "schedule": "sync", - "acquisitionGroup": "motor", - "readoutPriority": "baseline", - }, - "deviceTags": ["lamni"], - } - ) - if m[4]: - out[m[0]]["userParameter"] = m[4] - self.write_section(out, "LamNI Galil motors") - - def write_rt_motors(self): - lamni_rt_motors = [ - ("rtx", "A", 1), - ("rty", "B", 1), - ] - out = dict() - for m in lamni_rt_motors: - out[m[0]] = dict( - { - "status": {"enabled": True, "enabled_set": True}, - "deviceClass": "RtLamniMotor", - "deviceConfig": { - "axis_Id": m[1], - "name": m[0], - "labels": m[0], - "host": "mpc2680.psi.ch", - "port": 3333, - "limits": [0, 0], - "sign": m[2], - "device_access": True, - }, - "acquisitionConfig": { - "schedule": "sync", - "acquisitionGroup": "motor", - "readoutPriority": "baseline", - }, - "deviceTags": ["lamni"], - } - ) - self.write_section(out, "LamNI RT") - - def write_smaract_motors(self): - lamni_smaract_motors = [ - ("losax", "A", -1, {"in": -0.848000}), - ("losay", "B", -1, {"in": 0.135000, "out": 3.8}), - ("losaz", "C", 1, {"in": -1, "out": -3}), - ("lcsx", "D", -1, {}), - ("lcsy", "E", -1, {}), - ] - out = dict() - for m in lamni_smaract_motors: - out[m[0]] = dict( - { - "status": {"enabled": True, "enabled_set": True}, - "deviceClass": "SmaractMotor", - "deviceConfig": { - "axis_Id": m[1], - "name": m[0], - "labels": m[0], - "host": "mpc2680.psi.ch", - "port": 8085, - "limits": [0, 0], - "sign": m[2], - "tolerance": 0.05, - }, - "acquisitionConfig": { - "schedule": "sync", - "acquisitionGroup": "motor", - "readoutPriority": "baseline", - }, - "deviceTags": ["lamni"], - } - ) - if m[3]: - out[m[0]]["userParameter"] = m[3] - self.write_section(out, "LamNI SmarAct motors") - - def write_eiger1p5m(self): - out = { - "eiger1p5m": { - "description": "Eiger 1.5M in vacuum detector, in-house developed, PSI", - "status": {"enabled": True, "enabled_set": True}, - "deviceClass": "Eiger1p5MDetector", - "deviceConfig": {"device_access": True, "name": "eiger1p5m"}, - "acquisitionConfig": { - "schedule": "sync", - "acquisitionGroup": "detector", - "readoutPriority": "monitored", - }, - "deviceTags": ["detector"], - } - } - self.write_section(out, "LamNI Eiger 1.5M in vacuum") - - def load_csaxs_config(self): - CONFIG_PATH = "./init_scibec/configs/test_config_cSAXS.yaml" - content = {} - with open(CONFIG_PATH, "r") as csaxs_config_file: - content = yaml.safe_load(csaxs_config_file.read()) - - self.write_section(content, "Default cSAXS config") diff --git a/bec_plugins/scibec/test_config_cSAXS.yaml b/bec_plugins/scibec/test_config_cSAXS.yaml deleted file mode 100644 index a49d3f8..0000000 --- a/bec_plugins/scibec/test_config_cSAXS.yaml +++ /dev/null @@ -1,2326 +0,0 @@ -ppth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate angle - deviceClass: EpicsMotor - deviceConfig: - name: ppth - prefix: X12SA-ES2-ES23 - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: true -ppx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate - deviceClass: EpicsMotor - deviceConfig: - name: ppx - prefix: X12SA-ES2-ES01 - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: true -ppthenc: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: phase plate encoder - deviceClass: EpicsSignalRO - deviceConfig: - name: ppthenc - prefix: X12SA-ES2-EC1.VAL - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: false - -FBPMDX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMDX - read_pv: X12SA-ID-FBPMD:X - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -FBPMDY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMDY - read_pv: X12SA-ID-FBPMD:Y - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -FBPMUX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMUX - read_pv: X12SA-ID-FBPMU:X - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -FBPMUY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: FBPMUY - read_pv: X12SA-ID-FBPMU:Y - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -XASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: XASYM - read_pv: X12SA-LBB:X-ASYM - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -XSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: XSYM - read_pv: X12SA-LBB:X-SYM - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -YASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: YASYM - read_pv: X12SA-LBB:Y-ASYM - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -YSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: FOFB reference - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: YSYM - read_pv: X12SA-LBB:Y-SYM - deviceTags: - - cSAXS - - fofb - onFailure: buffer - status: - enabled: true - enabled_set: false -aptrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: ES aperture horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: aptrx - prefix: X12SA-ES1-PIN1:TRX1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -aptry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: ES aperture vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: aptry - prefix: X12SA-ES1-PIN1:TRY1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -bm1trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 1 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm1trx - prefix: X12SA-FE-BM1:TRH - deviceTags: - - cSAXS - - bm1 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 1 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm1try - prefix: X12SA-FE-BM1:TRV - deviceTags: - - cSAXS - - bm1 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm2trx - prefix: X12SA-FE-BM2:TRH - deviceTags: - - cSAXS - - bm2 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd XBPM 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm2try - prefix: X12SA-FE-BM2:TRV - deviceTags: - - cSAXS - - bm2 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm3trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 1 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm3trx - prefix: X12SA-OP-BM1:TRX1 - deviceTags: - - cSAXS - - bm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 1 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm3try - prefix: X12SA-OP-BM1:TRY1 - deviceTags: - - cSAXS - - bm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm4trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm4trx - prefix: X12SA-OP-BM2:TRX1 - deviceTags: - - cSAXS - - bm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm4try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm4try - prefix: X12SA-OP-BM2:TRY1 - deviceTags: - - cSAXS - - bm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm5trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 3 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: bm5trx - prefix: X12SA-OP-BM3:TRX1 - deviceTags: - - cSAXS - - bm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -bm5try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch XBPM 3 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: bm5try - prefix: X12SA-OP-BM3:TRY1 - deviceTags: - - cSAXS - - bm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 1: Somewhere around mono (VME)" - deviceClass: XbpmCsaxsOp - deviceConfig: - name: bpm1 - prefix: "X12SA-OP-BPM2:" - deviceTags: - - cSAXS - - bpm1 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm1i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some VME XBPM... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm1i - read_pv: X12SA-OP-BPM1:SUM - deviceTags: - - cSAXS - - bpm1 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 2: Somewhere around mono (VME)" - deviceClass: XbpmCsaxsOp - deviceConfig: - name: bpm2 - prefix: "X12SA-OP-BPM2:" - deviceTags: - - cSAXS - - bpm2 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm2i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some VME XBPM... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm2i - read_pv: X12SA-OP-BPM2:SUM - deviceTags: - - cSAXS - - bpm2 - onFailure: buffer - status: - enabled: true - enabled_set: false -# bpm3: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: "XBPM 3: White beam AH501 before mono" -# deviceClass: QuadEM -# deviceConfig: -# name: bpm3 -# prefix: "X12SA-OP-BPM3:" -# deviceTags: -# - cSAXS -# - bpm3 -# onFailure: buffer -# status: -# enabled: true -# enabled_set: false -bpm3a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 3: White beam AH501 before mono" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3a - read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm3b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 3: White beam AH501 before mono" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3b - read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm3c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 3: White beam AH501 before mono" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3c - read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm3d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 3: White beam AH501 before mono" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm3d - read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm3 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm4i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 4: integrated counts" - deviceClass: Bpm4i - deviceConfig: - name: bpm4i - prefix: X12SA-OP1-SCALER. - deviceTags: - - cSAXS - - bpm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm4a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 4: VME between mono and mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4a - read_pv: X12SA-OP1-SCALER.S2 - deviceTags: - - cSAXS - - bpm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm4b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 4: VME between mono and mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4b - read_pv: X12SA-OP1-SCALER.S3 - deviceTags: - - cSAXS - - bpm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm4c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 4: VME between mono and mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4c - read_pv: X12SA-OP1-SCALER.S4 - deviceTags: - - cSAXS - - bpm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm4d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 4: VME between mono and mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm4d - read_pv: X12SA-OP1-SCALER.S5 - deviceTags: - - cSAXS - - bpm4 - onFailure: buffer - status: - enabled: true - enabled_set: false -# bpm5: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: "XBPM 5: AH501 past the mirror" -# deviceClass: QuadEM -# deviceConfig: -# name: bpm5 -# prefix: "X12SA-OP-BPM5:" -# deviceTags: -# - cSAXS -# - bpm5 -# onFailure: buffer -# status: -# enabled: true -# enabled_set: false -bpm5a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 5: AH501 past the mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5a - read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm5b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 5: AH501 past the mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5b - read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm5c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 5: AH501 past the mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5c - read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm5d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 5: AH501 past the mirror" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm5d - read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm5 - onFailure: buffer - status: - enabled: true - enabled_set: false -# bpm6: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: "XBPM 6: Xbox, not commissioned" -# deviceClass: QuadEM -# deviceConfig: -# name: bpm6 -# prefix: "X12SA-OP-BPM6:" -# deviceTags: -# - cSAXS -# - bpm6 -# onFailure: buffer -# status: -# enabled: true -# enabled_set: false -bpm6a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 6: Xbox, not commissioned" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6a - read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm6b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 6: Xbox, not commissioned" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6b - read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm6c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 6: Xbox, not commissioned" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6c - read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - onFailure: buffer - status: - enabled: true - enabled_set: false -bpm6d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: "XBPM 6: Xbox, not commissioned" - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: bpm6d - read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV - deviceTags: - - cSAXS - - bpm6 - onFailure: buffer - status: - enabled: true - enabled_set: false -bs1x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 x - deviceClass: EpicsMotor - deviceConfig: - name: bs1x - prefix: X12SA-ES1-BS1:TRX1 - deviceTags: - - cSAXS - - beam stop - onFailure: buffer - status: - enabled: true - enabled_set: false -bs1y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 y - deviceClass: EpicsMotor - deviceConfig: - name: bs1y - prefix: X12SA-ES1-BS1:TRY1 - deviceTags: - - cSAXS - - beam stop - onFailure: buffer - status: - enabled: true - enabled_set: false -bs2x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 x - deviceClass: EpicsMotor - deviceConfig: - name: bs2x - prefix: X12SA-ES1-BS2:TRX1 - deviceTags: - - cSAXS - - beam stop - onFailure: buffer - status: - enabled: true - enabled_set: false -bs2y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 y - deviceClass: EpicsMotor - deviceConfig: - name: bs2y - prefix: X12SA-ES1-BS2:TRY1 - deviceTags: - - cSAXS - - beam stop - onFailure: buffer - status: - enabled: true - enabled_set: false -curr: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: SLS ring current - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: curr - read_pv: ARIDI-PCT:CURRENT - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -cyb: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: cyb - read_pv: X12SA-ES1-SCALER.S2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -dettrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dettrx - prefix: X12SA-ES1-DET1:TRX1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -di2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd diaphragm 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: di2trx - prefix: X12SA-FE-DI2:TRX1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -di2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd diaphragm 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: di2try - prefix: X12SA-FE-DI2:TRY1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -diode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: diode - read_pv: X12SA-ES1-SCALER.S3 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -dtpush: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower tilt pusher - deviceClass: EpicsMotor - deviceConfig: - name: dtpush - prefix: X12SA-ES1-DETT:ROX1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -dtth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower tilt rotation - deviceClass: PmDetectorRotation - deviceConfig: - name: dtth - prefix: X12SA-ES1-DETT:ROX1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -dttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dttrx - prefix: X12SA-ES1-DETT:TRX1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -dttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion, no encoder - deviceClass: EpicsMotor - deviceConfig: - name: dttry - prefix: X12SA-ES1-DETT:TRY1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -dttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Detector tower motion - deviceClass: EpicsMotor - deviceConfig: - name: dttrz - prefix: X12SA-ES1-DETT:TRZ1 - deviceTags: - - cSAXS - - detector table - onFailure: buffer - status: - enabled: true - enabled_set: false -ebtrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtrx - prefix: X12SA-ES1-EB:TRX1 - deviceTags: - - cSAXS - - xbox - onFailure: buffer - status: - enabled: true - enabled_set: false -ebtry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtry - prefix: X12SA-ES1-EB:TRY1 - deviceTags: - - cSAXS - - xbox - onFailure: buffer - status: - enabled: true - enabled_set: false -ebtrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Exposure box 2 axial movement - deviceClass: EpicsMotor - deviceConfig: - name: ebtrz - prefix: X12SA-ES1-EB:TRZ1 - deviceTags: - - cSAXS - - xbox - onFailure: buffer - status: - enabled: true - enabled_set: false -eyecenx: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyecenx - read_pv: XOMNYI-XEYE-XCEN:0 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -eyeceny: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyeceny - read_pv: XOMNYI-XEYE-YCEN:0 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -eyefoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye focusing motor - deviceClass: EpicsMotor - deviceConfig: - name: eyefoc - prefix: X12SA-ES2-ES25 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -eyeint: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: X-ray eye intensit math - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: eyeint - read_pv: XOMNYI-XEYE-INT_MEAN:0 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -eyex: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye motion - deviceClass: EpicsMotor - deviceConfig: - name: eyex - prefix: X12SA-ES2-ES01 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -eyey: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: X-ray eye motion - deviceClass: EpicsMotor - deviceConfig: - name: eyey - prefix: X12SA-ES2-ES02 - deviceTags: - - cSAXS - - xeye - onFailure: buffer - status: - enabled: true - enabled_set: false -fal0: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal0 - read_pv: X12SA-ES1-SCALER.S4 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -fal1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal1 - read_pv: X12SA-ES1-SCALER.S5 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -fal2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: fal2 - read_pv: X12SA-ES1-SCALER.S6 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -fi1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 1 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi1try - prefix: X12SA-OP-FI1:TRY1 - deviceTags: - - cSAXS - - filter - onFailure: buffer - status: - enabled: true - enabled_set: false -fi2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 2 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi2try - prefix: X12SA-OP-FI2:TRY1 - deviceTags: - - cSAXS - - filter - onFailure: buffer - status: - enabled: true - enabled_set: false -fi3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch filter 3 movement - deviceClass: EpicsMotor - deviceConfig: - name: fi3try - prefix: X12SA-OP-FI3:TRY1 - deviceTags: - - cSAXS - - filter - onFailure: buffer - status: - enabled: true - enabled_set: false -ftp: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Flight tube pressure - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: ftp - read_pv: X12SA-ES1-FT1MT1:PRESSURE - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -fttrx1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrx1 - prefix: X12SA-ES1-FTS1:TRX1 - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -fttrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrx2 - prefix: X12SA-ES1-FTS2:TRX1 - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -fttry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttry1 - prefix: X12SA-ES1-FTS1:TRY1 - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -fttry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttry2 - prefix: X12SA-ES1-FTS2:TRY1 - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -fttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Dunno these motors??? - deviceClass: EpicsMotor - deviceConfig: - name: fttrz - prefix: X12SA-ES1-FTS1:TRZ1 - deviceTags: - - cSAXS - - flight tube - onFailure: buffer - status: - enabled: true - enabled_set: false -idgap: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Undulator gap size [mm] - deviceClass: InsertionDevice - deviceConfig: - name: idgap - prefix: X12SA-ID - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -led: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: led - read_pv: X12SA-ES1-SCALER.S4 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -mibd1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror bender 1 - deviceClass: EpicsMotor - deviceConfig: - name: mibd1 - prefix: X12SA-OP-MI:TRZ1 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -mibd2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror bender 2 - deviceClass: EpicsMotor - deviceConfig: - name: mibd2 - prefix: X12SA-OP-MI:TRZ2 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -micfoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Microscope focusing motor - deviceClass: EpicsMotor - deviceConfig: - name: micfoc - prefix: X12SA-ES2-ES03 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -mitrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: mitrx - prefix: X12SA-OP-MI:TRX1 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -mitry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 1 - deviceClass: EpicsMotor - deviceConfig: - name: mitry1 - prefix: X12SA-OP-MI:TRY1 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -mitry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 2 - deviceClass: EpicsMotor - deviceConfig: - name: mitry2 - prefix: X12SA-OP-MI:TRY2 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -mitry3: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Mirror vertical movement 3 - deviceClass: EpicsMotor - deviceConfig: - name: mitry3 - prefix: X12SA-OP-MI:TRY3 - deviceTags: - - cSAXS - - mirror - onFailure: buffer - status: - enabled: true - enabled_set: false -mobd: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender virtual motor - deviceClass: PmMonoBender - deviceConfig: - name: mobd - prefix: "X12SA-OP-MO:" - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mobdai: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender inner motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdai - prefix: X12SA-OP-MO:TRYA - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mobdbo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender outer motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdbo - prefix: X12SA-OP-MO:TRYB - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mobdco: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender outer motor - deviceClass: EpicsMotor - deviceConfig: - name: mobdco - prefix: X12SA-OP-MO:TRYC - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mobddi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator bender inner motor - deviceClass: EpicsMotor - deviceConfig: - name: mobddi - prefix: X12SA-OP-MO:TRYD - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mokev: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator energy in keV - deviceClass: EnergyKev - deviceConfig: - auto_monitor: true - name: mokev - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mopush1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 angle - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: mopush1 - read_pv: X12SA-OP-MO:ROX1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -mopush2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 angle - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: mopush2 - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moroll1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 roll - deviceClass: EpicsMotor - deviceConfig: - name: moroll1 - prefix: X12SA-OP-MO:ROZ1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moroll2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 roll movement - deviceClass: EpicsMotor - deviceConfig: - name: moroll2 - prefix: X12SA-OP-MO:ROZ2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moth1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator Theta 1 - deviceClass: MonoTheta1 - deviceConfig: - auto_monitor: true - name: moth1 - read_pv: X12SA-OP-MO:ROX1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moth1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 theta encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: moth1e - read_pv: X12SA-OP-MO:ECX1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moth2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator Theta 2 - deviceClass: MonoTheta2 - deviceConfig: - auto_monitor: true - name: moth2 - read_pv: X12SA-OP-MO:ROX2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moth2e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 theta encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: moth2e - read_pv: X12SA-OP-MO:ECX2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -motrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 horizontal movement - deviceClass: EpicsMotor - deviceConfig: - name: motrx2 - prefix: X12SA-OP-MO:TRX2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -motry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch optical table vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: motry - prefix: X12SA-OP-OT:TRY - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -motry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 vertical movement - deviceClass: EpicsMotor - deviceConfig: - name: motry2 - prefix: X12SA-OP-MO:TRY2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -motrz1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 axial movement - deviceClass: EpicsMotor - deviceConfig: - name: motrz1 - prefix: X12SA-OP-MO:TRZ1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -motrz1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 1 axial movement encoder - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: motrz1e - read_pv: X12SA-OP-MO:ECZ1 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -moyaw2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Monochromator crystal 2 yaw movement - deviceClass: EpicsMotor - deviceConfig: - name: moyaw2 - prefix: X12SA-OP-MO:ROY2 - deviceTags: - - cSAXS - - mono - onFailure: buffer - status: - enabled: true - enabled_set: false -samx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Sample motion - deviceClass: EpicsMotor - deviceConfig: - name: samx - prefix: X12SA-ES2-ES04 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -samy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Sample motion - deviceClass: EpicsMotor - deviceConfig: - name: samy - prefix: X12SA-ES2-ES05 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sec: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Some scaler... - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sec - read_pv: X12SA-ES1-SCALER.S1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl0h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit virtual movement - deviceClass: SlitH - deviceConfig: - name: sl0h - prefix: "X12SA-FE-SH1:" - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl0trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl0trxi - prefix: X12SA-FE-SH1:TRX1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl0trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: FrontEnd slit outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl0trxo - prefix: X12SA-FE-SH1:TRX2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit virtual movement - deviceClass: SlitH - deviceConfig: - name: sl1h - prefix: "X12SA-OP-SH1:" - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1trxi - prefix: X12SA-OP-SH1:TRX2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1trxo - prefix: X12SA-OP-SH1:TRX1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit bottom blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1tryb - prefix: X12SA-OP-SV1:TRY2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit top blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl1tryt - prefix: X12SA-OP-SV1:TRY1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl1v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit virtual movement - deviceClass: SlitV - deviceConfig: - name: sl1v - prefix: "X12SA-OP-SV1:" - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 virtual movement - deviceClass: SlitH - deviceConfig: - name: sl2h - prefix: "X12SA-OP-SH2:" - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 inner blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2trxi - prefix: X12SA-OP-SH2:TRX2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 outer blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2trxo - prefix: X12SA-OP-SH2:TRX1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 bottom blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2tryb - prefix: X12SA-OP-SV2:TRY2 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 top blade movement - deviceClass: EpicsMotor - deviceConfig: - name: sl2tryt - prefix: X12SA-OP-SV2:TRY1 - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sl2v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: OpticsHutch slit 2 virtual movement - deviceClass: SlitV - deviceConfig: - name: sl2v - prefix: "X12SA-OP-SV2:" - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -strox: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual pitch - deviceClass: GirderMotorPITCH - deviceConfig: - name: strox - prefix: X12SA-HG - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -stroy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual yaw - deviceClass: GirderMotorYAW - deviceConfig: - name: stroy - prefix: X12SA-HG - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -stroz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder virtual roll - deviceClass: GirderMotorROLL - deviceConfig: - name: stroz - prefix: X12SA-HG - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder X translation - deviceClass: GirderMotorX1 - deviceConfig: - name: sttrx - prefix: X12SA-HG - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -sttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Girder Y translation - deviceClass: GirderMotorY1 - deviceConfig: - name: sttry - prefix: X12SA-HG - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false -transd: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: Transmission diode - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: transd - read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV - deviceTags: - - cSAXS - onFailure: buffer - status: - enabled: true - enabled_set: false diff --git a/bec_plugins/utils/csaxs_post_archive.py b/bec_plugins/utils/csaxs_post_archive.py deleted file mode 100755 index 03226b4..0000000 --- a/bec_plugins/utils/csaxs_post_archive.py +++ /dev/null @@ -1,182 +0,0 @@ -import os -import json -import subprocess -import requests - -from bec_lib.file_utils import FileWriterMixin -#from bec_lib.bec_service import SERVICE_CONFIG - - -class csaxs_archiver: - """Class to archive data from a beamtime. - To run this script from a shell, go to discovery.psi.ch and copy your token. - Complement the information in user_input below in the if __name__ == __main__ part of the script. - Afterwards, get a Keberos token (kinit) for yourself in the shell. - Activate the bec_venv by doing "source bec_venv/bin/activate" and then run this code via python $filename. - As a last step, adjust the dictionary below in if __name__ == '__main__' with your token as well as information about the experiment - """ - - def __init__( - self, - start_scan: int, - stop_scan: int, - base_path: str, - log_path: str, - eacc: str, - pi: str, - pi_email: str, - token: str, - type: str = "raw", - overwrite: bool = False, - online: bool = True, - ): - self.start_scan = start_scan - self.stop_scan = stop_scan - self.log_path = os.path.expanduser(log_path) - self.eacc = eacc - self.pi = pi - self.pi_email = pi_email - self.token = token - self.type = type - self.overwrite = overwrite - self.online = online - - #from bec_lib.bec_service import SERVICE_CONFIG - #SERVICE_CONFIG.config["service_config"]["file_writer"] - self._load_datacatalogue_module() - self._create_directory(base_path) - self._disable_mail_confirmation() - - self.service_cfg = {'base_path' : f'{self.base_path}'} - - self.file_writer = FileWriterMixin(self.service_cfg) - - def _disable_mail_confirmation(self): - # Define the URL and payload - url = "https://dacat.psi.ch/api/v3/Policies/updatewhere" - payload = { - "ownerGroupList": f'p{self.eacc[1:]}', - "data": '{"archiveEmailNotification": false, "accessGroups": ["slscsaxs"]}' - } - - # Define headers - headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", - } - - # Add the access_token to the URL - url += "?access_token=" + self.token - - # Make a POST request - print(url, payload, headers) - response = requests.post(url, data=payload, headers=headers) - - # Check the response - if response.status_code == 200: - print("Request was successful.") - print(response.json()) - else: - print("Request failed with status code:", response.status_code) - print(response.text) - - def _create_directory(self, base_path: str) -> None: - if self.online: - self.base_path = os.path.expanduser("~/Data10") - else: - self.base_path = base_path - - if not os.path.exists(self.log_path): - os.makedirs(self.log_path) - - def _load_datacatalogue_module(self): - command = 'module add datacatalog/1.1.9' - os.popen(command) - # result = subprocess.run( - # command, - # shell=False, - # stdout=subprocess.PIPE, - # stderr=subprocess.PIPE, - # universal_newlines=True, - # ) - # if result.returncode == 0: - # print(f"Command {command} was succesful") - # else: - # print(f"Failed to run command {command} with return message {result.returncode}") - - def prep_metadata(self, scannr: int) -> dict: - user_metadata = {} - user_metadata.update( - { - "principalInvestigator": self.pi_email, - "owner": self.pi, - "ownerEmail": self.pi_email, - "sourceFolder": self.base_path, - "creationLocation": "/PSI/SLS/CSAXS", - "type": "raw", - "ownerGroup": f"p{self.eacc.strip('e')}", - "datasetName": f"S{scannr:05d}", - } - ) - return user_metadata - - def _write_ingestion_log(self, scannr: int) -> None: - ... - - def run_for_all_scans(self): - for scan in range(self.start_scan, self.stop_scan + 1): - print(f"Start ingestion for scan {scan}") - fname = os.path.join(os.path.expanduser(self.log_path), f"ingestion_log_S{scan:05d}") - self.datafile_name = f"{fname}.txt" - if os.path.isfile(self.datafile_name) and not self.overwrite == True: - print( - f"Skipping scan {scan}, already ingested due to logs, moving on to next scan {scan+1}" - ) - continue - - user_metadata = self.prep_metadata(scan) - - # Write metadata file in json file - self.metadata_file = f"{fname}.json" - with open(self.metadata_file, "w") as file: - json.dump(user_metadata, file) - - # Compile datapath based on structure a cSAXS - datadir_path = os.path.join('data', self.file_writer.get_scan_directory(scan, 1000, 5)) - print(f"Archiving directory {datadir_path}") - if not os.path.isdir(os.path.join(self.base_path, datadir_path)): - print(f"Did not find directory {datadir_path}, skipping scan {scan}") - continue - - # Create datafile path for archiving - with open(self.datafile_name, "w") as file: - file.write(datadir_path) - - print(f"Starting ingestion for S#{scan}") - command = f'datasetIngestor -allowexistingsource -ingest -autoarchive -noninteractive -token {self.token} {self.metadata_file} {self.datafile_name}' - rtr = os.popen(command) - - #with open(os.path.join(fname,'_log.txt'), "w") as file: - # print(f'Writing reponse to file') - # file.write(rtr.read()) - # result = subprocess.run(command, shell=False, stdout = subprocess.PIPE, stderr = subprocess.PIPE, universal_newlines=True) - # if result.returncode == 0: - # print(f"Command {command} was succesful") - # else: - # print(f"Failed to run command {command} with return message {result.returncode}") - - -if __name__ == "__main__": - # Generate dictionary with user input. - user_input = { - "base_path": "~/Data10", - "eacc": "e20638", - "pi": "Emma Sparr", - "pi_email": "emma.sparr@fkem1.lu.se", - 'log_path' : '~/Data10/documentation/ingestion_logs/', - 'token' : 'YK8gkmQmEVxVxjiA57D6tVmpBVs7T235nWEuBT0behN9BPM2BdWARWPPgEsQVrPe', - 'start_scan' : 1, - 'stop_scan' : 450, - } - archiver = csaxs_archiver(**user_input) - archiver.run_for_all_scans() diff --git a/bec_plugins/utils/saxs_params.py b/bec_plugins/utils/saxs_params.py deleted file mode 100755 index ab7950b..0000000 --- a/bec_plugins/utils/saxs_params.py +++ /dev/null @@ -1,88 +0,0 @@ -import csv -import os -from collections import defaultdict -from collections.abc import Callable - -import numpy as np - - -class ScanItem: - def __init__(self, offset_xy: Callable) -> None: - self.start_entry = None - self.end_entry = None - self.offset_xy = offset_xy - - def to_scan_params(self) -> dict: - scan_params = { - "start_x": float(self.start_entry["X"]) + self.offset_xy()[0], - "start_y": float(self.start_entry["Y"]) + self.offset_xy()[1], - "end_x": float(self.end_entry["X"]) + self.offset_xy()[0], - "end_y": float(self.end_entry["Y"]) + self.offset_xy()[1], - "interval_x": int( - np.round( - np.abs(float(self.start_entry["X"]) - float(self.end_entry["X"])) - / (float(self.start_entry["step_x [mu]"]) * 1e-3) - ) - ), - "interval_y": int( - np.round( - np.abs(float(self.start_entry["Y"]) - float(self.end_entry["Y"])) - / (float(self.start_entry["step_y [mu]"]) * 1e-3) - ) - ), - "exp_time": float(self.start_entry["exp_time [s]"]), - "readout_time": 3e-3, - "md": {"sample_name": self.start_entry["sample name"]}, - } - if scan_params["interval_x"] < 1 or scan_params["interval_x"] < 1: - raise ValueError("Bugger off...") - return scan_params - - -class SAXSParams: - def __init__(self, offset: Callable): - self.offset_xy = offset - self.data = defaultdict(lambda: ScanItem(offset)) - - def load_from_csv(self, file_path: str) -> None: - """ - Load the acquisition parameter from a csv file. - """ - - if not os.path.exists(file_path): - raise FileNotFoundError( - f"The specified CSV file could not be found. Please check that the given path is correct: {file_path}." - ) - - data_transposed = defaultdict(lambda: []) - with open(os.path.expanduser(file_path), "r") as file: - csv_reader = csv.DictReader(file) - for row in csv_reader: - for key, val in row.items(): - data_transposed[key].append(val) - if int(row["start"]): - self.data[row["sample name"]].start_entry = row - else: - self.data[row["sample name"]].end_entry = row - self._check_params(dict(data_transposed)) - - def _check_params(self, data_transposed: dict) -> dict: - sample_names = set(data_transposed["sample name"]) - if len(data_transposed["start"]) != len(sample_names) * 2: - raise ValueError( - f"The given params file does not provide N*2 start/stop positions. Found {len(sample_names)} samples and {len(data_transposed['start'])} start/stop positions." - ) - - -if __name__ == "__main__": - from pprint import pprint - - INPUT_FILE = "/sls/X12SA/data/e21206/Data10/software/test_script.csv" - - def my_offset(): - return [0, 0] - - params = SAXSParams(my_offset) - params.load_from_csv(INPUT_FILE) - for key in params.data: - pprint(params.data[key].to_scan_params()) diff --git a/bec_plugins/utils/service_config.py b/bec_plugins/utils/service_config.py deleted file mode 100755 index 06ff5d8..0000000 --- a/bec_plugins/utils/service_config.py +++ /dev/null @@ -1,13 +0,0 @@ -import yaml - -CONFIG_PATH = "/sls/X12SA/data/gac-x12saop/bec/config/bec_service_config.yaml" - - -def load_service_config() -> dict: - """Load the service configuration from the YAML file.""" - with open(CONFIG_PATH, "r", encoding="utf-8") as stream: - try: - config = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - return config diff --git a/bec_plugins/utils/__init__.py b/bin/utils/__init__.py similarity index 100% rename from bec_plugins/utils/__init__.py rename to bin/utils/__init__.py diff --git a/bec_plugins/utils/saxs_params_start_stop.py b/bin/utils/saxs_params.py similarity index 95% rename from bec_plugins/utils/saxs_params_start_stop.py rename to bin/utils/saxs_params.py index f63ab64..94e396a 100755 --- a/bec_plugins/utils/saxs_params_start_stop.py +++ b/bin/utils/saxs_params.py @@ -1,3 +1,7 @@ +""" Script used for parsing scan parameters from a CSV file as created by the motormap GUI. +This needs to be reviewed and tested during the deployment phase.""" + +# pylint: skip-file import csv import os from collections import defaultdict diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..965195cffb7ce55bced133089bc0f9ebaf0906ab GIT binary patch literal 49460 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≶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 None: + self.client = client + self.lamni = lamni + self.device_manager = client.device_manager + self.scans = client.scans + self.xeye = self.device_manager.devices.xeye + self.alignment_values = defaultdict(list) + self._reset_init_values() + self.corr_pos_x = [] + self.corr_pos_y = [] + self.corr_angle = [] + self.corr_pos_x_2 = [] + self.corr_pos_y_2 = [] + self.corr_angle_2 = [] + + def reset_correction(self): + self.corr_pos_x = [] + self.corr_pos_y = [] + self.corr_angle = [] + + def reset_correction_2(self): + self.corr_pos_x_2 = [] + self.corr_pos_y_2 = [] + self.corr_angle_2 = [] + + def reset_xray_eye_correction(self): + self.client.delete_global_var("tomo_fit_xray_eye") + + @property + def tomo_fovx_offset(self): + val = self.client.get_global_var("tomo_fov_offset") + if val is None: + return 0.0 + return val[0] / 1000 + + @tomo_fovx_offset.setter + @typechecked + def tomo_fovx_offset(self, val: float): + val_old = self.client.get_global_var("tomo_fov_offset") + if val_old is None: + val_old = [0.0, 0.0] + self.client.set_global_var("tomo_fov_offset", [val * 1000, val_old[1]]) + + @property + def tomo_fovy_offset(self): + val = self.client.get_global_var("tomo_fov_offset") + if val is None: + return 0.0 + return val[1] / 1000 + + @tomo_fovy_offset.setter + @typechecked + def tomo_fovy_offset(self, val: float): + val_old = self.client.get_global_var("tomo_fov_offset") + if val_old is None: + val_old = [0.0, 0.0] + self.client.set_global_var("tomo_fov_offset", [val_old[0], val * 1000]) + + def _reset_init_values(self): + self.shift_xy = [0, 0] + self._xray_fov_xy = [0, 0] + + def save_frame(self): + epics_put("XOMNYI-XEYE-SAVFRAME:0", 1) + + def update_frame(self): + epics_put("XOMNYI-XEYE-ACQDONE:0", 0) + # start live + epics_put("XOMNYI-XEYE-ACQ:0", 1) + # wait for start live + while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: + time.sleep(0.5) + print("waiting for live view to start...") + fshopen() + + epics_put("XOMNYI-XEYE-ACQDONE:0", 0) + + while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: + print("waiting for new frame...") + time.sleep(0.5) + + time.sleep(0.5) + # stop live view + epics_put("XOMNYI-XEYE-ACQ:0", 0) + time.sleep(1) + # fshclose + print("got new frame") + + def _disable_rt_feedback(self): + self.device_manager.devices.rtx.controller.feedback_disable() + + def _enable_rt_feedback(self): + self.device_manager.devices.rtx.controller.feedback_enable_with_reset() + + def tomo_rotate(self, val: float): + # pylint: disable=undefined-variable + umv(self.device_manager.devices.lsamrot, val) + + def get_tomo_angle(self): + return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"] + + def update_fov(self, k: int): + self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0]) + self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0]) + + @property + def movement_buttons_enabled(self): + return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")] + + @movement_buttons_enabled.setter + def movement_buttons_enabled(self, enabled: bool): + enabled = int(enabled) + epics_put("XOMNYI-XEYE-ENAMVX:0", enabled) + epics_put("XOMNYI-XEYE-ENAMVY:0", enabled) + + def send_message(self, msg: str): + epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg) + + def align(self): + # reset shift xy and fov params + self._reset_init_values() + self.reset_correction() + self.reset_correction_2() + + # this makes sure we are in a defined state + self._disable_rt_feedback() + + epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION) + + self._enable_rt_feedback() + + # initialize + # disable movement buttons + self.movement_buttons_enabled = False + + epics_put("XOMNYI-XEYE-ACQ:0", 0) + self.send_message("please wait...") + + # put sample name + epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", "Let us LAMNI...") + + # first step + self._disable_rt_feedback() + k = 0 + + # move zone plate in, eye in to get beam position + self.lamni.lfzp_in() + + self.update_frame() + + # enable submit buttons + self.movement_buttons_enabled = False + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + epics_put("XOMNYI-XEYE-STEP:0", 0) + self.send_message("Submit center value of FZP.") + + while True: + if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1: + val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") * self.PIXEL_CALIBRATION # in mm + val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm + self.alignment_values[k] = [val_x, val_y] + print( + f"Clicked position {k}: x {self.alignment_values[k][0]}, y" + f" {self.alignment_values[k][1]}" + ) + + if k == 0: # received center value of FZP + self.send_message("please wait ...") + # perform movement: FZP out, Sample in + self.lamni.loptics_out() + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + self.movement_buttons_enabled = False + print("Moving sample in, FZP out") + + self._disable_rt_feedback() + time.sleep(0.3) + self._enable_rt_feedback() + time.sleep(0.3) + + # zero is now at the center + self.update_frame() + self.send_message("Go and find the sample") + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + self.movement_buttons_enabled = True + + elif ( + k == 1 + ): # received sample center value at samroy 0 ie the final base shift values + msg = ( + f"Base shift values from movement are x {self.shift_xy[0]}, y" + f" {self.shift_xy[1]}" + ) + print(msg) + logger.info(msg) + self.shift_xy[0] += ( + self.alignment_values[0][0] - self.alignment_values[1][0] + ) * 1000 + self.shift_xy[1] += ( + self.alignment_values[1][1] - self.alignment_values[0][1] + ) * 1000 + print( + "Base shift values from movement and clicked position are x" + f" {self.shift_xy[0]}, y {self.shift_xy[1]}" + ) + + self.scans.lamni_move_to_scan_center( + self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + ).wait() + + self.send_message("please wait ...") + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + self.movement_buttons_enabled = False + time.sleep(1) + + self.scans.lamni_move_to_scan_center( + self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + ).wait() + + epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) + self.update_frame() + self.send_message("Submit sample center and FOV (0 deg)") + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + self.update_fov(k) + + elif 1 < k < 10: # received sample center value at samroy 0 ... 315 + self.send_message("please wait ...") + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + + # we swtich feedback off before rotating to not have it on and off again later for smooth operation + self._disable_rt_feedback() + self.tomo_rotate((k - 1) * 45 - 45 / 2) + self.scans.lamni_move_to_scan_center( + self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + ).wait() + self._disable_rt_feedback() + self.tomo_rotate((k - 1) * 45) + self.scans.lamni_move_to_scan_center( + self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + ).wait() + + epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) + self.update_frame() + self.send_message("Submit sample center") + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + epics_put("XOMNYI-XEYE-ENAMVX:0", 1) + self.update_fov(k) + + elif k == 10: # received sample center value at samroy 270 and done + self.send_message("done...") + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + self.movement_buttons_enabled = False + self.update_fov(k) + break + + k += 1 + epics_put("XOMNYI-XEYE-STEP:0", k) + if k < 2: + # allow movements, store movements to calculate center + _xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0") + _xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0") + if _xrayeyalignmvx != 0 or _xrayeyalignmvy != 0: + self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx + self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy + self.scans.lamni_move_to_scan_center( + self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + ).wait() + print( + f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}" + ) + epics_put("XOMNYI-XEYE-MVY:0", 0) + epics_put("XOMNYI-XEYE-MVX:0", 0) + self.update_frame() + + time.sleep(0.2) + + self.write_output() + fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2 + fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2 + print( + f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy" + f" = {fovy:.0f} microns" + ) + print("Use matlab routine to fit the current alignment...") + + print( + "This additional shift is applied to the base shift values\n which are x" + f" {self.shift_xy[0]}, y {self.shift_xy[1]}" + ) + + self._disable_rt_feedback() + + self.tomo_rotate(0) + + print( + "\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running" + " lamni.align.read_xray_eye_correction()\n" + ) + + self.client.set_global_var("tomo_fov_offset", self.shift_xy) + + def write_output(self): + with open( + os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w" + ) as alignment_values_file: + alignment_values_file.write("angle\thorizontal\tvertical\n") + for k in range(2, 11): + fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000 + fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000 + print( + f"Writing to file new alignment: number {k}, value x {fovx_offset}, y" + f" {fovy_offset}" + ) + alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n") + + def read_xray_eye_correction(self, dir_path=os.path.expanduser("~/Data10/specES1/internal/")): + tomo_fit_xray_eye = np.zeros((2, 3)) + with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file: + tomo_fit_xray_eye[0][0] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file: + tomo_fit_xray_eye[0][1] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file: + tomo_fit_xray_eye[0][2] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file: + tomo_fit_xray_eye[1][0] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file: + tomo_fit_xray_eye[1][1] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file: + tomo_fit_xray_eye[1][2] = file.readline() + + self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist()) + # x amp, phase, offset, y amp, phase, offset + # 0 0 0 1 0 2 1 0 1 1 1 2 + + print("New alignment parameters loaded from X-ray eye") + print( + f"X Amplitude {tomo_fit_xray_eye[0][0]}," + f"X Phase {tomo_fit_xray_eye[0][1]}, " + f"X Offset {tomo_fit_xray_eye[0][2]}," + f"Y Amplitude {tomo_fit_xray_eye[1][0]}," + f"Y Phase {tomo_fit_xray_eye[1][1]}," + f"Y Offset {tomo_fit_xray_eye[1][2]}" + ) + + def lamni_compute_additional_correction_xeye_mu(self, angle): + tomo_fit_xray_eye = self.client.get_global_var("tomo_fit_xray_eye") + if tomo_fit_xray_eye is None: + print("Not applying any additional correction. No x-ray eye data available.\n") + return (0, 0) + + # x amp, phase, offset, y amp, phase, offset + # 0 0 0 1 0 2 1 0 1 1 1 2 + correction_x = ( + tomo_fit_xray_eye[0][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[0][1]) + + tomo_fit_xray_eye[0][2] + ) / 1000 + correction_y = ( + tomo_fit_xray_eye[1][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[1][1]) + + tomo_fit_xray_eye[1][2] + ) / 1000 + + print(f"Xeye correction x {correction_x}, y {correction_y} for angle {angle}\n") + return (correction_x, correction_y) + + def compute_additional_correction(self, angle): + if not self.corr_pos_x: + print("Not applying any additional correction. No data available.\n") + return (0, 0) + + # find index of closest angle + for j, _ in enumerate(self.corr_pos_x): + newangledelta = np.fabs(self.corr_angle[j] - angle) + if j == 0: + angledelta = newangledelta + additional_correction_shift_x = self.corr_pos_x[j] + additional_correction_shift_y = self.corr_pos_y[j] + continue + + if newangledelta < angledelta: + additional_correction_shift_x = self.corr_pos_x[j] + additional_correction_shift_y = self.corr_pos_y[j] + angledelta = newangledelta + + if additional_correction_shift_x == 0 and angle < self.corr_angle[0]: + additional_correction_shift_x = self.corr_pos_x[0] + additional_correction_shift_y = self.corr_pos_y[0] + + if additional_correction_shift_x == 0 and angle > self.corr_angle[-1]: + additional_correction_shift_x = self.corr_pos_x[-1] + additional_correction_shift_y = self.corr_pos_y[-1] + print( + "Additional correction shifts:" + f" {additional_correction_shift_x} {additional_correction_shift_y}" + ) + return (additional_correction_shift_x, additional_correction_shift_y) + + def read_additional_correction(self, correction_file: str): + with open(correction_file, "r") as f: + num_elements = f.readline() + int_num_elements = int(num_elements.split(" ")[2]) + print(int_num_elements) + corr_pos_x = [] + corr_pos_y = [] + corr_angle = [] + for j in range(0, int_num_elements * 3): + line = f.readline() + value = line.split(" ")[2] + name = line.split(" ")[0].split("[")[0] + if name == "corr_pos_x": + corr_pos_x.append(float(value) / 1000) + elif name == "corr_pos_y": + corr_pos_y.append(float(value) / 1000) + elif name == "corr_angle": + corr_angle.append(float(value)) + self.corr_pos_x = corr_pos_x + self.corr_pos_y = corr_pos_y + self.corr_angle = corr_angle + return + + def compute_additional_correction_2(self, angle): + if not self.corr_pos_x_2: + print("Not applying any additional secondary correction. No data available.\n") + return (0, 0) + + # find index of closest angle + for j, _ in enumerate(self.corr_pos_x_2): + newangledelta = np.fabs(self.corr_angle_2[j] - angle) + if j == 0: + angledelta = newangledelta + additional_correction_shift_x = self.corr_pos_x_2[j] + additional_correction_shift_y = self.corr_pos_y_2[j] + continue + + if newangledelta < angledelta: + additional_correction_shift_x = self.corr_pos_x_2[j] + additional_correction_shift_y = self.corr_pos_y_2[j] + angledelta = newangledelta + + if additional_correction_shift_x == 0 and angle < self.corr_angle_2[0]: + additional_correction_shift_x = self.corr_pos_x_2[0] + additional_correction_shift_y = self.corr_pos_y_2[0] + + if additional_correction_shift_x == 0 and angle > self.corr_angle_2[-1]: + additional_correction_shift_x = self.corr_pos_x_2[-1] + additional_correction_shift_y = self.corr_pos_y_2[-1] + print( + "Additional correction shifts 2:" + f" {additional_correction_shift_x} {additional_correction_shift_y}" + ) + return (additional_correction_shift_x, additional_correction_shift_y) + + def read_additional_correction_2(self, correction_file: str): + with open(correction_file, "r") as f: + num_elements = f.readline() + int_num_elements = int(num_elements.split(" ")[2]) + print(int_num_elements) + corr_pos_x = [] + corr_pos_y = [] + corr_angle = [] + for j in range(0, int_num_elements * 3): + line = f.readline() + value = line.split(" ")[2] + name = line.split(" ")[0].split("[")[0] + if name == "corr_pos_x": + corr_pos_x.append(float(value) / 1000) + elif name == "corr_pos_y": + corr_pos_y.append(float(value) / 1000) + elif name == "corr_angle": + corr_angle.append(float(value)) + self.corr_pos_x_2 = corr_pos_x + self.corr_pos_y_2 = corr_pos_y + self.corr_angle_2 = corr_angle + return + + +class LamNI(LamNIOpticsMixin): + def __init__(self, client): + super().__init__() + self.client = client + self.align = XrayEyeAlign(client, self) + + self.check_shutter = True + self.check_light_available = True + self.check_fofb = True + self._check_msgs = [] + self.tomo_id = -1 + self.special_angles = [] + self.special_angle_repeats = 20 + self.special_angle_tolerance = 20 + self._current_special_angles = [] + self._beam_is_okay = True + self._stop_beam_check_event = None + self.beam_check_thread = None + + def get_beamline_checks_enabled(self): + print( + f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:" + f" {self.check_light_available}" + ) + + @property + def beamline_checks_enabled(self): + return { + "shutter": self.check_shutter, + "fofb": self.check_fofb, + "light available": self.check_light_available, + } + + @beamline_checks_enabled.setter + def beamline_checks_enabled(self, val: bool): + self.check_shutter = val + self.check_light_available = val + self.check_fofb = val + self.get_beamline_checks_enabled() + + def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5): + """Set the special angles for a tomo + + Args: + angles (list): List of special angles + repeats (int, optional): Number of repeats at a special angle. Defaults to 20. + tolerance (float, optional): Number of repeats at a special angle. Defaults to 0.5. + + """ + self.special_angles = angles + self.special_angle_repeats = repeats + self.special_angle_tolerance = tolerance + + def remove_special_angles(self): + """Remove the special angles and set the number of repeats to 1""" + self.special_angles = [] + self.special_angle_repeats = 1 + + @property + def tomo_shellstep(self): + val = self.client.get_global_var("tomo_shellstep") + if val is None: + return 1 + return val + + @tomo_shellstep.setter + def tomo_shellstep(self, val: float): + self.client.set_global_var("tomo_shellstep", val) + + @property + def tomo_circfov(self): + val = self.client.get_global_var("tomo_circfov") + if val is None: + return 0.0 + return val + + @tomo_circfov.setter + def tomo_circfov(self, val: float): + self.client.set_global_var("tomo_circfov", val) + + @property + def tomo_countingtime(self): + val = self.client.get_global_var("tomo_countingtime") + if val is None: + return 0.1 + return val + + @tomo_countingtime.setter + def tomo_countingtime(self, val: float): + self.client.set_global_var("tomo_countingtime", val) + + @property + def manual_shift_x(self): + val = self.client.get_global_var("manual_shift_x") + if val is None: + return 0.0 + return val + + @manual_shift_x.setter + def manual_shift_x(self, val: float): + self.client.set_global_var("manual_shift_x", val) + + @property + def manual_shift_y(self): + val = self.client.get_global_var("manual_shift_y") + if val is None: + return 0.0 + return val + + @manual_shift_y.setter + def manual_shift_y(self, val: float): + self.client.set_global_var("manual_shift_y", val) + + @property + def lamni_piezo_range_x(self): + val = self.client.get_global_var("lamni_piezo_range_x") + if val is None: + return 20 + return val + + @lamni_piezo_range_x.setter + def lamni_piezo_range_x(self, val: float): + if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): + self.client.set_global_var("lamni_piezo_range_x", val) + return + if val > 80: + raise ValueError("Piezo range cannot be larger than 80 um.") + self.client.set_global_var("lamni_piezo_range_x", val) + + @property + def lamni_piezo_range_y(self): + val = self.client.get_global_var("lamni_piezo_range_y") + if val is None: + return 20 + return val + + @lamni_piezo_range_y.setter + def lamni_piezo_range_y(self, val: float): + if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): + self.client.set_global_var("lamni_piezo_range_y", val) + return + if val > 80: + raise ValueError("Piezo range cannot be larger than 80 um.") + self.client.set_global_var("lamni_piezo_range_y", val) + + @property + def corridor_size(self): + val = self.client.get_global_var("corridor_size") + if val is None: + val = -1 + return val + + @corridor_size.setter + def corridor_size(self, val: float): + self.client.set_global_var("corridor_size", val) + + @property + def lamni_stitch_x(self): + val = self.client.get_global_var("lamni_stitch_x") + if val is None: + return 0 + return val + + @lamni_stitch_x.setter + @typechecked + def lamni_stitch_x(self, val: int): + self.client.set_global_var("lamni_stitch_x", val) + + @property + def lamni_stitch_y(self): + val = self.client.get_global_var("lamni_stitch_y") + if val is None: + return 0 + return val + + @lamni_stitch_y.setter + @typechecked + def lamni_stitch_y(self, val: int): + self.client.set_global_var("lamni_stitch_y", val) + + @property + def ptycho_reconstruct_foldername(self): + val = self.client.get_global_var("ptycho_reconstruct_foldername") + if val is None: + return "ptycho_reconstruct" + return val + + @ptycho_reconstruct_foldername.setter + def ptycho_reconstruct_foldername(self, val: str): + self.client.set_global_var("ptycho_reconstruct_foldername", val) + + @property + def tomo_angle_stepsize(self): + val = self.client.get_global_var("tomo_angle_stepsize") + if val is None: + return 10.0 + return val + + @tomo_angle_stepsize.setter + def tomo_angle_stepsize(self, val: float): + self.client.set_global_var("tomo_angle_stepsize", val) + + @property + def tomo_stitch_overlap(self): + val = self.client.get_global_var("tomo_stitch_overlap") + if val is None: + return 0.2 + return val + + @tomo_stitch_overlap.setter + def tomo_stitch_overlap(self, val: float): + self.client.set_global_var("tomo_stitch_overlap", val) + + @property + def sample_name(self): + val = self.client.get_global_var("sample_name") + if val is None: + return "bec_test_sample" + return val + + @sample_name.setter + @typechecked + def sample_name(self, val: str): + self.client.set_global_var("sample_name", val) + + def write_to_spec_log(self, content): + try: + with open( + os.path.expanduser( + "~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log" + ), + "a", + ) as log_file: + log_file.write(content) + except Exception: + logger.warning("Failed to write to spec log file (omny web page).") + + def write_to_scilog(self, content, tags: list = None): + try: + if tags is not None: + tags.append("BEC") + else: + tags = ["BEC"] + msg = bec.logbook.LogbookMessage() + msg.add_text(content).add_tag(tags) + self.client.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to write to scilog.") + + def tomo_scan_projection(self, angle: float): + scans = builtins.__dict__.get("scans") + + additional_correction = self.align.compute_additional_correction(angle) + additional_correction_2 = self.align.compute_additional_correction_2(angle) + correction_xeye_mu = self.align.lamni_compute_additional_correction_xeye_mu(angle) + + self._current_scan_list = [] + + for stitch_x in range(-self.lamni_stitch_x, self.lamni_stitch_x + 1): + for stitch_y in range(-self.lamni_stitch_y, self.lamni_stitch_y + 1): + # pylint: disable=undefined-variable + self._current_scan_list.append(bec.queue.next_scan_number) + logger.info( + f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}]," + f" step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0}," + f" stitch_overlap={1},center_x={self.align.tomo_fovx_offset}," + f" center_y={self.align.tomo_fovy_offset}," + f" shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]}," + f" shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]}," + f" fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')" + ) + log_message = ( + f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan" + f" number {bec.queue.next_scan_number}.\n" + ) + self.write_to_spec_log(log_message) + # self.write_to_scilog(log_message, ["BEC_scans", self.sample_name]) + corridor_size = self.corridor_size if self.corridor_size > 0 else None + scans.lamni_fermat_scan( + fov_size=[self.lamni_piezo_range_x, self.lamni_piezo_range_y], + step=self.tomo_shellstep, + stitch_x=stitch_x, + stitch_y=stitch_y, + stitch_overlap=self.tomo_stitch_overlap, + center_x=self.align.tomo_fovx_offset, + center_y=self.align.tomo_fovy_offset, + shift_x=( + self.manual_shift_x + + correction_xeye_mu[0] + - additional_correction[0] + - additional_correction_2[0] + ), + shift_y=( + self.manual_shift_y + + correction_xeye_mu[1] + - additional_correction[1] + - additional_correction_2[1] + ), + fov_circular=self.tomo_circfov, + angle=angle, + scan_type="fly", + exp_time=self.tomo_countingtime, + optim_trajectory_corridor=corridor_size, + ) + + def _run_beamline_checks(self): + msgs = [] + dev = builtins.__dict__.get("dev") + try: + if self.check_shutter: + shutter_val = dev.x12sa_es1_shutter_status.read(cached=True) + if shutter_val["value"].lower() != "open": + self._beam_is_okay = False + msgs.append("Check beam failed: Shutter is closed.") + if self.check_light_available: + machine_status = dev.sls_machine_status.read(cached=True) + if machine_status["value"] not in ["Light Available", "Light-Available"]: + self._beam_is_okay = False + msgs.append("Check beam failed: Light not available.") + if self.check_fofb: + fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True) + if fast_orbit_feedback["value"] != "running": + self._beam_is_okay = False + msgs.append("Check beam failed: Fast orbit feedback is not running.") + except Exception: + logger.warning("Failed to check beam.") + return msgs + + def _check_beam(self): + while not self._stop_beam_check_event.is_set(): + self._check_msgs = self._run_beamline_checks() + + if not self._beam_is_okay: + self._stop_beam_check_event.set() + time.sleep(1) + + def _start_beam_check(self): + self._beam_is_okay = True + self._stop_beam_check_event = threading.Event() + + self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True) + self.beam_check_thread.start() + + def _was_beam_okay(self): + self._stop_beam_check_event.set() + self.beam_check_thread.join() + return self._beam_is_okay + + def _print_beamline_checks(self): + for msg in self._check_msgs: + logger.warning(msg) + + def _wait_for_beamline_checks(self): + self._print_beamline_checks() + try: + msg = bec.logbook.LogbookMessage() + msg.add_text( + "

Beamline checks failed at" + f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}

" + ).add_tag(["BEC", "beam_check"]) + self.client.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to send update to SciLog.") + + while True: + self._beam_is_okay = True + self._check_msgs = self._run_beamline_checks() + if self._beam_is_okay: + break + self._print_beamline_checks() + time.sleep(1) + + try: + msg = bec.logbook.LogbookMessage() + msg.add_text( + "

Operation resumed at" + f" {str(datetime.datetime.now())}.

" + ).add_tag(["BEC", "beam_check"]) + self.client.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to send update to SciLog.") + + def add_sample_database( + self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user + ): + """Add a sample to the omny sample database. This also retrieves the tomo id.""" + subprocess.run( + "wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt" + f" 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'", + shell=True, + ) + with open("/tmp/currsamplesnr.txt") as tomo_number_file: + tomo_number = int(tomo_number_file.read()) + return tomo_number + + def _at_each_angle(self, angle: float) -> None: + self.tomo_scan_projection(angle) + self.tomo_reconstruct() + + ### XMCD ### + # 2 projections, 1 for each polarization state + # cp() + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + # cm() + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + + def sub_tomo_scan(self, subtomo_number, start_angle=None): + """start a subtomo""" + dev = builtins.__dict__.get("dev") + bec = builtins.__dict__.get("bec") + if self.tomo_id > 0: + tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] + else: + tags = ["BEC_subtomo", self.sample_name] + self.write_to_scilog( + f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", + tags, + ) + + if start_angle is None: + if subtomo_number == 1: + start_angle = 0 + elif subtomo_number == 2: + start_angle = self.tomo_angle_stepsize / 8.0 * 4 + elif subtomo_number == 3: + start_angle = self.tomo_angle_stepsize / 8.0 * 2 + elif subtomo_number == 4: + start_angle = self.tomo_angle_stepsize / 8.0 * 6 + elif subtomo_number == 5: + start_angle = self.tomo_angle_stepsize / 8.0 * 1 + elif subtomo_number == 6: + start_angle = self.tomo_angle_stepsize / 8.0 * 5 + elif subtomo_number == 7: + start_angle = self.tomo_angle_stepsize / 8.0 * 3 + elif subtomo_number == 8: + start_angle = self.tomo_angle_stepsize / 8.0 * 7 + + # _tomo_shift_angles (potential global variable) + _tomo_shift_angles = 0 + angle_end = start_angle + 360 + for angle in np.linspace( + start_angle + _tomo_shift_angles, + angle_end, + num=int(360 / self.tomo_angle_stepsize) + 1, + endpoint=True, + ): + successful = False + error_caught = False + if 0 <= angle < 360.05: + print(f"Starting LamNI scan for angle {angle}") + while not successful: + self._start_beam_check() + if not self.special_angles: + self._current_special_angles = [] + if self._current_special_angles: + next_special_angle = self._current_special_angles[0] + if np.isclose(angle, next_special_angle, atol=0.5): + self._current_special_angles.pop(0) + num_repeats = self.special_angle_repeats + else: + num_repeats = 1 + try: + start_scan_number = bec.queue.next_scan_number + for i in range(num_repeats): + self._at_each_angle(angle) + error_caught = False + except AlarmBase as exc: + if exc.alarm_type == "TimeoutError": + bec.queue.request_queue_reset() + time.sleep(2) + error_caught = True + else: + raise exc + + if self._was_beam_okay() and not error_caught: + successful = True + else: + self._wait_for_beamline_checks() + end_scan_number = bec.queue.next_scan_number + for scan_nr in range(start_scan_number, end_scan_number): + self._write_tomo_scan_number(scan_nr, angle, subtomo_number) + + def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None: + tomo_scan_numbers_file = os.path.expanduser( + "~/Data10/specES1/dat-files/tomography_scannumbers.txt" + ) + with open(tomo_scan_numbers_file, "a+") as out_file: + # pylint: disable=undefined-variable + out_file.write( + f"{scan_number} {angle} {dev.lsamrot.read()['lsamrot']['value']:.3f} {self.tomo_id} {subtomo_number} {0} {'lamni'}\n" + ) + + def tomo_scan(self, subtomo_start=1, start_angle=None): + """start a tomo scan""" + bec = builtins.__dict__.get("bec") + scans = builtins.__dict__.get("scans") + self._current_special_angles = self.special_angles.copy() + + if subtomo_start == 1 and start_angle is None: + # pylint: disable=undefined-variable + self.tomo_id = self.add_sample_database( + self.sample_name, + str(datetime.date.today()), + bec.active_account.decode(), + bec.queue.next_scan_number, + "lamni", + "test additional info", + "BEC", + ) + self.write_pdf_report() + with scans.dataset_id_on_hold: + for ii in range(subtomo_start, 9): + self.sub_tomo_scan(ii, start_angle=start_angle) + start_angle = None + + def tomo_parameters(self): + """print and update the tomo parameters""" + print("Current settings:") + print(f"Counting time = {self.tomo_countingtime} s") + print(f"Stepsize microns = {self.tomo_shellstep}") + print( + f"Piezo range (max 80) = {self.lamni_piezo_range_x}," + f" {self.lamni_piezo_range_y}" + ) + print(f"Stitching number x,y = {self.lamni_stitch_x}, {self.lamni_stitch_y}") + print(f"Stitching overlap = {self.tomo_stitch_overlap}") + print(f"Circuilar FOV diam = {self.tomo_circfov}") + print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}") + print( + "For information, fov offset is rotating and finding the ROI, manual shift moves" + " rotation center" + ) + print(f" _tomo_fovx_offset = {self.align.tomo_fovx_offset}") + print(f" _tomo_fovy_offset = {self.align.tomo_fovy_offset}") + print(f" _manual_shift_x = {self.manual_shift_x}") + print(f" _manual_shift_y = {self.manual_shift_y}") + print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") + print(f"Resulting in number of projections: {360/self.tomo_angle_stepsize*8}") + print(f"Sample name: {self.sample_name}\n") + + user_input = input("Are these parameters correctly set for your scan? ") + if user_input == "y": + print("good then") + else: + self.tomo_countingtime = self._get_val(" s", self.tomo_countingtime, float) + self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) + self.lamni_piezo_range_x = self._get_val( + " um", self.lamni_piezo_range_x, float + ) + self.lamni_piezo_range_y = self._get_val( + " um", self.lamni_piezo_range_y, float + ) + self.lamni_stitch_x = self._get_val("", self.lamni_stitch_x, int) + self.lamni_stitch_y = self._get_val("", self.lamni_stitch_y, int) + self.tomo_circfov = self._get_val(" um", self.tomo_circfov, float) + self.ptycho_reconstruct_foldername = self._get_val( + "Reconstruction queue ", self.ptycho_reconstruct_foldername, str + ) + tomo_numberofprojections = self._get_val( + "Number of projections", 360 / self.tomo_angle_stepsize * 8, int + ) + + print(f"The angular step will be {360/tomo_numberofprojections}") + self.tomo_angle_stepsize = 360 / tomo_numberofprojections * 8 + print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}") + self.sample_name = self._get_val("sample name", self.sample_name, str) + + @staticmethod + def _get_val(msg: str, default_value, data_type): + return data_type(input(f"{msg} ({default_value}): ") or default_value) + + def tomo_reconstruct(self, base_path="~/Data10/specES1"): + """write the tomo reconstruct file for the reconstruction queue""" + bec = builtins.__dict__.get("bec") + base_path = os.path.expanduser(base_path) + ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername)) + ptycho_queue_path.mkdir(parents=True, exist_ok=True) + + # pylint: disable=undefined-variable + last_scan_number = bec.queue.next_scan_number - 1 + ptycho_queue_file = os.path.abspath( + os.path.join(ptycho_queue_path, f"scan_{last_scan_number:05d}.dat") + ) + with open(ptycho_queue_file, "w") as queue_file: + scans = " ".join([str(scan) for scan in self._current_scan_list]) + queue_file.write(f"p.scan_number {scans}\n") + queue_file.write("p.check_nextscan_started 1\n") + + def write_pdf_report(self): + """create and write the pdf report with the current LamNI settings""" + dev = builtins.__dict__.get("dev") + header = ( + " \n" * 3 + + " ::: ::: ::: ::: :::: ::: ::::::::::: \n" + + " :+: :+: :+: :+:+: :+:+: :+:+: :+: :+: \n" + + " +:+ +:+ +:+ +:+ +:+:+ +:+ :+:+:+ +:+ +:+ \n" + + " +#+ +#++:++#++: +#+ +:+ +#+ +#+ +:+ +#+ +#+ \n" + + " +#+ +#+ +#+ +#+ +#+ +#+ +#+#+# +#+ \n" + + " #+# #+# #+# #+# #+# #+# #+#+# #+# \n" + + " ########## ### ### ### ### ### #### ########### \n" + ) + padding = 20 + piezo_range = f"{self.lamni_piezo_range_x:.2f}/{self.lamni_piezo_range_y:.2f}" + stitching = f"{self.lamni_stitch_x:.2f}/{self.lamni_stitch_y:.2f}" + dataset_id = str(self.client.queue.next_dataset_number) + content = [ + f"{'Sample Name:':<{padding}}{self.sample_name:>{padding}}\n", + f"{'Measurement ID:':<{padding}}{str(self.tomo_id):>{padding}}\n", + f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", + f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", + f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n", + ( + f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n" + ), + f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", + ( + f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n" + ), + ( + f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n" + ), + f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", + f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", + f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range:>{padding}}\n", + f"{'Restriction to circular FOV:':<{padding}}{self.tomo_circfov:>{padding}.2f}\n", + f"{'Stitching:':<{padding}}{stitching:>{padding}}\n", + f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n", + ( + f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n" + ), + ] + content = "".join(content) + user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf") + with PDFWriter(user_target) as file: + file.write(header) + file.write(content) + subprocess.run( + "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True + ) + # status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True) + msg = bec.logbook.LogbookMessage() + logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") + msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).add_tag( + ["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name] + ) + self.client.logbook.send_logbook_message(msg) + + +class MagLamNI(LamNI): + def sub_tomo_scan(self, subtomo_number, start_angle=None): + super().sub_tomo_scan(subtomo_number, start_angle) + # self.rotate_slowly(0) + + def rotate_slowly(self, angle, step_size=20): + current_angle = dev.lsamrot.read(cached=True)["value"] + steps = int(np.ceil(np.abs(current_angle - angle) / step_size)) + 1 + for target_angle in np.linspace(current_angle, angle, steps, endpoint=True): + umv(dev.lsamrot, target_angle) + scans.lamni_move_to_scan_center( + self.align.tomo_fovx_offset, self.align.tomo_fovy_offset, target_angle + ) + + def _at_each_angle(self, angle: float) -> None: + if "lamni_at_each_angle" in builtins.__dict__: + lamni_at_each_angle(self, angle) + return + + self.tomo_scan_projection(angle) + self.tomo_reconstruct() + + # # cm() + # # umv(dev.ppth,15.1762) #11.567 keV + # for ii in range(2): + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + # # cp() + # # umv(dev.ppth,15.1827) #11.567 keV + # for ii in range(2): + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + + +class DataDrivenLamNI(LamNI): + def __init__(self, client): + super().__init__(client) + self.tomo_data = {} + + def tomo_scan( + self, + subtomo_start=1, + start_index=None, + fname="~/Data10/data_driven_config/datadriven_params.h5", + ): + """start a tomo scan""" + bec = builtins.__dict__.get("bec") + scans = builtins.__dict__.get("scans") + + fname = os.path.expanduser(fname) + + if not os.path.exists(fname): + raise FileNotFoundError(f"Could not find datadriven params file in {fname}.") + content = f"Loading tomo parameters from {fname}." + logger.warning(content) + tags = ["Data_driven_file", "BEC"] + msg = bec.logbook.LogbookMessage() + msg.add_text(content).add_tag(tags) + self.client.logbook.send_logbook_message(msg) + self._update_tomo_data_from_file(fname) + + self._current_special_angles = self.special_angles.copy() + + if subtomo_start == 1 and start_index is None: + # pylint: disable=undefined-variable + self.tomo_id = self.add_sample_database( + self.sample_name, + str(datetime.date.today()), + bec.active_account.decode(), + bec.queue.next_scan_number, + "lamni", + "test additional info", + "BEC", + ) + self.write_pdf_report() + with scans.dataset_id_on_hold: + self.sub_tomo_data_driven(start_index) + + def sub_tomo_scan(self): + raise NotImplementedError( + "Cannot run sub_tomo_scan with data-driven LamNI. Please use" + " lamni.tomo_scan(subtomo_start=) instead." + ) + + def _at_each_angle( + self, angle=None, stepsize=None, loptz_pos=None, manual_shift_x=0, manual_shift_y=0 + ): + # Do something... + # self.tomo_parameters + self.manual_shift_x = manual_shift_x + self.manual_shift_y = manual_shift_y + self.tomo_shellstep = stepsize # in microns + if loptz_pos is not None: + dev.rtx.controller.feedback_disable() + umv(dev.loptz, loptz_pos) + super()._at_each_angle(angle=angle) + + def sub_tomo_data_driven(self, start_index=None): + # for theta, stepsize, sample_to_focus, probe_diameter, subtomo_id in zip(*self.tomo_data.values()): + + for scan_index, scan_data in enumerate(zip(*self.tomo_data.values())): + if start_index and scan_index < start_index: + continue + ( + angle, + stepsize, + loptz_pos, + propagation_distance, + manual_shift_x, + manual_shift_y, + subtomo_number, + ) = scan_data + bec.metadata.update( + {key: float(val) for key, val in zip(self.tomo_data.keys(), scan_data)} + ) + successful = False + error_caught = False + if 0 <= angle < 360.05: + print(f"Starting LamNI scan for angle {angle}") + while not successful: + self._start_beam_check() + if not self.special_angles: + self._current_special_angles = [] + if self._current_special_angles: + next_special_angle = self._current_special_angles[0] + if np.isclose(angle, next_special_angle, atol=0.5): + self._current_special_angles.pop(0) + num_repeats = self.special_angle_repeats + else: + num_repeats = 1 + try: + start_scan_number = bec.queue.next_scan_number + for i in range(num_repeats): + self._at_each_angle( + float(angle), + stepsize=float(stepsize), + loptz_pos=float(loptz_pos), + manual_shift_x=float(manual_shift_x), + manual_shift_y=float(manual_shift_y), + ) + error_caught = False + except AlarmBase as exc: + if exc.alarm_type == "TimeoutError": + bec.queue.request_queue_reset() + time.sleep(2) + error_caught = True + else: + raise exc + + if self._was_beam_okay() and not error_caught: + successful = True + else: + self._wait_for_beamline_checks() + end_scan_number = bec.queue.next_scan_number + for scan_nr in range(start_scan_number, end_scan_number): + self._write_tomo_scan_number(scan_nr, angle, subtomo_number) + + def _update_tomo_data_from_file(self, fname: str) -> None: + with h5py.File(fname, "r") as file: + self.tomo_data["theta"] = np.array([*file["theta"]]).flatten() + self.tomo_data["stepsize"] = np.array([*file["stepsize"]]).flatten() + self.tomo_data["loptz"] = np.array([*file["loptz"]]).flatten() + self.tomo_data["propagation_distance"] = np.array( + [*file["relative_propagation_distance"]] + ).flatten() + self.tomo_data["manual_shift_x"] = np.array([*file["manual_shift_x"]]).flatten() + self.tomo_data["manual_shift_y"] = np.array([*file["manual_shift_y"]]).flatten() + self.tomo_data["subtomo_id"] = np.array([*file["subtomo_id"]]).flatten() + + shapes = [] + for data in self.tomo_data.values(): + shapes.append(data.shape) + if len(set(shapes)) > 1: + raise ValueError(f"Tomo data file has entries of inconsistent lengths: {shapes}.") diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/__init__.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/__init__.py new file mode 100644 index 0000000..dfadb2d --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/__init__.py @@ -0,0 +1,2 @@ +from .cSAXS_beamline import epics_get, epics_put, fshclose, fshopen, fshstatus +from .csaxs_bl_checks import cSAXSBeamlineChecks diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/beamline_info.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/beamline_info.py new file mode 100644 index 0000000..6902812 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/beamline_info.py @@ -0,0 +1,107 @@ +import builtins + +from bec_ipython_client.beamline_mixin import BeamlineShowInfo +from rich import box +from rich.table import Table + + +class BeamlineInfo(BeamlineShowInfo): + def show(self): + """Display information about the current beamline status""" + console = self._get_console() + + table = Table(title="X12SA Info", box=box.SQUARE) + table.add_column("Key", justify="left") + table.add_column("Value", justify="left") + + info = self._get_beamline_info_messages() + self._add_op_status(table, info) + self._add_id_gap(table, info) + self._add_storage_ring_vac(table, info) + self._add_shutter_status(table, info) + self._add_mokev(table, info) + self._add_fe_status(table, info) + self._add_es1_valve(table, info) + self._add_xbox1_pressure(table, info) + self._add_xbox2_pressure(table, info) + + console.print(table) + + def _add_op_status(self, table, info): + val = self._get_info_val(info, "x12sa_op_status") + if val not in ["attended"]: + return table.add_row("Beamline operation", val, style=self.ALARM_STYLE) + return table.add_row("Beamline operation", val, style=self.DEFAULT_STYLE) + + def _add_shutter_status(self, table, info): + val = self._get_info_val(info, "x12sa_es1_shutter_status") + if val.lower() not in ["open"]: + return table.add_row("Shutter", val, style=self.ALARM_STYLE) + return table.add_row("Shutter", val, style=self.DEFAULT_STYLE) + + def _add_storage_ring_vac(self, table, info): + val = self._get_info_val(info, "x12sa_storage_ring_vac") + if val.lower() not in ["ok"]: + return table.add_row("Storage ring vacuum", val, style=self.ALARM_STYLE) + return table.add_row("Storage ring vacuum", val, style=self.DEFAULT_STYLE) + + def _add_es1_valve(self, table, info): + val = self._get_info_val(info, "x12sa_es1_valve") + if val.lower() not in ["open"]: + return table.add_row("ES1 valve", val, style=self.ALARM_STYLE) + return table.add_row("ES1 valve", val, style=self.DEFAULT_STYLE) + + def _add_xbox1_pressure(self, table, info): + MAX_PRESSURE = 2e-6 + val = info["x12sa_exposure_box1_pressure"]["value"] + if val > MAX_PRESSURE: + return table.add_row( + f"Exposure box 1 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", + f"{val:.1e} mbar", + style=self.ALARM_STYLE, + ) + return table.add_row("Exposure box 1 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) + + def _add_xbox2_pressure(self, table, info): + MAX_PRESSURE = 2e-6 + val = info["x12sa_exposure_box2_pressure"]["value"] + if val > MAX_PRESSURE: + return table.add_row( + f"Exposure box 2 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", + f"{val:.1e} mbar", + style=self.ALARM_STYLE, + ) + return table.add_row("Exposure box 2 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) + + def _add_fe_status(self, table, info): + val = self._get_info_val(info, "x12sa_fe_status") + return table.add_row("Front end shutter", val, style=self.DEFAULT_STYLE) + + def _add_id_gap(self, table, info): + val = info["x12sa_id_gap"]["value"] + if val > 8: + return table.add_row("ID gap", f"{val:.3f} mm", style=self.ALARM_STYLE) + return table.add_row("ID gap", f"{val:.3f} mm", style=self.DEFAULT_STYLE) + + def _add_mokev(self, table, info): + val = info["x12sa_mokev"]["value"] + return table.add_row("Selected energy (mokev)", f"{val:.3f} keV", style=self.DEFAULT_STYLE) + + def _get_beamline_info_messages(self) -> dict: + dev = builtins.__dict__.get("dev") + + def _get_bl_msg(info, device_name): + info[device_name] = dev[device_name].read(cached=True) + + info = {} + _get_bl_msg(info, "x12sa_op_status") + _get_bl_msg(info, "x12sa_storage_ring_vac") + _get_bl_msg(info, "x12sa_es1_shutter_status") + _get_bl_msg(info, "x12sa_id_gap") + _get_bl_msg(info, "x12sa_mokev") + _get_bl_msg(info, "x12sa_fe_status") + _get_bl_msg(info, "x12sa_es1_valve") + _get_bl_msg(info, "x12sa_exposure_box1_pressure") + _get_bl_msg(info, "x12sa_exposure_box2_pressure") + + return info diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS_beamline.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS_beamline.py new file mode 100644 index 0000000..6863e46 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS_beamline.py @@ -0,0 +1,28 @@ +import epics + + +def epics_put(channel, value): + epics.caput(channel, value) + + +def epics_get(channel): + return epics.caget(channel) + + +def fshon(): + pass + + +def fshopen(): + """open the fast shutter""" + epics_put("X12SA-ES1-TTL:OUT_01", 1) + + +def fshclose(): + """close the fast shutter""" + epics_put("X12SA-ES1-TTL:OUT_01", 0) + + +def fshstatus(): + """show the fast shutter status""" + return epics_get("X12SA-ES1-TTL:OUT_01") diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/csaxs_bl_checks.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/csaxs_bl_checks.py new file mode 100644 index 0000000..9210354 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/csaxs_bl_checks.py @@ -0,0 +1,122 @@ +import builtins +import datetime +import threading +import time + +from bec_lib import bec_logger + +logger = bec_logger.logger + +if builtins.__dict__.get("bec"): + bec = builtins.__dict__.get("bec") + + +class cSAXSBeamlineChecks: + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.check_shutter = True + self.check_light_available = True + self.check_fofb = True + self._check_msgs = [] + self._beam_is_okay = True + self._stop_beam_check_event = None + self.beam_check_thread = None + + def get_beamline_checks_enabled(self): + print( + f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:" + f" {self.check_light_available}" + ) + + @property + def beamline_checks_enabled(self): + return { + "shutter": self.check_shutter, + "fofb": self.check_fofb, + "light available": self.check_light_available, + } + + @beamline_checks_enabled.setter + def beamline_checks_enabled(self, val: bool): + self.check_shutter = val + self.check_light_available = val + self.check_fofb = val + self.get_beamline_checks_enabled() + + def _run_beamline_checks(self): + msgs = [] + dev = builtins.__dict__.get("dev") + try: + if self.check_shutter: + shutter_val = dev.x12sa_es1_shutter_status.read(cached=True) + if shutter_val["value"].lower() != "open": + self._beam_is_okay = False + msgs.append("Check beam failed: Shutter is closed.") + if self.check_light_available: + machine_status = dev.sls_machine_status.read(cached=True) + if machine_status["value"] not in ["Light Available", "Light-Available"]: + self._beam_is_okay = False + msgs.append("Check beam failed: Light not available.") + if self.check_fofb: + fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True) + if fast_orbit_feedback["value"] != "running": + self._beam_is_okay = False + msgs.append("Check beam failed: Fast orbit feedback is not running.") + except Exception: + logger.warning("Failed to check beam.") + return msgs + + def _check_beam(self): + while not self._stop_beam_check_event.is_set(): + self._check_msgs = self._run_beamline_checks() + + if not self._beam_is_okay: + self._stop_beam_check_event.set() + time.sleep(1) + + def _start_beam_check(self): + self._beam_is_okay = True + self._stop_beam_check_event = threading.Event() + + self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True) + self.beam_check_thread.start() + + def _was_beam_okay(self): + self._stop_beam_check_event.set() + self.beam_check_thread.join() + return self._beam_is_okay + + def _print_beamline_checks(self): + for msg in self._check_msgs: + logger.warning(msg) + + def _wait_for_beamline_checks(self): + self._print_beamline_checks() + try: + msg = bec.logbook.LogbookMessage() + msg.add_text( + "

Beamline checks failed at" + f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}

" + ).add_tag(["BEC", "beam_check"]) + bec.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to send update to SciLog.") + + while True: + self._beam_is_okay = True + self._check_msgs = self._run_beamline_checks() + if self._beam_is_okay: + break + self._print_beamline_checks() + time.sleep(1) + + try: + msg = bec.logbook.LogbookMessage() + msg.add_text( + "

Operation resumed at" + f" {str(datetime.datetime.now())}.

" + ).add_tag(["BEC", "beam_check"]) + bec.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to send update to SciLog.") diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/__init__.py b/csaxs_bec/bec_ipython_client/plugins/flomni/__init__.py new file mode 100644 index 0000000..a9b4090 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/__init__.py @@ -0,0 +1 @@ +from .flomni import Flomni diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py new file mode 100644 index 0000000..20d74bb --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -0,0 +1,2044 @@ +import builtins +import datetime +import os +import subprocess +import time +from pathlib import Path + +import numpy as np +from bec_lib import bec_logger +from bec_lib.alarm_handler import AlarmBase +from bec_lib.pdf_writer import PDFWriter +from typeguard import typechecked + +from csaxs_bec.bec_ipython_client.plugins.cSAXS import cSAXSBeamlineChecks +from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import FlomniOpticsMixin +from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign + +logger = bec_logger.logger + +if builtins.__dict__.get("bec") is not None: + bec = builtins.__dict__.get("bec") + dev = builtins.__dict__.get("dev") + umv = builtins.__dict__.get("umv") + umvr = builtins.__dict__.get("umvr") + + +class FlomniInitError(Exception): + pass + + +class FlomniError(Exception): + pass + + +class FlomniInitStagesMixin: + + def flomni_init_stages(self): + + user_input = input("Starting initialization of flOMNI stages. OK? [y/n]") + if user_input == "y": + print("staring...") + else: + return + + if self.check_all_axes_of_fomni_referenced(): + user_input = input("Continue anyways? [y/n]") + if user_input == "y": + print("ok then...") + else: + return + + print("Starting to drive ftransy to +y limit") + self.drive_axis_to_limit(dev.ftransy, "forward") + dev.ftransy.limits = [-100, 0] + print("done") + + print("Starting to drive ftransz to -z limit") + self.drive_axis_to_limit(dev.ftransz, "reverse") + dev.ftransz.limits = [0, 145] + print("done") + + print("Starting to drive ftransx to -x limit") + self.drive_axis_to_limit(dev.ftransx, "reverse") + dev.ftransx.limits = [0, 50] + print("done") + + print("Starting to drive feyey to +y limit") + self.drive_axis_to_limit(dev.feyey, "forward") + dev.feyey.limits = [-1, -10] + print("done") + + print("Starting to drive feyex to +x limit") + self.drive_axis_to_limit(dev.feyex, "forward") + dev.feyex.limits = [-30, -1] + print("done") + + user_input = input( + "Init of foptz. Can the stage move to the upstream limit without collision? [y/n]" + ) + if user_input == "y": + print("good then") + else: + return + + print("Starting to drive foptz to -z limit") + self.drive_axis_to_limit(dev.foptz, "reverse") + dev.foptz.limits = [0, 27] + print("done") + + print("Init of Smaract stages") + ## smaract stages + max_repeat = 100 + repeat = 0 + axis_id_fosaz = dev.fosaz._config["deviceConfig"].get("axis_Id") + axis_id_numeric_fosaz = self.axis_id_to_numeric(axis_id_fosaz) + print("Moving fosaz upstream into the light curtain") + while True: + curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered() + if curtain_is_triggered: + break + if repeat > max_repeat: + raise FlomniInitError("Failed to initialize fosaz within 100 repeats.") + dev.fosaz.controller.move_open_loop_steps( + axis_id_numeric_fosaz, -500, amplitude=4000, frequency=2000 + ) + time.sleep(1) + repeat += 1 + + print("Finding index of fosax, fosay, fosaz") + for ii in range(3): + dev.fosax.controller.find_reference_mark(ii, 0, 1000, 1) + time.sleep(1) + + dev.fosax.limits = [10.2, 10.6] + dev.fosay.limits = [-3.1, -2.9] + dev.fosaz.limits = [-6, -4] + # dev.fosax.controller.describe() + + print("Moving fosa stages to approximate beam positions") + umv(dev.fosaz, -5) + umv(dev.fosax, 10.4, dev.fosay, -3) + print("done") + + print("Moving fheater to +y limit") + self.drive_axis_to_limit(dev.fheater, "reverse") + dev.fheater.limits = [-15, 0] + print("done") + + print("Moving fsamy to -y limit") + self.drive_axis_to_limit(dev.fsamy, "reverse") + dev.fsamy.limits = [2, 3.1] + print("done") + + user_input = input( + "Init of tracking stages. Did you remove the outer laser flight tubes? [y/n]" + ) + if user_input == "y": + print("good then") + else: + print("Stopping.") + return + + print("Moving tracky to -y limit") + self.drive_axis_to_limit(dev.ftracky, "reverse") + dev.ftracky.limits = [2.2, 2.8] + print("done") + + print("Moving ftrackz to -z limit") + self.drive_axis_to_limit(dev.ftrackz, "reverse") + dev.ftrackz.limits = [4.5, 5.5] + print("done") + + user_input = input("Init of sample stage. Is the piezo at about 0 deg? [y/n]") + if user_input == "y": + print("good then") + else: + print("Stopping.") + return + + print("Moving fsamx to +x limit") + self.drive_axis_to_limit(dev.fsamx, "forward") + dev.fsamx.limits = [-162, 0] + print("done") + + print("Moving ftray to IN limit") + self.drive_axis_to_limit(dev.ftray, "reverse") + dev.ftray.limits = [-200, 0] + print("done") + + print("Initializing UPR stage.") + user_input = input( + "To ensure that the end switches work, please check that they are currently not pushed." + " Is everything okay? [y/n]" + ) + if user_input == "y": + print("good then") + else: + print("Stopping.") + return + + while True: + low_limit, high_limit = dev.fsamroy.controller.get_motor_limit_switch("A") + if not high_limit: + print("Please push limit switch to the left.") + time.sleep(1) + continue + break + + while True: + low_limit, high_limit = dev.fsamroy.controller.get_motor_limit_switch("A") + if not low_limit: + print("Please push limit switch to the right.") + time.sleep(1) + continue + break + user_input = input("Shall I start the index search? [y/n]") + if user_input == "y": + print("good then. Starting index search.") + else: + print("Stopping.") + return + if dev.fsamroy.controller.is_motor_on("A"): + raise FlomniInitError("fsamroy should be off. Something is wrong. Mirko... help!") + dev.fsamroy.controller.socket_put_confirmed("XQ#MOTON") + dev.fsamroy.enabled = False + time.sleep(5) + dev.fsamroy.enabled = True + time.sleep(2) + dev.fsamroy.controller.socket_put_confirmed("XQ#REFAX") + while not dev.fsamroy.controller.all_axes_referenced(): + print("Waiting for fsamroy to be referenced.") + time.sleep(1) + dev.fsamroy.limits = [-5, 365] + print("done") + + user_input = input( + "Init of foptx. Can the stage move to the positive limit without collision? Attention:" + " tracker flight tube! [y/n]" + ) + if user_input == "y": + print("good then") + else: + print("Stopping.") + return + + print("Moving foptx to +x limit") + self.drive_axis_to_limit(dev.foptx, "forward") + dev.foptx.limits = [-16, -14] + print("done") + + axis_id_fopty = dev.fopty._config["deviceConfig"].get("axis_Id") + + while True: + low_limit, high_limit = dev.fopty.controller.get_motor_limit_switch(axis_id_fopty) + if not low_limit: + print( + "To ensure that the fopty end switch works, please push it down and hold it for" + " about 1 second." + ) + time.sleep(1) + continue + break + + user_input = input("Start limit switch search of fopty? [y/n]") + if user_input == "y": + print("good then") + else: + print("Stopping.") + return + + print("Moving fopty to -y limit") + self.drive_axis_to_limit(dev.fopty, "reverse") + dev.fopty.limits = [0, 4] + print("done") + + dev.fsamx.controller.galil_show_all() + + self.set_limits() + + self._align_setup() + + def check_all_axes_of_fomni_referenced(self) -> bool: + if ( + dev.fosax.controller.axis_is_referenced(0) + & dev.fosax.controller.axis_is_referenced(1) + & dev.fosax.controller.axis_is_referenced(2) + & dev.fsamx.controller.all_axes_referenced() + & dev.feyex.controller.all_axes_referenced() + & dev.fsamroy.controller.all_axes_referenced() + & dev.fsamroy.controller.is_motor_on("A") + ): + print("All axes of flomni are referenced.") + return True + else: + return False + + def set_limits(self): + user_input = input("Set default limits for flOMNI? [y/n]") + if user_input == "y": + print("setting limits...") + else: + print("Stopping.") + return + dev.ftransy.limits = [-100, 0] + dev.ftransz.limits = [0, 145] + dev.ftransx.limits = [0, 50] + dev.ftray.limits = [-200, 0] + dev.fsamy.limits = [2, 3.5] + dev.foptz.limits = [22.5, 28] + dev.foptx.limits = [-17, -12] + dev.fheater.limits = [-15, 0] + dev.feyex.limits = [-18, -1] + dev.feyey.limits = [-12, -1] + dev.fopty.limits = [0, 4] + dev.fosax.limits = [7, 10] + dev.fosay.limits = [-4.2, 7] + dev.fosaz.limits = [-6.5, 7.5] + # dev.rtx.limits = [-220, 220] + # dev.rty.limits = [-180, 180] + # dev.rtz.limits = [-220, 220] + dev.fsamroy.limits = [-5, 365] + dev.ftracky.limits = [2.2, 2.8] + dev.ftrackz.limits = [4.5, 5.5] + + def _align_setup(self): + user_input = input("Start moving stages to default initial positions? [y/n]") + if user_input == "y": + print("Start moving stages...") + else: + print("Stopping.") + return + # positions for optics out and 50 mm distance to sample + umv(dev.ftrackz, 4.73, dev.ftracky, 2.5170, dev.foptx, -14.3, dev.fopty, 3.87) + + # the fopty 3.87 should put us in place for a lower FZP on the lower FZP chip + + umv(dev.foptz, 23) + + flomni_samx_in = dev.fsamx.user_parameter.get("in") + if flomni_samx_in is None: + raise FlomniInitError( + "Could not find a fsamx in position. Please check your device config." + ) + umv(dev.fsamx, flomni_samx_in) + flomni_samy_in = dev.fsamy.user_parameter.get("in") + if flomni_samy_in is None: + raise FlomniInitError( + "Could not find a fsamy in position. Please check your device config." + ) + umv(dev.fsamy, flomni_samy_in) + + # after init reduce vertical stage speed + dev.fsamy.controller.socket_put_confirmed("axspeed[5]=20000") + + umv(dev.feyey, -8) + + +class FlomniSampleTransferMixin: + def ensure_osa_back(self): + dev.fosaz.limits = [-12.6, -12.4] + umv(dev.fosaz, -12.5) + + curtain_is_triggered = dev.fheater.controller.fosaz_light_curtain_is_triggered() + if not curtain_is_triggered: + raise FlomniError("Fosaz did not reach light curtain") + + def move_fheater_up(self): + self.ensure_fheater_up() + + def ensure_fheater_up(self): + axis_id = dev.fheater._config["deviceConfig"].get("axis_Id") + axis_id_numeric = self.axis_id_to_numeric(axis_id) + low, high = dev.fheater.controller.get_motor_limit_switch(axis_id) + if high: + raise FlomniError("fheater in high limit. How did we get here?? Aborting.") + if not low: + self.ensure_osa_back() + if dev.fheater.readback.get() < -0.2: + umv(dev.fheater, -0.2) + + dev.fheater.controller.drive_axis_to_limit(axis_id_numeric, "reverse") + + def move_fheater_down(self): + axis_id = dev.fheater._config["deviceConfig"].get("axis_Id") + axis_id_numeric = self.axis_id_to_numeric(axis_id) + self.ensure_osa_back() + + fsamx_in = dev.fsamx.user_parameter.get("in") + if not np.isclose(dev.fsamx.readback.get(), fsamx_in, 0.2): + raise FlomniError("fsamx not in position. Aborting.") + + fheater_in = dev.fheater.user_parameter.get("in") + umv(dev.fheater, fheater_in) + + def ensure_gripper_up(self): + axis_id = dev.ftransy._config["deviceConfig"].get("axis_Id") + axis_id_numeric = self.axis_id_to_numeric(axis_id) + low, high = dev.ftransy.controller.get_motor_limit_switch(axis_id) + if low: + raise FlomniError("Ftransy in low limit. How did we get here?? Aborting.") + + if high: + return + + if dev.ftransy.readback.get() < -0.5: + umv(dev.ftransy, -0.5) + dev.ftransy.controller.drive_axis_to_limit(axis_id_numeric, "forward") + + def check_tray_in(self): + axis_id = dev.ftray._config["deviceConfig"].get("axis_Id") + low, high = dev.ftray.controller.get_motor_limit_switch(axis_id) + if high: + raise FlomniError("Ftray is in the 'OUT' position. Aborting.") + + if not low: + raise FlomniError("Ftray is not at the 'IN' position. Aborting.") + + def ftransfer_flomni_stage_in(self): + sample_in_position = bool(float(dev.flomni_samples.sample_placed.sample0.get())) + if not sample_in_position: + raise FlomniError("There is no sample in the sample stage. Aborting.") + self.reset_correction() + dev.rtx.controller.feedback_disable() + self.ensure_fheater_up() + self.ensure_gripper_up() + self.check_tray_in() + + fsamx_in = dev.fsamx.user_parameter.get("in") + umv(dev.fsamx, fsamx_in) + dev.fsamx.limits = [fsamx_in - 0.4, fsamx_in + 0.4] + + def laser_tracker_show_all(self): + dev.rtx.controller.laser_tracker_show_all() + + def laser_tracker_on(self): + dev.rtx.controller.laser_tracker_on() + time.sleep(0.2) + self._laser_tracker_check_signalstrength() + + def laser_tracker_off(self): + dev.rtx.controller.laser_tracker_off() + + def show_signal_strength_interferometer(self): + dev.rtx.controller.show_signal_strength_interferometer() + + def rt_feedback_disable(self): + self.device_manager.devices.rtx.controller.feedback_disable() + + def rt_feedback_enable_with_reset(self): + self.device_manager.devices.rtx.controller.feedback_enable_with_reset() + self.rt_feedback_status() + + def rt_feedback_enable_without_reset(self): + self.device_manager.devices.rtx.controller.feedback_enable_without_reset() + self.rt_feedback_status() + + def rt_feedback_status(self): + feedback_status = self.device_manager.devices.rtx.controller.feedback_is_running() + if feedback_status == True: + print("The rt feedback is \x1b[92mrunning\x1b[0m.") + else: + print("The rt feedback is \x1b[91mNOT\x1b[0m running.") + + def lights_off(self): + self.device_manager.devices.fsamx.controller.lights_off() + + def lights_on(self): + self.device_manager.devices.fsamx.controller.lights_on() + + def ftransfer_flomni_stage_out(self): + target_pos = -162 + if np.isclose(dev.fsamx.readback.get(), target_pos, 0.01): + return + + umv(dev.fsamroy, 0) + + self.rt_feedback_disable() + + self.ensure_fheater_up() + + self.ensure_gripper_up() + + self.check_tray_in() + + self.laser_tracker_off() + time.sleep(0.05) + fsamy_in = dev.fsamy.user_parameter.get("in") + if fsamy_in is None: + raise FlomniError( + "Could not find an 'IN' position for fsamy. Please check your config." + ) + umv(dev.fsamy, fsamy_in) + time.sleep(0.05) + self.laser_tracker_on() + time.sleep(0.05) + self.laser_tracker_off() + time.sleep(0.05) + + self.drive_axis_to_limit(dev.fsamx, "forward") + dev.fsamx.limits = [-162, 0] + dev.fsamx.controller.socket_put_confirmed("axspeed[4]=25*stppermm[4]") + + umv(dev.fsamx, target_pos) + + def check_sensor_connected(self): + sensor_voltage_target = dev.ftransy.user_parameter.get("sensor_voltage") + sensor_voltage = float(dev.ftransy.controller.socket_put_and_receive("MG@AN[1]").strip()) + + if not np.isclose(sensor_voltage, sensor_voltage_target, 0.5): + raise FlomniError(f"Sensor voltage is {sensor_voltage}, indicates an error. Aborting.") + + def ftransfer_get_sample(self, position: int): + self.check_position_is_valid(position) + + self.check_tray_in() + self.check_sensor_connected() + + sample_in_gripper = bool(float(dev.flomni_samples.sample_in_gripper.get())) + if sample_in_gripper: + raise FlomniError( + "The gripper does carry a sample. Cannot proceed getting another sample." + ) + + sample_signal = getattr(dev.flomni_samples.sample_placed, f"sample{position}") + sample_in_position = bool(float(sample_signal.get())) + if not sample_in_position: + raise FlomniError(f"The planned pick position [{position}] does not have a sample.") + + user_input = input( + "Please confirm that there is currently no sample in the gripper. It would be dropped!" + " [y/n]" + ) + if user_input == "y": + print("good then") + else: + print("Stopping.") + raise FlomniError("The sample transfer was manually aborted.") + + self.ftransfer_gripper_move(position) + + self.ftransfer_controller_enable_mount_mode() + if position == 0: + sample_height = -45 + dev.fsamy.user_parameter.get("in") + + else: + sample_height = -17.5 + dev.ftransy.controller.socket_put_confirmed(f"getaprch={sample_height:.1f}") + dev.ftransy.controller.socket_put_confirmed("XQ#GRGET,3") + + print("The unmount process started.") + + time.sleep(1) + while True: + in_progress = bool( + float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip()) + ) + if not in_progress: + break + self.ftransfer_confirm() + time.sleep(1) + self.ftransfer_controller_disable_mount_mode() + self.ensure_gripper_up() + + signal_name = getattr(dev.flomni_samples.sample_names, f"sample{position}") + self.flomni_modify_storage_non_interactive(100, 1, signal_name.get()) + self.flomni_modify_storage_non_interactive(position, 0, "-") + + def ftransfer_show_all(self): + dev.flomni_samples.show_all() + + def ftransfer_put_sample(self, position: int): + self.check_position_is_valid(position) + + self.check_tray_in() + self.check_sensor_connected() + + sample_in_gripper = bool(float(dev.flomni_samples.sample_in_gripper.get())) + if not sample_in_gripper: + raise FlomniError("The gripper does not carry a sample.") + + sample_signal = getattr(dev.flomni_samples.sample_placed, f"sample{position}") + sample_in_position = bool(float(sample_signal.get())) + if sample_in_position: + raise FlomniError(f"The planned put position [{position}] already has a sample.") + + self.ftransfer_gripper_move(position) + + self.ftransfer_controller_enable_mount_mode() + if position == 0: + sample_height = -45 + dev.fsamy.user_parameter.get("in") + + else: + sample_height = -17.5 + dev.ftransy.controller.socket_put_confirmed(f"mntaprch={sample_height:.1f}") + dev.ftransy.controller.socket_put_confirmed("XQ#GRPUT,3") + + print("The mount process started.") + + time.sleep(1) + while True: + in_progress = bool( + float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip()) + ) + if not in_progress: + break + self.ftransfer_confirm() + time.sleep(1) + self.ftransfer_controller_disable_mount_mode() + self.ensure_gripper_up() + + sample_name = dev.flomni_samples.sample_in_gripper.get() + self.flomni_modify_storage_non_interactive(100, 0, "-") + self.flomni_modify_storage_non_interactive(position, 1, sample_name) + + # TODO: flomni_stage_in if position == 0 + # bec.queue.next_dataset_number += 1 + + def sample_get_name(self, position: int = 0) -> str: + """ + Get the name of the sample currently in the given position. + """ + signal_name = getattr(dev.flomni_samples.sample_names, f"sample{position}") + return signal_name.get() + + def ftransfer_sample_change(self, new_sample_position: int): + self.check_tray_in() + sample_in_gripper = dev.flomni_samples.sample_in_gripper.get() + if sample_in_gripper: + raise FlomniError("There is already a sample in the gripper. Aborting.") + + self.check_position_is_valid(new_sample_position) + + sample_placed = getattr( + dev.flomni_samples.sample_placed, f"sample{new_sample_position}" + ).get() + if not sample_placed: + raise FlomniError( + f"There is currently no sample in position [{new_sample_position}]. Aborting." + ) + + sample_in_sample_stage = dev.flomni_samples.sample_placed.sample0.get() + if sample_in_sample_stage: + # find a new home for the sample... + empty_slots = [] + for name, val in dev.flomni_samples.read().items(): + if "flomni_samples_sample_placed_sample" not in name: + continue + if val.get("value") == 0: + empty_slots.append(int(name.split("flomni_samples_sample_placed_sample")[1])) + if not empty_slots: + raise FlomniError("There are no empty slots available. Aborting.") + + print(f"The following slots are empty: {empty_slots}.") + + while True: + user_input = input(f"Where shall I put the sample? Default: [{empty_slots[0]}]") + try: + user_input = int(user_input) + if user_input not in empty_slots: + raise ValueError + break + except ValueError: + print("Please specify a valid number.") + continue + + self.check_position_is_valid(user_input) + + self.ftransfer_get_sample(0) + self.ftransfer_put_sample(user_input) + + self.ftransfer_get_sample(new_sample_position) + self.ftransfer_put_sample(0) + + def ftransfer_modify_storage(self, position: int, used: int): + if used: + name = input("What's the name of this sample? ") + else: + name = "-" + self.flomni_modify_storage_non_interactive(position, used, name) + + def flomni_modify_storage_non_interactive(self, position: int, used: int, name: str): + if position == 100: + dev.flomni_samples.sample_in_gripper.set(used) + dev.flomni_samples.sample_in_gripper_name.set(name) + else: + signal = getattr(dev.flomni_samples.sample_placed, f"sample{position}") + signal.set(used) + signal_name = getattr(dev.flomni_samples.sample_names, f"sample{position}") + signal_name.set(name) + + def check_position_is_valid(self, position: int): + if 0 <= position < 21: + return + raise FlomniError( + f"The given position number [{position}] is not in the valid range of 0-21. " + ) + + def ftransfer_controller_enable_mount_mode(self): + dev.ftransy.controller.socket_put_confirmed("XQ#MNTMODE") + time.sleep(0.5) + if not self.ftransfer_controller_in_mount_mode(): + raise FlomniError("System not switched to mount mode. Aborting.") + + def ftransfer_controller_disable_mount_mode(self): + dev.ftransy.controller.socket_put_confirmed("XQ#POSMODE") + time.sleep(0.5) + if self.ftransfer_controller_in_mount_mode(): + raise FlomniError("System is still in mount mode. Aborting.") + + def ftransfer_controller_in_mount_mode(self) -> bool: + in_mount_mode = bool( + float(dev.ftransy.controller.socket_put_and_receive("MG mntmod").strip()) + ) + return in_mount_mode + + def ftransfer_confirm(self): + confirm = int(float(dev.ftransy.controller.socket_put_and_receive("MG confirm").strip())) + + if confirm != -1: + return + + user_input = input("All OK? Continue? [y/n]") + if user_input == "y": + print("good then") + dev.ftransy.controller.socket_put_confirmed("confirm=1") + else: + print("Stopping.") + return + + def ftransfer_gripper_is_open(self) -> bool: + status = bool(float(dev.ftransy.controller.socket_put_and_receive("MG @OUT[9]").strip())) + return status + + def ftransfer_gripper_open(self): + sample_in_gripper = dev.flomni_samples.sample_in_gripper.get() + if sample_in_gripper: + raise FlomniError( + "Cannot open gripper. There is still a sample in the gripper! Aborting." + ) + if not self.ftransfer_gripper_is_open(): + dev.ftransy.controller.socket_put_confirmed("XQ#GROPEN,4") + + def ftransfer_gripper_close(self): + if self.ftransfer_gripper_is_open(): + dev.ftransy.controller.socket_put_confirmed("XQ#GRCLOS,4") + + def ftransfer_gripper_move(self, position: int): + self.check_position_is_valid(position) + + self._ftransfer_shiftx = -0.2 + self._ftransfer_shiftz = -0.5 + + fsamx_pos = dev.fsamx.readback.get() + if position == 0 and fsamx_pos > -160: + user_input = input( + "May the flomni stage be moved out for the sample change? Feedback will be disabled" + " and alignment will be lost! [y/n]" + ) + if user_input == "y": + print("good then") + self.ftransfer_flomni_stage_out() + else: + print("Stopping.") + return + + self.ensure_gripper_up() + self.check_tray_in() + + if position == 0: + umv(dev.ftransx, 10.715 + 0.2, dev.ftransz, 3.5950) + if position == 1: + umv( + dev.ftransx, + 41.900 + self._ftransfer_shiftx, + dev.ftransz, + 74.7500 + self._ftransfer_shiftz, + ) + if position == 2: + umv( + dev.ftransx, + 31.900 + self._ftransfer_shiftx, + dev.ftransz, + 74.7625 + self._ftransfer_shiftz, + ) + if position == 3: + umv( + dev.ftransx, + 21.900 + self._ftransfer_shiftx, + dev.ftransz, + 74.7750 + self._ftransfer_shiftz, + ) + if position == 4: + umv( + dev.ftransx, + 11.900 + self._ftransfer_shiftx, + dev.ftransz, + 74.7875 + self._ftransfer_shiftz, + ) + if position == 5: + umv( + dev.ftransx, + 1.9000 + self._ftransfer_shiftx, + dev.ftransz, + 74.8000 + self._ftransfer_shiftz, + ) + if position == 6: + umv( + dev.ftransx, + 41.900 + self._ftransfer_shiftx, + dev.ftransz, + 89.7500 + self._ftransfer_shiftz, + ) + if position == 7: + umv( + dev.ftransx, + 31.900 + self._ftransfer_shiftx, + dev.ftransz, + 89.7625 + self._ftransfer_shiftz, + ) + if position == 8: + umv( + dev.ftransx, + 21.900 + self._ftransfer_shiftx, + dev.ftransz, + 89.7750 + self._ftransfer_shiftz, + ) + if position == 9: + umv( + dev.ftransx, + 11.900 + self._ftransfer_shiftx, + dev.ftransz, + 89.7875 + self._ftransfer_shiftz, + ) + if position == 10: + umv( + dev.ftransx, + 1.900 + self._ftransfer_shiftx, + dev.ftransz, + 89.8000 + self._ftransfer_shiftz, + ) + if position == 11: + umv( + dev.ftransx, + 41.95 + self._ftransfer_shiftx, + dev.ftransz, + 124.75 + self._ftransfer_shiftz, + ) + if position == 12: + umv( + dev.ftransx, + 31.95 + self._ftransfer_shiftx, + dev.ftransz, + 124.7625 + self._ftransfer_shiftz, + ) + if position == 13: + umv( + dev.ftransx, + 21.95 + self._ftransfer_shiftx, + dev.ftransz, + 124.7750 + self._ftransfer_shiftz, + ) + if position == 14: + umv( + dev.ftransx, + 11.95 + self._ftransfer_shiftx, + dev.ftransz, + 124.7875 + self._ftransfer_shiftz, + ) + if position == 15: + umv( + dev.ftransx, + 1.95 + self._ftransfer_shiftx, + dev.ftransz, + 124.8000 + self._ftransfer_shiftz, + ) + if position == 16: + umv( + dev.ftransx, + 41.95 + self._ftransfer_shiftx, + dev.ftransz, + 139.7500 + self._ftransfer_shiftz, + ) + if position == 17: + umv( + dev.ftransx, + 31.95 + self._ftransfer_shiftx, + dev.ftransz, + 139.7625 + self._ftransfer_shiftz, + ) + if position == 18: + umv( + dev.ftransx, + 21.95 + self._ftransfer_shiftx, + dev.ftransz, + 139.7750 + self._ftransfer_shiftz, + ) + if position == 19: + umv( + dev.ftransx, + 11.95 + self._ftransfer_shiftx, + dev.ftransz, + 139.7875 + self._ftransfer_shiftz, + ) + if position == 20: + umv( + dev.ftransx, + 1.95 + self._ftransfer_shiftx, + dev.ftransz, + 139.8000 + self._ftransfer_shiftz, + ) + + +class FlomniAlignmentMixin: + default_correction_file = "correction_flomni_20210300_360deg.txt" + + def reset_correction(self, use_default_correction=True): + """ + Reset the correction to the default values. + If use_default_correction is False, the correction will be set to empty values. + Otherwise the default values will be loaded. + + Args: + use_default_correction (bool, optional): If set to true, a call reset the correction to the default values. Defaults to True. + """ + self.corr_pos_y = [] + self.corr_angle_y = [] + self.corr_pos_y_2 = [] + self.corr_angle_y_2 = [] + + if use_default_correction: + try: + self.read_additional_correction_y(self.default_correction_file) + logger.info(f"Applying default correction from {self.default_correction_file}") + except FileNotFoundError: + logger.warning( + f"Could not find default correction file {self.default_correction_file}." + ) + logger.warning("Not applying any correction.") + + def reset_tomo_alignment_fit(self): + self.client.delete_global_var("tomo_alignment_fit") + + def read_alignment_offset( + self, + dir_path=os.path.expanduser("~/Data10/specES1/internal/"), + setup="flomni", + use_vertical_default_values=True, + ): + """ + Read the alignment offset from the given directory and set the global parameter + tomo_alignment_fit. + + Args: + dir_path (str, optional): The directory to read the alignment offset from. Defaults to os.path.expanduser("~/Data10/specES1/internal/"). + """ + tomo_alignment_fit = np.zeros((2, 5)) + with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file: + tomo_alignment_fit[0][0] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file: + tomo_alignment_fit[0][1] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file: + tomo_alignment_fit[0][2] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file: + tomo_alignment_fit[1][0] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file: + tomo_alignment_fit[1][1] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file: + tomo_alignment_fit[1][2] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file: + tomo_alignment_fit[1][3] = file.readline() + + with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file: + tomo_alignment_fit[1][4] = file.readline() + + print("New alignment parameters loaded:") + print( + f"X Amplitude {tomo_alignment_fit[0][0]}, " + f"X Phase {tomo_alignment_fit[0][1]}, " + f"X Offset {tomo_alignment_fit[0][2]}, " + f"Y Amplitude {tomo_alignment_fit[1][0]}, " + f"Y Phase {tomo_alignment_fit[1][1]}, " + f"Y Offset {tomo_alignment_fit[1][2]}, " + f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, " + f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ." + ) + + if use_vertical_default_values: + print( + f"Using default values for vertical alignment for setup {setup}. Optional: use_vertical_default_values=False" + ) + if setup == "flomni": + tomo_alignment_fit[1][0] = 0 + tomo_alignment_fit[1][1] = 0 + tomo_alignment_fit[1][2] = 0 + tomo_alignment_fit[1][3] = 0 + tomo_alignment_fit[1][4] = 0 + elif setup == "omny": + tomo_alignment_fit[1][0] = 2.588628 + tomo_alignment_fit[1][1] = -2.385422 + tomo_alignment_fit[1][2] = 0 + tomo_alignment_fit[1][3] = 1.010583 + tomo_alignment_fit[1][4] = -1.359157 + + print("Follwing parameters will be used:") + print( + f"X Amplitude {tomo_alignment_fit[0][0]}, " + f"X Phase {tomo_alignment_fit[0][1]}, " + f"X Offset {tomo_alignment_fit[0][2]}, " + f"Y Amplitude {tomo_alignment_fit[1][0]}, " + f"Y Phase {tomo_alignment_fit[1][1]}, " + f"Y Offset {tomo_alignment_fit[1][2]}, " + f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, " + f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ." + ) + + self.client.set_global_var("tomo_alignment_fit", tomo_alignment_fit.tolist()) + # x amp, phase, offset, y amp, phase, offset, 3rd order amp, 3rd order phase + # 0 0 0 1 0 2 1 0 1 1 1 2 1 3 1 4 + + def get_alignment_offset(self, angle: float): + """ + Compute the alignment offset for the given angle. + + Args: + angle (float): The angle to compute the alignment offset for. + + Returns: + tuple: The alignment offset in x, y and z direction. + """ + tomo_alignment_fit = self.client.get_global_var("tomo_alignment_fit") + if tomo_alignment_fit is None: + print("Not applying any alignment offsets. No tomo alignment fit data available.\n") + return (0, 0, 0) + + # x amp, phase, offset, y amp, phase, offset + # 0 0 0 1 0 2 1 0 1 1 1 2 + correction_x = ( + tomo_alignment_fit[0][0] * np.sin(np.radians(angle) + tomo_alignment_fit[0][1]) + + tomo_alignment_fit[0][2] + ) + correction_y = ( + tomo_alignment_fit[1][0] * np.sin(np.radians(angle) + tomo_alignment_fit[1][1]) + + tomo_alignment_fit[1][2] + + tomo_alignment_fit[1][3] * np.sin(3 * np.radians(angle) + tomo_alignment_fit[1][4]) + ) + correction_z = tomo_alignment_fit[0][0] * np.sin( + np.radians(angle + 90) + tomo_alignment_fit[0][1] + ) + + print( + f"Alignment offset x {correction_x}, y {correction_y}, z {correction_z} for angle" + f" {angle}\n" + ) + return (correction_x, correction_y, correction_z) + + def _read_correction_file(self, correction_file: str): + with open(correction_file, "r") as f: + num_elements = f.readline() + int_num_elements = int(num_elements.split(" ")[2]) + corr_pos = [] + corr_angle = [] + for j in range(int_num_elements * 2): + line = f.readline() + value = line.split(" ")[2] + name = line.split(" ")[0].split("[")[0] + if name == "corr_pos": + corr_pos.append(float(value) / 1000) + elif name == "corr_angle": + corr_angle.append(float(value)) + print( + f"Loading default mirror correction from file {correction_file} containing {int_num_elements} elements." + ) + return corr_pos, corr_angle + + def read_additional_correction_y(self, correction_file: str): + self.corr_pos_y, self.corr_angle_y = self._read_correction_file(correction_file) + + def read_additional_correction_y_2(self, correction_file: str): + self.corr_pos_y_2, self.corr_angle_y_2 = self._read_correction_file(correction_file) + + def compute_additional_correction_y(self, angle): + return self._compute_additional_correction(angle, iteration=1) + + def compute_additional_correction_y_2(self, angle): + return self._compute_additional_correction(angle, iteration=2) + + def _compute_additional_correction(self, angle, iteration=1): + if iteration == 1: + corr_pos = self.corr_pos_y + corr_angle = self.corr_angle_y + elif iteration == 2: + corr_pos = self.corr_pos_y_2 + corr_angle = self.corr_angle_y_2 + if not corr_pos: + print("Not applying any additional correction. No data available.\n") + return 0 + + # find index of closest angle + for j, _ in enumerate(corr_pos): + newangledelta = np.fabs(corr_angle[j] - angle) + if j == 0: + angledelta = newangledelta + additional_correction_shift = corr_pos[j] + continue + + if newangledelta < angledelta: + additional_correction_shift = corr_pos[j] + angledelta = newangledelta + + if additional_correction_shift == 0 and angle > corr_angle[-1]: + additional_correction_shift = corr_pos[-1] + print(f"Additional correction shift {iteration} in y: {additional_correction_shift}") + return additional_correction_shift + + +class Flomni( + FlomniInitStagesMixin, + FlomniSampleTransferMixin, + FlomniAlignmentMixin, + FlomniOpticsMixin, + cSAXSBeamlineChecks, +): + def __init__(self, client): + super().__init__() + self.client = client + self.device_manager = client.device_manager + self.check_shutter = False + self.check_light_available = False + self.check_fofb = False + self._check_msgs = [] + self.tomo_id = -1 + self.special_angles = [] + self.special_angle_repeats = 20 + self.special_angle_tolerance = 20 + self._current_special_angles = [] + self._beam_is_okay = True + self._stop_beam_check_event = None + self.beam_check_thread = None + self.corr_pos_y = [] + self.corr_angle_y = [] + self.corr_pos_y_2 = [] + self.corr_angle_y_2 = [] + self.progress = {} + self.align = XrayEyeAlign(self.client, self) + + def start_x_ray_eye_alignment(self): + user_input = input( + "Starting Xrayeye alignment. Deleting any potential existing alignment for this sample. [Y/n]" + ) + if user_input == "y" or user_input == "": + self.align = XrayEyeAlign(self.client, self) + try: + self.align.align() + except KeyboardInterrupt as exc: + fsamx_in = self._get_user_param_safe(dev.fsamx, "in") + if np.isclose(fsamx_in, dev.fsamx.readback.get(), 0.5): + print("Stopping alignment. Returning to fsamx in position.") + self.rt_feedback_disable() + umv(dev.fsamx, fsamx_in) + raise exc + + def xrayeye_update_frame(self): + self.align.update_frame() + + def xrayeye_alignment_start(self): + self.start_x_ray_eye_alignment() + + def drive_axis_to_limit(self, device, direction): + axis_id = device._config["deviceConfig"].get("axis_Id") + axis_id_numeric = self.axis_id_to_numeric(axis_id) + device.controller.drive_axis_to_limit(axis_id_numeric, direction) + + def axis_id_to_numeric(self, axis_id) -> int: + return ord(axis_id.lower()) - 97 + + def get_beamline_checks_enabled(self): + print( + f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:" + f" {self.check_light_available}" + ) + + @property + def beamline_checks_enabled(self): + return { + "shutter": self.check_shutter, + "fofb": self.check_fofb, + "light available": self.check_light_available, + } + + @beamline_checks_enabled.setter + def beamline_checks_enabled(self, val: bool): + self.check_shutter = val + self.check_light_available = val + self.check_fofb = val + self.get_beamline_checks_enabled() + + def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5): + """Set the special angles for a tomo + + Args: + angles (list): List of special angles + repeats (int, optional): Number of repeats at a special angle. Defaults to 20. + tolerance (float, optional): Number of repeats at a special angle. Defaults to 0.5. + + """ + self.special_angles = angles + self.special_angle_repeats = repeats + self.special_angle_tolerance = tolerance + + def remove_special_angles(self): + """Remove the special angles and set the number of repeats to 1""" + self.special_angles = [] + self.special_angle_repeats = 1 + + @property + def tomo_shellstep(self): + val = self.client.get_global_var("tomo_shellstep") + if val is None: + return 1 + return val + + @tomo_shellstep.setter + def tomo_shellstep(self, val: float): + self.client.set_global_var("tomo_shellstep", val) + + @property + def tomo_countingtime(self): + val = self.client.get_global_var("tomo_countingtime") + if val is None: + return 0.1 + return val + + @tomo_countingtime.setter + def tomo_countingtime(self, val: float): + self.client.set_global_var("tomo_countingtime", val) + + @property + def manual_shift_y(self): + val = self.client.get_global_var("manual_shift_y") + if val is None: + return 0.0 + return val + + @manual_shift_y.setter + def manual_shift_y(self, val: float): + self.client.set_global_var("manual_shift_y", val) + + @property + def fovx(self): + val = self.client.get_global_var("fovx") + if val is None: + return 20 + return val + + @fovx.setter + def fovx(self, val: float): + if val > 200: + raise ValueError("FOV cannot be larger than 200 um.") + self.client.set_global_var("fovx", val) + + @property + def fovy(self): + val = self.client.get_global_var("fovy") + if val is None: + return 20 + return val + + @fovy.setter + def fovy(self, val: float): + if val > 100: + raise ValueError("FOV cannot be larger than 100 um.") + self.client.set_global_var("fovy", val) + + @property + def tomo_type(self): + val = self.client.get_global_var("tomo_type") + if val is None: + return 1 + return val + + @tomo_type.setter + def tomo_type(self, val: float): + if val == 1: + # equally spaced tomography with 8 sub tomograms + self.client.set_global_var("tomo_type", val) + elif val == 2: + # golden ratio tomography (sorted bunches) + self.client.set_global_var("tomo_type", val) + elif val == 3: + # equally spaced tomography with starting angles shifted by golden ratio + self.client.set_global_var("tomo_type", val) + else: + raise ValueError("Unknown tomo_type.") + + @property + def corridor_size(self): + val = self.client.get_global_var("corridor_size") + if val is None: + val = -1 + return val + + @corridor_size.setter + def corridor_size(self, val: float): + self.client.set_global_var("corridor_size", val) + + @property + def stitch_x(self): + val = self.client.get_global_var("stitch_x") + if val is None: + return 0 + return val + + @stitch_x.setter + @typechecked + def stitch_x(self, val: int): + self.client.set_global_var("stitch_x", val) + + @property + def stitch_y(self): + val = self.client.get_global_var("stitch_y") + if val is None: + return 0 + return val + + @stitch_y.setter + @typechecked + def stitch_y(self, val: int): + self.client.set_global_var("stitch_y", val) + + @property + def ptycho_reconstruct_foldername(self): + val = self.client.get_global_var("ptycho_reconstruct_foldername") + if val is None: + return "ptycho_reconstruct" + return val + + @ptycho_reconstruct_foldername.setter + def ptycho_reconstruct_foldername(self, val: str): + self.client.set_global_var("ptycho_reconstruct_foldername", val) + + @property + def tomo_angle_stepsize(self): + val = self.client.get_global_var("tomo_angle_stepsize") + if val is None: + return 10.0 + return val + + @tomo_angle_stepsize.setter + def tomo_angle_stepsize(self, val: float): + self.client.set_global_var("tomo_angle_stepsize", val) + + @property + def golden_max_number_of_projections(self): + val = self.client.get_global_var("golden_max_number_of_projections") + if val is None: + return 1000.0 + return val + + @golden_max_number_of_projections.setter + def golden_max_number_of_projections(self, val: float): + self.client.set_global_var("golden_max_number_of_projections", val) + + @property + def tomo_stitch_overlap(self): + val = self.client.get_global_var("tomo_stitch_overlap") + if val is None: + return 0.2 + return val + + @tomo_stitch_overlap.setter + def tomo_stitch_overlap(self, val: float): + self.client.set_global_var("tomo_stitch_overlap", val) + + @property + def golden_projections_at_0_deg_for_damage_estimation(self): + val = self.client.get_global_var("golden_projections_at_0_deg_for_damage_estimation") + if val is None: + return 0 + return val + + @golden_projections_at_0_deg_for_damage_estimation.setter + def golden_projections_at_0_deg_for_damage_estimation(self, val: float): + self.client.set_global_var("golden_projections_at_0_deg_for_damage_estimation", val) + + @property + def golden_ratio_bunch_size(self): + val = self.client.get_global_var("golden_ratio_bunch_size") + if val is None: + return 20 + return val + + @golden_ratio_bunch_size.setter + def golden_ratio_bunch_size(self, val: float): + self.client.set_global_var("golden_ratio_bunch_size", val) + + @property + def sample_name(self): + return self.sample_get_name(0) + + def write_to_spec_log(self, content): + try: + with open( + os.path.expanduser( + "~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log" + ), + "a", + ) as log_file: + log_file.write(content) + except Exception: + logger.warning("Failed to write to spec log file (omny web page).") + + def write_to_scilog(self, content, tags: list = None): + try: + if tags is not None: + tags.append("BEC") + else: + tags = ["BEC"] + msg = bec.logbook.LogbookMessage() + msg.add_text(content).add_tag(tags) + self.client.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to write to scilog.") + + def tomo_alignment_scan(self): + """ + Performs a tomogram alignment scan. + """ + if self.get_alignment_offset(0) == (0, 0, 0): + print("It appears that the xrayeye alignemtn was not performend or loaded. Aborting.") + return + dev = builtins.__dict__.get("dev") + bec = builtins.__dict__.get("bec") + tags = ["BEC_alignment_tomo", self.sample_name] + self.write_to_scilog( + f"Starting alignment scan. First scan number: {bec.queue.next_scan_number}.", tags + ) + + start_angle = 0 + + angle_end = start_angle + 180 + for angle in np.linspace(start_angle, angle_end, num=int(180 / 45) + 1, endpoint=True): + successful = False + error_caught = False + if 0 <= angle < 180.05: + print(f"Starting flOMNI scan for angle {angle}") + while not successful: + self._start_beam_check() + try: + start_scan_number = bec.queue.next_scan_number + self.tomo_scan_projection(angle) + self.tomo_reconstruct() + error_caught = False + except AlarmBase as exc: + if exc.alarm_type == "TimeoutError": + bec.queue.request_queue_reset() + time.sleep(2) + error_caught = True + else: + raise exc + + if self._was_beam_okay() and not error_caught: + successful = True + else: + self._wait_for_beamline_checks() + end_scan_number = bec.queue.next_scan_number + for scan_nr in range(start_scan_number, end_scan_number): + self._write_tomo_scan_number(scan_nr, angle, 0) + + print("Alignment scan finished. Please run SPEC_ptycho_align and load the new fit.") + + umv(dev.fsamroy, 0) + + def _write_subtomo_to_scilog(self, subtomo_number): + dev = builtins.__dict__.get("dev") + bec = builtins.__dict__.get("bec") + if self.tomo_id > 0: + tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] + else: + tags = ["BEC_subtomo", self.sample_name] + self.write_to_scilog( + f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", + tags, + ) + + def sub_tomo_scan(self, subtomo_number, start_angle=None): + """ + Performs a sub tomogram scan. + Args: + subtomo_number (int): The sub tomogram number. + start_angle (float, optional): The start angle of the scan. Defaults to None. + """ + # dev = builtins.__dict__.get("dev") + # bec = builtins.__dict__.get("bec") + # if self.tomo_id > 0: + # tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] + # else: + # tags = ["BEC_subtomo", self.sample_name] + # self.write_to_scilog( + # f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", + # tags, + # ) + + self._write_subtomo_to_scilog(subtomo_number) + + if start_angle is None: + if subtomo_number == 1: + start_angle = 0 + elif subtomo_number == 2: + start_angle = self.tomo_angle_stepsize / 8.0 * 4 + elif subtomo_number == 3: + start_angle = self.tomo_angle_stepsize / 8.0 * 2 + elif subtomo_number == 4: + start_angle = self.tomo_angle_stepsize / 8.0 * 6 + elif subtomo_number == 5: + start_angle = self.tomo_angle_stepsize / 8.0 * 1 + elif subtomo_number == 6: + start_angle = self.tomo_angle_stepsize / 8.0 * 5 + elif subtomo_number == 7: + start_angle = self.tomo_angle_stepsize / 8.0 * 3 + elif subtomo_number == 8: + start_angle = self.tomo_angle_stepsize / 8.0 * 7 + + # _tomo_shift_angles (potential global variable) + _tomo_shift_angles = 0 + angle_end = start_angle + 180 + angles = np.linspace( + start_angle + _tomo_shift_angles, + angle_end, + num=int(180 / self.tomo_angle_stepsize) + 1, + endpoint=True, + ) + # reverse even sub-tomograms + if not (subtomo_number % 2): + angles = np.flip(angles) + for angle in angles: + self.progress["subtomo"] = subtomo_number + self.progress["subtomo_projection"] = angles.index(angle) + self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize + self.progress["projection"] = (subtomo_number - 1) * self.progress[ + "subtomo_total_projections" + ] + self.progress["subtomo_projection"] + self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8 + self.progress["angle"] = angle + self._tomo_scan_at_angle(angle, subtomo_number) + + def _tomo_scan_at_angle(self, angle, subtomo_number): + successful = False + error_caught = False + if 0 <= angle < 180.05: + print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}") + self._print_progress() + while not successful: + self._start_beam_check() + if not self.special_angles: + self._current_special_angles = [] + if self._current_special_angles: + next_special_angle = self._current_special_angles[0] + if np.isclose(angle, next_special_angle, atol=0.5): + self._current_special_angles.pop(0) + num_repeats = self.special_angle_repeats + else: + num_repeats = 1 + try: + start_scan_number = bec.queue.next_scan_number + for i in range(num_repeats): + self._at_each_angle(angle) + error_caught = False + except AlarmBase as exc: + if exc.alarm_type == "TimeoutError": + bec.queue.request_queue_reset() + time.sleep(2) + error_caught = True + else: + raise exc + + if self._was_beam_okay() and not error_caught: + successful = True + else: + self._wait_for_beamline_checks() + end_scan_number = bec.queue.next_scan_number + for scan_nr in range(start_scan_number, end_scan_number): + self._write_tomo_scan_number(scan_nr, angle, subtomo_number) + + def tomo_scan(self, subtomo_start=1, start_angle=None, projection_number=None): + """start a tomo scan""" + bec = builtins.__dict__.get("bec") + scans = builtins.__dict__.get("scans") + self._current_special_angles = self.special_angles.copy() + # a new tomo scan was started + if ( + (self.tomo_type == 1 and subtomo_start == 1 and start_angle is None) + or (self.tomo_type == 2 and projection_number == None) + or (self.tomo_type == 3 and projection_number == None) + ): + + # pylint: disable=undefined-variable + if bec.active_account != "": + self.tomo_id = self.add_sample_database( + self.sample_name, + str(datetime.date.today()), + bec.active_account.decode(), + bec.queue.next_scan_number, + "flomni", + "test additional info", + "BEC", + ) + self.write_pdf_report() + else: + self.tomo_id = 0 + + with scans.dataset_id_on_hold: + if self.tomo_type == 1: + # 8 equally spaced sub-tomograms + self.progress["tomo_type"] = "Equally spaced sub-tomograms" + for ii in range(subtomo_start, 9): + self.sub_tomo_scan(ii, start_angle=start_angle) + start_angle = None + + elif self.tomo_type == 2: + # Golden ratio tomography + previous_subtomo_number = -1 + if projection_number == None: + ii = 0 + else: + ii = projection_number + while True: + angle, subtomo_number = self._golden(ii, self.golden_ratio_bunch_size, 180, 1) + if previous_subtomo_number != subtomo_number: + self._write_subtomo_to_scilog(subtomo_number) + if ( + subtomo_number % 2 == 1 + and ii > 10 + and self.golden_projections_at_0_deg_for_damage_estimation == 1 + ): + self._tomo_scan_at_angle(0, subtomo_number) + previous_subtomo_number = subtomo_number + self.progress["tomo_type"] = "Golden ratio tomography" + self.progress["subtomo"] = subtomo_number + self.progress["projection"] = ii + self.progress["angle"] = angle + if self.golden_ratio_bunch_size > 0: + self.progress["subtomo_total_projections"] = self.golden_ratio_bunch_size + self.progress["subtomo_projection"] = ( + ii - (subtomo_number - 1) * self.golden_ratio_bunch_size + ) + else: + self.progress["subtomo_total_projections"] = 0 + self.progress["subtomo_projection"] = 0 + + if self.golden_max_number_of_projections > 0: + self.progress["total_projections"] = self.golden_max_number_of_projections + else: + self.progress["total_projections"] = 0 + + self._tomo_scan_at_angle(angle, subtomo_number) + ii += 1 + if ( + ii > self.golden_max_number_of_projections + and self.golden_max_number_of_projections > 0 + ): + print( + f"Golden ratio tomography stopped automatically after the requested {self.golden_max_number_of_projections} projections" + ) + break + elif self.tomo_type == 3: + # Equally spaced tomography, golden ratio starting angle + previous_subtomo_number = -1 + if projection_number == None: + ii = 0 + else: + ii = projection_number + while True: + angle, subtomo_number = self._golden_equally_spaced( + ii, int(180 / self.tomo_angle_stepsize), 180, 1, 0 + ) + if previous_subtomo_number != subtomo_number: + self._write_subtomo_to_scilog(subtomo_number) + if ( + subtomo_number % 2 == 1 + and ii > 10 + and self.golden_projections_at_0_deg_for_damage_estimation == 1 + ): + self._tomo_scan_at_angle(0, subtomo_number) + previous_subtomo_number = subtomo_number + self.progress["tomo_type"] = ( + "Equally spaced tomography, golden ratio starting angle" + ) + self.progress["subtomo"] = subtomo_number + self.progress["projection"] = ii + self.progress["angle"] = angle + + self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize + self.progress["subtomo_projection"] = ( + ii - (subtomo_number - 1) * self.progress["subtomo_total_projections"] + ) + + if self.golden_max_number_of_projections > 0: + self.progress["total_projections"] = self.golden_max_number_of_projections + else: + self.progress["total_projections"] = 0 + self._tomo_scan_at_angle(angle, subtomo_number) + ii += 1 + if ( + ii > self.golden_max_number_of_projections + and self.golden_max_number_of_projections > 0 + ): + print( + f"Golden ratio tomography stopped automatically after the requested {self.golden_max_number_of_projections} projections" + ) + break + else: + raise FlomniError("undefined tomo type") + + def _print_progress(self): + print("\x1b[95mProgress report:") + print(f"Tomo type: ....................... {self.progress['tomo_type']}") + print(f"Projection: ...................... {self.progress['projection']}") + print(f"Total projections expected ....... {self.progress['total_projections']}") + print(f"Angle: ........................... {self.progress['angle']}") + print(f"Current subtomo: ................. {self.progress['subtomo']}") + print(f"Current projection within subtomo: {self.progress['subtomo_projection']}\x1b[0m") + + def add_sample_database( + self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user + ): + """Add a sample to the omny sample database. This also retrieves the tomo id.""" + subprocess.run( + f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'", + shell=True, + ) + with open("/tmp/currsamplesnr.txt") as tomo_number_file: + tomo_number = int(tomo_number_file.read()) + return tomo_number + + def _at_each_angle(self, angle: float) -> None: + if "flomni_at_each_angle" in builtins.__dict__: + # pylint: disable=undefined-variable + flomni_at_each_angle(self, angle) + return + + self.tomo_scan_projection(angle) + self.tomo_reconstruct() + + def _golden(self, ii, howmany_sorted, maxangle, reverse=False): + """returns the iis golden ratio angle of sorted bunches of howmany_sorted and its subtomo number""" + golden = [] + # occupy array with the range of golden angles + for iji in range( + (ii - (ii % howmany_sorted)), (ii - (ii % howmany_sorted)) + howmany_sorted, 1 + ): + golden.append( + ((iji * maxangle * (1 + pow(5, 0.5)) / 2) * 1000 % (maxangle * 1000)) / 1000 + ) + golden.sort() + subtomo_number = int(ii / howmany_sorted) + 1 + if reverse and not subtomo_number % 2: + golden.reverse() + return (golden[ii % howmany_sorted], subtomo_number) + + def _golden_equally_spaced( + self, ii, number_of_projections_per_subtomo, maxangle, reverse, verbose + ): + """returns angles for equally spaced tomography with starting angles of sub tomograms shifted according to golden ratio""" + """ii is projection number starting at 1, reverse will execute the even sub tomograms in reverse direction""" + # ii is projection number starting at 1 + angular_step = maxangle / number_of_projections_per_subtomo + subtomo_number = int(((ii - 1) * angular_step) / maxangle) + 1 + start_angle = self._golden(subtomo_number - 1, 1, angular_step) + projection_number_of_subtomo = ( + ii - (subtomo_number - 1) * number_of_projections_per_subtomo + ) - 1 + + if reverse: + if subtomo_number % 2: + angle = start_angle[0] + projection_number_of_subtomo * angular_step + else: + angle = ( + start_angle[0] + + (number_of_projections_per_subtomo - 1) * angular_step + - projection_number_of_subtomo * angular_step + ) + else: + angle = start_angle[0] + projection_number_of_subtomo * angular_step + + if verbose: + print( + f"Equally spaced golden ratio tomography.\nAngular step: {angular_step}\nSubtomo Number: {subtomo_number}\nAngle: {angle}" + ) + + return angle, subtomo_number + + def tomo_reconstruct(self, base_path="~/Data10/specES1"): + """write the tomo reconstruct file for the reconstruction queue""" + bec = builtins.__dict__.get("bec") + base_path = os.path.expanduser(base_path) + ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername)) + ptycho_queue_path.mkdir(parents=True, exist_ok=True) + + # pylint: disable=undefined-variable + last_scan_number = bec.queue.next_scan_number - 1 + ptycho_queue_file = os.path.abspath( + os.path.join(ptycho_queue_path, f"scan_{last_scan_number:05d}.dat") + ) + with open(ptycho_queue_file, "w") as queue_file: + scans = " ".join([str(scan) for scan in self._current_scan_list]) + queue_file.write(f"p.scan_number {scans}\n") + queue_file.write("p.check_nextscan_started 1\n") + + def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None: + tomo_scan_numbers_file = os.path.expanduser( + "~/Data10/specES1/dat-files/tomography_scannumbers.txt" + ) + with open(tomo_scan_numbers_file, "a+") as out_file: + # pylint: disable=undefined-variable + out_file.write( + f"{scan_number} {angle} {dev.fsamroy.read()['fsamroy']['value']:.3f} {self.tomo_id} {subtomo_number} {0} {self.sample_name}\n" + ) + + def tomo_scan_projection(self, angle: float): + scans = builtins.__dict__.get("scans") + + # additional_correction = self.align.compute_additional_correction(angle) + # additional_correction_2 = self.align.compute_additional_correction_2(angle) + # correction_xeye_mu = self.align.lamni_compute_additional_correction_xeye_mu(angle) + + offsets = self.get_alignment_offset(angle) + sum_offset_x = offsets[0] + sum_offset_y = ( + offsets[1] + - self.compute_additional_correction_y(angle) + - self.compute_additional_correction_y_2(angle) + ) + sum_offset_z = offsets[2] + + self._current_scan_list = [] + + for stitch_x in range(-self.stitch_x, self.stitch_x + 1): + for stitch_y in range(-self.stitch_y, self.stitch_y + 1): + # pylint: disable=undefined-variable + corridor_size = self.corridor_size if self.corridor_size > 0 else None + self._current_scan_list.append(bec.queue.next_scan_number) + cenx = sum_offset_x + stitch_x * (self.fovx - self.tomo_stitch_overlap) + ceny = sum_offset_y + stitch_y * (self.fovy - self.tomo_stitch_overlap) + logger.info( + f"scans.flomni_fermat_scan(fovx={self.fovx}, fovy={self.fovy}," + f" step={self.tomo_shellstep}, cenx={cenx}, ceny={ceny}," + f" zshift={sum_offset_z}, angle={angle}," + f" exp_time={self.tomo_countingtime}, corridor_size={corridor_size})" + ) + log_message = ( + f"{str(datetime.datetime.now())}: flomni scan projection at angle {angle}, scan" + f" number {bec.queue.next_scan_number}.\n" + ) + self.write_to_spec_log(log_message) + # self.write_to_scilog(log_message, ["BEC_scans", self.sample_name]) + scans.flomni_fermat_scan( + fovx=self.fovx, + fovy=self.fovy, + step=self.tomo_shellstep, + cenx=cenx, + ceny=ceny, + zshift=sum_offset_z, + angle=angle, + exp_time=self.tomo_countingtime, + corridor_size=corridor_size, + ) + + def tomo_parameters(self): + """print and update the tomo parameters""" + print("Current settings:") + print(f"Counting time = {self.tomo_countingtime} s") + print(f"Stepsize microns = {self.tomo_shellstep}") + print(f"FOV (200/100) = {self.fovx}, {self.fovy}") + print(f"Stitching number x,y = {self.stitch_x}, {self.stitch_y}") + print(f"Stitching overlap = {self.tomo_stitch_overlap}") + print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}") + print(f" _manual_shift_y = {self.manual_shift_y}") + print("") + if self.tomo_type == 1: + print("\x1b[1mTomo type 1:\x1b[0m 8 equally spaced sub-tomograms") + print(f"Total number of projections: {180/self.tomo_angle_stepsize*8}") + print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") + if self.tomo_type == 2: + print("\x1b[1mTomo type 2:\x1b[0m Golden ratio tomography") + print(f"Sorted in bunches of: {self.golden_ratio_bunch_size}") + if self.golden_max_number_of_projections > 0: + print(f"ending after {self.golden_max_number_of_projections} projections") + else: + print("ending by manual interruption") + if self.golden_projections_at_0_deg_for_damage_estimation == 1: + print( + "repeating prjections at 0 degrees at the beginning of every second subtomogram" + ) + if self.tomo_type == 3: + print( + "\x1b[1mTomo type 3:\x1b[0m Equally spaced tomography, golden ratio starting angle" + ) + print(f"Number of projections per sub-tomogram: {180/self.tomo_angle_stepsize}") + print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") + if self.golden_max_number_of_projections > 0: + print(f"ending after {self.golden_max_number_of_projections} projections") + else: + print("ending by manual interruption") + if self.golden_projections_at_0_deg_for_damage_estimation == 1: + print( + "repeating prjections at 0 degrees at the beginning of every second subtomogram" + ) + print(f"\nSample name: {self.sample_name}\n") + + user_input = input("Are these parameters correctly set for your scan? [Y/n]") + if user_input == "y" or user_input == "": + print("... excellent!") + else: + self.tomo_countingtime = self._get_val(" s", self.tomo_countingtime, float) + self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) + self.fovx = self._get_val(" um", self.fovx, float) + self.fovy = self._get_val(" um", self.fovy, float) + self.stitch_x = self._get_val("", self.stitch_x, int) + self.stitch_y = self._get_val("", self.stitch_y, int) + self.ptycho_reconstruct_foldername = self._get_val( + "Reconstruction queue ", self.ptycho_reconstruct_foldername, str + ) + + print("Tomography type:") + print(" 1: 8 equally spaced sub-tomograms") + print(" 2: Golden ratio tomography") + print(" 3: Equally spaced tomography, golden ratio starting angle") + self.tomo_type = self._get_val("Tomography type", self.tomo_type, int) + + if self.tomo_type == 1: + tomo_numberofprojections = self._get_val( + "Total number of projections", 180 / self.tomo_angle_stepsize * 8, int + ) + print(f"The angular step will be {180/tomo_numberofprojections}") + self.tomo_angle_stepsize = 180 / tomo_numberofprojections * 8 + print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}") + + if self.tomo_type == 2: + self.golden_ratio_bunch_size = self._get_val( + "Number of projections sorted per bunch (default 20)", + self.golden_ratio_bunch_size, + int, + ) + self.golden_max_number_of_projections = self._get_val( + "Stop after number of projections (zero for endless)", + self.golden_max_number_of_projections, + int, + ) + self.golden_projections_at_0_deg_for_damage_estimation = self._get_val( + "Repeat projections at 0 deg every second subtomo 1/0 ?", + self.golden_projections_at_0_deg_for_damage_estimation, + int, + ) + + if self.tomo_type == 3: + numprj = self._get_val( + "Number of projections per sub-tomogram", + int(180 / self.tomo_angle_stepsize), + int, + ) + self.tomo_angle_stepsize = 180 / numprj + self.golden_max_number_of_projections = self._get_val( + "Stop after number of projections (zero for endless)", + self.golden_max_number_of_projections, + int, + ) + self.golden_projections_at_0_deg_for_damage_estimation = self._get_val( + "Repeat projections at 0 deg every second subtomo", + self.golden_projections_at_0_deg_for_damage_estimation, + int, + ) + + @staticmethod + def _get_val(msg: str, default_value, data_type): + return data_type(input(f"{msg} ({default_value}): ") or default_value) + + def rt_off(self): + dev.rtx.enabled = False + dev.rty.enabled = False + dev.rtz.enabled = False + + def rt_on(self): + dev.rtx.enabled = True + dev.rty.enabled = True + dev.rtz.enabled = True + if dev.rtx.enabled == True: + print("rt is enabled") + else: + print("failed to enable rt") + + def write_pdf_report(self): + """create and write the pdf report with the current flomni settings""" + dev = builtins.__dict__.get("dev") + # header = "" + header = ( + " \n" * 3 + + " .d888 888 .d88888b. 888b d888 888b 888 8888888 \n" + + ' d88P" 888 d88P" "Y88b 8888b d8888 8888b 888 888 \n' + + " 888 888 888 888 88888b.d88888 88888b 888 888 \n" + + " 888888 888 888 888 888Y88888P888 888Y88b 888 888 \n" + + " 888 888 888 888 888 Y888P 888 888 Y88b888 888 \n" + + " 888 888 888 888 888 Y8P 888 888 Y88888 888 \n" + + ' 888 888 Y88b. .d88P 888 " 888 888 Y8888 888 \n' + + ' 888 888 "Y88888P" 888 888 888 Y888 8888888 \n' + ) + padding = 20 + fovxy = f"{self.fovx:.2f}/{self.fovy:.2f}" + stitching = f"{self.stitch_x:.2f}/{self.stitch_y:.2f}" + dataset_id = str(self.client.queue.next_dataset_number) + content = [ + f"{'Sample Name:':<{padding}}{self.sample_name:>{padding}}\n", + f"{'Measurement ID:':<{padding}}{str(self.tomo_id):>{padding}}\n", + f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", + f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", + f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n", + f"{'Number of projections:':<{padding}}{int(180 / self.tomo_angle_stepsize * 8):>{padding}}\n", + f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", + f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(180 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n", + f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\n", + f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", + f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", + f"{'FOV:':<{padding}}{fovxy:>{padding}}\n", + f"{'Stitching:':<{padding}}{stitching:>{padding}}\n", + f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n", + f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n", + ] + content = "".join(content) + user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf") + with PDFWriter(user_target) as file: + file.write(header) + file.write(content) + subprocess.run( + "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True + ) + # status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True) + msg = bec.logbook.LogbookMessage() + logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") + msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).add_tag( + ["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name] + ) + self.client.logbook.send_logbook_message(msg) + + +if __name__ == "__main__": + import builtins + + from bec_ipython_client import BECIPythonClient + + bec = BECIPythonClient() + bec.start() + scans = bec.scans + dev = bec.device_manager.devices + builtins.__dict__["scans"] = scans + builtins.__dict__["dev"] = dev + builtins.__dict__["bec"] = bec + builtins.__dict__["umv"] = umv + flomni = Flomni(bec) + flomni.start_x_ray_eye_alignment() diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py new file mode 100644 index 0000000..b79fa98 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py @@ -0,0 +1,177 @@ +import time + +from rich import box +from rich.console import Console +from rich.table import Table + +from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose + + +class FlomniOpticsMixin: + @staticmethod + def _get_user_param_safe(device, var): + param = dev[device].user_parameter + if not param or param.get(var) is None: + raise ValueError(f"Device {device} has no user parameter definition for {var}.") + return param.get(var) + + def feye_out(self): + fshclose() + self.foptics_in() + feyex_out = self._get_user_param_safe("feyex", "out") + umv(dev.feyex, feyex_out) + + epics_put("XOMNYI-XEYE-ACQ:0", 2) + # move rotation stage to zero to avoid problems with wires + umv(dev.fsamroy, 0) + # umv(dev.fttrx1, 9.2) + + def feye_in(self): + bec.queue.next_dataset_number += 1 + # umv(dev.fttrx1, -17) + + feyex_in = self._get_user_param_safe("feyex", "in") + feyey_in = self._get_user_param_safe("feyey", "in") + umv(dev.feyex, feyex_in, dev.feyey, feyey_in) + self.align.update_frame() + + def _ffzp_in(self): + foptx_in = self._get_user_param_safe("foptx", "in") + fopty_in = self._get_user_param_safe("fopty", "in") + umv(dev.foptx, foptx_in) + umv( + dev.fopty, fopty_in + ) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm + + def ffzp_in(self): + """ + move in the flomni zone plate. + This will disable rt feedback, move the FZP and re-enabled the feedback. + """ + if "rtx" in dev and dev.rtx.enabled: + dev.rtx.controller.feedback_disable() + + self._ffzp_in() + + if "rtx" in dev and dev.rtx.enabled: + dev.rtx.controller.feedback_enable_with_reset() + + def foptics_in(self): + """ + Move in the flomni optics, including the FZP and the OSA. + """ + self.ffzp_in() + self.fosa_in() + + def foptics_out(self): + """Move out the flomni optics""" + if "rtx" in dev and dev.rtx.enabled: + dev.rtx.controller.feedback_disable() + + self.fosa_out() + fopty_out = self._get_user_param_safe("fopty", "out") + umv(dev.fopty, fopty_out) + + if "rtx" in dev and dev.rtx.enabled: + time.sleep(1) + dev.rtx.controller.feedback_enable_with_reset() + + def fosa_in(self): + # 6.2 keV, 170 um FZP + # umv(dev.losax, -1.4450000, dev.losay, -0.1800) + # umv(dev.losaz, -1) + # 6.7, 170 + # umv(dev.losax, -1.4850, dev.losay, -0.1930) + # umv(dev.losaz, 1.0000) + # 7.2, 150 + fosax_in = self._get_user_param_safe("fosax", "in") + fosay_in = self._get_user_param_safe("fosay", "in") + fosaz_in = self._get_user_param_safe("fosaz", "in") + dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1] + dev.fosay.limits = [fosay_in - 0.1, fosay_in + 0.1] + dev.fosaz.limits = [fosaz_in - 0.1, fosaz_in + 0.1] + umv(dev.fosax, fosax_in, dev.fosay, fosay_in) + umv(dev.fosaz, fosaz_in) + + # 11 kev + # umv(dev.losax, -1.161000, dev.losay, -0.196) + # umv(dev.losaz, 1.0000) + + def fosa_out(self): + self.ensure_fheater_up() + curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered() + if not curtain_is_triggered: + fosaz_out = self._get_user_param_safe("fosaz", "out") + dev.fosaz.limits = [fosaz_out - 0.1, fosaz_out + 0.1] + umv(dev.fosaz, fosaz_out) + fosax_out = self._get_user_param_safe("fosax", "out") + dev.fosax.limits = [fosax_out - 0.1, fosax_out + 0.1] + umv(dev.fosax, fosax_out) + + def ffzp_info(self, mokev_val=-1): + + if mokev_val == -1: + try: + mokev_val = dev.mokev.readback.get() + except: + print( + "Device mokev does not exist. You can specify the energy in keV as an argument instead." + ) + return + foptz_val = dev.foptz.readback.get() + distance = -foptz_val + 43.15 + 36.7 + print(f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the FZP.\n") + print(f"At the current energy of {mokev_val:.4f} keV we have following options:\n") + + diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6] + + console = Console() + table = Table(title="Outermost zone width \033[1m60 nm\033[0m", box=box.SQUARE) + table.add_column("Diameter", justify="center") + table.add_column("Focal distance", justify="center") + table.add_column("Current beam size", justify="center") + + wavelength = 1.2398e-9 / mokev_val + + for diameter in diameters: + outermost_zonewidth = 60e-9 + focal_distance = diameter * outermost_zonewidth / wavelength * 1000 + beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9 + table.add_row( + f"{diameter*1e6:.2f} microns", + f"{focal_distance:.2f} mm", + f"{beam_size:.2f} microns", + ) + + console.print(table) + + diameters = [150e-6, 250e-6] + + console = Console() + table = Table(title="Outermost zone width \033[1m30 nm\033[0m", box=box.SQUARE) + table.add_column("Diameter", justify="center") + table.add_column("Focal distance", justify="center") + table.add_column("Current beam size", justify="center") + + wavelength = 1.2398e-9 / mokev_val + + for diameter in diameters: + outermost_zonewidth = 30e-9 + focal_distance = diameter * outermost_zonewidth / wavelength * 1000 + beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9 + table.add_row( + f"{diameter*1e6:.2f} microns", + f"{focal_distance:.2f} mm", + f"{beam_size:.2f} microns", + ) + + console.print(table) + + fosaz_val = dev.fosaz.readback.get() + + print("\nOSA Information:") + print(f" Current fosaz {fosaz_val:.1f}") + print( + f" The OSA will collide with a normal OMNY pin at fosaz \033[1m{(33-fosaz_val):.1f}\033[0m" + ) + print(f" Remaining space: \033[1m{-fosaz_val+(33-foptz_val):.1f}\033[0m") diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml new file mode 100644 index 0000000..3fa9d83 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml @@ -0,0 +1,340 @@ +fheater: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - -15 + - 0 + port: 8082 + sign: -1 + onFailure: buffer + enabled: true + readOnly: false +feyex: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: D + host: mpc2844.psi.ch + limits: + - -30 + - -1 + port: 8082 + sign: 1 + onFailure: buffer + userParameter: + in: -16.267 + out: -1 + enabled: true + readOnly: false +feyey: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: E + host: mpc2844.psi.ch + limits: + - -1 + - -10 + port: 8082 + sign: 1 + onFailure: buffer + userParameter: + in: -10.467 + enabled: true + readOnly: false +foptx: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - -17 + - -12 + port: 8082 + sign: 1 + onFailure: buffer + userParameter: + in: -13.761 + enabled: true + readOnly: false +fopty: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: F + host: mpc2844.psi.ch + limits: + - 0 + - 4 + port: 8082 + sign: 1 + onFailure: buffer + userParameter: + in: 0.552 + out: 0.752 + enabled: true + readOnly: false +foptz: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - 0 + - 27 + port: 8082 + sign: 1 + onFailure: buffer + userParameter: + in: 23 + enabled: true + readOnly: false +fosax: + readoutPriority: baseline + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - 10.2 + - 10.6 + port: 3334 + sign: -1 + onFailure: buffer + userParameter: + in: 9.124 + out: 5.3 + enabled: true + readOnly: false +fosay: + readoutPriority: baseline + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - -3.1 + - -2.9 + port: 3334 + sign: -1 + onFailure: buffer + userParameter: + in: 0.367 + enabled: true + readOnly: false +fosaz: + readoutPriority: baseline + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - -6 + - -4 + port: 3334 + sign: 1 + onFailure: buffer + userParameter: + in: 8.5 + out: 6 + enabled: true + readOnly: false +fsamroy: + readoutPriority: baseline + description: phase plate angle + deviceClass: FuprGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - -5 + - 365 + port: 8084 + sign: -1 + onFailure: buffer + enabled: true + readOnly: false +fsamx: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: E + host: mpc2844.psi.ch + limits: + - -162 + - 0 + port: 8081 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false + userParameter: + in: -1.1 +fsamy: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: F + host: mpc2844.psi.ch + limits: + - 2 + - 3.1 + port: 8081 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false + userParameter: + in: 2.75 +ftracky: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: H + host: mpc2844.psi.ch + limits: + - 2.2 + - 2.8 + port: 8082 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +ftrackz: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: G + host: mpc2844.psi.ch + limits: + - 4.5 + - 5.5 + port: 8082 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +ftransx: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - 0 + - 50 + port: 8081 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +ftransy: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - -100 + - 0 + port: 8081 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +ftransz: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - 0 + - 145 + port: 8081 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +ftray: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: D + host: mpc2844.psi.ch + limits: + - -200 + - 0 + port: 8081 + sign: -1 + onFailure: buffer + enabled: true + readOnly: false + +flomni_samples: + readoutPriority: baseline + description: phase plate angle + deviceClass: FlomniSampleStorage + deviceConfig: + onFailure: buffer + enabled: true + readOnly: false + +rtx: + readoutPriority: on_request + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + port: 2222 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +rty: + readoutPriority: on_request + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + port: 2222 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false +rtz: + readoutPriority: on_request + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + port: 2222 + sign: 1 + onFailure: buffer + enabled: true + readOnly: false \ No newline at end of file diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py new file mode 100644 index 0000000..4ad60fc --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import builtins +import os +import time +from typing import TYPE_CHECKING + +from bec_lib import bec_logger + +from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen + +logger = bec_logger.logger +# import builtins to avoid linter errors +bec = builtins.__dict__.get("bec") +dev = builtins.__dict__.get("dev") +umv = builtins.__dict__.get("umv") +umvr = builtins.__dict__.get("umvr") + +if TYPE_CHECKING: + from bec_ipython_client.plugins.flomni import Flomni + + +class XrayEyeAlign: + # pixel calibration, multiply to get mm + PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning + + def __init__(self, client, flomni: Flomni) -> None: + self.client = client + self.flomni = flomni + self.device_manager = client.device_manager + self.scans = client.scans + self.alignment_values = {} + self.flomni.reset_correction() + self.flomni.reset_tomo_alignment_fit() + + def _reset_init_values(self): + self.shift_xy = [0, 0] + self._xray_fov_xy = [0, 0] + + def save_frame(self): + epics_put("XOMNYI-XEYE-SAVFRAME:0", 1) + + def update_frame(self): + epics_put("XOMNYI-XEYE-ACQDONE:0", 0) + # start live + epics_put("XOMNYI-XEYE-ACQ:0", 1) + # wait for start live + while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: + time.sleep(0.5) + print("waiting for live view to start...") + fshopen() + + epics_put("XOMNYI-XEYE-ACQDONE:0", 0) + + while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: + print("waiting for new frame...") + time.sleep(0.5) + + time.sleep(0.5) + # stop live view + epics_put("XOMNYI-XEYE-ACQ:0", 0) + time.sleep(1) + # fshclose + print("got new frame") + + def tomo_rotate(self, val: float): + # pylint: disable=undefined-variable + umv(self.device_manager.devices.fsamroy, val) + + def get_tomo_angle(self): + return self.device_manager.devices.fsamroy.readback.get() + + def update_fov(self, k: int): + self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0]) + self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0]) + + @property + def movement_buttons_enabled(self): + return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")] + + @movement_buttons_enabled.setter + def movement_buttons_enabled(self, enabled: bool): + enabled = int(enabled) + epics_put("XOMNYI-XEYE-ENAMVX:0", enabled) + epics_put("XOMNYI-XEYE-ENAMVY:0", enabled) + + def send_message(self, msg: str): + epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg) + + def align(self): + # reset shift xy and fov params + self._reset_init_values() + + self.flomni.lights_off() + + self.tomo_rotate(0) + epics_put("XOMNYI-XEYE-ANGLE:0", 0) + + self.flomni.feye_in() + + self.flomni.laser_tracker_on() + + self.flomni.rt_feedback_enable_with_reset() + + # disable movement buttons + self.movement_buttons_enabled = False + + sample_name = self.flomni.sample_get_name(0) + epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name) + + # this makes sure we are in a defined state + self.flomni.rt_feedback_disable() + + epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION) + + self.flomni.fosa_out() + + fsamx_in = self.flomni._get_user_param_safe("fsamx", "in") + umv(dev.fsamx, fsamx_in - 0.25) + + self.flomni.ffzp_in() + self.update_frame() + + # enable submit buttons + self.movement_buttons_enabled = False + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + epics_put("XOMNYI-XEYE-STEP:0", 0) + self.send_message("Submit center value of FZP.") + + k = 0 + while True: + if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1: + val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm + self.alignment_values[k] = val_x + print(f"Clicked position {k}: x {self.alignment_values[k]}") + rtx_position = dev.rtx.readback.get() / 1000 + print(f"Current rtx position {rtx_position}") + self.alignment_values[k] -= rtx_position + print(f"Corrected position {k}: x {self.alignment_values[k]}") + + if k == 0: # received center value of FZP + self.send_message("please wait ...") + self.movement_buttons_enabled = False + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + + self.flomni.rt_feedback_disable() + fsamx_in = self.flomni._get_user_param_safe("fsamx", "in") + umv(dev.fsamx, fsamx_in) + + self.flomni.foptics_out() + + self.flomni.rt_feedback_disable() + umv(dev.fsamx, fsamx_in - 0.25) + + self.update_frame() + epics_put("XOMNYI-XEYE-RECBG:0", 1) + while epics_get("XOMNYI-XEYE-RECBG:0") == 1: + time.sleep(0.5) + print("waiting for background frame...") + + umv(dev.fsamx, fsamx_in) + time.sleep(0.5) + self.flomni.rt_feedback_enable_with_reset() + + self.update_frame() + self.send_message("Adjust sample height and submit center") + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + self.movement_buttons_enabled = True + + elif 1 <= k < 5: # received sample center value at samroy 0 ... 315 + self.send_message("please wait ...") + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) + self.movement_buttons_enabled = False + + umv(dev.rtx, 0) + self.tomo_rotate(k * 45) + epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) + self.update_frame() + self.send_message("Submit sample center") + epics_put("XOMNYI-XEYE-SUBMIT:0", 0) + epics_put("XOMNYI-XEYE-ENAMVX:0", 1) + self.update_fov(k) + + elif k == 5: # received sample center value at samroy 270 and done + self.send_message("done...") + epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button + self.movement_buttons_enabled = False + self.update_fov(k) + break + + k += 1 + epics_put("XOMNYI-XEYE-STEP:0", k) + + _xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0") + if _xrayeyalignmvx != 0: + umvr(dev.rtx, _xrayeyalignmvx) + print(f"Current rtx position {dev.rtx.readback.get() / 1000}") + epics_put("XOMNYI-XEYE-MVX:0", 0) + if k > 0: + epics_put(f"XOMNYI-XEYE-STAGEPOSX:{k}", dev.rtx.readback.get() / 1000) + time.sleep(3) + self.update_frame() + + if k < 2: + # allow movements, store movements to calculate center + _xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0") + if _xrayeyalignmvy != 0: + self.flomni.rt_feedback_disable() + umvr(dev.fsamy, _xrayeyalignmvy / 1000) + time.sleep(2) + epics_put("XOMNYI-XEYE-MVY:0", 0) + self.flomni.rt_feedback_enable_with_reset() + self.update_frame() + time.sleep(0.2) + + self.write_output() + fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2 + fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2 + + self.tomo_rotate(0) + + umv(dev.rtx, 0) + + # free camera + epics_put("XOMNYI-XEYE-ACQ:0", 2) + + print( + f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy" + f" = {fovy:.0f} microns" + ) + print("Use the matlab routine to FIT the current alignment...") + + print("Then LOAD ALIGNMENT PARAMETERS by running flomni.read_alignment_offset()\n") + + def write_output(self): + file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues") + if not os.path.exists(file): + os.makedirs(os.path.dirname(file), exist_ok=True) + with open(file, "w") as alignment_values_file: + alignment_values_file.write("angle\thorizontal\n") + for k in range(1, 6): + fovx_offset = self.alignment_values[0] - self.alignment_values[k] + print(f"Writing to file new alignment: number {k}, value x {fovx_offset}") + alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n") diff --git a/bec_plugins/bec_client/hli/__init__.py b/csaxs_bec/deployment/__init__.py similarity index 100% rename from bec_plugins/bec_client/hli/__init__.py rename to csaxs_bec/deployment/__init__.py diff --git a/bec_plugins/bec_client/startup/__init__.py b/csaxs_bec/deployment/device_server/__init__.py similarity index 100% rename from bec_plugins/bec_client/startup/__init__.py rename to csaxs_bec/deployment/device_server/__init__.py diff --git a/bec_plugins/device_server/startup.py b/csaxs_bec/deployment/device_server/startup.py similarity index 100% rename from bec_plugins/device_server/startup.py rename to csaxs_bec/deployment/device_server/startup.py diff --git a/bec_plugins/configs/bec_device_config_sastt.yaml b/csaxs_bec/device_configs/bec_device_config_sastt.yaml similarity index 100% rename from bec_plugins/configs/bec_device_config_sastt.yaml rename to csaxs_bec/device_configs/bec_device_config_sastt.yaml diff --git a/bec_plugins/configs/e21125_lamni_config.yaml b/csaxs_bec/device_configs/e21125_lamni_config.yaml similarity index 100% rename from bec_plugins/configs/e21125_lamni_config.yaml rename to csaxs_bec/device_configs/e21125_lamni_config.yaml diff --git a/csaxs_bec/device_configs/flomni_config.yaml b/csaxs_bec/device_configs/flomni_config.yaml new file mode 100644 index 0000000..942497c --- /dev/null +++ b/csaxs_bec/device_configs/flomni_config.yaml @@ -0,0 +1,344 @@ +feyex: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: D + host: mpc2844.psi.ch + limits: + - -30 + - -1 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: -16.267 + out: -1 +feyey: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: E + host: mpc2844.psi.ch + limits: + - -1 + - -10 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: -10.467 +fheater: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - -15 + - 0 + port: 8082 + sign: -1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +flomni_samples: + description: phase plate angle + deviceClass: FlomniSampleStorage + deviceConfig: {} + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +foptx: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - -17 + - -12 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: baseline + userParameter: + in: -13.761 +fopty: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: F + host: mpc2844.psi.ch + limits: + - 0 + - 4 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: baseline + userParameter: + in: 0.552 + out: 0.752 +foptz: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - 0 + - 27 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: 23 +fosax: + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - 10.2 + - 10.6 + port: 3334 + sign: -1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: 9.124 + out: 5.3 +fosay: + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - -3.1 + - -2.9 + port: 3334 + sign: -1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: 0.367 +fosaz: + description: phase plate angle + deviceClass: SmaractMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - -6 + - -4 + port: 3334 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + in: 8.5 + out: 6 +fsamroy: + description: phase plate angle + deviceClass: FuprGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - -5 + - 365 + port: 8084 + sign: -1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +fsamx: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: E + host: mpc2844.psi.ch + limits: + - -162 + - 0 + port: 8081 + sign: 1 + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: baseline + userParameter: + in: -1.1 +fsamy: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: F + host: mpc2844.psi.ch + limits: + - 2 + - 3.1 + port: 8081 + sign: 1 + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: baseline + userParameter: + in: 2.75 +ftracky: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: H + host: mpc2844.psi.ch + limits: + - 2.2 + - 2.8 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +ftrackz: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: G + host: mpc2844.psi.ch + limits: + - 4.5 + - 5.5 + port: 8082 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +ftransx: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + limits: + - 0 + - 50 + port: 8081 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +ftransy: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + limits: + - -100 + - 0 + port: 8081 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +ftransz: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + limits: + - 0 + - 145 + port: 8081 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +ftray: + description: phase plate angle + deviceClass: FlomniGalilMotor + deviceConfig: + axis_Id: D + host: mpc2844.psi.ch + limits: + - -200 + - 0 + port: 8081 + sign: -1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline +rtx: + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: A + host: mpc2844.psi.ch + port: 2222 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: on_request + userParameter: + low_signal: 11000 + min_signal: 10000 + rt_pid_voltage: -0.06219 +rty: + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: B + host: mpc2844.psi.ch + port: 2222 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: on_request + userParameter: + tomo_additional_offsety: 0 +rtz: + description: flomni rt + deviceClass: RtFlomniMotor + deviceConfig: + axis_Id: C + host: mpc2844.psi.ch + port: 2222 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: on_request diff --git a/csaxs_bec/device_configs/x12sa_database.yml b/csaxs_bec/device_configs/x12sa_database.yml new file mode 100644 index 0000000..ef1735e --- /dev/null +++ b/csaxs_bec/device_configs/x12sa_database.yml @@ -0,0 +1,1463 @@ +FBPMDX: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ID-FBPMD:X + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +FBPMDY: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ID-FBPMD:Y + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +FBPMUX: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ID-FBPMU:X + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +FBPMUY: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ID-FBPMU:Y + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +XASYM: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-LBB:X-ASYM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +XSYM: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-LBB:X-SYM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +YASYM: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-LBB:Y-ASYM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +YSYM: + description: FOFB reference + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-LBB:Y-SYM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +aptrx: + description: ES aperture horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-PIN1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +aptry: + description: ES aperture vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-PIN1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm1trx: + description: FrontEnd XBPM 1 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-BM1:TRH + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm1try: + description: FrontEnd XBPM 1 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-BM1:TRV + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm2trx: + description: FrontEnd XBPM 2 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-BM2:TRH + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm2try: + description: FrontEnd XBPM 2 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-BM2:TRV + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm3trx: + description: OpticsHutch XBPM 1 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm3try: + description: OpticsHutch XBPM 1 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm4trx: + description: OpticsHutch XBPM 2 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM2:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm4try: + description: OpticsHutch XBPM 2 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm5trx: + description: OpticsHutch XBPM 3 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM3:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bm5try: + description: OpticsHutch XBPM 3 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-BM3:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm1: + description: 'XBPM 1: Somewhere around mono (VME)' + deviceClass: XbpmCsaxsOp + deviceConfig: + prefix: 'X12SA-OP-BPM2:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm1i: + description: Some VME XBPM... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM1:SUM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm2: + description: 'XBPM 2: Somewhere around mono (VME)' + deviceClass: XbpmCsaxsOp + deviceConfig: + prefix: 'X12SA-OP-BPM2:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm2i: + description: Some VME XBPM... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM2:SUM + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM3:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3a: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3b: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3c: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3d: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm4a: + description: 'XBPM 4: VME between mono and mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP1-SCALER.S2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm4b: + description: 'XBPM 4: VME between mono and mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP1-SCALER.S3 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm4c: + description: 'XBPM 4: VME between mono and mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP1-SCALER.S4 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm4d: + description: 'XBPM 4: VME between mono and mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP1-SCALER.S5 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm4i: + description: 'XBPM 4: integrated counts' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP1-SCALER. + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM5:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5a: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5b: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5c: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5d: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM6:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6a: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6b: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6c: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6d: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bs1x: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-BS1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bs1y: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-BS1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bs2x: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-BS2:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bs2y: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-BS2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +curr: + description: SLS ring current + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: ARIDI-PCT:CURRENT + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +cyb: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dettrx: + description: Detector tower motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-DET1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +di2trx: + description: FrontEnd diaphragm 2 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-DI2:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +di2try: + description: FrontEnd diaphragm 2 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-DI2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +diode: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S3 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dtpush: + description: Detector tower tilt pusher + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-DETT:ROX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dtth: + description: Detector tower tilt rotation + deviceClass: PmDetectorRotation + deviceConfig: + prefix: X12SA-ES1-DETT:ROX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dttrx: + description: Detector tower motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-DETT:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dttry: + description: Detector tower motion, no encoder + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-DETT:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +dttrz: + description: Detector tower motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-DETT:TRZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +ebtrx: + description: Exposure box 2 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-EB:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +ebtry: + description: Exposure box 2 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-EB:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +ebtrz: + description: Exposure box 2 axial movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-EB:TRZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyecenx: + description: X-ray eye intensit math + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: XOMNYI-XEYE-XCEN:0 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyeceny: + description: X-ray eye intensit math + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: XOMNYI-XEYE-YCEN:0 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyefoc: + description: X-ray eye focusing motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES25 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyeint: + description: X-ray eye intensit math + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: XOMNYI-XEYE-INT_MEAN:0 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyex: + description: X-ray eye motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES01 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +eyey: + description: X-ray eye motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES02 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fal0: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S4 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fal1: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S5 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fal2: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S6 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fi1try: + description: OpticsHutch filter 1 movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-FI1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fi2try: + description: OpticsHutch filter 2 movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-FI2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fi3try: + description: OpticsHutch filter 3 movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-FI3:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +ftp: + description: Flight tube pressure + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-FT1MT1:PRESSURE + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fttrx1: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-FTS1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fttrx2: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-FTS2:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fttry1: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-FTS1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fttry2: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-FTS2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +fttrz: + description: Dunno these motors??? + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES1-FTS1:TRZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +idgap: + description: Undulator gap size [mm] + deviceClass: InsertionDevice + deviceConfig: + prefix: X12SA-ID + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +led: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S4 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mibd1: + description: Mirror bender 1 + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mibd2: + description: Mirror bender 2 + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRZ2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +micfoc: + description: Microscope focusing motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES03 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mitrx: + description: Mirror horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mitry1: + description: Mirror vertical movement 1 + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mitry2: + description: Mirror vertical movement 2 + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRY2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mitry3: + description: Mirror vertical movement 3 + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MI:TRY3 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mobd: + description: Monochromator bender virtual motor + deviceClass: PmMonoBender + deviceConfig: + prefix: 'X12SA-OP-MO:' + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mobdai: + description: Monochromator bender inner motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRYA + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mobdbo: + description: Monochromator bender outer motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRYB + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mobdco: + description: Monochromator bender outer motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRYC + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mobddi: + description: Monochromator bender inner motor + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRYD + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mokev: + description: Monochromator energy in keV + deviceClass: EnergyKev + deviceConfig: + read_pv: X12SA-OP-MO:ROX2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mopush1: + description: Monochromator crystal 1 angle + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-MO:ROX1 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +mopush2: + description: Monochromator crystal 2 angle + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-MO:ROX2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moroll1: + description: Monochromator crystal 1 roll + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:ROZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moroll2: + description: Monochromator crystal 2 roll movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:ROZ2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moth1: + description: Monochromator Theta 1 + deviceClass: MonoTheta1 + deviceConfig: + read_pv: X12SA-OP-MO:ROX1 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moth1e: + description: Monochromator crystal 1 theta encoder + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-MO:ECX1 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moth2: + description: Monochromator Theta 2 + deviceClass: MonoTheta2 + deviceConfig: + read_pv: X12SA-OP-MO:ROX2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moth2e: + description: Monochromator crystal 2 theta encoder + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-MO:ECX2 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +motrx2: + description: Monochromator crystal 2 horizontal movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRX2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +motry: + description: OpticsHutch optical table vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-OT:TRY + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +motry2: + description: Monochromator crystal 2 vertical movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRY2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +motrz1: + description: Monochromator crystal 1 axial movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:TRZ1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +motrz1e: + description: Monochromator crystal 1 axial movement encoder + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-MO:ECZ1 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +moyaw2: + description: Monochromator crystal 2 yaw movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-MO:ROY2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +samx: + description: Sample motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES04 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +samy: + description: Sample motion + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-ES2-ES05 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sec: + description: Some scaler... + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-ES1-SCALER.S1 + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl0h: + description: FrontEnd slit virtual movement + deviceClass: SlitH + deviceConfig: + prefix: 'X12SA-FE-SH1:' + deviceTags: + - epicsDevice + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl0trxi: + description: FrontEnd slit inner blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-SH1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl0trxo: + description: FrontEnd slit outer blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-FE-SH1:TRX2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1h: + description: OpticsHutch slit virtual movement + deviceClass: SlitH + deviceConfig: + prefix: 'X12SA-OP-SH1:' + deviceTags: + - epicsDevice + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1trxi: + description: OpticsHutch slit inner blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SH1:TRX2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1trxo: + description: OpticsHutch slit outer blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SH1:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1tryb: + description: OpticsHutch slit bottom blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SV1:TRY2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1tryt: + description: OpticsHutch slit top blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SV1:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl1v: + description: OpticsHutch slit virtual movement + deviceClass: SlitV + deviceConfig: + prefix: 'X12SA-OP-SV1:' + deviceTags: + - epicsDevice + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2h: + description: OpticsHutch slit 2 virtual movement + deviceClass: SlitH + deviceConfig: + prefix: 'X12SA-OP-SH2:' + deviceTags: + - epicsDevice + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2trxi: + description: OpticsHutch slit 2 inner blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SH2:TRX2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2trxo: + description: OpticsHutch slit 2 outer blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SH2:TRX1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2tryb: + description: OpticsHutch slit 2 bottom blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SV2:TRY2 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2tryt: + description: OpticsHutch slit 2 top blade movement + deviceClass: EpicsMotor + deviceConfig: + prefix: X12SA-OP-SV2:TRY1 + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sl2v: + description: OpticsHutch slit 2 virtual movement + deviceClass: SlitV + deviceConfig: + prefix: 'X12SA-OP-SV2:' + deviceTags: + - epicsDevice + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +strox: + description: Girder virtual pitch + deviceClass: GirderMotorPITCH + deviceConfig: + prefix: X12SA-HG + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +stroy: + description: Girder virtual yaw + deviceClass: GirderMotorYAW + deviceConfig: + prefix: X12SA-HG + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +stroz: + description: Girder virtual roll + deviceClass: GirderMotorROLL + deviceConfig: + prefix: X12SA-HG + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sttrx: + description: Girder X translation + deviceClass: GirderMotorX1 + deviceConfig: + prefix: X12SA-HG + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +sttry: + description: Girder Y translation + deviceClass: GirderMotorY1 + deviceConfig: + prefix: X12SA-HG + deviceTags: + - beamlineMotor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +transd: + description: Transmission diode + deviceClass: EpicsSignalRO + deviceConfig: + read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false diff --git a/bec_plugins/device_server/__init__.py b/csaxs_bec/devices/eiger1p5m_csaxs/__init__.py similarity index 100% rename from bec_plugins/device_server/__init__.py rename to csaxs_bec/devices/eiger1p5m_csaxs/__init__.py diff --git a/csaxs_bec/devices/eiger1p5m_csaxs/eiger1p5m.py b/csaxs_bec/devices/eiger1p5m_csaxs/eiger1p5m.py new file mode 100644 index 0000000..a7d3012 --- /dev/null +++ b/csaxs_bec/devices/eiger1p5m_csaxs/eiger1p5m.py @@ -0,0 +1,173 @@ +import os +import time + +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal + +logger = bec_logger.logger + + +class _SLSDetectorConfigSignal(Signal): + def put(self, value, *, timestamp=None, force=False): + self._readback = value + self.parent.sim_state[self.name] = value + + def get(self): + self._readback = self.parent.sim_state[self.name] + return self.parent.sim_state[self.name] + + +# if (_eigerinvac_is_on == 1) { +# tic("setup eiger in vac") +# epics_put("X12SA-ES1-DOUBLE-02",0) +# unix(sprintf("mkdir -p /sls/X12SA/data/%s/Data10/eiger_4/S%05d-%05d/S%05d",_username,int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1)) + +# epics_put("XOMNYI-DET-OUTDIR:0.DESC",sprintf("/sls/X12SA/data/%s/Data10/eiger_4/",_username)) +# epics_put("XOMNYI-DET-OUTDIR:1.DESC",sprintf("S%05d-%05d/S%05d",int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1)) +# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts) +# epics_put("XOMNYI-DET-EXPTIME:0", $2) +# epics_put("XOMNYI-DET-INDEX:0", SCAN_N+1) + +# epics_put("XOMNYI-DET-CONTROL:0.DESC", "begin") +# if(_eigerinvac_burst==0) +# { +# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts) +# epics_put("XOMNYI-DET-EXPTIME:0", $2) +# metadata_set("eiger_burst", "int", 1, 1) +# } +# else +# { +# metadata_set("eiger_burst", "int", 1, (int($2/0.0085))) +# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts*(int($2/0.0085))) +# epics_put("XOMNYI-DET-EXPTIME:0", 0.005) +# } +# global _DC_acquisition_ID +# _DC_acquisition_ID = SCAN_N+1 + + +class Eiger1p5MDetector(Device): + USER_ACCESS = [] + file_path = Cpt(EpicsSignal, name="file_path", read_pv="XOMNYI-DET-OUTDIR:0.DESC") + detector_out_scan_dir = Cpt( + EpicsSignal, name="detector_out_scan_dir", read_pv="XOMNYI-DET-OUTDIR:1.DESC" + ) + frames = Cpt(EpicsSignal, name="frames", read_pv="XOMNYI-DET-CYCLES:0") + exp_time = Cpt(EpicsSignal, name="exp_time", read_pv="XOMNYI-DET-EXPTIME:0") + index = Cpt(EpicsSignal, name="index", read_pv="XOMNYI-DET-INDEX:0") + detector_control = Cpt( + EpicsSignal, name="detector_control", read_pv="XOMNYI-DET-CONTROL:0.DESC" + ) + framescaught = Cpt(EpicsSignalRO, name="framescaught", read_pv="XOMNYI-DET-CONTROL:0.VAL") + + file_pattern = Cpt(_SLSDetectorConfigSignal, name="file_pattern", value="") + burst = Cpt(_SLSDetectorConfigSignal, name="burst", value=1) + save_file = Cpt(_SLSDetectorConfigSignal, name="save_file", value=False) + + def __init__(self, *, name, kind=None, parent=None, device_manager=None, **kwargs): + self.device_manager = device_manager + super().__init__(name=name, parent=parent, kind=kind, **kwargs) + self.sim_state = { + f"{self.name}_file_path": "~/Data10/data/", + f"{self.name}_file_pattern": f"{self.name}_{{:05d}}.h5", + f"{self.name}_frames": 1, + f"{self.name}_burst": 1, + f"{self.name}_save_file": False, + f"{self.name}_exp_time": 0, + } + self._stopped = False + self.file_name = "" + self.metadata = {} + self.username = "e20588" # TODO get from config + + def _get_current_scan_msg(self) -> messages.ScanStatusMessage: + return self.device_manager.connector.get(MessageEndpoints.scan_status()) + + def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None): + if leading_zeros is None: + leading_zeros = len(str(scan_bundle)) + floor_dir = scan_number // scan_bundle * scan_bundle + return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}" + + def stage(self) -> list[object]: + scan_msg = self._get_current_scan_msg() + self.metadata = { + "scan_id": scan_msg.content["scan_id"], + "RID": scan_msg.content["info"]["RID"], + "queue_id": scan_msg.content["info"]["queue_id"], + } + scan_number = scan_msg.content["info"]["scan_number"] + exp_time = scan_msg.content["info"]["exp_time"] + + # set base path for detector output + self.file_path.set(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/") + + # set scan directory (e.g. S00000-00999/S00020) + scan_dir = self._get_scan_dir(scan_bundle=1000, scan_number=scan_number, leading_zeros=5) + self.detector_out_scan_dir.set(scan_dir) + + self.file_name = os.path.join(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/", scan_dir) + + # set the scan number + self.index.set(scan_number) + + # set the number of frames + self.frames.set(scan_msg.content["info"]["num_points"]) + + # set the exposure time + self.exp_time.set(exp_time) + + # wait for detector control to become "ready" + while True: + det_ctrl = self.detector_control.get() + if det_ctrl == "ready": + break + time.sleep(0.005) + + # send the "begin" flag to start processing the above commands + self.detector_control.set("begin") + + # wait for detector to be "armed" + logger.info("Waiting for detector setup") + while True: + det_ctrl = self.detector_control.get() + if det_ctrl == "armed": + break + time.sleep(0.005) + + self.detector_control.put("acquiring") + + return super().stage() + + def unstage(self) -> list[object]: + time_waited = 0 + sleep_time = 0.2 + framesexpected = self.frames.get() + while True: + framescaught = self.framescaught.get() + if self.framescaught.get() < framesexpected: + logger.info( + f"Waiting for frames: Transferred {framescaught} out of {framesexpected}" + ) + time_waited += sleep_time + time.sleep(sleep_time) + if self._stopped: + break + continue + break + self.detector_control.put("stop") + signals = {"config": self.read(), "data": self.file_name} + msg = messages.DeviceMessage(signals=signals, metadata=self.metadata) + self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg) + self._stopped = False + return super().unstage() + + def stop(self, *, success=False): + self.detector_control.put("stop") + super().stop(success=success) + self._stopped = True + + +if __name__ == "__main__": + eiger = Eiger1p5MDetector(name="eiger1p5m", label="eiger1p5m") + breakpoint() diff --git a/csaxs_bec/devices/epics/__init__.py b/csaxs_bec/devices/epics/__init__.py new file mode 100644 index 0000000..daf9275 --- /dev/null +++ b/csaxs_bec/devices/epics/__init__.py @@ -0,0 +1,8 @@ +# Standard ophyd classes +from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd.quadem import QuadEM +from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal + +from .devices.delay_generator_csaxs import DelayGeneratorcSAXS +from .devices.flomni_sample_storage import FlomniSampleStorage +from .devices.InsertionDevice import InsertionDevice diff --git a/csaxs_bec/devices/epics/devices/InsertionDevice.py b/csaxs_bec/devices/epics/devices/InsertionDevice.py new file mode 100644 index 0000000..d7d51ad --- /dev/null +++ b/csaxs_bec/devices/epics/devices/InsertionDevice.py @@ -0,0 +1,28 @@ +from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind, PVPositioner + + +class InsertionDevice(PVPositioner): + """Python wrapper for the CSAXS insertion device control + + This wrapper provides a positioner interface for the ID control. + is completely custom XBPM with templates directly in the + VME repo. Thus it needs a custom ophyd template as well... + + WARN: The x and y are not updated by the IOC + """ + + status = Component(EpicsSignalRO, "-USER:STATUS", auto_monitor=True) + errorSource = Component(EpicsSignalRO, "-USER:ERROR-SOURCE", auto_monitor=True) + isOpen = Component(EpicsSignalRO, "-GAP:ISOPEN", auto_monitor=True) + + # PVPositioner interface + setpoint = Component(EpicsSignal, "-GAP:SET", auto_monitor=True) + readback = Component(EpicsSignalRO, "-GAP:READ", auto_monitor=True, kind=Kind.hinted) + done = Component(EpicsSignalRO, ":DONE", auto_monitor=True) + stop_signal = Component(EpicsSignal, "-GAP:STOP", kind=Kind.omitted) + + +# Automatically start simulation if directly invoked +# (NA for important devices) +if __name__ == "__main__": + pass diff --git a/csaxs_bec/devices/epics/devices/XbpmBase.py b/csaxs_bec/devices/epics/devices/XbpmBase.py new file mode 100644 index 0000000..1237387 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/XbpmBase.py @@ -0,0 +1,136 @@ +import numpy as np +from ophyd import Component, Device, EpicsSignal, EpicsSignalRO + + +class XbpmCsaxsOp(Device): + """Python wrapper for custom XBPMs in the cSAXS optics hutch + + This is completely custom XBPM with templates directly in the + VME repo. Thus it needs a custom ophyd template as well... + + WARN: The x and y are not updated by the IOC + """ + + sum = Component(EpicsSignalRO, "SUM", auto_monitor=True) + x = Component(EpicsSignalRO, "POSH", auto_monitor=True) + y = Component(EpicsSignalRO, "POSV", auto_monitor=True) + s1 = Component(EpicsSignalRO, "CHAN1", auto_monitor=True) + s2 = Component(EpicsSignalRO, "CHAN2", auto_monitor=True) + s3 = Component(EpicsSignalRO, "CHAN3", auto_monitor=True) + s4 = Component(EpicsSignalRO, "CHAN4", auto_monitor=True) + + +class XbpmBase(Device): + """Python wrapper for X-ray Beam Position Monitors + + XBPM's consist of a metal-coated diamond window that ejects + photoelectrons from the incoming X-ray beam. These electons + are collected and their current is measured. Effectively + they act as four quadrant photodiodes and are used as BPMs + at the undulator beamlines of SLS. + + Note: EPICS provided signals are read only, but the user can + change the beam position offset. + """ + + # Motor interface + s1 = Component(EpicsSignalRO, "Current1", auto_monitor=True) + s2 = Component(EpicsSignalRO, "Current2", auto_monitor=True) + s3 = Component(EpicsSignalRO, "Current3", auto_monitor=True) + s4 = Component(EpicsSignalRO, "Current4", auto_monitor=True) + sum = Component(EpicsSignalRO, "SumAll", auto_monitor=True) + asymH = Component(EpicsSignalRO, "asymH", auto_monitor=True) + asymV = Component(EpicsSignalRO, "asymV", auto_monitor=True) + x = Component(EpicsSignalRO, "X", auto_monitor=True) + y = Component(EpicsSignalRO, "Y", auto_monitor=True) + scaleH = Component(EpicsSignal, "PositionScaleX", auto_monitor=False) + offsetH = Component(EpicsSignal, "PositionOffsetX", auto_monitor=False) + scaleV = Component(EpicsSignal, "PositionScaleY", auto_monitor=False) + offsetV = Component(EpicsSignal, "PositionOffsetY", auto_monitor=False) + + +class XbpmSim(XbpmBase): + """Python wrapper for simulated X-ray Beam Position Monitors + + XBPM's consist of a metal-coated diamond window that ejects + photoelectrons from the incoming X-ray beam. These electons + are collected and their current is measured. Effectively + they act as four quadrant photodiodes and are used as BPMs + at the undulator beamlines of SLS. + + Note: EPICS provided signals are read only, but the user can + change the beam position offset. + + This simulation device extends the basic proxy with a script that + fills signals with quasi-randomized values. + """ + + # Motor interface + s1w = Component(EpicsSignal, "Current1:RAW.VAL", auto_monitor=False) + s2w = Component(EpicsSignal, "Current2:RAW.VAL", auto_monitor=False) + s3w = Component(EpicsSignal, "Current3:RAW.VAL", auto_monitor=False) + s4w = Component(EpicsSignal, "Current4:RAW.VAL", auto_monitor=False) + rangew = Component(EpicsSignal, "RANGEraw", auto_monitor=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._MX = 0 + self._MY = 0 + self._I0 = 255.0 + self._x = np.linspace(-5, 5, 64) + self._y = np.linspace(-5, 5, 64) + self._x, self._y = np.meshgrid(self._x, self._y) + + def _simFrame(self): + """Generator to simulate a jumping gaussian""" + + # define normalized 2D gaussian + def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1): + return np.exp(-((x - mx) ** 2.0 / (2.0 * sx**2.0) + (y - my) ** 2.0 / (2.0 * sy**2.0))) + + # Generator for dynamic values + self._MX = 0.75 * self._MX + 0.25 * (10.0 * np.random.random() - 5.0) + self._MY = 0.75 * self._MY + 0.25 * (10.0 * np.random.random() - 5.0) + self._I0 = 0.75 * self._I0 + 0.25 * (255.0 * np.random.random()) + + arr = self._I0 * gaus2d(self._x, self._y, self._MX, self._MY) + return arr + + def sim(self): + # Get next frame + beam = self._simFrame() + total = np.sum(beam) + rnge = np.floor(np.log10(total) - 0.0) + s1 = np.sum(beam[32:64, 32:64]) / 10**rnge + s2 = np.sum(beam[0:32, 32:64]) / 10**rnge + s3 = np.sum(beam[32:64, 0:32]) / 10**rnge + s4 = np.sum(beam[0:32, 0:32]) / 10**rnge + + self.s1w.set(s1).wait() + self.s2w.set(s2).wait() + self.s3w.set(s3).wait() + self.s4w.set(s4).wait() + self.rangew.set(rnge).wait() + # Print debug info + print(f"Raw signals: R={rnge}\t{s1}\t{s2}\t{s3}\t{s4}") + # plt.imshow(beam) + # plt.show(block=False) + # plt.pause(0.5) + + +# Automatically start simulation if directly invoked +if __name__ == "__main__": + xbpm1 = XbpmSim("X01DA-FE-XBPM1:", name="xbpm1") + xbpm2 = XbpmSim("X01DA-FE-XBPM2:", name="xbpm2") + + xbpm1.wait_for_connection(timeout=5) + xbpm2.wait_for_connection(timeout=5) + + xbpm1.rangew.set(1).wait() + xbpm2.rangew.set(1).wait() + + while True: + print("---") + xbpm1.sim() + xbpm2.sim() diff --git a/csaxs_bec/devices/epics/devices/__init__.py b/csaxs_bec/devices/epics/devices/__init__.py new file mode 100644 index 0000000..0c81223 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/__init__.py @@ -0,0 +1,14 @@ +# Standard ophyd classes +from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd.quadem import QuadEM +from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal + +from .delay_generator_csaxs import DelayGeneratorcSAXS +from .eiger9m_csaxs import Eiger9McSAXS + +# cSAXS +from .falcon_csaxs import FalconcSAXS +from .flomni_sample_storage import FlomniSampleStorage +from .InsertionDevice import InsertionDevice +from .mcs_csaxs import MCScSAXS +from .pilatus_csaxs import PilatuscSAXS diff --git a/csaxs_bec/devices/epics/devices/cSaxsVirtualMotors.py b/csaxs_bec/devices/epics/devices/cSaxsVirtualMotors.py new file mode 100644 index 0000000..5b11885 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/cSaxsVirtualMotors.py @@ -0,0 +1,32 @@ +""" TODO This class seems to be missing various imports and appears to have not been tested in motion yet.""" + +TABLES_DT_PUSH_DIST_MM = 890 + + +class DetectorTableTheta(PseudoPositioner): + """Detector table tilt motor + + Small wrapper to adjust the detector table tilt as angle. + The table is pushed from one side by a single vertical motor. + + Note: Rarely used! + """ + + # Real axis (in degrees) + pusher = Component(EpicsMotor, "", name="pusher") + # Virtual axis + theta = Component(PseudoSingle, name="theta") + + _real = ["pusher"] + + @pseudo_position_argument + def forward(self, pseudo_pos): + return self.RealPosition( + pusher=tan(pseudo_pos.theta * 3.141592 / 180.0) * TABLES_DT_PUSH_DIST_MM + ) + + @real_position_argument + def inverse(self, real_pos): + return self.PseudoPosition( + theta=-180 * atan(real_pos.pusher / TABLES_DT_PUSH_DIST_MM) / 3.141592 + ) diff --git a/csaxs_bec/devices/epics/devices/delay_generator_csaxs.py b/csaxs_bec/devices/epics/devices/delay_generator_csaxs.py new file mode 100644 index 0000000..3b0dee2 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/delay_generator_csaxs.py @@ -0,0 +1,345 @@ +from bec_lib import bec_logger +from ophyd import Component +from ophyd_devices.epics.devices.psi_delay_generator_base import ( + DDGCustomMixin, + PSIDelayGeneratorBase, + TriggerSource, +) +from ophyd_devices.utils import bec_utils + +logger = bec_logger.logger + + +class DelayGeneratorError(Exception): + """Exception raised for errors.""" + + +class DDGSetup(DDGCustomMixin): + """ + Mixin class for DelayGenerator logic at cSAXS. + + At cSAXS, multiple DDGs were operated at the same time. There different behaviour is + implemented in the ddg_config signals that are passed via the device config. + """ + + def initialize_default_parameter(self) -> None: + """Method to initialize default parameters.""" + for ii, channel in enumerate(self.parent.all_channels): + self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel]) + + self.parent.set_channels("amplitude", self.parent.amplitude.get()) + self.parent.set_channels("offset", self.parent.offset.get()) + # Setup reference + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # Set threshold level for ext. pulses + self.parent.level.put(self.parent.thres_trig_level.get()) + + def prepare_ddg(self) -> None: + """ + Method to prepare scan logic of cSAXS + + Two scantypes are supported: "step" and "fly": + - step: Scan is performed by stepping the motor and acquiring data at each step + - fly: Scan is performed by moving the motor with a constant velocity and acquiring data + + Custom logic for different DDG behaviour during scans. + + - set_high_on_exposure : If True, then TTL signal is high during + the full exposure time of the scan (all frames). + E.g. Keep shutter open for the full scan. + - fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel. + If the value is 0, then the width of the TTL pulse is determined, + no matter which parameters are passed from the scaninfo for exposure time + - set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones + were: SINGLE_SHOT, EXT_RISING_EDGE + """ + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # scantype "step" + if self.parent.scaninfo.scan_type == "step": + # High on exposure means that the signal + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.frames_per_trigger + * (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + # scantype "fly" + elif self.parent.scaninfo.scan_type == "fly": + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + + self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.num_points + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + + else: + raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}") + # Set common DDG parameters + self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.parent.set_channels("delay", 0.0) + + def on_trigger(self) -> None: + """Method to be executed upon trigger""" + if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT: + self.parent.trigger_shot.put(1) + + def check_scan_id(self) -> None: + """ + Method to check if scan_id has changed. + + If yes, then it changes parent.stopped to True, which will stop further actions. + """ + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + def finished(self) -> None: + """Method checks if DDG finished acquisition""" + + def on_pre_scan(self) -> None: + """ + Method called by pre_scan hook in parent class. + + Executes trigger if premove_trigger is Trus. + """ + if self.parent.premove_trigger.get() is True: + self.parent.trigger_shot.put(1) + + +class DelayGeneratorcSAXS(PSIDelayGeneratorBase): + """ + DG645 delay generator at cSAXS (multiple can be in use depending on the setup) + + Default values for setting up DDG. + Note: checks of set calues are not (only partially) included, check manual for details on possible settings. + https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf + + - delay_burst : (float >=0) Delay between trigger and first pulse in burst mode + - delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition + - additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line) + - polarity : (list of 0/1) polarity for different channels + - amplitude : (float) amplitude voltage of TTLs + - offset : (float) offset for ampltitude + - thres_trig_level : (float) threshold of trigger amplitude + + Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg): + + - set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan. + # TODO trigger_width and fixed_ttl could be combined into single list. + - fixed_ttl_width : (list of either 1 or 0), one for each channel. + - trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value. + - set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG. + - premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan). + - set_high_on_stage : (bool) if True, then TTL signal should go high already on stage. + """ + + custom_prepare_cls = DDGSetup + + delay_burst = Component( + bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_config" + ) + + delta_width = Component( + bec_utils.ConfigSignal, name="delta_width", kind="config", config_storage_name="ddg_config" + ) + + additional_triggers = Component( + bec_utils.ConfigSignal, + name="additional_triggers", + kind="config", + config_storage_name="ddg_config", + ) + + polarity = Component( + bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config" + ) + + fixed_ttl_width = Component( + bec_utils.ConfigSignal, + name="fixed_ttl_width", + kind="config", + config_storage_name="ddg_config", + ) + + amplitude = Component( + bec_utils.ConfigSignal, name="amplitude", kind="config", config_storage_name="ddg_config" + ) + + offset = Component( + bec_utils.ConfigSignal, name="offset", kind="config", config_storage_name="ddg_config" + ) + + thres_trig_level = Component( + bec_utils.ConfigSignal, + name="thres_trig_level", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_exposure = Component( + bec_utils.ConfigSignal, + name="set_high_on_exposure", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_stage = Component( + bec_utils.ConfigSignal, + name="set_high_on_stage", + kind="config", + config_storage_name="ddg_config", + ) + + set_trigger_source = Component( + bec_utils.ConfigSignal, + name="set_trigger_source", + kind="config", + config_storage_name="ddg_config", + ) + + trigger_width = Component( + bec_utils.ConfigSignal, + name="trigger_width", + kind="config", + config_storage_name="ddg_config", + ) + premove_trigger = Component( + bec_utils.ConfigSignal, + name="premove_trigger", + kind="config", + config_storage_name="ddg_config", + ) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + sim_mode=False, + ddg_config=None, + **kwargs, + ): + """ + Args: + prefix (str, optional): Prefix of the device. Defaults to "". + name (str): Name of the device. + kind (str, optional): Kind of the device. Defaults to None. + read_attrs (list, optional): List of attributes to read. Defaults to None. + configuration_attrs (list, optional): List of attributes to configure. Defaults to None. + parent (Device, optional): Parent device. Defaults to None. + device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None. + sim_mode (bool, optional): Simulation mode flag. Defaults to False. + ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None. + + """ + # Default values for ddg_config signals + self.ddg_config = { + # Setup default values + f"{name}_delay_burst": 0, + f"{name}_delta_width": 0, + f"{name}_additional_triggers": 0, + f"{name}_polarity": [1, 1, 1, 1, 1], + f"{name}_amplitude": 4.5, + f"{name}_offset": 0, + f"{name}_thres_trig_level": 2.5, + # Values for different behaviour during scans + f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0], + f"{name}_trigger_width": None, + f"{name}_set_high_on_exposure": False, + f"{name}_set_high_on_stage": False, + f"{name}_set_trigger_source": "SINGLE_SHOT", + f"{name}_premove_trigger": False, + } + if ddg_config is not None: + # pylint: disable=expression-not-assigned + [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + sim_mode=sim_mode, + **kwargs, + ) + + +if __name__ == "__main__": + # Start delay generator in simulation mode. + # Note: To run, access to Epics must be available. + dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True) diff --git a/csaxs_bec/devices/epics/devices/eiger9m_csaxs.py b/csaxs_bec/devices/epics/devices/eiger9m_csaxs.py new file mode 100644 index 0000000..bee199c --- /dev/null +++ b/csaxs_bec/devices/epics/devices/eiger9m_csaxs.py @@ -0,0 +1,427 @@ +import enum +import os +import threading +import time +from typing import Any + +import numpy as np +from bec_lib import messages, threadlocked +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger +from ophyd import ADComponent as ADCpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase +from std_daq_client import StdDaqClient + +logger = bec_logger.logger + + +class EigerError(Exception): + """Base class for exceptions in this module.""" + + +class EigerTimeoutError(EigerError): + """Raised when the Eiger does not respond in time.""" + + +class Eiger9MSetup(CustomDetectorMixin): + """Eiger setup class + + Parent class: CustomDetectorMixin + + """ + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.std_rest_server_url = ( + kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000" + ) + self.std_client = None + self._lock = threading.RLock() + + def initialize_default_parameter(self) -> None: + """Set default parameters for Eiger9M detector""" + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize detector""" + # Stops the detector + self.stop_detector() + # Sets the trigger source to GATING + self.parent.set_trigger(TriggerSource.GATING) + + def initialize_detector_backend(self) -> None: + """Initialize detector backend""" + + # Std client + self.std_client = StdDaqClient(url_base=self.std_rest_server_url) + + # Stop writer + self.std_client.stop_writer() + + # Change e-account + eacc = self.parent.scaninfo.username + self.update_std_cfg("writer_user_id", int(eacc.strip(" e"))) + + signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")] + if not self.wait_for_signals( + signal_conditions=signal_conditions, timeout=self.parent.timeout, all_signals=True + ): + raise EigerTimeoutError( + f"Std client not in READY state, returns: {self.std_client.get_status()}" + ) + + def update_std_cfg(self, cfg_key: str, value: Any) -> None: + """ + Update std_daq config + + Checks that the new value matches the type of the former entry. + + Args: + cfg_key (str) : config key of value to be updated + value (Any) : value to be updated for the specified key + + Raises: + Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value + + """ + + # Load config from client and check old value + cfg = self.std_client.get_config() + old_value = cfg.get(cfg_key) + if old_value is None: + raise EigerError( + f"Tried to change entry for key {cfg_key} in std_config that does not exist" + ) + if not isinstance(value, type(old_value)): + raise EigerError( + f"Type of new value {type(value)}:{value} does not match old value" + f" {type(old_value)}:{old_value}" + ) + + # Update config with new value and send back to client + cfg.update({cfg_key: value}) + logger.debug(cfg) + self.std_client.set_config(cfg) + logger.debug(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") + + def stop_detector(self) -> None: + """Stop the detector""" + + # Stop detector + self.parent.cam.acquire.put(0) + + # Check if detector returned in idle state + signal_conditions = [ + ( + lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][ + "value" + ], + DetectorState.IDLE, + ) + ] + + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, + all_signals=False, + ): + # Retry stop detector and wait for remaining time + self.parent.cam.acquire.put(0) + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, + all_signals=False, + ): + raise EigerTimeoutError( + f"Failed to stop detector, detector state {signal_conditions[0][0]}" + ) + + def stop_detector_backend(self) -> None: + """Close file writer""" + self.std_client.stop_writer() + + def prepare_detector(self) -> None: + """Prepare detector for scan""" + self.set_detector_threshold() + self.set_acquisition_params() + self.parent.set_trigger(TriggerSource.GATING) + + def set_detector_threshold(self) -> None: + """ + Set the detector threshold + + The function sets the detector threshold automatically to 1/2 of the beam energy. + """ + + # get current beam energy from device manageer + mokev = self.parent.device_manager.devices.mokev.obj.read()[ + self.parent.device_manager.devices.mokev.name + ]["value"] + factor = 1 + + # Check if energies are eV or keV, assume keV as the default + unit = getattr(self.parent.cam.threshold_energy, "units", None) + if unit is not None and unit == "eV": + factor = 1000 + + # set energy on detector + setpoint = int(mokev * factor) + energy = self.parent.cam.beam_energy.read()[self.parent.cam.beam_energy.name]["value"] + if setpoint != energy: + self.parent.cam.beam_energy.set(setpoint) + + # set threshold on detector + threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][ + "value" + ] + if not np.isclose(setpoint / 2, threshold, rtol=0.05): + self.parent.cam.threshold_energy.set(setpoint / 2) + + def set_acquisition_params(self) -> None: + """Set acquisition parameters for the detector""" + + # Set number of images and frames (frames is for internal burst of detector) + self.parent.cam.num_images.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.cam.num_frames.put(1) + + # Update the readout time of the detector + self.update_readout_time() + + def prepare_data_backend(self) -> None: + """Prepare the data backend for the scan""" + self.parent.filepath = self.parent.filewriter.compile_full_filename( + f"{self.parent.name}.h5" + ) + self.filepath_exists(self.parent.filepath) + self.stop_detector_backend() + try: + self.std_client.start_writer_async( + { + "output_file": self.parent.filepath, + "n_images": int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ), + } + ) + except Exception as exc: + time.sleep(5) + if self.std_client.get_status()["state"] == "READY": + raise EigerTimeoutError(f"Timeout of start_writer_async with {exc}") from exc + + # Check status of std_daq + signal_conditions = [ + (lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES") + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=False, + all_signals=True, + ): + raise EigerTimeoutError( + "Timeout of 5s reached for std_daq start_writer_async with std_daq client status" + f" {self.std_client.get_status()}" + ) + + def filepath_exists(self, filepath: str) -> None: + """Check if filepath exists""" + signal_conditions = [(lambda: os.path.exists(os.path.dirname(filepath)), True)] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=False, + all_signals=True, + ): + raise EigerError(f"Timeout of 3s reached for filepath {filepath}") + + def arm_acquisition(self) -> None: + """Arm Eiger detector for acquisition""" + self.parent.cam.acquire.put(1) + signal_conditions = [ + ( + lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][ + "value" + ], + DetectorState.RUNNING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=False, + ): + raise EigerTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and stops the scan if it has""" + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + def publish_file_location(self, done: bool = False, successful: bool = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + """ + pipe = self.parent.connector.pipeline() + if successful is None: + msg = messages.FileMessage(file_path=self.parent.filepath, done=done) + else: + msg = messages.FileMessage( + file_path=self.parent.filepath, done=done, successful=successful + ) + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + @threadlocked + def finished(self): + """Check if acquisition is finished.""" + signal_conditions = [ + ( + lambda: self.parent.cam.acquire.read()[self.parent.cam.acquire.name]["value"], + DetectorState.IDLE, + ), + (lambda: self.std_client.get_status()["acquisition"]["state"], "FINISHED"), + ( + lambda: self.std_client.get_status()["acquisition"]["stats"]["n_write_completed"], + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger), + ), + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=True, + ): + raise EigerTimeoutError( + f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state" + f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for" + " the file writer" + ) + self.stop_detector() + self.stop_detector_backend() + + +class SLSDetectorCam(Device): + """ + SLS Detector Camera - Eiger9M + + Base class to map EPICS PVs to ophyd signals. + """ + + threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") + bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") + num_images = ADCpt(EpicsSignalWithRBV, "NumCycles") + num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") + trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") + trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") + acquire = ADCpt(EpicsSignal, "Acquire") + detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") + + +class TriggerSource(enum.IntEnum): + """Trigger signals for Eiger9M detector""" + + AUTO = 0 + TRIGGER = 1 + GATING = 2 + BURST_TRIGGER = 3 + + +class DetectorState(enum.IntEnum): + """Detector states for Eiger9M detector""" + + IDLE = 0 + ERROR = 1 + WAITING = 2 + FINISHED = 3 + TRANSMITTING = 4 + RUNNING = 5 + STOPPED = 6 + STILL_WAITING = 7 + INITIALIZING = 8 + DISCONNECTED = 9 + ABORTED = 10 + + +class Eiger9McSAXS(PSIDetectorBase): + """ + Eiger9M detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + Various EpicsPVs for controlling the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = Eiger9MSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + # specify class attributes + cam = ADCpt(SLSDetectorCam, "cam1:") + + def set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector. + Check the TriggerSource enum for possible values + + Args: + trigger_source (TriggerSource): Trigger source for the detector + + """ + value = trigger_source + self.cam.trigger_mode.put(value) + + def stage(self) -> list[object]: + """ + Add functionality to stage, and arm the detector + + Additional call to: + - custom_prepare.arm_acquisition() + """ + rtr = super().stage() + self.custom_prepare.arm_acquisition() + return rtr + + +if __name__ == "__main__": + eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) diff --git a/csaxs_bec/devices/epics/devices/falcon_csaxs.py b/csaxs_bec/devices/epics/devices/falcon_csaxs.py new file mode 100644 index 0000000..170d2cf --- /dev/null +++ b/csaxs_bec/devices/epics/devices/falcon_csaxs.py @@ -0,0 +1,356 @@ +import enum +import os + +from bec_lib import messages +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd.mca import EpicsMCARecord +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase + +logger = bec_logger.logger + + +class FalconError(Exception): + """Base class for exceptions in this module.""" + + +class FalconTimeoutError(FalconError): + """Raised when the Falcon does not respond in time.""" + + +class DetectorState(enum.IntEnum): + """Detector states for Falcon detector""" + + DONE = 0 + ACQUIRING = 1 + + +class TriggerSource(enum.IntEnum): + """Trigger source for Falcon detector""" + + USER = 0 + GATE = 1 + SYNC = 2 + + +class MappingSource(enum.IntEnum): + """Mapping source for Falcon detector""" + + SPECTRUM = 0 + MAPPING = 1 + + +class EpicsDXPFalcon(Device): + """ + DXP parameters for Falcon detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ + + elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime") + elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime") + elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime") + + # Energy Filter PVs + energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold") + min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation") + detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True) + scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor") + risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization") + + # Misc PVs + detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity") + decay_time = Cpt(EpicsSignalWithRBV, "DecayTime") + + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + + +class FalconHDF5Plugins(Device): + """ + HDF5 parameters for Falcon detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ + + capture = Cpt(EpicsSignalWithRBV, "Capture") + enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config") + xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config") + lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'") + temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True) + file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config") + file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config") + file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config") + num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config") + file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config") + queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config") + array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") + + +class FalconSetup(CustomDetectorMixin): + """ + Falcon setup class for cSAXS + + Parent class: CustomDetectorMixin + + """ + + def initialize_default_parameter(self) -> None: + """ + Set default parameters for Falcon + + This will set: + - readout (float): readout time in seconds + - value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro + + """ + self.parent.value_pixel_per_buffer = 20 + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize Falcon detector""" + self.stop_detector() + self.stop_detector_backend() + self.parent.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + # 1 Realtime + self.parent.preset_mode.put(1) + # 0 Normal, 1 Inverted + self.parent.input_logic_polarity.put(0) + # 0 Manual 1 Auto + self.parent.auto_pixels_per_buffer.put(0) + # Sets the number of pixels/spectra in the buffer + self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer) + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + + signal_conditions = [ + (lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE) + ] + + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + all_signals=False, + ): + # Retry stop detector and wait for remaining time + raise FalconTimeoutError( + f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + ) + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + self.parent.hdf5.capture.put(0) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for Falcon.""" + self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + self.parent.hdf5.lazy_open.put(1) + self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + self.parent.nd_array_mode.put(1) + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.parent.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + self.parent.filepath = self.parent.filewriter.compile_full_filename( + f"{self.parent.name}.h5" + ) + file_path, file_name = os.path.split(self.parent.filepath) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put("%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=False, + ): + raise FalconTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and stops the scan if it has""" + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + def publish_file_location(self, done: bool = False, successful: bool = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + """ + pipe = self.parent.connector.pipeline() + if successful is None: + msg = messages.FileMessage(file_path=self.parent.filepath, done=done) + else: + msg = messages.FileMessage( + file_path=self.parent.filepath, done=done, successful=successful + ) + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + def finished(self) -> None: + """Check if scan finished succesfully""" + total_frames = int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ) + signal_conditions = [ + (self.parent.dxp.current_pixel.get, total_frames), + (self.parent.hdf5.array_counter.get, total_frames), + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()}," + f" send data {self.parent.hdf5.array_counter.get()} from total_frames" + f" {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + +class FalconcSAXS(PSIDetectorBase): + """ + Falcon Sitoro detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector + mca (EpicsMCARecord) : MCA parameters for Falcon detector + hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = FalconSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + + # specify class attributes + dxp = Cpt(EpicsDXPFalcon, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") + + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") + preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers + preset_real = Cpt(EpicsSignal, "PresetReal") + preset_events = Cpt(EpicsSignal, "PresetEvents") + preset_triggers = Cpt(EpicsSignal, "PresetTriggers") + triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True) + events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True) + input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True) + output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True) + collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping + pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode") + ignore_gate = Cpt(EpicsSignal, "IgnoreGate") + input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity") + auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer") + pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer") + pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") + nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") + + def set_trigger( + self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 + ) -> None: + """ + Set triggering mode for detector + + Args: + mapping_mode (MappingSource): Mapping mode for the detector + trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal + ignore_gate (int): Ignore gate from TTL signal; defaults to 0 + + """ + mapping = int(mapping_mode) + trigger = trigger_source + self.collect_mode.put(mapping) + self.pixel_advance_mode.put(trigger) + self.ignore_gate.put(ignore_gate) + + def stage(self) -> list[object]: + """Stage""" + rtr = super().stage() + self.custom_prepare.arm_acquisition() + return rtr + + +if __name__ == "__main__": + falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) diff --git a/csaxs_bec/devices/epics/devices/flomni_sample_storage.py b/csaxs_bec/devices/epics/devices/flomni_sample_storage.py new file mode 100644 index 0000000..bf154fa --- /dev/null +++ b/csaxs_bec/devices/epics/devices/flomni_sample_storage.py @@ -0,0 +1,116 @@ +import time + +from ophyd import Component as Cpt +from ophyd import Device +from ophyd import DynamicDeviceComponent as Dcpt +from ophyd import EpicsSignal +from prettytable import PrettyTable + + +class FlomniSampleStorageError(Exception): + pass + + +class FlomniSampleStorage(Device): + USER_ACCESS = [ + "is_sample_slot_used", + "is_sample_in_gripper", + "set_sample_slot", + "unset_sample_slot", + "set_sample_in_gripper", + "unset_sample_in_gripper", + "show_all", + ] + SUB_VALUE = "value" + _default_sub = SUB_VALUE + sample_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21) + } + sample_placed = Dcpt(sample_placed) + + sample_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True}) + for i in range(21) + } + sample_names = Dcpt(sample_names) + + sample_in_gripper = Cpt( + EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET" + ) + sample_in_gripper_name = Cpt( + EpicsSignal, + name="sample_in_gripper_name", + read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC", + string=True, + ) + + def __init__(self, prefix="", *, name, **kwargs): + super().__init__(prefix, name=name, **kwargs) + self.sample_placed.sample1.subscribe(self._emit_value) + + def _emit_value(self, **kwargs): + timestamp = kwargs.pop("timestamp", time.time()) + self.wait_for_connection() + self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self) + + def set_sample_slot(self, slot_nr: int, name: str) -> bool: + if slot_nr > 20: + raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.") + + getattr(self.sample_placed, f"sample{slot_nr}").set(1) + getattr(self.sample_names, f"sample{slot_nr}").set(name) + + def unset_sample_slot(self, slot_nr: int) -> bool: + if slot_nr > 20: + raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.") + + getattr(self.sample_placed, f"sample{slot_nr}").set(0) + getattr(self.sample_names, f"sample{slot_nr}").set("-") + + def set_sample_in_gripper(self, name: str) -> bool: + self.sample_in_gripper.set(1) + self.sample_in_gripper_name.set(name) + + def unset_sample_in_gripper(self) -> bool: + self.sample_in_gripper.set(0) + self.sample_in_gripper_name.set("-") + + def is_sample_slot_used(self, slot_nr: int) -> bool: + val = getattr(self.sample_placed, f"sample{slot_nr}").get() + return bool(val) + + def is_sample_in_gripper(self) -> bool: + val = self.sample_in_gripper.get() + return bool(val) + + def get_sample_name(self, slot_nr) -> str: + val = getattr(self.sample_names, f"sample{slot_nr}").get() + return str(val) + + def show_all(self): + t = PrettyTable() + t.title = "flOMNI sample storage" + field_names = [""] + field_names.extend(str(ax) for ax in range(1, 11)) + for ct in range(0, 2): + t.field_names = field_names + row = ["Container " + str(ct)] + row.extend( + "used" if self.is_sample_slot_used(slot_nr) else "free" + for slot_nr in range((ct * 10) + 1, (ct * 10) + 11) + ) + t.add_row(row) + print(t) + print("\n\nFollowing samples are currently loaded:\n") + for ct in range(1, 21): + if self.is_sample_slot_used(ct): + print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}") + if self.sample_in_gripper.get(): + print(f"\n Gripper: {self.sample_in_gripper_name.get()}\n") + else: + print(f"\n Gripper: no sample\n") + + if self.is_sample_slot_used(0): + print(f" flOMNI stage: {self.get_sample_name(0)}\n") + else: + print(f" flOMNI stage: no sample\n") diff --git a/csaxs_bec/devices/epics/devices/mcs_csaxs.py b/csaxs_bec/devices/epics/devices/mcs_csaxs.py new file mode 100644 index 0000000..e0fef5e --- /dev/null +++ b/csaxs_bec/devices/epics/devices/mcs_csaxs.py @@ -0,0 +1,310 @@ +import enum +import threading +from collections import defaultdict + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages, threadlocked +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase +from ophyd_devices.utils import bec_utils + +logger = bec_logger.logger + + +class MCSError(Exception): + """Base class for exceptions in this module.""" + + +class MCSTimeoutError(MCSError): + """Raise when MCS card runs into a timeout""" + + +class TriggerSource(int, enum.Enum): + """Trigger source for mcs card - see manual for more information""" + + MODE0 = 0 + MODE1 = 1 + MODE2 = 2 + MODE3 = 3 + MODE4 = 4 + MODE5 = 5 + MODE6 = 6 + + +class ChannelAdvance(int, enum.Enum): + """Channel advance pixel mode for mcs card - see manual for more information""" + + INTERNAL = 0 + EXTERNAL = 1 + + +class ReadoutMode(int, enum.Enum): + """Readout mode for mcs card - see manual for more information""" + + PASSIVE = 0 + EVENT = 1 + IO_INTR = 2 + FREQ_0_1HZ = 3 + FREQ_0_2HZ = 4 + FREQ_0_5HZ = 5 + FREQ_1HZ = 6 + FREQ_2HZ = 7 + FREQ_5HZ = 8 + FREQ_10HZ = 9 + FREQ_100HZ = 10 + + +class MCSSetup(CustomDetectorMixin): + """Setup mixin class for the MCS card""" + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._lock = threading.RLock() + self._stream_ttl = 1800 + self.acquisition_done = False + self.counter = 0 + self.n_points = 0 + self.mca_names = [ + signal for signal in self.parent.component_names if signal.startswith("mca") + ] + self.mca_data = defaultdict(lambda: []) + + def initialize_detector(self) -> None: + """Initialize detector""" + # External trigger for pixel advance + self.parent.channel_advance.set(ChannelAdvance.EXTERNAL) + # Use internal clock for channel 1 + self.parent.channel1_source.set(ChannelAdvance.INTERNAL) + self.parent.user_led.set(0) + # Set number of channels to 5 + self.parent.mux_output.set(5) + # Trigger Mode used for cSAXS + self.parent.set_trigger(TriggerSource.MODE3) + # specify polarity of trigger signals + self.parent.input_polarity.set(0) + self.parent.output_polarity.set(1) + # do not start counting on start + self.parent.count_on_start.set(0) + self.stop_detector() + + def initialize_detector_backend(self) -> None: + """Initialize detector backend""" + for mca in self.mca_names: + signal = getattr(self.parent, mca) + signal.subscribe(self._on_mca_data, run=False) + self.parent.current_channel.subscribe(self._progress_update, run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + num_lines = self.parent.num_lines.get() + max_value = self.parent.scaninfo.num_points + # self.counter seems to be a deprecated variable from a former implementation of the mcs card + # pylint: disable=protected-access + self.parent._run_subs( + sub_type=self.parent.SUB_PROGRESS, + value=self.counter * int(self.parent.scaninfo.num_points / num_lines) + value, + max_value=max_value, + # TODO check if that is correct with + done=bool(max_value == value), # == self.counter), + ) + + @threadlocked + def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None: + """Callback function for scan progress""" + if not isinstance(value, (list, np.ndarray)): + return + self.mca_data[obj.attr_name] = value + if len(self.mca_names) != len(self.mca_data): + return + self.acquisition_done = True + self._send_data_to_bec() + self.mca_data = defaultdict(lambda: []) + + def _send_data_to_bec(self) -> None: + """Sends bundled data to BEC""" + if self.parent.scaninfo.scan_msg is None: + return + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.num_lines.get()}) + msg = messages.DeviceMessage( + signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg={"data": msg}, + expire=self._stream_ttl, + ) + + def prepare_detector(self) -> None: + """Prepare detector for scan""" + self.set_acquisition_params() + self.parent.set_trigger(TriggerSource.MODE3) + + def set_acquisition_params(self) -> None: + """Set acquisition parameters for scan""" + if self.parent.scaninfo.scan_type == "step": + self.n_points = int(self.parent.scaninfo.frames_per_trigger) * int( + self.parent.scaninfo.num_points + ) + elif self.parent.scaninfo.scan_type == "fly": + self.n_points = int(self.parent.scaninfo.num_points) # / int(self.num_lines.get())) + else: + raise MCSError(f"Scantype {self.parent.scaninfo} not implemented for MCS card") + if self.n_points > 10000: + raise MCSError( + f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card" + " 10000 (N-1)" + ) + self.parent.num_use_all.set(self.n_points) + self.parent.preset_real.set(0) + + def prepare_detector_backend(self) -> None: + """Prepare detector backend for scan""" + self.parent.erase_all.set(1) + self.parent.read_mode.set(ReadoutMode.EVENT) + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.counter = 0 + self.parent.erase_start.set(1) + + def finished(self) -> None: + """Check if acquisition is finished, if not successful, rais MCSTimeoutError""" + signal_conditions = [ + (lambda: self.acquisition_done, True), + (self.parent.acquiring.get, 0), # Considering making a enum.Int class for this state + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=True, + ): + total_frames = self.counter * int( + self.parent.scaninfo.num_points / self.parent.num_lines.get() + ) + max(self.parent.current_channel.get(), 0) + raise MCSTimeoutError( + f"Reached timeout with mcs in state {self.parent.acquiring.get()} and" + f" {total_frames} frames arriving at the mcs card" + ) + + def stop_detector(self) -> None: + """Stop detector""" + self.parent.stop_all.set(1) + + return super().stop_detector() + + def stop_detector_backend(self) -> None: + """Stop acquisition of data""" + self.acquisition_done = True + + +class SIS38XX(Device): + """SIS38XX card for access to EPICs PVs at cSAXS beamline""" + + +class MCScSAXS(PSIDetectorBase): + """MCS card for cSAXS for implementation at cSAXS beamline""" + + USER_ACCESS = ["describe", "_init_mcs"] + SUB_PROGRESS = "progress" + SUB_VALUE = "value" + _default_sub = SUB_VALUE + + # specify Setup class + custom_prepare_cls = MCSSetup + # specify minimum readout time for detector + MIN_READOUT = 0 + + # PV access to SISS38XX card + # Acquisition + erase_all = Cpt(EpicsSignal, "EraseAll") + erase_start = Cpt(EpicsSignal, "EraseStart") # ,trigger_value=1 + start_all = Cpt(EpicsSignal, "StartAll") + stop_all = Cpt(EpicsSignal, "StopAll") + acquiring = Cpt(EpicsSignal, "Acquiring") + preset_real = Cpt(EpicsSignal, "PresetReal") + elapsed_real = Cpt(EpicsSignal, "ElapsedReal") + read_mode = Cpt(EpicsSignal, "ReadAll.SCAN") + read_all = Cpt(EpicsSignal, "DoReadAll.VAL") # ,trigger_value=1 + num_use_all = Cpt(EpicsSignal, "NuseAll") + current_channel = Cpt(EpicsSignal, "CurrentChannel") + dwell = Cpt(EpicsSignal, "Dwell") + channel_advance = Cpt(EpicsSignal, "ChannelAdvance") + count_on_start = Cpt(EpicsSignal, "CountOnStart") + software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance") + channel1_source = Cpt(EpicsSignal, "Channel1Source") + prescale = Cpt(EpicsSignal, "Prescale") + enable_client_wait = Cpt(EpicsSignal, "EnableClientWait") + client_wait = Cpt(EpicsSignal, "ClientWait") + acquire_mode = Cpt(EpicsSignal, "AcquireMode") + mux_output = Cpt(EpicsSignal, "MUXOutput") + user_led = Cpt(EpicsSignal, "UserLED") + input_mode = Cpt(EpicsSignal, "InputMode") + input_polarity = Cpt(EpicsSignal, "InputPolarity") + output_mode = Cpt(EpicsSignal, "OutputMode") + output_polarity = Cpt(EpicsSignal, "OutputPolarity") + model = Cpt(EpicsSignalRO, "Model", string=True) + firmware = Cpt(EpicsSignalRO, "Firmware") + max_channels = Cpt(EpicsSignalRO, "MaxChannels") + + # PV access to MCA signals + mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True) + mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True) + mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True) + current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True) + + # Custom signal readout from device config + num_lines = Cpt( + bec_utils.ConfigSignal, name="num_lines", kind="config", config_storage_name="mcs_config" + ) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + sim_mode=False, + mcs_config=None, + **kwargs, + ): + self.mcs_config = {f"{name}_num_lines": 1} + if mcs_config is not None: + # pylint: disable=expression-not-assigned + [self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()] + + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + sim_mode=sim_mode, + **kwargs, + ) + + def set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger mode from TriggerSource""" + value = int(trigger_source) + self.input_mode.set(value) + + def stage(self) -> list[object]: + """stage the detector for upcoming acquisition""" + rtr = super().stage() + self.custom_prepare.arm_acquisition() + return rtr + + +# Automatically connect to test environmenr if directly invoked +if __name__ == "__main__": + mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True) diff --git a/csaxs_bec/devices/epics/devices/omny_sample_storage.py b/csaxs_bec/devices/epics/devices/omny_sample_storage.py new file mode 100644 index 0000000..dcd5714 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/omny_sample_storage.py @@ -0,0 +1,267 @@ +import time + +from ophyd import Component as Cpt +from ophyd import Device +from ophyd import DynamicDeviceComponent as Dcpt +from ophyd import EpicsSignal +from prettytable import FRAME, PrettyTable + + +class OMNYSampleStorageError(Exception): + pass + + +class OMNYSampleStorage(Device): + USER_ACCESS = [ + "is_sample_slot_used", + "is_sample_in_gripper", + "set_sample_slot", + "unset_sample_slot", + "set_sample_in_gripper", + "unset_sample_in_gripper", + "set_sample_in_samplestage", + "unset_sample_in_samplestage", + "get_sample_name_in_samplestage", + "get_sample_name", + "is_sample_in_samplestage", + "set_shuttle_slot", + "unset_shuttle_slot", + "get_shuttle_name_slot", + "is_shuttle_slot_used", + "show_all", + ] + SUB_VALUE = "value" + _default_sub = SUB_VALUE + + sample_shuttle_A_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}", {}) for i in range(1, 7) + } + sample_shuttle_A_placed = Dcpt(sample_shuttle_A_placed) + + sample_shuttle_B_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}", {}) for i in range(1, 7) + } + sample_shuttle_B_placed = Dcpt(sample_shuttle_B_placed) + + sample_shuttle_C_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7) + } + sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed) + + sample_shuttle_C_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7) + } + sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed) + + parking_placed = { + f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}", {}) for i in range(1, 7) + } + parking_placed = Dcpt(parking_placed) + + sample_placed = { + f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {}) + for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101] + } + sample_placed = Dcpt(sample_placed) + + sample_shuttle_A_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}.DESC", {"string": True}) + for i in range(1, 7) + } + sample_shuttle_A_names = Dcpt(sample_shuttle_A_names) + + sample_shuttle_B_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}.DESC", {"string": True}) + for i in range(1, 7) + } + sample_shuttle_B_names = Dcpt(sample_shuttle_B_names) + + sample_shuttle_C_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}.DESC", {"string": True}) + for i in range(1, 7) + } + sample_shuttle_C_names = Dcpt(sample_shuttle_C_names) + + parking_names = { + f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}.DESC", {"string": True}) + for i in range(1, 7) + } + parking_names = Dcpt(parking_names) + + sample_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}.DESC", {"string": True}) + for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101] + } + sample_names = Dcpt(sample_names) + + sample_in_gripper = Cpt( + EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_omny:110.VAL" + ) + sample_in_gripper_name = Cpt( + EpicsSignal, + name="sample_in_gripper_name", + read_pv="XOMNY-SAMPLE_DB_omny:110.DESC", + string=True, + ) + + sample_in_samplestage = Cpt( + EpicsSignal, name="sample_in_samplestage", read_pv="XOMNY-SAMPLE_DB_omny:0.VAL" + ) + sample_in_samplestage_name = Cpt( + EpicsSignal, + name="sample_in_samplestage_name", + read_pv="XOMNY-SAMPLE_DB_omny:0.DESC", + string=True, + ) + + def __init__(self, prefix="", *, name, **kwargs): + super().__init__(prefix, name=name, **kwargs) + self.sample_shuttle_A_placed.sample1.subscribe(self._emit_value) + + def _emit_value(self, **kwargs): + timestamp = kwargs.pop("timestamp", time.time()) + self.wait_for_connection() + self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self) + + def set_sample_slot(self, container: str, slot_nr: int, name: str) -> bool: + if slot_nr > 20: + raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.") + + if container == "A": + getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(1) + getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set(name) + elif container == "B": + getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(1) + getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set(name) + elif container == "C": + getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(1) + getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set(name) + + def unset_sample_slot(self, shuttle: str, slot_nr: int) -> bool: + if slot_nr > 20: + raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.") + + if shuttle == "A": + getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(0) + getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set("-") + if shuttle == "B": + getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(0) + getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set("-") + if shuttle == "C": + getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(0) + getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set("-") + + def set_shuttle_slot(self, container: str, slot_nr: int) -> bool: + if slot_nr > 6: + raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.") + getattr(self.parking_placed, f"parking{slot_nr}").set(1) + getattr(self.parking_names, f"parking{slot_nr}").set(container) + + def unset_shuttle_slot(self, slot_nr: int) -> bool: + if slot_nr > 6: + raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.") + getattr(self.parking_placed, f"parking{slot_nr}").set(0) + getattr(self.parking_names, f"parking{slot_nr}").set("none") + + def set_sample_in_gripper(self, name: str) -> bool: + self.sample_in_gripper.set(1) + self.sample_in_gripper_name.set(name) + + def unset_sample_in_gripper(self) -> bool: + self.sample_in_gripper.set(0) + self.sample_in_gripper_name.set("-") + + def set_sample_in_samplestage(self, name: str) -> bool: + self.sample_in_samplestage.set(1) + self.sample_in_samplestage_name.set(name) + + def unset_sample_in_samplestage(self) -> bool: + self.sample_in_samplestage.set(0) + self.sample_in_samplestage_name.set("-") + + def is_sample_slot_used(self, container, slot_nr: int) -> bool: + if container == "A": + val = getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").get() + if container == "B": + val = getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").get() + if container == "C": + val = getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").get() + elif container == "O": + val = getattr(self.sample_placed, f"sample{slot_nr}").get() + return bool(val) + + def is_shuttle_slot_used(self, slot_nr: int) -> bool: + val = getattr(self.parking_placed, f"parking{slot_nr}").get() + return bool(val) + + def is_sample_in_gripper(self) -> bool: + val = self.sample_in_gripper.get() + return bool(val) + + def is_sample_in_samplestage(self) -> bool: + val = self.sample_in_samplestage.get() + return bool(val) + + def get_sample_name(self, container, slot_nr) -> str: + if container == "A": + val = getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").get() + elif container == "B": + val = getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").get() + elif container == "C": + val = getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").get() + elif container == "O": + val = getattr(self.sample_names, f"sample{slot_nr}").get() + else: + val = "unknown container" + return str(val) + + def get_shuttle_name_slot(self, slot_nr: int) -> str: + val = getattr(self.parking_names, f"parking{slot_nr}").get() + return str(val) + + def get_sample_name_in_gripper(self) -> str: + val = self.sample_in_gripper_name.get() + return str(val) + + def get_sample_name_in_samplestage(self) -> str: + val = self.sample_in_samplestage_name.get() + return str(val) + + def show_all(self): + t = PrettyTable() + for ch in ["A", "B", "C"]: + t.clear() + t.title = "Shuttle " + ch + field_names = [""] + for ax in [1, 3, 5]: + row = [] + row.extend([self.get_sample_name(ch, ax)]) + row.extend(str(ax)) + row.extend(str(ax + 1)) + row.extend([self.get_sample_name(ch, ax + 1)]) + t.add_row(row) + t.header = False + t.vrules = FRAME + print(t) + + if self.is_sample_in_samplestage(): + print(f"\n\n Sample stage: {self.get_sample_name_in_samplestage()}") + else: + print(f"\n\n Sample stage: no sample") + + if self.is_sample_in_gripper(): + print(f"\n Gripper: {self.get_sample_name_in_gripper()}\n") + else: + print(f"\n Gripper: no sample\n") + + t.clear() + t.title = "Fixed positions in OMNY" + for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]: + row = [] + row.extend([f"Position {i:3d}"]) + if self.is_sample_slot_used("O", i): + row.extend(self.get_sample_name("O", i)) + else: + row.extend(["free"]) + t.add_row(row) + print(t) diff --git a/csaxs_bec/devices/epics/devices/pilatus_csaxs.py b/csaxs_bec/devices/epics/devices/pilatus_csaxs.py new file mode 100644 index 0000000..4c55442 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/pilatus_csaxs.py @@ -0,0 +1,416 @@ +import enum +import json +import os +import time + +import numpy as np +import requests +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import ADComponent as ADCpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase + +logger = bec_logger.logger + +MIN_READOUT = 3e-3 + + +class PilatusError(Exception): + """Base class for exceptions in this module.""" + + +class PilatusTimeoutError(PilatusError): + """Raised when the Pilatus does not respond in time during unstage.""" + + +class TriggerSource(enum.IntEnum): + """Trigger source options for the detector""" + + INTERNAL = 0 + EXT_ENABLE = 1 + EXT_TRIGGER = 2 + MULTI_TRIGGER = 3 + ALGINMENT = 4 + + +class SLSDetectorCam(Device): + """SLS Detector Camera - Pilatus + + Base class to map EPICS PVs to ophyd signals. + """ + + num_images = ADCpt(EpicsSignalWithRBV, "NumImages") + num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures") + delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") + trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode") + acquire = ADCpt(EpicsSignal, "Acquire") + armed = ADCpt(EpicsSignalRO, "Armed") + + read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot") + detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV") + status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True) + acquire_time = ADCpt(EpicsSignal, "AcquireTime") + acquire_period = ADCpt(EpicsSignal, "AcquirePeriod") + threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + file_path = ADCpt(EpicsSignalWithRBV, "FilePath") + file_name = ADCpt(EpicsSignalWithRBV, "FileName") + file_number = ADCpt(EpicsSignalWithRBV, "FileNumber") + auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement") + file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate") + file_format = ADCpt(EpicsSignalWithRBV, "FileNumber") + gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill") + + +class PilatusSetup(CustomDetectorMixin): + """Pilatus setup class for cSAXS + + Parent class: CustomDetectorMixin + + """ + + def initialize_default_parameter(self) -> None: + """Set default parameters for Eiger9M detector""" + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize detector""" + # Stops the detector + self.stop_detector() + # Sets the trigger source to GATING + self.parent.set_trigger(TriggerSource.EXT_ENABLE) + + def prepare_detector(self) -> None: + """ + Prepare detector for scan. + + Includes checking the detector threshold, + setting the acquisition parameters and setting the trigger source + """ + self.set_detector_threshold() + self.set_acquisition_params() + self.parent.set_trigger(TriggerSource.EXT_ENABLE) + + def set_detector_threshold(self) -> None: + """ + Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance + + Threshold might be in ev or keV + """ + + # get current beam energy from device manageer + mokev = self.parent.device_manager.devices.mokev.obj.read()[ + self.parent.device_manager.devices.mokev.name + ]["value"] + factor = 1 + + # Check if energies are eV or keV, assume keV as the default + unit = getattr(self.parent.cam.threshold_energy, "units", None) + if unit is not None and unit == "eV": + factor = 1000 + + # set energy on detector + setpoint = int(mokev * factor) + + # set threshold on detector + threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][ + "value" + ] + if not np.isclose(setpoint / 2, threshold, rtol=0.05): + self.parent.cam.threshold_energy.set(setpoint / 2) + + def set_acquisition_params(self) -> None: + """Set acquisition parameters for the detector""" + + # Set number of images and frames (frames is for internal burst of detector) + self.parent.cam.num_images.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.cam.num_frames.put(1) + + # Update the readout time of the detector + self.update_readout_time() + + def create_directory(self, filepath: str) -> None: + """Create directory if it does not exist""" + os.makedirs(filepath, exist_ok=True) + + def stop_detector_backend(self) -> None: + """Stop the file writer zmq service for pilatus_2""" + self.close_file_writer() + time.sleep(0.1) + self.stop_file_writer() + time.sleep(0.1) + + def close_file_writer(self) -> None: + """ + Close the file writer for pilatus_2 + + Delete the data from x12sa-pd-2 + + """ + url = "http://x12sa-pd-2:8080/stream/pilatus_2" + try: + res = self.send_requests_delete(url=url) + if not res.ok: + res.raise_for_status() + except Exception as exc: + logger.info(f"Pilatus2 close threw Exception: {exc}") + + def stop_file_writer(self) -> None: + """ + Stop the file writer for pilatus_2 + + Runs on xbl-daq-34 + """ + url = "http://xbl-daq-34:8091/pilatus_2/stop" + res = self.send_requests_put(url=url) + if not res.ok: + res.raise_for_status() + + def prepare_data_backend(self) -> None: + """ + Prepare the detector backend of pilatus for a scan + + A zmq service is running on xbl-daq-34 that is waiting + for a zmq message to start the writer for the pilatus_2 x12sa-pd-2 + + """ + + self.stop_detector_backend() + + self.parent.filepath = self.parent.filewriter.compile_full_filename("pilatus_2.h5") + self.parent.cam.file_path.put("/dev/shm/zmq/") + self.parent.cam.file_name.put( + f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}" + ) + self.parent.cam.auto_increment.put(1) # auto increment + self.parent.cam.file_number.put(0) # first iter + self.parent.cam.file_format.put(0) # 0: TIFF + self.parent.cam.file_template.put("%s%s_%5.5d.cbf") + + # TODO better to remove hard coded path with link to home directory/pilatus_2 + basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/" + self.parent.filepath_raw = os.path.join( + basepath, + self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5), + ) + # Make directory if needed + self.create_directory(self.parent.filepath_raw) + + headers = {"Content-Type": "application/json", "Accept": "application/json"} + # start the stream on x12sa-pd-2 + url = "http://x12sa-pd-2:8080/stream/pilatus_2" + data_msg = { + "source": [ + { + "searchPath": "/", + "searchPattern": "glob:*.cbf", + "destinationPath": self.parent.filepath_raw, + } + ] + } + res = self.send_requests_put(url=url, data=data_msg, headers=headers) + logger.info(f"{res.status_code} - {res.text} - {res.content}") + + if not res.ok: + res.raise_for_status() + + # start the data receiver on xbl-daq-34 + url = "http://xbl-daq-34:8091/pilatus_2/run" + data_msg = [ + "zmqWriter", + self.parent.scaninfo.username, + { + "addr": "tcp://x12sa-pd-2:8888", + "dst": ["file"], + "numFrm": int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ), + "timeout": 2000, + "ifType": "PULL", + "user": self.parent.scaninfo.username, + }, + ] + res = self.send_requests_put(url=url, data=data_msg, headers=headers) + logger.info(f"{res.status_code} - {res.text} - {res.content}") + + if not res.ok: + res.raise_for_status() + + # Wait for server to become available again + time.sleep(0.1) + logger.info(f"{res.status_code} -{res.text} - {res.content}") + + # Send requests.put to xbl-daq-34 to wait for data + url = "http://xbl-daq-34:8091/pilatus_2/wait" + data_msg = [ + "zmqWriter", + self.parent.scaninfo.username, + { + "frmCnt": int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ), + "timeout": 2000, + }, + ] + try: + res = self.send_requests_put(url=url, data=data_msg, headers=headers) + logger.info(f"{res}") + + if not res.ok: + res.raise_for_status() + except Exception as exc: + logger.info(f"Pilatus2 wait threw Exception: {exc}") + + def send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object: + """ + Send a put request to the given url + + Args: + url (str): url to send the request to + data (dict): data to be sent with the request (optional) + headers (dict): headers to be sent with the request (optional) + + Returns: + status code of the request + """ + return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5) + + def send_requests_delete(self, url: str, headers: dict = None) -> object: + """ + Send a delete request to the given url + + Args: + url (str): url to send the request to + headers (dict): headers to be sent with the request (optional) + + Returns: + status code of the request + """ + return requests.delete(url=url, headers=headers, timeout=5) + + def pre_scan(self) -> None: + """ + Pre_scan function call + + This function is called just before the scan core. + Here it is used to arm the detector for the acquisition + + """ + self.arm_acquisition() + + def arm_acquisition(self) -> None: + """Arms the detector for the acquisition""" + self.parent.cam.acquire.put(1) + # TODO is this sleep needed? to be tested with detector and for how long + time.sleep(0.5) + + def publish_file_location(self, done: bool = False, successful: bool = None) -> None: + """ + Publish the filepath to REDIS and publish the event for the h5_converter + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + """ + pipe = self.parent.connector.pipeline() + if successful is None: + msg = messages.FileMessage( + file_path=self.parent.filepath, + done=done, + metadata={"input_path": self.parent.filepath_raw}, + ) + else: + msg = messages.FileMessage( + file_path=self.parent.filepath, + done=done, + successful=successful, + metadata={"input_path": self.parent.filepath_raw}, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + def finished(self) -> None: + """Check if acquisition is finished.""" + # pylint: disable=protected-access + # TODO: at the moment this relies on device.mcs.obj._staged attribute + signal_conditions = [ + (lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=True, + ): + raise PilatusTimeoutError( + f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state" + f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for" + " the file writer" + ) + self.stop_detector() + self.stop_detector_backend() + + def stop_detector(self) -> None: + """Stop detector""" + self.parent.cam.acquire.put(0) + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and stops the scan if it has""" + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + +class PilatuscSAXS(PSIDetectorBase): + """Pilatus_2 300k detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + cam (SLSDetectorCam) : Detector camera + MIN_READOUT (float) : Minimum readout time for the detector + + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = PilatusSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + # specify class attributes + cam = ADCpt(SLSDetectorCam, "cam1:") + + def set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector""" + value = trigger_source + self.cam.trigger_mode.put(value) + + +if __name__ == "__main__": + pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True) diff --git a/csaxs_bec/devices/galil/__init__.py b/csaxs_bec/devices/galil/__init__.py new file mode 100644 index 0000000..0ad6966 --- /dev/null +++ b/csaxs_bec/devices/galil/__init__.py @@ -0,0 +1,4 @@ +from .fgalil_ophyd import FlomniGalilController, FlomniGalilMotor +from .fupr_ophyd import FuprGalilController, FuprGalilMotor +from .galil_ophyd import GalilController, GalilMotor +from .sgalil_ophyd import SGalilMotor diff --git a/csaxs_bec/devices/galil/csaxs_sgalil_triggering.drawio b/csaxs_bec/devices/galil/csaxs_sgalil_triggering.drawio new file mode 100644 index 0000000..5fa8dac --- /dev/null +++ b/csaxs_bec/devices/galil/csaxs_sgalil_triggering.drawio @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/csaxs_bec/devices/galil/csaxs_sgalil_triggering.png b/csaxs_bec/devices/galil/csaxs_sgalil_triggering.png new file mode 100644 index 0000000000000000000000000000000000000000..977c37cbe0ed03a262926855d5424704ec58a761 GIT binary patch literal 160848 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 e+ofQ{Br`P75@ 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"Hz?R<}8d1oD3e+B1D(zc$7p?0eB!31+jzI{3 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 gC8-L-(@sM1B^+1K- bool: + if axis_Id is None and axis_Id_numeric is not None: + axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) + active_thread = self.is_thread_active(0) + motor_is_on = self.is_motor_on(axis_Id) + return bool(active_thread or motor_is_on) + + def all_axes_referenced(self) -> bool: + # TODO: check if all axes are referenced in all controllers + return super().all_axes_referenced() + + def fosaz_light_curtain_is_triggered(self) -> bool: + """ + Check the light curtain status for fosaz + + Returns: + bool: True if the light curtain is triggered + """ + + return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1 + + def lights_off(self) -> None: + """ + Turn off the lights + """ + self.socket_put_confirmed("CB15") + + def lights_on(self) -> None: + """ + Turn on the lights + """ + self.socket_put_confirmed("SB15") + + +class FlomniGalilReadbackSignal(GalilSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + + current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}")) + current_pos *= self.parent.sign + step_mm = self.parent.motor_resolution.get() + return current_pos / step_mm + + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + return val + + +class FlomniGalilSetpointSignal(GalilSetpointSignal): + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + GalilError: Raised if not all axes are referenced. + + """ + target_val = val * self.parent.sign + self.setpoint = target_val + axes_referenced = self.controller.all_axes_referenced() + if axes_referenced: + while self.controller.is_thread_active(0): + time.sleep(0.1) + + self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}") + self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}") + self.controller.socket_put_confirmed("movereq=1") + self.controller.socket_put_confirmed("XQ#NEWPAR") + while self.controller.is_thread_active(0): + time.sleep(0.005) + else: + raise GalilError("Not all axes are referenced.") + + +class FlomniGalilMotorResolution(GalilMotorResolution): + pass + + +class FlomniGalilMotorIsMoving(GalilMotorIsMoving): + pass + + +class FlomniGalilAxesReferenced(GalilAxesReferenced): + pass + + +class FlomniGalilMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint") + motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config") + motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") + all_axes_referenced = Cpt( + FlomniGalilAxesReferenced, signal_name="all_axes_referenced", kind="config" + ) + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=8081, + limits=None, + sign=1, + socket_cls=SocketIO, + device_manager=None, + **kwargs, + ): + self.controller = FlomniGalilController( + socket_cls=socket_cls, socket_host=host, socket_port=port + ) + self.axis_Id = axis_Id + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.sign = sign + self.tolerance = kwargs.pop("tolerance", 0.5) + self.device_mapping = kwargs.pop("device_mapping", {}) + self.device_manager = device_manager + + if len(self.device_mapping) > 0 and self.device_manager is None: + raise BECConfigError( + "device_mapping has been specified but the device_manager cannot be accessed." + ) + self.rt = self.device_mapping.get("rt") + self.pid_x_correction = 0 + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + logger.info("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.1) + val = self.readback.read() + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + + if not success: + print(" stop") + self._done_moving(success=success) + logger.info("Move finished") + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError("Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError("Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "mm" + + def stage(self) -> list[object]: + return super().stage() + + def unstage(self) -> list[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +# if __name__ == "__main__": +# mock = False +# if not mock: +# leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1) +# leyey.stage() +# status = leyey.move(0, wait=True) +# status = leyey.move(10, wait=True) +# leyey.read() + +# leyey.get() +# leyey.describe() + +# leyey.unstage() +# else: +# from ophyd_devices.utils.socket import SocketMock + +# leyex = GalilMotor( +# "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock +# ) +# leyey = GalilMotor( +# "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock +# ) +# leyex.stage() +# # leyey.stage() + +# leyex.controller.galil_show_all() diff --git a/csaxs_bec/devices/galil/fupr_ophyd.py b/csaxs_bec/devices/galil/fupr_ophyd.py new file mode 100644 index 0000000..84f6af8 --- /dev/null +++ b/csaxs_bec/devices/galil/fupr_ophyd.py @@ -0,0 +1,318 @@ +import functools +import threading +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable + +from csaxs_bec.devices.galil.galil_ophyd import ( + BECConfigError, + GalilAxesReferenced, + GalilCommunicationError, + GalilController, + GalilError, + GalilMotorIsMoving, + GalilMotorResolution, + GalilReadbackSignal, + GalilSetpointSignal, + retry_once, +) + +logger = bec_logger.logger + + +class FuprGalilController(GalilController): + _axes_per_controller = 1 + + def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool: + if axis_Id is None and axis_Id_numeric is not None: + axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) + is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0) + return is_moving + + def axis_is_referenced(self, axis_Id) -> bool: + return self.all_axes_referenced() + + def all_axes_referenced(self) -> bool: + return bool(float(self.socket_put_and_receive("MG axisref").strip())) + + def drive_axis_to_limit(self, axis_Id_numeric, direction: str) -> None: + raise NotImplementedError("This function is not implemented for the FuprGalilController.") + + +class FuprGalilReadbackSignal(GalilReadbackSignal): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + + current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}")) + current_pos *= self.parent.sign + step_mm = self.parent.motor_resolution.get() + return current_pos / step_mm + + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + if self.parent.axis_Id_numeric == 0: + try: + rt = self.parent.device_manager.devices[self.parent.rt] + if rt.enabled: + rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]) + except KeyError: + logger.warning("Failed to set RT value during readback.") + return val + + +class FuprGalilSetpointSignal(GalilSetpointSignal): + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + GalilError: Raised if not all axes are referenced. + + """ + target_val = val * self.parent.sign + self.setpoint = target_val + axes_referenced = self.controller.all_axes_referenced() + if axes_referenced: + self.controller.socket_put_confirmed( + f"PA{self.parent.axis_Id}={int(self.setpoint*self.parent.MOTOR_RESOLUTION)}" + ) + self.controller.socket_put_confirmed(f"BG{self.parent.axis_Id}") + else: + raise GalilError("Not all axes are referenced.") + + +class FuprGalilMotorResolution(GalilMotorResolution): + @retry_once + @threadlocked + def _socket_get(self): + return self.parent.MOTOR_RESOLUTION + + +class FuprGalilMotorIsMoving(GalilMotorIsMoving): + pass + + +class FuprGalilAxesReferenced(GalilAxesReferenced): + pass + + +class FuprGalilMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + MOTOR_RESOLUTION = 25600 + readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint") + motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config") + motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") + all_axes_referenced = Cpt( + FuprGalilAxesReferenced, signal_name="all_axes_referenced", kind="config" + ) + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=8081, + limits=None, + sign=1, + socket_cls=SocketIO, + device_manager=None, + **kwargs, + ): + self.controller = FuprGalilController( + socket_cls=socket_cls, socket_host=host, socket_port=port + ) + self.axis_Id = axis_Id + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.sign = sign + self.tolerance = kwargs.pop("tolerance", 0.5) + self.device_mapping = kwargs.pop("device_mapping", {}) + self.device_manager = device_manager + + if len(self.device_mapping) > 0 and self.device_manager is None: + raise BECConfigError( + "device_mapping has been specified but the device_manager cannot be accessed." + ) + self.rt = self.device_mapping.get("rt", "rtx") + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + logger.info("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.1) + val = self.readback.read() + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + + if not success: + print(" stop") + self._done_moving(success=success) + logger.info("Move finished") + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "mm" + + def stage(self) -> list[object]: + return super().stage() + + def unstage(self) -> list[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) diff --git a/csaxs_bec/devices/galil/galil_ophyd.py b/csaxs_bec/devices/galil/galil_ophyd.py new file mode 100644 index 0000000..2d04b0a --- /dev/null +++ b/csaxs_bec/devices/galil/galil_ophyd.py @@ -0,0 +1,604 @@ +import functools +import threading +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable + +logger = bec_logger.logger + + +class GalilCommunicationError(Exception): + pass + + +class GalilError(Exception): + pass + + +class BECConfigError(Exception): + pass + + +def retry_once(fcn): + """Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (GalilCommunicationError, GalilError): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class GalilController(Controller): + _axes_per_controller = 8 + USER_ACCESS = [ + "describe", + "show_running_threads", + "galil_show_all", + "socket_put_and_receive", + "socket_put_confirmed", + "lgalil_is_air_off_and_orchestra_enabled", + "drive_axis_to_limit", + "find_reference", + "get_motor_limit_switch", + "is_motor_on", + "all_axes_referenced", + ] + + @threadlocked + def socket_put(self, val: str) -> None: + self.sock.put(f"{val}\r".encode()) + + @threadlocked + def socket_get(self) -> str: + return self.sock.receive().decode() + + @retry_once + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + if remove_trailing_chars: + return self._remove_trailing_characters(self.sock.receive().decode()) + return self.socket_get() + + @retry_once + def socket_put_confirmed(self, val: str) -> None: + """Send message to controller and ensure that it is received by checking that the socket receives a colon. + + Args: + val (str): Message that should be sent to the socket + + Raises: + GalilCommunicationError: Raised if the return value is not a colon. + + """ + return_val = self.socket_put_and_receive(val) + if return_val != ":": + raise GalilCommunicationError( + f"Expected return value of ':' but instead received {return_val}" + ) + + def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool: + if axis_Id is None and axis_Id_numeric is not None: + axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) + is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0) + backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0) + return bool( + is_moving or backlash_is_active or self.is_thread_active(0) or self.is_thread_active(2) + ) + + def is_thread_active(self, thread_id: int) -> bool: + val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}")) + if val == -1: + return False + return True + + def _remove_trailing_characters(self, var) -> str: + if len(var) > 1: + return var.split("\r\n")[0] + return var + + def stop_all_axes(self) -> str: + return self.socket_put_and_receive(f"XQ#STOP,1") + + def lgalil_is_air_off_and_orchestra_enabled(self) -> bool: + # TODO: move this to the LamNI-specific controller + rt_not_blocked_by_galil = bool(self.socket_put_and_receive(f"MG@OUT[9]")) + air_off = bool(self.socket_put_and_receive(f"MG@OUT[13]")) + return rt_not_blocked_by_galil and air_off + + def axis_is_referenced(self, axis_Id_numeric) -> bool: + return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip())) + + def all_axes_referenced(self) -> bool: + """ + Check if all axes are referenced. + """ + return bool(float(self.socket_put_and_receive("MG allaxref").strip())) + + def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str) -> None: + """ + Drive an axis to the limit in a specified direction. + + Args: + axis_Id_numeric (int): Axis number + direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'. + """ + if direction == "forward": + direction_flag = 1 + elif direction == "reverse": + direction_flag = -1 + else: + raise ValueError(f"Invalid direction {direction}") + + self.socket_put_confirmed(f"naxis={axis_Id_numeric}") + self.socket_put_confirmed(f"ndir={direction_flag}") + self.socket_put_confirmed("XQ#NEWPAR") + time.sleep(0.005) + self.socket_put_confirmed("XQ#FES") + time.sleep(0.01) + while self.is_axis_moving(None, axis_Id_numeric): + time.sleep(0.01) + + axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) + # check if we actually hit the limit + if direction == "forward": + limit = self.get_motor_limit_switch(axis_Id)[1] + elif direction == "reverse": + limit = self.get_motor_limit_switch(axis_Id)[0] + + if not limit: + raise GalilError(f"Failed to drive axis {axis_Id}/{axis_Id_numeric} to limit.") + + def find_reference(self, axis_Id_numeric: int) -> None: + """ + Find the reference of an axis. + + Args: + axis_Id_numeric (int): Axis number + """ + self.socket_put_confirmed(f"naxis={axis_Id_numeric}") + self.socket_put_and_receive("XQ#NEWPAR") + self.socket_put_confirmed("XQ#FRM") + time.sleep(0.1) + while self.is_axis_moving(None, axis_Id_numeric): + time.sleep(0.1) + + if not self.axis_is_referenced(axis_Id_numeric): + raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.") + + logger.info(f"Successfully found reference of axis {axis_Id_numeric}.") + + def show_running_threads(self) -> None: + t = PrettyTable() + t.title = f"Threads on {self.sock.host}:{self.sock.port}" + t.field_names = [str(ax) for ax in range(self._axes_per_controller)] + t.add_row( + [ + "active" if self.is_thread_active(t) else "inactive" + for t in range(self._axes_per_controller) + ] + ) + print(t) + + def is_motor_on(self, axis_Id) -> bool: + return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip())) + + def get_motor_limit_switch(self, axis_Id) -> list: + """ + Get the status of the motor limit switches. + + Args: + axis_Id (str): Axis identifier (e.g. 'A', 'B', 'C', ...) + + Returns: + list: List of two booleans indicating if the low and high limit switch is active, respectively. + """ + ret = self.socket_put_and_receive(f"MG _LR{axis_Id}, _LF{axis_Id}") + low, high = ret.strip().split(" ") + return [not bool(float(low)), not bool(float(high))] + + def describe(self) -> None: + t = PrettyTable() + t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" + t.field_names = [ + "Axis", + "Name", + "Connected", + "Referenced", + "Motor On", + "Limits", + "Position", + ] + for ax in range(self._axes_per_controller): + axis = self._axis[ax] + if axis is not None: + t.add_row( + [ + f"{axis.axis_Id_numeric}/{axis.axis_Id}", + axis.name, + axis.connected, + self.axis_is_referenced(axis.axis_Id_numeric), + self.is_motor_on(axis.axis_Id), + self.get_motor_limit_switch(axis.axis_Id), + axis.readback.read().get(axis.name).get("value"), + ] + ) + else: + t.add_row([None for t in t.field_names]) + print(t) + + self.show_running_threads() + + def galil_show_all(self) -> None: + for controller in self._controller_instances.values(): + if isinstance(controller, GalilController): + controller.describe() + + @staticmethod + def axis_Id_to_numeric(axis_Id: str) -> int: + return ord(axis_Id.lower()) - 97 + + @staticmethod + def axis_Id_numeric_to_alpha(axis_Id_numeric: int) -> str: + return (chr(axis_Id_numeric + 97)).capitalize() + + +class GalilSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class GalilSignalRO(GalilSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class GalilReadbackSignal(GalilSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + + current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}")) + current_pos *= self.parent.sign + step_mm = self.parent.motor_resolution.get() + return current_pos / step_mm + + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + if self.parent.axis_Id_numeric == 2: + try: + rt = self.parent.device_manager.devices[self.parent.rt] + if rt.enabled: + rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]) + except KeyError: + logger.warning("Failed to set RT value during readback.") + return val + + +class GalilSetpointSignal(GalilSignalBase): + setpoint = 0 + + def _socket_get(self) -> float: + """Get command for receiving the setpoint / target value. + The value is not pulled from the controller but instead just the last setpoint used. + + Returns: + float: setpoint / target value + """ + return self.setpoint * self.parent.sign + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + GalilError: Raised if not all axes are referenced. + + """ + target_val = val * self.parent.sign + self.setpoint = target_val + axes_referenced = self.controller.all_axes_referenced() + if axes_referenced: + while self.controller.is_thread_active(0): + time.sleep(0.1) + + if self.parent.axis_Id_numeric == 2: + angle_status = self.parent.device_manager.devices[ + self.parent.rt + ].obj.controller.feedback_status_angle_lamni() + + if angle_status: + self.controller.socket_put_confirmed("angintf=1") + + self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}") + self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}") + self.controller.socket_put_confirmed("movereq=1") + self.controller.socket_put_confirmed("XQ#NEWPAR") + while self.controller.is_thread_active(0): + time.sleep(0.005) + else: + raise GalilError("Not all axes are referenced.") + + +class GalilMotorResolution(GalilSignalRO): + @retry_once + @threadlocked + def _socket_get(self): + return float( + self.controller.socket_put_and_receive(f"MG stppermm[{self.parent.axis_Id_numeric}]") + ) + + +class GalilMotorIsMoving(GalilSignalRO): + @threadlocked + def _socket_get(self): + return self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric) + + def get(self): + val = super().get() + if val is not None: + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) + return val + + +class GalilAxesReferenced(GalilSignalRO): + @threadlocked + def _socket_get(self): + return self.controller.all_axes_referenced() + + +class GalilMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint") + motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config") + motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") + all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=8081, + limits=None, + sign=1, + socket_cls=SocketIO, + device_manager=None, + **kwargs, + ): + self.controller = GalilController(socket_cls=socket_cls, socket_host=host, socket_port=port) + self.axis_Id = axis_Id + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.sign = sign + self.tolerance = kwargs.pop("tolerance", 0.5) + self.device_mapping = kwargs.pop("device_mapping", {}) + self.device_manager = device_manager + + if len(self.device_mapping) > 0 and self.device_manager is None: + raise BECConfigError( + "device_mapping has been specified but the device_manager cannot be accessed." + ) + self.rt = self.device_mapping.get("rt") + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + logger.info("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.1) + val = self.readback.read() + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + + if not success: + print(" stop") + self._done_moving(success=success) + logger.info("Move finished") + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = self.controller.axis_Id_to_numeric(val) + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = self.controller.axis_Id_numeric_to_alpha(val) + self._axis_Id_numeric = val + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "mm" + + def stage(self) -> list[object]: + return super().stage() + + def unstage(self) -> list[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + # pytest: skip-file + mock = False + if not mock: + leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1) + leyey.stage() + status = leyey.move(0, wait=True) + status = leyey.move(10, wait=True) + leyey.read() + + leyey.get() + leyey.describe() + + leyey.unstage() + else: + from ophyd_devices.utils.socket import SocketMock + + leyex = GalilMotor( + "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyey = GalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyex.stage() + # leyey.stage() + + leyex.controller.galil_show_all() diff --git a/csaxs_bec/devices/galil/sgalil.dmc b/csaxs_bec/devices/galil/sgalil.dmc new file mode 100644 index 0000000..5aaa863 --- /dev/null +++ b/csaxs_bec/devices/galil/sgalil.dmc @@ -0,0 +1,415 @@ +'****************************************************************************** +' scanning saxs stages controller code +' version 0.1 20160113, holler, based on example code +' version 0.2 20160321, holler, small adjustments +' version 0.3 20160323, position sampling +' version 0.4 grid scan implemented +' version 0.5 20160426, shutter control from controller +' version 0.6 20160614, code for manual stage tuning added +' version 0.7 20170609, prefact added +' version 0.8 20190327, stepper motor x axis E, encoder axis A +' DC motor y axis C +' version 0.9 20190403, Pos sampling averaging and ring buffered +' internal grid scan updated +' 20190507 various fixes during comm. at beamline +' off now in microns for higher resolution +' version 1.1 20191021, position samples were off compared to xrays +' use AL and RL commands for position latch +' to reduce delay for axis C (continuous) +' switch to DI3 required, averaging removed +' version 1.2 20200500, switch stepper motor to axis F +' because motor driver axis E defective +' version 2.0 20230816, adjustments in premove with BEC +' DO8 controls the shutter +' DI1 1 during exposure for pos sampling +' Thread overview +'****************************************************************************** +#AUTO +DA*,*[];'DEALLOCATE ARRAYS +ssaxs_v=1.3 +IA129,129,122,26 +'acctim determines pre motion +acctim=2.5 +prvspeed=0 +posest=0 +'prefact increases the distance for pre acceleration +'if in acctim limits +prefact=2.5 +off=0 +DM aposavg[2000] +DM cposavg[2000] +nums=1 +JS#INIT +JS#SETPLAT +EN +#CALIBC +'ACC=1000000 +'DCC=1000000 +'SPC=1*mm +PAC=2*mm +BGC;AMC +WT 1000 +PAC=5*mm +BGC;AMC +WT 1000 +JP#CALIBC +EN +'default settings +#SETTOMO +IF(allaxref=1) +EN +ENDIF +KPC=100 +PLC=0.3 +KIC=10 +KDC=30 +ILC=9 +FAC=10 +FVC=240 +EN +'set tuning parameters for scanning saxs plate +#SETPLAT +IF(allaxref=1) +EN +ENDIF +KPC=100 +PLC=0.3 +KIC=10 +KDC=30 +ILC=9 +FAC=10 +FVC=240 +EN +'called with AB before execution +#SAMPLE +posct=0 +sposct=0 +IF(_XQ2=-1) +'arm position latch rising axis C +'set latch direction rising +'we do this prior the loop, to do the switching later asap +CN ,,1 +ALC +XQ#SAMPLEL,2 +ELSE +runerr=1 +ENDIF +EN +#SAMPLEL +'wait for latch +AI3 +'WT1 +''#WAITLT1 +''JP#WAITLT1,(_ALC=1);'WAIT UNTIL CAPTURED' +'write encoder position to a array +aposstrt=_TPA +'write latched position to c array +cposstrt=_RLC +'change latch direction falling axis C +CN ,,-1 +ALC +'wait for latch +AI-3 +'WT1 +''#WAITLT2 +''JP#WAITLT2,(_ALC=1);'WAIT UNTIL CAPTURED +'write encoder position to a array +aposend=_TPA +'write latched position to c array +cposend=_RLC +'arm position latch rising axis C for next cycle +'set latch direction rising +CN ,,1 +ALC +aposavg[posct]=((aposstrt+aposend)/2) +cposavg[posct]=((cposstrt+cposend)/2) +posct=posct+1 +sposct=sposct+1 +IF(posct>1999) +posct=0 +ENDIF +JP #SAMPLEL,(posct(gridct/2)) +start=a_start +end=a_end +ELSE +start=a_end +end=a_start +ENDIF +'XQ#SCANL,5 +'scanstat=-1 +'#lineact +'WT2 +'JP#lineact,(scanstat<>0) +JS#SCANL +gridct=gridct+1 +JP #SCANGL,(gridct<=gridmax) +'close shutter +CB8 +EN +#TEMP1 +EN +' +#SCANL +'based on acceleration of 500 mm/s^2 +'and a max speed of 2 mm/s +'the max distance needed for acceleration is 4 microns +'so we pre-move 10 microns +'variables to set in mm, mm/s +'start = start position +'end = end position +'speed = velocity +'the scan axis is defined in the init section +IF(allaxref=0) +EN +ENDIF +' +IF(end>start) +dir=1 +ELSE +dir=-1 +ENDIF +'measure required premove +IF((@ABS[(prvspeed-speed)])>0.001) +premv=speed*mm*5 +IF(premv>(3*mm)) +premv=3*mm +ENDIF +measpre=1 +ELSE +measpre=0 +ENDIF +'for internal grid scans reduce overshoot +'IF(_XQ3<>-1) +'redpremv=0.1 ;'case for int grid scan +'ELSE +'redpremv=1 ;'case for line based scans +'ENDIF +'we are doing an internal grid +'reduce overshoot +prepos=(start*mm)-(dir*redpremv*(premv+(off/1000*mm))) +'move to pre-start position if needed +'prepos=(start*mm)-(dir*speed*acctim*mm) +'IF(@ABS[(speed*acctim)]<0.01) +'prepos=(start*mm)-(dir*0.01*mm*prefact) +'ENDIF +'IF(@ABS[(speed*acctim)]>0.1) +'ENDIF +IF((@ABS[(_TDC-prepos)])>(0.002*mm)) +scanstat=1 +SPC=2*mm +PAC=prepos +BGC +AMC +'open the shutter +SB8 +WT10 +ENDIF +IF((_LFC<>0)&(_LRC=<>0)) +scanstat=2 +SPC=@RND[speed*mm] +'arm trigger +trigpos=(start*mm)+(dir*off/1000*mm) +IF(dir=1) +OCC=trigpos,0 +ENDIF +IF(dir=-1) +OCC=trigpos,-65536; +ENDIF +'PAC=((end*mm)+(dir*premv)) +PAC=((end*mm)+(dir*redpremv*(premv+(off/1000*mm)))) +BGC +IF(measpre=1) +calstart=_TDC +WT25 +#CALIBV +prevvel=_TVC +WT10 +JP#CALIBV,((@ABS[(prevvel-(_TVC/mm))])<15) +calend=_TDC +premv=((@ABS[(calend-calstart)])*3) +'case of grid scan +prvspeed=speed +ENDIF +' +AMC +ENDIF +scanstat=0 +'close the shutter +'#SHUTWT +'JP#SHUTWT,(@IN[1]=1) +'WT10 +'JP#SHUTWT,(@IN[1]=1) +' +'we are doing an internal grid +IF(_XQ3=-1) +CB8 +ENDIF +EN +' +#POSE +posctr=0 +posest=1 +sttime=TIME +PTF=1;' Position Tracking aktiv +errE=(targE-(_TPA/mm));' Fehler in mm +IF((@ABS[errE])>0.2) +SPF=12*stpmm +ENDIF +#CORRE +posest=2 +errE=(targE-(_TPA/mm));' Fehler in mm +PAF=_TDF+(errE*stpmm) +IF((@ABS[errE])<0.1) +SPF=5*stpmm +ENDIF +IF((@ABS[errE])<0.0001) +posctr=posctr+1 +IF(posctr>5) +STF +'MG TIME-sttime, (targE-(_TPA/mm))*1000 +EN +ENDIF +ELSE +posctr=0 +ENDIF +WT5 +JP#CORRE +posest=3 +MCF +EN +' +#ZZ +targE=120;XQ#POSE +WT12000 +targE=20;XQ#POSE +WT12000 +JP#ZZ +EN +#FINDREF +SB1;' Bremse C-Achse loesen (nur in Verbindung mit SHC) +SHC +SHF +JS#LIMSWI +JS#REFE +JS#REFC +allaxref=1 +targE=0 +EN +' +#REFE +SHF +JGF=-2*stpmm +BGF +'MG "suche negativen Endschalter E" +AMF +WT100 +'step counter zero +DPF=0 +'encoder zero +DPA=0 +EN +' +#REFC +SB1;' Bremse loesen (nur in Verbindung mit SHC) +SHC +JGC=-2*mm +BGC +'MG "suche negativen Endschalter C" +AMC +WT100 +DPC=0 +EN +' +#LIMSWI +scanstat=0 +JS#LFF,_LFF=0;' +LIMIT E +JS#LRF,_LRF=0;' -LIMIT E +JS#LFC,_LFC=0;' +LIMIT C +JS#LRC,_LRC=0;' -LIMIT C +RE;' RETURN FROM ERROR INTERUPT +' +#LRF +'MG "- LIMIT E-ACHSE " +STF +AMF +JGF=stpmm +BGF +#LOOPA1 +JP#LOOPA1,_LRF=0 +STF +MCF +EN +' +#LFF +'MG "+ LIMIT E-ACHSE " +STF +AMF +JGF=-stpmm +BGF +#LOOPA2 +JP#LOOPA2,_LFF=0 +STF +MCF +EN +' +#LRC +'MG "- LIMIT C-ACHSE " +STC +AMC +JGC=mm +BGC +#LOOPC1 +JP#LOOPC1,_LRC=0 +STC +AMC +EN +' +#LFC +'MG "- LIMIT C-ACHSE " +STC +AMC +JGC=-mm +BGC +#LOOPC2 +JP#LOOPC2,_LFC=0 +STC +AMC +EN +' +#INIT +'define fast scan axis redefined by spec +scanstat=0 +mm=10000;' 100nm Aufloesung im encoder +stpmm=50000;' microsteps axis E/mm +ratio=5;' +allaxref=0 +'acceleration rates +ACF=3000000 +DCF=3000000 +SPF=2*mm +OEF=0;' OF ON ERROR axis E ausgeschaltet +MTF=2 +KSF=0.5;' Smoothing ausgeschaltet +ACC=10*mm +DCC=10*mm +DVC=1;' Dual loop deaktiviert +EN +' diff --git a/csaxs_bec/devices/galil/sgalil_ophyd.py b/csaxs_bec/devices/galil/sgalil_ophyd.py new file mode 100644 index 0000000..d88b191 --- /dev/null +++ b/csaxs_bec/devices/galil/sgalil_ophyd.py @@ -0,0 +1,713 @@ +import functools +import threading +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, DeviceStatus, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable + +logger = bec_logger.logger + + +class GalilCommunicationError(Exception): + pass + + +class GalilError(Exception): + pass + + +class BECConfigError(Exception): + pass + + +def retry_once(fcn): + """Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (GalilCommunicationError, GalilError): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class GalilController(Controller): + USER_ACCESS = [ + "describe", + "show_running_threads", + "galil_show_all", + "socket_put_and_receive", + "socket_put_confirmed", + "sgalil_reference", + "fly_grid_scan", + "read_encoder_position", + ] + + def __init__( + self, + *, + name="GalilController", + kind=None, + parent=None, + socket=None, + attr_name="", + labels=None, + ): + if not hasattr(self, "_initialized") or not self._initialized: + self._galil_axis_per_controller = 8 + self._axis = [None for axis_num in range(self._galil_axis_per_controller)] + super().__init__( + name=name, + socket=socket, + attr_name=attr_name, + parent=parent, + labels=labels, + kind=kind, + ) + + def on(self, controller_num=0) -> None: + """Open a new socket connection to the controller""" + if not self.connected: + self.sock.open() + self.connected = True + else: + logger.info("The connection has already been established.") + # warnings.warn(f"The connection has already been established.", stacklevel=2) + + def off(self) -> None: + """Close the socket connection to the controller""" + if self.connected: + self.sock.close() + self.connected = False + else: + logger.info("The connection is already closed.") + + def set_axis(self, axis: Device, axis_nr: int) -> None: + """Assign an axis to a device instance. + + Args: + axis (Device): Device instance (e.g. GalilMotor) + axis_nr (int): Controller axis number + + """ + self._axis[axis_nr] = axis + + @threadlocked + def socket_put(self, val: str) -> None: + time.sleep(0.01) + self.sock.put(f"{val}\r".encode()) + + @threadlocked + def socket_get(self) -> str: + time.sleep(0.01) + return self.sock.receive().decode() + + @retry_once + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + if remove_trailing_chars: + return self._remove_trailing_characters(self.sock.receive().decode()) + return self.socket_get() + + @retry_once + def socket_put_confirmed(self, val: str) -> None: + """Send message to controller and ensure that it is received by checking that the socket receives a colon. + + Args: + val (str): Message that should be sent to the socket + + Raises: + GalilCommunicationError: Raised if the return value is not a colon. + + """ + return_val = self.socket_put_and_receive(val) + if return_val != ":": + raise GalilCommunicationError( + f"Expected return value of ':' but instead received {return_val}" + ) + + def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool: + is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0) + # backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0) + return bool(is_moving) # bool(is_moving or backlash_is_active) + + def is_thread_active(self, thread_id: int) -> bool: + val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}")) + if val == -1: + return False + return True + + def _remove_trailing_characters(self, var) -> str: + if len(var) > 1: + return var.split("\r\n")[0] + return var + + def stop_all_axes(self) -> str: + # return self.socket_put_and_receive(f"XQ#STOP,1") + # Command stops all threads and motors! + self.socket_put_and_receive(f"CB8") + return self.socket_put_and_receive(f"ST") + + def axis_is_referenced(self) -> bool: + return bool(float(self.socket_put_and_receive(f"MG allaxref").strip())) + + def show_running_threads(self) -> None: + t = PrettyTable() + t.title = f"Threads on {self.sock.host}:{self.sock.port}" + t.field_names = [str(ax) for ax in range(self._galil_axis_per_controller)] + t.add_row( + [ + "active" if self.is_thread_active(t) else "inactive" + for t in range(self._galil_axis_per_controller) + ] + ) + print(t) + + def is_motor_on(self, axis_Id) -> bool: + return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip())) + + def get_motor_limit_switch(self, axis_Id) -> list: + # SGalil specific + if axis_Id == "C": + ret = self.socket_put_and_receive(f"MG _LF{axis_Id}, _LR{axis_Id}") + high, low = ret.strip().split(" ") + elif axis_Id == "E": + ret = self.socket_put_and_receive(f"MG _LF{'F'}, _LR{'F'}") + high, low = ret.strip().split(" ") + return [not bool(float(low)), not bool(float(high))] + + def describe(self) -> None: + t = PrettyTable() + t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" + t.field_names = [ + "Axis", + "Name", + "Connected", + "Referenced", + "Motor On", + "Limits", + "Position", + ] + for ax in range(self._galil_axis_per_controller): + axis = self._axis[ax] + if axis is not None: + t.add_row( + [ + f"{axis.axis_Id_numeric}/{axis.axis_Id}", + axis.name, + axis.connected, + self.axis_is_referenced(), + self.is_motor_on(axis.axis_Id), + self.get_motor_limit_switch(axis.axis_Id), + axis.readback.read().get(axis.name).get("value"), + ] + ) + else: + t.add_row([None for t in t.field_names]) + print(t) + + self.show_running_threads() + + def galil_show_all(self) -> None: + for controller in self._controller_instances.values(): + if isinstance(controller, GalilController): + controller.describe() + + def sgalil_reference(self) -> None: + """Reference all axes of the controller""" + if self.axis_is_referenced(): + print("All axes are already referenced.\n") + return + # Make sure no axes are moving, is this necessary? + self.stop_all_axes() + self.socket_put_and_receive(f"XQ#FINDREF") + print("Referencing. Please wait, timeout after 100s...\n") + + timeout = time.time() + 100 + while not self.axis_is_referenced(): + if time.time() > timeout: + print("Abort reference sequence, timeout reached\n") + break + time.sleep(0.5) + + # @threadlocked + def fly_grid_scan( + self, + status: DeviceStatus, + start_y: float, + end_y: float, + interval_y: int, + start_x: float, + end_x: float, + interval_x: int, + exp_time: float, + readout_time: float, + **kwargs, + ) -> tuple: + """_summary_ + + Args: + start_y (float): start position of y axis (fast axis) + end_y (float): end position of y axis (fast axis) + interval_y (int): number of points in y axis + start_x (float): start position of x axis (slow axis) + end_x (float): end position of x axis (slow axis) + interval_x (int): number of points in x axis + exp_time (float): exposure time in seconds + readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms) + + Raises: + + LimitError: Raised if any position of motion is outside of the limits + LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s + + """ + # + if not self.axis_is_referenced(): + raise GalilError("Axis are not referenced") + sign_y = self._axis[ord("c") - 97].sign + sign_x = self._axis[ord("e") - 97].sign + # Check limits + # TODO check sign of stage, or not necessary + check_values = [start_y, end_y, start_x, end_x] + for val in check_values: + self.check_value(val) + + start_x *= sign_x + end_x *= sign_x + start_y *= sign_y + end_y *= sign_y + + speed = np.abs(end_y - start_y) / ( + (interval_y) * exp_time + (interval_y - 1) * readout_time + ) + if speed > 2.00 or speed < 0.02: + raise LimitError( + f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s" + ) + + gridmax = int(interval_x - 1) + step_grid = (end_x - start_x) / interval_x + n_samples = int(interval_y * interval_x) + + # Hard coded to maximum offset of 0.1mm to avoid long motions. + self.socket_put_and_receive(f"off={(0):f}") + self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}") + self.socket_put_and_receive( + f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}" + ) + self.socket_put_and_receive(f"nums={n_samples}") + self.socket_put_and_receive("XQ#SAMPLE") + # sleep 50ms to avoid controller running into + time.sleep(0.1) + self.socket_put_and_receive("XQ#SCANG") + # self._block_while_active(3) + # time.sleep(0.1) + threading.Thread(target=self._block_while_active, args=(3, status), daemon=True).start() + # self._while_in_motion(3, n_samples) + + def _block_while_active(self, thread_id: int, status) -> None: + while self.is_thread_active(thread_id): + time.sleep(1) + time.sleep(1) + while self.is_thread_active(thread_id): + time.sleep(1) + status.set_finished() + + # TODO this is for reading out positions, readout is limited by stage triggering + def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple: + last_readout = 0 + val_axis2 = [] # y axis + val_axis4 = [] # x axis + while self.is_thread_active(thread_id): + posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) + logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}") + time.sleep(1) + if posct > last_readout: + positions = self.read_encoder_position(last_readout, posct) + val_axis4.extend(positions[0]) + val_axis2.extend(positions[1]) + last_readout = posct + 1 + logger.info(len(val_axis2)) + time.sleep(1) + # Readout of last positions after scan finished + posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) + logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}") + if posct > last_readout: + positions = self.read_encoder_position(last_readout, posct) + val_axis4.extend(positions[0]) + val_axis2.extend(positions[1]) + + return val_axis4, val_axis2 + + def read_encoder_position(self, fromval: int, toval: int) -> tuple: + val_axis2 = [] # y axis + val_axis4 = [] # x axis + for ii in range(fromval, toval + 1): + rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10") + if rts == ":": + val_axis4.append(rts) + val_axis2.append(rts) + continue + + val_axis4.append(float(rts.strip().split(" ")[0]) / 100000) + val_axis2.append(float(rts.strip().split(" ")[1]) / 100000) + return val_axis4, val_axis2 + + +class GalilSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class GalilSignalRO(GalilSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class GalilReadbackSignal(GalilSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + if self.parent.axis_Id_numeric == 2: + current_pos = float( + self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm") + ) + elif self.parent.axis_Id_numeric == 4: + # hardware controller readback from axis 4 is on axis 0, A instead of E + current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm")) + current_pos *= self.parent.sign + return current_pos + + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + return val + + +class GalilSetpointSignal(GalilSignalBase): + setpoint = 0 + + def _socket_get(self) -> float: + """Get command for receiving the setpoint / target value. + The value is not pulled from the controller but instead just the last setpoint used. + + Returns: + float: setpoint / target value + """ + return self.setpoint * self.parent.sign + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + GalilError: Raised if not all axes are referenced. + + """ + target_val = val * self.parent.sign + self.setpoint = target_val + axes_referenced = self.controller.axis_is_referenced() + if not axes_referenced: + raise GalilError( + "Not all axes are referenced. Please use controller.sgalil_reference(). BE AWARE that axes start moving, potentially beyond limits, make sure full range of motion is safe" + ) + while self.controller.is_thread_active(0): + time.sleep(0.1) + + if self.parent.axis_Id_numeric == 2: + self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm") + self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}") + elif self.parent.axis_Id_numeric == 4: + self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}") + self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}") + while self.controller.is_thread_active(0): + time.sleep(0.005) + + +class GalilMotorIsMoving(GalilSignalRO): + @threadlocked + def _socket_get(self): + if self.parent.axis_Id_numeric == 2: + ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric) + return ret + if self.parent.axis_Id_numeric == 4: + # Motion signal from axis 4 is mapped to axis 5 + ret = self.controller.is_axis_moving("F", 5) + return ret or self.controller.is_thread_active(4) + + def get(self): + val = super().get() + if val is not None: + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) + return val + + +class GalilAxesReferenced(GalilSignalRO): + @threadlocked + def _socket_get(self): + return self.controller.socket_put_and_receive("MG allaxref") + + +class SGalilMotor(Device, PositionerBase): + """ "SGalil Motors at cSAXS have a + DC motor (y axis - vertical) - implemented as C + and a step motor (x-axis horizontal) - implemented as E + that require different communication for control + """ + + USER_ACCESS = ["controller"] + readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint") + motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") + all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="129.129.122.26", + port=23, + limits=None, + sign=1, + socket_cls=SocketIO, + device_manager=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = GalilController(socket=socket_cls(host=host, port=port)) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.tolerance = kwargs.pop("tolerance", 0.5) + self.device_mapping = kwargs.pop("device_mapping", {}) + self.device_manager = device_manager + + if len(self.device_mapping) > 0 and self.device_manager is None: + raise BECConfigError( + "device_mapping has been specified but the device_manager cannot be accessed." + ) + self.rt = self.device_mapping.get("rt") + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + logger.info("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(1.5) + val = self.readback.read() + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + + if not success: + print(" stop") + self._done_moving(success=success) + logger.info("Move finished") + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + if val not in ["C", "E"]: + raise ValueError( + f"axis_id {val} is currently not supported, please use either 'C' or 'E'." + ) + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val not in [2, 4]: + raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}.") + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "mm" + + def stop(self, *, success=False): + self.controller.stop_all_axes() + # last_speed = self.controller.socket_put_and_receive("MG") + rtr = self.controller.socket_put_and_receive(f"SPC={2*10000}") + logger.info(f"{rtr}") + # logger.info(f'Motor stopped, restored speed for samy from {last_speed}mm/s to 2mm/s') + return super().stop(success=success) + + def kickoff(self) -> DeviceStatus: + status = DeviceStatus(self) + self.controller.fly_grid_scan( + status, + self._kickoff_params.get("start_y"), + self._kickoff_params.get("end_y"), + self._kickoff_params.get("interval_y"), + self._kickoff_params.get("start_x"), + self._kickoff_params.get("end_x"), + self._kickoff_params.get("interval_x"), + self._kickoff_params.get("exp_time"), + self._kickoff_params.get("readout_time"), + ) + return status + + def configure(self, parameter: dict, **kwargs) -> None: + self._kickoff_params = parameter + + +if __name__ == "__main__": + mock = False + if not mock: + samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1) + samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1) + else: + from ophyd_devices.utils.socket import SocketMock + + samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock) + samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock) + + samx.controller.galil_show_all() diff --git a/csaxs_bec/devices/galil/sgalil_readme.md b/csaxs_bec/devices/galil/sgalil_readme.md new file mode 100644 index 0000000..56b5039 --- /dev/null +++ b/csaxs_bec/devices/galil/sgalil_readme.md @@ -0,0 +1,79 @@ +# Documentation SGalil ophyd wrapper +Ophyd wrapper for the SGalil controller and stages. +## TODO tests and evaluate whether its good to combine common functionaltiy with galil lamni/omny/flomni controller +## Integration of the device in IPython kernel +BEC needs to be able to reach the host TCP to initiate a connection to the device. +```Python +from csaxs_bec.devices.galil.sgalil_ophyd import SGalilMotor +samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1) +samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1) +# connect to the controller +samx.controller.on() +samx.read() +samx.move(5) +dir(samx)# for full printout of commands +# useful for development, check below socket communication with sgalil controller +samx.controller.socket_put_and_receive('#string: message_to_controller') +``` +## TODO Integration of device in BEC device config! +to be tested too + +## Fly scans +2D grid fly scan as implemented on the controller. +TTL triggers are sent for the start of each line. +The scan on the controller needs to be matched with an appropriate triggering scheme, as for instance shown in the attached scheme together with the Stanford Research DG645 device at cSAXS. +![image info](./csaxs_sgalil_triggering.png) +```Python +samx.controller.(start_y, end_y, interval_y, start_x, end_x, interval_x, exp_time, readtime) +# for example +samx.controller.fly_grid_scan(start_y= 16, end_y= 24, interval_y= 100, start_x= 18, end_x= 17.6, interval_x= 2, exp_time= 0.08, readtime= 0.005) +``` + +## TODO implement line scan +Check SPEC implementation for line scans with sgalil controller, and complement it with a suitable triggering scheme of the DG645. + +## TODO readout of positions in encoder +Should this be integrated in the flyscan or not. +To be explored where this is most suitable. + +## Socket communication with sgalil controller +### vertical axis (samy) +- initiate with axis 2, C +- in motion: "MG _BG{axis_char}", e.g. "MG _BGC" , 0 or 1 +- limit switch not pressed: "MG _LR{axis_char}, _LF{axis_char}" , 0 or 1 +- position: "MG _TP{axis_char}/mm" , position in mm +- Axis referenced: "MG allaxref", 0 or 1 +- stop all axis: "XQ#STOP,1" +- is motor on: "MG _MO{axis_char}", 0 or 1 +- is thread active: "MG _XQ{thread_id}", 0 or 1 +**Specific for sgalil_y** +- set_motion_speed: "SP{axis_char}=2*mm", 2mm/s is max speed +- set_final_pos: "PA{axis_char}={val:04f}*mm", target pos in mm +- start motion: "BG{axis_char}", start motion +### horizontal axis (samx) +note: some hardware modifications were done that require access to different channels in the encoder. Encoder, motor and limit switches are not controlled by the same endpoint/axis of the controller... see below +- initiate with axis 4, E +**Specific for sgalil_x** +- set_final_pos: "targ{axis_char}={val:04f}", e.g. "targE=2.0000" +- start motion: "XQ#POSE,{axis_char}" +- For *in motion* and *limit switch not pressed* commands, +the key changes to AXIS 5 || F, e.g. "MG _BGF" +- For *position* switch to Axis 0 || A, e.g. "MG _TPA/mm" + +### flyscan 2D grid commanes: +Last command ('XQ#SCANG') has to come with sufficient delay, important for setting up dedicated scans +f***ast axis*** +- self.socket_put_and_receive(f'a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}') +***slow axis*** +- self.socket_put_and_receive(f'b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}') +- self.socket_put_and_receive(f'nums={n_samples}') # Declare number of triggers for encoder +- self.socket_put_and_receive('XQ#SAMPLE') # Reset encoder counting --> sampling starts with 0 +Start scan (be aware, needs some waiting from before) +- self.socket_put_and_receive('XQ#SCANG') + +### Encoder readings! +The encoder readout is triggered by an TTL pulse. +Unfortunately, TTL triggers to the encoder can only be accepted with at least 12.5ms time between rising/falling edges. Therefore, maximum readout has to be ~25Hz, rather 30Hz (experimentally determined). +Socket commands for the readout: +- self.socket_put_and_receive('MGsposct') # get current position counter +- self.socket_put_and_receive('MGaposavg[{ii%2000}]*10, cposavg[{ii%2000}]*10,') # loop over ii diff --git a/csaxs_bec/devices/npoint/__init__.py b/csaxs_bec/devices/npoint/__init__.py new file mode 100644 index 0000000..a35e22c --- /dev/null +++ b/csaxs_bec/devices/npoint/__init__.py @@ -0,0 +1 @@ +from .npoint import NPointAxis, NPointController diff --git a/csaxs_bec/devices/npoint/npoint.py b/csaxs_bec/devices/npoint/npoint.py new file mode 100644 index 0000000..9fcb5d9 --- /dev/null +++ b/csaxs_bec/devices/npoint/npoint.py @@ -0,0 +1,545 @@ +import functools +import socket +import threading +import time + +from ophyd_devices.utils.controller import threadlocked +from ophyd_devices.utils.socket import raise_if_disconnected +from prettytable import PrettyTable +from typeguard import typechecked + + +def channel_checked(fcn): + """Decorator to catch attempted access to channels that are not available.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + self._check_channel(args[0]) + return fcn(self, *args, **kwargs) + + return wrapper + + +class SocketIO: + """SocketIO helper class for TCP IP connections""" + + def __init__(self, sock=None): + self.is_open = False + if sock is None: + self.open() + else: + self.sock = sock + + def connect(self, host, port): + print(f"connecting to {host} port {port}") + # self.sock.create_connection((host, port)) + self.sock.connect((host, port)) + + def _put(self, msg_bytes): + return self.sock.send(msg_bytes) + + def _recv(self, buffer_length=1024): + return self.sock.recv(buffer_length) + + def _initialize_socket(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(5) + + def put(self, msg): + return self._put(msg) + + def receive(self, buffer_length=1024): + return self._recv(buffer_length=buffer_length) + + def open(self): + self._initialize_socket() + self.is_open = True + + def close(self): + self.sock.close() + self.sock = None + self.is_open = False + + +class NPointController: + _controller_instance = None + + NUM_CHANNELS = 3 + _read_single_loc_bit = "A0" + _write_single_loc_bit = "A2" + _trailing_bit = "55" + _range_offset = "78" + _channel_base = ["11", "83"] + + def __init__( + self, comm_socket: SocketIO, server_ip: str = "129.129.99.87", server_port: int = 23 + ) -> None: + self._lock = threading.RLock() + super().__init__() + self._server_and_port_name = (server_ip, server_port) + self.socket = comm_socket + self.connected = False + + def __new__(cls, *args, **kwargs): + if not NPointController._controller_instance: + NPointController._controller_instance = object.__new__(cls) + return NPointController._controller_instance + + @classmethod + def create(cls): + return cls(SocketIO()) + + def show_all(self) -> None: + """Display current status of all channels + + Returns: + None + """ + if not self.connected: + print("npoint controller is currently disabled.") + return + print(f"Connected to controller at {self._server_and_port_name}") + t = PrettyTable() + t.field_names = ["Channel", "Range", "Position", "Target"] + for ii in range(self.NUM_CHANNELS): + t.add_row( + [ii, self._get_range(ii), self._get_current_pos(ii), self._get_target_pos(ii)] + ) + print(t) + + @threadlocked + def on(self) -> None: + """Enable the NPoint controller and open a new socket. + + Raises: + TimeoutError: Raised if the socket connection raises a timeout. + + Returns: + None + """ + if self.connected: + print("You are already connected to the NPoint controller.") + return + if not self.socket.is_open: + self.socket.open() + try: + self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1]) + except socket.timeout: + raise TimeoutError( + f"Failed to connect to the specified server and port {self._server_and_port_name}." + ) + except OSError: + print("ERROR while connecting. Let's try again") + self.socket.close() + time.sleep(0.5) + self.socket.open() + self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1]) + self.connected = True + + @threadlocked + def off(self) -> None: + """Disable the controller and close the socket. + + Returns: + None + """ + self.socket.close() + self.connected = False + + @channel_checked + def _get_range(self, channel: int) -> int: + """Get the range of the specified channel axis. + + Args: + channel (int): Channel for which the range should be requested. + + Raises: + RuntimeError: Raised if the received message doesn't have the expected number of bytes (10). + + Returns: + int: Range + """ + + # for first channel: 0x11 83 10 78 + addr = self._channel_base.copy() + addr.extend([f"{16 + 16 * channel:x}", self._range_offset]) + send_buffer = self.__read_single_location_buffer(addr) + + recvd = self._put_and_receive(send_buffer) + if len(recvd) != 10: + raise RuntimeError( + f"Received buffer is corrupted. Expected 10 bytes and instead got {len(recvd)}" + ) + device_range = self._hex_list_to_int(recvd[5:-1], signed=False) + return device_range + + @channel_checked + def _get_current_pos(self, channel: int) -> float: + # for first channel: 0x11 83 13 34 + addr = self._channel_base.copy() + addr.extend([f"{19 + 16 * channel:x}", "34"]) + send_buffer = self.__read_single_location_buffer(addr) + + recvd = self._put_and_receive(send_buffer) + + pos_buffer = recvd[5:-1] + pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100 + return pos + + @channel_checked + def _set_target_pos(self, channel: int, pos: float) -> None: + # for first channel: 0x11 83 12 18 00 00 00 00 + addr = self._channel_base.copy() + addr.extend([f"{18 + channel * 16:x}", "18"]) + + target = int(round(1048574 / 100 * pos)) + data = [f"{m:02x}" for m in target.to_bytes(4, byteorder="big", signed=True)] + + send_buffer = self.__write_single_location_buffer(addr, data) + self._put(send_buffer) + + @channel_checked + def _get_target_pos(self, channel: int) -> float: + # for first channel: 0x11 83 12 18 + addr = self._channel_base.copy() + addr.extend([f"{18 + channel * 16:x}", "18"]) + send_buffer = self.__read_single_location_buffer(addr) + + recvd = self._put_and_receive(send_buffer) + pos_buffer = recvd[5:-1] + pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100 + return pos + + @channel_checked + def _set_servo(self, channel: int, enable: bool) -> None: + print("Not tested") + return + # for first channel: 0x11 83 10 84 00 00 00 00 + addr = self._channel_base.copy() + addr.extend([f"{16 + channel * 16:x}", "84"]) + + if enable: + data = ["00"] * 3 + ["01"] + else: + data = ["00"] * 4 + send_buffer = self.__write_single_location_buffer(addr, data) + + self._put(send_buffer) + + @channel_checked + def _get_servo(self, channel: int) -> int: + # for first channel: 0x11 83 10 84 00 00 00 00 + addr = self._channel_base.copy() + addr.extend([f"{16 + channel * 16:x}", "84"]) + send_buffer = self.__read_single_location_buffer(addr) + + recvd = self._put_and_receive(send_buffer) + buffer = recvd[5:-1] + status = self._hex_list_to_int(buffer) + return status + + @threadlocked + def _put(self, buffer: list) -> None: + """Translates a list of hex values to bytes and sends them to the socket. + + Args: + buffer (list): List of hex values without leading 0x + + Returns: + None + """ + + buffer = b"".join([bytes.fromhex(m) for m in buffer]) + self.socket.put(buffer) + + @threadlocked + def _put_and_receive(self, msg_hex_list: list) -> list: + """Send msg to socket and wait for a reply. + + Args: + msg_hex_list (list): List of hex values without leading 0x. + + Returns: + list: Received message as a list of hex values + """ + + buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list]) + self.socket.put(buffer) + recv_msg = self.socket.receive() + recv_hex_list = [hex(m) for m in recv_msg] + self._verify_received_msg(msg_hex_list, recv_hex_list) + return recv_hex_list + + def _verify_received_msg(self, in_list: list, out_list: list) -> None: + """Ensure that the first address bits of sent and received messages are the same. + + Args: + in_list (list): list containing the sent message + out_list (list): list containing the received message + + Raises: + RuntimeError: Raised if first two address bits of 'in' and 'out' are not identical + + Returns: + None + """ + + # first, translate hex (str) values to int + in_list_int = [int(val, 16) for val in in_list] + out_list_int = [int(val, 16) for val in out_list] + + # first ints of the reply should be the same. Otherwise something went wrong + if not in_list_int[:2] == out_list_int[:2]: + raise RuntimeError("Connection failure. Please restart the controller.") + + def _check_channel(self, channel: int) -> None: + if channel >= self.NUM_CHANNELS: + raise ValueError( + f"Channel {channel+1} exceeds the available number of channels ({self.NUM_CHANNELS})" + ) + + @staticmethod + def _hex_list_to_int(in_buffer: list, byteorder="little", signed=True) -> int: + """Translate hex list to int. + + Args: + in_buffer (list): Input buffer; received as list of hex values + byteorder (str, optional): Byteorder of in_buffer. Defaults to "little". + signed (bool, optional): Whether the hex list represents a signed int. Defaults to True. + + Returns: + int: Translated integer. + """ + if byteorder == "little": + in_buffer.reverse() + + # make sure that all hex strings have the same format ("FF") + val_hex = [f"{int(m, 16):02x}" for m in in_buffer] + + val_bytes = [bytes.fromhex(m) for m in val_hex] + val = int.from_bytes(b"".join(val_bytes), byteorder="big", signed=signed) + return val + + @staticmethod + def __read_single_location_buffer(addr) -> list: + """Prepare buffer for reading from a single memory location (hex address). + Number of bytes: 6 + Format: 0xA0 [addr] 0x55 + Return Value: 0xA0 [addr] [data] 0x55 + Sample Hex Transmission from PC to LC.400: A0 18 12 83 11 55 + Sample Hex Return Transmission from LC.400 to PC: A0 18 12 83 11 64 00 00 00 55 + + Args: + addr (list): Hex address to read from + + Returns: + list: List of hex values representing the read instruction. + """ + buffer = [] + buffer.append(NPointController._read_single_loc_bit) + if isinstance(addr, list): + addr.reverse() + buffer.extend(addr) + else: + buffer.append(addr) + buffer.append(NPointController._trailing_bit) + + return buffer + + @staticmethod + def __write_single_location_buffer(addr: list, data: list) -> list: + """Prepare buffer for writing to a single memory location (hex address). + Number of bytes: 10 + Format: 0xA2 [addr] [data] 0x55 + Return Value: none + Sample Hex Transmission from PC to C.400: A2 18 12 83 11 E8 03 00 00 55 + + Args: + addr (list): List of hex values representing the address to write to. + data (list): List of hex values representing the data that should be written. + + Returns: + list: List of hex values representing the write instruction. + """ + buffer = [] + buffer.append(NPointController._write_single_loc_bit) + if isinstance(addr, list): + addr.reverse() + buffer.extend(addr) + else: + buffer.append(addr) + + if isinstance(data, list): + data.reverse() + buffer.extend(data) + else: + buffer.append(data) + buffer.append(NPointController._trailing_bit) + return buffer + + @staticmethod + def __read_array(): + raise NotImplementedError + + @staticmethod + def __write_next_command(): + raise NotImplementedError + + def __del__(self): + if self.connected: + print("Closing npoint socket") + self.off() + + +class NPointAxis: + def __init__(self, controller: NPointController, channel: int, name: str) -> None: + super().__init__() + self._axis_range = 100 + self.controller = controller + self.channel = channel + self.name = name + self.controller._check_channel(channel) + self._settling_time = 0.1 + + if self.settling_time == 0: + self.settling_time = 0.1 + print(f"Setting the npoint settling time to {self.settling_time:.2f} s.") + print( + "You can set the settling time depending on the stage tuning\nusing the settling_time property." + ) + print("This is the waiting time before the counting is done.") + + def show_all(self) -> None: + self.controller.show_all() + + @raise_if_disconnected + def get(self) -> float: + """Get current position for this channel. + + Raises: + RuntimeError: Raised if channel is not connected. + + Returns: + float: position + """ + return self.controller._get_current_pos(self.channel) + + @raise_if_disconnected + def get_target_pos(self) -> float: + """Get target position for this channel. + + Raises: + RuntimeError: Raised if channel is not connected. + + Returns: + float: position + """ + return self.controller._get_target_pos(self.channel) + + @raise_if_disconnected + @typechecked + def set(self, pos: float) -> None: + """Set a new target position and wait until settled (settling_time). + + Args: + pos (float): New target position + + Raises: + RuntimeError: Raised if channel is not connected. + + Returns: + None + """ + self.controller._set_target_pos(self.channel, pos) + time.sleep(self.settling_time) + + @property + def connected(self) -> bool: + return self.controller.connected + + @property + @raise_if_disconnected + def servo(self) -> int: + """Get servo status + + Raises: + RuntimeError: Raised if channel is not connected. + + Returns: + int: Servo status + """ + return self.controller._get_servo(self.channel) + + @servo.setter + @raise_if_disconnected + @typechecked + def servo(self, val: bool) -> None: + """Set servo status + + Args: + val (bool): Servo status + + Raises: + RuntimeError: Raised if channel is not connected. + + Returns: + None + """ + self.controller._set_servo(self.channel, val) + + @property + def settling_time(self) -> float: + return self._settling_time + + @settling_time.setter + @typechecked + def settling_time(self, val: float) -> None: + self._settling_time = val + print(f"Setting the npoint settling time to {val:.2f} s.") + + +class NPointEpics(NPointAxis): + def __init__(self, controller: NPointController, channel: int, name: str) -> None: + super().__init__(controller, channel, name) + self.low_limit = -50 + self.high_limit = 50 + self._prefix = name + + def get_pv(self) -> str: + return self.name + + def get_position(self, readback=True) -> float: + if readback: + return self.get() + else: + return self.get_target_pos() + + def within_limits(self, pos: float) -> bool: + return pos > self.low_limit and pos < self.high_limit + + def move(self, position: float, wait=True) -> None: + self.set(position) + + +if __name__ == "__main__": + ## EXAMPLES ## + # + # Create controller and socket instance explicitly: + # controller = NPointController(SocketIO()) + # npointx = NPointAxis(controller, 0, "nx") + # npointy = NPointAxis(controller, 1, "ny") + + # Create controller instance explicitly + # controller = NPointController.create() + # npointx = NPointAxis(controller, 0, "nx") + # npointy = NPointAxis(controller, 1, "ny") + + # Single-line axis: + # npointx = NPointAxis(NPointController.create(), 0, "nx") + # + # EPICS wrapper: + # nx = NPointEpics(NPointController.create(), 0, "nx") + + controller = NPointController.create() + npointx = NPointAxis(NPointController.create(), 0, "nx") + npointy = NPointAxis(NPointController.create(), 0, "ny") diff --git a/csaxs_bec/devices/rt_lamni/__init__.py b/csaxs_bec/devices/rt_lamni/__init__.py new file mode 100644 index 0000000..5a60cfb --- /dev/null +++ b/csaxs_bec/devices/rt_lamni/__init__.py @@ -0,0 +1,2 @@ +from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor +from .rt_lamni_ophyd import RtLamniController, RtLamniMotor diff --git a/csaxs_bec/devices/rt_lamni/rt_flomni_ophyd.py b/csaxs_bec/devices/rt_lamni/rt_flomni_ophyd.py new file mode 100644 index 0000000..d3d14bf --- /dev/null +++ b/csaxs_bec/devices/rt_lamni/rt_flomni_ophyd.py @@ -0,0 +1,811 @@ +import threading +import time +from typing import List + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError +from csaxs_bec.devices.rt_lamni.rt_ophyd import ( + BECConfigError, + RtCommunicationError, + RtController, + RtError, + RtReadbackSignal, + RtSetpointSignal, + RtSignalRO, + retry_once, +) +from ophyd_devices.utils.controller import threadlocked +from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected +from prettytable import PrettyTable + +logger = bec_logger.logger + + +class RtFlomniController(RtController): + USER_ACCESS = [ + "socket_put_and_receive", + "set_rotation_angle", + "feedback_disable", + "feedback_enable_without_reset", + "feedback_enable_with_reset", + "feedback_is_running", + "add_pos_to_scan", + "get_pid_x", + "move_samx_to_scan_region", + "clear_trajectory_generator", + "show_cyclic_error_compensation", + "laser_tracker_on", + "laser_tracker_off", + "laser_tracker_show_all", + "show_signal_strength_interferometer", + "read_ssi_interferometer", + "laser_tracker_check_signalstrength", + "laser_tracker_check_enabled", + ] + + def __init__( + self, + *, + name=None, + socket_cls=None, + socket_host=None, + socket_port=None, + attr_name="", + parent=None, + labels=None, + kind=None, + ): + super().__init__( + name=name, + socket_cls=socket_cls, + socket_host=socket_host, + socket_port=socket_port, + attr_name=attr_name, + parent=parent, + labels=labels, + kind=kind, + ) + self.tracker_info = {} + self._min_scan_buffer_reached = False + self.rt_pid_voltage = None + + def add_pos_to_scan(self, positions) -> None: + def send_positions(parent, positions): + parent._min_scan_buffer_reached = False + start_time = time.time() + for pos_index, pos in enumerate(positions): + parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}") + if pos_index > 100: + parent._min_scan_buffer_reached = True + parent._min_scan_buffer_reached = True + logger.info( + f"Sending {len(positions)} positions took {time.time()-start_time} seconds." + ) + + threading.Thread(target=send_positions, args=(self, positions), daemon=True).start() + + def move_to_zero(self): + self.socket_put("pa0,0") + self.get_axis_by_name("rtx").user_setpoint.setpoint = 0 + self.socket_put("pa1,0") + self.get_axis_by_name("rty").user_setpoint.setpoint = 0 + self.socket_put("pa2,0") + self.get_axis_by_name("rtz").user_setpoint.setpoint = 0 + time.sleep(0.05) + + def feedback_is_running(self) -> bool: + status = int(float(self.socket_put_and_receive("l2").strip())) + if status == 1: + return False + return True + + def feedback_enable_with_reset(self): + self.socket_put("l0") # disable feedback + + self.move_to_zero() + + if not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: + print("Please wait, slew rate limiters not on target.") + logger.info("Please wait, slew rate limiters not on target.") + while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: + time.sleep(0.05) + + self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0}) + self.clear_trajectory_generator() + + self.laser_tracker_on() + + # move to 0. FUPR will set the rotation angle during readout + self.get_device_manager().devices.fsamroy.obj.move(0, wait=True) + + fsamx = self.get_device_manager().devices.fsamx + + fsamx.obj.pid_x_correction = 0 + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") + fsamx_in = fsamx.user_parameter.get("in") + if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3): + print( + "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." + ) + raise RtError( + "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." + ) + + if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01): + fsamx.read_only = False + fsamx.obj.move(fsamx_in, wait=True) + fsamx.read_only = True + time.sleep(1) + + self.socket_put("l1") + time.sleep(0.4) + + if not self.feedback_is_running(): + print("Feedback is not running; likely an error in the interferometer.") + raise RtError("Feedback is not running; likely an error in the interferometer.") + + time.sleep(1.5) + self.show_cyclic_error_compensation() + + self.rt_pid_voltage = self.get_pid_x() + rtx = self.get_device_manager().devices.rtx + rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage}) + + self.set_device_enabled("fsamx", False) + self.set_device_enabled("fsamy", False) + self.set_device_enabled("foptx", False) + self.set_device_enabled("fopty", False) + + def move_samx_to_scan_region(self, fovx: float, cenx: float): + time.sleep(0.05) + if self.rt_pid_voltage is None: + rtx = self.get_device_manager().devices.rtx + self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage") + if self.rt_pid_voltage is None: + raise RtError( + "rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first." + ) + logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}") + expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100 + logger.info(f"Expected PID voltage: {expected_voltage}") + logger.info(f"Current PID voltage: {self.get_pid_x()}") + + wait_on_exit = False + while True: + if np.abs(self.get_pid_x() - expected_voltage) < 1: + break + wait_on_exit = True + self.socket_put("v0") + fsamx = self.get_device_manager().devices.fsamx + fsamx.read_only = False + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") + fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007 + logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}") + fsamx_in = fsamx.user_parameter.get("in") + fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True) + fsamx.read_only = True + time.sleep(0.1) + self.laser_tracker_on() + time.sleep(0.01) + + if wait_on_exit: + time.sleep(1) + + self.socket_put("v1") + + @threadlocked + def clear_trajectory_generator(self): + self.socket_put("sc") + logger.info("flomni scan stopped and deleted, moving to start position") + + def feedback_enable_without_reset(self): + self.laser_tracker_on() + self.socket_put("l3") + time.sleep(0.01) + + if not self.feedback_is_running(): + print("Feedback is not running; likely an error in the interferometer.") + raise RtError("Feedback is not running; likely an error in the interferometer.") + + self.set_device_enabled("fsamx", False) + self.set_device_enabled("fsamy", False) + self.set_device_enabled("foptx", False) + self.set_device_enabled("fopty", False) + + def feedback_disable(self): + self.clear_trajectory_generator() + self.move_to_zero() + self.socket_put("l0") + + self.set_device_enabled("fsamx", True) + self.set_device_enabled("fsamy", True) + self.set_device_enabled("foptx", True) + self.set_device_enabled("fopty", True) + + fsamx = self.get_device_manager().devices.fsamx + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]") + print("rt feedback is now disalbed.") + + def get_pid_x(self) -> float: + voltage = float(self.socket_put_and_receive("g").strip()) + return voltage + + def show_cyclic_error_compensation(self): + cec0 = int(float(self.socket_put_and_receive("w0").strip())) + cec1 = int(float(self.socket_put_and_receive("w1").strip())) + + if cec0 == 32: + logger.info("Cyclic Error Compensation: y-axis is initialized") + else: + logger.info("Cyclic Error Compensation: y-axis is NOT initialized") + print("Cyclic Error Compensation: y-axis is NOT initialized") + if cec1 == 32: + logger.info("Cyclic Error Compensation: x-axis is initialized") + else: + logger.info("Cyclic Error Compensation: x-axis is NOT initialized") + print("Cyclic Error Compensation: x-axis is NOT initialized") + + def set_rotation_angle(self, val: float) -> None: + self.socket_put(f"a{val/180*np.pi}") + + def laser_tracker_check_enabled(self) -> bool: + self.laser_update_tracker_info() + if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]: + return True + else: + return False + + def laser_tracker_on(self): + if not self.laser_tracker_check_enabled(): + logger.info("Enabling the laser tracker. Please wait...") + print("Enabling the laser tracker. Please wait...") + + tracker_intensity = self.tracker_info["tracker_intensity"] + if ( + tracker_intensity < self.tracker_info["threshold_intensity_y"] + or tracker_intensity < self.tracker_info["threshold_intensity_z"] + ): + logger.info(self.tracker_info) + print("The tracker cannot be enabled because the beam intensity it low.") + raise RtError("The tracker cannot be enabled because the beam intensity it low.") + + self.move_to_zero() + self.socket_put("T1") + time.sleep(0.5) + + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed( + "trackyct=0" + ) + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed( + "trackzct=0" + ) + + self.laser_tracker_wait_on_target() + logger.info("Laser tracker running!") + print("Laser tracker running!") + + def laser_tracker_off(self): + if self.feedback_is_running(): + print( + "Interferometer feedback is running. Cannot disable the tracker. First disable the feedback using rt_feedback_disable()" + ) + else: + self.socket_put("T0") + logger.info("Disabled the laser tracker") + print("Disabled the laser tracker") + + def laser_tracker_show_all(self): + self.laser_update_tracker_info() + t = PrettyTable() + t.title = f"Laser Tracker Info" + t.field_names = ["Name", "Value"] + for key, val in self.tracker_info.items(): + t.add_row([key, val]) + print(t) + + def laser_update_tracker_info(self): + ret = self.socket_put_and_receive("Ts") + + # remove trailing \n + ret = ret.split("\n")[0] + + tracker_values = [float(val) for val in ret.split(",")] + self.tracker_info = { + "tracker_intensity": tracker_values[2], + "threshold_intensity_y": tracker_values[8], + "enabled_y": bool(tracker_values[10]), + "beampos_y": tracker_values[5], + "target_y": tracker_values[6], + "piezo_voltage_y": tracker_values[9], + "threshold_intensity_z": tracker_values[3], + "enabled_z": bool(tracker_values[10]), + "beampos_z": tracker_values[0], + "target_z": tracker_values[1], + "piezo_voltage_z": tracker_values[4], + } + + def laser_tracker_galil_enable(self): + ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller + ftrackz_con.socket_put_confirmed("tracken=1") + ftrackz_con.socket_put_confirmed("trackyct=0") + ftrackz_con.socket_put_confirmed("trackzct=0") + ftrackz_con.socket_put_confirmed("XQ#Tracker") + + def laser_tracker_on_target(self) -> bool: + self.laser_update_tracker_info() + if np.isclose( + self.tracker_info["beampos_y"], self.tracker_info["target_y"], atol=0.02 + ) and np.isclose(self.tracker_info["beampos_z"], self.tracker_info["target_z"], atol=0.02): + return True + return False + + def laser_tracker_wait_on_target(self): + max_repeat = 25 + count = 0 + while not self.laser_tracker_on_target(): + self.laser_tracker_galil_enable() + logger.info("Waiting for laser tracker to reach target.") + time.sleep(0.5) + count += 1 + if count > max_repeat: + print("Failed to reach laser target position.") + raise RtError("Failed to reach laser target position.") + + def slew_rate_limiters_on_target(self) -> bool: + ret = int(float(self.socket_put_and_receive("y").strip())) + if ret == 3: + return True + return False + + def pid_y(self) -> float: + ret = float(self.socket_put_and_receive("G").strip()) + return ret + + def read_ssi_interferometer(self, axis_number): + val = float(self.socket_put_and_receive(f"j{axis_number}").strip()) + return val + + def laser_tracker_check_signalstrength(self): + if not self.laser_tracker_check_enabled(): + returnval = "disabled" + else: + returnval = "ok" + self.laser_tracker_wait_on_target() + + signal = self.read_ssi_interferometer(1) + rtx = self.get_device_manager().devices.rtx + min_signal = rtx.user_parameter.get("min_signal") + low_signal = rtx.user_parameter.get("low_signal") + if signal < min_signal: + time.sleep(1) + if signal < min_signal: + print( + f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m" + ) + returnval = "toolow" + # raise RtError("The interferometer signal of tracker is too low.") + elif signal < low_signal: + print( + f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m" + ) + returnval = "low" + return returnval + + def show_signal_strength_interferometer(self): + t = PrettyTable() + t.title = f"Interferometer signal strength" + t.field_names = ["Axis", "Value"] + for i in range(4): + t.add_row([i, self.read_ssi_interferometer(i)]) + print(t) + + def _get_signals_from_table(self, return_table) -> dict: + self.average_stdeviations_x_st_fzp += float(return_table[4]) + self.average_stdeviations_y_st_fzp += float(return_table[7]) + signals = { + "target_x": {"value": float(return_table[2])}, + "average_x_st_fzp": {"value": float(return_table[3])}, + "stdev_x_st_fzp": {"value": float(return_table[4])}, + "target_y": {"value": float(return_table[5])}, + "average_y_st_fzp": {"value": float(return_table[6])}, + "stdev_y_st_fzp": {"value": float(return_table[7])}, + "average_rotz": {"value": float(return_table[8])}, + "stdev_rotz": {"value": float(return_table[9])}, + "average_stdeviations_x_st_fzp": { + "value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1) + }, + "average_stdeviations_y_st_fzp": { + "value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1) + }, + } + return signals + + @threadlocked + def start_scan(self): + if not self.feedback_is_running(): + logger.error( + "Cannot start scan because feedback loop is not running or there is an" + " interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an" + " interferometer error." + ) + # here exception + (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() + + if number_of_positions_planned == 0: + logger.error("Cannot start scan because no target positions are planned.") + raise RtError("Cannot start scan because no target positions are planned.") + # hier exception + # start a point-by-point scan (for cont scan in flomni it would be "sa") + self.socket_put_and_receive("sd") + + @retry_once + @threadlocked + def get_scan_status(self): + return_table = (self.socket_put_and_receive("sr")).split(",") + if len(return_table) != 3: + raise RtCommunicationError( + f"Expected to receive 3 return values. Instead received {return_table}" + ) + mode = int(float(return_table[0])) + # mode 0: direct positioning + # mode 1: running internal timer (not tested/used anymore) + # mode 2: rt point scan running + # mode 3: rt point scan starting + # mode 5/6: rt continuous scanning (not available in LamNI) + number_of_positions_planned = int(float(return_table[1])) + current_position_in_scan = int(float(return_table[2])) + return (mode, number_of_positions_planned, current_position_in_scan) + + def get_device_manager(self): + for axis in self._axis: + if hasattr(axis, "device_manager") and axis.device_manager: + return axis.device_manager + raise BECConfigError("Could not access the device_manager") + + def read_positions_from_sampler(self): + # this was for reading after the scan completed + number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + + read_counter = 0 + + self.average_stdeviations_x_st_fzp = 0 + self.average_stdeviations_y_st_fzp = 0 + self.average_lamni_angle = 0 + + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + + # if not (mode==2 or mode==3): + # error + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=1, metadata=self.readout_metadata + ).dumps(), + ) + # while scan is running + while mode > 0: + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + time.sleep(0.01) + if current_position_in_scan > 5: + while current_position_in_scan > read_counter + 1: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + # logger.info(f"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + time.sleep(0.05) + + # read the last samples even though scan is finished already + while number_of_positions_planned > read_counter: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + # logger.info(f"{return_table}") + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=0, metadata=self.readout_metadata + ).dumps(), + ) + + logger.info( + "Flomni statistics: Average of all standard deviations: x" + f" {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y" + f" {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}." + ) + + def publish_device_data(self, signals, point_id): + self.get_device_manager().connector.set_and_publish( + MessageEndpoints.device_read("rt_flomni"), + messages.DeviceMessage( + signals=signals, metadata={"point_id": point_id, **self.readout_metadata} + ).dumps(), + ) + + def start_readout(self): + readout = threading.Thread(target=self.read_positions_from_sampler) + readout.start() + + def kickoff(self, metadata): + self.readout_metadata = metadata + while not self._min_scan_buffer_reached: + time.sleep(0.001) + self.start_scan() + time.sleep(0.1) + self.start_readout() + + +class RtFlomniReadbackSignal(RtReadbackSignal): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + time.sleep(0.1) + return_table = (self.controller.socket_put_and_receive(f"pr")).split(",") + + current_pos = float(return_table[self.parent.axis_Id_numeric]) + + current_pos *= self.parent.sign + self.parent.user_setpoint.setpoint = current_pos + return current_pos + + +class RtFlomniSetpointSignal(RtSetpointSignal): + setpoint = 0 + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + RtError: Raised if interferometer feedback is disabled. + + """ + if not self.parent.controller.feedback_is_running(): + print( + "The interferometer feedback is not running. Either it is turned off or and" + " interferometer error occured." + ) + raise RtError( + "The interferometer feedback is not running. Either it is turned off or and" + " interferometer error occured." + ) + self.set_with_feedback_disabled(val) + + def set_with_feedback_disabled(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}") + + +class RtFlomniFeedbackRunning(RtSignalRO): + @threadlocked + def _socket_get(self): + return int(self.parent.controller.feedback_is_running()) + + +class RtFlomniMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(RtFlomniReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(RtFlomniSetpointSignal, signal_name="setpoint") + + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2844.psi.ch", + port=2222, + sign=1, + socket_cls=SocketIO, + device_manager=None, + limits=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = RtFlomniController( + socket_cls=socket_cls, socket_host=host, socket_port=port + ) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.device_manager = device_manager + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + self._stopped = False + + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while not self.controller.slew_rate_limiters_on_target() and not self._stopped: + print("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.01) + print("Move finished") + self._done_moving(success=(not self._stopped)) + + self._stopped = False + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + def kickoff(self, metadata, **kwargs) -> None: + self.controller.kickoff(metadata) + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "um" + + # how is this used later? + + def stage(self) -> List[object]: + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + self._stopped = True + return super().stop(success=success) + + +if __name__ == "__main__": + rtcontroller = RtFlomniController( + socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222 + ) + rtcontroller.on() + rtcontroller.laser_tracker_on() diff --git a/csaxs_bec/devices/rt_lamni/rt_lamni_ophyd.py b/csaxs_bec/devices/rt_lamni/rt_lamni_ophyd.py new file mode 100644 index 0000000..e206596 --- /dev/null +++ b/csaxs_bec/devices/rt_lamni/rt_lamni_ophyd.py @@ -0,0 +1,847 @@ +import functools +import threading +import time + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected + +logger = bec_logger.logger + + +class RtLamniCommunicationError(Exception): + pass + + +class RtLamniError(Exception): + pass + + +class BECConfigError(Exception): + pass + + +def retry_once(fcn): + """Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (RtLamniCommunicationError, RtLamniError): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class RtLamniController(Controller): + USER_ACCESS = [ + "socket_put_and_receive", + "set_rotation_angle", + "feedback_disable", + "feedback_enable_without_reset", + "feedback_disable_and_even_reset_lamni_angle_interferometer", + "feedback_enable_with_reset", + "add_pos_to_scan", + "clear_trajectory_generator", + "_set_axis_velocity", + "_set_axis_velocity_maximum_speed", + "_position_sampling_single_read", + "_position_sampling_single_reset_and_start_sampling", + ] + + def __init__( + self, + *, + name="RtLamniController", + kind=None, + parent=None, + socket=None, + attr_name="", + labels=None, + ): + if not hasattr(self, "_initialized") or not self._initialized: + self._rtlamni_axis_per_controller = 3 + self._axis = [None for axis_num in range(self._rtlamni_axis_per_controller)] + self._min_scan_buffer_reached = False + super().__init__( + name=name, + socket=socket, + attr_name=attr_name, + parent=parent, + labels=labels, + kind=kind, + ) + self.readout_metadata = {} + + def on(self, controller_num=0) -> None: + """Open a new socket connection to the controller""" + if not self.connected: + try: + self.sock.open() + # discuss - after disconnect takes a while for the server to be ready again + max_retries = 10 + tries = 0 + while not self.connected: + try: + welcome_message = self.sock.receive() + self.connected = True + except ConnectionResetError as conn_reset: + if tries > max_retries: + raise conn_reset + tries += 1 + time.sleep(2) + except ConnectionRefusedError as conn_error: + logger.error("Failed to open a connection to RTLamNI.") + raise RtLamniCommunicationError from conn_error + + else: + logger.info("The connection has already been established.") + # warnings.warn(f"The connection has already been established.", stacklevel=2) + + self._update_flyer_device_info() + + def off(self) -> None: + """Close the socket connection to the controller""" + if self.connected: + self.sock.close() + self.connected = False + else: + logger.info("The connection is already closed.") + + def set_axis(self, axis: Device, axis_nr: int) -> None: + """Assign an axis to a device instance. + + Args: + axis (Device): Device instance (e.g. GalilMotor) + axis_nr (int): Controller axis number + + """ + self._axis[axis_nr] = axis + + @threadlocked + def socket_put(self, val: str) -> None: + self.sock.put(f"{val}\n".encode()) + + @threadlocked + def socket_get(self) -> str: + return self.sock.receive().decode() + + @retry_once + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + if remove_trailing_chars: + return self._remove_trailing_characters(self.sock.receive().decode()) + return self.socket_get() + + def is_axis_moving(self, axis_Id) -> bool: + # this checks that axis is on target + axis_is_on_target = bool(float(self.socket_put_and_receive(f"o"))) + return not axis_is_on_target + + # def is_thread_active(self, thread_id: int) -> bool: + # val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}")) + # if val == -1: + # return False + # return True + + def _remove_trailing_characters(self, var) -> str: + if len(var) > 1: + return var.split("\r\n")[0] + return var + + @threadlocked + def set_rotation_angle(self, val: float): + self.socket_put(f"a{(val-300+30.538)/180*np.pi}") + + @threadlocked + def stop_all_axes(self): + self.socket_put("sc") + + @threadlocked + def feedback_disable(self): + self.socket_put("J0") + logger.info("LamNI Feedback disabled.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + @threadlocked + def _set_axis_velocity(self, um_per_s): + self.socket_put(f"V{um_per_s}") + + @threadlocked + def _set_axis_velocity_maximum_speed(self): + self.socket_put(f"V0") + + # for developement of soft continuous scanning + @threadlocked + def _position_sampling_single_reset_and_start_sampling(self): + self.socket_put(f"Ss") + + @threadlocked + def _position_sampling_single_read(self): + (number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive( + f"Sr" + ).split(",") + avg_x = float(sum1) / int(number_of_samples) + avg_y = float(sum0) / int(number_of_samples) + stdev_x = np.sqrt( + float(sum1_2) / int(number_of_samples) + - np.power(float(sum1) / int(number_of_samples), 2) + ) + stdev_y = np.sqrt( + float(sum0_2) / int(number_of_samples) + - np.power(float(sum0) / int(number_of_samples), 2) + ) + return (avg_x, avg_y, stdev_x, stdev_y) + + @threadlocked + def feedback_enable_without_reset(self): + # read current interferometer position + return_table = (self.socket_put_and_receive(f"J4")).split(",") + x_curr = float(return_table[2]) + y_curr = float(return_table[1]) + # set these as closed loop target position + self.socket_put(f"pa0,{x_curr:.4f}") + self.socket_put(f"pa1,{y_curr:.4f}") + self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr) + self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr) + self.socket_put("J5") + logger.info("LamNI Feedback enabled (without reset).") + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + @threadlocked + def feedback_disable_and_even_reset_lamni_angle_interferometer(self): + self.socket_put("J6") + logger.info("LamNI Feedback disabled including the angular interferometer.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + def get_device_manager(self): + for axis in self._axis: + if hasattr(axis, "device_manager") and axis.device_manager: + return axis.device_manager + raise BECConfigError("Could not access the device_manager") + + def get_axis_by_name(self, name): + for axis in self._axis: + if axis: + if axis.name == name: + return axis + raise RuntimeError(f"Could not find an axis with name {name}") + + @threadlocked + def clear_trajectory_generator(self): + self.socket_put("sc") + logger.info("LamNI scan stopped and deleted, moving to start position") + + def add_pos_to_scan(self, positions) -> None: + def send_positions(parent, positions): + parent._min_scan_buffer_reached = False + for pos_index, pos in enumerate(positions): + parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:05f},0") + if pos_index > 100: + parent._min_scan_buffer_reached = True + parent._min_scan_buffer_reached = True + + threading.Thread(target=send_positions, args=(self, positions), daemon=True).start() + + @retry_once + @threadlocked + def get_scan_status(self): + return_table = (self.socket_put_and_receive(f"sr")).split(",") + if len(return_table) != 3: + raise RtLamniCommunicationError( + f"Expected to receive 3 return values. Instead received {return_table}" + ) + mode = int(return_table[0]) + # mode 0: direct positioning + # mode 1: running internal timer (not tested/used anymore) + # mode 2: rt point scan running + # mode 3: rt point scan starting + # mode 5/6: rt continuous scanning (not available in LamNI) + number_of_positions_planned = int(return_table[1]) + current_position_in_scan = int(return_table[2]) + return (mode, number_of_positions_planned, current_position_in_scan) + + @threadlocked + def start_scan(self): + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtLamniError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + # here exception + (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() + + if number_of_positions_planned == 0: + logger.error("Cannot start scan because no target positions are planned.") + raise RtLamniError("Cannot start scan because no target positions are planned.") + # hier exception + # start a point-by-point scan (for cont scan in flomni it would be "sa") + self.socket_put_and_receive("sd") + + def start_readout(self): + readout = threading.Thread(target=self.read_positions_from_sampler) + readout.start() + + def _update_flyer_device_info(self): + flyer_info = self._get_flyer_device_info() + self.get_device_manager().connector.set( + MessageEndpoints.device_info("rt_scan"), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info), + ) + + def _get_flyer_device_info(self) -> dict: + return { + "device_name": self.name, + "device_attr_name": getattr(self, "attr_name", ""), + "device_dotted_name": getattr(self, "dotted_name", ""), + "device_info": { + "device_base_class": "ophydobject", + "signals": [], + "hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]}, + "describe": {}, + "describe_configuration": {}, + "sub_devices": [], + "custom_user_access": [], + }, + } + + def kickoff(self, metadata): + self.readout_metadata = metadata + while not self._min_scan_buffer_reached: + time.sleep(0.001) + self.start_scan() + time.sleep(0.1) + self.start_readout() + + def _get_signals_from_table(self, return_table) -> dict: + self.average_stdeviations_x_st_fzp += float(return_table[5]) + self.average_stdeviations_y_st_fzp += float(return_table[8]) + self.average_lamni_angle += float(return_table[19]) + signals = { + "target_x": {"value": float(return_table[3])}, + "average_x_st_fzp": {"value": float(return_table[4])}, + "stdev_x_st_fzp": {"value": float(return_table[5])}, + "target_y": {"value": float(return_table[6])}, + "average_y_st_fzp": {"value": float(return_table[7])}, + "stdev_y_st_fzp": {"value": float(return_table[8])}, + "average_cap1": {"value": float(return_table[9])}, + "stdev_cap1": {"value": float(return_table[10])}, + "average_cap2": {"value": float(return_table[11])}, + "stdev_cap2": {"value": float(return_table[12])}, + "average_cap3": {"value": float(return_table[13])}, + "stdev_cap3": {"value": float(return_table[14])}, + "average_cap4": {"value": float(return_table[15])}, + "stdev_cap4": {"value": float(return_table[16])}, + "average_cap5": {"value": float(return_table[17])}, + "stdev_cap5": {"value": float(return_table[18])}, + "average_angle_interf_ST": {"value": float(return_table[19])}, + "stdev_angle_interf_ST": {"value": float(return_table[20])}, + "average_stdeviations_x_st_fzp": { + "value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1) + }, + "average_stdeviations_y_st_fzp": { + "value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1) + }, + "average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)}, + } + return signals + + def read_positions_from_sampler(self): + # this was for reading after the scan completed + number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + + read_counter = 0 + previous_point_in_scan = 0 + + self.average_stdeviations_x_st_fzp = 0 + self.average_stdeviations_y_st_fzp = 0 + self.average_lamni_angle = 0 + + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + + # if not (mode==2 or mode==3): + # error + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=1, metadata=self.readout_metadata + ), + ) + # while scan is running + while mode > 0: + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + time.sleep(0.01) + if current_position_in_scan > 5: + while current_position_in_scan > read_counter + 1: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + # logger.info(f"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + time.sleep(0.05) + + # read the last samples even though scan is finished already + while number_of_positions_planned > read_counter: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + # logger.info(f"{return_table}") + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=0, metadata=self.readout_metadata + ), + ) + + logger.info( + f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}." + ) + + def publish_device_data(self, signals, point_id): + self.get_device_manager().connector.set_and_publish( + MessageEndpoints.device_read("rt_lamni"), + messages.DeviceMessage( + signals=signals, metadata={"point_id": point_id, **self.readout_metadata} + ), + ) + + def feedback_status_angle_lamni(self) -> bool: + return_table = (self.socket_put_and_receive(f"J7")).split(",") + logger.debug( + f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}" + ) + return bool(return_table[0]) + + def feedback_enable_with_reset(self): + if not self.feedback_status_angle_lamni(): + self.feedback_disable_and_even_reset_lamni_angle_interferometer() + logger.info(f"LamNI resetting interferometer inclusive angular interferomter.") + else: + self.feedback_disable() + logger.info( + f"LamNI resetting interferomter except angular interferometer which is already running." + ) + + # set these as closed loop target position + + self.socket_put(f"pa0,0") + self.get_axis_by_name("rtx").user_setpoint.setpoint = 0 + self.socket_put(f"pa1,0") + self.get_axis_by_name("rty").user_setpoint.setpoint = 0 + self.socket_put( + f"pa2,0" + ) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used + self.clear_trajectory_generator() + + self.get_device_manager().devices.lsamrot.obj.move(0, wait=True) + + galil_controller_rt_status = ( + self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled() + ) + + if galil_controller_rt_status == 0: + logger.error( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + raise RtLamniError( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + + time.sleep(0.03) + + lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter + if lsamx_user_params is None or lsamx_user_params.get("center") is None: + raise RuntimeError("lsamx center is not defined") + lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter + if lsamy_user_params is None or lsamy_user_params.get("center") is None: + raise RuntimeError("lsamy center is not defined") + lsamx_center = lsamx_user_params.get("center") + lsamy_center = lsamy_user_params.get("center") + self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True) + self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True) + self.socket_put("J1") + + _waitforfeedbackctr = 0 + + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + + while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100: + time.sleep(0.01) + _waitforfeedbackctr = _waitforfeedbackctr + 1 + interferometer_feedback_not_running = int( + (self.socket_put_and_receive("J2")).split(",")[0] + ) + + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtLamniError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + + time.sleep(0.01) + + # ptychography_alignment_done = 0 + + def set_device_enabled(self, device_name: str, enabled: bool) -> None: + """enable / disable a device""" + if device_name not in self.get_device_manager().devices: + logger.warning( + f"Device {device_name} is not configured and cannot be enabled/disabled." + ) + return + self.get_device_manager().devices[device_name].read_only = not enabled + + +class RtLamniSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class RtLamniSignalRO(RtLamniSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class RtLamniReadbackSignal(RtLamniSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + return_table = (self.controller.socket_put_and_receive(f"J4")).split(",") + print(return_table) + if self.parent.axis_Id_numeric == 0: + readback_index = 2 + elif self.parent.axis_Id_numeric == 1: + readback_index = 1 + else: + raise RtLamniError("Currently, only two axes are supported.") + + current_pos = float(return_table[readback_index]) + + current_pos *= self.parent.sign + return current_pos + + +class RtLamniSetpointSignal(RtLamniSignalBase): + setpoint = 0 + + def _socket_get(self) -> float: + """Get command for receiving the setpoint / target value. + The value is not pulled from the controller but instead just the last setpoint used. + + Returns: + float: setpoint / target value + """ + return self.setpoint + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + RtLamniError: Raised if interferometer feedback is disabled. + + """ + interferometer_feedback_not_running = int( + (self.controller.socket_put_and_receive("J2")).split(",")[0] + ) + if interferometer_feedback_not_running != 0: + raise RtLamniError( + "The interferometer feedback is not running. Either it is turned off or and interferometer error occured." + ) + self.set_with_feedback_disabled(val) + + def set_with_feedback_disabled(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}") + + +class RtLamniMotorIsMoving(RtLamniSignalRO): + def _socket_get(self): + return self.controller.is_axis_moving(self.parent.axis_Id_numeric) + + def get(self): + val = super().get() + if val is not None: + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) + return val + + +class RtLamniFeedbackRunning(RtLamniSignalRO): + @threadlocked + def _socket_get(self): + if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0: + return 1 + else: + return 0 + + +class RtLamniMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint") + + motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=3333, + sign=1, + socket_cls=SocketIO, + device_manager=None, + limits=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = RtLamniController(socket=socket_cls(host=host, port=port)) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.device_manager = device_manager + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + print("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.01) + print("Move finished") + self._done_moving() + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + def kickoff(self, metadata, **kwargs) -> None: + self.controller.kickoff(metadata) + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "um" + + # how is this used later? + + def stage(self) -> list[object]: + return super().stage() + + def unstage(self) -> list[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + mock = False + if not mock: + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1) + rty.stage() + status = rty.move(0, wait=True) + status = rty.move(10, wait=True) + rty.read() + + rty.get() + rty.describe() + + rty.unstage() + else: + from ophyd_devices.utils.socket import SocketMock + + rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rtx.stage() + # rty.stage() diff --git a/csaxs_bec/devices/rt_lamni/rt_ophyd.py b/csaxs_bec/devices/rt_lamni/rt_ophyd.py new file mode 100644 index 0000000..c3fa81b --- /dev/null +++ b/csaxs_bec/devices/rt_lamni/rt_ophyd.py @@ -0,0 +1,817 @@ +import functools +import threading +import time +from typing import List + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected + +logger = bec_logger.logger + + +class RtCommunicationError(Exception): + pass + + +class RtError(Exception): + pass + + +class BECConfigError(Exception): + pass + + +def retry_once(fcn): + """Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (RtCommunicationError, RtError): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class RtController(Controller): + _axes_per_controller = 3 + USER_ACCESS = [ + "socket_put_and_receive", + "set_rotation_angle", + "feedback_disable", + "feedback_enable_without_reset", + "feedback_disable_and_even_reset_lamni_angle_interferometer", + "feedback_enable_with_reset", + "add_pos_to_scan", + "clear_trajectory_generator", + "_set_axis_velocity", + "_set_axis_velocity_maximum_speed", + "_position_sampling_single_read", + "_position_sampling_single_reset_and_start_sampling", + ] + + def on(self, controller_num=0) -> None: + """Open a new socket connection to the controller""" + # if not self.connected: + # try: + # self.sock.open() + # # discuss - after disconnect takes a while for the server to be ready again + # max_retries = 10 + # tries = 0 + # while not self.connected: + # try: + # welcome_message = self.sock.receive() + # self.connected = True + # except ConnectionResetError as conn_reset: + # if tries > max_retries: + # raise conn_reset + # tries += 1 + # time.sleep(2) + # except ConnectionRefusedError as conn_error: + # logger.error("Failed to open a connection to RTLamNI.") + # raise RtCommunicationError from conn_error + + # else: + # logger.info("The connection has already been established.") + # # warnings.warn(f"The connection has already been established.", stacklevel=2) + super().on() + # self._update_flyer_device_info() + + def set_axis(self, axis: Device, axis_nr: int) -> None: + """Assign an axis to a device instance. + + Args: + axis (Device): Device instance (e.g. GalilMotor) + axis_nr (int): Controller axis number + + """ + self._axis[axis_nr] = axis + + @threadlocked + def socket_put(self, val: str) -> None: + self.sock.put(f"{val}\n".encode()) + + @threadlocked + def socket_get(self) -> str: + return self.sock.receive().decode() + + @retry_once + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + if remove_trailing_chars: + return self._remove_trailing_characters(self.sock.receive().decode()) + return self.socket_get() + + def is_axis_moving(self, axis_Id) -> bool: + # this checks that axis is on target + axis_is_on_target = bool(float(self.socket_put_and_receive(f"o"))) + return not axis_is_on_target + + # def is_thread_active(self, thread_id: int) -> bool: + # val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}")) + # if val == -1: + # return False + # return True + + def _remove_trailing_characters(self, var) -> str: + if len(var) > 1: + return var.split("\r\n")[0] + return var + + @threadlocked + def set_rotation_angle(self, val: float): + self.socket_put(f"a{(val-300+30.538)/180*np.pi}") + + @threadlocked + def stop_all_axes(self): + self.socket_put("sc") + + @threadlocked + def feedback_disable(self): + self.socket_put("J0") + logger.info("LamNI Feedback disabled.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + @threadlocked + def _set_axis_velocity(self, um_per_s): + self.socket_put(f"V{um_per_s}") + + @threadlocked + def _set_axis_velocity_maximum_speed(self): + self.socket_put(f"V0") + + # for developement of soft continuous scanning + @threadlocked + def _position_sampling_single_reset_and_start_sampling(self): + self.socket_put(f"Ss") + + @threadlocked + def _position_sampling_single_read(self): + (number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive( + f"Sr" + ).split(",") + avg_x = float(sum1) / int(number_of_samples) + avg_y = float(sum0) / int(number_of_samples) + stdev_x = np.sqrt( + float(sum1_2) / int(number_of_samples) + - np.power(float(sum1) / int(number_of_samples), 2) + ) + stdev_y = np.sqrt( + float(sum0_2) / int(number_of_samples) + - np.power(float(sum0) / int(number_of_samples), 2) + ) + return (avg_x, avg_y, stdev_x, stdev_y) + + @threadlocked + def feedback_enable_without_reset(self): + # read current interferometer position + return_table = (self.socket_put_and_receive(f"J4")).split(",") + x_curr = float(return_table[2]) + y_curr = float(return_table[1]) + # set these as closed loop target position + self.socket_put(f"pa0,{x_curr:.4f}") + self.socket_put(f"pa1,{y_curr:.4f}") + self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr) + self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr) + self.socket_put("J5") + logger.info("LamNI Feedback enabled (without reset).") + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + @threadlocked + def feedback_disable_and_even_reset_lamni_angle_interferometer(self): + self.socket_put("J6") + logger.info("LamNI Feedback disabled including the angular interferometer.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + def get_device_manager(self): + for axis in self._axis: + if hasattr(axis, "device_manager") and axis.device_manager: + return axis.device_manager + raise BECConfigError("Could not access the device_manager") + + def get_axis_by_name(self, name): + for axis in self._axis: + if axis: + if axis.name == name: + return axis + raise RuntimeError(f"Could not find an axis with name {name}") + + @threadlocked + def clear_trajectory_generator(self): + self.socket_put("sc") + logger.info("LamNI scan stopped and deleted, moving to start position") + + def add_pos_to_scan(self, positions) -> None: + def send_positions(parent, positions): + parent._min_scan_buffer_reached = False + for pos_index, pos in enumerate(positions): + parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},0") + if pos_index > 100: + parent._min_scan_buffer_reached = True + parent._min_scan_buffer_reached = True + + threading.Thread(target=send_positions, args=(self, positions), daemon=True).start() + + @retry_once + @threadlocked + def get_scan_status(self): + return_table = (self.socket_put_and_receive(f"sr")).split(",") + if len(return_table) != 3: + raise RtCommunicationError( + f"Expected to receive 3 return values. Instead received {return_table}" + ) + mode = int(return_table[0]) + # mode 0: direct positioning + # mode 1: running internal timer (not tested/used anymore) + # mode 2: rt point scan running + # mode 3: rt point scan starting + # mode 5/6: rt continuous scanning (not available in LamNI) + number_of_positions_planned = int(return_table[1]) + current_position_in_scan = int(return_table[2]) + return (mode, number_of_positions_planned, current_position_in_scan) + + @threadlocked + def start_scan(self): + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + # here exception + (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() + + if number_of_positions_planned == 0: + logger.error("Cannot start scan because no target positions are planned.") + raise RtError("Cannot start scan because no target positions are planned.") + # hier exception + # start a point-by-point scan (for cont scan in flomni it would be "sa") + self.socket_put_and_receive("sd") + + def start_readout(self): + readout = threading.Thread(target=self.read_positions_from_sampler) + readout.start() + + def _update_flyer_device_info(self): + flyer_info = self._get_flyer_device_info() + self.get_device_manager().connector.set( + MessageEndpoints.device_info("rt_scan"), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), + ) + + def _get_flyer_device_info(self) -> dict: + return { + "device_name": self.name, + "device_attr_name": getattr(self, "attr_name", ""), + "device_dotted_name": getattr(self, "dotted_name", ""), + "device_info": { + "device_base_class": "ophydobject", + "signals": [], + "hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]}, + "describe": {}, + "describe_configuration": {}, + "sub_devices": [], + "custom_user_access": [], + }, + } + + def kickoff(self, metadata): + self.readout_metadata = metadata + while not self._min_scan_buffer_reached: + time.sleep(0.001) + self.start_scan() + time.sleep(0.1) + self.start_readout() + + def _get_signals_from_table(self, return_table) -> dict: + self.average_stdeviations_x_st_fzp += float(return_table[5]) + self.average_stdeviations_y_st_fzp += float(return_table[8]) + self.average_lamni_angle += float(return_table[19]) + signals = { + "target_x": {"value": float(return_table[3])}, + "average_x_st_fzp": {"value": float(return_table[4])}, + "stdev_x_st_fzp": {"value": float(return_table[5])}, + "target_y": {"value": float(return_table[6])}, + "average_y_st_fzp": {"value": float(return_table[7])}, + "stdev_y_st_fzp": {"value": float(return_table[8])}, + "average_cap1": {"value": float(return_table[9])}, + "stdev_cap1": {"value": float(return_table[10])}, + "average_cap2": {"value": float(return_table[11])}, + "stdev_cap2": {"value": float(return_table[12])}, + "average_cap3": {"value": float(return_table[13])}, + "stdev_cap3": {"value": float(return_table[14])}, + "average_cap4": {"value": float(return_table[15])}, + "stdev_cap4": {"value": float(return_table[16])}, + "average_cap5": {"value": float(return_table[17])}, + "stdev_cap5": {"value": float(return_table[18])}, + "average_angle_interf_ST": {"value": float(return_table[19])}, + "stdev_angle_interf_ST": {"value": float(return_table[20])}, + "average_stdeviations_x_st_fzp": { + "value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1) + }, + "average_stdeviations_y_st_fzp": { + "value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1) + }, + "average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)}, + } + return signals + + def read_positions_from_sampler(self): + # this was for reading after the scan completed + number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + + read_counter = 0 + previous_point_in_scan = 0 + + self.average_stdeviations_x_st_fzp = 0 + self.average_stdeviations_y_st_fzp = 0 + self.average_lamni_angle = 0 + + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + + # if not (mode==2 or mode==3): + # error + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=1, metadata=self.readout_metadata + ).dumps(), + ) + # while scan is running + while mode > 0: + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + time.sleep(0.01) + if current_position_in_scan > 5: + while current_position_in_scan > read_counter + 1: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + # logger.info(f"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + time.sleep(0.05) + + # read the last samples even though scan is finished already + while number_of_positions_planned > read_counter: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + # logger.info(f"{return_table}") + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + self.publish_device_data(signals=signals, point_id=int(return_table[0])) + + self.get_device_manager().connector.set( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=0, metadata=self.readout_metadata + ).dumps(), + ) + + logger.info( + f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}." + ) + + def publish_device_data(self, signals, point_id): + self.get_device_manager().connector.set_and_publish( + MessageEndpoints.device_read("rt_lamni"), + messages.DeviceMessage( + signals=signals, metadata={"point_id": point_id, **self.readout_metadata} + ).dumps(), + ) + + def feedback_status_angle_lamni(self) -> bool: + return_table = (self.socket_put_and_receive(f"J7")).split(",") + logger.debug( + f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}" + ) + return bool(return_table[0]) + + def feedback_enable_with_reset(self): + if not self.feedback_status_angle_lamni(): + self.feedback_disable_and_even_reset_lamni_angle_interferometer() + logger.info(f"LamNI resetting interferometer inclusive angular interferomter.") + else: + self.feedback_disable() + logger.info( + f"LamNI resetting interferomter except angular interferometer which is already running." + ) + + # set these as closed loop target position + + self.socket_put(f"pa0,0") + self.get_axis_by_name("rtx").user_setpoint.setpoint = 0 + self.socket_put(f"pa1,0") + self.get_axis_by_name("rty").user_setpoint.setpoint = 0 + self.socket_put( + f"pa2,0" + ) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used + self.clear_trajectory_generator() + + self.get_device_manager().devices.lsamrot.obj.move(0, wait=True) + + galil_controller_rt_status = ( + self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled() + ) + + if galil_controller_rt_status == 0: + logger.error( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + raise RtError( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + + time.sleep(0.03) + + lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter + if lsamx_user_params is None or lsamx_user_params.get("center") is None: + raise RuntimeError("lsamx center is not defined") + lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter + if lsamy_user_params is None or lsamy_user_params.get("center") is None: + raise RuntimeError("lsamy center is not defined") + lsamx_center = lsamx_user_params.get("center") + lsamy_center = lsamy_user_params.get("center") + self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True) + self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True) + self.socket_put("J1") + + _waitforfeedbackctr = 0 + + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + + while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100: + time.sleep(0.01) + _waitforfeedbackctr = _waitforfeedbackctr + 1 + interferometer_feedback_not_running = int( + (self.socket_put_and_receive("J2")).split(",")[0] + ) + + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + + time.sleep(0.01) + + # ptychography_alignment_done = 0 + + def set_device_enabled(self, device_name: str, enabled: bool) -> None: + """enable / disable a device""" + if device_name not in self.get_device_manager().devices: + logger.warning( + f"Device {device_name} is not configured and cannot be enabled/disabled." + ) + return + self.get_device_manager().devices[device_name].read_only = not enabled + + +class RtSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class RtSignalRO(RtSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class RtReadbackSignal(RtSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + return_table = (self.controller.socket_put_and_receive(f"J4")).split(",") + print(return_table) + if self.parent.axis_Id_numeric == 0: + readback_index = 2 + elif self.parent.axis_Id_numeric == 1: + readback_index = 1 + else: + raise RtError("Currently, only two axes are supported.") + + current_pos = float(return_table[readback_index]) + + current_pos *= self.parent.sign + return current_pos + + +class RtSetpointSignal(RtSignalBase): + setpoint = 0 + + def _socket_get(self) -> float: + """Get command for receiving the setpoint / target value. + The value is not pulled from the controller but instead just the last setpoint used. + + Returns: + float: setpoint / target value + """ + return self.setpoint + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + RtError: Raised if interferometer feedback is disabled. + + """ + interferometer_feedback_not_running = int( + (self.controller.socket_put_and_receive("J2")).split(",")[0] + ) + if interferometer_feedback_not_running != 0: + raise RtError( + "The interferometer feedback is not running. Either it is turned off or and interferometer error occured." + ) + self.set_with_feedback_disabled(val) + + def set_with_feedback_disabled(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}") + + +class RtMotorIsMoving(RtSignalRO): + def _socket_get(self): + return self.controller.is_axis_moving(self.parent.axis_Id_numeric) + + def get(self): + val = super().get() + if val is not None: + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) + return val + + +class RtFeedbackRunning(RtSignalRO): + @threadlocked + def _socket_get(self): + if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0: + return 1 + else: + return 0 + + +class RtMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(RtReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint") + + motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=3333, + sign=1, + socket_cls=SocketIO, + device_manager=None, + limits=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = RtController(socket=socket_cls(host=host, port=port)) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.device_manager = device_manager + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + print("motor is moving") + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.01) + print("Move finished") + self._done_moving() + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + def kickoff(self, metadata, **kwargs) -> None: + self.controller.kickoff(metadata) + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "um" + + # how is this used later? + + def stage(self) -> List[object]: + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + mock = False + if not mock: + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1) + rty.stage() + status = rty.move(0, wait=True) + status = rty.move(10, wait=True) + rty.read() + + rty.get() + rty.describe() + + rty.unstage() + else: + from ophyd_devices.utils.socket import SocketMock + + rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rtx.stage() + # rty.stage() diff --git a/bec_plugins/scan_server/__init__.py b/csaxs_bec/devices/sls_devices/__init__.py similarity index 100% rename from bec_plugins/scan_server/__init__.py rename to csaxs_bec/devices/sls_devices/__init__.py diff --git a/csaxs_bec/devices/sls_devices/cSAXS/__init__.py b/csaxs_bec/devices/sls_devices/cSAXS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/devices/sls_devices/cSAXS/xeye.py b/csaxs_bec/devices/sls_devices/cSAXS/xeye.py new file mode 100644 index 0000000..f62cc60 --- /dev/null +++ b/csaxs_bec/devices/sls_devices/cSAXS/xeye.py @@ -0,0 +1,9 @@ +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal + + +class Xeye(Device): + save_frame = Cpt(EpicsSignal, "XOMNY-XEYE-SAVEFRAME:0") + acquisition_done = Cpt(EpicsSignal, "XOMNY-XEYE-ACQDONE:0") + acquisition = Cpt(EpicsSignal, "XOMNY-XEYE-ACQ:0") + x_width = Cpt(EpicsSignal, "XOMNY-XEYE-XWIDTH_X:0") diff --git a/csaxs_bec/devices/smaract/__init__.py b/csaxs_bec/devices/smaract/__init__.py new file mode 100644 index 0000000..2079813 --- /dev/null +++ b/csaxs_bec/devices/smaract/__init__.py @@ -0,0 +1,2 @@ +from .smaract_controller import SmaractController +from .smaract_ophyd import SmaractMotor diff --git a/csaxs_bec/devices/smaract/serializer.py b/csaxs_bec/devices/smaract/serializer.py new file mode 100644 index 0000000..a22cb81 --- /dev/null +++ b/csaxs_bec/devices/smaract/serializer.py @@ -0,0 +1,44 @@ +import json + +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.ophydobj import OphydObject +from ophyd_devices.utils.socket import SocketMock + + +def get_user_functions(obj) -> list: + exclude_list = ["log", "SUB_CONNECTION_CHANGE"] + exclude_classes = [Device, OphydObject, PositionerBase, Signal, Cpt] + for cls in exclude_classes: + exclude_list.extend(dir(cls)) + access_list = [ + func for func in dir(obj) if not func.startswith("_") and func not in exclude_list + ] + + return access_list + + +def is_serializable(f) -> bool: + try: + json.dumps(f) + return True + except (TypeError, OverflowError): + return False + + +def get_user_interface(obj, obj_interface): + # user_funcs = get_user_functions(obj) + for f in [f for f in dir(obj) if f in obj.USER_ACCESS]: + if f == "controller" or f == "on": + print(f) + m = getattr(obj, f) + if not callable(m): + if is_serializable(m): + obj_interface[f] = {"type": type(m).__name__} + elif isinstance(m, SocketMock): + pass + else: + obj_interface[f] = get_user_interface(m, {}) + else: + obj_interface[f] = {"type": "func"} + return obj_interface diff --git a/csaxs_bec/devices/smaract/smaract_controller.py b/csaxs_bec/devices/smaract/smaract_controller.py new file mode 100644 index 0000000..0c078f8 --- /dev/null +++ b/csaxs_bec/devices/smaract/smaract_controller.py @@ -0,0 +1,507 @@ +import enum +import functools +import json +import logging +import os +import time + +import numpy as np +from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked +from prettytable import PrettyTable +from typeguard import typechecked + +from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode + +logger = logging.getLogger("smaract_controller") + + +class SmaractCommunicationMode(enum.Enum): + SYNC = 0 + ASYNC = 1 + + +def retry_once(fcn): + """Decorator to rerun a function in case a SmaractCommunicationError was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (SmaractCommunicationError, SmaractErrorCode): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class SmaractChannelStatus(enum.Enum): + STOPPED = 0 + STEPPING = 1 + SCANNING = 2 + HOLDING = 3 + TARGETING = 4 + MOVE_DELAY = 5 + CALIBRATING = 6 + FINDING_REFERENCE_MARK = 7 + LOCKED = 9 + + +class SmaractSensorDefinition: + def __init__(self, symbol, type_code, positioner_series, comment, reference_type) -> None: + self.symbol = symbol + self.type_code = type_code + self.comment = comment + self.positioner_series = positioner_series + self.reference_type = reference_type + + +class SmaractSensors: + smaract_sensor_definition_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "smaract_sensors.json" + ) + + def __init__(self) -> None: + self.avail_sensors = {} + + with open(self.smaract_sensor_definition_file) as json_file: + sensor_list = json.load(json_file) + for sensor in sensor_list: + self.avail_sensors[sensor["type_code"]] = SmaractSensorDefinition(**sensor) + + +class SmaractController(Controller): + _axes_per_controller = 6 + _initialized = False + USER_ACCESS = [ + "socket_put_and_receive", + "smaract_show_all", + "move_open_loop_steps", + "find_reference_mark", + "describe", + "axis_is_referenced", + "all_axes_referenced", + ] + + def __init__( + self, + *, + name="SmaractController", + kind=None, + parent=None, + socket_cls=None, + socket_host=None, + socket_port=None, + attr_name="", + labels=None, + ): + if not self._initialized: + super().__init__( + name=name, + socket_cls=socket_cls, + socket_host=socket_host, + socket_port=socket_port, + attr_name=attr_name, + parent=parent, + labels=labels, + kind=kind, + ) + self._sensors = SmaractSensors() + + @threadlocked + def socket_put(self, val: str): + self.sock.put(f":{val}\n".encode()) + + @threadlocked + def socket_get(self): + return self.sock.receive().decode() + + @threadlocked + def socket_put_and_receive( + self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False + ) -> str: + self.socket_put(val) + return_val = "" + max_wait_time = 1 + elapsed_time = 0 + sleep_time = 0.01 + while True: + ret = self.socket_get() + return_val += ret + if ret.endswith("\n"): + break + time.sleep(sleep_time) + elapsed_time += sleep_time + if elapsed_time > max_wait_time: + break + if remove_trailing_chars: + return_val = self._remove_trailing_characters(return_val) + logger.debug(f"Sending {val}; Returned {return_val}") + if check_for_errors: + self._check_for_error(return_val, raise_if_not_status=raise_if_not_status) + return return_val + + @retry_once + @axis_checked + def get_status(self, axis_Id_numeric: int) -> SmaractChannelStatus: + """Returns the current movement status code of a positioner or end effector.This command can be used to check whether a previously issued movement command has been completed. + + Args: + axis_Id_numeric (int): Axis number + + Returns: + SmaractChannelStatus: Channel status + """ + return_val = self.socket_put_and_receive(f"GS{axis_Id_numeric}") + if self._message_starts_with(return_val, f":S{axis_Id_numeric}"): + return SmaractChannelStatus(int(return_val.split(",")[1])) + + @retry_once + @axis_checked + def is_axis_moving(self, axis_Id_numeric: int) -> bool: + """Check if axis is moving. Returns true upon open loop move, scanning, closed loop move or reference mark search. + + Args: + axis_Id_numeric (int): Axis number. + + Returns: + bool: True if axis is moving. + """ + axis_status = self.get_status(axis_Id_numeric) + return axis_status in [ + SmaractChannelStatus.STEPPING, + SmaractChannelStatus.SCANNING, + SmaractChannelStatus.TARGETING, + SmaractChannelStatus.FINDING_REFERENCE_MARK, + ] + + @retry_once + def stop_all_axes(self): + return [ + self.socket_put_and_receive(f"S{ax.axis_Id_numeric}", raise_if_not_status=True) + for ax in self._axis + if ax is not None + ] + + @retry_once + @axis_checked + def axis_is_referenced(self, axis_Id_numeric: int) -> bool: + return_val = self.socket_put_and_receive(f"GPPK{axis_Id_numeric}") + if self._message_starts_with(return_val, f":PPK{axis_Id_numeric}"): + return bool(int(return_val.split(",")[1])) + + def all_axes_referenced(self) -> bool: + return all( + self.axis_is_referenced(ax.axis_Id_numeric) for ax in self._axis if ax is not None + ) + + @retry_once + @axis_checked + def get_position(self, axis_Id_numeric: int) -> float: + """Returns the current position of a positioner. + + Args: + axis_Id_numeric (int): Axis number. + + Returns: + float: Position in mm + """ + return_val = self.socket_put_and_receive(f"GP{axis_Id_numeric}") + if self._message_starts_with(return_val, f":P{axis_Id_numeric}"): + return float(return_val.split(",")[1]) / 1e6 + + @retry_once + @axis_checked + @typechecked + def move_axis_to_absolute_position( + self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000 + ) -> None: + """Instructs a positioner to move to a specific position. + + Args: + axis_Id_numeric (int): Axis number. + target_val (float): Target position in mm. + hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000. + + """ + self.socket_put_and_receive( + f"MPA{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}", + raise_if_not_status=True, + ) + + @retry_once + @axis_checked + @typechecked + def move_axis_to_relative_position( + self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000 + ) -> None: + """Instructs a positioner to move to a position relative to its current position. + + Args: + axis_Id_numeric (int): Axis number. + target_val (float): Relative position to move to in mm. + hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000. + + """ + self.socket_put_and_receive( + f"MPR{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}", + raise_if_not_status=True, + ) + + @retry_once + @axis_checked + @typechecked + def move_open_loop_steps( + self, axis_Id_numeric: int, steps: int, amplitude: int = 4000, frequency: int = 2000 + ) -> None: + """Move open loop steps. It performs a burst of steps with the given parameters. + + Args: + axis_Id_numeric (int): Axis number. + steps (int): Number and direction of steps to perform. The valid range is -30,000..30,000. A value of 0 stops the positioner, but see S command. A value of 30,000 or -30,000 performs an unbounded move. This should be used with caution since the positioner will only stop on an S command. + amplitude (int): Amplitude that the steps are performed with. Lower amplitude values result in a smaller step width. The parameter must be given as a 12bit value (range 0..4,095). 0 corresponds to 0V, 4,095 to 100V. Default: 4000 + frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000. + """ + self.socket_put_and_receive( + f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True + ) + + @retry_once + def get_communication_mode(self) -> SmaractCommunicationMode: + return_val = self.socket_put_and_receive("GCM") + if self._message_starts_with(return_val, f":CM"): + return SmaractCommunicationMode(int(return_val.strip(":CM"))) + + @retry_once + @axis_checked + def get_channel_type(self, axis_Id_numeric) -> str: + return_val = self.socket_put_and_receive(f"GCT{axis_Id_numeric}") + if self._message_starts_with(return_val, f":CT{axis_Id_numeric}"): + return return_val.split(",")[1] + + @retry_once + def get_interface_version(self) -> str: + """This command may be used to retrieve the interface version of the system. It is useful to check if changes + have been made to the software interface. An application may check the version in order to ensure that the + system behaves as the application expects it to do. + + Returns: + str: interface version + """ + return_val = self.socket_put_and_receive("GIV") + if self._message_starts_with(return_val, f":IV"): + return return_val.strip(":IV") + + @retry_once + def get_number_of_channels(self) -> int: + """This command may be used to determine how many control channels are available on a system. This + includes positioner channels and end effector channels. Each channel is of a specific type. Use the GCT + command to determine the types of the channels. + Note that the number of channels does not represent the number positioners and/or end effectors that are + currently connected to the system. + The channel indexes throughout the interface are zero based. If your system has N channels then the valid + range for a channel index is 0.. N-1. + + Returns: + int: number of channels + """ + return_val = self.socket_put_and_receive("GNC") + if self._message_starts_with(return_val, f":N"): + return int(return_val.strip(":N")) + + @retry_once + def get_system_id(self) -> str: + """This command may be used to physically identify a system connected to the PC. Each system has a unique + ID which makes it possible to distinguish one from another. + The ID returned is a generic decimal number that uniquely identifies the system. + + """ + return_val = self.socket_put_and_receive("GSI") + if self._message_starts_with(return_val, f":ID"): + return return_val.strip(":ID") + + @retry_once + def reset(self) -> None: + """When this command is sent the system will perform a reset. It has the same effect as a power down/power + up cycle. The system replies with an acknowledge string before resetting itself. + """ + self.socket_put_and_receive("R", raise_if_not_status=True) + + @retry_once + def set_hcm_mode(self, mode: int): + """If a Hand Control Module (HCM) is connected to the system, this command may be used to enable or + disable it in order to avoid interference while the software is in control of the system. There are three possible + modes to set: + 0: In this mode the Hand Control Module is disabled. It may not be used to control positioners. + 1: This is the default setting where the Hand Control Module may be used to control the positioners. + 2: In this mode the Hand Control Module cannot be used to control the positioners. However, if there + are positioners with sensors attached, their position data will still be displayed. + + Args: + mode (int): HCM mode + + """ + if mode not in range(3): + raise ValueError(f"HCM mode must be 0, 1 or 2. Received: {mode}.") + self.socket_put_and_receive(f"SHE{mode}", raise_if_not_status=True) + + @retry_once + @axis_checked + def get_position_limits(self, axis_Id_numeric: int) -> list: + """May be used to read out the travel range limit that is currently + configured for a linear channel. + + Args: + axis_Id_numeric (int): Axis + + Returns: + list: [low_limit, high_limit] in mm + """ + return_val = self.socket_put_and_receive(f"GPL{axis_Id_numeric}") + if self._message_starts_with(return_val, f":GPL{axis_Id_numeric}"): + return [ + float(limit) / 1e6 + for limit in return_val.strip(f":GPL{axis_Id_numeric},").split(",") + ] + + @retry_once + @axis_checked + def set_position_limits( + self, axis_Id_numeric: int, low_limit: float, high_limit: float + ) -> None: + """For positioners with integrated sensors this command may be used to limit the travel range of a linear + positioner by software. By default there is no limit set. If defined the + positioner will not move beyond the limit. This affects open-loop as well as closed-loop movements. + + Args: + axis_Id_numeric (int): Axis + low_limit (float): low limit in mm + high_limit (float): high limit in mm + + """ + self.socket_put_and_receive( + f"SPL{axis_Id_numeric},{np.round(low_limit*1e6)},{np.round(high_limit*1e6)}", + raise_if_not_status=True, + ) + + @retry_once + @axis_checked + def get_sensor_type(self, axis_Id_numeric: int) -> SmaractSensorDefinition: + return_val = self.socket_put_and_receive(f"GST{axis_Id_numeric}") + if self._message_starts_with(return_val, f":ST{axis_Id_numeric}"): + return self._sensors.avail_sensors.get(int(return_val.strip(f":ST{axis_Id_numeric},"))) + + @retry_once + @axis_checked + def find_reference_mark( + self, axis_Id_numeric: int, direction: int, holdTime: int, autoZero: int + ) -> None: + return_val = self.socket_put_and_receive( + f"FRM{axis_Id_numeric},{direction},{holdTime},{autoZero}" + ) + + @retry_once + @axis_checked + def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None: + """This command configures the speed control feature of a channel for closed-loop commands move_axis_to_absolute_position. By default the speed control is inactive. In this state the behavior of closed-loop commands is influenced by the maximum driving frequency. If a movement speed is configured, all following closed-loop commands will be executed with the new speed. + + Args: + axis_Id_numeric (int): Axis number. + move_speed (float): Movement speed given in mm/s for linear positioners. The valid range is 0 .. 100. A value of 0 (default) deactivates the speed control feature. + """ + move_speed_in_nm_per_s = int(round(move_speed * 1e6)) + + if move_speed_in_nm_per_s > 100e6 or move_speed_in_nm_per_s < 0: + raise ValueError("Move speed must be within 0 to 100 mm/s.") + + self.socket_put_and_receive( + f"SCLS{axis_Id_numeric},{move_speed_in_nm_per_s}", raise_if_not_status=True + ) + + @retry_once + @axis_checked + def get_closed_loop_move_speed(self, axis_Id_numeric: int) -> float: + """Returns the currently configured movement speed that is used for closed-loop commands for a channel. + + Args: + axis_Id_numeric (int): Axis number. + + Returns: + float: move speed in mm/s. A return value of 0 means that the speed control feature is disabled. + """ + + return_val = self.socket_put_and_receive(f"GCLS{axis_Id_numeric}") + if self._message_starts_with(return_val, f":CLS{axis_Id_numeric}"): + return float(return_val.strip(f":CLS{axis_Id_numeric},")) * 1e6 + + def describe(self) -> None: + t = PrettyTable() + t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" + t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"] + for ax in range(self._axes_per_controller): + axis = self._axis[ax] + if axis is not None: + t.add_row( + [ + f"{axis.axis_Id_numeric}/{axis.axis_Id}", + axis.name, + axis.connected, + self.axis_is_referenced(axis.axis_Id_numeric), + self.get_closed_loop_move_speed(axis.axis_Id_numeric), + axis.readback.read().get(axis.name).get("value"), + ] + ) + else: + t.add_row([None for t in t.field_names]) + print(t) + + @axis_checked + def _error_str(self, axis_Id_numeric: int, error_number: int): + return f":E{axis_Id_numeric},{error_number}" + + def _get_error_code_from_msg(self, msg: str) -> int: + if msg.startswith(":E"): + return int(msg.split(",")[-1]) + else: + return -1 + + def _get_axis_from_error_code(self, msg: str) -> int: + if msg.startswith(":E"): + try: + return int(msg.strip(":E").split(",")[0]) + except ValueError: + return None + else: + return None + + def _check_for_error(self, msg: str, axis_Id_numeric: int = None, raise_if_not_status=False): + if msg.startswith(":E"): + if axis_Id_numeric is None: + axis_Id_numeric = self._get_axis_from_error_code(msg) + + if axis_Id_numeric is None: + raise SmaractCommunicationError( + "Could not retrieve axis number from error message." + ) + + if msg != self._error_str(axis_Id_numeric, 0): + error_code = self._get_error_code_from_msg(msg) + if error_code != 0: + raise SmaractErrorCode(error_code) + else: + if raise_if_not_status: + raise SmaractCommunicationError( + "Expected error / status message but failed to parse it." + ) + + def _remove_trailing_characters(self, var: str) -> str: + if len(var) > 1: + return var.split("\n")[0] + return var + + def _message_starts_with(self, msg: str, leading_chars: str) -> bool: + if msg.startswith(leading_chars): + return True + raise SmaractCommunicationError( + f"Expected to receive a return message starting with {leading_chars} but instead" + f" received '{msg}'" + ) diff --git a/csaxs_bec/devices/smaract/smaract_errors.py b/csaxs_bec/devices/smaract/smaract_errors.py new file mode 100644 index 0000000..672da41 --- /dev/null +++ b/csaxs_bec/devices/smaract/smaract_errors.py @@ -0,0 +1,47 @@ +SMARACT_ERRORS = { + 1: "Syntax Error: The command could not be processed due to a syntactical error.", + 2: "Invalid Command Error: The command given is not known to the system.", + 3: "Overflow Error: This error occurs if a parameter given is too large and therefore cannot be processed.", + 4: "Parse Error: The command could not be processed due to a parse error.", + 5: "Too Few Parameters Error: The specified command requires more parameters in order to be executed.", + 6: "Too Many Parameters Error: There were too many parameters given for the specified command.", + 7: "Invalid Parameter Error: A parameter given exceeds the valid range. Please see the command description for valid ranges of the parameters.", + 8: "Wrong Mode Error: This error is generated if the specified command is not available in the current communication mode. For example, the SRC command is not executable in synchronous mode.", + 129: "No Sensor Present Error: This error occurs if a command was given that requires sensor feedback, but the addressed positioner has none attached.", + 140: "Sensor Disabled Error: This error occurs if a command was given that requires sensor feedback, but the sensor of the addressed positioner is disabled (see SSE command).", + 141: "Command Overridden Error: This error is only generated in the asynchronous communication mode. When the software commands a movement which is then interrupted by the Hand Control Module, an error of this type is generated.", + 142: "End Stop Reached Error: This error is generated in asynchronous mode if the target position of a closed-loop command could not be reached, because a mechanical end stop was detected. After this error the positioner will have a movement status code of 0 (stopped).", + 143: "Wrong Sensor Type Error: This error occurs if a closed-loop command does not match the sensor type that is currently configured for the addressed channel. For example, issuing a GP command while the targeted channel is configured as rotary will lead to this error.", + 144: "Could Not Find Reference Mark Error: This error is generated in asynchronous mode (see SCM) if the search for a reference mark was aborted.", + 145: "Wrong End Effector Type Error: This error occurs if a command does not match the end effector type that is currently configured for the addressed channel. For example, sending GF while the targeted channel is configured for a gripper will lead to this error.", + 146: "Movement Locked Error: This error occurs if a movement command is issued while the system is in the locked state. ", + 147: "Range Limit Reached Error: If a range limit is defined (SPL or SAL) and the positioner is about to move beyond this limit, then the positioner will stop and report this error (only in asynchronous mode, see SCM). After this error the positioner will have status code of 0 (stopped).", + 148: "Physical Position Unknown Error: A range limit is only allowed to be defined if the positioner “knows” its physical position. If this is not the case, the commands SPL and SAL will return this error code.", + 150: "Command Not Processable Error: This error is generated if a command is sent to a channel when it is in a state where the command cannot be processed. For example, to change the sensor type of a channel the addressed channel must be completely stopped. In this case send a stop command before changing the type.", + 151: "Waiting For Trigger Error: If there is at least one command queued in the command queue then you may only append more commands (if the queue is not full), but you may not issue movement commands for immediate execution. Doing so will generate this error. See section 2.4.5 “Command Queues“.", + 152: "Command Not Triggerable Error: After sending a ATC command you are required to issue a movement command that is to be triggered by the given event source. Commands that cannot be triggered will generate this error.", + 153: "Command Queue Full Error: This error is generated if you attempt to append more commands to the command queue, but the queue cannot hold anymore commands. The queue capacity may be read out with a get channel property command (GCP on p.30).", + 154: "Invalid Component Error: Indicates that a component (e.g. SCP) was selected that does not exist.", + 155: "Invalid Sub Component Error: Indicates that a sub component (e.g. SCP) was selected that does not exist.", + 156: "Invalid Property Error: Indicates that a property (e.g. SCP) was selected that does not exist.", + 157: "Permission Denied Error: This error is generated when you call a functionality which is not unlocked for the system (e.g. Low Vibration Mode).", +} + + +class SmaractError(Exception): + pass + + +class SmaractCommunicationError(SmaractError): + pass + + +class SmaractErrorCode(SmaractError): + def __init__(self, error_code: int, message=""): + self.error_code = error_code + self.error_code_message = SMARACT_ERRORS.get(error_code, "UNKNOWN ERROR") + self.message = message + super().__init__(self.message) + + def __str__(self): + return f"{self.error_code} / {self.error_code_message}. {self.message}" diff --git a/csaxs_bec/devices/smaract/smaract_ophyd.py b/csaxs_bec/devices/smaract/smaract_ophyd.py new file mode 100644 index 0000000..ff088ac --- /dev/null +++ b/csaxs_bec/devices/smaract/smaract_ophyd.py @@ -0,0 +1,321 @@ +import logging +import threading +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected + +from csaxs_bec.devices.smaract.smaract_controller import SmaractController +from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError + +logger = bec_logger.logger + + +class SmaractSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class SmaractSignalRO(SmaractSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + @threadlocked + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class SmaractReadbackSignal(SmaractSignalRO): + @threadlocked + def _socket_get(self): + return self.controller.get_position(self.parent.axis_Id_numeric) * self.parent.sign + + +class SmaractSetpointSignal(SmaractSignalBase): + setpoint = 0 + + @threadlocked + def _socket_get(self): + return self.setpoint + + @threadlocked + def _socket_set(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + + if self.controller.axis_is_referenced(self.parent.axis_Id_numeric): + self.controller.move_axis_to_absolute_position(self.parent.axis_Id_numeric, target_val) + # parameters are axis_no,pos_mm*1e6, hold_time_sec*1e3 + else: + raise SmaractError(f"Axis {self.parent.axis_Id_numeric} is not referenced.") + + +class SmaractMotorIsMoving(SmaractSignalRO): + @threadlocked + def _socket_get(self): + return self.controller.is_axis_moving(self.parent.axis_Id_numeric) + + +class SmaractAxisReferenced(SmaractSignalRO): + @threadlocked + def _socket_get(self): + return self.parent.controller.axis_is_referenced(self.parent.axis_Id_numeric) + + +class SmaractAxisLimits(SmaractSignalBase): + @threadlocked + def _socket_get(self): + limits_msg = self.controller.socket_put_and_receive(f"GPL{self.parent.axis_Id_numeric}") + if limits_msg.startswith(":PL"): + limits = [ + float(limit) + for limit in limits_msg.strip(f":PL{self.parent.axis_Id_numeric},").split(",") + ] + else: + raise SmaractCommunicationError("Expected to receive message starting with :PL.") + return limits + + # def _socket_set(self, val): + + +class SmaractMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt(SmaractReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(SmaractSetpointSignal, signal_name="setpoint") + + # motor_resolution = Cpt( + # SmaractMotorResolution, signal_name="resolution", kind="config" + # ) + + motor_is_moving = Cpt(SmaractMotorIsMoving, signal_name="motor_is_moving", kind="normal") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + # all_axes_referenced = Cpt( + # SmaractAxesReferenced, signal_name="all_axes_referenced", kind="config" + # ) + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=8085, + limits=None, + sign=1, + socket_cls=SocketIO, + **kwargs, + ): + self.controller = SmaractController( + socket_cls=socket_cls, socket_host=host, socket_port=port + ) + self.axis_Id = axis_Id + self.sign = sign + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 4) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.1) + val = self.readback.read() + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + self._done_moving(success=success) + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "mm" + + def stage(self) -> list[object]: + return super().stage() + + def unstage(self) -> list[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + mock = False + if not mock: + lsmarA = SmaractMotor("A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1) + lsmarB = SmaractMotor("B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1) + + lsmarA.stage() + lsmarB.stage() + # status = leyey.move(2, wait=True) + # status = leyey.move(2, wait=True) + lsmarA.read() + lsmarB.read() + + lsmarA.get() + lsmarB.get() + lsmarA.describe() + + lsmarA.unstage() + lsmarA.controller.off() + # status = leyey.move(10, wait=False) + # print(lSmaract_controller) + else: + from ophyd_devices.utils.socket import SocketMock + + lsmarA = SmaractMotor( + "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock + ) + lsmarB = SmaractMotor( + "B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock + ) + lsmarA.stage() + lsmarB.stage() + + lsmarA.read() + lsmarB.read() + + lsmarA.get() + lsmarB.get() + lsmarA.describe() + + lsmarA.unstage() + lsmarA.controller.off() + # status = leyey.move(10, wait=False) + # print(lSmaract_controller) diff --git a/csaxs_bec/devices/smaract/smaract_sensors.json b/csaxs_bec/devices/smaract/smaract_sensors.json new file mode 100644 index 0000000..e570f13 --- /dev/null +++ b/csaxs_bec/devices/smaract/smaract_sensors.json @@ -0,0 +1,303 @@ +[ + { + "symbol": "S", + "type_code": 1, + "positioner_series": "SLCxxxxs", + "comment": "linear positioner with nano sensor", + "reference_type": "mark" + }, + { + "symbol": "SR", + "type_code": 2, + "positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s, SR7012s, SR4513s, SR5018s", + "comment": "rotary positioner with nano sensor", + "reference_type": "mark" + }, + { + "symbol": "SP", + "type_code": 5, + "positioner_series": "SLCxxxxrs", + "comment": "linear positioner with nano sensor, large actuator", + "reference_type": "mark" + }, + { + "symbol": "SC", + "type_code": 6, + "positioner_series": "SLCxxxxsc", + "comment": "linear positioner with nano sensor, distance coded reference marks", + "reference_type": "mark*" + }, + { + "symbol": "SR20", + "type_code": 8, + "positioner_series": "SR2013s, SR1612s", + "comment": "rotary positioner with nano sensor", + "reference_type": "mark" + }, + { + "symbol": "M", + "type_code": 9, + "positioner_series": "SLCxxxxm", + "comment": "linear positioner with micro sensor", + "reference_type": "end stop" + }, + { + "symbol": "GD", + "type_code": 11, + "positioner_series": "SGO60.5m", + "comment": "goniometer with micro sensor (60.5mm radius)", + "reference_type": "end stop" + }, + { + "symbol": "GE", + "type_code": 12, + "positioner_series": "SGO77.5m", + "comment": "goniometer with micro sensor (77.5mm radius)", + "reference_type": "end stop" + }, + { + "symbol": "GF", + "type_code": 14, + "positioner_series": "SR1209m", + "comment": "rotary positioner with micro sensor", + "reference_type": "end stop" + }, + { + "symbol": "G605S", + "type_code": 16, + "positioner_series": "SGO60.5s", + "comment": "goniometer with nano sensor (60.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "G775S", + "type_code": 17, + "positioner_series": "SGO77.5s", + "comment": "goniometer with nano sensor (77.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "SC500", + "type_code": 18, + "positioner_series": "SLLxxsc", + "comment": "linear positioner with nano sensor, distance coded reference marks", + "reference_type": "mark*" + }, + { + "symbol": "G955S", + "type_code": 19, + "positioner_series": "SGO95.5s", + "comment": "goniometer with nano sensor (95.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "SR77", + "type_code": 20, + "positioner_series": "SR77xxs", + "comment": "rotary positioner with nano sensor", + "reference_type": "mark" + }, + { + "symbol": "SD", + "type_code": 21, + "positioner_series": "SLCxxxxds, SLLxxs", + "comment": "like S, but with extended scanning Range", + "reference_type": "mark" + }, + { + "symbol": "R20ME", + "type_code": 22, + "positioner_series": "SR2013sx, SR1410sx", + "comment": "rotary positioner with MicroE sensor", + "reference_type": "mark" + }, + { + "symbol": "SR2", + "type_code": 23, + "positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s", + "comment": "like SR, for high applied masses", + "reference_type": "mark" + }, + { + "symbol": "SCD", + "type_code": 24, + "positioner_series": "SLCxxxxdsc", + "comment": "like SP, but with distance coded reference marks", + "reference_type": "mark*" + }, + { + "symbol": "SRC", + "type_code": 25, + "positioner_series": "SR7021sc", + "comment": "like SR, but with distance coded reference marks", + "reference_type": "mark*" + }, + { + "symbol": "SR36M", + "type_code": 26, + "positioner_series": "SR3610m", + "comment": "rotary positioner, no end stops", + "reference_type": "none" + }, + { + "symbol": "SR36ME", + "type_code": 27, + "positioner_series": "SR3610m", + "comment": "rotary positioner with end stops", + "reference_type": "end stop" + }, + { + "symbol": "SR50M", + "type_code": 28, + "positioner_series": "SR5018m", + "comment": "rotary positioner, no end stops", + "reference_type": "none" + }, + { + "symbol": "SR50ME", + "type_code": 29, + "positioner_series": "SR5018m", + "comment": "rotary positioner with end stops", + "reference_type": "end stop" + }, + { + "symbol": "G1045S", + "type_code": 30, + "positioner_series": "SGO104.5s", + "comment": "goniometer with nano sensor (104.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "G1395S", + "type_code": 31, + "positioner_series": "SGO139.5s", + "comment": "goniometer with nano sensor (139.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "MD", + "type_code": 32, + "positioner_series": "SLCxxxxdme", + "comment": "like M, but with large actuator", + "reference_type": "end stop" + }, + { + "symbol": "G935M", + "type_code": 33, + "positioner_series": "SGO93.5me", + "comment": "goniometer with micro sensor (93.5mm radius)", + "reference_type": "end stop" + }, + { + "symbol": "SHL20", + "type_code": 34, + "positioner_series": "SHL-20", + "comment": "high load vertical positioner", + "reference_type": "mark" + }, + { + "symbol": "SCT", + "type_code": 35, + "positioner_series": "SLCxxxxscu", + "comment": "like SCD, but with even larger actuator", + "reference_type": "mark*" + }, + { + "symbol": "SR77T", + "type_code": 36, + "positioner_series": "SR7021s", + "comment": "like SR77, but with larger actuator", + "reference_type": "mark" + }, + { + "symbol": "SR120", + "type_code": 37, + "positioner_series": "SR120xxs", + "comment": "large rotary positioner", + "reference_type": "mark" + }, + { + "symbol": "LC", + "type_code": 38, + "positioner_series": "SLCxxxxl", + "comment": "linear positioner with improved micro sensor", + "reference_type": "mark*" + }, + { + "symbol": "LR", + "type_code": 39, + "positioner_series": "SRxxxxl", + "comment": "rotary positioner with improved micro sensor", + "reference_type": "mark" + }, + { + "symbol": "LCD", + "type_code": 40, + "positioner_series": "SLCxxxxdl", + "comment": "like LC, but with large actuator", + "reference_type": "mark*" + }, + { + "symbol": "L", + "type_code": 41, + "positioner_series": "SLCxxxxl", + "comment": "linear positioner with improved micro sensor", + "reference_type": "mark" + }, + { + "symbol": "LD", + "type_code": 42, + "positioner_series": "SLCxxxxdl", + "comment": "like L, but with large actuator", + "reference_type": "mark" + }, + { + "symbol": "LE", + "type_code": 43, + "positioner_series": "SLCxxxxl", + "comment": "Linear positioner with improved micro sensor", + "reference_type": "end stop" + }, + { + "symbol": "LED", + "type_code": 44, + "positioner_series": "SLCxxxxdl", + "comment": "Like L, but with large actuator", + "reference_type": "end stop" + }, + { + "symbol": "GDD", + "type_code": 45, + "positioner_series": "SGO60.5md", + "comment": "goniometer with micro sensor (60.5mm radius), large actuator", + "reference_type": "end stop" + }, + { + "symbol": "GED", + "type_code": 46, + "positioner_series": "SGO77.5md", + "comment": "goniometer with micro sensor (77.5mm radius), large actuator", + "reference_type": "end stop" + }, + { + "symbol": "G935S", + "type_code": 47, + "positioner_series": "SGO96.5s", + "comment": "goniometer with nano sensor (93.5mm radius)", + "reference_type": "mark" + }, + { + "symbol": "G605DS", + "type_code": 48, + "positioner_series": "SGO60.5ds", + "comment": "goniometer with nano sensor (60.5mm radius), large actuator", + "reference_type": "mark" + }, + { + "symbol": "G775DS", + "type_code": 49, + "positioner_series": "SGO77.5ds", + "comment": "goniometer with nano sensor (77.5mm radius), large actuator", + "reference_type": "mark" + } +] \ No newline at end of file diff --git a/bec_plugins/scan_server/scan_plugins/LamNIFermatScan.py b/csaxs_bec/scans/LamNIFermatScan.py similarity index 93% rename from bec_plugins/scan_server/scan_plugins/LamNIFermatScan.py rename to csaxs_bec/scans/LamNIFermatScan.py index b4eb8dd..8618058 100644 --- a/bec_plugins/scan_server/scan_plugins/LamNIFermatScan.py +++ b/csaxs_bec/scans/LamNIFermatScan.py @@ -23,9 +23,9 @@ but they are executed in a specific order: import time import numpy as np -from bec_lib import MessageEndpoints, bec_logger, messages -from scan_server.errors import ScanAbortion -from scan_server.scans import RequestBase, ScanArgType, ScanBase +from bec_lib import MessageEndpoints, bec_logger +from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.scans import RequestBase, ScanArgType, ScanBase MOVEMENT_SCALE_X = np.sin(np.radians(15)) * np.cos(np.radians(30)) MOVEMENT_SCALE_Y = np.cos(np.radians(15)) @@ -96,8 +96,8 @@ class LamNIMixin: coarse_move_req_x = np.abs(lsamx_current - move_x) coarse_move_req_y = np.abs(lsamy_current - move_y) - self.device_manager.devices.lsamx.enabled_set = True - self.device_manager.devices.lsamy.enabled_set = True + self.device_manager.devices.lsamx.read_only = False + self.device_manager.devices.lsamy.read_only = False if ( np.abs(y_drift) > 150 @@ -126,12 +126,14 @@ class LamNIMixin: x_drift2 = x_center_expect * 1000 - rtx_current y_drift2 = y_center_expect * 1000 - rty_current logger.info( - f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f}, y={y_drift2:.3f}" + f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f}," + f" y={y_drift2:.3f}" ) if np.abs(x_drift2) > 5 or np.abs(y_drift2) > 5: logger.info( - f"Compensating second iteration {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}" + "Compensating second iteration" + f" {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}" ) move_x = ( x_stage @@ -153,18 +155,20 @@ class LamNIMixin: rty_current = yield from self.stubs.send_rpc_and_wait("rty", "readback.get") logger.info( - f"New scan center interferometer after second iteration {rtx_current:.3f}, {rty_current:.3f} microns" + f"New scan center interferometer after second iteration {rtx_current:.3f}," + f" {rty_current:.3f} microns" ) x_drift2 = x_center_expect * 1000 - rtx_current y_drift2 = y_center_expect * 1000 - rty_current logger.info( - f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f}, y={y_drift2:.3f}" + f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f}," + f" y={y_drift2:.3f}" ) else: logger.info("No second iteration required") - self.device_manager.devices.lsamx.enabled_set = False - self.device_manager.devices.lsamy.enabled_set = False + self.device_manager.devices.lsamx.read_only = True + self.device_manager.devices.lsamy.read_only = True yield from self.stubs.send_rpc_and_wait("rtx", "controller.feedback_enable_without_reset") @@ -174,8 +178,12 @@ class LamNIMoveToScanCenter(RequestBase, LamNIMixin): scan_report_hint = None scan_type = "step" required_kwargs = [] - arg_input = [ScanArgType.FLOAT, ScanArgType.FLOAT, ScanArgType.FLOAT] - arg_bundle_size = None + arg_input = { + "shift_x": ScanArgType.FLOAT, + "shift_y": ScanArgType.FLOAT, + "angle": ScanArgType.FLOAT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1} def __init__(self, *args, parameter=None, **kwargs): """ @@ -199,10 +207,10 @@ class LamNIFermatScan(ScanBase, LamNIMixin): scan_report_hint = "table" scan_type = "step" required_kwargs = ["fov_size", "exp_time", "step", "angle"] - arg_input = [] - arg_bundle_size = None + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} - def __init__(self, *args, parameter=None, **kwargs): + def __init__(self, *args, parameter: dict = None, **kwargs): """ A LamNI scan following Fermat's spiral. @@ -357,7 +365,8 @@ class LamNIFermatScan(ScanBase, LamNIMixin): self.stitch_x, self.stitch_y, self.angle ) logger.info( - f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x}, {_shfity+y_stitch_shift/1000+self.shift_y}" + f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x}," + f" {_shfity+y_stitch_shift/1000+self.shift_y}" ) return ( _shfitx + x_stitch_shift / 1000 + self.shift_x, @@ -448,16 +457,17 @@ class LamNIFermatScan(ScanBase, LamNIMixin): yield from self.stubs.kickoff(device="rtx") while True: yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") - msg = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan")) + msg = self.device_manager.connector.get(MessageEndpoints.device_status("rt_scan")) if msg: - status = messages.DeviceStatusMessage.loads(msg) + status = msg status_id = status.content.get("status", 1) request_id = status.metadata.get("RID") if status_id == 0 and self.metadata.get("RID") == request_id: break if status_id == 2 and self.metadata.get("RID") == request_id: raise ScanAbortion( - f"An error occured during the LamNI readout: {status.metadata.get('error')}" + "An error occured during the LamNI readout:" + f" {status.metadata.get('error')}" ) time.sleep(1) diff --git a/csaxs_bec/scans/flomni_fermat_scan.py b/csaxs_bec/scans/flomni_fermat_scan.py new file mode 100644 index 0000000..7735155 --- /dev/null +++ b/csaxs_bec/scans/flomni_fermat_scan.py @@ -0,0 +1,328 @@ +""" +SCAN PLUGINS + +All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden +but they are executed in a specific order: + +- self.initialize # initialize the class if needed +- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed) +- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions: + - self._calculate_positions # calculate the positions + - self._set_positions_offset # apply the previously retrieved scan position shift (if needed) + - self._check_limits # tests to ensure the limits won't be reached +- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names +- self.stage # stage all devices for the upcoming acquisiton +- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan +- self.scan_core # run a loop over all position + - self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments +- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish +- self.unstage # unstage all devices that have been staged before +- self.cleanup # send a close scan message and perform additional cleanups if needed +""" + +import time + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.scans import SyncFlyScanBase + +logger = bec_logger.logger + + +class FlomniFermatScan(SyncFlyScanBase): + scan_name = "flomni_fermat_scan" + scan_report_hint = "table" + scan_type = "fly" + required_kwargs = ["fov_size", "exp_time", "step", "angle"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__( + self, + fovx: float, + fovy: float, + cenx: float, + ceny: float, + exp_time: float, + step: float, + zshift: float, + angle: float = None, + corridor_size: float = 3, + parameter: dict = None, + **kwargs, + ): + """ + A flomni scan following Fermat's spiral. + + Args: + fovx(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 200 um + fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um + cenx(float) [mm]: center position in x. + ceny(float) [mm]: center position in y. + exp_time(float) [s]: exposure time + step(float) [um]: stepsize + zshift(float) [um]: shift in z + angle(float) [deg]: rotation angle (will rotate first) + corridor_size(float) [um]: corridor size for the corridor optimization. Default 3 um + + Returns: + + Examples: + >>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01) + """ + + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.fovx = fovx + self.fovy = fovy + self.cenx = cenx + self.ceny = ceny + self.exp_time = exp_time + self.step = step + self.zshift = zshift + self.angle = angle + self.optim_trajectory = "corridor" + self.optim_trajectory_corridor = corridor_size + if self.fovy > 100: + raise ScanAbortion("The FOV in y must be smaller than 100 um.") + if self.fovx > 200: + raise ScanAbortion("The FOV in x must be smaller than 200 um.") + if self.zshift > 100: + logger.warning("The zshift is larger than 100 um. It will be limited to 100 um.") + self.zshift = 100 + if self.zshift < -100: + logger.warning("The zshift is smaller than -100 um. It will be limited to -100 um.") + self.zshift = -100 + + def initialize(self): + self.scan_motors = [] + self.update_readout_priority() + + def _optimize_trajectory(self): + self.positions = self.optimize_corridor( + self.positions, corridor_size=self.optim_trajectory_corridor + ) + + @property + def monitor_sync(self): + return "rt_flomni" + + def reverse_trajectory(self): + """ + Reverse the trajectory. Every other scan should be reversed to + shorten the movement time. In order to keep the last state, even if the + server is restarted, the state is stored in a global variable in redis. + """ + producer = self.device_manager.producer + msg = producer.get(MessageEndpoints.global_vars("reverse_flomni_trajectory")) + if msg: + val = msg.content.get("value", False) + else: + val = False + producer.set( + MessageEndpoints.global_vars("reverse_flomni_trajectory"), + messages.VariableMessage(value=(not val)), + ) + return val + + def prepare_positions(self): + self._calculate_positions() + self._optimize_trajectory() + flip_axes = self.reverse_trajectory() + if flip_axes: + self.positions = np.flipud(self.positions) + + self.num_pos = len(self.positions) + self._check_min_positions() + + def _check_min_positions(self): + if self.num_pos < 20: + raise ScanAbortion( + f"The number of positions must exceed 20. Currently: {self.num_pos}." + ) + + def _prepare_setup(self): + yield from self.stubs.send_rpc_and_wait("rtx", "controller.clear_trajectory_generator") + yield from self.flomni_rotation(self.angle) + + yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1]) + + def _prepare_setup_part2(self): + yield from self.stubs.wait(wait_type="move", device="fsamroy", wait_group="flomni_rotation") + yield from self.stubs.set( + device="rtx", value=self.positions[0][0], wait_group="prepare_setup_part2" + ) + yield from self.stubs.set( + device="rtz", value=self.positions[0][2], wait_group="prepare_setup_part2" + ) + yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on") + yield from self.stubs.wait( + wait_type="move", device=["rtx", "rtz"], wait_group="prepare_setup_part2" + ) + yield from self._transfer_positions_to_flomni() + yield from self.stubs.send_rpc_and_wait( + "rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx + ) + tracker_signal_status = yield from self.stubs.send_rpc_and_wait( + "rtx", "controller.laser_tracker_check_signalstrength" + ) + if tracker_signal_status == "low": + self.device_manager.connector.raise_alarm( + severity=0, + alarm_type="LaserTrackerSignalStrength", + source="rtx", + metadata={}, + msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!", + ) + elif tracker_signal_status == "toolow": + raise ScanAbortion( + "Signal strength of the laser tracker is too low for scanning. Realignment required!" + ) + + def flomni_rotation(self, angle): + # get last setpoint (cannot be based on pos get because they will deviate slightly) + fsamroy_current_setpoint = yield from self.stubs.send_rpc_and_wait( + "fsamroy", "user_setpoint.get" + ) + if angle == fsamroy_current_setpoint: + logger.info("No rotation required") + else: + logger.info("Rotating to requested angle") + yield from self.stubs.scan_report_instruction( + { + "readback": { + "RID": self.metadata["RID"], + "devices": ["fsamroy"], + "start": [fsamroy_current_setpoint], + "end": [angle], + } + } + ) + yield from self.stubs.set(device="fsamroy", value=angle, wait_group="flomni_rotation") + + def _transfer_positions_to_flomni(self): + yield from self.stubs.send_rpc_and_wait( + "rtx", "controller.add_pos_to_scan", self.positions.tolist() + ) + + def _calculate_positions(self): + self.positions = self.get_flomni_fermat_spiral_pos( + -np.abs(self.fovx / 2), + np.abs(self.fovx / 2), + -np.abs(self.fovy / 2), + np.abs(self.fovy / 2), + step=self.step, + spiral_type=0, + center=False, + ) + + def get_flomni_fermat_spiral_pos( + self, m1_start, m1_stop, m2_start, m2_stop, step=1, spiral_type=0, center=False + ): + """ + Calculate positions for a Fermat spiral scan. + + Args: + m1_start(float): start position in m1 + m1_stop(float): stop position in m1 + m2_start(float): start position in m2 + m2_stop(float): stop position in m2 + step(float): stepsize + spiral_type(int): 0 for traditional Fermat spiral + center(bool): whether to include the center position + + Returns: + positions(array): positions + """ + positions = [] + phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2.0) + spiral_type * np.pi + + start = int(not center) + + length_axis1 = np.abs(m1_stop - m1_start) + length_axis2 = np.abs(m2_stop - m2_start) + n_max = int(length_axis1 * length_axis2 * 3.2 / step / step) + + z_pos = self.zshift + + for ii in range(start, n_max): + radius = step * 0.57 * np.sqrt(ii) + # FOV is restructed below at check pos in range + if abs(radius * np.sin(ii * phi)) > length_axis1 / 2: + continue + if abs(radius * np.cos(ii * phi)) > length_axis2 / 2: + continue + x = radius * np.sin(ii * phi) + y = radius * np.cos(ii * phi) + positions.append([x + self.cenx, y + self.ceny, z_pos]) + left_lower_corner = [ + min(m1_start, m1_stop) + self.cenx, + min(m2_start, m2_stop) + self.ceny, + z_pos, + ] + right_upper_corner = [ + max(m1_start, m1_stop) + self.cenx, + max(m2_start, m2_stop) + self.ceny, + z_pos, + ] + positions.append(left_lower_corner) + positions.append(right_upper_corner) + return np.array(positions) + + def scan_core(self): + # use a device message to receive the scan number and + # scan ID before sending the message to the device server + yield from self.stubs.kickoff(device="rtx") + while True: + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") + status = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan")) + if status: + status_id = status.content.get("status", 1) + request_id = status.metadata.get("RID") + if status_id == 0 and self.metadata.get("RID") == request_id: + break + if status_id == 2 and self.metadata.get("RID") == request_id: + raise ScanAbortion( + "An error occured during the flomni readout:" + f" {status.metadata.get('error')}" + ) + + time.sleep(1) + logger.debug("reading monitors") + # yield from self.device_rpc("rtx", "controller.kickoff") + + def return_to_start(self): + """return to the start position""" + # in flomni, we need to move to the start position of the next scan + if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3: + yield from self.stubs.set( + device="rtx", value=self.positions[-1][0], wait_group="scan_motor" + ) + yield from self.stubs.set( + device="rty", value=self.positions[-1][1], wait_group="scan_motor" + ) + yield from self.stubs.set( + device="rtz", value=self.positions[-1][2], wait_group="scan_motor" + ) + + yield from self.stubs.wait( + wait_type="move", device=["rtx", "rty", "rtz"], wait_group="scan_motor" + ) + return + + logger.warning("No positions found to return to start") + + def run(self): + self.initialize() + yield from self.read_scan_motors() + self.prepare_positions() + yield from self._prepare_setup() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + yield from self._prepare_setup_part2() + yield from self.scan_core() + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() diff --git a/csaxs_bec/scans/owis_grid.py b/csaxs_bec/scans/owis_grid.py new file mode 100644 index 0000000..653396c --- /dev/null +++ b/csaxs_bec/scans/owis_grid.py @@ -0,0 +1,309 @@ +""" +SCAN PLUGINS + +All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden +but they are executed in a specific order: + +- self.initialize # initialize the class if needed +- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed) +- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions: + - self._calculate_positions # calculate the positions + - self._set_positions_offset # apply the previously retrieved scan position shift (if needed) + - self._check_limits # tests to ensure the limits won't be reached +- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names +- self.stage # stage all devices for the upcoming acquisiton +- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan +- self.scan_core # run a loop over all position + - self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments +- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish +- self.unstage # unstage all devices that have been staged before +- self.cleanup # send a close scan message and perform additional cleanups if needed +""" + +import time + +from bec_lib import MessageEndpoints, bec_logger +from bec_server.scan_server.scans import AsyncFlyScanBase, ScanAbortion + +logger = bec_logger.logger + + +class OwisGrid(AsyncFlyScanBase): + """Owis-based grid scan.""" + + scan_name = "owis_grid" + scan_report_hint = "scan_progress" + required_kwargs = [] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__( + self, + start_y: float, + end_y: float, + interval_y: int, + start_x: float, + end_x: float, + interval_x: int, + *args, + exp_time: float = 0.1, + readout_time: float = 3e-3, + **kwargs, + ): + """ + Owis-based grid scan. + + Args: + start_y (float): start position of y axis (fast axis) + end_y (float): end position of y axis (fast axis) + interval_y (int): number of points in y axis + start_x (float): start position of x axis (slow axis) + end_x (float): end position of x axis (slow axis) + interval_x (int): number of points in x axis + exp_time (float): exposure time in seconds. Default is 0.1s + readout_time (float): readout time in seconds, minimum of 3e-3s (3ms) + + Exp: + scans.sgalil_grid(start_y = val1, end_y= val1, interval_y = val1, start_x = val1, end_x = val1, interval_x = val1, exp_time = 0.02, readout_time = 3e-3) + + + """ + super().__init__(*args, **kwargs) + + # Enforce scanning from positive to negative + if start_y > end_y: + self.start_y = start_y + self.end_y = end_y + else: + self.start_y = end_y + self.end_y = start_y + if start_x > end_x: + self.start_x = start_x + self.end_x = end_x + else: + self.start_x = end_x + self.end_x = start_x + # set scan parameter + self.interval_y = interval_y + self.interval_x = interval_x + self.exp_time = exp_time + self.readout_time = readout_time + self.num_pos = int(interval_x * interval_y) + self.scan_motors = ["samx", "samy"] + + # Scan progress related variables + self.timeout_progress = 0 + self.progress_point = 0 + self.timeout_scan_abortion = 10 # 42 # duty cycles of scan segment update + self.sleep_time = 1 + + # Keep the shutter open for longer to allow acquisitions to fly in + self.shutter_additional_width = 0.15 + + # Scan related variables + self.sign = 1 + # add offset time if needed + self.add_pre_move_time = 0.0 + + self.stepping_x = None + self.stepping_y = None + self.high_velocity = None + self.high_acc_time = None + self.base_velocity = None + self.target_velocity = None + self.acc_time = None + self.premove_distance = None + + def get_initial_motor_properties(self): + self.high_velocity = yield from self.stubs.send_rpc_and_wait("samy", "velocity.get") + self.high_acc_time = yield from self.stubs.send_rpc_and_wait("samy", "acceleration.get") + self.base_velocity = yield from self.stubs.send_rpc_and_wait("samy", "base_velocity.get") + + def compute_scan_params(self): + """Compute scan parameters. This includes the velocity, acceleration and premove distance.""" + + ########### Owis stage parameters + # scanning related parameters + self.stepping_y = abs(self.start_y - self.end_y) / self.interval_y + self.stepping_x = abs(self.start_x - self.end_x) / self.interval_x + + # Get current velocity, acceleration and base_velocity + yield from self.get_initial_motor_properties() + + # Relevant parameters for scan + self.target_velocity = self.stepping_y / (self.exp_time + self.readout_time) + self.acc_time = ( + (self.target_velocity - self.base_velocity) + / (self.high_velocity - self.base_velocity) + * self.high_acc_time + ) + self.premove_distance = ( + 0.5 * (self.target_velocity + self.base_velocity) * self.acc_time + + self.add_pre_move_time * self.target_velocity + ) + + # Checks and set acc_time and premove for the designated scan + if self.target_velocity > self.high_velocity or self.target_velocity < self.base_velocity: + raise ScanAbortion( + f"Requested velocity of {self.target_velocity} exceeds {self.high_velocity}" + ) + + def scan_report_instructions(self): + """Scan report instructions for the progress bar, yields from mcs card""" + if not self.scan_report_hint: + yield None + return + yield from self.stubs.scan_report_instruction({"scan_progress": ["mcs"]}) + + def pre_scan(self): + """Pre scan instructions, move to start position""" + yield from self._move_and_wait([self.start_x, self.start_y]) + yield from self.stubs.pre_scan() + + def scan_progress(self) -> int: + """Timeout of the progress bar. This gets updated in the frequency of scan segments""" + msg = self.device_manager.connector.get(MessageEndpoints.device_progress("mcs")) + if not msg: + self.timeout_progress += 1 + return self.timeout_progress + updated_progress = int(msg.content["value"]) + if updated_progress == int(self.progress_point): + self.timeout_progress += 1 + return self.timeout_progress + else: + self.timeout_progress = 0 + self.progress_point = updated_progress + return self.timeout_progress + + def scan_core(self): + """This is the main event loop.""" + + # Compute scan parameters including velocity, acceleration and premove distance + yield from self.compute_scan_params() + + # Start acquisition with 10ms delay to allow fast shutter to open + yield from self.stubs.send_rpc_and_wait( + "ddg_detectors", + "burst_enable", + count=self.interval_y, + delay=0.01, + period=(self.exp_time + self.readout_time), + config="first", + ) + yield from self.stubs.send_rpc_and_wait( + "ddg_mcs", + "burst_enable", + count=self.interval_y, + delay=0, + period=(self.exp_time + self.readout_time), + config="first", + ) + + yield from self.stubs.send_rpc_and_wait("ddg_fsh", "burst_disable") + + # Set width of signals from ddg fsh to 0, except the one to the MCS card + yield from self.stubs.send_rpc_and_wait( + "ddg_fsh", "set_channels", "width", 0, channels=["channelCD"] + ) + yield from self.stubs.send_rpc_and_wait( + "ddg_fsh", "set_channels", "width", 0, channels=["channelEF", "channelGH"] + ) + # Trigger MCS card to enable the acquisition + time.sleep(0.05) + yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger") + time.sleep(0.05) + + # Set width of signal to fast shutter to appropriate value for single lines + yield from self.stubs.send_rpc_and_wait( + "ddg_fsh", + "set_channels", + "width", + (self.interval_y * (self.exp_time + self.readout_time) + self.shutter_additional_width), + channels=["channelCD"], + ) + + # Set width of signal to MCS card to 0 --> It is already enabled + yield from self.stubs.send_rpc_and_wait( + "ddg_fsh", "set_channels", "width", 0, channels=["channelAB"] + ) + + # remove delay for signals of ddg_mcs + yield from self.stubs.send_rpc_and_wait("ddg_mcs", "set_channels", "delay", 0) + + # Set ddg_mcs on ext trigger from ddg_detectors + status_ddg_mcs_source = yield from self.stubs.send_rpc_and_wait("ddg_mcs", "source.set", 1) + # Set ddg_detectors and ddg_fsh to software trigger + status_ddg_detectors_source = yield from self.stubs.send_rpc_and_wait( + "ddg_detectors", "source.set", 5 + ) + # Set ddg_fsh to software trigger + status_ddg_fsh_source = yield from self.stubs.send_rpc_and_wait("ddg_fsh", "source.set", 5) + + # Wait for a signal from all ddgs, this ensures that all commands before were executed + status_ddg_mcs_source.wait() + status_ddg_detectors_source.wait() + status_ddg_fsh_source.wait() + + # Prepare motors + # Move to start position (taking premove_distance for acceleration into account) + status_prepos = yield from self.stubs.send_rpc_and_wait( + "samy", "move", (self.start_y - self.premove_distance) + ) + status_prepos.wait() + + # Set speed and acceleration for scan + yield from self.stubs.send_rpc_and_wait("samy", "velocity.put", self.target_velocity) + yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.acc_time) + + for ii in range(self.interval_x): + # Set speed and acceleration + yield from self.stubs.send_rpc_and_wait("samy", "velocity.put", self.target_velocity) + yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.acc_time) + + # Start motion and send triggers + yield from self.stubs.set( + device="samy", + value=(self.end_y + (self.sign * self.premove_distance)), + wait_group="flyer", + ) + # Trigger fast shutter, open them right away + yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger") + + time.sleep(self.acc_time) + + # Trigger detectors + yield from self.stubs.send_rpc_and_wait("ddg_detectors", "trigger") + + # Readout primary devices, this waits and could lead to additional overheads + # if devices are slow to response. For optimizing performance, primary devices + # could be read out only once at beginning and end + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + + # Wait for motion to finish + yield from self.stubs.wait(device="samy", wait_group="flyer", wait_type="move") + + # Move second axis by a step + yield from self.stubs.set( + device="samx", value=(self.start_x - ii * self.stepping_x), wait_group="motion" + ) + # Set acceleration and velocity to max + yield from self.stubs.send_rpc_and_wait("samy", "velocity.put", self.high_velocity) + yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.high_acc_time) + + # Move back to start + status_prepos = yield from self.stubs.send_rpc_and_wait( + "samy", "move", (self.start_y - self.premove_distance) + ) + + # Wait for motion to finish + status_prepos.wait() + + # Set speed and acceleration to initial values + def finalize(self): + """Finalize scan, set motor speed and acceleration to initial values""" + yield from self.stubs.send_rpc_and_wait("samy", "velocity.put", self.high_velocity) + yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.high_acc_time) + super().finalize() diff --git a/csaxs_bec/scans/scan_plugin_template.py b/csaxs_bec/scans/scan_plugin_template.py new file mode 100644 index 0000000..bb8ea06 --- /dev/null +++ b/csaxs_bec/scans/scan_plugin_template.py @@ -0,0 +1,32 @@ +""" +SCAN PLUGINS + +All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden +but they are executed in a specific order: + +- self.initialize # initialize the class if needed +- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed) +- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions: + - self._calculate_positions # calculate the positions + - self._set_positions_offset # apply the previously retrieved scan position shift (if needed) + - self._check_limits # tests to ensure the limits won't be reached +- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names +- self.stage # stage all devices for the upcoming acquisiton +- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan +- self.pre_scan # perform additional actions before the scan starts +- self.scan_core # run a loop over all position + - self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments +- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish +- self.unstage # unstage all devices that have been staged before +- self.cleanup # send a close scan message and perform additional cleanups if needed +""" + +# import time + +# import numpy as np + +# from bec_lib import MessageEndpoints, bec_logger, messages +# from bec_server.scan_server.errors import ScanAbortion +# from bec_server.scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase + +# logger = bec_logger.logger diff --git a/csaxs_bec/scans/sgalil_grid.py b/csaxs_bec/scans/sgalil_grid.py new file mode 100644 index 0000000..2162870 --- /dev/null +++ b/csaxs_bec/scans/sgalil_grid.py @@ -0,0 +1,216 @@ +""" +SCAN PLUGINS + +All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden +but they are executed in a specific order: + +- self.initialize # initialize the class if needed +- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed) +- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions: + - self._calculate_positions # calculate the positions + - self._set_positions_offset # apply the previously retrieved scan position shift (if needed) + - self._check_limits # tests to ensure the limits won't be reached +- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names +- self.stage # stage all devices for the upcoming acquisiton +- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan +- self.scan_core # run a loop over all position + - self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments +- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish +- self.unstage # unstage all devices that have been staged before +- self.cleanup # send a close scan message and perform additional cleanups if needed +""" + +import time + +from bec_lib import MessageEndpoints, bec_logger +from bec_server.scan_server.scans import AsyncFlyScanBase + +logger = bec_logger.logger + + +class SgalilGrid(AsyncFlyScanBase): + scan_name = "sgalil_grid" + scan_report_hint = "scan_progress" + required_kwargs = [] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__( + self, + start_y: float, + end_y: float, + interval_y: int, + start_x: float, + end_x: float, + interval_x: int, + *args, + exp_time: float = 0.1, + readout_time: float = 0.1, + **kwargs, + ): + """ + SGalil-based grid scan. + + Args: + start_y (float): start position of y axis (fast axis) + end_y (float): end position of y axis (fast axis) + interval_y (int): number of points in y axis + start_x (float): start position of x axis (slow axis) + end_x (float): end position of x axis (slow axis) + interval_x (int): number of points in x axis + exp_time (float): exposure time in seconds. Default is 0.1s + readout_time (float): readout time in seconds, minimum of 3e-3s (3ms) + + Exp: + scans.sgalil_grid(start_y = val1, end_y= val1, interval_y = val1, start_x = val1, end_x = val1, interval_x = val1, exp_time = 0.02, readout_time = 3e-3) + + + """ + super().__init__(*args, **kwargs) + # Always scan from positive x & y to negative x & y + if start_y > end_y: + self.start_y = start_y + self.end_y = end_y + else: + self.start_y = end_y + self.end_y = start_y + if start_x > end_x: + self.start_x = start_x + self.end_x = end_x + else: + self.start_x = end_x + self.end_x = start_x + self.interval_y = interval_y + self.interval_x = interval_x + self.exp_time = exp_time + self.readout_time = readout_time + self.num_pos = int(interval_x * interval_y) + self.scan_motors = ["samx", "samy"] + # Scan progress related variables + self.timeout_progress = 0 + self.progress_point = 0 + self.timeout_scan_abortion = 10 # 42 # duty cycles of scan segment update + self.sleep_time = 1 + + def scan_report_instructions(self): + if not self.scan_report_hint: + yield None + return + yield from self.stubs.scan_report_instruction({"scan_progress": ["mcs"]}) + + def pre_scan(self): + yield from self._move_and_wait([self.start_x, self.start_y]) + yield from self.stubs.pre_scan() + # TODO move to start position + + def scan_progress(self) -> int: + """Timeout of the progress bar. This gets updated in the frequency of scan segments""" + msg = self.device_manager.connector.get(MessageEndpoints.device_progress("mcs")) + if not msg: + self.timeout_progress += 1 + return self.timeout_progress + # TODO which update is that! + updated_progress = int(msg.content["value"]) + if updated_progress == int(self.progress_point): + self.timeout_progress += 1 + return self.timeout_progress + else: + self.timeout_progress = 0 + self.progress_point = updated_progress + return self.timeout_progress + + def scan_core(self): + """ + This is the main event loop. + """ + + # set up the delay generators + status_ddg_detectors_burst = yield from self.stubs.send_rpc_and_wait( + "ddg_detectors", + "burst_enable", + count=self.interval_y, + delay=0, + period=(self.exp_time + self.readout_time), + config="first", + ) + status_ddg_mcs_burst = yield from self.stubs.send_rpc_and_wait( + "ddg_mcs", + "burst_enable", + count=self.interval_y, + delay=0, + period=(self.exp_time + self.readout_time), + config="first", + ) + # Disable burst mod on DDF for fsh and EN of MCS card + status_ddg_fsh_burst = yield from self.stubs.send_rpc_and_wait("ddg_fsh", "burst_disable") + # Set width of FSH opening to 0 + status_ddg_fsh_ttlwidth = yield from self.stubs.send_rpc_and_wait( + "ddg_fsh", "set_channels", "width", 0, channels=["channelCD"] + ) + + # TODO disable fsh ddg bc SGalil trigger it directly + # Setup triggering + status_ddg_detectors_source = yield from self.stubs.send_rpc_and_wait( + "ddg_detectors", "source.set", 2 + ) + status_ddg_mcs_source = yield from self.stubs.send_rpc_and_wait("ddg_mcs", "source.set", 1) + # Setup mcs_points per line + # status_mcs_points_per_line = yield from self.stubs.send_rpc_and_wait( + # "mcs", "num_use_all.set", self.interval_y + 1 + # ) + # status_mcs_lines = yield from self.stubs.send_rpc_and_wait( + # "mcs", "num_lines.set", self.interval_x + # ) + + # status_ddg_mcs_ttlwidth = yield from self.stubs.send_rpc_and_wait( + # "ddg_mcs", "set_channels", "width", 3e-3 + # ) + status_ddg_mcs_ttldelay = yield from self.stubs.send_rpc_and_wait( + "ddg_mcs", "set_channels", "delay", 0 + ) + + # wait for the delay generators to finish setting up + status_ddg_detectors_source.wait() + status_ddg_mcs_source.wait() + trigger_ddg_fsh = yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger") + # trigger_ddg_fsh.wait() + # status_mcs_points_per_line.wait() + # status_mcs_lines.wait() + + yield from self.stubs.kickoff( + device="samx", + parameter={ + "start_y": self.start_y, + "end_y": self.end_y, + "interval_y": self.interval_y, + "start_x": self.start_x, + "end_x": self.end_x, + "interval_x": self.interval_x, + "exp_time": self.exp_time, + "readout_time": self.readout_time, + }, + ) + target_diid = self.DIID - 1 + while True: + # readout the primary device and wait for the fly scan to finish + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + status = self.stubs.get_req_status( + device="samx", RID=self.metadata["RID"], DIID=target_diid + ) + if status: + break + time.sleep(self.sleep_time) + if self.scan_progress() > int(self.timeout_scan_abortion / self.sleep_time): + logger.info("would have raised a scan abortion here") + # raise ScanAbortion() + + # try: + # logger.info(f'Scan progress check {self.scan_progress()} and {int(self.timeout_scan_abortion/self.sleep_time)}') + # logger.info(f'Potential scan abortion {self.scan_progress() > int(self.timeout_scan_abortion/self.sleep_time)}') + # if self.scan_progress() > int(self.timeout_scan_abortion/self.sleep_time): + # logger.info('Testing Scan abortion, would have raised here!') + # except Exception as exc: + # logger.info(f'{exc}') diff --git a/pyproject.toml b/pyproject.toml index 017a063..4cb571b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,10 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -dependencies = ["bec_ipython_client", "bec_lib", "bec_server", "rich", "pyepics"] +dependencies = ["bec_ipython_client", "bec_lib", "bec_server", "ophyd_devices", "std_daq_client", "rich", "pyepics"] [project.optional-dependencies] -dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order"] +dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order", "pytest-redis"] [project.entry-points."bec"] plugin_bec = "csaxs_bec" diff --git a/tests/tests_bec_ipython_client/README.md b/tests/tests_bec_ipython_client/README.md new file mode 100644 index 0000000..5762245 --- /dev/null +++ b/tests/tests_bec_ipython_client/README.md @@ -0,0 +1,31 @@ +# 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 +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 ``. + +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). + diff --git a/tests/tests_bec_ipython_client/test_beamline_info.py b/tests/tests_bec_ipython_client/test_beamline_info.py new file mode 100644 index 0000000..652d6ff --- /dev/null +++ b/tests/tests_bec_ipython_client/test_beamline_info.py @@ -0,0 +1,61 @@ +import io +from unittest import mock + +import pytest +from bec_ipython_client.beamline_mixin import BeamlineMixin +from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo +from rich.console import Console + +from csaxs_bec.bec_ipython_client.plugins.cSAXS.beamline_info import BeamlineInfo + + +def test_bl_show_all(): + mixin = BeamlineMixin() + mixin._bl_info_register(SLSInfo) + mixin._bl_info_register(OperatorInfo) + mixin._bl_info_register(BeamlineInfo) + calls = mixin._bl_calls + with mock.patch.object(calls[0], "show") as sls_info: + with mock.patch.object(calls[1], "show") as op_msgs: + with mock.patch.object(calls[2], "show") as bl_info: + mixin.bl_show_all() + bl_info.assert_called_once() + op_msgs.assert_called_once() + sls_info.assert_called_once() + + +@pytest.mark.parametrize( + "info,out", + ( + [ + ( + { + "x12sa_op_status": {"value": "attended"}, + "x12sa_id_gap": {"value": 4.2}, + "x12sa_storage_ring_vac": {"value": "OK"}, + "x12sa_es1_shutter_status": {"value": "OPEN"}, + "x12sa_mokev": {"value": 6.2002}, + "x12sa_fe_status": {"value": "Open enabled"}, + "x12sa_es1_valve": {"value": "open"}, + "x12sa_exposure_box1_pressure": {"value": 7.975205787427068e-09}, + "x12sa_exposure_box2_pressure": {"value": 7.975205787427068e-09}, + }, + " X12SA Info \n┌─────────────────────────┬──────────────┐\n│ Key │ Value │\n├─────────────────────────┼──────────────┤\n│ Beamline operation │ attended │\n│ ID gap │ 4.200 mm │\n│ Storage ring vacuum │ OK │\n│ Shutter │ OPEN │\n│ Selected energy (mokev) │ 6.200 keV │\n│ Front end shutter │ Open enabled │\n│ ES1 valve │ open │\n│ Exposure box 1 pressure │ 8.0e-09 mbar │\n│ Exposure box 2 pressure │ 8.0e-09 mbar │\n└─────────────────────────┴──────────────┘\n", + ) + ] + ), +) +def test_bl_info(info, out): + mixin = BeamlineMixin() + mixin._bl_info_register(BeamlineInfo) + bl_call = mixin._bl_calls[-1] + with mock.patch.object( + bl_call, "_get_beamline_info_messages", return_value=info + ) as get_bl_info: + console = Console(file=io.StringIO(), width=120) + with mock.patch.object(bl_call, "_get_console", return_value=console): + mixin.bl_show_all() + get_bl_info.assert_called_once() + # pylint: disable=no-member + output = console.file.getvalue() + assert output == out diff --git a/tests/tests_bec_ipython_client/test_x_ray_eye_align.py b/tests/tests_bec_ipython_client/test_x_ray_eye_align.py new file mode 100644 index 0000000..c295295 --- /dev/null +++ b/tests/tests_bec_ipython_client/test_x_ray_eye_align.py @@ -0,0 +1,105 @@ +from unittest import mock + +from bec_lib.device import DeviceBase + +from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI, XrayEyeAlign + +# pylint: disable=unused-import + + +# pylint: disable=no-member +# pylint: disable=missing-function-docstring +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access + + +class RTControllerMock: + def feedback_disable(self): + pass + + def feedback_enable_with_reset(self): + pass + + +class RTMock(DeviceBase): + controller = RTControllerMock() + enabled = True + + +def test_save_frame(bec_client_mock): + client = bec_client_mock + client.device_manager.devices.xeye = DeviceBase(name="xeye", config={}) + lamni = LamNI(client) + align = XrayEyeAlign(client, lamni) + with mock.patch( + "csaxs_bec.bec_ipython_client.plugins.LamNI.x_ray_eye_align.epics_put" + ) as epics_put_mock: + align.save_frame() + epics_put_mock.assert_called_once_with("XOMNYI-XEYE-SAVFRAME:0", 1) + + +def test_update_frame(bec_client_mock): + epics_put = "csaxs_bec.bec_ipython_client.plugins.LamNI.x_ray_eye_align.epics_put" + epics_get = "csaxs_bec.bec_ipython_client.plugins.LamNI.x_ray_eye_align.epics_get" + fshopen = "csaxs_bec.bec_ipython_client.plugins.LamNI.x_ray_eye_align.fshopen" + client = bec_client_mock + client.device_manager.devices.xeye = DeviceBase(name="xeye", config={}) + lamni = LamNI(client) + align = XrayEyeAlign(client, lamni) + with mock.patch(epics_put) as epics_put_mock: + with mock.patch(epics_get) as epics_get_mock: + with mock.patch(fshopen) as fshopen_mock: + align.update_frame() + epics_put_mock.assert_has_calls( + [ + mock.call("XOMNYI-XEYE-ACQDONE:0", 0), + mock.call("XOMNYI-XEYE-ACQ:0", 1), + mock.call("XOMNYI-XEYE-ACQDONE:0", 0), + mock.call("XOMNYI-XEYE-ACQ:0", 0), + ] + ) + fshopen_mock.assert_called_once() + epics_get_mock.assert_called_with("XOMNYI-XEYE-ACQDONE:0") + + +def test_disable_rt_feedback(bec_client_mock): + client = bec_client_mock + client.device_manager.devices.xeye = DeviceBase(name="xeye", config={}) + lamni = LamNI(client) + align = XrayEyeAlign(client, lamni) + client.device_manager.devices.rtx = RTMock(name="rtx", config={}) + with mock.patch.object( + align.device_manager.devices.rtx.controller, "feedback_disable" + ) as fdb_disable: + align._disable_rt_feedback() + fdb_disable.assert_called_once() + + +def test_enable_rt_feedback(bec_client_mock): + client = bec_client_mock + client.device_manager.devices.xeye = DeviceBase(name="xeye", config={}) + lamni = LamNI(client) + align = XrayEyeAlign(client, lamni) + client.device_manager.devices.rtx = RTMock(name="rtx", config={}) + with mock.patch.object( + align.device_manager.devices.rtx.controller, "feedback_enable_with_reset" + ) as fdb_enable: + align._enable_rt_feedback() + fdb_enable.assert_called_once() + + +def test_tomo_rotate(bec_client_mock): + import builtins + + client = bec_client_mock + client._ip = mock.MagicMock() + client._update_namespace_callback = mock.MagicMock() + client.callbacks = mock.MagicMock() + client.load_high_level_interface("bec_hli") + client.device_manager.devices.xeye = DeviceBase(name="xeye", config={}) + lamni = LamNI(client) + align = XrayEyeAlign(client, lamni) + client.device_manager.devices.lsamrot = RTMock(name="lsamrot", config={}) + with mock.patch.object(builtins, "umv") as umv: + align.tomo_rotate(5) + umv.assert_called_once_with(client.device_manager.devices.lsamrot, 5) diff --git a/tests/tests_bec_widgets/README.md b/tests/tests_bec_widgets/README.md new file mode 100644 index 0000000..5762245 --- /dev/null +++ b/tests/tests_bec_widgets/README.md @@ -0,0 +1,31 @@ +# 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 +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 ``. + +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). + diff --git a/tests/tests_dap_services/README.md b/tests/tests_dap_services/README.md new file mode 100644 index 0000000..5762245 --- /dev/null +++ b/tests/tests_dap_services/README.md @@ -0,0 +1,31 @@ +# 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 +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 ``. + +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). + diff --git a/tests/tests_devices/README.md b/tests/tests_devices/README.md new file mode 100644 index 0000000..5762245 --- /dev/null +++ b/tests/tests_devices/README.md @@ -0,0 +1,31 @@ +# 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 +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 ``. + +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). + diff --git a/tests/tests_devices/test_delay_generator_csaxs.py b/tests/tests_devices/test_delay_generator_csaxs.py new file mode 100644 index 0000000..4b92ca5 --- /dev/null +++ b/tests/tests_devices/test_delay_generator_csaxs.py @@ -0,0 +1,298 @@ +# pylint: skip-file +from unittest import mock + +import pytest +from ophyd_devices.epics.devices.psi_delay_generator_base import TriggerSource + +from csaxs_bec.devices.epics.devices.delay_generator_csaxs import DDGSetup + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_DDGSetup(): + mock_ddg = mock.MagicMock() + yield DDGSetup(parent=mock_ddg) + + +# Fixture for scaninfo +@pytest.fixture( + params=[ + { + "scan_id": "1234", + "scan_type": "step", + "num_points": 500, + "frames_per_trigger": 1, + "exp_time": 0.1, + "readout_time": 0.1, + }, + { + "scan_id": "1234", + "scan_type": "step", + "num_points": 500, + "frames_per_trigger": 5, + "exp_time": 0.01, + "readout_time": 0, + }, + { + "scan_id": "1234", + "scan_type": "fly", + "num_points": 500, + "frames_per_trigger": 1, + "exp_time": 1, + "readout_time": 0.2, + }, + { + "scan_id": "1234", + "scan_type": "fly", + "num_points": 500, + "frames_per_trigger": 5, + "exp_time": 0.1, + "readout_time": 0.4, + }, + ] +) +def scaninfo(request): + return request.param + + +# Fixture for DDG config default values +@pytest.fixture( + params=[ + { + "delay_burst": 0.0, + "delta_width": 0.0, + "additional_triggers": 0, + "polarity": [0, 0, 0, 0, 0], + "amplitude": 0.0, + "offset": 0.0, + "thres_trig_level": 0.0, + }, + { + "delay_burst": 0.1, + "delta_width": 0.1, + "additional_triggers": 1, + "polarity": [0, 0, 1, 0, 0], + "amplitude": 5, + "offset": 0.0, + "thres_trig_level": 2.5, + }, + ] +) +def ddg_config_defaults(request): + return request.param + + +# Fixture for DDG config scan values +@pytest.fixture( + params=[ + { + "fixed_ttl_width": [0, 0, 0, 0, 0], + "trigger_width": None, + "set_high_on_exposure": False, + "set_high_on_stage": False, + "set_trigger_source": "SINGLE_SHOT", + "premove_trigger": False, + }, + { + "fixed_ttl_width": [0, 0, 0, 0, 0], + "trigger_width": 0.1, + "set_high_on_exposure": True, + "set_high_on_stage": False, + "set_trigger_source": "SINGLE_SHOT", + "premove_trigger": True, + }, + { + "fixed_ttl_width": [0, 0, 0, 0, 0], + "trigger_width": 0.1, + "set_high_on_exposure": False, + "set_high_on_stage": False, + "set_trigger_source": "EXT_RISING_EDGE", + "premove_trigger": False, + }, + ] +) +def ddg_config_scan(request): + return request.param + + +# Fixture for delay pairs +@pytest.fixture( + params=[ + {"all_channels": ["channelAB", "channelCD"], "all_delay_pairs": ["AB", "CD"]}, + {"all_channels": [], "all_delay_pairs": []}, + {"all_channels": ["channelT0", "channelAB", "channelCD"], "all_delay_pairs": ["AB", "CD"]}, + ] +) +def channel_pairs(request): + return request.param + + +def test_check_scan_id(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan): + """Test the check_scan_id method.""" + # Set first attributes of parent class + for k, v in scaninfo.items(): + setattr(mock_DDGSetup.parent.scaninfo, k, v) + for k, v in ddg_config_defaults.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + for k, v in ddg_config_scan.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + # Call the function you want to test + mock_DDGSetup.check_scan_id() + mock_DDGSetup.parent.scaninfo.load_scan_metadata.assert_called_once() + + +def test_on_pre_scan(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan): + """Test the check_scan_id method.""" + # Set first attributes of parent class + for k, v in scaninfo.items(): + setattr(mock_DDGSetup.parent.scaninfo, k, v) + for k, v in ddg_config_defaults.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + for k, v in ddg_config_scan.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + # Call the function you want to test + mock_DDGSetup.on_pre_scan() + if ddg_config_scan["premove_trigger"]: + mock_DDGSetup.parent.trigger_shot.put.assert_called_once_with(1) + + +@pytest.mark.parametrize("source", ["SINGLE_SHOT", "EXT_RISING_EDGE"]) +def test_on_trigger(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, source): + """Test the on_trigger method.""" + # Set first attributes of parent class + for k, v in scaninfo.items(): + setattr(mock_DDGSetup.parent.scaninfo, k, v) + for k, v in ddg_config_defaults.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + for k, v in ddg_config_scan.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + # Call the function you want to test + mock_DDGSetup.parent.source.name = "source" + mock_DDGSetup.parent.source.read.return_value = { + mock_DDGSetup.parent.source.name: {"value": getattr(TriggerSource, source)} + } + mock_DDGSetup.on_trigger() + if source == "SINGLE_SHOT": + mock_DDGSetup.parent.trigger_shot.put.assert_called_once_with(1) + + +def test_initialize_default_parameter( + mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, channel_pairs +): + """Test the initialize_default_parameter method.""" + # Set first attributes of parent class + for k, v in scaninfo.items(): + setattr(mock_DDGSetup.parent.scaninfo, k, v) + for k, v in ddg_config_defaults.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + for k, v in ddg_config_scan.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + # Call the function you want to test + mock_DDGSetup.parent.all_channels = channel_pairs["all_channels"] + mock_DDGSetup.parent.all_delay_pairs = channel_pairs["all_delay_pairs"] + calls = [] + calls.extend( + [ + mock.call("polarity", ddg_config_defaults["polarity"][ii], [channel]) + for ii, channel in enumerate(channel_pairs["all_channels"]) + ] + ) + calls.extend([mock.call("amplitude", ddg_config_defaults["amplitude"])]) + calls.extend([mock.call("offset", ddg_config_defaults["offset"])]) + calls.extend( + [ + mock.call( + "reference", 0, [f"channel{pair}.ch1" for pair in channel_pairs["all_delay_pairs"]] + ) + ] + ) + calls.extend( + [ + mock.call( + "reference", 0, [f"channel{pair}.ch2" for pair in channel_pairs["all_delay_pairs"]] + ) + ] + ) + mock_DDGSetup.initialize_default_parameter() + mock_DDGSetup.parent.set_channels.assert_has_calls(calls) + + +def test_prepare_ddg(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, channel_pairs): + """Test the prepare_ddg method.""" + # Set first attributes of parent class + for k, v in scaninfo.items(): + setattr(mock_DDGSetup.parent.scaninfo, k, v) + for k, v in ddg_config_defaults.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + for k, v in ddg_config_scan.items(): + getattr(mock_DDGSetup.parent, k).get.return_value = v + # Call the function you want to test + mock_DDGSetup.parent.all_channels = channel_pairs["all_channels"] + mock_DDGSetup.parent.all_delay_pairs = channel_pairs["all_delay_pairs"] + + mock_DDGSetup.prepare_ddg() + mock_DDGSetup.parent.set_trigger.assert_called_once_with( + getattr(TriggerSource, ddg_config_scan["set_trigger_source"]) + ) + if scaninfo["scan_type"] == "step": + if ddg_config_scan["set_high_on_exposure"]: + num_burst_cycle = 1 + ddg_config_defaults["additional_triggers"] + exp_time = ddg_config_defaults["delta_width"] + scaninfo["frames_per_trigger"] * ( + scaninfo["exp_time"] + scaninfo["readout_time"] + ) + total_exposure = exp_time + delay_burst = ddg_config_defaults["delay_burst"] + else: + exp_time = ddg_config_defaults["delta_width"] + scaninfo["exp_time"] + total_exposure = exp_time + scaninfo["readout_time"] + delay_burst = ddg_config_defaults["delay_burst"] + num_burst_cycle = ( + scaninfo["frames_per_trigger"] + ddg_config_defaults["additional_triggers"] + ) + elif scaninfo["scan_type"] == "fly": + if ddg_config_scan["set_high_on_exposure"]: + num_burst_cycle = 1 + ddg_config_defaults["additional_triggers"] + exp_time = ( + ddg_config_defaults["delta_width"] + + scaninfo["num_points"] * scaninfo["exp_time"] + + (scaninfo["num_points"] - 1) * scaninfo["readout_time"] + ) + total_exposure = exp_time + delay_burst = ddg_config_defaults["delay_burst"] + else: + exp_time = ddg_config_defaults["delta_width"] + scaninfo["exp_time"] + total_exposure = exp_time + scaninfo["readout_time"] + delay_burst = ddg_config_defaults["delay_burst"] + num_burst_cycle = scaninfo["num_points"] + ddg_config_defaults["additional_triggers"] + + # mock_DDGSetup.parent.burst_enable.assert_called_once_with( + # mock.call(num_burst_cycle, delay_burst, total_exposure, config="first") + # ) + mock_DDGSetup.parent.burst_enable.assert_called_once_with( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) + if not ddg_config_scan["trigger_width"]: + call = mock.call("width", exp_time) + assert call in mock_DDGSetup.parent.set_channels.mock_calls + else: + call = mock.call("width", ddg_config_scan["trigger_width"]) + assert call in mock_DDGSetup.parent.set_channels.mock_calls + if ddg_config_scan["set_high_on_exposure"]: + calls = [ + mock.call("width", value, channels=[channel]) + for value, channel in zip( + ddg_config_scan["fixed_ttl_width"], channel_pairs["all_channels"] + ) + if value != 0 + ] + if calls: + assert all(calls in mock_DDGSetup.parent.set_channels.mock_calls) diff --git a/tests/tests_devices/test_eiger9m_csaxs.py b/tests/tests_devices/test_eiger9m_csaxs.py new file mode 100644 index 0000000..2feffe2 --- /dev/null +++ b/tests/tests_devices/test_eiger9m_csaxs.py @@ -0,0 +1,456 @@ +# pylint: skip-file +import threading +from unittest import mock + +import ophyd +import pytest +from bec_lib import MessageEndpoints, messages +from bec_server.device_server.tests.utils import DMMock +from ophyd_devices.tests.utils import MockPV + +from csaxs_bec.devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_det(): + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"), + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ), + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + mock_cl.thread_class = threading.Thread + with mock.patch.object(Eiger9McSAXS, "_init"): + det = Eiger9McSAXS( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + patch_dual_pvs(det) + yield det + + +def test_init(): + """Test the _init function:""" + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"), + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ), + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + with ( + mock.patch( + "csaxs_bec.devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_default_parameter" + ) as mock_default, + mock.patch( + "csaxs_bec.devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_detector" + ) as mock_init_det, + mock.patch( + "csaxs_bec.devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_detector_backend" + ) as mock_init_backend, + ): + Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + mock_default.assert_called_once() + mock_init_det.assert_called_once() + mock_init_backend.assert_called_once() + + +@pytest.mark.parametrize( + "trigger_source, detector_state, expected_exception", [(2, 1, True), (2, 0, False)] +) +def test_initialize_detector(mock_det, trigger_source, detector_state, expected_exception): + """Test the _init function: + + This includes testing the functions: + - _init_detector + - _stop_det + - _set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + mock_det.cam.detector_state._read_pv.mock_data = detector_state + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.initialize_detector() + else: + mock_det.custom_prepare.initialize_detector() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + + +def test_trigger(mock_det): + """Test the trigger function: + Validate that trigger calls the custom_prepare.on_trigger() function + """ + with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger: + mock_det.trigger() + mock_on_trigger.assert_called_once() + + +@pytest.mark.parametrize( + "readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)] +) +def test_update_readout_time(mock_det, readout_time, expected_value): + if readout_time is None: + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + else: + mock_det.scaninfo.readout_time = readout_time + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + + +@pytest.mark.parametrize( + "eacc, exp_url, daq_status, daq_cfg, expected_exception", + [ + ("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_user_id": 12543}, False), + ("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_user_id": 15421}, False), + ("e12345", "http://xbl-daq-29:5000", {"state": "BUSY"}, {"writer_user_id": 15421}, True), + ("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_ud": 12345}, True), + ], +) +def test_initialize_detector_backend( + mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception +): + """Test self.custom_prepare.initialize_detector_backend (std daq in this case) + + This includes testing the functions: + + - _update_service_config + + Validation upon checking set values in mocked std_daq instance + """ + with mock.patch("csaxs_bec.devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: + instance = mock_std_daq.return_value + instance.stop_writer.return_value = None + instance.get_status.return_value = daq_status + instance.get_config.return_value = daq_cfg + mock_det.scaninfo.username = eacc + # scaninfo.username.return_value = eacc + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.initialize_detector_backend() + else: + mock_det.custom_prepare.initialize_detector_backend() + + instance.stop_writer.assert_called_once() + instance.get_status.assert_called() + instance.set_config.assert_called_once_with(daq_cfg) + + +@pytest.mark.parametrize( + "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 12.4, + }, + {"state": "READY"}, + {"writer_user_id": 12543}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 12.4, + }, + {"state": "BUSY"}, + {"writer_user_id": 15421}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 18.4, + }, + {"state": "READY"}, + {"writer_user_id": 12345}, + 4, + False, + True, + ), + ], +) +def test_stage( + mock_det, scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception +): + with ( + mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + ): + mock_std_daq.stop_writer.return_value = None + mock_std_daq.get_status.return_value = daq_status + mock_std_daq.get_config.return_value = daq_cfg + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + # TODO consider putting energy as variable in scaninfo + mock_det.device_manager.add_device("mokev", value=12.4) + mock_det.cam.beam_energy.put(scaninfo["mokev"]) + mock_det.stopped = stopped + mock_det.cam.detector_state._read_pv.mock_data = detector_state + with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw: + mock_det.filepath = scaninfo["filepath"] + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.stage() + else: + mock_det.stage() + mock_prep_fw.assert_called_once() + # Check _prep_det + assert mock_det.cam.num_images.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.cam.num_frames.get() == 1 + + mock_publish_file_location.assert_called_with(done=False) + assert mock_det.cam.acquire.get() == 1 + + +@pytest.mark.parametrize( + "scaninfo, daq_status, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + }, + {"state": "BUSY", "acquisition": {"state": "ERROR"}}, + True, + ), + ], +) +def test_prepare_detector_backend(mock_det, scaninfo, daq_status, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq, + mock.patch.object(mock_det.custom_prepare, "filepath_exists") as mock_file_path_exists, + mock.patch.object(mock_det.custom_prepare, "stop_detector_backend") as mock_stop_backend, + mock.patch.object(mock_det, "scaninfo"), + ): + mock_std_daq.start_writer_async.return_value = None + mock_std_daq.get_status.return_value = daq_status + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.prepare_data_backend() + mock_file_path_exists.assert_called_once() + assert mock_stop_backend.call_count == 2 + + else: + mock_det.custom_prepare.prepare_data_backend() + mock_file_path_exists.assert_called_once() + mock_stop_backend.assert_called_once() + + daq_writer_call = { + "output_file": scaninfo["filepath"], + "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), + } + mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) + + +@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)]) +def test_unstage(mock_det, stopped, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + ): + mock_det.stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det.stopped is True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det.stopped is False + + +def test_stop_detector_backend(mock_det): + with mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq: + mock_std_daq.stop_writer.return_value = None + mock_det.std_client = mock_std_daq + mock_det.custom_prepare.stop_detector_backend() + mock_std_daq.stop_writer.assert_called_once() + + +@pytest.mark.parametrize( + "scaninfo", + [ + ({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}), + ({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}), + ({"filepath": "test.h5", "successful": None, "done": True, "scan_id": "123"}), + ], +) +def test_publish_file_location(mock_det, scaninfo): + mock_det.scaninfo.scan_id = scaninfo["scan_id"] + mock_det.filepath = scaninfo["filepath"] + mock_det.custom_prepare.publish_file_location( + done=scaninfo["done"], successful=scaninfo["successful"] + ) + if scaninfo["successful"] is None: + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) + else: + msg = messages.FileMessage( + file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] + ) + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + ] + assert mock_det.connector.set_and_publish.call_args_list == expected_calls + + +def test_stop(mock_det): + with ( + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" + ) as mock_stop_detector_backend, + ): + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_detector_backend.assert_called_once() + assert mock_det.stopped is True + + +@pytest.mark.parametrize( + "stopped, scaninfo, cam_state, daq_status, expected_exception", + [ + ( + False, + {"num_points": 500, "frames_per_trigger": 4}, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, + False, + ), + ( + False, + {"num_points": 500, "frames_per_trigger": 4}, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, + True, + ), + ( + False, + {"num_points": 500, "frames_per_trigger": 1}, + 1, + {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, + True, + ), + ( + False, + {"num_points": 500, "frames_per_trigger": 1}, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, + False, + ), + ], +) +def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq, + mock.patch.object(mock_det.custom_prepare, "stop_detector_backend") as mock_stop_backend, + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + ): + mock_std_daq.get_status.return_value = daq_status + mock_det.cam.acquire._read_pv.mock_state = cam_state + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.finished() + assert mock_det.stopped is stopped + else: + mock_det.custom_prepare.finished() + if stopped: + assert mock_det.stopped is stopped + + mock_stop_backend.assert_called() + mock_stop_det.assert_called_once() diff --git a/tests/tests_devices/test_falcon_csaxs.py b/tests/tests_devices/test_falcon_csaxs.py new file mode 100644 index 0000000..de1f2db --- /dev/null +++ b/tests/tests_devices/test_falcon_csaxs.py @@ -0,0 +1,313 @@ +# pylint: skip-file +import os +import threading +from unittest import mock + +import ophyd +import pytest +from bec_lib import MessageEndpoints, messages +from bec_server.device_server.tests.utils import DMMock +from ophyd_devices.tests.utils import MockPV + +from csaxs_bec.devices.epics.devices.falcon_csaxs import FalconcSAXS, FalconTimeoutError + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_det(): + name = "falcon" + prefix = "X12SA-SITORO:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter") as filemixin, + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ) as mock_service_config, + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + mock_cl.thread_class = threading.Thread + with mock.patch.object(FalconcSAXS, "_init"): + det = FalconcSAXS( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + patch_dual_pvs(det) + yield det + + +@pytest.mark.parametrize( + "trigger_source, mapping_source, ignore_gate, pixels_per_buffer, detector_state," + " expected_exception", + [(1, 1, 0, 20, 0, False), (1, 1, 0, 20, 1, True)], +) +# TODO rewrite this one, write test for init_detector, init_filewriter is tested +def test_init_detector( + mock_det, + trigger_source, + mapping_source, + ignore_gate, + pixels_per_buffer, + detector_state, + expected_exception, +): + """Test the _init function: + + This includes testing the functions: + - _init_detector + - _stop_det + - _set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + mock_det.value_pixel_per_buffer = pixels_per_buffer + mock_det.state._read_pv.mock_data = detector_state + if expected_exception: + with pytest.raises(FalconTimeoutError): + mock_det.timeout = 0.1 + mock_det.custom_prepare.initialize_detector() + else: + mock_det.custom_prepare.initialize_detector() + assert mock_det.state.get() == detector_state + assert mock_det.collect_mode.get() == mapping_source + assert mock_det.pixel_advance_mode.get() == trigger_source + assert mock_det.ignore_gate.get() == ignore_gate + + assert mock_det.preset_mode.get() == 1 + assert mock_det.erase_all.get() == 1 + assert mock_det.input_logic_polarity.get() == 0 + assert mock_det.auto_pixels_per_buffer.get() == 0 + assert mock_det.pixels_per_buffer.get() == pixels_per_buffer + + +@pytest.mark.parametrize( + "readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)] +) +def test_update_readout_time(mock_det, readout_time, expected_value): + if readout_time is None: + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + else: + mock_det.scaninfo.readout_time = readout_time + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + + +def test_initialize_default_parameter(mock_det): + with mock.patch.object( + mock_det.custom_prepare, "update_readout_time" + ) as mock_update_readout_time: + mock_det.custom_prepare.initialize_default_parameter() + assert mock_det.value_pixel_per_buffer == 20 + mock_update_readout_time.assert_called_once() + + +@pytest.mark.parametrize( + "scaninfo", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "exp_time": 0.1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 12.4, + } + ) + ], +) +def test_stage(mock_det, scaninfo): + """Test the stage function: + + This includes testing _prep_det + """ + with ( + mock.patch.object(mock_det, "set_trigger") as mock_set_trigger, + mock.patch.object( + mock_det.custom_prepare, "prepare_detector_backend" + ) as mock_prep_data_backend, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + mock.patch.object(mock_det.custom_prepare, "arm_acquisition") as mock_arm_acquisition, + ): + mock_det.scaninfo.exp_time = scaninfo["exp_time"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.stage() + mock_set_trigger.assert_called_once() + assert mock_det.preset_real.get() == scaninfo["exp_time"] + assert mock_det.pixels_per_run.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + mock_prep_data_backend.assert_called_once() + mock_publish_file_location.assert_called_once_with(done=False) + mock_arm_acquisition.assert_called_once() + + +@pytest.mark.parametrize( + "scaninfo", + [ + ( + { + "filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data.h5", + "num_points": 500, + "frames_per_trigger": 1, + } + ), + ( + { + "filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data1234.h5", + "num_points": 500, + "frames_per_trigger": 1, + } + ), + ], +) +def test_prepare_data_backend(mock_det, scaninfo): + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.scaninfo.scan_number = 1 + mock_det.custom_prepare.prepare_data_backend() + file_path, file_name = os.path.split(scaninfo["filepath"]) + assert mock_det.hdf5.file_path.get() == file_path + assert mock_det.hdf5.file_name.get() == file_name + assert mock_det.hdf5.file_template.get() == "%s%s" + assert mock_det.hdf5.num_capture.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.hdf5.file_write_mode.get() == 2 + assert mock_det.hdf5.array_counter.get() == 0 + assert mock_det.hdf5.capture.get() == 1 + + +@pytest.mark.parametrize( + "scaninfo", + [ + ({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}), + ({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}), + ({"filepath": "test.h5", "successful": None, "done": True, "scan_id": "123"}), + ], +) +def test_publish_file_location(mock_det, scaninfo): + mock_det.scaninfo.scan_id = scaninfo["scan_id"] + mock_det.filepath = scaninfo["filepath"] + mock_det.custom_prepare.publish_file_location( + done=scaninfo["done"], successful=scaninfo["successful"] + ) + if scaninfo["successful"] is None: + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) + else: + msg = messages.FileMessage( + file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] + ) + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + ] + assert mock_det.connector.set_and_publish.call_args_list == expected_calls + + +@pytest.mark.parametrize("detector_state, expected_exception", [(1, False), (0, True)]) +def test_arm_acquisition(mock_det, detector_state, expected_exception): + with mock.patch.object(mock_det, "stop") as mock_stop: + mock_det.state._read_pv.mock_data = detector_state + if expected_exception: + with pytest.raises(FalconTimeoutError): + mock_det.timeout = 0.1 + mock_det.custom_prepare.arm_acquisition() + mock_stop.assert_called_once() + else: + mock_det.custom_prepare.arm_acquisition() + assert mock_det.start_all.get() == 1 + + +def test_trigger(mock_det): + with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger: + mock_det.trigger() + mock_on_trigger.assert_called_once() + + +@pytest.mark.parametrize("stopped, expected_abort", [(False, False), (True, True)]) +def test_unstage(mock_det, stopped, expected_abort): + with ( + mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + ): + mock_det.stopped = stopped + if expected_abort: + mock_det.unstage() + assert mock_det.stopped is stopped + assert mock_publish_file_location.call_count == 0 + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det.stopped is stopped + + +def test_stop(mock_det): + with ( + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" + ) as mock_stop_detector_backend, + ): + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_detector_backend.assert_called_once() + assert mock_det.stopped is True + + +@pytest.mark.parametrize( + "stopped, scaninfo", + [ + (False, {"num_points": 500, "frames_per_trigger": 1}), + (True, {"num_points": 500, "frames_per_trigger": 1}), + ], +) +def test_finished(mock_det, stopped, scaninfo): + with ( + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" + ) as mock_stop_file_writer, + ): + mock_det.stopped = stopped + mock_det.dxp.current_pixel._read_pv.mock_data = int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + mock_det.hdf5.array_counter._read_pv.mock_data = int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.custom_prepare.finished() + assert mock_det.stopped is stopped + mock_stop_det.assert_called_once() + mock_stop_file_writer.assert_called_once() diff --git a/tests/tests_devices/test_fupr_ophyd.py b/tests/tests_devices/test_fupr_ophyd.py new file mode 100644 index 0000000..b8ba38e --- /dev/null +++ b/tests/tests_devices/test_fupr_ophyd.py @@ -0,0 +1,60 @@ +from unittest import mock + +import pytest +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.galil.fupr_ophyd import FuprGalilController, FuprGalilMotor + + +@pytest.fixture +def fsamroy(): + FuprGalilController._reset_controller() + fsamroy_motor = FuprGalilMotor( + "A", name="fsamroy", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + fsamroy_motor.controller.on() + assert isinstance(fsamroy_motor.controller, FuprGalilController) + yield fsamroy_motor + fsamroy_motor.controller.off() + fsamroy_motor.controller._reset_controller() + + +@pytest.mark.parametrize( + "pos,msg_received,msg_put,sign", + [ + (-0.5, b" -12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], 1), + (-0.5, b" 12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], -1), + ], +) +def test_axis_get(fsamroy, pos, msg_received, msg_put, sign): + fsamroy.sign = sign + fsamroy.device_manager = mock.MagicMock() + fsamroy.controller.sock.flush_buffer() + fsamroy.controller.sock.buffer_recv = msg_received + val = fsamroy.read() + assert val["fsamroy"]["value"] == pos + assert fsamroy.readback.get() == pos + assert fsamroy.controller.sock.buffer_put == msg_put + + +@pytest.mark.parametrize( + "target_pos,socket_put_messages,socket_get_messages", + [ + ( + 0, + [b"MG axisref\r", b"PAA=0\r", b"PAA=0\r", b"BGA\r"], + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) + ], +) +def test_axis_put(fsamroy, target_pos, socket_put_messages, socket_get_messages): + fsamroy.controller.sock.flush_buffer() + fsamroy.controller.sock.buffer_recv = socket_get_messages + fsamroy.user_setpoint.put(target_pos) + assert fsamroy.controller.sock.buffer_put == socket_put_messages + + +def test_drive_axis_to_limit(fsamroy): + fsamroy.controller.sock.flush_buffer() + with pytest.raises(NotImplementedError): + fsamroy.controller.drive_axis_to_limit(0, "forward") diff --git a/tests/tests_devices/test_galil.py b/tests/tests_devices/test_galil.py new file mode 100644 index 0000000..d375409 --- /dev/null +++ b/tests/tests_devices/test_galil.py @@ -0,0 +1,149 @@ +from unittest import mock + +import pytest +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.galil.galil_ophyd import GalilController, GalilMotor + + +@pytest.fixture(scope="function") +def leyey(): + GalilController._reset_controller() + leyey_motor = GalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyey_motor.controller.on() + yield leyey_motor + leyey_motor.controller.off() + leyey_motor.controller._reset_controller() + + +@pytest.fixture(scope="function") +def leyex(): + GalilController._reset_controller() + leyex_motor = GalilMotor( + "A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyex_motor.controller.on() + yield leyex_motor + leyex_motor.controller.off() + leyex_motor.controller._reset_controller() + + +@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)]) +def test_axis_get(leyey, pos, msg, sign): + leyey.sign = sign + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = msg + val = leyey.read() + assert val["leyey"]["value"] == pos + assert leyey.readback.get() == pos + + +@pytest.mark.parametrize( + "target_pos,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"MG allaxref\r", + b"MG_XQ0\r", + b"naxis=7\r", + b"ntarget=0.000\r", + b"movereq=1\r", + b"XQ#NEWPAR\r", + b"MG_XQ0\r", + ], + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) + ], +) +def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = socket_get_messages + leyey.user_setpoint.put(target_pos) + assert leyey.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,direction,socket_put_messages,socket_get_messages", + [ + ( + 0, + "forward", + [ + b"naxis=0\r", + b"ndir=1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_BGA\r", + b"MGbcklact[axis]\r", + b"MG_XQ0\r", + b"MG_XQ2\r", + b"MG _LRA, _LFA\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], + ), + ( + 1, + "reverse", + [ + b"naxis=1\r", + b"ndir=-1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_BGB\r", + b"MGbcklact[axis]\r", + b"MG_XQ0\r", + b"MG_XQ2\r", + b"MG _LRB, _LFB\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], + ), + ], +) +def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.drive_axis_to_limit(axis_nr, direction) + assert leyex.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"naxis=0\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_BGA\r", + b"MGbcklact[axis]\r", + b"MG_XQ0\r", + b"MG_XQ2\r", + b"MG axisref[0]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ( + 1, + [ + b"naxis=1\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_BGB\r", + b"MGbcklact[axis]\r", + b"MG_XQ0\r", + b"MG_XQ2\r", + b"MG axisref[1]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ], +) +def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.find_reference(axis_nr) + assert leyex.controller.sock.buffer_put == socket_put_messages diff --git a/tests/tests_devices/test_galil_flomni.py b/tests/tests_devices/test_galil_flomni.py new file mode 100644 index 0000000..3450d29 --- /dev/null +++ b/tests/tests_devices/test_galil_flomni.py @@ -0,0 +1,172 @@ +from unittest import mock + +import pytest +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.galil.fgalil_ophyd import FlomniGalilController, FlomniGalilMotor + + +@pytest.fixture(scope="function") +def leyey(): + FlomniGalilController._reset_controller() + leyey_motor = FlomniGalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyey_motor.controller.on() + yield leyey_motor + leyey_motor.controller.off() + leyey_motor.controller._reset_controller() + + +@pytest.fixture(scope="function") +def leyex(): + FlomniGalilController._reset_controller() + leyex_motor = FlomniGalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyex_motor.controller.on() + yield leyex_motor + leyex_motor.controller.off() + leyex_motor.controller._reset_controller() + + +@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)]) +def test_axis_get(leyey, pos, msg, sign): + leyey.sign = sign + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = msg + val = leyey.read() + assert val["leyey"]["value"] == pos + assert leyey.readback.get() == pos + + +@pytest.mark.parametrize( + "target_pos,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"MG allaxref\r", + b"MG_XQ0\r", + b"naxis=7\r", + b"ntarget=0.000\r", + b"movereq=1\r", + b"XQ#NEWPAR\r", + b"MG_XQ0\r", + ], + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) + ], +) +def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = socket_get_messages + leyey.user_setpoint.put(target_pos) + assert leyey.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,direction,socket_put_messages,socket_get_messages", + [ + ( + 0, + "forward", + [ + b"naxis=0\r", + b"ndir=1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG _LRA, _LFA\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], + ), + ( + 1, + "reverse", + [ + b"naxis=1\r", + b"ndir=-1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG _LRB, _LFB\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], + ), + ], +) +def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.drive_axis_to_limit(axis_nr, direction) + assert leyex.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"naxis=0\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG axisref[0]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ( + 1, + [ + b"naxis=1\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG axisref[1]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ], +) +def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.find_reference(axis_nr) + assert leyex.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_Id,socket_put_messages,socket_get_messages,triggered", + [ + ("A", [b"MG @IN[14]\r"], [b" 1.0000\n"], True), + ("B", [b"MG @IN[14]\r"], [b" 0.0000\n"], False), + ], +) +def test_fosaz_light_curtain_is_triggered( + axis_Id, socket_put_messages, socket_get_messages, triggered +): + """test that the light curtain is triggered""" + fosaz = FlomniGalilMotor( + axis_Id, name="fosaz", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + fosaz.controller.on() + fosaz.controller.sock.flush_buffer() + fosaz.controller.sock.buffer_recv = socket_get_messages + assert fosaz.controller.fosaz_light_curtain_is_triggered() == triggered + assert fosaz.controller.sock.buffer_put == socket_put_messages + fosaz.controller.off() + fosaz.controller._reset_controller() diff --git a/tests/tests_devices/test_mcs_card.py b/tests/tests_devices/test_mcs_card.py new file mode 100644 index 0000000..d6b38c9 --- /dev/null +++ b/tests/tests_devices/test_mcs_card.py @@ -0,0 +1,332 @@ +# pylint: skip-file +import threading +from unittest import mock + +import ophyd +import pytest +from bec_lib import MessageEndpoints, messages +from bec_server.device_server.tests.utils import DMMock +from ophyd_devices.tests.utils import MockPV + +from csaxs_bec.devices.epics.devices.mcs_csaxs import ( + MCScSAXS, + MCSError, + MCSTimeoutError, + ReadoutMode, + TriggerSource, +) + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_det(): + name = "mcs" + prefix = "X12SA-MCS:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter") as filemixin, + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ) as mock_service_config, + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + mock_cl.thread_class = threading.Thread + with mock.patch.object(MCScSAXS, "_init"): + det = MCScSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + patch_dual_pvs(det) + yield det + + +def test_init(): + """Test the _init function:""" + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"), + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ), + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + with ( + mock.patch( + "csaxs_bec.devices.epics.devices.mcs_csaxs.MCSSetup.initialize_default_parameter" + ) as mock_default, + mock.patch( + "csaxs_bec.devices.epics.devices.mcs_csaxs.MCSSetup.initialize_detector" + ) as mock_init_det, + mock.patch( + "csaxs_bec.devices.epics.devices.mcs_csaxs.MCSSetup.initialize_detector_backend" + ) as mock_init_backend, + ): + MCScSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + mock_default.assert_called_once() + mock_init_det.assert_called_once() + mock_init_backend.assert_called_once() + + +@pytest.mark.parametrize( + "trigger_source, channel_advance, channel_source1, pv_channels", + [ + ( + 3, + 1, + 0, + { + "user_led": 0, + "mux_output": 5, + "input_pol": 0, + "output_pol": 1, + "count_on_start": 0, + "stop_all": 1, + }, + ) + ], +) +def test_initialize_detector( + mock_det, trigger_source, channel_advance, channel_source1, pv_channels +): + """Test the _init function: + + This includes testing the functions: + - initialize_detector + - stop_det + - parent.set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + mock_det.custom_prepare.initialize_detector() # call the method you want to test + assert mock_det.channel_advance.get() == channel_advance + assert mock_det.channel1_source.get() == channel_source1 + assert mock_det.user_led.get() == pv_channels["user_led"] + assert mock_det.mux_output.get() == pv_channels["mux_output"] + assert mock_det.input_polarity.get() == pv_channels["input_pol"] + assert mock_det.output_polarity.get() == pv_channels["output_pol"] + assert mock_det.count_on_start.get() == pv_channels["count_on_start"] + assert mock_det.input_mode.get() == trigger_source + + +def test_trigger(mock_det): + """Test the trigger function: + Validate that trigger calls the custom_prepare.on_trigger() function + """ + with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger: + mock_det.trigger() + mock_on_trigger.assert_called_once() + + +@pytest.mark.parametrize( + "value, num_lines, num_points, done", [(100, 5, 500, False), (500, 5, 500, True)] +) +def test_progress_update(mock_det, value, num_lines, num_points, done): + mock_det.num_lines.set(num_lines) + mock_det.scaninfo.num_points = num_points + calls = mock.call(sub_type="progress", value=value, max_value=num_points, done=done) + with mock.patch.object(mock_det, "_run_subs") as mock_run_subs: + mock_det.custom_prepare._progress_update(value=value) + mock_run_subs.assert_called_once() + assert mock_run_subs.call_args == calls + + +@pytest.mark.parametrize( + "values, expected_nothing", + [([[100, 120, 140], [200, 220, 240], [300, 320, 340]], False), ([100, 200, 300], True)], +) +def test_on_mca_data(mock_det, values, expected_nothing): + """Test the on_mca_data function: + Validate that on_mca_data calls the custom_prepare.on_mca_data() function + """ + with mock.patch.object(mock_det.custom_prepare, "_send_data_to_bec") as mock_send_data: + mock_object = mock.MagicMock() + for ii, name in enumerate(mock_det.custom_prepare.mca_names): + mock_object.attr_name = name + mock_det.custom_prepare._on_mca_data(obj=mock_object, value=values[ii]) + if not expected_nothing and ii < (len(values) - 1): + assert mock_det.custom_prepare.mca_data[name] == values[ii] + + if not expected_nothing: + mock_send_data.assert_called_once() + assert mock_det.custom_prepare.acquisition_done is True + + +@pytest.mark.parametrize( + "metadata, mca_data", + [ + ( + {"scan_id": 123}, + {"mca1": [100, 120, 140], "mca3": [200, 220, 240], "mca4": [300, 320, 340]}, + ) + ], +) +def test_send_data_to_bec(mock_det, metadata, mca_data): + mock_det.scaninfo.scan_msg = mock.MagicMock() + mock_det.scaninfo.scan_msg.metadata = metadata + mock_det.scaninfo.scan_id = metadata["scan_id"] + mock_det.custom_prepare.mca_data = mca_data + mock_det.custom_prepare._send_data_to_bec() + device_metadata = mock_det.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()}) + data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata) + calls = mock.call( + topic=MessageEndpoints.device_async_readback( + scan_id=metadata["scan_id"], device=mock_det.name + ), + msg={"data": data}, + expire=1800, + ) + + assert mock_det.connector.xadd.call_args == calls + + +@pytest.mark.parametrize( + "scaninfo, triggersource, stopped, expected_exception", + [ + ( + {"num_points": 500, "frames_per_trigger": 1, "scan_type": "step"}, + TriggerSource.MODE3, + False, + False, + ), + ( + {"num_points": 500, "frames_per_trigger": 1, "scan_type": "fly"}, + TriggerSource.MODE3, + False, + False, + ), + ( + {"num_points": 5001, "frames_per_trigger": 2, "scan_type": "step"}, + TriggerSource.MODE3, + False, + True, + ), + ( + {"num_points": 500, "frames_per_trigger": 2, "scan_type": "random"}, + TriggerSource.MODE3, + False, + True, + ), + ], +) +def test_stage(mock_det, scaninfo, triggersource, stopped, expected_exception): + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.scaninfo.scan_type = scaninfo["scan_type"] + mock_det.stopped = stopped + with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw: + if expected_exception: + with pytest.raises(MCSError): + mock_det.stage() + mock_prep_fw.assert_called_once() + else: + mock_det.stage() + mock_prep_fw.assert_called_once() + # Check set_trigger + mock_det.input_mode.get() == triggersource + if scaninfo["scan_type"] == "step": + assert mock_det.num_use_all.get() == int(scaninfo["frames_per_trigger"]) * int( + scaninfo["num_points"] + ) + elif scaninfo["scan_type"] == "fly": + assert mock_det.num_use_all.get() == int(scaninfo["num_points"]) + mock_det.preset_real.get() == 0 + + # # CHeck custom_prepare.arm_acquisition + # assert mock_det.custom_prepare.counter == 0 + # assert mock_det.erase_start.get() == 1 + # mock_prep_fw.assert_called_once() + # # Check _prep_det + # assert mock_det.cam.num_images.get() == int( + # scaninfo["num_points"] * scaninfo["frames_per_trigger"] + # ) + # assert mock_det.cam.num_frames.get() == 1 + + # mock_publish_file_location.assert_called_with(done=False) + # assert mock_det.cam.acquire.get() == 1 + + +def test_prepare_detector_backend(mock_det): + mock_det.custom_prepare.prepare_detector_backend() + assert mock_det.erase_all.get() == 1 + assert mock_det.read_mode.get() == ReadoutMode.EVENT + + +@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)]) +def test_unstage(mock_det, stopped, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + ): + mock_det.stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det.stopped is True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det.stopped is False + + +def test_stop_detector_backend(mock_det): + mock_det.custom_prepare.stop_detector_backend() + assert mock_det.custom_prepare.acquisition_done is True + + +def test_stop(mock_det): + with ( + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" + ) as mock_stop_detector_backend, + ): + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_detector_backend.assert_called_once() + assert mock_det.stopped is True + + +@pytest.mark.parametrize( + "stopped, acquisition_done, acquiring_state, expected_exception", + [ + (False, True, 0, False), + (False, False, 0, True), + (False, True, 1, True), + (True, True, 0, True), + ], +) +def test_finished(mock_det, stopped, acquisition_done, acquiring_state, expected_exception): + mock_det.custom_prepare.acquisition_done = acquisition_done + mock_det.acquiring._read_pv.mock_data = acquiring_state + mock_det.scaninfo.num_points = 500 + mock_det.num_lines.put(500) + mock_det.current_channel._read_pv.mock_data = 1 + mock_det.stopped = stopped + + if expected_exception: + with pytest.raises(MCSTimeoutError): + mock_det.timeout = 0.1 + mock_det.custom_prepare.finished() + else: + mock_det.custom_prepare.finished() + if stopped: + assert mock_det.stopped is stopped diff --git a/tests/tests_devices/test_npoint_piezo.py b/tests/tests_devices/test_npoint_piezo.py new file mode 100644 index 0000000..84087d2 --- /dev/null +++ b/tests/tests_devices/test_npoint_piezo.py @@ -0,0 +1,141 @@ +import pytest + +from csaxs_bec.devices.npoint import NPointAxis, NPointController + + +class SocketMock: + def __init__(self, sock=None): + self.buffer_put = "" + self.buffer_recv = "" + self.is_open = False + if sock is None: + self.open() + else: + self.sock = sock + + def connect(self, host, port): + print(f"connecting to {host} port {port}") + # self.sock.create_connection((host, port)) + # self.sock.connect((host, port)) + + def _put(self, msg_bytes): + self.buffer_put = msg_bytes + print(self.buffer_put) + + def _recv(self, buffer_length=1024): + print(self.buffer_recv) + return self.buffer_recv + + def _initialize_socket(self): + pass + + def put(self, msg): + return self._put(msg) + + def receive(self, buffer_length=1024): + return self._recv(buffer_length=buffer_length) + + def open(self): + self._initialize_socket() + self.is_open = True + + def close(self): + self.sock = None + self.is_open = False + + +@pytest.mark.parametrize( + "pos,msg", + [ + (5, b"\xa2\x18\x12\x83\x11\xcd\xcc\x00\x00U"), + (0, b"\xa2\x18\x12\x83\x11\x00\x00\x00\x00U"), + (-5, b"\xa2\x18\x12\x83\x1133\xff\xffU"), + ], +) +def test_axis_put(pos, msg): + controller = NPointController(SocketMock()) + npointx = NPointAxis(controller, 0, "nx") + controller.on() + npointx.set(pos) + assert npointx.controller.socket.buffer_put == msg + + +@pytest.mark.parametrize( + "pos, msg_in, msg_out", + [ + (5.0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\xcd\xcc\x00\x00U"), + (0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\x00\x00\x00\x00U"), + (-5, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x1133\xff\xffU"), + ], +) +def test_axis_get_out(pos, msg_in, msg_out): + controller = NPointController(SocketMock()) + npointx = NPointAxis(controller, 0, "nx") + controller.on() + npointx.controller.socket.buffer_recv = msg_out + assert pytest.approx(npointx.get(), rel=0.01) == pos + # assert controller.socket.buffer_put == msg_in + + +@pytest.mark.parametrize( + "axis, msg_in, msg_out", + [ + (0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\xcd\xcc\x00\x00U"), + (1, b"\xa04#\x83\x11U", b"\xa0\x34\x13\x83\x11\x00\x00\x00\x00U"), + (2, b"\xa043\x83\x11U", b"\xa0\x34\x13\x83\x1133\xff\xffU"), + ], +) +def test_axis_get_in(axis, msg_in, msg_out): + controller = NPointController(SocketMock()) + npointx = NPointAxis(controller, 0, "nx") + controller.on() + controller.socket.buffer_recv = msg_out + controller._get_current_pos(axis) + assert controller.socket.buffer_put == msg_in + + +def test_axis_out_of_range(): + controller = NPointController(SocketMock()) + with pytest.raises(ValueError): + npointx = NPointAxis(controller, 3, "nx") + + +def test_get_axis_out_of_range(): + controller = NPointController(SocketMock()) + with pytest.raises(ValueError): + controller._get_current_pos(3) + + +def test_set_axis_out_of_range(): + controller = NPointController(SocketMock()) + with pytest.raises(ValueError): + controller._set_target_pos(3, 5) + + +@pytest.mark.parametrize( + "in_buffer, byteorder, signed, val", + [ + (["0x0", "0x0", "0xcc", "0xcd"], "big", True, 52429), + (["0xcd", "0xcc", "0x0", "0x0"], "little", True, 52429), + (["cd", "cc", "00", "00"], "little", True, 52429), + ], +) +def test_hex_list_to_int(in_buffer, byteorder, signed, val): + assert NPointController._hex_list_to_int(in_buffer, byteorder=byteorder, signed=signed) == val + + +@pytest.mark.parametrize( + "axis, msg_in, msg_out", + [ + (0, b"\xa0x\x10\x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"), + (1, b"\xa0x \x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"), + (2, b"\xa0x0\x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"), + ], +) +def test_get_range(axis, msg_in, msg_out): + controller = NPointController(SocketMock()) + npointx = NPointAxis(controller, 0, "nx") + controller.on() + controller.socket.buffer_recv = msg_out + val = controller._get_range(axis) + assert controller.socket.buffer_put == msg_in and val == 100 diff --git a/tests/tests_devices/test_pilatus_csaxs.py b/tests/tests_devices/test_pilatus_csaxs.py new file mode 100644 index 0000000..75d0bfe --- /dev/null +++ b/tests/tests_devices/test_pilatus_csaxs.py @@ -0,0 +1,469 @@ +# pylint: skip-file +import os +import threading +from unittest import mock + +import ophyd +import pytest +from bec_lib import MessageEndpoints, messages +from bec_server.device_server.tests.utils import DMMock +from ophyd_devices.tests.utils import MockPV + +from csaxs_bec.devices.epics.devices.pilatus_csaxs import PilatuscSAXS + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_det(): + name = "pilatus" + prefix = "X12SA-ES-PILATUS300K:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "connector"): + with ( + mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"), + mock.patch( + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" + ), + ): + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + mock_cl.thread_class = threading.Thread + with mock.patch.object(PilatuscSAXS, "_init"): + det = PilatuscSAXS( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + patch_dual_pvs(det) + yield det + + +@pytest.mark.parametrize("trigger_source, detector_state", [(1, 0)]) +# TODO rewrite this one, write test for init_detector, init_filewriter is tested +def test_init_detector(mock_det, trigger_source, detector_state): + """Test the _init function: + + This includes testing the functions: + - _init_detector + - _stop_det + - _set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + mock_det.custom_prepare.initialize_detector() # call the method you want to test + assert mock_det.cam.acquire.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + + +@pytest.mark.parametrize( + "scaninfo, stopped, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 12.4, + }, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scan_id": "123", + "mokev": 12.4, + }, + True, + False, + ), + ], +) +def test_stage(mock_det, scaninfo, stopped, expected_exception): + with mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location: + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + mock_det.device_manager.add_device("mokev", value=12.4) + mock_det.stopped = stopped + with ( + mock.patch.object( + mock_det.custom_prepare, "prepare_detector_backend" + ) as mock_data_backend, + mock.patch.object( + mock_det.custom_prepare, "update_readout_time" + ) as mock_update_readout_time, + ): + mock_det.filepath = scaninfo["filepath"] + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.stage() + else: + mock_det.stage() + mock_data_backend.assert_called_once() + mock_update_readout_time.assert_called() + # Check _prep_det + assert mock_det.cam.num_images.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.cam.num_frames.get() == 1 + + mock_publish_file_location.assert_called_with(done=False) + + +def test_pre_scan(mock_det): + mock_det.custom_prepare.pre_scan() + assert mock_det.cam.acquire.get() == 1 + + +@pytest.mark.parametrize( + "readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)] +) +def test_update_readout_time(mock_det, readout_time, expected_value): + if readout_time is None: + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + else: + mock_det.scaninfo.readout_time = readout_time + mock_det.custom_prepare.update_readout_time() + assert mock_det.readout_time == expected_value + + +@pytest.mark.parametrize( + "scaninfo", + [ + ( + { + "filepath": "test.h5", + "filepath_raw": "test5_raw.h5", + "successful": True, + "done": False, + "scan_id": "123", + } + ), + ( + { + "filepath": "test.h5", + "filepath_raw": "test5_raw.h5", + "successful": False, + "done": True, + "scan_id": "123", + } + ), + ( + { + "filepath": "test.h5", + "filepath_raw": "test5_raw.h5", + "successful": None, + "done": True, + "scan_id": "123", + } + ), + ], +) +def test_publish_file_location(mock_det, scaninfo): + mock_det.scaninfo.scan_id = scaninfo["scan_id"] + mock_det.filepath = scaninfo["filepath"] + mock_det.filepath_raw = scaninfo["filepath_raw"] + mock_det.custom_prepare.publish_file_location( + done=scaninfo["done"], successful=scaninfo["successful"] + ) + if scaninfo["successful"] is None: + msg = messages.FileMessage( + file_path=scaninfo["filepath"], + done=scaninfo["done"], + metadata={"input_path": scaninfo["filepath_raw"]}, + ) + else: + msg = messages.FileMessage( + file_path=scaninfo["filepath"], + done=scaninfo["done"], + metadata={"input_path": scaninfo["filepath_raw"]}, + successful=scaninfo["successful"], + ) + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det.connector.pipeline.return_value, + ), + ] + assert mock_det.connector.set_and_publish.call_args_list == expected_calls + + +@pytest.mark.parametrize( + "requests_state, expected_exception, url_delete, url_put", + [ + ( + True, + False, + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/stop", + ), + ( + False, + False, + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/stop", + ), + ], +) +def test_stop_detector_backend(mock_det, requests_state, expected_exception, url_delete, url_put): + with ( + mock.patch.object( + mock_det.custom_prepare, "send_requests_delete" + ) as mock_send_requests_delete, + mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put, + ): + instance_delete = mock_send_requests_delete.return_value + instance_delete.ok = requests_state + instance_put = mock_send_requests_put.return_value + instance_put.ok = requests_state + if expected_exception: + mock_det.custom_prepare.stop_detector_backend() + mock_send_requests_delete.assert_called_once_with(url=url_delete) + mock_send_requests_put.assert_called_once_with(url=url_put) + instance_delete.raise_for_status.called_once() + instance_put.raise_for_status.called_once() + else: + mock_det.custom_prepare.stop_detector_backend() + mock_send_requests_delete.assert_called_once_with(url=url_delete) + mock_send_requests_put.assert_called_once_with(url=url_put) + + +@pytest.mark.parametrize( + "scaninfo, data_msgs, urls, requests_state, expected_exception", + [ + ( + { + "filepath_raw": "pilatus_2.h5", + "eacc": "e12345", + "scan_number": 1000, + "scan_directory": "S00000_00999", + "num_points": 500, + "frames_per_trigger": 1, + "headers": {"Content-Type": "application/json", "Accept": "application/json"}, + }, + [ + { + "source": [ + { + "searchPath": "/", + "searchPattern": "glob:*.cbf", + "destinationPath": ( + "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999" + ), + } + ] + }, + [ + "zmqWriter", + "e12345", + { + "addr": "tcp://x12sa-pd-2:8888", + "dst": ["file"], + "numFrm": 500, + "timeout": 2000, + "ifType": "PULL", + "user": "e12345", + }, + ], + ["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}], + ], + [ + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/run", + "http://xbl-daq-34:8091/pilatus_2/wait", + ], + True, + False, + ), + ( + { + "filepath_raw": "pilatus_2.h5", + "eacc": "e12345", + "scan_number": 1000, + "scan_directory": "S00000_00999", + "num_points": 500, + "frames_per_trigger": 1, + "headers": {"Content-Type": "application/json", "Accept": "application/json"}, + }, + [ + { + "source": [ + { + "searchPath": "/", + "searchPattern": "glob:*.cbf", + "destinationPath": ( + "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999" + ), + } + ] + }, + [ + "zmqWriter", + "e12345", + { + "addr": "tcp://x12sa-pd-2:8888", + "dst": ["file"], + "numFrm": 500, + "timeout": 2000, + "ifType": "PULL", + "user": "e12345", + }, + ], + ["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}], + ], + [ + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/run", + "http://xbl-daq-34:8091/pilatus_2/wait", + ], + False, # return of res.ok is False! + True, + ), + ], +) +def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer, + mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer, + mock.patch.object(mock_det, "filewriter") as mock_filewriter, + mock.patch.object(mock_det.custom_prepare, "create_directory") as mock_create_directory, + mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put, + ): + mock_det.scaninfo.scan_number = scaninfo["scan_number"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.scaninfo.username = scaninfo["eacc"] + mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"] + mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"] + instance = mock_send_requests_put.return_value + instance.ok = requests_state + instance.raise_for_status.side_effect = Exception + + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.prepare_data_backend() + mock_close_file_writer.assert_called_once() + mock_stop_file_writer.assert_called_once() + instance.raise_for_status.assert_called_once() + else: + mock_det.custom_prepare.prepare_data_backend() + + mock_close_file_writer.assert_called_once() + mock_stop_file_writer.assert_called_once() + + # Assert values set on detector + assert mock_det.cam.file_path.get() == "/dev/shm/zmq/" + assert ( + mock_det.cam.file_name.get() + == f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}" + ) + assert mock_det.cam.auto_increment.get() == 1 + assert mock_det.cam.file_number.get() == 0 + assert mock_det.cam.file_format.get() == 0 + assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf" + # Remove last / from destinationPath + mock_create_directory.assert_called_once_with( + os.path.join(data_msgs[0]["source"][0]["destinationPath"]) + ) + assert mock_send_requests_put.call_count == 3 + + calls = [ + mock.call(url=url, data=data_msg, headers=scaninfo["headers"]) + for url, data_msg in zip(urls, data_msgs) + ] + for call, mock_call in zip(calls, mock_send_requests_put.call_args_list): + assert call == mock_call + + +@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)]) +def test_unstage(mock_det, stopped, expected_exception): + with ( + mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, + mock.patch.object( + mock_det.custom_prepare, "publish_file_location" + ) as mock_publish_file_location, + ): + mock_det.stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det.stopped is True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det.stopped is False + + +def test_stop(mock_det): + with ( + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer, + mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer, + ): + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_file_writer.assert_called_once() + mock_close_file_writer.assert_called_once() + assert mock_det.stopped is True + + +@pytest.mark.parametrize( + "stopped, mcs_stage_state, expected_exception", + [ + (False, ophyd.Staged.no, False), + (True, ophyd.Staged.no, True), + (False, ophyd.Staged.yes, True), + ], +) +def test_finished(mock_det, stopped, mcs_stage_state, expected_exception): + with ( + mock.patch.object(mock_det, "device_manager") as mock_dm, + mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_friter, + mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det, + mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer, + ): + mock_dm.devices.mcs.obj._staged = mcs_stage_state + mock_det.stopped = stopped + if expected_exception: + with pytest.raises(Exception): + mock_det.timeout = 0.1 + mock_det.custom_prepare.finished() + assert mock_det.stopped is stopped + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + mock_close_file_writer.assert_called_once() + else: + mock_det.custom_prepare.finished() + if stopped: + assert mock_det.stopped is stopped + + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + mock_close_file_writer.assert_called_once() diff --git a/tests/tests_devices/test_rt_flomni.py b/tests/tests_devices/test_rt_flomni.py new file mode 100644 index 0000000..3184f69 --- /dev/null +++ b/tests/tests_devices/test_rt_flomni.py @@ -0,0 +1,89 @@ +from unittest import mock + +import pytest +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.rt_lamni import RtFlomniController, RtFlomniMotor +from csaxs_bec.devices.rt_lamni.rt_ophyd import RtError + + +@pytest.fixture() +def rt_flomni(): + rt_flomni = RtFlomniController( + name="rt_flomni", socket_cls=SocketMock, socket_host="localhost", socket_port=8081 + ) + with mock.patch.object(rt_flomni, "get_device_manager"): + with mock.patch.object(rt_flomni, "sock"): + rtx = mock.MagicMock(spec=RtFlomniMotor) + rtx.name = "rtx" + rty = mock.MagicMock(spec=RtFlomniMotor) + rty.name = "rty" + rtz = mock.MagicMock(spec=RtFlomniMotor) + rtz.name = "rtz" + rt_flomni.set_axis(rtx, 0) + rt_flomni.set_axis(rty, 1) + rt_flomni.set_axis(rtz, 2) + yield rt_flomni + + +def test_rt_flomni_move_to_zero(rt_flomni): + rt_flomni.move_to_zero() + assert rt_flomni.sock.mock_calls == [ + mock.call.put(b"pa0,0\n"), + mock.call.put(b"pa1,0\n"), + mock.call.put(b"pa2,0\n"), + ] + + +@pytest.mark.parametrize("return_value,is_running", [(b"1.00\n", False), (b"0.00\n", True)]) +def test_rt_flomni_feedback_is_running(rt_flomni, return_value, is_running): + rt_flomni.sock.receive.return_value = return_value + assert rt_flomni.feedback_is_running() == is_running + assert mock.call.put(b"l2\n") in rt_flomni.sock.mock_calls + + +def test_feedback_enable_with_reset(rt_flomni): + + device_manager = rt_flomni.get_device_manager() + device_manager.devices.fsamx.user_parameter.get.return_value = 0.05 + device_manager.devices.fsamx.obj.readback.get.return_value = 0.05 + + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + with mock.patch.object(rt_flomni, "pid_y", return_value=0.05): + with mock.patch.object( + rt_flomni, "slew_rate_limiters_on_target", return_value=True + ) as slew_rate_limiters_on_target: + + rt_flomni.feedback_enable_with_reset() + laser_tracker_on.assert_called_once() + + +def test_move_samx_to_scan_region(rt_flomni): + device_manager = rt_flomni.get_device_manager() + device_manager.devices.rtx.user_parameter.get.return_value = 1 + rt_flomni.move_samx_to_scan_region(20, 2) + assert mock.call(b"v0\n") not in rt_flomni.sock.put.mock_calls + assert mock.call(b"v1\n") in rt_flomni.sock.put.mock_calls + + +def test_feedback_enable_without_reset(rt_flomni): + with mock.patch.object(rt_flomni, "set_device_enabled") as set_device_enabled: + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + rt_flomni.feedback_enable_without_reset() + laser_tracker_on.assert_called_once() + assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls + assert mock.call("fsamx", False) in set_device_enabled.mock_calls + assert mock.call("fsamy", False) in set_device_enabled.mock_calls + assert mock.call("foptx", False) in set_device_enabled.mock_calls + assert mock.call("fopty", False) in set_device_enabled.mock_calls + + +def test_feedback_enable_without_reset_raises(rt_flomni): + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=False): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + with pytest.raises(RtError): + rt_flomni.feedback_enable_without_reset() + laser_tracker_on.assert_called_once() + assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls diff --git a/tests/tests_devices/test_smaract.py b/tests/tests_devices/test_smaract.py new file mode 100644 index 0000000..f4ef4de --- /dev/null +++ b/tests/tests_devices/test_smaract.py @@ -0,0 +1,217 @@ +from unittest import mock + +import pytest +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.smaract import SmaractController +from csaxs_bec.devices.smaract.smaract_controller import SmaractCommunicationMode +from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode +from csaxs_bec.devices.smaract.smaract_ophyd import SmaractMotor + + +@pytest.fixture +def controller(): + SmaractController._reset_controller() + controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) + controller.on() + controller.sock.flush_buffer() + yield controller + + +@pytest.fixture +def lsmarA(): + SmaractController._reset_controller() + motor_a = SmaractMotor( + "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock + ) + motor_a.controller.on() + motor_a.controller.sock.flush_buffer() + motor_a.stage() + yield motor_a + + +@pytest.mark.parametrize( + "axis,position,get_message,return_msg", + [ + (0, 50, b":GP0\n", b":P0,50000000\n"), + (1, 0, b":GP1\n", b":P1,0\n"), + (0, -50, b":GP0\n", b":P0,-50000000\n"), + (0, -50.23, b":GP0\n", b":P0,-50230000\n"), + ], +) +def test_get_position(controller, axis, position, get_message, return_msg): + controller.sock.buffer_recv = return_msg + val = controller.get_position(axis) + assert val == position + assert controller.sock.buffer_put[0] == get_message + + +@pytest.mark.parametrize( + "axis,is_referenced,get_message,return_msg,exception", + [ + (0, True, b":GPPK0\n", b":PPK0,1\n", None), + (1, True, b":GPPK1\n", b":PPK1,1\n", None), + (0, False, b":GPPK0\n", b":PPK0,0\n", None), + (200, False, b":GPPK0\n", b":PPK0,0\n", ValueError), + ], +) +def test_axis_is_referenced(controller, axis, is_referenced, get_message, return_msg, exception): + controller.sock.buffer_recv = return_msg + if exception is not None: + with pytest.raises(exception): + val = controller.axis_is_referenced(axis) + else: + val = controller.axis_is_referenced(axis) + assert val == is_referenced + assert controller.sock.buffer_put[0] == get_message + + +@pytest.mark.parametrize( + "return_msg,exception,raised", + [ + (b"false\n", SmaractCommunicationError, False), + (b":E0,1", SmaractErrorCode, True), + (b":E,1", SmaractCommunicationError, True), + (b":E,-1", SmaractCommunicationError, True), + ], +) +def test_socket_put_and_receive_raises_exception(controller, return_msg, exception, raised): + controller.sock.buffer_recv = return_msg + with pytest.raises(exception): + controller.socket_put_and_receive(b"test", raise_if_not_status=True) + + controller.sock.flush_buffer() + controller.sock.buffer_recv = return_msg + + if raised: + with pytest.raises(exception): + controller.socket_put_and_receive(b"test") + else: + assert controller.socket_put_and_receive(b"test") == return_msg.split(b"\n")[0].decode() + + +@pytest.mark.parametrize( + "mode,get_message,return_msg", [(0, b":GCM\n", b":CM0\n"), (1, b":GCM\n", b":CM1\n")] +) +def test_communication_mode(controller, mode, get_message, return_msg): + controller.sock.buffer_recv = return_msg + val = controller.get_communication_mode() + assert controller.sock.buffer_put[0] == get_message + assert val == SmaractCommunicationMode(mode) + + +@pytest.mark.parametrize( + "is_moving,get_message,return_msg", + [ + (0, b":GS0\n", b":S0,0\n"), + (1, b":GS0\n", b":S0,1\n"), + (1, b":GS0\n", b":S0,2\n"), + (0, b":GS0\n", b":S0,3\n"), + (1, b":GS0\n", b":S0,4\n"), + (0, b":GS0\n", b":S0,5\n"), + (0, b":GS0\n", b":S0,6\n"), + (1, b":GS0\n", b":S0,7\n"), + (0, b":GS0\n", b":S0,9\n"), + (0, [b":GS0\n", b":GS0\n"], [b":E0,0\n", b":S0,9"]), + ], +) +def test_axis_is_moving(controller, is_moving, get_message, return_msg): + controller.sock.buffer_recv = return_msg + val = controller.is_axis_moving(0) + assert val == is_moving + if isinstance(controller.sock.buffer_put, list) and len(controller.sock.buffer_put) == 1: + controller.sock.buffer_put = controller.sock.buffer_put[0] + assert controller.sock.buffer_put == get_message + + +@pytest.mark.parametrize( + "sensor_id,axis,get_msg,return_msg", + [ + (1, 0, b":GST0\n", b":ST0,1\n"), + (6, 0, b":GST0\n", b":ST0,6\n"), + (6, 1, b":GST1\n", b":ST1,6\n"), + ], +) +def test_get_sensor_definition(controller, sensor_id, axis, get_msg, return_msg): + controller.sock.buffer_recv = return_msg + sensor = controller.get_sensor_type(axis) + assert sensor.type_code == sensor_id + + +@pytest.mark.parametrize( + "move_speed,axis,get_msg,return_msg", + [ + (50, 0, b":SCLS0,50000000\n", b":E-1,0"), + (0, 0, b":SCLS0,0\n", b":E-1,0"), + (20.23, 1, b":SCLS1,20230000\n", b":E-1,0"), + ], +) +def test_set_move_speed(controller, move_speed, axis, get_msg, return_msg): + controller.sock.buffer_recv = return_msg + controller.set_closed_loop_move_speed(axis, move_speed) + assert controller.sock.buffer_put[0] == get_msg + + +@pytest.mark.parametrize( + "pos,axis,hold_time,get_msg,return_msg", + [ + (50, 0, None, b":MPA0,50000000,1000\n", b":E0,0"), + (0, 0, 800, b":MPA0,0,800\n", b":E0,0"), + (20.23, 1, None, b":MPA1,20230000,1000\n", b":E0,0"), + ], +) +def test_move_axis_to_absolute_position(controller, pos, axis, hold_time, get_msg, return_msg): + controller.sock.buffer_recv = return_msg + if hold_time is not None: + controller.move_axis_to_absolute_position(axis, pos, hold_time=hold_time) + else: + controller.move_axis_to_absolute_position(axis, pos) + assert controller.sock.buffer_put[0] == get_msg + + +@pytest.mark.parametrize( + "pos,get_msg,return_msg", + [ + ( + 50, + [b":GPPK0\n", b":MPA0,50000000,1000\n", b":GS0\n", b":GP0\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,50000000\n"], + ), + ( + 0, + [b":GPPK0\n", b":MPA0,0,1000\n", b":GS0\n", b":GP0\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,0000000\n"], + ), + ( + 20.23, + [b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"], + ), + ( + 20.23, + [b":GPPK0\n", b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"], + [b":S0,0\n", b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"], + ), + ], +) +def test_move_axis(lsmarA, pos, get_msg, return_msg): + controller = lsmarA.controller + controller.sock.buffer_recv = return_msg + lsmarA.move(pos) + assert controller.sock.buffer_put == get_msg + + +@pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])]) +def test_stop_axis(lsmarA, num_axes, get_msg, return_msg): + controller = lsmarA.controller + controller.sock.buffer_recv = return_msg + controller.stop_all_axes() + assert controller.sock.buffer_put == get_msg + + +def test_all_axes_referenced(lsmarA): + controller = lsmarA.controller + with mock.patch.object(controller, "axis_is_referenced", return_value=True) as mock_is_ref: + val = controller.all_axes_referenced() + assert val + mock_is_ref.assert_called_once_with(0) diff --git a/tests/tests_scans/README.md b/tests/tests_scans/README.md new file mode 100644 index 0000000..5762245 --- /dev/null +++ b/tests/tests_scans/README.md @@ -0,0 +1,31 @@ +# 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 +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 ``. + +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). + diff --git a/tests/tests_scans/test_flomni_fermat_scan.py b/tests/tests_scans/test_flomni_fermat_scan.py new file mode 100644 index 0000000..c6ff627 --- /dev/null +++ b/tests/tests_scans/test_flomni_fermat_scan.py @@ -0,0 +1,61 @@ +from unittest import mock + +import pytest +from bec_server.device_server.tests.utils import DMMock + +from csaxs_bec.scans.flomni_fermat_scan import FlomniFermatScan + + +@pytest.fixture +def scan_request(): + device_manager = DMMock() + device_manager.producer = mock.MagicMock() + flomni_request = FlomniFermatScan( + fovx=5, + fovy=5, + cenx=0.0, + ceny=0.0, + exp_time=0.1, + step=1, + zshift=0.0, + angle=0.0, + device_manager=device_manager, + metadata={"RID": "1234"}, + ) + yield flomni_request + + +def test_flomni_fermat_scan(scan_request): + assert scan_request.fovx == 5 + assert scan_request.fovy == 5 + + +def test_flomni_rotation_no_rotation_required(scan_request): + with mock.patch.object(scan_request.stubs, "_get_from_rpc") as get_from_rpc_mock: + get_from_rpc_mock.return_value = 90 + with mock.patch.object(scan_request.stubs, "scan_report_instruction") as scan_report_mock: + with mock.patch.object(scan_request.stubs, "set") as set_mock: + list(scan_request.flomni_rotation(90)) + scan_report_mock.assert_not_called() + assert not set_mock.called + + +def test_flomni_rotation_rotation_required(scan_request): + with mock.patch.object(scan_request.stubs, "_get_from_rpc") as get_from_rpc_mock: + get_from_rpc_mock.return_value = 0 + with mock.patch.object(scan_request.stubs, "scan_report_instruction") as scan_report_mock: + with mock.patch.object(scan_request.stubs, "set") as set_mock: + list(scan_request.flomni_rotation(90)) + scan_report_mock.assert_called_once_with( + { + "readback": { + "RID": scan_request.metadata["RID"], + "devices": ["fsamroy"], + "start": [0], + "end": [90], + } + } + ) + set_mock.assert_called_once_with( + device="fsamroy", value=90, wait_group="flomni_rotation" + ) diff --git a/tests/tests_scans/test_lamni_fermat_scan.py b/tests/tests_scans/test_lamni_fermat_scan.py new file mode 100644 index 0000000..75fcbfa --- /dev/null +++ b/tests/tests_scans/test_lamni_fermat_scan.py @@ -0,0 +1,426 @@ +from unittest import mock + +import numpy as np +import pytest +from bec_lib import messages +from bec_server.device_server.tests.utils import DMMock +from bec_server.scan_server.errors import ScanAbortion + +from csaxs_bec.scans.LamNIFermatScan import LamNIFermatScan + + +@pytest.mark.parametrize( + "scan_msg,reference_scan_list", + [ + ( + messages.ScanQueueMessage( + scan_type="lamni_fermat_scan", + parameter={ + "args": {}, + "kwargs": { + "fov_size": [5], + "exp_time": 0.1, + "step": 2, + "angle": 10, + "scan_type": "step", + }, + }, + queue="primary", + metadata={"RID": "1234"}, + ), + [ + messages.DeviceInstructionMessage( + device=["rtx", "rty"], + action="read", + parameter={"wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 0}, + ), + messages.DeviceInstructionMessage( + device=["rtx", "rty"], + action="wait", + parameter={"type": "read", "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 1}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="rpc", + parameter={ + "device": "rtx", + "func": "controller.clear_trajectory_generator", + "rpc_id": "e4897d7b-f8d9-4792-ac27-375d72d02aef", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 2, "response": True}, + ), + messages.DeviceInstructionMessage( + device="lsamrot", + action="rpc", + parameter={ + "device": "lsamrot", + "func": "user_setpoint.get", + "rpc_id": "7feb8d9e-b536-4958-9965-708a27c5e5f9", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 2, "response": True}, + ), + messages.DeviceInstructionMessage( + device=None, + action="scan_report_instruction", + parameter={ + "readback": { + "RID": "1234", + "devices": ["lsamrot"], + "start": [0], + "end": [10], + } + }, + metadata={"readout_priority": "monitored", "DIID": 0}, + ), + messages.DeviceInstructionMessage( + device="lsamrot", + action="set", + parameter={"value": 10, "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 3}, + ), + messages.DeviceInstructionMessage( + device=["lsamrot"], + action="wait", + parameter={"type": "move", "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 4}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="rpc", + parameter={ + "device": "rtx", + "func": "controller.feedback_disable", + "rpc_id": "a5f5167b-61f2-4c24-8a08-698c0b52a971", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 5, "response": True}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="rpc", + parameter={ + "device": "rtx", + "func": "readback.get", + "rpc_id": "409d1afc-39a5-442b-87e5-18145e59f367", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 6, "response": True}, + ), + messages.DeviceInstructionMessage( + device="rty", + action="rpc", + parameter={ + "device": "rty", + "func": "readback.get", + "rpc_id": "80e560c8-c11a-4b6c-87e3-11addea3e80d", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 7, "response": True}, + ), + messages.DeviceInstructionMessage( + device="lsamx", + action="rpc", + parameter={ + "device": "lsamx", + "func": "readback.get", + "rpc_id": "5cef7087-3537-40fc-b558-8a2256019783", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 8, "response": True}, + ), + messages.DeviceInstructionMessage( + device="lsamy", + action="rpc", + parameter={ + "device": "lsamy", + "func": "readback.get", + "rpc_id": "61a7376c-36cf-41af-94b1-76c1ba821d47", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 9, "response": True}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="rpc", + parameter={ + "device": "rtx", + "func": "readback.get", + "rpc_id": "a1d3c021-12fb-483e-a5b9-95a59d3c1304", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 10, "response": True}, + ), + messages.DeviceInstructionMessage( + device="rty", + action="rpc", + parameter={ + "device": "rty", + "func": "readback.get", + "rpc_id": "bde7e130-b7b7-41d0-a56a-c83d740450df", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 11, "response": True}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="rpc", + parameter={ + "device": "rtx", + "func": "controller.feedback_enable_without_reset", + "rpc_id": "aa2117b4-ef44-4c0d-8537-6b6ccea86d1e", + "args": (), + "kwargs": {}, + }, + metadata={"readout_priority": "monitored", "DIID": 12, "response": True}, + ), + messages.DeviceInstructionMessage( + device=None, + action="open_scan", + parameter={ + "scan_motors": ["rtx", "rty"], + "readout_priority": { + "monitored": [], + "baseline": [], + "on_request": [], + "async": [], + }, + "num_points": 2, + "positions": [ + [1.3681828686580249, 2.1508313829565298], + [-0.7700589354581364, -0.8406005210092851], + ], + "scan_name": "lamni_fermat_scan", + "scan_type": "step", + }, + metadata={"readout_priority": "monitored", "DIID": 13}, + ), + messages.DeviceInstructionMessage( + device=None, + action="stage", + parameter={}, + metadata={"readout_priority": "monitored", "DIID": 14}, + ), + messages.DeviceInstructionMessage( + device=None, + action="baseline_reading", + parameter={}, + metadata={"readout_priority": "baseline", "DIID": 15}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="set", + parameter={"value": 1.3681828686580249, "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 17}, + ), + messages.DeviceInstructionMessage( + device="rty", + action="set", + parameter={"value": 2.1508313829565298, "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 18}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "move", "group": "scan_motor", "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 19}, + ), + messages.DeviceInstructionMessage( + device=None, + action="trigger", + parameter={"group": "trigger"}, + metadata={"readout_priority": "monitored", "DIID": 20, "point_id": 0}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "trigger", "group": "trigger", "time": 0.1}, + metadata={"readout_priority": "monitored", "DIID": 21}, + ), + messages.DeviceInstructionMessage( + device=None, + action="read", + parameter={"group": "primary", "wait_group": "readout_primary"}, + metadata={"readout_priority": "monitored", "DIID": 22, "point_id": 0}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={ + "type": "read", + "group": "scan_motor", + "wait_group": "readout_primary", + }, + metadata={"readout_priority": "monitored", "DIID": 23}, + ), + messages.DeviceInstructionMessage( + device="rtx", + action="set", + parameter={"value": -0.7700589354581364, "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 24}, + ), + messages.DeviceInstructionMessage( + device="rty", + action="set", + parameter={"value": -0.8406005210092851, "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 25}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "move", "group": "scan_motor", "wait_group": "scan_motor"}, + metadata={"readout_priority": "monitored", "DIID": 26}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "read", "group": "primary", "wait_group": "readout_primary"}, + metadata={"readout_priority": "monitored", "DIID": 27}, + ), + messages.DeviceInstructionMessage( + device=None, + action="trigger", + parameter={"group": "trigger"}, + metadata={"readout_priority": "monitored", "DIID": 28, "point_id": 1}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "trigger", "group": "trigger", "time": 0.1}, + metadata={"readout_priority": "monitored", "DIID": 29}, + ), + messages.DeviceInstructionMessage( + device=None, + action="read", + parameter={"group": "primary", "wait_group": "readout_primary"}, + metadata={"readout_priority": "monitored", "DIID": 30, "point_id": 1}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={ + "type": "read", + "group": "scan_motor", + "wait_group": "readout_primary", + }, + metadata={"readout_priority": "monitored", "DIID": 31}, + ), + messages.DeviceInstructionMessage( + device=None, + action="wait", + parameter={"type": "read", "group": "primary", "wait_group": "readout_primary"}, + metadata={"readout_priority": "monitored", "DIID": 16}, + ), + messages.DeviceInstructionMessage( + **{"device": None, "action": "complete", "parameter": {}}, + metadata={"readout_priority": "monitored", "DIID": 31}, + ), + messages.DeviceInstructionMessage( + device=None, + action="unstage", + parameter={}, + metadata={"readout_priority": "monitored", "DIID": 17}, + ), + messages.DeviceInstructionMessage( + device=None, + action="close_scan", + parameter={}, + metadata={"readout_priority": "monitored", "DIID": 18}, + ), + ], + ) + ], +) +def test_LamNIFermatScan(scan_msg, reference_scan_list): + device_manager = DMMock() + device_manager.add_device("lsamx") + device_manager.devices["lsamx"]._config["userParameter"] = {"center": 8.1} + device_manager.add_device("lsamy") + device_manager.devices["lsamy"]._config["userParameter"] = {"center": 10} + device_manager.add_device("samx") + device_manager.devices["samx"].read_buffer = {"value": 0} + device_manager.add_device("samy") + device_manager.devices["samy"].read_buffer = {"value": 0} + scan = LamNIFermatScan( + parameter=scan_msg.content.get("parameter"), + device_manager=device_manager, + metadata=scan_msg.metadata, + **scan_msg.content["parameter"]["kwargs"], + ) + scan.stubs._get_from_rpc = lambda x: 0 + with mock.patch.object(scan, "_check_min_positions") as check_min_pos: + scan_instructions = list(scan.run()) + check_min_pos.assert_called_once() + scan_uid = scan_instructions[0].metadata.get("scan_id") + for ii, instr in enumerate(reference_scan_list): + if instr.metadata.get("scan_id") is not None: + instr.metadata["scan_id"] = scan_uid + instr.metadata["DIID"] = ii + instr.metadata["RID"] = scan.metadata.get("RID") + if instr.content["action"] == "rpc": + reference_scan_list[ii].content["parameter"]["rpc_id"] = scan_instructions[ + ii + ].content["parameter"]["rpc_id"] + if instr.content["parameter"].get("value"): + assert np.isclose( + instr.content["parameter"].get("value"), + scan_instructions[ii].content["parameter"].get("value"), + ) + instr.content["parameter"]["value"] = scan_instructions[ii].content["parameter"][ + "value" + ] + if instr.content["parameter"].get("positions"): + assert np.isclose( + instr.content["parameter"].get("positions"), + scan_instructions[ii].content["parameter"].get("positions"), + ).all() + instr.content["parameter"]["positions"] = scan_instructions[ii].content[ + "parameter" + ]["positions"] + assert scan_instructions == reference_scan_list + + +def test_LamNIFermatScan_min_positions(): + scan_msg = messages.ScanQueueMessage( + scan_type="lamni_fermat_scan", + parameter={ + "args": {}, + "kwargs": { + "fov_size": [5], + "exp_time": 0.1, + "step": 2, + "angle": 10, + "scan_type": "step", + }, + }, + queue="primary", + metadata={"RID": "1234"}, + ) + device_manager = DMMock() + device_manager.add_device("lsamx") + device_manager.devices["lsamx"]._config["userParameter"] = {"center": 8.1} + device_manager.add_device("lsamy") + device_manager.devices["lsamy"]._config["userParameter"] = {"center": 10} + device_manager.add_device("samx") + device_manager.devices["samx"].read_buffer = {"value": 0} + device_manager.add_device("samy") + device_manager.devices["samy"].read_buffer = {"value": 0} + scan = LamNIFermatScan( + parameter=scan_msg.content.get("parameter"), + device_manager=device_manager, + metadata=scan_msg.metadata, + ) + with pytest.raises(ScanAbortion): + instructions = list(scan.run()) diff --git a/tests/tests_scans/test_owis_grid.py b/tests/tests_scans/test_owis_grid.py new file mode 100644 index 0000000..0ebd47f --- /dev/null +++ b/tests/tests_scans/test_owis_grid.py @@ -0,0 +1,62 @@ +from unittest import mock + +import numpy as np +import pytest +from bec_lib import messages + +from csaxs_bec.scans.owis_grid import OwisGrid + + +@pytest.mark.parametrize( + "scan_msg", + [ + messages.ScanQueueMessage( + scan_type="owis_grid", + parameter={ + "args": { + "start_y": 0, + "end_y": 1, + "interval_y": 10, + "start_x": 0, + "end_x": 1, + "interval_x": 5, + }, + "kwargs": {"exp_time": 0.1, "readout_time": 3e-3}, + }, + queue="primary", + metadata={"RID": "1234"}, + ) + ], +) +def test_owis_grid(scan_msg): + dm = mock.MagicMock() + request = OwisGrid(*scan_msg.content["parameter"]["args"].values(), device_manager=dm) + request.high_velocity = 10 + request.high_acc_time = 0.2 + request.base_velocity = 0.0625 + # pylint: disable=protected-access + request.stubs._get_from_rpc = lambda x: mock.MagicMock() + with ( + mock.patch.object(request.stubs, "get_req_status", return_value=1), + mock.patch.object( + request, "get_initial_motor_properties" + ) as mock_get_init_motor_properties, + ): + scan_instructions = list(request.run()) + mock_get_init_motor_properties.assert_called_once() + assert request.point_id == scan_msg.content["parameter"]["args"]["interval_x"] + assert np.isclose( + request.target_velocity, + ( + ( + scan_msg.content["parameter"]["args"]["end_y"] + - scan_msg.content["parameter"]["args"]["start_y"] + ) + / scan_msg.content["parameter"]["args"]["interval_y"] + ) + / ( + scan_msg.content["parameter"]["kwargs"]["exp_time"] + + scan_msg.content["parameter"]["kwargs"]["readout_time"] + ), + rtol=1e-2, + ) From daf1ec0317546142929f07f09fdb8226d79b6b14 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 19 Apr 2024 14:04:15 +0200 Subject: [PATCH 3/4] refactor: updated configs, minor improvements, formatting --- .../bec_ipython_client/startup/pre_startup.py | 32 +- .../bec_device_config_sastt.yaml | 2719 +--------------- .../device_configs/e21125_lamni_config.yaml | 2820 ++++++----------- csaxs_bec/device_configs/flomni_config.yaml | 44 +- .../flomni_test_config.yaml | 44 +- csaxs_bec/device_configs/x12sa_database.yml | 266 +- csaxs_bec/devices/epics/devices/specMotors.py | 304 ++ pyproject.toml | 3 +- 8 files changed, 1535 insertions(+), 4697 deletions(-) rename csaxs_bec/{bec_ipython_client/plugins/flomni => device_configs}/flomni_test_config.yaml (77%) create mode 100644 csaxs_bec/devices/epics/devices/specMotors.py diff --git a/csaxs_bec/bec_ipython_client/startup/pre_startup.py b/csaxs_bec/bec_ipython_client/startup/pre_startup.py index dcfa194..6678e60 100644 --- a/csaxs_bec/bec_ipython_client/startup/pre_startup.py +++ b/csaxs_bec/bec_ipython_client/startup/pre_startup.py @@ -1,25 +1,15 @@ """ Pre-startup script for BEC client. This script is executed before the BEC client -is started. It can be used to set up the BEC client configuration. The script is -executed in the global namespace of the BEC client. This means that all -variables defined here are available in the BEC client. - -To set up the BEC client configuration, use the ServiceConfig class. For example, -to set the configuration file path, add the following lines to the script: - - import pathlib - from bec_lib.core import ServiceConfig - - current_path = pathlib.Path(__file__).parent.resolve() - CONFIG_PATH = f"{current_path}/" - - config = ServiceConfig(CONFIG_PATH) - -If this startup script defined a ServiceConfig object, the BEC client will use -it to configure itself. Otherwise, the BEC client will use the default config. +is started. It can be used to add additional command line arguments. """ -# example: -# current_path = pathlib.Path(__file__).parent.resolve() -# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml" -# config = ServiceConfig(CONFIG_PATH) + +def extend_command_line_args(parser): + """ + Extend the command line arguments of the BEC client. + """ + + # example: + # parser.add_argument("--session", help="Session name", type=str, default="my_default_session") + + return parser diff --git a/csaxs_bec/device_configs/bec_device_config_sastt.yaml b/csaxs_bec/device_configs/bec_device_config_sastt.yaml index 697ce30..0eba5bc 100755 --- a/csaxs_bec/device_configs/bec_device_config_sastt.yaml +++ b/csaxs_bec/device_configs/bec_device_config_sastt.yaml @@ -1,2437 +1,36 @@ -# FBPMDX: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: FBPMDX -# read_pv: X12SA-ID-FBPMD:X -# deviceTags: -# - cSAXS -# - fofb -# name: FBPMDX -# onFailure: buffer -# status: -# enabled: true -# FBPMDY: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: FBPMDY -# read_pv: X12SA-ID-FBPMD:Y -# deviceTags: -# - cSAXS -# - fofb -# name: FBPMDY -# onFailure: buffer -# status: -# enabled: true -# FBPMUX: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: FBPMUX -# read_pv: X12SA-ID-FBPMU:X -# deviceTags: -# - cSAXS -# - fofb -# name: FBPMUX -# onFailure: buffer -# status: -# enabled: true -# FBPMUY: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: FBPMUY -# read_pv: X12SA-ID-FBPMU:Y -# deviceTags: -# - cSAXS -# - fofb -# name: FBPMUY -# onFailure: buffer -# status: -# enabled: true -# XASYM: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: XASYM -# read_pv: X12SA-LBB:X-ASYM -# deviceTags: -# - cSAXS -# - fofb -# name: XASYM -# onFailure: buffer -# status: -# enabled: true -# XSYM: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: XSYM -# read_pv: X12SA-LBB:X-SYM -# deviceTags: -# - cSAXS -# - fofb -# name: XSYM -# onFailure: buffer -# status: -# enabled: true -# YASYM: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: YASYM -# read_pv: X12SA-LBB:Y-ASYM -# deviceTags: -# - cSAXS -# - fofb -# name: YASYM -# onFailure: buffer -# status: -# enabled: true -# YSYM: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: FOFB reference -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: YSYM -# read_pv: X12SA-LBB:Y-SYM -# deviceTags: -# - cSAXS -# - fofb -# name: YSYM -# onFailure: buffer -# status: -# enabled: true -# aptrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: ES aperture horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: aptrx -# prefix: X12SA-ES1-PIN1:TRX1 -# deviceTags: -# - cSAXS -# name: aptrx -# onFailure: buffer -# status: -# enabled: true -# aptry: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: ES aperture vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: aptry -# prefix: X12SA-ES1-PIN1:TRY1 -# deviceTags: -# - cSAXS -# name: aptry -# onFailure: buffer -# status: -# enabled: true -# bm1trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd XBPM 1 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm1trx -# prefix: X12SA-FE-BM1:TRH -# deviceTags: -# - cSAXS -# - bm1 -# name: bm1trx -# onFailure: buffer -# status: -# enabled: true -# bm1try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd XBPM 1 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm1try -# prefix: X12SA-FE-BM1:TRV -# deviceTags: -# - cSAXS -# - bm1 -# name: bm1try -# onFailure: buffer -# status: -# enabled: true -# bm2trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd XBPM 2 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm2trx -# prefix: X12SA-FE-BM2:TRH -# deviceTags: -# - cSAXS -# - bm2 -# name: bm2trx -# onFailure: buffer -# status: -# enabled: true -# bm2try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd XBPM 2 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm2try -# prefix: X12SA-FE-BM2:TRV -# deviceTags: -# - cSAXS -# - bm2 -# name: bm2try -# onFailure: buffer -# status: -# enabled: true -# bm3trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 1 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm3trx -# prefix: X12SA-OP-BM1:TRX1 -# deviceTags: -# - cSAXS -# - bm3 -# name: bm3trx -# onFailure: buffer -# status: -# enabled: true -# bm3try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 1 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm3try -# prefix: X12SA-OP-BM1:TRY1 -# deviceTags: -# - cSAXS -# - bm3 -# name: bm3try -# onFailure: buffer -# status: -# enabled: true -# bm4trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 2 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm4trx -# prefix: X12SA-OP-BM2:TRX1 -# deviceTags: -# - cSAXS -# - bm4 -# name: bm4trx -# onFailure: buffer -# status: -# enabled: true -# bm4try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 2 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm4try -# prefix: X12SA-OP-BM2:TRY1 -# deviceTags: -# - cSAXS -# - bm4 -# name: bm4try -# onFailure: buffer -# status: -# enabled: true -# bm5trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 3 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm5trx -# prefix: X12SA-OP-BM3:TRX1 -# deviceTags: -# - cSAXS -# - bm5 -# name: bm5trx -# onFailure: buffer -# status: -# enabled: true -# bm5try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch XBPM 3 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: bm5try -# prefix: X12SA-OP-BM3:TRY1 -# deviceTags: -# - cSAXS -# - bm5 -# name: bm5try -# onFailure: buffer -# status: -# enabled: true -# bpm1: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 1: Somewhere around mono (VME)' -# deviceClass: XbpmCsaxsOp -# deviceConfig: -# name: bpm1 -# prefix: 'X12SA-OP-BPM2:' -# deviceTags: -# - cSAXS -# - bpm1 -# name: bpm1 -# onFailure: buffer -# status: -# enabled: true -# bpm1i: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some VME XBPM... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm1i -# read_pv: X12SA-OP-BPM1:SUM -# deviceTags: -# - cSAXS -# - bpm1 -# name: bpm1i -# onFailure: buffer -# status: -# enabled: true -# bpm2: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 2: Somewhere around mono (VME)' -# deviceClass: XbpmCsaxsOp -# deviceConfig: -# name: bpm2 -# prefix: 'X12SA-OP-BPM2:' -# deviceTags: -# - cSAXS -# - bpm2 -# name: bpm2 -# onFailure: buffer -# status: -# enabled: true -# bpm2i: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some VME XBPM... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm2i -# read_pv: X12SA-OP-BPM2:SUM -# deviceTags: -# - cSAXS -# - bpm2 -# name: bpm2i -# onFailure: buffer -# status: -# enabled: true -# bpm3a: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 3: White beam AH501 before mono' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm3a -# read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm3 -# name: bpm3a -# onFailure: buffer -# status: -# enabled: true -# bpm3b: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 3: White beam AH501 before mono' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm3b -# read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm3 -# name: bpm3b -# onFailure: buffer -# status: -# enabled: true -# bpm3c: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 3: White beam AH501 before mono' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm3c -# read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm3 -# name: bpm3c -# onFailure: buffer -# status: -# enabled: true -# bpm3d: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 3: White beam AH501 before mono' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm3d -# read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm3 -# name: bpm3d -# onFailure: buffer -# status: -# enabled: true -# bpm4a: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 4: VME between mono and mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm4a -# read_pv: X12SA-OP1-SCALER.S2 -# deviceTags: -# - cSAXS -# - bpm4 -# name: bpm4a -# onFailure: buffer -# status: -# enabled: true -# bpm4b: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 4: VME between mono and mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm4b -# read_pv: X12SA-OP1-SCALER.S3 -# deviceTags: -# - cSAXS -# - bpm4 -# name: bpm4b -# onFailure: buffer -# status: -# enabled: true -# bpm4c: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 4: VME between mono and mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm4c -# read_pv: X12SA-OP1-SCALER.S4 -# deviceTags: -# - cSAXS -# - bpm4 -# name: bpm4c -# onFailure: buffer -# status: -# enabled: true -# bpm4d: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 4: VME between mono and mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm4d -# read_pv: X12SA-OP1-SCALER.S5 -# deviceTags: -# - cSAXS -# - bpm4 -# name: bpm4d -# onFailure: buffer -# status: -# enabled: true -# bpm5a: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 5: AH501 past the mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm5a -# read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm5 -# name: bpm5a -# onFailure: buffer -# status: -# enabled: true -# bpm5b: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 5: AH501 past the mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm5b -# read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm5 -# name: bpm5b -# onFailure: buffer -# status: -# enabled: true -# bpm5c: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 5: AH501 past the mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm5c -# read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm5 -# name: bpm5c -# onFailure: buffer -# status: -# enabled: true -# bpm5d: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 5: AH501 past the mirror' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm5d -# read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm5 -# name: bpm5d -# onFailure: buffer -# status: -# enabled: true +# This configuration file was used for the cSAXS beamtimes in September 2023 +################################################## +#############Config for cSAXS SAXS imaging######## +################################################## - - - -# bpm6a: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 6: Xbox, not commissioned' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm6a -# read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm6 -# name: bpm6a -# onFailure: buffer -# status: -# enabled: true -# bpm6b: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 6: Xbox, not commissioned' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm6b -# read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm6 -# name: bpm6b -# onFailure: buffer -# status: -# enabled: true -# bpm6c: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 6: Xbox, not commissioned' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm6c -# read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm6 -# name: bpm6c -# onFailure: buffer -# status: -# enabled: true -# bpm6d: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: 'XBPM 6: Xbox, not commissioned' -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: bpm6d -# read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV -# deviceTags: -# - cSAXS -# - bpm6 -# name: bpm6d -# onFailure: buffer -# status: -# enabled: true - - - -# bs1x: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Beam stop 1 x -# deviceClass: EpicsMotor -# deviceConfig: -# name: bs1x -# prefix: X12SA-ES1-BS1:TRX1 -# deviceTags: -# - cSAXS -# - beam stop -# name: bs1x -# onFailure: buffer -# status: -# enabled: true -# bs1y: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Beam stop 1 y -# deviceClass: EpicsMotor -# deviceConfig: -# name: bs1y -# prefix: X12SA-ES1-BS1:TRY1 -# deviceTags: -# - cSAXS -# - beam stop -# name: bs1y -# onFailure: buffer -# status: -# enabled: true -# bs2x: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Beam stop 2 x -# deviceClass: EpicsMotor -# deviceConfig: -# name: bs2x -# prefix: X12SA-ES1-BS2:TRX1 -# deviceTags: -# - cSAXS -# - beam stop -# name: bs2x -# onFailure: buffer -# status: -# enabled: true -# bs2y: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Beam stop 2 y -# deviceClass: EpicsMotor -# deviceConfig: -# name: bs2y -# prefix: X12SA-ES1-BS2:TRY1 -# deviceTags: -# - cSAXS -# - beam stop -# name: bs2y -# onFailure: buffer -# status: -# enabled: true -# curr: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: SLS ring current -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: curr -# read_pv: ARIDI-PCT:CURRENT -# deviceTags: -# - cSAXS -# name: curr -# onFailure: buffer -# status: -# enabled: true -# dettrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower motion -# deviceClass: EpicsMotor -# deviceConfig: -# name: dettrx -# prefix: X12SA-ES1-DET1:TRX1 -# deviceTags: -# - cSAXS -# - detector table -# name: dettrx -# onFailure: buffer -# status: -# enabled: true -# di2trx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd diaphragm 2 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: di2trx -# prefix: X12SA-FE-DI2:TRX1 -# deviceTags: -# - cSAXS -# name: di2trx -# onFailure: buffer -# status: -# enabled: true -# di2try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd diaphragm 2 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: di2try -# prefix: X12SA-FE-DI2:TRY1 -# deviceTags: -# - cSAXS -# name: di2try -# onFailure: buffer -# status: -# enabled: true -# dtpush: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower tilt pusher -# deviceClass: EpicsMotor -# deviceConfig: -# name: dtpush -# prefix: X12SA-ES1-DETT:ROX1 -# deviceTags: -# - cSAXS -# - detector table -# name: dtpush -# onFailure: buffer -# status: -# enabled: true -# dtth: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower tilt rotation -# deviceClass: PmDetectorRotation -# deviceConfig: -# name: dtth -# prefix: X12SA-ES1-DETT:ROX1 -# deviceTags: -# - cSAXS -# - detector table -# name: dtth -# onFailure: buffer -# status: -# enabled: true -# dttrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower motion -# deviceClass: EpicsMotor -# deviceConfig: -# name: dttrx -# prefix: X12SA-ES1-DETT:TRX1 -# deviceTags: -# - cSAXS -# - detector table -# name: dttrx -# onFailure: buffer -# status: -# enabled: true -# dttry: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower motion, no encoder -# deviceClass: EpicsMotor -# deviceConfig: -# name: dttry -# prefix: X12SA-ES1-DETT:TRY1 -# deviceTags: -# - cSAXS -# - detector table -# name: dttry -# onFailure: buffer -# status: -# enabled: true -# dttrz: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Detector tower motion -# deviceClass: EpicsMotor -# deviceConfig: -# name: dttrz -# prefix: X12SA-ES1-DETT:TRZ1 -# deviceTags: -# - cSAXS -# - detector table -# name: dttrz -# onFailure: buffer -# status: -# enabled: true -# ebtrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Exposure box 2 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: ebtrx -# prefix: X12SA-ES1-EB:TRX1 -# deviceTags: -# - cSAXS -# - xbox -# name: ebtrx -# onFailure: buffer -# status: -# enabled: true -# ebtry: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Exposure box 2 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: ebtry -# prefix: X12SA-ES1-EB:TRY1 -# deviceTags: -# - cSAXS -# - xbox -# name: ebtry -# onFailure: buffer -# status: -# enabled: true -# ebtrz: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Exposure box 2 axial movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: ebtrz -# prefix: X12SA-ES1-EB:TRZ1 -# deviceTags: -# - cSAXS -# - xbox -# name: ebtrz -# onFailure: buffer -# status: -# enabled: true -# eyex: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: X-ray eye motion -# deviceClass: EpicsMotor -# deviceConfig: -# name: eyex -# prefix: X12SA-ES2-ES07 -# deviceTags: -# - cSAXS -# - xeye -# name: eyex -# onFailure: buffer -# status: -# enabled: true -# eyey: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: X-ray eye motion -# deviceClass: EpicsMotor -# deviceConfig: -# name: eyey -# prefix: X12SA-ES2-ES08 -# deviceTags: -# - cSAXS -# - xeye -# name: eyey -# onFailure: buffer -# status: -# enabled: true -# fal0: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some scaler... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: fal0 -# read_pv: X12SA-ES1-SCALER.S4 -# deviceTags: -# - cSAXS -# name: fal0 -# onFailure: buffer -# status: -# enabled: true -# fal1: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some scaler... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: fal1 -# read_pv: X12SA-ES1-SCALER.S5 -# deviceTags: -# - cSAXS -# name: fal1 -# onFailure: buffer -# status: -# enabled: true -# fal2: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some scaler... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: fal2 -# read_pv: X12SA-ES1-SCALER.S6 -# deviceTags: -# - cSAXS -# name: fal2 -# onFailure: buffer -# status: -# enabled: true -# fi1try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch filter 1 movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: fi1try -# prefix: X12SA-OP-FI1:TRY1 -# deviceTags: -# - cSAXS -# - filter -# name: fi1try -# onFailure: buffer -# status: -# enabled: true -# fi2try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch filter 2 movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: fi2try -# prefix: X12SA-OP-FI2:TRY1 -# deviceTags: -# - cSAXS -# - filter -# name: fi2try -# onFailure: buffer -# status: -# enabled: true -# fi3try: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch filter 3 movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: fi3try -# prefix: X12SA-OP-FI3:TRY1 -# deviceTags: -# - cSAXS -# - filter -# name: fi3try -# onFailure: buffer -# status: -# enabled: true -# ftp: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Flight tube pressure -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: ftp -# read_pv: X12SA-ES1-FT1MT1:PRESSURE -# deviceTags: -# - cSAXS -# - flight tube -# name: ftp -# onFailure: buffer -# status: -# enabled: true -# fttrx1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Dunno these motors??? -# deviceClass: EpicsMotor -# deviceConfig: -# name: fttrx1 -# prefix: X12SA-ES1-FTS1:TRX1 -# deviceTags: -# - cSAXS -# - flight tube -# name: fttrx1 -# onFailure: buffer -# status: -# enabled: true -# fttrx2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Dunno these motors??? -# deviceClass: EpicsMotor -# deviceConfig: -# name: fttrx2 -# prefix: X12SA-ES1-FTS2:TRX1 -# deviceTags: -# - cSAXS -# - flight tube -# name: fttrx2 -# onFailure: buffer -# status: -# enabled: true -# fttry1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Dunno these motors??? -# deviceClass: EpicsMotor -# deviceConfig: -# name: fttry1 -# prefix: X12SA-ES1-FTS1:TRY1 -# deviceTags: -# - cSAXS -# - flight tube -# name: fttry1 -# onFailure: buffer -# status: -# enabled: true -# fttry2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Dunno these motors??? -# deviceClass: EpicsMotor -# deviceConfig: -# name: fttry2 -# prefix: X12SA-ES1-FTS2:TRY1 -# deviceTags: -# - cSAXS -# - flight tube -# name: fttry2 -# onFailure: buffer -# status: -# enabled: true -# fttrz: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Dunno these motors??? -# deviceClass: EpicsMotor -# deviceConfig: -# name: fttrz -# prefix: X12SA-ES1-FTS1:TRZ1 -# deviceTags: -# - cSAXS -# - flight tube -# name: fttrz -# onFailure: buffer -# status: -# enabled: true -# idgap: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Undulator gap size [mm] -# deviceClass: InsertionDevice -# deviceConfig: -# name: idgap -# prefix: X12SA-ID -# deviceTags: -# - cSAXS -# name: idgap -# onFailure: buffer -# status: -# enabled: true -# led: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some scaler... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: led -# read_pv: X12SA-ES1-SCALER.S4 -# deviceTags: -# - cSAXS -# name: led -# onFailure: buffer -# status: -# enabled: true -# mibd1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror bender 1 -# deviceClass: EpicsMotor -# deviceConfig: -# name: mibd1 -# prefix: X12SA-OP-MI:TRZ1 -# deviceTags: -# - cSAXS -# - mirror -# name: mibd1 -# onFailure: buffer -# status: -# enabled: true -# mibd2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror bender 2 -# deviceClass: EpicsMotor -# deviceConfig: -# name: mibd2 -# prefix: X12SA-OP-MI:TRZ2 -# deviceTags: -# - cSAXS -# - mirror -# name: mibd2 -# onFailure: buffer -# status: -# enabled: true -# mitrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: mitrx -# prefix: X12SA-OP-MI:TRX1 -# deviceTags: -# - cSAXS -# - mirror -# name: mitrx -# onFailure: buffer -# status: -# enabled: true -# mitry1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror vertical movement 1 -# deviceClass: EpicsMotor -# deviceConfig: -# name: mitry1 -# prefix: X12SA-OP-MI:TRY1 -# deviceTags: -# - cSAXS -# - mirror -# name: mitry1 -# onFailure: buffer -# status: -# enabled: true -# mitry2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror vertical movement 2 -# deviceClass: EpicsMotor -# deviceConfig: -# name: mitry2 -# prefix: X12SA-OP-MI:TRY2 -# deviceTags: -# - cSAXS -# - mirror -# name: mitry2 -# onFailure: buffer -# status: -# enabled: true -# mitry3: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Mirror vertical movement 3 -# deviceClass: EpicsMotor -# deviceConfig: -# name: mitry3 -# prefix: X12SA-OP-MI:TRY3 -# deviceTags: -# - cSAXS -# - mirror -# name: mitry3 -# onFailure: buffer -# status: -# enabled: true -# mobd: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator bender virtual motor -# deviceClass: PmMonoBender -# deviceConfig: -# name: mobd -# prefix: 'X12SA-OP-MO:' -# deviceTags: -# - cSAXS -# - mono -# name: mobd -# onFailure: buffer -# status: -# enabled: true -# mobdai: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator bender inner motor -# deviceClass: EpicsMotor -# deviceConfig: -# name: mobdai -# prefix: X12SA-OP-MO:TRYA -# deviceTags: -# - cSAXS -# - mono -# name: mobdai -# onFailure: buffer -# status: -# enabled: true -# mobdbo: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator bender outer motor -# deviceClass: EpicsMotor -# deviceConfig: -# name: mobdbo -# prefix: X12SA-OP-MO:TRYB -# deviceTags: -# - cSAXS -# - mono -# name: mobdbo -# onFailure: buffer -# status: -# enabled: true -# mobdco: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator bender outer motor -# deviceClass: EpicsMotor -# deviceConfig: -# name: mobdco -# prefix: X12SA-OP-MO:TRYC -# deviceTags: -# - cSAXS -# - mono -# name: mobdco -# onFailure: buffer -# status: -# enabled: true -# mobddi: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator bender inner motor -# deviceClass: EpicsMotor -# deviceConfig: -# name: mobddi -# prefix: X12SA-OP-MO:TRYD -# deviceTags: -# - cSAXS -# - mono -# name: mobddi -# onFailure: buffer -# status: -# enabled: true -# mopush1: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 1 angle -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: mopush1 -# read_pv: X12SA-OP-MO:ROX1 -# deviceTags: -# - cSAXS -# - mono -# name: mopush1 -# onFailure: buffer -# status: -# enabled: true -# mopush2: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 angle -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: mopush2 -# read_pv: X12SA-OP-MO:ROX2 -# deviceTags: -# - cSAXS -# - mono -# name: mopush2 -# onFailure: buffer -# status: -# enabled: true -# moroll1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 1 roll -# deviceClass: EpicsMotor -# deviceConfig: -# name: moroll1 -# prefix: X12SA-OP-MO:ROZ1 -# deviceTags: -# - cSAXS -# - mono -# name: moroll1 -# onFailure: buffer -# status: -# enabled: true -# moroll2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 roll movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: moroll2 -# prefix: X12SA-OP-MO:ROZ2 -# deviceTags: -# - cSAXS -# - mono -# name: moroll2 -# onFailure: buffer -# status: -# enabled: true -# moth1: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator Theta 1 -# deviceClass: MonoTheta1 -# deviceConfig: -# auto_monitor: true -# name: moth1 -# read_pv: X12SA-OP-MO:ROX1 -# deviceTags: -# - cSAXS -# - mono -# name: moth1 -# onFailure: buffer -# status: -# enabled: true -# moth1e: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 1 theta encoder -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: moth1e -# read_pv: X12SA-OP-MO:ECX1 -# deviceTags: -# - cSAXS -# - mono -# name: moth1e -# onFailure: buffer -# status: -# enabled: true -# moth2: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator Theta 2 -# deviceClass: MonoTheta2 -# deviceConfig: -# auto_monitor: true -# name: moth2 -# read_pv: X12SA-OP-MO:ROX2 -# deviceTags: -# - cSAXS -# - mono -# name: moth2 -# onFailure: buffer -# status: -# enabled: true -# moth2e: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 theta encoder -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: moth2e -# read_pv: X12SA-OP-MO:ECX2 -# deviceTags: -# - cSAXS -# - mono -# name: moth2e -# onFailure: buffer -# status: -# enabled: true -# motrx2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 horizontal movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: motrx2 -# prefix: X12SA-OP-MO:TRX2 -# deviceTags: -# - cSAXS -# - mono -# name: motrx2 -# onFailure: buffer -# status: -# enabled: true -# motry: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch optical table vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: motry -# prefix: X12SA-OP-OT:TRY -# deviceTags: -# - cSAXS -# - mono -# name: motry -# onFailure: buffer -# status: -# enabled: true -# motry2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 vertical movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: motry2 -# prefix: X12SA-OP-MO:TRY2 -# deviceTags: -# - cSAXS -# - mono -# name: motry2 -# onFailure: buffer -# status: -# enabled: true -# motrz1: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 1 axial movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: motrz1 -# prefix: X12SA-OP-MO:TRZ1 -# deviceTags: -# - cSAXS -# - mono -# name: motrz1 -# onFailure: buffer -# status: -# enabled: true -# motrz1e: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 1 axial movement encoder -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: motrz1e -# read_pv: X12SA-OP-MO:ECZ1 -# deviceTags: -# - cSAXS -# - mono -# name: motrz1e -# onFailure: buffer -# status: -# enabled: true -# moyaw2: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Monochromator crystal 2 yaw movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: moyaw2 -# prefix: X12SA-OP-MO:ROY2 -# deviceTags: -# - cSAXS -# - mono -# name: moyaw2 -# onFailure: buffer -# status: -# enabled: true -# sec: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# description: Some scaler... -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sec -# read_pv: X12SA-ES1-SCALER.S1 -# deviceTags: -# - cSAXS -# name: sec -# onFailure: buffer -# status: -# enabled: true -# sl0h: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd slit virtual movement -# deviceClass: SlitH -# deviceConfig: -# name: sl0h -# prefix: 'X12SA-FE-SH1:' -# deviceTags: -# - cSAXS -# name: sl0h -# onFailure: buffer -# status: -# enabled: true -# sl0trxi: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd slit inner blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl0trxi -# prefix: X12SA-FE-SH1:TRX1 -# deviceTags: -# - cSAXS -# name: sl0trxi -# onFailure: buffer -# status: -# enabled: true -# sl0trxo: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: FrontEnd slit outer blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl0trxo -# prefix: X12SA-FE-SH1:TRX2 -# deviceTags: -# - cSAXS -# name: sl0trxo -# onFailure: buffer -# status: -# enabled: true -# sl1h: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit virtual movement -# deviceClass: SlitH -# deviceConfig: -# name: sl1h -# prefix: 'X12SA-OP-SH1:' -# deviceTags: -# - cSAXS -# name: sl1h -# onFailure: buffer -# status: -# enabled: true -# sl1trxi: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit inner blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl1trxi -# prefix: X12SA-OP-SH1:TRX2 -# deviceTags: -# - cSAXS -# name: sl1trxi -# onFailure: buffer -# status: -# enabled: true -# sl1trxo: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit outer blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl1trxo -# prefix: X12SA-OP-SH1:TRX1 -# deviceTags: -# - cSAXS -# name: sl1trxo -# onFailure: buffer -# status: -# enabled: true -# sl1tryb: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit bottom blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl1tryb -# prefix: X12SA-OP-SV1:TRY2 -# deviceTags: -# - cSAXS -# name: sl1tryb -# onFailure: buffer -# status: -# enabled: true -# sl1tryt: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit top blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl1tryt -# prefix: X12SA-OP-SV1:TRY1 -# deviceTags: -# - cSAXS -# name: sl1tryt -# onFailure: buffer -# status: -# enabled: true -# sl1v: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit virtual movement -# deviceClass: SlitV -# deviceConfig: -# name: sl1v -# prefix: 'X12SA-OP-SV1:' -# deviceTags: -# - cSAXS -# name: sl1v -# onFailure: buffer -# status: -# enabled: true -# sl2h: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 virtual movement -# deviceClass: SlitH -# deviceConfig: -# name: sl2h -# prefix: 'X12SA-OP-SH2:' -# deviceTags: -# - cSAXS -# name: sl2h -# onFailure: buffer -# status: -# enabled: true -# sl2trxi: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 inner blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl2trxi -# prefix: X12SA-OP-SH2:TRX2 -# deviceTags: -# - cSAXS -# name: sl2trxi -# onFailure: buffer -# status: -# enabled: true -# sl2trxo: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 outer blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl2trxo -# prefix: X12SA-OP-SH2:TRX1 -# deviceTags: -# - cSAXS -# name: sl2trxo -# onFailure: buffer -# status: -# enabled: true -# sl2tryb: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 bottom blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl2tryb -# prefix: X12SA-OP-SV2:TRY2 -# deviceTags: -# - cSAXS -# name: sl2tryb -# onFailure: buffer -# status: -# enabled: true -# sl2tryt: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 top blade movement -# deviceClass: EpicsMotor -# deviceConfig: -# name: sl2tryt -# prefix: X12SA-OP-SV2:TRY1 -# deviceTags: -# - cSAXS -# name: sl2tryt -# onFailure: buffer -# status: -# enabled: true -# sl2v: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: OpticsHutch slit 2 virtual movement -# deviceClass: SlitV -# deviceConfig: -# name: sl2v -# prefix: 'X12SA-OP-SV2:' -# deviceTags: -# - cSAXS -# name: sl2v -# onFailure: buffer -# status: -# enabled: true -# sls_crane_usage: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_crane_usage -# read_pv: IBWKR-0101-QH10003:D01_H_D-WA -# string: true -# deviceTags: -# - SLS status -# name: sls_crane_usage -# onFailure: buffer -# status: -# enabled: true -# sls_current_deadband: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_current_deadband -# read_pv: ALIRF-GUN:CUR-DBAND -# string: false -# deviceTags: -# - SLS status -# name: sls_current_deadband -# onFailure: buffer -# status: -# enabled: true -# sls_current_threshold: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_current_threshold -# read_pv: ALIRF-GUN:CUR-LOWLIM -# string: false -# deviceTags: -# - SLS status -# name: sls_current_threshold -# onFailure: buffer -# status: -# enabled: true -# sls_fast_orbit_feedback: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_fast_orbit_feedback -# read_pv: ARIDI-BPM:FOFBSTATUS-G -# string: true -# deviceTags: -# - SLS status -# name: sls_fast_orbit_feedback -# onFailure: buffer -# status: -# enabled: true -# sls_filling_life_time: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_filling_life_time -# read_pv: ARIDI-PCT:TAU-HOUR -# string: false -# deviceTags: -# - SLS status -# name: sls_filling_life_time -# onFailure: buffer -# status: -# enabled: true -# sls_filling_pattern: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_filling_pattern -# read_pv: ACORF-FILL:PAT-SELECT -# string: true -# deviceTags: -# - SLS status -# name: sls_filling_pattern -# onFailure: buffer -# status: -# enabled: true -# sls_info: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: SLSInfo -# deviceConfig: -# name: sls_info -# deviceTags: -# - SLS status -# name: sls_info -# onFailure: buffer -# status: -# enabled: true -# sls_injection_mode: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_injection_mode -# read_pv: ALIRF-GUN:INJ-MODE -# string: true -# deviceTags: -# - SLS status -# name: sls_injection_mode -# onFailure: buffer -# status: -# enabled: true -sls_machine_status: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_machine_status - read_pv: ACOAU-ACCU:OP-MODE - string: true - deviceTags: - - SLS status - name: sls_machine_status - onFailure: buffer - status: - enabled: true -# sls_operator: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: SLSOperatorMessages -# deviceConfig: -# name: sls_operator -# deviceTags: -# - SLS status -# name: sls_operator -# onFailure: buffer -# status: -# enabled: true -# sls_orbit_feedback_mode: -# acquisitionConfig: -# acquisitionGroup: monitor -# readoutPriority: baseline -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: sls_orbit_feedback_mode -# read_pv: ARIDI-BPM:OFB-MODE -# string: true -# deviceTags: -# - SLS status -# name: sls_orbit_feedback_mode -# onFailure: buffer -# status: -# enabled: true -sls_ring_current: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: monitored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: sls_ring_current - read_pv: ARIDI-PCT:CURRENT - string: false - deviceTags: - - SLS status - name: sls_ring_current - onFailure: buffer - status: - enabled: true -# strox: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Girder virtual pitch -# deviceClass: GirderMotorPITCH -# deviceConfig: -# name: strox -# prefix: X12SA-HG -# deviceTags: -# - cSAXS -# name: strox -# onFailure: buffer -# status: -# enabled: true -# stroy: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Girder virtual yaw -# deviceClass: GirderMotorYAW -# deviceConfig: -# name: stroy -# prefix: X12SA-HG -# deviceTags: -# - cSAXS -# name: stroy -# onFailure: buffer -# status: -# enabled: true -# stroz: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Girder virtual roll -# deviceClass: GirderMotorROLL -# deviceConfig: -# name: stroz -# prefix: X12SA-HG -# deviceTags: -# - cSAXS -# name: stroz -# onFailure: buffer -# status: -# enabled: true -# sttrx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Girder X translation -# deviceClass: GirderMotorX1 -# deviceConfig: -# name: sttrx -# prefix: X12SA-HG -# deviceTags: -# - cSAXS -# name: sttrx -# onFailure: buffer -# status: -# enabled: true -# sttry: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# description: Girder Y translation -# deviceClass: GirderMotorY1 -# deviceConfig: -# name: sttry -# prefix: X12SA-HG -# deviceTags: -# - cSAXS -# name: sttry -# onFailure: buffer -# status: -# enabled: true -# # transd: -# # acquisitionConfig: -# # acquisitionGroup: monitor -# # readoutPriority: baseline -# # schedule: sync -# # description: Transmission diode -# # deviceClass: EpicsSignalRO -# # deviceConfig: -# # auto_monitor: true -# # name: transd -# # read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV -# # deviceTags: -# # - cSAXS -# # name: transd -# # onFailure: buffer -# # status: -# # enabled: true -x12sa_es1_shutter_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO - deviceConfig: - auto_monitor: true - name: x12sa_es1_shutter_status - read_pv: X12SA-OP-ST1:OPEN_EPS - string: true - deviceTags: - - X12SA status - name: x12sa_es1_shutter_status - onFailure: retry - status: - enabled: true -# x12sa_es1_valve: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_es1_valve -# read_pv: X12SA-ES-VW1:OPEN -# string: true -# deviceTags: -# - X12SA status -# name: x12sa_es1_valve -# onFailure: retry -# status: -# enabled: true -# x12sa_exposure_box1_pressure: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_exposure_box1_pressure -# read_pv: X12SA-ES-CH1MF1:PRESSURE -# string: false -# deviceTags: -# - X12SA status -# name: x12sa_exposure_box1_pressure -# onFailure: retry -# status: -# enabled: true -# x12sa_exposure_box2_pressure: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_exposure_box2_pressure -# read_pv: X12SA-ES-EB1MF1:PRESSURE -# string: false -# deviceTags: -# - X12SA status -# name: x12sa_exposure_box2_pressure -# onFailure: retry -# status: -# enabled: true -# x12sa_fe_status: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_fe_status -# read_pv: X12SA-FE-PH1:CLOSE4BL -# string: true -# deviceTags: -# - X12SA status -# name: x12sa_fe_status -# onFailure: retry -# status: -# enabled: true -# x12sa_id_gap: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_id_gap -# read_pv: X12SA-ID-GAP:READ -# string: false -# deviceTags: -# - X12SA status -# name: x12sa_id_gap -# onFailure: retry -# status: -# enabled: true -# x12sa_mokev: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_mokev -# read_pv: X12SA-OP-MO:E-GET -# string: false -# deviceTags: -# - X12SA status -# name: x12sa_mokev -# onFailure: retry -# status: -# enabled: true -# x12sa_op_status: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_op_status -# read_pv: ACOAU-ACCU:OP-X12SA -# string: true -# deviceTags: -# - X12SA status -# name: x12sa_op_status -# onFailure: retry -# status: -# enabled: true -# x12sa_storage_ring_vac: -# acquisitionConfig: -# acquisitionGroup: status -# readoutPriority: ignored -# schedule: sync -# deviceClass: EpicsSignalRO -# deviceConfig: -# auto_monitor: true -# name: x12sa_storage_ring_vac -# read_pv: X12SA-SR-VAC:SETPOINT -# string: true -# deviceTags: -# - X12SA status -# name: x12sa_storage_ring_vac -# onFailure: retry -# status: -# enabled: true bpm4i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: monitored - schedule: sync description: 'XBPM 4: integrated counts' - deviceClass: Bpm4i + deviceClass: ophyd.EpicsSignalRO deviceConfig: - name: bpm4i - prefix: X12SA-OP1-SCALER. + read_pv: X12SA-OP1-SCALER. deviceTags: - - cSAXS - - bpm4 - name: bpm4i + - monitor + enabled: true + readOnly: false onFailure: buffer - status: - enabled: true + readoutPriority: baseline + softwareTrigger: false mokev: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: monitored - schedule: sync description: Monochromator energy in keV - deviceClass: EnergyKev + deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev deviceConfig: - auto_monitor: true - name: mokev read_pv: X12SA-OP-MO:ROX2 deviceTags: - - cSAXS - - mono - name: mokev + - monitor + enabled: true + readOnly: false onFailure: buffer - status: - enabled: true + readoutPriority: baseline + softwareTrigger: false mcs: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: ignored - schedule: sync description: Mcs scalar card for transmission readout - deviceClass: epics:devices:McsCsaxs + deviceClass: csaxs_bec.devices.epics.devices.MCSCsaxs deviceConfig: - name: mcs prefix: 'X12SA-MCS:' mcs_config: num_lines: 1 @@ -2439,33 +38,25 @@ mcs: - cSAXS - mcs onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: monitored + softwareTrigger: false eiger9m: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: ignored - schedule: sync description: Eiger9m HPC area detector 9M - deviceClass: epics:devices:Eiger9mCsaxs + deviceClass: csaxs_bec.devices.epics.devices.Eiger9MCsaxs deviceConfig: - name: eiger9m prefix: 'X12SA-ES-EIGER9M:' deviceTags: - cSAXS - eiger9m onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false ddg_detectors: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: ignored - schedule: sync description: DelayGenerator for detector triggering - deviceClass: epics:devices:DelayGeneratorDG645 + deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS deviceConfig: - name: ddg_detectors prefix: 'delaygen:DG1:' ddg_config: delay_burst: 40.e-3 @@ -2486,17 +77,13 @@ ddg_detectors: - cSAXS - ddg_detectors onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false ddg_mcs: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: ignored - schedule: sync description: DelayGenerator for mcs triggering - deviceClass: epics:devices:DelayGeneratorDG645 + deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS deviceConfig: - name: ddg_mcs prefix: 'delaygen:DG2:' ddg_config: delay_burst: 0 @@ -2519,17 +106,13 @@ ddg_mcs: - cSAXS - ddg_mcs onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false ddg_fsh: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: ignored - schedule: sync description: DelayGenerator for fast shutter control - deviceClass: epics:devices:DelayGeneratorDG645 + deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS deviceConfig: - name: ddg_fsh prefix: 'delaygen:DG3:' ddg_config: delay_burst: 0 @@ -2550,50 +133,38 @@ ddg_fsh: - cSAXS - ddg_fsh onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false falcon: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: baseline - schedule: sync description: Falcon detector x-ray fluoresence - deviceClass: epics:devices:FalconCsaxs + deviceClass: csaxs_bec.devices.epics.devices.FalconCSAXS deviceConfig: - name: falcon prefix: 'X12SA-SITORO:' deviceTags: - cSAXS - falcon onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false pilatus_2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: ignored - schedule: sync description: Pilatus2 HPC area detector 300k - deviceClass: epics:devices:PilatusCsaxs + deviceClass: csaxs_bec.devices.epics.devices.PilatusCSAXS deviceConfig: - name: pilatus_2 prefix: 'X12SA-ES-PILATUS300K:' deviceTags: - cSAXS - pilatus_2 onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: async + softwareTrigger: false samx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: SGalil motor stage - deviceClass: galil:SGalilMotor + deviceClass: csaxs_bec.devices.galil.SGalilMotor deviceConfig: axis_Id: "E" - name: samx host: '129.129.122.26' port: 23 sign: -1 @@ -2604,18 +175,14 @@ samx: - cSAXS - sgalil onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: baseline + softwareTrigger: false samy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: SGalil motor stage - deviceClass: galil:SGalilMotor + deviceClass: csaxs_bec.devices.galil.SGalilMotor deviceConfig: axis_Id: "C" - name: samy host: '129.129.122.26' port: 23 sign: -1 @@ -2626,16 +193,13 @@ samy: - cSAXS - sgalil onFailure: buffer - status: - enabled: true + enabled: true + readoutPriority: baseline + softwareTrigger: false micfoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: epics:devices:EpicsMotorEx + description: Focusing motor of Microscope stage + deviceClass: ophyd_devices.epics.devices.EpicsMotorEx deviceConfig: - name: micfoc prefix: X12SA-ES2-ES06 motor_resolution: 0.00125 base_velocity: 0.25 @@ -2647,96 +211,85 @@ micfoc: - cSAXS - micfoc onFailure: buffer - status: - enabled: true -# owis_samx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: ignored -# schedule: sync -# deviceClass: epics:devices:EpicsMotorEx -# deviceConfig: -# name: owis_samx -# prefix: X12SA-ES2-ES01 -# motor_resolution: 0.00125 -# base_velocity: 0.0625 -# velocity: 10 -# backlash_distance: 0.125 -# acceleration: 0.2 -# user_offset_dir: 0 -# deviceTags: -# - cSAXS -# - owis_samx -# onFailure: buffer -# status: -# enabled: true -# owis_samy: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: ignored -# schedule: sync -# deviceClass: epics:devices:EpicsMotorEx -# deviceConfig: -# name: owis_samy -# prefix: X12SA-ES2-ES02 -# motor_resolution: 0.00125 -# base_velocity: 0.0625 -# velocity: 10 -# backlash_distance: 0.125 -# acceleration: 0.2 -# user_offset_dir: 0 -# deviceTags: -# - cSAXS -# - owis_samy -# onFailure: buffer -# status: -# enabled: true -# rotx: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# deviceClass: epics:devices:EpicsMotorEx -# deviceConfig: -# name: rotx -# prefix: X12SA-ES2-ES05 -# motor_resolution: 0.0025 -# base_velocity: 0.5 -# velocity: 7.5 -# backlash_distance: 0.25 -# acceleration: 0.2 -# user_offset_dir: 1 -# limits: -# - -0.1 -# - 0.1 -# deviceTags: -# - cSAXS -# - rotx -# onFailure: buffer -# status: -# enabled: true -# roty: -# acquisitionConfig: -# acquisitionGroup: motor -# readoutPriority: baseline -# schedule: sync -# deviceClass: epics:devices:EpicsMotorEx -# deviceConfig: -# name: roty -# prefix: X12SA-ES2-ES04 -# motor_resolution: 0.0025 -# base_velocity: 0.5 -# velocity: 7.5 -# backlash_distance: 0.25 -# acceleration: 0.2 -# user_offset_dir: 0 -# limits: -# - -365 -# - 5 -# deviceTags: -# - cSAXS -# - roty -# onFailure: buffer -# status: -# enabled: true + enabled: true + readoutPriority: baseline + softwareTrigger: false +owis_samx: + description: Owis motor stage samx + deviceClass: ophyd_devices.epics.devices.EpicsMotorEx + deviceConfig: + prefix: X12SA-ES2-ES01 + motor_resolution: 0.00125 + base_velocity: 0.0625 + velocity: 10 + backlash_distance: 0.125 + acceleration: 0.2 + user_offset_dir: 0 + deviceTags: + - cSAXS + - owis_samx + onFailure: buffer + enabled: true + readoutPriority: baseline + softwareTrigger: false +owis_samy: + description: Owis motor stage samx + deviceClass: ophyd_devices.epics.devices.EpicsMotorEx + deviceConfig: + prefix: X12SA-ES2-ES02 + motor_resolution: 0.00125 + base_velocity: 0.0625 + velocity: 10 + backlash_distance: 0.125 + acceleration: 0.2 + user_offset_dir: 0 + deviceTags: + - cSAXS + - owis_samx + onFailure: buffer + enabled: true + readoutPriority: baseline + softwareTrigger: false +rotx: + description: Rotation stage rotx + deviceClass: ophyd_devices.epics.devices.EpicsMotorEx + deviceConfig: + prefix: X12SA-ES2-ES05 + motor_resolution: 0.0025 + base_velocity: 0.5 + velocity: 7.5 + backlash_distance: 0.25 + acceleration: 0.2 + user_offset_dir: 1 + limits: + - -0.1 + - 0.1 + deviceTags: + - cSAXS + - rotx + onFailure: buffer + enabled: true + readoutPriority: baseline + softwareTrigger: false +roty: + description: Rotation stage rotx + deviceClass: ophyd_devices.epics.devices.EpicsMotorEx + deviceConfig: + prefix: X12SA-ES2-ES04 + motor_resolution: 0.0025 + base_velocity: 0.5 + velocity: 7.5 + backlash_distance: 0.25 + acceleration: 0.2 + user_offset_dir: 1 + limits: + - -0.1 + - 0.1 + deviceTags: + - cSAXS + - rotx + onFailure: buffer + enabled: true + readoutPriority: baseline + softwareTrigger: false diff --git a/csaxs_bec/device_configs/e21125_lamni_config.yaml b/csaxs_bec/device_configs/e21125_lamni_config.yaml index 3255098..bd0caa4 100644 --- a/csaxs_bec/device_configs/e21125_lamni_config.yaml +++ b/csaxs_bec/device_configs/e21125_lamni_config.yaml @@ -1,226 +1,158 @@ - - ############################################################ #################### LamNI Galil motors #################### ############################################################ - leyex: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: G - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: leyex limits: - - 0 - - 0 - name: leyex + - 0 + - 0 port: 8081 sign: -1 - tolerance: 0.001 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: 14.117 leyey: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: H - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: leyey limits: - - 0 - - 0 - name: leyey + - 0 + - 0 port: 8081 sign: -1 - tolerance: 0.001 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: 48.069 out: 0.5 loptx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: E - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: loptx limits: - - 0 - - 0 - name: loptx + - 0 + - 0 port: 8081 sign: 1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: -0.244 out: -0.699 lopty: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: F - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: lopty limits: - - 0 - - 0 - name: lopty + - 0 + - 0 port: 8081 sign: 1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: 3.724 out: 3.53 loptz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: D - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: loptz limits: - - 0 - - 0 - name: loptz + - 0 + - 0 port: 8081 sign: -1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline lsamrot: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: C - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: lsamrot limits: - - 0 - - 0 - name: lsamrot + - 0 + - 0 port: 8081 sign: 1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline lsamx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: A - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: lsamx limits: - - 0 - - 0 - name: lsamx + - 0 + - 0 port: 8081 sign: -1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: center: 8.768 lsamy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: GalilMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor deviceConfig: axis_Id: B - device_access: true - device_mapping: - rt: rtx host: mpc2680.psi.ch - labels: lsamy limits: - - 0 - - 0 - name: lsamy + - 0 + - 0 port: 8081 sign: 1 - tolerance: 0.5 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: center: 10.041 - ############### LamNI Galil motors end here ################ @@ -230,11 +162,7 @@ lsamy: ############################################################ rtx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: RtLamniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_lamni_ophyd.RtLamniMotor deviceConfig: axis_Id: A device_access: true @@ -243,20 +171,15 @@ rtx: limits: - 0 - 0 - name: rtx port: 3333 sign: 1 deviceTags: - lamni - status: - enabled: true - enabled_set: true + readoutPriority: baseline + enabled: true + readOnly: False rty: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: RtLamniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_lamni_ophyd.RtLamniMotor deviceConfig: axis_Id: B device_access: true @@ -265,15 +188,13 @@ rty: limits: - 0 - 0 - name: rty port: 3333 sign: 1 deviceTags: - lamni - status: - enabled: true - enabled_set: true - + readoutPriority: baseline + enabled: true + readOnly: False #################### LamNI RT end here ##################### @@ -284,103 +205,84 @@ rty: ############################################################ lmagnet: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: F host: mpc2680.psi.ch - labels: lmagnet limits: - - 0 - - 0 - name: lmagnet + - 0 + - 0 port: 8085 sign: -1 - tolerance: 0.05 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + userParameter: + tolerance: 0.05 losax: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: A host: mpc2680.psi.ch - labels: losax limits: - - 0 - - 0 - name: losax + - 0 + - 0 port: 8085 sign: -1 - tolerance: 0.05 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: -1.442 losay: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: B host: mpc2680.psi.ch - labels: losay limits: - - 0 - - 0 - name: losay + - 0 + - 0 port: 8085 sign: -1 - tolerance: 0.05 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: -0.171 out: 3.8 losaz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - deviceClass: SmaractMotor + description: phase plate angle + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: C host: mpc2680.psi.ch - labels: losaz limits: - - 0 - - 0 - name: losaz + - 0 + - 0 port: 8085 sign: 1 - tolerance: 0.05 deviceTags: - lamni - status: - enabled: true - enabled_set: true + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline userParameter: in: -1 out: -3 - ############## LamNI SmarAct motors end here ############### @@ -390,20 +292,16 @@ losaz: ############################################################ eiger1p5m: - acquisitionConfig: - acquisitionGroup: detector - readoutPriority: monitored - schedule: sync description: Eiger 1.5M in vacuum detector, in-house developed, PSI - deviceClass: Eiger1p5MDetector + deviceClass: csaxs_bec.devices.eiger1p5m_csaxs.eiger1p5m.Eiger1p5MDetector deviceConfig: device_access: true - name: eiger1p5m deviceTags: - - detector - status: - enabled: true - enabled_set: true + - detector + readoutPriority: monitored + onFailure: buffer + enabled: true + readOnly: True ########### LamNI Eiger 1.5M in vacuum end here ############ @@ -415,149 +313,113 @@ eiger1p5m: ############################################################ x12sa_es1_shutter_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_es1_shutter_status read_pv: X12SA-OP-ST1:OPEN_EPS string: true deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_es1_valve: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_es1_valve read_pv: X12SA-ES-VW1:OPEN string: true deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_exposure_box1_pressure: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_exposure_box1_pressure read_pv: X12SA-ES-CH1MF1:PRESSURE string: false deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_exposure_box2_pressure: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_exposure_box2_pressure read_pv: X12SA-ES-EB1MF1:PRESSURE string: false deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_fe_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_fe_status read_pv: X12SA-FE-PH1:CLOSE4BL string: true deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_id_gap: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_id_gap read_pv: X12SA-ID-GAP:READ string: false deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_mokev: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_mokev read_pv: X12SA-OP-MO:E-GET string: false deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_op_status: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_op_status read_pv: ACOAU-ACCU:OP-X12SA string: true deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + readoutPriority: on_request + onFailure: buffer + enabled: true + readOnly: True x12sa_storage_ring_vac: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: on_request + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: x12sa_storage_ring_vac read_pv: X12SA-SR-VAC:SETPOINT string: true deviceTags: - X12SA status - status: - enabled: true - enabled_set: false + onFailure: buffer + enabled: true + readOnly: True ################ X12SA status PVs end here ################# @@ -569,203 +431,146 @@ x12sa_storage_ring_vac: ############################################################ sls_crane_usage: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_crane_usage read_pv: IBWKR-0101-QH10003:D01_H_D-WA string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_current_deadband: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_current_deadband read_pv: ALIRF-GUN:CUR-DBAND string: false deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_current_threshold: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_current_threshold read_pv: ALIRF-GUN:CUR-LOWLIM string: false deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_fast_orbit_feedback: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_fast_orbit_feedback read_pv: ARIDI-BPM:FOFBSTATUS-G string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_filling_life_time: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_filling_life_time read_pv: ARIDI-PCT:TAU-HOUR string: false deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_filling_pattern: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_filling_pattern read_pv: ACORF-FILL:PAT-SELECT string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_info: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: SLSInfo + readoutPriority: on_request + deviceClass: ophyd_devices.sls_devices.sls_devices.SLSInfo deviceConfig: - name: sls_info deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_injection_mode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_injection_mode read_pv: ALIRF-GUN:INJ-MODE string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_machine_status: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_machine_status read_pv: ACOAU-ACCU:OP-MODE string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True sls_operator: - acquisitionConfig: - acquisitionGroup: status - readoutPriority: ignored - schedule: sync - deviceClass: SLSOperatorMessages - deviceConfig: - name: sls_operator - deviceTags: - - SLS status - onFailure: buffer - status: - enabled: true - enabled_set: false -sls_orbit_feedback_mode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - deviceClass: EpicsSignalRO + readoutPriority: on_request + deviceClass: ophyd_devices.sls_devices.sls_devices.SLSOperatorMessages deviceConfig: auto_monitor: true - name: sls_orbit_feedback_mode read_pv: ARIDI-BPM:OFB-MODE string: true deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false -sls_ring_current: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: monitored - schedule: sync - deviceClass: EpicsSignalRO + enabled: true + readOnly: True +sls_orbit_feedback_mode: + readoutPriority: baseline + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: ARIDI-BPM:OFB-MODE + string: true + deviceTags: + - SLS status + onFailure: buffer + enabled: true + readOnly: True +sls_ring_current: + readoutPriority: monitored + deviceClass: ophyd.EpicsSignalRO deviceConfig: auto_monitor: true - name: sls_ring_current read_pv: ARIDI-PCT:CURRENT string: false deviceTags: - SLS status onFailure: buffer - status: - enabled: true - enabled_set: false + enabled: true + readOnly: True ################# SLS status PVs end here ################## @@ -775,2282 +580,1467 @@ sls_ring_current: ############################################################ ################### Default cSAXS config ################### ############################################################ - FBPMDX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: FBPMDX read_pv: X12SA-ID-FBPMD:X deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false FBPMDY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: FBPMDY read_pv: X12SA-ID-FBPMD:Y deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false FBPMUX: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: FBPMUX read_pv: X12SA-ID-FBPMU:X deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false FBPMUY: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: FBPMUY read_pv: X12SA-ID-FBPMU:Y deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false XASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: XASYM read_pv: X12SA-LBB:X-ASYM deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false XSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: XSYM read_pv: X12SA-LBB:X-SYM deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false YASYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: YASYM read_pv: X12SA-LBB:Y-ASYM deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false YSYM: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: YSYM read_pv: X12SA-LBB:Y-SYM deviceTags: - - cSAXS - - fofb + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false aptrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: ES aperture horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: aptrx prefix: X12SA-ES1-PIN1:TRX1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false aptry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: ES aperture vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: aptry prefix: X12SA-ES1-PIN1:TRY1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm1trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd XBPM 1 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm1trx prefix: X12SA-FE-BM1:TRH deviceTags: - - cSAXS - - bm1 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd XBPM 1 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm1try prefix: X12SA-FE-BM1:TRV deviceTags: - - cSAXS - - bm1 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd XBPM 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm2trx prefix: X12SA-FE-BM2:TRH deviceTags: - - cSAXS - - bm2 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd XBPM 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm2try prefix: X12SA-FE-BM2:TRV deviceTags: - - cSAXS - - bm2 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm3trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 1 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm3trx prefix: X12SA-OP-BM1:TRX1 deviceTags: - - cSAXS - - bm3 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 1 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm3try prefix: X12SA-OP-BM1:TRY1 deviceTags: - - cSAXS - - bm3 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm4trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm4trx prefix: X12SA-OP-BM2:TRX1 deviceTags: - - cSAXS - - bm4 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm4try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm4try prefix: X12SA-OP-BM2:TRY1 deviceTags: - - cSAXS - - bm4 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm5trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 3 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm5trx prefix: X12SA-OP-BM3:TRX1 deviceTags: - - cSAXS - - bm5 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bm5try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch XBPM 3 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bm5try prefix: X12SA-OP-BM3:TRY1 deviceTags: - - cSAXS - - bm5 + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 1: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp + deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp deviceConfig: - name: bpm1 prefix: 'X12SA-OP-BPM2:' deviceTags: - - cSAXS - - bpm1 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm1i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some VME XBPM... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm1i read_pv: X12SA-OP-BPM1:SUM deviceTags: - - cSAXS - - bpm1 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 2: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp + deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp deviceConfig: - name: bpm2 prefix: 'X12SA-OP-BPM2:' deviceTags: - - cSAXS - - bpm2 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm2i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some VME XBPM... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm2i read_pv: X12SA-OP-BPM2:SUM deviceTags: - - cSAXS - - bpm2 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false -bpm3a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync + readoutPriority: baseline + softwareTrigger: false +bpm3: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM3:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm3a: + description: 'XBPM 3: White beam AH501 before mono' + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm3a read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV deviceTags: - - cSAXS - - bpm3 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm3b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm3b read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV deviceTags: - - cSAXS - - bpm3 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm3c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm3c read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV deviceTags: - - cSAXS - - bpm3 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm3d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm3d read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV deviceTags: - - cSAXS - - bpm3 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm4a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm4a read_pv: X12SA-OP1-SCALER.S2 deviceTags: - - cSAXS - - bpm4 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm4b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm4b read_pv: X12SA-OP1-SCALER.S3 deviceTags: - - cSAXS - - bpm4 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm4c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm4c read_pv: X12SA-OP1-SCALER.S4 deviceTags: - - cSAXS - - bpm4 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm4d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm4d read_pv: X12SA-OP1-SCALER.S5 deviceTags: - - cSAXS - - bpm4 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm4i: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 4: integrated counts' - deviceClass: Bpm4i + deviceClass: ophyd.EpicsSignalRO deviceConfig: - name: bpm4i - prefix: X12SA-OP1-SCALER. + read_pv: X12SA-OP1-SCALER. deviceTags: - - cSAXS - - bpm4 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false -bpm5a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync + readoutPriority: baseline + softwareTrigger: false +bpm5: description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM5:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm5a: + description: 'XBPM 5: AH501 past the mirror' + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm5a read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV deviceTags: - - cSAXS - - bpm5 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm5b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm5b read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV deviceTags: - - cSAXS - - bpm5 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm5c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm5c read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV deviceTags: - - cSAXS - - bpm5 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm5d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm5d read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV deviceTags: - - cSAXS - - bpm5 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false -bpm6a: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync + readoutPriority: baseline + softwareTrigger: false +bpm6: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.QuadEM + deviceConfig: + prefix: 'X12SA-OP-BPM6:' + deviceTags: + - monitor + enabled: true + onFailure: buffer + readoutPriority: baseline + softwareTrigger: false +bpm6a: + description: 'XBPM 6: Xbox, not commissioned' + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm6a read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV deviceTags: - - cSAXS - - bpm6 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm6b: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm6b read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV deviceTags: - - cSAXS - - bpm6 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm6c: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm6c read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV deviceTags: - - cSAXS - - bpm6 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bpm6d: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: bpm6d read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV deviceTags: - - cSAXS - - bpm6 + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bs1x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 x - deviceClass: EpicsMotor + description: Dunno these motors??? + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bs1x prefix: X12SA-ES1-BS1:TRX1 deviceTags: - - cSAXS - - beam stop + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bs1y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 1 y - deviceClass: EpicsMotor + description: Dunno these motors??? + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bs1y prefix: X12SA-ES1-BS1:TRY1 deviceTags: - - cSAXS - - beam stop + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bs2x: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 x - deviceClass: EpicsMotor + description: Dunno these motors??? + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bs2x prefix: X12SA-ES1-BS2:TRX1 deviceTags: - - cSAXS - - beam stop + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false bs2y: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: Beam stop 2 y - deviceClass: EpicsMotor + description: Dunno these motors??? + deviceClass: ophyd.EpicsMotor deviceConfig: - name: bs2y prefix: X12SA-ES1-BS2:TRY1 deviceTags: - - cSAXS - - beam stop + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false curr: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: SLS ring current - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: curr read_pv: ARIDI-PCT:CURRENT deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false cyb: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: cyb read_pv: X12SA-ES1-SCALER.S2 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dettrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: dettrx prefix: X12SA-ES1-DET1:TRX1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false di2trx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd diaphragm 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: di2trx prefix: X12SA-FE-DI2:TRX1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false di2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd diaphragm 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: di2try prefix: X12SA-FE-DI2:TRY1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false diode: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: diode read_pv: X12SA-ES1-SCALER.S3 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dtpush: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower tilt pusher - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: dtpush prefix: X12SA-ES1-DETT:ROX1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dtth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower tilt rotation - deviceClass: PmDetectorRotation + deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmDetectorRotation deviceConfig: - name: dtth prefix: X12SA-ES1-DETT:ROX1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: dttrx prefix: X12SA-ES1-DETT:TRX1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower motion, no encoder - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: dttry prefix: X12SA-ES1-DETT:TRY1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false dttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: dttrz prefix: X12SA-ES1-DETT:TRZ1 deviceTags: - - cSAXS - - detector table + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false ebtrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Exposure box 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: ebtrx prefix: X12SA-ES1-EB:TRX1 deviceTags: - - cSAXS - - xbox + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false ebtry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Exposure box 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: ebtry prefix: X12SA-ES1-EB:TRY1 deviceTags: - - cSAXS - - xbox + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false ebtrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Exposure box 2 axial movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: ebtrz prefix: X12SA-ES1-EB:TRZ1 deviceTags: - - cSAXS - - xbox + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyecenx: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: eyecenx read_pv: XOMNYI-XEYE-XCEN:0 deviceTags: - - cSAXS - - xeye + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyeceny: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: eyeceny read_pv: XOMNYI-XEYE-YCEN:0 deviceTags: - - cSAXS - - xeye + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyefoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: X-ray eye focusing motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: eyefoc prefix: X12SA-ES2-ES25 deviceTags: - - cSAXS - - xeye + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyeint: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: eyeint read_pv: XOMNYI-XEYE-INT_MEAN:0 deviceTags: - - cSAXS - - xeye + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyex: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: X-ray eye motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: eyex prefix: X12SA-ES2-ES01 deviceTags: - - cSAXS - - xeye + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false eyey: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: X-ray eye motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: eyey prefix: X12SA-ES2-ES02 deviceTags: - - cSAXS - - xeye + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fal0: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: fal0 read_pv: X12SA-ES1-SCALER.S4 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fal1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: fal1 read_pv: X12SA-ES1-SCALER.S5 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fal2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: fal2 read_pv: X12SA-ES1-SCALER.S6 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fi1try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch filter 1 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fi1try prefix: X12SA-OP-FI1:TRY1 deviceTags: - - cSAXS - - filter + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fi2try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch filter 2 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fi2try prefix: X12SA-OP-FI2:TRY1 deviceTags: - - cSAXS - - filter + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fi3try: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch filter 3 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fi3try prefix: X12SA-OP-FI3:TRY1 deviceTags: - - cSAXS - - filter + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false ftp: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Flight tube pressure - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: ftp read_pv: X12SA-ES1-FT1MT1:PRESSURE deviceTags: - - cSAXS - - flight tube + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fttrx1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fttrx1 prefix: X12SA-ES1-FTS1:TRX1 deviceTags: - - cSAXS - - flight tube + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fttrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fttrx2 prefix: X12SA-ES1-FTS2:TRX1 deviceTags: - - cSAXS - - flight tube + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fttry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fttry1 prefix: X12SA-ES1-FTS1:TRY1 deviceTags: - - cSAXS - - flight tube + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fttry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fttry2 prefix: X12SA-ES1-FTS2:TRY1 deviceTags: - - cSAXS - - flight tube + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false fttrz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: fttrz prefix: X12SA-ES1-FTS1:TRZ1 deviceTags: - - cSAXS - - flight tube + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false idgap: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Undulator gap size [mm] - deviceClass: InsertionDevice + deviceClass: csaxs_bec.devices.epics.devices.InsertionDevice deviceConfig: - name: idgap prefix: X12SA-ID deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false led: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: led read_pv: X12SA-ES1-SCALER.S4 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mibd1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror bender 1 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mibd1 prefix: X12SA-OP-MI:TRZ1 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mibd2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror bender 2 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mibd2 prefix: X12SA-OP-MI:TRZ2 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false micfoc: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Microscope focusing motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: micfoc prefix: X12SA-ES2-ES03 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mitrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mitrx prefix: X12SA-OP-MI:TRX1 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mitry1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror vertical movement 1 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mitry1 prefix: X12SA-OP-MI:TRY1 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mitry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror vertical movement 2 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mitry2 prefix: X12SA-OP-MI:TRY2 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mitry3: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Mirror vertical movement 3 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mitry3 prefix: X12SA-OP-MI:TRY3 deviceTags: - - cSAXS - - mirror + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mobd: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator bender virtual motor - deviceClass: PmMonoBender + deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmMonoBender deviceConfig: - name: mobd prefix: 'X12SA-OP-MO:' deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mobdai: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator bender inner motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mobdai prefix: X12SA-OP-MO:TRYA deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mobdbo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator bender outer motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mobdbo prefix: X12SA-OP-MO:TRYB deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mobdco: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator bender outer motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mobdco prefix: X12SA-OP-MO:TRYC deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mobddi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator bender inner motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: mobddi prefix: X12SA-OP-MO:TRYD deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mokev: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator energy in keV - deviceClass: EnergyKev + deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev deviceConfig: - auto_monitor: true - name: mokev read_pv: X12SA-OP-MO:ROX2 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mopush1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 1 angle - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: mopush1 read_pv: X12SA-OP-MO:ROX1 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false mopush2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 angle - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: mopush2 read_pv: X12SA-OP-MO:ROX2 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moroll1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 1 roll - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: moroll1 prefix: X12SA-OP-MO:ROZ1 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moroll2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 roll movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: moroll2 prefix: X12SA-OP-MO:ROZ2 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moth1: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator Theta 1 - deviceClass: MonoTheta1 + deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta1 deviceConfig: - auto_monitor: true - name: moth1 read_pv: X12SA-OP-MO:ROX1 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moth1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 1 theta encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: moth1e read_pv: X12SA-OP-MO:ECX1 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moth2: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator Theta 2 - deviceClass: MonoTheta2 + deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta2 deviceConfig: - auto_monitor: true - name: moth2 read_pv: X12SA-OP-MO:ROX2 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moth2e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 theta encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: moth2e read_pv: X12SA-OP-MO:ECX2 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false motrx2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: motrx2 prefix: X12SA-OP-MO:TRX2 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false motry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch optical table vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: motry prefix: X12SA-OP-OT:TRY deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false motry2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: motry2 prefix: X12SA-OP-MO:TRY2 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false motrz1: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 1 axial movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: motrz1 prefix: X12SA-OP-MO:TRZ1 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false motrz1e: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 1 axial movement encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: motrz1e read_pv: X12SA-OP-MO:ECZ1 deviceTags: - - cSAXS - - mono + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false moyaw2: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Monochromator crystal 2 yaw movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: moyaw2 prefix: X12SA-OP-MO:ROY2 deviceTags: - - cSAXS - - mono + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false -ppth: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate angle - deviceClass: EpicsMotor - deviceConfig: - name: ppth - prefix: X12SA-ES2-ES23 - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: true -ppthenc: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync - description: phase plate encoder - deviceClass: EpicsSignalRO - deviceConfig: - name: ppthenc - read_pv: X12SA-ES2-EC1.VAL - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: false -ppx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync - description: phase plate - deviceClass: EpicsMotor - deviceConfig: - name: ppx - prefix: X12SA-ES2-ES01 - deviceTags: - - cSAXS - - lamni - - phase plates - onFailure: buffer - status: - enabled: true - enabled_set: true + readoutPriority: baseline + softwareTrigger: false samx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Sample motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: samx prefix: X12SA-ES2-ES04 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false samy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Sample motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: samy prefix: X12SA-ES2-ES05 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sec: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: sec read_pv: X12SA-ES1-SCALER.S1 deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl0h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd slit virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: - name: sl0h prefix: 'X12SA-FE-SH1:' deviceTags: - - cSAXS + - epicsDevice + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl0trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd slit inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl0trxi prefix: X12SA-FE-SH1:TRX1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl0trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: FrontEnd slit outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl0trxo prefix: X12SA-FE-SH1:TRX2 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: - name: sl1h prefix: 'X12SA-OP-SH1:' deviceTags: - - cSAXS + - epicsDevice + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl1trxi prefix: X12SA-OP-SH1:TRX2 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl1trxo prefix: X12SA-OP-SH1:TRX1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit bottom blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl1tryb prefix: X12SA-OP-SV1:TRY2 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit top blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl1tryt prefix: X12SA-OP-SV1:TRY1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl1v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit virtual movement - deviceClass: SlitV + deviceClass: ophyd_devices.epics.devices.SlitV deviceConfig: - name: sl1v prefix: 'X12SA-OP-SV1:' deviceTags: - - cSAXS + - epicsDevice + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2h: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: - name: sl2h prefix: 'X12SA-OP-SH2:' deviceTags: - - cSAXS + - epicsDevice + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2trxi: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl2trxi prefix: X12SA-OP-SH2:TRX2 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2trxo: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl2trxo prefix: X12SA-OP-SH2:TRX1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2tryb: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 bottom blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl2tryb prefix: X12SA-OP-SV2:TRY2 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2tryt: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 top blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: - name: sl2tryt prefix: X12SA-OP-SV2:TRY1 deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sl2v: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: OpticsHutch slit 2 virtual movement - deviceClass: SlitV + deviceClass: ophyd_devices.epics.devices.SlitV deviceConfig: - name: sl2v prefix: 'X12SA-OP-SV2:' deviceTags: - - cSAXS + - epicsDevice + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false strox: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Girder virtual pitch - deviceClass: GirderMotorPITCH + deviceClass: ophyd_devices.epics.devices.GirderMotorPITCH deviceConfig: - name: strox prefix: X12SA-HG deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false stroy: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Girder virtual yaw - deviceClass: GirderMotorYAW + deviceClass: ophyd_devices.epics.devices.GirderMotorYAW deviceConfig: - name: stroy prefix: X12SA-HG deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false stroz: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Girder virtual roll - deviceClass: GirderMotorROLL + deviceClass: ophyd_devices.epics.devices.GirderMotorROLL deviceConfig: - name: stroz prefix: X12SA-HG deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sttrx: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Girder X translation - deviceClass: GirderMotorX1 + deviceClass: ophyd_devices.epics.devices.GirderMotorX1 deviceConfig: - name: sttrx prefix: X12SA-HG deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false sttry: - acquisitionConfig: - acquisitionGroup: motor - readoutPriority: baseline - schedule: sync description: Girder Y translation - deviceClass: GirderMotorY1 + deviceClass: ophyd_devices.epics.devices.GirderMotorY1 deviceConfig: - name: sttry prefix: X12SA-HG deviceTags: - - cSAXS + - beamlineMotor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false + readoutPriority: baseline + softwareTrigger: false transd: - acquisitionConfig: - acquisitionGroup: monitor - readoutPriority: baseline - schedule: sync description: Transmission diode - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: - auto_monitor: true - name: transd read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV deviceTags: - - cSAXS + - monitor + enabled: true onFailure: buffer - status: - enabled: true - enabled_set: false - - -############## Default cSAXS config end here ############### + readoutPriority: baseline + softwareTrigger: false diff --git a/csaxs_bec/device_configs/flomni_config.yaml b/csaxs_bec/device_configs/flomni_config.yaml index 942497c..4cd38ec 100644 --- a/csaxs_bec/device_configs/flomni_config.yaml +++ b/csaxs_bec/device_configs/flomni_config.yaml @@ -1,6 +1,6 @@ feyex: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: D host: mpc2844.psi.ch @@ -18,7 +18,7 @@ feyex: out: -1 feyey: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: E host: mpc2844.psi.ch @@ -35,7 +35,7 @@ feyey: in: -10.467 fheater: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -50,7 +50,7 @@ fheater: readoutPriority: baseline flomni_samples: description: phase plate angle - deviceClass: FlomniSampleStorage + deviceClass: csaxs_bec.devices.epics.devices.flomni_sample_storage.FlomniSampleStorage deviceConfig: {} enabled: true onFailure: buffer @@ -58,7 +58,7 @@ flomni_samples: readoutPriority: baseline foptx: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -75,7 +75,7 @@ foptx: in: -13.761 fopty: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: F host: mpc2844.psi.ch @@ -93,7 +93,7 @@ fopty: out: 0.752 foptz: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -110,7 +110,7 @@ foptz: in: 23 fosax: description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -128,7 +128,7 @@ fosax: out: 5.3 fosay: description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -145,7 +145,7 @@ fosay: in: 0.367 fosaz: description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -163,7 +163,7 @@ fosaz: out: 6 fsamroy: description: phase plate angle - deviceClass: FuprGalilMotor + deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -178,7 +178,7 @@ fsamroy: readoutPriority: baseline fsamx: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: E host: mpc2844.psi.ch @@ -195,7 +195,7 @@ fsamx: in: -1.1 fsamy: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: F host: mpc2844.psi.ch @@ -212,7 +212,7 @@ fsamy: in: 2.75 ftracky: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: H host: mpc2844.psi.ch @@ -227,7 +227,7 @@ ftracky: readoutPriority: baseline ftrackz: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: G host: mpc2844.psi.ch @@ -242,7 +242,7 @@ ftrackz: readoutPriority: baseline ftransx: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -257,7 +257,7 @@ ftransx: readoutPriority: baseline ftransy: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -272,7 +272,7 @@ ftransy: readoutPriority: baseline ftransz: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -287,7 +287,7 @@ ftransz: readoutPriority: baseline ftray: description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: D host: mpc2844.psi.ch @@ -302,7 +302,7 @@ ftray: readoutPriority: baseline rtx: description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -318,7 +318,7 @@ rtx: rt_pid_voltage: -0.06219 rty: description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -332,7 +332,7 @@ rty: tomo_additional_offsety: 0 rtz: description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml b/csaxs_bec/device_configs/flomni_test_config.yaml similarity index 77% rename from csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml rename to csaxs_bec/device_configs/flomni_test_config.yaml index 3fa9d83..1251eb9 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_test_config.yaml +++ b/csaxs_bec/device_configs/flomni_test_config.yaml @@ -1,7 +1,7 @@ fheater: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -16,7 +16,7 @@ fheater: feyex: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: D host: mpc2844.psi.ch @@ -34,7 +34,7 @@ feyex: feyey: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: E host: mpc2844.psi.ch @@ -51,7 +51,7 @@ feyey: foptx: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -68,7 +68,7 @@ foptx: fopty: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: F host: mpc2844.psi.ch @@ -86,7 +86,7 @@ fopty: foptz: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -103,7 +103,7 @@ foptz: fosax: readoutPriority: baseline description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -121,7 +121,7 @@ fosax: fosay: readoutPriority: baseline description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -138,7 +138,7 @@ fosay: fosaz: readoutPriority: baseline description: phase plate angle - deviceClass: SmaractMotor + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -156,7 +156,7 @@ fosaz: fsamroy: readoutPriority: baseline description: phase plate angle - deviceClass: FuprGalilMotor + deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -171,7 +171,7 @@ fsamroy: fsamx: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: E host: mpc2844.psi.ch @@ -188,7 +188,7 @@ fsamx: fsamy: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: F host: mpc2844.psi.ch @@ -205,7 +205,7 @@ fsamy: ftracky: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: H host: mpc2844.psi.ch @@ -220,7 +220,7 @@ ftracky: ftrackz: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: G host: mpc2844.psi.ch @@ -235,7 +235,7 @@ ftrackz: ftransx: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch @@ -250,7 +250,7 @@ ftransx: ftransy: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -265,7 +265,7 @@ ftransy: ftransz: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -280,7 +280,7 @@ ftransz: ftray: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniGalilMotor + deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor deviceConfig: axis_Id: D host: mpc2844.psi.ch @@ -296,7 +296,7 @@ ftray: flomni_samples: readoutPriority: baseline description: phase plate angle - deviceClass: FlomniSampleStorage + deviceClass: csaxs_bec.devices.epics.devices.FlomniSampleStorage deviceConfig: onFailure: buffer enabled: true @@ -305,7 +305,7 @@ flomni_samples: rtx: readoutPriority: on_request description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: A host: mpc2844.psi.ch @@ -317,7 +317,7 @@ rtx: rty: readoutPriority: on_request description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: B host: mpc2844.psi.ch @@ -329,7 +329,7 @@ rty: rtz: readoutPriority: on_request description: flomni rt - deviceClass: RtFlomniMotor + deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor deviceConfig: axis_Id: C host: mpc2844.psi.ch diff --git a/csaxs_bec/device_configs/x12sa_database.yml b/csaxs_bec/device_configs/x12sa_database.yml index ef1735e..2bee4f9 100644 --- a/csaxs_bec/device_configs/x12sa_database.yml +++ b/csaxs_bec/device_configs/x12sa_database.yml @@ -1,6 +1,6 @@ FBPMDX: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ID-FBPMD:X deviceTags: @@ -11,7 +11,7 @@ FBPMDX: softwareTrigger: false FBPMDY: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ID-FBPMD:Y deviceTags: @@ -22,7 +22,7 @@ FBPMDY: softwareTrigger: false FBPMUX: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ID-FBPMU:X deviceTags: @@ -33,7 +33,7 @@ FBPMUX: softwareTrigger: false FBPMUY: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ID-FBPMU:Y deviceTags: @@ -44,7 +44,7 @@ FBPMUY: softwareTrigger: false XASYM: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-LBB:X-ASYM deviceTags: @@ -55,7 +55,7 @@ XASYM: softwareTrigger: false XSYM: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-LBB:X-SYM deviceTags: @@ -66,7 +66,7 @@ XSYM: softwareTrigger: false YASYM: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-LBB:Y-ASYM deviceTags: @@ -77,7 +77,7 @@ YASYM: softwareTrigger: false YSYM: description: FOFB reference - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-LBB:Y-SYM deviceTags: @@ -88,7 +88,7 @@ YSYM: softwareTrigger: false aptrx: description: ES aperture horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-PIN1:TRX1 deviceTags: @@ -99,7 +99,7 @@ aptrx: softwareTrigger: false aptry: description: ES aperture vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-PIN1:TRY1 deviceTags: @@ -110,7 +110,7 @@ aptry: softwareTrigger: false bm1trx: description: FrontEnd XBPM 1 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-BM1:TRH deviceTags: @@ -121,7 +121,7 @@ bm1trx: softwareTrigger: false bm1try: description: FrontEnd XBPM 1 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-BM1:TRV deviceTags: @@ -132,7 +132,7 @@ bm1try: softwareTrigger: false bm2trx: description: FrontEnd XBPM 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-BM2:TRH deviceTags: @@ -143,7 +143,7 @@ bm2trx: softwareTrigger: false bm2try: description: FrontEnd XBPM 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-BM2:TRV deviceTags: @@ -154,7 +154,7 @@ bm2try: softwareTrigger: false bm3trx: description: OpticsHutch XBPM 1 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM1:TRX1 deviceTags: @@ -165,7 +165,7 @@ bm3trx: softwareTrigger: false bm3try: description: OpticsHutch XBPM 1 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM1:TRY1 deviceTags: @@ -176,7 +176,7 @@ bm3try: softwareTrigger: false bm4trx: description: OpticsHutch XBPM 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM2:TRX1 deviceTags: @@ -187,7 +187,7 @@ bm4trx: softwareTrigger: false bm4try: description: OpticsHutch XBPM 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM2:TRY1 deviceTags: @@ -198,7 +198,7 @@ bm4try: softwareTrigger: false bm5trx: description: OpticsHutch XBPM 3 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM3:TRX1 deviceTags: @@ -209,7 +209,7 @@ bm5trx: softwareTrigger: false bm5try: description: OpticsHutch XBPM 3 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-BM3:TRY1 deviceTags: @@ -220,7 +220,7 @@ bm5try: softwareTrigger: false bpm1: description: 'XBPM 1: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp + deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp deviceConfig: prefix: 'X12SA-OP-BPM2:' deviceTags: @@ -231,7 +231,7 @@ bpm1: softwareTrigger: false bpm1i: description: Some VME XBPM... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM1:SUM deviceTags: @@ -242,7 +242,7 @@ bpm1i: softwareTrigger: false bpm2: description: 'XBPM 2: Somewhere around mono (VME)' - deviceClass: XbpmCsaxsOp + deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp deviceConfig: prefix: 'X12SA-OP-BPM2:' deviceTags: @@ -253,7 +253,7 @@ bpm2: softwareTrigger: false bpm2i: description: Some VME XBPM... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM2:SUM deviceTags: @@ -264,7 +264,7 @@ bpm2i: softwareTrigger: false bpm3: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: QuadEM + deviceClass: ophyd.QuadEM deviceConfig: prefix: 'X12SA-OP-BPM3:' deviceTags: @@ -275,7 +275,7 @@ bpm3: softwareTrigger: false bpm3a: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM3:Current1:MeanValue_RBV deviceTags: @@ -286,7 +286,7 @@ bpm3a: softwareTrigger: false bpm3b: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM3:Current2:MeanValue_RBV deviceTags: @@ -297,7 +297,7 @@ bpm3b: softwareTrigger: false bpm3c: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM3:Current3:MeanValue_RBV deviceTags: @@ -308,7 +308,7 @@ bpm3c: softwareTrigger: false bpm3d: description: 'XBPM 3: White beam AH501 before mono' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM3:Current4:MeanValue_RBV deviceTags: @@ -319,7 +319,7 @@ bpm3d: softwareTrigger: false bpm4a: description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP1-SCALER.S2 deviceTags: @@ -330,7 +330,7 @@ bpm4a: softwareTrigger: false bpm4b: description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP1-SCALER.S3 deviceTags: @@ -341,7 +341,7 @@ bpm4b: softwareTrigger: false bpm4c: description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP1-SCALER.S4 deviceTags: @@ -352,7 +352,7 @@ bpm4c: softwareTrigger: false bpm4d: description: 'XBPM 4: VME between mono and mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP1-SCALER.S5 deviceTags: @@ -363,7 +363,7 @@ bpm4d: softwareTrigger: false bpm4i: description: 'XBPM 4: integrated counts' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP1-SCALER. deviceTags: @@ -374,7 +374,7 @@ bpm4i: softwareTrigger: false bpm5: description: 'XBPM 5: AH501 past the mirror' - deviceClass: QuadEM + deviceClass: ophyd.QuadEM deviceConfig: prefix: 'X12SA-OP-BPM5:' deviceTags: @@ -385,7 +385,7 @@ bpm5: softwareTrigger: false bpm5a: description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM5:Current1:MeanValue_RBV deviceTags: @@ -396,7 +396,7 @@ bpm5a: softwareTrigger: false bpm5b: description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM5:Current2:MeanValue_RBV deviceTags: @@ -407,7 +407,7 @@ bpm5b: softwareTrigger: false bpm5c: description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM5:Current3:MeanValue_RBV deviceTags: @@ -418,7 +418,7 @@ bpm5c: softwareTrigger: false bpm5d: description: 'XBPM 5: AH501 past the mirror' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM5:Current4:MeanValue_RBV deviceTags: @@ -429,7 +429,7 @@ bpm5d: softwareTrigger: false bpm6: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: QuadEM + deviceClass: ophyd.QuadEM deviceConfig: prefix: 'X12SA-OP-BPM6:' deviceTags: @@ -440,7 +440,7 @@ bpm6: softwareTrigger: false bpm6a: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM6:Current1:MeanValue_RBV deviceTags: @@ -451,7 +451,7 @@ bpm6a: softwareTrigger: false bpm6b: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM6:Current2:MeanValue_RBV deviceTags: @@ -462,7 +462,7 @@ bpm6b: softwareTrigger: false bpm6c: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM6:Current3:MeanValue_RBV deviceTags: @@ -473,7 +473,7 @@ bpm6c: softwareTrigger: false bpm6d: description: 'XBPM 6: Xbox, not commissioned' - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM6:Current4:MeanValue_RBV deviceTags: @@ -484,7 +484,7 @@ bpm6d: softwareTrigger: false bs1x: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-BS1:TRX1 deviceTags: @@ -495,7 +495,7 @@ bs1x: softwareTrigger: false bs1y: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-BS1:TRY1 deviceTags: @@ -506,7 +506,7 @@ bs1y: softwareTrigger: false bs2x: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-BS2:TRX1 deviceTags: @@ -517,7 +517,7 @@ bs2x: softwareTrigger: false bs2y: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-BS2:TRY1 deviceTags: @@ -528,7 +528,7 @@ bs2y: softwareTrigger: false curr: description: SLS ring current - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: ARIDI-PCT:CURRENT deviceTags: @@ -539,7 +539,7 @@ curr: softwareTrigger: false cyb: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S2 deviceTags: @@ -550,7 +550,7 @@ cyb: softwareTrigger: false dettrx: description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-DET1:TRX1 deviceTags: @@ -561,7 +561,7 @@ dettrx: softwareTrigger: false di2trx: description: FrontEnd diaphragm 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-DI2:TRX1 deviceTags: @@ -572,7 +572,7 @@ di2trx: softwareTrigger: false di2try: description: FrontEnd diaphragm 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-DI2:TRY1 deviceTags: @@ -583,7 +583,7 @@ di2try: softwareTrigger: false diode: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S3 deviceTags: @@ -594,7 +594,7 @@ diode: softwareTrigger: false dtpush: description: Detector tower tilt pusher - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-DETT:ROX1 deviceTags: @@ -605,7 +605,7 @@ dtpush: softwareTrigger: false dtth: description: Detector tower tilt rotation - deviceClass: PmDetectorRotation + deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmDetectorRotation deviceConfig: prefix: X12SA-ES1-DETT:ROX1 deviceTags: @@ -616,7 +616,7 @@ dtth: softwareTrigger: false dttrx: description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-DETT:TRX1 deviceTags: @@ -627,7 +627,7 @@ dttrx: softwareTrigger: false dttry: description: Detector tower motion, no encoder - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-DETT:TRY1 deviceTags: @@ -638,7 +638,7 @@ dttry: softwareTrigger: false dttrz: description: Detector tower motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-DETT:TRZ1 deviceTags: @@ -649,7 +649,7 @@ dttrz: softwareTrigger: false ebtrx: description: Exposure box 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-EB:TRX1 deviceTags: @@ -660,7 +660,7 @@ ebtrx: softwareTrigger: false ebtry: description: Exposure box 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-EB:TRY1 deviceTags: @@ -671,7 +671,7 @@ ebtry: softwareTrigger: false ebtrz: description: Exposure box 2 axial movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-EB:TRZ1 deviceTags: @@ -682,7 +682,7 @@ ebtrz: softwareTrigger: false eyecenx: description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: XOMNYI-XEYE-XCEN:0 deviceTags: @@ -693,7 +693,7 @@ eyecenx: softwareTrigger: false eyeceny: description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: XOMNYI-XEYE-YCEN:0 deviceTags: @@ -704,7 +704,7 @@ eyeceny: softwareTrigger: false eyefoc: description: X-ray eye focusing motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES25 deviceTags: @@ -715,7 +715,7 @@ eyefoc: softwareTrigger: false eyeint: description: X-ray eye intensit math - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: XOMNYI-XEYE-INT_MEAN:0 deviceTags: @@ -726,7 +726,7 @@ eyeint: softwareTrigger: false eyex: description: X-ray eye motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES01 deviceTags: @@ -737,7 +737,7 @@ eyex: softwareTrigger: false eyey: description: X-ray eye motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES02 deviceTags: @@ -748,7 +748,7 @@ eyey: softwareTrigger: false fal0: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S4 deviceTags: @@ -759,7 +759,7 @@ fal0: softwareTrigger: false fal1: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S5 deviceTags: @@ -770,7 +770,7 @@ fal1: softwareTrigger: false fal2: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S6 deviceTags: @@ -781,7 +781,7 @@ fal2: softwareTrigger: false fi1try: description: OpticsHutch filter 1 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-FI1:TRY1 deviceTags: @@ -792,7 +792,7 @@ fi1try: softwareTrigger: false fi2try: description: OpticsHutch filter 2 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-FI2:TRY1 deviceTags: @@ -803,7 +803,7 @@ fi2try: softwareTrigger: false fi3try: description: OpticsHutch filter 3 movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-FI3:TRY1 deviceTags: @@ -814,7 +814,7 @@ fi3try: softwareTrigger: false ftp: description: Flight tube pressure - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-FT1MT1:PRESSURE deviceTags: @@ -825,7 +825,7 @@ ftp: softwareTrigger: false fttrx1: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-FTS1:TRX1 deviceTags: @@ -836,7 +836,7 @@ fttrx1: softwareTrigger: false fttrx2: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-FTS2:TRX1 deviceTags: @@ -847,7 +847,7 @@ fttrx2: softwareTrigger: false fttry1: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-FTS1:TRY1 deviceTags: @@ -858,7 +858,7 @@ fttry1: softwareTrigger: false fttry2: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-FTS2:TRY1 deviceTags: @@ -869,7 +869,7 @@ fttry2: softwareTrigger: false fttrz: description: Dunno these motors??? - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES1-FTS1:TRZ1 deviceTags: @@ -880,7 +880,7 @@ fttrz: softwareTrigger: false idgap: description: Undulator gap size [mm] - deviceClass: InsertionDevice + deviceClass: csaxs_bec.devices.epics.devices.InsertionDevice.InsertionDevice deviceConfig: prefix: X12SA-ID deviceTags: @@ -891,7 +891,7 @@ idgap: softwareTrigger: false led: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S4 deviceTags: @@ -902,7 +902,7 @@ led: softwareTrigger: false mibd1: description: Mirror bender 1 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRZ1 deviceTags: @@ -913,7 +913,7 @@ mibd1: softwareTrigger: false mibd2: description: Mirror bender 2 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRZ2 deviceTags: @@ -924,7 +924,7 @@ mibd2: softwareTrigger: false micfoc: description: Microscope focusing motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES03 deviceTags: @@ -935,7 +935,7 @@ micfoc: softwareTrigger: false mitrx: description: Mirror horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRX1 deviceTags: @@ -946,7 +946,7 @@ mitrx: softwareTrigger: false mitry1: description: Mirror vertical movement 1 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRY1 deviceTags: @@ -957,7 +957,7 @@ mitry1: softwareTrigger: false mitry2: description: Mirror vertical movement 2 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRY2 deviceTags: @@ -968,7 +968,7 @@ mitry2: softwareTrigger: false mitry3: description: Mirror vertical movement 3 - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MI:TRY3 deviceTags: @@ -979,7 +979,7 @@ mitry3: softwareTrigger: false mobd: description: Monochromator bender virtual motor - deviceClass: PmMonoBender + deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmMonoBender deviceConfig: prefix: 'X12SA-OP-MO:' deviceTags: @@ -990,7 +990,7 @@ mobd: softwareTrigger: false mobdai: description: Monochromator bender inner motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRYA deviceTags: @@ -1001,7 +1001,7 @@ mobdai: softwareTrigger: false mobdbo: description: Monochromator bender outer motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRYB deviceTags: @@ -1012,7 +1012,7 @@ mobdbo: softwareTrigger: false mobdco: description: Monochromator bender outer motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRYC deviceTags: @@ -1023,7 +1023,7 @@ mobdco: softwareTrigger: false mobddi: description: Monochromator bender inner motor - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRYD deviceTags: @@ -1034,7 +1034,7 @@ mobddi: softwareTrigger: false mokev: description: Monochromator energy in keV - deviceClass: EnergyKev + deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev deviceConfig: read_pv: X12SA-OP-MO:ROX2 deviceTags: @@ -1045,7 +1045,7 @@ mokev: softwareTrigger: false mopush1: description: Monochromator crystal 1 angle - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-MO:ROX1 deviceTags: @@ -1056,7 +1056,7 @@ mopush1: softwareTrigger: false mopush2: description: Monochromator crystal 2 angle - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-MO:ROX2 deviceTags: @@ -1067,7 +1067,7 @@ mopush2: softwareTrigger: false moroll1: description: Monochromator crystal 1 roll - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:ROZ1 deviceTags: @@ -1078,7 +1078,7 @@ moroll1: softwareTrigger: false moroll2: description: Monochromator crystal 2 roll movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:ROZ2 deviceTags: @@ -1089,7 +1089,7 @@ moroll2: softwareTrigger: false moth1: description: Monochromator Theta 1 - deviceClass: MonoTheta1 + deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta1 deviceConfig: read_pv: X12SA-OP-MO:ROX1 deviceTags: @@ -1100,7 +1100,7 @@ moth1: softwareTrigger: false moth1e: description: Monochromator crystal 1 theta encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-MO:ECX1 deviceTags: @@ -1111,7 +1111,7 @@ moth1e: softwareTrigger: false moth2: description: Monochromator Theta 2 - deviceClass: MonoTheta2 + deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta2 deviceConfig: read_pv: X12SA-OP-MO:ROX2 deviceTags: @@ -1122,7 +1122,7 @@ moth2: softwareTrigger: false moth2e: description: Monochromator crystal 2 theta encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-MO:ECX2 deviceTags: @@ -1133,7 +1133,7 @@ moth2e: softwareTrigger: false motrx2: description: Monochromator crystal 2 horizontal movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRX2 deviceTags: @@ -1144,7 +1144,7 @@ motrx2: softwareTrigger: false motry: description: OpticsHutch optical table vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-OT:TRY deviceTags: @@ -1155,7 +1155,7 @@ motry: softwareTrigger: false motry2: description: Monochromator crystal 2 vertical movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRY2 deviceTags: @@ -1166,7 +1166,7 @@ motry2: softwareTrigger: false motrz1: description: Monochromator crystal 1 axial movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:TRZ1 deviceTags: @@ -1177,7 +1177,7 @@ motrz1: softwareTrigger: false motrz1e: description: Monochromator crystal 1 axial movement encoder - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-MO:ECZ1 deviceTags: @@ -1188,7 +1188,7 @@ motrz1e: softwareTrigger: false moyaw2: description: Monochromator crystal 2 yaw movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-MO:ROY2 deviceTags: @@ -1199,7 +1199,7 @@ moyaw2: softwareTrigger: false samx: description: Sample motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES04 deviceTags: @@ -1210,7 +1210,7 @@ samx: softwareTrigger: false samy: description: Sample motion - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-ES2-ES05 deviceTags: @@ -1221,7 +1221,7 @@ samy: softwareTrigger: false sec: description: Some scaler... - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-ES1-SCALER.S1 deviceTags: @@ -1232,7 +1232,7 @@ sec: softwareTrigger: false sl0h: description: FrontEnd slit virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: prefix: 'X12SA-FE-SH1:' deviceTags: @@ -1243,7 +1243,7 @@ sl0h: softwareTrigger: false sl0trxi: description: FrontEnd slit inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-SH1:TRX1 deviceTags: @@ -1254,7 +1254,7 @@ sl0trxi: softwareTrigger: false sl0trxo: description: FrontEnd slit outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-FE-SH1:TRX2 deviceTags: @@ -1265,7 +1265,7 @@ sl0trxo: softwareTrigger: false sl1h: description: OpticsHutch slit virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: prefix: 'X12SA-OP-SH1:' deviceTags: @@ -1276,7 +1276,7 @@ sl1h: softwareTrigger: false sl1trxi: description: OpticsHutch slit inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SH1:TRX2 deviceTags: @@ -1287,7 +1287,7 @@ sl1trxi: softwareTrigger: false sl1trxo: description: OpticsHutch slit outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SH1:TRX1 deviceTags: @@ -1298,7 +1298,7 @@ sl1trxo: softwareTrigger: false sl1tryb: description: OpticsHutch slit bottom blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SV1:TRY2 deviceTags: @@ -1309,7 +1309,7 @@ sl1tryb: softwareTrigger: false sl1tryt: description: OpticsHutch slit top blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SV1:TRY1 deviceTags: @@ -1320,7 +1320,7 @@ sl1tryt: softwareTrigger: false sl1v: description: OpticsHutch slit virtual movement - deviceClass: SlitV + deviceClass: ophyd_devices.epics.devices.SlitV deviceConfig: prefix: 'X12SA-OP-SV1:' deviceTags: @@ -1331,7 +1331,7 @@ sl1v: softwareTrigger: false sl2h: description: OpticsHutch slit 2 virtual movement - deviceClass: SlitH + deviceClass: ophyd_devices.epics.devices.SlitH deviceConfig: prefix: 'X12SA-OP-SH2:' deviceTags: @@ -1342,7 +1342,7 @@ sl2h: softwareTrigger: false sl2trxi: description: OpticsHutch slit 2 inner blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SH2:TRX2 deviceTags: @@ -1353,7 +1353,7 @@ sl2trxi: softwareTrigger: false sl2trxo: description: OpticsHutch slit 2 outer blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SH2:TRX1 deviceTags: @@ -1364,7 +1364,7 @@ sl2trxo: softwareTrigger: false sl2tryb: description: OpticsHutch slit 2 bottom blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SV2:TRY2 deviceTags: @@ -1375,7 +1375,7 @@ sl2tryb: softwareTrigger: false sl2tryt: description: OpticsHutch slit 2 top blade movement - deviceClass: EpicsMotor + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X12SA-OP-SV2:TRY1 deviceTags: @@ -1386,7 +1386,7 @@ sl2tryt: softwareTrigger: false sl2v: description: OpticsHutch slit 2 virtual movement - deviceClass: SlitV + deviceClass: ophyd_devices.epics.devices.SlitV deviceConfig: prefix: 'X12SA-OP-SV2:' deviceTags: @@ -1397,7 +1397,7 @@ sl2v: softwareTrigger: false strox: description: Girder virtual pitch - deviceClass: GirderMotorPITCH + deviceClass: ophyd_devices.epics.devices.GirderMotorPITCH deviceConfig: prefix: X12SA-HG deviceTags: @@ -1408,7 +1408,7 @@ strox: softwareTrigger: false stroy: description: Girder virtual yaw - deviceClass: GirderMotorYAW + deviceClass: ophyd_devices.epics.devices.GirderMotorYAW deviceConfig: prefix: X12SA-HG deviceTags: @@ -1419,7 +1419,7 @@ stroy: softwareTrigger: false stroz: description: Girder virtual roll - deviceClass: GirderMotorROLL + deviceClass: ophyd_devices.epics.devices.GirderMotorROLL deviceConfig: prefix: X12SA-HG deviceTags: @@ -1430,7 +1430,7 @@ stroz: softwareTrigger: false sttrx: description: Girder X translation - deviceClass: GirderMotorX1 + deviceClass: ophyd_devices.epics.devices.GirderMotorX1 deviceConfig: prefix: X12SA-HG deviceTags: @@ -1441,7 +1441,7 @@ sttrx: softwareTrigger: false sttry: description: Girder Y translation - deviceClass: GirderMotorY1 + deviceClass: ophyd_devices.epics.devices.GirderMotorY1 deviceConfig: prefix: X12SA-HG deviceTags: @@ -1452,7 +1452,7 @@ sttry: softwareTrigger: false transd: description: Transmission diode - deviceClass: EpicsSignalRO + deviceClass: ophyd.EpicsSignalRO deviceConfig: read_pv: X12SA-OP-BPM1:Current1:MeanValue_RBV deviceTags: diff --git a/csaxs_bec/devices/epics/devices/specMotors.py b/csaxs_bec/devices/epics/devices/specMotors.py new file mode 100644 index 0000000..65a8e70 --- /dev/null +++ b/csaxs_bec/devices/epics/devices/specMotors.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Oct 13 18:06:15 2021 + +@author: mohacsi_i + +IMPORTANT: Virtual monochromator axes should be implemented already in EPICS!!! +""" + +import time +from math import asin, atan, isclose, sin, sqrt, tan + +import numpy as np +from ophyd import ( + Component, + Device, + EpicsMotor, + EpicsSignal, + EpicsSignalRO, + Kind, + PseudoPositioner, + PseudoSingle, + PVPositioner, + Signal, +) +from ophyd.pseudopos import pseudo_position_argument, real_position_argument + + +class PmMonoBender(PseudoPositioner): + """Monochromator bender + + Small wrapper to combine the four monochromator bender motors. + """ + + # Real axes + ai = Component(EpicsMotor, "TRYA", name="ai") + bo = Component(EpicsMotor, "TRYB", name="bo") + co = Component(EpicsMotor, "TRYC", name="co") + di = Component(EpicsMotor, "TRYD", name="di") + + # Virtual axis + bend = Component(PseudoSingle, name="bend") + + _real = ["ai", "bo", "co", "di"] + + @pseudo_position_argument + def forward(self, pseudo_pos): + delta = pseudo_pos.bend - 0.25 * ( + self.ai.position + self.bo.position + self.co.position + self.di.position + ) + return self.RealPosition( + ai=self.ai.position + delta, + bo=self.bo.position + delta, + co=self.co.position + delta, + di=self.di.position + delta, + ) + + @real_position_argument + def inverse(self, real_pos): + return self.PseudoPosition( + bend=0.25 * (real_pos.ai + real_pos.bo + real_pos.co + real_pos.di) + ) + + +def r2d(radians): + return radians * 180 / 3.141592 + + +def d2r(degrees): + return degrees * 3.141592 / 180.0 + + +class PmDetectorRotation(PseudoPositioner): + """Detector rotation pseudo motor + + Small wrapper to convert detector pusher position to rotation angle. + """ + + _tables_dt_push_dist_mm = 890 + # Real axes + dtpush = Component(EpicsMotor, "", name="dtpush") + + # Virtual axis + dtth = Component(PseudoSingle, name="dtth") + + _real = ["dtpush"] + + @pseudo_position_argument + def forward(self, pseudo_pos): + return self.RealPosition( + dtpush=d2r(tan(-3.14 / 180 * pseudo_pos.dtth)) * self._tables_dt_push_dist_mm + ) + + @real_position_argument + def inverse(self, real_pos): + return self.PseudoPosition(dtth=r2d(-atan(real_pos.dtpush / self._tables_dt_push_dist_mm))) + + +class GirderMotorX1(PVPositioner): + """Girder X translation pseudo motor""" + + setpoint = Component(EpicsSignal, ":X_SET", name="sp") + readback = Component(EpicsSignalRO, ":X1", name="rbv") + done = Component(EpicsSignal, ":M-DMOV", name="dmov") + + +class GirderMotorY1(PVPositioner): + """Girder Y translation pseudo motor""" + + setpoint = Component(EpicsSignal, ":Y_SET", name="sp") + readback = Component(EpicsSignalRO, ":Y1", name="rbv") + done = Component(EpicsSignal, ":M-DMOV", name="dmov") + + +class GirderMotorYAW(PVPositioner): + """Girder YAW pseudo motor""" + + setpoint = Component(EpicsSignal, ":YAW_SET", name="sp") + readback = Component(EpicsSignalRO, ":YAW1", name="rbv") + done = Component(EpicsSignal, ":M-DMOV", name="dmov") + + +class GirderMotorROLL(PVPositioner): + """Girder ROLL pseudo motor""" + + setpoint = Component(EpicsSignal, ":ROLL_SET", name="sp") + readback = Component(EpicsSignalRO, ":ROLL1", name="rbv") + done = Component(EpicsSignal, ":M-DMOV", name="dmov") + + +class GirderMotorPITCH(PVPositioner): + """Girder YAW pseudo motor""" + + setpoint = Component(EpicsSignal, ":PITCH_SET", name="sp") + readback = Component(EpicsSignalRO, ":PITCH1", name="rbv") + done = Component(EpicsSignal, ":M-DMOV", name="dmov") + + +class VirtualEpicsSignalRO(EpicsSignalRO): + """This is a test class to create derives signals from one or + multiple original signals... + """ + + def calc(self, val): + return val + + def get(self, *args, **kwargs): + raw = super().get(*args, **kwargs) + return self.calc(raw) + + +class MonoTheta1(VirtualEpicsSignalRO): + """Converts the pusher motor position to theta angle""" + + _mono_a0_enc_scale1 = -1.0 + _mono_a1_lever_length1 = 206.706 + _mono_a2_pusher_offs1 = 6.85858 + _mono_a3_enc_offs1 = -16.9731 + + def calc(self, val): + asin_arg = (val - self._mono_a2_pusher_offs1) / self._mono_a1_lever_length1 + theta1 = ( + self._mono_a0_enc_scale1 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs1 + ) + return theta1 + + +class MonoTheta2(VirtualEpicsSignalRO): + """Converts the pusher motor position to theta angle""" + + _mono_a3_enc_offs2 = -19.7072 + _mono_a2_pusher_offs2 = 5.93905 + _mono_a1_lever_length2 = 206.572 + _mono_a0_enc_scale2 = -1.0 + + def calc(self, val): + asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2 + theta2 = ( + self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2 + ) + return theta2 + + +MONO_THETA2_OFFSETS_FILENAME = ( + "/sls/X12SA/data/gac-x12saop/spec/macros/spec_data/mono_th2_offsets.txt" +) + + +class EnergyKev(VirtualEpicsSignalRO): + """Converts the pusher motor position to energy in keV""" + + _mono_add_offs = True + _mono_a3_enc_offs2 = -19.7072 + _mono_a2_pusher_offs2 = 5.93905 + _mono_a1_lever_length2 = 206.572 + _mono_a0_enc_scale2 = -1.0 + _mono_hce = 12.39852066 + _mono_2d2 = 2 * 5.43102 / sqrt(3) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._th2_offsets = np.loadtxt(MONO_THETA2_OFFSETS_FILENAME) + + def _mono_get_th2_offs(self, energy_keV): + if self._th2_offsets is None: + return 0.0 + + max_offs = np.max(self._th2_offsets[:, 1]) + + if max_offs > 0.2: + raise ValueError( + f"\nThe empirical moth2 corrections are as high as {max_offs} deg\nThis is unreasonable and the corrections will not be used.\n\n***PLEASE INFORM BEAMLINE SCIENTISTS***\n" + ) + + offs = np.interp(energy_keV, self._th2_offsets[:, 0], self._th2_offsets[:, 1]) + # print(offs) + return offs + + def calc(self, val): + _mono_sintheta2_to_Ekev = -self._mono_hce / self._mono_2d2 + asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2 + theta2_deg = ( + self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2 + ) + E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592) + + if self._mono_add_offs: + theta2_deg -= self._mono_get_th2_offs(E_keV) + E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592) + return E_keV + + +class CurrentSum(Signal): + """Adds up four current signals from the parent""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.parent.ch1.subscribe(self._emit_value) + + def _emit_value(self, **kwargs): + timestamp = kwargs.pop("timestamp", time.time()) + self.wait_for_connection() + self._run_subs(sub_type="value", timestamp=timestamp, obj=self) + + def get(self, *args, **kwargs): + # self.parent._cnt.set(1).wait() + self._metadata["timestamp"] = time.time() + total = ( + self.parent.ch1.get() + + self.parent.ch2.get() + + self.parent.ch3.get() + + self.parent.ch4.get() + ) + return total + + +class Bpm4i(Device): + SUB_VALUE = "value" + _default_sub = SUB_VALUE + _cont = Component(EpicsSignal, "CONT", put_complete=True, kind=Kind.omitted) + _cnt = Component(EpicsSignal, "CNT", put_complete=True, kind=Kind.omitted) + ch1 = Component(EpicsSignalRO, "S2", auto_monitor=True, kind=Kind.omitted, name="ch1") + ch2 = Component(EpicsSignalRO, "S3", auto_monitor=True, kind=Kind.omitted, name="ch2") + ch3 = Component(EpicsSignalRO, "S4", auto_monitor=True, kind=Kind.omitted, name="ch3") + ch4 = Component(EpicsSignalRO, "S5", auto_monitor=True, kind=Kind.omitted, name="ch4") + sum = Component(CurrentSum, kind=Kind.hinted, name="sum") + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.sum.name = self.name + # Ensure the scaler counts automatically + self._cont.wait_for_connection() + self._cont.set(1).wait() + self.ch1.subscribe(self._emit_value) + + def _emit_value(self, **kwargs): + timestamp = kwargs.pop("timestamp", time.time()) + self.wait_for_connection() + self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self) + + +if __name__ == "__main__": + dut = Bpm4i("X12SA-OP1-SCALER.", name="bpm4") + dut.wait_for_connection() + print(dut.read()) + print(dut.describe()) diff --git a/pyproject.toml b/pyproject.toml index 4cb571b..a34a0d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ plugin_bec = "csaxs_bec" plugin_scans = "csaxs_bec.scans" [project.entry-points."bec.ipython_client"] -plugin_ipython_client = "csaxs_bec.bec_ipython_client" +plugin_ipython_client_pre = "simulations_bec.bec_ipython_client.startup.pre_startup" +plugin_ipython_client_post = "simulations_bec.bec_ipython_client.startup" [project.entry-points."bec.widgets"] plugin_widgets = "csaxs_bec.bec_widgets" From 4326c5b4a001168721008f43a33983a93a2a9f30 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 19 Apr 2024 15:18:13 +0200 Subject: [PATCH 4/4] ci: add ci file --- .gitlab-ci.yml | 105 ++++++++++++++++++ .../bec_device_config_sastt.yaml | 8 +- 2 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..673a6a7 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,105 @@ +# This file is a template, and might need editing before it works on your project. +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/python/tags/ +image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10 + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + - if: $CI_PIPELINE_SOURCE == "web" + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - if: $CI_COMMIT_BRANCH + +include: + - template: Security/Secret-Detection.gitlab-ci.yml + + +#commands to run in the Docker container before starting each job. +before_script: + - pip install -e .[dev] + +# different stages in the pipeline +stages: + - Formatter + - test # must be called test for security/secret-detection to work + - AdditionalTests + - Deploy + +pylint: + stage: Formatter + script: + - pip install pylint pylint-exit anybadge + - pip install -e .[dev] + - mkdir ./pylint + - pylint ./csaxs_bec --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $? + - PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log) + - anybadge --label=Pylint --file=pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green + - echo "Pylint score is $PYLINT_SCORE" + artifacts: + paths: + - ./pylint/ + expire_in: 1 week + +pylint-check: + stage: Formatter + needs: [] + allow_failure: true + before_script: + - pip install pylint pylint-exit anybadge + - apt-get update + - apt-get install -y bc + script: + # Identify changed Python files + - if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then + TARGET_BRANCH_COMMIT_SHA=$(git rev-parse $CI_MERGE_REQUEST_TARGET_BRANCH_NAME); + CHANGED_FILES=$(git diff --name-only $SOURCE_BRANCH_COMMIT_SHA $TARGET_BRANCH_COMMIT_SHA | grep '\.py$' || true); + else + CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true); + fi + - if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi + + # Run pylint only on changed files + - mkdir ./pylint + - pylint $CHANGED_FILES --output-format=text . | tee ./pylint/pylint_changed_files.log || pylint-exit $? + - PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log) + - echo "Pylint score is $PYLINT_SCORE" + + # Fail the job if the pylint score is below 9 + - if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi + artifacts: + paths: + - ./pylint/ + expire_in: 1 week + +secret_detection: + before_script: + - '' + +config_test: + stage: test + script: + - ophyd_test --config ./csaxs_bec/device_configs/ --output ./config_tests + artifacts: + paths: + - ./config_tests + when: on_failure + expire_in: "30 days" + allow_failure: true + + +pytest: + stage: test + script: + - pip install coverage + - coverage run --source=./csaxs_bec -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests + - coverage report + - coverage xml + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + junit: report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml diff --git a/csaxs_bec/device_configs/bec_device_config_sastt.yaml b/csaxs_bec/device_configs/bec_device_config_sastt.yaml index 0eba5bc..4bf3464 100755 --- a/csaxs_bec/device_configs/bec_device_config_sastt.yaml +++ b/csaxs_bec/device_configs/bec_device_config_sastt.yaml @@ -29,7 +29,7 @@ mokev: softwareTrigger: false mcs: description: Mcs scalar card for transmission readout - deviceClass: csaxs_bec.devices.epics.devices.MCSCsaxs + deviceClass: csaxs_bec.devices.epics.devices.MCScSAXS deviceConfig: prefix: 'X12SA-MCS:' mcs_config: @@ -43,7 +43,7 @@ mcs: softwareTrigger: false eiger9m: description: Eiger9m HPC area detector 9M - deviceClass: csaxs_bec.devices.epics.devices.Eiger9MCsaxs + deviceClass: csaxs_bec.devices.epics.devices.Eiger9McSAXS deviceConfig: prefix: 'X12SA-ES-EIGER9M:' deviceTags: @@ -138,7 +138,7 @@ ddg_fsh: softwareTrigger: false falcon: description: Falcon detector x-ray fluoresence - deviceClass: csaxs_bec.devices.epics.devices.FalconCSAXS + deviceClass: csaxs_bec.devices.epics.devices.FalconcSAXS deviceConfig: prefix: 'X12SA-SITORO:' deviceTags: @@ -150,7 +150,7 @@ falcon: softwareTrigger: false pilatus_2: description: Pilatus2 HPC area detector 300k - deviceClass: csaxs_bec.devices.epics.devices.PilatusCSAXS + deviceClass: csaxs_bec.devices.epics.devices.PilatuscSAXS deviceConfig: prefix: 'X12SA-ES-PILATUS300K:' deviceTags: