!w9yYYYj5PNtnsmfJ1$d57Yv|? z{s%q?b*ksOp)$bbGDEHak=daK(icZXv}TVlUek{$ z%>TN!SK^KF<4JyXmd^kqc(ZTykgYHl{nf9Z<5N_&s&paB8(=U^~cQhx#2T zaF-=g0+jgkok#|y30ng)F;VZ&7uVhlhv7p0@&IWD;zp+yNhSo}wK$pCNzBtt0jZ4V ze!d_v1|md~A( ScJ#+T~r5fJ-NPy4<&`OBW1kh@nX$A(?*S?(3a<_A* zd|%Fhza;i)3+JYW(I_BzPhdVq@VE$BR$%f7pyXM;fZiG(g-17r0+`f`Ya&<4ywFUj z;Q=27Hb($GhXK?+LnOeyqwmP2 ;7*Zz!(hA9s&aQFMLpOao%lHSn5!99m#EoS?2KYvtM>+!5Y-pjJ)69{v zr`Mx-K*{)&+&Fc{emE}gAW#<4{@bikC1ViH4R<=kgNs_*x8Bk;ABb-|ewsKY5b?VB z>{2p1Toi0(4qKLH40%A`9wgD-67U14cJc+1J-B|sl>}JC_DT_QD#}NSjG{drEl0tJ zm;JkSr9ba5n&k7d372m&cD4dac9aiig`=0CVHuviGCz>rXPiajlww7?_wf$ICT~5y zM@KLHXgK7Gh~?X!zWB`8O5*vR4U|l$Ssg3N0Zl?RGk^~;2JP#J3&TI$dc~I;Fd->W z1pKgqm}hwxlPqs0Tw=@u=>Vy&OzkrD5cjeE@7gU&;$KB<9704sP2cA77jTZD-FMkr zRnrYOc1e wtY2b+1;JvEbM+r$my$3buh8060Y>}_`vR$JFH0)Sl z0`#?`f4AR&@cz9UBtk9g0-tXL9@#KcI*lZ7U`Mr@SywyT`!Z(DM8EJoQgy5f_dX66 z2~-Se*dPBHgq>$3(W!5{?KdXbsPT7R4SI&VN2;E6?m!;_ghTlPIz%rQ5N7~e4(IDZ zOm&L=Th{r ~&_bG-Nm7i>~`#??z~=Oh#$p>J+Rdy$Y9nW2)XmQ z<@r#KVz`u_f4 q~ zI@NQ5)kqJx UiZqe05b!XZ7uaqb8J*NZutAaK-0*#QX^0h*Fo!OZAnLfDYe#Qeeju?qkf z3@@K8z=PKshi=^LFev9j+V%Y4tqPkCzRLpU7BxKX557t%Bg}Z=Ba=;cqU{~J*pQ0Y z&^eS?>Jt)){BYQHZTOI^pz(~G{4ixI@o|)pLLJhS&xOgx*RmqPDPcV=Y7Tmcua&fb zPEIK&Y>@N$=1-s_7JeKSv6ZYG*xUiZ>JumKea3G7il5>hwGL88A~W#`DYB76lFrwJ z!g(G-k7XuQ3&^e{miLUbl7rer$U_qePYLa)NB{*4M=%rWF_!_SHp|w2$sS?ThUo!u zw!(}oEIfSv@naCaP>d&|LosQ>o4}WmchW*>(V<^kmn+6lKCKjuMAFa)c!2LSkc0E# z^iUB+a!TK7(_b&eXqj;!hr=u3fxTv2vwT2-2gIj`_X-#r+&V1H$V=Mt-KYD*ZzNO< zc!=?ytrHTOEtKcdB~h+IBU1sOiGoewP7_hrJV0M;t%Mk!2=Q3_7WvbkLp~Lrnbr_O z&Ii-=Im{B4j4&-W_8R>uhM3~egDqPAliiNEp<7eU#1z*70Q@j2yi+(frJJB34u{SW zgN5wE?hRdUx*_s5dfh<)P-8*4SA|qdNa$}m&t7a3Qn~|gid7ASt-?(2+Fi!}IUkV9 zqvaK!8B&`%M(2KC)+11vLLgLx>iyzqc3_deuwY`~ok~66Iys><`;H-vxuNY?V(=SK zPKRGYWe7qDYY>4=<-bUbL$CBO1~i?Y@_^KaAztT;RC2(e$q*8-ao|P}2L8sK)eG+Y zT;_7>k0YH7(L|3p4}u?U6SL;L983YY^X{kzU6G<| qjALDoPh!NJ!Kq0@&cg2p&8;29(WJcqE{4Orfz&v_t31AB{Y zgXmxcauC4^K^iqX#n1rUzR$hp+ V{sZ&paEhT5zO7J z;MR5gOgjHE+Iix3N01G>pmA3KkDEHT2i*`bfRIcG&@zTZ@ZZIPv!)M5@UJxm4*qk{ zs{naw+CY&Th(H9u65HKhS17C)1qADV@ikYSZOd9L@PXWMuZMxYJ_SFNC>^Y?Ihk=8 zOcjVI;}QCH@t`bHoL)-Et_UgzbOzYuRQJ}6?%AWVi4Y4V&ssYo_`rW#C_d5e@%jZz zX6ivk9>Tx~Fpt 7FyO@AzUbiRr=F*ecS-1 z$HvbT!wknTx$G~q76U1TL|zCgi8C?cKgkc&j5CulXz|A4YyLg+Z(U(9Cex=%`&HpE z{W{SX!StfU+8BX`6ev^Cgz#w3?w1Aq3#$chR$w&xVUa}H8VS`#`$E4`5G&u`@?zX4 zAsGHG7ZRwVKGc^0w%6=Z2Bd*WV9$;epg37pellc=lr%71Yu@r-tWU_uDo>iM0+E|B zd)coh+fV*=?a|{Wg)`@CuW-0J%jR^Fs+#e7E2q7#Zb7=gt=!^xMMr;~OK{}NAAef2 zcHjOpzIO(ibEM!vN2TS=rB|}g7kWgv1g>5g{CaBCiTWqu3wCQ`r$u$HX_@PBRQICl zhK9^TtF2NIn(Ahs&K0iuPro$le{oTwG@CK)lf#P@3vEtZvbvuXaA|F($Nrt0-z?a^ zbKRTy;2TS#8)hzEa;HIMS?!$bHYYC5x^8{K|Aw=|yM-yQ<=-tx$=alUe&5w<$8-Cx z?sEJ|J@c2`W6Reh q4ASd6sHe2n* z|0IiB{jUgSG-#ESUIl9Tto )I{)bvyW8Q+qmH&J8;) Y7)vFNbaQm7c_8$M5aUTp~y} zbRw*a6)t4}(q?Ljl3X3hPQ%vNr(C@wS>GM{qi4U8Ukn^SsrW|2%#>ptkpI8rVNBOr zvq5$8*%oO?j&y!K8V0CJ@|1>bh`^&??;tETWBT`6)93xO@yPLmg71&{jWNOqAu?Fm z8HKB@=H|jyo zZp2%@mF_ywz~JhmCT#`{8oF;_%c1$KgdRN}a5a;LD!1VTG&rrHp(?ET|M _+^ z7{4N+ppD5{8qoqbzi- {$h91cT)6n6HcI?wqwF!WTVf$+uHR?@p;|ZHF~Lp zg+^XIA^Z=Sw |*^MKW;LRNfNa2a8zJ3J5S8s5c@qpOnbn|kZK5^AwnSFZq7 z-4d@mGo2dQt6hvT+x0fK#$a4MyAv|@lW;ceJD#ta%JDv;okWME813^6Lno);kN9lV zH9?uDAfNu7msxYk)9)TDvnVyft~x!nM!)w84=CLIp_iM!$19{}&l#h4 n5O+__r-W8>B0Gizl7Dw5hkD{c$bwn*Arv;lA50^?KJ8UyXd= zHJpMHiQ|$^Vn_8*N_X?gDg5L2=^>wOQwV?1MJd?O9#Vt68jfea8+C>M%I=MJv%(~i zv_cWqk#w&j1R3J54x_hx{@UHoU%F{1V$GdMub8s3`@iZlZJtZL`M~jTcJ(TmGOZnV zLVDWE9+2CHtyh>QCH%@LtS-FX2{VSPQ3>VYZH=XV&+*yC _oC>Y$H}i+pt7 zE^pO4+JlN{#W*t|$mb;bZj1%B-4Bt{Zh+rTBG}zmWg(Vk1+SPf{ve?0LAi_P2+-FW z^P?6R?$=PBJGF7taN$-lQSUhtV&Dj~i!&XrbgV@Mh39F$W0thT{LJs_9wN1dKd3ya z=Z?NT*VPo&kldEaA@1}vnsqaj2VAZWX7~j9Iahe<@BN&!2tj*@cuSh$>;u=#K*Z8& znvR9RpSe`UR9)ZSObwW=@q5C;ipXc}o*di`*Hqp5^Cp?c g`fG z``4a^{X9V9l0nk+`*K> mHsIuwGU>&WQDaEzxwwxGYRxtekd?W#&aZ%yO#Lke$J_ z_2lAe5_y1iP0f`0(kCqh+t{#Qp-Wn4t#8Dg)*v`ndg>o+{otcy8@sNYs|9IQsr5|w ztA>t(rC#=pdYNi=nE<`iZrKp{F1-zpeCF!1rI79Fxt<5KLF*r3ZK&DQ_S<{RUFNd2 zkGLId^Lm(kpwvEjUAo%dxy#sluy3@BQO-}>y_BB1F!vI+Vs`Jai$*j+Q2t +C?;q}M-nsrV$SdK}TH;gM`@L*h#@r4r$w|Dj&s94-HwI${`Ct2?u z(=W!=CfU$kPPbWKo}_*IW6eB+nIt7rs`@MkN4aVh+lWQUaa5S|qUUqZVNa%c)4~jM z4>;=2&jh8Tvw+;=bu6wY)nZ#2gyx%Kh&WVD<31`+bOH2x4DLN*^vXiZot`(t4_CW- zPCI!CVF;yRncB3uuM#Q7h&_frK+E$)(vgSm<(Z4MXYE^*QLV1*2KOMZ2n^kD_n(TH zN a+) z M|BB#GVNtzln$6`Tg|y*|DJUgR4fS=7FWX(Fx5)iTHnZqQQoE48wCFh ztVoKxw||w@6kQ)JPek0TQ08LFNhu!CxXHyJ%y|>VQ^P)abAF`vwS9{=hC;FbWZ!dJ zND8}ydQj@XK?iW>%e~|QG8z_{gqGFV%AR!e(M7W%XO=h>a;DW}+r3y5r<6+#+|i!d zDc44LKnlO}E~epjjAPv{MJXWr;Vf8u8H15%eCjHu0Df^+wx_hKoW^n3jM(FK4!bCi z{MN7Ex5q^G%rt+)AGp~G2b8=y`)SEBJy!5T @)Y-L4j1C^%Cm5!DYBq*qapOv`HMMrpc49WuV(^|ui)mq*Or%E@ zHT4tv!s=hHG({GYT$y+n)(pKn$4c?{ k 7B(S zxLq6v0bkO(qsuanGy9=ozDkb99+(Obp!;Mvca~>HYR_`$Fkxsl{*Vst_#r*hLR1+A zmWq7QKBF-0C$PG=ErZmPr||yHFp3RNFoG)QB5Xqz>N#vrs=<}6<-Q2>Sf5>ids1}9 zmAuT#^tgI-LJ7Ki)(+jB ;HH}#1g8qG9pf4vm z>hBJdMKF^`kWSx*s+ ~*{klY`67^+lX0F 9C4#a9K84H!4wvx&Zh7w*nMx^Dhc%P-LbV(}%~4jD8h2t;BrGQ4t&0yL2YOmO z{3RNOXRY#@O4dt5sYeI7)f{8cITuemKXK;)Rlww&tow?YgvI()i-rcw(;L)>&2ILQ zMaN=t@AX hc4QP zJ7bdL13;-~esPa8K&^r70C9>R514G&>QHdy<_=|(l$Vgae98eOG-@|3A<}gVx-6y^ zV?l9aAsk_qCXGH%hF2>98n Qv745Q$&fzv7l9%fK$E!rLbpK%b9)T#fz0*cS-GWs;>Ewb>MJG^1H?N>NjZ9 zvYpBJCg)0hW8gguVi~u&_-H2=dU1+QkeN Op;yz$ma+EI#T+I8V8u_xTM<7=o)tuqGlCZHf5zQWCb zaP+9om4|w?Nw_B*e6 n#6mz#qu8oO|DPD7?K~;rzfVCnC`3DX2q6U*lrs4_>-$)ll7#*ubo1D;mM9NHp zMC5Dr ty&mqzwMl@qD$u){YQryZ zZQ|Mau$Kf+t{qbe{4)Gg- 2Ewl?;YR zrjz6pJU3Zgo|XZJqS{iY)wrV)j@*x}$^NUL*>BGVo8sd>m-RZP_|hh^y0vT51Gwf7 z{(~j6*Z18ovhRM0c=*p82s`Ek|A#4`WA0$ClCs%rV7JkKctU$EUZ8d{E!*`XVdQ2{ zrTcUZ#SZt~LGHU_yzK~#N`Yy|^YTZ;9FXEP#WcS3jG)$Xk zR#)6w*IjU{Yl*#y {94$OWWIa*Y?kH^$aL%dHg8p%qG+&;- 9*FJmac^<#oEOiU=K(*dWbu#ltqb#Vs}u3lkd4<(vgcKWXzjd 3p#W?FSo6V<8CGBj-Ru6?5|0k>3Vqy6 zjcMl?My1DBP~c8ot*TdVIz>0cUL+sSm0MjzTZvkZyhpGxM7H4Pa8&c9nSS@YYYZ;M zuoO?%x~bNzlN6t9Dt0E-#=7?`rO+SHYhTsgYr)a|4zo%Edu*A=KQgT)15T+cs$& z*~<23>B2&wcZplm@|saojMS@V(7B-t7;8^rlAEVRIsNS)6W4IJ=)mDgTg*Pe-az*z zKT5yob>JzP8bvWe+^M}+>kRikU`4zUlyFVBbkZfrx=ZQ_Dn176$(zUA{kvt)`tMOH zDJH`x;&TkZ^0St2oML%^JipF(-ql}MX=(DSM&!0Q8eE(+cb2QZ!hM*U@=3YnJ A<&T{xh?hTre zpKD=osgX?sq35Klp5493$n14Li_|ZlqG0plJaG&r)|D?V`<%B>8 !{Qqdu$ri z5+7Mi=K%!#(>52`1n`H vfXB75TY FwlL zBfa&ls*l?2xCON}IrXzgIyy>N+!dVv_o~?b
v)wW+y5)M>&qDj)v|n zdyH;Pzu?(L^<2lwTNhlyvYN#8vg ^Kg$-~B1?0sMVubf`~eRrGTQmNshvr8tzQyQKU(xyJ82vDT47>| zMhUB(0fq*=aJA5M8jhJ+!~-(Z+Xx=%+ax%EeP!IRWYn)MsO`+7q;TJ&wP0@21wtj7 zk<`s D9aXd0NT6|N*V*bp)4tbtGTt3JvZZ2Z|F8Z5lk*ly}@{NNaNW7Hsv z&3Kw1T9g8BG&E*H&k2P)4R$LMHm7R5(zA&uYe_bb`}=rKUA**4t!%ted>Ks_Z 3JHa-nDWl$bZ8VfH8V)r8|6nZ=eENlp?}dibMT8R?*|4SB76E0!Bl@8`3rLK zuwD&>6T{ZLJ2C}sMDfJ5g=JHeh+c=?@fWJ_;Kr { {qBMgRxuhQ66Bk(rtON#huYs}Rtxnk;{zrZ=Fp#PiXBj$%Dc;v# z7qA^cNXL*80@Z9#fAtS>6Gg0w2~!AsqllZXUGFYe8BW&8XQe%V_rUEkYMm>vQ50 zpZ2cbVG;6Ib&sb}!l42*!L{1eCbEjUrLCCMvtjEt^ 0wi;p}uzs8QHeU?X0B@36<`6NE_>6 z*@NO{cO%GS7bbqbb;0?%fb4R^)=V !ySnC+Sha{HYH?lRB z-zyRrE33XoxDRACDkS|Ktc|Hl;^ZNskS}OpwEixsM&0|S!E7>?2TZw m$JUA?jSSKPjCFtiX^g6rQzTq?QyQGszl9iRp%;?d13Gx# z2+w$qUc5ux2< 4zb8Xj5V 6=bGYa~H42+P)2^^Ivi0`W~z;`^xtTWXCZ9ZTGdi)4aS%gh~5Rob_Ji}AF! zEgB~W_IGglOD^~SWC(r5-1Q0?T+Gq4;WW_8@;0@Vw(x)#BT&LuH-mmvkY$Rjy6>No zk`{x|-C#of1QoyVfQ>EH`weUr5zK530Y$2ZZ^Thf4SGAHC5Gf=BR~5*QHIZuQ8b=* zTuE|1D3S5?9h!emwAWE7XSQ^He3zaFX;ISSNf{p`GW{a3#cTqu2-ZlZ>u>H-9`J-i zPl&)6!H>9KjCoGlv3Jy3szH-07cqsQ_Ny=qA0tOHuB!?+bb{@Wl6#a5%sJcO^$Nt5 z%4o=jj^WkHpH>Krl1MjmL*Acg&s}NUB6@8j57?Kf*B02_5z=TX2I%{7o7tH5{8Yr7 z+6i?*_cut6k#!$TtvSl+tKB{{wM&lS1Nl&CfL|xSb?%tu3 ^M0?M?67&!MPK&c7JK%Ep9MsU1@KHM!Xn}r^AT8qR|34Yp zx%z#1;)d~!fl%<2x&&@s)#qWt=D)lIUeOn|6E92OFXAq}SGwE>(x8RdR=NeY?sfBc zRP~_S;=1VA^3g4bp*!-~-b0vjr^oC$tKcXKW-{sc>UatL`5N;hq;wpoM*Q?iCUPEB zt7?1#Al%5G2u!H|eq4!lx!ct171q=LzU?EW^%cGEGvmq~$B=<+Uk8nlIy?-FAmqIS zODy+odFOz>g?=eMKB8m8h{y*@)gsh3!i?YSxx% cPb>012}No8_&wVXE}zX-a6_J Bdk0S{S#D1+Q4U$ zj!J-}i;r)pg&MGI{`g!RkrsLp#V(X5Ql2&K=fAo0trYd3CHoF5>>7k9!eC--12EW| zFApZ$B}&V-od!*HcFAN+ic=C>^=>xNi}KRo1;Ut3H9&A5SdR2T7P75rPq*pTIbxss zNHOM4tXA$Dyq*z7VFpLLE-E^Nd{S?e8Q0L4OJC-CFC5bGS)26`DR3G(nRVW9F%1uc zv52_9qWCNqIimz;rVhtfP$Tkbma*!4SG&)aGqrC@EYcF=>5BlhPvFLnH9lpJbhV>u zY}8(jhS;srQff~JO~N 4nnpP^AflKDxMVvpbj`aVL@zQe%w9X{9!LGj%icCmXuF)SlxpH~ zTOtgM17fpg8o%q%4@^pUGkMzhfS!;(_{!GJ3+szTiVfvV7c@*>dLs*jf?Lb$Ku WS(vG#-+>tqPh%;6~a?7+gD};OX zJ4uUmqv%;pOnqBUc`IC|nY)KYK{0ggx;MGZ!9=*}Lr>F)Y5lTti7@r}a>Y7@Mp4`N z&0~>-M9L>a%8NaURh8JmXa(pf-PpTcy4rMs)ct>FHqqH_i %yQPpePDKhKzQ=nw{WYY*k<4Wq-M$-h03mSK2faUc%lw@Jz$D{$E<@ zNK@B8rb)=~C^xUllD1;+MQk%$V+)@+FZ-A%H`Mdzm1Vg{BQ1Jn@BoK)jGNOV_Ke70 zUkaX+221{;Mm?!sh+o}SLB-T`gji=yV!SZKo^N=2PIiG>@>qaS98MFOjW#kdb=CL8 zMT292YG9;Z23&iY=k0|lDUx#DB3-m!n>z{H7 Se;PPA(3Fa&P1p JRV|pV3 zGdq_0ebFO&v1lmg_sd%{^rM)dl(iZnYvlS8DlW`FjT8N*Vo(1!s}*DB)<*t0P$A-1 zk5Im^;HnImWj9P*kovP0S<-TTP jT6x9~ z7C1GO`G8&Io-eD4SlF5yVC_*xDl`(A4%YIV25aco7qlFUDZuNn1DFR6v99$cO)gxW z3uY`vjhl@X!ycZkmZ&ro09C@U0fCaUC33T$t4<)``)wolF5X>teu+~_3F40D>vQ-O zFF5ZI4g XtDW QTz@!WR7MIX$785>bvJ zg=f RE%6tn$XtV~U8{G#-F$-kseXZUlU2BdjrGBAh^7{hfx|PN)NQct7P}X6Ks)v7zQR z_7Ka5jiK3|VqM2tuw|Zc NV@#F(=@S@4E9PY4 zca1m*dvOv?mKYh8=t2o0!to@)zrA2tvq9eIQM>eHBXt5N6AVJAIx~N~9g1oyh6C-G zwVkbW=!x`NS$1%X5jbO|E98}=7~@yQ6eQMukJgSZDcvIyoseuThOim-B#3wuQZVgE zE5XgPD6wdnYV?K3iv7p~UM`oCquk)nk>k0VxZ0QDQhX8I`Tq(kiQ&7U*frfyuR12B z{paTJ2$o+T)Nc*}T}H4 E8{zjk=&Q20(w0CWvn~39G$6 z-=*0;x2O#uf+{$X4SB|yj?PP`m$jyW=ErO6VlPD10XWPLtQ^UV8XoZbHgx%w@-;kW z_YHi!+N<*m&GPv(%?m_seM2I9YBFa!r=g+Irj`pS ud&0fit- zM*kgIX+bzqTR{LDDVgpaEu$9hOZvU @@36w|`_fGM;`(8w3159y0T}fiLl6HqEUVe= zKv!u`!*Xyv@cM8=_)CyYpk9c*dvZ5um0$8zxL13o86Zn_H7~2W2F`TKatG(=M9`s; z9!z&0kZ51R15!|i43~~ktIo&Dp9xnjd`qOIq`w1XlJVp+#((PD3kL*OqY(38VXSA! z*Uhj9*7?Bf4hw8^8O6W#(j3c|kVciuPU$AKwB7E;kR2|!T+|#ouc^0dS8R+auPsXC z0Tq-@T#a#!2>REp-l$b?6LYK@NJi=5UifEJC2J_x6tRz?Yf3##ul-zMh>l;Zyk %A~z=?t<*5RECo_Ng*X;-RL z 5ER z)Wa+os}?<_j57xt-KJlJJAkRDZ02fsFIs4Y)*J6opKq3++b-!d#g57rN 6Y#9dv%R20_U-Vj8lv4~nJL*yB=ADQl%l+-OHB>@<(JSb= zx#EiK#ux5unQk@r5B0hrp>ft Bo=j>TiN+GGS0DwyMr}^1oA^*@vOD>)n)V;3OQ0yT|~gW!x*H3TBS{ z`LzwJQPz9Vw^(^>qvs?-=4v;>87gKu<^a(v;bkh}MnUT=XveA6w5hBEg@p4P`idd8 zf~m4$P3+(7+KB)?St=cY*wU0u)@Pto9CdVM{vr$!{}6Q|@}4vjT 3*Yj9rQsrL zi`)7K4^VyPIDr!Maiz3op(ktINo`ohl)Ea^DJjbN4SA_7H4-t(<2F1O`}dv3f#wA4 zJ>7 `<1<+tvFniHZUTXcRi3QDl zvY?e(DOVf+5NQ* w-35#xyP|v z3o}N<4H`%dnC2Mb0q4V>$sKtb$#f0)dt?Q@wt*F%@Z||=Ilx&;`}uJjW}`Tl3Ct4v z<$>+FBd#h*5X6amA4FVWD|66jS {?(hl{KTwK~VMGK hMMnhfiL0~{=SWF9UGOUK^(PHV>wTO&C1Q=iRE4!ZbBBud zL=HUcv0A{irjVT~)8j!e)RYyD`+^k^FcRBI4Jg;C=HxOF9rq*- JU!tx|fN+YRNQCI}6WxBOC?KLfIx4I%B|j*~o2NjmQX2 zo@F DHC7x zyOwEBkZI?*>gFd@Gl6(Hv4Dk&vxD!NI8cO+9^Z543}U2361E+kJ@?;Iok_EJz!w}i zlSHnwfJ=_y7gamABfFJau~9zID5T{B+@DO_4)PdoQ__P8+Nz8VDJfvVl`(L6@}QeE zo^ZxjWvLWB=AYGBKzRK^_{1E3 e}I7yFcAh!_TF-8nZ><~T5ZU( zDJ1xb8(<(wnr>sSj&k wkc}=+2d!nuTuVu z{t0qpKN2hio ErX)u5&uFE8iMD>0Fk#z}3#_mH!ImmD! zusc #AP{^Mf(=U>EcJ#SX<}aIc$WB_XDy^}7`={~v^B&B0Ii_~coV>=E zc#L9aA-Bx~iX!m z!-sP9N77cKPQjTom;pTCoRKYknucC`*=c%h4E< pbL&670z-S}t;*)qLd_%$lo&FI z-_yNXlIA~AR8`Lp%XgB}AGDx-TkvJ~JaMl1t*oU*QJ(nh6Q#$)V7GpLw|tcf%@lPN zcCW`I4SUqf8k~dt^z_0qa*YA^UQVvd%1uueEn2V*3Mlz3gRswC+r2SSR^|9{r)H&! z6(sm(taDp}D*;KMJ=%j;Oiynpqy={pOkHxborp~LI40&0$v(8!#jrN}MT?&8v-VU) zXiUlams{qIJ73_Edl{0GW#O|IkTpC%xncH9mw#pgq2RdXhPbT}aBWFJnQ^tuOvq;w zjyCeGs)mkrk)3HAd+Of4U^`;n_FW2cSC49JMLvBxz8Wjd7P{>-{DroU >0{~u3LHxY)ASP+Y(;cmipKYef0; N2c}`6J2nHpI4=^R(J^KgDRtQ>ISSgUajqtcRLL*C@_MP$o3uAu_j`!rV=F zstzz9A$|ysjcz8Nx0!I%)L+g{!K5V$2Mk1WzC9T?9+Ql)$RXUCmz@Ke(#Sh7Wz6Hu z=3~>uR5N1X+hGq`h!#k)U8#}RbKUroC1F^nLc)ofo^_2a538QdhQ)BU(%nmlNX>XM zXHS^o*~~=h$1Csx)~^*(#f^V@<9gwRgm}#kreQmjnw-q6XQ3bKG7&`v_c4)l|FqM0 ziw~YS0o{}y!!jiilWj|l@YTv3t$3m*X6xx{x7}NQ?E!LI88G1hk5C3qLrYpF<&sSI zBR(cLE@IuqRc(D!MlLd%Jiq!7h<*7pDU^?{Njk@$%GFuos @%!$chD7GM7UFiJ zzmn-qp5Scsd+{GC-I3E>kVz*k*V0;!Ur42Z^Zl8s6<*+|XJ$e(its3{sny6j{8-a- zMrdSVYY@k&dfwxhH>w__RL#g)4xQbOOBBY93<87ipZTPbuXG`@ZkaydZFL_!A9-9@ zUUH75^P5hZ5U$U^! J)CU=D;q*6hikaHWf1$px%<_8PrDi8coV|i*sv18&!>KR9_2tmRH>_Z~np2 zeZw-rW2nH>X*8^)m4R fGcU{*O z2mQg0QIQ7CdGl=jx5)LwbS>OzMKg%#v<;DT-hkO3+5LRoW 5RxJln7usf;PO<=ykjDlKRVt5SaUO}%>j4@4Vc3qlKL^^Xn8kWRa*aMHW zXSH((Ww`hj&Yqg=NcvGsGA#?_sZy6G1a?R>y^vzfBv8W3@(mO?ikWvJpZB0UkJ185 zEL6FVs8Mc4es`IAcWL1_TTscCPg}h-YYa3Jj$Pl{0#=!fKgg;mmE&8o2CRNjjGLq? z`||ka_v6+DlA>MTWX30K{f4^=+T;U;=xnmmBTw$7z0J&o{dUi($J!py9@b4$_)FK; zJR95%?oFaQ8b{Jdws}hNTAD`VlsOkqCz!_<{ tyF6qm29t5**#QK{{m5WDi?#cUqMwY1e;%aFFI3J%p1P z j^P#F{A?#ZbYN(KmEfA`6dZ> zo>{a`D>8p|*;re42M@?Glb^DR@f@4@uK7w^>eL?nvNT5nPVO(BkagXK&A;JN4dJ#^ zl6uF{Uz)XFf}s{*i-|kks-L$vC?goY3hz3OE&m=4tr9IT#`0bvS=aJGA&FXHfY+6} zQsDP;yA3!WaWX6i8@U@3Y0pIa9V&EcBibGyGZD7hcQGMtX2d9$c0w2QHWWc5L+}s; z-WGX`X<|W$?>TRP)3+$G;Mjy+H$3Ktc-Bl!)zG{}O*PWlYH@49wr$p+aY^h${nx%n z9dz-%@gWXzO>`5TgE-)r8?TeI%oy8tp~u-00o{@w=BZbL-0RM*=z*U#a@DYZm-@~v ztneTyLG5U%5vGd!v`x>x2Ws5&u$)$Vw*{Z+Vi <#PVZ=c>KNQ FOvkx}V6VAK6kPiLCnotXGqcO8IJ2Bt1* zdqV$NnhT68>L3Sr14rehas!>>{P{Ueom(P>JtA@HaU4S@oL&;nj*e2Jqk(J>9ovKo zWXzwnB>j>;9z~!ZZALw*O>0Iy*Jv`E&7n9ZuO=WCr)F<1iaT9=M$Owdg+;Le&pKY( zILXS)P>!Vx^Mmg91n7AQv?ONJ11f|H3m~_q%}%LjR$v5|7edws=3VjNQ0^VZ43-_{ z*>Jlq=}=le2mv!o6O2^D_NisOSKmK1K2qt*gsr+bNf49-X~&dm=qt$es?aPsKUhYi z$w(yi*xB^T9&TE)nXTo%+I>vOK9m`q_z2m2lNs6GN#;JTQl*vMc9;J+*E IciVOA6XHgb2pe1DAFTkBXLM!ATR+#W`cR$z!dH+Mg5}H~HmvUIy_DVwoNYZ2 zoFC|u7$YZ{yJR^y=+UKQYY^~M&P3+3$&=~u>8BMTFX1vX$FzLo_{i0Ar@VChm1d|l zDuxtZ+4In^$_sAQJk#+vYV~e|3y&j#cbasX?L*Wmy7N6RWa(9mc`LHsAWqJr;Z9kL zYmv(dI>C{pU51Wg5ehl+%@^T!-8q)z9{*m8s@ut=9wJ*A)4dXVfMuGG@@sQ8r !P!kKBPq4 zW7&MP*?;y_SySCT*)tCu!6RgqkIAjn2G4Q2f<{lBZY=-*in|)Grs^%~xUUu)@ H#`l+zqyi*}f)N8Kj^BC^!{C9N4NT_08p`5=sLwSWr34f20ic*cBjeo(Ke;FS6 zs+0RZf_g!ra*1Pl{jDX?D4L5pV1hN_M9)dNR8~zPkaaZrnZgqN=Xzfat ?h~GCg zlqY%`x#(ocY1^+4TaNI>BF!I!=?$tLFS^G?qA;X9{2e-JwcQ_Q!AWL3aiR92Abo7( z;%2*PQk5jQOj(`1t9$9ffvmN|wt>T(7Ox9eW$LW#&ZtE@gtgUOYRb0C??+_~j{tVG z;-A6f;n=uA{i@%>4)zouSW(=U+dsNR)6`OVA-4O{gB$ypxgFqNqsmk!AFUAev(FuH zcNX?m3e~q5;I$v9;kS4(Khz97?ymd-g?7hnw% 3EQt712BpTiO3}rgKMj<41KZ;C`BrK=ZeLzd}F7rm8FXI%?iOEPtO1 zF-e=dTg!!qVr0o@^Yl!H5rrx$ B?uWK%f|Fda}PQEaRo3e6&cudjRSsN6;HSU|h zGf+0~bIBi+{UB|#CQZLdL*rg?mHtXx6Fg{jS5@W0U1#%N3b3^6Q~Fg85i^1{n$fS9 zEWg*M9zS_{o`=FYD#wao`z_(l_a>;56PfPimR$>~`T4%{s@|OOSGZet_b8k3XcC}K zYR1*7{kWTMXF-w6o)b-gwRt!>opT6G6dQ0A1A7n)ZeP=V#s1Y~{$R1%YrIaX_Nx4v zrQ>vq^t7@KRr{hMmoGO@cNv>cVbTTBaqGNCS~Q9j7Nx#ot&G`F{;l+zaCPVSyS=H` znLhp;;Rthk#ZaSW{G!S=JLb#G+FzoVN8FV;_}MBsr3BZA_M;Vrj8b)s6P*{)5w%rZ z-z(x~(tZF63M{y0RjBCzAYLr0Tj^%yAW&T@Nuc2?A5b!ae?j^}ijQNGLl|bFGW; 0mQ-OLNG-&KCO>~Hxy*SZ*uf^(cyO=_FdcV%9$WQKNwu+2w =BCOia&PznzC$F9+CH8LL#h7dkr?2)Lp^`EG1`rV|LQEA1v=WMVbOLH^1 zui)PYd-T@*v3ZR=nZB&e2b{Hdi1Jmt3;nERvF)@)7IhzsYg*~o1Y!@h&jz}F#OiZB zW#@2cvg^5{H}|FMXn) #KxGn^muKM#8QbbADB!zIj_%} zA?=a|F3=j<($r@rYSkv$dvlwPsU#H%7_Ii?9crAT*44|QnjstutIh!FMP{{@i1`Uj zKH0$=S7=2ctSX+b`F3m6JYceAN;Ak^pAIlk4UJ*`CYy3554_Th>tWMn{$tRhjr7&0 zsCUMCjvZcq(9lxj3X7=>PxwReI~ZoR^4E6-tVzEFUvY}9b8zHUgPi+D2n!BbsKZ#F zua)vxT8G(V)#y!qkZ9(EEhm3LKd{y#gs5H02$ooO&uS`;^b Mk>j5+{g)EY-zz021n#f3lLO(?e;n7a;8k#=%J%9&M>Dj7 zdXnr+6DI}(y8=Ar$L@#Cm4k;NG!}mBXRsFL1PPA}oq$ISZ1#oJEAOTz66V{`?IA&& z3xR p{Yaj`fIy1nR8!7bsr-#e`EvVdZ4Gf_04we-ZsV-7DljXlrp$Rj zz>u%}t&z#EA#g$wsb{{c$>Q85g0(fM)GWaTy`XZRQOTWhNDquROg#Naf;2L(&rP}z z4~r6hTVX*}F#HLUDiEB!4LJcKt|2V(KpGR7q)nhbMC8~ ` z%($-;5+pVrS4n6u1?*NFCd`FQ6HkGK91EzJ>m C#^C9Z&&C)qpk-~7FNHQSGr1Px zLzb12|6E@_K`z8c9-6ZC`^81agpm1vGNVBQ1z?Rlj^W (KrI6hJ8B( Date: Sun, 22 Dec 2024 23:45:08 -0800 Subject: [PATCH 002/184] adding optogenetics tagging to toolbar --- src/foraging_gui/ForagingGUI_Ephys.ui | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index d2e3e3371..3c9f74531 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -4620,6 +4620,7 @@ + @@ -4745,6 +4746,7 @@ + @@ -4765,7 +4767,6 @@ - + @@ -5013,7 +5014,7 @@ -- +resources/pngtree-letter-t-logo-png-png-image_6100844.jpg resources/pngtree-letter-t-logo-png-png-image_6100844.jpgresources/OpticalTagging.jpg resources/OpticalTagging.jpgOptical Tagging From 2e1272505204e6d9f4ed25d575e4c624dc1f5ace Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:47:42 -0800 Subject: [PATCH 003/184] adding optical tagging to the main function --- src/foraging_gui/Foraging.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index e30abd628..2be1af0b7 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -184,6 +184,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self.OpenLaserCalibration=0 self.OpenCamera=0 self.OpenMetadata=0 + self.OpenOpticalTagging=0 self.NewTrialRewardOrder=0 self.LickSta=0 self.LickSta_ToInitializeVisual=1 @@ -205,6 +206,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self._LaserCalibration()# to open the laser calibration panel self._WaterCalibration()# to open the water calibration panel self._Camera() + self._OpticalTagging() self._InitializeMotorStage() self._load_stage() self._Metadata() @@ -2338,6 +2340,16 @@ def _Camera(self): else: self.Camera_dialog.hide() + def _OpticalTagging(self): + '''Open the optical tagging dialog''' + if self.OpenOpticalTagging==0: + self.OpticalTagging_dialog = OpticalTaggingDialog(MainWindow=self) + self.OpenOpticalTagging=1 + if self.actionOptical_Tagging.isChecked()==True: + self.OpticalTagging_dialog.show() + else: + self.OpticalTagging_dialog.hide() + def _Metadata(self): '''Open the metadata dialog''' if self.OpenMetadata==0: From d54b6aa7d975e68012ac2afeb919e387c0627946 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:51:29 -0800 Subject: [PATCH 004/184] adding OpticalTaggingDialog --- src/foraging_gui/Dialogs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f785937e0..dd12473e2 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3029,3 +3029,9 @@ def headerData(self, col, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self._data.columns[col] return None + +class OpticalTaggingDialog(QDialog): + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('OpticalTagging.ui', self) + \ No newline at end of file From 66debaf35fee453f8baf3ff67ded3f80e99dcd77 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:53:22 -0800 Subject: [PATCH 005/184] adding OpticalTagging ui --- src/foraging_gui/OpticalTagging.ui | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/foraging_gui/OpticalTagging.ui diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui new file mode 100644 index 000000000..93ceec3e2 --- /dev/null +++ b/src/foraging_gui/OpticalTagging.ui @@ -0,0 +1,19 @@ + ++ From a37f2695dc15a784d9ec5125726dc7a1dc4228c2 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:54:57 -0800 Subject: [PATCH 006/184] loading OpticalTaggingDialog --- src/foraging_gui/Foraging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 2be1af0b7..903564858 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -42,7 +42,7 @@ import foraging_gui.rigcontrol as rigcontrol from foraging_gui.Visualization import PlotV,PlotLickDistribution,PlotTimeDistribution from foraging_gui.Dialogs import OptogeneticsDialog,WaterCalibrationDialog,CameraDialog,MetadataDialog -from foraging_gui.Dialogs import LaserCalibrationDialog +from foraging_gui.Dialogs import LaserCalibrationDialog,OpticalTaggingDialog from foraging_gui.Dialogs import LickStaDialog,TimeDistributionDialog from foraging_gui.Dialogs import AutoTrainDialog, MouseSelectorDialog from foraging_gui.MyFunctions import GenerateTrials, Worker,TimerWorker, NewScaleSerialY, EphysRecording From 11118db55d38e20f1a678228060b067a2d9d9bfc Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:56:32 -0800 Subject: [PATCH 007/184] connected to _OpticalTagging --- src/foraging_gui/Foraging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 903564858..a3551a136 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -321,6 +321,7 @@ def connectSignalsSlots(self): self.action_About.triggered.connect(self._about) self.action_Camera.triggered.connect(self._Camera) self.actionMeta_Data.triggered.connect(self._Metadata) + self.actionOptical_Tagging.triggered.connect(self._OpticalTagging) self.action_Optogenetics.triggered.connect(self._Optogenetics) self.actionLicks_sta.triggered.connect(self._LickSta) self.actionTime_distribution.triggered.connect(self._TimeDistribution) From 1f5143f9dffdb53ea57c42d07e263c7204c9ccb5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:58:57 -0800 Subject: [PATCH 008/184] set it as checkable --- src/foraging_gui/ForagingGUI_Ephys.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index 3c9f74531..5ba27694f 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -5012,6 +5012,9 @@OpticalTagging ++ ++ ++ +0 +0 +703 +474 ++ +Optical Tagging ++ + + + true +@@ -390,7 +390,7 @@ From 37ea62c935c50fb5f609da48b5f320668e479800 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:07:26 -0800 Subject: [PATCH 009/184] updated the Optical Tagging ui --- src/foraging_gui/OpticalTagging.ui | 428 +++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 93ceec3e2..6d5a780e6 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -13,6 +13,434 @@ resources/OpticalTagging.jpg resources/OpticalTagging.jpg+ Optical Tagging + ++ ++ +60 +100 +76 +20 ++ ++ +90 +16777215 ++ +lasers= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +60 +70 +76 +20 ++ ++ +90 +16777215 ++ +laser color= ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +140 +130 +70 +20 ++ ++ +0 +0 ++ ++ +1000 +16777215 +- +
++ +Pulse ++ ++ +true ++ ++ +58 +250 +76 +20 ++ ++ +90 +16777215 ++ +pulse dur(s)= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +58 +130 +76 +20 ++ ++ +90 +16777215 ++ +protocol= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +20 +190 +116 +20 ++ +laser_2 power (mw)= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +54 +220 +80 +20 ++ ++ +90 +16777215 ++ +frequency (Hz)= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ ++ +140 +70 +70 +20 ++ ++ +0 +0 ++ ++ +70 +16777215 ++ +4 +- +
++ +Blue +- +
++ +Red +- +
++ +Orange +- +
++ +Green +- +
++ +NA ++ ++ ++ +140 +100 +70 +20 ++ ++ +0 +0 ++ ++ +1000 +16777215 +- +
++ +Both +- +
++ +Laser_1 +- +
++ +Laser_2 ++ ++ +true ++ ++ +140 +250 +70 +20 ++ ++ +1000 +16777215 ++ +0.002 ++ +false ++ ++ ++ +20 +160 +116 +20 ++ +laser_1 power (mw)= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ +true ++ ++ +140 +220 +70 +20 ++ ++ +1000 +16777215 ++ +20 ++ +false ++ ++ +true ++ ++ +140 +160 +70 +20 ++ ++ +1000 +16777215 ++ +1,5,10 ++ +false ++ ++ +true ++ ++ +140 +190 +70 +20 ++ ++ +1000 +16777215 ++ +1,5,10 ++ +false ++ ++ ++ +140 +32 +70 +31 ++ +Start ++ ++ +true ++ ++ +60 +280 +76 +20 ++ ++ +90 +16777215 ++ +location tag= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ + ++ +140 +280 +71 +22 +From 70a2e7c5dd7c31e4c6043ae68b93848a9db4f13e Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:09:56 -0800 Subject: [PATCH 010/184] updated the optical tagging ui --- src/foraging_gui/OpticalTagging.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 6d5a780e6..5f38a9b64 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -6,7 +6,7 @@ 0 0 -703 +265 474 false + 140 From fc6aa8edeaf7797d8456400fb1aad3eb97922272 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:12:37 -0800 Subject: [PATCH 011/184] set Start checkable --- src/foraging_gui/OpticalTagging.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 5f38a9b64..671d806f7 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -402,6 +402,9 @@+ Start + true +From 6f6a191625314b0175466910be83693277bddb6a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:37:49 -0800 Subject: [PATCH 012/184] toggle color and disable widgets --- src/foraging_gui/Dialogs.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index dd12473e2..26f98d1c5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3034,4 +3034,26 @@ class OpticalTaggingDialog(QDialog): def __init__(self, MainWindow, parent=None): super().__init__(parent) uic.loadUi('OpticalTagging.ui', self) - \ No newline at end of file + self._connectSignalsSlots() + + def _connectSignalsSlots(self): + self.Start.clicked.connect(self._Start) + self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) + + def _Start(self): + '''Start the optical tagging''' + # toggle the button color + if self.Start.isChecked(): + self.Start.setStyleSheet("background-color : green;") + else: + self.Start.setStyleSheet("background-color : none") + + def _WhichLaser(self): + '''Select the laser to use and disable non-relevant widgets''' + laser_name = self.WhichLaser.currentText() + if laser_name=='Laser_1': + self.Laser_2_power.setEnabled(False) + self.label1_16.setEnabled(False) + elif laser_name=='Laser_2': + self.Laser_1_power.setEnabled(False) + self.label1_3.setEnabled(False) From c67388cc3b536498d32563b0020fc1c7dae497f3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:40:01 -0800 Subject: [PATCH 013/184] enable widgets --- src/foraging_gui/Dialogs.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 26f98d1c5..396326a38 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3054,6 +3054,15 @@ def _WhichLaser(self): if laser_name=='Laser_1': self.Laser_2_power.setEnabled(False) self.label1_16.setEnabled(False) + self.label1_3.setEnabled(True) + self.Laser_1_power.setEnabled(True) elif laser_name=='Laser_2': self.Laser_1_power.setEnabled(False) self.label1_3.setEnabled(False) + self.label1_16.setEnabled(True) + self.Laser_2_power.setEnabled(True) + else: + self.Laser_1_power.setEnabled(True) + self.Laser_2_power.setEnabled(True) + self.label1_3.setEnabled(True) + self.label1_16.setEnabled(True) From 620ee2d59c26b124fed06fc54b6ca5e5d8707577 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:46:38 -0800 Subject: [PATCH 014/184] updating the ui --- src/foraging_gui/OpticalTagging.ui | 170 +++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 671d806f7..ec0efac5b 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -6,8 +6,8 @@ 0 0 -265 -474 +275 +530 @@ -402,9 +402,6 @@ - Start - true -+ @@ -413,7 +410,7 @@ @@ -438,12 +435,171 @@ 60 -280 +400 76 20 140 -280 +400 71 22 + ++ +true ++ ++ +15 +300 +121 +20 ++ ++ +1000 +16777215 ++ +duration each cycle= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ +true ++ ++ +140 +300 +70 +20 ++ ++ +1000 +16777215 ++ +1 ++ +false ++ ++ +true ++ ++ +15 +330 +121 +20 ++ ++ +1000 +16777215 ++ +interval between cycles= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ++ ++ +true ++ ++ +140 +330 +70 +20 ++ ++ +1000 +16777215 ++ +0.5 ++ +false ++ ++ +true ++ ++ +140 +360 +70 +20 ++ ++ +1000 +16777215 ++ +100 ++ +false ++ + +true ++ ++ +5 +360 +131 +20 ++ ++ +1000 +16777215 ++ +cycles each laser power= ++ +Qt::AutoText ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter +From ff20908c6bb2a2dedcf336ada03d930eb57f49c7 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:56:21 -0800 Subject: [PATCH 015/184] separating laser 1 color and laser 2 color --- src/foraging_gui/OpticalTagging.ui | 120 +++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 24 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index ec0efac5b..10c193110 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -17,7 +17,7 @@ @@ -42,7 +42,7 @@ 60 -100 +70 76 20 @@ -54,7 +54,7 @@ 60 -70 +130 76 20 - laser color= +laser_1 color= Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -64,7 +64,7 @@@@ -94,7 +94,7 @@ 140 -130 +100 70 20 @@ -119,7 +119,7 @@ 58 -250 +280 76 20 @@ -16,8 +16,8 @@ @@ -144,7 +144,7 @@ 58 -130 +100 76 20 - @@ -163,7 +163,7 @@ 20 -190 +220 116 20 - @@ -184,11 +184,11 @@ 54 -220 +250 80 20 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + @@ -238,7 +238,7 @@ 140 -70 +130 70 20 @@ -278,7 +278,7 @@ 140 -100 +70 70 20 @@ -300,7 +300,7 @@ 140 -250 +280 70 20 @@ -322,7 +322,7 @@ 20 -160 +190 116 20 @@ -347,7 +347,7 @@ 140 -220 +250 70 20 @@ -372,7 +372,7 @@ 140 -160 +190 70 20 @@ -410,7 +410,7 @@ 140 -190 +220 70 20 @@ -435,7 +435,7 @@ 60 -400 +430 76 20 @@ -448,7 +448,7 @@ 140 -400 +430 71 22 @@ -476,7 +476,7 @@ 15 -300 +330 121 20 @@ -501,7 +501,7 @@ 140 -300 +330 70 20 @@ -529,7 +529,7 @@ 15 -330 +360 121 20 @@ -554,7 +554,7 @@ 140 -330 +360 70 20 @@ -579,7 +579,7 @@ 140 -360 +390 70 20 @@ -600,6 +600,78 @@ 5 -360 +390 131 20 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + ++ ++ +140 +160 +70 +20 ++ ++ +0 +0 ++ ++ +70 +16777215 ++ +4 +- +
++ +Blue +- +
++ +Red +- +
++ +Orange +- +
++ +Green +- +
++ +NA ++ + ++ +60 +160 +76 +20 ++ ++ +90 +16777215 ++ +laser_2 color= ++ +Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter +From 142161856a0f52b55fc0a5ee4e495a6a8e920c38 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:57:50 -0800 Subject: [PATCH 016/184] prototype --- src/foraging_gui/Dialogs.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 396326a38..84628ec8a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3035,7 +3035,8 @@ def __init__(self, MainWindow, parent=None): super().__init__(parent) uic.loadUi('OpticalTagging.ui', self) self._connectSignalsSlots() - + self.MainWindow = MainWindow + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) @@ -3047,7 +3048,15 @@ def _Start(self): self.Start.setStyleSheet("background-color : green;") else: self.Start.setStyleSheet("background-color : none") + # produce the waveforms + self._produce_waveforms() + # initiate the laser + # receiving the timestamps of laser start + + # save the timestamps + + # save parameters def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' laser_name = self.WhichLaser.currentText() @@ -3066,3 +3075,13 @@ def _WhichLaser(self): self.Laser_2_power.setEnabled(True) self.label1_3.setEnabled(True) self.label1_16.setEnabled(True) + + def _produce_waveforms(self): + '''Produce the waveforms for the optical tagging''' + # get the amplitude + self._get_lasers_amplitude() + + def _get_lasers_amplitude(self): + '''Get the amplitude of the laser based on the calibraion results''' + # get the current calibration results + \ No newline at end of file From 2c33e13b6a19118c133a5cba57d0d4772d80ae6c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:02:24 -0800 Subject: [PATCH 017/184] updated the name --- src/foraging_gui/OpticalTagging.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 10c193110..39cb8be6b 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -184,7 +184,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + +- 140 @@ -600,7 +600,7 @@Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 140 From e63e1f1b70d81bfea3bae78a265070c952f451a3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:44:05 -0800 Subject: [PATCH 018/184] produce waveform --- src/foraging_gui/Dialogs.py | 71 ++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 84628ec8a..a4a3b672e 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -24,6 +24,7 @@ from aind_auto_train.auto_train_manager import DynamicForagingAutoTrainManager from aind_auto_train.schema.task import TrainingStage from aind_auto_train.schema.curriculum import DynamicForagingCurriculum +from foraging_gui.GenerateMetadata import generate_metadata codebase_curriculum_schema_version = DynamicForagingCurriculum.model_fields['curriculum_schema_version'].default logger = logging.getLogger(__name__) @@ -3030,13 +3031,15 @@ def headerData(self, col, orientation, role): return self._data.columns[col] return None + class OpticalTaggingDialog(QDialog): + def __init__(self, MainWindow, parent=None): super().__init__(parent) uic.loadUi('OpticalTagging.ui', self) self._connectSignalsSlots() self.MainWindow = MainWindow - + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) @@ -3078,10 +3081,68 @@ def _WhichLaser(self): def _produce_waveforms(self): '''Produce the waveforms for the optical tagging''' + # get the laser name + if self.WhichLaser.currentText()=="Both": + laser_name_selected = ['Laser_1','Laser_2'] + else: + laser_name_selected = [self.WhichLaser.currentText()] # get the amplitude - self._get_lasers_amplitude() + protocol = self.Protocol.currentText() + for current_laser_name in laser_name_selected: + if current_laser_name=='Laser_1': + target_power = self.Laser_1_power.value() + laser_color = self.Laser_1_color.currentText() + elif current_laser_name=='Laser_2': + target_power = self.Laser_2_power.value() + laser_color = self.Laser_2_color.currentText() + else: + raise ValueError(f"Unknown laser name: {current_laser_name}") + if protocol!='Pulse': + raise ValueError(f"Unknown protocol: {protocol}") + else: + self._get_lasers_amplitude(target_power,laser_color,protocol) + # get the waveform + + # save the waveform + + - def _get_lasers_amplitude(self): - '''Get the amplitude of the laser based on the calibraion results''' + def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str)->float: + '''Get the amplitude of the laser based on the calibraion results + Args: + target_power: The target power of the laser. + laser_color: The color of the laser. + protocol: The protocol to use. + Returns: + float: The amplitude of the laser. + ''' # get the current calibration results - \ No newline at end of file + latest_calibration_date=find_latest_calibration_date(self.MainWindow.LaserCalibrationResults,laser_color) + # get the selected laser + if latest_calibration_date=='NA': + logger.error(f"No calibration results found for {laser_color}") + else: + calibration_results=self.MainWindow.LaserCalibrationResults[latest_calibration_date][laser_color][protocol] + +def find_latest_calibration_date(calibration:list,Laser:str)->str: + """ + Find the latest calibration date for the selected laser. + + Args: + calibration: The calibration object. + Laser: The selected laser name. + + Returns: + str: The latest calibration date for the selected laser. + """ + if not hasattr(calibration,'LaserCalibrationResults') : + return 'NA' + Dates=[] + for Date in calibration.LaserCalibrationResults: + if Laser in calibration.LaserCalibrationResults[Date].keys(): + Dates.append(Date) + sorted_dates = sorted(Dates) + if sorted_dates==[]: + return 'NA' + else: + return sorted_dates[-1] \ No newline at end of file From 964a838bdc95875e51d806ac55fa1a70e327e36c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:48:24 -0800 Subject: [PATCH 019/184] changed the data type to float --- src/foraging_gui/Dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a4a3b672e..fbdb07fc5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3090,10 +3090,10 @@ def _produce_waveforms(self): protocol = self.Protocol.currentText() for current_laser_name in laser_name_selected: if current_laser_name=='Laser_1': - target_power = self.Laser_1_power.value() + target_power = float(self.Laser_1_power.currentText()) laser_color = self.Laser_1_color.currentText() elif current_laser_name=='Laser_2': - target_power = self.Laser_2_power.value() + target_power = float(self.Laser_2_power.currentText()) laser_color = self.Laser_2_color.currentText() else: raise ValueError(f"Unknown laser name: {current_laser_name}") From 7b434a25ded5aacff471bcaac363f6c4f4e10ffb Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:53:55 -0800 Subject: [PATCH 020/184] grammar --- src/foraging_gui/Dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index fbdb07fc5..581a7e214 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3090,10 +3090,10 @@ def _produce_waveforms(self): protocol = self.Protocol.currentText() for current_laser_name in laser_name_selected: if current_laser_name=='Laser_1': - target_power = float(self.Laser_1_power.currentText()) + target_power = float(self.Laser_1_power.text()) laser_color = self.Laser_1_color.currentText() elif current_laser_name=='Laser_2': - target_power = float(self.Laser_2_power.currentText()) + target_power = float(self.Laser_2_power.text()) laser_color = self.Laser_2_color.currentText() else: raise ValueError(f"Unknown laser name: {current_laser_name}") From 9fd8f66ef1136c1031c1a9ee6e973a3b049e7930 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:49:16 -0800 Subject: [PATCH 021/184] _generate_random_conditions --- src/foraging_gui/Dialogs.py | 54 ++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 581a7e214..975ec16b5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3051,6 +3051,8 @@ def _Start(self): self.Start.setStyleSheet("background-color : green;") else: self.Start.setStyleSheet("background-color : none") + # generate random conditions including lasers, laser power, laser color, and protocol + self._generate_random_conditions() # produce the waveforms self._produce_waveforms() # initiate the laser @@ -3060,6 +3062,57 @@ def _Start(self): # save the timestamps # save parameters + + def _generate_random_conditions(self): + """ + Generate random conditions for the optical tagging process. Each condition corresponds to one cycle, with parameters randomly selected for each cycle. + + The parameters are chosen as follows: + - **Lasers**: One of the following is selected: `Laser_1` or `Laser_2`. + - **Laser Power**: If `Laser_1` is selected, `Laser_1_power` is used. If `Laser_2` is selected, `Laser_2_power` is used. + - **Laser Color**: If `Laser_1` is selected, `Laser_1_color` is used. If `Laser_2` is selected, `Laser_2_color` is used. + *Note: `Laser`, `Laser Power`, and `Laser Color` are selected together as a group.* + + Additional parameters: + - **Protocol**: Currently supports only `Pulse`. + - **Frequency (Hz)**: Applied to both lasers during the cycle. + - **Pulse Duration (s)**: Applied to both lasers during the cycle. + """ + # get the number of cycles + number_of_cycles = int(self.Cycles_each_power.text()) + # get the frequency + frequency = float(self.Frequency.text()) + # get the pulse duration + pulse_duration = float(self.Pulse_duration.text()) + # get the protocol + protocol = self.Protocol.currentText() + if protocol!='Pulse': + raise ValueError(f"Unknown protocol: {protocol}") + # get the laser name + if self.WhichLaser.currentText()=="Both": + laser_name = ['Laser_1','Laser_2'] + else: + laser_name = [self.WhichLaser.currentText()] + + + # get the amplitude + for current_laser_name in laser_name: + if current_laser_name=='Laser_1': + target_power = float(self.Laser_1_power.text()) + laser_color = self.Laser_1_color.currentText() + elif current_laser_name=='Laser_2': + target_power = float(self.Laser_2_power.text()) + laser_color = self.Laser_2_color.currentText() + else: + raise ValueError(f"Unknown laser name: {current_laser_name}") + if protocol!='Pulse': + raise ValueError(f"Unknown protocol: {protocol}") + else: + self._get_lasers_amplitude(target_power,laser_color,protocol) + # get the waveform + + # save the waveform + def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' laser_name = self.WhichLaser.currentText() @@ -3106,7 +3159,6 @@ def _produce_waveforms(self): # save the waveform - def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str)->float: '''Get the amplitude of the laser based on the calibraion results Args: From d6711f07fa4e829e6b86aa1b60087999a00001c9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:32:50 -0800 Subject: [PATCH 022/184] generate random conditions --- src/foraging_gui/Dialogs.py | 101 +++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 975ec16b5..e3b6a1391 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -7,6 +7,8 @@ from datetime import datetime import logging import webbrowser +import re +import random from typing import Literal import numpy as np @@ -3039,7 +3041,8 @@ def __init__(self, MainWindow, parent=None): uic.loadUi('OpticalTagging.ui', self) self._connectSignalsSlots() self.MainWindow = MainWindow - + self.optical_tagging_par={} + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) @@ -3065,7 +3068,7 @@ def _Start(self): def _generate_random_conditions(self): """ - Generate random conditions for the optical tagging process. Each condition corresponds to one cycle, with parameters randomly selected for each cycle. + Generate random conditions for the optical tagging process. Each condition corresponds to one cycle, with parameters randomly selected for each duration. The parameters are chosen as follows: - **Lasers**: One of the following is selected: `Laser_1` or `Laser_2`. @@ -3078,40 +3081,66 @@ def _generate_random_conditions(self): - **Frequency (Hz)**: Applied to both lasers during the cycle. - **Pulse Duration (s)**: Applied to both lasers during the cycle. """ - # get the number of cycles - number_of_cycles = int(self.Cycles_each_power.text()) - # get the frequency - frequency = float(self.Frequency.text()) - # get the pulse duration - pulse_duration = float(self.Pulse_duration.text()) # get the protocol protocol = self.Protocol.currentText() if protocol!='Pulse': raise ValueError(f"Unknown protocol: {protocol}") + # get the number of cycles + number_of_cycles = int(math.floor(float(self.Cycles_each_power.text()))) + # get the frequency + frequency_list = list(map(int, extract_numbers_from_string(self.Frequency.text()))) + # get the pulse duration (seconds) + pulse_duration_list = extract_numbers_from_string(self.Pulse_duration.text()) # get the laser name if self.WhichLaser.currentText()=="Both": - laser_name = ['Laser_1','Laser_2'] + laser_name_list = ['Laser_1','Laser_2'] + laser_config = { + 'Laser_1': (self.Laser_1_power, self.Laser_1_color), + 'Laser_2': (self.Laser_2_power, self.Laser_2_color) + } else: - laser_name = [self.WhichLaser.currentText()] + laser_name_list = [self.WhichLaser.currentText()] + if laser_name_list[0]=='Laser_1': + laser_config = { + 'Laser_1': (self.Laser_1_power, self.Laser_1_color) + } + elif laser_name_list[0]=='Laser_2': + laser_config = { + 'Laser_2': (self.Laser_2_power, self.Laser_2_color) + } - - # get the amplitude - for current_laser_name in laser_name: - if current_laser_name=='Laser_1': - target_power = float(self.Laser_1_power.text()) - laser_color = self.Laser_1_color.currentText() - elif current_laser_name=='Laser_2': - target_power = float(self.Laser_2_power.text()) - laser_color = self.Laser_2_color.currentText() - else: - raise ValueError(f"Unknown laser name: {current_laser_name}") - if protocol!='Pulse': - raise ValueError(f"Unknown protocol: {protocol}") - else: - self._get_lasers_amplitude(target_power,laser_color,protocol) - # get the waveform - - # save the waveform + # Generate combinations for each laser + protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled = zip(*[ + (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText()) + for frequency in frequency_list + for pulse_duration in pulse_duration_list + for laser_name, (power_field, _) in laser_config.items() + for target_power in extract_numbers_from_string(power_field.text()) + ]) + + self.optical_tagging_par.protocol_sampled_all = [] + self.optical_tagging_par.frequency_sampled_all = [] + self.optical_tagging_par.pulse_duration_sampled_all = [] + self.optical_tagging_par.laser_name_sampled_all = [] + self.optical_tagging_par.target_power_sampled_all = [] + self.optical_tagging_par.laser_color_sampled_all = [] + for _ in range(number_of_cycles): + # Generate a random index to sample conditions + random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) + # Use the random indices to shuffle the conditions + protocol_sampled_now = [protocol_sampled[i] for i in random_indices] + frequency_sampled_now = [frequency_sampled[i] for i in random_indices] + pulse_duration_sampled_now = [pulse_duration_sampled[i] for i in random_indices] + laser_name_sampled_now = [laser_name_sampled[i] for i in random_indices] + target_power_sampled_now = [target_power_sampled[i] for i in random_indices] + laser_color_sampled_now = [laser_color_sampled[i] for i in random_indices] + # Append the conditions + self.optical_tagging_par.protocol_sampled_all.extend(protocol_sampled_now) + self.optical_tagging_par.optical_tagging_par.frequency_sampled_all.extend(frequency_sampled_now) + self.optical_tagging_par.pulse_duration_sampled_all.extend(pulse_duration_sampled_now) + self.optical_tagging_par.laser_name_sampled_all.extend(laser_name_sampled_now) + self.optical_tagging_par.target_power_sampled_all.extend(target_power_sampled_now) + self.optical_tagging_par.laser_color_sampled_all.extend(laser_color_sampled_now) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' @@ -3197,4 +3226,18 @@ def find_latest_calibration_date(calibration:list,Laser:str)->str: if sorted_dates==[]: return 'NA' else: - return sorted_dates[-1] \ No newline at end of file + return sorted_dates[-1] + +def extract_numbers_from_string(input_string:str)->list: + """ + Extract numbers from a string. + + Args: + string: The input string. + + Returns: + list: The list of numbers. + """ + # Regular expression to match floating-point numbers + float_pattern = r"[-+]?\d*\.\d+|\d+" # Matches numbers like 0.4, -0.5, etc. + return [float(num) for num in re.findall(float_pattern, input_string)] \ No newline at end of file From 47dea1eb0954ec52a922e8eadc7cc96fd519a8fa Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:37:10 -0800 Subject: [PATCH 023/184] changing pulse duration to ms --- src/foraging_gui/Dialogs.py | 4 ++-- src/foraging_gui/OpticalTagging.ui | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index e3b6a1391..3e8cc7da4 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3042,7 +3042,7 @@ def __init__(self, MainWindow, parent=None): self._connectSignalsSlots() self.MainWindow = MainWindow self.optical_tagging_par={} - + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) @@ -3079,7 +3079,7 @@ def _generate_random_conditions(self): Additional parameters: - **Protocol**: Currently supports only `Pulse`. - **Frequency (Hz)**: Applied to both lasers during the cycle. - - **Pulse Duration (s)**: Applied to both lasers during the cycle. + - **Pulse Duration (ms)**: Applied to both lasers during the cycle. """ # get the protocol protocol = self.Protocol.currentText() diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 39cb8be6b..5606bae89 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -106,7 +106,7 @@- pulse dur(s)= +pulse dur(ms)= Qt::AutoText @@ -290,7 +290,7 @@- 0.002 +2 false From ca83df605b4d618336250a90306efd3efe040564 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:27:03 -0800 Subject: [PATCH 024/184] get input voltage --- src/foraging_gui/Dialogs.py | 134 +++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 54 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 3e8cc7da4..e106b8980 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -9,10 +9,11 @@ import webbrowser import re import random -from typing import Literal +from typing import Literal,Tuple import numpy as np import pandas as pd +from sklearn.linear_model import LinearRegression from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QMessageBox, QGridLayout from PyQt5.QtWidgets import QLabel, QDialogButtonBox,QFileDialog,QInputDialog, QLineEdit @@ -3056,15 +3057,26 @@ def _Start(self): self.Start.setStyleSheet("background-color : none") # generate random conditions including lasers, laser power, laser color, and protocol self._generate_random_conditions() - # produce the waveforms - self._produce_waveforms() - # initiate the laser - # receiving the timestamps of laser start + # iterate each condition + for i in range(len(self.optical_tagging_par['protocol_sampled_all'])): + # get the current parameters + protocol = self.optical_tagging_par['protocol_sampled_all'][i] + frequency = self.optical_tagging_par['frequency_sampled_all'][i] + pulse_duration = self.optical_tagging_par['pulse_duration_sampled_all'][i] + laser_name = self.optical_tagging_par['laser_name_sampled_all'][i] + target_power = self.optical_tagging_par['target_power_sampled_all'][i] + laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] + + # produce the waveforms + self._produce_waveforms(protocol=protocol, frequency=frequency, pulse_duration=pulse_duration, laser_name=laser_name, target_power=target_power, laser_color=laser_color) + # initiate the laser + + # receiving the timestamps of laser start - # save the timestamps + # save the timestamps - # save parameters + # save parameters def _generate_random_conditions(self): """ @@ -3098,7 +3110,7 @@ def _generate_random_conditions(self): 'Laser_1': (self.Laser_1_power, self.Laser_1_color), 'Laser_2': (self.Laser_2_power, self.Laser_2_color) } - else: + elif self.WhichLaser.currentText() in ['Laser_1','Laser_2']: laser_name_list = [self.WhichLaser.currentText()] if laser_name_list[0]=='Laser_1': laser_config = { @@ -3108,7 +3120,11 @@ def _generate_random_conditions(self): laser_config = { 'Laser_2': (self.Laser_2_power, self.Laser_2_color) } - + else: + # give an popup error window if the laser is not selected + QMessageBox.critical(self.MainWindow, "Error", "Please select the laser to use.") + return + # Generate combinations for each laser protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled = zip(*[ (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText()) @@ -3118,12 +3134,12 @@ def _generate_random_conditions(self): for target_power in extract_numbers_from_string(power_field.text()) ]) - self.optical_tagging_par.protocol_sampled_all = [] - self.optical_tagging_par.frequency_sampled_all = [] - self.optical_tagging_par.pulse_duration_sampled_all = [] - self.optical_tagging_par.laser_name_sampled_all = [] - self.optical_tagging_par.target_power_sampled_all = [] - self.optical_tagging_par.laser_color_sampled_all = [] + self.optical_tagging_par['protocol_sampled_all'] = [] + self.optical_tagging_par['frequency_sampled_all'] = [] + self.optical_tagging_par['pulse_duration_sampled_all'] = [] + self.optical_tagging_par['laser_name_sampled_all'] = [] + self.optical_tagging_par['target_power_sampled_all'] = [] + self.optical_tagging_par['laser_color_sampled_all'] = [] for _ in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3135,12 +3151,12 @@ def _generate_random_conditions(self): target_power_sampled_now = [target_power_sampled[i] for i in random_indices] laser_color_sampled_now = [laser_color_sampled[i] for i in random_indices] # Append the conditions - self.optical_tagging_par.protocol_sampled_all.extend(protocol_sampled_now) - self.optical_tagging_par.optical_tagging_par.frequency_sampled_all.extend(frequency_sampled_now) - self.optical_tagging_par.pulse_duration_sampled_all.extend(pulse_duration_sampled_now) - self.optical_tagging_par.laser_name_sampled_all.extend(laser_name_sampled_now) - self.optical_tagging_par.target_power_sampled_all.extend(target_power_sampled_now) - self.optical_tagging_par.laser_color_sampled_all.extend(laser_color_sampled_now) + self.optical_tagging_par['protocol_sampled_all'].extend(protocol_sampled_now) + self.optical_tagging_par['frequency_sampled_all'].extend(frequency_sampled_now) + self.optical_tagging_par['pulse_duration_sampled_all'].extend(pulse_duration_sampled_now) + self.optical_tagging_par['laser_name_sampled_all'].extend(laser_name_sampled_now) + self.optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) + self.optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' @@ -3161,34 +3177,17 @@ def _WhichLaser(self): self.label1_3.setEnabled(True) self.label1_16.setEnabled(True) - def _produce_waveforms(self): + def _produce_waveforms(self,protocol:str,frequency:int,pulse_duration:float,laser_name:str,target_power:float,laser_color:str): '''Produce the waveforms for the optical tagging''' - # get the laser name - if self.WhichLaser.currentText()=="Both": - laser_name_selected = ['Laser_1','Laser_2'] - else: - laser_name_selected = [self.WhichLaser.currentText()] - # get the amplitude - protocol = self.Protocol.currentText() - for current_laser_name in laser_name_selected: - if current_laser_name=='Laser_1': - target_power = float(self.Laser_1_power.text()) - laser_color = self.Laser_1_color.currentText() - elif current_laser_name=='Laser_2': - target_power = float(self.Laser_2_power.text()) - laser_color = self.Laser_2_color.currentText() - else: - raise ValueError(f"Unknown laser name: {current_laser_name}") - if protocol!='Pulse': - raise ValueError(f"Unknown protocol: {protocol}") - else: - self._get_lasers_amplitude(target_power,laser_color,protocol) - # get the waveform - - # save the waveform + input_voltage=self._get_lasers_amplitude(target_power=target_power,laser_color=laser_color,protocol=protocol,laser_name=laser_name) + + + # get the waveform + + # save the waveform - def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str)->float: + def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,laser_name:str)->float: '''Get the amplitude of the laser based on the calibraion results Args: target_power: The target power of the laser. @@ -3201,26 +3200,53 @@ def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str)- latest_calibration_date=find_latest_calibration_date(self.MainWindow.LaserCalibrationResults,laser_color) # get the selected laser if latest_calibration_date=='NA': - logger.error(f"No calibration results found for {laser_color}") + logger.info(f"No calibration results found for {laser_color}") + return else: - calibration_results=self.MainWindow.LaserCalibrationResults[latest_calibration_date][laser_color][protocol] + try: + calibration_results=self.MainWindow.LaserCalibrationResults[latest_calibration_date][laser_color][protocol][laser_name]['LaserPowerVoltage'] + except: + logger.info(f"No calibration results found for {laser_color} and {laser_name}") + return + # fit the calibration results with a linear model + slope,intercept=self._fit_calibration_results(calibration_results) + # Find the corresponding input voltage for a target laser power + input_voltage_for_target = (target_power - intercept) / slope + return input_voltage_for_target + + def _fit_calibration_results(self,calibration_results:dict,target_power:float)->Tuple[float, float]: + '''Fit the calibration results with a linear model + Args: + calibration_results: The calibration results. + ''' + # Separate input voltage and laser power + input_voltage = calibration_results[:, 0].reshape(-1, 1) # X (features) + laser_power = calibration_results[:, 1] # y (target) + + # Fit the linear model + model = LinearRegression() + model.fit(input_voltage, laser_power) + + # Display the model coefficients + slope = model.coef_[0] + intercept = model.intercept_ + + return slope,intercept -def find_latest_calibration_date(calibration:list,Laser:str)->str: +def find_latest_calibration_date(calibration:list,laser_color:str)->str: """ Find the latest calibration date for the selected laser. Args: calibration: The calibration object. - Laser: The selected laser name. + Laser: The selected laser color. Returns: str: The latest calibration date for the selected laser. """ - if not hasattr(calibration,'LaserCalibrationResults') : - return 'NA' Dates=[] - for Date in calibration.LaserCalibrationResults: - if Laser in calibration.LaserCalibrationResults[Date].keys(): + for Date in calibration: + if laser_color in calibration[Date].keys(): Dates.append(Date) sorted_dates = sorted(Dates) if sorted_dates==[]: From 8761665a958824b4dbc90560bd498a5a50dacf79 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:30:08 -0800 Subject: [PATCH 025/184] removing target power --- src/foraging_gui/Dialogs.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index e106b8980..c92369dec 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3180,11 +3180,9 @@ def _WhichLaser(self): def _produce_waveforms(self,protocol:str,frequency:int,pulse_duration:float,laser_name:str,target_power:float,laser_color:str): '''Produce the waveforms for the optical tagging''' input_voltage=self._get_lasers_amplitude(target_power=target_power,laser_color=laser_color,protocol=protocol,laser_name=laser_name) - - # get the waveform - # save the waveform + def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,laser_name:str)->float: @@ -3214,7 +3212,7 @@ def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,l input_voltage_for_target = (target_power - intercept) / slope return input_voltage_for_target - def _fit_calibration_results(self,calibration_results:dict,target_power:float)->Tuple[float, float]: + def _fit_calibration_results(self,calibration_results:dict)->Tuple[float, float]: '''Fit the calibration results with a linear model Args: calibration_results: The calibration results. From 4c9323c9a571c0c93618ac3c3117b87b5f66e4f2 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:35:39 -0800 Subject: [PATCH 026/184] updating fit_calibration_results --- src/foraging_gui/Dialogs.py | 40 ++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index c92369dec..3349cb68c 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3207,29 +3207,37 @@ def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,l logger.info(f"No calibration results found for {laser_color} and {laser_name}") return # fit the calibration results with a linear model - slope,intercept=self._fit_calibration_results(calibration_results) + slope,intercept=fit_calibration_results(calibration_results) # Find the corresponding input voltage for a target laser power input_voltage_for_target = (target_power - intercept) / slope return input_voltage_for_target - def _fit_calibration_results(self,calibration_results:dict)->Tuple[float, float]: - '''Fit the calibration results with a linear model - Args: - calibration_results: The calibration results. - ''' - # Separate input voltage and laser power - input_voltage = calibration_results[:, 0].reshape(-1, 1) # X (features) - laser_power = calibration_results[:, 1] # y (target) +def fit_calibration_results(calibration_results: list) -> Tuple[float, float]: + """ + Fit the calibration results with a linear model. + + Args: + calibration_results: A list of calibration results where each entry is [input_voltage, laser_power]. + + Returns: + A tuple (slope, intercept) of the fitted linear model. + """ + # Convert to numpy array for easier manipulation + calibration_results = np.array(calibration_results) + + # Separate input voltage and laser power + input_voltage = calibration_results[:, 0].reshape(-1, 1) # X (features) + laser_power = calibration_results[:, 1] # y (target) - # Fit the linear model - model = LinearRegression() - model.fit(input_voltage, laser_power) + # Fit the linear model + model = LinearRegression() + model.fit(input_voltage, laser_power) - # Display the model coefficients - slope = model.coef_[0] - intercept = model.intercept_ + # Extract model coefficients + slope = model.coef_[0] + intercept = model.intercept_ - return slope,intercept + return slope, intercept def find_latest_calibration_date(calibration:list,laser_color:str)->str: """ From d7b6eb6d05e67b32bbc682f069196701c4d3c521 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:50:58 -0800 Subject: [PATCH 027/184] adding s --- src/foraging_gui/OpticalTagging.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 5606bae89..4ab1fd5d1 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -460,7 +460,7 @@- duration each cycle= +duration each cycle (s)= Qt::AutoText From 7e89ecef8c7097cfcc80be5fbaadb3b910c67da0 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:51:08 -0800 Subject: [PATCH 028/184] produce the waveform --- src/foraging_gui/Dialogs.py | 55 +++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 3349cb68c..dc4e68939 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3179,13 +3179,56 @@ def _WhichLaser(self): def _produce_waveforms(self,protocol:str,frequency:int,pulse_duration:float,laser_name:str,target_power:float,laser_color:str): '''Produce the waveforms for the optical tagging''' - input_voltage=self._get_lasers_amplitude(target_power=target_power,laser_color=laser_color,protocol=protocol,laser_name=laser_name) - # get the waveform - + # get the amplitude of the laser + input_voltage=self._get_laser_amplitude(target_power=target_power,laser_color=laser_color,protocol=protocol,laser_name=laser_name) + if input_voltage is None: + return - + # produce the waveform + my_wave=self._get_laser_waveform(protocol=protocol,frequency=frequency,pulse_duration=pulse_duration,input_voltage=input_voltage) - def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,laser_name:str)->float: + return my_wave + + def _get_laser_waveform(self,protocol:str,frequency:int,pulse_duration:float,input_voltage:float): + '''Get the waveform for the laser + Args: + protocol: The protocol to use (only 'Pulse' is supported). + frequency: The frequency of the pulse. + pulse_duration: The duration of the pulse. + input_voltage: The input voltage of the laser. + Returns: + np.array: The waveform of the laser. + ''' + # get the waveform + if protocol!='Pulse': + logger.warning(f"Unknown protocol: {protocol}") + return + sample_frequency=5000 # should be replaced + duration=self.Duration_each_cycle.text() + PointsEachPulse=int(sample_frequency*pulse_duration/1000) + PulseIntervalPoints=int(1/frequency*sample_frequency-PointsEachPulse) + if PulseIntervalPoints<0: + logging.warning('Pulse frequency and pulse duration are not compatible!', + extra={'tags': [self.MainWindow.warning_log_tag]}) + TotalPoints=int(sample_frequency*duration) + PulseNumber=np.floor(duration*frequency) + EachPulse=input_voltage*np.ones(PointsEachPulse) + PulseInterval=np.zeros(PulseIntervalPoints) + WaveFormEachCycle=np.concatenate((EachPulse, PulseInterval), axis=0) + my_wave=np.empty(0) + # pulse number should be greater than 0 + if PulseNumber>1: + for i in range(int(PulseNumber-1)): + my_wave=np.concatenate((my_wave, WaveFormEachCycle), axis=0) + else: + logging.warning('Pulse number is less than 1!', extra={'tags': [self.MainWindow.warning_log_tag]}) + return + my_wave=np.concatenate((my_wave, EachPulse), axis=0) + my_wave=np.concatenate((my_wave, np.zeros(TotalPoints-np.shape(my_wave)[0])), axis=0) + my_wave=np.append(my_wave,[0,0]) + return my_wave + + def _get_laser_amplitude(self,target_power:float,laser_color:str,protocol:str,laser_name:str)->float: '''Get the amplitude of the laser based on the calibraion results Args: target_power: The target power of the laser. @@ -3210,7 +3253,7 @@ def _get_lasers_amplitude(self,target_power:float,laser_color:str,protocol:str,l slope,intercept=fit_calibration_results(calibration_results) # Find the corresponding input voltage for a target laser power input_voltage_for_target = (target_power - intercept) / slope - return input_voltage_for_target + return round(input_voltage_for_target, 2) def fit_calibration_results(calibration_results: list) -> Tuple[float, float]: """ From f006eed09c0327488c6a80ac6aeee530994c8030 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:55:43 -0800 Subject: [PATCH 029/184] transfer to float --- src/foraging_gui/Dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index dc4e68939..b77093ef3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3069,7 +3069,7 @@ def _Start(self): laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] # produce the waveforms - self._produce_waveforms(protocol=protocol, frequency=frequency, pulse_duration=pulse_duration, laser_name=laser_name, target_power=target_power, laser_color=laser_color) + my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, pulse_duration=pulse_duration, laser_name=laser_name, target_power=target_power, laser_color=laser_color) # initiate the laser # receiving the timestamps of laser start @@ -3204,7 +3204,7 @@ def _get_laser_waveform(self,protocol:str,frequency:int,pulse_duration:float,inp logger.warning(f"Unknown protocol: {protocol}") return sample_frequency=5000 # should be replaced - duration=self.Duration_each_cycle.text() + duration=float(self.Duration_each_cycle.text()) # should be replaced PointsEachPulse=int(sample_frequency*pulse_duration/1000) PulseIntervalPoints=int(1/frequency*sample_frequency-PointsEachPulse) if PulseIntervalPoints<0: From 856139577896284df05ee5aaddfacb83e3c4b93a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:02:24 -0800 Subject: [PATCH 030/184] updating duration_each_cycle --- src/foraging_gui/Dialogs.py | 41 +++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index b77093ef3..f842ced05 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3067,9 +3067,17 @@ def _Start(self): laser_name = self.optical_tagging_par['laser_name_sampled_all'][i] target_power = self.optical_tagging_par['target_power_sampled_all'][i] laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] + duration_each_cycle = self.optical_tagging_par['duration_each_cycle_sampled_all'][i] # produce the waveforms - my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, pulse_duration=pulse_duration, laser_name=laser_name, target_power=target_power, laser_color=laser_color) + my_wave=self._produce_waveforms(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + laser_name=laser_name, + target_power=target_power, + laser_color=laser_color, + duration_each_cycle=duration_each_cycle + ) # initiate the laser # receiving the timestamps of laser start @@ -3124,14 +3132,15 @@ def _generate_random_conditions(self): # give an popup error window if the laser is not selected QMessageBox.critical(self.MainWindow, "Error", "Please select the laser to use.") return - + duration_each_cycle_list = extract_numbers_from_string(self.Duration_each_cycle.text()) # Generate combinations for each laser - protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled = zip(*[ - (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText()) + protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled,duration_each_cycle_sampled = zip(*[ + (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText(),duration_each_cycle) for frequency in frequency_list for pulse_duration in pulse_duration_list for laser_name, (power_field, _) in laser_config.items() for target_power in extract_numbers_from_string(power_field.text()) + for duration_each_cycle in duration_each_cycle_list ]) self.optical_tagging_par['protocol_sampled_all'] = [] @@ -3140,6 +3149,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['laser_name_sampled_all'] = [] self.optical_tagging_par['target_power_sampled_all'] = [] self.optical_tagging_par['laser_color_sampled_all'] = [] + self.optical_tagging_par['duration_each_cycle_sampled_all'] = [] for _ in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3157,6 +3167,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['laser_name_sampled_all'].extend(laser_name_sampled_now) self.optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) self.optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) + self.optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' @@ -3177,19 +3188,28 @@ def _WhichLaser(self): self.label1_3.setEnabled(True) self.label1_16.setEnabled(True) - def _produce_waveforms(self,protocol:str,frequency:int,pulse_duration:float,laser_name:str,target_power:float,laser_color:str): + def _produce_waveforms(self,protocol:str,frequency:int,pulse_duration:float,laser_name:str,target_power:float,laser_color:str,duration_each_cycle:float): '''Produce the waveforms for the optical tagging''' # get the amplitude of the laser - input_voltage=self._get_laser_amplitude(target_power=target_power,laser_color=laser_color,protocol=protocol,laser_name=laser_name) + input_voltage=self._get_laser_amplitude(target_power=target_power, + laser_color=laser_color, + protocol=protocol, + laser_name=laser_name + ) if input_voltage is None: return # produce the waveform - my_wave=self._get_laser_waveform(protocol=protocol,frequency=frequency,pulse_duration=pulse_duration,input_voltage=input_voltage) + my_wave=self._get_laser_waveform(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + input_voltage=input_voltage, + duration_each_cycle=duration_each_cycle + ) return my_wave - def _get_laser_waveform(self,protocol:str,frequency:int,pulse_duration:float,input_voltage:float): + def _get_laser_waveform(self,protocol:str,frequency:int,pulse_duration:float,input_voltage:float,duration_each_cycle:float)->np.array: '''Get the waveform for the laser Args: protocol: The protocol to use (only 'Pulse' is supported). @@ -3204,14 +3224,13 @@ def _get_laser_waveform(self,protocol:str,frequency:int,pulse_duration:float,inp logger.warning(f"Unknown protocol: {protocol}") return sample_frequency=5000 # should be replaced - duration=float(self.Duration_each_cycle.text()) # should be replaced PointsEachPulse=int(sample_frequency*pulse_duration/1000) PulseIntervalPoints=int(1/frequency*sample_frequency-PointsEachPulse) if PulseIntervalPoints<0: logging.warning('Pulse frequency and pulse duration are not compatible!', extra={'tags': [self.MainWindow.warning_log_tag]}) - TotalPoints=int(sample_frequency*duration) - PulseNumber=np.floor(duration*frequency) + TotalPoints=int(sample_frequency*duration_each_cycle) + PulseNumber=np.floor(duration_each_cycle*frequency) EachPulse=input_voltage*np.ones(PointsEachPulse) PulseInterval=np.zeros(PulseIntervalPoints) WaveFormEachCycle=np.concatenate((EachPulse, PulseInterval), axis=0) From 9c453113a962865e6b263be88e4a5569cc5b1c46 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:26:19 -0800 Subject: [PATCH 031/184] send waveform and initiate the laser --- src/foraging_gui/Dialogs.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f842ced05..cfd2ae1a7 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3058,6 +3058,10 @@ def _Start(self): # generate random conditions including lasers, laser power, laser color, and protocol self._generate_random_conditions() + self.optical_tagging_par['success_tag'] = np.zeros(len(self.optical_tagging_par['protocol_sampled_all'])) + + # send the trigger source + self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') # iterate each condition for i in range(len(self.optical_tagging_par['protocol_sampled_all'])): # get the current parameters @@ -3078,13 +3082,41 @@ def _Start(self): laser_color=laser_color, duration_each_cycle=duration_each_cycle ) + my_wave_control=self._produce_waveforms(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + laser_name=laser_name, + target_power=0, + laser_color=laser_color, + duration_each_cycle=duration_each_cycle + ) + if my_wave is None: + continue + # send the waveform and size to the bonsai + if laser_name=='Laser_1': + getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave.tolist())[1:-1]) + getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave_control.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave_control.tolist())[1:-1]) + elif laser_name=='Laser_2': + getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave.tolist())[1:-1]) + getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave_control.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave_control.tolist())[1:-1]) + FinishOfWaveForm=self.MainWindow.Channel4.receive() # initiate the laser - + self._initiate_laser() # receiving the timestamps of laser start # save the timestamps # save parameters + + def _initiate_laser(self): + '''Initiate laser in bonsai''' + # start generating waveform in bonsai + self.MainWindow.Channel.OptogeneticsCalibration(int(1)) + self.MainWindow.Channel.receive() def _generate_random_conditions(self): """ From 9e096199edcdd7786af474013e5beabe2d0cd20f Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:28:14 -0800 Subject: [PATCH 032/184] update the overview --- src/foraging_gui/Dialogs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index cfd2ae1a7..22c95e5fa 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3108,9 +3108,15 @@ def _Start(self): self._initiate_laser() # receiving the timestamps of laser start + # change the success_tag to 1 + self.optical_tagging_par['success_tag'][i]=1 # save the timestamps # save parameters + + # wait to start the next cycle + + # show current cycle and parameters def _initiate_laser(self): '''Initiate laser in bonsai''' From e14f3fa39e6f71f3341a65401a3cac8267e192d9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:15:20 -0800 Subject: [PATCH 033/184] adding interval --- src/foraging_gui/Dialogs.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 22c95e5fa..dead916e2 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3072,7 +3072,7 @@ def _Start(self): target_power = self.optical_tagging_par['target_power_sampled_all'][i] laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] duration_each_cycle = self.optical_tagging_par['duration_each_cycle_sampled_all'][i] - + interval_between_cycles = self.optical_tagging_par['interval_between_cycles_sampled_all'][i] # produce the waveforms my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, @@ -3105,19 +3105,20 @@ def _Start(self): getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave_control.tolist())[1:-1]) FinishOfWaveForm=self.MainWindow.Channel4.receive() # initiate the laser + # need to change the bonsai code to initiate the laser self._initiate_laser() - # receiving the timestamps of laser start - - # change the success_tag to 1 - self.optical_tagging_par['success_tag'][i]=1 - # save the timestamps - - # save parameters - + # receiving the timestamps of laser start and saving them + Rec=self.MainWindow.Channel1.receive() + if Rec[0].address=='/ITIStartTimeHarp': + self.optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] + # change the success_tag to 1 + self.optical_tagging_par['success_tag'][i]=1 + else: + self.optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag # wait to start the next cycle - + time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - + def _initiate_laser(self): '''Initiate laser in bonsai''' # start generating waveform in bonsai @@ -3188,6 +3189,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['target_power_sampled_all'] = [] self.optical_tagging_par['laser_color_sampled_all'] = [] self.optical_tagging_par['duration_each_cycle_sampled_all'] = [] + self.optical_tagging_par['Interval_between_cycles'] = [] for _ in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3206,7 +3208,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) self.optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) self.optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) - + self.optical_tagging_par['Interval_between_cycles'].append(float(self.Interval_between_cycles.text())) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' laser_name = self.WhichLaser.currentText() From 1cb037400d78442bdc810bf5ef3fbece956f5ff8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:15:34 -0800 Subject: [PATCH 034/184] adding interval --- src/foraging_gui/Dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index dead916e2..0f04ac3d9 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3189,7 +3189,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['target_power_sampled_all'] = [] self.optical_tagging_par['laser_color_sampled_all'] = [] self.optical_tagging_par['duration_each_cycle_sampled_all'] = [] - self.optical_tagging_par['Interval_between_cycles'] = [] + self.optical_tagging_par['Interval_between_cycles_sampled_all'] = [] for _ in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3208,7 +3208,7 @@ def _generate_random_conditions(self): self.optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) self.optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) self.optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) - self.optical_tagging_par['Interval_between_cycles'].append(float(self.Interval_between_cycles.text())) + self.optical_tagging_par['Interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' laser_name = self.WhichLaser.currentText() From c88d7c0ef1b5e5bebfe898e8ac2118009217cfee Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:21:09 -0800 Subject: [PATCH 035/184] adding label show current --- src/foraging_gui/OpticalTagging.ui | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 4ab1fd5d1..d65430a76 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -6,8 +6,8 @@0 0 -275 -530 +270 +575 @@ -672,6 +672,34 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + +true ++ ++ +30 +500 +191 +20 ++ ++ +9000000 +16777215 ++ ++ + +Qt::AutoText ++ +Qt::AlignCenter +From 395471609de7cbbd60efb8270f9af02ea537eea5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:22:51 -0800 Subject: [PATCH 036/184] adding show current cycle and parameters --- src/foraging_gui/Dialogs copy.py | 3068 ++++++++++++++++++++++++++++++ src/foraging_gui/Dialogs.py | 4 +- 2 files changed, 3070 insertions(+), 2 deletions(-) create mode 100644 src/foraging_gui/Dialogs copy.py diff --git a/src/foraging_gui/Dialogs copy.py b/src/foraging_gui/Dialogs copy.py new file mode 100644 index 000000000..396326a38 --- /dev/null +++ b/src/foraging_gui/Dialogs copy.py @@ -0,0 +1,3068 @@ +import time +import math +import json +import os +import shutil +import subprocess +from datetime import datetime +import logging +import webbrowser +from typing import Literal + +import numpy as np +import pandas as pd +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QMessageBox, QGridLayout +from PyQt5.QtWidgets import QLabel, QDialogButtonBox,QFileDialog,QInputDialog, QLineEdit +from PyQt5 import QtWidgets, uic, QtGui +from PyQt5.QtCore import QThreadPool,Qt, QAbstractTableModel, QItemSelectionModel, QObject, QTimer +from PyQt5.QtSvg import QSvgWidget + +from foraging_gui.MyFunctions import Worker +from foraging_gui.Visualization import PlotWaterCalibration +from aind_auto_train.curriculum_manager import CurriculumManager +from aind_auto_train.auto_train_manager import DynamicForagingAutoTrainManager +from aind_auto_train.schema.task import TrainingStage +from aind_auto_train.schema.curriculum import DynamicForagingCurriculum +codebase_curriculum_schema_version = DynamicForagingCurriculum.model_fields['curriculum_schema_version'].default + +logger = logging.getLogger(__name__) + +class MouseSelectorDialog(QDialog): + + def __init__(self, MainWindow, mice, parent=None): + super().__init__(parent) + self.mice = ['']+mice + self.MainWindow = MainWindow + self.setWindowTitle('Box {}, Load Mouse'.format(self.MainWindow.box_letter)) + self.setFixedSize(250,125) + + QBtns = QDialogButtonBox.Ok | QDialogButtonBox.Cancel + self.buttonBox = QDialogButtonBox(QBtns) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + combo = QtWidgets.QComboBox() + combo.addItems(self.mice) + combo.setEditable(True) + combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + combo.completer().setCompletionMode(QtWidgets.QCompleter.PopupCompletion) + font = combo.font() + font.setPointSize(15) + combo.setFont(font) + self.combo = combo + + msg = QLabel('Enter the Mouse ID: \nuse 0-9, single digit as test ID') + font = msg.font() + font.setPointSize(12) + msg.setFont(font) + + self.layout = QVBoxLayout(self) + self.layout.addWidget(msg) + self.layout.addWidget(self.combo) + self.layout.addWidget(self.buttonBox) + self.setLayout(self.layout) + +class LickStaDialog(QDialog): + '''Lick statistics dialog''' + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('LicksDistribution.ui', self) + + self.MainWindow=MainWindow + +class TimeDistributionDialog(QDialog): + '''Simulated distribution of ITI/Delay/Block length''' + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('TimeDistribution.ui', self) + + self.MainWindow=MainWindow + +class OptogeneticsDialog(QDialog): + '''Optogenetics dialog''' + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('Optogenetics.ui', self) + self.condition_idx = [1, 2, 3, 4, 5, 6] # corresponding to optogenetics condition 1, 2, 3, 4, 5, 6 + self.laser_tags=[1,2] # corresponding to Laser_1 and Laser_2 + self._connectSignalsSlots() + self.MainWindow=MainWindow + for i in self.condition_idx: + getattr(self, f'_LaserColor')(i) + self._Laser_calibration() + self._SessionWideControl() + def _connectSignalsSlots(self): + for i in self.condition_idx: + # Connect LaserColor signals + self._connectSignalSlot(f'LaserColor_{i}', self._LaserColor, i) + + # Connect Protocol signals + self._connectSignalSlot(f'Protocol_{i}', self._activated, i) + self._connectSignalSlot(f'Protocol_{i}', self._LaserColor, i) + + # Connect Frequency signals + self._connectSignalSlot(f'Frequency_{i}', self._Frequency, i) + + # Connect LaserStart and LaserEnd signals + self._connectSignalSlot(f'LaserStart_{i}', self._activated, i) + self._connectSignalSlot(f'LaserEnd_{i}', self._activated, i) + + self.Laser_calibration.currentIndexChanged.connect(self._Laser_calibration) + self.Laser_calibration.activated.connect(self._Laser_calibration) + self.SessionWideControl.currentIndexChanged.connect(self._SessionWideControl) + + def _connectSignalSlot(self, signal_name, slot_method, index): + signal = getattr(self, signal_name) + signal.currentIndexChanged.connect(lambda: slot_method(index)) + signal.activated.connect(lambda: slot_method(index)) + + def _SessionWideControl(self): + '''enable/disable items based on session wide control''' + if self.SessionWideControl.currentText()=='on': + enable=True + else: + enable=False + self.label3_18.setEnabled(enable) + self.label3_21.setEnabled(enable) + self.FractionOfSession.setEnabled(enable) + self.label3_19.setEnabled(enable) + self.SessionStartWith.setEnabled(enable) + self.label3_17.setEnabled(enable) + self.SessionAlternating.setEnabled(enable) + def _Laser_calibration(self): + ''''change the laser calibration date''' + # find the latest calibration date for the selected laser + Laser=self.Laser_calibration.currentText() + latest_calibration_date=self._FindLatestCalibrationDate(Laser) + # set the latest calibration date + self.LatestCalibrationDate.setText(latest_calibration_date) + + def _FindLatestCalibrationDate(self,Laser): + '''find the latest calibration date for the selected laser''' + if not hasattr(self.MainWindow,'LaserCalibrationResults'): + return 'NA' + Dates=[] + for Date in self.MainWindow.LaserCalibrationResults: + if Laser in self.MainWindow.LaserCalibrationResults[Date].keys(): + Dates.append(Date) + sorted_dates = sorted(Dates) + if sorted_dates==[]: + return 'NA' + else: + return sorted_dates[-1] + + def _Frequency(self,Numb): + try: + Color = getattr(self, f"LaserColor_{str(Numb)}").currentText() + Protocol = getattr(self, f"Protocol_{str(Numb)}").currentText() + CurrentFrequency = getattr(self, f"Frequency_{str(Numb)}").currentText() + latest_calibration_date=self._FindLatestCalibrationDate(Color) + if latest_calibration_date=='NA': + RecentLaserCalibration={} + else: + RecentLaserCalibration=self.MainWindow.LaserCalibrationResults[latest_calibration_date] + for laser_tag in self.laser_tags: + ItemsLaserPower=[] + CurrentlaserPowerLaser = getattr(self, f"Laser{str(laser_tag)}_power_{str(Numb)}").currentText() + if Protocol in ['Sine']: + for i in range(len(RecentLaserCalibration[Color][Protocol][CurrentFrequency][f"Laser_{str(laser_tag)}"]['LaserPowerVoltage'])): + ItemsLaserPower.append(str(RecentLaserCalibration[Color][Protocol][CurrentFrequency][f"Laser_{str(laser_tag)}"]['LaserPowerVoltage'][i])) + if Protocol in ['Constant','Pulse']: + for i in range(len(RecentLaserCalibration[Color][Protocol][f"Laser_{str(laser_tag)}"]['LaserPowerVoltage'])): + ItemsLaserPower.append(str(RecentLaserCalibration[Color][Protocol][f"Laser_{str(laser_tag)}"]['LaserPowerVoltage'][i])) + ItemsLaserPower=sorted(ItemsLaserPower) + getattr(self, f"Laser{str(laser_tag)}_power_{str(Numb)}").clear() + getattr(self, f"Laser{str(laser_tag)}_power_{str(Numb)}").addItems(ItemsLaserPower) + index = getattr(self, f"Laser{str(laser_tag)}_power_{str(Numb)}").findText(CurrentlaserPowerLaser) + if index != -1: + getattr(self, f"Laser{str(laser_tag)}_power_{str(Numb)}").setCurrentIndex(index) + + except Exception as e: + logging.error(str(e)) + + def _activated(self,Numb): + '''enable/disable items based on protocols and laser start/end''' + Inactlabel1=15 # pulse duration + Inactlabel2=13 # frequency + Inactlabel3=14 # Ramping down + if getattr(self, f'Protocol_{Numb}').currentText() == 'Sine': + getattr(self, f'label{Numb}_{Inactlabel1}').setEnabled(False) + getattr(self, f'PulseDur_{Numb}').setEnabled(False) + getattr(self, f'label{Numb}_{Inactlabel2}').setEnabled(True) + getattr(self, f'Frequency_{Numb}').setEnabled(True) + getattr(self, f'label{Numb}_{Inactlabel3}').setEnabled(True) + getattr(self, f'RD_{Numb}').setEnabled(True) + getattr(self, f'Frequency_{Numb}').setEditable(False) + if getattr(self, f'Protocol_{Numb}').currentText() == 'Pulse': + getattr(self, f'label{Numb}_{Inactlabel1}').setEnabled(True) + getattr(self, f'PulseDur_{Numb}').setEnabled(True) + getattr(self, f'label{Numb}_{Inactlabel2}').setEnabled(True) + getattr(self, f'Frequency_{Numb}').setEnabled(True) + getattr(self, f'label{Numb}_{Inactlabel3}').setEnabled(False) + getattr(self, f'RD_{Numb}').setEnabled(False) + getattr(self, f'Frequency_{Numb}').setEditable(True) + if getattr(self, f'Protocol_{Numb}').currentText() == 'Constant': + getattr(self, f'label{Numb}_{Inactlabel1}').setEnabled(False) + getattr(self, f'PulseDur_{Numb}').setEnabled(False) + getattr(self, f'label{Numb}_{Inactlabel2}').setEnabled(False) + getattr(self, f'Frequency_{Numb}').setEnabled(False) + getattr(self, f'label{Numb}_{Inactlabel3}').setEnabled(True) + getattr(self, f'RD_{Numb}').setEnabled(True) + getattr(self, f'Frequency_{Numb}').clear() + getattr(self, f'Frequency_{Numb}').setEditable(False) + if getattr(self, f'LaserStart_{Numb}').currentText() == 'NA': + getattr(self, f'label{Numb}_9').setEnabled(False) + getattr(self, f'OffsetStart_{Numb}').setEnabled(False) + else: + getattr(self, f'label{Numb}_9').setEnabled(True) + getattr(self, f'OffsetStart_{Numb}').setEnabled(True) + if getattr(self, f'LaserEnd_{Numb}').currentText() == 'NA': + getattr(self, f'label{Numb}_11').setEnabled(False) + getattr(self, f'OffsetEnd_{Numb}').setEnabled(False) + else: + getattr(self, f'label{Numb}_11').setEnabled(True) + getattr(self, f'OffsetEnd_{Numb}').setEnabled(True) + def _LaserColor(self,Numb): + ''' enable/disable items based on laser (blue/green/orange/red/NA)''' + Inactlabel=range(2,17) + if getattr(self, 'LaserColor_' + str(Numb)).currentText() == 'NA': + Label=False + else: + Label=True + Color = getattr(self, 'LaserColor_' + str(Numb)).currentText() + Protocol = getattr(self, 'Protocol_' + str(Numb)).currentText() + CurrentFrequency = getattr(self, 'Frequency_' + str(Numb)).currentText() + latest_calibration_date=self._FindLatestCalibrationDate(Color) + if latest_calibration_date=='NA': + RecentLaserCalibration={} + else: + RecentLaserCalibration=self.MainWindow.LaserCalibrationResults[latest_calibration_date] + no_calibration=False + if not RecentLaserCalibration=={}: + if Color in RecentLaserCalibration.keys(): + if Protocol in RecentLaserCalibration[Color].keys(): + if Protocol=='Sine': + Frequency=RecentLaserCalibration[Color][Protocol].keys() + ItemsFrequency=[] + for Fre in Frequency: + ItemsFrequency.append(Fre) + ItemsFrequency=sorted(ItemsFrequency) + getattr(self, f'Frequency_{Numb}').clear() + getattr(self, f'Frequency_{Numb}').addItems(ItemsFrequency) + if not CurrentFrequency in Frequency: + CurrentFrequency = getattr(self, 'Frequency_' + str(Numb)).currentText() + for laser_tag in self.laser_tags: + ItemsLaserPower=[] + for i in range(len(RecentLaserCalibration[Color][Protocol][CurrentFrequency][f"Laser_{laser_tag}"]['LaserPowerVoltage'])): + ItemsLaserPower.append(str(RecentLaserCalibration[Color][Protocol][CurrentFrequency][f"Laser_{laser_tag}"]['LaserPowerVoltage'][i])) + ItemsLaserPower=sorted(ItemsLaserPower) + getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() + getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").addItems(ItemsLaserPower) + elif Protocol=='Constant' or Protocol=='Pulse': + for laser_tag in self.laser_tags: + ItemsLaserPower=[] + for i in range(len(RecentLaserCalibration[Color][Protocol][f"Laser_{laser_tag}"]['LaserPowerVoltage'])): + ItemsLaserPower.append(str(RecentLaserCalibration[Color][Protocol][f"Laser_{laser_tag}"]['LaserPowerVoltage'][i])) + ItemsLaserPower=sorted(ItemsLaserPower) + getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() + getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").addItems(ItemsLaserPower) + else: + no_calibration=True + else: + no_calibration=True + else: + no_calibration=True + + if no_calibration: + for laser_tag in self.laser_tags: + getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() + logging.warning('No calibration for this protocol identified!', + extra={'tags': [self.MainWindow.warning_log_tag]}) + + getattr(self, 'Location_' + str(Numb)).setEnabled(Label) + getattr(self, 'Laser1_power_' + str(Numb)).setEnabled(Label) + getattr(self, 'Laser2_power_' + str(Numb)).setEnabled(Label) + getattr(self, 'Probability_' + str(Numb)).setEnabled(Label) + getattr(self, 'Duration_' + str(Numb)).setEnabled(Label) + getattr(self, 'Condition_' + str(Numb)).setEnabled(Label) + getattr(self, 'ConditionP_' + str(Numb)).setEnabled(Label) + getattr(self, 'LaserStart_' + str(Numb)).setEnabled(Label) + getattr(self, 'OffsetStart_' + str(Numb)).setEnabled(Label) + getattr(self, 'LaserEnd_' + str(Numb)).setEnabled(Label) + getattr(self, 'OffsetEnd_' + str(Numb)).setEnabled(Label) + getattr(self, 'Protocol_' + str(Numb)).setEnabled(Label) + getattr(self, 'Frequency_' + str(Numb)).setEnabled(Label) + getattr(self, 'RD_' + str(Numb)).setEnabled(Label) + getattr(self, 'PulseDur_' + str(Numb)).setEnabled(Label) + + for i in Inactlabel: + getattr(self, 'label' + str(Numb) + '_' + str(i)).setEnabled(Label) + if getattr(self, 'LaserColor_' + str(Numb)).currentText() != 'NA': + getattr(self, '_activated')(Numb) + + +class WaterCalibrationDialog(QDialog): + '''Water valve calibration''' + def __init__(self, MainWindow,parent=None): + super().__init__(parent) + uic.loadUi('Calibration.ui', self) + + self.MainWindow=MainWindow + self.calibrating_left = False + self.calibrating_right= False + self._LoadCalibrationParameters() + if not hasattr(self.MainWindow,'WaterCalibrationResults'): + self.MainWindow.WaterCalibrationResults={} + self.WaterCalibrationResults={} + else: + self.WaterCalibrationResults=self.MainWindow.WaterCalibrationResults + self._connectSignalsSlots() + self.ToInitializeVisual=1 + self._UpdateFigure() + self.setWindowTitle('Water Calibration: {}'.format(self.MainWindow.current_box)) + self.Warning.setText('') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + # find all buttons and set them to not be the default button + for container in [self]: + for child in container.findChildren((QtWidgets.QPushButton)): + child.setDefault(False) + child.setAutoDefault(False) + + # setup QTimers to keep lines open + self.left_open_timer = QTimer(timeout=lambda: self.reopen_valve('Left'), interval=10000) + self.right_open_timer = QTimer(timeout=lambda: self.reopen_valve('Right'), interval=10000) + + # setup QTimers to keep close lines after 5ml + self.left_close_timer = QTimer(timeout=lambda: self.OpenLeft5ml.setChecked(False)) # trigger _ToggleValve call + self.right_close_timer = QTimer(timeout=lambda: self.OpenRight5ml.setChecked(False)) + + # setup Qtimers for updating text countdown + self.left_text_timer = QTimer(timeout=lambda: + self.OpenLeft5ml.setText(f'Open left 5ml: {round(self.left_close_timer.remainingTime()/1000)}s'), + interval=1000) + self.right_text_timer = QTimer(timeout=lambda: + self.OpenRight5ml.setText(f'Open right 5ml: {round(self.right_close_timer.remainingTime()/1000)}s'), + interval=1000) + + def _connectSignalsSlots(self): + self.SpotCheckLeft.clicked.connect(lambda: self._SpotCheck('Left')) + self.SpotCheckRight.clicked.connect(lambda: self._SpotCheck('Right')) + + # Set up OpenLeftForever button + self.OpenLeftForever.clicked.connect(lambda: self._ToggleValve(self.OpenLeftForever, 'Left')) + self.OpenLeftForever.clicked.connect(lambda: self.OpenLeft5ml.setDisabled(self.OpenLeftForever.isChecked())) + # Set up OpenRightForever button + self.OpenRightForever.clicked.connect(lambda: self._ToggleValve(self.OpenRightForever, 'Right')) + self.OpenRightForever.clicked.connect(lambda: self.OpenRight5ml.setDisabled(self.OpenRightForever.isChecked())) + # Set up OpenLeft5ml button + self.OpenLeft5ml.toggled.connect(lambda val: self._ToggleValve(self.OpenLeft5ml, 'Left')) + self.OpenLeft5ml.toggled.connect(lambda val: self.OpenLeftForever.setDisabled(val)) + # Set up OpenRight5ml button + self.OpenRight5ml.toggled.connect(lambda val: self._ToggleValve(self.OpenRight5ml, 'Right')) + self.OpenRight5ml.toggled.connect(lambda val: self.OpenRightForever.setDisabled(val)) + + self.SaveLeft.clicked.connect(lambda: self._SaveValve('Left')) + self.SaveRight.clicked.connect(lambda: self._SaveValve('Right')) + self.StartCalibratingLeft.clicked.connect(self._StartCalibratingLeft) + self.StartCalibratingRight.clicked.connect(self._StartCalibratingRight) + self.Continue.clicked.connect(self._Continue) + self.Repeat.clicked.connect(self._Repeat) + self.Finished.clicked.connect(self._Finished) + self.EmergencyStop.clicked.connect(self._EmergencyStop) + self.showrecent.textChanged.connect(self._Showrecent) + self.showspecificcali.activated.connect(self._ShowSpecifcDay) + + def _Showrecent(self): + '''update the calibration figure''' + self._UpdateFigure() + + def _ShowSpecifcDay(self): + '''update the calibration figure''' + self._UpdateFigure() + + def _Finished(self): + if (not self.calibrating_left) and (not self.calibrating_right): + return + + if self.calibrating_left and (not np.all(self.left_measurements)): + reply = QMessageBox.question(self, "Box {}, Finished".format(self.MainWindow.box_letter), + f"Calibration incomplete, are you sure you want to finish?\n", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if reply == QMessageBox.No: + return + if self.calibrating_right and (not np.all(self.right_measurements)): + reply = QMessageBox.question(self, "Box {}, Finished".format(self.MainWindow.box_letter), + f"Calibration incomplete, are you sure you want to finish?\n", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if reply == QMessageBox.No: + return + + self.calibrating_left = False + self.calibrating_right= False + self.Continue.setStyleSheet("color: black;background-color : none") + self.Repeat.setStyleSheet("color: black;background-color : none") + self.Finished.setStyleSheet("color: black;background-color : none") + self.StartCalibratingLeft.setStyleSheet("background-color : none") + self.StartCalibratingRight.setStyleSheet("background-color : none") + self.StartCalibratingLeft.setChecked(False) + self.StartCalibratingRight.setChecked(False) + self.StartCalibratingLeft.setEnabled(True) + self.StartCalibratingRight.setEnabled(True) + self.Warning.setText('Calibration Finished') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + + def _Continue(self): + '''Change the color of the continue button''' + if (not self.calibrating_left) and (not self.calibrating_right): + return + + self.Continue.setStyleSheet("color: black; background-color : none") + logging.info('Continue pressed') + if self.calibrating_left: + self._CalibrateLeftOne() + if self.calibrating_right: + self._CalibrateRightOne() + + def _Repeat(self): + '''Change the color of the continue button''' + + if (not self.calibrating_left) and (not self.calibrating_right): + return + self.Repeat.setStyleSheet("color: black; background-color : none") + if self.calibrating_left: + self._CalibrateLeftOne(repeat=True) + if self.calibrating_right: + self._CalibrateRightOne(repeat=True) + + + def _EmergencyStop(self): + '''Change the color of the EmergencyStop button''' + if self.EmergencyStop.isChecked(): + self.EmergencyStop.setStyleSheet("background-color : green;") + else: + self.EmergencyStop.setStyleSheet("background-color : none") + + def _SaveValve(self, valve: Literal['Left', 'Right']): + """ + save the calibration result of the single point calibration (left valve) + :param valve: string specifying valve side + """ + save = getattr(self, f'Save{valve}') + save.setStyleSheet("background-color : green;") + QApplication.processEvents() + + valve_open_time = str(self.SpotLeftOpenTime) + water_txt = getattr(self, f'TotalWaterSingle{valve}').text() + before_txt = getattr(self, f'SpotCheckPreWeight{valve}').text() + + self._Save( + valve= f'Spot{valve}', + valve_open_time=str(valve_open_time), + valve_open_interval=str(self.SpotInterval), + cycle=str(self.SpotCycle), + total_water=float(water_txt), + tube_weight=float(before_txt), + append=True) + save.setStyleSheet("background-color : none") + save.setChecked(False) + + def _LoadCalibrationParameters(self): + self.WaterCalibrationPar={} + if os.path.exists(self.MainWindow.WaterCalibrationParFiles): + with open(self.MainWindow.WaterCalibrationParFiles, 'r') as f: + self.WaterCalibrationPar = json.load(f) + logging.info('loaded water calibration parameters') + else: + logging.warning('could not find water calibration parameters: {}'.format(self.MainWindow.WaterCalibrationParFiles)) + self.WaterCalibrationPar = {} + + # if no parameters are stored, store default parameters + if 'Full' not in self.WaterCalibrationPar: + self.WaterCalibrationPar['Full'] = {} + self.WaterCalibrationPar['Full']['TimeMin'] = 0.02 + self.WaterCalibrationPar['Full']['TimeMax'] = 0.03 + self.WaterCalibrationPar['Full']['Stride'] = 0.01 + self.WaterCalibrationPar['Full']['Interval']= 0.1 + self.WaterCalibrationPar['Full']['Cycle'] = 1000 + + if 'Spot' not in self.WaterCalibrationPar: + self.WaterCalibrationPar['Spot'] = {} + self.WaterCalibrationPar['Spot']['Interval']= 0.1 + self.WaterCalibrationPar['Spot']['Cycle'] = 200 + + self.SpotCycle = float(self.WaterCalibrationPar['Spot']['Cycle']) + self.SpotInterval = float(self.WaterCalibrationPar['Spot']['Interval']) + + # Add other calibration types to drop down list, but only if they have all parameters + other_types = set(self.WaterCalibrationPar.keys()) - set(['Full','Spot']) + required = set(['TimeMin','TimeMax','Stride','Interval','Cycle']) + if len(other_types) > 0: + for t in other_types: + if required.issubset(set(self.WaterCalibrationPar[t].keys())): + self.CalibrationType.addItem(t) + else: + logging.info('Calibration Type "{}" missing required fields'.format(t)) + + def _StartCalibratingLeft(self): + '''start the calibration loop of left valve''' + + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + self.StartCalibratingLeft.setChecked(False) + self.StartCalibratingLeft.setStyleSheet("background-color : none") + self.Warning.setText('Calibration was terminated!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.StartCalibratingRight.setEnabled(True) + return + + if self.StartCalibratingLeft.isChecked(): + # change button color + self.StartCalibratingLeft.setStyleSheet("background-color : green;") + QApplication.processEvents() + # disable the right valve calibration + self.StartCalibratingRight.setEnabled(False) + else: + self.StartCalibratingLeft.setChecked(True) + self._Finished() + return + + # Get Calibration parameters + self.params = self.WaterCalibrationPar[self.CalibrationType.currentText()] + + # Populate options for calibrations + self.left_opentimes = np.arange( + float(self.params['TimeMin']), + float(self.params['TimeMax'])+0.0001, + float(self.params['Stride']) + ) + self.left_opentimes = [np.round(x,3) for x in self.left_opentimes] + self.LeftOpenTime.clear() + for t in self.left_opentimes: + self.LeftOpenTime.addItem('{0:.3f}'.format(t)) + self.WeightBeforeLeft.setText('') + self.WeightAfterLeft.setText('') + self.Warning.setText('') + + # Keep track of calibration status + self.calibrating_left = True + self.left_measurements = np.empty(np.shape(self.left_opentimes)) + self.left_measurements[:] = False + + # Start the first calibration + self._CalibrateLeftOne() + + def _CalibrateLeftOne(self,repeat=False): + ''' + Calibrate a single value + ''' + + # Determine what valve time we are measuring + if not repeat: + if np.all(self.left_measurements): + self.Warning.setText('All measurements have been completed. Either press Repeat, or Finished') + return + next_index = np.where(self.left_measurements != True)[0][0] + self.LeftOpenTime.setCurrentIndex(next_index) + else: + next_index = self.LeftOpenTime.currentIndex() + logging.info('Calibrating left: {}'.format(self.left_opentimes[next_index])) + + # Shuffle weights of before/after + self.WeightBeforeLeft.setText(self.WeightAfterLeft.text()) + self.WeightAfterLeft.setText('') + + #Prompt for before weight, using field value as default + if self.WeightBeforeLeft.text() != '': + before_weight = float(self.WeightBeforeLeft.text()) + else: + before_weight = 0.0 + before_weight, ok = QInputDialog().getDouble( + self, + 'Box {}, Left'.format(self.MainWindow.box_letter), + "Before weight (g): ", + before_weight, + 0,1000,4) + if not ok: + # User cancels + self.Warning.setText('Press Continue, Repeat, or Finished') + return + self.WeightBeforeLeft.setText(str(before_weight)) + + # Perform this measurement + current_valve_opentime = self.left_opentimes[next_index] + for i in range(int(self.params['Cycle'])): + QApplication.processEvents() + if (not self.EmergencyStop.isChecked()): + self._CalibrationStatus( + float(current_valve_opentime), + self.WeightBeforeLeft.text(), + i,self.params['Cycle'], float(self.params['Interval']) + ) + + # set the valve open time + self.MainWindow.Channel.LeftValue(float(current_valve_opentime)*1000) + # open the valve + self.MainWindow.Channel3.ManualWater_Left(int(1)) + # delay + time.sleep(current_valve_opentime+float(self.params['Interval'])) + else: + self.Warning.setText('Please repeat measurement') + self.WeightBeforeLeft.setText('') + self.WeightAfterLeft.setText('') + self.Repeat.setStyleSheet("color: white;background-color : mediumorchid;") + self.Continue.setStyleSheet("color: black;background-color : none;") + self.EmergencyStop.setChecked(False) + self.EmergencyStop.setStyleSheet("background-color : none;") + return + + # Prompt for weight + final_tube_weight = 0.0 + final_tube_weight, ok = QInputDialog().getDouble( + self, + 'Box {}, Left'.format(self.MainWindow.box_letter), + "Weight after (g): ", + final_tube_weight, + 0, 1000, 4) + if not ok: + self.Warning.setText('Please repeat measurement') + self.WeightBeforeLeft.setText('') + self.WeightAfterLeft.setText('') + self.Repeat.setStyleSheet("color: white;background-color : mediumorchid;") + self.Continue.setStyleSheet("color: black;background-color : none;") + return + self.WeightAfterLeft.setText(str(final_tube_weight)) + + # Mark measurement as complete, save data, and update figure + self.left_measurements[next_index] = True + self._Save( + valve='Left', + valve_open_time=str(current_valve_opentime), + valve_open_interval=str(self.params['Interval']), + cycle=str(self.params['Cycle']), + total_water=float(final_tube_weight), + tube_weight=float(before_weight) + ) + self._UpdateFigure() + + # Direct user for next steps + if np.all(self.left_measurements): + self.Warning.setText('Measurements recorded for all values. Please press Repeat, or Finished') + self.Repeat.setStyleSheet("color: black;background-color : none;") + self.Finished.setStyleSheet("color: white;background-color : mediumorchid;") + else: + self.Warning.setText('Please press Continue, Repeat, or Finished') + self.Continue.setStyleSheet("color: white;background-color : mediumorchid;") + self.Repeat.setStyleSheet("color: black;background-color : none;") + + def _StartCalibratingRight(self): + '''start the calibration loop of right valve''' + + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + self.StartCalibratingRight.setChecked(False) + self.StartCalibratingRight.setStyleSheet("background-color : none") + self.Warning.setText('Calibration was terminated!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.StartCalibratingRight.setEnabled(True) + return + + if self.StartCalibratingRight.isChecked(): + # change button color + self.StartCalibratingRight.setStyleSheet("background-color : green;") + QApplication.processEvents() + # disable the right valve calibration + self.StartCalibratingRight.setEnabled(False) + else: + self.StartCalibratingRight.setChecked(True) + self._Finished() + return + + # Get Calibration parameters + self.params = self.WaterCalibrationPar[self.CalibrationType.currentText()] + + # Populate options for calibrations + self.right_opentimes = np.arange( + float(self.params['TimeMin']), + float(self.params['TimeMax'])+0.0001, + float(self.params['Stride']) + ) + self.right_opentimes = [np.round(x,3) for x in self.right_opentimes] + self.RightOpenTime.clear() + for t in self.right_opentimes: + self.RightOpenTime.addItem('{0:.3f}'.format(t)) + self.WeightBeforeRight.setText('') + self.WeightAfterRight.setText('') + self.Warning.setText('') + + # Keep track of calibration status + self.calibrating_right = True + self.right_measurements = np.empty(np.shape(self.right_opentimes)) + self.right_measurements[:] = False + + # Start the first calibration + self._CalibrateRightOne() + + def _CalibrateRightOne(self,repeat=False): + ''' + Calibrate a single value + ''' + + # Determine what valve time we are measuring + if not repeat: + if np.all(self.right_measurements): + self.Warning.setText('All measurements have been completed. Either press Repeat, or Finished') + return + next_index = np.where(self.right_measurements != True)[0][0] + self.RightOpenTime.setCurrentIndex(next_index) + else: + next_index = self.RightOpenTime.currentIndex() + logging.info('Calibrating right: {}'.format(self.right_opentimes[next_index])) + + # Shuffle weights of before/after + self.WeightBeforeRight.setText(self.WeightAfterRight.text()) + self.WeightAfterRight.setText('') + + #Prompt for before weight, using field value as default + if self.WeightBeforeRight.text() != '': + before_weight = float(self.WeightBeforeRight.text()) + else: + before_weight = 0.0 + before_weight, ok = QInputDialog().getDouble( + self, + 'Box {}, Right'.format(self.MainWindow.box_letter), + "Before weight (g): ", + before_weight, + 0,1000,4) + if not ok: + # User cancels + self.Warning.setText('Press Continue, Repeat, or Finished') + return + self.WeightBeforeRight.setText(str(before_weight)) + + # Perform this measurement + current_valve_opentime = self.right_opentimes[next_index] + for i in range(int(self.params['Cycle'])): + QApplication.processEvents() + if (not self.EmergencyStop.isChecked()): + self._CalibrationStatus( + float(current_valve_opentime), + self.WeightBeforeRight.text(), + i,self.params['Cycle'], float(self.params['Interval']) + ) + + # set the valve open time + self.MainWindow.Channel.RightValue(float(current_valve_opentime)*1000) + # open the valve + self.MainWindow.Channel3.ManualWater_Right(int(1)) + # delay + time.sleep(current_valve_opentime+float(self.params['Interval'])) + else: + self.Warning.setText('Please repeat measurement') + self.WeightBeforeRight.setText('') + self.WeightAfterRight.setText('') + self.Repeat.setStyleSheet("color: white;background-color : mediumorchid;") + self.Continue.setStyleSheet("color: black;background-color : none;") + self.EmergencyStop.setChecked(False) + self.EmergencyStop.setStyleSheet("background-color : none;") + return + + # Prompt for weight + final_tube_weight = 0.0 + final_tube_weight, ok = QInputDialog().getDouble( + self, + 'Box {}, Right'.format(self.MainWindow.box_letter), + "Weight after (g): ", + final_tube_weight, + 0, 1000, 4) + if not ok: + self.Warning.setText('Please repeat measurement') + self.WeightBeforeRight.setText('') + self.WeightAfterRight.setText('') + self.Repeat.setStyleSheet("color: white;background-color : mediumorchid;") + self.Continue.setStyleSheet("color: black;background-color : none;") + return + self.WeightAfterRight.setText(str(final_tube_weight)) + + # Mark measurement as complete, save data, and update figure + self.right_measurements[next_index] = True + self._Save( + valve='Right', + valve_open_time=str(current_valve_opentime), + valve_open_interval=str(self.params['Interval']), + cycle=str(self.params['Cycle']), + total_water=float(final_tube_weight), + tube_weight=float(before_weight) + ) + self._UpdateFigure() + + # Direct user for next steps + if np.all(self.right_measurements): + self.Warning.setText('Measurements recorded for all values. Please press Repeat, or Finished') + self.Repeat.setStyleSheet("color: black;background-color : none;") + else: + self.Warning.setText('Please press Continue, Repeat, or Finished') + self.Continue.setStyleSheet("color: white;background-color : mediumorchid;") + self.Repeat.setStyleSheet("color: black;background-color : none;") + + def _CalibrationStatus(self,opentime, weight_before, i, cycle, interval): + self.Warning.setText( + 'Measuring left valve: {}s'.format(opentime) + \ + '\nEmpty tube weight: {}g'.format(weight_before) + \ + '\nCurrent cycle: '+str(i+1)+'/{}'.format(int(cycle)) + \ + '\nTime remaining: {}'.format(self._TimeRemaining( + i,cycle,opentime,interval)) + ) + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + def _Save(self,valve,valve_open_time,valve_open_interval,cycle,total_water,tube_weight,append=False): + '''save the calibrated result and update the figure''' + if total_water=='' or tube_weight=='': + return + # total water equals to total water minus tube weight + total_water=(total_water-tube_weight)*1000 # The input unit is g and converted to mg. + WaterCalibrationResults=self.WaterCalibrationResults.copy() + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + # Check and assign items to the nested dictionary + if date_str not in WaterCalibrationResults: + WaterCalibrationResults[date_str] = {} + if valve not in WaterCalibrationResults[date_str]: + WaterCalibrationResults[date_str][valve] = {} + if valve_open_time not in WaterCalibrationResults[date_str][valve]: + WaterCalibrationResults[date_str][valve][valve_open_time] = {} + if valve_open_interval not in WaterCalibrationResults[date_str][valve][valve_open_time]: + WaterCalibrationResults[date_str][valve][valve_open_time][valve_open_interval] = {} + if cycle not in WaterCalibrationResults[date_str][valve][valve_open_time][valve_open_interval]: + WaterCalibrationResults[date_str][valve][valve_open_time][valve_open_interval][cycle] = [] + if append: + WaterCalibrationResults[date_str][valve][valve_open_time][valve_open_interval][cycle].append(np.round(total_water,1)) + else: + WaterCalibrationResults[date_str][valve][valve_open_time][valve_open_interval][cycle]=[np.round(total_water,1)] + self.WaterCalibrationResults=WaterCalibrationResults.copy() + + # save to the json file + if not os.path.exists(os.path.dirname(self.MainWindow.WaterCalibrationFiles)): + os.makedirs(os.path.dirname(self.MainWindow.WaterCalibrationFiles)) + with open(self.MainWindow.WaterCalibrationFiles, "w") as file: + json.dump(WaterCalibrationResults, file,indent=4) + + # update the figure + self._UpdateFigure() + + def _UpdateFigure(self): + '''plot the calibration result''' + if self.ToInitializeVisual==1: # only run once + PlotM=PlotWaterCalibration(water_win=self) + self.PlotM=PlotM + layout=self.VisuCalibration.layout() + if layout is not None: + for i in reversed(range(layout.count())): + layout.itemAt(i).widget().setParent(None) + layout.invalidate() + if layout is None: + layout=QVBoxLayout(self.VisuCalibration) + toolbar = NavigationToolbar(PlotM, self) + toolbar.setMaximumHeight(20) + toolbar.setMaximumWidth(300) + layout.addWidget(toolbar) + layout.addWidget(PlotM) + self.ToInitializeVisual=0 + self.PlotM._Update() + + def _ToggleValve(self, button, valve: Literal['Left', 'Right']): + """ + Toggle open/close state of specified valve and set up logic based on button pressed + :param button: button that was pressed + :param valve: which valve to open. Restricted to Right or Left + """ + + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + + set_valve_time = getattr(self.MainWindow.Channel, f'{valve}Value') + toggle_valve_state = getattr(self.MainWindow.Channel3, f'ManualWater_{valve}') + open_timer = getattr(self, f'{valve.lower()}_open_timer') + close_timer = getattr(self, f'{valve.lower()}_close_timer') + text_timer = getattr(self, f'{valve.lower()}_text_timer') + + if button.isChecked(): # open valve + button.setStyleSheet("background-color : green;") + set_valve_time(float(1000) * 1000) # set the valve open time to max value + toggle_valve_state(int(1)) # set valve initially open + + if button.text() == f'Open {valve.lower()} 5ml': # set up additional logic to only open for 5ml + five_ml_time_ms = round(self._VolumeToTime(5000, valve) * 1000) # calculate time for valve to stay open + close_timer.setInterval(five_ml_time_ms) # set interval of valve close time to be five_ml_time_ms + close_timer.setSingleShot(True) # only trigger once when 5ml has been expelled + text_timer.start() # start timer to update text + close_timer.start() + + open_timer.start() + + else: # close open valve + # change button color + button.setStyleSheet("background-color : none") + open_timer.stop() + if f'Open {valve.lower()} 5ml' in button.text(): + close_timer.stop() + text_timer.stop() + button.setText(f'Open {valve.lower()} 5ml') + + # close the valve + toggle_valve_state(int(1)) + + # reset the default valve open time + time.sleep(0.01) + set_valve_time(float(getattr(self.MainWindow, f'{valve}Value').text())*1000) + + def reopen_valve(self, valve: Literal['Left', 'Right']): + """Function to reopen the right or left water line open. Valve must be open prior to calling this function. + Calling ManualWater_ will toggle state of valve so need to call twice on already open valve. + param valve: string specifying right or left valve""" + + # get correct function based on input valve name + getattr(self.MainWindow.Channel3, f'ManualWater_{valve}')(int(1)) # close valve + getattr(self.MainWindow.Channel3, f'ManualWater_{valve}')(int(1)) # open valve + + def _TimeRemaining(self,i, cycles, opentime, interval): + total_seconds = (cycles-i)*(opentime+interval) + minutes = int(np.floor(total_seconds/60)) + seconds = int(np.ceil(np.mod(total_seconds,60))) + return '{}:{:02}'.format(minutes, seconds) + + def _VolumeToTime(self, volume, valve: Literal['Left', 'Right'] ): + """ + Function to return the amount of time(s) it takes for water line to flush specified volume of water (mg) + :param volume: volume to flush in mg + :param valve: string specifying right or left valve + """ + # x = (y-b)/m + if hasattr(self.MainWindow, 'latest_fitting') and self.MainWindow.latest_fitting != {}: + fit = self.MainWindow.latest_fitting[valve] + m = fit[0] + b = fit[1] + else: + m = 1 + b = 0 + return (volume-b)/m + + def _TimeToVolume(self,time): + # y= mx +b + if hasattr(self.MainWindow, 'latest_fitting'): + print(self.MainWindow.latest_fitting) + else: + m = 1 + b = 0 + return time*m+b + + def _SpotCheck(self, valve: Literal['Left', 'Right']): + + """ + Calibration of valve in a different thread + :param valve: string specifying which valve + """ + + spot_check = getattr(self, f'SpotCheck{valve}') + save = getattr(self, f'Save{valve}') + total_water = getattr(self, f'TotalWaterSingle{valve}') + pre_weight = getattr(self, f'SpotCheckPreWeight{valve}') + volume = getattr(self, f'Spot{valve}Volume').text() + + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully == 0: + spot_check.setChecked(False) + spot_check.setStyleSheet("background-color : none;") + save.setStyleSheet("color: black;background-color : none;") + total_water.setText('') + pre_weight.setText('') + return + + if spot_check.isChecked(): + if valve not in self.MainWindow.latest_fitting: + reply = QMessageBox.critical(self, f'Spot check {valve.lower()}', + 'Please perform full calibration before spot check', + QMessageBox.Ok) + logging.warning('Cannot perform spot check before full calibration') + spot_check.setStyleSheet("background-color : none;") + spot_check.setChecked(False) + self.Warning.setText('') + pre_weight.setText('') + total_water.setText('') + save.setStyleSheet("color: black;background-color : none;") + return + + logging.info(f'starting spot check {valve.lower()}') + spot_check.setStyleSheet("background-color : green;") + + # Get empty tube weight, using field value as default + if pre_weight.text() != '': + empty_tube_weight = float(pre_weight.text()) + else: + empty_tube_weight = 0.0 + empty_tube_weight, ok = QInputDialog().getDouble( + self, + f'Box {self.MainWindow.box_letter}, f{valve}', + "Empty tube weight (g): ", + empty_tube_weight, + 0, 1000, 4) + if not ok: + # User cancels + logging.warning('user cancelled spot calibration') + spot_check.setStyleSheet("background-color : none;") + spot_check.setChecked(False) + self.Warning.setText(f'Spot check {valve.lower()} cancelled') + pre_weight.setText('') + total_water.setText('') + save.setStyleSheet("color: black;background-color : none;") + return + pre_weight.setText(str(empty_tube_weight)) + + # Determine what open time to use + open_time = self._VolumeToTime(float(volume), valve) + open_time = np.round(open_time, 4) + setattr(self, f'Spot{valve}OpenTime', open_time) + logging.info('Using a calibration spot check of {}s to deliver {}uL'.format(open_time, + volume)) + + # start the open/close/delay cycle + for i in range(int(self.SpotCycle)): + QApplication.processEvents() + if spot_check.isChecked() and (not self.EmergencyStop.isChecked()): + self.Warning.setText( + f'Measuring {valve.lower()} valve: {volume}uL' + \ + '\nEmpty tube weight: {}g'.format(empty_tube_weight) + \ + '\nCurrent cycle: ' + str(i + 1) + '/{}'.format(int(self.SpotCycle)) + \ + '\nTime remaining: {}'.format(self._TimeRemaining( + i, self.SpotCycle, open_time, self.SpotInterval)) + ) + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + # set the valve open time + getattr(self.MainWindow.Channel, f'{valve}Value')(float(open_time) * 1000) + # open the valve + getattr(self.MainWindow.Channel3, f'ManualWater_{valve}')(int(1)) + # delay + time.sleep(open_time + self.SpotInterval) + else: + self.Warning.setText(f'Spot check {valve.lower()} cancelled') + pre_weight.setText('') + total_water.setText('') + save.setStyleSheet("color: black;background-color : none;") + self.EmergencyStop.setChecked(False) + self.EmergencyStop.setStyleSheet("background-color : none;") + spot_check.setChecked(False) + spot_check.setStyleSheet("background-color : none") + return + + # Get final value, using field as default + if total_water.text() != '': + final_tube_weight = float(total_water.text()) + else: + final_tube_weight = 0.0 + final_tube_weight, ok = QInputDialog().getDouble( + self, + f'Box {self.MainWindow.box_letter}, {valve}', + "Final tube weight (g): ", + final_tube_weight, + 0, 1000, 4) + total_water.setText(str(final_tube_weight)) + + # Determine result + result = (final_tube_weight - empty_tube_weight) / int(self.SpotCycle) * 1000 + + error = result - float(volume) + error = np.round(error, 4) + self.Warning.setText( + f'Measuring {valve.lower()} valve: {volume}uL' + \ + '\nEmpty tube weight: {}g'.format(empty_tube_weight) + \ + '\nFinal tube weight: {}g'.format(final_tube_weight) + \ + '\nAvg. error from target: {}uL'.format(error) + ) + + TOLERANCE = float(volume) * .15 + if np.abs(error) > TOLERANCE: + reply = QMessageBox.critical(self, f'Spot check {valve}', + 'Measurement is outside expected tolerance.
' + 'If this is a typo, please press cancel.' + '
IMPORTANT: ' + 'If the measurement was correctly entered, please press okay and repeat' + 'spot check once.'.format(np.round(result, 2)), + QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Cancel: + logging.warning('Spot check discarded due to type', extra={'tags': self.MainWindow.warning_log_tag}) + else: + logging.error('Water calibration spot check exceeds tolerance: {}'.format(error)) + save.setStyleSheet("color: white;background-color : mediumorchid;") + self.Warning.setText( + f'Measuring {valve.lower()} valve: {volume}uL' + \ + '\nEmpty tube weight: {}g'.format(empty_tube_weight) + \ + '\nFinal tube weight: {}g'.format(final_tube_weight) + \ + '\nAvg. error from target: {}uL'.format(error) + ) + self._SaveValve(valve) + if self.check_spot_failures(valve) >= 2: + msg = 'Two or more spot checks have failed in the last 30 days. Please create a SIPE ticket to ' \ + 'check rig.' + logging.error(msg, extra={'tags': self.MainWindow.warning_log_tag}) + QMessageBox.critical(self, f'Spot check {valve}', msg, QMessageBox.Ok) + else: + self.Warning.setText( + f'Measuring {valve.lower()} valve: {volume}uL' + \ + '\nEmpty tube weight: {}g'.format(empty_tube_weight) + \ + '\nFinal tube weight: {}g'.format(final_tube_weight) + \ + '\nAvg. error from target: {}uL'.format(error) + \ + '\nCalibration saved' + ) + self._SaveValve(valve) + + # set the default valve open time + value = getattr(self.MainWindow, f'{valve}Value').text() + getattr(self.MainWindow.Channel, f'{valve}Value')(float(value) * 1000) + + spot_check.setChecked(False) + spot_check.setStyleSheet("background-color : none") + logging.info(f'Done with spot check {valve}') + + def check_spot_failures(self, valve: Literal['Left', 'Right']) -> int: + + """" + Parse water calibration file to check if 2 spot failures have occurred in the past 30 days + :param valve: side to check for failures + :return integer signifying the number of spot checks that have failed within the last 30 days + """ + + today = datetime.now() + # filter spot counts within the last 30 days + spot_counts = {k: v for k, v in self.WaterCalibrationResults.items() if f'Spot{valve}' in v.keys() + and (today-datetime.strptime(k, "%Y-%m-%d")).days < 30} + + # based on information in spot check dictionary, calculate volume + over_tolerance = 0 + volume = float(getattr(self, f'Spot{valve}Volume').text()) + TOLERANCE = volume * .15 + for info in spot_counts.values(): + for intervals in info[f'Spot{valve}'].values(): + for interval in intervals.values(): + for cycle, measurements in interval.items(): + for measurement in measurements: + result = float(measurement) / float(cycle) + if np.abs(np.round(result - volume, 4)) > TOLERANCE: + over_tolerance += 1 + return over_tolerance + +class CameraDialog(QDialog): + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('Camera.ui', self) + + self.MainWindow=MainWindow + self._connectSignalsSlots() + self.camera_start_time='' + self.camera_stop_time='' + + self.info_label = QLabel(parent=self) + self.info_label.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.info_label.move(50, 350) + self.info_label.setFixedSize(171, 51) + self.info_label.setAlignment(Qt.AlignCenter) + + def _connectSignalsSlots(self): + self.StartRecording.toggled.connect(self._StartCamera) + self.StartPreview.toggled.connect(self._start_preview) + self.AutoControl.currentIndexChanged.connect(self._AutoControl) + self.OpenSaveFolder.clicked.connect(self._OpenSaveFolder) + + def _OpenSaveFolder(self): + '''Open the log/save folder of the camera''' + + text = self.info_label.text() + if hasattr(self.MainWindow,'Ot_log_folder'): + try: + subprocess.Popen(['explorer', os.path.join(os.path.dirname(os.path.dirname(self.MainWindow.Ot_log_folder)),'behavior-videos')]) + except Exception as e: + logging.error(str(e)) + logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) + if 'No logging folder found!' not in text: + self.info_label.setText(text + '\n No logging folder found!') + else: + logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) + if 'No logging folder found!' not in text: + self.info_label.setText(text + '\n No logging folder found!') + + def _start_preview(self): + '''Start the camera preview''' + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + if self.StartPreview.isChecked(): + # disable the start recording button + self.StartRecording.setEnabled(False) + # subscribe to the camera preview + self.MainWindow.Channel.CameraStartType(int(2)) + # set the camera frequency + self.MainWindow.Channel.CameraFrequency(int(self.FrameRate.text())) + # start the video triggers + self.MainWindow.Channel.CameraControl(int(1)) + + self.StartPreview.setStyleSheet("background-color : green;") + logging.info('Camera is on', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is on') + + else: + # enable the start recording button + self.StartRecording.setEnabled(True) + # stop camera triggers + self.MainWindow.Channel.CameraControl(int(2)) + # stop the camera preview workflow + self.MainWindow.Channel.StopCameraPreview(int(1)) + + self.StartPreview.setStyleSheet("background-color : none;") + logging.info('Camera is off', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is off') + + def _AutoControl(self): + '''Trigger the camera during the start of a new behavior session''' + if self.AutoControl.currentText()=='Yes': + self.StartRecording.setChecked(False) + + def _StartCamera(self): + '''Start/stop the camera recording based on if the StartRecording button is toggled on/off''' + + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + if self.StartRecording.isChecked(): + self.StartRecording.setStyleSheet("background-color : green;") + logging.info('Camera is turning on', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is turning on') + QApplication.processEvents() + # untoggle the preview button + if self.StartPreview.isChecked(): + self.StartPreview.setChecked(False) + # sleep for 1 second to make sure the trigger is off + time.sleep(1) + # Start logging if the formal logging is not started + if self.MainWindow.logging_type!=0 or self.MainWindow.logging_type==-1: + self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() + # set to check drop frame as true + self.MainWindow.to_check_drop_frames=1 + # disable the start preview button + self.StartPreview.setEnabled(False) + # disable the Load button + self.MainWindow.Load.setEnabled(False) + # disable the Animal ID + self.MainWindow.ID.setEnabled(False) + # set the camera start type + self.MainWindow.Channel.CameraStartType(int(1)) + # set the camera frequency. + self.MainWindow.Channel.CameraFrequency(int(self.FrameRate.text())) + # start the video triggers + self.MainWindow.Channel.CameraControl(int(1)) + time.sleep(5) + self.camera_start_time = str(datetime.now()) + logging.info('Camera is on!', extra={'tags': [self.MainWindow.warning_log_tag]}) + self.info_label.setText('Camera is on!') + else: + self.StartRecording.setStyleSheet("background-color : none") + logging.info('Camera is turning off', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is turning off') + QApplication.processEvents() + self.MainWindow.Channel.CameraControl(int(2)) + self.camera_stop_time = str(datetime.now()) + time.sleep(5) + logging.info('Camera is off!', extra={'tags': [self.MainWindow.warning_log_tag]}) + self.info_label.setText('Camera is off!') + +def is_file_in_use(file_path): + '''check if the file is open''' + if os.path.exists(file_path): + try: + os.rename(file_path, file_path) + return False + except OSError as e: + return True + +class LaserCalibrationDialog(QDialog): + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + self.MainWindow=MainWindow + uic.loadUi('CalibrationLaser.ui', self) + + self._connectSignalsSlots() + self.SleepComplete=1 + self.SleepComplete2=0 + self.Initialize1=0 + self.Initialize2=0 + self.threadpool1=QThreadPool() + self.threadpool2=QThreadPool() + self.laser_tags=[1,2] + self.condition_idx=[1,2,3,4,5,6] + def _connectSignalsSlots(self): + self.Open.clicked.connect(self._Open) + self.KeepOpen.clicked.connect(self._KeepOpen) + self.CopyFromOpto.clicked.connect(self._CopyFromOpto) + self.Save.clicked.connect(self._Save) + self.Capture.clicked.connect(self._Capture) + self.LaserColor_1.currentIndexChanged.connect(self._LaserColor_1) + self.Protocol_1.activated.connect(self._activated_1) + self.Protocol_1.currentIndexChanged.connect(self._activated_1) + self.Flush_DO0.clicked.connect(self._FLush_DO0) + self.Flush_DO1.clicked.connect(self._FLush_DO1) + self.Flush_DO2.clicked.connect(self._FLush_DO2) + self.Flush_DO3.clicked.connect(self._FLush_DO3) + self.Flush_Port2.clicked.connect(self._FLush_Port2) + self.CopyToSession.clicked.connect(self._CopyToSession) + def _CopyToSession(self): + '''Copy the calibration data to the session calibration''' + if self.Location_1.currentText()=='Laser_1': + self.MainWindow.Opto_dialog.laser_1_calibration_voltage.setText(self.voltage.text()) + self.MainWindow.Opto_dialog.laser_1_calibration_power.setText(self.LaserPowerMeasured.text()) + elif self.Location_1.currentText()=='Laser_2': + self.MainWindow.Opto_dialog.laser_2_calibration_voltage.setText(self.voltage.text()) + self.MainWindow.Opto_dialog.laser_2_calibration_power.setText(self.LaserPowerMeasured.text()) + + def _FLush_DO0(self): + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + self.MainWindow.Channel.DO0(int(1)) + self.MainWindow.Channel.receive() + def _FLush_DO1(self): + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + self.MainWindow.Channel.DO1(int(1)) + def _FLush_DO2(self): + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + #self.MainWindow.Channel.DO2(int(1)) + self.MainWindow.Channel.receive() + def _FLush_DO3(self): + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + self.MainWindow.Channel.DO3(int(1)) + self.MainWindow.Channel.receive() + def _FLush_Port2(self): + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + self.MainWindow.Channel.Port2(int(1)) + + def _LaserColor_1(self): + self._LaserColor(1) + def _activated_1(self): + self._activated(1) + def _activated(self,Numb): + '''enable/disable items based on protocols and laser start/end''' + Inactlabel1=15 # pulse duration + Inactlabel2=13 # frequency + Inactlabel3=14 # Ramping down + if getattr(self, 'Protocol_'+str(Numb)).currentText() == 'Sine': + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel1)).setEnabled(False) + getattr(self, 'PulseDur_'+str(Numb)).setEnabled(False) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel2)).setEnabled(True) + getattr(self, 'Frequency_'+str(Numb)).setEnabled(True) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel3)).setEnabled(True) + getattr(self, 'RD_'+str(Numb)).setEnabled(True) + if getattr(self, 'Protocol_'+str(Numb)).currentText() =='Pulse': + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel1)).setEnabled(True) + getattr(self, 'PulseDur_'+str(Numb)).setEnabled(True) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel2)).setEnabled(True) + getattr(self, 'Frequency_'+str(Numb)).setEnabled(True) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel3)).setEnabled(False) + getattr(self, 'RD_'+str(Numb)).setEnabled(False) + if getattr(self, 'Protocol_'+str(Numb)).currentText() =='Constant': + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel1)).setEnabled(False) + getattr(self, 'PulseDur_'+str(Numb)).setEnabled(False) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel2)).setEnabled(False) + getattr(self, 'Frequency_'+str(Numb)).setEnabled(False) + getattr(self, 'label'+str(Numb)+'_'+str(Inactlabel3)).setEnabled(True) + getattr(self, 'RD_'+str(Numb)).setEnabled(True) + + def _LaserColor(self,Numb): + ''' enable/disable items based on laser (blue/green/orange/red/NA)''' + Inactlabel=[2,3,5,12,13,14,15] + if getattr(self, 'LaserColor_'+str(Numb)).currentText()=='NA': + Label=False + else: + Label=True + getattr(self, 'Location_'+str(Numb)).setEnabled(Label) + getattr(self, 'Duration_'+str(Numb)).setEnabled(Label) + getattr(self, 'Protocol_'+str(Numb)).setEnabled(Label) + getattr(self, 'Frequency_'+str(Numb)).setEnabled(Label) + getattr(self, 'RD_'+str(Numb)).setEnabled(Label) + getattr(self, 'PulseDur_'+str(Numb)).setEnabled(Label) + for i in Inactlabel: + getattr(self, 'label'+str(Numb)+'_'+str(i)).setEnabled(Label) + if getattr(self, 'LaserColor_'+str(Numb)).currentText() != 'NA': + getattr(self, '_activated_' + str(Numb))() + + def _GetLaserWaveForm(self): + '''Get the waveform of the laser. It dependens on color/duration/protocol(frequency/RD/pulse duration)/locations/laser power''' + N=str(1) + # CLP, current laser parameter + self.CLP_Color = getattr(self, 'LC_LaserColor_' + N) + self.CLP_Location = getattr(self, 'LC_Location_' + N) + self.CLP_Duration = float(getattr(self, 'LC_Duration_' + N)) + self.CLP_Protocol = getattr(self, 'LC_Protocol_' + N) + self.CLP_Frequency = float(getattr(self, 'LC_Frequency_' + N)) + self.CLP_RampingDown = float(getattr(self, 'LC_RD_' + N)) + self.CLP_PulseDur = getattr(self, 'LC_PulseDur_' + N) + self.CLP_SampleFrequency = float(self.LC_SampleFrequency) + self.CLP_CurrentDuration = self.CLP_Duration + self.CLP_InputVoltage = float(self.voltage.text()) + # generate the waveform based on self.CLP_CurrentDuration and Protocol, Frequency, RampingDown, PulseDur + self._GetLaserAmplitude() + # send the trigger source. It's '/Dev1/PFI0' ( P2.0 of NIdaq USB6002) by default + self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') + # dimension of self.CurrentLaserAmplitude indicates how many locations do we have + for i in range(len(self.CurrentLaserAmplitude)): + # in some cases the other paramters except the amplitude could also be different + self._ProduceWaveForm(self.CurrentLaserAmplitude[i]) + setattr(self, 'WaveFormLocation_' + str(i+1), self.my_wave) + setattr(self, f"Location{i+1}_Size", getattr(self, f"WaveFormLocation_{i+1}").size) + #send waveform and send the waveform size + getattr(self.MainWindow.Channel, 'Location'+str(i+1)+'_Size')(int(getattr(self, 'Location'+str(i+1)+'_Size'))) + getattr(self.MainWindow.Channel4, 'WaveForm' + str(1)+'_'+str(i+1))(str(getattr(self, 'WaveFormLocation_'+str(i+1)).tolist())[1:-1]) + FinishOfWaveForm=self.MainWindow.Channel4.receive() + def _ProduceWaveForm(self,Amplitude): + '''generate the waveform based on Duration and Protocol, Laser Power, Frequency, RampingDown, PulseDur and the sample frequency''' + if self.CLP_Protocol=='Sine': + resolution=self.CLP_SampleFrequency*self.CLP_CurrentDuration # how many datapoints to generate + cycles=self.CLP_CurrentDuration*self.CLP_Frequency # how many sine cycles + length = np.pi * 2 * cycles + self.my_wave = Amplitude*(1+np.sin(np.arange(0+1.5*math.pi, length+1.5*math.pi, length / resolution)))/2 + # add ramping down + if self.CLP_RampingDown>0: + if self.CLP_RampingDown>self.CLP_CurrentDuration: + logging.warning('Ramping down is longer than the laser duration!', + extra={'tags': [self.MainWindow.warning_log_tag]}) + else: + Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) + RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) + RampingDown = np.concatenate((Constant, RD), axis=0) + self.my_wave=self.my_wave*RampingDown + self.my_wave=np.append(self.my_wave,[0,0]) + elif self.CLP_Protocol=='Pulse': + if self.CLP_PulseDur=='NA': + logging.warning('Pulse duration is NA!', extra={'tags': [self.MainWindow.warning_log_tag]}) + else: + self.CLP_PulseDur=float(self.CLP_PulseDur) + PointsEachPulse=int(self.CLP_SampleFrequency*self.CLP_PulseDur) + PulseIntervalPoints=int(1/self.CLP_Frequency*self.CLP_SampleFrequency-PointsEachPulse) + if PulseIntervalPoints<0: + logging.warning('Pulse frequency and pulse duration are not compatible!', + extra={'tags': [self.MainWindow.warning_log_tag]}) + TotalPoints=int(self.CLP_SampleFrequency*self.CLP_CurrentDuration) + PulseNumber=np.floor(self.CLP_CurrentDuration*self.CLP_Frequency) + EachPulse=Amplitude*np.ones(PointsEachPulse) + PulseInterval=np.zeros(PulseIntervalPoints) + WaveFormEachCycle=np.concatenate((EachPulse, PulseInterval), axis=0) + self.my_wave=np.empty(0) + # pulse number should be greater than 0 + if PulseNumber>1: + for i in range(int(PulseNumber-1)): + self.my_wave=np.concatenate((self.my_wave, WaveFormEachCycle), axis=0) + else: + logging.warning('Pulse number is less than 1!', extra={'tags': [self.MainWindow.warning_log_tag]}) + return + self.my_wave=np.concatenate((self.my_wave, EachPulse), axis=0) + self.my_wave=np.concatenate((self.my_wave, np.zeros(TotalPoints-np.shape(self.my_wave)[0])), axis=0) + self.my_wave=np.append(self.my_wave,[0,0]) + elif self.CLP_Protocol=='Constant': + resolution=self.CLP_SampleFrequency*self.CLP_CurrentDuration # how many datapoints to generate + self.my_wave=Amplitude*np.ones(int(resolution)) + if self.CLP_RampingDown>0: + # add ramping down + if self.CLP_RampingDown>self.CLP_CurrentDuration: + logging.warning('Ramping down is longer than the laser duration!', + extra={'tags': [self.MainWindow.warning_log_tag]}) + else: + Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) + RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) + RampingDown = np.concatenate((Constant, RD), axis=0) + self.my_wave=self.my_wave*RampingDown + self.my_wave=np.append(self.my_wave,[0,0]) + else: + logging.warning('Unidentified optogenetics protocol!', extra={'tags': [self.MainWindow.warning_log_tag]}) + + def _GetLaserAmplitude(self): + '''the voltage amplitude dependens on Protocol, Laser Power, Laser color, and the stimulation locations<>''' + if self.CLP_Location=='Laser_1': + self.CurrentLaserAmplitude=[self.CLP_InputVoltage,0] + elif self.CLP_Location=='Laser_2': + self.CurrentLaserAmplitude=[0,self.CLP_InputVoltage] + elif self.CLP_Location=='Both': + self.CurrentLaserAmplitude=[self.CLP_InputVoltage,self.CLP_InputVoltage] + else: + logging.warning('No stimulation location defined!', extra={'tags': [self.MainWindow.warning_log_tag]}) + + # get training parameters + def _GetTrainingParameters(self,win): + '''Get training parameters''' + Prefix='LC' # laser calibration + # Iterate over each container to find child widgets and store their values in self + for container in [win.LaserCalibration_dialog]: + # Iterate over each child of the container that is a QLineEdit or QDoubleSpinBox + for child in container.findChildren((QtWidgets.QLineEdit, QtWidgets.QDoubleSpinBox)): + # Set an attribute in self with the name 'TP_' followed by the child's object name + # and store the child's text value + setattr(self, Prefix+'_'+child.objectName(), child.text()) + # Iterate over each child of the container that is a QComboBox + for child in container.findChildren(QtWidgets.QComboBox): + # Set an attribute in self with the name 'TP_' followed by the child's object name + # and store the child's current text value + setattr(self, Prefix+'_'+child.objectName(), child.currentText()) + # Iterate over each child of the container that is a QPushButton + for child in container.findChildren(QtWidgets.QPushButton): + # Set an attribute in self with the name 'TP_' followed by the child's object name + # and store whether the child is checked or not + setattr(self, Prefix+'_'+child.objectName(), child.isChecked()) + def _InitiateATrial(self): + '''Initiate calibration in bonsai''' + # start generating waveform in bonsai + self.MainWindow.Channel.OptogeneticsCalibration(int(1)) + self.MainWindow.Channel.receive() + def _CopyFromOpto(self): + '''Copy the optogenetics parameters''' + condition=self.CopyCondition.currentText().split('_')[1] + copylaser=self.CopyLaser.currentText().split('_')[1] + if self.MainWindow.Opto_dialog.__getattribute__("LaserColor_" + condition).currentText()=="NA": + return + #self.Duration_1.setText(self.MainWindow.Opto_dialog.__getattribute__("Duration_" + condition).text()) + self.Frequency_1.setText(self.MainWindow.Opto_dialog.__getattribute__("Frequency_" + condition).currentText()) + self.RD_1.setText(self.MainWindow.Opto_dialog.__getattribute__("RD_" + condition).text()) + self.PulseDur_1.setText(self.MainWindow.Opto_dialog.__getattribute__("PulseDur_" + condition).text()) + self.LaserColor_1.setCurrentIndex(self.MainWindow.Opto_dialog.__getattribute__("LaserColor_" + condition).currentIndex()) + self.Location_1.setCurrentIndex(self.Location_1.findText(self.CopyLaser.currentText())) + if self.MainWindow.Opto_dialog.__getattribute__("Protocol_" + condition).currentText()=='Pulse': + ind=self.Protocol_1.findText('Constant') + else: + ind=self.MainWindow.Opto_dialog.__getattribute__("Protocol_" + condition).currentIndex() + self.Protocol_1.setCurrentIndex(ind) + self.voltage.setText(str(eval(self.MainWindow.Opto_dialog.__getattribute__(f"Laser{copylaser}_power_{condition}").currentText())[0])) + + + def _Capture(self): + '''Save the measured laser power''' + self.Capture.setStyleSheet("background-color : green;") + QApplication.processEvents() + self._GetTrainingParameters(self.MainWindow) + self.Warning.setText('') + if self.Location_1.currentText()=='Both': + self.Warning.setText('Data not captured! Please choose left or right, not both!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.Warning.setAlignment(Qt.AlignCenter) + return + if self.LaserPowerMeasured.text()=='': + self.Warning.setText('Data not captured! Please enter power measured!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.Warning.setAlignment(Qt.AlignCenter) + return + for attr_name in dir(self): + if attr_name.startswith('LC_'): + if hasattr(self,'LCM_'+attr_name[3:]): # LCM means measured laser power from calibration + self.__getattribute__('LCM_'+attr_name[3:]).append(getattr(self,attr_name)) + else: + setattr(self,'LCM_'+attr_name[3:],[getattr(self,attr_name)]) + # save the measure time + if hasattr(self,'LCM_MeasureTime'): + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + time_str = current_time.strftime("%H:%M:%S") + self.LCM_MeasureTime.append(date_str+' '+time_str) + else: + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + time_str = current_time.strftime("%H:%M:%S") + self.LCM_MeasureTime=[date_str+' '+time_str] + time.sleep(0.01) + self.Capture.setStyleSheet("background-color : none") + self.Capture.setChecked(False) + def _Save(self): + '''Save captured laser calibration results to json file and update the GUI''' + self.Save.setStyleSheet("background-color : green;") + QApplication.processEvents() + if not hasattr(self.MainWindow,'LaserCalibrationResults'): + self.MainWindow.LaserCalibrationResults={} + LaserCalibrationResults={} + else: + LaserCalibrationResults=self.MainWindow.LaserCalibrationResults + try: + self.LCM_MeasureTime.copy() + except Exception as e: + logging.error(str(e)) + self.Warning.setText('Data not saved! Please Capture the power first!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.Warning.setAlignment(Qt.AlignCenter) + return + # delete invalid indices + empty_indices = [index for index, value in enumerate(self.LCM_LaserPowerMeasured) if value == ''] + both_indices = [index for index, value in enumerate(self.LCM_Location_1) if value == 'Both'] + delete_indices=both_indices+empty_indices + delete_indices=list(set(delete_indices)) + delete_indices.sort(reverse=True) + for index in delete_indices: + del self.LCM_MeasureTime[index] + del self.LCM_LaserColor_1[index] + del self.LCM_Protocol_1[index] + del self.LCM_Frequency_1[index] + del self.LCM_LaserPowerMeasured[index] + del self.LCM_Location_1[index] + del self.LCM_voltage[index] + LCM_MeasureTime_date=[] + for i in range(len(self.LCM_MeasureTime)): + LCM_MeasureTime_date.append(self.LCM_MeasureTime[i].split()[0]) + date_unique = list(set(LCM_MeasureTime_date)) + for i in range(len(date_unique)): + current_date=date_unique[i] + current_date_name=current_date + ''' + #give different names to calibrations in the same day + while 1: + if len(current_date_name.split('_'))==1: + current_date_name=current_date_name+'_1' + else: + current_date_name=current_date_name.split('_')[0]+'_'+str(int(current_date_name.split('_')[1])+1) + if not current_date_name in LaserCalibrationResults.keys(): + break + ''' + current_date_ind=[index for index, value in enumerate(LCM_MeasureTime_date) if value == current_date] + laser_colors= self._extract_elements(self.LCM_LaserColor_1,current_date_ind) + laser_colors_unique= list(set(laser_colors)) + for j in range(len(laser_colors_unique)): + current_color=laser_colors_unique[j] + current_color_ind=[index for index, value in enumerate(self.LCM_LaserColor_1) if value == current_color] + current_color_ind=list(set(current_color_ind) & set(current_date_ind)) + Protocols= self._extract_elements(self.LCM_Protocol_1,current_color_ind) + Protocols_unique=list(set(Protocols)) + for k in range(len(Protocols_unique)): + current_protocol=Protocols_unique[k] + current_protocol_ind=[index for index, value in enumerate(self.LCM_Protocol_1) if value == current_protocol] + current_protocol_ind = list(set(current_protocol_ind) & set(current_color_ind)) + if current_protocol=='Sine': + Frequency=self._extract_elements(self.LCM_Frequency_1,current_protocol_ind) + Frequency_unique=list(set(Frequency)) + for m in range(len(Frequency_unique)): + current_frequency=Frequency_unique[m] + current_frequency_ind=[index for index, value in enumerate(self.LCM_Frequency_1) if value == current_frequency] + current_frequency_ind = list(set(current_frequency_ind) & set(current_protocol_ind)) + for laser_tag in self.laser_tags: + ItemsLaserPower=self._get_laser_power_list(current_frequency_ind,laser_tag) + LaserCalibrationResults=initialize_dic(LaserCalibrationResults,key_list=[current_date_name,current_color,current_protocol,current_frequency,f"Laser_{laser_tag}"]) + if 'LaserPowerVoltage' not in LaserCalibrationResults[current_date_name][current_color][current_protocol][current_frequency][f"Laser_{laser_tag}"]: + LaserCalibrationResults[current_date_name][current_color][current_protocol][current_frequency][f"Laser_{laser_tag}"]['LaserPowerVoltage']=ItemsLaserPower + else: + LaserCalibrationResults[current_date_name][current_color][current_protocol][current_frequency][f"Laser_{laser_tag}"]['LaserPowerVoltage']=self._unique(LaserCalibrationResults[current_date_name][current_color][current_protocol][current_frequency][f"Laser_{laser_tag}"]['LaserPowerVoltage']+ItemsLaserPower) + elif current_protocol=='Constant' or current_protocol=='Pulse': + for laser_tag in self.laser_tags: + ItemsLaserPower=self._get_laser_power_list(current_protocol_ind,laser_tag) + # Check and assign items to the nested dictionary + LaserCalibrationResults=initialize_dic(LaserCalibrationResults,key_list=[current_date_name,current_color,current_protocol,f"Laser_{laser_tag}"]) + if 'LaserPowerVoltage' not in LaserCalibrationResults[current_date_name][current_color][current_protocol][f"Laser_{laser_tag}"]: + LaserCalibrationResults[current_date_name][current_color][current_protocol][f"Laser_{laser_tag}"]['LaserPowerVoltage']=ItemsLaserPower + else: + LaserCalibrationResults[current_date_name][current_color][current_protocol][f"Laser_{laser_tag}"]['LaserPowerVoltage']=self._unique(LaserCalibrationResults[current_date_name][current_color][current_protocol][f"Laser_{laser_tag}"]['LaserPowerVoltage']+ItemsLaserPower) + if current_protocol=='Constant':# copy results of constant to pulse + LaserCalibrationResults=initialize_dic(LaserCalibrationResults,key_list=[current_date_name,current_color,'Pulse',f"Laser_{laser_tag}"]) + if 'LaserPowerVoltage' not in LaserCalibrationResults[current_date_name][current_color]['Pulse'][f"Laser_{laser_tag}"]: + LaserCalibrationResults[current_date_name][current_color]['Pulse'][f"Laser_{laser_tag}"]['LaserPowerVoltage']=ItemsLaserPower + else: + LaserCalibrationResults[current_date_name][current_color]['Pulse'][f"Laser_{laser_tag}"]['LaserPowerVoltage']=self._unique(LaserCalibrationResults[current_date_name][current_color]['Pulse'][f"Laser_{laser_tag}"]['LaserPowerVoltage']+ItemsLaserPower) + # save to json file + if not os.path.exists(os.path.dirname(self.MainWindow.LaserCalibrationFiles)): + os.makedirs(os.path.dirname(self.MainWindow.LaserCalibrationFiles)) + with open(self.MainWindow.LaserCalibrationFiles, "w") as file: + json.dump(LaserCalibrationResults, file,indent=4) + self.Warning.setText('') + if LaserCalibrationResults=={}: + self.Warning.setText('Data not saved! Please enter power measured!') + self.Warning.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.Warning.setAlignment(Qt.AlignCenter) + return + self.MainWindow.LaserCalibrationResults=LaserCalibrationResults + self.MainWindow._GetLaserCalibration() + for i in self.condition_idx: + getattr(self.MainWindow.Opto_dialog, f'_LaserColor')(i) + time.sleep(0.01) + self.Save.setStyleSheet("background-color : none") + self.Save.setChecked(False) + # Clear captured data + self.LCM_MeasureTime=[] + self.LCM_LaserColor_1=[] + self.LCM_Protocol_1=[] + self.LCM_Frequency_1=[] + self.LCM_LaserPowerMeasured=[] + self.LCM_Location_1=[] + self.LCM_voltage=[] + def _get_laser_power_list(self,ind,laser_tag): + '''module to get the laser power list''' + ItemsLaserPower=[] + current_laser_tag_ind=[index for index, value in enumerate(self.LCM_Location_1) if value == f"Laser_{laser_tag}"] + ind = list(set(ind) & set(current_laser_tag_ind)) + input_voltages= self._extract_elements(self.LCM_voltage,ind) + laser_power_measured=self._extract_elements(self.LCM_LaserPowerMeasured,ind) + input_voltages_unique=list(set(input_voltages)) + for n in range(len(input_voltages_unique)): + current_voltage=input_voltages_unique[n] + laser_ind = [k for k in range(len(input_voltages)) if input_voltages[k] == current_voltage] + measured_power=self._extract_elements(laser_power_measured,laser_ind) + measured_power_mean=self._getmean(measured_power) + ItemsLaserPower.append([float(current_voltage), measured_power_mean]) + return ItemsLaserPower + + def _unique(self,input): + '''average the laser power with the same input voltage''' + if input==[]: + return [] + items=[] + input_array=np.array(input) + voltage_unique=list(set(input_array[:,0])) + for current_votage in voltage_unique: + laser_power=input_array[ np.logical_and(input_array[:,0]==current_votage,input_array[:,1]!='NA') ][:,1] + mean_laser_power=self._getmean(list(laser_power)) + items.append([float(current_votage), mean_laser_power]) + return items + def _extract_elements(self,my_list, indices): + extracted_elements = [my_list[index] for index in indices] + return extracted_elements + def _getmean(self,List): + if List==[]: + return 'NA' + Sum=0 + N=0 + for i in range(len(List)): + try: + Sum=Sum+float(List[i]) + N=N+1 + except Exception as e: + logging.error(str(e)) + + Sum=Sum/N + return Sum + def _Sleep(self,SleepTime): + time.sleep(SleepTime) + def _thread_complete(self): + self.SleepComplete=1 + def _thread_complete2(self): + self.SleepComplete2=1 + def _Open(self): + '''Open the laser only once''' + self.MainWindow._ConnectBonsai() + if self.MainWindow.InitializeBonsaiSuccessfully==0: + return + if self.Open.isChecked(): + self.SleepComplete2=0 + # change button color and disable the open button + self.Open.setEnabled(False) + self.Open.setStyleSheet("background-color : green;") + self._GetTrainingParameters(self.MainWindow) + self._GetLaserWaveForm() + self.worker2 = Worker(self._Sleep,float(self.LC_Duration_1)+1) + self.worker2.signals.finished.connect(self._thread_complete2) + self._InitiateATrial() + self.SleepStart=1 + while 1: + QApplication.processEvents() + if self.SleepStart==1: # only run once + self.SleepStart=0 + self.threadpool2.start(self.worker2) + if self.Open.isChecked()==False or self.SleepComplete2==1: + break + self.Open.setStyleSheet("background-color : none") + self.Open.setChecked(False) + self.Open.setEnabled(True) + else: + # change button color + self.Open.setStyleSheet("background-color : none") + self.Open.setChecked(False) + self.Open.setEnabled(True) + def _KeepOpen(self): + '''Keep the laser open''' + if self.KeepOpen.isChecked(): + # change button color + self.KeepOpen.setStyleSheet("background-color : green;") + self._GetTrainingParameters(self.MainWindow) + self.LC_RD_1=0 # set RM to zero + self._GetLaserWaveForm() + if self.Initialize1==0: + self.worker1 = Worker(self._Sleep,float(self.LC_Duration_1)) + self.worker1.signals.finished.connect(self._thread_complete) + self.Initialize1=1 + time.sleep(1) + while 1: + QApplication.processEvents() + if self.SleepComplete==1: + self.SleepComplete=0 + self._InitiateATrial() + self.threadpool1.start(self.worker1) + if self.KeepOpen.isChecked()==False: + break + self.KeepOpen.setStyleSheet("background-color : none") + self.KeepOpen.setChecked(False) + else: + # change button color + self.KeepOpen.setStyleSheet("background-color : none") + self.KeepOpen.setChecked(False) + +def initialize_dic(dic_name,key_list=[]): + '''initialize the parameters''' + if key_list==[]: + return dic_name + key = key_list[0] + key_list_new = key_list[1:] + if key not in dic_name: + dic_name[key]={} + initialize_dic(dic_name[key],key_list=key_list_new) + return dic_name + + +class MetadataDialog(QDialog): + '''For adding metadata to the session''' + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('MetaData.ui', self) + self.MainWindow = MainWindow + self._connectSignalsSlots() + self.meta_data = {} + self.meta_data['rig_metadata'] = {} + self.meta_data['session_metadata'] = {} + self.meta_data['rig_metadata_file'] = '' + self.GoCueDecibel.setText(str(self.MainWindow.Other_go_cue_decibel)) + self.LickSpoutDistance.setText(str(self.MainWindow.Other_lick_spout_distance)) + self._get_basics() + self._show_project_names() + + # create reference position boxes based on stage coordinate keys + positions = self.MainWindow._GetPositions() if self.MainWindow._GetPositions() is not None else {} + grid_layout = QGridLayout() + # add in reference area widget + grid_layout.addWidget(self.label_95, 0, 0) + grid_layout.addWidget(self.LickSpoutReferenceArea, 0, 1) + for i, axis in enumerate(positions.keys()): + label = QLabel(f'{axis.upper()} (um):') + setattr(self, f'LickSpoutReference{axis.upper()}', QLineEdit()) + grid_layout.addWidget(label, i+1, 0) + grid_layout.addWidget(getattr(self, f'LickSpoutReference{axis.upper()}'), i+1, 1) + # add in lick spout distance + grid_layout.addWidget(self.label_96, len(positions.keys())+1, 0) + grid_layout.addWidget(self.LickSpoutDistance, len(positions.keys())+1, 1) + self.groupBox.setLayout(grid_layout) + + def _connectSignalsSlots(self): + self.SelectRigMetadata.clicked.connect(lambda: self._SelectRigMetadata(rig_metadata_file=None)) + self.EphysProbes.currentIndexChanged.connect(self._show_angles) + self.StickMicroscopes.currentIndexChanged.connect(self._show_angles) + self.ArcAngle.textChanged.connect(self._save_configuration) + self.ModuleAngle.textChanged.connect(self._save_configuration) + self.ProbeTarget.textChanged.connect(self._save_configuration) + self.RotationAngle.textChanged.connect(self._save_configuration) + self.ManipulatorX.textChanged.connect(self._save_configuration) + self.ManipulatorY.textChanged.connect(self._save_configuration) + self.ManipulatorZ.textChanged.connect(self._save_configuration) + self.SaveMeta.clicked.connect(self._save_metadata) + self.LoadMeta.clicked.connect(self._load_metadata) + self.ClearMetadata.clicked.connect(self._clear_metadata) + self.Stick_ArcAngle.textChanged.connect(self._save_configuration) + self.Stick_ModuleAngle.textChanged.connect(self._save_configuration) + self.Stick_RotationAngle.textChanged.connect(self._save_configuration) + self.ProjectName.currentIndexChanged.connect(self._show_project_info) + self.GoCueDecibel.textChanged.connect(self._save_go_cue_decibel) + self.LickSpoutDistance.textChanged.connect(self._save_lick_spout_distance) + + def _set_reference(self, reference: dict): + ''' + set the reference + :param referencee: dictionary with keys that correspond to reference QLinEdits attributes + ''' + self.reference = reference + for axis, pos in reference.items(): + line_edit = getattr(self, f'LickSpoutReference{axis.upper()}') + line_edit.setText(str(pos)) + + def _show_project_info(self): + '''show the project information based on current project name''' + current_project_index = self.ProjectName.currentIndex() + self.current_project_name=self.ProjectName.currentText() + self.funding_institution=self.project_info['Funding Institution'][current_project_index] + self.grant_number=self.project_info['Grant Number'][current_project_index] + self.investigators=self.project_info['Investigators'][current_project_index] + self.fundee=self.project_info['Fundee'][current_project_index] + self.FundingSource.setText(str(self.funding_institution)) + self.Investigators.setText(str(self.investigators)) + self.GrantNumber.setText(str(self.grant_number)) + self.Fundee.setText(str(self.fundee)) + + def _save_lick_spout_distance(self): + '''save the lick spout distance''' + self.MainWindow.Other_lick_spout_distance=self.LickSpoutDistance.text() + + def _save_go_cue_decibel(self): + '''save the go cue decibel''' + self.MainWindow.Other_go_cue_decibel=self.GoCueDecibel.text() + + def _show_project_names(self): + '''show the project names from the project spreadsheet''' + # load the project spreadsheet + project_info_file = self.MainWindow.project_info_file + if not os.path.exists(project_info_file): + return + self.project_info = pd.read_excel(project_info_file) + project_names = self.project_info['Project Name'].tolist() + # show the project information + # adding project names to the project combobox + self._manage_signals(enable=False,keys=['ProjectName'],action=self._show_project_info) + self.ProjectName.addItems(project_names) + self._manage_signals(enable=True,keys=['ProjectName'],action=self._show_project_info) + self._show_project_info() + + def _get_basics(self): + '''get the basic information''' + self.probe_types = ['StickMicroscopes','EphysProbes'] + self.metadata_keys = ['microscopes','probes'] + self.widgets = [self.Microscopes,self.Probes] + + def _clear_metadata(self): + '''clear the metadata''' + self.meta_data = {} + self.meta_data['rig_metadata'] = {} + self.meta_data['session_metadata'] = {} + self.meta_data['rig_metadata_file'] = '' + self.ExperimentDescription.clear() + self._update_metadata() + + def _load_metadata(self): + '''load the metadata from a json file''' + metadata_dialog_file, _ = QFileDialog.getOpenFileName( + self, + "Select Metadata File", + self.MainWindow.metadata_dialog_folder, + "JSON Files (*.json)" + ) + if not metadata_dialog_file: + return + if os.path.exists(metadata_dialog_file): + with open(metadata_dialog_file, 'r') as file: + self.meta_data = json.load(file) + self.meta_data['metadata_dialog_file'] = metadata_dialog_file + self._update_metadata(dont_clear=True) + + def _update_metadata(self,update_rig_metadata=True,update_session_metadata=True,dont_clear=False): + '''update the metadata''' + if (update_rig_metadata) and ('rig_metadata_file' in self.meta_data): + if os.path.basename(self.meta_data['rig_metadata_file'])!=self.RigMetadataFile.text() and self.RigMetadataFile.text() != '': + if dont_clear==False: + # clear probe angles if the rig metadata file is changed + self.meta_data['session_metadata']['probes'] = {} + self.meta_data['session_metadata']['microscopes'] = {} + self.RigMetadataFile.setText(os.path.basename(self.meta_data['rig_metadata_file'])) + if update_session_metadata: + widget_dict = self._get_widgets() + self._set_widgets_value(widget_dict, self.meta_data['session_metadata']) + + self._show_ephys_probes() + self._show_stick_microscopes() + self._iterate_probes_microscopes() + + def _iterate_probes_microscopes(self): + '''iterate the probes and microscopes to save the probe information''' + keys = ['EphysProbes', 'StickMicroscopes'] + for key in keys: + current_combo = getattr(self, key) + current_index = current_combo.currentIndex() + for index in range(current_combo.count()): + current_combo.setCurrentIndex(index) + current_combo.setCurrentIndex(current_index) + + def _set_widgets_value(self, widget_dict, metadata): + '''set the widgets value''' + for key, value in widget_dict.items(): + if key in metadata: + if isinstance(value, QtWidgets.QLineEdit): + value.setText(metadata[key]) + elif isinstance(value, QtWidgets.QTextEdit): + value.setPlainText(metadata[key]) + elif isinstance(value, QtWidgets.QComboBox): + index = value.findText(metadata[key]) + if index != -1: + value.setCurrentIndex(index) + elif isinstance(value, QtWidgets.QComboBox): + value.setCurrentIndex(0) + elif isinstance(value, QtWidgets.QLineEdit): + value.setText('') + elif isinstance(value, QtWidgets.QTextEdit): + value.setPlainText('') + + def _clear_angles(self, keys): + '''Clear the angles and target area for the given widget + Parameters + ---------- + keys : List of str + The key to clear + + ''' + for key in keys: + getattr(self, key).setText('') + + def _save_metadata_dialog_parameters(self): + '''save the metadata dialog parameters''' + widget_dict = self._get_widgets() + self.meta_data=self.MainWindow._Concat(widget_dict, self.meta_data, 'session_metadata') + self.meta_data['rig_metadata_file'] = self.RigMetadataFile.text() + + def _save_metadata(self): + '''save the metadata collected from this dialogue to an independent json file''' + # save metadata parameters + self._save_metadata_dialog_parameters() + # Save self.meta_data to JSON + metadata_dialog_folder=self.MainWindow.metadata_dialog_folder + if not os.path.exists(metadata_dialog_folder): + os.makedirs(metadata_dialog_folder) + json_file=os.path.join(metadata_dialog_folder, self.MainWindow.current_box+'_'+datetime.now().strftime("%Y-%m-%d_%H-%M-%S")+ '_metadata_dialog.json') + + with open(json_file, 'w') as file: + json.dump(self.meta_data, file, indent=4) + + def _get_widgets(self): + '''get the widgets used for saving/loading metadata''' + exclude_widgets=self._get_children_keys(self.Probes) + exclude_widgets+=self._get_children_keys(self.Microscopes) + exclude_widgets+=['EphysProbes','RigMetadataFile','StickMicroscopes'] + widget_dict = {w.objectName(): w for w in self.findChildren( + (QtWidgets.QLineEdit, QtWidgets.QTextEdit, QtWidgets.QComboBox)) + if w.objectName() not in exclude_widgets} + return widget_dict + + def _save_configuration(self): + '''save the angles and target area of the selected probe type ('StickMicroscopes','EphysProbes')''' + + probe_types = self.probe_types + metadata_keys = self.metadata_keys + widgets = self.widgets + + for i in range(len(probe_types)): + probe_type=probe_types[i] + metadata_key=metadata_keys[i] + widget=widgets[i] + current_probe = getattr(self, probe_type).currentText() + self.meta_data['session_metadata'] = initialize_dic(self.meta_data['session_metadata'], key_list=[metadata_key, current_probe]) + keys = self._get_children_keys(widget) + for key in keys: + self.meta_data['session_metadata'][metadata_key][current_probe][key] = getattr(self, key).text() + + def _show_angles(self): + ''' + show the angles and target area of the selected probe type ('StickMicroscopes','EphysProbes') + ''' + + probe_types = self.probe_types + metadata_keys = self.metadata_keys + widgets = self.widgets + + for i in range(len(probe_types)): + probe_type = probe_types[i] + metadata_key = metadata_keys[i] + widget = widgets[i] + action=self._save_configuration + + self._manage_signals(enable=False, keys=self._get_children_keys(widget),action=action) + self._manage_signals(enable=False, keys=[probe_type], action=self._show_angles) + + current_probe = getattr(self, probe_type).currentText() + self.meta_data['session_metadata'] = initialize_dic(self.meta_data['session_metadata'], key_list=[metadata_key]) + if current_probe == '' or current_probe not in self.meta_data['session_metadata'][metadata_key]: + self._clear_angles(self._get_children_keys(widget)) + self._manage_signals(enable=True, keys=[probe_type], action=self._show_angles) + self._manage_signals(enable=True, keys=self._get_children_keys(widget), action=action) + continue + + self.meta_data['session_metadata'] = initialize_dic(self.meta_data['session_metadata'], key_list=[metadata_key, current_probe]) + keys = self._get_children_keys(widget) + for key in keys: + self.meta_data['session_metadata'][metadata_key][current_probe].setdefault(key, '') + getattr(self, key).setText(self.meta_data['session_metadata'][metadata_key][current_probe][key]) + + self._manage_signals(enable=True, keys=self._get_children_keys(widget), action=action) + self._manage_signals(enable=True, keys=[probe_type], action=self._show_angles) + + + def _get_children_keys(self,parent_widget = None): + '''get the children QLineEidt objectName''' + if parent_widget is None: + parent_widget = self.Probes + probe_keys = [] + for child_widget in parent_widget.children(): + if isinstance(child_widget, QtWidgets.QLineEdit): + probe_keys.append(child_widget.objectName()) + if isinstance(child_widget, QtWidgets.QGroupBox): + for child_widget2 in child_widget.children(): + if isinstance(child_widget2, QtWidgets.QLineEdit): + probe_keys.append(child_widget2.objectName()) + return probe_keys + + def _show_stick_microscopes(self): + '''setting the stick microscopes from the rig metadata''' + if self.meta_data['rig_metadata'] == {}: + self.StickMicroscopes.clear() + self._show_angles() + self.meta_data['session_metadata']['microscopes'] = {} + return + items=[] + if 'stick_microscopes' in self.meta_data['rig_metadata']: + for i in range(len(self.meta_data['rig_metadata']['stick_microscopes'])): + items.append(self.meta_data['rig_metadata']['stick_microscopes'][i]['name']) + if items==[]: + self.StickMicroscopes.clear() + self._show_angles() + return + + self._manage_signals(enable=False,keys=['StickMicroscopes'],action=self._show_angles) + self._manage_signals(enable=False,keys=self._get_children_keys(self.Microscopes),action=self._save_configuration) + self.StickMicroscopes.clear() + self.StickMicroscopes.addItems(items) + self._manage_signals(enable=True,keys=['StickMicroscopes'],action=self._show_angles) + self._manage_signals(enable=True,keys=self._get_children_keys(self.Microscopes),action=self._save_configuration) + self._show_angles() + + def _show_ephys_probes(self): + '''setting the ephys probes from the rig metadata''' + if self.meta_data['rig_metadata'] == {}: + self.EphysProbes.clear() + self._show_angles() + return + items=[] + if 'ephys_assemblies' in self.meta_data['rig_metadata']: + for assembly in self.meta_data['rig_metadata']['ephys_assemblies']: + for probe in assembly['probes']: + items.append(probe['name']) + if items==[]: + self.EphysProbes.clear() + self._show_angles() + return + + self._manage_signals(enable=False,keys=['EphysProbes'],action=self._show_angles) + self._manage_signals(enable=False,keys=self._get_children_keys(self.Probes),action=self._save_configuration) + self.EphysProbes.clear() + self.EphysProbes.addItems(items) + self._manage_signals(enable=True,keys=['EphysProbes'],action=self._show_angles) + self._manage_signals(enable=True,keys=self._get_children_keys(self.Probes),action=self._save_configuration) + self._show_angles() + + def _manage_signals(self, enable=True,keys='',signals='',action=''): + '''manage signals + Parameters + ---------- + enable : bool + enable (connect) or disable (disconnect) the signals + action : function + the function to be connected or disconnected + keys : list + the keys of the widgets to be connected or disconnected + ''' + if keys == '': + keys=self._get_children_keys(self.Probes) + if signals == '': + signals = [] + for attr in keys: + if isinstance(getattr(self, attr),QtWidgets.QLineEdit): + signals.append(getattr(self, attr).textChanged) + elif isinstance(getattr(self, attr),QtWidgets.QComboBox): + signals.append(getattr(self, attr).currentIndexChanged) + if action == '': + action = self._save_configuration + + for signal in signals: + if enable: + signal.connect(action) + else: + signal.disconnect(action) + + def _SelectRigMetadata(self,rig_metadata_file=None): + '''Select the rig metadata file and load it + Parameters + ---------- + rig_metadata_file : str + The rig metadata file path + + Returns + ------- + None + ''' + if rig_metadata_file is None: + rig_metadata_file, _ = QFileDialog.getOpenFileName( + self, + "Select Rig Metadata File", + self.MainWindow.rig_metadata_folder, + "JSON Files (*.json)" + ) + if not rig_metadata_file: + return + self.meta_data['rig_metadata_file'] = rig_metadata_file + self.meta_data['session_metadata']['RigMetadataFile'] = rig_metadata_file + if os.path.exists(rig_metadata_file): + with open(rig_metadata_file, 'r') as file: + self.meta_data['rig_metadata'] = json.load(file) + + # Update the text box + self._update_metadata(update_session_metadata=False) + +class AutoTrainDialog(QDialog): + '''For automatic training''' + + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('AutoTrain.ui', self) + self.MainWindow = MainWindow + + # Initializations + self.auto_train_engaged = False + self.widgets_locked_by_auto_train = [] + self.stage_in_use = None + self.curriculum_in_use = None + + # Connect to Auto Training Manager and Curriculum Manager + self.aws_connected = self._connect_auto_training_manager() + + # Disable Auto Train button if not connected to AWS + if not self.aws_connected: + self.MainWindow.AutoTrain.setEnabled(False) + return + + self._connect_curriculum_manager() + + # Signals slots + self._setup_allbacks() + + # Sync selected subject_id + self.update_auto_train_fields(subject_id=self.MainWindow.behavior_session_model.subject) + + def _setup_allbacks(self): + self.checkBox_show_this_mouse_only.stateChanged.connect( + self._show_auto_training_manager + ) + self.checkBox_override_stage.stateChanged.connect( + self._override_stage_clicked + ) + self.comboBox_override_stage.currentIndexChanged.connect( + self._update_stage_to_apply + ) + self.pushButton_apply_auto_train_paras.clicked.connect( + self.update_auto_train_lock + ) + self.checkBox_override_curriculum.stateChanged.connect( + self._override_curriculum_clicked + ) + self.pushButton_apply_curriculum.clicked.connect( + self._apply_curriculum + ) + self.pushButton_show_curriculum_in_streamlit.clicked.connect( + self._show_curriculum_in_streamlit + ) + self.pushButton_show_auto_training_history_in_streamlit.clicked.connect( + self._show_auto_training_history_in_streamlit + ) + self.pushButton_preview_auto_train_paras.clicked.connect( + self._preview_auto_train_paras + ) + + def update_auto_train_fields(self, + subject_id: str, + curriculum_just_overridden: bool = False, + auto_engage: bool = False): + # Do nothing if not connected to AWS + if not self.aws_connected: + return + + self.selected_subject_id = subject_id + self.label_subject_id.setText(self.selected_subject_id) + + # Get the latest entry from auto_train_manager + self.df_this_mouse = self.df_training_manager.query( + f"subject_id == '{self.selected_subject_id}'" + ) + + if self.df_this_mouse.empty: + logger.info(f"No entry found in df_training_manager for subject_id: {self.selected_subject_id}") + self.last_session = None + self.curriculum_in_use = None + self.label_session.setText('subject not found') + self.label_curriculum_name.setText('subject not found') + self.label_last_actual_stage.setText('subject not found') + self.label_next_stage_suggested.setText('subject not found') + self.label_subject_id.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + # disable some stuff + self.checkBox_override_stage.setChecked(False) + self.checkBox_override_stage.setEnabled(False) + self.pushButton_apply_auto_train_paras.setEnabled(False) + self.pushButton_preview_auto_train_paras.setEnabled(False) + + # override curriculum is checked by default and disabled + self.checkBox_override_curriculum.setChecked(True) + self.checkBox_override_curriculum.setEnabled(False) + + # prompt user to create a new mouse + if self.isVisible(): + QMessageBox.information( + self, + "Info", + f"Mouse {self.selected_subject_id} does not exist in the auto training manager!\n" + f"If it is a new mouse (not your typo), please select a curriculum to add it." + ) + + self.tableView_df_curriculum.clearSelection() # Unselect any curriculum + self.selected_curriculum = None + self.pushButton_apply_curriculum.setEnabled(True) + self._add_border_curriculum_selection() + + else: # If the mouse exists in the auto_train_manager + # get curriculum in use from the last session, unless we just overrode it + if not curriculum_just_overridden: + # fetch last session + self.last_session = self.df_this_mouse.iloc[0] # The first row is the latest session + + last_curriculum_schema_version = self.last_session['curriculum_schema_version'] + if codebase_curriculum_schema_version != last_curriculum_schema_version: + # schema version don't match. prompt user to choose another curriculum + if self.isVisible(): + QMessageBox.information( + self, + "Info", + f"The curriculum_schema_version of the last session ({last_curriculum_schema_version}) does not match " + f"that of the current code base ({codebase_curriculum_schema_version})!\n" + f"This is likely because the AutoTrain system has been updated since the last session.\n\n" + f"Please choose another valid curriculum and a training stage for this mouse." + ) + + # Clear curriculum in use + self.curriculum_in_use = None + self.pushButton_preview_auto_train_paras.setEnabled(False) + + # Turn on override curriculum + self.checkBox_override_curriculum.setChecked(True) + self.checkBox_override_curriculum.setEnabled(False) + self.tableView_df_curriculum.clearSelection() # Unselect any curriculum + self.selected_curriculum = None + self.pushButton_apply_curriculum.setEnabled(True) + self._add_border_curriculum_selection() + + # Update UI + self._update_available_training_stages() + self._update_stage_to_apply() + + # Update df_auto_train_manager and df_curriculum_manager + self._show_auto_training_manager() + self._show_available_curriculums() + + # Eearly return + return + else: + self.curriculum_in_use = self.curriculum_manager.get_curriculum( + curriculum_name=self.last_session['curriculum_name'], + curriculum_schema_version=last_curriculum_schema_version, + curriculum_version=self.last_session['curriculum_version'], + )['curriculum'] + + self.pushButton_preview_auto_train_paras.setEnabled(True) + + # update stage info + self.label_last_actual_stage.setText(str(self.last_session['current_stage_actual'])) + self.label_next_stage_suggested.setText(str(self.last_session['next_stage_suggested'])) + self.label_next_stage_suggested.setStyleSheet("color: black;") + + else: + self.label_last_actual_stage.setText('irrelevant (curriculum overridden)') + self.label_next_stage_suggested.setText('irrelevant') + self.label_next_stage_suggested.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + # Set override stage automatically + self.checkBox_override_stage.setChecked(True) + self.checkBox_override_stage.setEnabled(True) + + # update more info + self.label_curriculum_name.setText( + get_curriculum_string(self.curriculum_in_use) + ) + self.label_session.setText(str(self.last_session['session'])) + self.label_subject_id.setStyleSheet("color: black;") + + # enable apply training stage + self.pushButton_apply_auto_train_paras.setEnabled(True) + self.pushButton_preview_auto_train_paras.setEnabled(True) + + # disable apply curriculum + self.pushButton_apply_curriculum.setEnabled(False) + self._remove_border_curriculum_selection() + + # Reset override curriculum + self.checkBox_override_curriculum.setChecked(False) + if not self.auto_train_engaged: + self.checkBox_override_curriculum.setEnabled(True) + + # Update UI + self._update_available_training_stages() + self._update_stage_to_apply() + + # Update df_auto_train_manager and df_curriculum_manager + self._show_auto_training_manager() + self._show_available_curriculums() + + # auto engage + if auto_engage: + try: + self.pushButton_apply_auto_train_paras.click() + logger.info(f"Auto engage successful for mouse {self.selected_subject_id}") + except Exception as e: + logger.warning(f"Auto engage failed: {str(e)}") + + + def _add_border_curriculum_selection(self): + self.tableView_df_curriculum.setStyleSheet( + ''' + QTableView::item:selected { + background-color: lightblue; + color: black; + } + + QTableView { + border:7px solid rgb(255, 170, 255); + } + ''' + ) + + def _remove_border_curriculum_selection(self): + self.tableView_df_curriculum.setStyleSheet( + ''' + QTableView::item:selected { + background-color: lightblue; + color: black; + } + ''' + ) + + def _connect_auto_training_manager(self): + try: + self.auto_train_manager = DynamicForagingAutoTrainManager( + manager_name='447_demo', + df_behavior_on_s3=dict(bucket='aind-behavior-data', + root='foraging_nwb_bonsai_processed/', + file_name='df_sessions.pkl'), + df_manager_root_on_s3=dict(bucket='aind-behavior-data', + root='foraging_auto_training/') + ) + except: + logger.error("AWS connection failed!") + QMessageBox.critical(self.MainWindow, + 'Box {}, Error'.format(self.MainWindow.box_letter), + f'AWS connection failed!\n' + f'Please check your AWS credentials at ~\.aws\credentials and restart the GUI!\n\n' + f'The AutoTrain will be disabled until the connection is restored.') + return False + df_training_manager = self.auto_train_manager.df_manager + + # Format dataframe + df_training_manager['session'] = df_training_manager['session'].astype(int) + df_training_manager['foraging_efficiency'] = \ + df_training_manager['foraging_efficiency'].round(3) + + # Sort by subject_id and session + df_training_manager.sort_values( + by=['subject_id', 'session'], + ascending=[False, False], # Newest sessions on the top, + inplace=True + ) + self.df_training_manager = df_training_manager + return True + + def _show_auto_training_manager(self): + if_this_mouse_only = self.checkBox_show_this_mouse_only.isChecked() + + if if_this_mouse_only: + df_training_manager_to_show = self.df_this_mouse + else: + df_training_manager_to_show = self.df_training_manager + + df_training_manager_to_show = df_training_manager_to_show[ + ['subject_id', + 'session', + 'session_date', + 'curriculum_name', + 'curriculum_version', + 'curriculum_schema_version', + 'task', + 'current_stage_suggested', + 'current_stage_actual', + 'decision', + 'next_stage_suggested', + 'if_closed_loop', + 'if_overriden_by_trainer', + 'finished_trials', + 'foraging_efficiency', + ] + ] + + # Show dataframe in QTableView + model = PandasModel(df_training_manager_to_show) + self.tableView_df_training_manager.setModel(model) + + # Format table + self.tableView_df_training_manager.resizeColumnsToContents() + self.tableView_df_training_manager.setSortingEnabled(False) + + # Highlight the latest session + if self.last_session is not None: + session_index = df_training_manager_to_show.reset_index().index[ + (df_training_manager_to_show['subject_id'] == self.last_session['subject_id']) & + (df_training_manager_to_show['session'] == self.last_session['session']) + ][0] + _index = self.tableView_df_training_manager.model().index(session_index, 0) + self.tableView_df_training_manager.clearSelection() + self.tableView_df_training_manager.selectionModel().select( + _index, + QItemSelectionModel.Select | QItemSelectionModel.Rows + ) + self.tableView_df_training_manager.scrollTo(_index) + + def _connect_curriculum_manager(self): + self.curriculum_manager = CurriculumManager( + saved_curriculums_on_s3=dict( + bucket='aind-behavior-data', + root='foraging_auto_training/saved_curriculums/' + ), + # saved to tmp folder under user's home directory + saved_curriculums_local=os.path.expanduser('~/.aind_auto_train/curriculum_manager/') + ) + + def _show_available_curriculums(self): + self.df_curriculums = self.curriculum_manager.df_curriculums() + + # Show dataframe in QTableView + model = PandasModel(self.df_curriculums) + self.tableView_df_curriculum.setModel(model) + + # Format table + self.tableView_df_curriculum.resizeColumnsToContents() + self.tableView_df_curriculum.setSortingEnabled(True) + + # Add callback + self.tableView_df_curriculum.clicked.connect(self._update_curriculum_diagrams) + + self._sync_curriculum_in_use_to_table() + + def _sync_curriculum_in_use_to_table(self): + # Auto select the curriculum_in_use, if any + if self.curriculum_in_use is None: + return + + self.tableView_df_curriculum.clearSelection() # Unselect any curriculum + + curriculum_index = self.df_curriculums.reset_index().index[ + (self.df_curriculums['curriculum_name'] == self.curriculum_in_use.curriculum_name) & + (self.df_curriculums['curriculum_version'] == self.curriculum_in_use.curriculum_version) & + (self.df_curriculums['curriculum_schema_version'] == self.curriculum_in_use.curriculum_schema_version) + ][0] + + # Auto click the curriculum of the latest session + _index = self.tableView_df_curriculum.model().index(curriculum_index, 0) + self.tableView_df_curriculum.selectionModel().select( + _index, + QItemSelectionModel.Select | QItemSelectionModel.Rows + ) + self.tableView_df_curriculum.scrollTo(_index) + + self._update_curriculum_diagrams(_index) # Update diagrams + + def _update_curriculum_diagrams(self, index): + # Retrieve selected curriculum + row = index.row() + selected_row = self.df_curriculums.iloc[row] + logger.info(f"Selected curriculum: {selected_row.to_dict()}") + self.selected_curriculum = self.curriculum_manager.get_curriculum( + curriculum_name=selected_row['curriculum_name'], + curriculum_schema_version=selected_row['curriculum_schema_version'], + curriculum_version=selected_row['curriculum_version'], + ) + + def _show_curriculum_in_streamlit(self): + if self.selected_curriculum is not None: + webbrowser.open( + 'https://foraging-behavior-browser.allenneuraldynamics-test.org/' + '?tab_id=tab_auto_train_curriculum' + f'&auto_training_curriculum_name={self.selected_curriculum["curriculum"].curriculum_name}' + f'&auto_training_curriculum_version={self.selected_curriculum["curriculum"].curriculum_version}' + f'&auto_training_curriculum_schema_version={self.selected_curriculum["curriculum"].curriculum_schema_version}' + ) + + def _show_auto_training_history_in_streamlit(self): + webbrowser.open( + 'https://foraging-behavior-browser.allenneuraldynamics-test.org/?' + f'&filter_subject_id={self.selected_subject_id}' + f'&tab_id=tab_auto_train_history' + f'&auto_training_history_x_axis=session' + f'&auto_training_history_sort_by=subject_id' + f'&auto_training_history_sort_order=descending' + ) + + + def _update_available_training_stages(self): + # If AutoTrain is engaged, and override stage is checked + if self.auto_train_engaged and self.checkBox_override_stage.isChecked(): + # Restore stage_in_use. No need to reload available stages + self.comboBox_override_stage.setCurrentText(self.stage_in_use) + else: + # Reload available stages + if self.curriculum_in_use is not None: + available_training_stages = [v.name for v in + self.curriculum_in_use.parameters.keys()] + else: + available_training_stages = [] + + self.comboBox_override_stage.clear() + self.comboBox_override_stage.addItems(available_training_stages) + + + def _override_stage_clicked(self, state): + logger.info(f"Override stage clicked: state={state}") + if state: + self.comboBox_override_stage.setEnabled(True) + else: + self.comboBox_override_stage.setEnabled(False) + self._update_stage_to_apply() + + def _override_curriculum_clicked(self, state): + logger.info(f"Override stage clicked: state={state}") + if state: + self.pushButton_apply_curriculum.setEnabled(True) + self._add_border_curriculum_selection() + else: + self.pushButton_apply_curriculum.setEnabled(False) + self._remove_border_curriculum_selection() + + # Always sync + self._sync_curriculum_in_use_to_table() + + def _update_stage_to_apply(self): + if self.checkBox_override_stage.isChecked(): + self.stage_in_use = self.comboBox_override_stage.currentText() + logger.info(f"Stage overridden to: {self.stage_in_use}") + elif self.last_session is not None: + self.stage_in_use = self.last_session['next_stage_suggested'] + else: + self.stage_in_use = 'unknown training stage' + + self.pushButton_apply_auto_train_paras.setText( + f"Apply and lock\n" + + '\n'.join(get_curriculum_string(self.curriculum_in_use).split('(')).strip(')') + + f"\n{self.stage_in_use}" + ) + + logger.info(f"Current stage to apply: {self.stage_in_use} @" + f"{get_curriculum_string(self.curriculum_in_use)}") + + def _apply_curriculum(self): + # Check if a curriculum is selected + if not hasattr(self, 'selected_curriculum') or self.selected_curriculum is None: + QMessageBox.critical(self, "Box {}, Error".format(self.MainWindow.box_letter), "Please select a curriculum!") + return + + # Always enable override stage + self.checkBox_override_stage.setEnabled(True) + + if self.df_this_mouse.empty: + # -- This is a new mouse, we add the first dummy session -- + # Update global curriculum_in_use + self.curriculum_in_use = self.selected_curriculum['curriculum'] + + # Add a dummy entry to df_training_manager + self.df_training_manager = pd.concat( + [self.df_training_manager, + pd.DataFrame.from_records([ + dict(subject_id=self.selected_subject_id, + session=0, + session_date='unknown', + curriculum_name=self.curriculum_in_use.curriculum_name, + curriculum_version=self.curriculum_in_use.curriculum_version, + curriculum_schema_version=self.curriculum_in_use.curriculum_schema_version, + task=None, + current_stage_suggested=None, + current_stage_actual=None, + decision=None, + next_stage_suggested='STAGE_1_WARMUP' + if 'STAGE_1_WARMUP' in [k.name for k in self.curriculum_in_use.parameters.keys()] + else 'STAGE_1', + if_closed_loop=None, + if_overriden_by_trainer=None, + finished_trials=None, + foraging_efficiency=None, + ) + ])] + ) + logger.info(f"Added a dummy session 0 for mouse {self.selected_subject_id} ") + + self.checkBox_override_curriculum.setChecked(False) + self.checkBox_override_curriculum.setEnabled(True) + + # Refresh the GUI + self.update_auto_train_fields(subject_id=self.selected_subject_id) + else: + # -- This is an existing mouse, we are changing the curriculum -- + # Not sure whether we should leave this option open. But for now, I allow this freedom. + if self.selected_curriculum['curriculum'] == self.curriculum_in_use: + # The selected curriculum is the same as the one in use + logger.info(f"Selected curriculum is the same as the one in use. No change is made.") + QMessageBox.information(self, "Box {}, Info".format(self.MainWindow.box_letter), "Selected curriculum is the same as the one in use. No change is made.") + return + else: + # Confirm with the user about overriding the curriculum + reply = QMessageBox.question(self, "Box {}, Confirm".format(self.MainWindow.box_letter), + f"Are you sure you want to override the curriculum?\n" + f"If yes, please also manually select a training stage.", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if reply == QMessageBox.Yes: + # Update curriculum in use + logger.info(f"Change curriculum from " + f"{get_curriculum_string(self.curriculum_in_use)} to " + f"{get_curriculum_string(self.selected_curriculum['curriculum'])}") + self.curriculum_in_use = self.selected_curriculum['curriculum'] + + self.checkBox_override_curriculum.setChecked(False) + self.pushButton_apply_curriculum.setEnabled(False) + self._remove_border_curriculum_selection() # Remove the highlight of curriculum table view + + # Refresh the GUI + self.update_auto_train_fields(subject_id=self.selected_subject_id, + curriculum_just_overridden=reply == QMessageBox.Yes) + + def _preview_auto_train_paras(self, preview_checked): + """Apply parameters to the GUI without applying and locking the widgets. + """ + + if preview_checked: + if self.curriculum_in_use is None: + return + + # Get parameter settings + paras = self.curriculum_in_use.parameters[ + TrainingStage[self.stage_in_use] + ] + + # Convert to GUI format and set the parameters + paras_dict = paras.to_GUI_format() + widgets_set, self.widgets_changed = self._set_training_parameters( + paras_dict=paras_dict, + if_apply_and_lock=False + ) + + # Clear the style of all widgets + for widget in widgets_set: + widget.setStyleSheet("font-weight: normal") + + # Highlight the changed widgets + for widget in self.widgets_changed.keys(): + widget.setStyleSheet( + ''' + background-color: rgb(225, 225, 0); + font-weight: bold + ''' + ) + elif hasattr(self, 'widgets_changed'): # Revert to previous values + paras_to_revert = {widget.objectName():value + for widget, value in self.widgets_changed.items()} + + _, widgets_changed = self._set_training_parameters( + paras_dict=paras_to_revert, + if_apply_and_lock=False + ) + + # Clear the style of all widgets + for widget in widgets_changed: + widget.setStyleSheet("font-weight: normal") + + self.widgets_changed = {} + + def update_auto_train_lock(self, engaged): + if engaged: + logger.info(f"AutoTrain engaged! {self.stage_in_use} @ {get_curriculum_string(self.curriculum_in_use)}") + + # Update the flag + self.auto_train_engaged = True + + # Get parameter settings + paras = self.curriculum_in_use.parameters[ + TrainingStage[self.stage_in_use] + ] + + # Convert to GUI format and set the parameters + paras_dict = paras.to_GUI_format() + self.widgets_locked_by_auto_train, _ = self._set_training_parameters( + paras_dict=paras_dict, + if_apply_and_lock=True + ) + + if self.widgets_locked_by_auto_train == []: # Error in setting parameters + self.update_auto_train_lock(engaged=False) # Uncheck the "apply" button + return + + # lock the widgets that have been set by auto training + for widget in self.widgets_locked_by_auto_train: + # Exclude some fields so that RAs can change them without going off-curriculum + # See https://github.com/AllenNeuralDynamics/aind-behavior-blog/issues/620 + if widget.objectName() in ["auto_stop_ignore_win", "MaxTrial", "MaxTime"]: + continue + widget.setEnabled(False) + # set the border color to green + widget.setStyleSheet("border: 2px solid rgb(0, 214, 103);") + + self.MainWindow.TrainingParameters.setStyleSheet( + '''QGroupBox { + border: 5px solid rgb(0, 214, 103) + } + ''' + ) + self.MainWindow.label_auto_train_stage.setText( + '\n'.join(get_curriculum_string(self.curriculum_in_use).split('(')).strip(')') + + f", {self.stage_in_use}" + ) + self.MainWindow.label_auto_train_stage.setStyleSheet("color: rgb(0, 214, 103);") + + # disable override + self.checkBox_override_stage.setEnabled(False) + self.comboBox_override_stage.setEnabled(False) + + # disable preview + self.pushButton_preview_auto_train_paras.setEnabled(False) + + else: + logger.info("AutoTrain disengaged!") + + # Update the flag + self.auto_train_engaged = False + + # Uncheck the button (when this is called from the MainWindow, not from actual button click) + self.pushButton_apply_auto_train_paras.setChecked(False) + self.pushButton_preview_auto_train_paras.setEnabled(True) + + # Unlock the previously engaged widgets + for widget in self.widgets_locked_by_auto_train: + widget.setEnabled(True) + # clear style + widget.setStyleSheet("") + self.MainWindow.TrainingParameters.setStyleSheet("") + self.MainWindow.label_auto_train_stage.setText("off curriculum") + self.MainWindow.label_auto_train_stage.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + + + # enable override + self.checkBox_override_stage.setEnabled(True) + self.comboBox_override_stage.setEnabled(self.checkBox_override_stage.isChecked()) + + + def _set_training_parameters(self, paras_dict, if_apply_and_lock=False): + """Accepts a dictionary of parameters and set the GUI accordingly + Trying to refactor Foraging.py's _TrainingStage() here. + + paras_dict: a dictionary of parameters following Xinxin's convention + if_apply_and_lock: if True, press enter after setting the parameters + """ + # Track widgets that have been set by auto training + widgets_set = [] + widgets_changed = {} # Dict of {changed_key: previous_value} + + # If warmup exists, always turn it off first, set other parameters, + # and then turn it to the desired state + keys = list(paras_dict.keys()) + if 'warmup' in keys: + keys.remove('warmup') + keys += ['warmup'] + + # Set warmup to off first so that all AutoTrain parameters + # can be correctly registered in WarmupBackup if warmup is turned on later + if paras_dict and paras_dict['warmup'] != self.MainWindow.warmup.currentText(): + widgets_changed.update( + {self.MainWindow.warmup: + self.MainWindow.warmup.currentText() + } + ) # Track the changes + + index=self.MainWindow.warmup.findText('off') + self.MainWindow.warmup.setCurrentIndex(index) + + # Loop over para_dict and try to set the values + for key in keys: + value = paras_dict[key] + + if key == 'task': + widget_task = self.MainWindow.Task + task_ind = widget_task.findText(paras_dict['task']) + if task_ind < 0: + logger.error(f"Task {paras_dict['task']} not found!") + QMessageBox.critical(self, "Box {}, Error".format(self.MainWindow.box_letter), + f'''Task "{paras_dict['task']}" not found. Check the curriculum!''') + return [] # Return an empty list without setting anything + else: + if task_ind != widget_task.currentIndex(): + widgets_changed.update( + {widget_task: widget_task.currentIndex()} + ) # Track the changes + widget_task.setCurrentIndex(task_ind) + logger.info(f"Task is set to {paras_dict['task']}") + widgets_set.append(widget_task) + + continue # Continue to the next parameter + + # For other parameters, try to find the widget and set the value + widget = self.MainWindow.findChild(QObject, key) + if widget is None: + logger.info(f''' Widget "{key}" not found. skipped...''') + continue + + # Enable warmup-related widgets if warmup will be turned on later. + # Otherwise, they are now disabled (see 30 lines above) + # and thus cannot be set by AutoTrain (see above line) + if 'warm' in key and paras_dict['warmup'] == 'on': + widget.setEnabled(True) + + # If the parameter is disabled by the GUI in the first place, skip it + # For example, the field "uncoupled reward" in a coupled task. + if not widget.isEnabled(): + logger.info(f''' Widget "{key}" has been disabled by the GUI. skipped...''') + continue + + # Set the value according to the widget type + if isinstance(widget, (QtWidgets.QLineEdit, + QtWidgets.QTextEdit)): + if value != widget.text(): + widgets_changed.update({widget: widget.text()}) # Track the changes + widget.setText(value) + elif isinstance(widget, QtWidgets.QComboBox): + ind = widget.findText(value) + if ind < 0: + logger.error(f"Parameter choice {key}={value} not found!") + continue # Still allow other parameters to be set + else: + if ind != widget.currentIndex(): + widgets_changed.update({widget: widget.currentText()}) # Track the changes + widget.setCurrentIndex(ind) + elif isinstance(widget, (QtWidgets.QDoubleSpinBox)): + if float(value) != widget.value(): + widgets_changed.update({widget: widget.value()}) # Track the changes + widget.setValue(float(value)) + elif isinstance(widget, QtWidgets.QSpinBox): + if float(value) != widget.value(): + widgets_changed.update({widget: widget.value()}) # Track the changes + widget.setValue(int(value)) + elif isinstance(widget, QtWidgets.QPushButton): + if key=='AutoReward': + if bool(value) != widget.isChecked(): + widgets_changed.update({widget: widget.isChecked()}) # Track the changes + widget.setChecked(bool(value)) + self.MainWindow._AutoReward() + + # Append the widgets that have been set + widgets_set.append(widget) + logger.info(f"{key} is set to {value}") + + # Lock all water reward-related widgets if one exists + if 'LeftValue' in key: + widgets_set.extend( + [self.MainWindow.findChild(QObject, 'LeftValue'), + self.MainWindow.findChild(QObject, 'LeftValue_volume') + ] + ) + if 'RightValue' in key: + widgets_set.extend( + [self.MainWindow.findChild(QObject, 'RightValue'), + self.MainWindow.findChild(QObject, 'RightValue_volume') + ] + ) + + # Mimic an "ENTER" press event to update the parameters + if if_apply_and_lock: + self.MainWindow._keyPressEvent() + + return widgets_set, widgets_changed + + def _clear_layout(self, layout): + # Remove all existing widgets from the layout + for i in reversed(range(layout.count())): + layout.itemAt(i).widget().setParent(None) + +def get_curriculum_string(curriculum): + if curriculum is None: + return "unknown curriculum" + else: + return (f"{curriculum.curriculum_name} " + f"(v{curriculum.curriculum_version}" + f"@{curriculum.curriculum_schema_version})") + + +# --- Helpers --- +class PandasModel(QAbstractTableModel): + ''' A helper class to display pandas dataframe in QTableView + https://learndataanalysis.org/display-pandas-dataframe-with-pyqt5-qtableview-widget/ + ''' + def __init__(self, data): + QAbstractTableModel.__init__(self) + self._data = data.copy() + + def rowCount(self, parent=None): + return self._data.shape[0] + + def columnCount(self, parent=None): + return self._data.shape[1] + + def data(self, index, role=Qt.DisplayRole): + if index.isValid(): + if role == Qt.DisplayRole: + return str(self._data.iloc[index.row(), index.column()]) + return None + + def headerData(self, col, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self._data.columns[col] + return None + +class OpticalTaggingDialog(QDialog): + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('OpticalTagging.ui', self) + self._connectSignalsSlots() + + def _connectSignalsSlots(self): + self.Start.clicked.connect(self._Start) + self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) + + def _Start(self): + '''Start the optical tagging''' + # toggle the button color + if self.Start.isChecked(): + self.Start.setStyleSheet("background-color : green;") + else: + self.Start.setStyleSheet("background-color : none") + + def _WhichLaser(self): + '''Select the laser to use and disable non-relevant widgets''' + laser_name = self.WhichLaser.currentText() + if laser_name=='Laser_1': + self.Laser_2_power.setEnabled(False) + self.label1_16.setEnabled(False) + self.label1_3.setEnabled(True) + self.Laser_1_power.setEnabled(True) + elif laser_name=='Laser_2': + self.Laser_1_power.setEnabled(False) + self.label1_3.setEnabled(False) + self.label1_16.setEnabled(True) + self.Laser_2_power.setEnabled(True) + else: + self.Laser_1_power.setEnabled(True) + self.Laser_2_power.setEnabled(True) + self.label1_3.setEnabled(True) + self.label1_16.setEnabled(True) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 0f04ac3d9..0669b0697 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3107,7 +3107,7 @@ def _Start(self): # initiate the laser # need to change the bonsai code to initiate the laser self._initiate_laser() - # receiving the timestamps of laser start and saving them + # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. Rec=self.MainWindow.Channel1.receive() if Rec[0].address=='/ITIStartTimeHarp': self.optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] @@ -3118,7 +3118,7 @@ def _Start(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - + self.label_show_current.setText(f'Cycle {i+1}/{len(self.optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') def _initiate_laser(self): '''Initiate laser in bonsai''' # start generating waveform in bonsai From d92e7f0722ac6a57e3f8ced672c7d3a4607132a6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:33:47 -0800 Subject: [PATCH 037/184] adding emergency stop --- src/foraging_gui/OpticalTagging.ui | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index d65430a76..264436962 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -679,7 +679,7 @@@@ -700,6 +700,19 @@ 30 -500 +520 191 20 Qt::AlignCenter + + ++ +109 +460 +101 +31 ++ +Emergency Stop +From fb98c6dc2d28a0edea1e2a67c3ceeb32d6625ecb Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:57:10 -0800 Subject: [PATCH 038/184] finish start --- src/foraging_gui/Dialogs.py | 151 +++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 0669b0697..7e98bee0f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3043,10 +3043,12 @@ def __init__(self, MainWindow, parent=None): self._connectSignalsSlots() self.MainWindow = MainWindow self.optical_tagging_par={} - + self.finish_tag = 1 + self.threadpool = QThreadPool() def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) + self.EmergencyStop.clicked.connect(self._emegency_stop) def _Start(self): '''Start the optical tagging''' @@ -3055,70 +3057,97 @@ def _Start(self): self.Start.setStyleSheet("background-color : green;") else: self.Start.setStyleSheet("background-color : none") + return # generate random conditions including lasers, laser power, laser color, and protocol - self._generate_random_conditions() - - self.optical_tagging_par['success_tag'] = np.zeros(len(self.optical_tagging_par['protocol_sampled_all'])) + if self.finish_tag==1: + # generate new random conditions + self._generate_random_conditions() + self.optical_tagging_par['success_tag'] = np.zeros(len(self.optical_tagging_par['protocol_sampled_all'])) + self.index=range(len(self.optical_tagging_par['protocol_sampled_all'])) + self.finish_tag = 0 # send the trigger source self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') - # iterate each condition - for i in range(len(self.optical_tagging_par['protocol_sampled_all'])): - # get the current parameters - protocol = self.optical_tagging_par['protocol_sampled_all'][i] - frequency = self.optical_tagging_par['frequency_sampled_all'][i] - pulse_duration = self.optical_tagging_par['pulse_duration_sampled_all'][i] - laser_name = self.optical_tagging_par['laser_name_sampled_all'][i] - target_power = self.optical_tagging_par['target_power_sampled_all'][i] - laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] - duration_each_cycle = self.optical_tagging_par['duration_each_cycle_sampled_all'][i] - interval_between_cycles = self.optical_tagging_par['interval_between_cycles_sampled_all'][i] - # produce the waveforms - my_wave=self._produce_waveforms(protocol=protocol, - frequency=frequency, - pulse_duration=pulse_duration, - laser_name=laser_name, - target_power=target_power, - laser_color=laser_color, - duration_each_cycle=duration_each_cycle - ) - my_wave_control=self._produce_waveforms(protocol=protocol, - frequency=frequency, - pulse_duration=pulse_duration, - laser_name=laser_name, - target_power=0, - laser_color=laser_color, - duration_each_cycle=duration_each_cycle - ) - if my_wave is None: - continue - # send the waveform and size to the bonsai - if laser_name=='Laser_1': - getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave.size)) - getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave.tolist())[1:-1]) - getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave_control.size)) - getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave_control.tolist())[1:-1]) - elif laser_name=='Laser_2': - getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave.size)) - getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave.tolist())[1:-1]) - getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave_control.size)) - getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave_control.tolist())[1:-1]) - FinishOfWaveForm=self.MainWindow.Channel4.receive() - # initiate the laser - # need to change the bonsai code to initiate the laser - self._initiate_laser() - # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. - Rec=self.MainWindow.Channel1.receive() - if Rec[0].address=='/ITIStartTimeHarp': - self.optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] - # change the success_tag to 1 - self.optical_tagging_par['success_tag'][i]=1 - else: - self.optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag - # wait to start the next cycle - time.sleep(duration_each_cycle+interval_between_cycles) - # show current cycle and parameters - self.label_show_current.setText(f'Cycle {i+1}/{len(self.optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + + # start the optical tagging in a different thread + worker_tagging = Worker(self._start_optical_tagging) + worker_tagging.signals.finished.connect(self._thread_complete_tagging) + + # Execute + self.threadpool.start(worker_tagging) + + def _emegency_stop(self): + '''Stop the optical tagging''' + self.finish_tag = 1 + self.Start.setChecked(False) + + def _thread_complete_tagging(self): + '''Complete the optical tagging''' + self.finish_tag = 1 + + def _start_optical_tagging(self): + '''Start the optical tagging in a different thread''' + while self.finish_tag==0 and self.Start.isChecked(): + # iterate each condition + for i in self.index: + # exclude the index that has been run + self.index.remove(i) + # get the current parameters + protocol = self.optical_tagging_par['protocol_sampled_all'][i] + frequency = self.optical_tagging_par['frequency_sampled_all'][i] + pulse_duration = self.optical_tagging_par['pulse_duration_sampled_all'][i] + laser_name = self.optical_tagging_par['laser_name_sampled_all'][i] + target_power = self.optical_tagging_par['target_power_sampled_all'][i] + laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] + duration_each_cycle = self.optical_tagging_par['duration_each_cycle_sampled_all'][i] + interval_between_cycles = self.optical_tagging_par['interval_between_cycles_sampled_all'][i] + # produce the waveforms + my_wave=self._produce_waveforms(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + laser_name=laser_name, + target_power=target_power, + laser_color=laser_color, + duration_each_cycle=duration_each_cycle + ) + my_wave_control=self._produce_waveforms(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + laser_name=laser_name, + target_power=0, + laser_color=laser_color, + duration_each_cycle=duration_each_cycle + ) + if my_wave is None: + continue + # send the waveform and size to the bonsai + if laser_name=='Laser_1': + getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave.tolist())[1:-1]) + getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave_control.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave_control.tolist())[1:-1]) + elif laser_name=='Laser_2': + getattr(self.MainWindow.Channel, 'Location2_Size')(int(my_wave.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_2')(str(my_wave.tolist())[1:-1]) + getattr(self.MainWindow.Channel, 'Location1_Size')(int(my_wave_control.size)) + getattr(self.MainWindow.Channel4, 'WaveForm1_1')(str(my_wave_control.tolist())[1:-1]) + FinishOfWaveForm=self.MainWindow.Channel4.receive() + # initiate the laser + # need to change the bonsai code to initiate the laser + self._initiate_laser() + # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. + Rec=self.MainWindow.Channel1.receive() + if Rec[0].address=='/ITIStartTimeHarp': + self.optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] + # change the success_tag to 1 + self.optical_tagging_par['success_tag'][i]=1 + else: + self.optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag + # wait to start the next cycle + time.sleep(duration_each_cycle+interval_between_cycles) + # show current cycle and parameters + self.label_show_current.setText(f'Cycle {i+1}/{len(self.optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + def _initiate_laser(self): '''Initiate laser in bonsai''' # start generating waveform in bonsai From e4eab19ad72fed39554dc40e07e0e3e103baa3ed Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:22:51 -0800 Subject: [PATCH 039/184] saving optical tagging parameters --- src/foraging_gui/Dialogs.py | 104 ++++++++++++++++++++++++----------- src/foraging_gui/Foraging.py | 2 + 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 7e98bee0f..cbfeb25fb 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3042,9 +3042,11 @@ def __init__(self, MainWindow, parent=None): uic.loadUi('OpticalTagging.ui', self) self._connectSignalsSlots() self.MainWindow = MainWindow + self.current_optical_tagging_par={} self.optical_tagging_par={} self.finish_tag = 1 self.threadpool = QThreadPool() + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) @@ -3062,8 +3064,8 @@ def _Start(self): if self.finish_tag==1: # generate new random conditions self._generate_random_conditions() - self.optical_tagging_par['success_tag'] = np.zeros(len(self.optical_tagging_par['protocol_sampled_all'])) - self.index=range(len(self.optical_tagging_par['protocol_sampled_all'])) + self.current_optical_tagging_par['success_tag'] = np.zeros(len(self.current_optical_tagging_par['protocol_sampled_all'])) + self.index=range(len(self.current_optical_tagging_par['protocol_sampled_all'])) self.finish_tag = 0 # send the trigger source @@ -3075,7 +3077,7 @@ def _Start(self): # Execute self.threadpool.start(worker_tagging) - + def _emegency_stop(self): '''Stop the optical tagging''' self.finish_tag = 1 @@ -3084,6 +3086,8 @@ def _emegency_stop(self): def _thread_complete_tagging(self): '''Complete the optical tagging''' self.finish_tag = 1 + # Add 1 to the location tag + self.LocationTag.setValue(self.LocationTag.value()+1) def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' @@ -3093,14 +3097,15 @@ def _start_optical_tagging(self): # exclude the index that has been run self.index.remove(i) # get the current parameters - protocol = self.optical_tagging_par['protocol_sampled_all'][i] - frequency = self.optical_tagging_par['frequency_sampled_all'][i] - pulse_duration = self.optical_tagging_par['pulse_duration_sampled_all'][i] - laser_name = self.optical_tagging_par['laser_name_sampled_all'][i] - target_power = self.optical_tagging_par['target_power_sampled_all'][i] - laser_color = self.optical_tagging_par['laser_color_sampled_all'][i] - duration_each_cycle = self.optical_tagging_par['duration_each_cycle_sampled_all'][i] - interval_between_cycles = self.optical_tagging_par['interval_between_cycles_sampled_all'][i] + protocol = self.current_optical_tagging_par['protocol_sampled_all'][i] + frequency = self.current_optical_tagging_par['frequency_sampled_all'][i] + pulse_duration = self.current_optical_tagging_par['pulse_duration_sampled_all'][i] + laser_name = self.current_optical_tagging_par['laser_name_sampled_all'][i] + target_power = self.current_optical_tagging_par['target_power_sampled_all'][i] + laser_color = self.current_optical_tagging_par['laser_color_sampled_all'][i] + duration_each_cycle = self.current_optical_tagging_par['duration_each_cycle_sampled_all'][i] + interval_between_cycles = self.current_optical_tagging_par['interval_between_cycles_sampled_all'][i] + location_tag = self.current_optical_tagging_par['location_tag'][i] # produce the waveforms my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, @@ -3138,16 +3143,50 @@ def _start_optical_tagging(self): # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. Rec=self.MainWindow.Channel1.receive() if Rec[0].address=='/ITIStartTimeHarp': - self.optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] + self.current_optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] # change the success_tag to 1 - self.optical_tagging_par['success_tag'][i]=1 + self.current_optical_tagging_par['success_tag'][i]=1 else: - self.optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag + self.current_optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag + # save the data + self._save_data(protocol=protocol, + frequency=frequency, + pulse_duration=pulse_duration, + laser_name=laser_name, + target_power=target_power, + laser_color=laser_color, + duration_each_cycle=duration_each_cycle, + interval_between_cycles=interval_between_cycles, + location_tag=location_tag + ) # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText(f'Cycle {i+1}/{len(self.optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + self.label_show_current.setText(f'Cycle {i+1}/{len(self.current_optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): + '''Extend the current parameters to self.optical_tagging_par''' + if 'protocol' not in self.optical_tagging_par.keys(): + self.optical_tagging_par['protocol']=[] + self.optical_tagging_par['frequency']=[] + self.optical_tagging_par['pulse_duration']=[] + self.optical_tagging_par['laser_name']=[] + self.optical_tagging_par['target_power']=[] + self.optical_tagging_par['laser_color']=[] + self.optical_tagging_par['duration_each_cycle']=[] + self.optical_tagging_par['interval_between_cycles']=[] + self.optical_tagging_par['location_tag']=[] + else: + self.optical_tagging_par['protocol'].append(protocol) + self.optical_tagging_par['frequency'].append(frequency) + self.optical_tagging_par['pulse_duration'].append(pulse_duration) + self.optical_tagging_par['laser_name'].append(laser_name) + self.optical_tagging_par['target_power'].append(target_power) + self.optical_tagging_par['laser_color'].append(laser_color) + self.optical_tagging_par['duration_each_cycle'].append(duration_each_cycle) + self.optical_tagging_par['interval_between_cycles'].append(interval_between_cycles) + self.optical_tagging_par['location_tag'].append(location_tag) + def _initiate_laser(self): '''Initiate laser in bonsai''' # start generating waveform in bonsai @@ -3211,14 +3250,15 @@ def _generate_random_conditions(self): for duration_each_cycle in duration_each_cycle_list ]) - self.optical_tagging_par['protocol_sampled_all'] = [] - self.optical_tagging_par['frequency_sampled_all'] = [] - self.optical_tagging_par['pulse_duration_sampled_all'] = [] - self.optical_tagging_par['laser_name_sampled_all'] = [] - self.optical_tagging_par['target_power_sampled_all'] = [] - self.optical_tagging_par['laser_color_sampled_all'] = [] - self.optical_tagging_par['duration_each_cycle_sampled_all'] = [] - self.optical_tagging_par['Interval_between_cycles_sampled_all'] = [] + self.current_optical_tagging_par['protocol_sampled_all'] = [] + self.current_optical_tagging_par['frequency_sampled_all'] = [] + self.current_optical_tagging_par['pulse_duration_sampled_all'] = [] + self.current_optical_tagging_par['laser_name_sampled_all'] = [] + self.current_optical_tagging_par['target_power_sampled_all'] = [] + self.current_optical_tagging_par['laser_color_sampled_all'] = [] + self.current_optical_tagging_par['duration_each_cycle_sampled_all'] = [] + self.current_optical_tagging_par['interval_between_cycles_sampled_all'] = [] + self.current_optical_tagging_par['location_tag_sampled_all'] = [] for _ in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3230,14 +3270,16 @@ def _generate_random_conditions(self): target_power_sampled_now = [target_power_sampled[i] for i in random_indices] laser_color_sampled_now = [laser_color_sampled[i] for i in random_indices] # Append the conditions - self.optical_tagging_par['protocol_sampled_all'].extend(protocol_sampled_now) - self.optical_tagging_par['frequency_sampled_all'].extend(frequency_sampled_now) - self.optical_tagging_par['pulse_duration_sampled_all'].extend(pulse_duration_sampled_now) - self.optical_tagging_par['laser_name_sampled_all'].extend(laser_name_sampled_now) - self.optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) - self.optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) - self.optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) - self.optical_tagging_par['Interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) + self.current_optical_tagging_par['protocol_sampled_all'].extend(protocol_sampled_now) + self.current_optical_tagging_par['frequency_sampled_all'].extend(frequency_sampled_now) + self.current_optical_tagging_par['pulse_duration_sampled_all'].extend(pulse_duration_sampled_now) + self.current_optical_tagging_par['laser_name_sampled_all'].extend(laser_name_sampled_now) + self.current_optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) + self.current_optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) + self.current_optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) + self.current_optical_tagging_par['interval_between_cycles_sampled_all'].extend(float(self.Interval_between_cycles.text())) + self.current_optical_tagging_par['location_tag_sampled_all'].extend(float(self.LocationTag.value())) + def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' laser_name = self.WhichLaser.currentText() diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index a3551a136..e36a356a7 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2694,6 +2694,8 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): Obj['MetadataFolder']=self.MetadataFolder Obj['SaveFile']=self.SaveFile + # save optical tagging parameters + Obj['optical_tagging_par']=self.OpticalTagging_dialog.optical_tagging_par # generate the metadata file and update slims try: # save the metadata collected in the metadata dialogue From e66c71439a11da87d0028a19a973e78b4f1fafa3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:26:42 -0800 Subject: [PATCH 040/184] open and save OpticalTagging_dialog --- src/foraging_gui/Foraging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index e36a356a7..01eec4211 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2577,7 +2577,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): QtWidgets.QComboBox,QtWidgets.QDoubleSpinBox,QtWidgets.QSpinBox))} widget_dict.update({w.objectName(): w for w in self.TrainingParameters.findChildren(QtWidgets.QDoubleSpinBox)}) self._Concat(widget_dict,Obj,'None') - dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','Metadata_dialog'] + dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','Metadata_dialog','OpticalTagging_dialog'] for dialog_name in dialogs: if hasattr(self, dialog_name): widget_dict = {w.objectName(): w for w in getattr(self, dialog_name).findChildren( @@ -3061,7 +3061,7 @@ def _Open(self,open_last = False,input_file = ''): self.Obj = Obj widget_dict={} - dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','centralwidget','TrainingParameters'] + dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','centralwidget','TrainingParameters','OpticalTagging_dialog'] for dialog_name in dialogs: if hasattr(self, dialog_name): widget_types = (QtWidgets.QPushButton, QtWidgets.QLineEdit, QtWidgets.QTextEdit, From 98e2cfd13d5cba8463a2bd1831c3c33acab557f8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:38:48 -0800 Subject: [PATCH 041/184] set Start checkable --- src/foraging_gui/OpticalTagging.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 264436962..0cdcbb098 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -402,6 +402,9 @@ + Start + true +- From 455bb4adb57949ba3cd179292683e3e636868bce Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:48:59 -0800 Subject: [PATCH 042/184] changing extend to append --- src/foraging_gui/Dialogs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index cbfeb25fb..37fd1646d 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3269,6 +3269,7 @@ def _generate_random_conditions(self): laser_name_sampled_now = [laser_name_sampled[i] for i in random_indices] target_power_sampled_now = [target_power_sampled[i] for i in random_indices] laser_color_sampled_now = [laser_color_sampled[i] for i in random_indices] + duration_each_cycle_sampled = [duration_each_cycle_sampled[i] for i in random_indices] # Append the conditions self.current_optical_tagging_par['protocol_sampled_all'].extend(protocol_sampled_now) self.current_optical_tagging_par['frequency_sampled_all'].extend(frequency_sampled_now) @@ -3277,8 +3278,8 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) self.current_optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) self.current_optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) - self.current_optical_tagging_par['interval_between_cycles_sampled_all'].extend(float(self.Interval_between_cycles.text())) - self.current_optical_tagging_par['location_tag_sampled_all'].extend(float(self.LocationTag.value())) + self.current_optical_tagging_par['interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) + self.current_optical_tagging_par['location_tag_sampled_all'].append(float(self.LocationTag.value())) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' From 261c981fd6e7c4706b3550fe68b861aff50ee83c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:59:28 -0800 Subject: [PATCH 043/184] adding epoch and iteration --- src/foraging_gui/Dialogs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 37fd1646d..d2f87aaf1 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3106,6 +3106,7 @@ def _start_optical_tagging(self): duration_each_cycle = self.current_optical_tagging_par['duration_each_cycle_sampled_all'][i] interval_between_cycles = self.current_optical_tagging_par['interval_between_cycles_sampled_all'][i] location_tag = self.current_optical_tagging_par['location_tag'][i] + current_epoch = self.current_optical_tagging_par['epoch'][i] # produce the waveforms my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, @@ -3162,7 +3163,7 @@ def _start_optical_tagging(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText(f'Cycle {i+1}/{len(self.current_optical_tagging_par["protocol_sampled_all"])}\nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + self.label_show_current.setText(f'Epoch: {current_epoch}\n Iteration: {i%float(self.Cycles_each_power.text())} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): '''Extend the current parameters to self.optical_tagging_par''' @@ -3259,7 +3260,8 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['duration_each_cycle_sampled_all'] = [] self.current_optical_tagging_par['interval_between_cycles_sampled_all'] = [] self.current_optical_tagging_par['location_tag_sampled_all'] = [] - for _ in range(number_of_cycles): + self.current_optical_tagging_par['epoch'] = [] + for i in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) # Use the random indices to shuffle the conditions @@ -3280,6 +3282,7 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) self.current_optical_tagging_par['interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) self.current_optical_tagging_par['location_tag_sampled_all'].append(float(self.LocationTag.value())) + self.current_optical_tagging_par['epoch'].extend([i+1]*len(protocol_sampled_now)) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' From 59e3750aa35c98942d3011c7709158904803fcba Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:59:59 -0800 Subject: [PATCH 044/184] changing name to Cycles_each_condition --- src/foraging_gui/Dialogs.py | 4 ++-- src/foraging_gui/OpticalTagging.ui | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index d2f87aaf1..6707c8170 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3163,7 +3163,7 @@ def _start_optical_tagging(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText(f'Epoch: {current_epoch}\n Iteration: {i%float(self.Cycles_each_power.text())} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + self.label_show_current.setText(f'Epoch: {current_epoch}\n Iteration: {i%float(self.Cycles_each_condition.text())} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): '''Extend the current parameters to self.optical_tagging_par''' @@ -3214,7 +3214,7 @@ def _generate_random_conditions(self): if protocol!='Pulse': raise ValueError(f"Unknown protocol: {protocol}") # get the number of cycles - number_of_cycles = int(math.floor(float(self.Cycles_each_power.text()))) + number_of_cycles = int(math.floor(float(self.Cycles_each_condition.text()))) # get the frequency frequency_list = list(map(int, extract_numbers_from_string(self.Frequency.text()))) # get the pulse duration (seconds) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 0cdcbb098..5e7cfea7a 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -550,7 +550,7 @@ false + From 19cdd7cd1989bc590d65d58c62aed1bdac9ae5b5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:03:21 -0800 Subject: [PATCH 045/184] changing the name --- src/foraging_gui/OpticalTagging.ui | 116 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 5e7cfea7a..55ee11089 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -6,7 +6,7 @@ true 0 0 -270 +286 575 @@ -1813,7 +1811,7 @@ - @@ -41,8 +41,8 @@60 -70 +81 +60 76 20 @@ -1759,8 +1757,8 @@ - @@ -63,8 +63,8 @@60 -130 +81 +120 76 20 - @@ -93,8 +93,8 @@140 -100 +161 +90 70 20 - @@ -118,8 +118,8 @@58 -280 +79 +270 76 20 @@ -1534,8 +1533,7 @@ - @@ -143,8 +143,8 @@58 -100 +79 +90 76 20 - @@ -162,8 +162,8 @@20 -220 +41 +210 116 20 - @@ -187,8 +187,8 @@54 -250 +75 +240 80 20 From 03bb7406c0aa24a0b08f472a7b82479149ebdca9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:39:25 -0800 Subject: [PATCH 081/184] save the optical tagging results --- src/foraging_gui/Dialogs.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index b4bc5fe2b..0e3b82050 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3051,7 +3051,22 @@ def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) self.EmergencyStop.clicked.connect(self._emegency_stop) - + self.Save.clicked.connect(self._Save) + + def _Save(self): + '''Save the optical tagging results''' + if self.optical_tagging_par=={}: + return + # get the save folder + save_folder = QFileDialog.getExistingDirectory(self, 'Select the folder to save the optical tagging results') + if save_folder=='': + return + # create the file name AnimalID_Date_OpticalTaggingResults.csv + save_file = os.path.join(save_folder, f'{self.MainWindow.AnimalID.text()}_{datetime.now().strftime("%Y-%m-%d")}_OpticalTaggingResults.json') + # save the data + with open(save_file, 'w') as f: + json.dump(self.optical_tagging_par, f) + def _Start(self): '''Start the optical tagging''' # toggle the button color From a8e909cd5e0f1491c37edf6c3af5ea0b597eeb7e Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:42:07 -0800 Subject: [PATCH 082/184] ID typo --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 0e3b82050..0461f81e3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3062,7 +3062,7 @@ def _Save(self): if save_folder=='': return # create the file name AnimalID_Date_OpticalTaggingResults.csv - save_file = os.path.join(save_folder, f'{self.MainWindow.AnimalID.text()}_{datetime.now().strftime("%Y-%m-%d")}_OpticalTaggingResults.json') + save_file = os.path.join(save_folder, f'{self.MainWindow.ID.text()}_{datetime.now().strftime("%Y-%m-%d")}_OpticalTaggingResults.json') # save the data with open(save_file, 'w') as f: json.dump(self.optical_tagging_par, f) From 9908c1204ed343cbffa69ec5280ab85e74c7e3d9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:48:34 -0800 Subject: [PATCH 083/184] changing the saving format --- src/foraging_gui/Dialogs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 0461f81e3..34d0abb20 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3061,11 +3061,11 @@ def _Save(self): save_folder = QFileDialog.getExistingDirectory(self, 'Select the folder to save the optical tagging results') if save_folder=='': return - # create the file name AnimalID_Date_OpticalTaggingResults.csv - save_file = os.path.join(save_folder, f'{self.MainWindow.ID.text()}_{datetime.now().strftime("%Y-%m-%d")}_OpticalTaggingResults.json') - # save the data + # create the file name AnimalID_Date(day+hour+minute)_OpticalTaggingResults.csv + save_file = os.path.join(save_folder, f"{self.optical_tagging_par['AnimalID']}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") + # save the data with open(save_file, 'w') as f: - json.dump(self.optical_tagging_par, f) + json.dump(self.optical_tagging_par, f, indent=4) def _Start(self): '''Start the optical tagging''' From f96674c2626e72de1e4ca42e52e30a5fcbba6786 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:54:32 -0800 Subject: [PATCH 084/184] changing the position --- src/foraging_gui/Dialogs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 34d0abb20..77e81bff3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3113,6 +3113,10 @@ def _start_optical_tagging(self,update_label): # iterate each condition for i in self.index[:]: if self.Start.isChecked(): + if i == self.index[-1]: + self.cycle_finish_tag = 1 + # exclude the index that has been run + self.index.remove(i) success_tag=0 # get the current parameters protocol = self.current_optical_tagging_par['protocol_sampled_all'][i] @@ -3196,10 +3200,7 @@ def _start_optical_tagging(self,update_label): f"Duration: {duration_each_cycle} s\n" f"Interval: {interval_between_cycles} s" ) - if i == self.index[-1]: - self.cycle_finish_tag = 1 - # exclude the index that has been run - self.index.remove(i) + else: break From d41edd4e272031a56efdc2688ebf959793e8c15e Mon Sep 17 00:00:00 2001 From: Xinxin Yin - @@ -237,8 +237,8 @@140 -130 +161 +120 70 20 - @@ -277,8 +277,8 @@140 -70 +161 +60 70 20 - @@ -299,8 +299,8 @@140 -280 +161 +270 70 20 - @@ -321,8 +321,8 @@20 -190 +41 +180 116 20 - @@ -346,8 +346,8 @@140 -250 +161 +240 70 20 - @@ -371,8 +371,8 @@140 -190 +161 +180 70 20 - @@ -393,8 +393,8 @@140 -220 +161 +210 70 20 - @@ -412,8 +412,8 @@140 -32 +161 +22 70 31 - @@ -437,8 +437,8 @@60 -430 +81 +420 76 20 - @@ -450,9 +450,9 @@140 -430 +161 +420 71 22 @@ -478,8 +478,8 @@ - 15 -330 -121 +16 +320 +141 20 - @@ -503,9 +503,9 @@140 -330 +161 +320 70 20 @@ -531,8 +531,8 @@ - 15 -360 -121 +16 +350 +141 20 - @@ -556,8 +556,8 @@140 -360 +161 +350 70 20 - @@ -581,8 +581,8 @@140 -390 +161 +380 70 20 - @@ -594,7 +594,7 @@5 -390 +26 +380 131 20 - cycles each laser power= +cycles each condition= Qt::AutoText @@ -606,8 +606,8 @@+ - @@ -656,8 +656,8 @@140 -160 +161 +150 70 20 - @@ -681,8 +681,8 @@60 -160 +81 +150 76 20 - @@ -706,8 +706,8 @@30 -520 +51 +510 191 20 - From 0e649f33f96a908dfc6044d8977e935d5da1965a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:05:45 -0800 Subject: [PATCH 046/184] showing only cycles --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 6707c8170..a4c3edb74 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3163,7 +3163,7 @@ def _start_optical_tagging(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText(f'Epoch: {current_epoch}\n Iteration: {i%float(self.Cycles_each_condition.text())} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') + self.label_show_current.setText(f'Cycles: {i}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): '''Extend the current parameters to self.optical_tagging_par''' From 799a6ecd618b8a269198aaa3f50e24d7f2282745 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:08:27 -0800 Subject: [PATCH 047/184] correcting the syntax --- src/foraging_gui/Dialogs.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a4c3edb74..792585827 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3163,8 +3163,18 @@ def _start_optical_tagging(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText(f'Cycles: {i}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \nprotocol: {protocol}\nFrequency: {frequency} Hz\nPulse Duration: {pulse_duration} ms\nLaser: {laser_name}\nPower: {target_power} mW\nColor: {laser_color}\nDuration: {duration_each_cycle} s\nInterval: {interval_between_cycles} s') - + self.label_show_current.setText( + f"Cycles: {i}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" + f"protocol: {protocol}\n" + f"Frequency: {frequency} Hz\n" + f"Pulse Duration: {pulse_duration} ms\n" + f"Laser: {laser_name}\n" + f"Power: {target_power} mW\n" + f"Color: {laser_color}\n" + f"Duration: {duration_each_cycle} s\n" + f"Interval: {interval_between_cycles} s" + ) + def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): '''Extend the current parameters to self.optical_tagging_par''' if 'protocol' not in self.optical_tagging_par.keys(): From 9beb5436d2f9170381901594333f4b455eb1a8d8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:17:39 -0800 Subject: [PATCH 048/184] correcting grammar and naming --- src/foraging_gui/Dialogs.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 792585827..873c273ec 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3065,7 +3065,7 @@ def _Start(self): # generate new random conditions self._generate_random_conditions() self.current_optical_tagging_par['success_tag'] = np.zeros(len(self.current_optical_tagging_par['protocol_sampled_all'])) - self.index=range(len(self.current_optical_tagging_par['protocol_sampled_all'])) + self.index=list(range(len(self.current_optical_tagging_par['protocol_sampled_all']))) self.finish_tag = 0 # send the trigger source @@ -3077,6 +3077,7 @@ def _Start(self): # Execute self.threadpool.start(worker_tagging) + #self._start_optical_tagging() def _emegency_stop(self): '''Stop the optical tagging''' @@ -3105,8 +3106,7 @@ def _start_optical_tagging(self): laser_color = self.current_optical_tagging_par['laser_color_sampled_all'][i] duration_each_cycle = self.current_optical_tagging_par['duration_each_cycle_sampled_all'][i] interval_between_cycles = self.current_optical_tagging_par['interval_between_cycles_sampled_all'][i] - location_tag = self.current_optical_tagging_par['location_tag'][i] - current_epoch = self.current_optical_tagging_par['epoch'][i] + location_tag = self.current_optical_tagging_par['location_tag_sampled_all'][i] # produce the waveforms my_wave=self._produce_waveforms(protocol=protocol, frequency=frequency, @@ -3270,7 +3270,6 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['duration_each_cycle_sampled_all'] = [] self.current_optical_tagging_par['interval_between_cycles_sampled_all'] = [] self.current_optical_tagging_par['location_tag_sampled_all'] = [] - self.current_optical_tagging_par['epoch'] = [] for i in range(number_of_cycles): # Generate a random index to sample conditions random_indices = random.sample(range(len(protocol_sampled)), len(protocol_sampled)) @@ -3292,7 +3291,6 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) self.current_optical_tagging_par['interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) self.current_optical_tagging_par['location_tag_sampled_all'].append(float(self.LocationTag.value())) - self.current_optical_tagging_par['epoch'].extend([i+1]*len(protocol_sampled_now)) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' From d6a288e9ed96772962bfc7a291b05692aad7a435 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:26:34 -0800 Subject: [PATCH 049/184] Changing the channel name --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 873c273ec..e1039992d 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3142,7 +3142,7 @@ def _start_optical_tagging(self): # need to change the bonsai code to initiate the laser self._initiate_laser() # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. - Rec=self.MainWindow.Channel1.receive() + Rec=self.MainWindow.Channel.receive() if Rec[0].address=='/ITIStartTimeHarp': self.current_optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] # change the success_tag to 1 From 6a2e9c2e269197a9ba5985dcf8d6d445dafd398c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:29:36 -0800 Subject: [PATCH 050/184] adding QApplication.processEvents() --- src/foraging_gui/Dialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index e1039992d..125a44702 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3093,6 +3093,7 @@ def _thread_complete_tagging(self): def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' while self.finish_tag==0 and self.Start.isChecked(): + QApplication.processEvents() # iterate each condition for i in self.index: # exclude the index that has been run From 24af4930f2d9d9e342f6f8529b8185f6ce2c0189 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:43:12 -0800 Subject: [PATCH 051/184] adding unit --- src/foraging_gui/OpticalTagging.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 55ee11089..4760dc1d4 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -516,7 +516,7 @@109 -460 +130 +450 101 31 - interval between cycles= +interval between cycles (s)= @@ -681,8 +681,8 @@ Qt::AutoText From fc1fc902821463652bcb339c8a8f4bb8e397a57b Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:55:30 -0800 Subject: [PATCH 052/184] removing receive --- src/foraging_gui/Dialogs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 125a44702..5ea07e93b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3203,7 +3203,6 @@ def _initiate_laser(self): '''Initiate laser in bonsai''' # start generating waveform in bonsai self.MainWindow.Channel.OptogeneticsCalibration(int(1)) - self.MainWindow.Channel.receive() def _generate_random_conditions(self): """ From a478726c10fad72dca6adad5052ff2edf8bac17c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:07:27 -0800 Subject: [PATCH 053/184] saving success_tag and laser_start_timestamp --- src/foraging_gui/Dialogs.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 5ea07e93b..d87a0c1e3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3064,7 +3064,6 @@ def _Start(self): if self.finish_tag==1: # generate new random conditions self._generate_random_conditions() - self.current_optical_tagging_par['success_tag'] = np.zeros(len(self.current_optical_tagging_par['protocol_sampled_all'])) self.index=list(range(len(self.current_optical_tagging_par['protocol_sampled_all']))) self.finish_tag = 0 @@ -3096,6 +3095,7 @@ def _start_optical_tagging(self): QApplication.processEvents() # iterate each condition for i in self.index: + success_tag=0 # exclude the index that has been run self.index.remove(i) # get the current parameters @@ -3144,12 +3144,14 @@ def _start_optical_tagging(self): self._initiate_laser() # receiving the timestamps of laser start and saving them. The laser waveforms should be sent to the NI-daq as a backup. Rec=self.MainWindow.Channel.receive() + if Rec[0].address=='/ITIStartTimeHarp': - self.current_optical_tagging_par['laser_start_timestamp'][i]=Rec[1][1][0] + laser_start_timestamp=Rec[1][1][0] # change the success_tag to 1 - self.current_optical_tagging_par['success_tag'][i]=1 + success_tag=1 else: - self.current_optical_tagging_par['laser_start_timestamp'][i]=-999 # error tag + laser_start_timestamp=-999 # error tag + # save the data self._save_data(protocol=protocol, frequency=frequency, @@ -3159,7 +3161,9 @@ def _start_optical_tagging(self): laser_color=laser_color, duration_each_cycle=duration_each_cycle, interval_between_cycles=interval_between_cycles, - location_tag=location_tag + location_tag=location_tag, + laser_start_timestamp=laser_start_timestamp, + success_tag=success_tag ) # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) @@ -3176,7 +3180,7 @@ def _start_optical_tagging(self): f"Interval: {interval_between_cycles} s" ) - def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag): + def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag, laser_start_timestamp, success_tag): '''Extend the current parameters to self.optical_tagging_par''' if 'protocol' not in self.optical_tagging_par.keys(): self.optical_tagging_par['protocol']=[] From 68dd9060bd6e955a08a94f3994e735413ab5bbc4 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:14:36 -0800 Subject: [PATCH 054/184] removing while --- src/foraging_gui/Dialogs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index d87a0c1e3..6a75c30ba 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3091,10 +3091,10 @@ def _thread_complete_tagging(self): def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' - while self.finish_tag==0 and self.Start.isChecked(): + # iterate each condition + for i in self.index: QApplication.processEvents() - # iterate each condition - for i in self.index: + if self.Start.isChecked() and self.finish_tag==0: success_tag=0 # exclude the index that has been run self.index.remove(i) @@ -3179,6 +3179,8 @@ def _start_optical_tagging(self): f"Duration: {duration_each_cycle} s\n" f"Interval: {interval_between_cycles} s" ) + else: + break def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_power, laser_color, duration_each_cycle, interval_between_cycles, location_tag, laser_start_timestamp, success_tag): '''Extend the current parameters to self.optical_tagging_par''' From 048a9389765646d7ae2f8086707b54160f0d5f1c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:19:27 -0800 Subject: [PATCH 055/184] updating interval_between_cycles_sampled and location_tag_sampled_all --- src/foraging_gui/Dialogs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 6a75c30ba..eed2f98c1 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3258,13 +3258,14 @@ def _generate_random_conditions(self): return duration_each_cycle_list = extract_numbers_from_string(self.Duration_each_cycle.text()) # Generate combinations for each laser - protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled,duration_each_cycle_sampled = zip(*[ + protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled,duration_each_cycle_sampled,interval_between_cycles_sampled = zip(*[ (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText(),duration_each_cycle) for frequency in frequency_list for pulse_duration in pulse_duration_list for laser_name, (power_field, _) in laser_config.items() for target_power in extract_numbers_from_string(power_field.text()) for duration_each_cycle in duration_each_cycle_list + for interval_between_cycles in extract_numbers_from_string(self.Interval_between_cycles.text()) ]) self.current_optical_tagging_par['protocol_sampled_all'] = [] @@ -3295,8 +3296,8 @@ def _generate_random_conditions(self): self.current_optical_tagging_par['target_power_sampled_all'].extend(target_power_sampled_now) self.current_optical_tagging_par['laser_color_sampled_all'].extend(laser_color_sampled_now) self.current_optical_tagging_par['duration_each_cycle_sampled_all'].extend(duration_each_cycle_sampled) - self.current_optical_tagging_par['interval_between_cycles_sampled_all'].append(float(self.Interval_between_cycles.text())) - self.current_optical_tagging_par['location_tag_sampled_all'].append(float(self.LocationTag.value())) + self.current_optical_tagging_par['interval_between_cycles_sampled_all'].extend(interval_between_cycles_sampled) + self.current_optical_tagging_par['location_tag_sampled_all'].extend([float(self.LocationTag.value())]*len(protocol_sampled)) def _WhichLaser(self): '''Select the laser to use and disable non-relevant widgets''' From 536535b68e4027cac1a4cfd29b8bdb687d69a641 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:21:40 -0800 Subject: [PATCH 056/184] grammar --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index eed2f98c1..a32b5ec6f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3259,7 +3259,7 @@ def _generate_random_conditions(self): duration_each_cycle_list = extract_numbers_from_string(self.Duration_each_cycle.text()) # Generate combinations for each laser protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled,duration_each_cycle_sampled,interval_between_cycles_sampled = zip(*[ - (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText(),duration_each_cycle) + (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText(),duration_each_cycle,interval_between_cycles) for frequency in frequency_list for pulse_duration in pulse_duration_list for laser_name, (power_field, _) in laser_config.items() From 28494ac8787cc484cda398619d780012a149ad46 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:24:23 -0800 Subject: [PATCH 057/184] simplify --- src/foraging_gui/Dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a32b5ec6f..31571bf4a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3256,7 +3256,7 @@ def _generate_random_conditions(self): # give an popup error window if the laser is not selected QMessageBox.critical(self.MainWindow, "Error", "Please select the laser to use.") return - duration_each_cycle_list = extract_numbers_from_string(self.Duration_each_cycle.text()) + # Generate combinations for each laser protocol_sampled, frequency_sampled, pulse_duration_sampled, laser_name_sampled, target_power_sampled, laser_color_sampled,duration_each_cycle_sampled,interval_between_cycles_sampled = zip(*[ (protocol, frequency, pulse_duration, laser_name, target_power, laser_config[laser_name][1].currentText(),duration_each_cycle,interval_between_cycles) @@ -3264,7 +3264,7 @@ def _generate_random_conditions(self): for pulse_duration in pulse_duration_list for laser_name, (power_field, _) in laser_config.items() for target_power in extract_numbers_from_string(power_field.text()) - for duration_each_cycle in duration_each_cycle_list + for duration_each_cycle in extract_numbers_from_string(self.Duration_each_cycle.text()) for interval_between_cycles in extract_numbers_from_string(self.Interval_between_cycles.text()) ]) From 0ae42ad81f93e590dbe4ff416c4b212a7f7aaa87 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:26:20 -0800 Subject: [PATCH 058/184] change the location of information label --- src/foraging_gui/OpticalTagging.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 4760dc1d4..a85646c12 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -6,7 +6,7 @@0 0 -286 +501 575 - From 34d58cdb454168b29386b025042199e6834f5814 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:28:34 -0800 Subject: [PATCH 059/184] changing the color of Start --- src/foraging_gui/Dialogs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 31571bf4a..ca44ea0b3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3082,7 +3082,8 @@ def _emegency_stop(self): '''Stop the optical tagging''' self.finish_tag = 1 self.Start.setChecked(False) - + self.Start.setStyleSheet("background-color : none") + def _thread_complete_tagging(self): '''Complete the optical tagging''' self.finish_tag = 1 From f68ef0d772e93326b0c5a229c7835e9acb9214b3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:18:06 -0800 Subject: [PATCH 060/184] changing the size of the information label --- src/foraging_gui/OpticalTagging.ui | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index a85646c12..a559107b3 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -684,7 +684,7 @@51 -510 +270 +30 191 20 270 30 191 -20 +321 @@ -702,6 +702,9 @@ + Qt::AlignCenter + false +From 8c87ffb978c0b45eb90407fff68fc34e1b95a68e Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:38:54 -0800 Subject: [PATCH 061/184] adding the cycle finish tag --- src/foraging_gui/Dialogs.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index ca44ea0b3..3cf1b8cf1 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3044,7 +3044,8 @@ def __init__(self, MainWindow, parent=None): self.MainWindow = MainWindow self.current_optical_tagging_par={} self.optical_tagging_par={} - self.finish_tag = 1 + self.thread_finish_tag = 0 + self.cycle_finish_tag = 1 self.threadpool = QThreadPool() def _connectSignalsSlots(self): @@ -3061,18 +3062,18 @@ def _Start(self): self.Start.setStyleSheet("background-color : none") return # generate random conditions including lasers, laser power, laser color, and protocol - if self.finish_tag==1: + if self.cycle_finish_tag==1: # generate new random conditions self._generate_random_conditions() self.index=list(range(len(self.current_optical_tagging_par['protocol_sampled_all']))) - self.finish_tag = 0 + self.cycle_finish_tag = 0 # send the trigger source self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') # start the optical tagging in a different thread worker_tagging = Worker(self._start_optical_tagging) - worker_tagging.signals.finished.connect(self._thread_complete_tagging) + worker_tagging.signals.finished.connect(self._thread_complete_tag) # Execute self.threadpool.start(worker_tagging) @@ -3080,22 +3081,24 @@ def _Start(self): def _emegency_stop(self): '''Stop the optical tagging''' - self.finish_tag = 1 + self.thread_finish_tag = 1 + self.cycle_finish_tag = 1 self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") - - def _thread_complete_tagging(self): + + def _thread_complete_tag(self): '''Complete the optical tagging''' - self.finish_tag = 1 - # Add 1 to the location tag - self.LocationTag.setValue(self.LocationTag.value()+1) + self.thread_finish_tag = 1 + # Add 1 to the location tag when the cycle is finished + if self.cycle_finish_tag == 1: + self.LocationTag.setValue(self.LocationTag.value()+1) def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' # iterate each condition for i in self.index: QApplication.processEvents() - if self.Start.isChecked() and self.finish_tag==0: + if self.Start.isChecked() and self.thread_finish_tag==0: success_tag=0 # exclude the index that has been run self.index.remove(i) @@ -3180,6 +3183,8 @@ def _start_optical_tagging(self): f"Duration: {duration_each_cycle} s\n" f"Interval: {interval_between_cycles} s" ) + if i == self.index[-1]: + self.cycle_finish_tag = 1 else: break From a5709d1cec7d58f90d3f81b8d25db2168082c6a3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 29 Dec 2024 23:03:43 -0800 Subject: [PATCH 062/184] removing QApplication.processEvents() --- src/foraging_gui/Dialogs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 3cf1b8cf1..a38da0175 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3096,8 +3096,7 @@ def _thread_complete_tag(self): def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' # iterate each condition - for i in self.index: - QApplication.processEvents() + for i in self.index[:]: if self.Start.isChecked() and self.thread_finish_tag==0: success_tag=0 # exclude the index that has been run From e50d4b13ac3ee3b2ef3846d6d20b9f20e8636f71 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:03:54 -0800 Subject: [PATCH 063/184] changing the location --- src/foraging_gui/Dialogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a38da0175..f96bbb38a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3072,7 +3072,7 @@ def _Start(self): self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') # start the optical tagging in a different thread - worker_tagging = Worker(self._start_optical_tagging) + worker_tagging = WorkerTagging(self._start_optical_tagging) worker_tagging.signals.finished.connect(self._thread_complete_tag) # Execute @@ -3099,8 +3099,6 @@ def _start_optical_tagging(self): for i in self.index[:]: if self.Start.isChecked() and self.thread_finish_tag==0: success_tag=0 - # exclude the index that has been run - self.index.remove(i) # get the current parameters protocol = self.current_optical_tagging_par['protocol_sampled_all'][i] frequency = self.current_optical_tagging_par['frequency_sampled_all'][i] @@ -3184,6 +3182,8 @@ def _start_optical_tagging(self): ) if i == self.index[-1]: self.cycle_finish_tag = 1 + # exclude the index that has been run + self.index.remove(i) else: break From 5bd36dd1db31b6cd987b1be13c15f47da271e9ae Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:05:01 -0800 Subject: [PATCH 064/184] removing the thread finish tag --- src/foraging_gui/Dialogs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f96bbb38a..7cc6a818f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3044,7 +3044,6 @@ def __init__(self, MainWindow, parent=None): self.MainWindow = MainWindow self.current_optical_tagging_par={} self.optical_tagging_par={} - self.thread_finish_tag = 0 self.cycle_finish_tag = 1 self.threadpool = QThreadPool() @@ -3081,14 +3080,12 @@ def _Start(self): def _emegency_stop(self): '''Stop the optical tagging''' - self.thread_finish_tag = 1 self.cycle_finish_tag = 1 self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") def _thread_complete_tag(self): '''Complete the optical tagging''' - self.thread_finish_tag = 1 # Add 1 to the location tag when the cycle is finished if self.cycle_finish_tag == 1: self.LocationTag.setValue(self.LocationTag.value()+1) @@ -3097,7 +3094,7 @@ def _start_optical_tagging(self): '''Start the optical tagging in a different thread''' # iterate each condition for i in self.index[:]: - if self.Start.isChecked() and self.thread_finish_tag==0: + if self.Start.isChecked(): success_tag=0 # get the current parameters protocol = self.current_optical_tagging_par['protocol_sampled_all'][i] From 661225ac857924b61c54b13319f22b56c35951c3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:05:35 -0800 Subject: [PATCH 065/184] adding one to the tag --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 7cc6a818f..f5740ab44 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3167,7 +3167,7 @@ def _start_optical_tagging(self): time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters self.label_show_current.setText( - f"Cycles: {i}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" + f"Cycles: {i+1}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" f"protocol: {protocol}\n" f"Frequency: {frequency} Hz\n" f"Pulse Duration: {pulse_duration} ms\n" From a1677c57ce5dbb31d89a158dfc6b633b440fd0c0 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:45:28 -0800 Subject: [PATCH 066/184] updating the worker --- src/foraging_gui/MyFunctions.py | 72 +++++++++++++-------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index ce98b746c..e141fb435 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -1985,73 +1985,59 @@ def readLine(self): return data class WorkerSignals(QtCore.QObject): - ''' + """ Defines the signals available from a running worker thread. - Supported signals are: - - finished - No data - - error - tuple (exctype, value, traceback.format_exc() ) - - result - object data returned from processing, anything - - progress - int indicating % progress - - ''' + Signals: + finished: Emitted when the task is complete. + error: Emitted with a tuple (exctype, value, traceback) on failure. + result: Emitted with the result of the processing. + progress: Emitted with an integer indicating % progress (optional use). + """ finished = QtCore.pyqtSignal() error = QtCore.pyqtSignal(tuple) result = QtCore.pyqtSignal(object) progress = QtCore.pyqtSignal(int) - class Worker(QtCore.QRunnable): - ''' - Worker thread - - Inherits from QRunnable to handler worker thread setup, signals and wrap-up. + """ + Worker thread. - :param callback: The function callback to run on this worker thread. Supplied args and - kwargs will be passed through to the runner. - :type callback: function - :param args: Arguments to pass to the callback function - :param kwargs: Keywords to pass to the callback function + Inherits from QRunnable to handle worker thread setup, signals, and wrap-up. - ''' + :param fn: The function to execute on this worker thread. + :param args: Positional arguments to pass to the function. + :param kwargs: Keyword arguments to pass to the function. + """ def __init__(self, fn, *args, **kwargs): - super(Worker, self).__init__() + super().__init__() - # Store constructor arguments (re-used for processing) self.fn = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() - self.setAutoDelete(False) - # Add the callback to our kwargs + + # If progress callback exists, pass it to the worker function + self.kwargs['progress_callback'] = self.signals.progress.emit @QtCore.pyqtSlot() def run(self): - ''' - Initialise the runner function with passed args, kwargs. - ''' - - # Retrieve args/kwargs here; and fire processing using them + """ + Run the worker function and emit signals for result or errors. + """ try: + # Execute the function with provided arguments result = self.fn(*self.args, **self.kwargs) - except ValueError as e: - exctype, value = sys.exc_info()[:2] + except Exception as e: # Catch all exceptions + exctype, value, tb = sys.exc_info() self.signals.error.emit((exctype, value, traceback.format_exc())) - logging.error(str(e)) + logging.error(f"Error in worker thread: {e}") else: - self.signals.result.emit(result) # Return the result of the processing + # Emit the result if the function completes successfully + self.signals.result.emit(result) finally: - self.signals.finished.emit() # Done - - + # Always emit the finished signal + self.signals.finished.emit() class TimerWorker(QtCore.QObject): ''' From a31546c58a3092489ed6a3f7d635cd683548cba8 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 11:47:33 -0800 Subject: [PATCH 067/184] changing the worker name --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f5740ab44..bfccfe7a1 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3071,7 +3071,7 @@ def _Start(self): self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') # start the optical tagging in a different thread - worker_tagging = WorkerTagging(self._start_optical_tagging) + worker_tagging = Worker(self._start_optical_tagging) worker_tagging.signals.finished.connect(self._thread_complete_tag) # Execute From f9bb53dc771aac0394d77675552afa98ef35426b Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:52:00 -0800 Subject: [PATCH 068/184] updating the worker --- src/foraging_gui/MyFunctions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index e141fb435..40daa3212 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -2028,10 +2028,10 @@ def run(self): try: # Execute the function with provided arguments result = self.fn(*self.args, **self.kwargs) - except Exception as e: # Catch all exceptions - exctype, value, tb = sys.exc_info() + except ValueError as e: + exctype, value = sys.exc_info()[:2] self.signals.error.emit((exctype, value, traceback.format_exc())) - logging.error(f"Error in worker thread: {e}") + logging.error(str(e)) else: # Emit the result if the function completes successfully self.signals.result.emit(result) From f5553fed541eae57b7d6875bb2c8c3de1bc88161 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:58:56 -0800 Subject: [PATCH 069/184] removing progress call --- src/foraging_gui/MyFunctions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 40daa3212..bfcb02aee 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -2017,9 +2017,6 @@ def __init__(self, fn, *args, **kwargs): self.kwargs = kwargs self.signals = WorkerSignals() - # If progress callback exists, pass it to the worker function - self.kwargs['progress_callback'] = self.signals.progress.emit - @QtCore.pyqtSlot() def run(self): """ From 54b5c6b2f0d64520a54e985f92889b19b78b4d86 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:04:02 -0800 Subject: [PATCH 070/184] revert --- src/foraging_gui/MyFunctions.py | 61 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index bfcb02aee..ebbe3a5f6 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -1985,56 +1985,71 @@ def readLine(self): return data class WorkerSignals(QtCore.QObject): - """ + ''' Defines the signals available from a running worker thread. - Signals: - finished: Emitted when the task is complete. - error: Emitted with a tuple (exctype, value, traceback) on failure. - result: Emitted with the result of the processing. - progress: Emitted with an integer indicating % progress (optional use). - """ + Supported signals are: + + finished + No data + + error + tuple (exctype, value, traceback.format_exc() ) + + result + object data returned from processing, anything + + progress + int indicating % progress + + ''' finished = QtCore.pyqtSignal() error = QtCore.pyqtSignal(tuple) result = QtCore.pyqtSignal(object) progress = QtCore.pyqtSignal(int) + class Worker(QtCore.QRunnable): - """ - Worker thread. + ''' + Worker thread - Inherits from QRunnable to handle worker thread setup, signals, and wrap-up. + Inherits from QRunnable to handler worker thread setup, signals and wrap-up. - :param fn: The function to execute on this worker thread. - :param args: Positional arguments to pass to the function. - :param kwargs: Keyword arguments to pass to the function. - """ + :param callback: The function callback to run on this worker thread. Supplied args and + kwargs will be passed through to the runner. + :type callback: function + :param args: Arguments to pass to the callback function + :param kwargs: Keywords to pass to the callback function + + ''' def __init__(self, fn, *args, **kwargs): - super().__init__() + super(Worker, self).__init__() + # Store constructor arguments (re-used for processing) self.fn = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() + self.setAutoDelete(False) + # Add the callback to our kwargs @QtCore.pyqtSlot() def run(self): - """ - Run the worker function and emit signals for result or errors. - """ + ''' + Initialise the runner function with passed args, kwargs. + ''' + + # Retrieve args/kwargs here; and fire processing using them try: - # Execute the function with provided arguments result = self.fn(*self.args, **self.kwargs) except ValueError as e: exctype, value = sys.exc_info()[:2] self.signals.error.emit((exctype, value, traceback.format_exc())) logging.error(str(e)) else: - # Emit the result if the function completes successfully - self.signals.result.emit(result) + self.signals.result.emit(result) # Return the result of the processing finally: - # Always emit the finished signal - self.signals.finished.emit() + self.signals.finished.emit() # Done class TimerWorker(QtCore.QObject): ''' From bc2893878e11fe6897ae3ce223d3698eac1bc37c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:08:19 -0800 Subject: [PATCH 071/184] adding WorkerTagging --- src/foraging_gui/MyFunctions.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index ebbe3a5f6..62619f682 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -2051,6 +2051,24 @@ def run(self): finally: self.signals.finished.emit() # Done +class WorkerSignals(QtCore.QObject): + update_label = QtCore.pyqtSignal(str) + finished = QtCore.pyqtSignal() + +class WorkerTagging(QtCore.QRunnable): + def __init__(self, function, *args, **kwargs): + super(WorkerTagging, self).__init__() + self.function = function + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + def run(self): + try: + self.function(*self.args, **self.kwargs, update_label=self.signals.update_label.emit) + finally: + self.signals.finished.emit() + class TimerWorker(QtCore.QObject): ''' Worker for photometry timer From 8fd3b820b65d6fcd6d901c69ce8883a9cc54a2d2 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:10:59 -0800 Subject: [PATCH 072/184] using the worker tagging --- src/foraging_gui/Dialogs.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index bfccfe7a1..77b7cf90a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -21,7 +21,7 @@ from PyQt5.QtCore import QThreadPool,Qt, QAbstractTableModel, QItemSelectionModel, QObject, QTimer from PyQt5.QtSvg import QSvgWidget -from foraging_gui.MyFunctions import Worker +from foraging_gui.MyFunctions import Worker,WorkerTagging from foraging_gui.Visualization import PlotWaterCalibration from aind_auto_train.curriculum_manager import CurriculumManager from aind_auto_train.auto_train_manager import DynamicForagingAutoTrainManager @@ -3071,7 +3071,8 @@ def _Start(self): self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') # start the optical tagging in a different thread - worker_tagging = Worker(self._start_optical_tagging) + worker_tagging = WorkerTagging(self._start_optical_tagging) + worker_tagging.signals.update_label.connect(self.label_show_current.setText) # Connect to label update worker_tagging.signals.finished.connect(self._thread_complete_tag) # Execute @@ -3090,7 +3091,7 @@ def _thread_complete_tag(self): if self.cycle_finish_tag == 1: self.LocationTag.setValue(self.LocationTag.value()+1) - def _start_optical_tagging(self): + def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' # iterate each condition for i in self.index[:]: @@ -3166,17 +3167,18 @@ def _start_optical_tagging(self): # wait to start the next cycle time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters - self.label_show_current.setText( - f"Cycles: {i+1}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" - f"protocol: {protocol}\n" - f"Frequency: {frequency} Hz\n" - f"Pulse Duration: {pulse_duration} ms\n" - f"Laser: {laser_name}\n" - f"Power: {target_power} mW\n" - f"Color: {laser_color}\n" - f"Duration: {duration_each_cycle} s\n" - f"Interval: {interval_between_cycles} s" - ) + # Emit signal to update the label + update_label( + f"Cycles: {i+1}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" + f"protocol: {protocol}\n" + f"Frequency: {frequency} Hz\n" + f"Pulse Duration: {pulse_duration} ms\n" + f"Laser: {laser_name}\n" + f"Power: {target_power} mW\n" + f"Color: {laser_color}\n" + f"Duration: {duration_each_cycle} s\n" + f"Interval: {interval_between_cycles} s" + ) if i == self.index[-1]: self.cycle_finish_tag = 1 # exclude the index that has been run From 1a045c96a39322e5036989a2eb632f3c2fdeba4b Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:18:49 -0800 Subject: [PATCH 073/184] changing the WorkerSignalsTagging --- src/foraging_gui/MyFunctions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 62619f682..b3e8f18b7 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -2051,7 +2051,7 @@ def run(self): finally: self.signals.finished.emit() # Done -class WorkerSignals(QtCore.QObject): +class WorkerSignalsTagging(QtCore.QObject): update_label = QtCore.pyqtSignal(str) finished = QtCore.pyqtSignal() @@ -2061,7 +2061,7 @@ def __init__(self, function, *args, **kwargs): self.function = function self.args = args self.kwargs = kwargs - self.signals = WorkerSignals() + self.signals = WorkerSignalsTagging() def run(self): try: From efc1db543cb246eae4493de72c944fcb83819fb6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:23:21 -0800 Subject: [PATCH 074/184] toggle the Start button when finishing --- src/foraging_gui/Dialogs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 77b7cf90a..bcb475f05 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3090,7 +3090,9 @@ def _thread_complete_tag(self): # Add 1 to the location tag when the cycle is finished if self.cycle_finish_tag == 1: self.LocationTag.setValue(self.LocationTag.value()+1) - + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' # iterate each condition From 3b97684e2feafb4c60fee6ce55568a5fb2b05a0d Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:24:20 -0800 Subject: [PATCH 075/184] saving the laser_start_timestamp and success_tag --- src/foraging_gui/Dialogs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index bcb475f05..cf01e6f94 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3092,7 +3092,7 @@ def _thread_complete_tag(self): self.LocationTag.setValue(self.LocationTag.value()+1) self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") - + def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' # iterate each condition @@ -3200,6 +3200,8 @@ def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_pow self.optical_tagging_par['duration_each_cycle']=[] self.optical_tagging_par['interval_between_cycles']=[] self.optical_tagging_par['location_tag']=[] + self.optical_tagging_par['laser_start_timestamp']=[] + self.optical_tagging_par['success_tag']=[] else: self.optical_tagging_par['protocol'].append(protocol) self.optical_tagging_par['frequency'].append(frequency) @@ -3210,6 +3212,8 @@ def _save_data(self, protocol, frequency, pulse_duration, laser_name, target_pow self.optical_tagging_par['duration_each_cycle'].append(duration_each_cycle) self.optical_tagging_par['interval_between_cycles'].append(interval_between_cycles) self.optical_tagging_par['location_tag'].append(location_tag) + self.optical_tagging_par['laser_start_timestamp'].append(laser_start_timestamp) + self.optical_tagging_par['success_tag'].append(success_tag) def _initiate_laser(self): '''Initiate laser in bonsai''' From 997c6f1313563ee06263069178d0a6538c6d0f84 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:26:28 -0800 Subject: [PATCH 076/184] changing the font size --- src/foraging_gui/OpticalTagging.ui | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index a559107b3..a39aeb70a 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -682,9 +682,9 @@ 270 -30 +80 191 -321 +331 @@ -693,6 +693,13 @@ +16777215 + + 15 +75 +true + +@@ -700,7 +707,7 @@ Qt::AutoText - Qt::AlignCenter +Qt::AlignHCenter|Qt::AlignTop false From bcea79b5798fd170d5be199f8f0640ef3ea9b488 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:31:01 -0800 Subject: [PATCH 077/184] adjusting the font size --- src/foraging_gui/OpticalTagging.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index a39aeb70a..ddfeeab72 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -681,9 +681,9 @@@@ -695,7 +695,7 @@ - 270 +250 80 -191 +241 331 - 15 +13 75 true From 3ac5451c5a2e8e301f7f75692efbf70b36f8e7d3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:33:02 -0800 Subject: [PATCH 078/184] changing the show order --- src/foraging_gui/Dialogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index cf01e6f94..b4bc5fe2b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3172,12 +3172,12 @@ def _start_optical_tagging(self,update_label): # Emit signal to update the label update_label( f"Cycles: {i+1}/{len(self.current_optical_tagging_par['protocol_sampled_all'])} \n" + f"Color: {laser_color}\n" + f"Laser: {laser_name}\n" + f"Power: {target_power} mW\n" f"protocol: {protocol}\n" f"Frequency: {frequency} Hz\n" f"Pulse Duration: {pulse_duration} ms\n" - f"Laser: {laser_name}\n" - f"Power: {target_power} mW\n" - f"Color: {laser_color}\n" f"Duration: {duration_each_cycle} s\n" f"Interval: {interval_between_cycles} s" ) From 8d785db6a17169873b2848a5ae84fd1d6d1ab0a9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:34:51 -0800 Subject: [PATCH 079/184] adding the save button --- src/foraging_gui/OpticalTagging.ui | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index ddfeeab72..520b31a0d 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -717,7 +717,7 @@@@ -726,6 +726,22 @@ 130 -450 +490 101 31 Emergency Stop + + ++ +160 +450 +70 +31 ++ +Save ++ +true +From eefb2b2452b2c35a17b5f69e4f0c66e1bcf3c3cb Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:35:43 -0800 Subject: [PATCH 080/184] disable checkable --- src/foraging_gui/OpticalTagging.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 520b31a0d..bef089379 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -739,7 +739,7 @@ Save - true +false Date: Mon, 30 Dec 2024 12:56:30 -0800 Subject: [PATCH 085/184] set button not default --- src/foraging_gui/Dialogs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 77e81bff3..0e7e7183f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3046,7 +3046,12 @@ def __init__(self, MainWindow, parent=None): self.optical_tagging_par={} self.cycle_finish_tag = 1 self.threadpool = QThreadPool() - + # find all buttons and set them to not be the default button + for container in [self]: + for child in container.findChildren((QtWidgets.QPushButton)): + child.setDefault(False) + child.setAutoDefault(False) + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) From 04ccdaab32e9b8d6b3f2af646ec36900ba7682e6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:58:41 -0800 Subject: [PATCH 086/184] ID typo --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 0e7e7183f..7ce9dc951 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3067,7 +3067,7 @@ def _Save(self): if save_folder=='': return # create the file name AnimalID_Date(day+hour+minute)_OpticalTaggingResults.csv - save_file = os.path.join(save_folder, f"{self.optical_tagging_par['AnimalID']}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") + save_file = os.path.join(save_folder, f"{self.MainWindow.ID}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") # save the data with open(save_file, 'w') as f: json.dump(self.optical_tagging_par, f, indent=4) From 1584c5da5e1129e033ed15dc37f75a8aaa74e52d Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:04:01 -0800 Subject: [PATCH 087/184] correcting syntax error --- src/foraging_gui/Dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 7ce9dc951..8711ac044 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3067,7 +3067,7 @@ def _Save(self): if save_folder=='': return # create the file name AnimalID_Date(day+hour+minute)_OpticalTaggingResults.csv - save_file = os.path.join(save_folder, f"{self.MainWindow.ID}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") + save_file = os.path.join(save_folder, f"{self.MainWindow.ID.text()}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") # save the data with open(save_file, 'w') as f: json.dump(self.optical_tagging_par, f, indent=4) From 1e49bb6650fdfcce436d923469bb470ea06292e7 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:20:00 -0800 Subject: [PATCH 088/184] adding the start and end time --- src/foraging_gui/Dialogs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 8711ac044..5470ab60b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3095,6 +3095,9 @@ def _Start(self): worker_tagging.signals.update_label.connect(self.label_show_current.setText) # Connect to label update worker_tagging.signals.finished.connect(self._thread_complete_tag) + # get the first start time + if "optical_tagging_start_time" not in self.optical_tagging_par: + self.optical_tagging_par["optical_tagging_start_time"] = str(datetime.now()) # Execute self.threadpool.start(worker_tagging) #self._start_optical_tagging() @@ -3112,6 +3115,9 @@ def _thread_complete_tag(self): self.LocationTag.setValue(self.LocationTag.value()+1) self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") + # update the stop time + self.optical_tagging_par["optical_tagging_end_time"] = str(datetime.now()) + def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' From d4a7c90d5e6a1c8f27e8efd74f8a33efd7a6ecea Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:20:20 -0800 Subject: [PATCH 089/184] toggle the start button --- src/foraging_gui/Dialogs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 5470ab60b..9686c1861 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3117,7 +3117,9 @@ def _thread_complete_tag(self): self.Start.setStyleSheet("background-color : none") # update the stop time self.optical_tagging_par["optical_tagging_end_time"] = str(datetime.now()) - + # toggle the start button in the main window + self.MainWindow.unsaved_data=True + self.MainWindow.Save.setStyleSheet("color: white;background-color : mediumorchid;") def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' From c93233d734f15b72f6468cda6e498e7813899f1a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:57:38 -0800 Subject: [PATCH 090/184] adding the optical tagging to the metadata --- src/foraging_gui/GenerateMetadata.py | 76 +++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index a8d46b6f6..f70d84bf9 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -481,6 +481,16 @@ def _handle_edge_cases(self): else: self.Obj['settings_box']['AINDLickDetector']=int(self.Obj['settings_box']['AINDLickDetector']) + # Handle the edge cases for the optical tagging + if 'OpticalTagging_dialog' not in self.Obj: + self.Obj['OpticalTagging_dialog'] = {} + if 'optical_tagging_par' not in self.Obj['OpticalTagging_dialog']: + self.Obj['OpticalTagging_dialog']['optical_tagging_par'] = {} + if 'optical_tagging_start_time' not in self.Obj['OpticalTagging_dialog']['optical_tagging_par']: + self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time'] = '' + if 'optical_tagging_end_time' not in self.Obj['OpticalTagging_dialog']['optical_tagging_par']: + self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time'] = '' + def _initialize_fields(self,dic,keys,default_value=''): ''' Initialize fields @@ -745,8 +755,46 @@ def _get_stimulus(self): self.stimulus=[] self._get_behavior_stimulus() self._get_optogenetics_stimulus() + self._get_optical_tagging_stimulus() self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus + def _get_optical_tagging_stimulus(self): + ''' + Make the optical tagging stimulus metadata + ''' + self.optical_tagging_stimulus=[] + if self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time']=='': + logging.info('No optical tagging data stream detected!') + return + self._get_optical_tagging_light_source_config() + self.optical_tagging_stimulus.append(StimulusEpoch( + software=self.behavior_software, + stimulus_device_names=self.light_names_used_in_optical_tagging, + stimulus_name='The optical tagging stimulus', + stimulus_modalities=[StimulusModality.OPTOGENETICS], + stimulus_start_time=self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time'], + stimulus_end_time=self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time'], + light_source_config=self.optical_tagging_light_source_config, + output_parameters=self._get_optical_tagging_output_parameters(), + )) + + def _get_optical_tagging_output_parameters(self): + '''Get the output parameters for optical tagging''' + output_parameters = { + 'Laser':self.Obj['OpticalTagging_dialog'].WhichLaser.currentText(), + 'Protocol':self.Obj['OpticalTagging_dialog'].Protocol.currentText(), + 'Cycles_each_condition':self.Obj['OpticalTagging_dialog'].Cycles_each_condition.text(), + 'Frequency':self.Obj['OpticalTagging_dialog'].Frequency.text(), + 'Pulse_duration':self.Obj['OpticalTagging_dialog'].Pulse_duration.text(), + 'Laser_1_color':self.Obj['OpticalTagging_dialog'].Laser_1_color.currentText(), + 'Laser_2_color':self.Obj['OpticalTagging_dialog'].Laser_2_color.currentText(), + 'Laser_1_power':self.Obj['OpticalTagging_dialog'].Laser_1_power.text(), + 'Laser_2_power':self.Obj['OpticalTagging_dialog'].Laser_2_power.text(), + 'Duration_each_cycle':self.Obj['OpticalTagging_dialog'].Duration_each_cycle.text(), + 'Interval_between_cycles':self.Obj['OpticalTagging_dialog'].Interval_between_cycles.text(), + } + return output_parameters + def _get_behavior_stimulus(self): ''' Make the audio stimulus metadata @@ -916,6 +964,18 @@ def _get_light_source_config(self): self.light_source_config.append(LightEmittingDiodeConfig( name=light_source, )) + def _get_optical_tagging_light_source_config(self): + ''' + get the optical tagging light source config + ''' + self.optical_tagging_light_source_config=[] + self._get_light_names_used_in_optical_tagging() + for light_source in self.light_names_used_in_optical_tagging: + wavelength=self._get_light_pars(light_source) + self.optical_tagging_light_source_config.append(LaserConfig( + name=light_source, + wavelength=wavelength, + )) def _get_light_pars(self,light_source): ''' @@ -925,7 +985,21 @@ def _get_light_pars(self,light_source): if current_stimulus_device['name']==light_source: return current_stimulus_device['wavelength'] - + def _get_light_names_used_in_optical_tagging(self): + ''' + Get the optogenetics laser names used in the optical tagging + ''' + self.light_names_used_in_optical_tagging=[] + light_sources=[] + for i in range(len(self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_color'])): + if self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_name'][i]=="Laser_1": + laser_tag=1 + elif self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_name'][i]=="Laser_2": + laser_tag=2 + light_sources.append({'color':self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_color'][i],'laser_tag':laser_tag}) + for light_source in light_sources: + self.light_names_used_in_optical_tagging.append([key for key, value in self.name_mapper['laser_name_mapper'].items() if value == light_source][0]) + def _get_light_names_used_in_session(self): ''' Get the optogenetics laser names used in the session From d8ef04915cbbd39d307fc2768ee0276ed7613413 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 14:15:00 -0800 Subject: [PATCH 091/184] updating the optical tagging metadata --- src/foraging_gui/GenerateMetadata.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index f70d84bf9..c411e10c2 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -484,12 +484,10 @@ def _handle_edge_cases(self): # Handle the edge cases for the optical tagging if 'OpticalTagging_dialog' not in self.Obj: self.Obj['OpticalTagging_dialog'] = {} - if 'optical_tagging_par' not in self.Obj['OpticalTagging_dialog']: - self.Obj['OpticalTagging_dialog']['optical_tagging_par'] = {} - if 'optical_tagging_start_time' not in self.Obj['OpticalTagging_dialog']['optical_tagging_par']: - self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time'] = '' - if 'optical_tagging_end_time' not in self.Obj['OpticalTagging_dialog']['optical_tagging_par']: - self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time'] = '' + if 'optical_tagging_start_time' not in self.Obj['optical_tagging_par']: + self.Obj['optical_tagging_par']['optical_tagging_start_time'] = '' + if 'optical_tagging_end_time' not in self.Obj['optical_tagging_par']: + self.Obj['optical_tagging_par']['optical_tagging_end_time'] = '' def _initialize_fields(self,dic,keys,default_value=''): ''' @@ -763,8 +761,8 @@ def _get_optical_tagging_stimulus(self): Make the optical tagging stimulus metadata ''' self.optical_tagging_stimulus=[] - if self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time']=='': - logging.info('No optical tagging data stream detected!') + if self.Obj['OpticalTagging_dialog']=={} or self.Obj['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['optical_tagging_par']['optical_tagging_end_time']=='': + logging.info('No optical tagging detected!') return self._get_optical_tagging_light_source_config() self.optical_tagging_stimulus.append(StimulusEpoch( @@ -772,8 +770,8 @@ def _get_optical_tagging_stimulus(self): stimulus_device_names=self.light_names_used_in_optical_tagging, stimulus_name='The optical tagging stimulus', stimulus_modalities=[StimulusModality.OPTOGENETICS], - stimulus_start_time=self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_start_time'], - stimulus_end_time=self.Obj['OpticalTagging_dialog']['optical_tagging_par']['optical_tagging_end_time'], + stimulus_start_time=self.Obj['optical_tagging_par']['optical_tagging_start_time'], + stimulus_end_time=self.Obj['optical_tagging_par']['optical_tagging_end_time'], light_source_config=self.optical_tagging_light_source_config, output_parameters=self._get_optical_tagging_output_parameters(), )) @@ -991,12 +989,12 @@ def _get_light_names_used_in_optical_tagging(self): ''' self.light_names_used_in_optical_tagging=[] light_sources=[] - for i in range(len(self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_color'])): - if self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_name'][i]=="Laser_1": + for i in range(len(self.Obj['optical_tagging_par']['laser_color'])): + if self.Obj['optical_tagging_par']['laser_name'][i]=="Laser_1": laser_tag=1 - elif self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_name'][i]=="Laser_2": + elif self.Obj['optical_tagging_par']['laser_name'][i]=="Laser_2": laser_tag=2 - light_sources.append({'color':self.Obj['OpticalTagging_dialog']['optical_tagging_par']['laser_color'][i],'laser_tag':laser_tag}) + light_sources.append({'color':self.Obj['optical_tagging_par']['laser_color'][i],'laser_tag':laser_tag}) for light_source in light_sources: self.light_names_used_in_optical_tagging.append([key for key, value in self.name_mapper['laser_name_mapper'].items() if value == light_source][0]) From a00681d8ca4653500f83cd2fd915c24f6c8d2aa6 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 14:26:02 -0800 Subject: [PATCH 092/184] check empty --- src/foraging_gui/Dialogs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 9686c1861..f0f7713db 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3098,6 +3098,9 @@ def _Start(self): # get the first start time if "optical_tagging_start_time" not in self.optical_tagging_par: self.optical_tagging_par["optical_tagging_start_time"] = str(datetime.now()) + if self.optical_tagging_par["optical_tagging_start_time"]=='': + self.optical_tagging_par["optical_tagging_start_time"] = str(datetime.now()) + # Execute self.threadpool.start(worker_tagging) #self._start_optical_tagging() From ebd969c68e490aba4d98f618ef2c0b33c55c6435 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 14:33:52 -0800 Subject: [PATCH 093/184] changing the outputparameters --- src/foraging_gui/GenerateMetadata.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index c411e10c2..8ed1d6df7 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -754,7 +754,7 @@ def _get_stimulus(self): self._get_behavior_stimulus() self._get_optogenetics_stimulus() self._get_optical_tagging_stimulus() - self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus + self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus+self.optical_tagging_stimulus def _get_optical_tagging_stimulus(self): ''' @@ -779,17 +779,17 @@ def _get_optical_tagging_stimulus(self): def _get_optical_tagging_output_parameters(self): '''Get the output parameters for optical tagging''' output_parameters = { - 'Laser':self.Obj['OpticalTagging_dialog'].WhichLaser.currentText(), - 'Protocol':self.Obj['OpticalTagging_dialog'].Protocol.currentText(), - 'Cycles_each_condition':self.Obj['OpticalTagging_dialog'].Cycles_each_condition.text(), - 'Frequency':self.Obj['OpticalTagging_dialog'].Frequency.text(), - 'Pulse_duration':self.Obj['OpticalTagging_dialog'].Pulse_duration.text(), - 'Laser_1_color':self.Obj['OpticalTagging_dialog'].Laser_1_color.currentText(), - 'Laser_2_color':self.Obj['OpticalTagging_dialog'].Laser_2_color.currentText(), - 'Laser_1_power':self.Obj['OpticalTagging_dialog'].Laser_1_power.text(), - 'Laser_2_power':self.Obj['OpticalTagging_dialog'].Laser_2_power.text(), - 'Duration_each_cycle':self.Obj['OpticalTagging_dialog'].Duration_each_cycle.text(), - 'Interval_between_cycles':self.Obj['OpticalTagging_dialog'].Interval_between_cycles.text(), + 'Laser':self.Obj['OpticalTagging_dialog']['WhichLaser'], + 'Protocol':self.Obj['OpticalTagging_dialog']['Protocol'], + 'Cycles_each_condition':self.Obj['OpticalTagging_dialog']['Cycles_each_condition'], + 'Frequency':self.Obj['OpticalTagging_dialog']['Frequency'], + 'Pulse_duration':self.Obj['OpticalTagging_dialog']['Pulse_duration'], + 'Laser_1_color':self.Obj['OpticalTagging_dialog']['Laser_1_color'], + 'Laser_2_color':self.Obj['OpticalTagging_dialog']['Laser_2_color'], + 'Laser_1_power':self.Obj['OpticalTagging_dialog']['Laser_1_power'], + 'Laser_2_power':self.Obj['OpticalTagging_dialog']['Laser_2_power'], + 'Duration_each_cycle':self.Obj['OpticalTagging_dialog']['Duration_each_cycle'], + 'Interval_between_cycles':self.Obj['OpticalTagging_dialog']['Interval_between_cycles'], } return output_parameters From 420477164306b2eceb02ef3cb6617e553ed21067 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 14:36:04 -0800 Subject: [PATCH 094/184] check empty optical_tagging_par --- src/foraging_gui/GenerateMetadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 8ed1d6df7..149a234b0 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -484,6 +484,8 @@ def _handle_edge_cases(self): # Handle the edge cases for the optical tagging if 'OpticalTagging_dialog' not in self.Obj: self.Obj['OpticalTagging_dialog'] = {} + if 'optical_tagging_par' not in self.Obj: + self.Obj['optical_tagging_par'] = {} if 'optical_tagging_start_time' not in self.Obj['optical_tagging_par']: self.Obj['optical_tagging_par']['optical_tagging_start_time'] = '' if 'optical_tagging_end_time' not in self.Obj['optical_tagging_par']: From 6fb44b6fcab29568d586572d9dec69be2ff155d4 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Mon, 30 Dec 2024 14:40:17 -0800 Subject: [PATCH 095/184] deduplicate --- src/foraging_gui/GenerateMetadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 149a234b0..a0b86b63f 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -508,7 +508,6 @@ def _initialize_fields(self,dic,keys,default_value=''): if key not in dic: dic[key] = default_value - def _session(self): ''' Create metadata related to Session class in the aind_data_schema @@ -999,7 +998,8 @@ def _get_light_names_used_in_optical_tagging(self): light_sources.append({'color':self.Obj['optical_tagging_par']['laser_color'][i],'laser_tag':laser_tag}) for light_source in light_sources: self.light_names_used_in_optical_tagging.append([key for key, value in self.name_mapper['laser_name_mapper'].items() if value == light_source][0]) - + self.light_names_used_in_optical_tagging = list(set(self.light_names_used_in_optical_tagging)) + def _get_light_names_used_in_session(self): ''' Get the optogenetics laser names used in the session From 186a85dc4a82d4c1e9f32559bfb56065a981f51a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:52:34 -0800 Subject: [PATCH 096/184] updating the behavior GUI --- src/foraging_gui/ForagingGUI.ui | 57 +++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/foraging_gui/ForagingGUI.ui b/src/foraging_gui/ForagingGUI.ui index ccf1dc1bb..2a2db3286 100644 --- a/src/foraging_gui/ForagingGUI.ui +++ b/src/foraging_gui/ForagingGUI.ui @@ -79,7 +79,7 @@ 0 0 894 -236 +226 @@ -691,8 +691,8 @@ 0 0 -627 -325 +624 +334 @@ -1194,8 +1194,7 @@ - -
- +- -
- +@@ -1628,8 +1626,8 @@ 0 0 -610 -339 +655 +365 @@ -1731,7 +1729,7 @@ - 0.030000000000000 + true - 0.030000000000000 - true ++ true - 3.000000000000000 + @@ -1841,9 +1839,9 @@ true - 3.000000000000000 - +true -+ true +- @@ -2272,8 +2270,8 @@ Bias:
0 0 -330 -209 +349 +189 @@ -2344,8 +2342,8 @@ Double dipping: 0 0 -1072 -463 +1540 +504 @@ -4447,8 +4445,8 @@ Current pair: 0 0 -627 -325 +714 +286 @@ -4805,7 +4803,7 @@ Current pair: 0 0 960 -21 +31 @@ -4856,6 +4854,7 @@ Current pair: + @@ -4982,6 +4981,7 @@ Current pair: + @@ -5246,6 +5246,15 @@ Current pair: Open video folder + + ++ +resources/OpticalTagging.jpg resources/OpticalTagging.jpg+ +Optical Tagging +Task @@ -5285,4 +5294,4 @@ Current pair: - \ No newline at end of file + From f278aa2530c8e80f5fa0e5e061d7b59ea8570278 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:33:47 -0800 Subject: [PATCH 097/184] adding random reward ui --- src/foraging_gui/RandomReward.ui | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/foraging_gui/RandomReward.ui diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui new file mode 100644 index 000000000..a78716439 --- /dev/null +++ b/src/foraging_gui/RandomReward.ui @@ -0,0 +1,19 @@ + ++ From 6f73ef75f18c4b22253f6625737f66b335940de6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:34:26 -0800 Subject: [PATCH 098/184] adding random reward to the toolbar --- src/foraging_gui/ForagingGUI_Ephys.ui | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index 5ba27694f..2e4c97c5a 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -71,8 +71,8 @@Dialog ++ ++ ++ +0 +0 +1105 +393 ++ +Random Reward ++ + 620 20 -222 -27 +152 +20