From 0c984e119b193a1f4fb6b473e964d21c1c5cc2d6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Sun, 22 Dec 2024 20:34:23 -0800 Subject: [PATCH 001/184] adding the optical tagging icon --- src/foraging_gui/ForagingGUI_Ephys.ui | 42 +++++++++++------- src/foraging_gui/resources/OpticalTagging.jpg | Bin 0 -> 58880 bytes 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/foraging_gui/resources/OpticalTagging.jpg diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index ce38a260a..d2e3e3371 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -71,8 +71,8 @@ 620 20 - 152 - 20 + 222 + 27 @@ -1463,9 +1463,9 @@ 0.030000000000000 - - true - + + true + @@ -1491,9 +1491,9 @@ 0.030000000000000 - - true - + + true + @@ -1884,9 +1884,9 @@ 3.000000000000000 - - true - + + true + @@ -1912,9 +1912,9 @@ 3.000000000000000 - - true - + + true + @@ -2176,7 +2176,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 863 @@ -4569,7 +4569,7 @@ 0 0 1951 - 21 + 31 @@ -4765,6 +4765,7 @@ + @@ -5009,6 +5010,15 @@ Open video folder + + + + resources/pngtree-letter-t-logo-png-png-image_6100844.jpgresources/pngtree-letter-t-logo-png-png-image_6100844.jpg + + + Optical Tagging + + NextBlock diff --git a/src/foraging_gui/resources/OpticalTagging.jpg b/src/foraging_gui/resources/OpticalTagging.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e73ca6116caa0feb05e350c17802ebd03d86429d GIT binary patch literal 58880 zcmdpf2|U#K|NqBPBgxT4D@L)aYg5~9S~5ebNy?E*tumo=J5=imGow_L))bY>GKmls z=^z@TDAM7+k0A;(7{{0ybN)Z$7&FW`s@>oB@o&H1rnx@z{=D9=c~LFczyU8XM*bPRL9~qpYOZ%(Fk$5 z+c9ypUlo4OE#*p=$CAFaob0=YX`ckQGU9GAKO_#`=7{%%NC3X41P@FM&g-Yu*68??pz~;a-M+!U6r|Be5kTqk0PpM}RTu5m&-;D)PfO_Q+%H0mgF$+$clmuLXfJO} zJKy2MzZ%;wB4ai3_>ootC!EZ<`eupXncxQb3m4vSd?%Ee!uAWy)$2yyy3V%ZUkqH@ zTwl6w1yn;|m0#EfRdkjK479C^kny{FYRC+xqr4lBb$1ei*-DK@P@Rv){Odt4Q%^$I zz-xXVv~54iCn0VQKGA-{SEB-LkNIopSx-||o?g@HqIA1h>@NP5`Yz>xCZqEQzj4TZ zqtk35)xuZxLpuI^=g>3E9Jp<7i=$yL?dxoBQQKOC>@+uew$__&!XeCNU(tC7k~kg>Cx@&_vR>s1;RR3=a4g54gz# zcAO!%QO*l=S?S?n_De)t2nkf%{@305?D?AThnVIi{aCJzt<+--N#@8$_9HONt@Kr= zWHxdEOzDJa}>mc!5Yw+((+phNvfVt(@vw}R~KB6z@$0{4p$)&Zb6RPulz z{)*jwoyWsoRPDy+OXU&5!vyM}--B9QT{4_!K%5gX z?AsFjqH2arq7=6I??HYU-6@l(ZhrV6uyY_4G@H6PjesV+`N|-Z2uLHaeSh1dI4EZ#D6Jnxujaa<5~WBeCMa#T zL9k8R2ZT<-gFybPKkpn=3ZlxdPeow-dMoGv!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 zRnrYOc1ewtY2b+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>~`J+Rdy$Y9nW2)XmQ z<@r#KVz`u_f4q~ zI@NQ5)kqJxUiZqe05b!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~Fpt7FyO@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`W6Rehq4ASd6sHe2n* 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#h4n5O+__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?cg`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 zNa+) zM|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?{k7B(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^+lX0F9C4#a9K84H!4wvx&Zh7w*nMx^Dhc%P-LbV(}%~4jD8h2t;BrGQ4t&0yL2YOmO z{3RNOXRY#@O4dt5sYeI7)f{8cITuemKXK;)Rlww&tow?YgvI()i-rcw(;L)>&2ILQ zMaN=t@AXhc4QP zJ7bdL13;-~esPa8K&^r70C9>R514G&>QHdy<_=|(l$Vgae98eOG-@|3A<}gVx-6y^ zV?l9aAsk_qCXGH%hF2>98nQv745Q$&fzv7l9%fK$E!rLbpK%b9)T#fz0*cS-GWs;>Ewb>MJG^1H?N>NjZ9 zvYpBJCg)0hW8gguVi~u&_-H2=dU1+QkeNOp;yz$ma+EI#T+I8V8u_xTM<7=o)tuqGlCZHf5zQWCb zaP+9om4|w?Nw_B*e6n#6mz#qu8oO|DPD7?K~;rzfVCnC`3DX2q6U*lrs4_>-$)ll7#*ubo1D;mM9NHp zMC5Drty&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<(vgcKWXzjd3p#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)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@=3YnJA<&T{xh?hTre zpKD=osgX?sq35Klp5493$n14Li_|ZlqG0plJaG&r)|D?V`<%B>8!{Qqdu$ri z5+7Mi=K%!#(>52`1n`HvfXB75TYFwlL zBfa&ls*l?2xCON}IrXzgIyy>N+!dVv_o~?bv)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<`sD9aXd0NT6|N*V*bp)4tbtGTt3JvZZ2Z|F8Z5lk*ly}@{NNaNW7Hsv z&3Kw1T9g8BG&E*H&k2P)4R$LMHm7R5(zA&uYe_bb`}=rKUA**4t!%ted>Ks_Z3JHa-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>?2TZwm$JUA?jSSKPjCFtiX^g6rQzTq?QyQGszl9iRp%;?d13Gx# z2+w$qUc5ux2<4zb8Xj5V6=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_JBdk0S{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~N4nnpP^AflKDxMVvpbj`aVL@zQe%w9X{9!LGj%icCmXuF)SlxpH~ zTOtgM17fpg8o%q%4@^pUGkMzhfS!;(_{!GJ3+szTiVfvV7c@*>dLs*jf?Lb$KuWS(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{H7Se;PPA(3Fa&P1pJRV|pV3 zGdq_0ebFO&v1lmg_sd%{^rM)dl(iZnYvlS8DlW`FjT8N*Vo(1!s}*DB)<*t0P$A-1 zk5Im^;HnImWj9P*kovP0S<-TTPjT6x9~ z7C1GO`G8&Io-eD4SlF5yVC_*xDl`(A4%YIV25aco7qlFUDZuNn1DFR6v99$cO)gxW z3uY`vjhl@X!ycZkmZ&ro09C@U0fCaUC33T$t4<)``)wolF5X>teu+~_3F40D>vQ-O zFF5ZI4gXtDWQTz@!WR7MIX$785>bvJ zg=fRE%6tn$XtV~U8{G#-F$-kseXZUlU2BdjrGBAh^7{hfx|PN)NQct7P}X6Ks)v7zQR z_7Ka5jiK3|VqM2tuw|ZcNV@#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}H4E8{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`pSud&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 z5ER z)Wa+os}?<_j57xt-KJlJJAkRDZ02fsFIs4Y)*J6opKq3++b-!d#g57rN6Y#9dv%R20_U-Vj8lv4~nJL*yB=ADQl%l+-OHB>@<(JSb= zx#EiK#ux5unQk@r5B0hrp>ftBo=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|@}4vjT3*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~VMGKhMMnhfiL0~{=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@FDHC7x 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^_{1E3e}I7yFcAh!_TF-8nZ><~T5ZU( zDJ1xb8(<(wnr>sSj&kwkc}=+2d!nuTuVu z{t0qpKN2hioErX)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^)m4RfGcU{*O z2mQg0QIQ7CdGl=jx5)LwbS>OzMKg%#v<;DT-hkO3+5LRoW5RxJln7usf;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 zj^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>KNQFOvkx}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+*EIciVOA6XHgb2pe1DAFTkBXLM!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`;^bMk>j5+{g)EY-zz021n#f3lLO(?e;n7a;8k#=%J%9&M>Dj7 zdXnr+6DI}(y8=Ar$L@#Cm4k;NG!}mBXRsFL1PPA}oq$ISZ1#oJEAOTz66V{`?IA&& z3xRp{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>mC#^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.jpgresources/pngtree-letter-t-logo-png-png-image_6100844.jpg + resources/OpticalTagging.jpgresources/OpticalTagging.jpg Optical 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 @@ + + + OpticalTagging + + + + 0 + 0 + 703 + 474 + + + + Optical Tagging + + + + + 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 @@ + + true + resources/OpticalTagging.jpgresources/OpticalTagging.jpg 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 @@ 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 @@ -390,7 +390,7 @@ 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 @@ 60 - 280 + 400 76 20 @@ -438,12 +435,171 @@ 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 @@ 60 - 100 + 70 76 20 @@ -42,7 +42,7 @@ 60 - 70 + 130 76 20 @@ -54,7 +54,7 @@ - laser color= + laser_1 color= Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -64,7 +64,7 @@ 140 - 130 + 100 70 20 @@ -94,7 +94,7 @@ 58 - 250 + 280 76 20 @@ -119,7 +119,7 @@ 58 - 130 + 100 76 20 @@ -144,7 +144,7 @@ 20 - 190 + 220 116 20 @@ -163,7 +163,7 @@ 54 - 220 + 250 80 20 @@ -184,11 +184,11 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 140 - 70 + 130 70 20 @@ -238,7 +238,7 @@ 140 - 100 + 70 70 20 @@ -278,7 +278,7 @@ 140 - 250 + 280 70 20 @@ -300,7 +300,7 @@ 20 - 160 + 190 116 20 @@ -322,7 +322,7 @@ 140 - 220 + 250 70 20 @@ -347,7 +347,7 @@ 140 - 160 + 190 70 20 @@ -372,7 +372,7 @@ 140 - 190 + 220 70 20 @@ -410,7 +410,7 @@ 60 - 400 + 430 76 20 @@ -435,7 +435,7 @@ 140 - 400 + 430 71 22 @@ -448,7 +448,7 @@ 15 - 300 + 330 121 20 @@ -476,7 +476,7 @@ 140 - 300 + 330 70 20 @@ -501,7 +501,7 @@ 15 - 330 + 360 121 20 @@ -529,7 +529,7 @@ 140 - 330 + 360 70 20 @@ -554,7 +554,7 @@ 140 - 360 + 390 70 20 @@ -579,7 +579,7 @@ 5 - 360 + 390 131 20 @@ -600,6 +600,78 @@ 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 @@ 30 - 500 + 520 191 20 @@ -700,6 +700,19 @@ 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 - + true 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 @@ 0 0 - 270 + 286 575
@@ -16,8 +16,8 @@ - 60 - 70 + 81 + 60 76 20 @@ -41,8 +41,8 @@ - 60 - 130 + 81 + 120 76 20 @@ -63,8 +63,8 @@ - 140 - 100 + 161 + 90 70 20 @@ -93,8 +93,8 @@ - 58 - 280 + 79 + 270 76 20 @@ -118,8 +118,8 @@ - 58 - 100 + 79 + 90 76 20 @@ -143,8 +143,8 @@ - 20 - 220 + 41 + 210 116 20 @@ -162,8 +162,8 @@ - 54 - 250 + 75 + 240 80 20 @@ -187,8 +187,8 @@ - 140 - 130 + 161 + 120 70 20 @@ -237,8 +237,8 @@ - 140 - 70 + 161 + 60 70 20 @@ -277,8 +277,8 @@ - 140 - 280 + 161 + 270 70 20 @@ -299,8 +299,8 @@ - 20 - 190 + 41 + 180 116 20 @@ -321,8 +321,8 @@ - 140 - 250 + 161 + 240 70 20 @@ -346,8 +346,8 @@ - 140 - 190 + 161 + 180 70 20 @@ -371,8 +371,8 @@ - 140 - 220 + 161 + 210 70 20 @@ -393,8 +393,8 @@ - 140 - 32 + 161 + 22 70 31 @@ -412,8 +412,8 @@ - 60 - 430 + 81 + 420 76 20 @@ -437,8 +437,8 @@ - 140 - 430 + 161 + 420 71 22 @@ -450,9 +450,9 @@ - 15 - 330 - 121 + 16 + 320 + 141 20 @@ -478,8 +478,8 @@ - 140 - 330 + 161 + 320 70 20 @@ -503,9 +503,9 @@ - 15 - 360 - 121 + 16 + 350 + 141 20 @@ -531,8 +531,8 @@ - 140 - 360 + 161 + 350 70 20 @@ -556,8 +556,8 @@ - 140 - 390 + 161 + 380 70 20 @@ -581,8 +581,8 @@ - 5 - 390 + 26 + 380 131 20 @@ -594,7 +594,7 @@ - cycles each laser power= + cycles each condition= Qt::AutoText @@ -606,8 +606,8 @@ - 140 - 160 + 161 + 150 70 20 @@ -656,8 +656,8 @@ - 60 - 160 + 81 + 150 76 20 @@ -681,8 +681,8 @@ - 30 - 520 + 51 + 510 191 20 @@ -706,8 +706,8 @@ - 109 - 460 + 130 + 450 101 31 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 @@ - interval between cycles= + interval between cycles (s)= 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 @@ -681,8 +681,8 @@ - 51 - 510 + 270 + 30 191 20 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 @@ 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 @@ - 270 + 250 80 - 191 + 241 331 @@ -695,7 +695,7 @@ - 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 @@ 130 - 450 + 490 101 31 @@ -726,6 +726,22 @@ 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 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 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 @@ - - + @@ -1534,8 +1533,7 @@ - - + @@ -1628,8 +1626,8 @@ 0 0 - 610 - 339 + 655 + 365 @@ -1731,7 +1729,7 @@ 0.030000000000000 - + true @@ -1759,8 +1757,8 @@ 0.030000000000000 - - true + + true @@ -1813,7 +1811,7 @@ 3.000000000000000 - + true @@ -1841,9 +1839,9 @@ 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.jpgresources/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 @@ + + + Dialog + + + + 0 + 0 + 1105 + 393 + + + + Random Reward + + + + + 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 @@ 620 20 - 222 - 27 + 152 + 20
@@ -4569,7 +4569,7 @@ 0 0 1951 - 31 + 21 @@ -4621,6 +4621,7 @@ + @@ -5023,6 +5024,11 @@ Optical Tagging + + + Random Reward + + NextBlock From 39a9390b01efeb45f13344f22096d100a34486bb Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:35:13 -0800 Subject: [PATCH 099/184] adding random reward to the behavior GUI --- src/foraging_gui/ForagingGUI.ui | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/foraging_gui/ForagingGUI.ui b/src/foraging_gui/ForagingGUI.ui index 2a2db3286..f28ac32cc 100644 --- a/src/foraging_gui/ForagingGUI.ui +++ b/src/foraging_gui/ForagingGUI.ui @@ -79,7 +79,7 @@ 0 0 894 - 226 + 236 @@ -691,7 +691,7 @@ 0 0 - 624 + 621 334 @@ -1626,8 +1626,8 @@ 0 0 - 655 - 365 + 610 + 339 @@ -2270,8 +2270,8 @@ Bias: 0 0 - 349 - 189 + 330 + 209 @@ -2342,8 +2342,8 @@ Double dipping: 0 0 - 1540 - 504 + 1177 + 463 @@ -4445,8 +4445,8 @@ Current pair: 0 0 - 714 - 286 + 627 + 325 @@ -4803,7 +4803,7 @@ Current pair: 0 0 960 - 31 + 21 @@ -4855,6 +4855,7 @@ Current pair: + @@ -5255,6 +5256,11 @@ Current pair: Optical Tagging + + + Random Reward + + Task From 0e4cc86305f53b18940e540efd9d8f10c8bc9936 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:51:03 -0800 Subject: [PATCH 100/184] adding fields to the random reward panel --- src/foraging_gui/RandomReward.ui | 444 ++++++++++++++++++++++++++++++- 1 file changed, 442 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index a78716439..c675db6b7 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -6,13 +6,453 @@ 0 0 - 1105 - 393 + 606 + 400 Random Reward + + + + 180 + 60 + 81 + 31 + + + + Start + + + true + + + + + + 180 + 98 + 81 + 20 + + + + + 0 + 0 + + + + + 1000 + 16777215 + + + + + Both + + + + + Left + + + + + Right + + + + + + + 100 + 98 + 76 + 20 + + + + + 90 + 16777215 + + + + Spouts= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 180 + 170 + 81 + 20 + + + + + 1000 + 16777215 + + + + 3,5,7 + + + false + + + + + + 60 + 140 + 116 + 20 + + + + Left volume (ul)= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 180 + 140 + 81 + 20 + + + + + 1000 + 16777215 + + + + 3,5,7 + + + false + + + + + + 60 + 170 + 116 + 20 + + + + Right volume (ul)= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 45 + 200 + 131 + 20 + + + + + 1000 + 16777215 + + + + Reward N each condition= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 20 + 240 + 151 + 20 + + + + + 1000 + 16777215 + + + + Reward interval distribution= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 180 + 240 + 81 + 20 + + + + + 0 + 0 + + + + + 1000 + 16777215 + + + + + Exponential + + + + + Uniform + + + + + + + 180 + 200 + 81 + 22 + + + + 99999 + + + 100 + + + + + true + + + + 180 + 270 + 81 + 20 + + + + + 1000 + 16777215 + + + + 20 + + + false + + + + + + 60 + 270 + 116 + 20 + + + + Beta= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 60 + 300 + 116 + 20 + + + + Min (s)= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 180 + 300 + 81 + 20 + + + + + 1000 + 16777215 + + + + 5 + + + false + + + + + + 60 + 330 + 116 + 20 + + + + Max (s)= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + + + + 180 + 330 + 81 + 20 + + + + + 1000 + 16777215 + + + + 30 + + + false + + + + + true + + + + 290 + 30 + 241 + 331 + + + + + 9000000 + 16777215 + + + + + 13 + 75 + true + + + + + + + Qt::AutoText + + + Qt::AlignHCenter|Qt::AlignTop + + + false + + From 52a56160338350134a1654da590aa2b4dfa24bd5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:53:55 -0800 Subject: [PATCH 101/184] adding RandomRewardDialog --- src/foraging_gui/Dialogs.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f0f7713db..342a3a15d 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3506,4 +3506,22 @@ def extract_numbers_from_string(input_string:str)->list: """ # 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 + return [float(num) for num in re.findall(float_pattern, input_string)] + + +class RandomRewardDialog(QDialog): + + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('RandomReward.ui', self) + self._connectSignalsSlots() + self.MainWindow = MainWindow + 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): + pass \ No newline at end of file From 0bcb5e812a402520f3ae569c6406ed56ea244df5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:56:26 -0800 Subject: [PATCH 102/184] adding random reward to main --- src/foraging_gui/Foraging.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 01eec4211..e69b942f1 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,OpticalTaggingDialog +from foraging_gui.Dialogs import LaserCalibrationDialog,OpticalTaggingDialog,RandomRewardDialog from foraging_gui.Dialogs import LickStaDialog,TimeDistributionDialog from foraging_gui.Dialogs import AutoTrainDialog, MouseSelectorDialog from foraging_gui.MyFunctions import GenerateTrials, Worker,TimerWorker, NewScaleSerialY, EphysRecording @@ -185,6 +185,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self.OpenCamera=0 self.OpenMetadata=0 self.OpenOpticalTagging=0 + self.OpenRandomReward=0 self.NewTrialRewardOrder=0 self.LickSta=0 self.LickSta_ToInitializeVisual=1 @@ -2351,6 +2352,16 @@ def _OpticalTagging(self): else: self.OpticalTagging_dialog.hide() + def _RandomReward(self): + '''Open the random reward dialog''' + if self.OpenRandomReward==0: + self.RandomReward_dialog = RandomRewardDialog(MainWindow=self) + self.OpenRandomReward=1 + if self.actionRandom_Reward.isChecked()==True: + self.RandomReward_dialog.show() + else: + self.RandomReward_dialog.hide() + def _Metadata(self): '''Open the metadata dialog''' if self.OpenMetadata==0: From 6144ee04eb7c7c1b3a916a019626b13603fb2a24 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:58:02 -0800 Subject: [PATCH 103/184] set checkable --- src/foraging_gui/ForagingGUI.ui | 3 +++ src/foraging_gui/ForagingGUI_Ephys.ui | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/foraging_gui/ForagingGUI.ui b/src/foraging_gui/ForagingGUI.ui index f28ac32cc..580bf0972 100644 --- a/src/foraging_gui/ForagingGUI.ui +++ b/src/foraging_gui/ForagingGUI.ui @@ -5257,6 +5257,9 @@ Current pair: + + true + Random Reward diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index 2e4c97c5a..1e16c0e5a 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -5025,6 +5025,9 @@ + + true + Random Reward From f147fc21339b60a8640643cae55a8b1f951493c7 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:58:56 -0800 Subject: [PATCH 104/184] set checkable --- src/foraging_gui/ForagingGUI.ui | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/foraging_gui/ForagingGUI.ui b/src/foraging_gui/ForagingGUI.ui index 2a2db3286..0e7c6119d 100644 --- a/src/foraging_gui/ForagingGUI.ui +++ b/src/foraging_gui/ForagingGUI.ui @@ -79,7 +79,7 @@ 0 0 894 - 226 + 236 @@ -691,7 +691,7 @@ 0 0 - 624 + 621 334 @@ -1626,8 +1626,8 @@ 0 0 - 655 - 365 + 610 + 339 @@ -2270,8 +2270,8 @@ Bias: 0 0 - 349 - 189 + 330 + 209 @@ -2342,8 +2342,8 @@ Double dipping: 0 0 - 1540 - 504 + 1177 + 463 @@ -4445,8 +4445,8 @@ Current pair: 0 0 - 714 - 286 + 627 + 325 @@ -4803,7 +4803,7 @@ Current pair: 0 0 960 - 31 + 21 @@ -5247,6 +5247,9 @@ Current pair: + + true + resources/OpticalTagging.jpgresources/OpticalTagging.jpg From fcc4536d6c1299647404a1f001831447e1dfd114 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:03:18 -0800 Subject: [PATCH 105/184] open random reward --- 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 e69b942f1..22c9fcf94 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -208,6 +208,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self._WaterCalibration()# to open the water calibration panel self._Camera() self._OpticalTagging() + self._RandomReward() self._InitializeMotorStage() self._load_stage() self._Metadata() From b1ee7b818cd6ed81802ab4c2b02cd267b644cf63 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:04:36 -0800 Subject: [PATCH 106/184] connecting random reward --- 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 22c9fcf94..fdb310031 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -324,6 +324,7 @@ def connectSignalsSlots(self): self.action_Camera.triggered.connect(self._Camera) self.actionMeta_Data.triggered.connect(self._Metadata) self.actionOptical_Tagging.triggered.connect(self._OpticalTagging) + self.actionRandom_Reward.triggered.connect(self._RandomReward) self.action_Optogenetics.triggered.connect(self._Optogenetics) self.actionLicks_sta.triggered.connect(self._LickSta) self.actionTime_distribution.triggered.connect(self._TimeDistribution) From bc2c1b46617ff7a5dc188a0ae68dea2641481385 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:06:08 -0800 Subject: [PATCH 107/184] deleting dialogs copy --- src/foraging_gui/Dialogs copy.py | 3068 ------------------------------ 1 file changed, 3068 deletions(-) delete mode 100644 src/foraging_gui/Dialogs copy.py diff --git a/src/foraging_gui/Dialogs copy.py b/src/foraging_gui/Dialogs copy.py deleted file mode 100644 index 396326a38..000000000 --- a/src/foraging_gui/Dialogs copy.py +++ /dev/null @@ -1,3068 +0,0 @@ -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) From 85c60f904c9c88a8b5bfde0924f8f7ea937e6b8a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:15:08 -0800 Subject: [PATCH 108/184] toggle color and disable/enable fields --- src/foraging_gui/Dialogs.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 342a3a15d..1cad16e24 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3524,4 +3524,33 @@ def __init__(self, MainWindow, parent=None): child.setAutoDefault(False) def _connectSignalsSlots(self): - pass \ No newline at end of file + self.Start.clicked.connect(self._Start) + self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) + + def _Start(self): + '''Start giving random rewards''' + # toggle the button color + if self.Start.isChecked(): + self.Start.setStyleSheet("background-color : green;") + else: + self.Start.setStyleSheet("background-color : none") + return + + def _WhichSpout(self): + '''Select the lick spout to use and disable non-relevant widgets''' + spout_name = self.WhichSpout.currentText() + if spout_name=='Left': + self.label1_21.setEnabled(False) + self.RightVolume.setEnabled(False) + self.label1_6.setEnabled(True) + self.LeftVolume.setEnabled(True) + elif spout_name=='Right': + self.label1_6.setEnabled(False) + self.LeftVolume.setEnabled(False) + self.label1_21.setEnabled(True) + self.RightVolume.setEnabled(True) + else: + self.label1_6.setEnabled(True) + self.LeftVolume.setEnabled(True) + self.label1_21.setEnabled(True) + self.RightVolume.setEnabled(True) \ No newline at end of file From ebc2009412cdec205090504658fbdbbf0274ab5d Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:47:26 -0800 Subject: [PATCH 109/184] _generate_random_conditions --- src/foraging_gui/Dialogs.py | 82 ++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 1cad16e24..efcd426ef 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3517,6 +3517,7 @@ def __init__(self, MainWindow, parent=None): self._connectSignalsSlots() self.MainWindow = MainWindow self.threadpool = QThreadPool() + self.cycle_finish_tag = 1 # find all buttons and set them to not be the default button for container in [self]: for child in container.findChildren((QtWidgets.QPushButton)): @@ -3535,7 +3536,14 @@ def _Start(self): else: self.Start.setStyleSheet("background-color : none") return - + # generate random conditions including lick spouts, reward volume, and reward interval + if self.cycle_finish_tag==1: + # generate new random conditions + self._generate_random_conditions() + self.index=list(range(len(self.current_random_reward_par['volumes_all_random']))) + self.cycle_finish_tag = 0 + + def _WhichSpout(self): '''Select the lick spout to use and disable non-relevant widgets''' spout_name = self.WhichSpout.currentText() @@ -3553,4 +3561,74 @@ def _WhichSpout(self): self.label1_6.setEnabled(True) self.LeftVolume.setEnabled(True) self.label1_21.setEnabled(True) - self.RightVolume.setEnabled(True) \ No newline at end of file + self.RightVolume.setEnabled(True) + + def _generate_random_conditions(self): + """ + Generate random conditions + + The parameters are chosen as follows: + - **Lick Spouts**: One of the following is selected: `Left`, `Right`, or `Both`. + - **Reward Volume**: If `Left` is selected, `LeftVolume` is used. If `Right` is selected, `RightVolume` is used. + - **Reward Interval**: The interval between rewards. + """ + # Get the volume and reward sides + spout_name = self.WhichSpout.currentText() + + if spout_name == 'Both': + # Extract volumes from left and right spouts + left_volumes = extract_numbers_from_string(self.LeftVolume.text()) + right_volumes = extract_numbers_from_string(self.RightVolume.text()) + volumes = left_volumes + right_volumes # Combine into a single list + + # Create sides: 0 for left, 1 for right + sides = np.zeros(len(left_volumes)).tolist() + np.ones(len(right_volumes)).tolist() + + elif spout_name == 'Left': + # Extract volumes and assign sides for left spout + volumes = extract_numbers_from_string(self.LeftVolume.text()) + sides = np.zeros(len(volumes)).tolist() # 0 for left + + elif spout_name == 'Right': + # Extract volumes and assign sides for right spout + volumes = extract_numbers_from_string(self.RightVolume.text()) + sides = np.ones(len(volumes)).tolist() # 1 for right + + else: + # Popup error if spout is not selected or invalid input + QMessageBox.critical(self.MainWindow, "Error", "Please select a valid lick spout.") + return + + + # get all rewards + volumes_all = volumes*int(self.RewardN.value()) + sides_all = sides*int(self.RewardN.value()) + + # randomize the rewards and sides + random_indices = random.sample(range(len(volumes_all)), len(volumes_all)) + volumes_all_random = [volumes_all[i] for i in random_indices] + sides_all_random = [sides_all[i] for i in random_indices] + + # get the reward interval + if self.IntervalDistribution.currentText() == "Exponential": + reward_intervals = np.random.exponential(float(self.IntervalBeta.text()), len(volumes_all_random))+float(self.IntervalMin.text()) + if self.IntervalMax.text()!='': + reward_intervals = np.minimum(reward_intervals,float(self.IntervalMax.text())) + # keep one decimal + reward_intervals = np.round(reward_intervals,1) + elif self.IntervalDistribution.currentText() == "Uniform": + reward_intervals = np.random.uniform(float(self.IntervalMin.text()), float(self.IntervalMax.text()), len(volumes_all_random)) + # keep one decimal + reward_intervals = np.round(reward_intervals,1) + + # save the data + self.current_random_reward_par={} + self.current_random_reward_par['volumes_all_random']=volumes_all_random + self.current_random_reward_par['sides_all_random']=sides_all_random + self.current_random_reward_par['reward_intervals']=reward_intervals + + + + + + \ No newline at end of file From 478be084034c5e6f55e539f1f4ee1634c56b7a02 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:52:23 -0800 Subject: [PATCH 110/184] _start_random_reward --- src/foraging_gui/Dialogs.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index efcd426ef..a6f3cde5b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3518,6 +3518,7 @@ def __init__(self, MainWindow, parent=None): self.MainWindow = MainWindow self.threadpool = QThreadPool() self.cycle_finish_tag = 1 + self.random_reward_par={} # find all buttons and set them to not be the default button for container in [self]: for child in container.findChildren((QtWidgets.QPushButton)): @@ -3543,6 +3544,50 @@ def _Start(self): self.index=list(range(len(self.current_random_reward_par['volumes_all_random']))) self.cycle_finish_tag = 0 + # start the random reward in a different thread + worker_random_reward = WorkerTagging(self._start_random_reward) + worker_random_reward.signals.update_label.connect(self.label_show_current.setText) # Connect to label update + worker_random_reward.signals.finished.connect(self._thread_complete_tag) + + # get the first start time + if "random_reward_start_time" not in self.random_reward_par: + self.random_reward_par["random_reward_start_time"] = str(datetime.now()) + if self.random_reward_par["random_reward_start_time"]=='': + self.random_reward_par["random_reward_start_time"] = str(datetime.now()) + + # Execute + self.threadpool.start(worker_random_reward) + + def _start_random_reward(self,update_label): + '''Start the random reward in a different thread''' + # 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) + # get the current parameters + volume = self.current_random_reward_par['volumes_all_random'][i] + side = self.current_random_reward_par['sides_all_random'][i] + interval = self.current_random_reward_par['reward_intervals'][i] + # give the reward + self._give_reward(volume=volume, side=side) + # save the data + self._save_data(volume=volume, side=side, interval=interval) + # wait to start the next cycle + time.sleep(interval) + # show current cycle and parameters + # Emit signal to update the label + update_label( + f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" + f"Volume: {volume} uL\n" + f"Side: {side}\n" + f"Interval: {interval} s" + ) + else: + break + def _WhichSpout(self): '''Select the lick spout to use and disable non-relevant widgets''' From 804d68f643df31621f98372593b0642752e27a1d Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:10:07 -0800 Subject: [PATCH 111/184] adding multiple functions --- src/foraging_gui/Dialogs.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a6f3cde5b..6544007dc 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3587,6 +3587,38 @@ def _start_random_reward(self,update_label): ) else: break + + def _save_data(self, volume:float, side:int, interval:float): + '''Extend the current parameters to self.random_reward_par''' + if 'volumes' not in self.random_reward_par.keys(): + self.random_reward_par['volumes']=[] + self.random_reward_par['sides']=[] + self.random_reward_par['intervals']=[] + else: + self.random_reward_par['volumes'].append(volume) + self.random_reward_par['sides'].append(side) + self.random_reward_par['intervals'].append(interval) + + def _thread_complete_tag(self): + '''Complete the random reward''' + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + # update the stop time + self.random_reward_par["random_reward_end_time"] = str(datetime.now()) + + def _give_reward(self,volume:float,side:int): + '''Give the reward''' + if side==0: + left_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Left'][1])/self.MainWindow.latest_fitting['Left'][0])*1000 + # set the left valve open time + # open the left valve + + elif side==1: + right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 + # set the right valve open time + # open the right valve + + # update the reward suggestion def _WhichSpout(self): From 64cfe5d631128deeb773a747c5a249689af0b279 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:10:42 -0800 Subject: [PATCH 112/184] adding emergency stop --- src/foraging_gui/RandomReward.ui | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index c675db6b7..257e6360e 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -453,6 +453,19 @@ false
+ + + + 160 + 360 + 101 + 31 + + + + Emergency Stop + + From d8d72d9226c952e8c98c95bedfeb0171288e2d31 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:11:43 -0800 Subject: [PATCH 113/184] adding emergency stop --- src/foraging_gui/Dialogs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 6544007dc..ab21e193e 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3528,7 +3528,14 @@ def __init__(self, MainWindow, parent=None): def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) + self.EmergencyStop.clicked.connect(self._emegency_stop) + def _emegency_stop(self): + '''Stop the random reward''' + self.cycle_finish_tag = 1 + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + def _Start(self): '''Start giving random rewards''' # toggle the button color From 5cef84c53602293303938b15d05fd3eef1b4d5f5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:16:56 -0800 Subject: [PATCH 114/184] move sleep to the end --- 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 ab21e193e..7bd2dbeb2 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3582,8 +3582,6 @@ def _start_random_reward(self,update_label): self._give_reward(volume=volume, side=side) # save the data self._save_data(volume=volume, side=side, interval=interval) - # wait to start the next cycle - time.sleep(interval) # show current cycle and parameters # Emit signal to update the label update_label( @@ -3592,6 +3590,8 @@ def _start_random_reward(self,update_label): f"Side: {side}\n" f"Interval: {interval} s" ) + # wait to start the next cycle + time.sleep(interval) else: break From eecee5a9306f61a64a70ad0625f4b5d678e72371 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:37:24 -0800 Subject: [PATCH 115/184] adding RandomWater_Left and RandomWater_Right --- src/foraging_gui/rigcontrol.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/foraging_gui/rigcontrol.py b/src/foraging_gui/rigcontrol.py index d48383ece..b2ae3ea11 100644 --- a/src/foraging_gui/rigcontrol.py +++ b/src/foraging_gui/rigcontrol.py @@ -146,6 +146,12 @@ def AutoWater_Left(self, value): def AutoWater_Right(self, value): self.send("/AutoWater_Right", value) + + def RandomWater_Left(self, value): + self.send("/RandomWater_Left", value) + + def RandomWater_Right(self, value): + self.send("/RandomWater_Right", value) def Location1_Size(self, value): self.send("/Location1Size", value) From cc2136bded0fba54fb40747f9729e146d55c9e32 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:43:05 -0800 Subject: [PATCH 116/184] adding random water to bonsai workflow --- src/workflows/foraging.bonsai | 152 ++++-- src/workflows/foraging.bonsai.layout | 758 +++++++++++++++++++++++++-- 2 files changed, 841 insertions(+), 69 deletions(-) diff --git a/src/workflows/foraging.bonsai b/src/workflows/foraging.bonsai index 1670682d2..9d3a85a66 100644 --- a/src/workflows/foraging.bonsai +++ b/src/workflows/foraging.bonsai @@ -1530,7 +1530,7 @@ On Disabled false - COM12 + COM10 @@ -1555,7 +1555,7 @@ Write - 60 + 0 @@ -1580,7 +1580,7 @@ Write - 60 + 0 @@ -1710,6 +1710,9 @@ AutoWater_Left + + RandomWater_Left + @@ -1774,6 +1777,9 @@ AutoWater_Right + + RandomWater_Right + @@ -1796,28 +1802,30 @@ - + - - - + + + - - - + + + - - + + - - - - + + + + + + @@ -2835,7 +2843,7 @@ On Disabled false - COM16 + COM3 @@ -3950,6 +3958,26 @@ CameraStartType + + FromBonsaiOSC3 + + + /RandomWater_Left + i + + + RandomWater_Left + + + FromBonsaiOSC3 + + + /RandomWater_Right + i + + + RandomWater_Right + @@ -4058,6 +4086,10 @@ + + + + @@ -4185,6 +4217,48 @@ ToBonsaiOSC + + AutoWater_Left + + + Timestamps + + + + + + Item2 + + + new(it as Seconds) + + + /AutoLeftWaterStartTime + + + ToBonsaiOSC2 + + + AutoWater_Right + + + Timestamps + + + + + + Item2 + + + new(it as Seconds) + + + /AutoRightWaterStartTime + + + ToBonsaiOSC2 + TrialEnd @@ -4412,7 +4486,7 @@ ToBonsaiOSC2 - AutoWater_Left + RandomWater_Left Timestamps @@ -4427,13 +4501,13 @@ new(it as Seconds) - /AutoLeftWaterStartTime + /RandomLeftWaterStartTime ToBonsaiOSC2 - AutoWater_Right + RandomWater_Right Timestamps @@ -4448,7 +4522,7 @@ new(it as Seconds) - /AutoRightWaterStartTime + /RandomRightWaterStartTime ToBonsaiOSC2 @@ -4505,18 +4579,18 @@ - - - - + + + + + - - - - - + + + + + - @@ -4555,6 +4629,18 @@ + + + + + + + + + + + + @@ -8884,7 +8970,7 @@ - C:\Users\svc_aind_ephys\Documents\ForagingSettings\Settings_box1.csv + C:\Users\xinxin.yin\Documents\ForagingSettings\Settings_box1.csv %s,%s 0 @@ -8921,7 +9007,7 @@ true true Microsoft Sans Serif, 15.75pt, style=Bold - 4xx-x-A + XY diff --git a/src/workflows/foraging.bonsai.layout b/src/workflows/foraging.bonsai.layout index f086a9c14..9f753b2f9 100644 --- a/src/workflows/foraging.bonsai.layout +++ b/src/workflows/foraging.bonsai.layout @@ -68,8 +68,8 @@ 3 - 1506 - 895 + 270 + 297 Normal @@ -586,8 +586,8 @@ 3 - 632 - 654 + 1329 + 895 Normal @@ -610,8 +610,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -918,6 +918,464 @@ + + false + + 0 + 0 + + + 0 + 0 + + Normal + + true + + 3 + 3 + + + 1329 + 882 + + Normal + + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false @@ -2958,18 +3416,6 @@ Normal - - false - - 0 - 0 - - - 0 - 0 - - Normal - @@ -2990,8 +3436,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -3134,8 +3580,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -3616,8 +4062,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -4148,8 +4594,8 @@ 3 - 632 - 654 + 1329 + 895 Normal @@ -4172,8 +4618,8 @@ 3 - 632 - 654 + 1329 + 895 Normal @@ -6002,6 +6448,78 @@ Normal + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + @@ -6022,8 +6540,8 @@ 3 - 632 - 654 + 1329 + 895 Normal @@ -7468,6 +7986,174 @@ Normal + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + + + false + + 0 + 0 + + + 0 + 0 + + Normal + @@ -8419,8 +9105,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -8618,8 +9304,8 @@ 3 - 632 - 654 + 270 + 297 Normal @@ -8642,8 +9328,8 @@ 3 - 632 - 654 + 270 + 297 Normal From 79e8ed2bdce1bf2bf2fae93c11e57bab904c1c4a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:51:33 -0800 Subject: [PATCH 117/184] giving reward --- src/foraging_gui/Dialogs.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 7bd2dbeb2..b34db9f2b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3524,7 +3524,8 @@ def __init__(self, MainWindow, parent=None): for child in container.findChildren((QtWidgets.QPushButton)): child.setDefault(False) child.setAutoDefault(False) - + self.RandomWaterVolume=[0,0] + def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) @@ -3533,6 +3534,7 @@ def _connectSignalsSlots(self): def _emegency_stop(self): '''Stop the random reward''' self.cycle_finish_tag = 1 + self.RandomWaterVolume=[0,0] self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") @@ -3618,14 +3620,21 @@ def _give_reward(self,volume:float,side:int): if side==0: left_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Left'][1])/self.MainWindow.latest_fitting['Left'][0])*1000 # set the left valve open time + self.Channel.LeftValue(left_valve_open_time) # open the left valve - + time.sleep(0.01) + self.Channel3.RandomWater_Left(int(1)) + self.RandomWaterVolume[0]=self.ManualWaterVolume[0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time + self.Channel.RightValue(right_valve_open_time) # open the right valve - + time.sleep(0.01) + self.Channel3.RandomWater_Right(int(1)) + self.RandomWaterVolume[1]=self.ManualWaterVolume[1]+float(volume)/1000 # update the reward suggestion + self.MainWindow._UpdateSuggestedWater() def _WhichSpout(self): From 1b8bf2e47cf8608ddef53fe674898e66f6ac2df5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:56:11 -0800 Subject: [PATCH 118/184] adding RandomWaterVolume to water suggestion --- src/foraging_gui/Foraging.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index fdb310031..e554d57a3 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2691,6 +2691,9 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): # save manual water Obj['ManualWaterVolume']=self.ManualWaterVolume + # save the random water + Obj['RandomWaterVolume']=self.RandomReward_dialog.RandomWaterVolume + # save camera start/stop time Obj['Camera_dialog']['camera_start_time']=self.Camera_dialog.camera_start_time Obj['Camera_dialog']['camera_stop_time']=self.Camera_dialog.camera_stop_time @@ -4627,7 +4630,11 @@ def _UpdateSuggestedWater(self,ManualWater=0): ManualWaterVolume=np.sum(self.ManualWaterVolume) else: ManualWaterVolume=0 - water_in_session=BS_TotalReward+ManualWaterVolume + + if hasattr(self,'RandomReward_dialog'): + RandomWaterVolume=np.sum(self.RandomReward_dialog.RandomWaterVolume) + + water_in_session=BS_TotalReward+ManualWaterVolume+RandomWaterVolume self.water_in_session=water_in_session if self.WeightAfter.text()!='' and self.BaseWeight.text()!='' and self.TargetRatio.text()!='': # calculate the suggested water From 109e7bed0eccee15f41d5810ccbda3797e000d27 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:09:42 -0800 Subject: [PATCH 119/184] adding timestamp --- src/foraging_gui/Dialogs.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index b34db9f2b..6654f8ca9 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3582,8 +3582,10 @@ def _start_random_reward(self,update_label): interval = self.current_random_reward_par['reward_intervals'][i] # give the reward self._give_reward(volume=volume, side=side) + # receiving the timestamps of reward start time. + timestamp_computer, timestamp_harp=self._receving_timestamps(side=side) # save the data - self._save_data(volume=volume, side=side, interval=interval) + self._save_data(volume=volume, side=side, interval=interval,timestamp_computer=timestamp_computer,timestamp_harp=timestamp_harp) # show current cycle and parameters # Emit signal to update the label update_label( @@ -3596,17 +3598,39 @@ def _start_random_reward(self,update_label): time.sleep(interval) else: break - - def _save_data(self, volume:float, side:int, interval:float): + def _receving_timestamps(self,side:int): + '''Receiving the timestamps of reward start time''' + if side==0: + for i in range(2): + Rec=self.MainWindow.Channel2.receive() + if Rec[0].address=='/RandomLeftWaterStartTime': + random_left_water_start_time_harp=Rec[1][1][0] + if Rec[0].address=='/LeftRewardDeliveryTimeHarp': + random_left_reward_delivery_time_harp=Rec[1][1][0] + return random_left_water_start_time_harp,random_left_reward_delivery_time_harp + elif side==1: + for i in range(2): + Rec=self.MainWindow.Channel2.receive() + if Rec[0].address=='/RandomRightWaterStartTime': + random_right_water_start_time_harp=Rec[1][1][0] + if Rec[0].address=='/RightRewardDeliveryTimeHarp': + random_right_reward_delivery_time_harp=Rec[1][1][0] + return random_right_water_start_time_harp,random_right_reward_delivery_time_harp + + def _save_data(self, volume:float, side:int, interval:float, timestamp_computer:float, timestamp_harp:float): '''Extend the current parameters to self.random_reward_par''' if 'volumes' not in self.random_reward_par.keys(): self.random_reward_par['volumes']=[] self.random_reward_par['sides']=[] self.random_reward_par['intervals']=[] + self.random_reward_par['timestamp_computer']=[] + self.random_reward_par['timestamp_harp']=[] else: self.random_reward_par['volumes'].append(volume) self.random_reward_par['sides'].append(side) self.random_reward_par['intervals'].append(interval) + self.random_reward_par['timestamp_computer'].append(timestamp_computer) + self.random_reward_par['timestamp_harp'].append(timestamp_harp) def _thread_complete_tag(self): '''Complete the random reward''' From 6a3257babf326f75dfe6207bfe65ca18046f75d6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:12:19 -0800 Subject: [PATCH 120/184] adding MainWindow --- src/foraging_gui/Dialogs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 6654f8ca9..b6f868cc6 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3598,6 +3598,7 @@ def _start_random_reward(self,update_label): time.sleep(interval) else: break + def _receving_timestamps(self,side:int): '''Receiving the timestamps of reward start time''' if side==0: @@ -3644,18 +3645,18 @@ def _give_reward(self,volume:float,side:int): if side==0: left_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Left'][1])/self.MainWindow.latest_fitting['Left'][0])*1000 # set the left valve open time - self.Channel.LeftValue(left_valve_open_time) + self.MainWindow.Channel.LeftValue(left_valve_open_time) # open the left valve time.sleep(0.01) - self.Channel3.RandomWater_Left(int(1)) + self.MainWindow.Channel3.RandomWater_Left(int(1)) self.RandomWaterVolume[0]=self.ManualWaterVolume[0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time - self.Channel.RightValue(right_valve_open_time) + self.MainWindow.Channel.RightValue(right_valve_open_time) # open the right valve time.sleep(0.01) - self.Channel3.RandomWater_Right(int(1)) + self.MainWindow.Channel3.RandomWater_Right(int(1)) self.RandomWaterVolume[1]=self.ManualWaterVolume[1]+float(volume)/1000 # update the reward suggestion self.MainWindow._UpdateSuggestedWater() @@ -3716,7 +3717,6 @@ def _generate_random_conditions(self): QMessageBox.critical(self.MainWindow, "Error", "Please select a valid lick spout.") return - # get all rewards volumes_all = volumes*int(self.RewardN.value()) sides_all = sides*int(self.RewardN.value()) From f9d9580519d224a374ba22202b6a4ff2d94909b8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:14:39 -0800 Subject: [PATCH 121/184] adding random volume to random_reward_par --- src/foraging_gui/Dialogs copy.py | 3527 ++++++++++++++++++++++++++++++ src/foraging_gui/Dialogs.py | 8 +- src/foraging_gui/Foraging.py | 6 +- 3 files changed, 3534 insertions(+), 7 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..342a3a15d --- /dev/null +++ b/src/foraging_gui/Dialogs copy.py @@ -0,0 +1,3527 @@ +import time +import math +import json +import os +import shutil +import subprocess +from datetime import datetime +import logging +import webbrowser +import re +import random +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 +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,WorkerTagging +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 +from foraging_gui.GenerateMetadata import generate_metadata +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() + self.MainWindow = MainWindow + self.current_optical_tagging_par={} + 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) + 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(day+hour+minute)_OpticalTaggingResults.csv + 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) + + 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") + return + # generate random conditions including lasers, laser power, laser color, and protocol + 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.cycle_finish_tag = 0 + + # send the trigger source + self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') + + # start the optical tagging in a different thread + 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) + + # 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() + + def _emegency_stop(self): + '''Stop the optical tagging''' + self.cycle_finish_tag = 1 + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + + def _thread_complete_tag(self): + '''Complete the optical tagging''' + # 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") + # 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''' + # 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] + 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_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.Channel.receive() + + if Rec[0].address=='/ITIStartTimeHarp': + laser_start_timestamp=Rec[1][1][0] + # change the success_tag to 1 + success_tag=1 + else: + laser_start_timestamp=-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, + laser_start_timestamp=laser_start_timestamp, + success_tag=success_tag + ) + # wait to start the next cycle + time.sleep(duration_each_cycle+interval_between_cycles) + # show current cycle and parameters + # 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"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''' + 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']=[] + 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) + 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) + 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''' + # start generating waveform in bonsai + self.MainWindow.Channel.OptogeneticsCalibration(int(1)) + + 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 duration. + + 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 (ms)**: Applied to both lasers during the cycle. + """ + # 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_condition.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_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) + } + elif self.WhichLaser.currentText() in ['Laser_1','Laser_2']: + 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) + } + 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,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) + 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 extract_numbers_from_string(self.Duration_each_cycle.text()) + for interval_between_cycles in extract_numbers_from_string(self.Interval_between_cycles.text()) + ]) + + 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 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 + 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] + 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) + 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(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''' + 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) + + 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 + ) + 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, + duration_each_cycle=duration_each_cycle + ) + + return my_wave + + 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). + 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 + 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_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) + 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. + laser_color: The color of the laser. + protocol: The protocol to use. + Returns: + float: The amplitude of the laser. + ''' + # get the current calibration results + latest_calibration_date=find_latest_calibration_date(self.MainWindow.LaserCalibrationResults,laser_color) + # get the selected laser + if latest_calibration_date=='NA': + logger.info(f"No calibration results found for {laser_color}") + return + else: + 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=fit_calibration_results(calibration_results) + # Find the corresponding input voltage for a target laser power + input_voltage_for_target = (target_power - intercept) / slope + return round(input_voltage_for_target, 2) + +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) + + # Extract model coefficients + slope = model.coef_[0] + intercept = model.intercept_ + + return slope, intercept + +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 color. + + Returns: + str: The latest calibration date for the selected laser. + """ + Dates=[] + for Date in calibration: + if laser_color in calibration[Date].keys(): + Dates.append(Date) + sorted_dates = sorted(Dates) + if sorted_dates==[]: + return 'NA' + else: + 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)] + + +class RandomRewardDialog(QDialog): + + def __init__(self, MainWindow, parent=None): + super().__init__(parent) + uic.loadUi('RandomReward.ui', self) + self._connectSignalsSlots() + self.MainWindow = MainWindow + 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): + pass \ No newline at end of file diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index b6f868cc6..618dd6b92 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3524,7 +3524,7 @@ def __init__(self, MainWindow, parent=None): for child in container.findChildren((QtWidgets.QPushButton)): child.setDefault(False) child.setAutoDefault(False) - self.RandomWaterVolume=[0,0] + self.random_reward_par['RandomWaterVolume']=[0,0] def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) @@ -3534,7 +3534,7 @@ def _connectSignalsSlots(self): def _emegency_stop(self): '''Stop the random reward''' self.cycle_finish_tag = 1 - self.RandomWaterVolume=[0,0] + self.random_reward_par['RandomWaterVolume']=[0,0] self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") @@ -3649,7 +3649,7 @@ def _give_reward(self,volume:float,side:int): # open the left valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Left(int(1)) - self.RandomWaterVolume[0]=self.ManualWaterVolume[0]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][0]=self.ManualWaterVolume[0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time @@ -3657,7 +3657,7 @@ def _give_reward(self,volume:float,side:int): # open the right valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Right(int(1)) - self.RandomWaterVolume[1]=self.ManualWaterVolume[1]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][1]=self.ManualWaterVolume[1]+float(volume)/1000 # update the reward suggestion self.MainWindow._UpdateSuggestedWater() diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index e554d57a3..d7a94cadc 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2692,8 +2692,8 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): Obj['ManualWaterVolume']=self.ManualWaterVolume # save the random water - Obj['RandomWaterVolume']=self.RandomReward_dialog.RandomWaterVolume - + Obj['RandomWaterVolume']=self.RandomReward_dialog.random_reward_par['RandomWaterVolume'] + # save camera start/stop time Obj['Camera_dialog']['camera_start_time']=self.Camera_dialog.camera_start_time Obj['Camera_dialog']['camera_stop_time']=self.Camera_dialog.camera_stop_time @@ -4632,7 +4632,7 @@ def _UpdateSuggestedWater(self,ManualWater=0): ManualWaterVolume=0 if hasattr(self,'RandomReward_dialog'): - RandomWaterVolume=np.sum(self.RandomReward_dialog.RandomWaterVolume) + RandomWaterVolume=np.sum(self.RandomReward_dialog.random_reward_par['RandomWaterVolume']) water_in_session=BS_TotalReward+ManualWaterVolume+RandomWaterVolume self.water_in_session=water_in_session From 0c63b5a7e28f9c9cbda87ee610cecc562e7a8765 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:18:44 -0800 Subject: [PATCH 122/184] changing the emergency stop to start over --- src/foraging_gui/Dialogs.py | 19 ++++++++++++------- src/foraging_gui/RandomReward.ui | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 618dd6b92..838fe2c27 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3529,15 +3529,20 @@ def __init__(self, MainWindow, parent=None): def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) - self.EmergencyStop.clicked.connect(self._emegency_stop) + self.StartOver.clicked.connect(self._start_over) - def _emegency_stop(self): + def _start_over(self): '''Stop the random reward''' - self.cycle_finish_tag = 1 - self.random_reward_par['RandomWaterVolume']=[0,0] - self.Start.setChecked(False) - self.Start.setStyleSheet("background-color : none") - + # ask user if they want to start over + reply = QMessageBox.question(self, 'Message', 'Do you want to start over?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.cycle_finish_tag = 1 + self.random_reward_par['RandomWaterVolume']=[0,0] + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + else: + return + def _Start(self): '''Start giving random rewards''' # toggle the button color diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 257e6360e..1df454fa8 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -453,7 +453,7 @@ false
- + 160 @@ -463,7 +463,7 @@ - Emergency Stop + Start over From ac228e33fd5a4742a1518a8fca283c392c50f0f1 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:20:46 -0800 Subject: [PATCH 123/184] adding clear data button and function --- src/foraging_gui/Dialogs.py | 13 +++++++++++++ src/foraging_gui/RandomReward.ui | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 838fe2c27..4ab247846 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3530,6 +3530,19 @@ def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) self.StartOver.clicked.connect(self._start_over) + self.ClearData.clicked.connect(self._clear_data) + + def _clear_data(self): + '''Clear the data''' + # ask user if they want to clear the data + reply = QMessageBox.question(self, 'Message', 'Do you want to clear the data?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.random_reward_par={} + self.cycle_finish_tag = 1 + self.random_reward_par['RandomWaterVolume']=[0,0] + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + self.label_show_current.setText('') def _start_over(self): '''Stop the random reward''' diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 1df454fa8..8e022757e 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -7,7 +7,7 @@ 0 0 606 - 400 + 460 @@ -421,8 +421,8 @@ - 290 - 30 + 300 + 60 241 331 @@ -466,6 +466,19 @@ Start over + + + + 160 + 400 + 101 + 31 + + + + Clear data + + From 1c971d8fd5d76433637468cd1fa3defe2af134b1 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:22:05 -0800 Subject: [PATCH 124/184] changing dataformat 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 4ab247846..a1bfdc84d 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3663,7 +3663,7 @@ def _give_reward(self,volume:float,side:int): if side==0: left_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Left'][1])/self.MainWindow.latest_fitting['Left'][0])*1000 # set the left valve open time - self.MainWindow.Channel.LeftValue(left_valve_open_time) + self.MainWindow.Channel.LeftValue(float(left_valve_open_time)) # open the left valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Left(int(1)) @@ -3671,7 +3671,7 @@ def _give_reward(self,volume:float,side:int): elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time - self.MainWindow.Channel.RightValue(right_valve_open_time) + self.MainWindow.Channel.RightValue(float(right_valve_open_time)) # open the right valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Right(int(1)) From b9c206da44b38a4516b30f4c0c33b798c8b179d9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:24:11 -0800 Subject: [PATCH 125/184] typo --- src/foraging_gui/Dialogs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index a1bfdc84d..2c4a96f95 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3667,7 +3667,7 @@ def _give_reward(self,volume:float,side:int): # open the left valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Left(int(1)) - self.random_reward_par['RandomWaterVolume'][0]=self.ManualWaterVolume[0]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][0]=elf.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time @@ -3675,11 +3675,10 @@ def _give_reward(self,volume:float,side:int): # open the right valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Right(int(1)) - self.random_reward_par['RandomWaterVolume'][1]=self.ManualWaterVolume[1]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][1]=elf.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 # update the reward suggestion self.MainWindow._UpdateSuggestedWater() - def _WhichSpout(self): '''Select the lick spout to use and disable non-relevant widgets''' spout_name = self.WhichSpout.currentText() From f909be4415d6717e39671d15869500ab45c11e34 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:24:27 -0800 Subject: [PATCH 126/184] 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 2c4a96f95..fdfedd26c 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3667,7 +3667,7 @@ def _give_reward(self,volume:float,side:int): # open the left valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Left(int(1)) - self.random_reward_par['RandomWaterVolume'][0]=elf.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time From 4b48761367d245f271f547b5427c88706a00c18a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:26:22 -0800 Subject: [PATCH 127/184] typo --- 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 fdfedd26c..12be0fe18 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3668,6 +3668,7 @@ def _give_reward(self,volume:float,side:int): time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Left(int(1)) self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 + print(float(left_valve_open_time)) elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time @@ -3675,7 +3676,8 @@ def _give_reward(self,volume:float,side:int): # open the right valve time.sleep(0.01) self.MainWindow.Channel3.RandomWater_Right(int(1)) - self.random_reward_par['RandomWaterVolume'][1]=elf.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 + self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 + print(float(right_valve_open_time)) # update the reward suggestion self.MainWindow._UpdateSuggestedWater() From 0018307a03686f93d43bd68444ff4081b5fa2b9c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:32:32 -0800 Subject: [PATCH 128/184] changing the name --- src/foraging_gui/Dialogs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 12be0fe18..546245fe5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3623,18 +3623,18 @@ def _receving_timestamps(self,side:int): for i in range(2): Rec=self.MainWindow.Channel2.receive() if Rec[0].address=='/RandomLeftWaterStartTime': - random_left_water_start_time_harp=Rec[1][1][0] + random_left_water_start_time=Rec[1][1][0] if Rec[0].address=='/LeftRewardDeliveryTimeHarp': random_left_reward_delivery_time_harp=Rec[1][1][0] - return random_left_water_start_time_harp,random_left_reward_delivery_time_harp + return random_left_water_start_time,random_left_reward_delivery_time_harp elif side==1: for i in range(2): Rec=self.MainWindow.Channel2.receive() if Rec[0].address=='/RandomRightWaterStartTime': - random_right_water_start_time_harp=Rec[1][1][0] + random_right_water_start_time=Rec[1][1][0] if Rec[0].address=='/RightRewardDeliveryTimeHarp': random_right_reward_delivery_time_harp=Rec[1][1][0] - return random_right_water_start_time_harp,random_right_reward_delivery_time_harp + return random_right_water_start_time,random_right_reward_delivery_time_harp def _save_data(self, volume:float, side:int, interval:float, timestamp_computer:float, timestamp_harp:float): '''Extend the current parameters to self.random_reward_par''' @@ -3665,7 +3665,7 @@ def _give_reward(self,volume:float,side:int): # set the left valve open time self.MainWindow.Channel.LeftValue(float(left_valve_open_time)) # open the left valve - time.sleep(0.01) + time.sleep(1) self.MainWindow.Channel3.RandomWater_Left(int(1)) self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 print(float(left_valve_open_time)) @@ -3674,7 +3674,7 @@ def _give_reward(self,volume:float,side:int): # set the right valve open time self.MainWindow.Channel.RightValue(float(right_valve_open_time)) # open the right valve - time.sleep(0.01) + time.sleep(1) self.MainWindow.Channel3.RandomWater_Right(int(1)) self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 print(float(right_valve_open_time)) From 8fd2bafef58e2b1fc8cc17a0346dfa0aea05c521 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:51:15 -0800 Subject: [PATCH 129/184] wait for the thread to finish --- src/foraging_gui/Dialogs.py | 16 +++++++++++----- src/foraging_gui/Foraging.py | 1 - 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 546245fe5..b19c80724 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3518,6 +3518,7 @@ def __init__(self, MainWindow, parent=None): self.MainWindow = MainWindow self.threadpool = QThreadPool() self.cycle_finish_tag = 1 + self.thread_finish_tag = 1 self.random_reward_par={} # find all buttons and set them to not be the default button for container in [self]: @@ -3537,11 +3538,15 @@ def _clear_data(self): # ask user if they want to clear the data reply = QMessageBox.question(self, 'Message', 'Do you want to clear the data?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: - self.random_reward_par={} self.cycle_finish_tag = 1 - self.random_reward_par['RandomWaterVolume']=[0,0] self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") + # wait for the thread to finish + while self.thread_finish_tag == 0: + QApplication.processEvents() + time.sleep(0.1) + self.random_reward_par={} + self.random_reward_par['RandomWaterVolume']=[0,0] self.label_show_current.setText('') def _start_over(self): @@ -3550,7 +3555,6 @@ def _start_over(self): reply = QMessageBox.question(self, 'Message', 'Do you want to start over?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.cycle_finish_tag = 1 - self.random_reward_par['RandomWaterVolume']=[0,0] self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") else: @@ -3588,6 +3592,7 @@ def _Start(self): def _start_random_reward(self,update_label): '''Start the random reward in a different thread''' # iterate each condition + self.thread_finish_tag = 0 for i in self.index[:]: if self.Start.isChecked(): if i == self.index[-1]: @@ -3653,6 +3658,7 @@ def _save_data(self, volume:float, side:int, interval:float, timestamp_computer: def _thread_complete_tag(self): '''Complete the random reward''' + self.thread_finish_tag = 1 self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") # update the stop time @@ -3665,7 +3671,7 @@ def _give_reward(self,volume:float,side:int): # set the left valve open time self.MainWindow.Channel.LeftValue(float(left_valve_open_time)) # open the left valve - time.sleep(1) + time.sleep(0.1) self.MainWindow.Channel3.RandomWater_Left(int(1)) self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 print(float(left_valve_open_time)) @@ -3674,7 +3680,7 @@ def _give_reward(self,volume:float,side:int): # set the right valve open time self.MainWindow.Channel.RightValue(float(right_valve_open_time)) # open the right valve - time.sleep(1) + time.sleep(0.1) self.MainWindow.Channel3.RandomWater_Right(int(1)) self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 print(float(right_valve_open_time)) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index d7a94cadc..3e481bb8a 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -4532,7 +4532,6 @@ def _GiveLeft(self): logger.info('Give left manual water (ul): '+str(np.round(float(self.TP_GiveWaterL_volume),3)), extra={'tags': [self.warning_log_tag]}) - def _give_reserved_water(self,valve=None): '''give reserved water usually after the go cue''' if valve=='left': From cd9675f7e3cfe3a5fde30a64d3b2d58a820a8e3a Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:54:01 -0800 Subject: [PATCH 130/184] updating the reward volume when the thread finishes --- 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 b19c80724..57607d446 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3663,7 +3663,9 @@ def _thread_complete_tag(self): self.Start.setStyleSheet("background-color : none") # update the stop time self.random_reward_par["random_reward_end_time"] = str(datetime.now()) - + # update the reward suggestion + self.MainWindow._UpdateSuggestedWater() + def _give_reward(self,volume:float,side:int): '''Give the reward''' if side==0: @@ -3684,8 +3686,6 @@ def _give_reward(self,volume:float,side:int): self.MainWindow.Channel3.RandomWater_Right(int(1)) self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 print(float(right_valve_open_time)) - # update the reward suggestion - self.MainWindow._UpdateSuggestedWater() def _WhichSpout(self): '''Select the lick spout to use and disable non-relevant widgets''' From 2bb5300c872d30051d6ddc0c944abc1a742916b8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:55:40 -0800 Subject: [PATCH 131/184] removing print --- src/foraging_gui/Dialogs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 57607d446..0c7a4be42 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3665,7 +3665,7 @@ def _thread_complete_tag(self): self.random_reward_par["random_reward_end_time"] = str(datetime.now()) # update the reward suggestion self.MainWindow._UpdateSuggestedWater() - + def _give_reward(self,volume:float,side:int): '''Give the reward''' if side==0: @@ -3676,7 +3676,6 @@ def _give_reward(self,volume:float,side:int): time.sleep(0.1) self.MainWindow.Channel3.RandomWater_Left(int(1)) self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 - print(float(left_valve_open_time)) elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 # set the right valve open time @@ -3685,7 +3684,6 @@ def _give_reward(self,volume:float,side:int): time.sleep(0.1) self.MainWindow.Channel3.RandomWater_Right(int(1)) self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 - print(float(right_valve_open_time)) def _WhichSpout(self): '''Select the lick spout to use and disable non-relevant widgets''' From 9bd313d7f591de011af36fee456588b969ab8420 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:56:56 -0800 Subject: [PATCH 132/184] using the spout name --- 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 0c7a4be42..282d82f6a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3611,10 +3611,14 @@ def _start_random_reward(self,update_label): self._save_data(volume=volume, side=side, interval=interval,timestamp_computer=timestamp_computer,timestamp_harp=timestamp_harp) # show current cycle and parameters # Emit signal to update the label + if side==0: + side_spout='Left' + elif side==1: + side_spout='Right' update_label( f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" f"Volume: {volume} uL\n" - f"Side: {side}\n" + f"Side: {side_spout}\n" f"Interval: {interval} s" ) # wait to start the next cycle From e46c7826d6d8d398fdd33a644240dbfa18cab1ce Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:59:12 -0800 Subject: [PATCH 133/184] adding save button --- src/foraging_gui/RandomReward.ui | 56 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 8e022757e..596cbed29 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -7,7 +7,7 @@ 0 0 606 - 460 + 523 @@ -17,7 +17,7 @@ 180 - 60 + 50 81 31 @@ -33,7 +33,7 @@ 180 - 98 + 128 81 20 @@ -70,7 +70,7 @@ 100 - 98 + 128 76 20 @@ -98,7 +98,7 @@ 180 - 170 + 200 81 20 @@ -120,7 +120,7 @@ 60 - 140 + 170 116 20 @@ -142,7 +142,7 @@ 180 - 140 + 170 81 20 @@ -164,7 +164,7 @@ 60 - 170 + 200 116 20 @@ -186,7 +186,7 @@ 45 - 200 + 230 131 20 @@ -214,7 +214,7 @@ 20 - 240 + 270 151 20 @@ -239,7 +239,7 @@ 180 - 240 + 270 81 20 @@ -271,7 +271,7 @@ 180 - 200 + 230 81 22 @@ -290,7 +290,7 @@ 180 - 270 + 300 81 20 @@ -312,7 +312,7 @@ 60 - 270 + 300 116 20 @@ -331,7 +331,7 @@ 60 - 300 + 330 116 20 @@ -353,7 +353,7 @@ 180 - 300 + 330 81 20 @@ -375,7 +375,7 @@ 60 - 330 + 360 116 20 @@ -397,7 +397,7 @@ 180 - 330 + 360 81 20 @@ -457,7 +457,7 @@ 160 - 360 + 390 101 31 @@ -470,7 +470,7 @@ 160 - 400 + 430 101 31 @@ -479,6 +479,22 @@ Clear data + + + + 180 + 90 + 81 + 31 + + + + Save + + + true + + From dd4e62820386956dfb73ff827651ab4d2c023cc4 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:59:50 -0800 Subject: [PATCH 134/184] not checkable --- src/foraging_gui/RandomReward.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 596cbed29..55aa4e462 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -492,7 +492,7 @@ Save - true + false From 717b1d2297ce6e1caf5359c950e34a7843c9f5b8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:07:07 -0800 Subject: [PATCH 135/184] saving the random reward results --- src/foraging_gui/Dialogs.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 282d82f6a..27dc67f01 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3532,6 +3532,32 @@ def _connectSignalsSlots(self): self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) self.StartOver.clicked.connect(self._start_over) self.ClearData.clicked.connect(self._clear_data) + self.Save.clicked.connect(self._Save) + + def _Save(self): + '''Save the random reward results''' + if self.random_reward_par=={}: + return + # get the save folder + save_folder = QFileDialog.getExistingDirectory(self, 'Select the folder to save the random reward results') + if save_folder=='': + return + self.random_reward_par['task_parameters'] = { + 'task': 'Random reward', + 'spout': self.WhichSpout.currentText(), + 'left_reward_volume': self.LeftVolume.text(), + 'right_reward_volume': self.RightVolume.text(), + 'reward_number_each_condition': self.RewardN.text(), + 'interval_distribution': self.IntervalDistribution.currentText(), + 'interval_beta': self.IntervalBeta.text(), + 'interval_min': self.IntervalMin.text(), + 'interval_max': self.IntervalMax.text(), + } + # create the file name AnimalID_Date(day+hour+minute)_RandomRewardResults.csv + save_file = os.path.join(save_folder, f"{self.MainWindow.ID.text()}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_RandomRewardResults.json") + # save the data + with open(save_file, 'w') as f: + json.dump(self.random_reward_par, f, indent=4) def _clear_data(self): '''Clear the data''' From 90d6ee1d260d9e7100ae91c2cb841aed91a14b11 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:14:15 -0800 Subject: [PATCH 136/184] adding 0.2 --- src/foraging_gui/Dialogs.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 27dc67f01..771671db2 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3647,8 +3647,8 @@ def _start_random_reward(self,update_label): f"Side: {side_spout}\n" f"Interval: {interval} s" ) - # wait to start the next cycle - time.sleep(interval) + # wait to start the next cycle (minus 0.2s to account for the delay to wait for the value to be set) + time.sleep(interval-0.2) else: break @@ -3702,16 +3702,18 @@ def _give_reward(self,volume:float,side:int): left_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Left'][1])/self.MainWindow.latest_fitting['Left'][0])*1000 # set the left valve open time self.MainWindow.Channel.LeftValue(float(left_valve_open_time)) - # open the left valve - time.sleep(0.1) + # add 0.5s for the value to be set + time.sleep(0.2) + # open the left valve (adding 0.5s for the value to be set) self.MainWindow.Channel3.RandomWater_Left(int(1)) self.random_reward_par['RandomWaterVolume'][0]=self.random_reward_par['RandomWaterVolume'][0]+float(volume)/1000 elif side==1: right_valve_open_time=((float(volume)-self.MainWindow.latest_fitting['Right'][1])/self.MainWindow.latest_fitting['Right'][0])*1000 - # set the right valve open time + # set the right valve open time self.MainWindow.Channel.RightValue(float(right_valve_open_time)) - # open the right valve - time.sleep(0.1) + # add 0.5s for the value to be set + time.sleep(0.2) + # open the left valve (adding 0.5s for the value to be set) self.MainWindow.Channel3.RandomWater_Right(int(1)) self.random_reward_par['RandomWaterVolume'][1]=self.random_reward_par['RandomWaterVolume'][1]+float(volume)/1000 From baff49ad238dcb4ba882e48a7890592fd0de4020 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:15:50 -0800 Subject: [PATCH 137/184] saving random reward parameters --- src/foraging_gui/Foraging.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 3e481bb8a..ea6b0fcb5 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2590,7 +2590,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','OpticalTagging_dialog'] + dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','Metadata_dialog','OpticalTagging_dialog','RandomReward_dialog'] for dialog_name in dialogs: if hasattr(self, dialog_name): widget_dict = {w.objectName(): w for w in getattr(self, dialog_name).findChildren( @@ -2712,6 +2712,10 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): # save optical tagging parameters Obj['optical_tagging_par']=self.OpticalTagging_dialog.optical_tagging_par + + # save random reward parameters + Obj['random_reward_par']=self.RandomReward_dialog.random_reward_par + # generate the metadata file and update slims try: # save the metadata collected in the metadata dialogue From 452861fdde59f8cef2b99bfbd9319badb6ed9386 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:16:52 -0800 Subject: [PATCH 138/184] loading random reward parameters --- 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 ea6b0fcb5..774c04994 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2715,7 +2715,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): # save random reward parameters Obj['random_reward_par']=self.RandomReward_dialog.random_reward_par - + # generate the metadata file and update slims try: # save the metadata collected in the metadata dialogue @@ -3081,7 +3081,7 @@ def _Open(self,open_last = False,input_file = ''): self.Obj = Obj widget_dict={} - dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','centralwidget','TrainingParameters','OpticalTagging_dialog'] + dialogs = ['LaserCalibration_dialog', 'Opto_dialog', 'Camera_dialog','centralwidget','TrainingParameters','OpticalTagging_dialog','RandomReward_dialog'] for dialog_name in dialogs: if hasattr(self, dialog_name): widget_types = (QtWidgets.QPushButton, QtWidgets.QLineEdit, QtWidgets.QTextEdit, From 41066373c70e733be438d6d22d4801a5f3e29552 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:21:16 -0800 Subject: [PATCH 139/184] removing copy --- src/foraging_gui/Dialogs copy.py | 3527 ------------------------------ 1 file changed, 3527 deletions(-) delete mode 100644 src/foraging_gui/Dialogs copy.py diff --git a/src/foraging_gui/Dialogs copy.py b/src/foraging_gui/Dialogs copy.py deleted file mode 100644 index 342a3a15d..000000000 --- a/src/foraging_gui/Dialogs copy.py +++ /dev/null @@ -1,3527 +0,0 @@ -import time -import math -import json -import os -import shutil -import subprocess -from datetime import datetime -import logging -import webbrowser -import re -import random -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 -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,WorkerTagging -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 -from foraging_gui.GenerateMetadata import generate_metadata -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() - self.MainWindow = MainWindow - self.current_optical_tagging_par={} - 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) - 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(day+hour+minute)_OpticalTaggingResults.csv - 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) - - 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") - return - # generate random conditions including lasers, laser power, laser color, and protocol - 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.cycle_finish_tag = 0 - - # send the trigger source - self.MainWindow.Channel.TriggerSource('/Dev1/PFI0') - - # start the optical tagging in a different thread - 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) - - # 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() - - def _emegency_stop(self): - '''Stop the optical tagging''' - self.cycle_finish_tag = 1 - self.Start.setChecked(False) - self.Start.setStyleSheet("background-color : none") - - def _thread_complete_tag(self): - '''Complete the optical tagging''' - # 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") - # 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''' - # 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] - 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_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.Channel.receive() - - if Rec[0].address=='/ITIStartTimeHarp': - laser_start_timestamp=Rec[1][1][0] - # change the success_tag to 1 - success_tag=1 - else: - laser_start_timestamp=-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, - laser_start_timestamp=laser_start_timestamp, - success_tag=success_tag - ) - # wait to start the next cycle - time.sleep(duration_each_cycle+interval_between_cycles) - # show current cycle and parameters - # 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"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''' - 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']=[] - 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) - 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) - 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''' - # start generating waveform in bonsai - self.MainWindow.Channel.OptogeneticsCalibration(int(1)) - - 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 duration. - - 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 (ms)**: Applied to both lasers during the cycle. - """ - # 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_condition.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_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) - } - elif self.WhichLaser.currentText() in ['Laser_1','Laser_2']: - 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) - } - 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,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) - 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 extract_numbers_from_string(self.Duration_each_cycle.text()) - for interval_between_cycles in extract_numbers_from_string(self.Interval_between_cycles.text()) - ]) - - 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 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 - 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] - 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) - 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(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''' - 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) - - 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 - ) - 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, - duration_each_cycle=duration_each_cycle - ) - - return my_wave - - 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). - 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 - 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_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) - 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. - laser_color: The color of the laser. - protocol: The protocol to use. - Returns: - float: The amplitude of the laser. - ''' - # get the current calibration results - latest_calibration_date=find_latest_calibration_date(self.MainWindow.LaserCalibrationResults,laser_color) - # get the selected laser - if latest_calibration_date=='NA': - logger.info(f"No calibration results found for {laser_color}") - return - else: - 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=fit_calibration_results(calibration_results) - # Find the corresponding input voltage for a target laser power - input_voltage_for_target = (target_power - intercept) / slope - return round(input_voltage_for_target, 2) - -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) - - # Extract model coefficients - slope = model.coef_[0] - intercept = model.intercept_ - - return slope, intercept - -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 color. - - Returns: - str: The latest calibration date for the selected laser. - """ - Dates=[] - for Date in calibration: - if laser_color in calibration[Date].keys(): - Dates.append(Date) - sorted_dates = sorted(Dates) - if sorted_dates==[]: - return 'NA' - else: - 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)] - - -class RandomRewardDialog(QDialog): - - def __init__(self, MainWindow, parent=None): - super().__init__(parent) - uic.loadUi('RandomReward.ui', self) - self._connectSignalsSlots() - self.MainWindow = MainWindow - 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): - pass \ No newline at end of file From 9733096a678d936109e2aa1382904e034817d836 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:40:22 -0800 Subject: [PATCH 140/184] adding random reward stimulus --- src/foraging_gui/GenerateMetadata.py | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index a0b86b63f..ab3d6e243 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -755,8 +755,44 @@ def _get_stimulus(self): self._get_behavior_stimulus() self._get_optogenetics_stimulus() self._get_optical_tagging_stimulus() + self._get_random_reward_stimulus() self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus+self.optical_tagging_stimulus + def _get_random_reward_stimulus(self): + ''' + Make the random reward stimulus metadata + ''' + self.random_reward_stimulus=[] + if self.Obj['RandomReward_dialog']=={} or self.Obj['random_reward_par']['random_reward_start_time']=='' or self.Obj['random_reward_par']['random_reward_end_time']=='': + logging.info('No random reward detected!') + return + self.random_reward_stimulus.append(StimulusEpoch( + software=self.behavior_software, + stimulus_device_names=['Reward lick spout'], # this should be improved in the future + stimulus_name='The random reward stimulus', + stimulus_modalities=[StimulusModality.NONE], + stimulus_start_time=self.Obj['random_reward_par']['random_reward_start_time'], + stimulus_end_time=self.Obj['random_reward_par']['random_reward_end_time'], + output_parameters=self._get_random_reward_output_parameters(), + reward_consumed_during_epoch=np.sum(self.Obj['random_reward_par']['RandomWaterVolume']), + reward_consumed_unit="microliter", + trials_total= len(self.Obj['random_reward_par']['volumes']), + )) + + def _get_random_reward_output_parameters(self): + '''Get the output parameters for random reward''' + output_parameters = { + 'spout':self.Obj['random_reward_par']['WhichSpout'], + 'left_reward_volume':self.Obj['random_reward_par']['LeftVolume'], + 'right_reward_volume':self.Obj['random_reward_par']['RightVolume'], + 'reward_number_each_condition':self.Obj['random_reward_par']['RewardN'], + 'interval_distribution':self.Obj['random_reward_par']['IntervalDistribution'], + 'interval_beta':self.Obj['random_reward_par']['IntervalBeta'], + 'interval_min':self.Obj['random_reward_par']['IntervalMin'], + 'interval_max':self.Obj['random_reward_par']['IntervalMax'] + } + return output_parameters + def _get_optical_tagging_stimulus(self): ''' Make the optical tagging stimulus metadata From 296bc9db3b890e5e4a8f87bc2ae51d58e2902a96 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:42:21 -0800 Subject: [PATCH 141/184] handle edge cases --- src/foraging_gui/GenerateMetadata.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index ab3d6e243..33c358718 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -491,6 +491,17 @@ def _handle_edge_cases(self): if 'optical_tagging_end_time' not in self.Obj['optical_tagging_par']: self.Obj['optical_tagging_par']['optical_tagging_end_time'] = '' + # Handle the edge cases for the random reward + if 'RandomReward_dialog' not in self.Obj: + self.Obj['RandomReward_dialog'] = {} + if 'random_reward_par' not in self.Obj: + self.Obj['random_reward_par'] = {} + if 'random_reward_start_time' not in self.Obj['random_reward_par']: + self.Obj['random_reward_par']['random_reward_start_time'] = '' + if 'random_reward_end_time' not in self.Obj['random_reward_par']: + self.Obj['random_reward_par']['random_reward_end_time'] = '' + + def _initialize_fields(self,dic,keys,default_value=''): ''' Initialize fields From 5be817c2a09f21e90890458e6e4c0829b5470438 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:47:01 -0800 Subject: [PATCH 142/184] Saving is permitted when we only have random reward stimulus --- src/foraging_gui/GenerateMetadata.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 33c358718..9aee70126 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -501,7 +501,6 @@ def _handle_edge_cases(self): if 'random_reward_end_time' not in self.Obj['random_reward_par']: self.Obj['random_reward_par']['random_reward_end_time'] = '' - def _initialize_fields(self,dic,keys,default_value=''): ''' Initialize fields @@ -536,10 +535,16 @@ def _session(self): self._get_ophys_stream() self._get_high_speed_camera_stream() self._get_session_time() - if self.session_start_time == '' or self.session_end_time == '': + if (self.session_start_time == '' or self.session_end_time == '') or (self.Obj['random_reward_par']['random_reward_start_time'] or self.Obj['random_reward_par']['random_reward_end_time']): logging.info('session start time or session end time is empty!') return - + if self.session_start_time == '' or self.session_end_time == '': + start_time=self.Obj['random_reward_par']['random_reward_start_time'] + end_time=self.Obj['random_reward_par']['random_reward_end_time'] + else: + start_time=self.session_start_time + end_time=self.session_end_time + self._get_stimulus() self._combine_data_streams() #self.data_streams = self.ephys_streams+self.ophys_streams+self.high_speed_camera_streams @@ -547,8 +552,8 @@ def _session(self): session_params = { "experimenter_full_name": [self.Obj['Experimenter']], "subject_id": self.Obj['ID'], - "session_start_time": self.session_start_time, - "session_end_time": self.session_end_time, + "session_start_time": start_time, + "session_end_time": end_time, "session_type": self.Obj['Task'], "iacuc_protocol": self.Obj['meta_data_dialog']['session_metadata']['IACUCProtocol'], "rig_id": self.Obj['meta_data_dialog']['rig_metadata']['rig_id'], From ae8dbbde741fe6fc67ae8044b704c7cdd53dffdd Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:54:41 -0800 Subject: [PATCH 143/184] changing the logic --- 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 9aee70126..626be9389 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -535,7 +535,7 @@ def _session(self): self._get_ophys_stream() self._get_high_speed_camera_stream() self._get_session_time() - if (self.session_start_time == '' or self.session_end_time == '') or (self.Obj['random_reward_par']['random_reward_start_time'] or self.Obj['random_reward_par']['random_reward_end_time']): + if (self.session_start_time == '' or self.session_end_time == '') and (self.Obj['random_reward_par']['random_reward_start_time']=='' or self.Obj['random_reward_par']['random_reward_end_time']==''): logging.info('session start time or session end time is empty!') return if self.session_start_time == '' or self.session_end_time == '': @@ -544,7 +544,7 @@ def _session(self): else: start_time=self.session_start_time end_time=self.session_end_time - + self._get_stimulus() self._combine_data_streams() #self.data_streams = self.ephys_streams+self.ophys_streams+self.high_speed_camera_streams From fe6276a280dbafb5457a45e8dbe0a4839ba0f703 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 17:02:21 -0800 Subject: [PATCH 144/184] adding random reward to the session metadata --- src/foraging_gui/GenerateMetadata.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 626be9389..66e69e8c1 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -772,7 +772,7 @@ def _get_stimulus(self): self._get_optogenetics_stimulus() self._get_optical_tagging_stimulus() self._get_random_reward_stimulus() - self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus+self.optical_tagging_stimulus + self.stimulus=self.behavior_stimulus+self.optogenetics_stimulus+self.optical_tagging_stimulus+self.random_reward_stimulus def _get_random_reward_stimulus(self): ''' @@ -798,14 +798,14 @@ def _get_random_reward_stimulus(self): def _get_random_reward_output_parameters(self): '''Get the output parameters for random reward''' output_parameters = { - 'spout':self.Obj['random_reward_par']['WhichSpout'], - 'left_reward_volume':self.Obj['random_reward_par']['LeftVolume'], - 'right_reward_volume':self.Obj['random_reward_par']['RightVolume'], - 'reward_number_each_condition':self.Obj['random_reward_par']['RewardN'], - 'interval_distribution':self.Obj['random_reward_par']['IntervalDistribution'], - 'interval_beta':self.Obj['random_reward_par']['IntervalBeta'], - 'interval_min':self.Obj['random_reward_par']['IntervalMin'], - 'interval_max':self.Obj['random_reward_par']['IntervalMax'] + 'spout':self.Obj['RandomReward_dialog']['WhichSpout'], + 'left_reward_volume':self.Obj['RandomReward_dialog']['LeftVolume'], + 'right_reward_volume':self.Obj['RandomReward_dialog']['RightVolume'], + 'reward_number_each_condition':self.Obj['RandomReward_dialog']['RewardN'], + 'interval_distribution':self.Obj['RandomReward_dialog']['IntervalDistribution'], + 'interval_beta':self.Obj['RandomReward_dialog']['IntervalBeta'], + 'interval_min':self.Obj['RandomReward_dialog']['IntervalMin'], + 'interval_max':self.Obj['RandomReward_dialog']['IntervalMax'] } return output_parameters @@ -1503,6 +1503,6 @@ def _get_reward_delivery(self): if __name__ == '__main__': - generate_metadata(json_file=r'Y:\753126\behavior_753126_2024-10-07_10-14-07\behavior\753126_2024-10-07_10-14-07.json',output_folder=r'H:\test') + generate_metadata(json_file=r'I:\BehaviorData\323_EPHYS3\0\behavior_0_2024-12-31_16-55-18\behavior\0_2024-12-31_16-55-18.json',output_folder=r'H:\test') #generate_metadata(json_file=r'Y:\753126\behavior_753126_2024-10-15_12-20-35\behavior\753126_2024-10-15_12-20-35.json',output_folder=r'H:\test') #generate_metadata(json_file=r'F:\Test\Metadata\715083_2024-04-22_14-32-07.json', dialog_metadata_file=r'C:\Users\xinxin.yin\Documents\ForagingSettings\metadata_dialog\323_EPHYS3_2024-05-09_12-42-30_metadata_dialog.json', output_folder=r'F:\Test\Metadata') From 9a0e342fe1994c320bf77310b0c0f62d3e556763 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Tue, 31 Dec 2024 17:06:33 -0800 Subject: [PATCH 145/184] get software name ealier --- 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 66e69e8c1..16023a202 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -526,6 +526,7 @@ def _session(self): if self.Obj['meta_data_dialog']['rig_metadata']=={}: logging.info('rig metadata is empty!') return + self._get_behavior_software() self._get_reward_delivery() self._get_water_calibration() self._get_opto_calibration() @@ -1217,7 +1218,6 @@ def _get_behavior_stream(self): daq_names=self.name_mapper['rig_daq_names_janelia_lick_detector'] self.behavior_streams=[] - self._get_behavior_software() self.behavior_streams.append(Stream( stream_modalities=[Modality.BEHAVIOR], stream_start_time=datetime.strptime(self.Obj['Other_SessionStartTime'], '%Y-%m-%d %H:%M:%S.%f'), @@ -1503,6 +1503,6 @@ def _get_reward_delivery(self): if __name__ == '__main__': - generate_metadata(json_file=r'I:\BehaviorData\323_EPHYS3\0\behavior_0_2024-12-31_16-55-18\behavior\0_2024-12-31_16-55-18.json',output_folder=r'H:\test') + generate_metadata(json_file=r'I:\BehaviorData\323_EPHYS3\0\behavior_0_2024-12-31_17-03-06\behavior\0_2024-12-31_17-03-06.json',output_folder=r'H:\test') #generate_metadata(json_file=r'Y:\753126\behavior_753126_2024-10-15_12-20-35\behavior\753126_2024-10-15_12-20-35.json',output_folder=r'H:\test') #generate_metadata(json_file=r'F:\Test\Metadata\715083_2024-04-22_14-32-07.json', dialog_metadata_file=r'C:\Users\xinxin.yin\Documents\ForagingSettings\metadata_dialog\323_EPHYS3_2024-05-09_12-42-30_metadata_dialog.json', output_folder=r'F:\Test\Metadata') From 7c40576fe8ddca14a733018d3d7ee56ac25cfca8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:45:15 -0800 Subject: [PATCH 146/184] changing name to start over --- 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 bef089379..aa736326f 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -713,7 +713,7 @@ false
- + 130 @@ -723,7 +723,7 @@ - Emergency Stop + Start over From ac3defbf5fe310d3a953077b925ec280fde65be4 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:46:32 -0800 Subject: [PATCH 147/184] changing name to start over --- 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 f0f7713db..fe706a3c4 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3055,7 +3055,7 @@ def __init__(self, MainWindow, parent=None): def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) - self.EmergencyStop.clicked.connect(self._emegency_stop) + self.StartOver.clicked.connect(self._start_over) self.Save.clicked.connect(self._Save) def _Save(self): @@ -3105,7 +3105,7 @@ def _Start(self): self.threadpool.start(worker_tagging) #self._start_optical_tagging() - def _emegency_stop(self): + def _start_over(self): '''Stop the optical tagging''' self.cycle_finish_tag = 1 self.Start.setChecked(False) From d85c14cae72ba5d3f2b7b2d96840cd0bb182bd6d Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:50:06 -0800 Subject: [PATCH 148/184] adding clear data button --- src/foraging_gui/Dialogs.py | 22 +++++++++++++++++++--- src/foraging_gui/OpticalTagging.ui | 15 ++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index fe706a3c4..505a81ac7 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3057,6 +3057,7 @@ def _connectSignalsSlots(self): self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) self.StartOver.clicked.connect(self._start_over) self.Save.clicked.connect(self._Save) + self.ClearData.clicked.connect(self._clear_data) def _Save(self): '''Save the optical tagging results''' @@ -3105,11 +3106,26 @@ def _Start(self): self.threadpool.start(worker_tagging) #self._start_optical_tagging() + def _clear_data(self): + '''Clear the optical tagging data''' + # ask for confirmation + reply = QMessageBox.question(self, 'Message', 'Are you sure to clear the optical tagging data?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.optical_tagging_par={} + self.label_show_current.setText('') + self.LocationTag.setValue(0) + self.cycle_finish_tag = 1 + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + def _start_over(self): '''Stop the optical tagging''' - self.cycle_finish_tag = 1 - self.Start.setChecked(False) - self.Start.setStyleSheet("background-color : none") + # ask for confirmation + reply = QMessageBox.question(self, 'Message', 'Are you sure to start over the optical tagging?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.cycle_finish_tag = 1 + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") def _thread_complete_tag(self): '''Complete the optical tagging''' diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index aa736326f..58d2ed50d 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -7,7 +7,7 @@ 0 0 501 - 575 + 582
@@ -742,6 +742,19 @@ false + + + + 130 + 530 + 101 + 31 + + + + Clear data + + From 81d5cc70e4177b7dac2ed632982ae15a7e2136c9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:53:10 -0800 Subject: [PATCH 149/184] changing the button name to Restart --- 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 505a81ac7..7a0881971 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3055,7 +3055,7 @@ def __init__(self, MainWindow, parent=None): def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichLaser.currentIndexChanged.connect(self._WhichLaser) - self.StartOver.clicked.connect(self._start_over) + self.Restart.clicked.connect(self._start_over) self.Save.clicked.connect(self._Save) self.ClearData.clicked.connect(self._clear_data) @@ -3119,7 +3119,7 @@ def _clear_data(self): self.Start.setStyleSheet("background-color : none") def _start_over(self): - '''Stop the optical tagging''' + '''Stop the optical tagging and start over (parameters will be shuffled)''' # ask for confirmation reply = QMessageBox.question(self, 'Message', 'Are you sure to start over the optical tagging?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: diff --git a/src/foraging_gui/OpticalTagging.ui b/src/foraging_gui/OpticalTagging.ui index 58d2ed50d..deef36c0f 100644 --- a/src/foraging_gui/OpticalTagging.ui +++ b/src/foraging_gui/OpticalTagging.ui @@ -713,7 +713,7 @@ false
- + 130 @@ -723,7 +723,7 @@ - Start over + Restart From 0b6e9ffcfe0a8eacc4a9662a0e7cfc3474a358d7 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:55:54 -0800 Subject: [PATCH 150/184] changing the name to Restart --- src/foraging_gui/Dialogs.py | 4 ++-- src/foraging_gui/RandomReward.ui | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 4d1627543..f0ec6793f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3546,7 +3546,7 @@ def __init__(self, MainWindow, parent=None): def _connectSignalsSlots(self): self.Start.clicked.connect(self._Start) self.WhichSpout.currentIndexChanged.connect(self._WhichSpout) - self.StartOver.clicked.connect(self._start_over) + self.Restart.clicked.connect(self._start_over) self.ClearData.clicked.connect(self._clear_data) self.Save.clicked.connect(self._Save) @@ -3592,7 +3592,7 @@ def _clear_data(self): self.label_show_current.setText('') def _start_over(self): - '''Stop the random reward''' + '''Stop the random reward and start over (parameters will be shuffled)''' # ask user if they want to start over reply = QMessageBox.question(self, 'Message', 'Do you want to start over?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 55aa4e462..68ed831c7 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -453,7 +453,7 @@ false
- + 160 @@ -463,7 +463,7 @@ - Start over + Restart From d5e6b608a699ebcfdf15da83eb0b71fcbcd7e1d5 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:57:53 -0800 Subject: [PATCH 151/184] format --- src/foraging_gui/Foraging.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 774c04994..5caa4879e 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2737,8 +2737,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): if self.BaseWeight.text() != '' and self.WeightAfter.text() != '' and self.behavior_session_model.subject not in ['0','1','2','3','4','5','6','7','8','9','10']: self._AddWaterLogResult(session) self.bias_indicator.clear() # prepare for new session - - + except Exception as e: logging.warning('Meta data is not saved!', extra= {'tags': {self.warning_log_tag}}) logging.error('Error generating session metadata: '+str(e)) From 523a3ee5bedb13a6fd3d463ae891f8f07b9c41d4 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:34:14 -0800 Subject: [PATCH 152/184] adding random reward water to total water --- src/foraging_gui/GenerateMetadata.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 16023a202..8b52716b9 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -464,7 +464,9 @@ def _handle_edge_cases(self): self.trials_finished=np.count_nonzero(self.Obj['B_AnimalResponseHistory']!=2) self.trials_rewarded=np.count_nonzero(np.logical_or(self.Obj['B_RewardedHistory'][0],self.Obj['B_RewardedHistory'][1])) self.total_reward_consumed_in_session= float(self.Obj.get('BS_TotalReward', 0)) - + if 'random_reward_par' in self.Obj: + if 'RandomWaterVolume' in self.Obj['random_reward_par']: + self.total_reward_consumed_in_session+=np.sum(self.Obj['random_reward_par']['RandomWaterVolume']) # Wrong format of WeightAfter # Remove all the non-numeric characters except the dot in the WeightAfter if self.Obj['WeightAfter']!='': From b168bf3d04387e0525b8199f7c1688ffc8bf9b12 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:49:57 -0800 Subject: [PATCH 153/184] saving the data --- src/foraging_gui/Dialogs.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index f0ec6793f..eebc544f5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3554,10 +3554,23 @@ def _Save(self): '''Save the random reward results''' if self.random_reward_par=={}: return + # giving the user a warning message to show "This will only save the current parameters and results related to the random reward. If you want to save more including the metadata, please go to the main window and click the save button." + QMessageBox.warning( + self, + "Save Warning", + "Only the current parameters and results related to the random reward will be saved. " + "To save additional data, including metadata, please use the Save button in the main window." + ) # get the save folder - save_folder = QFileDialog.getExistingDirectory(self, 'Select the folder to save the random reward results') - if save_folder=='': - return + if self.MainWindow.CreateNewFolder == 1: + self._GetSaveFolder() + self.CreateNewFolder = 0 + + save_file=self.MainWindow.SaveFileJson + if not os.path.exists(os.path.dirname(save_file)): + os.makedirs(os.path.dirname(save_file)) + logging.info(f"Created new folder: {os.path.dirname(save_file)}") + self.random_reward_par['task_parameters'] = { 'task': 'Random reward', 'spout': self.WhichSpout.currentText(), @@ -3569,9 +3582,8 @@ def _Save(self): 'interval_min': self.IntervalMin.text(), 'interval_max': self.IntervalMax.text(), } - # create the file name AnimalID_Date(day+hour+minute)_RandomRewardResults.csv - save_file = os.path.join(save_folder, f"{self.MainWindow.ID.text()}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_RandomRewardResults.json") - # save the data + + # save the data in the standard format and folder with open(save_file, 'w') as f: json.dump(self.random_reward_par, f, indent=4) @@ -3604,6 +3616,10 @@ def _start_over(self): def _Start(self): '''Start giving random rewards''' + # restart the logging if it is not started + if self.MainWindow.logging_type!=0 or self.MainWindow.logging_type==-1: + self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() + # toggle the button color if self.Start.isChecked(): self.Start.setStyleSheet("background-color : green;") From f3c4c5414dddc77e4dfa4e38cf6b88f226e781d3 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:54:05 -0800 Subject: [PATCH 154/184] adding the MainWindow --- 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 eebc544f5..6105e49d5 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3563,8 +3563,8 @@ def _Save(self): ) # get the save folder if self.MainWindow.CreateNewFolder == 1: - self._GetSaveFolder() - self.CreateNewFolder = 0 + self.MainWindow._GetSaveFolder() + self.MainWindow.CreateNewFolder = 0 save_file=self.MainWindow.SaveFileJson if not os.path.exists(os.path.dirname(save_file)): From 9bf33a7e63ad7e90a0149f4ed3fadebe2b723599 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:57:54 -0800 Subject: [PATCH 155/184] restart logging --- src/foraging_gui/Dialogs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 7a0881971..c1305840b 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3075,6 +3075,10 @@ def _Save(self): def _Start(self): '''Start the optical tagging''' + # restart the logging if it is not started + if self.MainWindow.logging_type!=0 or self.MainWindow.logging_type==-1: + self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() + # toggle the button color if self.Start.isChecked(): self.Start.setStyleSheet("background-color : green;") From 4fd3d43b30d16ac1a2d05c748277d018be029dd6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 20:08:20 -0800 Subject: [PATCH 156/184] saving the data --- src/foraging_gui/Dialogs.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index c1305840b..0e1bfe6e9 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3063,12 +3063,36 @@ def _Save(self): '''Save the optical tagging results''' if self.optical_tagging_par=={}: return + # giving the user a warning message to show "This will only save the current parameters and results related to the random reward. If you want to save more including the metadata, please go to the main window and click the save button." + QMessageBox.warning( + self, + "Save Warning", + "Only the current parameters and results related to the optical tagging will be saved. " + "To save additional data, including metadata, please use the Save button in the main window." + ) # 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(day+hour+minute)_OpticalTaggingResults.csv - save_file = os.path.join(save_folder, f"{self.MainWindow.ID.text()}_{datetime.now().strftime('%Y-%m-%d-%H-%M')}_OpticalTaggingResults.json") + if self.MainWindow.CreateNewFolder == 1: + self.MainWindow._GetSaveFolder() + self.MainWindow.CreateNewFolder = 0 + + save_file=self.MainWindow.SaveFileJson + if not os.path.exists(os.path.dirname(save_file)): + os.makedirs(os.path.dirname(save_file)) + logging.info(f"Created new folder: {os.path.dirname(save_file)}") + + self.optical_tagging_par["task_parameters"]={ + "laser_name": self.WhichLaser.currentText(), + "protocol": self.Protocol.currentText(), + "laser_1_color": self.Laser_1_color.text(), + "laser_2_color": self.Laser_2_color.text(), + "laser_1_power": self.Laser_1_power.text(), + "laser_2_power": self.Laser_2_power.text(), + "frequency": self.Frequency.text(), + "pulse_duration": self.Pulse_duration.text(), + "duration_each_cycle": self.Duration_each_cycle.text(), + "interval_between_cycles": self.Interval_between_cycles.text(), + "cycles_each_condition": self.Cycles_each_condition.text(), + } # save the data with open(save_file, 'w') as f: json.dump(self.optical_tagging_par, f, indent=4) From cbac6f0fcf753d5b234b933012a2699c367557d6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:20:20 -0800 Subject: [PATCH 157/184] adding logging type check --- src/foraging_gui/Foraging.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 5caa4879e..0e334ec22 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -4094,14 +4094,15 @@ def _Start(self): try: # Do not start a new session if the camera is already open, this means the session log has been started or the existing session has not been completed. if (not (self.Camera_dialog.StartRecording.isChecked() and self.Camera_dialog.AutoControl.currentText()=='No')) and (not self.FIP_started): - # Turn off the camera recording - self.Camera_dialog.StartRecording.setChecked(False) - # Turn off the preview if it is on and the autocontrol is on, which can make sure the trigger is off before starting the logging. - if self.Camera_dialog.AutoControl.currentText()=='Yes' and self.Camera_dialog.StartPreview.isChecked(): - self.Camera_dialog.StartPreview.setChecked(False) - # sleep for 1 second to make sure the trigger is off - time.sleep(1) - self.Ot_log_folder=self._restartlogging() + if self.logging_type!=0: + # Turn off the camera recording + self.Camera_dialog.StartRecording.setChecked(False) + # Turn off the preview if it is on and the autocontrol is on, which can make sure the trigger is off before starting the logging. + if self.Camera_dialog.AutoControl.currentText()=='Yes' and self.Camera_dialog.StartPreview.isChecked(): + self.Camera_dialog.StartPreview.setChecked(False) + # sleep for 1 second to make sure the trigger is off + time.sleep(1) + self.Ot_log_folder=self._restartlogging() except Exception as e: if 'ConnectionAbortedError' in str(e): logging.info('lost bonsai connection: restartlogging()') From fa634864d6d91a7b283d4881dedaf32cf264734e Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:21:30 -0800 Subject: [PATCH 158/184] removing check logging type -1 --- 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 e6392f5e8..a76abc7d1 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -1251,7 +1251,7 @@ def _StartCamera(self): # 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: + if self.MainWindow.logging_type!=0: self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() # set to check drop frame as true self.MainWindow.to_check_drop_frames=1 @@ -3100,7 +3100,7 @@ def _Save(self): def _Start(self): '''Start the optical tagging''' # restart the logging if it is not started - if self.MainWindow.logging_type!=0 or self.MainWindow.logging_type==-1: + if self.MainWindow.logging_type!=0 : self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() # toggle the button color @@ -3645,7 +3645,7 @@ def _start_over(self): def _Start(self): '''Start giving random rewards''' # restart the logging if it is not started - if self.MainWindow.logging_type!=0 or self.MainWindow.logging_type==-1: + if self.MainWindow.logging_type!=0 : self.MainWindow.Ot_log_folder=self.MainWindow._restartlogging() # toggle the button color From 04a92b1839eb46a61da499be3e93a5b7ce5102ad Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:36:14 -0800 Subject: [PATCH 159/184] changing to currentText --- 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 0e1bfe6e9..1de45d6bd 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3083,8 +3083,8 @@ def _Save(self): self.optical_tagging_par["task_parameters"]={ "laser_name": self.WhichLaser.currentText(), "protocol": self.Protocol.currentText(), - "laser_1_color": self.Laser_1_color.text(), - "laser_2_color": self.Laser_2_color.text(), + "laser_1_color": self.Laser_1_color.currentText(), + "laser_2_color": self.Laser_2_color.currentText(), "laser_1_power": self.Laser_1_power.text(), "laser_2_power": self.Laser_2_power.text(), "frequency": self.Frequency.text(), From 40dbb9d52b23e6ba8ff98135712920266c787c8f Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:37:03 -0800 Subject: [PATCH 160/184] move sleep to the end --- src/foraging_gui/Dialogs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 1de45d6bd..1ab4d91ef 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3245,8 +3245,6 @@ def _start_optical_tagging(self,update_label): laser_start_timestamp=laser_start_timestamp, success_tag=success_tag ) - # wait to start the next cycle - time.sleep(duration_each_cycle+interval_between_cycles) # show current cycle and parameters # Emit signal to update the label update_label( @@ -3260,7 +3258,8 @@ def _start_optical_tagging(self,update_label): f"Duration: {duration_each_cycle} s\n" f"Interval: {interval_between_cycles} s" ) - + # wait to start the next cycle + time.sleep(duration_each_cycle+interval_between_cycles) else: break From c1c733c793d2cb319c606666ddcc02397cfb5fc8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:41:39 -0800 Subject: [PATCH 161/184] check the optical tagging time --- src/foraging_gui/GenerateMetadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index a0b86b63f..ae1e86fc4 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -525,7 +525,7 @@ def _session(self): self._get_ophys_stream() self._get_high_speed_camera_stream() self._get_session_time() - if self.session_start_time == '' or self.session_end_time == '': + if (self.session_start_time == '' or self.session_end_time == '') and (self.Obj['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['optical_tagging_par']['optical_tagging_start_time']==''): logging.info('session start time or session end time is empty!') return From dcc2f2989481f2572d609dd94062bfaf54dc9092 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:52:41 -0800 Subject: [PATCH 162/184] adding start and end time --- src/foraging_gui/GenerateMetadata.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 8b52716b9..dbe869eae 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -538,12 +538,15 @@ def _session(self): self._get_ophys_stream() self._get_high_speed_camera_stream() self._get_session_time() - if (self.session_start_time == '' or self.session_end_time == '') and (self.Obj['random_reward_par']['random_reward_start_time']=='' or self.Obj['random_reward_par']['random_reward_end_time']==''): + if (self.session_start_time == '' or self.session_end_time == '') and (self.Obj['random_reward_par']['random_reward_start_time']=='' or self.Obj['random_reward_par']['random_reward_end_time']=='') and (self.Obj['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['optical_tagging_par']['optical_tagging_start_time']==''): logging.info('session start time or session end time is empty!') return if self.session_start_time == '' or self.session_end_time == '': start_time=self.Obj['random_reward_par']['random_reward_start_time'] end_time=self.Obj['random_reward_par']['random_reward_end_time'] + if start_time=='' or end_time=='': + start_time=self.Obj['optical_tagging_par']['optical_tagging_start_time'] + end_time=self.Obj['optical_tagging_par']['optical_tagging_end_time'] else: start_time=self.session_start_time end_time=self.session_end_time From ffa6fca383837cb019c34e0a6516efb521a1af4d Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Thu, 2 Jan 2025 11:10:56 -0800 Subject: [PATCH 163/184] force the input_voltage to be 0 --- src/foraging_gui/Dialogs.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 1ab4d91ef..7513b78af 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3406,11 +3406,15 @@ def _WhichLaser(self): 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 - ) + if target_power==0: + # force the input_voltage to be 0 when the target_power is 0 + input_voltage=0 + else: + 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 From df5be082382a675e7d051dce2531db06098f82f6 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:39:21 -0800 Subject: [PATCH 164/184] adding check reward collection combobox --- src/foraging_gui/Dialogs.py | 2 + src/foraging_gui/RandomReward.ui | 64 +++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 9a82e52be..af6313141 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3708,6 +3708,8 @@ def _start_random_reward(self,update_label): ) # wait to start the next cycle (minus 0.2s to account for the delay to wait for the value to be set) time.sleep(interval-0.2) + # check if the reward has been collected by the animal + else: break diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 68ed831c7..030f59ad1 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -457,7 +457,7 @@ 160 - 390 + 430 101 31 @@ -470,7 +470,7 @@ 160 - 430 + 470 101 31 @@ -495,6 +495,66 @@ false + + + + 180 + 390 + 81 + 20 + + + + + 0 + 0 + + + + + 1000 + 16777215 + + + + + Yes + + + + + No + + + + + + true + + + + 20 + 390 + 151 + 20 + + + + + 1000 + 16777215 + + + + Check reward collection= + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + From 89d7766b82518ba69469cbe026f1478f27e93179 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:08:40 -0800 Subject: [PATCH 165/184] checking reward collection --- src/foraging_gui/Dialogs.py | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index af6313141..336218eb6 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3688,6 +3688,8 @@ def _start_random_reward(self,update_label): volume = self.current_random_reward_par['volumes_all_random'][i] side = self.current_random_reward_par['sides_all_random'][i] interval = self.current_random_reward_par['reward_intervals'][i] + # get all licks + self._get_lick_timestampes() # give the reward self._give_reward(volume=volume, side=side) # receiving the timestamps of reward start time. @@ -3708,11 +3710,42 @@ def _start_random_reward(self,update_label): ) # wait to start the next cycle (minus 0.2s to account for the delay to wait for the value to be set) time.sleep(interval-0.2) - # check if the reward has been collected by the animal - + if self.CheckRewardCollection.currentText()=='Yes': + # check if the reward has been collected by the animal + received_licks=self._get_lick_timestampes() + sleep_again=0 + if not received_licks: + sleep_again=1 + # if not received any licks, sleep until we receive a lick + while not received_licks: + time.sleep(0.01) + received_licks=self._get_lick_timestampes() + if sleep_again==1: + # sleep another interval-0.2s when detected a lick + time.sleep(interval-0.2) else: break - + + def _get_lick_timestampes(self)->bool: + '''Get the lick timestamps''' + if 'left_lick_time' not in self.random_reward_par: + self.random_reward_par['left_lick_time'] = [] + self.random_reward_par['right_lick_time'] = [] + + Return = False # no licks received + while not self.MainWindow.Channel2.msgs.empty(): + Rec = self.MainWindow.Channel2.receive() + address = Rec[0].address + lick_time = Rec[1][1][0] + + if address == '/LeftLickTime': + self.random_reward_par['left_lick_time'].append(lick_time) + elif address == '/RightLickTime': + self.random_reward_par['right_lick_time'].append(lick_time) + Return = True # licks received + + return Return + def _receving_timestamps(self,side:int): '''Receiving the timestamps of reward start time''' if side==0: From 375c914a10618257a68905eec3ab60089064bf89 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:10:34 -0800 Subject: [PATCH 166/184] adding label_show_2 --- src/foraging_gui/RandomReward.ui | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index 030f59ad1..febc75c4f 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -555,6 +555,39 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ + + true + + + + 310 + 420 + 201 + 61 + + + + + 1000 + 16777215 + + + + + 13 + + + + + + + Qt::AutoText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + From 6a7d99886a85f5aa5fc329b86441ab2fce8032e8 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:17:01 -0800 Subject: [PATCH 167/184] updating the reward collection check --- 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 336218eb6..e9591386c 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3716,10 +3716,25 @@ def _start_random_reward(self,update_label): sleep_again=0 if not received_licks: sleep_again=1 + # show the licks have not been received + update_label( + f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" + f"Volume: {volume} uL\n" + f"Side: {side_spout}\n" + f"Interval: {interval} s\n" + f"Reward not collected" + ) # if not received any licks, sleep until we receive a lick - while not received_licks: + while (not received_licks) and self.Start.isChecked(): time.sleep(0.01) received_licks=self._get_lick_timestampes() + update_label( + f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" + f"Volume: {volume} uL\n" + f"Side: {side_spout}\n" + f"Interval: {interval} s\n" + f"Reward collected" + ) if sleep_again==1: # sleep another interval-0.2s when detected a lick time.sleep(interval-0.2) From 25bec31806c69cda845a2aa66b0b924375e738cf Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:17:17 -0800 Subject: [PATCH 168/184] removing information 2 --- src/foraging_gui/RandomReward.ui | 33 -------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/foraging_gui/RandomReward.ui b/src/foraging_gui/RandomReward.ui index febc75c4f..030f59ad1 100644 --- a/src/foraging_gui/RandomReward.ui +++ b/src/foraging_gui/RandomReward.ui @@ -555,39 +555,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
- - - true - - - - 310 - 420 - 201 - 61 - - - - - 1000 - 16777215 - - - - - 13 - - - - - - - Qt::AutoText - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - From 5439cbaabc6cefc14c6c32f09495c44270bf9892 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Thu, 2 Jan 2025 12:26:41 -0800 Subject: [PATCH 169/184] adding user stop --- src/foraging_gui/Dialogs.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index e9591386c..735a5fc51 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3728,13 +3728,22 @@ def _start_random_reward(self,update_label): while (not received_licks) and self.Start.isChecked(): time.sleep(0.01) received_licks=self._get_lick_timestampes() - update_label( - f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" - f"Volume: {volume} uL\n" - f"Side: {side_spout}\n" - f"Interval: {interval} s\n" - f"Reward collected" - ) + if self.Start.isChecked(): + update_label( + f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" + f"Volume: {volume} uL\n" + f"Side: {side_spout}\n" + f"Interval: {interval} s\n" + f"User stopped" + ) + else: + update_label( + f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" + f"Volume: {volume} uL\n" + f"Side: {side_spout}\n" + f"Interval: {interval} s\n" + f"Reward collected" + ) if sleep_again==1: # sleep another interval-0.2s when detected a lick time.sleep(interval-0.2) From 6cc4bca6bdf70557a45057359e15a654b153a6d6 Mon Sep 17 00:00:00 2001 From: Xinxin Yin Date: Thu, 2 Jan 2025 12:28:35 -0800 Subject: [PATCH 170/184] adding not --- 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 735a5fc51..2a606c296 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3728,7 +3728,7 @@ def _start_random_reward(self,update_label): while (not received_licks) and self.Start.isChecked(): time.sleep(0.01) received_licks=self._get_lick_timestampes() - if self.Start.isChecked(): + if not self.Start.isChecked(): update_label( f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" f"Volume: {volume} uL\n" From bdcb8e707b4ad8eb4675cb44627648bfba3270e9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:40:43 -0800 Subject: [PATCH 171/184] check finish of the thread --- src/foraging_gui/Dialogs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 2a606c296..22cc1edf0 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3676,6 +3676,10 @@ def _Start(self): def _start_random_reward(self,update_label): '''Start the random reward in a different thread''' + if self.thread_finish_tag==0: + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") + return # iterate each condition self.thread_finish_tag = 0 for i in self.index[:]: From 5a69e12a79d69f3ae7de7c7cf929c3f305084be4 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:44:32 -0800 Subject: [PATCH 172/184] check index --- 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 22cc1edf0..c0d8db2b3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3687,7 +3687,11 @@ def _start_random_reward(self,update_label): if i == self.index[-1]: self.cycle_finish_tag = 1 # exclude the index that has been run - self.index.remove(i) + # check if i is in the index + if i in self.index: + self.index.remove(i) + else: + continue # get the current parameters volume = self.current_random_reward_par['volumes_all_random'][i] side = self.current_random_reward_par['sides_all_random'][i] From c6a1a0477b3c09628a0bd03a1a40acdade201c7b Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:45:00 -0800 Subject: [PATCH 173/184] sleep again only when the start is checked --- 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 c0d8db2b3..56f88aab3 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3752,9 +3752,9 @@ def _start_random_reward(self,update_label): f"Interval: {interval} s\n" f"Reward collected" ) - if sleep_again==1: - # sleep another interval-0.2s when detected a lick - time.sleep(interval-0.2) + if sleep_again==1: + # sleep another interval-0.2s when detected a lick + time.sleep(interval-0.2) else: break From 7b245b1efd62763fa6ed2d009e33e0d9c2b2c0d1 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:46:31 -0800 Subject: [PATCH 174/184] continue if received licks --- src/foraging_gui/Dialogs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 56f88aab3..379091474 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3732,6 +3732,8 @@ def _start_random_reward(self,update_label): f"Interval: {interval} s\n" f"Reward not collected" ) + else: + continue # if not received any licks, sleep until we receive a lick while (not received_licks) and self.Start.isChecked(): time.sleep(0.01) From 2eddeb25ab7a467fd571ead8f9eaae2a9b36ac9c Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:50:46 -0800 Subject: [PATCH 175/184] don't start a new thread --- 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 379091474..496b886fd 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3672,7 +3672,11 @@ def _Start(self): self.random_reward_par["random_reward_start_time"] = str(datetime.now()) # Execute - self.threadpool.start(worker_random_reward) + if self.thread_finish_tag==1: + self.threadpool.start(worker_random_reward) + else: + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") def _start_random_reward(self,update_label): '''Start the random reward in a different thread''' From 4ccb062b849b3ac613fda5d12e1929689aa3f712 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:56:24 -0800 Subject: [PATCH 176/184] adding thread finish tag --- src/foraging_gui/Dialogs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 496b886fd..ac3b75be4 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3045,6 +3045,7 @@ def __init__(self, MainWindow, parent=None): self.current_optical_tagging_par={} self.optical_tagging_par={} self.cycle_finish_tag = 1 + self.thread_finish_tag = 1 self.threadpool = QThreadPool() # find all buttons and set them to not be the default button for container in [self]: @@ -3131,7 +3132,11 @@ def _Start(self): self.optical_tagging_par["optical_tagging_start_time"] = str(datetime.now()) # Execute - self.threadpool.start(worker_tagging) + if self.thread_finish_tag == 1: + self.threadpool.start(worker_tagging) + else: + self.Start.setChecked(False) + self.Start.setStyleSheet("background-color : none") #self._start_optical_tagging() def _clear_data(self): @@ -3157,6 +3162,7 @@ def _start_over(self): 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) @@ -3170,6 +3176,7 @@ def _thread_complete_tag(self): def _start_optical_tagging(self,update_label): '''Start the optical tagging in a different thread''' + self.thread_finish_tag = 0 # iterate each condition for i in self.index[:]: if self.Start.isChecked(): From 025c54dad5b0e8ff18b201a52b9dd5c337a36edb Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:58:44 -0800 Subject: [PATCH 177/184] waiting for the finish of the current cycle --- src/foraging_gui/Dialogs.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index ac3b75be4..baacfa742 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3144,12 +3144,16 @@ def _clear_data(self): # ask for confirmation reply = QMessageBox.question(self, 'Message', 'Are you sure to clear the optical tagging data?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: - self.optical_tagging_par={} - self.label_show_current.setText('') - self.LocationTag.setValue(0) self.cycle_finish_tag = 1 self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") + # wait for the thread to finish + while self.thread_finish_tag == 0: + QApplication.processEvents() + time.sleep(0.1) + self.optical_tagging_par={} + self.label_show_current.setText('') + self.LocationTag.setValue(0) def _start_over(self): '''Stop the optical tagging and start over (parameters will be shuffled)''' From 6c9c700fdf19be4716b8e0cdb0dd23161ce2b957 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:27:40 -0800 Subject: [PATCH 178/184] changing the start_time and end_time --- src/foraging_gui/GenerateMetadata.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index dbe869eae..979754e08 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -541,15 +541,14 @@ def _session(self): if (self.session_start_time == '' or self.session_end_time == '') and (self.Obj['random_reward_par']['random_reward_start_time']=='' or self.Obj['random_reward_par']['random_reward_end_time']=='') and (self.Obj['optical_tagging_par']['optical_tagging_start_time']=='' or self.Obj['optical_tagging_par']['optical_tagging_start_time']==''): logging.info('session start time or session end time is empty!') return - if self.session_start_time == '' or self.session_end_time == '': + start_time=self.session_start_time + end_time=self.session_end_time + if start_time == '' or end_time == '': start_time=self.Obj['random_reward_par']['random_reward_start_time'] end_time=self.Obj['random_reward_par']['random_reward_end_time'] - if start_time=='' or end_time=='': + elif start_time=='' or end_time=='': start_time=self.Obj['optical_tagging_par']['optical_tagging_start_time'] end_time=self.Obj['optical_tagging_par']['optical_tagging_end_time'] - else: - start_time=self.session_start_time - end_time=self.session_end_time self._get_stimulus() self._combine_data_streams() @@ -1508,6 +1507,6 @@ def _get_reward_delivery(self): if __name__ == '__main__': - generate_metadata(json_file=r'I:\BehaviorData\323_EPHYS3\0\behavior_0_2024-12-31_17-03-06\behavior\0_2024-12-31_17-03-06.json',output_folder=r'H:\test') + generate_metadata(json_file=r'I:\BehaviorData\323_EPHYS3\0\behavior_0_2025-01-02_13-11-29\behavior\0_2025-01-02_13-11-29.json',output_folder=r'H:\test') #generate_metadata(json_file=r'Y:\753126\behavior_753126_2024-10-15_12-20-35\behavior\753126_2024-10-15_12-20-35.json',output_folder=r'H:\test') #generate_metadata(json_file=r'F:\Test\Metadata\715083_2024-04-22_14-32-07.json', dialog_metadata_file=r'C:\Users\xinxin.yin\Documents\ForagingSettings\metadata_dialog\323_EPHYS3_2024-05-09_12-42-30_metadata_dialog.json', output_folder=r'F:\Test\Metadata') From e69ca51da0382ff2f10e38b02d990a2895c34b73 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:28:37 -0800 Subject: [PATCH 179/184] changing the start time and end time --- src/foraging_gui/GenerateMetadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 979754e08..44bc46972 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -546,7 +546,7 @@ def _session(self): if start_time == '' or end_time == '': start_time=self.Obj['random_reward_par']['random_reward_start_time'] end_time=self.Obj['random_reward_par']['random_reward_end_time'] - elif start_time=='' or end_time=='': + if start_time=='' or end_time=='': start_time=self.Obj['optical_tagging_par']['optical_tagging_start_time'] end_time=self.Obj['optical_tagging_par']['optical_tagging_end_time'] From 22e7e0ce37815694e2b17af67a07ae20655d5d36 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:29:46 -0800 Subject: [PATCH 180/184] return in empty start or end time --- src/foraging_gui/GenerateMetadata.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index 44bc46972..b11540c51 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -549,7 +549,9 @@ def _session(self): if start_time=='' or end_time=='': start_time=self.Obj['optical_tagging_par']['optical_tagging_start_time'] end_time=self.Obj['optical_tagging_par']['optical_tagging_end_time'] - + if start_time=='' or end_time=='': + logging.info('session start time or session end time is empty!') + return self._get_stimulus() self._combine_data_streams() #self.data_streams = self.ephys_streams+self.ophys_streams+self.high_speed_camera_streams From eb48e51544e5995bfa97c52dd727f71e19ed28dd Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:34:00 -0800 Subject: [PATCH 181/184] check left or right licks --- src/foraging_gui/Dialogs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index ca7f94b3f..3f3334e8a 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3756,7 +3756,7 @@ def _start_random_reward(self,update_label): # if not received any licks, sleep until we receive a lick while (not received_licks) and self.Start.isChecked(): time.sleep(0.01) - received_licks=self._get_lick_timestampes() + received_licks=self._get_lick_timestampes(side=side) if not self.Start.isChecked(): update_label( f"Cycles: {i+1}/{len(self.current_random_reward_par['volumes_all_random'])} \n" @@ -3779,7 +3779,7 @@ def _start_random_reward(self,update_label): else: break - def _get_lick_timestampes(self)->bool: + def _get_lick_timestampes(self,side==None)->bool: '''Get the lick timestamps''' if 'left_lick_time' not in self.random_reward_par: self.random_reward_par['left_lick_time'] = [] @@ -3793,10 +3793,12 @@ def _get_lick_timestampes(self)->bool: if address == '/LeftLickTime': self.random_reward_par['left_lick_time'].append(lick_time) + if side==0: + Return = True # left licks received elif address == '/RightLickTime': self.random_reward_par['right_lick_time'].append(lick_time) - Return = True # licks received - + if side==1: + Return = True # right licks received return Return def _receving_timestamps(self,side:int): From 6061643985fa90e39ef18181c21c3462fab084c9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:34:21 -0800 Subject: [PATCH 182/184] 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 3f3334e8a..feafe0f9f 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3779,7 +3779,7 @@ def _start_random_reward(self,update_label): else: break - def _get_lick_timestampes(self,side==None)->bool: + def _get_lick_timestampes(self,side=None)->bool: '''Get the lick timestamps''' if 'left_lick_time' not in self.random_reward_par: self.random_reward_par['left_lick_time'] = [] From d650a2fcae8d032d244658fda1ecbf456567bbd9 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:36:17 -0800 Subject: [PATCH 183/184] adding side --- 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 feafe0f9f..1e6fc38b2 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3739,7 +3739,7 @@ def _start_random_reward(self,update_label): time.sleep(interval-0.2) if self.CheckRewardCollection.currentText()=='Yes': # check if the reward has been collected by the animal - received_licks=self._get_lick_timestampes() + received_licks=self._get_lick_timestampes(side=side) sleep_again=0 if not received_licks: sleep_again=1 From 2b48714909598a69ec5e6e95e6a417cd39a6a371 Mon Sep 17 00:00:00 2001 From: XX-Yin <109394934+XX-Yin@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:42:09 -0800 Subject: [PATCH 184/184] changing the name --- src/foraging_gui/Dialogs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 1e6fc38b2..56f8ac085 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -3720,9 +3720,9 @@ def _start_random_reward(self,update_label): # give the reward self._give_reward(volume=volume, side=side) # receiving the timestamps of reward start time. - timestamp_computer, timestamp_harp=self._receving_timestamps(side=side) + reward_start_timestamp_computer, reward_start_timestamp_harp=self._receving_timestamps(side=side) # save the data - self._save_data(volume=volume, side=side, interval=interval,timestamp_computer=timestamp_computer,timestamp_harp=timestamp_harp) + self._save_data(volume=volume, side=side, interval=interval,timestamp_computer=reward_start_timestamp_computer,timestamp_harp=reward_start_timestamp_harp) # show current cycle and parameters # Emit signal to update the label if side==0: @@ -3826,14 +3826,14 @@ def _save_data(self, volume:float, side:int, interval:float, timestamp_computer: self.random_reward_par['volumes']=[] self.random_reward_par['sides']=[] self.random_reward_par['intervals']=[] - self.random_reward_par['timestamp_computer']=[] - self.random_reward_par['timestamp_harp']=[] + self.random_reward_par['reward_start_timestamp_computer']=[] + self.random_reward_par['reward_start_timestamp_harp']=[] else: self.random_reward_par['volumes'].append(volume) self.random_reward_par['sides'].append(side) self.random_reward_par['intervals'].append(interval) - self.random_reward_par['timestamp_computer'].append(timestamp_computer) - self.random_reward_par['timestamp_harp'].append(timestamp_harp) + self.random_reward_par['reward_start_timestamp_computer'].append(timestamp_computer) + self.random_reward_par['reward_start_timestamp_harp'].append(timestamp_harp) def _thread_complete_tag(self): '''Complete the random reward'''