From e5244e10c84dfee466e11cd015929646e88bfe0f Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Tue, 3 Oct 2023 14:03:28 +0000 Subject: [PATCH 01/11] Add Eiger Stream2 API --- pyproject.toml | 3 +- src/tickit_devices/eiger/__init__.py | 19 +- .../eiger/data/stream2/end.cbor | 1 + .../eiger/data/stream2/image.cbor | Bin 0 -> 513315 bytes .../eiger/data/stream2/start.cbor | Bin 0 -> 957 bytes src/tickit_devices/eiger/eiger.py | 26 ++- src/tickit_devices/eiger/eiger_adapters.py | 20 +- src/tickit_devices/eiger/eiger_settings.py | 2 + .../eiger/stream/eiger_stream.py | 12 +- .../eiger/stream/eiger_stream_2.py | 161 ++++++++++++++++ src/tickit_devices/eiger/stream/stream2.py | 60 ++++++ .../eiger/stream/stream_config.py | 8 + tests/eiger/test_eiger.py | 10 +- tests/eiger/test_eiger_stream.py | 6 +- tests/eiger/test_eiger_stream_2.py | 176 ++++++++++++++++++ 15 files changed, 468 insertions(+), 36 deletions(-) create mode 100644 src/tickit_devices/eiger/data/stream2/end.cbor create mode 100644 src/tickit_devices/eiger/data/stream2/image.cbor create mode 100644 src/tickit_devices/eiger/data/stream2/start.cbor create mode 100644 src/tickit_devices/eiger/stream/eiger_stream_2.py create mode 100644 src/tickit_devices/eiger/stream/stream2.py create mode 100644 tests/eiger/test_eiger_stream_2.py diff --git a/pyproject.toml b/pyproject.toml index 9add3847..b81c1544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ dependencies = [ "typing_extensions", "softioc", "pydantic>1", - "apischema" + "apischema", + "cbor2", ] dynamic = ["version"] license.file = "LICENSE" diff --git a/src/tickit_devices/eiger/__init__.py b/src/tickit_devices/eiger/__init__.py index cbb21ba1..44f5f3ee 100644 --- a/src/tickit_devices/eiger/__init__.py +++ b/src/tickit_devices/eiger/__init__.py @@ -8,6 +8,7 @@ from tickit_devices.eiger.eiger import EigerDevice from tickit_devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter +from tickit_devices.eiger.stream.stream_config import CBOR_STREAM, LEGACY_STREAM @pydantic.v1.dataclasses.dataclass @@ -16,8 +17,9 @@ class Eiger(ComponentConfig): host: str = "0.0.0.0" port: int = 8081 - zmq_host: str = "127.0.0.1" - zmq_port: int = 9999 + stream_host: str = "127.0.0.1" + stream_legacy_port: int = 9999 + stream_cbor_port: int = 31001 def __call__(self) -> Component: # noqa: D102 logging.getLogger("aiohttp.access").setLevel(logging.WARNING) @@ -31,10 +33,17 @@ def __call__(self) -> Component: # noqa: D102 ), ), AdapterContainer( - EigerZMQAdapter(device), + EigerZMQAdapter(device.streams[LEGACY_STREAM]), ZeroMqPushIo( - self.zmq_host, - self.zmq_port, + self.stream_host, + self.stream_legacy_port, + ), + ), + AdapterContainer( + EigerZMQAdapter(device.streams[CBOR_STREAM]), + ZeroMqPushIo( + self.stream_host, + self.stream_cbor_port, ), ), ] diff --git a/src/tickit_devices/eiger/data/stream2/end.cbor b/src/tickit_devices/eiger/data/stream2/end.cbor new file mode 100644 index 00000000..0530124a --- /dev/null +++ b/src/tickit_devices/eiger/data/stream2/end.cbor @@ -0,0 +1 @@ +ÙÙ÷£dtypecendiseries_id<þpseries_unique_idx01HBV3JPF9T4ZDPADX6EMK6XMZ \ No newline at end of file diff --git a/src/tickit_devices/eiger/data/stream2/image.cbor b/src/tickit_devices/eiger/data/stream2/image.cbor new file mode 100644 index 0000000000000000000000000000000000000000..a5d8cb46d600240a08dcf65d30bcf7bc0c21cd02 GIT binary patch literal 513315 zcmeFa2b5gJ5iZF$o@H14ZH4Zx>aOalx8C~OP3w0Z zwPVAE&D+*&+^~7)hC?@R*tu%+`ZidYA5Huz_Cc1sRx{hD1H z9y_vZ(11aM`wkq?cfjEJ1BZ_qg8z-|H)QaT-U9}W8Zh9Xox9c?y2~Ia9=&LL24>5$&564sK;oG> z(71rs_b?j%$)Y_=qPr5b-ODgv19W3@ckJtAJhq*`H?!rt{PO{pnI*)`Av%7N>IA_I zSDnB?_x6d=W-a|kbq?&Z+LjDrov{>FdPMc61?M?&DJHg$wJ5^ zo0ITDkG>{ZG#1U%5Y4Y6E1CwDnX&CKUD_p5wCHb#gq~!cF7gN?{EG!xNb~JANaJ%J zSy!hoQjJ>1wQ8AdK%m_q=eqcOIVb139_T8I%!}?F&{VA^>+BNM5=aendXj3U6J1p! zozkiie$*J?FJNbBfY06@@CPmUcq04tlFa9-dv(}^2we*-S|rOn7-4%0!ZYDBQ21}= zo%XfQK?yC`sC<8rWoG2KFbSD_301~oWa2mb1QFJ=gU2<)(2mt%eUjwokjQ^nBAdJr z%0iTv0-{HlY)TLxfQr8|gqMoHTZGw!&@9trA41YWc?^(4z!L4-eS;^IkE}mh%nF(( z^}tX&RS$#BL?5j)m8t{q#^Oh9d|+Q#und7U25TL8*6s!DM>Uhv#};EIVaV35b#}1Q zAY(0Hk5Fstvmx>d?OlsEiSXKBa6fPos3$U zGC(~H&{vqf;Xar!Zs}neb;Hon*g67rFW{c5nT&qvV)Vtx392tJ#I}0n@}$LJ#SaD@ z)7TBY5oiyqK6*e#^?~C11Gc-5t4AytGc^hjm1-15id*C*=st@vl@yf$K?`8V1NFm_ z*fpwqn6!JBcdgxNp_rVhAZIyTgFuM~=_Ti87h@L4S*vCN<7}W^Q!_nX>tf8u$jxd# zFpQ&JqpjyM7h@^t*shiW<9ML`q-Jzn;9{)7$ivkdV3;FGjp#VX#n=csj)k8as9iui zP3^*WYkbq>bc-?7Y{6SG!N`7W16mb(&3TKm&4ZP-P_qg*c9dMb+8BU zC<{2ZaC=_kP}pCmCaNw4s6H}xTGUw`Fsb`hhb?K-Usg1kTP@&RS|zey_tfdJssy-` zfPW$6d8?|>*%hiGsGa2DJv?u=%JSLQ9DZfvKA>nqS6M>&JT3b;x#-2|e2EJwTlx5+ zshn#emyp%TYNrX zo87*R&h%vyM1Ed7Kp9g+v$dTMP__fUQ;@B{mkz4b!2s)~v!hhE#)0Zn9r6ixkq=W$ z0lTr5k>yK@Srzi&~EaM?F@p|g$;SDk@GXUeYlybm&co>zZYAF0P# zK%P}%B--Xlt@Xn1Z6Vp=5M$A(MNAWkw8ICmH~L7Fvow<^hNDT*BzCYQ@+>lT zWT=i+sM3;DAEI$MXuJ+IKEO13y^whpa2`#2V^rGfV1Q~5x2h!GSk+_issmsjg+yQD zt+a!W^#d*JGJ?(7nFK@8l5e9Exrq-_d*YZw2X#3``20)RQeGnXoCz`)5(FQ$6A970 z*^2C0F^S*ht0E$LWA22zFP&>GGyi3jF}ZHRmRY6RWBdcT@$G#R>HrH^X^nC`P%YDZ z;GPXQxfnJwVzj7mKO1M@w%!BPJdyt1?|_%>*9oy zJqn+J!ngRze4W~a3xsj-WcGi79fpC0ewmh>>lj2;UK3oVT-PM3OpUfov18~uM+u$U zp~g4mjW~fB@M3NOCVUgXqQ<@^{RcYUS_N2vox@~9mF$jdsX$FLv+BUyDy&Gp6>C)Y zo9}o$mM?ia+7KP?3F?JwCZmNn`XDoieWe7C>%MysEc& zR;R8w!e3YzmW}JNM^!d4%hVj2 zoaW5XxiX_vVwCEP;~}I&hp*xI%=>iSSod->-{P|l$xk}O#r z9YFka)v1xHD}2AM)ghnX*F#lpbo^R1fxf{EIL5a=p0|j6T3p{Mex&LeCK+w4&dx#P zwMtC`65VIa(wS5FWaqthknNMBOS^C;JdN)&qbT*|Rz@>O>c>Dn=cra$I7zrTs?TmW z37%J4eKoYvih&8#mtrcd|ue1)hp6@-k)PDG8u; z1loYsh&bZ=_4xi!Ab0ejhC?kZEAVU(X)ea2s6aUs@Ul4{GP+rH*Rj#6yNNWB4Ii9W zA;i9I1m@Y9z)dp~)^PiSK3r@oXJau^El4cXD?co9a z(vryX-U|{c#mSkP<-M& zB#jRtjlV}z8ncn=!(Ti{KZguVz79?DsKpB;RxqkkRfkZ_S5QpADNNp+h_J3*u1i{A zC~olh5tj30;Dubz&n@lLL_LaZ9Q#GT)`tA6Fd) zW$>W`By+Fkl#YJ*V-r9AYUEXjpI%4E7J`y|tG?NzV zrQ;8xxGA}3sw8nrQH^5R^iT`56IL$x#~y(EZogkL)2t9Yfs@J z7Lbp^F~lZYtFjcQr%VSb*@|C7cwck+f>)Qa=kZ;BeT;0+Gj(3k6t1@vGNkP3Gjou& zm*Pqb$*DTBySLEU?y7}Jiy*^x6(-QnIKyI$Z`zz~F?s3AZr?>`N7#FzQz4McSd_k- zp>VqeWWHtVA6c|0r@N412?iA{%1IWI*%rf4PiNZl;iUp{JP&fbiRD<~ljBAU$ZU&U z=vp+5!!6_-BnW4*4~~EuW+9{$AjD)=&IFxVs7gU*U)7*py39M#3$lfoPO%&0nbBIT z{j}_5>10T;Aan*SZh3dMknFpOL7`U*Ol5D%?`#+FOiWcoDMR(0Kb!GBCNIeGg4%^5`3>S4_KSjJiyaUt1#dJ3qv`1@sjVV z6S#YaaY|Q*da<@a2H^^4-Y z0dcd&PNKzy_7^XB%2WgVzArS@9G}x0BNHccu$qqVk7-x?fV-n7_z_cg^)71w;r z5_|b_(xW^sVrsZf%s`%)JfZaEGir;-0bJJ~h^m;TTBdlPc0H1>kywz+$DNxO6s2tL z-wBp%XO@$ABvW#dkJ`IcT79FeocQpObBVA&rp#6RFz`mh5-ceNNYB!fD@z3Z!C8|u}+R1#y zUZhj)(B5=+&?1&n@dRz%2Q;aokCl1lH~B`z{~r7Pci&;Tw8?8!aTp|blS3Krsm7#B ziiGF$81f5xnr}fbv!J}7T^Bv7bHA}=if>CA>R1Q7Bnng$IN+UB6MRRpPvXuS2LIGT zEo_Bl)GEfeL1W^O0pnPED*CDRvS?&!mOBW}bv*-ioYd z37js~A>a3W0tx+#RozUduJd1$e`8_Fscs+cUO7+sT~K4@zn2mz+{alu z0Y;W^j^kS(<@ZdAH&f~lmI{91*#ILua7b^sPS47vnpw1v@1lN2qwO(qJclV-;(GvufE(b zk(1Q`VB8Ee<1Bjx^SH%e!3@L58GJ!`9nc<7V|98C zSG{6S7DV*VrpS(4lGtz;*-n$7*@|*bchySm_U)Qi-hG7xT6r07vmtxx)c!F1Nho;KgIk zYB0t@7|BG?O^E-Y4TzEFFvNW=*MsopknIwG~)Hnx&`Nx{zxT9Hvr`qv&7w zpl4XTmMwlx4$@FL zRmYd1l%*6a@?|cq1F3PIF&t}>{k@(}x8(|(l)}{ua=jbMe}>c6ybJ6)mw(ykCppAY z7{8{4^cK31?gA@L?NX0s6^T?f|NL9?u5FMP)h6Kjr&6e7bOF`Am+0rZREupGSoB1g zU?Eo%)nY1oeXDG@>I|&>R~i-RQCj{FNmVcWJuNc)Ujjd)ZK^5iP)wV!(&63v}h`&`o>}su2`B>4swZ3Y*Pwr$tfwt*e!VWv8w@+1g4o;^Uk4Gz(>3PR8Me&-5G$Q5Z9COGO52(_?RQwxyaZUf0N`g)G2}lx( z_U>yAqbeiGDC5Nb01rM&RI5a>We+~GQ<`(R8sMEk2UV8lcLJ?6-u<*PS@RTNU0xu1 zU*cOR=MfI8i#4VP?ok!~I4dn?%Jl}K%G3zU6vx&J{E-kXr-m22#T(fY^g%Mq&-rYYN zn}*N>YI4GQ-Hp%QXgoF0W4`3E8pG{qo<33GCYuwRsVLDsx^zOUo=%yY3h%|%@s_t| z>EK{)*ND0(DoT7>X68g3N-yHO%P2B4(`ts3fL7_m&fEh;N=~E_aKzPU&vXfYWMNn) z=RzjeM^+}{H#0LA;{NT{$TB1TGBa~IG;?=knHl6xGZSEm>OLZk1z0n;Kr@d;Rx@%m zV24veaHr`+8JnL2V17=+Ua|n@r@m)DNxOQ!AEJ37vZ4`RoKLJfbo@j$8heT<5<+FV zn(6`bxpftFC#6C$M|tEEib3TQ>mCT`Z8b<|rywQ_PfFKW!NFmAvYiCp({S{McrfK| zg{R~5pwQH+iK1DH+X=*P^Im~`KII9N)bVRnYaQ#QTAQn)ym%-clJtn^Xp72|ToMJ6 zy6!cKDnZl+O?v;VC8wx^9)XG@P}Z>^7765))nRt$iRkWEitc{>RVK}y(wboP^e|*D z(5c7kG^QBBi~FHl;#VBD=+giFIwQH1buQ_?5|Q`%^k>k_1b-$je};=t&(m34bmT)A zZnVPw*Lso*r|+;6(OZ{~gO?d7d89yRxg6Pc)PH^Rloju|)f5@aJ&OMwW@5<7^M>;a z`VmY*btr|tG<>T35F#}N$|~Rvzl*Z;`@g*j|7l4nZx5V&s~kzM@{;mri@>qmIGAX| zO@rP5Ev9sw*KPjF#b}6;nx9#{3^Y@#%}d9pE=Dtq?1raPfqE8bCJ(?%$NMfu2}bte z`d?21&7_Zb>3G{>a8g1Cj2x`m>jYZLfS9uZX==lIiFwrmQKP*yv?EFDQjf};wq8&= zgB8>p1Z8;E-VRz!^G`1=GGiHIAVyA61Hq5vT6+canB|8tMt}}e6y$24nF_96I(}|3 zDrhTdpyQ^pKwUtXV&`uF*33Fz&*xAwkC>pz7`aqU22#b zLHJ}FTId8y9^sobL^4#R@!gPu5N`_){9cPbt8lx{UnkW*GeutTv417I_re^(wGZ%a zvNSTJZ06Lah-6TED%V&@?hq_{dj%jT>fmlgWsk-{q;t4JVktzLFG%1mM1Wv#?u+S_ zO@Ct1^v<`C6`aDIZK2?VwdgtoFTGYj^Ww1;Hb1~~akOb}H9sL-h+Q4%0fM?h9* zayd0xZq)3c!h**puPwvSXSk&YL>ilKk9@?bx}*r=g8TLF*1 z)=dDXSuoB>$r&p&cLG##`xQUoU_CA zUMJ~9tJr4zn}yKdN%&lXlUp6ny!gb5eDA(iGK!~g5|L>+M>3kZuW5cvCnm)x+qOGI z^cX%nKt$yp5nXOY#Jh-VnMdlx&$yT#bt$+k7u_7`K@7DEDVDjTjtxbv+$xm;Tz(&< zRGy3vQnGg+nM!+bJess7Yxo@e_%g#69bfa{t>RRf(ZQ*6WwoFfE< zbx_G~I?^UUe1J}@MZ+#?CG?G2-3TRwtGoxdJ{F&MjE*`rRMmm$F&@k!h#gh;cB@6E zWFqAaJ?hbLD8;OKCAiK~RP~Uua$=@j|17FZHE@}7J;bOoW#7=n?jk>`z%511F=`%8 zUTK@;G}YSFAbXQ`F8cQV7(M!G{QYnKU&vsaor;{(<2wD3oQk+cvOIAOyxgT&!+_2R zkz{EHd-ME%e68S|dg`@|bt3za)~O*})v+G_9pqca3>eAFMT9RuU@XDlf869KR zdZw&AvSz`Y48dFyS;0uz`6(or&N>**FOD@NG=!t?i8H=+~|+f`Hh${!*Dw9=8;WXyU8({bU+!W)?FV6`Rc{?m#>==S%eS=r@$tzbuo-Ad}Z4D-$`5F*DNfDT~F@kdc!q>A%QZpkUIw`X6g zv{RiUeV<}xe8W?HN3sqZ=uI%tJCaqw_GA`uK0H3&KcEThR^u>|=^w%qGED*(==f$X zpS1{0ul5gs#4V2G@Ns4wwUF!>Cb_`PXQ1uhOq(}Dq@agnc?lr8SBLzb=tHbzld9zn zS^mQcW+H`?`-RDtbUJ$nL{u{oLv{LDME^YQ+#f9rCwBa=oEa}EU%D8L!9+*S*LxAf zd{{Gyd~7k;3O0vCx~q~bC$RkrfTl*7mz4J`471Y)Bm3d78mPxX${RH! zRjGhm-~6vSGTs17L0TTu)D1L^=7Qe$fTm|_M$<1XO?OA8;9|lR=g_>3HYyBNnQHjuj&@@&ImSY3X-;~r68%D zPV_f660oM06qOV45_N$~)M|`AOsxjsF@QE{U0yj!wGlpAj?wYisszJLlH@M{`KH>Y ztB?2hNxhe;skVFbR$X1C4hA(QIf$-_4)#s%6D>!x3iswskOp~Iwa>CRS^x`o>iiqc z+UG4v=5uA?#$BpYeYhq%RdJ+(Qg=Xdd-!gwkB{lQEsdN}A)EMu9NUJ<-a0mp!_E2f zH$G_|XmPnPiEQlcbPV&&#EiP6;$vueno}4lsSEzAu8-!QxY)9_PIGA1Iz5>0Ct?7u zDcwsfHU}Za3XJCv!GS8S6Q!Jwd>e%P3$?MyZfd5xlA;8j&2zGa=6ITWe?l{?_7lQb z8S*E1RU7*d*KtE3mGYvSR+BkR;`QCNhId&Sc{(7B*plnUY6)yp z6>XG{_pdE3^PV#t7*EeGid$VHUC5jPse?qKEI+rTHyqRn_7ZF;cW@?~w{9ENmNH*# zlQO-?jI@n&%jMaPq2WNi2d)2EZEOs!d(?fE1!I=QkWh`2;?ymVg_hDLE_S6!4zjmS z;RV4Q@K7DHV@Y+&@>AdEFH2S=C)pY^GgAkv(U+u(gU1y*vqBAmJg1_(d4TBq?wHqQ zkb5!~gBTvVS1nHS&8=j4Z^@aS+!ZbEA#)8-XMj+XJe|l(!GB44jK$^^wGkM{s*M3$ z`y91oj_BZ!m#XK`kZTWK4V6sgQv+M3YDjN>S+Ggz(TIWqjy1M6WoR!Zngno#p+lhL|Zi z#tnf;qRLbcm#IE5jN{q+>=$9CI$5T8`xif@tW|s<*?@- zc8su;q~;zol{;=@iMNd~Q%zk>x&B&I7pb|kB$r_x*@i|si`S}9R0@7gSER+Gc`j7e zKqrn?(@lnY1N`g^-!3ir)&;I&zOQo+YQ|k3Fx0=Z2R?7hS+X7;*!QglIMK&-6(gwu zoJK59e4gYBpj-kl8RR${WU7SP6@sFRm(DtKJXb&;>ycLYkcH;=@s6uHwt0LJ9aqTa zj>YFiITiRo@3WZ0X}*{{kC4h#dMoM3^$7>wc43B{##e4p)SUH8V^5JBTIy^MKJNC0 zl{2qh`$MuZNOhiBHgpNF9ggX|oS?t|)ptZ16YPv;hx-THv)By7Z8trbS7EnTX?%HB;Tmd*Eq|p4|DUT$ga3CRuCNfGq*N$wLL`#Z*x6Zx`aO`x4fo!gd&?fg{6;$eIxYraAbuozB@Bxt};p3QQWJ=XJ=;ag=U_M ztY)M_HZ$`eChkj-Wk${kyiq;@&HPHOXqPdy1EfYboupQI)}MGgESP5?nBPQJFyfXo zGkYPJk0Q%V%C|k2|5FrjEKKk1kMRi7->2@ zyaOss7LS(-3Cb}>eT+0u{a*#zXEmeacb1NYv|yTGr0HXCC2P{Wo9lQtZllx*>xTk| zzN!b|M;4i>YYFOl^2n#~H{Pikb-%F~bBH>V9N0?7QScrlTn5Oy6s{}As2--h1GSCu zg~`HSSx_ddBT$C%OVpo(uor7a*h?1UKq9P;&X%Y;h+y@s#cLhXwk7}c1eK`e-LZ&Ck*?ywkquQClISEy+?Ei_BjBi)<=&`Z&tEFK&>r8^{;6eVT5db1ni$2!l#jDmvkLcgs@Y6{wNK4B z(gH3k+~QXQu#Qfhu6paVDfXAsfr)R{!QWa!|HQ}pDhtoo8nXW{Et=vjE@aLeU}J2R z<-Q{sAU6kn&q2-aadwe6+9~;hOr=Re3o>(znvIo)SHe&N%QyqjX)#Y|9#_~It%3`;0Oc84OQnPqX)bdrVS%`Rsi zz;B-%m0QQftHUZ*cRuA|tB3V*7L&;w0x~O5mMq|xN5zpj+(NR7#87AyGSI!)MGYz($jw0o?LzH!Y=CM85L4)pih&8WUVlYe;V@VyJz^MQ@nO<*kIbj0 znc3+D+iUq#yZWt#4pK%wMzvD1<>G1NV#Sn?WLgTt|zaBaP-xxw44A4FZZw$ca z^|-AW;M3rP7I5#r<}ZqOj3jQU!pBi>*W^Z(sROK@I6y3Cz_rvLrFjrGd>UV z>14=ZG3EhsBB?dww|>27VRzF0d^-6jiL{-fGl^pUTF+4 z)IK{NZwjkaCH}&xIl24t)*rQq>@T_=awO%B3kJ*6D`b>&7Z3DoHKJ4L4!VPvFFwIK zXNA!vv8Ci*P^DZ-$G9Y?ecwIeBUVz}Bh5K2L{a&2QsDF6cAOMXL|%OYdG(OW2uO$0 z{>bIa`$P?VK9M}|;t;01u#BR7mRWtXe2#^D&`AX1I726NtiH||L0te3_zV_QJmwRS zoEBIratkDL-XIp;LU=W=G|X`NceZ|PGI&C?J~2@VI7J#(j#l{Sm!hY-!R zkrj=UC}(DV0@2(OS!PDLnzt|4mBHL~fcT3i986Nk_!rKrw%eref}}iEDg7+(Ybqghlg@NU~Es z+cc`mrQ!V!dNHm2=)4xS*p-|{N3Wtv1<=e_&`f}XL=B4T6f~eKZ;7E@!X&3Bd<>bG zT1MUsrf=-_Q{EN`y{f|$-4eVHB24<7Hy7ux7J=V`2N+qxy=UG86<_fb<97?*NbDah z5GT_lFuaTtyIuvP$-(my^SO(W#KnoQm&tcaMiVHiDw z`)yqd*q_%-RT5fd3~5t#1<>wc46k(VwRG@>=|qfNtfnqiV$8=^+ODIq$0nKE| zd8KoSOUDX~JXBG`ogV=0M>V74JQrgfMjnZ$Wt1fsCQX){myWaTuFqL=2VvxK>L7IQ zRAEeg8LLZ=4iEIr-&1CDloo?}YYh9=kZtZ4^}qk;-y)*#CG#zU3BQ-j_mcU35`5bn z_+B#q-%DmBo3f9{|4)Xo@S~Pt81opyy4E0bOfP4UO*xmyozxL!*=G3}MSf|{Ie@G7 zXLtqPY7M8@(R`h>1MbO5?ASz66suikj)0$ZW@MR>3X~jc6PNP|omj}RwpNI>Jpm=R z$1Lz2U)sB(IT50{D6*oFa+fR`ITZvt`xAaQ5kM@j<%0ZyXG4$^A_sjeyegZ!8LGiX8#Bi{cHa{OdQfJOojj*)RTy&=rP_(?J`CyK+FuXV~htlqjtkNVm zhh>u0ne$M7s5+GLLnM=lKA9Zi%H$TvAj1)>`X7)fcZ%0-$k~PBn$mu$Bx$293WCG`N_s#9C-F*AVMmt*><^r9$ zMRkW@S3ec@NU~b2O*PIe0y7zqmN?Lpt`7is=B4 z<1tsv6M1?f*&A(d$XTH7)V=Xpl`M(*9EOlW{U)r$Z3e!CpklFvPPgY9`(c1;2&k|U z{#irMJp9zopK+^+;xNf?i$4dEroMvr!nL3|M0qP50J>I(jVOvCJsrIFi~q7jlyi^q zFxi|CBuLoia6W zPX+8{YOW3rM9yOszcKNh7{d=-%Koc00dTyOz3pO@K|-dG$Ri-g1Vy|Odd*^RKZ!0F zIZ{!nd`UJzN00MVjGKDQ^h~|*1q;VY>jjd=p{jDA?gX$Yg5f3UX^X)m4aCSPY9KHs z>+BLW8Ts4!#x&IKz$>N4EF6xDq3Gs+GX_0jM(}6R!e)YzbnF4H$VeD(u#(eao4^Fk3WZYyllvwFMYEfOeAF zf$zLEdr!7>%rD%V$LrJr6dk>Wff${pO^!z{ln-5RyXW4G;SZHRiEmRE$s@2%mqH`KDJPcXvK;hAlHM~ zHPm(0iIJ)+RkW#jn}TT;nl&vpfx51VBuEB=KC*qqcsQ zXzri#HT^5{YE7TmyDRRMyA@Y%!u01SRXVR+cc9DW^B_e zJz$M}owizMcX1P~PS8}PPmJ9J{5;k* zQjt53<4&K%DWzVRDLE9)hKQodloXr!ALjZ!ot}-#AlSAr_ur#eiSgJ9&xG14^NChe z&RkP*vosesqtX@K0GYRsG)cWTM3O=m%O!F;lMV5LKpnS*G@JT1s`g zk7>#Z<>zbKkn$IKCRDh>g)~k}6PLpJ6yaY@6ET~`()^a0{=+AM*QC4(y!k6~N5iQAhvEov3dbzEX&eHFV#<)+zXMB(&Xzu# zFC|SGO$u3Fsxt#P=VCN=vP*$D0Xvy54PdI}jQ2+F4Cpd&hSNOFl5+u1XY(Vb@iO=9r9XE$+I;m6L*8H6)`5E5I zt5XbGh{gW{iV4)gHjaDJr43Q*>&Y34Bh1&AkAckB6)q-v<3hr;s%nGJ zW;)eXHNzPWuoPUO)>UmhQZ3lnSV>YMe5#JW!-g1&E9c3t&}KS%fMn5n~8Ux%iq%8M|p`QB$;J$k8RNmAwL z5&fxp|Cb;&#;*ns@{9}%nk7}HFf?5qa=?9)&K|2ap}%jZFv)GBsp9h(oq5h5)C*%) zu`{@NZJ6Y4ZgX^Y0|(~XfV_TOzkEDEy#c;|Sh(^sP}sLRLF3PI;rCP5h(nkF}|f1 ze)yJtDqoUK&1+eb91qvQ>1qmSUZz^>crU)&ri;irIx&fFBfI!i^@-(zuSunbuIc$= zq8p8Pf9MXtB!@6d$7NKeAIq59 zZBc+=ppS34#pS^5NRWDf8i|pncNcjN-W7DerH+#l#(}!|XzLcJOFM?iJ5{p;FZs2V z$P&sNg_8B2iF1#IW0GX1NT)R~Nw-)G*1$}Vw1zv%Si3h0oK`~efl7l%uS&18fSlzp z52H7sT}0Nji^u}sl=1gcCf1k@$r6k_7&+8Ph3Xt8FHkMgJYQGFP+vQCgj$K?xKCae zTGCivt3ldf9L+cy*gs?=;HBzZi^0?1R=bvU@H4w*ihV8{xjY{SR zgGCc3r8e%qAZB=?CCITGfsUJiSUHqNjW}KhS-<1>UBV})f-r8O-JnF7?YItVo8lHA zvmrF|`ZW$ZIYLTzMf;z#vUkoZv?0q08bsDU3bxI%KZoi#w+@=xRlY6<+C`^Fs4lpm z$y=-$9wNiPSy1*@#duT}P3LMCQjEs59AxbS_mbkto{ZRy2?Pn3p?Q{T7_Wim4_9mG zIG@uKe&-RviuVxXGlK$ddq1{VN{V>DN^pv$qYd;BmQ!Kj3X=al1w486kFs zA~?m(+>tJ%7!>NfO^~&p5%Ro)sTBJ{9q)^yvX4up*ckFgilefdg=BxnW=NbdQ80b6 zPW+tRfq_^!9erwf*^;-n@L01MKvBGGAstBwZsxw`@fMvL5?hOZGaRzH1)qa)57^Tq zBk>BS?Q0Xa>V$C!4g#KO9<={u@U;~;d;N8>cG~j&Py7DyG>V;=^pIhotc!-{M7{xVmh);t58N8yMDpGGs3s;Y_xDJBiHwfs*DmYQ*z%**%%-th@FUKI`(CqjrbAX8PVnG8hm@~ zzW1p48&zql$ax*EFC59a09VLLjW2Q?XZ(UTsryN6cf;|z0H0Ig$hGn;nDrKq6P^#m zdFB{(U>!F0om<4q36Bz|TkF`t{1)jLSbxf`rq+PI*BNr}I-FFz4W3<<3J=9-Z!Usd z5%Hm8C+Ip=b=Qf}s=Mi9(Gw_x^Ya5V`8?Keg5{J0s7HesbAJ}anIN|Y{Ni@BPE@La zkdb*WMy?-m<;@-bnJbf1Ad^e^PBDrysj$pUquF*A#BsG++d6$XIj*(rej@QZwo2zg zFgH;MYqo7RL+{jhZ^GR3HJl0Mz{_**VvuGU=SES^!>pW{nX4e1pG9`kh)d7R+yKoy z6j^4(m1kydgJymaS!TqApF_*}9^8aKtI7g?<3(u~WSM4{WY*K_rW%ktu@HS%#Gzz2 zvxFXmgkFlQgv51bW**1feIv5WB;8%*8EED=k!404cGk>`(9DOCWkwuzF3Vwpa*@nD zYgka0gL!!eim8sKV&Vn7aE{S$v10zFX6naQqf0G1v|&r6<6+)M@ik|=b#}hY2T$40 zAen!&WC$Q?)zA;Jo5_@McpaFpg;nL9rbadjj*)IUiB=dRcFzEw`*eIr>1xEHzEuae7@V#`r>(bE{boA$xo5z6m zdd=h^uQ8d9G)4|ZvPhuBCGgTAaU#Z$ibrq7&QHc(Aq* zS*L-Ub?Pnb_M}B+%7y^Obc??p_$JfVtEq=A1}DXg!bp=+BMyH;XD&tJjb{zrXMva) z!_#<(`XWG@glsP{lH$u4Q!#Qm?l%HuPG&V)MmM{38~~xL<8AUxpqZ3?FC7w7Vmjty z`=ork2pz}=+0PY~pui-}qT801a>Q0_RqM4e-| zf5zAZqR`777>5Gw6h(J7-j1QCSpiQqyRM1;6_X-!oP{#I(+d(w$sW}{&EO~tIHd)4 z;Z!_f(^d1gj3$xC`&#VZ7KtP5vKueWS=-tZx7CG|&6)Zi5Wlt0G1}ll%3eLIXew8^ zkg|18Et<;3E~M<_WT1;v^jsHGHun6Z`r6A<(!=>OsH9!c+ot-=Da~x46DS4HO-i2k zIjmH#;t3>Me&3?yC<)?B;gEi`-*-rGt}`WT#Xz0$b*Z)j?#}n`_1xsjP`3Vb(G(7J zA;kz#&*kDY-P?r}o6w?YD!aIlVj`&3adA|3a3RHF)GM0G5(~+9<2eIjdJY#yWHT2r zXF<#}Q9`W!nrUe53CCV?_C%iNMeRgEQrKjrG<7teD%z#E7|bkFVkuvEXk*iSv??En z`tMJ9Rj{}4SV0dZqIe}t(phGuv$yb^vnIkUNtr1QCcCCSs(O-#Sj?2`Y($l*X?F54 zQ?9!cRi>oa7c=ELTv268%8fBouFDryriQrF&viPZ%2Yp>DY@RDuzOUQ>h3b-I^R)c zs-w%4T=&p3<)|{%)@8~)ghZ7oDXGW1pL-07DpO6YruaZ>4q|0GI}phd0oUQv_@qi- zJ=1ocg}%>OSAI`kDkL1#;1J)vliL64ca z6q>m+vdqWCC3nqx=X9iCG*E^MFpsY0=KTerNa9`GYY5t^nz zyU$~+!zC7tC44)k%af6nu$*C-nS4cpXr5o>?83}E1kJo0Snv z_blbhEe_{eyaB4dP}?fe_$PKEI&IV(=t2v`1m)@+jSGL+tab#QYjO6_LEu9W^!I4$ zd^RFA{KcIj=IE5EBh?YA8piL#M?%k}CvPs;l~yJ*S5b2*$Ys8cZQ`?!ThMwynhu~| zik@&WhGOJ&)QAn#^*}QzW?soW0pFQdNMMa{+mM&1Ub7E=DCru2+@7kgBj=I<9sx7J`l~D2W-U z6M<&>410xgiN&a(?YoUm_2rs*(#Ju1$@vNdhHqLhZV;0$iw#jt*=21E+B$O40m*Rh z*7WzW+;4r;@NBy`v&1)pzukEANjYa!d6;s}*7-0_wZv8AH|2cRS?%+%c3ZI7g*)FV#IY`~S)Btcxm@}U zSPF96>FiRjxA+r?^cEIrJN%03j`~{n{m0_6WW|;=D_XLhT}Ux4jf$46)RSN;#nQx! zrn0rAlGE9jV@p3oEjNt@D$4ML90D}Ar_}X~tScq!Re03&GmFZBlXcL?k%h&gYQNwc zSkuGlX*ttF^%%{r6vgy3DSh$FLQ@#UNC+8*)mG_y9b*g)MU|zc!qUFJKvY?pXIbJv zt?P|Mm8n^lDc%@eKd76IUs3C37~ww2G_D^MRXL#?Lhg%|?|NcUWoo3$l4cc<&?B41D=pujCI)2aW9bQbZn zS^TbB998kRvEpZ@T*o}BOi8g*X39MgM3pHiKf_G9$C0QqRo6~GJ`}hIq8D^5RE@Ce zx8j)b0zR7|h~lfEJB2?wJC|}ywEf(^5x&m^}Ls3-cHp>rRY&fT}`%-J!^# zp7Wxl9&k-<)24oT`wTbsYS=#3j{ut_2Xfn)Ciwn&fRl|F?*R@|mQs#|9**(VGz^|6ec${YYR7?RaB$&_k{a<{Pu|CH}O^I(Yl}A>4T(7@UziB z{#OgBBa*w5AJFK+w2&$#F+Wz?BjEGZ$SO_Ta9*5GLa=W!GmUim3Z!aOv2*v!oWJC2 zgM6l4oIC~Y(7}_cKIGG0)rU6b0Syf|j#2YW*;9(_J>Z)HlB!ujQ{ZI~_z4qutj?sk zyXi9c6vqN;ZG*V)!F^LpA6KQ4*lghx$~&AC+xX7@ynaDds#i%cBXnXWS8;Zq=kLic zg&!w(*VzLpwb^Gjf7dwNWmR+xu~pvxvDiHB&}g3UBz4f4Gsmd6N;B}&M`tpaqgC8l z=0%-afL`4J3g_;QO)wpY*0jx@KyK(s_)#<39rjyHmy#2ZafGi`^yeq*#1ix)LKZ1w?`BJ#?!h~(WHVRj)NhbAupe4z=lhq%o7WC_y{kj+tuw1dB2IEU0_*oo z)mEq7Zfhfbuz#_zoODnJFr{1*>{nRAU-A<6rdRyl#b|($rU}d|Kr@L>UU_}yVx%y# zt4e{f=YaNJ&FJ{R#b}L@y}3B^9-x^-CodiExEN&^If&nFJq)y0YDULvE=FgJ9D(A{ zfw~`PCd0`~$BPz&H`HDjIZpKgMq?eA#ukSF_T-uoB+rak$pbKQvKj!eTL5bkoV)}* zZVBRY!ox9grW&Ht)8GuE-xDBrs+En{hSlxe@E-KsAX@UV83vF(zT; z67+`()K7qROU>kRtBWxmBUhn^E#)|cNt5H`rQ-${V-7|h$W4z<1KQ;^qeD_DSssfp za;sVdI!*+d$#L@1aj~U?FAWl{W9?OI%7H`EG@zI&YPSOU38txqZ)4ae^Kd;z9)+4$ z`JJXlo8VcX$!ya+iMN1`Eb@0ziCbrean7Ig@Os5Z(8-p;>2^cT=Qhhfs;37ek=Ry}CCR5YNrSRUDziW|a6mHZj0ZBfKA+?82jX}cV?${wdws%@McGhHr zUX%l@eYAhGh2#_x*{`W-7tuISrzfkP&{jF>$OqVVB!$G+Wb+D(&E(2Ho^K6Zdvce% zkg~0lWfzfB21P#6#n*CC9qh=DtX0$Gv#2%JJh7=Q$Dq4^oE#$KPakzT3!QcjTR6bYH!z$`u z{tIlzZ=$jC_SnQmtoAmAzw}6^>R!7BGPstiW}&6uFkbW$)TXtR!2 zapSbFe8h;+nPrVmXPrSOTvK0+QVfUXAOg({v}MT>o$I zbI%NW-4e|4KiB@%lxyPRyL9(3*)qjtRK@U4Y#v`kS8JQhbxvLbUWZaz*6}_eX1ASA z^~d{>D(*d=-#JMfQ+$H2vNW>bU9%fi!AsEz4o2J+8xvpelW-6xjr zTK%Z1R4PjH)R*&V8D{<0NQ+Gc+?Q)5l=?N@)KRAF{x_YTR{A{kUDqS3a%$)@knf;VD>+g^sq2*da3@wug-7|}Br7P+uXcT|ND2y@kmZS= z1eWTc4?hoZBMHP@N1u4!vQY1m$a8lMCJ`hr9i=m8qb#^-bdzs3Pa7YvJPIS>hggRE zY5k1viiMWVmyLvRL%ZqhDAmn$j;6*(@rRk2-8lLDII_%0{cvXHBxvU1$TG9fnmGfSxiYfMh{Mn4 zcx0LB;!d->pqZy5%S;ECnV&;5FGiM`eGYJsLNl+SZ&1SR8Qm=p) zc2M4jUj9;i)8I$#`s0#wC+Nfl%6;TOXN9Kz^Dzct!1*vvf5Y zm%u@n&L#F)P+`6gX1ds>8+v@e$h2m3kE;#^K4Xp!CWVpL+`e|F%iPE&a#C>HE z`I4d$*h+J*Y5PuiG|_|_GVzM8|Sul*BKZ2HTl!gqZmFme{BXP(zKxY5!xNXMoi?n=pv zRG`5#iGObCVzrI~UGq^m1?h%u2V7Y@wccf+n8nE;Zz<w74uGWDr`(l&LoLscsuga5FI{LXYPsM@IJg2AKP#?H1vE7KXg5H%46N(T2{XkB4p znb{?v@L;apb3FdQPim&m3oQm$zgmTnyHpRIrCgg@WiI{50DU@2 ztw$#6L=_&@d8Wh}gvLgPS=$P-j>o7#Z3pg&eCKzd591_D(L%e|Bj=-imeW8fU%w6j z@6w5<3dkFe07V@i^6xD2tU?p84v=&kS^M0oT>@BW5%QO|wa)`#qs<3;j>kySSU&t) zdm5!W2t$ftsLZildn(shwi!|kL`Bh(lqYEnS&HdmOvHqushn@AZN>Et=e^mRyDui_t#^S$oxIEMx`UoDV{3 z_Egm>)sQ@arS+?f`%@kMLv5_@&sMp|g7I$Ow{>c~b)wuAw9F^HCoR-+qDu@1C3MkB zY_Br|&~E^}2C#vcGLTfwjA9)8YtJ8$a&9b2u_hGDFK(rGvm~?MF%z?94XVb1LO}M> zsWt3woDFiXK^$UtET0DAOaJ)L;59a`X;dl63q2kJOp56B%3&t~E z3=1Wu5Z_}oBJO1%(qwuMFOaFPgtY|r*kd&YXB9nZC|0BrP-n!P<&oTt+V4yCh<~;| zFVMWoTc0Yl!iZigbJEv(tI)mH6ynF!xHgLMB4SC3_{}M_!5wsV5PRx0pD6pEN_o(~ zrz@*op7kC+Y~?h2Upq{4YNP|rXCcb(%|{D;Z^sU>n4HJpn&PNRUn&VQQ?4@*Ri-9d zP4Tu@;EvQq{m$%xsc zP08&Bz4DGKQ&LisnR314s4|tb(~qxIT-Q9NgRxw%os2)d#(tf4Y5v}`P46Qs*FBG_ ztO6^mjpWYC^-}^Oxml;);u`xug6+8(vKj*0le>C)RHx_0VTa;eNZ@f(NFWUVh=0`~ zsT%W7>Hp~cHGa@A3?;>a4|gRx(;M$F5M{vGqYYvWCjXoS=`DNowxya|(#t_%RCO5R zE1Z>-ND3&EALY@koV1@OAvmXmC@S_KD;^HZ$QgySLnp_)fBL_&$3$x1jcrZNC(O)F z%$8I6{xXW1k&_8Ca}<%rI=Pg+2FgCqfu(CrnM?pq6F5}IPu?fFVhg1R91Ob@cqA4c z=Mwe-2>WZ%gvmLL^94rhppvs&>g#wrE}qpILNqmJ^2G^!5rj})x=uu=JWP=;{j;%= zstcGq7Q~**#CkI;LcZna80olX0O?pAa*fu{L6FG>@+MS-@}j4_6c9baWK+5n6p?wU z_`4PM1ghwNP>LfDE%|B@g#3khS?^QXUo8q}+SJ3ycKq76uTGoFa`PaVO{i2BKoOdjb6s6Xli8$1X+-jO@WZwPX=`#UrIbm=5Vl_XN;>RWmx?bm>TA z6akq zeHM$ak$OSu7>jlkMpmj(m>}YDdC9rYlEWAo(6K;efMMzl)F|ENP8VY;=vc0%0z({P zFC90#7_&gfTC~9p)Y(A0re@Pb?$eo<`53vGiz=TCv`ZMnOUGp{9ZNBCyIKl5jt82_ z$nh?l3oHf~T3v&YhpRQ{zoo;N`b(k}-DfTIZ3ySg=F1suCcSBxy{Y+@Z|Z;l&%c+< z_mkjT=0NzpWWJZo_mkk;=D`1Nk{QXS>>bL?NQSZSow6{Dc?@A)YfQO}fEZ*-4~gPQ ze>?J;EKeNC5C%NQ#0sZVim)@{2Ge+I-0!H zDd2@~XLsrNscIRNvqLR2^=-ed?@w{~!E94tS1EQX%>8ph}I?@gvwP_%n_P z+yFMwt}pEQvjGpX3ygfe4r|H zb_F_eRH+%D*7r;#lowpgXG=izy|IJeSc3SKK|72bz!il}J(qbpwGm;# zDs-WUui^M$!YBPVEFx3Y5mXIBc|tTe0=CI9@KW`X#o)6=ef=0hzc}A_3X=(%0-pl( z8=hO!eB_A4nVfzYIT5cc19dCNG07-ia(-dS;b5se>^6m>+T)aN-V8!e^yYtrkEn+% zAitqDB^+AnxM^E+1t>GMw7ir_V0$Iar}jEAKs8455+HPk?--~T!S9FY{5%t?QpPFHK;LL#ReZzLF&Vt!cG1_z))(ic`>=TZDK1uX3oEs-R6Nk($Y7p zaNl2@12y+xwdpx=hy~>5x3cpuap2ht z+XKsOEElB6zvis{o8C#TOvQ5aDO#qI^U73;A?aGQOov-4XK?M21lJzvf<40IH|0Ag zm}$D-j&BsMk-{VV@?!$zxQJ`Al!C&wRBxy$_OT??Gz@>jbe`;h<+Rq6*j<}^HO`X>ttjqq5Vn1gj*#ib_ljdnWf_)yi6 zX6iTixySpELP2wAx6c`4L?c_o*@>spkkkNdZf+{kvmkn>fo_&xk?IuX*;9P9acgovi<-X6g0(pbS>i9EU2Jjlly@pMl zy zfcx49^9KvVuboZ+eHW_LrRiO?)N*H6Vw8N)wH~m)TPzMvpN5l~=|LRD*(BWH7c4NT9l zXe{yial+aVbRgc zt=vQ0d<3DHb^%fIGmuNcrBRhJBVRx*3TxG@`l$7T*l6eT80uC{tf()cnizBBjbMj$ z?D%rpbGlTAY-v7$P)xRr@7ADT(<$d(&|#8mTfGY+%xw1(@fS-2hvEZ_Ea64|7N{`2 zP`p%#N6i=wFtUScfW`ST&_3Z+=cVH_OUFW*;mID=VHZ^z7u~Q-ktWah?LwPWl|3zE;VLaq3y=^FYFXSxinT=?MbMRZp;Y z7icprvs&oP4gMlS&sZQ<>;Md(jK1Q5x*3qhtMN+faf`v?mr!9>9gO5#ts#J|KvD&D zYp#LB9<+c=)L4w3tHxsVWgyDmX~gH%+-;T)K8{XD{f1Sl zG|MygH2|7=99~jxurTZtK`|yPqYfZSxyitrpy?}hkj_-$yzB|I>{Ie!5r%F>$S_bR z0Pez?Eyas1MSOv=0wWJq<0^3}5G>@*8dHF}%yuF13hF$I$5}SLbi5qVq|RK{Z#2h` ze~CX~3J>&8{w5YD{~N#d@p_iEB|Oa!0?2XdAn)6qVlQ!Ihg?LJPC zN%BN%pDXYKi#DrpSKp8WtbLBi;R0B=$*(M$!W|Zn5Bjp_FD{x!dA7!oVgl$qUz{A* zxl|^hUa18}khPcOatp~N|HTyKtH+e;gz2i>8h_0cmMO;}BA*~s>X}KmSbUa(ScdYV z1uIXhxCBmTOzjbzZfCj+nL_nbAPsZ3LMb>?JXD5pseUTJaN|kjXO&q>-jrQA*R3VRTOhM=|Ojv$-Vkr7~$m=5l_%Pp)^nM2Z2bljFPg zM7FY!oB_OaRf2A(59QJ*bWtOFf%k6u0t?0bu7?PZQtM6U^Wt`?gf&9tG+XmV2)9Vh z(npbh`bVF{#QHq%Sf8jJ?eZmKS6LbF-Pis_m8B(?rCEg*c#4iKg_}~v4pu$~jKT(s zd^(qFJ-%OYEig$bspFm5HR^;-Eic)m2X=}jlOr##5hm28m8+G?bmcBF9SeY)dVnQ= zgUNMGF#$K%@orqdw0!}pugB{!OD_9`VvV`(80{B>=v52#*=j-mfzN9p+{GSZLve3- zk-Jv-Vx0*UT`g?|r5EFK5-taQcKtd_DR1V+2shEOj%+9?2Q&$!S@@()gl2$GRiX5W z-9yIskmi|E9=b0X=?*+@+fO0qPrgwVJ%PGa=>`L^L1hqx2IhQ=IRv~2yYR-fflad z7G?!nPy*!5_5n;x0Bf^5_9g?zV?IDhS$m!wRmb&>;xyW7<+G3D{eH9fUr9KJ`TPWN zy!qzM+!|3VqP0ek*2(k@U><`tr}KYn58P{(9*+MVfhm1vGy_g>h{WM#W_IJ8^yA1f zGuLW{AJCl$&0M6m=onQrHaSh3v5tHX)V{PRERr)Ik}D%ClF61Cp8DrPB-b-DPwK>C zh=M%s-LWU}*%l)5^)%pUF%IRqpq|{JM{!>nVMW7wz8s>tJF3U$fB`mBs3PqNh@cRab(;Z>6p38Jq9u| zHPRmOh$H;O%7epM2U8~0e;*5cRxmul!mwD)+ch$(Q538A-ptJNSUw*_cKL|U&6gOj zU}}7-7V7LlxM6{+b!H=9wNRcXMN==T!BvY~_VS7}DFeNA!W8MCKyiPVkG}Z43246H zrCj{;3|y|`JJfQFRiLz$BA0F{X#31Jk*?rw~}j;D@0-lOT4o4+2>#oX(Lm zXp+-G*E>`Og-CLjlql?hR1!g<91nyOYjGYvMR2dHDItLU9T} zLjY)ggJ_c7NHAVmTcP{_jVlZRn7&(vcl`jL@T}YeHxM}m=njx~6GGmhgvlNHWK%Eb0 zQ#H~{lw2w^#te*H&Gm!M0orvnn?BcD#u#HRMsCEr+(4ZQG*tDhiIT3c7`#juW8^lq zxE@p-G@`x7#6D{@fiAL8OwKBhvrDZ4hPXCfvBkOf4$j2n zRkam#9FG_R@lqY0ph|SiyvjMxx2jIE49!{%$ozIp-PFmt)IKbHi~tty%j=W!M&v05TPedK9zjHBPzUeCPj>)@*+yC6t&-+2zL+P2~YD zm9pQvbwg@XglDRSxg*-C{{$ZfZ5qo39-Q=aM!7qGENCJmEpTq|A1n=v0 z_8v}l90Os_L9%3k9rM3wUo#{zFDzCu4V{V>>kumgh7?Od?=Op6@_k)Mu@$X~ zrn0*WDW-$!J`_h~+JzKLLPrdOto_#1)`iR&7C@1gRUDNqEF?#Za|Q;lCyOSsiHn#s zH{_BQOJhAkWaIM!9q;qTr%LSr>rp$8kecL8>)w5hU=;5Oh^3WQ>wEV#!%<~PuE_T8 z>j*@ZB`G_`ac|cZi7Hdm?bMq?+k@)_Rh1HL9Jg3!PSTS+s#91j6&rXL)KKIk54@t^bW+c4Uf^fc+d+v#( ztdVnXd3uq&7U$pgxk;$SK(5sz)b ze!B&G_ZH8-y-&W^nUKm=k(J6Mck%rg^YzBaG9#66lYr_Ag~;_ii{*NV<-y2`MGkDtj5O$oqP38N8b<)`hGw2ot#quXYK6^+ z!T=@sq=aL-NTlaOC7#VGZ0Rm&>8BD!`N;8zmHimx^Lk`wt{jUvC&V<>*w_p`m7w3v zXk1gEW?$s`R;y*cgDcTl6J$OT5;{w*Yn3>H&^92jjt_4Jk0gdLWv^kY`CP4SojsgJ z=i+1e3FwQHU1urdbJE)&=nraJBh2LNIjSV!+s0}5Pt51HY5$^c_kTkuadAfc0Mz}J z5nCtDQLWRQH!!_ch`TL~yzHb=&{Q3~qgv`}(_-gw_^?w!C&kXDdWbZEw`w@~xR}6p z{S~MT;0f0taiySb%Iszs+A&PlC-Hm+(fwQ1(P(HzFC8<$qB5$}TBer!L>7j0hDz~~ zgF8NSuMWBV$cG^7@4(EsnBG!3|F9A(?*J0}g~^sg;yWO*nir&R!iIme2>hnAE=IOx ze%=5{U#R*zj_yrTRIe+zr9wg80`lUGF>Yg^bu>+l@Lc^vRhHsRvV@Me$J2lFUX&>J zJ&FlGw)AppNOREJogcD3i@#vH8G6%+-g7b9U}Qg3GYiz?KzpNRa(mNaRFE5>F>(l( z-8@+b7Z=nxpiBWTL9bXat{Ks3JLPeO$%hXX1wv4&Z#JK0q0- z`hdyX!S0jFrgKpswhMkd&_V7dX&5B*o0Dqr$w_=v@0O^yW00bH#7 zY9O1WF0bJ3cQG;;xd1CNP!|L3_L^z(PK&{soPv?2R*vcZJ66Z$u*n|>SZ_A|Yt7Eg)J7?air5NWOeYwC{Bo~nHmiACePwT&PM&EFx|oj^NPZPV4q`&Y~9 zOi}g!Veh@8s#q*2zQoHGgtA(2E5#)x6TWRr}+Srr-S5-7gtT=MhnUH3&hCJ0c1+2j^Z}R$3nMfL(5xJlrKaKqo~y>)&!l>=NqRQQQd z>Bq>!tbN~`yfTN)suI`Hs%p7vOCLin1ZkVpLXg(fA*~?^f>MWoA;qQ8o}@bMx3yIA zMq(|fG&LiuCQ_aOaqN@08QIDuenzZ@@2I9RlUN6rGm~9TXI(U9$2ySj=?aOK%*Q57 zpRT@zF2Umr@;(!(5Gj{0-iXzBoZ4uLgpo9Tt@F%|0(9YPu5~<=xLx24*lfr!4V+R* zFFLF6*6EW1oZIWWo)AwfNRHxA=94){oF7vB+BagsQ$4vX{vFB7$i0G+FwfYD%YBB$ z{iSqdo-omNi5ienpNA4`Z{O^=!@}}zz&O9gy3mF9WxLZmzvjSPjI(jFU)!NlpP`B+ zc;^dk*X@O>)LpHhn5in8esg%Vcjj$WZP2#e?!|)lnAJDNDee__;tm0K|f{|}*mq)e9 zPa~JN8OR3#xvvlQL`y1rIX57=K^Hz{H*yl(#|?lSjm6#119^l+% zSOqa(Y-$g}qupB7y})JW8|%mf5#3s|Ni4k(+po3X)E;Iv!$&sBhomsp8#{iqX%#@P#1_aaMrR>K7>DJq+2rko1dT&9|JEA~(= zbL79M&R}z$TCZB_be@mWsn;?^g3n>Lv}jqED*JaHMM&iOH2tl!vF*4vSUpU#dSHir z0R(p=E-s>Edk-PejYXknVG%iODp2q}2&|b$MDaum%QCtYGP)yJ8HrWktcB4!HIofZ zO9;!{%g~YCmlZsF|D`LIpFu1S1uGVbOIgRaK^l(*%S>-qGrxdlo(YzjZk8FYEwWH| z-%l-^fXCOHQ7+t6=KHdB@Pk(}dle8jgmVAy>Z zkFQu!^G56g$mk!cvo0ik1fVfG&zpqrr4t+9dEW2l#YvRMSs$N)(*Lm38|(bV_z~TDDPcwZky&)~NDAGFZR;bw30~E*R*pg@)-xfH%>tb%+{wWCjHxuZ+ zVEvC3)_86(AEQn(_;?>e`x_&86Ab=g5o%~kDvUI>ftu)aS5(-Hl(-NtIbT{ZzR+w6 zM!NGM{>y;L9CTLAiq>ef9sLUqq@v`j4=df+#E_-3@c&0urB3y@4B zq8s|f*#}F_n0D2v8r7z0vc>uC4QKq2<(Q-Rv4i65Jk_Pxnx^?n!2bjrv09r$NHxTArn0U{tKeND`7$W@r0K{FQ`sobXMzw&i zu26)RSv+>ID}i!|nxG2{xtCE5c;8zM*2VE^FqIyu<)jvGI?RO@ly8)~>AYdJG2l$X zlW~@fbkWVhYVOncYu5U+f_muGPCS{Lf^K7+Oyb_l~Bm2f%s%^9(9D{XYKpK`lx5N;n`7|lJPQjjr< zP2fjfNAkW!=9b4}F>;<7i;+4ZcCk)xR%sZSE_}l~5Be&la4hzini`AEf)FRwrE1v@ zwvg=iW`Nw)%APk;jU&3NG&Yt}fxI`C=eU3wU8wJ_s1kupguI96Slraw5tLKcE<2FZ z`I~^$ZXAftYL{6{4rM=3dL+_R&rsPnQXNYF3`Bcx<7ErJlI(+VT`Or*X-Kt6Cwt(J znrz3eWqR`o&xof)SF z!jkkH$y>1Ym>k}MpwfAjFH_wJDActnCF+VE>P%^8Ma4|HQFf?IwQ`v3Frt99D)XrA>Ct&!w`G$)WumJi*yV^(=BY zN|&dl)1oaE6!ZV9K+;6Ar>-*~tJP|#jtV%|t>tRN{8FU?$Uj*~zRUkUHpl1j$#{q& zUu2o#E5%xf;CXp^)h*n3nD0&>JR}v$F z-Cp!R+|t+|$-NYx(OnB=-KTOo>dT1*{ejjNXr(ts6lM9^_r`-PfvmI}LEyu|DoyHN zbJo`#kk1p$Oj;LyqMB*s%~9G{PY8-0z`T@*-S_zA1}h^zE58RadRFysn;E0pHsa_1 zB>9|9ujYgOoLNcD=x6vNPvVaZ^yrmK02qhEC#9y#O<$efUG+szN+8j8zf{4U^xEHp z_%BN`tMxIE{6?^9mH2W7IfkA(KZ+~uPtfl#RPMI;S01^PKtX-=YONxfyjaM5ZJtiYNd0g z0e2(Ja;lv{Hj(y-1)nCd>Bf~Beeo3k9HhofDvb`Up1Oonv+%yZE{3#zsEb#tS&-H` zTpH$aY1oV{!9Z~5OJ|DXw3Ym%_Q2{JXPeVC@y8I-zgS4iy63;}hW4_tp9F2GT%ozj zf1~r%K0ZbLx21)1VIr{9iQV2CpyrQEjW=WH&n`v=BYSX>ke7jGl6<_$Cvw?SL$fi9 zk$w5}5wcC_CA9!ay)n6xD#kksvf#%vKMIQt3lpqpKzYC zL~+K@2*6C?xc&Mz>0df81iZ98YN1%pyMeZu@Pv`N4xsl}CL=lWzLpd}4kJxZkt>0A zH)9w&kVM$I5e3o6Qz{|P^TrT)+PM=ANI5&mhDBwqV#R%Q6(S7u~Z z{#lN~5P+4Bj4!sV^5%Rph+C#6;~!oGf^R`kp2BIr3Cf#v$RTlvu7kciV2p3Gm~+Vv zw9oL%R`a}_nq`>~aA%h(BQ8PJ)iEB z=1N$zfUypztC2bf;Mm!;to0hu|FS-cWwk#Bo}l&z#32Ct0eAlJR)9FwQnR4s8t598 zyqw9=iFCm{mK3jGo;Uk{etX^D~t- z4!sS@DLTZG$;mb1J4%n&yXed?uG7>W*VBVk+cXE{E6Ql@;+fD7+dsjZDse3fs-}7$ z7g9V8ozIiv8lJDS+Cp;bhxnVR1WAw9O|<|s@mG_XNYv&(CK3D35)eN$zG`ykS#mkC zP}~yLz8au2r*Nx2vmR(ZP#&epr}P*!IY4Jx#aGc&sOm&J+0x38;;n`xkPT0T8|Olb z*P;aC>Zsh!g%l@7yIC;?4Ov_xTu5;^lyy)Ym2#ZGvaErb+gsH@xoxVZQu1dQQv6Nx zs;TVmGA`~XT{V>*EF|B7CCiZBP9X`fdvpMK>ysgki*fl?dFJ1}7M1blO;=w90yw*y3dwqWxDoe{POS9@b<2!V; zTZ$TR?uyBH2fhzSZGqt)olDj^--x-sI8-6cvO;2}T<;$$Q&QoJnR4TgP??eo3*M!= z5zyu~nODiwZpK`u(i@ax;fwR%wW?=8H|$x{t?)Pj(jqxJ0bJu*Ad;TTVt0d{P{lsf zN{N|r1Fle+>SviM3;9~=jCn&(w~MWzcY6LzzJU(hTYGdL%a)eqKLZaXl!JQ|im zK)&oU$A<^OKZPg?*=sQ~N8?oVEIv05kr~-#F*C2mVlWtDK4STR@nz~39FZ&nNPyhOsldI zR?Lg!Kn1sCnwIgxlk7d{<*#At<=iBdkawI<&<0IXo<;o$05MQj7FQAnao(Gt- z%+L}eH(EzEYb~*qG~NCT+pPbf^l70dS39K#3N7KZ_VPKbvTMunl+xI*F6JDgk0B(J z80pP4l61!D^zsE2Ub$`AB|3GRa&rfK7Z9<$c;WP*MqsKlC&9h}a!h8VmmGQG!Whjl zQnO4Xwb@I@CsxRekpms3UCFaRd!JXdmyX}L7@aV3D7VCv+;cA-Z@CyfK*uPqoAfZy zOhTlWj#n%OKiukzk$Z46vyQsaU^MGOF?qn+TXZi?&s#jcB^r#u6Zlcgoj`sPj|wv8 zQ33Vip|XzNDfnv(SVMMZC%{bO=eEssBLgL;J6`IxxnQF~-7GHR+DMNyHLK)w#Va}~ z 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 GIllm7xOe;j literal 0 HcmV?d00001 diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index fa02b826..74a93e3a 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -1,5 +1,6 @@ import asyncio import logging +from collections.abc import Mapping from queue import Queue from tickit.core.device import Device, DeviceUpdate @@ -13,6 +14,13 @@ from tickit_devices.eiger.monitor.monitor_config import MonitorConfig from tickit_devices.eiger.monitor.monitor_status import MonitorStatus from tickit_devices.eiger.stream.eiger_stream import EigerStream +from tickit_devices.eiger.stream.eiger_stream_2 import EigerStream2 +from tickit_devices.eiger.stream.stream_config import ( + CBOR_STREAM, + LEGACY_STREAM, + StreamConfig, +) +from tickit_devices.eiger.stream.stream_status import StreamStatus from .eiger_status import EigerStatus, State @@ -35,7 +43,8 @@ class EigerDevice(Device): settings: EigerSettings status: EigerStatus - stream: EigerStream + stream: EigerStream | EigerStream2 + streams: Mapping[str, EigerStream | EigerStream2] _num_frames_left: int _data_queue: Queue @@ -49,7 +58,7 @@ def __init__( self, settings: EigerSettings | None = None, status: EigerStatus | None = None, - stream: EigerStream | None = None, + stream: EigerStream | EigerStream2 | None = None, ) -> None: """Construct a new eiger. @@ -61,7 +70,13 @@ def __init__( self.settings = settings or EigerSettings() self.status = status or EigerStatus() - self.stream = stream or EigerStream(callback_period=SimTime(int(1e9))) + self.stream_status: StreamStatus = StreamStatus() + self.stream_config: StreamConfig = StreamConfig() + self.streams = { + LEGACY_STREAM: stream or EigerStream(callback_period=SimTime(int(1e9))), + CBOR_STREAM: stream or EigerStream2(callback_period=SimTime(int(1e9))), + } + self.stream = self.streams[CBOR_STREAM] self.filewriter_status: FileWriterStatus = FileWriterStatus() self.filewriter_config: FileWriterConfig = FileWriterConfig() @@ -102,8 +117,11 @@ async def arm(self) -> None: Required for triggering. """ + self.stream = self.streams[self.stream_config.format] self._series_id += 1 - self.stream.begin_series(self.settings, self._series_id) + self.stream.begin_series( + self.settings, self._series_id, self.stream_config.header_detail + ) self._num_frames_left = self.settings.nimages self._num_triggers_left = self.settings.ntrigger self._set_state(State.READY) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index bac24b5e..be954ce7 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -8,6 +8,8 @@ from tickit_devices.eiger.eiger import EigerDevice from tickit_devices.eiger.eiger_schema import SequenceComplete, construct_value +from tickit_devices.eiger.stream.eiger_stream import EigerStream +from tickit_devices.eiger.stream.eiger_stream_2 import EigerStream2 API_VERSION = "1.8.0" DETECTOR_API = f"detector/api/{API_VERSION}" @@ -319,8 +321,8 @@ async def get_stream_status(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - if hasattr(self.device.stream.status, param): - return web.json_response(construct_value(self.device.stream.status, param)) + if hasattr(self.device.stream_status, param): + return web.json_response(construct_value(self.device.stream_status, param)) else: return web.json_response(status=404) @@ -337,8 +339,8 @@ async def get_stream_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - if hasattr(self.device.stream.config, param): - return web.json_response(construct_value(self.device.stream.config, param)) + if hasattr(self.device.stream_config, param): + return web.json_response(construct_value(self.device.stream_config, param)) else: return web.json_response(status=404) @@ -358,12 +360,12 @@ async def put_stream_config(self, request: web.Request) -> web.Response: response = await request.json() - if hasattr(self.device.stream.config, param): + if hasattr(self.device.stream_config, param): attr = response["value"] LOGGER.debug(f"Changing to {attr} for {param}") - self.device.stream.config[param] = attr + self.device.stream_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) return web.json_response(serialize([param])) @@ -511,11 +513,11 @@ class EigerZMQAdapter(ZeroMqPushAdapter): device: EigerDevice - def __init__(self, device: EigerDevice) -> None: + def __init__(self, stream: EigerStream | EigerStream2) -> None: super().__init__() - self.device = device + self.stream = stream def after_update(self) -> None: """Updates IOC values immediately following a device update.""" - if buffered_data := list(self.device.stream.consume_data()): + if buffered_data := list(self.stream.consume_data()): self.add_message_to_stream(buffered_data) diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index c47bca41..68e4e0ff 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -84,6 +84,7 @@ def config_keys() -> list[str]: "threshold_energy", "total_flux", "trigger_mode", + "trigger_start_delay", "two_theta_increment", "two_theta_start", "virtual_pixel_correction_applied", @@ -256,6 +257,7 @@ class EigerSettings: allowed_values=["eies", "exte", "extg", "exts", "inte", "ints"] ), ) + trigger_start_delay: float = field(default=0.0, metadata=rw_float(min=0.0)) two_theta_increment: float = field(default=0.0, metadata=rw_float()) two_theta_start: float = field(default=0.0, metadata=rw_float()) virtual_pixel_correction_applied: bool = field(default=True, metadata=rw_bool()) diff --git a/src/tickit_devices/eiger/stream/eiger_stream.py b/src/tickit_devices/eiger/stream/eiger_stream.py index 6fac5782..bbd6b055 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream.py +++ b/src/tickit_devices/eiger/stream/eiger_stream.py @@ -17,8 +17,6 @@ ImageHeader, ) from tickit_devices.eiger.eiger_settings import EigerSettings -from tickit_devices.eiger.stream.stream_config import StreamConfig -from tickit_devices.eiger.stream.stream_status import StreamStatus LOGGER = logging.getLogger(__name__) @@ -29,8 +27,6 @@ class EigerStream: """Simulation of an Eiger stream.""" - status: StreamStatus - config: StreamConfig callback_period: SimTime _message_buffer: Queue[_Message] @@ -41,21 +37,21 @@ class Outputs(TypedDict): ... def __init__(self, callback_period: int = int(1e9)) -> None: """An Eiger Stream constructor.""" - self.status = StreamStatus() - self.config = StreamConfig() self.callback_period = SimTime(callback_period) self._message_buffer = Queue() - def begin_series(self, settings: EigerSettings, series_id: int) -> None: + def begin_series( + self, settings: EigerSettings, series_id: int, header_detail: str + ) -> None: """Send the headers marking the beginning of the acquisition series. Args: settings: Current detector configuration, a snapshot may be sent with the headers. series_id: ID for the acquisition series. + header_detail: Header detail for start message - "none", "basic" or "all" """ - header_detail = self.config.header_detail header = AcquisitionSeriesHeader( header_detail=header_detail, series=series_id, diff --git a/src/tickit_devices/eiger/stream/eiger_stream_2.py b/src/tickit_devices/eiger/stream/eiger_stream_2.py new file mode 100644 index 00000000..b923b616 --- /dev/null +++ b/src/tickit_devices/eiger/stream/eiger_stream_2.py @@ -0,0 +1,161 @@ +import logging +from collections.abc import Iterable +from pathlib import Path +from queue import Queue +from typing import Any, TypedDict + +import cbor2 +import numpy as np +from tickit.core.typedefs import SimTime + +from tickit_devices.eiger.data.dummy_image import Image +from tickit_devices.eiger.eiger_settings import EigerSettings +from tickit_devices.eiger.stream.stream2 import stream2_tag_decoder + +LOGGER = logging.getLogger(__name__) +DATA_PATH = Path(__file__).parent.parent / "data" / "stream2" +STREAM_SETTINGS_MAP = { + # Direct Mappings + # TODO: These are ints, but they should be floats + # https://github.com/DiamondLightSource/tickit-devices/issues/120 + "beam_center_x": "beam_center_x", + "beam_center_y": "beam_center_y", + "count_time": "count_time", + "frame_time": "frame_time", + "sensor_material": "sensor_material", + "sensor_thickness": "sensor_thickness", + # TODO: This is broken because it is now a map of thresholds + # https://github.com/DiamondLightSource/tickit-devices/issues/121 + # threshold_energy="threshold_energy", + # Indirect Mappings + "countrate_correction_enabled": "countrate_correction_applied", + "detector_description": "description", + "detector_serial_number": "detector_number", + "flatfield_enabled": "flatfield_correction_applied", + "image_size_x": "x_pixels_in_detector", + "image_size_y": "y_pixels_in_detector", + "incident_energy": "threshold_energy", + "incident_wavelength": "wavelength", + "pixel_mask_enabled": "pixel_mask_applied", + "pixel_size_x": "x_pixel_size", + "pixel_size_y": "y_pixel_size", + "saturation_value": "countrate_correction_count_cutoff", +} +START_ALL_FIELDS = ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] +GONIO_AXES = ["chi", "kappa", "omega", "phi", "two_theta"] + + +def _load_messages(): + start = image = end = None + with open(DATA_PATH / "start.cbor", "rb") as f: + start = cbor2.load(f, tag_hook=stream2_tag_decoder) + + # Populate missing large datasets + sensor_shape = (start["image_size_y"], start["image_size_x"]) + start["countrate_correction_lookup_table"] = np.zeros((65536,)).tobytes() + start["flatfield"]["threshold_1"] = np.zeros(sensor_shape).tobytes() + start["pixel_mask"]["threshold_1"] = np.zeros(sensor_shape).tobytes() + + with open(DATA_PATH / "image.cbor", "rb") as f: + image = cbor2.load(f) + with open(DATA_PATH / "end.cbor", "rb") as f: + end = cbor2.load(f) + + return start, image, end + + +class EigerStream2: + """Simulation of an Eiger stream.""" + + callback_period: SimTime + + _message_buffer: Queue[bytes] + + class Inputs(TypedDict): + """No inputs.""" + + class Outputs(TypedDict): + """No outputs.""" + + def __init__(self, callback_period: int = int(1e9)) -> None: + """Eiger Stream2 constructor.""" + self.callback_period = SimTime(callback_period) + + self._message_buffer = Queue() + + self._start, self._image, self._end = _load_messages() + + def begin_series( + self, settings: EigerSettings, series_id: int, header_detail: str + ) -> None: + """Send the start message marking the beginning of the acquisition series. + + Args: + settings: Current detector configuration, a snapshot may be sent with the + headers. + series_id: ID for the acquisition series. + header_detail: Header detail for start message - 'none', 'basic' or 'all' + """ + if header_detail == "all": + # Use loaded message in place + start = self._start + else: + # Make a copy with "all" fields removed + start = {k: v for k, v in self._start.items() if k not in START_ALL_FIELDS} + + # Update message with current state + start["number_of_images"] = settings.nimages * settings.ntrigger + for stream_field, setting in STREAM_SETTINGS_MAP.items(): + start[stream_field] = getattr(settings, setting) + for axis in GONIO_AXES: + axis_fields: dict[str, float] = start["goniometer"][axis] + axis_fields["start"] = float(getattr(settings, f"{axis}_start")) + axis_fields["increment"] = float(getattr(settings, f"{axis}_increment")) + + start["series_id"] = series_id + + self._buffer(cbor_dumps(start)) + + def insert_image(self, image: Image, series_id: int) -> None: + """Send headers and an data blob for a single image. + + Args: + image: The image with associated metadata + series_id: ID for the acquisition series. + """ + self._image["series_id"] = series_id + self._image["image_id"] = image.index + + self._buffer(cbor_dumps(self._image)) + + def end_series(self, series_id: int) -> None: + """Send footer marking the end of an acquisition series. + + Args: + series_id: ID of the series to end. + """ + self._end["series_id"] = series_id + self._buffer(cbor_dumps(self._end)) + + def consume_data(self) -> Iterable[bytes]: + """Consume all headers and data buffered by other methods. + + Returns: + Iterable[_Message]: Iterable of headers and data + """ + while not self._message_buffer.empty(): + message = self._message_buffer.get() + yield message + + def _buffer(self, message: bytes) -> None: + self._message_buffer.put_nowait(message) + + +def cbor_dumps(message: dict[str, Any]) -> bytes: + """Serialize dictionary to cbor, including headers. + + Args: + message: Message to be serialized + + """ + return cbor2.dumps(cbor2.CBORTag(55799, message)) diff --git a/src/tickit_devices/eiger/stream/stream2.py b/src/tickit_devices/eiger/stream/stream2.py new file mode 100644 index 00000000..94881693 --- /dev/null +++ b/src/tickit_devices/eiger/stream/stream2.py @@ -0,0 +1,60 @@ +import cbor2 +import numpy as np + + +def decode_multi_dim_array(tag, column_major): + dimensions, contents = tag.value + if isinstance(contents, list): + array = np.empty((len(contents),), dtype=object) + array[:] = contents + elif isinstance(contents, np.ndarray | np.generic): + array = contents + else: + raise cbor2.CBORDecodeValueError("expected array or typed array") + return array.reshape(dimensions, order="F" if column_major else "C") + + +def decode_typed_array(tag, dtype): + if not isinstance(tag.value, bytes): + raise cbor2.CBORDecodeValueError("expected byte string in typed array") + return np.frombuffer(tag.value, dtype=dtype) + + +def decode_dectris_compression(tag): + _algorithm, _elem_size, encoded = tag.value + return encoded + + +tag_decoders = { + 40: lambda tag: decode_multi_dim_array(tag, column_major=False), + 64: lambda tag: decode_typed_array(tag, dtype="u1"), + 65: lambda tag: decode_typed_array(tag, dtype=">u2"), + 66: lambda tag: decode_typed_array(tag, dtype=">u4"), + 67: lambda tag: decode_typed_array(tag, dtype=">u8"), + 68: lambda tag: decode_typed_array(tag, dtype="u1"), + 69: lambda tag: decode_typed_array(tag, dtype=" list[str]: return ["format", "header_appendix", "header_detail", "image_appendix", "mode"] +LEGACY_STREAM = "legacy" +CBOR_STREAM = "cbor" + + @dataclass class StreamConfig: """Eiger stream configuration taken from the API spec.""" @@ -15,6 +19,10 @@ class StreamConfig: mode: str = field( default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) ) + format: str = field( + default=LEGACY_STREAM, + metadata=rw_str(allowed_values=[LEGACY_STREAM, CBOR_STREAM]), + ) header_detail: str = field( default="basic", metadata=rw_str(allowed_values=["none", "basic", "all"]) ) diff --git a/tests/eiger/test_eiger.py b/tests/eiger/test_eiger.py index ff2acdce..9d183588 100644 --- a/tests/eiger/test_eiger.py +++ b/tests/eiger/test_eiger.py @@ -112,7 +112,7 @@ async def test_armed_eiger_starts_series(eiger: EigerDevice, mock_stream: Mock): await eiger.initialize() eiger.settings.trigger_mode = "ints" await eiger.arm() - mock_stream.begin_series.assert_called_once_with(eiger.settings, 1) + mock_stream.begin_series.assert_called_once_with(eiger.settings, 1, "basic") @pytest.mark.asyncio @@ -123,7 +123,7 @@ async def test_disarmed_eiger_starts_and_ends_series( eiger.settings.trigger_mode = "ints" await eiger.arm() await eiger.disarm() - mock_stream.begin_series.assert_called_once_with(eiger.settings, 1) + mock_stream.begin_series.assert_called_once_with(eiger.settings, 1, "basic") mock_stream.end_series.assert_called_once_with(1) @@ -135,7 +135,7 @@ async def test_cancelled_eiger_starts_and_ends_series( eiger.settings.trigger_mode = "ints" await eiger.arm() await eiger.cancel() - mock_stream.begin_series.assert_called_once_with(eiger.settings, 1) + mock_stream.begin_series.assert_called_once_with(eiger.settings, 1, "basic") mock_stream.end_series.assert_called_once_with(1) @@ -164,7 +164,7 @@ async def test_acquire_frames_in_ints_mode( update = eiger.update(SimTime(0.0), {}) assert update.call_at is None - mock_stream.begin_series.assert_called_with(eiger.settings, series) + mock_stream.begin_series.assert_called_with(eiger.settings, series, "basic") assert mock_stream.begin_series.call_count == series if num_frames > 0: mock_stream.insert_image.assert_called_with(ANY, series) @@ -203,7 +203,7 @@ async def test_acquire_frames_in_exts_mode( update = eiger.update(SimTime(0.0), {}) assert update.call_at is None - mock_stream.begin_series.assert_called_with(eiger.settings, series) + mock_stream.begin_series.assert_called_with(eiger.settings, series, "basic") assert mock_stream.begin_series.call_count == series if num_frames > 0: mock_stream.insert_image.assert_called_with(ANY, series) diff --git a/tests/eiger/test_eiger_stream.py b/tests/eiger/test_eiger_stream.py index ad83c9d9..15ba9aa8 100644 --- a/tests/eiger/test_eiger_stream.py +++ b/tests/eiger/test_eiger_stream.py @@ -90,8 +90,7 @@ def test_begin_series_produces_correct_headers( expected_headers: list[BaseModel | bytes | Mapping[str, Any]], ) -> None: settings = EigerSettings() - stream.config.header_detail = header_detail - stream.begin_series(settings, TEST_SERIES_ID) + stream.begin_series(settings, TEST_SERIES_ID, header_detail) blobs = list(stream.consume_data()) for a, b in zip(expected_headers, blobs, strict=True): @@ -119,8 +118,7 @@ def test_end_series_produces_correct_headers( def test_data_buffered(stream: EigerStream) -> None: settings = EigerSettings() - stream.config.header_detail = "all" - stream.begin_series(settings, TEST_SERIES_ID) + stream.begin_series(settings, TEST_SERIES_ID, "all") image = Image.create_dummy_image(0, (X_SIZE, Y_SIZE)) stream.insert_image(image, TEST_SERIES_ID) stream.end_series(TEST_SERIES_ID) diff --git a/tests/eiger/test_eiger_stream_2.py b/tests/eiger/test_eiger_stream_2.py new file mode 100644 index 00000000..5522dcd3 --- /dev/null +++ b/tests/eiger/test_eiger_stream_2.py @@ -0,0 +1,176 @@ +import datetime + +import cbor2 +import pytest + +from tickit_devices.eiger.data.dummy_image import Image +from tickit_devices.eiger.eiger import EigerDevice +from tickit_devices.eiger.eiger_settings import EigerSettings +from tickit_devices.eiger.stream.eiger_stream_2 import EigerStream2 + + +@pytest.fixture +def stream() -> EigerStream2: + return EigerStream2() + + +@pytest.fixture +def eiger() -> EigerDevice: + return EigerDevice() + + +TEST_SERIES_ID = 1 + +EIGER_SETTINGS_HEADER = EigerSettings().filtered( + ["flatfield", "pixelmask" "countrate_correction_table"] +) +X_SIZE = EIGER_SETTINGS_HEADER["x_pixels_in_detector"] +Y_SIZE = EIGER_SETTINGS_HEADER["y_pixels_in_detector"] + +BASIC_START_MESSAGE = { + "type": "start", + "series_id": 1, + "series_unique_id": "01H95H3ZJ0B256900H4DV38G24", + "arm_date": datetime.datetime( + 2023, + 8, + 31, + 12, + 9, + 45, + 24000, + tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), + ), + "beam_center_x": 2049.2682222222224, + "beam_center_y": 2157.8508000000006, + "channels": ["threshold_1"], + "count_time": 0.1, + "countrate_correction_enabled": True, + "detector_description": "Simulated Eiger X 16M Detector", + "detector_serial_number": "EIGERSIM001", + "detector_translation": [0.15369511666666666, 0.16183881000000003, -0.2893], + "flatfield_enabled": True, + "frame_time": 0.12, + "goniometer": { + "chi": {"increment": 0.0, "start": 0.0}, + "kappa": {"increment": 0.0, "start": 0.0}, + "omega": {"increment": 0.0, "start": 0.0}, + "phi": {"increment": 0.0, "start": 0.0}, + "two_theta": {"increment": 0.0, "start": 0.0}, + }, + "image_size_x": 4148, + "image_size_y": 4362, + "incident_energy": 4020.5, + "incident_wavelength": 1.0, + "number_of_images": 1, + "pixel_mask_enabled": False, + "pixel_size_x": 0.01, + "pixel_size_y": 0.01, + "saturation_value": 1000, + "sensor_material": "Silicon", + "sensor_thickness": 0.01, + "threshold_energy": { + "threshold_1": 6349.949919757628, + "threshold_2": 17779.859775321358, + }, + "virtual_pixel_interpolation_enabled": True, +} + + +# This does not include 'image_id' or 'data' as these are tested separately +IMAGE_MESSAGE = { + "type": "image", + "series_id": 1, + "series_unique_id": "01H93H861TK4FMT4H5CT49PCN5", + "real_time": [500000000, 50000000], + "series_date": datetime.datetime( + 2023, + 8, + 30, + 17, + 33, + 41, + 934000, + tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), + ), + "start_time": [0, 50000000], + "stop_time": [500000000, 50000000], +} + + +END_MESSAGE = { + "type": "end", + "series_id": 1, + "series_unique_id": "01H93H861TK4FMT4H5CT49PCN5", +} + + +@pytest.mark.parametrize( + "header_detail", + [("none"), ("basic")], +) +def test_begin_series_message_basic(stream: EigerStream2, header_detail: str) -> None: + settings = EigerSettings() + + stream.begin_series(settings, TEST_SERIES_ID, header_detail) + message = cbor2.loads(list(stream.consume_data())[0]) + + assert message == BASIC_START_MESSAGE + assert not any( + f in message + for f in ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] + ) + + +def test_begin_series_message_all(stream: EigerStream2) -> None: + settings = EigerSettings() + + stream.begin_series(settings, TEST_SERIES_ID, "all") + message = cbor2.loads(list(stream.consume_data())[0]) + + assert all( + f in message + for f in ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] + ) + + +def test_insert_image_produces_correct_message(stream: EigerStream2) -> None: + for i in range(2): + image = Image.create_dummy_image(i, (X_SIZE, Y_SIZE)) + + stream.insert_image(image, TEST_SERIES_ID) + message = cbor2.loads(list(stream.consume_data())[0]) + + # Image data is too big to compare - just sanity check size + assert message["data"]["threshold_1"].value[0] == [Y_SIZE, X_SIZE] + del message["data"] + + # Check image_id and remove it to compare against generic expected message + assert message["image_id"] == i + del message["image_id"] + + assert message == IMAGE_MESSAGE + + +def test_end_series_produces_correct_message(stream: EigerStream2) -> None: + stream.end_series(TEST_SERIES_ID) + + message = stream.consume_data() + message = cbor2.loads(list(stream.consume_data())[0]) + + assert message == END_MESSAGE + + +def test_data_buffered(stream: EigerStream2) -> None: + settings = EigerSettings() + image = Image.create_dummy_image(0, (X_SIZE, Y_SIZE)) + + stream.begin_series(settings, TEST_SERIES_ID, "basic") + stream.insert_image(image, TEST_SERIES_ID) + stream.insert_image(image, TEST_SERIES_ID) + stream.insert_image(image, TEST_SERIES_ID) + stream.end_series(TEST_SERIES_ID) + + messages = [cbor2.loads(b) for b in stream.consume_data()] + + assert [m["type"] for m in messages] == ["start", "image", "image", "image", "end"] From 8d30e2ed613aeac7f33c9a8176fd5909dfde828d Mon Sep 17 00:00:00 2001 From: James Souter Date: Thu, 7 Mar 2024 15:52:40 +0000 Subject: [PATCH 02/11] fix eiger stream 2 tests change type of eiger image config time parameters to int --- tests/eiger/test_eiger_adapters.py | 2 +- tests/eiger/test_eiger_stream_2.py | 130 +++++++++++++++++------------ 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/tests/eiger/test_eiger_adapters.py b/tests/eiger/test_eiger_adapters.py index 205c26f7..931f1f0c 100644 --- a/tests/eiger/test_eiger_adapters.py +++ b/tests/eiger/test_eiger_adapters.py @@ -12,7 +12,7 @@ def test_after_update(mocker: MockerFixture) -> None: device_mock = mocker.MagicMock() device_mock.stream.consume_data.side_effect = [test_data, []] - zmq_adapter = EigerZMQAdapter(device_mock) + zmq_adapter = EigerZMQAdapter(device_mock.stream) add_mock = mocker.patch.object(zmq_adapter, "add_message_to_stream") # Test after_update only calls add_message_to_stream with non-empty data diff --git a/tests/eiger/test_eiger_stream_2.py b/tests/eiger/test_eiger_stream_2.py index 5522dcd3..ee09bf07 100644 --- a/tests/eiger/test_eiger_stream_2.py +++ b/tests/eiger/test_eiger_stream_2.py @@ -1,4 +1,5 @@ import datetime +from typing import Any import cbor2 import pytest @@ -19,7 +20,7 @@ def eiger() -> EigerDevice: return EigerDevice() -TEST_SERIES_ID = 1 +TEST_SERIES_ID = 15614 EIGER_SETTINGS_HEADER = EigerSettings().filtered( ["flatfield", "pixelmask" "countrate_correction_table"] @@ -27,51 +28,58 @@ def eiger() -> EigerDevice: X_SIZE = EIGER_SETTINGS_HEADER["x_pixels_in_detector"] Y_SIZE = EIGER_SETTINGS_HEADER["y_pixels_in_detector"] -BASIC_START_MESSAGE = { +ALL_FIELDS = ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] + +BASIC_START_MESSAGE: dict[str, Any] = { "type": "start", - "series_id": 1, - "series_unique_id": "01H95H3ZJ0B256900H4DV38G24", + "series_id": 15614, + "series_unique_id": "01HBV3JPF9T4ZDPADX6EMK6XMZ", "arm_date": datetime.datetime( 2023, - 8, - 31, - 12, - 9, - 45, - 24000, + 10, + 3, + 17, + 47, + 48, + 329000, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), ), - "beam_center_x": 2049.2682222222224, - "beam_center_y": 2157.8508000000006, + "beam_center_x": 2049.3840906675064, + "beam_center_y": 2163.621048575148, "channels": ["threshold_1"], - "count_time": 0.1, + "count_time": 0.004317472232502031, + "countrate_correction_lookup_table": b"\x00\x00\x00\x00", "countrate_correction_enabled": True, - "detector_description": "Simulated Eiger X 16M Detector", - "detector_serial_number": "EIGERSIM001", - "detector_translation": [0.15369511666666666, 0.16183881000000003, -0.2893], + "detector_description": "Dectris EIGER2 Si 16M", + "detector_serial_number": "E-32-0117", + "detector_translation": [ + 0.15370380680006296, + 0.1622715786431361, + -0.23715919962215962, + ], + "flatfield": {"threshold_1": b"\x00\x00\x00\x00"}, "flatfield_enabled": True, - "frame_time": 0.12, + "frame_time": 0.004317572232502031, "goniometer": { - "chi": {"increment": 0.0, "start": 0.0}, - "kappa": {"increment": 0.0, "start": 0.0}, - "omega": {"increment": 0.0, "start": 0.0}, + "chi": {"increment": 0.0, "start": 30.0}, + "omega": {"increment": 0.1, "start": 0.0}, "phi": {"increment": 0.0, "start": 0.0}, - "two_theta": {"increment": 0.0, "start": 0.0}, }, "image_size_x": 4148, "image_size_y": 4362, - "incident_energy": 4020.5, - "incident_wavelength": 1.0, + "incident_energy": 13500.299829398293, + "incident_wavelength": 0.918381073013, "number_of_images": 1, - "pixel_mask_enabled": False, - "pixel_size_x": 0.01, - "pixel_size_y": 0.01, - "saturation_value": 1000, - "sensor_material": "Silicon", - "sensor_thickness": 0.01, + "pixel_mask": {"threshold_1": b"\x00\x00\x00\x00"}, + "pixel_mask_enabled": True, + "pixel_size_x": 7.5e-05, + "pixel_size_y": 7.5e-05, + "saturation_value": 21517, + "sensor_material": "Si", + "sensor_thickness": 0.00045, "threshold_energy": { - "threshold_1": 6349.949919757628, - "threshold_2": 17779.859775321358, + "threshold_1": 6750.149914699146, + "threshold_2": 18900.41976115761, }, "virtual_pixel_interpolation_enabled": True, } @@ -80,28 +88,28 @@ def eiger() -> EigerDevice: # This does not include 'image_id' or 'data' as these are tested separately IMAGE_MESSAGE = { "type": "image", - "series_id": 1, - "series_unique_id": "01H93H861TK4FMT4H5CT49PCN5", - "real_time": [500000000, 50000000], + "series_id": 15614, + "series_unique_id": "01HBV3JPF9T4ZDPADX6EMK6XMZ", + "real_time": [215873, 50000000], "series_date": datetime.datetime( 2023, - 8, - 30, + 10, + 3, 17, - 33, - 41, - 934000, + 47, + 49, + 434000, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), ), "start_time": [0, 50000000], - "stop_time": [500000000, 50000000], + "stop_time": [215873, 50000000], } END_MESSAGE = { "type": "end", - "series_id": 1, - "series_unique_id": "01H93H861TK4FMT4H5CT49PCN5", + "series_id": 15614, + "series_unique_id": "01HBV3JPF9T4ZDPADX6EMK6XMZ", } @@ -113,25 +121,39 @@ def test_begin_series_message_basic(stream: EigerStream2, header_detail: str) -> settings = EigerSettings() stream.begin_series(settings, TEST_SERIES_ID, header_detail) - message = cbor2.loads(list(stream.consume_data())[0]) - - assert message == BASIC_START_MESSAGE - assert not any( - f in message - for f in ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] - ) + data = list(stream.consume_data())[0] + assert isinstance(data, bytes) + message = cbor2.loads(data) + for axis, start_value in BASIC_START_MESSAGE["goniometer"].items(): + assert start_value == message["goniometer"][axis] + message.pop("goniometer") + reduced_start_message = BASIC_START_MESSAGE.copy() + reduced_start_message.pop("goniometer") + for f in ALL_FIELDS: + reduced_start_message.pop(f) + assert message == reduced_start_message + assert not any(f in message for f in ALL_FIELDS) def test_begin_series_message_all(stream: EigerStream2) -> None: settings = EigerSettings() - stream.begin_series(settings, TEST_SERIES_ID, "all") + message = cbor2.loads(list(stream.consume_data())[0]) - assert all( - f in message - for f in ["flatfield", "pixel_mask", "countrate_correction_lookup_table"] - ) + for axis, start_value in BASIC_START_MESSAGE["goniometer"].items(): + assert start_value == message["goniometer"][axis] + + message.pop("goniometer") + reduced_start_message = BASIC_START_MESSAGE.copy() + reduced_start_message.pop("goniometer") + # the ALL_FIELDS entries get set to default value, overwriting initial message + for f in ALL_FIELDS: + assert f in message + message.pop(f) + reduced_start_message.pop(f) + + assert message == reduced_start_message def test_insert_image_produces_correct_message(stream: EigerStream2) -> None: From 0b2add1a48a463fe886d25ca44e3d16bf7a1a30f Mon Sep 17 00:00:00 2001 From: James Souter Date: Fri, 23 Aug 2024 17:41:33 +0000 Subject: [PATCH 03/11] Insert gonio fields if they don't exist in start message --- .../eiger/stream/eiger_stream_2.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/tickit_devices/eiger/stream/eiger_stream_2.py b/src/tickit_devices/eiger/stream/eiger_stream_2.py index b923b616..eb91ded3 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream_2.py +++ b/src/tickit_devices/eiger/stream/eiger_stream_2.py @@ -106,11 +106,18 @@ def begin_series( # Update message with current state start["number_of_images"] = settings.nimages * settings.ntrigger for stream_field, setting in STREAM_SETTINGS_MAP.items(): - start[stream_field] = getattr(settings, setting) - for axis in GONIO_AXES: - axis_fields: dict[str, float] = start["goniometer"][axis] - axis_fields["start"] = float(getattr(settings, f"{axis}_start")) - axis_fields["increment"] = float(getattr(settings, f"{axis}_increment")) + if stream_field not in start: + start[stream_field] = getattr(settings, setting) + for axis in [a for a in GONIO_AXES if a not in start["goniometer"]]: + # get default values for axes not in start message + # TODO: Captured cbor start message should have all axes? + start["goniometer"][axis] = {} + start["goniometer"][axis]["start"] = float( + getattr(settings, f"{axis}_start") + ) + start["goniometer"][axis]["increment"] = float( + getattr(settings, f"{axis}_increment") + ) start["series_id"] = series_id From 01f8083225bad20c3dbe895c46e74f225d3d6a39 Mon Sep 17 00:00:00 2001 From: James Souter Date: Fri, 23 Aug 2024 17:40:18 +0000 Subject: [PATCH 04/11] Base64 encode zeroes for flatfield, mask, countrate --- src/tickit_devices/eiger/stream/eiger_stream_2.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/stream/eiger_stream_2.py b/src/tickit_devices/eiger/stream/eiger_stream_2.py index eb91ded3..69c8a7cf 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream_2.py +++ b/src/tickit_devices/eiger/stream/eiger_stream_2.py @@ -1,3 +1,4 @@ +import base64 import logging from collections.abc import Iterable from pathlib import Path @@ -52,9 +53,13 @@ def _load_messages(): # Populate missing large datasets sensor_shape = (start["image_size_y"], start["image_size_x"]) - start["countrate_correction_lookup_table"] = np.zeros((65536,)).tobytes() - start["flatfield"]["threshold_1"] = np.zeros(sensor_shape).tobytes() - start["pixel_mask"]["threshold_1"] = np.zeros(sensor_shape).tobytes() + # we need a base64 encoded array of 4 bit integers, numpy can't provide uint4s + # we can construct it manually for the trivial zero case + start["countrate_correction_lookup_table"] = base64.b64encode(65536 // 2 * b"\x00") + start["flatfield"]["threshold_1"] = base64.b64encode( + np.prod(sensor_shape) // 2 * b"\x00" # 2 pixels per byte + ) + start["pixel_mask"]["threshold_1"] = start["flatfield"]["threshold_1"] # copy value with open(DATA_PATH / "image.cbor", "rb") as f: image = cbor2.load(f) From 27748abf5f9e16f1d9f144cbd988f0dc75919984 Mon Sep 17 00:00:00 2001 From: jsouter <107045742+jsouter@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:19:32 +0100 Subject: [PATCH 05/11] Fix eiger put responses to reflect real detector behaviour (Fixes #114) (#115) Return correct changed parameters list for eiger detector put requests Return empty list for puts to non detector eiger API paths to reflect real API Test response of get_changed_parameters serialises properly --- src/tickit_devices/eiger/eiger.py | 70 ++++++++++++++++++++++ src/tickit_devices/eiger/eiger_adapters.py | 24 ++++---- tests/eiger/test_eiger_adapters.py | 59 +++++++++++++++++- tests/eiger/test_eiger_system.py | 16 +++-- 4 files changed, 152 insertions(+), 17 deletions(-) diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index 74a93e3a..22b78f9e 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -242,3 +242,73 @@ def _set_state(self, state: State) -> None: def _is_in_state(self, state: State) -> bool: return self.get_state() is state + + +def get_changed_parameters(key: str) -> list[str]: + """Get the list of parameters that may have changed as a result of putting + to the parameter provided. + + Args: + key: string key of the changed parameter within the detector subsystem + + Returns: + list[str]: a list of keys which may have been changed after a PUT request + """ + match key: + case "auto_summation": + return ["auto_summation", "frame_count_time"] + case "count_time" | "frame_time": + return [ + "bit_depth_image", + "bit_depth_readout", + "count_time", + "countrate_correction_count_cutoff", + "frame_count_time", + "frame_time", + ] + case "flatfield": + return ["flatfield", "threshold/1/flatfield"] + case "incident_energy" | "photon_energy": + return [ + "element", + "flatfield", + "incident_energy", + "photon_energy", + "threshold/1/energy", + "threshold/1/flatfield", + "threshold/2/energy", + "threshold/2/flatfield", + "threshold_energy", + "wavelength", + ] + case "pixel_mask": + return ["pixel_mask", "threshold/1/pixel_mask"] + case "threshold/1/flatfield": + return ["flatfield", "threshold/1/flatfield"] + case "roi_mode": + return ["count_time", "frame_time", "roi_mode"] + case "threshold_energy" | "threshold/1/energy": + return [ + "flatfield", + "threshold/1/energy", + "threshold/1/flatfield", + "threshold/2/flatfield", + "threshold_energy", + ] + case "threshold/2/energy": + return [ + "flatfield", + "threshold/1/flatfield", + "threshold/2/energy", + "threshold/2/flatfield", + ] + case "threshold/1/mode": + return ["threshold/1/mode", "threshold/difference/mode"] + case "threshold/2/mode": + return ["threshold/2/mode", "threshold/difference/mode"] + case "threshold/1/pixel_mask": + return ["pixel_mask", "threshold/1/pixel_mask"] + case "threshold/difference/mode": + return ["difference_mode"] # replicating API inconsistency + case _: + return [key] diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index be954ce7..f1e927ee 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -6,7 +6,7 @@ from tickit.adapters.specifications import HttpEndpoint from tickit.adapters.zmq import ZeroMqPushAdapter -from tickit_devices.eiger.eiger import EigerDevice +from tickit_devices.eiger.eiger import EigerDevice, get_changed_parameters from tickit_devices.eiger.eiger_schema import SequenceComplete, construct_value from tickit_devices.eiger.stream.eiger_stream import EigerStream from tickit_devices.eiger.stream.eiger_stream_2 import EigerStream2 @@ -75,7 +75,10 @@ async def put_config(self, request: web.Request) -> web.Response: self.device.settings[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + + changed_parameters = get_changed_parameters(param) + + return web.json_response(serialize(changed_parameters)) else: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) @@ -123,15 +126,14 @@ async def put_threshold_config(self, request: web.Request) -> web.Response: config = self.device.settings.threshold_config if threshold in config and hasattr(config[threshold], param): attr = response["value"] - - LOGGER.debug( - f"Changing to {str(attr)} for threshold/{threshold}{str(param)}" - ) - config[threshold][param] = attr LOGGER.debug(f"Set threshold/{threshold}{str(param)} to {str(attr)}") - return web.json_response(serialize([param])) + + full_param = f"threshold/{threshold}/{param}" + changed_parameters = get_changed_parameters(full_param) + + return web.json_response(serialize(changed_parameters)) else: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) @@ -368,7 +370,7 @@ async def put_stream_config(self, request: web.Request) -> web.Response: self.device.stream_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) @@ -415,7 +417,7 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: self.device.monitor_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) @@ -482,7 +484,7 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: self.device.filewriter_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) diff --git a/tests/eiger/test_eiger_adapters.py b/tests/eiger/test_eiger_adapters.py index 931f1f0c..779fd2ca 100644 --- a/tests/eiger/test_eiger_adapters.py +++ b/tests/eiger/test_eiger_adapters.py @@ -1,7 +1,7 @@ import pytest from pytest_mock import MockerFixture -from tickit_devices.eiger.eiger import EigerDevice +from tickit_devices.eiger.eiger import EigerDevice, get_changed_parameters from tickit_devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter @@ -72,3 +72,60 @@ async def test_rest_adapter_command_404(mocker: MockerFixture): assert (await eiger_adapter.trigger_eiger(request)).status == 404 assert (await eiger_adapter.cancel_eiger(request)).status == 404 assert (await eiger_adapter.abort_eiger(request)).status == 404 + + +@pytest.mark.asyncio +async def test_detector_put_responses(mocker: MockerFixture): + # test special keys have non-trivial response + custom_response_keys = [ + "auto_summation", + "count_time", + "frame_time", + "flatfield", + "incident_energy", + "photon_energy", + "pixel_mask", + "threshold/1/flatfield", + "roi_mode", + "threshold_energy", + "threshold/1/energy", + "threshold/2/energy", + "threshold/1/mode", + "threshold/2/mode", + "threshold/1/pixel_mask", + "threshold/difference/mode", + ] + + for key in custom_response_keys: + assert get_changed_parameters(key) != [key] + + assert get_changed_parameters("phi_start") == ["phi_start"] + assert get_changed_parameters("threshold/1/pixel_mask") == [ + "pixel_mask", + "threshold/1/pixel_mask", + ] + assert get_changed_parameters("threshold/difference/mode") == ["difference_mode"] + + eiger_adapter = EigerRESTAdapter(EigerDevice()) + + request = mocker.MagicMock() + request.json = mocker.AsyncMock() + + request.match_info = {"parameter_name": "count_time", "value": 1.0} + response = await eiger_adapter.put_config(request) + assert response.body == ( + b'["bit_depth_image", "bit_depth_readout", "count_time",' + b' "countrate_correction_count_cutoff",' + b' "frame_count_time", "frame_time"]' + ) + # trivial case just returns the single parameter + request.match_info = {"parameter_name": "phi_start", "value": 1.0} + response = await eiger_adapter.put_config(request) + assert response.body == b'["phi_start"]' + + # test threshold responses work + + request.match_info = {"parameter_name": "mode", "threshold": "1", "value": 1} + request.json = mocker.AsyncMock(return_value={"value": 1}) + response = await eiger_adapter.put_threshold_config(request) + assert response.body == b'["threshold/1/mode", "threshold/difference/mode"]' diff --git a/tests/eiger/test_eiger_system.py b/tests/eiger/test_eiger_system.py index d77a13ff..7dd20b95 100644 --- a/tests/eiger/test_eiger_system.py +++ b/tests/eiger/test_eiger_system.py @@ -87,7 +87,7 @@ async def get_status(status, expected): json={"value": "enabled"}, timeout=REQUEST_TIMEOUT, ) as response: - assert ["mode"] == (await response.json()) + assert [] == (await response.json()) # Test filewriter, monitor and stream endpoints async with session.get( @@ -108,7 +108,7 @@ async def get_status(status, expected): json={"value": "enabled"}, timeout=REQUEST_TIMEOUT, ) as response: - assert ["mode"] == (await response.json()) + assert [] == (await response.json()) async with session.get( MONITOR_URL + "status/error", @@ -128,7 +128,7 @@ async def get_status(status, expected): json={"value": "enabled"}, timeout=REQUEST_TIMEOUT, ) as response: - assert ["mode"] == (await response.json()) + assert [] == (await response.json()) async with session.put( DETECTOR_URL + "config/threshold/1/energy", @@ -136,7 +136,13 @@ async def get_status(status, expected): json={"value": 6829}, timeout=REQUEST_TIMEOUT, ) as response: - assert ["energy"] == (await response.json()) + assert [ + "flatfield", + "threshold/1/energy", + "threshold/1/flatfield", + "threshold/2/flatfield", + "threshold_energy", + ] == (await response.json()) async with session.get( DETECTOR_URL + "config/threshold/1/energy", @@ -151,7 +157,7 @@ async def get_status(status, expected): json={"value": "enabled"}, timeout=REQUEST_TIMEOUT, ) as response: - assert ["mode"] == (await response.json()) + assert ["difference_mode"] == (await response.json()) async with session.get( DETECTOR_URL + "config/threshold/difference/mode", From cb07a58aef219ee53bbfafadf5519c1743e063c6 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Tue, 1 Apr 2025 09:52:09 +0000 Subject: [PATCH 06/11] Convert default values to correct type Fastcs-eiger raises a valueerror for a few attributes, as they default to ints but are floats. --- src/tickit_devices/eiger/eiger_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 68e4e0ff..cdd9a385 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -133,7 +133,7 @@ class KA_Energy(Enum): class Threshold: """Data container for a single threshold configuration.""" - energy: float = field(default=6729, metadata=rw_float()) + energy: float = field(default=6729.0, metadata=rw_float()) mode: str = field( default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) ) @@ -221,7 +221,7 @@ class EigerSettings: frame_count_time: float = field(default=0.01, metadata=ro_float()) frame_time: float = field(default=0.12, metadata=rw_float()) frame_period: float = field(default=0.12, metadata=rw_float()) - incident_energy: float = field(default=13458, metadata=rw_float()) + incident_energy: float = field(default=13458.0, metadata=rw_float()) incident_particle_type: str = field(default="photons", metadata=ro_str()) instrument_name: str = field(default="", metadata=rw_str()) kappa_increment: float = field(default=0.0, metadata=rw_float()) @@ -272,7 +272,7 @@ class EigerSettings: def __post_init__(self): self._threshold_config = { "1": Threshold(), - "2": Threshold(energy=18841), + "2": Threshold(energy=18841.0), "difference": ThresholdDifference(), } From a2619af03ad89a1192f8eb624f787f04d73616eb Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 6 Feb 2026 16:21:17 +0000 Subject: [PATCH 07/11] Fix string list typos --- src/tickit_devices/eiger/stream/eiger_stream.py | 2 +- tests/eiger/test_eiger_stream.py | 2 +- tests/eiger/test_eiger_stream_2.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/stream/eiger_stream.py b/src/tickit_devices/eiger/stream/eiger_stream.py index bbd6b055..cb3fe456 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream.py +++ b/src/tickit_devices/eiger/stream/eiger_stream.py @@ -60,7 +60,7 @@ def begin_series( if header_detail != "none": config_header = settings.filtered( - ["flatfield", "pixelmask" "countrate_correction_table"] + ["flatfield", "pixelmask", "countrate_correction_table"] ) self._buffer(config_header) diff --git a/tests/eiger/test_eiger_stream.py b/tests/eiger/test_eiger_stream.py index 15ba9aa8..d9c3e995 100644 --- a/tests/eiger/test_eiger_stream.py +++ b/tests/eiger/test_eiger_stream.py @@ -33,7 +33,7 @@ def stream() -> EigerStream: ] EIGER_SETTINGS_HEADER = EigerSettings().filtered( - ["flatfield", "pixelmask" "countrate_correction_table"] + ["flatfield", "pixelmask", "countrate_correction_table"] ) X_SIZE = EIGER_SETTINGS_HEADER["x_pixels_in_detector"] Y_SIZE = EIGER_SETTINGS_HEADER["y_pixels_in_detector"] diff --git a/tests/eiger/test_eiger_stream_2.py b/tests/eiger/test_eiger_stream_2.py index ee09bf07..aa3058e8 100644 --- a/tests/eiger/test_eiger_stream_2.py +++ b/tests/eiger/test_eiger_stream_2.py @@ -23,7 +23,7 @@ def eiger() -> EigerDevice: TEST_SERIES_ID = 15614 EIGER_SETTINGS_HEADER = EigerSettings().filtered( - ["flatfield", "pixelmask" "countrate_correction_table"] + ["flatfield", "pixelmask", "countrate_correction_table"] ) X_SIZE = EIGER_SETTINGS_HEADER["x_pixels_in_detector"] Y_SIZE = EIGER_SETTINGS_HEADER["y_pixels_in_detector"] From 70e7f03be02a065f36d06170830aef284e655446 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 6 Feb 2026 16:22:56 +0000 Subject: [PATCH 08/11] Make ruff happy --- src/tickit_devices/zebra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tickit_devices/zebra/__init__.py b/src/tickit_devices/zebra/__init__.py index a8d0e1a4..80205c0d 100644 --- a/src/tickit_devices/zebra/__init__.py +++ b/src/tickit_devices/zebra/__init__.py @@ -13,7 +13,7 @@ def _default() -> dict[str, int]: - return {k: 0 for k in param_types.keys()} + return dict.fromkeys(param_types.keys(), 0) @pydantic.v1.dataclasses.dataclass From 230c7ea3df95f391a4d78be40635f3a80428d56d Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 6 Feb 2026 18:33:33 +0000 Subject: [PATCH 09/11] Make mypy happy about enum type hints --- src/tickit_devices/eiger/eiger_schema.py | 30 ++++++------ src/tickit_devices/eiger/eiger_settings.py | 56 +++++++++++----------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 8a1cb50e..2216e3c3 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -33,26 +33,26 @@ def field_config(**kwargs) -> Mapping[str, Any]: class AccessMode(Enum): """Possible access modes for field metadata.""" - READ_ONLY: str = "r" - WRITE_ONLY: str = "w" - READ_WRITE: str = "rw" + READ_ONLY = "r" + WRITE_ONLY = "w" + READ_WRITE = "rw" class ValueType(Enum): """Possible value types for field metadata.""" - FLOAT: str = "float" - INT: str = "int" - UINT: str = "uint" - STRING: str = "string" - STR_LIST: str = "string[]" - BOOL: str = "bool" - FLOAT_GRID: str = "float[][]" - UINT_GRID: str = "uint[][]" - DATE: str = "date" - DATETIME: str = "datetime" - NONE: str = "none" - STATE: str = "State" + FLOAT = "float" + INT = "int" + UINT = "uint" + STRING = "string" + STR_LIST = "string[]" + BOOL = "bool" + FLOAT_GRID = "float[][]" + UINT_GRID = "uint[][]" + DATE = "date" + DATETIME = "datetime" + NONE = "none" + STATE = "State" # diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index cdd9a385..12d5ae35 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -99,34 +99,34 @@ def config_keys() -> list[str]: class KA_Energy(Enum): """Possible element K-alpha energies for samples.""" - Li: float = 54.3 - Be: float = 108.5 - B: float = 183.3 - C: float = 277.0 - N: float = 392.4 - O: float = 524.9 # noqa: E741 - F: float = 676.8 - Ne: float = 848.6 - Na: float = 1040.98 - Mg: float = 1253.6 - Al: float = 1486.7 - Si: float = 1739.98 - P: float = 2013.7 - S: float = 2307.84 - Cl: float = 2622.39 - Ar: float = 2957.7 - K: float = 3313.8 - Ca: float = 3691.68 - Sc: float = 4090.6 - Ti: float = 4510.84 - V: float = 4952.2 - Cr: float = 5414.72 - Mn: float = 5898.75 - Fe: float = 6403.84 - Co: float = 6930.32 - Ni: float = 7478.15 - Cu: float = 8047.78 - Zn: float = 8638.86 + Li = 54.3 + Be = 108.5 + B = 183.3 + C = 277.0 + N = 392.4 + O = 524.9 # noqa: E741 + F = 676.8 + Ne = 848.6 + Na = 1040.98 + Mg = 1253.6 + Al = 1486.7 + Si = 1739.98 + P = 2013.7 + S = 2307.84 + Cl = 2622.39 + Ar = 2957.7 + K = 3313.8 + Ca = 3691.68 + Sc = 4090.6 + Ti = 4510.84 + V = 4952.2 + Cr = 5414.72 + Mn = 5898.75 + Fe = 6403.84 + Co = 6930.32 + Ni = 7478.15 + Cu = 8047.78 + Zn = 8638.86 @dataclass From b1d1485e9af806b3b393461ea432277c5177ae36 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 6 Feb 2026 18:34:05 +0000 Subject: [PATCH 10/11] Ignore stream2.py in coverage and mypy This is code from dectris --- .codecov.yml | 2 ++ pyproject.toml | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..d7fe2f8d --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "src/tickit_devices/eiger/stream/stream2.py" diff --git a/pyproject.toml b/pyproject.toml index b81c1544..8c5ea5ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ version_file = "src/tickit_devices/_version.py" [tool.mypy] ignore_missing_imports = true # Ignore missing stubs in imported modules +exclude = ["src/tickit_devices/eiger/stream/stream2.py"] [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error @@ -85,6 +86,7 @@ testpaths = "docs src tests" [tool.coverage.run] data_file = "/tmp/tickit_devices.coverage" +omit = ["src/tickit_devices/eiger/stream/stream2.py"] [tool.coverage.paths] # Tests are run from installed location, map back to the src directory From b3c2b06ee75c596019b6a080f14a56ef41b62e34 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Mon, 9 Feb 2026 12:19:33 +0000 Subject: [PATCH 11/11] Add TODO with issue link --- src/tickit_devices/eiger/stream/eiger_stream_2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tickit_devices/eiger/stream/eiger_stream_2.py b/src/tickit_devices/eiger/stream/eiger_stream_2.py index 69c8a7cf..5064cd91 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream_2.py +++ b/src/tickit_devices/eiger/stream/eiger_stream_2.py @@ -109,6 +109,8 @@ def begin_series( start = {k: v for k, v in self._start.items() if k not in START_ALL_FIELDS} # Update message with current state + # TODO: Check what fields should be updated from current state + # https://github.com/DiamondLightSource/tickit-devices/issues/122 start["number_of_images"] = settings.nimages * settings.ntrigger for stream_field, setting in STREAM_SETTINGS_MAP.items(): if stream_field not in start: