From a51eb0ef7a1d1625c49ad48d2868a8e587b50690 Mon Sep 17 00:00:00 2001 From: Jennifer Kotler Date: Fri, 9 Dec 2022 16:35:17 -0500 Subject: [PATCH 001/283] Added .ico and .svg files --- docs/logos/cube.ico | Bin 0 -> 432254 bytes docs/logos/cube.svg | 1 + docs/logos/imviz.ico | Bin 0 -> 432254 bytes docs/logos/mos.ico | Bin 0 -> 432254 bytes docs/logos/mos.svg | 1 + docs/logos/specicon.svg | 1 + docs/logos/specviz.ico | Bin 0 -> 432254 bytes docs/logos/specviz2d.ico | Bin 0 -> 432254 bytes 8 files changed, 3 insertions(+) create mode 100644 docs/logos/cube.ico create mode 100644 docs/logos/cube.svg create mode 100644 docs/logos/imviz.ico create mode 100644 docs/logos/mos.ico create mode 100644 docs/logos/mos.svg create mode 100644 docs/logos/specicon.svg create mode 100644 docs/logos/specviz.ico create mode 100644 docs/logos/specviz2d.ico diff --git a/docs/logos/cube.ico b/docs/logos/cube.ico new file mode 100644 index 0000000000000000000000000000000000000000..35c7e0a46a402e2360ed464a1a3b8e6836f997ec GIT binary patch literal 432254 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 l4!_fB$9oYC}NaKSli`Ywbwuk~Gn2NA2> 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 \ No newline at end of file diff --git a/docs/logos/imviz.ico b/docs/logos/imviz.ico new file mode 100644 index 0000000000000000000000000000000000000000..ec2b3ac553185358968fe027abee3e2244b155c5 GIT binary patch literal 432254 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 literal 0 HcmV?d00001 diff --git a/docs/logos/mos.svg b/docs/logos/mos.svg new file mode 100644 index 0000000000..9667f16213 --- /dev/null +++ b/docs/logos/mos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/logos/specicon.svg b/docs/logos/specicon.svg new file mode 100644 index 0000000000..2deb534e68 --- /dev/null +++ b/docs/logos/specicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/logos/specviz.ico b/docs/logos/specviz.ico new file mode 100644 index 0000000000000000000000000000000000000000..f051cec174fc71c0bfebc7b55602cfa8cd2aacd2 GIT binary patch literal 432254 zcmeF41z=srmG_mT*)*j<({5?olxd4!n{v0?c8j~+Zrg2}G+>9B6UVV-v1C~?Gcz;A zF*7qWGc!oCJ>UO#=8p6vKe1%UdhDzF-hKDZojG&n%;8B$xFO-zgxhaV(E09!C*PHj z&@Le%;UgdM-y7eWkZ@7g?!Mc9AE5g(Kah~{&2L8EKcA5B^-m=v{Oo7``L z2?@XXP4xU<^n7MQLY+F%{cZI8#)O3A&rzu&3-G#HGb-YuF?GoF8QIGTTQSjRf9 z-d_@2YTb9c^vB-s(jR@VOMAp0>5sn8uBFty#qJr^<0-4-;>8Pgo<1tmz45;Z_U??w zKcIX4{XCcU$a|vt-sdtN`+&=O@=m*V&f6{Q*@Ju6>)Hoh(u41E4em{F4ez_bCH{jW z!8Ll|U3%tyuHip!RQu_}P5PYRk{-BGefg+6c4)hsIOt7R|E~$IL*jSbi;dl4Q_Xz>z(sE*GtFDG1;zT(mnRf^pTmaQ}PcCHqBnT+qJF# zHMe2;I5&M{y32m%pAA;sGJfW!jYxIN=k#++X7q5)U;eaf`oh1sM8UIr)-Uw=lY-eT z2CG;8J3-H8xxUT*;9mTFg6r4(Pj2bVUaoz^@9N#ZcLQ4fuY3K^2?mptIybvkuYb{= zPpSJ)2Af6?sI6`9)&1XcZ~i?&ZTr64vu%lMqjoiV;0Ak#jfKW0*`EKBwLj^>8wIsdG1=<(YOf96hTRSXA5bbiF@NGZf$k9)pd5&qWk z_&c`ObH$!@&d-6lZ#TC#mo|TLp8behFsYqA>!NwXJUnwo^HB$7@2+)jT)#IQbGGp_ zpKy(z{e)}R==*N`z=m%Bo=tYonKP&Dex5B!4=Vc|IwMk-AaZ8($DS9>D_KsRI^|-I z6UUFa)2Gk4^XmG+1H0YaNnKsPR`H+tR-sl|YR$6q8-=Htt+pfX4+}<6_ zt&UJvc*lhc^w7C=OGc@U@6jB&(PawXV(~iQamh39=HA5nZxD{&?zXI%Vs%}N>I~=Y z8)m8R@6jB%#pZm;ddm9_eNBGoX1(K%!hQrgeD3TC!Q{&}x1qI{)Mc>o^YD`2qWQ^n z-U0n>pZHyOz97%~!JjF^Q;cr5^X%NI^9oz>gm0zJqT{a!x1k6Wv8W*zF&?7!Tml@r~jRTJHsMI&6xSHED- zrVEBkXZEypfH7UBXEv^!;8rgf>YBdzNgG4PV%^d)Zdk{Ih8~_(54Qc2WOzxmHsfpXadyQiq&3^jBwhmDT@P*Ef>-$O!oqwOv zQ}Fq<|4DHDwWb_Dy2tQ4?U8re`a=7km6YJ8HVaaZWX!eS^{SD_EtIhK!wy}Qm ztj)vd%oub``d(rEVJ`7*+5%iw%^zfIKlQ@{3~c>Zw`0?Mw{U7_doT5YThtwUaGm-) zseaaRzYco3&wY$F{7|!(K5b(KF4Eujq5<$dv&S{F{($eH-zDoe_~iW74ezA!)mTtR z>^;20;HI^TzOk-1(KrvK|ce(w$ zR{6RkJ~^x}86n+qM{wQN7vW3R6CZTj*U$2GN9&7y(Vq+F{rbq9 zZ6n%e>!WA|^-+f(`@q*nG+Dnjdf+Czw?m`vs@5!lYzrUm(@wdQq~Z18j8@EzjO zZgk0_vBtOAxZZ4^lOMc6zulzu>MqwO_qSRT2Nlkxz)#0}7j(Dq&<)JHF!xr?AM9F6 zCVEpe6B)3OcNgCJ`4I7Tg}AJFsk;cJ^FAKUi=F31pEZX>$9aZ11phaF!0Vn*jr!`}R{U>x_nA{iT(``B_v?h} zVU8BnU3|SO?>g8jTmHEa_G(38 z6GGpIcYfH$x+dQp+8EZ4@%`)BdjstaXLuL*kFscdsH>`H#u6C<-jWhz0QmG4uYO** zc2m)K3#SX^mS`O_dR-?MKgPLe?eOp7yFH`l{BQcMxtJun03YVpN3ScOj+OHUc^C-yI;Z@|bS}OQ_{H~& zcf7-8LT}WThMN0HnoHT$o;mJz zZ(S^2>!9e{5b<$-jwFfy=$!f^(?1>Fzsco3{|VPK`&aIy?n57P^4LDNO7~N@^-*x4 zZHM=7mR_o9F+LI)wo^ZQX8(ulsI_;(p!(L%!}~Uwd<(CWD;*Qdcy7cJZd>1bH*C+b!eyjX!#)n`}Jj zd#Il_A;-Mk?-kcA{of2fXj9?u`pi58f6%8*H zoqs#R%>>7ot`|RvYyxk`xW@9h&_tdeAbJFT_;F#rMe~NUo?$IP-;{Umv>VpxVXGhf z;r-CmmGcI=Y0?cYlTL5O=q!65{QHD~Z@AGtp0@kxYq5F*{3AK5ZG&$ZjAGm8?}xX4 zx4|3UzEAG&4dlG^v?9#WHHg%*k|K?4)9+wr?1UZPqQzTOX5B2ujw7ged-VM4q5+GlhS>v|VR$*ut^PytTNAMI38fgrmkL#C?Eu0&rx7j}z=x^WV|84V{KMO@$ z;PFb6i{sTvTcIJ)0Q3UP$BI19`&oa`iG-4O?v(TqUn;)-7K2%Q9D=^AmwjLYHV4`- zx)86e;TrEortc^{e&Bfm{{gS-`HxHKMR>mW*^gt>7e6K4`G<{G7XQ6uo4srYPr7;a z6zh86Kf-xrw!OhXT%f|b_15O;=z}_AD?Z?3Lg6I}|HvYBe0i|Cn zdQZu|6n*#ey{vaj{}$;Mv871bD1?0&f6jp?K70C@*4|$i@!xUuFBNGQ_b}ezDY!Yf z?cZ%(wD*+czoUi(oIrmvrsuPU5AcNGm0w57>SusSkUzjRt>YT+$yz6gJIIDHGzw0C2QxnOS$e1Q;)=pWQgXucQ+!TeN|Zf&_PEL% zyLaz)yLRqyyLat$`wt4n$IiGtd-u83%jdYMZ*_8myS(DMWdFsrPW*S5^Xhk8?(6rs z){TDQy5;=c4eR-)n=zrMTfb(J+q?geJ96~2+jroQ+qr9p+ok%byE1Lz-s+Si`_@aZ zmFxO8{he#^^4+2*;>`^Aq&JuSDovj`Qo%{=S^AtMzO&V9pLYXW{mCt!+QA(^vNLLe z$Lq?rCw9)1`kA>K>H98-#%x?UUbg4|cB#-Ho5Nn$pC+CTePfurg>t6ma=Naka)`fx zcWWT zTrwW>@+SOC_ER60{tH4#@er4W%Dut#+5(6*Txclo_-9M zogTX|C9Dq>?MHw+wux5tzT)|(NZzY_yW+#Ww0giTaFV&vHRGr5#Ie1;4;37T^U*v# zuzQ{4^UoOnT@n7ZwDwf>I=m6%fNl5W@%_FJk$s}HaR}ptj-<8Vj_hzH+8+9h9tM7+ zyL8m~(%n)@ozu}=KQG<@J`Y|c=y5`QSK`b#fJ?(WKWgo#eep`bd2FuSeC)M zaB0c3Zr1eB*3gfDH@h}3jAUi6b1PZTmHG}3n=Jk{`&oR+yf0E{v-Kgu9q^_+oxzpr z1Ngug;PXZaV-Re2>!mMBL|34(s#+V7K`J7LRF0k&wZ+$+s(0+(wj=_3kxi(#Z12Dx z*{bLnFKclv_Ar?QIX+s4q1j$XgReu`wiQp`v7c~XQGcn^zr*{rg!iRATGU_G1wRH) z6dMEh8n4GHy4UnE;&0$hkpDTtXY*sy`_Th(4ZUod-k+*I0fR=!hT=71;lSq%@e1gt zpl$GP=EDgLbYRbdR`MHl@IKm_TF2}9tuEk%jz~K5%;Y-SQ>}|0ntR1IM9(3I^f8mzPHbgmbbf3wy_yoYO7RYuzrsp$JoxoT0L;8?h z`2q!-InQlcHA!;#4bnA#!(rc}pf@~q{D8|p=hv%lnLqROMEVBsig6h%y)NY-ewFfF z@iv(vd<37-$>V#<+`vbpN8?|ZUjOXbmS!}k3A32m%>=o7$!_6`4^c=`GE+mH@k z2OD(zi>Gz9>jT@|>&8kCK^ffsA1++)uKH;Ux^dtR58&xZkq#4B@UB*`eNn#2C*10V z!-cQjZWPA4@C<%y9KZ$6*q7?be|Y_}arSQdfE|Fb0xr}I-eyUMiCl#ak~xkZAHUs; z7tcF%7x*CKw|8*g26y7{ZkJz>FTdTs(e+z}FX+i{wsv}%D0W`rFC;plckSM~$W0Yr z4=$Hx2R1rE+jIa&`htHjW%u@_hToo!__4#jh0bu|Ag>R@H@V)Q5;TYW`rohl|F~qq zfp_9(7Yep0>W0o!pFfJ`Pa#Z22WUHZw?e+kBfC868d>`6m4^E4%}^K_l_&#$L5z?f}D!!~3_$k8+vX?Qzv`K+m0) z4QAiYm1b8Ab(D9u_6tw(*JFM%7K}-G&js3r9~cE3F|9Ab1T&|z??cz=CAvFXM>U*2ch&-|hN_&-$C#zDJSH(A3dJ+go4;il`c z?+oLF-#hx2i37cF!2sE{Os(hrU(Hg&Q6n}ToaX; z{A>=!fm^^&)@TZLT8?I~$ZuF{0q`!X{_=jaxg+|uQhdhhMZ;{aRdnw7y3rAo-e2lU zl8&vFY)|=WH|5yD?WSwB? z6!~-nm{q2orQK_A(j3S4o`Ua$`7n#WDeal^uG`uk$?+$}V^e}0+_&j}nk-e;yezN& z*xv(7u!7`V+xpR8Jq% z81&=7Cg_6pZWBKbA7}DGb!oJxp*hkyb&y{GYhCDDWzX~(c_!!if010Yn66c#@@+qV-p3r4*Y@yx@ZCXIg+C*6I@n7~dw2D&NA^3(7sxncdORbXIb*c9 zqHqsw4`-eO=QyKt$W$yNYbZJ$TO)L?UYkXCqPmd3pcy&OeoX$ay{!r8ivli$HdXd) z?T>t_4=7${fMV9*gMm?s^dO8iyiR3g67JD_(o;}q1Me!mEydev4^{dPy^!Aa$PV+t7$jeeHm`k7^04UC!}1%di~j~b8^Lb_ z-whr5t?^U0EAH|uZdiwZxRrBy$=BKQk-(-ZW!J7<@?ELiv17+}$FX1E4s`wNWrhHLT0hf~tfSCRiwh;n#*G_YjU5}-yY=hWxs97P zxoz8bxgERqyDeLHx)sY7x!F^PyD@{ByZ#;J$CY!xYn%2**R=l6U9$%N;o4>V#dXbn z&<*HP&%HgOy_+**j9as2nbom<*8#V6+YYsHgWI6GH|QNT)?W3!R*Ke#!+Y1t{?XBm z?pjyzD)&f_ty~UID&FKVtu4x506j^6L^(Bd*k@hCPb~ZCJLEI=8P`KTBI9~JD_v3_ z*&?>pN;9dnqVzQ0eXJ#c{!`as`G?PXtGVlv@e}hQYbZJne}Ntw-8u0c*jJ$U@eX{g zuy>|NZ%Z5;>lf=fcB5YM?^-gwhsDx_cAGz$YzOh$R?%zJRjFd+Tx(7AUY^~rv3|SH zOJ>_@AikFVBGa;dV>2|_kG0PFUAjzT_EFYC?$i31sb_gUMYdLKG2|&|t(+Cp6+3eB zc;r6>FQm4x%v5cqsYLJf0&F$sHSUxXk{PkNHkMtDabk=;-y+?0Ieo1Ze}b+W zlJkRiHB&75Oy%e~d*+0-#drhYP>Th87qso)ia7&ZbJ?pI?=pTbM@|E_gk_8O6?!D<@HY;+97Q-K6nw z@!DG(*St*x9ui%Y%{9>!Y@X;Um;;8NH8dIMf6#&K-MPwjQZ62BgZ6&oVQZt06|WN- z6J(Tr&HpSPfqgcVrTK%EHqND84}L(K*GyEb-p6E9B|pvOm)kF$3js!=v!bo(k9+-d zyTor;?g{!6WZlyGR@!w9x#=@5$o@}RFCA*uW4C)7k?EPMu5&BBAE7^0JZE3EB)eYTGk{sVvqLVIPFH*IxUp+V7(0(xI8s?O}^-PVO@4nkbc( z)dS4-$*$i-azB1mS9{+J$3?U-T`*`U8$R(gr%!l)9=|S>PZY!Yaays+?ce;C`B2n^ z-o3DWao4q?F|3*U@c);eviO94(s3Ejg3hKK9VNID`aihiLuS*zl6uzaz{30nG+*Jt zXN+u2*CJlsK3?+2*3HPreEIC&W;gXX53gR+<8yiH%McHm64{n^ZJcd=E^SS~9|IT= zAKMh4Bl8DM&*|)3`b-q_LRvp*B-que~!{8 z)`Qtm-q?c3uF2X!A+|kRdIt2+@RI>|il5`_EsEdc+v4SuNQW=@!>1!B8IMvyd@$XA zDYBKfKNjiJ3*`~+kMS)JZR2-*E88ds0=`3@0u=Fi)3EO{?@REvv^VG@($U>S2cPXb zzat0fy{6|!F4F=1#Ks5hF<)Js!NJtWfI)ot8f>tUi*8d#s(dm`r$C*0rqCBI@?pon z4;!-OjFC@^<(bm8bm@`e!I?2YkFt942pe;whvEs47dy&NyflAH-b;Vb$(SE4v{7{d zt5o$HnJDmx$%^GndFY>%Bk&vYZGXso{3Z=aGMTw`{eRUvupN*~H$Dt9AMv&ueD%rO z()7iDagBI4HX-1G>@WL6y6$VCJd^Dj-Q)0c;%KKlH~6xdo+*xB0BZqbJh<&Ytp9=6 zA-)DU$J0;Y50<3<4U?boPUTG9x_YwPD&HaehqkPnX1?n4l+za9eC8Lv?2DzRJEyq# zqT;dblynF98#R6LZuy4dFI!xm@MEsrcjCDhPHHP3wDs=j!EMR`+f-vOyb#Rr2`8`d z=G9Y8*2SkDTE=lscumfoiOS<_F)Jlutg%Yb_>kKVU;0p{jm|FNKUzdjZ2eTfmWd~# z1X>xMg?>`-XKOE;F!>!QDEkHWixEHZU?x{Y1#bDwYrqP&3g_r#Gs?vUam4((CipvZ?~@zhSX-g*A5s4vh0 z^hfB8)-Uz%;61&Szm#|pqfxpi&_k6+cx+!wwL!|Vx1Z&TjHoL zk8!(v@$+2Mmp|#oM?TJb<-^=L^#_*6rHOoD$k)_ddh^c8TcY_7=NcnbUn+0yrv~ZBW=vDdPkL>Q_cFxJJO_?*UsjIJC=tjpx z?=m7hga>J<{JML0ueAFXP48}XGEU|LCAzXlzCx5Kik%@I0eOD?GVklQXmV$_U3r=) zBf35o#U)5zF8wgEEWi?4h@QPv{zdeIcJyrWA6CzBiNR~k#G>V zfUMn9`Clmo`99Zg-)8ci7MzO9PUO|N-PITU=_5R#96u&r`$hTvtG!8DA9^(QZ9-;k zsvJ}qnhW^w_igqc4nNbZG}qaOgrRw#EYc`r=A zToU)o`EK+`{qL0gL#-i-ZCBgiE66hx;sFBu!d$}sktsV6^OZu5JLoI(Z|3MGc8y$T z^cz2=5HAHC429fa;9|V_UF>~v+wX1d#FOk(9_!iCp#%H4b3g9$Mf8FG)6X_q*MiUO zF~xr|HmoPWx>$Q2MK0GGq`oel<^3OLjma^(2Jf(K-3;sE=$^`jCAk%yUIuzS-M@Hx z*U)|0#h$Zgs@8weTBB*oYb!cCPV@wxAVYMaSX;~KJNJ5Ax>>e}PrI{GoEC8v#xF&3 zXURI@L(w0P?e&87hunV5m8MaCG%j{X_G^Qui@IO^A0mAOg*Bmza&TE}f?!#){)&7D z*Li;FtUjV$Px`v5*#A`N1&QZ7dDPpZHY}fDJYQ+^i7^9rdnzW3a_sOfv-x#N`>Fpf z-wSfGgxI`*W9^iak3!5jzBurPWzF-l;6U3L^SRCBOy&L?*Y`!ux$4C#*!Zje z3)KICZU1KYSJnR0N1olc!>?(?P{CuCE<<{lA(Mxg$Ii+%K6zLp>;J;3U5d?Zza{|- z#fFJzr0*WCRoB7C_J36O#|{4j9~N)S%DaYquye}-YdzZR1vL=t;5z0%|BwR zDZ`a3aM85R_IZ+WewU18@-RTg^0JlTo!%K+XJtNz{=-VxB|AG)Iieuwsi{x6%|OY2|rYT!R@ z5e*hCBL298WU+MlFL)n^(%Wge2=$*li4@{}4=Hyjg_yRA#+`-!RQ3!FO49ngQM7us za{MO~-(SV?x0oU5pm;HOffVT;%HktwtMLHp{~+xRKp{VBUY_JQ>7U8pTskjaI!r4H z1Nx4RC+n#@-2R=b-7du+!DAHpC|2zq)LT&)LTeb~uBksUem2PA#}zM*EQ7p7eHHCf zW$vSm@V_ly{hVawqwdV<Uvt4_j9k|pJbKh(zE9&);pB_T0@F-7q!&? zi2vx-ruxtN zGI8*mh3`cMfZwi7np?arer%a@J0p3P#w<}x6mhh*IS$DG$(noY(-jIdAX)Uw>rHD1 z?@IQcd5FG|oNJ*F^Gg3~Z5%{X&|{!~J99F`KFIzd+h~X6@7g+!uA)%SS`8kfcz;XI z5$Pwy|G_7&jLEBtpL%WLA#er0!#X`=Ajjqv{2ZohUpL-udR^&{%Wi+k{#5=uZ9xuP zG_6Y*e%DX&Nha5q!8=x`UEmyg(RIqdOQHWkpLs;_0pusPye@I<2i19R$!Ec@O!_ZA zZ$|j=rDySsQD=y-)ZZ_MZ!SW%?cXj|02cx^%5cX~gW-68`}_3ddjgKU(?+$^q?P zg+3A9tftpK@E*NrOYJjr`pmJ2=kjB2FV~*{5BymJ4wT2omNvE(xgPZJy_AE8vUb^M zkNdR%clb?ozKdsevv*mHY6%~JV8_7k2fLDwiABFubve6yICz^*pQe9sEnfeM>91=V z?~pa%sk%4*m24B<$0N*p14hiBf&%aFw{qTKuYVQZ)FuuXe5BiHC_W%fJbz8&J+L%; zwtN@&D3=%|%=MUV`;Q`p{1(=OQQe;Oey-R!)yA}1Toa+q%+Z?WI~aS`SaR`Ff*-H% za4CPId2#m4N#$KvUK8OCJTtKg<>PbBtq;|G7q%C89rA&(%3V~PXqe-r^t|wLz=6Zc zla=$rr!B7rJb6uzPj&lP+Vi3BmDo8~C~ z;2>M0e6yi$&+O3} z;$2zSD?aKmosT{SzF^npxh_|EcP*!wc-Qn==8=oWv^01W(Ifaplj}9#yC@e^uoYT- zl;M1JARyWi@yCbvZ&bdIpZaw{{r5Jr+T;&P>$|Q;^cWhC9s&M_y)5%pTL!)^BJ${!G}FYPwjxk-r>! z*0y`&>t?scRtFq>oSWzG%WHpCe-Hf*x!${L|GC}T>y^S7!NYhiujc!2FA;phd^mIJ zkm8Qg6c_VRgF_Oz{iR>B9IG|R$Ae$sxtC*hjr9NB7i~c}T zm|uaHJR}|FWcge*fBEyKLjorYeKVqVS7wgb7z@8cpV5cYXKa6MwXV&b&|1D5exBpM z8GJa4T3GQ<%$Ld@ypJ{Yf_SQeC?^4LJb7%t@&u^fHtF3Vm-#DL2N6u%JWh9 zC_=+Ir0*3yV_mgaPtg_DG4!v`2u4ChSUB~SET+i0exnaH1+@vu*+=982-MV#a z+?H*--PY}U6i>g^tzEZPG4ZS2`gQBvI>o`)RGUkr1GxV6{}=?w6Y(1zEp!y-iTIyO zfv{%H8sT3}9;>as)vH%)eynt>SFaMzZ_vEiu49M3uXT&&&u~-U9_WVm&2jxYyz06% zeb}|j{F`gm@V7dC?>c1P@47a9)b;CB&y5<~+Rd0U!Yx`f$E{b6h)r8|xDA`OyESXq z`TAF_Qk|>aYR#dV?T3d+B}h&cZuRSWp7L{VTR+v!eJfjX{omyO`W@wp_@w-yKBOEq zqN|U{A5wlY8KSQoe9zGL#!q}eK6xK@EnoeN_RsvO8`ZV0TQs??d`%a~{^5OAtI()Q zJyO|qDgOQB8}c?Aey5x{b2dak) zIgDnF&e48Z-xctE|Bp!q~W_q}jR7wv2L zdDlpD$Mz|U^s=$IP0!;x56T>2T<_&Mn5x{ohJ&exye@&7PAiVU_AC`~NA#e}uDDsVEoZ#w*o=BwCSu|~woT($G5 z-1#V7kJeRie{9d^jaIPER27fdvlji&`3vVPmce`%#P1u=R&HA=Ft(Su4tPrT6J<|K za%)W+k!omTxKv35Vm<}?&D+{j1=v`=-pkylOXf?p-p?HKKD6i#iFsykD+=}=th{HzfoJOr0=hZoa$F2Y>7dI z-^Y(+e7~0rhw!B+s{}uy1yed&?4sEyuTqv(lu>gKg+{NYg)V#aO=pS zoyv(Se@E$OYTDz76RP%+-o6Qzry;gm^(-ZY#m9z$LIIbYNL zs>%AyPf@glJ@+VK?Y1whY|@cX*lU$sgvsR572efk8?NTMy#4nE*FioWv3W0miOCVs zoSQl{#eBc;v96u<9GHjW>cUB2g#CHd!eM~L^u?S5?vU@HrE=&J17&$@O7JJ1w?dvl zmw`?Y*^hJ4f$qof#CTQig&))JrZ4k)p;XaAyCsKLtrRufz@lJcMZ=dO3q#v?(>$#F=bZq!ElJkQSWHa`nr>xN4rO0p8dC9ZZ4)Iia zH}Tixf5l&&yc6M^Ed7|-xphw(c3$a?=|?L5GLoGwwoi3uJbtI?7i}mjE`t4V<>$)IpkWu@m1}0`ep4&_GQ2NPr;|B8F7_p^%0?_qffHm{yyd!Q1h)mCv4=&9*9coyCl zdu9#J)NhFo-C(q_XOmyM-nqYZ*`kw458bHtf(w33uyv5DW#r`Y%K+2)vVtxM`pV;^-Z7mf6DtCTSWr-19wjt|&#@!ro5;rax{5BE|2 zkWknoc*~mU@XTIQ%oRF;d`h^R>4R zW!I)f+B^3y)4L4Q-khPFR_rMI5ANT-Qntip?#RAP%FnP*a%jFAs68!9%P9nIV2?{z zKA1y#CX~1Oy=JyMzi!2$UD7ipYtE2wVNl!q+{kW^YoE(U{L%GMJCEx6q}vh2;-641 z67Y+1SnuhP{aZatEC@M?WZzSq3pq}gY5yo>8Oj#rAj#5R9%(k$im!3ol<$f{y@T4_ z??x&oR(_thpN^NWN|NT@A<2uB?VIOmAKUMlor!!?InRE?HGAPxmZPgn%8x`t<$ksc}72eM})JXPJ=sfXpLANk@cxs_s1Fl17*?%~cvAth(_5Ugxs@lJDuHTz$ z_tqtv$3C|c_w{Q22luA#K|U|cU&LA>I!6qcts8OaGyN9qM1$DJ?XA8(kIKp82QBY0 zxzgg%p5k*<@MR5B+i8C|;I&!T$f1^}eYTnB`tMGj*f0C}zj&WE=wfIWc0#Qav+lTTlN9!r%&>QW z2dwLy!{OKY!Zw7qlsMBT#y;d!+PP(s+KGLWHkZVmihZXI=t0PdeC$Y|$IfDSZEY&i zhS0`z`J}UE4rt|R6&{-4GHY?VV3e!*aZK_cW!9KnyEp4G#i6M_a&hj8VuS-dB3B{u zcy(kW;EMll3-N;JJytIoYPvM^>41J1sHaVEw0-{g*mE$cG_JS>q7NXxbxXQ;d z=Y@|e|Ez~AIRhC#c$eO}o>!y39@+mPxcPMl++vPR88}l;e8c5`}weK{Vc`PC;D*_ z4^dV4@J^$-;zhQtoo?;r&zRmX8DCJA&4b^_tu{xw5-A6Et+jnqpv`L*jU8zo1ehTrSG>s^l0)sw`h77$vgilnBoUtJ=_WKVU1ih zcYqt(xvudU$hXz;yD*xmd9qkG4+_3YEnX%634B9**~+(tyn*CS_>}Q~JUc=>X9wlj zr$Al0W&GS|HaKI?fEUKgiKmks1HB^8L{-;c>Wn>X%p&-V@BgyVI(Yf&!iO=QJE^tx zi~OSGcf+5*Vq>cHTKP06(?+Cg&G)$*$+g&0`{kJY9|LDEgTuSRhar z(_j1jHZb}VVpL-9tIX#RXEk9^ee2_n&GW^hpywz)Hu37De@W2eMLz#9xnU;{Yhd*p z-oM%6-_2fCjP8o>gJ&xo`w%}+{`=y!A@*9J&uf>AaC67Ej@GZzX+-R^rGM7^6d&K> zmCq~gww3EC9RxhR>17K05WnrrO=#Ebam@`T+t$x4)CF%{H(jzqq*E)Wp3>VE|Gj11 zpIqlww`0RBw{FoW&0q1W=uoQhe&(j_OCa4fK5mwG$;P{Up3`z>C?@1B<-epHR!&gz zO;T2B4Z&Vs7LQP!c3<+W=sPqaOF2UhE0-xbq)o?Efq8IAUB%$(;Q*YC){9nnxwicE zB&_iRTY8#*oo}TiJ}g#wAZzjE+_wSIdSg zdDnc-p-xHPm(I@XJA&@+ZRuW+slwV-UR~Awy`+4~AazszUu2%L*5T^Dx6J2-)8v$I z`r;=nFEnimHmJdE?k(gotFykA`Tm%Bz&s}Y>+LAtHEkI`u%2rmod@>liq=!3_emx| zr;(}saLM&e33${+aRu5ti#Z|m9kfavTmtpwB$Y0UB|$(Hr^kJ`c{iDObJaZrY>U&(a#_3R$mzK7ik{+wc^PiA>d}ZvW&~(qU-r&|P z_wp3&IxbyOE;)ad#~e97K5T1_9-2%iJn5e4a}dM5hj~7z-CuqFQRS`=F`2d9p4j@y z#ZLUnGV!dT?A2aw#(x*%3u_J@vG3Fx>4Ks1K*?hogv!=`LZb3V}KDd;2FuvHrFNZ!Sk{gN&LRl zJnUGPvZTYVud$mkD%0A=pGC3bZ(-{{aX4Q{~@LY*aRG!F8wY0l6hXW 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

uZF)VE=9Z<98&H%Z{Ska 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 E0EGJr&;S4c literal 0 HcmV?d00001 diff --git a/docs/logos/specviz2d.ico b/docs/logos/specviz2d.ico new file mode 100644 index 0000000000000000000000000000000000000000..957345236f53acce1eef854e8038bc724429235a GIT binary patch literal 432254 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

Y=X8IvU)1xaz&+Z_yk`rXQtl&mbO z*A{x*<0pKVU&nnG`Mv7BSiT890dlFd)$v+qZpU?=64B=Dhqc zJ&CWgJ8sC>=!Rc{t7EK&+JPG72UZ;czbh`F95vycJQeu2h>xIJ0<8~QN6%8_t*Q@M zE+rd6I0hSF#B+{Q>R$xDIHut_2*Ru9A<@f(?@Dm3bNVd(=%d~tOIdqiT)#)MB1;XWMv^C;)90kOn)^ZY$Q7=cH!UFt zi{w&Em)G$dU54rJb03jhbGEdLP%u>hI%!on;R*PdoGJ ziMDY5i{$FQ$GX`&z%^PPd`m_<-g(h_Qo?`SA_evp1vFdKy0eKm0vUzyoHoT7+BR*p zm2b_lXCJ-QCf=C0F(Z20h~iJ}!XeBd_E+5rwFABBK?I*GKe*t%38^0f9~t#52LD;` zpg5vqU9tdd=)I1)Z-M?)O@VZy^mUebt~~W>T7yP?j?-W-!}lrYElVzQn!1)+bf@&K z@U(02y?UQTzd9Brb6jo&ot~vGCQH6tt~|15@%PE0O@<#A{;8*t>{4Dt4t#64Owe!9 zdB-z)9=TBB8rc=9lhRzBFt0kD48OA)c`}&KL%0X6@m8n&U3#`Q4nEEIuGK;{u3X7k8z6>*e4W_FWCyOZD$U- zg?QplF&ly9ovrAaR^}Ajb}-Mh)|M`Q#h!oSPd536tX(ziG#fgA9t2g#f{CNhzu*MS zdfx%^inH{XsR0WE!Q>$H_7Fx5G9tsssiuWmL**2D9-H!znCq$|FFD%>d6MX3FrN4b zjy>1S!lwEh2y?1el06|@3ZES3b8;*vY%Lp4)}BVQQo zB_p(^g4a`HPlUA0_I`|<}k-$fXFsplU5QKWz=}34(^JOU0}~qTR_xlb!+&j;X1;G1(5p1x>nm1!`XQF>b(rU^ zW`!esa_?!b{6a7$dtdrG=yK^%;a+k<`W5`SE(TX;3xD|X437zNF7e^z$LGO*;cRl~ z7=FA;{x%pE*5$t=XFS)DV~Pb9kOPuWidz)1AJh+_h<%~lNXdr+_Dex=NaRf|GA2Vj zv`&2&iHTqoGS_Vy`N*fc&$z4Yp(ba$E7%Ondps$@b8si*4eVK6dT-r`i>z zpV?^gk4I%sBnSC;)|Tw)F~fT7fC9R;mOPhgWP*BKc&MGf@}kHY8L-r zGD7Ek%@gpPWhmaScwyIL`mE!c6Y#hq*J&@=l1WJ}a8B_%zTbTYY!Br)$Ofq`*FMU2 zNahvt=MeK7NPTS$K796jc3J7icIEJ2+I1s)*?`*P=;6#1KRmVUGMMpU0f&m5V0@H#%-TbKn$jklVGeN=)-r( zkVsH(LeC}a!jH-|ZbNUW&P4J-`ADiaZQen=V8?dWjoV_IKKaBptXpHt-+cocVTwI? z*A+JLy0neG@V9nh<^N<>U@@cEj~wvctqxm zL+O=HOfQA4!JNgQ-ydStr<2z}p8bf-_iRl4Np|`0lkG}+V&8K80BUJVHue6??1gE! z*u0rj?9JDnwM7eN+sZ}rZS~4U^vwRyHg4MFze~2dNLVHfGO-*_;kd-9tRz3H>*VK? zuOoY(k3J+nL!Uhz(?Kq0+pX`*2g=8EJejBQ3*$?qz+O>6{G|D&z<&wfN#BSagWiFQ zWqUH*A=Tc`hig=Gq&(!rV^GY1r~#J}Q-tRejC}ew^|v;GqxEY(u#d?Qr8ts8wOLqHp)iz=5c{cItL3aDKLu?Xz zy`OZ=;P9Ef-cR7zpRPL3ZYP)FUUc_^w_RZOO*r4~pLCH;z2^#gri`;$&rGygGyY6% z#3T0B{OPuQ$sAkz(Yv-|>so%M@^5xyP`^Q+Brd(q&heYrK^%h?>aFQGtH^VJ0^zdy*(Ku0bwZ{>flXR?dd`lfIn`tG2h%BT{Sb*cW1_SziZ;om28Q$Oyg_XBzUx7UA) zTckjwK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vVK%_vV RK%_vVK%_vVzyVKz{{h&uNRj{m literal 0 HcmV?d00001 From c46b6d8f0e45ff2e7c05517b300670d871be438d Mon Sep 17 00:00:00 2001 From: Jennifer Kotler Date: Wed, 22 Mar 2023 12:40:08 -0400 Subject: [PATCH 002/283] New color Imviz logo --- docs/logos/imviz.png | Bin 13776 -> 7219 bytes docs/logos/imviz.svg | 46 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/logos/imviz.png b/docs/logos/imviz.png index 17122dd45761499770d84ab4b00f8d5347ac4dca..24e3cdcd81a5a49f24d3736e07c63fe5c89c6e8c 100644 GIT binary patch literal 7219 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 BtI_}f literal 13776 zcma)iWmuG96Yc^6(kiJSsB||-tB5oxNOyNgcY`1T(!EP4h|=99U5j)`NjL1m!jk9l zJKuHAb)EC$oFDMA%k#ePJTvpm%ze*9y;YGV!heJhfk24l+t?dR)@ z<0rY~s;U`nQ$g$IWO{&vZ`+Fd_tqov2RNOdG4Fnqpt-Ny9ttcUk|Bi6Xv7V)Z#?1zQXO7>sPc751W-;QQJzTa}Y;|kr2)Aie^ zHGY!TaAEZB94E%F(PPqlpY62Qv$XBjZU4uo0=cDF2Tems+N+r-YQ+&Ywj=Y z><`@V*4_Hx8?}7yF_JnIY8zpf&Y26W%HM+?d|dO13jBbI+vo|5rf=pnZ+jj*mXO8d zH+!2!$x6mDxb3ZVma{4f`*k`k>JpHDd9}JN_Dt`MY(-n&d3^}?p7b?5P(rE0owM1) zUE_wEw57KXo-^D0@d9@1W1cU(Pbz&#z-d_~&8NE_dn=OY?6M{tW~~w|um;Ox9;v~! zi3=>aFw@#})x>w(ioH#|{vGmb#XK;{WK$3q*%wiMfsR=qA;NH?owK>ldwoeKLjTIl zKyX^HNUr>L)uDMYP_^wA8gLFh5WjSQd-2tv`?f=j%m^0V{B+zT7`Zw{iC6h@i8N9} z!-hTb@Z`Q7IaBLe%Q zF!NiOG9tc&a0jBE0*g`izaOd4>LG>0b_S(#3qCyCOhxBk7tj}0amORI)_zv3k&Gp>ul(Y*mY@4?tXZ_v ztA@W`Rf_1tZY@I9OZ4(hkQy9x3YkR2+<(xmkLM>=s|-Yo6RUc^=AacL%$K4+;)rYf z++8Rc_{7&zVro-l>;JCIhQ!@)a>pms2$-uS!vB+4fK*jZfaEOd3uCO@#^mnoB9exS zZo%p*oP9NFkIpsgz)cL+macP@(nc*fM^G68r7G2?<&m^&y%^dU9NB#S`R$%Lm!7Q+ z{TE@h=Mng3m#Warkwn-kHWyvF-4dMOWM(6DXrgg;lyVPUZ>tFJPkr#UDHXdZK2R2) zxIt<8Ltu&Hf(lBOfVg2SugOW#g+QQ9-T8y5k~Mbdg7zzXC*7>5m^H6hQHi(G*kVG4 z-Q(G37jIfpk1v>7QgbU7Hq|FDOlyC?PV~%wOflUu8-T_iOi7ucrRt+-j5HQy>GfN+ zFw!&lo-;f6UiCBgW`=j+vXzs{0iMeY&s)6qn-Lv!0DjC=Fy=Bz4^XZ~xhotgA>4-by`(Mu;nQom@7H}F>EpKts+ zA2!izQtsi0Nq*6#76)=iD`Z>Q50sC8!XAGwW}z1^lrgFc8D~VG$yS7k5~l7!M_ERM z55>@$nhy|8MW`w`-k+M-#k;w<%v!Rqxb+a~tT8kPbFwbR`n^U&98OZrq^dEs4$REl zA+K(JEsH)kGMSL}?T6O)ZxobNwAS&>Rk@o!dx%$ryEA8yz1>vBuq5!@q+FcmPmStB zRzd8Duasfa12?*@t_4uiAFpz%8Kf#RVhp(3J`BY1dAVeUaWxK`QCRtU`9jNnZQU34gY61kZZ0Of!%4P*ewB^yz|O{+g(!t|hjtloz5$ zFG`3%vSc7ih_Brm>cqzKG;`0t8N}$KcA2p!QK%#v4oSCsBq~YcQkH26~ujV0_VD*{t^k$Sq`=Fh-&^O z`|W$wqfFC?TAl$GC!3xO^E`b^Y34Iz^X(yM#@Ek(W3De#2;JPKB z@io~xsGDtrMBcyyEbN*(jQX!%_Vap8LR{T8eQ*|UEqVJ~s?O5spnjLIsz7@C#pw%M z*p9rzo=lp7p`vo_GNv2Mzk{a8@^{wlrnpyl!;pAQa;x#zv6G=mGv}EOj-dHDG7d;0ld(B>M4(fdYVysMWhF31K)OZ@rB&Bl;t$$XOR*ZfA zR1mp1tkISq&=+{b)*m;lv^V+Eb9w9G8~Q#g8dL=^-&zWx{N11ZTj*YHZVbW7upnxHLsv z@0gjA2nVi=uu*5qfpm*Ur(rIgc9evn-9#teTKc_cqNZZ3X zw5V^H{Io|qG2^fKd48I^mXx6?VkQ_o4_8(+awzNTJ!-bsw%`&v z*>vWrD{qp;Htd+3n4dDM%=_e8hki{Ze$@i=fkQRgYdb=jhs(~OXumdJ7&>?HMi~?V z6MAAJubeE6mjkWD8sJd#*$n9;-zEP>kK_#)wAA3+8FQ-1p z&2NVNQPi+vA+bFv?X$K3P4)Vx9X-Oq@?391WR(y>`5 zHJ(uyHyTi%KYP2OCM5ZpusEgsQKU~AHL{!G_tekBXHw4!eVY;1Vvez# zcq0lY->>)f5e)BMWGTP=ES!U zOL7!6`2KWK*VwemD7A-+kb{OQZNk*r+ztaw|D}@8ad5im@urS*0kt6Er|%H7Xd~%lkh4GvrQ}C z=F-!Bg~;m5=4ZalM?H#vzCyMO-In+hGBVBJEv!=OV8VEiu%+TcSY6nmm_HZ7a)myn zc$;F&{8nlAJCZf;?E}}DiQ(@HlQuL8!DJZ^LNZtvvC;nxWmRWfUa0?sGGi!7&}RD= z@GEErYgyz83gSil5Xsh)Sakk+XaD$)JYGzO@%oz*@lZ&)4|RWAb8&Uo{Q!KS%o9<# z!PuanP5cWRc~JA&CmvIL&AkqrwUu)G(e{_`^G`MECvBS*)()!v?86DIVwZaz+;%0{ z3W{D;(s$VjDKXr}F4YlKNg%emUUNrc*!Hn4<5TQ=(y~i1B z1{Ijj8CO^zAJ`D8&*aj?b4I2!!gQC32J)-|d;iWsX>3ulZa@g zzGO73>%3#yyc29XM^b^S(vgt5m_8Fpm7Bd6S34PgM_+DzHkA48JYGHdMUBL?j>Jzp zov(SUn09o|bV+lIpJ+4%WQJ%*N}(|_3d^ZxezEW@u{wcTe^rD3C5>(Ec4mIh6CcTTj??*xaK{1iaeTAA4{)#bp$XDKz11@E56?WWtXK=XC_4Bic=pHTTMr-gJS z(BUUte@C@BppTc>r1*>|$u6TmGr|((;imXLqNygU5jxvNiw<9(Z_Z6>;JCZ6s-%t2 zdhvbJv25S-?&@pC%*D+Rad3(rB1Pob-bh{lU6#5I8Al&lQ3yR2*(WQL)XX=OQGe~9 zh;k!gS&Y%Fhv|H`{t0OK`B}nsvfA5Pb~EG3X$Tgp zF&fZ9r6xB+4EtW(48`Z|{V?Pa5BFSX6coT_p}{Yj$<$3h(T{F(zm#4*B;%;+i~{vE zhhbgr^_`Bo7bY$RDr6(^g+q4k%FUXq+13tDZ!J?FB``{(8lN2P%O=xtjy$3zb9nZB z-aIH2(;k^RN+E12kQk!>P<2!}#tGcmRe$#D$oLCAdUPUj>EWBd==Ci3^SHPxCn?pr}3{UASPlboS zvlP0k88$?A`IXqXWpZBTe7G~wPraclEobQV)G_fwtK*va;3Xytdx%pU){9O!jH;ac7 zC_98FqdOC?Y@YI80hNH9V6pQBwVyd(=cFk~Z{H)>TIAR&Mr#ARiymuBXrJQYu(X)N?}}Mln5e5Gyyp3QQj?vQFN@``H)&pb+wbn*{TYkmCj=E%k4v`Iwzwo&$j}6VH*3TD)=OS5*_r?X6WVU zkG3pdli45{HWyfyF4CV+2avM+4!%3wrp<|TwJA2!99?*wL1fDwVi(pXqJJ2?8AVCKRB6;G#CX`<(2?tuk9psUKk?7fl`Krk zo2K-Y1xvpp?={9ME8+Uuv%*^bIN@#fh{sfEsc9O+^u+GcnQ$%ipr$OWcfBhx+LIS4b+sjQnu_@+_BYAKY>Qg2AKd=!zxm#cNytFvHf_?n z6_nt~So?fU*H46%t(W{Nma#(XErsF)Uc1~q8-*7oig+e}D>iFR>T!7P`C@ZAn^*L1Xpu(r zq!b!QWC|Mw!C&~8nNH~_bu z4Dm?<5=5%cfrdcRga21jj+B5vv6v(v&DifHAd8rMBtdmSLs-k4PmmBVf#J5) zx2<{}QI7z(&kU?G<>E&}89zopDU>G11zV-3DZni@`h5>0&)(uoHE*ZS{@UcyQ7r@> z>|?WwrL)f#vBtZr?HJwroCR2lW z-S8f*!+$u&4b`>IF>_QLFXv{PPO!QYtA1TiCT+xJ+w;$Vv*i=O#lJKf@XFa#>6;S1 z%LYcylMzRwrk}1Bc4Fb}{PJ(x>a23|SY1rqhW>g(oJF6Rx1LV6f*Sw4c^b`<;56`n zAZTz|tPV&b<$Dy?oyb#W)=*i~!lWIAZ`GdZJb@(IXD%iXXLe*ze|z4Zd6a+#H(v=t zHEU}EMev#C4aBN4q z-U!^XNj_xd>8T!?J`m+t;Z#-*Sm`;d0rKk^Sf2+nOJ1Clu1W(L6ul593$qPF&789~ ztS;LWm(c>*1pg{hQpMz-!lVQ1cdL#aMO_+mFrSlBgE!(OrBKCXQoiX$p$N&E^AtKL zG6t%Cc#aR1N&}c4-{X99n9dN#de>yYz}585<| zaO>66h|(CFM~->YygPdlfO$4~f+Ea4Dlx7hAy0K$&kT)fXc^-6SBV_yEDSUah>Adw zs_0CzE7Z&wnSW68pidVirS6X<|7q+0`>zO0!s}H#*O2c9HvIn64@x{wkstxPEU2VD zbt+>`EA9k}QwtBC+_xa_7T`^u(rZ;qOK+h^GHsw@eYewBhu6HxmG7_9{c_z<4GLW^ z*p;Zrmf_Y@NPPM$>dQcLvh6*T=p;{9(nCV3FNfnb6^m}kxo448Izjew7K_C?lO<0i zgqT?hrU(^jfuZi4yn8US+2Zh%02GClfxrALt?7Hjw1a4hvNH5KRpm$VU*79)E{gKM zL=ErH3sVO&ld)v(#cdE0#lFxMF_``ZFG@GfJ}kQU_S)l8-7>3Lz2qKzN&5m?<~)HakGRUdd;ok2?$7T0P+dHYIU_{uTFS z`~1udPJ#QH-!fHfXoFPFPJa%<9?5zzkz^Wq)uUDrCM_)_p-StU-vobdZX;$C<%o1X zEq3xbrE;4%Z|B?oP!e?55Sw0Nj*D3d3o{8vZ#l#o29|`XybfItUMcm@2=p=Y90bR) zxH=`{JS<`;^YMfb!e|J<3;AcG%moD{N?msqIVn@nNP2xX4M@OiA?q*?owbM8MKLi| z(6a>AUDwYWIQMD2!Z79U%NM<9UC^F!gcpetLC|TZJ!X8Hm5d6yBs)EXbPKBI%G*=O z9$H`3>C%iVS7LqYlAv+^Drn^ierWPTc<7h5FU>pfCTd=cT-w)uInnkw;-ZCRpme$0 zdW!wU`_U8RnCaBopJ(R0s>BOwb4jZ49In2f6a;sk7A{_W>n0Mf;^BRaLHi{&UgHZ} zSPECdusx-Dy!U@rv^W{8k3KUdv1~bZsoi3&wCKai4o#dT%TJrfYT%)MpP4`Ncts)? zFPok)mrg|}$w#u+PJ)buT09`s{J#uN;N z@i-0<5uUsVs-vxhqQuJ;b1Vsxuc1Bz`+qpzxH+QSVofS%_9ypF)}mcczpQ;8jS+>x z&+5H@;XUas7KW9hE)qO;X)y_~pB)kAp>Za(YMkwXZvOI^mQ$9`?Wv`+#(Cr4xtlYfqeIoj)xm zuQ%-#_dg~Q#Sf|5)po4n-sGMh;>OYSjl4g!_WGKHol!skS$5NY(A?+MPNKAx$PCDh z6(k9gurll4(=5cP_t-D<+R3hlylSbBwz3>_wyD+91CgVuEF-k4OX-bBCrOyo%DB`! z8e#~Ez3}pHonCFQq)FoRq;*rbc~dv*Kzo)a*?>$gx&`>Am0pA^Dd`O=#+~-Z^v|e5 zHj2R@JGOoc=}mjP-z3b`q-qftBKKGHY_6CT1`*V>wckBKo2^`cs_O<`VseO1`B z$!mM=`CnaYTW;nQ3FnPfHb4UFIGQ7V1=Y4w?Z{8aijm>?t|(sBLQ=50*@8y#>nmgU zR`8x-WLND!+4Y215BVPLr~a6Tnsy#`Ntl~WK|hg6WR@-G)&&iz8LJ!WXo_%5i*=*G zFm1qpMrKn!7QgFn#$E~nKyBttt)%mi;lV*Rq~KVNqkoqCFqH zC8-o9dFb$!SASIFMZd4f@3Y$V(<&U1BeslZ3VD~viu<*>h%ncO?bL;t$H02YG>gA; zsdhKC2NM&wBsQkTRG8;$k7j6a$n5uVtbZ&9K`n1=}Q$ zjMW>ApUXPUh2n<+E8$#{imPcUu1Dxas{Xg~3}oIf_18S#ZH^r$G)BBT5HAi12rJ_u zn&lbJv!?BdAl!ZCuRlLlI9GL1e51hEje|AzQ+zqmYcDon>yQO$7dM%6i>{0Tx(JtV7{GW}< z`{UGNZ%&t%h787#+BkkMM72BMNii1pBH%P4JKo6_igZJH4ywduN*mdXM{WmsJrwwj zdzRv3?=m_+ar~Ca9Z$b-CBzMR94yCmmJHY?CAs+0i1CZtB2T?;6p+*;wq^kU1Y;6J{{>LN>MD$FN3}I;=RdLYy{Gmxql!+7a%Uzp@pkyZi#>!RDrSj;cq{w-h)LDAL8EdW22T`3;!oaTrz6pf(0p&| zLm0%$kn9bCFu&fL#DJ_}{6AsO|3*TGoM0Ye*^;LT7hL}k3P}9tW6m}PxC&z_lYmS8 zLu(&!X&-(ujs>ocI2p9y;>;fyRjAktK$|JswM;)Eom(;G7PW%EF{q%}+Eo4e`uL^E znJh&M@p1zH~M<; zdI+-+Dg1GlK4yjSraNndB#6C%59)RE9+n0rzl;V{@)500mup6Vk0lCpQP976(@#b5 zfaj$|Tx>#%J-+4H`qv!p42PxJVGm;cGPvy0CxB4id;Cd;yC>|_>}XWQ-cz@W^zsB$ z$0|hKS*3HX00r%fdln)8Fs@H`AcFGF*KM8(f<|0ekv9yUdd0FZ8h|-K3t86uQ?w0y z$0|a${!xGhJ^Gyw7?oIrVpue^HSzB0mq@ueC>@c@G@3#nLB{&uwpaTn0I1TNIsl<* zU0UnR7+zC~iF+{-EiZXy$L-gWo zkd;d9K;X9$n}rOvRQG!j$P1y^-zTo=Xk{r0A~r+2OPycO=1Y7v<5g9#V_OYVC#fNg%4^t{SybofA77k`-^&ipJpo@hnRY)1VtFkSCcfdR+= zk_8&GZ2>30%P2Y3B$3EWWaOzN44kfdJGyz-X9K`cRL>lhxyMXlMDCHz(eG;JKtWxn z9nOt@*O+o=U)B@cPp|a7`)DD&++AmpX@!=f(=Q)30&l9 zPwW>}KTxgs4;h02gVzC^Fgun&4hyV}1`G-4!-Heqpt?csy1XwX{Ex5!$5D}1rx~p4 z-h{Q>yvAS&yQ|Et@zlHBX6NXts5Q1^w?zjP@E1OiKq?A2J!YT#yz3GNeD*qY>V@`PK1FoY>-K0Z*mR$^BuC%NyL|Vc5o&D z>D8rqY4_i$F{Aw?nXpN?KgVW2`ow!SEx(k#FwdVeLrlHge-z!u#(>nduZ| zS(V*kckP#^FwzZeZ3`nkp%J<{kMFZs-z|p=Iu%3UAN+=)osKEvrnoUe)X@{KKH7 zglnSJzl!v7N6>=%^H%qala=MY6O>T(`Gzlq%tD!j6N-TODBpwXcM|jB0gR_)dzJJV#R=z1;T?VKyAl4JP0CW_ zB^#4`$N?!qdfM_-Z~~{a8ZLz~UW~+;|5#4&H*1L`-}l|vKQdK1(;jVl&|`POUvXcr z*~zE0TOv7hidDZQ9k7cL6$Q|__dw57P<`sgac4zX>%d78rn$Kpxv70m$ zfPn(nxxX2Eacs929`>sr0@~vM-T3zav*DS`~3(jWi_!^8etV_62A7tknxQH4hQJqco6pD2su|}W&P$^#h+%JhG?*2Et@n<113&eA) zsrTC@zxIZ8^B-`KR0~X$>8XDco&-$8%=kDrQ!h;6C1+Cww5dSYBBt|bGeiL>TZPuPX)(V9f?^8Vcs>=^*SQn6Q1FDuxQT$7fbo}=e>M_{i1la z`r5&RsJ~=ixVfQ9@zy>VnJ1jYMx*TinnNFaEUUT_i+)*gM+tv+w>N-$RGY8HpP}mA zSSy0;vwGb|AH|KU9A*EtWYlYP#|ZE~jNflXE`9}pj7MLxG3sr(OT8V;*w@^(9*S=2 zHCC2NP4}EVkRwJLUIaUVxz9bs3!R%4x+_bCjlWe{&S#3x_*EWufjV#im2iX_R17%D zN$`1DC32|j!_X*fsZ&%>g9ZFsoQZ-^pyfmJCxUV{_0q~bj@zTsgO?<>K8^ZIkQrdz z=JVGkSdxc$U&j59>-i-3*>h4VI~wnGZW(?fsm>gal+sTp4lvacYVXDs>M5LD&G4@X>%Cg@M+?K&s%I}Bd#q352C+9I*9{uH2RFx2^&5|7 zyXG}l-h%$_UB&8Nap_oELIHoEj?sa51LKX=aMhT$ocB5XH!+ZaqnSWf^u~0!0as>b z-OcIJVc^gu+s?4k&C=4wRaEJf>J6au>GYB73vdo4rEh}l2loo5a|WAXH;j)ka89jQyU(x%Q3GZx#ibs`on{8~YxIUf0vk4qibOfeJk;_1CHEd3w?)6sYNnlk3C^THl= zGtWCr^S7r|zn0p4I9 zrb)bW?WK9E-tj}16XT+7z^eNHqVM#OoCp&t>K98Uw-~VEe(&u`0odP^`|TD2Qy+%% zev#CxL=Sn|DgJXRt6%=|#?OqF?=qy1VYc66zE}sSY0n+M>Tx{Uaen}4)J(a}Q8~H% z@hoT;ArAfQUk{N7>pY|GjQoRrAg$CY4hRJp0+GqWw^S1IPYNSgtJ&^FT`y^;{kcMv z#;R$3I|U$;`Rel^?V%twhzw#a4Q5(DLoUo^K9wawW!!YQ71%8eOnJ~+O|60a-wFm4 zOj5BJ06(!dn~xbJ0)_^OKMCnJH4ap@dWHP=J}wQr)N@KBNw(h<*!SQ^zpv|e+q*fA zxec42P}{8RfpU!VxaexL|h<+Af2V~a!9oQ>c@=dcG3sIS|G zK=+02=en*U%bMu-k9XD|ePF?3#Ju7k?vXbrOx0n4lTVP1MMC?sK^AP~4XP}TY{w=ET z{j?%upl&Tw@e`f*YxMep)YWCuA96HkpzrwS%7jEfjHO&Q2JMQGqalXiaWJ^&f0{TS z2J{DTl*^7ba<9@zW zqOjSZ2w(u~u5YcCp{8zS6YtQdrEz9~7OD>P{Ga`LdYcbW-@Q;>H>m2Z1hrM`*1CG0@7XWo{NhXyx5zT}**M-{KweDgvWv2 zofluXd5>jVftI}y;3Pou%8b4GlM7e6S${6bGmZ5kay;z%8?u6F$ zduyt{9r6a3$Zmn8vKc;X&OT8^5y-1o=4g;Dl*Wgq=|AaDnAW?W`O>vtT)q%wBG z_E>M))7}77L`&lmPaMcuZET3A!Md3!+#p=k#$adxZpL%9advR_HPy;Mb2VR@*H;TY zHkJFX1D_VcJF}j;S)9TCf&p+LY+&FhS)*00=IE$F5n=wLGS%37`J2P;<5aRLu%~h? z%kn=Mtpkd*A0i(1+#iUr%hqYSLg>Ec+)BkO9@E?7=A-R+E%Zjb*ylgI;gF65^Cn=G zigMGYs{@viV|P`~2JY(+J=MN>(iM6z13IuluSWo~15vvip_D!LZw?xBCwlr0yRVV; zN=vXilaPz8wYJg&9$txoH%fA3Cb>jf2WyoJmhM#r@QnelJZ_=RX5wImVgrN8vA2h< zAr!xJ|B+ZKt~I8C?-b9AHX?lLIRJwfqb1-^48Z0ht?F}6;u&qNYcSE^ZueEL4;#=q zmf}CQUDF1KdYRRn;+X&TC)rQkfXPG_ZM+sh5(E)j`_KdSlDS|EfcSaXE!^qBJq{8M zJzc~M;)Qj0s8UKfGvHW^vHwdG(tIJJ1f&II;{RX \ No newline at end of file + + + + + + + + + + + + + + + + + + + + From 7b82682b4fba82cd973a37fea1e19c08b48af511 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen Date: Wed, 22 Mar 2023 13:37:25 -0400 Subject: [PATCH 003/283] Changelog back to dev after 3.4.0 --- CHANGES.rst | 121 ++++++++++++++++++++++++++++++++++----------------- CITATION.cff | 4 +- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a8fc619510..9c5df7fcdc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,29 +1,74 @@ -3.4 (unreleased) +3.5 (unreleased) ================ New Features ------------ -- CLI launchers no longer require data to be specified. [#1890] +Cubeviz +^^^^^^^ -- Configurations that support multiple, simultaneous data files now allow - multiple data products to be specified in the command line. [#1890] +Imviz +^^^^^ -- Ability to cycle through datasets to expose information during mouseover. [#1953] +Mosviz +^^^^^^ -- New markers plugin to log mouseover information to a table. [#1953] +Specviz +^^^^^^^ + +Specviz2d +^^^^^^^^^ + +API Changes +----------- Cubeviz ^^^^^^^ -- Moment map output now has celestial WCS, when applicable. [#2009] +Imviz +^^^^^ -- Custom Spectrum1D writer for spectral cube generated by Cubeviz. [#2012] +Mosviz +^^^^^^ + +Specviz +^^^^^^^ + +Specviz2d +^^^^^^^^^ + +Bug Fixes +--------- + +Cubeviz +^^^^^^^ Imviz ^^^^^ -- Table exposing past results in the aperture photometry plugin. [#1985, #2015] +Mosviz +^^^^^^ + +Specviz +^^^^^^^ + +Specviz2d +^^^^^^^^^ + +Other Changes and Additions +--------------------------- + +3.4.1 (unreleased) +================== + +Bug Fixes +--------- + +Cubeviz +^^^^^^^ + +Imviz +^^^^^ Mosviz ^^^^^^ @@ -34,6 +79,33 @@ Specviz Specviz2d ^^^^^^^^^ +3.4 (2023-03-22) +================ + +New Features +------------ + +- CLI launchers no longer require data to be specified. [#1890] + +- Configurations that support multiple, simultaneous data files now allow + multiple data products to be specified in the command line. [#1890] + +- Ability to cycle through datasets to expose information during mouseover. [#1953] + +- New markers plugin to log mouseover information to a table. [#1953] + +Cubeviz +^^^^^^^ + +- Moment map output now has celestial WCS, when applicable. [#2009] + +- Custom Spectrum1D writer for spectral cube generated by Cubeviz. [#2012] + +Imviz +^^^^^ + +- Table exposing past results in the aperture photometry plugin. [#1985, #2015] + API Changes ----------- @@ -67,8 +139,6 @@ Specviz - Removed deprecated ``SpecViz``; use ``Specviz``. [#2092] -Specviz2d -^^^^^^^^^ Bug Fixes --------- @@ -87,14 +157,6 @@ Imviz - Pressing "Home" button on empty additional viewer when images are linked by WCS no longer crashes. [#2082] -Mosviz -^^^^^^ - -Specviz -^^^^^^^ - -Specviz2d -^^^^^^^^^ Other Changes and Additions --------------------------- @@ -104,27 +166,6 @@ Mosviz - Removed subset selection from the Mosviz image viewer. [#2102] -3.3.2 (unreleased) -================== - -Bug Fixes ---------- - -Cubeviz -^^^^^^^ - -Imviz -^^^^^ - -Mosviz -^^^^^^ - -Specviz -^^^^^^^ - -Specviz2d -^^^^^^^^^ - 3.3.1 (2023-03-09) ================== diff --git a/CITATION.cff b/CITATION.cff index 222022e0c9..c031df92a5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -66,9 +66,9 @@ authors: - family-names: "Volfman" given-names: "Sabrina" title: "Jdaviz" -version: 3.3.1 +version: 3.4.0 doi: https://doi.org/10.5281/zenodo.5513927 -date-released: 2023-03-09 +date-released: 2023-03-22 url: "https://github.com/spacetelescope/jdaviz" # see a full list of contributors here: https://github.com/spacetelescope/jdaviz/graphs/contributors From b6dd6c100390c5e3dde47993c994eed7ad62c057 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 24 Mar 2023 10:54:23 -0400 Subject: [PATCH 004/283] Model fitting table (#2093) * table subcomponent: support for adding additional columns * results table for model fitting * units, fixed (and eventually uncert) will be hidden in the UI by default * store uncertainties in table and expose in model_fitting.get_model_component * test coverage * add docs * skip table logging for cube fits with note added to UI when enabling cube fit --- CHANGES.rst | 2 + docs/specviz/export_data.rst | 11 ++++ docs/specviz/plugins.rst | 5 ++ .../default/plugins/markers/markers.py | 1 + .../plugins/model_fitting/model_fitting.py | 63 +++++++++++++++++-- .../plugins/model_fitting/model_fitting.vue | 9 +++ .../model_fitting/tests/test_fitting.py | 39 ++++++++++++ jdaviz/core/template_mixin.py | 32 +++++++++- 8 files changed, 154 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9c5df7fcdc..3edac9644d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ New Features ------------ +* Model fitting results are logged in a table within the plugin [#2093]. + Cubeviz ^^^^^^^ diff --git a/docs/specviz/export_data.rst b/docs/specviz/export_data.rst index f7c0fab6f3..f87e74fca2 100644 --- a/docs/specviz/export_data.rst +++ b/docs/specviz/export_data.rst @@ -96,6 +96,17 @@ To extract all of the model parameters: myparams where the ``model_label`` parameter identifies which model should be returned. + +Alternatively, the table of logged parameter values in the model fitting plugin can be exported to +an :ref:`astropy table ` +by calling :meth:`~jdaviz.core.template_mixin.TableMixin.export_table` (see :ref:`plugin-apis`): + +.. code-block:: python + + model_fitting = specviz.plugins['Model Fitting'] + model_fitting.export_table() + + .. _specviz-export-markers: Markers Table diff --git a/docs/specviz/plugins.rst b/docs/specviz/plugins.rst index c824c0cae2..e6ed909749 100644 --- a/docs/specviz/plugins.rst +++ b/docs/specviz/plugins.rst @@ -103,6 +103,11 @@ show the fitted value of each parameter rather than the initial value, and will additionally show the standard deviation uncertainty of the fitted parameter value if the parameter was not set to be fixed to the initial value. +Parameter values for each fitting run are stored in the plugin table. +To export the table into the notebook via the API, call +:meth:`~jdaviz.core.template_mixin.TableMixin.export_table` +(see :ref:`plugin-apis`). + .. seealso:: :ref:`Export Models ` diff --git a/jdaviz/configs/default/plugins/markers/markers.py b/jdaviz/configs/default/plugins/markers/markers.py index 30ed465594..ed06f00910 100644 --- a/jdaviz/configs/default/plugins/markers/markers.py +++ b/jdaviz/configs/default/plugins/markers/markers.py @@ -72,6 +72,7 @@ def __init__(self, *args, **kwargs): self.table.headers_avail = headers self.table.headers_visible = headers + self.table._default_values_by_colname = _default_table_values # subscribe to mouse events on any new viewers self.hub.subscribe(self, ViewerAddedMessage, handler=self._on_viewer_added) diff --git a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py index ac2ebb9b22..1620db5df7 100644 --- a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py @@ -17,7 +17,8 @@ DatasetSelectMixin, DatasetSpectralSubsetValidMixin, AutoTextField, - AddResultsMixin) + AddResultsMixin, + TableMixin) from jdaviz.core.custom_traitlets import IntHandleEmpty from jdaviz.core.user_api import PluginUserApi from jdaviz.configs.default.plugins.model_fitting.fitting_backend import fit_model_to_spectrum @@ -39,7 +40,7 @@ def __init__(self, value, unit=None): @tray_registry('g-model-fitting', label="Model Fitting", viewer_requirements='spectrum') class ModelFitting(PluginTemplateMixin, DatasetSelectMixin, SpectralSubsetSelectMixin, DatasetSpectralSubsetValidMixin, - AddResultsMixin): + AddResultsMixin, TableMixin): """ See the :ref:`Model Fitting Plugin Documentation ` for more details. @@ -63,6 +64,7 @@ class ModelFitting(PluginTemplateMixin, DatasetSelectMixin, * :meth:`set_model_component` * :meth:`reestimate_model_parameters` * ``equation`` (:class:`~jdaviz.core.template_mixin.AutoTextField`) + * :meth:`equation_components` * ``cube_fit`` Only exposed for Cubeviz. Whether to fit the model to the cube instead of to the collapsed spectrum. @@ -154,6 +156,16 @@ def __init__(self, *args, **kwargs): self.residuals = AutoTextField(self, 'residuals_label', 'residuals_label_default', 'residuals_label_auto', 'residuals_label_invalid_msg') + headers = ['model', 'data_label', 'spectral_subset', 'equation'] + if self.config == 'cubeviz': + headers += ['spatial_subset', 'cube_fit'] + + self.table.headers_avail = headers + self.table.headers_visible = headers + # when model parameters are added as columns, only show the value columns by default + # (other columns can be show in the dropdown by the user) + self.table._new_col_visible = lambda colname: colname.split(':')[-1] not in ('unit', 'fixed', 'uncert', 'std') # noqa + # set the filter on the viewer options self._update_viewer_filters() @@ -165,10 +177,11 @@ def user_api(self): expose += ['spectral_subset', 'model_component', 'poly_order', 'model_component_label', 'model_components', 'create_model_component', 'remove_model_component', 'get_model_component', 'set_model_component', 'reestimate_model_parameters', - 'equation', 'add_results', 'residuals_calculate', 'residuals'] + 'equation', 'equation_components', + 'add_results', 'residuals_calculate', 'residuals'] if self.config == "cubeviz": expose += ['cube_fit'] - expose += ['calculate_fit'] + expose += ['calculate_fit', 'clear_table', 'export_table'] return PluginUserApi(self, expose=expose) def _param_units(self, param, model_type=None): @@ -531,6 +544,7 @@ def get_model_component(self, model_component_label, parameter=None): comp = {"model_type": model_component['model_type'], "parameters": {p['name']: {'value': p['value'], 'unit': p['unit'], + 'std': p.get('std', np.nan), 'fixed': p['fixed']} for p in model_component['parameters']}} # noqa if parameter is not None: @@ -630,6 +644,13 @@ def model_components(self): """ return [x["id"] for x in self.component_models] + @property + def equation_components(self): + """ + List of the labels of model components in the current equation + """ + return re.split('[+*/-]', self.equation.value) + def vue_add_model(self, event): self.create_model_component() @@ -688,9 +709,39 @@ def calculate_fit(self, add_data=True): raise ValueError(f"spectral subset '{self.spectral_subset.selected}' {subset_range} is outside data range of '{self.dataset.selected}' {spec_range}") # noqa if self.cube_fit: - return self._fit_model_to_cube(add_data=add_data) + ret = self._fit_model_to_cube(add_data=add_data) else: - return self._fit_model_to_spectrum(add_data=add_data) + ret = self._fit_model_to_spectrum(add_data=add_data) + + if ret is None: # pragma: no cover + # something went wrong in the fitting call and (hopefully) already raised a warning, + # but we don't have anything to add to the table + return ret + + if self.cube_fit: + # cube fits are currently unsupported in tables + return ret + + row = {'model': self.results_label if add_data else '', + 'data_label': self.dataset_selected, + 'spectral_subset': self.spectral_subset_selected, + 'equation': self.equation.value} + if self.app.config == 'cubeviz': + row['spatial_subset'] = self.spatial_subset_selected + row['cube_fit'] = self.cube_fit + + equation_components = self.equation_components + for comp_ind, comp in enumerate(equation_components): + for param_name, param_dict in self.get_model_component(comp).get('parameters', {}).items(): # noqa + colprefix = f"{comp}:{param_name}_{comp_ind}" + row[colprefix] = param_dict.get('value') + row[f"{colprefix}:unit"] = param_dict.get('unit') + row[f"{colprefix}:fixed"] = param_dict.get('fixed') + row[f"{colprefix}:std"] = param_dict.get('std') + + self.table.add_item(row) + + return ret def vue_apply(self, event): self.calculate_fit() diff --git a/jdaviz/configs/default/plugins/model_fitting/model_fitting.vue b/jdaviz/configs/default/plugins/model_fitting/model_fitting.vue index b0a1dd296a..e42107cf78 100644 --- a/jdaviz/configs/default/plugins/model_fitting/model_fitting.vue +++ b/jdaviz/configs/default/plugins/model_fitting/model_fitting.vue @@ -219,6 +219,12 @@ > + + + Note: cube fit results are not logged to table. + + + + + Results History + diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py index 9b06640887..583c409a71 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py @@ -263,3 +263,42 @@ def test_cube_fitting_backend(cubeviz_helper, unc, tmp_path): data_mask = cubeviz_helper.app.data_collection["fitted_cube.fits[MASK]"] flux_mask = data_mask.get_component("flux") assert_array_equal(flux_mask.data, mask) + + +@pytest.mark.filterwarnings(r"ignore:Model is linear in parameters.*") +@pytest.mark.filterwarnings(r"ignore:The fit may be unsuccessful.*") +def test_results_table(specviz_helper, spectrum1d): + data_label = 'test' + specviz_helper.load_data(spectrum1d, data_label=data_label) + + mf = specviz_helper.plugins['Model Fitting'] + mf.create_model_component('Linear1D') + + mf.add_results.label = 'linear model' + mf.calculate_fit(add_data=True) + mf_table = mf.export_table() + assert len(mf_table) == 1 + assert mf_table['equation'][-1] == 'L' + assert mf_table.colnames == ['model', 'data_label', 'spectral_subset', 'equation', + 'L:slope_0', 'L:slope_0:unit', + 'L:slope_0:fixed', 'L:slope_0:std', + 'L:intercept_0', 'L:intercept_0:unit', + 'L:intercept_0:fixed', 'L:intercept_0:std'] + + mf.create_model_component('Gaussian1D') + mf.add_results.label = 'composite model' + mf.calculate_fit(add_data=True) + mf_table = mf.export_table() + assert len(mf_table) == 2 + assert mf_table['equation'][-1] == 'L+G' + assert mf_table.colnames == ['model', 'data_label', 'spectral_subset', 'equation', + 'L:slope_0', 'L:slope_0:unit', + 'L:slope_0:fixed', 'L:slope_0:std', + 'L:intercept_0', 'L:intercept_0:unit', + 'L:intercept_0:fixed', 'L:intercept_0:std', + 'G:amplitude_1', 'G:amplitude_1:unit', + 'G:amplitude_1:fixed', 'G:amplitude_1:std', + 'G:mean_1', 'G:mean_1:unit', + 'G:mean_1:fixed', 'G:mean_1:std', + 'G:stddev_1', 'G:stddev_1:unit', + 'G:stddev_1:fixed', 'G:stddev_1:std'] diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 7007f6c8e4..722ddfb7ff 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -2293,6 +2293,8 @@ class Table(PluginSubcomponent): """ template_file = __file__, "../components/plugin_table.vue" + _default_values_by_colname = {} + headers_visible = List([]).tag(sync=True) # list of strings headers_avail = List([]).tag(sync=True) # list of strings items = List().tag(sync=True) # list of dictionaries, pass single dict to add_row @@ -2301,6 +2303,21 @@ def __init__(self, plugin, *args, **kwargs): self._qtable = None super().__init__(plugin, 'Table', *args, **kwargs) + def default_value_for_column(self, colname=None, value=None): + if colname in self._default_values_by_colname: + return self._default_values_by_colname.get(colname) + if isinstance(value, (tuple, list)): + return [self.default_value_for_column(value=v) for v in value] + if isinstance(value, (float, int)): + return np.nan + if isinstance(value, str): + return '' + return None + + @staticmethod + def _new_col_visible(colname): + return True + def add_item(self, item): """ Add an item/row to the table. @@ -2351,10 +2368,21 @@ def float_precision(column, item): if self._qtable is None: self._qtable = QTable([item]) else: - # NOTE: this does not support adding columns that did not exist in the first - # call to add_row since the last call to clear_table + # add any missing columns with a default value for all previous rows + for colname, value in item.items(): + if colname in self._qtable.colnames: + continue + default_value = self.default_value_for_column(colname=colname, + value=value) + self._qtable.add_column(default_value, name=colname) + self._qtable.add_row(item) + missing_headers = [k for k in item.keys() if k not in self.headers_avail] + if len(missing_headers): + self.headers_avail = self.headers_avail + missing_headers + self.headers_visible = self.headers_visible + [m for m in missing_headers if self._new_col_visible(m)] # noqa + # clean data to show in the UI self.items = self.items + [{k: json_safe(k, v) for k, v in item.items()}] From 8a8af3febe12177569e7a954e4159c6a7174f2f4 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Fri, 24 Mar 2023 16:05:07 -0400 Subject: [PATCH 005/283] DOC: readme tweaks (#2113) * Simple readme tweaks * Minor edits * capitalization * Update README.rst --------- Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- README.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a934cbbce6..7e881ea4c7 100644 --- a/README.rst +++ b/README.rst @@ -96,10 +96,14 @@ The power of ``jdaviz`` is that it can integrated into your Jupyter notebook wor from jdaviz import Specviz specviz = Specviz() - specviz.app + specviz.show() To learn more about the various ``jdaviz`` application configurations and loading data, see the -`specviz `_, `cubeviz `_, `mosviz `_, or `imviz `_ tools. +`Specviz `_, +`Cubeviz `_, +`Mosviz `_, +`Imviz `_, +or `Specviz2D `_ tools. ``jdaviz`` also provides a directory of `sample notebooks `_ to test the application, located in the ``notebooks`` sub-directory of the git repository. ``CubevizExample.ipynb`` is provided as an example that loads a JWST data cube with the @@ -132,7 +136,7 @@ Cite ``jdaviz`` via our Zenodo record: https://doi.org/10.5281/zenodo.5513927. Contributing ------------ -We love contributions! jdaviz is open source, +We love contributions! ``jdaviz`` is open source, built on open source, and we'd love to have you hang out in our community. **Imposter syndrome disclaimer**: We want your help. No, really. From e761361a332fd84b404db30ef905d1f5293b50a0 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 27 Mar 2023 10:34:22 -0400 Subject: [PATCH 006/283] Refactor get subsets to work with composite subset state (#2087) * Retrieve accurate spectral subset bounds Add get_subsets method which replaces get_subsets_from_viewer Remove unused imports Fix specviz helper tests Fix failing tests and address review comments Address review comments Ignore ipykernel deprecation warning Add tests covering composite spectral regions Add check if subset label not in dc subset groups Rename to object_only * Fix surrounding continuum in line analysis * Fix line analysis tests * Only get spectrum viewer if subset is range subset * Decrease size of test data * Update jdaviz/app.py Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> * Cover case where subset state is not ROI, Range, or Composite * Update changes file --------- Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 2 + docs/cubeviz/export_data.rst | 6 +- jdaviz/app.py | 275 +++++++++++++----- .../cubeviz/plugins/tests/test_parsers.py | 4 +- .../model_fitting/tests/test_plugin.py | 2 +- jdaviz/configs/specviz/helper.py | 4 +- .../plugins/line_analysis/line_analysis.py | 32 +- .../line_analysis/tests/test_line_analysis.py | 2 +- jdaviz/configs/specviz/tests/test_helper.py | 104 +++++-- jdaviz/core/tests/test_template_mixin.py | 4 +- jdaviz/tests/test_subsets.py | 129 +++++++- 11 files changed, 438 insertions(+), 126 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3edac9644d..698862b570 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,6 +24,8 @@ Specviz2d API Changes ----------- +- Add ``get_subsets()`` method to app level to centralize subset information retrieval. [#2087] + Cubeviz ^^^^^^^ diff --git a/docs/cubeviz/export_data.rst b/docs/cubeviz/export_data.rst index 6c2419eb0c..70ebac4c9d 100644 --- a/docs/cubeviz/export_data.rst +++ b/docs/cubeviz/export_data.rst @@ -30,12 +30,12 @@ An example without accessing Specviz: subset1_spec1d = cubeviz.get_data(data_label=flux_data_label, subset_to_apply="Subset 1", - statistic="mean") + function="mean") -Note that in the above example, the ``statistic`` keyword is used to tell Cubeviz +Note that in the above example, the ``function`` keyword is used to tell Cubeviz how to collapse the flux cube down to a one dimensional spectrum - this is not necessarily equivalent to the collapsed spectrum in the spectrum viewer, which -may have used a different collapse statistic. +may have used a different collapse function. To get all subsets from the spectrum viewer: diff --git a/jdaviz/app.py b/jdaviz/app.py index 132ebec1c8..6155b59bcf 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -4,12 +4,16 @@ import uuid import warnings from inspect import isclass +import operator from ipywidgets import widget_serialization import ipyvue from astropy.nddata import CCDData, NDData from astropy.io import fits +from astropy import units as u +from astropy.coordinates import Angle +from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion from echo import CallbackProperty, DictCallbackProperty, ListCallbackProperty from ipygoldenlayout import GoldenLayout @@ -31,7 +35,9 @@ SubsetUpdateMessage, SubsetDeleteMessage) from glue.core.state_objects import State -from glue.core.subset import Subset, RangeSubsetState, RoiSubsetState +from glue.core.subset import (Subset, RangeSubsetState, RoiSubsetState, + CompositeSubsetState, InvertState) +from glue.core.roi import CircularROI, EllipticalROI, RectangularROI from glue_astronomy.spectral_coordinates import SpectralCoordinates from glue_jupyter.app import JupyterApplication from glue_jupyter.common.toolbar_vuetify import read_icon @@ -729,54 +735,6 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type cls=None) regions = {} - def _get_all_subregions(mask, spec_axis_data): - """ - Return all subregions within a subset. - - Parameters - ---------- - mask : list - List of indices in spec_axis_data that are part of the subset. - spec_axis_data : list - List of spectral axis values. - Returns - ------- - combined_spec_region : `~specutils.SpectralRegion` - SpectralRegion object containing all subregions of the subset. - """ - if len(mask) == 0: - # Mask should only be 0 if ApplyROI is used to incorrectly - # create subsets via the API - raise ValueError("Mask has length 0, ApplyROI may have been used incorrectly") - - current_edge = 0 - combined_spec_region = None - for index in range(1, len(mask)): - # Find spot where mask == True is for a different region of the subset - # i.e. mask = [0, 1, 4, 5] - # mask[2] != mask[1] + 1 - if mask[index] != mask[index - 1] + 1: - subset_region = spec_axis_data[mask[current_edge]: mask[index - 1] + 1] - if not combined_spec_region: - combined_spec_region = SpectralRegion(min(subset_region), - max(subset_region)) - else: - combined_spec_region += SpectralRegion(min(subset_region), - max(subset_region)) - current_edge = index - - # Get last region within the subset - if current_edge != index: - subset_region = spec_axis_data[mask[current_edge]: mask[index] + 1] - # No if check here because len(mask) must be greater than 1 - # so combined_spec_region will have been instantiated in the for loop - if combined_spec_region is None: - combined_spec_region = SpectralRegion(min(subset_region), max(subset_region)) - else: - combined_spec_region += SpectralRegion(min(subset_region), max(subset_region)) - - return combined_spec_region - if data_label is not None: data = {data_label: data} @@ -811,30 +769,12 @@ def _get_all_subregions(mask, spec_axis_data): # translation machinery entirely and construct the astropy # region ourselves. elif value.data.ndim == 1: - # Grab the data units from the glue-astronomy spectral axis - # TODO: this needs to be much simpler; i.e. data units in - # the glue component objects - # Cases where there is a single subset - if '_orig_spec' in value.data.meta: # Hack for Cubeviz WCS propagation - data_wcs = value.data.meta['_orig_spec'] - else: - data_wcs = value.data.coords - - subregions_in_subset = _get_all_subregions( - np.where(value.to_mask() == True)[0], # noqa - data_wcs.spectral_axis) - - regions[key] = subregions_in_subset + regions[key] = self.get_subsets(key) continue temp_data = self.get_data_from_viewer(viewer_reference, value.label) if isinstance(temp_data, Spectrum1D): - # Note that we look for mask == False here, rather than True above, - # because specutils masks are the reverse of Glue (of course) - subregions_in_subset = _get_all_subregions( - np.where(~temp_data.mask)[0], # noqa - temp_data.spectral_axis) - regions[key] = subregions_in_subset + regions[key] = self.get_subsets(key) continue # Get the pixel coordinate [z] of the 3D data, repeating the @@ -887,6 +827,203 @@ def _get_all_subregions(mask, spec_axis_data): return regions + def get_subsets(self, subset_name=None, spectral_only=False, + spatial_only=False, object_only=False): + """ + Returns all branches of glue subset tree in the form that subset plugin can recognize. + + Parameters + ---------- + subset_name : str + The subset name. + spectral_only : bool + Return only spectral subsets. + spatial_only : bool + Return only spatial subsets. + object_only : bool + Return only object relevant information and + leave out the region class name and glue_state. + + Returns + ------- + data : dict + A dict with keys representing the subset name and values as astropy regions + objects + """ + + dc = self.data_collection + subsets = dc.subset_groups + + all_subsets = {} + + for subset in subsets: + label = subset.label + if isinstance(subset.subset_state, CompositeSubsetState): + # Region composed of multiple ROI or Range subset + # objects that must be traversed + subset_region = self.get_sub_regions(subset.subset_state) + elif isinstance(subset.subset_state, RoiSubsetState): + # 3D regions represented as a dict including an + # AstropyRegion object if possible + subset_region = self._get_roi_subset_definition(subset.subset_state) + elif isinstance(subset.subset_state, RangeSubsetState): + # 2D regions represented as SpectralRegion objects + subset_region = self._get_range_subset_bounds(subset.subset_state) + else: + # subset.subset_state can be an instance of MaskSubsetState + # or something else we do not know how to handle + all_subsets[label] = None + continue + + if isinstance(subset_region, SpectralRegion): + subset_region = self._remove_duplicate_bounds(subset_region) + + if spectral_only and isinstance(subset_region, SpectralRegion): + all_subsets[label] = subset_region + elif spatial_only and not isinstance(subset_region, SpectralRegion): + if object_only: + all_subsets[label] = [reg['region'] for reg in subset_region] + else: + all_subsets[label] = subset_region + elif not spectral_only and not spatial_only: + if object_only and not isinstance(subset_region, SpectralRegion): + all_subsets[label] = [reg['region'] for reg in subset_region] + else: + all_subsets[label] = subset_region + + all_subset_names = [subset.label for subset in dc.subset_groups] + if subset_name and subset_name in all_subset_names: + return all_subsets[subset_name] + elif subset_name: + raise ValueError(f"{subset_name} not in {all_subset_names}") + else: + return all_subsets + + def _remove_duplicate_bounds(self, spec_regions): + regions_no_dups = None + + for region in spec_regions: + if not regions_no_dups: + regions_no_dups = region + elif region.bounds not in regions_no_dups.subregions: + regions_no_dups += region + return regions_no_dups + + def _get_range_subset_bounds(self, subset_state): + # TODO: Use global display units + # units = dc[0].data.coords.spectral_axis.unit + viewer = self.get_viewer(self._jdaviz_helper. _default_spectrum_viewer_reference_name) + data = viewer.data() + if viewer: + units = u.Unit(viewer.state.x_display_unit) + elif data and len(data) > 0 and isinstance(data[0], Spectrum1D): + units = data[0].spectral_axis.unit + else: + raise ValueError("Unable to find spectral axis units") + return SpectralRegion(subset_state.lo * units, subset_state.hi * units) + + def _get_roi_subset_definition(self, subset_state): + _around_decimals = 6 + roi = subset_state.roi + roi_as_region = None + if isinstance(roi, CircularROI): + x, y = roi.get_center() + r = roi.radius + roi_as_region = CirclePixelRegion(PixCoord(x, y), r) + + elif isinstance(roi, RectangularROI): + theta = np.around(np.degrees(roi.theta), decimals=_around_decimals) + roi_as_region = RectanglePixelRegion(PixCoord(roi.center()[0], roi.center()[1]), + roi.width(), roi.height(), Angle(theta, "deg")) + + elif isinstance(roi, EllipticalROI): + xc = roi.xc + yc = roi.yc + rx = roi.radius_x + ry = roi.radius_y + theta = np.around(np.degrees(roi.theta), decimals=_around_decimals) + roi_as_region = EllipsePixelRegion(PixCoord(xc, yc), rx, ry, Angle(theta, "deg")) + + return [{"name": subset_state.roi.__class__.__name__, + "glue_state": subset_state.__class__.__name__, + "region": roi_as_region}] + + def get_sub_regions(self, subset_state): + + if isinstance(subset_state, CompositeSubsetState): + if subset_state and hasattr(subset_state, "state2") and subset_state.state2: + one = self.get_sub_regions(subset_state.state1) + two = self.get_sub_regions(subset_state.state2) + + if (isinstance(one, list) and "glue_state" in one[0] and + one[0]["glue_state"] == "RoiSubsetState"): + one[0]["glue_state"] = subset_state.__class__.__name__ + + if isinstance(subset_state.state2, InvertState): + # This covers the REMOVE subset mode + + # As an example for how this works: + # a = SpectralRegion(4 * u.um, 7 * u.um) + SpectralRegion(9 * u.um, 11 * u.um) + # b = SpectralRegion(5 * u.um, 6 * u.um) + # After running the following code with a as one and b as two: + # Spectral Region, 3 sub-regions: + # (4.0 um, 5.0 um) (6.0 um, 7.0 um) (9.0 um, 11.0 um) + if isinstance(two, SpectralRegion): + new_spec = None + for sub in one: + if not new_spec: + new_spec = two.invert(sub.lower, sub.upper) + else: + new_spec += two.invert(sub.lower, sub.upper) + return new_spec + else: + if isinstance(two, list): + # two[0]['glue_state'] = subset_state.state2.__class__.__name__ + two[0]['glue_state'] = "AndNotState" + # Return two first so that we preserve the chronology of how + # subset regions are applied. + return two + one + elif subset_state.op is operator.and_: + # This covers the AND subset mode + + # Example of how this works: + # a = SpectralRegion(4 * u.um, 7 * u.um) + # b = SpectralRegion(5 * u.um, 6 * u.um) + # + # b.invert(a.lower, a.upper) + # Spectral Region, 2 sub-regions: + # (4.0 um, 5.0 um) (6.0 um, 7.0 um) + if isinstance(two, SpectralRegion): + return two.invert(one.lower, one.upper) + else: + return one + two + elif subset_state.op is operator.or_: + # This covers the ADD subset mode + # one + two works for both Range and ROI subsets + if one and two: + return one + two + elif one: + return one + elif two: + return two + elif subset_state.op is operator.xor: + # This covers the XOR case which is currently not working + return None + else: + return None + else: + # This gets triggered in the InvertState case where state1 + # is an object and state2 is None + return self.get_sub_regions(subset_state.state1) + elif subset_state is not None: + # This is the leaf node of the glue subset state tree where + # a subset_state is either ROI or Range. + if isinstance(subset_state, RoiSubsetState): + return self._get_roi_subset_definition(subset_state) + + elif isinstance(subset_state, RangeSubsetState): + return self._get_range_subset_bounds(subset_state) + def add_data(self, data, data_label=None, notify_done=True): """ Add data to the Glue ``DataCollection``. diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index 5a131ab9e0..48b9ba81f9 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -77,8 +77,8 @@ def test_spectrum1d_with_fake_fixed_units(spectrum1d, cubeviz_helper): reg = subsets.get('Subset 1') assert len(subsets) == 1 - assert_allclose(reg.lower.value, 6666.666666666667) - assert_allclose(reg.upper.value, 7333.333333333334) + assert_allclose(reg.lower.value, 6600.) + assert_allclose(reg.upper.value, 7400.) assert reg.lower.unit == 'Angstrom' assert reg.upper.unit == 'Angstrom' diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py index 62621b918d..42232520bc 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py @@ -350,7 +350,7 @@ def test_invalid_subset(specviz_helper, spectrum1d): plugin.spectral_subset = 'Subset 1' assert not plugin._obj.spectral_subset_valid - with pytest.raises(ValueError, match=r"spectral subset 'Subset 1' \(5000.0, 5888.888888888889\) is outside data range of 'right_spectrum' \(6000.0, 8000.0\)"): # noqa + with pytest.raises(ValueError, match=r"spectral subset 'Subset 1' \(5000.0, 6000.0\) is outside data range of 'right_spectrum' \(6000.0, 8000.0\)"): # noqa plugin.calculate_fit() plugin.dataset = 'left_spectrum' diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 84f7ce2160..04db731d89 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -139,9 +139,7 @@ def get_spectral_regions(self): Mapping from the names of the subsets to the subsets expressed as `specutils.SpectralRegion` objects. """ - return self.app.get_subsets_from_viewer( - self._default_spectrum_viewer_reference_name, subset_type="spectral" - ) + return self.app.get_subsets(spectral_only=True) def x_limits(self, x_min=None, x_max=None): """Sets the limits of the x-axis diff --git a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py index 7373a5ab59..134a9b917d 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py @@ -330,13 +330,13 @@ def _calculate_statistics(self, *args, **kwargs): self.update_results(None) return - sr = self.app.get_subsets_from_viewer(self._default_spectrum_viewer_reference_name, - subset_type="spectral").get(self.spectral_subset_selected) # noqa - + sr = self.app.get_subsets().get(self.spectral_subset_selected) if self.spectral_subset_selected == "Entire Spectrum": spectrum = full_spectrum else: spectrum = extract_region(full_spectrum, sr, return_single_spectrum=True) + sr_lower = spectrum.spectral_axis[spectrum.spectral_axis.value >= sr.lower.value][0] + sr_upper = spectrum.spectral_axis[spectrum.spectral_axis.value <= sr.upper.value][-1] # compute continuum if self.continuum_subset_selected == "Surrounding" and self.spectral_subset_selected == "Entire Spectrum": # noqa @@ -355,29 +355,27 @@ def _calculate_statistics(self, *args, **kwargs): self.update_results(None) return - spectral_region_width = sr.upper - sr.lower + spectral_region_width = sr_upper - sr_lower # convert width from total relative width, to width per "side" width = (self.width - 1) / 2 - left, = np.where((spectral_axis < sr.lower) & - (spectral_axis > sr.lower - spectral_region_width*width)) - + left, = np.where((spectral_axis < sr_lower) & + (spectral_axis > sr_lower - spectral_region_width*width)) if not len(left): # then no points matching the width are available outside the line region, # so we'll default to the left-most point of the line region. left, = np.where(spectral_axis == min(spectrum.spectral_axis)) - right, = np.where((spectral_axis > sr.upper) & - (spectral_axis < sr.upper + spectral_region_width*width)) - + right, = np.where((spectral_axis > sr_upper) & + (spectral_axis < sr_upper + spectral_region_width*width)) if not len(right): # then no points matching the width are available outside the line region, # so we'll default to the right-most point of the line region. right, = np.where(spectral_axis == max(spectrum.spectral_axis)) continuum_mask = np.concatenate((left, right)) - mark_x = {'left': np.array([min(spectral_axis.value[continuum_mask]), sr.lower.value]), - 'center': np.array([sr.lower.value, sr.upper.value]), - 'right': np.array([sr.upper.value, max(spectral_axis.value[continuum_mask])])} + mark_x = {'left': np.array([min(spectral_axis.value[continuum_mask]), sr_lower.value]), + 'center': np.array([sr_lower.value, sr_upper.value]), + 'right': np.array([sr_upper.value, max(spectral_axis.value[continuum_mask])])} else: # we'll access the mask of the continuum and then apply that to the spectrum. For a @@ -393,8 +391,8 @@ def _calculate_statistics(self, *args, **kwargs): 'center': spectral_axis.value, 'right': []} else: - mark_x = {'left': spectral_axis_nanmasked[spectral_axis.value < sr.lower.value], - 'right': spectral_axis_nanmasked[spectral_axis.value > sr.upper.value]} + mark_x = {'left': spectral_axis_nanmasked[spectral_axis.value < sr_lower.value], + 'right': spectral_axis_nanmasked[spectral_axis.value > sr_upper.value]} # Center should extend (at least) across the line region between the full # range defined by the continuum subset(s). # OK for mark_x to be all NaNs. @@ -402,8 +400,8 @@ def _calculate_statistics(self, *args, **kwargs): warnings.simplefilter('ignore', category=RuntimeWarning) mark_x_min = np.nanmin(mark_x['left']) mark_x_max = np.nanmax(mark_x['right']) - left_min = np.nanmin([mark_x_min, sr.lower.value]) - right_max = np.nanmax([mark_x_max, sr.upper.value]) + left_min = np.nanmin([mark_x_min, sr_lower.value]) + right_max = np.nanmax([mark_x_max, sr_upper.value]) mark_x['center'] = np.array([left_min, right_max]) continuum_x = spectral_axis[continuum_mask].value diff --git a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py index a6f6d13a28..34812a2453 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py @@ -427,7 +427,7 @@ def test_invalid_subset(specviz_helper, spectrum1d): plugin.spectral_subset = 'Subset 1' assert not plugin._obj.spectral_subset_valid - with pytest.raises(ValueError, match=r"spectral subset 'Subset 1' \(5000.0, 5888.888888888889\) is outside data range of 'right_spectrum' \(6000.0, 8000.0\)"): # noqa + with pytest.raises(ValueError, match=r"spectral subset 'Subset 1' \(5000.0, 6000.0\) is outside data range of 'right_spectrum' \(6000.0, 8000.0\)"): # noqa plugin.get_results() plugin.dataset = 'left_spectrum' diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index e7dd0ef490..82543695ff 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -5,7 +5,7 @@ from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from glue.core.roi import XRangeROI -from glue.core.edit_subset_mode import OrMode +from glue.core.edit_subset_mode import OrMode, AndMode, AndNotMode from specutils import Spectrum1D, SpectrumList, SpectrumCollection from astropy.utils.data import download_file @@ -136,28 +136,89 @@ def test_get_spectral_regions_three(self): assert_quantity_allclose(spec_region['Subset 1'].subregions[0][0].value, 6000., atol=1e-5) assert_quantity_allclose(spec_region['Subset 1'].subregions[0][1].value, - 6222.22222222, atol=1e-5) + 6400., atol=1e-5) assert_quantity_allclose(spec_region['Subset 1'].subregions[1][0].value, - 6666.66666667, atol=1e-5) + 6600., atol=1e-5) assert_quantity_allclose(spec_region['Subset 1'].subregions[1][1].value, - 6888.88888889, atol=1e-5) + 7000., atol=1e-5) assert_quantity_allclose(spec_region['Subset 1'].subregions[2][0].value, - 7333.33333333, atol=1e-5) + 7300., atol=1e-5) assert_quantity_allclose(spec_region['Subset 1'].subregions[2][1].value, - 7777.77777778, atol=1e-5) + 7800., atol=1e-5) - def test_get_spectral_regions_raise_value_error(self): - with pytest.raises(ValueError): - spectrum_viewer = self.spec_app.app.get_viewer("spectrum-viewer") + def test_get_spectral_regions_does_not_raise_value_error(self): + spectrum_viewer = self.spec_app.app.get_viewer("spectrum-viewer") + + spectrum_viewer.session.edit_subset_mode._mode = OrMode + # Selecting ROIs that are not part of the actual spectrum no longer raises an error + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(1, 3)) + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(4, 6)) + + spec_region = self.spec_app.get_spectral_regions() + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][0].value, + 1, atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][1].value, + 3, atol=1e-5) + + assert_quantity_allclose(spec_region['Subset 1'].subregions[1][0].value, + 4, atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[1][1].value, + 6, atol=1e-5) + + def test_get_spectral_regions_composite_region(self): + spectrum_viewer = self.spec_app.app.get_viewer("spectrum-viewer") + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6000, 6400)) + + spectrum_viewer.session.edit_subset_mode._mode = AndNotMode + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6600, 7000)) + + spectrum_viewer.session.edit_subset_mode._mode = AndMode + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(7300, 7800)) + + spec_region = self.spec_app.get_spectral_regions() + + assert len(spec_region['Subset 1'].subregions) == 1 + # Assert correct values for test with 3 subregions + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][0].value, + 7300., atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][1].value, + 7800., atol=1e-5) + + def test_get_spectral_regions_composite_region_multiple_and_nots(self): + spectrum_viewer = self.spec_app.app.get_viewer("spectrum-viewer") + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6000, 7800)) + + spectrum_viewer.session.edit_subset_mode._mode = AndNotMode + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6200, 6600)) + + spectrum_viewer.session.edit_subset_mode._mode = AndNotMode + + self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(7300, 7700)) + + spec_region = self.spec_app.get_spectral_regions() - spectrum_viewer.session.edit_subset_mode._mode = OrMode - # Selecting ROIs that are not part of the actual spectrum raises an error - self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(1, 3)) - self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(4, 6)) + assert len(spec_region['Subset 1'].subregions) == 3 + # Assert correct values for test with 3 subregions + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][0].value, + 6000., atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[0][1].value, + 6200., atol=1e-5) - self.spec_app.get_spectral_regions() + assert_quantity_allclose(spec_region['Subset 1'].subregions[1][0].value, + 6600., atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[1][1].value, + 7300., atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[2][0].value, + 7700., atol=1e-5) + assert_quantity_allclose(spec_region['Subset 1'].subregions[2][1].value, + 7800., atol=1e-5) def test_get_spectra_no_spectra(specviz_helper, spectrum1d): @@ -200,8 +261,8 @@ def test_get_spectral_regions_unit(specviz_helper, spectrum1d): subsets = specviz_helper.get_spectral_regions() reg = subsets.get('Subset 1') - assert spectrum1d.wavelength.unit == reg.lower.unit - assert spectrum1d.wavelength.unit == reg.upper.unit + assert spectrum1d.spectral_axis.unit == reg.lower.unit + assert spectrum1d.spectral_axis.unit == reg.upper.unit def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): @@ -246,12 +307,13 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(0.6, 0.7)) + # TODO: Is this test still relevant with the upcoming glue unit conversion changes? # Retrieve the Subset - subsets = specviz_helper.get_spectral_regions() - reg = subsets.get('Subset 1') - - assert reg.lower.unit == u.Unit(new_spectral_axis) - assert reg.upper.unit == u.Unit(new_spectral_axis) + # subsets = specviz_helper.get_spectral_regions() + # reg = subsets.get('Subset 1') + # + # assert reg.lower.unit == u.Unit(new_spectral_axis) + # assert reg.upper.unit == u.Unit(new_spectral_axis) # Coordinates info panel should show new unit label_mouseover._viewer_mouse_event(spec_viewer, diff --git a/jdaviz/core/tests/test_template_mixin.py b/jdaviz/core/tests/test_template_mixin.py index 1ecefb7f34..3523896965 100644 --- a/jdaviz/core/tests/test_template_mixin.py +++ b/jdaviz/core/tests/test_template_mixin.py @@ -28,7 +28,9 @@ def test_spectralsubsetselect(specviz_helper, spectrum1d): assert p.spectral_subset.selected_obj is not None expected_min = spectrum1d.spectral_axis[spectrum1d.spectral_axis.value >= 6500][0] expected_max = spectrum1d.spectral_axis[spectrum1d.spectral_axis.value <= 7400][-1] - assert p.spectral_subset.selected_min_max(spectrum1d) == (expected_min, expected_max) + np.testing.assert_allclose(expected_min.value, 6666.66666667, atol=1e-5) + np.testing.assert_allclose(expected_max.value, 7333.33333333, atol=1e-5) + assert p.spectral_subset.selected_min_max(spectrum1d) == (6500 * u.AA, 7400 * u.AA) # check selected subset mask available via API: expected_mask_with_spectral_subset = ( diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index c8fd0ae926..e8e69d10cb 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -4,9 +4,11 @@ from astropy.tests.helper import assert_quantity_allclose from glue.core import Data from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, XRangeROI -from glue.core.edit_subset_mode import OrMode + +from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode +from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion + from numpy.testing import assert_allclose -from regions import EllipsePixelRegion, RectanglePixelRegion from specutils import SpectralRegion from jdaviz.core.marks import ShadowSpatialSpectral @@ -148,7 +150,7 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): assert len(subsets) == 1 assert isinstance(reg, SpectralRegion) assert_quantity_allclose(reg.lower, 5.0 * u.Hz) - assert_quantity_allclose(reg.upper, 15.0 * u.Hz) + assert_quantity_allclose(reg.upper, 15.5 * u.Hz) assert subset_plugin.subset_selected == "Subset 1" assert subset_plugin.subset_types == ["Range"] @@ -171,14 +173,14 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): reg = subsets.get('Subset 1') assert_quantity_allclose(reg.lower, 10.0 * u.Hz) - assert_quantity_allclose(reg.upper, 15.0 * u.Hz) + assert_quantity_allclose(reg.upper, 15.5 * u.Hz) # Move the Subset. subset_plugin.set_center(10, update=True) subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer', subset_type='spectral') reg = subsets.get('Subset 1') - assert_quantity_allclose(reg.lower, 8 * u.Hz) - assert_quantity_allclose(reg.upper, 12 * u.Hz) + assert_quantity_allclose(reg.lower, 7.25 * u.Hz) + assert_quantity_allclose(reg.upper, 12.75 * u.Hz) def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): @@ -212,7 +214,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): assert isinstance(reg, SpectralRegion) assert_quantity_allclose(reg.lower, 5.0 * u.Hz) - assert_quantity_allclose(reg.upper, 15 * u.Hz) + assert_quantity_allclose(reg.upper, 15.5 * u.Hz) subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer', subset_type='spatial') reg = subsets.get('Subset 2') @@ -277,7 +279,7 @@ def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): assert len(reg) == 2 assert isinstance(reg, SpectralRegion) assert_quantity_allclose(reg[0].lower, 5.0*u.Hz) - assert_quantity_allclose(reg[0].upper, 15.0*u.Hz) + assert_quantity_allclose(reg[0].upper, 15.5*u.Hz) assert_quantity_allclose(reg[1].lower, 30.0*u.Hz) assert_quantity_allclose(reg[1].upper, 35.0*u.Hz) @@ -308,3 +310,114 @@ def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): subset_plugin.subset_selected = "Create New" subset_plugin.subset_selected = "Subset 1" assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "value") == 30 + + +def test_composite_region_from_subset_3d(cubeviz_helper): + data = Data(flux=np.ones((128, 128, 10)), label='Test 3D Flux') + cubeviz_helper.app.data_collection.append(data) + + cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test 3D Flux') + viewer = cubeviz_helper.app.get_viewer('flux-viewer') + + viewer.apply_roi(CircularROI(xc=25, yc=25, radius=5)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + circle1 = CirclePixelRegion(center=PixCoord(x=25, y=25), radius=5) + assert reg[0] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1} + + cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(RectangularROI(25, 30, 25, 30)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + rectangle1 = RectanglePixelRegion(center=PixCoord(x=27.5, y=27.5), + width=5, height=5, angle=0.0 * u.deg) + assert reg[0] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1} + + cubeviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), + width=3, height=6, angle=0.0 * u.deg) + assert reg[0] == {'name': 'EllipticalROI', 'glue_state': 'OrState', 'region': ellipse1} + + cubeviz_helper.app.session.edit_subset_mode.mode = AndMode + viewer.apply_roi(RectangularROI(20, 25, 20, 25)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + rectangle2 = RectanglePixelRegion(center=PixCoord(x=22.5, y=22.5), + width=5, height=5, angle=0.0 * u.deg) + assert reg[0] == {'name': 'RectangularROI', 'glue_state': 'AndState', 'region': rectangle2} + + cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(CircularROI(xc=21, yc=24, radius=1)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + circle2 = CirclePixelRegion(center=PixCoord(x=21, y=24), radius=1) + assert reg[0] == {'name': 'CircularROI', 'glue_state': 'AndNotState', 'region': circle2} + + +def test_composite_region_with_consecutive_and_not_states(cubeviz_helper): + data = Data(flux=np.ones((128, 128, 10)), label='Test 3D Flux') + cubeviz_helper.app.data_collection.append(data) + + cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test 3D Flux') + viewer = cubeviz_helper.app.get_viewer('flux-viewer') + + viewer.apply_roi(CircularROI(xc=25, yc=25, radius=5)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + circle1 = CirclePixelRegion(center=PixCoord(x=25, y=25), radius=5) + assert reg[0] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1} + + cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(RectangularROI(25, 30, 25, 30)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + rectangle1 = RectanglePixelRegion(center=PixCoord(x=27.5, y=27.5), + width=5, height=5, angle=0.0 * u.deg) + assert reg[0] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1} + + cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) + reg = cubeviz_helper.app.get_subsets("Subset 1") + ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), + width=3, height=6, angle=0.0 * u.deg) + assert reg[0] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1} + + regions_list = cubeviz_helper.app.get_subsets("Subset 1", object_only=True) + assert len(regions_list) == 3 + assert regions_list[0].width == 3 + + regions_list = cubeviz_helper.app.get_subsets("Subset 1", spatial_only=True, + object_only=True) + assert len(regions_list) == 3 + assert regions_list[0].width == 3 + + spatial_list = cubeviz_helper.app.get_subsets("Subset 1", spatial_only=True) + assert len(spatial_list) == 3 + + +def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): + arr = np.ones((10, 10)) + + data_label = 'image-data' + viewer = imviz_helper.app.get_viewer('imviz-0') + imviz_helper.load_data(arr, data_label=data_label, show_in_viewer=True) + viewer.apply_roi(CircularROI(xc=5, yc=5, radius=2)) + reg = imviz_helper.app.get_subsets("Subset 1") + circle1 = CirclePixelRegion(center=PixCoord(x=5, y=5), radius=2) + assert reg[0] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1} + + imviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(RectangularROI(2, 4, 2, 4)) + reg = imviz_helper.app.get_subsets("Subset 1") + rectangle1 = RectanglePixelRegion(center=PixCoord(x=3, y=3), + width=2, height=2, angle=0.0 * u.deg) + assert reg[0] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1} + + imviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(EllipticalROI(3, 3, 3, 6)) + reg = imviz_helper.app.get_subsets("Subset 1") + ellipse1 = EllipsePixelRegion(center=PixCoord(x=3, y=3), + width=3, height=6, angle=0.0 * u.deg) + assert reg[0] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1} + + +def test_with_invalid_subset_name(cubeviz_helper): + subset_name = "Test" + with pytest.raises(ValueError, match=f'{subset_name} not in '): + cubeviz_helper.app.get_subsets(subset_name=subset_name) From 4cd8c3073246898161c2372514a61a910587515f Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 27 Mar 2023 13:26:06 -0400 Subject: [PATCH 007/283] aperture photometry: do not hide prev results when resetting (#2112) * do not hide prev results when resetting * show in-plugin warning text if last "calculate" call failed * changes entry --- CHANGES.rst | 3 +++ .../aper_phot_simple/aper_phot_simple.py | 26 ++++++------------- .../aper_phot_simple/aper_phot_simple.vue | 6 +++++ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 698862b570..48b82e9da6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -74,6 +74,9 @@ Cubeviz Imviz ^^^^^ +* Do not hide previous results in aperture photometry when there is a failure, but rather show + the failure message within the plugin UI to indicate the shown results are "out of date". [#2112] + Mosviz ^^^^^^ diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index d9994f334b..9616567df4 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -45,6 +45,7 @@ class SimpleAperturePhotometry(PluginTemplateMixin, DatasetSelectMixin, TableMix counts_factor = Any(0).tag(sync=True) flux_scaling = Any(0).tag(sync=True) result_available = Bool(False).tag(sync=True) + result_failed_msg = Unicode("").tag(sync=True) results = List().tag(sync=True) plot_types = List([]).tag(sync=True) current_plot_type = Unicode().tag(sync=True) @@ -90,19 +91,11 @@ def __init__(self, *args, **kwargs): self.session.hub.subscribe(self, SubsetUpdateMessage, handler=self._on_subset_update) self.session.hub.subscribe(self, LinkUpdatedMessage, handler=self._on_link_update) - def reset_results(self): - self.result_available = False - self.results = [] - self.plot_available = False - self.radial_plot = '' - bqplot_clear_figure(self._fig) - @observe('dataset_selected') def _dataset_selected_changed(self, event={}): try: self._selected_data = self.dataset.selected_dc_item if self._selected_data is None: - self.reset_results() return self.counts_factor = 0 self.pixel_area = 0 @@ -143,7 +136,6 @@ def _dataset_selected_changed(self, event={}): self.pixel_area = 0.04 * 0.04 except Exception as e: - self.reset_results() self._selected_data = None self.hub.broadcast(SnackbarMessage( f"Failed to extract {self.dataset_selected}: {repr(e)}", @@ -174,7 +166,6 @@ def _on_link_update(self, msg): def _subset_selected_changed(self, event={}): subset_selected = event.get('new', self.subset_selected) if self._selected_data is None or subset_selected == '': - self.reset_results() return try: @@ -196,7 +187,6 @@ def _subset_selected_changed(self, event={}): except Exception as e: self._selected_subset = None - self.reset_results() self.hub.broadcast(SnackbarMessage( f"Failed to extract {subset_selected}: {repr(e)}", color='error', sender=self)) @@ -249,7 +239,6 @@ def _bg_subset_selected_changed(self, event={}): def vue_do_aper_phot(self, *args, **kwargs): if self._selected_data is None or self._selected_subset is None: - self.reset_results() self.hub.broadcast(SnackbarMessage( "No data for aperture photometry", color='error', sender=self)) return @@ -430,14 +419,15 @@ def vue_do_aper_phot(self, *args, **kwargs): else: bqplot_marks = [bqplot_line] - self._fig.marks = bqplot_marks - except Exception as e: # pragma: no cover - self.reset_results() - self.hub.broadcast(SnackbarMessage( - f"Aperture photometry failed: {repr(e)}", color='error', sender=self)) - + bqplot_clear_figure(self._fig) + msg = f"Aperture photometry failed: {repr(e)}" + self.hub.broadcast(SnackbarMessage(msg, color='error', sender=self)) + self.result_failed_msg = msg else: + self.result_failed_msg = '' + self._fig.marks = bqplot_marks + # Parse results for GUI. tmp = [] for key in phot_table.colnames: diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue index fb49bba3f3..690ef28cd2 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue @@ -139,6 +139,12 @@ + + + WARNING: {{result_failed_msg}} + + + -

- + +
+ + Subset type: {{ subset_types[index] }} + + + Glue state: {{ glue_state_types[index] }} + + +
- Update + Update -
- -
- Subset Region Definition -
- - - - Subset type: - {{ subset_types[index] }} - - - {{ item.name }}: - - - - - - -
-
diff --git a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py index 423adccb7f..b867e62489 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py @@ -1,4 +1,6 @@ +import warnings import pytest + from glue.core.roi import XRangeROI @@ -16,3 +18,10 @@ def test_plugin(specviz_helper, spectrum1d): po = specviz_helper.plugins['Plot Options'] po.layer = 'Subset 1' po.line_color = 'green' + + +def test_subset_definition_with_composite_subset(cubeviz_helper, spectrum1d_cube): + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + cubeviz_helper.load_data(spectrum1d_cube) + cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 28f5fbeb75..972231c691 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -62,8 +62,8 @@ def test_region_from_subset_3d(cubeviz_helper): cubeviz_helper.app.get_viewer('flux-viewer').apply_roi(RectangularROI(1, 3.5, -0.2, 3.3)) - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = cubeviz_helper.app.get_subsets('Subset 1', object_only=True)[0] assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) @@ -273,8 +273,7 @@ def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): cubeviz_helper.app.session.edit_subset_mode.mode = OrMode spec_viewer.apply_roi(XRangeROI(30, 35)) - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer') - reg = subsets.get('Subset 1') + reg = cubeviz_helper.app.get_subsets('Subset 1') assert len(reg) == 2 assert isinstance(reg, SpectralRegion) @@ -285,31 +284,32 @@ def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): assert subset_plugin.subset_selected == "Subset 1" assert subset_plugin.subset_types == ["Range", "Range"] - assert not subset_plugin.is_editable + assert subset_plugin.glue_state_types == ["RangeSubsetState", "OrState"] + # assert not subset_plugin.is_editable assert subset_plugin.get_center() is None - subset_plugin.set_center(99, update=True) # This is no-op + # TODO: Should this be changed to something else? + # subset_plugin.set_center(99, update=True) # This is no-op for key in ("orig", "value"): - assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", key) == 30 - assert subset_plugin._get_value_from_subset_definition(0, "Upper bound", key) == 35 - assert subset_plugin._get_value_from_subset_definition(1, "Lower bound", key) == 5 - assert subset_plugin._get_value_from_subset_definition(1, "Upper bound", key) == 15.5 + assert subset_plugin._get_value_from_subset_definition(1, "Lower bound", key) == 30 + assert subset_plugin._get_value_from_subset_definition(1, "Upper bound", key) == 35 + assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", key) == 5 + assert subset_plugin._get_value_from_subset_definition(0, "Upper bound", key) == 15.5 # This should not be possible via GUI but here we change # something to make sure no-op is really no-op. subset_plugin._set_value_in_subset_definition(0, "Lower bound", "value", 25) - subset_plugin.vue_update_subset() + # subset_plugin.vue_update_subset() # "value" here does not matter. It is going to get overwritten next time Subset is processed. assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "value") == 25 - assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "orig") == 30 + assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "orig") == 5 - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer') - reg = subsets.get('Subset 1') + reg = cubeviz_helper.app.get_subsets('Subset 1') assert_quantity_allclose(reg[1].lower, 30.0*u.Hz) # Still the old value # See, never happened. subset_plugin.subset_selected = "Create New" subset_plugin.subset_selected = "Subset 1" - assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "value") == 30 + assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", "value") == 5 def test_composite_region_from_subset_3d(cubeviz_helper): @@ -356,6 +356,13 @@ def test_composite_region_from_subset_3d(cubeviz_helper): assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'AndNotState', 'region': circle2, 'subset_state': reg[-1]['subset_state']} + subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.subset_selected == "Subset 1" + assert subset_plugin.subset_types == ['CircularROI', 'RectangularROI', 'EllipticalROI', + 'RectangularROI', 'CircularROI'] + assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', + 'OrState', 'AndState', 'AndNotState'] + def test_composite_region_with_consecutive_and_not_states(cubeviz_helper): data = Data(flux=np.ones((128, 128, 10)), label='Test 3D Flux') @@ -398,6 +405,11 @@ def test_composite_region_with_consecutive_and_not_states(cubeviz_helper): spatial_list = cubeviz_helper.app.get_subsets("Subset 1", spatial_only=True) assert len(spatial_list) == 3 + subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.subset_selected == "Subset 1" + assert subset_plugin.subset_types == ['CircularROI', 'RectangularROI', 'EllipticalROI'] + assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'AndNotState'] + def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): arr = np.ones((10, 10)) @@ -427,6 +439,11 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1, 'subset_state': reg[-1]['subset_state']} + subset_plugin = imviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.subset_selected == "Subset 1" + assert subset_plugin.subset_types == ['CircularROI', 'RectangularROI', 'EllipticalROI'] + assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'AndNotState'] + def test_with_invalid_subset_name(cubeviz_helper): subset_name = "Test" @@ -470,3 +487,8 @@ def test_composite_region_from_subset_2d(specviz_helper, spectrum1d): 7500 * spectrum1d.spectral_axis.unit) assert reg[-1]['region'].lower == subset1.lower and reg[-1]['region'].upper == subset1.upper assert reg[-1]['glue_state'] == 'AndState' + + subset_plugin = specviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.subset_selected == "Subset 1" + assert subset_plugin.subset_types == ['Range', 'Range', 'Range', 'Range'] + assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'OrState', 'AndState'] From 82bebcb7392eb5ea522295133277485662f11ecf Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 8 May 2023 10:21:39 -0400 Subject: [PATCH 056/283] basic single-viewer/layer histogram in plot options (#2153) * basic single-viewer/layer histogram in plot options ported from https://github.com/spacetelescope/jdaviz/pull/2097 * switch for whether to constrain to zoom limits or entire layer * subscribe to zoom-limit changes * basic test coverage --------- Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 2 + .../plugins/plot_options/plot_options.py | 127 ++++++++++++++++++ .../plugins/plot_options/plot_options.vue | 18 ++- .../plot_options/tests/test_plot_options.py | 30 +++++ jdaviz/conftest.py | 15 ++- 5 files changed, 189 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d2a31ae229..91b8ccdc1a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,8 @@ New Features - Update Subset Plugin to utilize ``get_subsets()``. [#2157] +- Histogram showing image values in stretch limits section of plot options plugin. [#2153] + Cubeviz ^^^^^^^ diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 862fbd8a4f..bf3d45596b 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -1,5 +1,9 @@ import os +import bqplot +import numpy as np +from astropy.visualization import PercentileInterval +from ipywidgets import widget_serialization from traitlets import Any, Dict, Float, Bool, Int, List, Unicode, observe from glue.viewers.profile.state import ProfileViewerState, ProfileLayerState @@ -12,6 +16,7 @@ PlotOptionsSyncState) from jdaviz.core.user_api import PluginUserApi from jdaviz.core.tools import ICON_DIR +from jdaviz.utils import bqplot_clear_figure __all__ = ['PlotOptions'] @@ -60,6 +65,8 @@ class PlotOptions(PluginTemplateMixin): not exposed for Specviz * ``stretch_vmax`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz + * ``stretch_hist_zoom_limits`` : whether to show the histogram for the current zoom + limits instead of all data within the layer; not exposed for Specviz. * ``image_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): whether the image bitmap is visible; not exposed for Specviz. * ``image_color_mode`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): @@ -132,6 +139,9 @@ class PlotOptions(PluginTemplateMixin): stretch_vmax_value = Float().tag(sync=True) stretch_vmax_sync = Dict().tag(sync=True) + stretch_hist_zoom_limits = Bool().tag(sync=True) + stretch_histogram = Any().tag(sync=True, **widget_serialization) + subset_visible_value = Bool().tag(sync=True) subset_visible_sync = Dict().tag(sync=True) @@ -317,6 +327,7 @@ def user_api(self): if self.config != "specviz": expose += ['subset_color', 'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax', + 'stretch_hist_zoom_limits', 'image_visible', 'image_color_mode', 'image_color', 'image_colormap', 'image_opacity', 'image_contrast', 'image_bias', @@ -358,3 +369,119 @@ def vue_set_value(self, data): attr_name = data.get('name') value = data.get('value') setattr(self, attr_name, value) + + @observe('plugin_opened', 'layer_selected', 'viewer_selected', + 'stretch_hist_zoom_limits') + def _update_stretch_histogram(self, msg={}): + if not self.stretch_function_sync.get('in_subscribed_states'): # pragma: no cover + # no (image) viewer with stretch function options + return + if not hasattr(self, 'viewer'): # pragma: no cover + # plugin hasn't been fully initialized yet + return + if (not self.plugin_opened + or not self.viewer.selected + or not self.layer.selected): # pragma: no cover + # no need to make updates, updates will be redrawn when plugin is opened + # NOTE: this won't update when the plugin is shown but not open in the tray + return + if not isinstance(msg, dict) and not self.stretch_hist_zoom_limits: # pragma: no cover + # then this is from the limits callbacks and we don't want to waste resources + # IMPORTANT: this assumes the only non-observe callback to this method comes + # from state callbacks from zoom limits. + return + + if self.multiselect and (len(self.viewer.selected) > 1 + or len(self.layer.selected) > 1): # pragma: no cover + # currently only support single-layer/viewer. For now we'll just clear and return. + # TODO: add support for multi-layer/viewer + bqplot_clear_figure(self.stretch_histogram) + return + + viewer = self.viewer.selected_obj[0] if self.multiselect else self.viewer.selected_obj + + # manage viewer zoom limit callbacks + if ((isinstance(msg, dict) and msg.get('name') == 'viewer_selected') + or not self.stretch_hist_zoom_limits): + vs = viewer.state + for attr in ('x_min', 'x_max', 'y_min', 'y_max'): + vs.add_callback(attr, self._update_stretch_histogram) + if isinstance(msg, dict) and msg.get('name') == 'viewer_selected': + viewer_label_old = msg.get('old')[0] if self.multiselect else msg.get('old') + if len(viewer_label_old): + vs_old = self.app.get_viewer(viewer_label_old).state + for attr in ('x_min', 'x_max', 'y_min', 'y_max'): + vs_old.remove_callback(attr, self._update_stretch_histogram) + + data = self.layer.selected_obj[0][0].layer if self.multiselect else self.layer.selected_obj[0].layer # noqa + comp = data.get_component(data.main_components[0]) + + if self.stretch_hist_zoom_limits: + if hasattr(viewer, '_get_zoom_limits'): + # Viewer limits. This takes account of Imviz linking. + xy_limits = viewer._get_zoom_limits(data).astype(int) + x_limits = xy_limits[:, 0] + y_limits = xy_limits[:, 1] + x_min = max(x_limits.min(), 0) + x_max = x_limits.max() + y_min = max(y_limits.min(), 0) + y_max = y_limits.max() + + sub_data = comp.data[y_min:y_max, x_min:x_max].ravel() + + else: + # spectrum-2d-viewer, for example. We'll assume the viewer + # limits correspond to the fixed data components from glue + # and filter directly. + x_data = data.get_component(data.components[1]).data + y_data = data.get_component(data.components[0]).data + + inds = np.where((x_data >= viewer.state.x_min) & + (x_data <= viewer.state.x_max) & + (y_data >= viewer.state.y_min) & + (y_data <= viewer.state.y_max)) + + sub_data = comp.data[inds].ravel() + else: + # include all data, regardless of zoom limits + sub_data = comp.data.ravel() + + # filter out nans (or else bqplot will fail) + if np.any(np.isnan(sub_data)): + sub_data = sub_data[~np.isnan(sub_data)] + + if self.stretch_histogram is None: + # first time the figure is requested, need to build from scratch + self.stretch_histogram = bqplot.Figure(padding_y=0) + + hist_x_sc = bqplot.LinearScale() + hist_y_sc = bqplot.LinearScale() + # TODO: Let user change the number of bins? + # TODO: Let user set y-scale to log + hist_mark = bqplot.Bins(sample=sub_data, bins=25, + density=True, colors="gray", + scales={'x': hist_x_sc, + 'y': hist_y_sc}) + + self.stretch_histogram.marks = [hist_mark] + self.stretch_histogram.axes = [bqplot.Axis(scale=hist_x_sc, + num_ticks=3, + tick_format='0.1e', + label='pixel value'), + bqplot.Axis(scale=hist_y_sc, + num_ticks=2, + orientation='vertical', + label='density')] + + self.bqplot_figs_resize = [self.stretch_histogram] + + else: + hist_mark = self.stretch_histogram.marks[0] + hist_mark.sample = sub_data + # TODO: Let user change the number of bins? + # TODO: Let user set y-scale to log + + interval = PercentileInterval(95) + if len(sub_data) > 0: + hist_lims = interval.get_limits(sub_data) + hist_mark.min, hist_mark.max = hist_lims diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index bfad881c3c..f2bbb81372 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -186,6 +186,19 @@ + + + + + + + Image @@ -385,10 +398,13 @@ module.exports = { } - diff --git a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py index 42f498e070..ed0328ac15 100644 --- a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py @@ -1,4 +1,6 @@ import pytest +from numpy import allclose +from numpy.testing import assert_allclose @pytest.mark.filterwarnings('ignore') @@ -57,6 +59,34 @@ def test_multiselect(cubeviz_helper, spectrum1d_cube): assert len(po.axes_visible.linked_states) == 0 +@pytest.mark.filterwarnings('ignore') +def test_stretch_histogram(cubeviz_helper, spectrum1d_cube_with_uncerts): + cubeviz_helper.load_data(spectrum1d_cube_with_uncerts) + po = cubeviz_helper.app.get_tray_item_from_name('g-plot-options') + po.open_in_tray() # forces histogram to draw + + assert po.stretch_histogram is not None + + flux_cube_sample = po.stretch_histogram.marks[0].sample + + # changing viewer should change results + po.viewer.selected = 'uncert-viewer' + assert not allclose(po.stretch_histogram.marks[0].sample, flux_cube_sample) + + po.viewer.selected = 'flux-viewer' + assert_allclose(po.stretch_histogram.marks[0].sample, flux_cube_sample) + + # change viewer limits + fv = cubeviz_helper.app.get_viewer('flux-viewer') + fv.state.x_max = 0.5 * fv.state.x_max + # viewer limits should not be affected by default + assert_allclose(po.stretch_histogram.marks[0].sample, flux_cube_sample) + + # set to listen to viewer limits, the length of the samples will change + po.stretch_hist_zoom_limits = True + assert len(po.stretch_histogram.marks[0].sample) != len(flux_cube_sample) + + @pytest.mark.filterwarnings('ignore') def test_user_api(cubeviz_helper, spectrum1d_cube): cubeviz_helper.load_data(spectrum1d_cube) diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index 8a1135091d..e241c31549 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -179,7 +179,7 @@ def multi_order_spectrum_list(spectrum1d, spectral_orders=10): return SpectrumList(sc) -def _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(2, 2, 4)): +def _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(2, 2, 4), with_uncerts=False): flux = np.arange(np.prod(shape)).reshape(shape) * fluxunit wcs_dict = {"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CTYPE3": "WAVE-LOG", @@ -187,8 +187,14 @@ def _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(2, 2, 4)): "CDELT1": -0.0001, "CDELT2": 0.0001, "CDELT3": 8e-11, "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0} w = WCS(wcs_dict) + if with_uncerts: + uncert = StdDevUncertainty(np.abs(np.random.normal(flux) * u.Jy)) - return Spectrum1D(flux=flux, wcs=w) + return Spectrum1D(flux=flux, + uncertainty=uncert, + wcs=w) + else: + return Spectrum1D(flux=flux, wcs=w) @pytest.fixture @@ -196,6 +202,11 @@ def spectrum1d_cube(): return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy) +@pytest.fixture +def spectrum1d_cube_with_uncerts(): + return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, with_uncerts=True) + + @pytest.fixture def spectrum1d_cube_larger(): return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(SPECTRUM_SIZE, 2, 4)) From 6ccfa91ea8c5c624dc6a913f2c457f33454fdb56 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Mon, 11 Oct 2021 14:51:08 -0400 Subject: [PATCH 057/283] Eliminate Imviz save pop-up dialog --- CHANGES.rst | 3 +++ .../imviz/tests/test_astrowidgets_api.py | 6 +++--- jdaviz/core/astrowidgets_api.py | 18 ++++++++++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 91b8ccdc1a..468f4f6d43 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -55,6 +55,9 @@ Cubeviz Imviz ^^^^^ +- ``imviz.default_viewer.save()`` method no longer pops up a file dialog. + If the given file exists, it is silently overwritten. [#929] + Mosviz ^^^^^^ diff --git a/jdaviz/configs/imviz/tests/test_astrowidgets_api.py b/jdaviz/configs/imviz/tests/test_astrowidgets_api.py index 434c1f9c23..1800097685 100644 --- a/jdaviz/configs/imviz/tests/test_astrowidgets_api.py +++ b/jdaviz/configs/imviz/tests/test_astrowidgets_api.py @@ -14,8 +14,8 @@ from jdaviz.configs.imviz.tests.utils import BaseImviz_WCS_NoWCS, BaseImviz_WCS_WCS -# TODO: Remove skip when https://github.com/bqplot/bqplot/issues/1393 is resolved. -@pytest.mark.skip(reason="Cannot test due to file dialog popup") +# TODO: Remove skip when https://github.com/bqplot/bqplot/pull/1397/files#r726500097 is resolved. +@pytest.mark.skip(reason="Cannot test due to async JS callback") class TestSave(BaseImviz_WCS_NoWCS): def test_save(self, tmpdir): @@ -23,7 +23,7 @@ def test_save(self, tmpdir): self.viewer.save(filename) # This only tests that something saved, not the content. - assert os.path.isfile(os.path.join(tmpdir.strpath, 'myimage.png')) + assert os.path.isfile(f'{filename}.png') class TestCenterOffset(BaseImviz_WCS_NoWCS): diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index bcada63482..e2ec8029e8 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -37,10 +37,24 @@ def init_astrowidgets_api(self): self.marker = {'color': 'red', 'alpha': 1.0, 'markersize': 5} def save(self, filename): - """Save out the current image view to given PNG filename.""" + """Save out the current image view to given PNG filename. + + Parameters + ---------- + filename : str + PNG filename. If the given file already exists, it will be + silently overwritten. + + """ if not filename.lower().endswith('.png'): filename = filename + '.png' - self.figure.save_png(filename=filename) + + # https://github.com/bqplot/bqplot/pull/1397 + def on_png_received(data): + with open(filename, 'bw') as f: + f.write(data) + + self.figure.get_png_data(on_png_received) def center_on(self, point): """Centers the view on a particular point on the top visible layer. From ded2188b0450ef291b188969e57df50b436dc95b Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 8 May 2023 15:12:39 -0400 Subject: [PATCH 058/283] implement same solution in plugin API * limited to png, with docstring updated --- CHANGES.rst | 5 +++-- .../default/plugins/export_plot/export_plot.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 468f4f6d43..b3ea93fbbd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -55,8 +55,9 @@ Cubeviz Imviz ^^^^^ -- ``imviz.default_viewer.save()`` method no longer pops up a file dialog. - If the given file exists, it is silently overwritten. [#929] +- Saving a plot to a PNG (via the astrowidgets API or export plot plugin API) with a provided + filename will no longer show the file dialog. If the given file exists, it is silently + overwritten. [#929] Mosviz ^^^^^^ diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.py b/jdaviz/configs/default/plugins/export_plot/export_plot.py index 4c7b6837e4..6c98cff85b 100644 --- a/jdaviz/configs/default/plugins/export_plot/export_plot.py +++ b/jdaviz/configs/default/plugins/export_plot/export_plot.py @@ -32,6 +32,9 @@ def save_figure(self, filename=None, filetype=None): """ Save the figure to an image with a provided filename or through an interactive save dialog. + If ``filetype`` is 'png' (or defaults to 'png' based on ``filename``), the interactive save + dialog will be bypassed (this is not supported for 'svg'). + Parameters ---------- filename : str or `None` @@ -49,7 +52,15 @@ def save_figure(self, filename=None, filetype=None): viewer = self.viewer.selected_obj if filetype == "png": - viewer.figure.save_png(filename) + if filename is None: + viewer.figure.save_png() + else: + # support writing without save dialog + # https://github.com/bqplot/bqplot/pull/1397 + def on_img_received(data): + with open(filename, 'bw') as f: + f.write(data) + viewer.figure.get_png_data(on_img_received) elif filetype == "svg": viewer.figure.save_svg(filename) else: From 30fe94e8f9404359469332d6279387f63f05adbe Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 9 May 2023 14:57:37 -0400 Subject: [PATCH 059/283] Fix obscure mouseover bug where it blinks like crazy when WCS() is used as image WCS. --- jdaviz/configs/imviz/plugins/coords_info/coords_info.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 3da6b7dc49..35a2ff2a1f 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -242,7 +242,6 @@ def _image_viewer_update(self, viewer, x, y): sky = image.coords.pixel_to_world(x, y).icrs except Exception: # WCS might not be celestial coords_status = False - self.reset_coords_display() elif isinstance(viewer, CubevizImageView): # TODO: This assumes data_collection[0] is the main reference From ce82e34926f4d3a6f34e9e621855d6442ab15ecb Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 May 2023 11:19:44 -0400 Subject: [PATCH 060/283] lcviz quality of life tweaks (#2186) * generalize viewer tab-naming logic * so that any config that provides reference will use that, but fallback on id. * data-menu support for styling "vs" in tab title this is motivated by wanting to use "flux-vs-time" as a viewer reference within lcviz, but is also extendable in jdaviz for other words that we might want to exclude from title case in the future * generalize _on_new_viewer * to only call imviz-specific linking-logic if applicable * improve repr styling for SelectPluginComponent --- jdaviz/app.py | 21 +++++++++++---------- jdaviz/components/viewer_data_select.vue | 4 +++- jdaviz/container.vue | 2 +- jdaviz/core/template_mixin.py | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index e04b932c84..668d92c711 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -1881,16 +1881,17 @@ def _on_new_viewer(self, msg, vid=None, name=None): msg.cls, data=msg.data, show=False) viewer.figure_widget.layout.height = '100%' - links_control_plugin = self._jdaviz_helper.plugins.get('Links Control', None) - if links_control_plugin is not None: - viewer.state.linked_by_wcs = links_control_plugin.link_type.selected == 'WCS' - elif len(self._viewer_store): - # The plugin would only not exist for instances of Imviz where the user has - # intentionally removed the Links Control plugin, but in that case we will - # adopt "linked_by_wcs" from the first (assuming all are the same) - # NOTE: deleting the default viewer is forbidden both by API and UI, but if - # for some reason that was the case here, linked_by_wcs will default to False - viewer.state.linked_by_wcs = list(self._viewer_store.values())[0].state.linked_by_wcs + if hasattr(viewer.state, 'linked_by_wcs'): + links_control_plugin = self._jdaviz_helper.plugins.get('Links Control', None) + if links_control_plugin is not None: + viewer.state.linked_by_wcs = links_control_plugin.link_type.selected == 'WCS' + elif len(self._viewer_store): + # The plugin would only not exist for instances of Imviz where the user has + # intentionally removed the Links Control plugin, but in that case we will + # adopt "linked_by_wcs" from the first (assuming all are the same) + # NOTE: deleting the default viewer is forbidden both by API and UI, but if + # for some reason that was the case here, linked_by_wcs will default to False + viewer.state.linked_by_wcs = list(self._viewer_store.values())[0].state.linked_by_wcs # noqa if msg.x_attr is not None: x = msg.data.id[msg.x_attr] diff --git a/jdaviz/components/viewer_data_select.vue b/jdaviz/components/viewer_data_select.vue index b844149c7f..c90776f411 100644 --- a/jdaviz/components/viewer_data_select.vue +++ b/jdaviz/components/viewer_data_select.vue @@ -180,7 +180,9 @@ module.exports = { computed: { viewerTitleCase() { var title = this.$props.viewer.reference || this.$props.viewer.id - return title.toLowerCase().replaceAll('-', ' ').split(' ').map((word) => {return (word.charAt(0).toUpperCase() + word.slice(1))}).join(' '); + // this translates from kebab-case to human readable (individual words, in title case) + // each word that should NOT be capitalized needs to explicitly be set here + return title.toLowerCase().replaceAll('-', ' ').split(' ').map((word) => {if (['vs'].indexOf(word) !== -1) {return word} else {return word.charAt(0).toUpperCase() + word.slice(1)}}).join(' '); }, showModeToggle() { if (this.$props.viewer.config === 'cubeviz') { diff --git a/jdaviz/container.vue b/jdaviz/container.vue index 3e88240f79..03171c4813 100644 --- a/jdaviz/container.vue +++ b/jdaviz/container.vue @@ -19,7 +19,7 @@ " # noqa - return f"" + return f"" def __eq__(self, other): return self.selected == other From c4911b31d92d7b5d63dd63c91da6818c6e803acf Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 11 May 2023 13:56:18 -0400 Subject: [PATCH 061/283] DOC: get_subsets do not return masked subset state (#2184) * DOC: get_subsets do not return masked subset state * Improve docstring verbiage Co-authored-by: Kyle Conroy --------- Co-authored-by: Kyle Conroy --- jdaviz/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 668d92c711..35d8b4baac 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -845,7 +845,7 @@ def get_subsets(self, subset_name=None, spectral_only=False, spectral_only : bool Return only spectral subsets. spatial_only : bool - Return only spatial subsets. + Return only spatial subsets, except masked subsets (uncommon). object_only : bool Return only object relevant information and leave out the region class name and glue_state. @@ -856,7 +856,7 @@ def get_subsets(self, subset_name=None, spectral_only=False, ------- data : dict A dict with keys representing the subset name and values as astropy regions - objects + objects. """ dc = self.data_collection From 8bc5b8e010f61f6d4d325550ad97b4a5a26c79d5 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 12 May 2023 08:43:30 -0400 Subject: [PATCH 062/283] Let app.get_viewer() fallback to ID when reference is None (#2156) * Add test that would fail on main currently * Let app.get_viewer() auto fallback on using ID when viewer has no reference. --- jdaviz/app.py | 18 ++++++++++-------- jdaviz/configs/imviz/tests/test_viewers.py | 7 +++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 35d8b4baac..8ab36feb77 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -572,25 +572,24 @@ def get_viewer(self, viewer_reference): Notes ----- - This is only used in cases where the viewers have been pre-defined - in the configuration file. Otherwise, viewers are not stored via - reference. + If viewer does not have a reference, it is going to try to look + up the viewer using the given reference as ID. Parameters ---------- viewer_reference : str The reference to the viewer defined with the ``reference`` key - in the yaml configuration file. + in the YAML configuration file. Returns ------- - `~glue_jupyter.bqplot.common.BqplotBaseView` + viewer : `~glue_jupyter.bqplot.common.BqplotBaseView` The viewer class instance. """ - return self._viewer_by_reference(viewer_reference) + return self._viewer_by_reference(viewer_reference, fallback_to_id=True) def get_viewer_by_id(self, vid): - """Like :meth:`get_viewer` but use ID instead of reference name. + """Like :meth:`get_viewer` but use ID directly instead of reference name. This is useful when reference name is `None`. """ return self._viewer_store.get(vid) @@ -1460,7 +1459,7 @@ def find_viewer_item(stack_items): return viewer_item - def _viewer_by_reference(self, reference): + def _viewer_by_reference(self, reference, fallback_to_id=False): """ Viewer instance by reference defined in the yaml configuration file. @@ -1476,6 +1475,9 @@ def _viewer_by_reference(self, reference): """ viewer_item = self._viewer_item_by_reference(reference) + if viewer_item is None and fallback_to_id: + return self._viewer_by_id(reference) + return self._viewer_store[viewer_item['id']] def _viewer_item_by_reference(self, reference): diff --git a/jdaviz/configs/imviz/tests/test_viewers.py b/jdaviz/configs/imviz/tests/test_viewers.py index 491c34d834..80b4ee04ba 100644 --- a/jdaviz/configs/imviz/tests/test_viewers.py +++ b/jdaviz/configs/imviz/tests/test_viewers.py @@ -30,6 +30,13 @@ def test_create_destroy_viewer(imviz_helper, desired_name, actual_name): assert imviz_helper.app.get_viewer_ids() == ['imviz-0'] +def test_get_viewer_created(imviz_helper): + # This viewer has no reference but has ID. + viewer1 = imviz_helper.create_image_viewer() + viewer2 = imviz_helper.app.get_viewer('imviz-1') + assert viewer1 is viewer2 + + def test_destroy_viewer_invalid(imviz_helper): assert imviz_helper.app.get_viewer_ids() == ['imviz-0'] From 9d16ea9a407523183bbbc1f4764b733553b582b1 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 12 May 2023 09:56:01 -0400 Subject: [PATCH 063/283] unpin max glue (#2196) * update glue pin * to forbid broken 1.10, but allow versions after * TST: skip failing remote data tests --- .../spectral_extraction/tests/test_spectral_extraction.py | 1 + jdaviz/core/tests/test_data_menu.py | 1 + pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py index d3685bfc91..5aa1e96064 100644 --- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -172,6 +172,7 @@ def test_user_api(specviz2d_helper): pext.bg_sub_add_results.auto = True +@pytest.mark.skip(reason='filenames changed') @pytest.mark.remote_data @pytest.mark.skipif(GWCS_LT_0_18_1, reason='Needs GWCS 0.18.1 or later') def test_spectrum_on_top(specviz2d_helper): diff --git a/jdaviz/core/tests/test_data_menu.py b/jdaviz/core/tests/test_data_menu.py index dfba4b9c89..9c18584630 100644 --- a/jdaviz/core/tests/test_data_menu.py +++ b/jdaviz/core/tests/test_data_menu.py @@ -101,6 +101,7 @@ def test_visibility_toggle(imviz_helper): assert po.stretch_preset.value == 90 +@pytest.mark.skip(reason="filenames changed") @pytest.mark.remote_data @pytest.mark.filterwarnings(r"ignore::astropy.wcs.wcs.FITSFixedWarning") @pytest.mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index 5ef2555863..ebd23f4b9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "traitlets>=5.0.5", "bqplot>=0.12.37", "bqplot-image-gl>=1.4.11", - "glue-core>=1.6.0,!=1.9.0,<1.10", + "glue-core>=1.6.0,!=1.9.0,!=1.10", "glue-jupyter>=0.15.0", "echo>=0.5.0", "ipykernel>=6.19.4", From cc1e0e6a754d0ed502bb97d232bd9a1088a017dd Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Fri, 12 May 2023 10:09:50 -0400 Subject: [PATCH 064/283] Disable Canvas Rotation Plugin in non-Chromium Browsers (#2192) * Add Chromium check to canvas rotation * Add docs mention to chrome requirement for rotation * Move browser check to disabled_msg * Changelog * Change doc wording * Show description and docs even when plugins are disabled --- CHANGES.rst | 1 + docs/imviz/plugins.rst | 2 + jdaviz/components/tray_plugin.vue | 17 +- .../plugins/rotate_canvas/rotate_canvas.vue | 148 ++++++++++-------- 4 files changed, 96 insertions(+), 72 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 91b8ccdc1a..4a7750a4a5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,7 @@ Imviz - Preliminary support for Roman ASDF data products. This requires ``roman-datamodels`` to be installed separately by the user. [#1822] +- Canvas Rotation plugin is now disabled for non-Chromium based browsers [#2192] Mosviz ^^^^^^ diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index 4edba9eace..4ae367b37e 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -318,6 +318,8 @@ right or the left, as well as a slider and input to set the angle and a switch t axes should be flipped horizontally after applying the rotation (a vertical flip can be achieved via a 180 deg rotation and a horizontal flip). +Due to browser limitations, Canvas Rotation is only available on Chromium-based browsers. + .. _imviz-export-plot: diff --git a/jdaviz/components/tray_plugin.vue b/jdaviz/components/tray_plugin.vue index db938433de..63ecf2b530 100644 --- a/jdaviz/components/tray_plugin.vue +++ b/jdaviz/components/tray_plugin.vue @@ -1,17 +1,18 @@ diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 51f9e64ef1..2d5dc13b23 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -351,10 +351,23 @@ def add_data(self, data, color=None, alpha=None, **layer_state): result : bool `True` if successful, `False` otherwise. """ + # If this is the first loaded data, set things up for unit conversion. + if len(self.layers) == 0: + reset_plot_axes = True + else: + reset_plot_axes = False + # The base class handles the plotting of the main # trace representing the spectrum itself. result = super().add_data(data, color, alpha, **layer_state) + if reset_plot_axes: + x_units = data.get_component(self.state.x_att.label).units + y_units = data.get_component("flux").units + self.state.x_display_unit = x_units if len(x_units) else None + self.state.y_display_unit = y_units if len(y_units) else None + self.set_plot_axes() + self._plot_uncertainties() self._plot_mask() @@ -477,22 +490,21 @@ def _plot_uncertainties(self): self.figure.marks = list(self.figure.marks) + [error_line_mark] def set_plot_axes(self): - # Get data to be used for axes labels - data = self.data()[0] - # Set axes labels for the spectrum viewer - spectral_axis_unit_type = str(data.spectral_axis.unit.physical_type).title() - # flux_unit_type = data.flux.unit.physical_type.title() flux_unit_type = "Flux density" - - if data.spectral_axis.unit.is_equivalent(u.m): + x_disp_unit = self.state.x_display_unit + x_unit = u.Unit(x_disp_unit) if x_disp_unit else u.dimensionless_unscaled + if x_unit.is_equivalent(u.m): spectral_axis_unit_type = "Wavelength" - elif data.spectral_axis.unit.is_equivalent(u.pixel): - spectral_axis_unit_type = "pixel" + elif x_unit.is_equivalent(u.Hz): + spectral_axis_unit_type = "Frequency" + elif x_unit.is_equivalent(u.pixel): + spectral_axis_unit_type = "Pixel" + else: + spectral_axis_unit_type = str(x_unit.physical_type).title() - label_0 = f"{spectral_axis_unit_type} [{data.spectral_axis.unit.to_string()}]" - self.figure.axes[0].label = label_0 - self.figure.axes[1].label = f"{flux_unit_type} [{data.flux.unit.to_string()}]" + self.figure.axes[0].label = f"{spectral_axis_unit_type} [{self.state.x_display_unit}]" + self.figure.axes[1].label = f"{flux_unit_type} [{self.state.y_display_unit}]" # Make it so y axis label is not covering tick numbers. self.figure.axes[1].label_offset = "-50" diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index 12603091ff..0f47db0d02 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -10,7 +10,6 @@ from astropy.utils.data import download_file from jdaviz.app import Application -from jdaviz.configs.specviz.plugins.unit_conversion import unit_conversion as uc from jdaviz.core.marks import LineUncertainties from jdaviz import Specviz @@ -277,7 +276,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): # If the reference (visible) data changes via unit conversion, # check that the region's units convert too - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_spectrum(spectrum1d) # Originally Angstrom # Also check coordinates info panel. # x=0 -> 6000 A, x=1 -> 6222.222 A @@ -293,27 +292,22 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): assert label_mouseover.as_text() == ('', '', '') assert label_mouseover.icon == '' - # Convert the wavelength axis to microns - new_spectral_axis = "micron" - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_spectral_axis=new_spectral_axis) - - # Add this new data and clear the other, making the converted spectrum our reference - specviz_helper.app.add_data(converted_spectrum, "Converted Spectrum") - specviz_helper.app.add_data_to_viewer("spectrum-viewer", - "Converted Spectrum", - clear_other_data=True) + # Convert the wavelength axis to micron + new_spectral_axis = "um" + specviz_helper.plugins['Unit Conversion'].spectral_unit = new_spectral_axis - specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(0.6, 0.7)) + spec_viewer.apply_roi(XRangeROI(0.6, 0.7)) - # TODO: Is this test still relevant with the upcoming glue unit conversion changes? # Retrieve the Subset - # subsets = specviz_helper.get_spectral_regions() - # reg = subsets.get('Subset 1') - # - # assert reg.lower.unit == u.Unit(new_spectral_axis) - # assert reg.upper.unit == u.Unit(new_spectral_axis) + subsets = specviz_helper.get_spectral_regions(use_display_units=False) + reg = subsets.get('Subset 1') + assert reg.lower.unit == u.Angstrom + assert reg.upper.unit == u.Angstrom + + subsets = specviz_helper.get_spectral_regions(use_display_units=True) + reg = subsets.get('Subset 1') + assert reg.lower.unit == u.um + assert reg.upper.unit == u.um # Coordinates info panel should show new unit label_mouseover._viewer_mouse_event(spec_viewer, @@ -321,7 +315,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): label_mouseover.as_text() == ('Cursor 6.10000e-01, 1.25000e+01', 'Wave 6.00000e-01 micron (0 pix)', 'Flux 1.24967e+01 Jy') - assert label_mouseover.icon == 'b' + assert label_mouseover.icon == 'a' label_mouseover._viewer_mouse_event(spec_viewer, {'event': 'mouseleave'}) assert label_mouseover.as_text() == ('', '', '') diff --git a/jdaviz/configs/specviz2d/specviz2d.yaml b/jdaviz/configs/specviz2d/specviz2d.yaml index d62dac9ed9..6126211888 100644 --- a/jdaviz/configs/specviz2d/specviz2d.yaml +++ b/jdaviz/configs/specviz2d/specviz2d.yaml @@ -21,7 +21,6 @@ tray: - spectral-extraction - g-gaussian-smooth - g-model-fitting - - g-unit-conversion - g-line-list - specviz-line-analysis - g-export-plot diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py index 1e4fea74b8..78a4096068 100644 --- a/jdaviz/configs/specviz2d/tests/test_parsers.py +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -81,7 +81,7 @@ def test_2d_parser_no_unit(specviz2d_helper, mos_spectrum2d): label_mouseover._viewer_mouse_event(viewer_1d, {'event': 'mousemove', 'domain': {'x': 6.5, 'y': 3}}) assert label_mouseover.as_text() == ('Cursor 6.50000e+00, 3.00000e+00', - 'Wave 6.00000e+00 pix', + 'Wave 6.00000e+00 pixel', 'Flux -3.59571e+00') assert label_mouseover.icon == 'b' diff --git a/jdaviz/core/events.py b/jdaviz/core/events.py index 46126bb65e..1bfbb9d9b6 100644 --- a/jdaviz/core/events.py +++ b/jdaviz/core/events.py @@ -1,3 +1,4 @@ +import astropy.units as u from glue.core.message import Message __all__ = ['NewViewerMessage', 'ViewerAddedMessage', 'ViewerRemovedMessage', 'LoadDataMessage', @@ -6,7 +7,8 @@ 'SliceSelectSliceMessage', 'SliceToolStateMessage', 'TableClickMessage', 'LinkUpdatedMessage', 'ExitBatchLoadMessage', - 'MarkersChangedMessage', 'CanvasRotationChangedMessage'] + 'MarkersChangedMessage', 'CanvasRotationChangedMessage', + 'GlobalDisplayUnitChanged'] class NewViewerMessage(Message): @@ -337,3 +339,19 @@ def angle(self): @property def flip_horizontal(self): return self._flip_horizontal + + +class GlobalDisplayUnitChanged(Message): + '''Message generated when the global app-wide display unit is changed''' + def __init__(self, axis, unit, *args, **kwargs): + super().__init__(*args, **kwargs) + self._axis = axis + self._unit = unit + + @property + def axis(self): + return self._axis + + @property + def unit(self): + return u.Unit(self._unit) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 0d1a14600f..6405bf035b 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -8,7 +8,7 @@ __all__ = ['FreezableState', 'FreezableProfileViewerState', 'FreezableBqplotImageViewerState'] -class FreezableState(): +class FreezableState: _frozen_state = [] def __setattr__(self, k, v): diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index c6dc0fb457..986547f28b 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -412,8 +412,32 @@ def show_in_new_tab(self, title=None): # pragma: no cover return self.show(loc="sidecar:tab-after", title=title) def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, - mask_subset=None, function=None, cls=None): - # Start validity checks + mask_subset=None, function=None, cls=None, use_display_units=False): + def _handle_display_units(data, use_display_units): + if use_display_units: + if isinstance(data, Spectrum1D): + spectral_unit = self.app._get_display_unit('spectral') + if not spectral_unit: + return data + if self.app.config == 'cubeviz' and spectral_unit == 'deg': + # this happens before the correct axis is set for the spectrum-viewer + # and would result in a unit-conversion error if attempting to convert + # to the display units. This should only ever be temporary during + # app intialization. + return data + flux_unit = self.app._get_display_unit('flux') + # TODO: any other attributes (meta, wcs, etc)? + # TODO: implement uncertainty.to upstream + new_uncert = data.uncertainty.__class__(data.uncertainty.quantity.to(flux_unit)) if data.uncertainty is not None else None # noqa + data = Spectrum1D(spectral_axis=data.spectral_axis.to(spectral_unit, + u.spectral()), + flux=data.flux.to(flux_unit, + u.spectral_density(data.spectral_axis)), + uncertainty=new_uncert) + else: # pragma: nocover + raise NotImplementedError(f"converting {data.__class__.__name__} to display units is not supported") # noqa + return data + list_of_valid_function_values = ('minimum', 'maximum', 'mean', 'median', 'sum') if function and function not in list_of_valid_function_values: @@ -471,7 +495,7 @@ def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, else: data = data.get_object(cls=cls, **object_kwargs) - return data + return _handle_display_units(data, use_display_units) if not cls and spatial_subset: raise AttributeError(f"A valid cls must be provided to" @@ -528,9 +552,9 @@ def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, else: data = spec_subset - return data + return _handle_display_units(data, use_display_units) - def get_data(self, data_label=None, cls=None): + def get_data(self, data_label=None, cls=None, use_display_units=False): """ Returns data with name equal to data_label of type cls. @@ -540,6 +564,8 @@ def get_data(self, data_label=None, cls=None): Provide a label to retrieve a specific data set from data_collection. cls : `~specutils.Spectrum1D`, `~astropy.nddata.CCDData`, optional The type that data will be returned as. + use_display_units: bool, optional + Whether to convert to the display units defined in the plugin. Returns ------- @@ -548,7 +574,8 @@ def get_data(self, data_label=None, cls=None): """ return self._get_data(data_label=data_label, spatial_subset=None, - spectral_subset=None, function=None, cls=None) + spectral_subset=None, function=None, + cls=None, use_display_units=use_display_units) class ImageConfigHelper(ConfigHelper): diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py index 2a6590d711..ae2307ec13 100644 --- a/jdaviz/core/marks.py +++ b/jdaviz/core/marks.py @@ -7,13 +7,14 @@ from glue.core import HubListener from specutils import Spectrum1D +from jdaviz.core.events import GlobalDisplayUnitChanged from jdaviz.core.events import (SliceToolStateMessage, LineIdentifyMessage, SpectralMarksChangedMessage, RedshiftMessage) __all__ = ['OffscreenLinesMarks', 'BaseSpectrumVerticalLine', 'SpectralLine', 'SliceIndicatorMarks', 'ShadowMixin', 'ShadowLine', 'ShadowLabelFixedY', - 'PluginMark', 'PluginLine', 'PluginScatter', + 'PluginMark', 'LinesAutoUnit', 'PluginLine', 'PluginScatter', 'LineAnalysisContinuum', 'LineAnalysisContinuumCenter', 'LineAnalysisContinuumLeft', 'LineAnalysisContinuumRight', 'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark'] @@ -58,11 +59,85 @@ def _update_counts(self, *args): self.right.text = [f'{oob_right} \u25b6' if oob_right > 0 else ''] -class BaseSpectrumVerticalLine(Lines, HubListener): +class PluginMark(): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.xunit = None + self.yunit = None + # whether to update existing marks when global display units are changed + self.auto_update_units = True + self.hub.subscribe(self, GlobalDisplayUnitChanged, + handler=self._on_global_display_unit_changed) + + if self.xunit is None: + self.set_x_unit() + if self.yunit is None: + self.set_y_unit() + + @property + def hub(self): + return self.viewer.hub + + def update_xy(self, x, y): + self.x = np.asarray(x) + self.y = np.asarray(y) + + def append_xy(self, x, y): + self.x = np.append(self.x, x) + self.y = np.append(self.y, y) + + def set_x_unit(self, unit=None): + if unit is None: + if not hasattr(self.viewer.state, 'x_display_unit'): + return + unit = self.viewer.state.x_display_unit + unit = u.Unit(unit) + + if self.xunit is not None and not np.all([s == 0 for s in self.x.shape]): + x = (self.x * self.xunit).to_value(unit, u.spectral()) + self.xunit = unit + self.x = x + self.xunit = unit + + def set_y_unit(self, unit=None): + if unit is None: + if not hasattr(self.viewer.state, 'y_display_unit'): + return + unit = self.viewer.state.y_display_unit + unit = u.Unit(unit) + + if self.yunit is not None and not np.all([s == 0 for s in self.y.shape]): + if self.viewer.default_class is Spectrum1D: + spec = self.viewer.state.reference_data.get_object(cls=Spectrum1D) + eqv = u.spectral_density(spec.spectral_axis) + y = (self.y * self.yunit).to_value(unit, equivalencies=eqv) + else: + y = (self.y * self.yunit).to_value(unit) + self.yunit = unit + self.y = y + + self.yunit = unit + + def _on_global_display_unit_changed(self, msg): + if not self.auto_update_units: + return + if self.viewer.__class__.__name__ in ['SpecvizProfileView', 'CubevizProfileView']: + axis_map = {'spectral': 'x', 'flux': 'y'} + elif self.viewer.__class__.__name__ == 'MosvizProfile2DView': + axis_map = {'spectral': 'x'} + else: + return + axis = axis_map.get(msg.axis, None) + if axis is not None: + getattr(self, f'set_{axis}_unit')(msg.unit) + + def clear(self): + self.update_xy([], []) + + +class BaseSpectrumVerticalLine(Lines, PluginMark, HubListener): def __init__(self, viewer, x, **kwargs): - # we'll store the current units so that we can automatically update the - # positioning on a change to the x-units - self._x_unit = viewer.state.reference_data.get_object(cls=Spectrum1D).spectral_axis.unit + self.viewer = viewer # the location of the marker will need to update automatically if the # underlying data changes (through a unit conversion, for example) @@ -83,14 +158,14 @@ def _update_reference_data(self, reference_data): def _update_data(self, x_all): # the x-units may have changed. We want to convert the internal self.x - # from self._x_unit to the new units (x_all.unit) + # from self.xunit to the new units (x_all.unit) new_unit = x_all.unit - if new_unit == self._x_unit: + if new_unit == self.xunit: return - old_quant = self.x[0]*self._x_unit + old_quant = self.x[0]*self.xunit x = old_quant.to_value(x_all.unit, equivalencies=u.spectral()) self.x = [x, x] - self._x_unit = new_unit + self.xunit = new_unit class SpectralLine(BaseSpectrumVerticalLine): @@ -110,7 +185,7 @@ def __init__(self, viewer, rest_value, redshift=0, name=None, **kwargs): # setting redshift will set self.x and enable the obs_value property, # but to do that we need x_unit set first (would normally be assigned # in the super init) - self._x_unit = viewer.state.reference_data.get_object(cls=Spectrum1D).spectral_axis.unit + self.xunit = u.Unit(viewer.state.x_display_unit) self.redshift = redshift viewer.session.hub.subscribe(self, LineIdentifyMessage, @@ -131,6 +206,11 @@ def rest_value(self): def obs_value(self): return self.x[0] + def set_x_unit(self, unit=None): + prev_unit = self.xunit + super().set_x_unit(unit=unit) + self._rest_value = (self._rest_value * prev_unit).to_value(unit, u.spectral()) + @property def redshift(self): return self._redshift @@ -138,16 +218,16 @@ def redshift(self): @redshift.setter def redshift(self, redshift): self._redshift = redshift - if str(self._x_unit.physical_type) == 'length': + if str(self.xunit.physical_type) == 'length': obs_value = self._rest_value*(1+redshift) - elif str(self._x_unit.physical_type) == 'frequency': + elif str(self.xunit.physical_type) == 'frequency': obs_value = self._rest_value/(1+redshift) else: # catch all for anything else (wavenumber, energy, etc) - rest_angstrom = (self._rest_value*self._x_unit).to_value(u.Angstrom, - equivalencies=u.spectral()) + rest_angstrom = (self._rest_value*self.xunit).to_value(u.Angstrom, + equivalencies=u.spectral()) obs_angstrom = rest_angstrom*(1+redshift) - obs_value = (obs_angstrom*u.Angstrom).to_value(self._x_unit, + obs_value = (obs_angstrom*u.Angstrom).to_value(self.xunit, equivalencies=u.spectral()) self.x = [obs_value, obs_value] @@ -168,14 +248,14 @@ def _process_identify_change(self, msg): def _update_data(self, x_all): new_unit = x_all.unit - if new_unit == self._x_unit: + if new_unit == self.xunit: return - old_quant = self._rest_value*self._x_unit + old_quant = self._rest_value*self.xunit self._rest_value = old_quant.to_value(new_unit, equivalencies=u.spectral()) # re-compute self.x from current redshift (instead of converting that as well) self.redshift = self._redshift - self._x_unit = new_unit + self.xunit = new_unit class SliceIndicatorMarks(BaseSpectrumVerticalLine, HubListener): @@ -268,7 +348,7 @@ def _update_colors_opacities(self): self.colors = ["#c75109" if self._active else "#007BA1"] self.opacities = [1.0 if self._active else 0.9] - def _on_change_state(self, msg): + def _on_change_state(self, msg={}): if isinstance(msg, dict): changes = msg else: @@ -484,33 +564,32 @@ def _on_shadowing_changed(self, change): self._update_align() -class PluginMark(): - def update_xy(self, x, y): - self.x = np.asarray(x) - self.y = np.asarray(y) - - def append_xy(self, x, y): - self.x = np.append(self.x, x) - self.y = np.append(self.y, y) - - def clear(self): - self.update_xy([], []) +class LinesAutoUnit(PluginMark, Lines, HubListener): + def __init__(self, viewer, *args, **kwargs): + self.viewer = viewer + super().__init__(*args, **kwargs) class PluginLine(Lines, PluginMark, HubListener): def __init__(self, viewer, x=[], y=[], **kwargs): + self.viewer = viewer # color is same blue as import button super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) class PluginScatter(Scatter, PluginMark, HubListener): def __init__(self, viewer, x=[], y=[], **kwargs): + self.viewer = viewer # color is same blue as import button super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) class LineAnalysisContinuum(PluginLine): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # units do not need to be updated because the plugin itself reruns + # the computation and automatically changes the arrays themselves + self.auto_update_units = False class LineAnalysisContinuumCenter(LineAnalysisContinuum): @@ -529,9 +608,9 @@ class LineAnalysisContinuumRight(LineAnalysisContinuumLeft): pass -class LineUncertainties(Lines): - def __init__(self, **kwargs): - super().__init__(**kwargs) +class LineUncertainties(LinesAutoUnit): + def __init__(self, viewer, *args, **kwargs): + super().__init__(viewer, *args, **kwargs) class ScatterMask(Scatter): diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index c36967ad30..a8bb410257 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -29,7 +29,8 @@ __all__ = ['show_widget', 'TemplateMixin', 'PluginTemplateMixin', - 'BasePluginComponent', 'SelectPluginComponent', + 'ViewerPropertiesMixin', + 'BasePluginComponent', 'SelectPluginComponent', 'UnitSelectPluginComponent', 'PluginSubcomponent', 'SubsetSelect', 'SpatialSubsetSelectMixin', 'SpectralSubsetSelectMixin', 'DatasetSpectralSubsetValidMixin', @@ -103,7 +104,32 @@ def _subset_type(subset): return 'spectral' -class TemplateMixin(VuetifyTemplate, HubListener): +class ViewerPropertiesMixin: + # assumes that self.app is defined by the class + @cached_property + def spectrum_viewer(self): + if hasattr(self, '_default_spectrum_viewer_reference_name'): + viewer_reference = self._default_spectrum_viewer_reference_name + else: + viewer_reference = self.app._get_first_viewer_reference_name( + require_spectrum_viewer=True + ) + + return self.app.get_viewer(viewer_reference) + + @cached_property + def spectrum_2d_viewer(self): + if hasattr(self, '_default_spectrum_2d_viewer_reference_name'): + viewer_reference = self._default_spectrum_2d_viewer_reference_name + else: + viewer_reference = self.app._get_first_viewer_reference_name( + require_spectrum_2d_viewer=True + ) + + return self.app.get_viewer(viewer_reference) + + +class TemplateMixin(VuetifyTemplate, HubListener, ViewerPropertiesMixin): config = Unicode("").tag(sync=True) vdocs = Unicode("").tag(sync=True) popout_button = Any().tag(sync=True, **widget_serialization) @@ -295,7 +321,7 @@ def show(self, loc="inline", title=None): # pragma: no cover show_widget(self, loc=loc, title=title) -class BasePluginComponent(HubListener): +class BasePluginComponent(HubListener, ViewerPropertiesMixin): """ This base class handles attaching traitlets from the plugin itself to logic handled within the component, support for caching and clearing caches on properties, @@ -377,28 +403,6 @@ def _dict_from_viewer(viewer, viewer_item): for vid, viewer in self.app._viewer_store.items() if viewer.__class__.__name__ != 'MosvizTableViewer'] - @cached_property - def spectrum_viewer(self): - if hasattr(self, '_default_spectrum_viewer_reference_name'): - viewer_reference = self._default_spectrum_viewer_reference_name - else: - viewer_reference = self.app._get_first_viewer_reference_name( - require_spectrum_viewer=True - ) - - return self._plugin.app.get_viewer(viewer_reference) - - @cached_property - def spectrum_2d_viewer(self): - if hasattr(self, '_default_spectrum_2d_viewer_reference_name'): - viewer_reference = self._default_spectrum_2d_viewer_reference_name - else: - viewer_reference = self.app._get_first_viewer_reference_name( - require_spectrum_2d_viewer=True - ) - - return self._plugin.app.get_viewer(viewer_reference) - class SelectPluginComponent(BasePluginComponent, HasTraits): """ @@ -475,6 +479,10 @@ def __hash__(self): def choices(self): return self.labels + @choices.setter + def choices(self, choices=[]): + self.items = [{'label': choice} for choice in choices] + @property def is_multiselect(self): if not hasattr(self, 'multiselect'): @@ -643,6 +651,48 @@ def _selected_changed(self, event): raise ValueError(f"{event['new']} not one of {self.labels}, reverting selection to {event['old']}") # noqa +class UnitSelectPluginComponent(SelectPluginComponent): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_observe('items', lambda _: self._clear_cache('unit_choices')) + self._addl_unit_strings = [] + + @cached_property + def unit_choices(self): + return [u.Unit(lbl) for lbl in self.labels] + + @property + def addl_unit_choices(self): + return [u.Unit(choice) for choice in self._addl_unit_strings] + + def _selected_changed(self, event): + self._clear_cache() + if event['new'] in self.labels + ['']: + # the string is an exact match, no converting necessary + return + elif not len(self.labels): + raise ValueError("no valid unit choices") + try: + new_u = u.Unit(event['new']) + except ValueError: + self.selected = event['old'] + raise ValueError(f"{event['new']} could not be converted to a valid unit, reverting selection to {event['old']}") # noqa + if new_u not in self.unit_choices: + if new_u in self.addl_unit_choices: + # append this one (as the valid string representation) to the list of user-choices + addl_index = self.addl_unit_choices.index(new_u) + self.choices = self.choices + [self._addl_unit_strings[addl_index]] + # clear the cache so we can find the appropriate entry in unit_choices + self._clear_cache('unit_choices') + else: + self.selected = event['old'] + raise ValueError(f"{event['new']} not one of {self.labels}, reverting selection to {event['old']}") # noqa + + # convert to default string representation from the valid choices + ind = self.unit_choices.index(new_u) + self.selected = self.labels[ind] + + class LayerSelect(SelectPluginComponent): """ Plugin select for layers, with support for single or multi-selection. @@ -1486,11 +1536,14 @@ def selected_obj(self): # from the data collection return self.get_object(cls=self.default_data_cls) - def selected_spectrum_for_spatial_subset(self, spatial_subset=SPATIAL_DEFAULT_TEXT): + def selected_spectrum_for_spatial_subset(self, + spatial_subset=SPATIAL_DEFAULT_TEXT, + use_display_units=True): if spatial_subset == SPATIAL_DEFAULT_TEXT: spatial_subset = None return self.plugin._specviz_helper.get_data(data_label=self.selected, - spatial_subset=spatial_subset) + spatial_subset=spatial_subset, + use_display_units=use_display_units) def _is_valid_item(self, data): def not_from_plugin(data): diff --git a/jdaviz/core/tests/test_tools.py b/jdaviz/core/tests/test_tools.py index b281486965..81f4400276 100644 --- a/jdaviz/core/tests/test_tools.py +++ b/jdaviz/core/tests/test_tools.py @@ -1,28 +1,22 @@ -import numpy as np from numpy.testing import assert_allclose -from glue.core import Data -def test_boxzoom(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') +def test_boxzoom(cubeviz_helper, image_cube_hdu_obj_microns): + cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label="Test Flux") flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') assert flux_viewer.state.y_min == -0.5 - assert flux_viewer.state.y_max == 127.5 + assert flux_viewer.state.y_max == 8.5 assert flux_viewer.state.x_min == -0.5 - assert flux_viewer.state.x_max == 127.5 + assert flux_viewer.state.x_max == 9.5 t = flux_viewer.toolbar.tools['jdaviz:boxzoom'] t.activate() - t.interact.selected_x = [10, 20] - t.interact.selected_y = [20, 60] + t.interact.selected_x = [1, 4] + t.interact.selected_y = [2, 6] - assert t.get_x_axis_with_aspect_ratio() == (-5., 35.) + assert_allclose(t.get_x_axis_with_aspect_ratio(), [0.277778, 4.722222], rtol=1e-6) def _get_lims(viewer): diff --git a/jdaviz/core/user_api.py b/jdaviz/core/user_api.py index 9ef69ed4f6..4a31da9197 100644 --- a/jdaviz/core/user_api.py +++ b/jdaviz/core/user_api.py @@ -1,3 +1,5 @@ +import astropy.units as u + __all__ = ['UserApiWrapper', 'PluginUserApi'] @@ -38,12 +40,15 @@ def __setattr__(self, attr, value): exp_obj = getattr(self._obj, attr) from jdaviz.core.template_mixin import (SelectPluginComponent, + UnitSelectPluginComponent, PlotOptionsSyncState, AddResults, AutoTextField) if isinstance(exp_obj, SelectPluginComponent): # this allows setting the selection directly without needing to access the underlying # .selected traitlet + if isinstance(exp_obj, UnitSelectPluginComponent) and isinstance(value, u.Unit): + value = value.to_string() exp_obj.selected = value return elif isinstance(exp_obj, AddResults): diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py index 01a939ead0..372a5f2904 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -1,5 +1,4 @@ -import astropy.units as u -import numpy as np +from astropy import units as u __all__ = ['units_to_strings', 'create_spectral_equivalencies_list', 'create_flux_equivalencies_list'] @@ -19,68 +18,61 @@ def units_to_strings(unit_list): result : list A list of the units with their best (i.e., most readable) string version. """ - return [u.Unit(unit).name - if u.Unit(unit) == u.Unit("Angstrom") - else u.Unit(unit).long_names[0] if ( - hasattr(u.Unit(unit), "long_names") and len(u.Unit(unit).long_names) > 0) - else u.Unit(unit).to_string() - for unit in unit_list] + return [u.Unit(unit).to_string() for unit in unit_list] -def create_spectral_equivalencies_list(spectrum, +def create_spectral_equivalencies_list(spectral_axis_unit, exclude=[u.jupiterRad, u.earthRad, u.solRad, - u.lyr, u.AU, u.pc]): - """Get all possible conversions from current spectral_axis_unit. - """ - if spectrum.spectral_axis.unit == u.pix: + u.lyr, u.AU, u.pc, u.Bq, u.micron, u.lsec]): + """Get all possible conversions from current spectral_axis_unit.""" + if spectral_axis_unit in (u.pix, u.dimensionless_unscaled): return [] # Get unit equivalencies. - curr_spectral_axis_unit_equivalencies = u.Unit( - spectrum.spectral_axis.unit).find_equivalent_units( - equivalencies=u.spectral()) + try: + curr_spectral_axis_unit_equivalencies = spectral_axis_unit.find_equivalent_units( + equivalencies=u.spectral()) + except u.core.UnitConversionError: + return [] # Get local units. - locally_defined_spectral_axis_units = ['angstrom', 'nanometer', - 'micron', 'hertz', 'erg'] + locally_defined_spectral_axis_units = ['Angstrom', 'nm', + 'um', 'Hz', 'erg'] local_units = [u.Unit(unit) for unit in locally_defined_spectral_axis_units] # Remove overlap units. curr_spectral_axis_unit_equivalencies = list(set(curr_spectral_axis_unit_equivalencies) - - set(local_units+exclude)) + - set(local_units + exclude)) # Convert equivalencies into readable versions of the units and sorted alphabetically. spectral_axis_unit_equivalencies_titles = sorted(units_to_strings( curr_spectral_axis_unit_equivalencies)) # Concatenate both lists with the local units coming first. - spectral_axis_unit_equivalencies_titles = sorted(units_to_strings( - local_units)) + spectral_axis_unit_equivalencies_titles - - return spectral_axis_unit_equivalencies_titles + return sorted(units_to_strings(local_units)) + spectral_axis_unit_equivalencies_titles -def create_flux_equivalencies_list(spectrum): - """Get all possible conversions for flux from current flux units. - """ - if ((spectrum.flux.unit == u.count) or (spectrum.spectral_axis.unit == u.pix)): +def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): + """Get all possible conversions for flux from current flux units.""" + if ((flux_unit in (u.count, (u.MJy / u.sr), u.dimensionless_unscaled)) + or (spectral_axis_unit in (u.pix, u.dimensionless_unscaled))): return [] - # Get unit equivalencies. - curr_flux_unit_equivalencies = u.Unit( - spectrum.flux.unit).find_equivalent_units( - equivalencies=u.spectral_density(np.sum(spectrum.spectral_axis)), + # Get unit equivalencies. Value passed into u.spectral_density() is irrelevant. + try: + curr_flux_unit_equivalencies = flux_unit.find_equivalent_units( + equivalencies=u.spectral_density(1 * spectral_axis_unit), include_prefix_units=False) + except u.core.UnitConversionError: + return [] # Get local units. locally_defined_flux_units = ['Jy', 'mJy', 'uJy', 'W / (m2 Hz)', 'eV / (s m2 Hz)', 'erg / (s cm2)', - 'erg / (s cm2 um)', 'erg / (s cm2 Angstrom)', 'erg / (s cm2 Hz)', - 'ph / (s cm2 um)', 'ph / (s cm2 Angstrom)', 'ph / (s cm2 Hz)'] local_units = [u.Unit(unit) for unit in locally_defined_flux_units] @@ -93,7 +85,4 @@ def create_flux_equivalencies_list(spectrum): flux_unit_equivalencies_titles = sorted(units_to_strings(curr_flux_unit_equivalencies)) # Concatenate both lists with the local units coming first. - flux_unit_equivalencies_titles = (sorted(units_to_strings(local_units)) + - flux_unit_equivalencies_titles) - - return flux_unit_equivalencies_titles + return sorted(units_to_strings(local_units)) + flux_unit_equivalencies_titles diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 5a2fd2b2a1..f6c08ec574 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -4,12 +4,10 @@ from astropy.tests.helper import assert_quantity_allclose from glue.core import Data from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, XRangeROI - from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode, XorMode from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion - from numpy.testing import assert_allclose -from specutils import SpectralRegion +from specutils import SpectralRegion, Spectrum1D from jdaviz.core.marks import ShadowSpatialSpectral @@ -135,12 +133,10 @@ def test_region_from_subset_3d(cubeviz_helper): def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test 1D Flux', coords=spectral_cube_wcs) + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test 1D Flux') + cubeviz_helper.load_data(data, data_label='Test 1D Flux') cubeviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(5, 15.5)) @@ -184,11 +180,8 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") # use gaussian smooth plugin as a regression test for # https://github.com/spacetelescope/jdaviz/issues/1853 @@ -236,11 +229,8 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): def test_disjoint_spatial_subset(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") flux_viewer = cubeviz_helper.app.get_viewer("flux-viewer") flux_viewer.apply_roi(CircularROI(xc=3, yc=4, radius=1)) @@ -260,11 +250,8 @@ def test_disjoint_spatial_subset(cubeviz_helper, spectral_cube_wcs): def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") spec_viewer = cubeviz_helper.app.get_viewer("spectrum-viewer") spec_viewer.apply_roi(XRangeROI(5, 15.5)) diff --git a/jdaviz/utils.py b/jdaviz/utils.py index 3d042d99f0..59ad0cf814 100644 --- a/jdaviz/utils.py +++ b/jdaviz/utils.py @@ -4,7 +4,6 @@ from collections import deque import matplotlib.pyplot as plt - from astropy.io import fits from ipyvue import watch from glue.config import settings diff --git a/notebooks/concepts/specviz_glue_unit_conversion.ipynb b/notebooks/concepts/specviz_glue_unit_conversion.ipynb new file mode 100644 index 0000000000..614d7c46b1 --- /dev/null +++ b/notebooks/concepts/specviz_glue_unit_conversion.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "57c41ae5", + "metadata": {}, + "source": [ + "This is a concept notebook to investigate Glue unit conversion behavior integrated into Jdaviz spectrum viewer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b3bbfb1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from astropy import units as u\n", + "from specutils import Spectrum1D\n", + "\n", + "from jdaviz import Specviz" + ] + }, + { + "cell_type": "markdown", + "id": "0d253733", + "metadata": {}, + "source": [ + "First spectrum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cba69bb7", + "metadata": {}, + "outputs": [], + "source": [ + "wave1 = np.linspace(2, 5, 10) * u.um\n", + "flux1 = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1] * u.Jy\n", + "spec1 = Spectrum1D(flux=flux1, spectral_axis=wave1)\n", + "\n", + "print(wave1)\n", + "print(flux1)" + ] + }, + { + "cell_type": "markdown", + "id": "1e0e7dc5", + "metadata": {}, + "source": [ + "Second spectrum in different units and with slight offsets in spectral axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26bc794d", + "metadata": {}, + "outputs": [], + "source": [ + "wave2 = (wave1 + (0.1 * u.um)).to(u.GHz, u.spectral())\n", + "flux2 = flux1.to(u.mJy)\n", + "spec2 = Spectrum1D(flux=flux2, spectral_axis=wave2)\n", + "\n", + "print(wave2)\n", + "print(wave2.to(u.um, u.spectral()))\n", + "print(flux2)" + ] + }, + { + "cell_type": "markdown", + "id": "2bb4f4ea", + "metadata": {}, + "source": [ + "Fire up Specviz." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74692a02", + "metadata": {}, + "outputs": [], + "source": [ + "specviz = Specviz()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efdba701", + "metadata": {}, + "outputs": [], + "source": [ + "specviz.show()" + ] + }, + { + "cell_type": "markdown", + "id": "642fbae1", + "metadata": {}, + "source": [ + "Load the data into Specviz. Desired behavior:\n", + "\n", + "1. \"Jy_um\" would load with Jy in Y-axis and um in X-axis.\n", + "2. \"mJy_GHz\" would load with data automatically converted to Jy and um in the plot. You would see the same shape but slightly offset in X-axis, just slightly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ee09160", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "specviz.load_data(spec1, data_label=\"Jy_um\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9655292b", + "metadata": {}, + "outputs": [], + "source": [ + "specviz.load_data(spec2, data_label=\"mJy_GHz\")" + ] + }, + { + "cell_type": "markdown", + "id": "e6012551", + "metadata": {}, + "source": [ + "Change the spectral axis display unit to GHz." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a4611b3", + "metadata": {}, + "outputs": [], + "source": [ + "viewer = specviz.app.get_viewer(\"spectrum-viewer\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac319e71", + "metadata": {}, + "outputs": [], + "source": [ + "viewer.state.x_display_unit = \"GHz\"" + ] + }, + { + "cell_type": "markdown", + "id": "b2e84da2", + "metadata": {}, + "source": [ + "Change the flux axis display unit to FLAM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4892cb38", + "metadata": {}, + "outputs": [], + "source": [ + "FLAM = u.erg / (u.s * u.cm * u.cm * u.AA)\n", + "\n", + "# If astropy can do it, Jdaviz should too.\n", + "spec1.flux.to(FLAM, u.spectral_density(spec1.spectral_axis))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3be87c54", + "metadata": {}, + "outputs": [], + "source": [ + "# this MIGHT fail depending on the version of astropy (since glue harcodes the expected string formatting \n", + "# for units, whereas astropy recently changed the default order of units)\n", + "try:\n", + " viewer.state.y_display_unit = FLAM.to_string()\n", + "except ValueError as e:\n", + " print(\"setting y_display_unit failed: \", repr(e))" + ] + }, + { + "cell_type": "markdown", + "id": "116e5806", + "metadata": {}, + "source": [ + "The plugin select component, however, is unit-aware and will handle mapping to the string formatting expected by glue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da5df8d4", + "metadata": {}, + "outputs": [], + "source": [ + "uc = specviz.plugins['Unit Conversion']\n", + "uc.flux_unit.choices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b74ae8aa", + "metadata": {}, + "outputs": [], + "source": [ + "uc.flux_unit = FLAM.to_string()" + ] + }, + { + "cell_type": "markdown", + "id": "8be218b4", + "metadata": {}, + "source": [ + "Change the spectral axis again, this time to Angstrom via the plugin API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f9073f7", + "metadata": {}, + "outputs": [], + "source": [ + "uc.spectral_unit = 'Angstrom'" + ] + }, + { + "cell_type": "markdown", + "id": "fa487529", + "metadata": {}, + "source": [ + "Change everything back to original units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c110686", + "metadata": {}, + "outputs": [], + "source": [ + "viewer.state.x_display_unit = \"micron\"\n", + "viewer.state.y_display_unit = \"Jy\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87642b18", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/concepts/specviz_unit_conversion.ipynb b/notebooks/concepts/specviz_unit_conversion.ipynb deleted file mode 100644 index 1402e6c56c..0000000000 --- a/notebooks/concepts/specviz_unit_conversion.ipynb +++ /dev/null @@ -1,61 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from jdaviz.configs.specviz.helper import Specviz\n", - "import specutils\n", - "import astropy.units as u\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "spec_url = 'https://dr14.sdss.org/optical/spectrum/view/data/format=fits/spec=lite?plateid=1323&mjd=52797&fiberid=12'\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "specviz = Specviz()\n", - "spec = specutils.Spectrum1D.read(spec_url)\n", - "specviz.load_spectrum(spec)\n", - "\n", - "specviz.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/pyproject.toml b/pyproject.toml index 380533e946..44e46a75a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "bqplot>=0.12.37", "bqplot-image-gl>=1.4.11", "glue-core>=1.11", - "glue-jupyter>=0.15.0", + "glue-jupyter>=0.16.3", "echo>=0.5.0", "ipykernel>=6.19.4", "ipyvue>=1.6", From 2154035e08b1a21f071d202551ec2ac85349d7a5 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen Date: Tue, 13 Jun 2023 12:08:45 -0400 Subject: [PATCH 101/283] Remove changelog entry that was included in #2127 --- CHANGES.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1865c17c79..561ce5b2d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,8 +32,6 @@ Specviz2d API Changes ----------- -- Subset Plugin now respects the chosen display unit after using Unit Conversion. [#2195] - Cubeviz ^^^^^^^ From f6afd62621d9de6004112c80b8e66f2820fb02bf Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 13 Jun 2023 15:24:54 -0400 Subject: [PATCH 102/283] subset plugin: fallback if unit cannot be found * introduced by #2127: spatial subsets crashed when trying to display the unit in the subset plugin, this now implements a fallback to not show the unit suffix --- jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue index 515a40723b..e3d0080d0d 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue @@ -83,7 +83,7 @@ v-model.number="item.value" type="number" style="padding-top: 0px; margin-top: 0px" - :suffix="item.unit.replace('Angstrom', 'A')" + :suffix="item.unit ? item.unit.replace('Angstrom', 'A') : ''" > From a3bf463ac22961cdef298caf97a4cf84166e930c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 13 Jun 2023 15:31:20 -0400 Subject: [PATCH 103/283] specviz2d: support plotting 1d spectrum with spectral axis (#2219) * spectral extraction: revert exposing result in pixel-space * create separate 1d profile viewer for mosviz/specviz2d * add new "matched" icons * use matched icons in specviz2d/mosviz * update internal call to use new get_subsets * specviz2d gaussian smooth to only consider 1d data as input * fix mosviz row change * update specviz2d mouseover test to use wavelength instead of pixels * fix case where requested limits entirely out of range * fix linking logic for plugins in specviz2d/mosviz * avoid recursion when syncing axes limits * update mosviz test * fixture to use the same spectral axis for the 1d and 2d case --------- Co-authored-by: jenneh Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 5 + jdaviz/app.py | 9 +- .../gaussian_smooth/gaussian_smooth.py | 2 +- .../tests/test_gaussian_smooth.py | 18 ++- .../default/plugins/line_lists/line_lists.py | 2 - jdaviz/configs/imviz/plugins/tools.py | 71 +++------ jdaviz/configs/imviz/tests/test_tools.py | 1 + jdaviz/configs/mosviz/helper.py | 133 ---------------- jdaviz/configs/mosviz/mosviz.yaml | 2 +- jdaviz/configs/mosviz/plugins/__init__.py | 1 + jdaviz/configs/mosviz/plugins/tools.py | 67 +++++++++ jdaviz/configs/mosviz/plugins/viewers.py | 142 +++++++++++++++++- jdaviz/configs/mosviz/tests/test_helper.py | 20 --- jdaviz/configs/mosviz/tests/test_tools.py | 41 +++++ jdaviz/configs/specviz2d/helper.py | 86 ----------- .../spectral_extraction.py | 9 -- jdaviz/configs/specviz2d/specviz2d.yaml | 2 +- .../configs/specviz2d/tests/test_parsers.py | 6 +- jdaviz/conftest.py | 4 +- jdaviz/core/template_mixin.py | 4 +- jdaviz/core/tools.py | 78 ++++++++++ jdaviz/data/icons/home_match.svg | 13 ++ jdaviz/data/icons/pan_x_match.svg | 14 ++ jdaviz/data/icons/pan_y_match.svg | 14 ++ jdaviz/data/icons/zoom_xrange.svg | 15 +- jdaviz/data/icons/zoom_xrange_match.svg | 18 +++ jdaviz/data/icons/zoom_yrange.svg | 15 +- jdaviz/data/icons/zoom_yrange_match.svg | 18 +++ 28 files changed, 476 insertions(+), 334 deletions(-) create mode 100644 jdaviz/configs/mosviz/plugins/tools.py create mode 100644 jdaviz/configs/mosviz/tests/test_tools.py create mode 100644 jdaviz/data/icons/home_match.svg create mode 100644 jdaviz/data/icons/pan_x_match.svg create mode 100644 jdaviz/data/icons/pan_y_match.svg create mode 100644 jdaviz/data/icons/zoom_xrange_match.svg create mode 100644 jdaviz/data/icons/zoom_yrange_match.svg diff --git a/CHANGES.rst b/CHANGES.rst index 561ce5b2d4..05293eb7e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,12 +23,17 @@ Imviz Mosviz ^^^^^^ +- Improved x-axis limit-matching between 2d and 1d spectrum viewers. [#2219] + Specviz ^^^^^^^ Specviz2d ^^^^^^^^^ +- Re-enable support for displaying the 1d spectrum in wavelength/frequency space, with improved + x-axis limit-matching. [#2219] + API Changes ----------- diff --git a/jdaviz/app.py b/jdaviz/app.py index 08d0e65fdf..9dc4b38471 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -499,10 +499,11 @@ def _link_new_data(self, reference_data=None, data_to_be_linked=None): LinkSame(linked_data.components[0], ref_data.components[1])] dc.add_link(links) return - elif (linked_data.meta.get('Plugin', None) == 'SpectralExtraction' or - (linked_data.meta.get('Plugin', None) == ('GaussianSmooth') and - linked_data.ndim < 3 and # Cube linking requires special logic. See below - ref_data.ndim < 3) + elif ((self.config in ('specviz2d', 'mosviz') and + linked_data.meta.get('Plugin', None) is not None) or + (linked_data.meta.get('Plugin', None) == 'GaussianSmooth' and + linked_data.ndim < 3 and # Cube linking requires special logic. See below + ref_data.ndim < 3) ): links = [LinkSame(linked_data.components[0], ref_data.components[0]), LinkSame(linked_data.components[1], ref_data.components[1])] diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py index 3142843f56..a1d8c11e8b 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py @@ -66,7 +66,7 @@ def __init__(self, *args, **kwargs): ] # clear the cache in case the spectrum-viewer selection was already cached self.dataset._clear_cache() - elif self.config == "mosviz": + elif self.config in ("mosviz", "specviz2d"): # only allow smoothing 1d spectra self.dataset._viewers = [self._default_spectrum_viewer_reference_name] self.dataset._clear_cache() diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py index 4c6cb0574e..20e703c857 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py @@ -120,7 +120,7 @@ def test_spatial_convolution(cubeviz_helper, spectrum1d_cube): == (2, 4, 2)) -def test_spectrum1d_smooth(specviz_helper, spectrum1d): +def test_specviz_smooth(specviz_helper, spectrum1d): data_label = 'test' dc = specviz_helper.app.data_collection specviz_helper.load_data(spectrum1d, data_label=data_label) @@ -160,7 +160,7 @@ def test_spectrum1d_smooth(specviz_helper, spectrum1d): assert label_mouseover.icon == 'mdi-cursor-default' -def test_spectrum2d_smooth(specviz2d_helper, spectrum2d): +def test_specviz2d_smooth(specviz2d_helper, spectrum2d): data_label = 'test' dc = specviz2d_helper.app.data_collection specviz2d_helper.load_data(spectrum_2d=spectrum2d, spectrum_2d_label=data_label) @@ -170,18 +170,20 @@ def test_spectrum2d_smooth(specviz2d_helper, spectrum2d): # The Autocollapsed spectrum is given the label of "Spectrum 1D by default" smooth_source_dataset = "Spectrum 1D" gs_plugin.dataset = smooth_source_dataset - test_stddev_level = 100.0 + test_stddev_level = 10.0 gs_plugin.stddev = test_stddev_level - gs_plugin.smooth() + smoothed_spectrum = gs_plugin.smooth(add_data=True) assert len(dc) == 3 assert dc[2].label == f'{smooth_source_dataset} smooth stddev-{test_stddev_level}' + np.testing.assert_allclose(smoothed_spectrum.spectral_axis.value, + spectrum2d.spectral_axis.value) # Ensure all marks were created properly (i.e. not in their initialized state) # [0,1] is the default (initialization) value for the marks marks = specviz2d_helper.app.get_viewer('spectrum-viewer').native_marks - assert len(marks) == 2 - for mark in marks: - np.testing.assert_allclose(mark.x, spectrum2d.spectral_axis.value) - assert not np.array_equal(mark.y, [0, 1]) + + gp_mark = marks[-1] + np.testing.assert_allclose(gp_mark.x, smoothed_spectrum.spectral_axis.value) + np.testing.assert_allclose(gp_mark.y, smoothed_spectrum.flux.value) diff --git a/jdaviz/configs/default/plugins/line_lists/line_lists.py b/jdaviz/configs/default/plugins/line_lists/line_lists.py index 1061ff195d..d324ea420c 100644 --- a/jdaviz/configs/default/plugins/line_lists/line_lists.py +++ b/jdaviz/configs/default/plugins/line_lists/line_lists.py @@ -411,10 +411,8 @@ def vue_slider_reset(self, event): def _on_spectrum_viewer_limits_changed(self, event=None): sv = self.app.get_viewer(self._default_spectrum_viewer_reference_name) - if sv.state.x_min is None or sv.state.x_max is None: return - self.spectrum_viewer_min = float(sv.state.x_min) self.spectrum_viewer_max = float(sv.state.x_max) diff --git a/jdaviz/configs/imviz/plugins/tools.py b/jdaviz/configs/imviz/plugins/tools.py index eb5c2757d8..955f03ed89 100644 --- a/jdaviz/configs/imviz/plugins/tools.py +++ b/jdaviz/configs/imviz/plugins/tools.py @@ -7,70 +7,33 @@ from glue_jupyter.bqplot.image import BqplotImageView from glue_jupyter.utils import debounced -from jdaviz.core.tools import BoxZoom, PanZoom +from jdaviz.core.tools import BoxZoom, PanZoom, _MatchedZoomMixin __all__ = [] ICON_DIR = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'icons') -class _MatchedZoomMixin: - def _iter_image_viewers(self): - for viewer in self.viewer.session.application.viewers: - if isinstance(viewer, BqplotImageView): - yield viewer +class _ImvizMatchedZoomMixin(_MatchedZoomMixin): + match_keys = ('x_min', 'x_max', 'y_min', 'y_max') + disable_matched_zoom_in_other_viewer = False - def save_prev_zoom(self): - # override the behavior in core.tools._BaseZoomHistory to store viewer limits - # for all referenced viewers. This enables the previous zoom button to work for - # a viewer whose zoom was changed by a MatchedZoom instance from another viewer - for viewer in self._iter_image_viewers(): - viewer._prev_limits = (viewer.state.x_min, viewer.state.x_max, - viewer.state.y_min, viewer.state.y_max) - - def activate(self): - - super().activate() - self.viewer.state.add_callback('x_min', self.on_limits_change) - self.viewer.state.add_callback('x_max', self.on_limits_change) - self.viewer.state.add_callback('y_min', self.on_limits_change) - self.viewer.state.add_callback('y_max', self.on_limits_change) + def _is_matched_viewer(self, viewer): + return isinstance(viewer, BqplotImageView) + def _post_activate(self): # NOTE: For Imviz only. # Set the reference data in other viewers to be the same as the current viewer. # If adding the data to the viewer, make sure it is not actually shown since the # user didn't request it. - if self.viewer.jdaviz_app.config == 'imviz': - for viewer in self._iter_image_viewers(): - if viewer is not self.viewer: - if self.viewer.state.reference_data not in viewer.state.layers_data: - viewer.add_data(self.viewer.state.reference_data) - for layer in viewer.state.layers: - if layer.layer is self.viewer.state.reference_data: - layer.visible = False - break - viewer.state.reference_data = self.viewer.state.reference_data - - # Trigger a sync so the initial limits match - self.on_limits_change() - - def deactivate(self): - - self.viewer.state.remove_callback('x_min', self.on_limits_change) - self.viewer.state.remove_callback('x_max', self.on_limits_change) - self.viewer.state.remove_callback('y_min', self.on_limits_change) - self.viewer.state.remove_callback('y_max', self.on_limits_change) - - super().deactivate() - - def on_limits_change(self, *args): - for viewer in self._iter_image_viewers(): - if viewer is not self.viewer: - with delay_callback(viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'): - viewer.state.x_min = self.viewer.state.x_min - viewer.state.x_max = self.viewer.state.x_max - viewer.state.y_min = self.viewer.state.y_min - viewer.state.y_max = self.viewer.state.y_max + for viewer in self._iter_matched_viewers(include_self=False): + if self.viewer.state.reference_data not in viewer.state.layers_data: + viewer.add_data(self.viewer.state.reference_data) + for layer in viewer.state.layers: + if layer.layer is self.viewer.state.reference_data: + layer.visible = False + break + viewer.state.reference_data = self.viewer.state.reference_data @viewer_tool @@ -124,7 +87,7 @@ def on_click(self, data): @viewer_tool -class MatchBoxZoom(_MatchedZoomMixin, BoxZoom): +class MatchBoxZoom(_ImvizMatchedZoomMixin, BoxZoom): icon = os.path.join(ICON_DIR, 'zoom_box_match.svg') tool_id = 'jdaviz:boxzoommatch' action_text = 'Box zoom, matching between viewers' @@ -132,7 +95,7 @@ class MatchBoxZoom(_MatchedZoomMixin, BoxZoom): @viewer_tool -class MatchPanZoom(_MatchedZoomMixin, ImagePanZoom): +class MatchPanZoom(_ImvizMatchedZoomMixin, ImagePanZoom): icon = os.path.join(ICON_DIR, 'panzoom_match.svg') tool_id = 'jdaviz:panzoommatch' action_text = 'Pan, matching between viewers' diff --git a/jdaviz/configs/imviz/tests/test_tools.py b/jdaviz/configs/imviz/tests/test_tools.py index 1711bebf0d..71a726cd6c 100644 --- a/jdaviz/configs/imviz/tests/test_tools.py +++ b/jdaviz/configs/imviz/tests/test_tools.py @@ -15,6 +15,7 @@ def test_panzoom_tools(self): # original limits (x_min, x_max, y_min, y_max): -0.5 9.5 -0.5 9.5 original_limits = (v.state.x_min, v.state.x_max, v.state.y_min, v.state.y_max) assert_allclose(original_limits, (-0.5, 9.5, -0.5, 9.5)) + assert_allclose((v2.state.x_min, v2.state.x_max, v2.state.y_min, v2.state.y_max), original_limits) # noqa t.activate() t.save_prev_zoom() v.state.x_min, v.state.x_max, v.state.y_min, v.state.y_max = (1, 8, 1, 8) diff --git a/jdaviz/configs/mosviz/helper.py b/jdaviz/configs/mosviz/helper.py index 8d16f085a7..669a3442ac 100644 --- a/jdaviz/configs/mosviz/helper.py +++ b/jdaviz/configs/mosviz/helper.py @@ -2,7 +2,6 @@ import warnings from copy import deepcopy from pathlib import Path -from time import time from zipfile import is_zipfile import numpy as np @@ -39,10 +38,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) spec1d = self.app.get_viewer(self._default_spectrum_viewer_reference_name) - spec1d.scales['x'].observe(self._update_spec2d_x_axis, names=['min', 'max']) - spec2d = self.app.get_viewer(self._default_spectrum_2d_viewer_reference_name) - spec2d.scales['x'].observe(self._update_spec1d_x_axis, names=['min', 'max']) image_viewer = self.app.get_viewer(self._default_image_viewer_reference_name) @@ -78,12 +74,6 @@ def __init__(self, *args, **kwargs): self._shared_image = False - self._scales1d = spec1d.scales['x'] - self._scales2d = spec2d.scales['x'] - - self._panning_warning_triggered = False - self._last_panning_warning = 0 - self._update_in_progress = False self._initialize_table() @@ -140,94 +130,6 @@ def _on_row_selected_end(self, event): self._frozen_layers_cache = [] - # Make sure world flipping has been handled correctly, as internal - # callbacks may have been made while limits were frozen. This is - # especially important for NIRISS data. - self._update_spec2d_x_axis() - - def _extend_world(self, spec1d, ext): - # Extend 1D spectrum world axis to enable panning (within reason) past - # the bounds of data - world = self.app.data_collection[spec1d]["World 0"].copy() - dw = world[1]-world[0] - prepend = np.linspace(world[0]-dw*ext, world[0]-dw, ext) - dw = world[-1]-world[-2] - append = np.linspace(world[-1]+dw, world[-1]+dw*ext, ext) - world = np.hstack((prepend, world, append)) - return world - - def _update_spec2d_x_axis(self, change=None): - # This assumes the two spectrum viewers have the same x-axis shape and - # wavelength solution, which should always hold - table_viewer = self.app.get_viewer(self._default_table_viewer_reference_name) - - if self._update_in_progress or table_viewer.row_selection_in_progress: - return - - min_1d = self._scales1d.min - max_1d = self._scales1d.max - - spec1d = table_viewer._selected_data[self._default_spectrum_viewer_reference_name] - extend_by = int(self.app.data_collection[spec1d]["World 0"].shape[0]) - world = self._extend_world(spec1d, extend_by) - - # Workaround for flipped data - min_world, max_world = ((world[0], world[-1]) if not self._is_world_flipped() - else (world[-1], world[0])) - - # Warn the user if they've panned far enough away from the data - # that the viewers will desync - if min_1d < min_world or max_1d > max_world: - self._show_panning_warning() - return - - self._panning_warning_triggered = False - - idx_min = float((np.abs(world - min_1d)).argmin()) - extend_by - idx_max = float((np.abs(world - max_1d)).argmin()) - extend_by - - self._update_in_progress = True - with self._scales2d.hold_sync(): - self._scales2d.min = idx_min - self._scales2d.max = idx_max - - self._update_in_progress = False - - def _update_spec1d_x_axis(self, change=None): - # This assumes the two spectrum viewers have the same x-axis shape and - # wavelength solution, which should always hold - table_viewer = self.app.get_viewer(self._default_table_viewer_reference_name) - - if self._update_in_progress or table_viewer.row_selection_in_progress: - return - - min_2d = self._scales2d.min - max_2d = self._scales2d.max - - spec1d = table_viewer._selected_data[self._default_spectrum_viewer_reference_name] - extend_by = int(self.app.data_collection[spec1d]["World 0"].shape[0]) - world = self._extend_world(spec1d, extend_by) - - idx_min = int(np.around(min_2d)) + extend_by - idx_max = int(np.around(max_2d)) + extend_by - - # Warn the user if they've panned far enough away from the data - # that the viewers will desync - # Note: Because of the flipped data workaround, idx_min can be > idx_max - max_world = len(world) - if not (0 <= idx_min < max_world and 0 <= idx_max < max_world): - self._show_panning_warning() - return - - self._panning_warning_triggered = False - - self._update_in_progress = True - with self._scales1d.hold_sync(): - self._scales1d.min = world[idx_min] - self._scales1d.max = world[idx_max] - - self._update_in_progress = False - def _redshift_listener(self, msg): '''Save new redshifts (including from the helper itself)''' if self._update_in_progress: @@ -255,35 +157,8 @@ def _apply_redshift_from_table(self, row, value=None): if value is not None: self.specviz.set_redshift(value) - def _show_panning_warning(self): - now = time() - - # Limit the number of messages that can be send to 1 per 5 seconds - panning_warning_timeout = 5 - - if (not self._panning_warning_triggered - and now > self._last_panning_warning + panning_warning_timeout): - self._panning_warning_triggered = True - self._last_panning_warning = now - msg = ("Warning: panning too far away from the data may desync" - "the 1D and 2D spectrum viewers") - msg = SnackbarMessage(msg, color='warning', sender=self) - self.app.hub.broadcast(msg) - - def _is_world_flipped(self): - spec1d = self.app.get_viewer( - self._default_table_viewer_reference_name - )._selected_data.get( - self._default_spectrum_viewer_reference_name - ) - if not spec1d: - return False - world = self.app.data_collection[spec1d]["World 0"] - return world[0] > world[-1] - def _row_click_message_handler(self, msg): self._handle_image_zoom(msg) - self._handle_flipped_data() # expose the row to vue for each of the viewers self.app.state.settings = {**self.app.state.settings, 'mosviz_row': msg.selected_index} @@ -317,14 +192,6 @@ def _handle_image_zoom(self, msg): if center is not None: imview.center_on(center) - def _handle_flipped_data(self): - # Workaround for flipped data - if self._is_world_flipped(): - min, max = self._scales2d.max, self._scales2d.min - with self._scales2d.hold_sync(): - self._scales2d.min = min - self._scales2d.max = max - def _zoom_to_object_params(self, msg): table_data = self.app.data_collection['MOS Table'] diff --git a/jdaviz/configs/mosviz/mosviz.yaml b/jdaviz/configs/mosviz/mosviz.yaml index b5a846350c..2f3da84762 100644 --- a/jdaviz/configs/mosviz/mosviz.yaml +++ b/jdaviz/configs/mosviz/mosviz.yaml @@ -43,7 +43,7 @@ viewer_area: plot: mosviz-profile-2d-viewer reference: spectrum-2d-viewer - name: Spectrum - plot: specviz-profile-viewer + plot: mosviz-profile-viewer reference: spectrum-viewer - container: row viewers: diff --git a/jdaviz/configs/mosviz/plugins/__init__.py b/jdaviz/configs/mosviz/plugins/__init__.py index 19b2dd1908..9be5c4323d 100644 --- a/jdaviz/configs/mosviz/plugins/__init__.py +++ b/jdaviz/configs/mosviz/plugins/__init__.py @@ -1,4 +1,5 @@ from .viewers import * # noqa from .parsers import * # noqa +from .tools import * # noqa from .slit_overlay.slit_overlay import * # noqa from .row_lock.row_lock import * # noqa diff --git a/jdaviz/configs/mosviz/plugins/tools.py b/jdaviz/configs/mosviz/plugins/tools.py new file mode 100644 index 0000000000..bde5a25f82 --- /dev/null +++ b/jdaviz/configs/mosviz/plugins/tools.py @@ -0,0 +1,67 @@ +import os + +from glue.config import viewer_tool + +from jdaviz.configs.mosviz.plugins.viewers import MosvizProfileView, MosvizProfile2DView +from jdaviz.core.tools import _MatchedZoomMixin, HomeZoom, BoxZoom, XRangeZoom, PanZoom, PanZoomX + +__all__ = [] + +ICON_DIR = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'icons') + + +class _MatchedXZoomMixin(_MatchedZoomMixin): + match_axes = ('x',) + disable_matched_zoom_in_other_viewer = True + + def _is_matched_viewer(self, viewer): + return isinstance(viewer, (MosvizProfile2DView, MosvizProfileView)) + + def _map_limits(self, from_viewer, to_viewer, limits={}): + if isinstance(from_viewer, MosvizProfileView) and isinstance(to_viewer, MosvizProfile2DView): # noqa + limits['x_min'], limits['x_max'] = to_viewer.world_to_pixel_limits((limits['x_min'], + limits['x_max'])) + elif isinstance(from_viewer, MosvizProfile2DView) and isinstance(to_viewer, MosvizProfileView): # noqa + limits['x_min'], limits['x_max'] = from_viewer.pixel_to_world_limits((limits['x_min'], + limits['x_max'])) + return limits + + +@viewer_tool +class MosvizHomeZoom(_MatchedXZoomMixin, HomeZoom): + icon = os.path.join(ICON_DIR, 'home_match.svg') + tool_id = 'mosviz:homezoom' + action_text = 'Reset zoom' + tool_tip = 'Reset zoom to show all visible data, matching x-limits in all viewers' + + +@viewer_tool +class MosvizBoxZoom(_MatchedXZoomMixin, BoxZoom): + icon = os.path.join(ICON_DIR, 'zoom_box_match.svg') + tool_id = 'mosviz:boxzoom' + action_text = 'Box zoom, matching x-limits between viewers' + tool_tip = 'Zoom to a drawn rectangle, matching x-limits in all viewers' + + +@viewer_tool +class MosvizXRangeZoom(_MatchedXZoomMixin, XRangeZoom): + icon = os.path.join(ICON_DIR, 'zoom_xrange_match.svg') + tool_id = 'mosviz:xrangezoom' + action_text = 'Horizontal zoom, matching x-limits between viewers' + tool_tip = 'Zoom to a drawn horizontal region, matching x-limits in all viewers' + + +@viewer_tool +class MosvizPanZoom(_MatchedXZoomMixin, PanZoom): + icon = os.path.join(ICON_DIR, 'panzoom_match.svg') + tool_id = 'mosviz:panzoom' + action_text = 'Pan, matching x-limits between viewers' + tool_tip = 'Pan (click-drag) or zoom (scroll), matching x-limits in all viewers' # noqa + + +@viewer_tool +class MosvizPanZoomX(_MatchedXZoomMixin, PanZoomX): + icon = os.path.join(ICON_DIR, 'pan_x_match.svg') + tool_id = 'mosviz:panzoom_x' + action_text = 'Pan, matching x-limits between viewers' + tool_tip = 'Pan (click-drag) or zoom (scroll) in x only, matching limits in all viewers' # noqa diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py index 7e35675103..ba65587373 100644 --- a/jdaviz/configs/mosviz/plugins/viewers.py +++ b/jdaviz/configs/mosviz/plugins/viewers.py @@ -1,8 +1,12 @@ +import numpy as np + from astropy.coordinates import SkyCoord from astropy.table import QTable +from functools import cached_property from glue.core import BaseData from glue_jupyter.bqplot.image import BqplotImageView from glue_jupyter.table import TableViewer +from scipy.interpolate import interp1d from specutils import Spectrum1D from jdaviz.core.events import (AddDataToViewerMessage, @@ -12,9 +16,10 @@ from jdaviz.core.registries import viewer_registry from jdaviz.core.freezable_state import FreezableBqplotImageViewerState from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin +from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView __all__ = ['MosvizImageView', 'MosvizProfile2DView', - 'MosvizTableViewer'] + 'MosvizProfileView', 'MosvizTableViewer'] @viewer_registry("mosviz-image-viewer", label="Image 2D (Mosviz)") @@ -66,9 +71,9 @@ class MosvizProfile2DView(JdavizViewerMixin, BqplotImageView): # categories: zoom resets, zoom, pan, subset, select tools, shortcuts tools_nested = [ - ['jdaviz:homezoom', 'jdaviz:prevzoom'], - ['jdaviz:boxzoom', 'jdaviz:xrangezoom', 'jdaviz:yrangezoom'], - ['jdaviz:panzoom', 'jdaviz:panzoom_x'], + ['mosviz:homezoom'], + ['mosviz:boxzoom', 'mosviz:xrangezoom', 'jdaviz:yrangezoom'], + ['mosviz:panzoom', 'mosviz:panzoom_x', 'jdaviz:panzoom_y'], ['bqplot:xrange'], ['jdaviz:sidebar_plot', 'jdaviz:sidebar_export'] ] @@ -88,6 +93,120 @@ def __init__(self, *args, **kwargs): "spectrum_2d_viewer_reference_name", "spectrum-2d-viewer" ) + self.session.hub.subscribe(self, AddDataToViewerMessage, + handler=self._on_viewer_data_changed) + self.session.hub.subscribe(self, RemoveDataFromViewerMessage, + handler=self._on_viewer_data_changed) + + for k in ('x_min', 'x_max'): + self.state.add_callback(k, self._handle_x_axis_orientation) + + @cached_property + def reference_spectral_axis(self): + return self.state.reference_data.get_object().spectral_axis.value + + @cached_property + def pixel_to_world_interp(self): + pixels = range(len(self.reference_spectral_axis)) + return interp1d(pixels, self.reference_spectral_axis) + + def pixel_to_world_limits(self, limits): + if not len(limits) == 2: + raise ValueError("limits must be length 2") + + pixels = np.arange(0, len(self.reference_spectral_axis)) + + # we'll use interpolation when possible, but also want to fit a line between + # the outermost edge of the data within the limits + line_edges_pix = np.array([max((min(pixels), min(limits))), + min((max(pixels), max(limits)))]) + if line_edges_pix[0] > line_edges_pix[1]: + # then the limits are entirely out of range, so use the whole range + # when fitting the linear approximation + line_edges_pix = np.array([min(pixels), max(pixels)]) + line_edges_world = self.pixel_to_world_interp(line_edges_pix) + line_coeffs = np.polyfit(line_edges_pix, line_edges_world, deg=1) + + def pixel_to_world_line(pixel): + return line_coeffs[0] * pixel + line_coeffs[1] + + def map_pixel_to_world(pixel): + if pixels[0] <= pixel <= pixels[-1]: + # interpolate directly + return float(self.pixel_to_world_interp(pixel)) + else: + # use the line model to extrapolate + return pixel_to_world_line(pixel) + + invert = (-1) ** sum((self.inverted_x_axis, limits[0] > limits[1])) + out_lims = list(map(map_pixel_to_world, limits))[::invert] + + return out_lims + + @cached_property + def world_to_pixel_interp(self): + pixels = range(len(self.reference_spectral_axis)) + return interp1d(self.reference_spectral_axis, pixels) + + def world_to_pixel_limits(self, limits): + if not len(limits) == 2: + raise ValueError("limits must be length 2") + + # we'll use interpolation when possible, but also want to fit a line between + # the outermost edge of the data within the limits + line_edges_world = np.array([max((min(self.reference_spectral_axis), min(limits))), + min((max(self.reference_spectral_axis), max(limits)))]) + if line_edges_world[0] > line_edges_world[1]: + # then the limits are entirely out of range, so use the whole range + # when fitting the linear approximation + line_edges_world = np.array([min(self.reference_spectral_axis), + max(self.reference_spectral_axis)]) + line_edges_pixels = self.world_to_pixel_interp(line_edges_world) + line_coeffs = np.polyfit(line_edges_world, line_edges_pixels, deg=1) + + def world_to_pixel_line(world): + return line_coeffs[0] * world + line_coeffs[1] + + def map_world_to_pixel(world): + if min(self.reference_spectral_axis) <= world <= max(self.reference_spectral_axis): + # interpolate directly + return float(self.world_to_pixel_interp(world)) + else: + # use the line model to extrapolate + return world_to_pixel_line(world) + + invert = (-1) ** sum((self.inverted_x_axis, limits[0] > limits[1])) + out_lims = list(map(map_world_to_pixel, limits))[::invert] + + return out_lims + + def _on_viewer_data_changed(self, msg): + if msg.viewer_reference != self.reference: + return + # clear cached properties that are based on reference data - this is probably + # overly-conservative and we might be able to limit the clearing for only when + # reference data is changed (perhaps with a callback on the state for reference_data) + for attr in ('reference_spectral_axis', 'inverted_x_axis', + 'pixel_to_world_interp', 'world_to_pixel_interp'): + if attr in self.__dict__: + del self.__dict__[attr] + if len(self.data()): + self._handle_x_axis_orientation() + + @cached_property + def inverted_x_axis(self): + return self.reference_spectral_axis[0] > self.reference_spectral_axis[-1] + + def _handle_x_axis_orientation(self, *args): + x_scales = self.scales['x'] + limits = [x_scales.min, x_scales.max] + limits_inverted = limits[0] > limits[1] + if limits_inverted == self.inverted_x_axis: + return + with x_scales.hold_sync(): + x_scales.min = max(limits) if self.inverted_x_axis else min(limits) + x_scales.max = min(limits) if self.inverted_x_axis else max(limits) + def data(self, cls=None): return [layer_state.layer.get_object(cls=cls or self.default_class) for layer_state in self.state.layers @@ -95,7 +214,7 @@ def data(self, cls=None): isinstance(layer_state.layer, BaseData)] def set_plot_axes(self): - self.figure.axes[0].visible = False + self.figure.axes[0].label = "x: pixels" self.figure.axes[1].label = "y: pixels" self.figure.axes[1].tick_format = None @@ -105,6 +224,19 @@ def set_plot_axes(self): self.figure.axes[1].label_offset = "-50" +@viewer_registry("mosviz-profile-viewer", label="Profile 1D") +class MosvizProfileView(SpecvizProfileView): + # categories: zoom resets, zoom, pan, subset, select tools, shortcuts + tools_nested = [ + ['mosviz:homezoom'], + ['mosviz:boxzoom', 'mosviz:xrangezoom', 'jdaviz:yrangezoom'], # noqa + ['mosviz:panzoom', 'mosviz:panzoom_x', 'jdaviz:panzoom_y'], # noqa + ['bqplot:xrange'], + ['jdaviz:selectline'], + ['jdaviz:sidebar_plot', 'jdaviz:sidebar_export'] + ] + + @viewer_registry("mosviz-table-viewer", label="Table (Mosviz)") class MosvizTableViewer(TableViewer, JdavizViewerMixin): def __init__(self, session, *args, **kwargs): diff --git a/jdaviz/configs/mosviz/tests/test_helper.py b/jdaviz/configs/mosviz/tests/test_helper.py index 60a2a7ecdb..351300588b 100644 --- a/jdaviz/configs/mosviz/tests/test_helper.py +++ b/jdaviz/configs/mosviz/tests/test_helper.py @@ -7,26 +7,6 @@ from jdaviz.configs.specviz2d.helper import Specviz2d -def test_viewer_axis_link(mosviz_helper, mos_spectrum1d, mos_spectrum2d): - label1d = "Test 1D Spectrum" - mosviz_helper.load_1d_spectra(mos_spectrum1d, data_labels=label1d) - - label2d = "Test 2D Spectrum" - mosviz_helper.load_2d_spectra(mos_spectrum2d, data_labels=label2d, add_redshift_column=True) - - table = mosviz_helper.app.get_viewer('table-viewer') - table.widget_table.vue_on_row_clicked(0) - - scale_2d = mosviz_helper.app.get_viewer('spectrum-2d-viewer').scales['x'] - scale_1d = mosviz_helper.app.get_viewer('spectrum-viewer').scales['x'] - - scale_2d.min = 200.0 - assert scale_1d.min == mos_spectrum1d.spectral_axis.value[200] - - scale_1d.max = 7564 - assert scale_2d.max == 800.0 - - def test_to_csv(tmp_path, mosviz_helper, spectrum_collection): labels = [f"Test Spectrum Collection {i}" for i in range(5)] mosviz_helper.load_1d_spectra(spectrum_collection, data_labels=labels, add_redshift_column=True) diff --git a/jdaviz/configs/mosviz/tests/test_tools.py b/jdaviz/configs/mosviz/tests/test_tools.py new file mode 100644 index 0000000000..de639d3e45 --- /dev/null +++ b/jdaviz/configs/mosviz/tests/test_tools.py @@ -0,0 +1,41 @@ +from numpy.testing import assert_allclose + + +def test_viewer_axis_link(mosviz_helper, mos_spectrum1d, mos_spectrum2d): + label1d = "Test 1D Spectrum" + mosviz_helper.load_1d_spectra(mos_spectrum1d, data_labels=label1d) + + label2d = "Test 2D Spectrum" + mosviz_helper.load_2d_spectra(mos_spectrum2d, data_labels=label2d, add_redshift_column=True) + + table = mosviz_helper.app.get_viewer('table-viewer') + table.select_row(0) + + s2dv = mosviz_helper.app.get_viewer('spectrum-2d-viewer') + sv = mosviz_helper.app.get_viewer('spectrum-viewer') + + def _viewer_limits(v): + return (v.state.x_min, v.state.x_max, v.state.y_min, v.state.y_max) + + t = sv.toolbar.tools['mosviz:boxzoom'] + s2dv_orig_limits = _viewer_limits(s2dv) + assert_allclose(s2dv_orig_limits, (-0.5, 14.5, -0.5, 1023.5)) + + sv_orig_limits = _viewer_limits(sv) + assert_allclose(sv_orig_limits, (1e-06, 1.4999999999999999e-05, -1.9132802166577978, 1.5792128295073915)) # noqa + + t.activate() + # changes to sv should map to s2dv + sv.state.x_min = 1e-5 + assert_allclose(_viewer_limits(sv), (1e-05, 1.4999999999999999e-05, -1.9132802166577978, 1.5792128295073915)) # noqa + # shift in x_max caused by original padding + assert_allclose(_viewer_limits(s2dv), (9.000000000000002, 14.0, -0.5, 1023.5)) + + t2 = s2dv.toolbar.tools['mosviz:panzoom'] + t2.activate() + # should have deactivated the tool in the spectrum-viewer + assert sv.toolbar.active_tool_id is None + # and now changes to s2dv should map to sv + s2dv.state.x_min = -0.5 + assert_allclose(_viewer_limits(s2dv), (-0.5, 14.0, -0.5, 1023.5)) + assert_allclose(_viewer_limits(sv), (5.000000000000006e-07, 1.4999999999999999e-05, -1.9132802166577978, 1.5792128295073915)) # noqa diff --git a/jdaviz/configs/specviz2d/helper.py b/jdaviz/configs/specviz2d/helper.py index c41be5df54..74bf898251 100644 --- a/jdaviz/configs/specviz2d/helper.py +++ b/jdaviz/configs/specviz2d/helper.py @@ -1,5 +1,3 @@ -import numpy as np - from jdaviz.configs.specviz import Specviz from jdaviz.core.helpers import ConfigHelper from jdaviz.core.events import SnackbarMessage @@ -20,12 +18,6 @@ class Specviz2d(ConfigHelper, LineListMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - spec1d = self.app.get_viewer(self._default_spectrum_viewer_reference_name) - spec1d.scales['x'].observe(self._update_spec2d_x_axis) - - spec2d = self.app.get_viewer(self._default_spectrum_2d_viewer_reference_name) - spec2d.scales['x'].observe(self._update_spec1d_x_axis) - @property def specviz(self): """ @@ -36,84 +28,6 @@ def specviz(self): self._specviz = Specviz(app=self.app) return self._specviz - def _extend_world(self, spec1d, ext): - # Extend 1D spectrum world axis to enable panning (within reason) past - # the bounds of data - world = self.app.data_collection[spec1d]["World 0"].copy() - dw = world[1]-world[0] - prepend = np.linspace(world[0]-dw*ext, world[0]-dw, ext) - dw = world[-1]-world[-2] - append = np.linspace(world[-1]+dw, world[-1]+dw*ext, ext) - world = np.hstack((prepend, world, append)) - return world - - def _update_spec2d_x_axis(self, change): - # This assumes the two spectrum viewers have the same x-axis shape and - # wavelength solution, which should always hold - if change['old'] is None: - pass - else: - name = change['name'] - if name not in ['min', 'max']: - return - new_val = change['new'] - spec1d = self.app.get_viewer( - self._default_spectrum_viewer_reference_name - ).state.reference_data.label - extend_by = int(self.app.data_collection[spec1d]["World 0"].shape[0]) - world = self._extend_world(spec1d, extend_by) - - # Warn the user if they've panned far enough away from the data - # that the viewers will desync - if new_val > world[-1] or new_val < world[0]: - self.app.hub.broadcast(SnackbarMessage( - "Panning too far away from the data may desync the 1D and 2D spectrum viewers.", - color='warning', sender=self)) - - idx = float((np.abs(world - new_val)).argmin()) - extend_by - scales = self.app.get_viewer(self._default_spectrum_2d_viewer_reference_name).scales - old_idx = getattr(scales['x'], name) - if idx != old_idx: - setattr(scales['x'], name, idx) - - def _update_spec1d_x_axis(self, change): - if self.app.get_viewer( - self._default_spectrum_viewer_reference_name - ).state.reference_data is None: - return - - # This assumes the two spectrum viewers have the same x-axis shape and - # wavelength solution, which should always hold - if change['old'] is None: - pass - else: - name = change['name'] - if name not in ['min', 'max']: - return - new_idx = int(np.around(change['new'])) - spec1d = self.app.get_viewer( - self._default_spectrum_viewer_reference_name - ).state.reference_data.label - extend_by = int(self.app.data_collection[spec1d]["World 0"].shape[0]) - world = self._extend_world(spec1d, extend_by) - - scales = self.app.get_viewer( - self._default_spectrum_viewer_reference_name - ).scales - old_val = getattr(scales['x'], name) - - # Warn the user if they've panned far enough away from the data - # that the viewers will desync - try: - val = world[new_idx+extend_by] - except IndexError: - val = old_val - self.app.hub.broadcast(SnackbarMessage( - "Panning too far away from the data may desync the 1D and 2D spectrum viewers.", - color='warning', sender=self)) - if val != old_val: - setattr(scales['x'], name, val) - def load_data(self, spectrum_2d=None, spectrum_1d=None, spectrum_1d_label=None, spectrum_2d_label=None, show_in_viewer=True, ext=1, transpose=False): diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py index 280e2c960d..4f2068604a 100644 --- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py @@ -15,7 +15,6 @@ from astropy.modeling import models from astropy.nddata import StdDevUncertainty, VarianceUncertainty, UnknownUncertainty -from astropy import units from specreduce import tracing from specreduce import background from specreduce import extract @@ -829,10 +828,6 @@ def export_bg_spectrum(self, add_data=False, **kwargs): if add_data: self.bg_spec_add_results.add_results_from_plugin(spec, replace=False) - # TEMPORARY: override spectral axis to be in pixels until properly supporting plotting - # in wavelength/frequency - spec._spectral_axis = np.arange(len(spec.spectral_axis)) * units.pix - return spec def vue_create_bg_spec(self, *args): @@ -924,10 +919,6 @@ def export_extract_spectrum(self, add_data=False, **kwargs): extract = self.export_extract(**kwargs) spectrum = extract.spectrum - # TEMPORARY: override spectral axis to be in pixels until properly supporting plotting - # in wavelength/frequency - spectrum._spectral_axis = np.arange(len(spectrum.spectral_axis)) * units.pix - if add_data: self.ext_add_results.add_results_from_plugin(spectrum, replace=False) diff --git a/jdaviz/configs/specviz2d/specviz2d.yaml b/jdaviz/configs/specviz2d/specviz2d.yaml index 6126211888..9822653865 100644 --- a/jdaviz/configs/specviz2d/specviz2d.yaml +++ b/jdaviz/configs/specviz2d/specviz2d.yaml @@ -35,5 +35,5 @@ viewer_area: - container: row viewers: - name: Spectrum - plot: specviz-profile-viewer + plot: mosviz-profile-viewer reference: spectrum-viewer diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py index 78a4096068..cf7140975f 100644 --- a/jdaviz/configs/specviz2d/tests/test_parsers.py +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -79,9 +79,9 @@ def test_2d_parser_no_unit(specviz2d_helper, mos_spectrum2d): # need to trigger a mouseleave or mouseover to reset the traitlets label_mouseover._viewer_mouse_event(viewer_1d, {'event': 'mouseenter'}) label_mouseover._viewer_mouse_event(viewer_1d, - {'event': 'mousemove', 'domain': {'x': 6.5, 'y': 3}}) - assert label_mouseover.as_text() == ('Cursor 6.50000e+00, 3.00000e+00', - 'Wave 6.00000e+00 pixel', + {'event': 'mousemove', 'domain': {'x': 7.2e-6, 'y': 3}}) + assert label_mouseover.as_text() == ('Cursor 7.20000e-06, 3.00000e+00', + 'Wave 7.00000e-06 m (6 pix)', 'Flux -3.59571e+00') assert label_mouseover.icon == 'b' diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index e241c31549..d56ecf77c0 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -218,7 +218,7 @@ def spectrum1d_cube_custom_fluxunit(): @pytest.fixture -def mos_spectrum1d(): +def mos_spectrum1d(mos_spectrum2d): ''' A specially defined Spectrum1d that matches the corresponding spectrum2d below. @@ -228,7 +228,7 @@ def mos_spectrum1d(): Unless linking the two is required, try to use the global spectrum1d fixture. ''' - spec_axis = np.linspace(6000, 8000, 1024) * u.AA + spec_axis = mos_spectrum2d.spectral_axis np.random.seed(42) flux = (np.random.randn(len(spec_axis.value)) + 10*np.exp(-0.001*(spec_axis.value-6563)**2) + diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index a8bb410257..c3b63a27f4 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -1065,13 +1065,11 @@ def _update_has_subregions(self): def selected_obj(self): if self.selected in self.manual_options or self.selected not in self.labels: return None - subset_type = self.selected_item['type'] # NOTE: we use reference names here instead of IDs since get_subsets_from_viewer requires # that. For imviz, this will mean we won't be able to loop through each of the viewers, # but the original viewer should have access to all the subsets. for viewer_ref in self.viewer_refs: - match = self.app.get_subsets_from_viewer(viewer_ref, - subset_type=subset_type).get(self.selected) + match = self.app.get_subsets(self.selected) if match is not None: return match diff --git a/jdaviz/core/tools.py b/jdaviz/core/tools.py index 30ed7394ea..f86618d77e 100644 --- a/jdaviz/core/tools.py +++ b/jdaviz/core/tools.py @@ -37,6 +37,84 @@ def save_prev_zoom(self): self.viewer.state.y_min, self.viewer.state.y_max) +class _MatchedZoomMixin: + match_axes = ('x', 'y') + disable_matched_zoom_in_other_viewer = False + + def _is_matched_viewer(self, viewer): + return True + + def _iter_matched_viewers(self, include_self=False): + for viewer in self.viewer.session.application.viewers: + if viewer is self.viewer and not include_self: + continue + elif self._is_matched_viewer(viewer): + yield viewer + + def _map_limits(self, from_viewer, to_viewer, limits={}): + return limits + + def _post_activate(self): + return + + @property + def match_keys(self): + keys = [] + for ax in self.match_axes: + keys += [f'{ax}_min', f'{ax}_max'] + return keys + + def activate(self): + if self.disable_matched_zoom_in_other_viewer: + # mapping limits are not guaranteed to roundtrip, so we need to disable + # any linked tool in the "other" viewer + for viewer in self._iter_matched_viewers(include_self=False): + if isinstance(viewer.toolbar.active_tool, _MatchedZoomMixin): + viewer.toolbar.active_tool_id = None + + super().activate() + for k in self.match_keys: + self.viewer.state.add_callback(k, self.on_limits_change) + + self._post_activate() + + # Trigger a sync so the initial limits match + self.on_limits_change() + + def deactivate(self): + for k in self.match_keys: + self.viewer.state.remove_callback(k, self.on_limits_change) + + super().deactivate() + + def on_limits_change(self, *args): + # from_lims: limits in the viewer belonging to the tool + from_lims = {k: getattr(self.viewer.state, k) for k in self.match_keys} + + for viewer in self._iter_matched_viewers(include_self=False): + # orig_lims: limits in this "matched" viewer + # to_lims: proposed new limits for this "matched" viewer + orig_lims = {k: getattr(viewer.state, k) for k in self.match_keys} + to_lims = self._map_limits(self.viewer, viewer, from_lims) + with delay_callback(viewer.state, *self.match_keys): + for ax in self.match_axes: + # to avoid recursion we'll only update the state if there is a change + # outside a tolerance set by some fraction of the limits range + if None in orig_lims.values(): + orig_range = np.inf + else: + orig_range = abs(orig_lims.get(f'{ax}_max') - orig_lims.get(f'{ax}_min')) + to_range = abs(to_lims.get(f'{ax}_max') - to_lims.get(f'{ax}_min')) + tol = 1e-6 * min(orig_range, to_range) + + for k in (f'{ax}_min', f'{ax}_max'): + value = to_lims.get(k) + orig_value = orig_lims.get(k) + if not np.isnan(value) and (orig_value is None or + abs(value-orig_lims.get(k, np.inf)) > tol): + setattr(viewer.state, k, value) + + @viewer_tool class PrevZoom(Tool, _BaseZoomHistory): icon = os.path.join(ICON_DIR, 'zoom_back.svg') diff --git a/jdaviz/data/icons/home_match.svg b/jdaviz/data/icons/home_match.svg new file mode 100644 index 0000000000..5a68900c4f --- /dev/null +++ b/jdaviz/data/icons/home_match.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/jdaviz/data/icons/pan_x_match.svg b/jdaviz/data/icons/pan_x_match.svg new file mode 100644 index 0000000000..504a38a11c --- /dev/null +++ b/jdaviz/data/icons/pan_x_match.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/jdaviz/data/icons/pan_y_match.svg b/jdaviz/data/icons/pan_y_match.svg new file mode 100644 index 0000000000..050181032d --- /dev/null +++ b/jdaviz/data/icons/pan_y_match.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/jdaviz/data/icons/zoom_xrange.svg b/jdaviz/data/icons/zoom_xrange.svg index a15ef9a285..a289dc15bd 100644 --- a/jdaviz/data/icons/zoom_xrange.svg +++ b/jdaviz/data/icons/zoom_xrange.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + diff --git a/jdaviz/data/icons/zoom_xrange_match.svg b/jdaviz/data/icons/zoom_xrange_match.svg new file mode 100644 index 0000000000..743b468378 --- /dev/null +++ b/jdaviz/data/icons/zoom_xrange_match.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/jdaviz/data/icons/zoom_yrange.svg b/jdaviz/data/icons/zoom_yrange.svg index 39faa57238..75aab0edb9 100644 --- a/jdaviz/data/icons/zoom_yrange.svg +++ b/jdaviz/data/icons/zoom_yrange.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + diff --git a/jdaviz/data/icons/zoom_yrange_match.svg b/jdaviz/data/icons/zoom_yrange_match.svg new file mode 100644 index 0000000000..ea4d3e4c81 --- /dev/null +++ b/jdaviz/data/icons/zoom_yrange_match.svg @@ -0,0 +1,18 @@ + + + + + + + + + From 40ca0c9a64ee4a2a5b04b97d206f8022bf270d6d Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 14 Jun 2023 11:52:40 -0400 Subject: [PATCH 104/283] re-add stdatamodels to devdeps --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 73ddbd9c90..c101fc241b 100644 --- a/tox.ini +++ b/tox.ini @@ -50,8 +50,7 @@ deps = devdeps: git+https://github.com/spacetelescope/gwcs.git devdeps: git+https://github.com/asdf-format/asdf.git devdeps: git+https://github.com/astropy/asdf-astropy.git - # FIXME: https://github.com/spacetelescope/stdatamodels/issues/159 - #devdeps: git+https://github.com/spacetelescope/stdatamodels.git + devdeps: git+https://github.com/spacetelescope/stdatamodels.git devdeps: git+https://github.com/bqplot/bqplot.git@0.12.x devdeps: git+https://github.com/glue-viz/glue.git devdeps: git+https://github.com/voila-dashboards/voila.git From 28d8aab35d5e861b10cde3b2a956718d59a59b74 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 14 Jun 2023 13:17:40 -0400 Subject: [PATCH 105/283] temporary subset plugin updates to handle Time in lcviz * update button does not work (it is setting the value but not roundtripping) * simplify button does not show --- .../plugins/subset_plugin/subset_plugin.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 4a505c320f..9b89f2a3ca 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -2,6 +2,7 @@ import numpy as np +from astropy.time import Time import astropy.units as u from glue.core.message import EditSubsetMessage, SubsetUpdateMessage from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode, @@ -192,12 +193,21 @@ def _unpack_get_subsets_for_ui(self): subset_type = subset_state.roi.__class__.__name__ elif isinstance(subset_state, RangeSubsetState): - lo = spec['region'].lower - hi = spec['region'].upper - subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo.value, - "orig": lo.value, "unit": str(lo.unit)}, - {"name": "Upper bound", "att": "hi", "value": hi.value, - "orig": hi.value, "unit": str(hi.unit)}] + region = spec['region'] + if isinstance(region, Time): + lo = region.min() + hi = region.max() + subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo.value, + "orig": lo.value}, + {"name": "Upper bound", "att": "hi", "value": hi.value, + "orig": hi.value}] + else: + lo = region.lower + hi = region.upper + subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo.value, + "orig": lo.value, "unit": str(lo.unit)}, + {"name": "Upper bound", "att": "hi", "value": hi.value, + "orig": hi.value, "unit": str(hi.unit)}] subset_type = "Range" if len(subset_definition) > 0: # Note: .append() does not work for List traitlet. From 8790525bcc31582b4828c4b0dfb50200621e3da6 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 14 Jun 2023 16:17:14 -0400 Subject: [PATCH 106/283] Plugin plot subcomponent (#2254) * plugin plot subcomponent (similar to table) * update tests --------- Co-authored-by: Mario Buikhuizen --- CHANGES.rst | 2 + jdaviz/components/plugin_plot.vue | 37 ++++++++ .../aper_phot_simple/aper_phot_simple.py | 68 ++++++-------- .../aper_phot_simple/aper_phot_simple.vue | 5 +- .../line_profile_xy/line_profile_xy.py | 72 ++++++-------- .../line_profile_xy/line_profile_xy.vue | 7 +- .../imviz/tests/test_line_profile_xy.py | 20 ++-- .../imviz/tests/test_simple_aper_phot.py | 4 +- jdaviz/core/template_mixin.py | 93 +++++++++++++++++++ 9 files changed, 203 insertions(+), 105 deletions(-) create mode 100644 jdaviz/components/plugin_plot.vue diff --git a/CHANGES.rst b/CHANGES.rst index 05293eb7e2..98c9b63545 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ New Features - Add Simplify button to subset plugin to make composite spectral subsets more user friendly. [#2237] +- Plots within plugins can now be popped-out into their own windows. [#2254] + Cubeviz ^^^^^^^ diff --git a/jdaviz/components/plugin_plot.vue b/jdaviz/components/plugin_plot.vue new file mode 100644 index 0000000000..cdc9ac7ec7 --- /dev/null +++ b/jdaviz/components/plugin_plot.vue @@ -0,0 +1,37 @@ + + + + + + diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index 9616567df4..5091df49d5 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -3,7 +3,6 @@ from datetime import datetime import astropy -import bqplot import numpy as np from astropy import units as u from astropy.modeling.fitting import LevMarLSQFitter @@ -24,14 +23,14 @@ from jdaviz.core.region_translators import regions2aperture, _get_region_from_spatial_subset from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetSelectMixin, - SubsetSelect, TableMixin) -from jdaviz.utils import bqplot_clear_figure, PRIHDR_KEY + SubsetSelect, TableMixin, PlotMixin) +from jdaviz.utils import PRIHDR_KEY __all__ = ['SimpleAperturePhotometry'] @tray_registry('imviz-aper-phot-simple', label="Imviz Simple Aperture Photometry") -class SimpleAperturePhotometry(PluginTemplateMixin, DatasetSelectMixin, TableMixin): +class SimpleAperturePhotometry(PluginTemplateMixin, DatasetSelectMixin, TableMixin, PlotMixin): template_file = __file__, "aper_phot_simple.vue" subset_items = List([]).tag(sync=True) subset_selected = Unicode("").tag(sync=True) @@ -83,11 +82,14 @@ def __init__(self, *args, **kwargs): self._selected_data = None self._selected_subset = None - self._fig = bqplot.Figure() self.plot_types = ["Curve of Growth", "Radial Profile", "Radial Profile (Raw)"] self.current_plot_type = self.plot_types[0] self._fitted_model_name = 'phot_radial_profile' + self.plot.add_line('line', color='gray', marker_size=32) + self.plot.add_scatter('scatter', color='gray', default_size=1) + self.plot.add_line('fit_line', color='magenta', line_style='dashed') + self.session.hub.subscribe(self, SubsetUpdateMessage, handler=self._on_subset_update) self.session.hub.subscribe(self, LinkUpdatedMessage, handler=self._on_link_update) @@ -345,48 +347,40 @@ def vue_do_aper_phot(self, *args, **kwargs): self.table.add_item(phot_table) # Plots. - # TODO: Jenn wants title at bottom. - bqplot_clear_figure(self._fig) - self._fig.title_style = {'font-size': '12px'} - # NOTE: default margin in bqplot is 60 in all directions - self._fig.fig_margin = {'top': 60, 'bottom': 60, 'left': 40, 'right': 10} - line_x_sc = bqplot.LinearScale() - line_y_sc = bqplot.LinearScale() + line = self.plot.marks['line'] + sc = self.plot.marks['scatter'] + fit_line = self.plot.marks['fit_line'] if self.current_plot_type == "Curve of Growth": - self._fig.title = 'Curve of growth from aperture center' + self.plot.figure.title = 'Curve of growth from aperture center' x_arr, sum_arr, x_label, y_label = _curve_of_growth( comp_data, (xcenter, ycenter), aperture, phot_table['sum'][0], wcs=data.coords, background=bg, pixarea_fac=pixarea_fac) - self._fig.axes = [bqplot.Axis(scale=line_x_sc, label=x_label), - bqplot.Axis(scale=line_y_sc, orientation='vertical', - label=y_label)] - bqplot_line = bqplot.Lines(x=x_arr, y=sum_arr, marker='circle', - scales={'x': line_x_sc, 'y': line_y_sc}, - marker_size=32, colors='gray') - bqplot_marks = [bqplot_line] + line.x, line.y = x_arr, sum_arr + self.plot.clear_marks('scatter', 'fit_line') + self.plot.figure.axes[0].label = x_label + self.plot.figure.axes[1].label = y_label else: # Radial profile - self._fig.axes = [bqplot.Axis(scale=line_x_sc, label='pix'), - bqplot.Axis(scale=line_y_sc, orientation='vertical', - label=comp.units or 'Value')] + self.plot.figure.axes[0].label = 'pix' + self.plot.figure.axes[1].label = comp.units or 'Value' if self.current_plot_type == "Radial Profile": - self._fig.title = 'Radial profile from aperture center' + self.plot.figure.title = 'Radial profile from aperture center' x_data, y_data = _radial_profile( phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter), raw=False) - bqplot_line = bqplot.Lines(x=x_data, y=y_data, marker='circle', - scales={'x': line_x_sc, 'y': line_y_sc}, - marker_size=32, colors='gray') + line.x, line.y = x_data, y_data + self.plot.clear_marks('scatter') + else: # Radial Profile (Raw) - self._fig.title = 'Raw radial profile from aperture center' + self.plot.figure.title = 'Raw radial profile from aperture center' x_data, y_data = _radial_profile( phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter), raw=True) - bqplot_line = bqplot.Scatter(x=x_data, y=y_data, marker='circle', - scales={'x': line_x_sc, 'y': line_y_sc}, - default_size=1, colors='gray') + + sc.x, sc.y = x_data, y_data + self.plot.clear_marks('line') # Fit Gaussian1D to radial profile data. if self.fit_radial_profile: @@ -412,21 +406,17 @@ def vue_do_aper_phot(self, *args, **kwargs): f"Radial profile fitting: {msg}", color='warning', sender=self)) y_fit = fit_model(x_data) self.app.fitted_models[self._fitted_model_name] = fit_model - bqplot_fit = bqplot.Lines(x=x_data, y=y_fit, marker=None, - scales={'x': line_x_sc, 'y': line_y_sc}, - colors='magenta', line_style='dashed') - bqplot_marks = [bqplot_line, bqplot_fit] + fit_line.x, fit_line.y = x_data, y_fit else: - bqplot_marks = [bqplot_line] + self.plot.clear_marks('fit_line') except Exception as e: # pragma: no cover - bqplot_clear_figure(self._fig) + self.plot.clear_all_marks() msg = f"Aperture photometry failed: {repr(e)}" self.hub.broadcast(SnackbarMessage(msg, color='error', sender=self)) self.result_failed_msg = msg else: self.result_failed_msg = '' - self._fig.marks = bqplot_marks # Parse results for GUI. tmp = [] @@ -464,8 +454,6 @@ def vue_do_aper_phot(self, *args, **kwargs): self.results = tmp self.fit_results = fit_tmp self.result_available = True - self.radial_plot = self._fig - self.bqplot_figs_resize = [self._fig] self.plot_available = True diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue index 690ef28cd2..f4b0ca21f0 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue @@ -146,10 +146,7 @@ - - +
diff --git a/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py b/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py index 1360b26070..5e65c2f162 100644 --- a/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py +++ b/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py @@ -1,12 +1,9 @@ -import bqplot -from ipywidgets import widget_serialization from traitlets import Any, Bool, List, Unicode, observe from jdaviz.configs.imviz.helper import get_top_layer_index from jdaviz.core.events import ViewerAddedMessage, ViewerRemovedMessage from jdaviz.core.registries import tray_registry -from jdaviz.core.template_mixin import PluginTemplateMixin -from jdaviz.utils import bqplot_clear_figure +from jdaviz.core.template_mixin import PluginTemplateMixin, Plot __all__ = ['LineProfileXY'] @@ -19,17 +16,27 @@ class LineProfileXY(PluginTemplateMixin): plot_available = Bool(False).tag(sync=True) selected_x = Any('').tag(sync=True) selected_y = Any('').tag(sync=True) - line_plot_across_x = Any('').tag(sync=True, **widget_serialization) - line_plot_across_y = Any('').tag(sync=True, **widget_serialization) + + plot_across_x_widget = Unicode().tag(sync=True) + plot_across_y_widget = Unicode().tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._default_viewer = f'{self.app.config}-0' + self.plot_across_x = Plot(self) + self.plot_across_x.add_line('line', color='gray') + self.plot_across_x.figure.axes[0].label = 'Y (pix)' + self.plot_across_x_widget = 'IPY_MODEL_'+self.plot_across_x.model_id + + self.plot_across_y = Plot(self) + self.plot_across_y.add_line('line', color='gray') + self.plot_across_y.figure.axes[0].label = 'X (pix)' + self.plot_across_y_widget = 'IPY_MODEL_'+self.plot_across_y.model_id + self.hub.subscribe(self, ViewerAddedMessage, handler=self._on_viewers_changed) self.hub.subscribe(self, ViewerRemovedMessage, handler=self._on_viewers_changed) - self._figs = [bqplot.Figure(), bqplot.Figure()] self._on_viewers_changed() # Populate it on start-up def _on_viewers_changed(self, msg=None): @@ -41,10 +48,8 @@ def _on_viewers_changed(self, msg=None): def reset_results(self): self.plot_available = False - self.line_plot_across_x = '' - self.line_plot_across_y = '' - for fig in self._figs: - bqplot_clear_figure(fig) + self.plot_across_x.clear_all_marks() + self.plot_across_y.clear_all_marks() # This is also triggered from viewer code. @observe("selected_viewer") @@ -81,24 +86,12 @@ def vue_draw_plot(self, *args, **kwargs): else: y_label = 'Value' - # Clear bqplot figures. - fig_x = self._figs[0] - bqplot_clear_figure(fig_x) - fig_y = self._figs[1] - bqplot_clear_figure(fig_y) - - fig_x.title = f'X={x}' - fig_x.title_style = {'font-size': '12px'} - fig_x.fig_margin = {'top': 60, 'bottom': 60, 'left': 40, 'right': 10} - line_x_x_sc = bqplot.LinearScale() - line_x_y_sc = bqplot.LinearScale() - line_x = bqplot.Lines(x=range(comp.data.shape[0]), y=comp.data[:, x], - scales={'x': line_x_x_sc, 'y': line_x_y_sc}, colors='gray') - fig_x.marks = [line_x] - fig_x.axes = [bqplot.Axis(scale=line_x_x_sc, label='Y (pix)'), - bqplot.Axis(scale=line_x_y_sc, orientation='vertical', label=y_label)] - line_x.scales['x'].min = y_min - line_x.scales['x'].max = y_max + self.plot_across_x.figure.title = f'X={x}' + line_x = self.plot_across_x.marks['line'] + line_x.x, line_x.y = range(comp.data.shape[0]), comp.data[:, x] + line_x.scales['x'].min, line_x.scales['x'].max = y_min, y_max + self.plot_across_x.figure.axes[1].label = y_label + y_min = max(int(y_min), 0) y_max = min(int(y_max), ny) zoomed_data_x = comp.data[y_min:y_max, x] @@ -106,18 +99,12 @@ def vue_draw_plot(self, *args, **kwargs): line_x.scales['y'].min = zoomed_data_x.min() * 0.95 line_x.scales['y'].max = zoomed_data_x.max() * 1.05 - fig_y.title = f'Y={y}' - fig_y.title_style = {'font-size': '12px'} - fig_y.fig_margin = {'top': 60, 'bottom': 60, 'left': 40, 'right': 10} - line_y_x_sc = bqplot.LinearScale() - line_y_y_sc = bqplot.LinearScale() - line_y = bqplot.Lines(x=range(comp.data.shape[1]), y=comp.data[y, :], - scales={'x': line_y_x_sc, 'y': line_y_y_sc}, colors='gray') - fig_y.marks = [line_y] - fig_y.axes = [bqplot.Axis(scale=line_y_x_sc, label='X (pix)'), - bqplot.Axis(scale=line_y_y_sc, orientation='vertical', label=y_label)] - line_y.scales['x'].min = x_min - line_y.scales['x'].max = x_max + self.plot_across_y.figure.title = f'Y={y}' + line_y = self.plot_across_y.marks['line'] + line_y.x, line_y.y = range(comp.data.shape[1]), comp.data[y, :] + line_y.scales['x'].min, line_y.scales['x'].max = x_min, x_max + self.plot_across_y.figure.axes[1].label = y_label + x_min = max(int(x_min), 0) x_max = min(int(x_max), nx) zoomed_data_y = comp.data[y, x_min:x_max] @@ -125,7 +112,4 @@ def vue_draw_plot(self, *args, **kwargs): line_y.scales['y'].min = zoomed_data_y.min() * 0.95 line_y.scales['y'].max = zoomed_data_y.max() * 1.05 - self.line_plot_across_x = fig_x - self.line_plot_across_y = fig_y - self.bqplot_figs_resize = self._figs self.plot_available = True diff --git a/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.vue b/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.vue index d6ce8cc433..6134e717c4 100644 --- a/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.vue +++ b/jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.vue @@ -43,12 +43,9 @@ - - +
- +
diff --git a/jdaviz/configs/imviz/tests/test_line_profile_xy.py b/jdaviz/configs/imviz/tests/test_line_profile_xy.py index 6760a1689b..616538fdd1 100644 --- a/jdaviz/configs/imviz/tests/test_line_profile_xy.py +++ b/jdaviz/configs/imviz/tests/test_line_profile_xy.py @@ -17,8 +17,8 @@ def test_plugin_linked_by_pixel(self): assert lp_plugin.selected_viewer == 'imviz-0' # Plot attempt with null X/Y should not crash but also no-op. - assert not lp_plugin.line_plot_across_x - assert not lp_plugin.line_plot_across_y + assert len(lp_plugin.plot_across_x.marks['line'].x) == 0 + assert len(lp_plugin.plot_across_y.marks['line'].x) == 0 lp_plugin.vue_draw_plot() assert not lp_plugin.plot_available @@ -27,8 +27,8 @@ def test_plugin_linked_by_pixel(self): {'event': 'keydown', 'key': 'l', 'domain': {'x': 5.1, 'y': 5}}) assert_allclose(lp_plugin.selected_x, 5.1) assert_allclose(lp_plugin.selected_y, 5) - assert lp_plugin.line_plot_across_x - assert lp_plugin.line_plot_across_y + assert len(lp_plugin.plot_across_x.marks['line'].x) > 0 + assert len(lp_plugin.plot_across_y.marks['line'].x) > 0 assert lp_plugin.plot_available # Add data with unit @@ -46,23 +46,23 @@ def test_plugin_linked_by_pixel(self): assert lp_plugin.selected_viewer == 'imviz-1' assert_allclose(lp_plugin.selected_x, 5.1) assert_allclose(lp_plugin.selected_y, 5) - assert lp_plugin.line_plot_across_x - assert lp_plugin.line_plot_across_y + assert len(lp_plugin.plot_across_x.marks['line'].x) > 0 + assert len(lp_plugin.plot_across_y.marks['line'].x) > 0 assert lp_plugin.plot_available # Wrong input resets plots without error. lp_plugin.selected_x = -1 lp_plugin.vue_draw_plot() - assert not lp_plugin.line_plot_across_x - assert not lp_plugin.line_plot_across_y + assert len(lp_plugin.plot_across_x.marks['line'].x) == 0 + assert len(lp_plugin.plot_across_y.marks['line'].x) == 0 assert not lp_plugin.plot_available # Mimic manual GUI inputs. lp_plugin.selected_x = '1.1' lp_plugin.selected_y = '9' lp_plugin.selected_viewer = 'imviz-0' - assert lp_plugin.line_plot_across_x - assert lp_plugin.line_plot_across_y + assert len(lp_plugin.plot_across_x.marks['line'].x) > 0 + assert len(lp_plugin.plot_across_y.marks['line'].x) > 0 assert lp_plugin.plot_available # Nothing should update on "l" when plugin closed. diff --git a/jdaviz/configs/imviz/tests/test_simple_aper_phot.py b/jdaviz/configs/imviz/tests/test_simple_aper_phot.py index 906111802a..0ce5abfe3f 100644 --- a/jdaviz/configs/imviz/tests/test_simple_aper_phot.py +++ b/jdaviz/configs/imviz/tests/test_simple_aper_phot.py @@ -61,7 +61,7 @@ def test_plugin_wcs_dithered(self): tbl = self.imviz.get_aperture_photometry_results() assert len(tbl) == 2 assert phot_plugin.plot_available - assert phot_plugin.radial_plot != '' # Does not check content + assert len(phot_plugin.plot.marks['scatter'].x) > 0 # Check photometry results. assert tbl.colnames == [ @@ -168,7 +168,7 @@ def test_plugin_wcs_dithered(self): # Curve of growth phot_plugin.current_plot_type = 'Curve of Growth' phot_plugin.vue_do_aper_phot() - assert phot_plugin._fig.title == 'Curve of growth from aperture center' + assert phot_plugin.plot.figure.title == 'Curve of growth from aperture center' class TestSimpleAperPhot_NoWCS(BaseImviz_WCS_NoWCS): diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index c3b63a27f4..5bc71b4e57 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -2,6 +2,7 @@ from astropy.table import QTable from astropy.table.row import Row as QTableRow import astropy.units as u +import bqplot import numpy as np from functools import cached_property @@ -38,6 +39,7 @@ 'LayerSelect', 'LayerSelectMixin', 'DatasetSelect', 'DatasetSelectMixin', 'Table', 'TableMixin', + 'Plot', 'PlotMixin', 'AutoTextField', 'AutoTextFieldMixin', 'AddResults', 'AddResultsMixin', 'PlotOptionsSyncState', @@ -2535,3 +2537,94 @@ def export_table(self): Export the QTable representation of the table. """ return self.table.export_table() + + +class Plot(PluginSubcomponent): + """ + Plot subcomponent. For most cases where a plugin only requires a single plot, use the mixin + instead. + + To use in a plugin, define ``plugin.plot = Plot(plugin)``, create a ``plot_widget`` Unicode + traitlet, and set ``plugin.plot_widget = 'IPY_MODEL_'+self.plot.model_id``. + + To render in the plugin's vue file:: + + + + """ + template_file = __file__, "../components/plugin_plot.vue" + + figure = Any().tag(sync=True, **widget_serialization) + + def __init__(self, plugin, *args, **kwargs): + super().__init__(plugin, 'Plot', *args, **kwargs) + self.figure = bqplot.Figure() + self._marks = {} + + self.figure.axes = [bqplot.Axis(scale=bqplot.LinearScale(), label='x'), + bqplot.Axis(scale=bqplot.LinearScale(), + orientation='vertical', label='y')] + + self.figure.title_style = {'font-size': '12px'} + self.figure.fig_margin = {'top': 60, 'bottom': 60, 'left': 40, 'right': 10} + + plugin.bqplot_figs_resize += [self.figure] + + @property + def marks(self): + return self._marks + + def clear_marks(self, *mark_labels): + for mark_label, mark in self.marks.items(): + if mark_label in mark_labels: + mark.x, mark.y = [], [] + + def clear_all_marks(self): + self.clear_marks(*self.marks.keys()) + + def _add_mark(self, cls, label, **kwargs): + if label in self._marks: + raise ValueError(f"mark with label '{label}' already exists") + mark = cls(scales={'x': self.figure.axes[0].scale, + 'y': self.figure.axes[1].scale}, + **kwargs) + self.figure.marks = self.figure.marks + [mark] + self._marks[label] = mark + return mark + + def add_line(self, label, x=[], y=[], **kwargs): + return self._add_mark(bqplot.Lines, label, x=x, y=y, + colors=kwargs.pop('color', kwargs.pop('colors', 'gray')), + **kwargs) + + def add_scatter(self, label, x=[], y=[], **kwargs): + return self._add_mark(bqplot.Scatter, label, x=x, y=y, + colors=kwargs.pop('color', kwargs.pop('colors', 'gray')), + **kwargs) + + +class PlotMixin(VuetifyTemplate, HubListener): + """ + Plot subcomponent mixin. + + In addition to ``plot``, this provides the following methods at the plugin-level: + + * :meth:`clear_plot` + + To render in the plugin's vue file:: + + + + """ + plot_widget = Unicode().tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.plot = Plot(self) + self.plot_widget = 'IPY_MODEL_'+self.plot.model_id + + def clear_plot(self): + """ + Clear all data from the current plot. + """ + self.plot.clear_plot() From 1174347d10ba17298ccde00d8385b8e95b1df241 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 15 Jun 2023 10:55:35 -0400 Subject: [PATCH 107/283] Add handling of overlapping spectral regions when simplifying (#2248) * Add handling of overlapping spectral regions when simplifying Fix bug Add test to cover case with XOR, AND, ANDNOT and overlapping spectral region * Rebase and remove commented out code * Increase test coverage * Fix test * Fix code style * Add overlap tests for specviz2d viewer * Try for full test coverage --- jdaviz/app.py | 83 +++++++++++++++--- .../plugins/subset_plugin/subset_plugin.py | 3 + jdaviz/tests/test_subsets.py | 85 +++++++++++++++++++ 3 files changed, 159 insertions(+), 12 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 9dc4b38471..3b8afb2c34 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -1244,18 +1244,21 @@ def simplify_spectral_subset(self, subset_name, att, overwrite=False): Whether to update the current subset with the simplified state or apply it to a new subset. """ - spectral_region = self.get_subsets(subset_name, spectral_only=True) - new_state = None - # Reverse through sub regions so that they are added back - # in the order of lowest values to highest - for index in range(len(spectral_region) - 1, -1, -1): - convert_to_range = RangeSubsetState(spectral_region[index].lower.value, - spectral_region[index].upper.value, - att) - if new_state is None: - new_state = convert_to_range - else: - new_state = new_state | convert_to_range + if self.is_there_overlap_spectral_subset(subset_name): + new_state = self.merge_overlapping_spectral_regions(subset_name, att) + else: + new_state = None + spectral_region = self.get_subsets(subset_name, spectral_only=True) + # Reverse through sub regions so that they are added back + # in the order of lowest values to highest + for index in range(len(spectral_region) - 1, -1, -1): + convert_to_range = RangeSubsetState(spectral_region[index].lower.value, + spectral_region[index].upper.value, + att) + if new_state is None: + new_state = convert_to_range + else: + new_state = new_state | convert_to_range dc = self.data_collection if not overwrite: @@ -1265,6 +1268,62 @@ def simplify_spectral_subset(self, subset_name, att, overwrite=False): if subsets.label == subset_name][0] old_subset.subset_state = new_state + def is_there_overlap_spectral_subset(self, subset_name): + """ + Returns True if the spectral subset with subset_name has overlapping + subregions. + """ + spectral_region = self.get_subsets(subset_name, spectral_only=True) + if not spectral_region or len(spectral_region) < 2: + return False + for index in range(0, len(spectral_region) - 1): + if spectral_region[index].upper.value >= spectral_region[index + 1].lower.value: + return True + return False + + def merge_overlapping_spectral_regions(self, subset_name, att): + """ + Takes a spectral subset with subset_name and returns an ``OrState`` object + that merges all overlapping subregions. + + Parameters + ---------- + subset_name : str + Name of subset to simplify. + att : str + Attribute that the subset uses to apply to data. + """ + spectral_region = self.get_subsets(subset_name, spectral_only=True) + merged_regions = None + # Convert SpectralRegion object into a list with tuples representing + # the lower and upper values of each region. + reg_as_tup = [(sr.lower.value, sr.upper.value) for sr in spectral_region] + for index in range(0, len(spectral_region)): + # Instantiate merged regions + if not merged_regions: + merged_regions = [reg_as_tup[index]] + else: + last_merged = merged_regions[-1] + # If the lower value of the current subregion is less than or equal to the upper + # value of the last subregion added to merged_regions, update last_merged + # with the max upper value between the two regions. + if reg_as_tup[index][0] <= last_merged[1]: + last_merged = (last_merged[0], max(last_merged[1], reg_as_tup[index][1])) + merged_regions = merged_regions[:-1] + merged_regions.append(last_merged) + else: + merged_regions.append(reg_as_tup[index]) + + new_state = None + for region in reversed(merged_regions): + convert_to_range = RangeSubsetState(region[0], region[1], att) + if new_state is None: + new_state = convert_to_range + else: + new_state = new_state | convert_to_range + + return new_state + def add_data(self, data, data_label=None, notify_done=True): """ Add data to the Glue ``DataCollection``. diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 9b89f2a3ca..5bd4fb22b3 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -222,6 +222,9 @@ def _unpack_get_subsets_for_ui(self): if (len(self.subset_states) > 1 and isinstance(self.subset_states[0], RangeSubsetState) and len(simplifiable_states - set(self.glue_state_types)) < 3): self.can_simplify = True + elif (len(self.subset_states) > 1 and isinstance(self.subset_states[0], RangeSubsetState) + and self.app.is_there_overlap_spectral_subset(self.subset_selected)): + self.can_simplify = True else: self.can_simplify = False diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index f6c08ec574..7dd56bca67 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -537,6 +537,8 @@ def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): assert reg.lower.value == reg2.lower.value assert reg.upper.value == reg2.upper.value + assert subset_plugin.can_simplify + viewer.apply_roi(XRangeROI(7800, 8000)) with pytest.raises(ValueError, match="AND mode should overlap with existing subset"): specviz_helper.app.get_subsets("Subset 1") @@ -560,3 +562,86 @@ def test_edit_composite_spectral_with_xor(specviz_helper, spectrum1d): assert reg[1].lower.value == 6700 and reg[1].upper.value == 7200 assert reg[2].lower.value == 7400 and reg[2].upper.value == 7600 assert reg[3].lower.value == 7700 and reg[3].upper.value == 7800 + + +def test_overlapping_spectral_regions(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d) + viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6400, 7400)) + specviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(XRangeROI(6600, 7200)) + + specviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(6600, 7300)) + + subset_plugin = specviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.can_simplify + subset_plugin.vue_simplify_subset() + + reg = specviz_helper.app.get_subsets("Subset 1") + assert reg.lower.value == 6400 and reg.upper.value == 7400 + + +def test_only_overlapping_spectral_regions(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d) + viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6400, 6600)) + assert specviz_helper.app.is_there_overlap_spectral_subset("Subset 1") is False + specviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(7000, 7400)) + + viewer.apply_roi(XRangeROI(6600, 7300)) + + viewer.apply_roi(XRangeROI(7600, 7800)) + + subset_plugin = specviz_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.can_simplify + subset_plugin.vue_simplify_subset() + + reg = specviz_helper.app.get_subsets("Subset 1") + assert reg[0].lower.value == 6400 and reg[0].upper.value == 7400 + assert reg[1].lower.value == 7600 and reg[1].upper.value == 7800 + + +def test_overlapping_in_specviz2d(specviz2d_helper, mos_spectrum2d): + specviz2d_helper.load_data(spectrum_2d=mos_spectrum2d) + viewer = specviz2d_helper.app.get_viewer( + specviz2d_helper._default_spectrum_2d_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6400, 7400)) + specviz2d_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(XRangeROI(6600, 7200)) + + specviz2d_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(6600, 7300)) + + subset_plugin = specviz2d_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.can_simplify + subset_plugin.vue_simplify_subset() + + reg = specviz2d_helper.app.get_subsets("Subset 1") + assert reg.lower.value == 6400 and reg.upper.value == 7400 + + +def test_only_overlapping_in_specviz2d(specviz2d_helper, mos_spectrum2d): + specviz2d_helper.load_data(spectrum_2d=mos_spectrum2d) + viewer = specviz2d_helper.app.get_viewer( + specviz2d_helper._default_spectrum_2d_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6400, 6600)) + specviz2d_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(7000, 7400)) + + viewer.apply_roi(XRangeROI(6600, 7300)) + + viewer.apply_roi(XRangeROI(7600, 7800)) + + subset_plugin = specviz2d_helper.app.get_tray_item_from_name('g-subset-plugin') + assert subset_plugin.can_simplify + subset_plugin.vue_simplify_subset() + + reg = specviz2d_helper.app.get_subsets("Subset 1") + assert reg[0].lower.value == 6400 and reg[0].upper.value == 7400 + assert reg[1].lower.value == 7600 and reg[1].upper.value == 7800 From df0ace82ca0430e9931b0a528a6b241d198d49a1 Mon Sep 17 00:00:00 2001 From: Pey Lian Lim <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:48:40 -0400 Subject: [PATCH 108/283] BUG: Fix mouseover behavior in Cubeviz spectrum viewer when spatial subset is present. Co-authored-by: Duy Nguyen --- CHANGES.rst | 2 ++ jdaviz/configs/imviz/plugins/coords_info/coords_info.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 98c9b63545..cd4d24ad70 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -91,6 +91,8 @@ Bug Fixes Cubeviz ^^^^^^^ +- Fixed mouseover not behaving properly in spectrum viewer when spatial subset is present. [#2258] + Imviz ^^^^^ diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 5cc0ecdbeb..65c51ab88c 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -464,7 +464,7 @@ def _copy_axes_to_spectral(): sp = self.app._get_object_cache[cache_key] else: sp = self._specviz_helper.get_data(data_label=data_label, - spectral_subset=subset_label) + spatial_subset=subset_label) self.app._get_object_cache[cache_key] = sp # Calculations have to happen in the frame of viewer display units. From 54ae4c813948dcfce6b57f32190f62786a86578d Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jun 2023 13:48:28 -0400 Subject: [PATCH 109/283] Remove change log from #2258 because the bug only affects unreleased code [ci skip] [rtd skip] --- CHANGES.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cd4d24ad70..98c9b63545 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -91,8 +91,6 @@ Bug Fixes Cubeviz ^^^^^^^ -- Fixed mouseover not behaving properly in spectrum viewer when spatial subset is present. [#2258] - Imviz ^^^^^ From 8350d839855197dc0b92cc7530083b105966aaed Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Wed, 7 Jun 2023 09:51:19 -0400 Subject: [PATCH 110/283] Deprecate get_subsets_from_viewer --- docs/cubeviz/export_data.rst | 2 +- jdaviz/app.py | 4 ++++ .../cubeviz/plugins/tests/test_parsers.py | 4 ++-- .../cubeviz/plugins/tests/test_tools.py | 12 +++++----- jdaviz/tests/test_subsets.py | 22 +++++++++---------- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/cubeviz/export_data.rst b/docs/cubeviz/export_data.rst index 248ad932f4..ede78dd192 100644 --- a/docs/cubeviz/export_data.rst +++ b/docs/cubeviz/export_data.rst @@ -41,7 +41,7 @@ To get all subsets from the spectrum viewer: .. code-block:: python - subset1_spec1d = cubeviz.app.get_subsets_from_viewer("spectrum-viewer") + subset1_spec1d = cubeviz.app.get_subsets() To access the spatial regions themselves: diff --git a/jdaviz/app.py b/jdaviz/app.py index 3b8afb2c34..0289466bc8 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -14,6 +14,7 @@ from astropy.io import fits from astropy.coordinates import Angle from astropy.time import Time +from astropy.utils.exceptions import AstropyDeprecationWarning from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion from echo import CallbackProperty, DictCallbackProperty, ListCallbackProperty @@ -802,6 +803,9 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type representing the subset name and values as astropy regions objects. """ + warnings.warn(AstropyDeprecationWarning("get_subsets_from_viewer() is deprecated in v3.6 " + "and will be removed in a future release. Use " + "get_subsets() instead.")) viewer = self.get_viewer(viewer_reference) data = self.get_data_from_viewer(viewer_reference, data_label, diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index 48b9ba81f9..f82765bdac 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -73,8 +73,8 @@ def test_spectrum1d_with_fake_fixed_units(spectrum1d, cubeviz_helper): cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'test') cubeviz_helper.app.get_viewer('spectrum-viewer').apply_roi(XRangeROI(6600, 7400)) - subsets = cubeviz_helper.app.get_subsets_from_viewer("spectrum-viewer") - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets(spatial_only=True) + reg = subsets.get('Subset 1')[0]['region'] assert len(subsets) == 1 assert_allclose(reg.lower.value, 6600.) diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py index 5ce4867451..b97d181aba 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py @@ -25,8 +25,8 @@ def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube): assert len(spectrum_viewer.data()) == 2 # Check that a new subset was created - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1')[0]['region'] assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) @@ -64,8 +64,8 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): assert len(spectrum_viewer.data()) == 2 # Check that subset was created - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1')[0]['region'] assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) @@ -77,8 +77,8 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): assert len(flux_viewer.figure.marks) == 4 assert len(spectrum_viewer.data()) == 3 - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg2 = subsets.get('Subset 2') + subsets = cubeviz_helper.app.get_subsets() + reg2 = subsets.get('Subset 2')[0]['region'] assert len(subsets) == 2 assert isinstance(reg2, RectanglePixelRegion) diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 7dd56bca67..4e8faf2e8c 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -22,8 +22,8 @@ def test_region_from_subset_2d(cubeviz_helper): cubeviz_helper.app.get_viewer('flux-viewer').apply_roi(EllipticalROI(1, 3.5, 1.2, 3.3)) - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1')[0]['region'] assert len(subsets) == 1 assert isinstance(reg, EllipsePixelRegion) @@ -99,8 +99,8 @@ def test_region_from_subset_3d(cubeviz_helper): assert subset_plugin._get_value_from_subset_definition(0, "Ymax", key) == 3.3 assert subset_plugin._get_value_from_subset_definition(0, "Angle", key) == 45 - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1')[0]['region'] assert_allclose(reg.center.x, 2.75) assert_allclose(reg.center.y, 1.65) @@ -110,8 +110,8 @@ def test_region_from_subset_3d(cubeviz_helper): # Move the rectangle subset_plugin.set_center((3, 2), update=True) - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer') - reg = subsets.get('Subset 1') + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1')[0]['region'] assert_allclose(reg.center.x, 3) assert_allclose(reg.center.y, 2) assert_allclose(reg.width, 1.5) @@ -140,7 +140,7 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): cubeviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(5, 15.5)) - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer', subset_type='spectral') + subsets = cubeviz_helper.app.get_subsets(spectral_only=True) reg = subsets.get('Subset 1') assert len(subsets) == 1 @@ -165,7 +165,7 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): assert subset_plugin._get_value_from_subset_definition(0, "Lower bound", key) == 10 assert subset_plugin._get_value_from_subset_definition(0, "Upper bound", key) == 15.5 - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer', subset_type='spectral') + subsets = cubeviz_helper.app.get_subsets(spectral_only=True) reg = subsets.get('Subset 1') assert_quantity_allclose(reg.lower, 10.0 * u.Hz) @@ -173,7 +173,7 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): # Move the Subset. subset_plugin.set_center(10, update=True) - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer', subset_type='spectral') + subsets = cubeviz_helper.app.get_subsets(spectral_only=True) reg = subsets.get('Subset 1') assert_quantity_allclose(reg.lower, 7.25 * u.Hz) assert_quantity_allclose(reg.upper, 12.75 * u.Hz) @@ -200,7 +200,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): assert len([m for m in spectrum_viewer.figure.marks if isinstance(m, ShadowSpatialSpectral)]) == 1 # noqa - subsets = cubeviz_helper.app.get_subsets_from_viewer('spectrum-viewer', subset_type='spectral') + subsets = cubeviz_helper.app.get_subsets(spectral_only=True) reg = subsets.get('Subset 1') assert len(subsets) == 1 @@ -209,7 +209,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): assert_quantity_allclose(reg.lower, 5.0 * u.Hz) assert_quantity_allclose(reg.upper, 15.5 * u.Hz) - subsets = cubeviz_helper.app.get_subsets_from_viewer('flux-viewer', subset_type='spatial') + subsets = cubeviz_helper.app.get_subsets(spectral_only=True) reg = subsets.get('Subset 2') assert len(subsets) == 1 From 48726953884b2ae85e208c10912a7da4b6ae9369 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Wed, 7 Jun 2023 10:01:22 -0400 Subject: [PATCH 111/283] Missing region index --- jdaviz/tests/test_subsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 4e8faf2e8c..faaa5f6d1a 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -210,7 +210,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): assert_quantity_allclose(reg.upper, 15.5 * u.Hz) subsets = cubeviz_helper.app.get_subsets(spectral_only=True) - reg = subsets.get('Subset 2') + reg = subsets.get('Subset 2')[0]['region'] assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) From 83ccc6c4b79b8f7f292af2a93a13ec995929cc8c Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Wed, 7 Jun 2023 15:52:26 -0400 Subject: [PATCH 112/283] Fix subset args --- jdaviz/configs/cubeviz/plugins/tests/test_parsers.py | 4 ++-- jdaviz/tests/test_subsets.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index f82765bdac..5fb34a91d4 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -73,8 +73,8 @@ def test_spectrum1d_with_fake_fixed_units(spectrum1d, cubeviz_helper): cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'test') cubeviz_helper.app.get_viewer('spectrum-viewer').apply_roi(XRangeROI(6600, 7400)) - subsets = cubeviz_helper.app.get_subsets(spatial_only=True) - reg = subsets.get('Subset 1')[0]['region'] + subsets = cubeviz_helper.app.get_subsets() + reg = subsets.get('Subset 1') assert len(subsets) == 1 assert_allclose(reg.lower.value, 6600.) diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index faaa5f6d1a..8d4066bdf7 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -209,7 +209,7 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): assert_quantity_allclose(reg.lower, 5.0 * u.Hz) assert_quantity_allclose(reg.upper, 15.5 * u.Hz) - subsets = cubeviz_helper.app.get_subsets(spectral_only=True) + subsets = cubeviz_helper.app.get_subsets(spatial_only=True) reg = subsets.get('Subset 2')[0]['region'] assert len(subsets) == 1 From 4c4ef73011f5a6a9109bedd258cdbf9aa0c5280f Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 10:00:45 -0400 Subject: [PATCH 113/283] Remove get_data_from_viewer from imviz viewer tests --- jdaviz/configs/imviz/tests/test_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/imviz/tests/test_helper.py b/jdaviz/configs/imviz/tests/test_helper.py index f7c8a33041..f2e4410cb7 100644 --- a/jdaviz/configs/imviz/tests/test_helper.py +++ b/jdaviz/configs/imviz/tests/test_helper.py @@ -25,12 +25,12 @@ def test_create_new_viewer(imviz_helper, image_2d_wcs): assert len(imviz_helper.app.get_viewer_ids()) == 2 # there should be no data in the new viewer - assert len(imviz_helper.app.get_data_from_viewer(viewer_name)) == 0 + assert len(imviz_helper.app.get_viewer(viewer_name).data()) == 0 # then add data, and check that data were added to the new viewer imviz_helper.app.add_data_to_viewer(viewer_name, data_label) - assert len(imviz_helper.app.get_data_from_viewer(viewer_name)) == 1 + assert len(imviz_helper.app.get_viewer(viewer_name).data()) == 1 # remove data from the new viewer, check that it was removed imviz_helper.app.remove_data_from_viewer(viewer_name, data_label) - assert len(imviz_helper.app.get_data_from_viewer(viewer_name)) == 0 + assert len(imviz_helper.app.get_viewer(viewer_name).data()) == 0 From 30c5ec8c476a8196648eb479e0743f259cea591f Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 13:31:52 -0400 Subject: [PATCH 114/283] Remove get_data_from_viewer from mosviz data loading test and sub hardcoded viewer ref names --- .../configs/mosviz/tests/test_data_loading.py | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index 9a5ab2ca5d..b40264f8aa 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -20,12 +20,14 @@ def test_load_spectrum1d(mosviz_helper, spectrum1d): assert dc_1.label == label assert dc_1.meta['uncertainty_type'] == 'std' - table = mosviz_helper.app.get_viewer('table-viewer') + table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.widget_table.vue_on_row_clicked(0) - data = mosviz_helper.app.get_data_from_viewer('spectrum-viewer') + data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_viewer_reference_name + ).data() - assert isinstance(data[label], Spectrum1D) + assert len(data) == 1 + assert isinstance(data[0], Spectrum1D) with pytest.raises(AttributeError): mosviz_helper.load_1d_spectra([1, 2, 3]) @@ -42,12 +44,14 @@ def test_load_image(mosviz_helper, mos_image): assert PRIHDR_KEY not in dc_1.meta assert dc_1.meta['RADESYS'] == 'ICRS' - table = mosviz_helper.app.get_viewer('table-viewer') + table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.widget_table.vue_on_row_clicked(0) - data = mosviz_helper.app.get_data_from_viewer('image-viewer') + data = mosviz_helper.app.get_viewer(mosviz_helper._default_image_viewer_reference_name + ).data() - dataval = data[f"{label} 0"] + assert len(data) == 1 + dataval = data[0] assert isinstance(dataval, CCDData) assert dataval.shape == (55, 55) @@ -63,12 +67,14 @@ def test_load_spectrum_collection(mosviz_helper, spectrum_collection): assert dc_1.label == labels[0] assert dc_1.meta['uncertainty_type'] == 'std' - table = mosviz_helper.app.get_viewer('table-viewer') + table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.select_row(0) - data = mosviz_helper.app.get_data_from_viewer('spectrum-viewer') + data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_viewer_reference_name + ).data() - assert isinstance(data[labels[0]], Spectrum1D) + assert len(data) == 1 + assert isinstance(data[0], Spectrum1D) def test_load_list_of_spectrum1d(mosviz_helper, spectrum1d): @@ -83,12 +89,14 @@ def test_load_list_of_spectrum1d(mosviz_helper, spectrum1d): assert dc_1.label == labels[0] assert dc_1.meta['uncertainty_type'] == 'std' - table = mosviz_helper.app.get_viewer('table-viewer') + table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.widget_table.vue_on_row_clicked(0) - data = mosviz_helper.app.get_data_from_viewer('spectrum-viewer') + data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_viewer_reference_name + ).data() - assert isinstance(data[labels[0]], Spectrum1D) + assert len(data) == 1 + assert isinstance(data[0], Spectrum1D) def test_load_mos_spectrum2d(mosviz_helper, mos_spectrum2d): @@ -102,12 +110,14 @@ def test_load_mos_spectrum2d(mosviz_helper, mos_spectrum2d): assert dc_1.label == label assert dc_1.meta['INSTRUME'] == 'nirspec' - table = mosviz_helper.app.get_viewer('table-viewer') + table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.widget_table.vue_on_row_clicked(0) - data = mosviz_helper.app.get_data_from_viewer('spectrum-2d-viewer') + data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_viewer_reference_name + ).data() - assert data[label].shape == (1024, 15) + assert len(data) == 0 + assert data[0].shape == (1024, 15) @pytest.mark.parametrize('label', [None, "Test Label"]) @@ -118,7 +128,8 @@ def test_load_multi_image_spec(mosviz_helper, mos_image, spectrum1d, mos_spectru mosviz_helper.load_data(spectra1d, spectra2d, images=images, images_label=label) - assert mosviz_helper.app.get_viewer("table-viewer").figure_widget.highlighted == 0 + assert mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name + ).figure_widget.highlighted == 0 assert len(mosviz_helper.app.data_collection) == 10 qtable = mosviz_helper.to_table() @@ -134,7 +145,8 @@ def test_load_multi_image_and_spec1d_only(mosviz_helper, mos_image, spectrum1d): mosviz_helper.load_data(spectra_1d=spectra1d, images=images) - assert mosviz_helper.app.get_viewer("table-viewer").figure_widget.highlighted == 0 + assert mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name + ).figure_widget.highlighted == 0 assert len(mosviz_helper.app.data_collection) == 7 @@ -144,7 +156,8 @@ def test_load_multi_image_and_spec2d_only(mosviz_helper, mos_image, mos_spectrum mosviz_helper.load_data(spectra_2d=spectra2d, images=images) - assert mosviz_helper.app.get_viewer("table-viewer").figure_widget.highlighted == 0 + assert mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name + ).figure_widget.highlighted == 0 assert len(mosviz_helper.app.data_collection) == 7 @@ -170,7 +183,8 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ mosviz_helper.load_data(spectra1d, spectra2d, images=mos_image, images_label=label) - assert mosviz_helper.app.get_viewer("table-viewer").figure_widget.highlighted == 0 + assert mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name + ).figure_widget.highlighted == 0 assert len(mosviz_helper.app.data_collection) == 8 qtable = mosviz_helper.to_table() From 3638a5061984189bb114869a7440c52b6de83d9d Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 15:01:07 -0400 Subject: [PATCH 115/283] Mosviz test update image truth class --- jdaviz/configs/mosviz/tests/test_data_loading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index b40264f8aa..ecca78fdff 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -2,7 +2,7 @@ from zipfile import ZipFile -from astropy.nddata import CCDData +from glue.core import Data import numpy as np import pytest from specutils import Spectrum1D @@ -52,7 +52,7 @@ def test_load_image(mosviz_helper, mos_image): assert len(data) == 1 dataval = data[0] - assert isinstance(dataval, CCDData) + assert isinstance(dataval, Data) assert dataval.shape == (55, 55) From 2b1eff9ea717fc4d81ee70222bb2aa3590a6a926 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 15:12:40 -0400 Subject: [PATCH 116/283] Fix incorrect viewer ref --- jdaviz/configs/mosviz/tests/test_data_loading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index ecca78fdff..83b12651d3 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -113,10 +113,10 @@ def test_load_mos_spectrum2d(mosviz_helper, mos_spectrum2d): table = mosviz_helper.app.get_viewer(mosviz_helper._default_table_viewer_reference_name) table.widget_table.vue_on_row_clicked(0) - data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_viewer_reference_name + data = mosviz_helper.app.get_viewer(mosviz_helper._default_spectrum_2d_viewer_reference_name ).data() - assert len(data) == 0 + assert len(data) == 1 assert data[0].shape == (1024, 15) From 4d6658dd6e6347bc6011080c7a0a79b42e06fe4f Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 16:51:30 -0400 Subject: [PATCH 117/283] Set Mos2Dviewer data statistic to None by default --- jdaviz/configs/mosviz/plugins/viewers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py index ba65587373..c4cd8e1760 100644 --- a/jdaviz/configs/mosviz/plugins/viewers.py +++ b/jdaviz/configs/mosviz/plugins/viewers.py @@ -207,8 +207,9 @@ def _handle_x_axis_orientation(self, *args): x_scales.min = max(limits) if self.inverted_x_axis else min(limits) x_scales.max = min(limits) if self.inverted_x_axis else max(limits) - def data(self, cls=None): - return [layer_state.layer.get_object(cls=cls or self.default_class) + def data(self, cls=None, statistic=None): + return [layer_state.layer.get_object(statistic=statistic, + cls=cls or self.default_class) for layer_state in self.state.layers if hasattr(layer_state, 'layer') and isinstance(layer_state.layer, BaseData)] From f43ee63eb43670b17c8b67332bb0fdef88512ddb Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 17:00:18 -0400 Subject: [PATCH 118/283] Non-existent data check --- jdaviz/tests/test_app.py | 284 +++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py index f0138f082e..db0a72f522 100644 --- a/jdaviz/tests/test_app.py +++ b/jdaviz/tests/test_app.py @@ -1,142 +1,142 @@ -import pytest - -from jdaviz import Application, Specviz -from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth - - -# This applies to all viz but testing with Imviz should be enough. -def test_viewer_calling_app(imviz_helper): - viewer = imviz_helper.default_viewer - assert viewer.session.jdaviz_app is imviz_helper.app - - -def test_get_tray_item_from_name(): - app = Application(configuration='default') - plg = app.get_tray_item_from_name('g-gaussian-smooth') - assert isinstance(plg, GaussianSmooth) - - with pytest.raises(KeyError, match='not found in app'): - app.get_tray_item_from_name('imviz-compass') - - -def test_nonstandard_specviz_viewer_name(spectrum1d): - config = {'settings': {'configuration': 'nonstandard', - 'data': {'parser': 'specviz-spectrum1d-parser'}, - 'visible': {'menu_bar': False, - 'toolbar': True, - 'tray': True, - 'tab_headers': False}, - 'context': {'notebook': {'max_height': '750px'}}}, - 'toolbar': ['g-data-tools', 'g-subset-tools'], - 'tray': ['g-metadata-viewer', - 'g-plot-options', - 'g-subset-plugin', - 'g-gaussian-smooth', - 'g-model-fitting', - 'g-unit-conversion', - 'g-line-list', - 'specviz-line-analysis', - 'g-export-plot'], - 'viewer_area': [{'container': 'col', - 'children': [{'container': 'row', - 'viewers': [{'name': 'H', - 'plot': 'specviz-profile-viewer', - 'reference': 'h'}, - {'name': 'K', - 'plot': 'specviz-profile-viewer', - 'reference': 'k'}]}]}]} - - class Customviz(Specviz): - _default_configuration = config - _default_spectrum_viewer_reference_name = 'h' - - viz = Customviz() - assert viz.app.get_viewer_reference_names() == ['h', 'k'] - - viz.load_spectrum(spectrum1d, data_label='example label') - assert not len(viz.app.get_data_from_viewer("h", "non-existent label")) - - -def test_duplicate_data_labels(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test") - specviz_helper.load_spectrum(spectrum1d, data_label="test") - dc = specviz_helper.app.data_collection - assert dc[0].label == "test" - assert dc[1].label == "test (1)" - specviz_helper.load_spectrum(spectrum1d, data_label="test_1") - specviz_helper.load_spectrum(spectrum1d, data_label="test") - assert dc[2].label == "test_1" - assert dc[3].label == "test (2)" - - -def test_duplicate_data_labels_with_brackets(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") - dc = specviz_helper.app.data_collection - assert len(dc) == 2 - assert dc[0].label == "test[test]" - assert dc[1].label == "test[test] (1)" - - -def test_return_data_label_is_none(specviz_helper): - data_label = specviz_helper.app.return_data_label(None) - assert data_label == "Unknown" - - -def test_return_data_label_is_image(specviz_helper): - data_label = specviz_helper.app.return_data_label("data/path/test.jpg") - assert data_label == "test[jpg]" - - -def test_hdulist_with_filename(cubeviz_helper, image_cube_hdu_obj): - image_cube_hdu_obj.file_name = "test" - data_label = cubeviz_helper.app.return_data_label(image_cube_hdu_obj) - assert data_label == "test[HDU object]" - - -def test_file_path_not_image(imviz_helper, tmp_path): - path = tmp_path / "myimage.fits" - path.touch() - data_label = imviz_helper.app.return_data_label(str(path)) - assert data_label == "myimage" - - -def test_unique_name_variations(specviz_helper, spectrum1d): - data_label = specviz_helper.app.return_unique_name(None) - assert data_label == "Unknown" - - specviz_helper.load_spectrum(spectrum1d, data_label="test[flux]") - data_label = specviz_helper.app.return_data_label("test[flux]", ext="flux") - assert data_label == "test[flux][flux]" - - data_label = specviz_helper.app.return_data_label("test", ext="flux") - assert data_label == "test[flux] (1)" - - -def test_substring_in_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="M31") - specviz_helper.load_spectrum(spectrum1d, data_label="M32") - data_label = specviz_helper.app.return_data_label("M") - assert data_label == "M" - - -@pytest.mark.parametrize('data_label', ('111111', 'aaaaa', '///(#$@)', - 'two spaces repeating', - 'word42word42word two spaces')) -def test_edge_cases(specviz_helper, spectrum1d, data_label): - dc = specviz_helper.app.data_collection - - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - assert dc[1].label == f"{data_label} (1)" - - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - assert dc[2].label == f"{data_label} (2)" - - -def test_case_that_used_to_break_return_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break (1)") - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break") - dc = specviz_helper.app.data_collection - assert dc[0].label == "this used to break (1)" - assert dc[1].label == "this used to break (2)" +import pytest + +from jdaviz import Application, Specviz +from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth + + +# This applies to all viz but testing with Imviz should be enough. +def test_viewer_calling_app(imviz_helper): + viewer = imviz_helper.default_viewer + assert viewer.session.jdaviz_app is imviz_helper.app + + +def test_get_tray_item_from_name(): + app = Application(configuration='default') + plg = app.get_tray_item_from_name('g-gaussian-smooth') + assert isinstance(plg, GaussianSmooth) + + with pytest.raises(KeyError, match='not found in app'): + app.get_tray_item_from_name('imviz-compass') + + +def test_nonstandard_specviz_viewer_name(spectrum1d): + config = {'settings': {'configuration': 'nonstandard', + 'data': {'parser': 'specviz-spectrum1d-parser'}, + 'visible': {'menu_bar': False, + 'toolbar': True, + 'tray': True, + 'tab_headers': False}, + 'context': {'notebook': {'max_height': '750px'}}}, + 'toolbar': ['g-data-tools', 'g-subset-tools'], + 'tray': ['g-metadata-viewer', + 'g-plot-options', + 'g-subset-plugin', + 'g-gaussian-smooth', + 'g-model-fitting', + 'g-unit-conversion', + 'g-line-list', + 'specviz-line-analysis', + 'g-export-plot'], + 'viewer_area': [{'container': 'col', + 'children': [{'container': 'row', + 'viewers': [{'name': 'H', + 'plot': 'specviz-profile-viewer', + 'reference': 'h'}, + {'name': 'K', + 'plot': 'specviz-profile-viewer', + 'reference': 'k'}]}]}]} + + class Customviz(Specviz): + _default_configuration = config + _default_spectrum_viewer_reference_name = 'h' + + viz = Customviz() + assert viz.app.get_viewer_reference_names() == ['h', 'k'] + + viz.load_spectrum(spectrum1d, data_label='example label') + assert not len(viz.app.get_data("non-existent label")) + + +def test_duplicate_data_labels(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_spectrum(spectrum1d, data_label="test") + dc = specviz_helper.app.data_collection + assert dc[0].label == "test" + assert dc[1].label == "test (1)" + specviz_helper.load_spectrum(spectrum1d, data_label="test_1") + specviz_helper.load_spectrum(spectrum1d, data_label="test") + assert dc[2].label == "test_1" + assert dc[3].label == "test (2)" + + +def test_duplicate_data_labels_with_brackets(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") + specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") + dc = specviz_helper.app.data_collection + assert len(dc) == 2 + assert dc[0].label == "test[test]" + assert dc[1].label == "test[test] (1)" + + +def test_return_data_label_is_none(specviz_helper): + data_label = specviz_helper.app.return_data_label(None) + assert data_label == "Unknown" + + +def test_return_data_label_is_image(specviz_helper): + data_label = specviz_helper.app.return_data_label("data/path/test.jpg") + assert data_label == "test[jpg]" + + +def test_hdulist_with_filename(cubeviz_helper, image_cube_hdu_obj): + image_cube_hdu_obj.file_name = "test" + data_label = cubeviz_helper.app.return_data_label(image_cube_hdu_obj) + assert data_label == "test[HDU object]" + + +def test_file_path_not_image(imviz_helper, tmp_path): + path = tmp_path / "myimage.fits" + path.touch() + data_label = imviz_helper.app.return_data_label(str(path)) + assert data_label == "myimage" + + +def test_unique_name_variations(specviz_helper, spectrum1d): + data_label = specviz_helper.app.return_unique_name(None) + assert data_label == "Unknown" + + specviz_helper.load_spectrum(spectrum1d, data_label="test[flux]") + data_label = specviz_helper.app.return_data_label("test[flux]", ext="flux") + assert data_label == "test[flux][flux]" + + data_label = specviz_helper.app.return_data_label("test", ext="flux") + assert data_label == "test[flux] (1)" + + +def test_substring_in_label(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="M31") + specviz_helper.load_spectrum(spectrum1d, data_label="M32") + data_label = specviz_helper.app.return_data_label("M") + assert data_label == "M" + + +@pytest.mark.parametrize('data_label', ('111111', 'aaaaa', '///(#$@)', + 'two spaces repeating', + 'word42word42word two spaces')) +def test_edge_cases(specviz_helper, spectrum1d, data_label): + dc = specviz_helper.app.data_collection + + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + assert dc[1].label == f"{data_label} (1)" + + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + assert dc[2].label == f"{data_label} (2)" + + +def test_case_that_used_to_break_return_label(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="this used to break (1)") + specviz_helper.load_spectrum(spectrum1d, data_label="this used to break") + dc = specviz_helper.app.data_collection + assert dc[0].label == "this used to break (1)" + assert dc[1].label == "this used to break (2)" From a402b029c637aa042b5ff31792ae7be8b44dfcbc Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 8 Jun 2023 17:03:42 -0400 Subject: [PATCH 119/283] Properly deprecate getters --- jdaviz/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 0289466bc8..3052cb935b 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -14,6 +14,7 @@ from astropy.io import fits from astropy.coordinates import Angle from astropy.time import Time +from astropy.utils.decorators import deprecated from astropy.utils.exceptions import AstropyDeprecationWarning from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion @@ -663,6 +664,7 @@ def get_viewer_by_id(self, vid): """ return self._viewer_store.get(vid) + @deprecated(since="3.6", alternative="get_data") def get_data_from_viewer(self, viewer_reference, data_label=None, cls='default', include_subsets=True): """ @@ -770,6 +772,7 @@ def get_data_from_viewer(self, viewer_reference, data_label=None, # If a data label was provided, return only the corresponding data, otherwise return all: return data.get(data_label, data) + @deprecated(since="3.6", alternative="get_subsets") def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type=None): """ Returns the subsets of a specified viewer converted to astropy regions @@ -803,9 +806,6 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type representing the subset name and values as astropy regions objects. """ - warnings.warn(AstropyDeprecationWarning("get_subsets_from_viewer() is deprecated in v3.6 " - "and will be removed in a future release. Use " - "get_subsets() instead.")) viewer = self.get_viewer(viewer_reference) data = self.get_data_from_viewer(viewer_reference, data_label, From e6b9eb41d1e5c22968c03fe7b9e551f8ecfec7d5 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 9 Jun 2023 09:38:21 -0400 Subject: [PATCH 120/283] Properly check for valueError on non-existent label --- jdaviz/app.py | 1 - jdaviz/tests/test_app.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 3052cb935b..fd6debc2a1 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -15,7 +15,6 @@ from astropy.coordinates import Angle from astropy.time import Time from astropy.utils.decorators import deprecated -from astropy.utils.exceptions import AstropyDeprecationWarning from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion from echo import CallbackProperty, DictCallbackProperty, ListCallbackProperty diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py index db0a72f522..69dd0d1eaa 100644 --- a/jdaviz/tests/test_app.py +++ b/jdaviz/tests/test_app.py @@ -54,7 +54,8 @@ class Customviz(Specviz): assert viz.app.get_viewer_reference_names() == ['h', 'k'] viz.load_spectrum(spectrum1d, data_label='example label') - assert not len(viz.app.get_data("non-existent label")) + with pytest.raises(ValueError): + viz.get_data("non-existent label") def test_duplicate_data_labels(specviz_helper, spectrum1d): From 723fe1a2baddfbfede17e46f2d7331baa253c1a5 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 9 Jun 2023 10:12:44 -0400 Subject: [PATCH 121/283] Update Specviz get_data_from_viewer test --- jdaviz/configs/specviz/tests/test_helper.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index 0f47db0d02..bade4d68aa 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -32,10 +32,9 @@ def test_load_spectrum1d(self): assert dc_0.label == self.label assert dc_0.meta['uncertainty_type'] == 'std' - data = self.spec_app.app.get_data_from_viewer('spectrum-viewer') + data = self.spec_app.get_data() - assert isinstance(list(data.values())[0], Spectrum1D) - assert list(data.keys())[0] == self.label + assert isinstance(data, Spectrum1D) def test_load_spectrum_list_no_labels(self): # now load three more spectra from a SpectrumList, without labels From 4d6e2a4d70728681e484b14efa1db14789511bbd Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 9 Jun 2023 10:58:19 -0400 Subject: [PATCH 122/283] Rename "subset_to_apply" to "spectral_subset --- docs/cubeviz/export_data.rst | 2 +- jdaviz/configs/specviz/helper.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/cubeviz/export_data.rst b/docs/cubeviz/export_data.rst index ede78dd192..f125aab68a 100644 --- a/docs/cubeviz/export_data.rst +++ b/docs/cubeviz/export_data.rst @@ -22,7 +22,7 @@ can be used to extract the *spectrum* of a spatial subset named "Subset 1": .. code-block:: python - subset1_spec1d = cubeviz.specviz.get_spectra(subset_to_apply="Subset 1") + subset1_spec1d = cubeviz.specviz.get_spectra(spectral_subset="Subset 1") An example without accessing Specviz: diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index e0969e7cdd..a2297a0597 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -1,6 +1,7 @@ import warnings from astropy import units as u +from astropy.utils.decorators import deprecated_renamed_argument from regions.core.core import Region from glue.core.subset_group import GroupedSubset from specutils import SpectralRegion, Spectrum1D @@ -68,7 +69,8 @@ def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, show_in_viewer=show_in_viewer, concat_by_file=concat_by_file) - def get_spectra(self, data_label=None, subset_to_apply=None, apply_slider_redshift="Warn"): + @deprecated_renamed_argument(old_name="subset_to_apply", new_name="spectral_subset", since="3.6") + def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): """Returns the current data loaded into the main viewer """ @@ -81,16 +83,16 @@ def get_spectra(self, data_label=None, subset_to_apply=None, apply_slider_redshi if data_label is not None: spectrum = get_data_method(data_label=data_label, - spectral_subset=subset_to_apply, + spectral_subset=spectral_subset, cls=Spectrum1D) spectra[data_label] = spectrum else: for layer_state in viewer.state.layers: lyr = layer_state.layer - if subset_to_apply is not None: - if lyr.label == subset_to_apply: + if spectral_subset is not None: + if lyr.label == spectral_subset: spectrum = get_data_method(data_label=lyr.data.label, - spectral_subset=subset_to_apply, + spectral_subset=spectral_subset, cls=Spectrum1D, **function_kwargs) spectra[lyr.data.label] = spectrum @@ -291,7 +293,7 @@ def get_data(self, data_label=None, spectral_subset=None, cls=None, use_display_units=False, **kwargs): """ Returns data with name equal to data_label of type cls with subsets applied from - subset_to_apply. + spectral_subset. Parameters ---------- From ef6a38b80d693fe76675e6baefd0563de7320cd5 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 9 Jun 2023 12:04:22 -0400 Subject: [PATCH 123/283] Codestyle --- jdaviz/configs/specviz/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index a2297a0597..3a7c843bcd 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -69,7 +69,7 @@ def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, show_in_viewer=show_in_viewer, concat_by_file=concat_by_file) - @deprecated_renamed_argument(old_name="subset_to_apply", new_name="spectral_subset", since="3.6") + @deprecated_renamed_argument("subset_to_apply", "spectral_subset", "3.6") def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): """Returns the current data loaded into the main viewer From 0817e83d30bce0b8beddd61b462e79a4d5ca2728 Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Fri, 9 Jun 2023 16:41:36 -0400 Subject: [PATCH 124/283] Fix docs wording Co-authored-by: Jesse Averbukh --- docs/cubeviz/export_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cubeviz/export_data.rst b/docs/cubeviz/export_data.rst index f125aab68a..11c439e7ad 100644 --- a/docs/cubeviz/export_data.rst +++ b/docs/cubeviz/export_data.rst @@ -41,7 +41,7 @@ To get all subsets from the spectrum viewer: .. code-block:: python - subset1_spec1d = cubeviz.app.get_subsets() + subset1_spec1d = cubeviz.specviz.app.get_subsets() To access the spatial regions themselves: From 867210656cd4f7f11eda1a4ee30fac0346238161 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jun 2023 15:42:07 -0400 Subject: [PATCH 125/283] Catch missed code, fix bug --- CHANGES.rst | 9 +++++++++ jdaviz/app.py | 14 +++++++++----- .../cubeviz/plugins/tests/test_data_retrieval.py | 13 ++++++++----- jdaviz/core/helpers.py | 14 ++++++++------ jdaviz/core/template_mixin.py | 2 +- notebooks/CubevizExample.ipynb | 4 ++-- notebooks/ImvizDitheredExample.ipynb | 2 +- notebooks/ImvizExample.ipynb | 2 +- notebooks/concepts/cubeviz_data_interactions.ipynb | 6 +++--- notebooks/concepts/cubeviz_ndarray_gif.ipynb | 5 ++--- .../default_programmatic_viewers_from_blank.ipynb | 2 +- notebooks/concepts/imviz_roman_asdf.ipynb | 2 +- notebooks/concepts/mosviz_concept.ipynb | 2 +- 13 files changed, 47 insertions(+), 30 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 98c9b63545..55d25bf0d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -39,6 +39,13 @@ Specviz2d API Changes ----------- +- ``viz.app.get_data_from_viewer()`` is deprecated; use ``viz.get_data()``. [#2242] + +- ``viz.app.get_subsets_from_viewer()`` is deprecated; use ``viz.app.get_subsets()``. [#2242] + +- ``viz.get_data()`` now takes optional ``**kwargs``; e.g., you could pass in + ``function="sum"`` to collapse a cube in Cubeviz. [#2242] + Cubeviz ^^^^^^^ @@ -61,6 +68,8 @@ Bug Fixes - Fixed wrong elliptical region translation in ``app.get_subsets()``. [#2244] +- Fixed ``cls`` input being ignored in ``viz.get_data()``. [#2242] + Cubeviz ^^^^^^^ diff --git a/jdaviz/app.py b/jdaviz/app.py index fd6debc2a1..f8d16356be 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -663,7 +663,7 @@ def get_viewer_by_id(self, vid): """ return self._viewer_store.get(vid) - @deprecated(since="3.6", alternative="get_data") + @deprecated(since="3.6", alternative="viz_helper.get_data") def get_data_from_viewer(self, viewer_reference, data_label=None, cls='default', include_subsets=True): """ @@ -806,9 +806,11 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type objects. """ viewer = self.get_viewer(viewer_reference) - data = self.get_data_from_viewer(viewer_reference, - data_label, - cls=None) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + data = self.get_data_from_viewer(viewer_reference, + data_label, + cls=None) regions = {} if data_label is not None: @@ -848,7 +850,9 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type regions[key] = self.get_subsets(key) continue - temp_data = self.get_data_from_viewer(viewer_reference, value.label) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + temp_data = self.get_data_from_viewer(viewer_reference, value.label) if isinstance(temp_data, Spectrum1D): regions[key] = self.get_subsets(key) continue diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_data_retrieval.py b/jdaviz/configs/cubeviz/plugins/tests/test_data_retrieval.py index 7509b7cc44..b5f1dfc6f5 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_data_retrieval.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_data_retrieval.py @@ -1,16 +1,17 @@ +import warnings + import pytest import numpy as np from astropy.utils.data import download_file -@pytest.mark.filterwarnings('ignore') @pytest.mark.remote_data def test_data_retrieval(cubeviz_helper): """The purpose of this test is to check that both methods: - app.get_viewer('spectrum-viewer').data() - - app.get_data_from_viewer("spectrum-viewer") + - cubeviz_helper.get_data() return the same spectrum values. """ @@ -20,14 +21,16 @@ def test_data_retrieval(cubeviz_helper): spectrum_viewer_reference_name = "spectrum-viewer" fn = download_file(URL, cache=True) - cubeviz_helper.load_data(fn) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cubeviz_helper.load_data(fn) # two ways of retrieving data from the viewer. # They should return the same spectral values a1 = cubeviz_helper.app.get_viewer(spectrum_viewer_reference_name).data() - a2 = cubeviz_helper.app.get_data_from_viewer(spectrum_viewer_reference_name) + a2 = cubeviz_helper.get_data("contents[FLUX]", function="sum") test_value_1 = a1[0].data - test_value_2 = list(a2.values())[0].data + test_value_2 = a2.flux.value assert np.allclose(test_value_1, test_value_2, atol=1e-5) diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index 986547f28b..7a021d6421 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -554,7 +554,7 @@ def _handle_display_units(data, use_display_units): return _handle_display_units(data, use_display_units) - def get_data(self, data_label=None, cls=None, use_display_units=False): + def get_data(self, data_label=None, cls=None, use_display_units=False, **kwargs): """ Returns data with name equal to data_label of type cls. @@ -564,18 +564,20 @@ def get_data(self, data_label=None, cls=None, use_display_units=False): Provide a label to retrieve a specific data set from data_collection. cls : `~specutils.Spectrum1D`, `~astropy.nddata.CCDData`, optional The type that data will be returned as. - use_display_units: bool, optional + use_display_units : bool, optional Whether to convert to the display units defined in the plugin. + kwargs : dict + For Cubeviz, you could also pass in ``function`` (str) to collapse + the cube into 1D spectrum using provided function. Returns ------- data : cls - Data is returned as type cls. + Data is returned as type ``cls``. """ - return self._get_data(data_label=data_label, spatial_subset=None, - spectral_subset=None, function=None, - cls=None, use_display_units=use_display_units) + return self._get_data(data_label=data_label, + cls=cls, use_display_units=use_display_units, **kwargs) class ImageConfigHelper(ConfigHelper): diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 5bc71b4e57..1b8fece455 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -1067,7 +1067,7 @@ def _update_has_subregions(self): def selected_obj(self): if self.selected in self.manual_options or self.selected not in self.labels: return None - # NOTE: we use reference names here instead of IDs since get_subsets_from_viewer requires + # NOTE: we use reference names here instead of IDs since get_subsets requires # that. For imviz, this will mean we won't be able to loop through each of the viewers, # but the original viewer should have access to all the subsets. for viewer_ref in self.viewer_refs: diff --git a/notebooks/CubevizExample.ipynb b/notebooks/CubevizExample.ipynb index ac78ed88e7..af16b3998f 100644 --- a/notebooks/CubevizExample.ipynb +++ b/notebooks/CubevizExample.ipynb @@ -155,7 +155,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = cubeviz.app.get_data_from_viewer('flux-viewer', 'contents[FLUX]')" + "data = cubeviz.get_data('jw02732-o004_t004_miri_ch1-shortmediumlong_s3d.fits[SCI]')" ] }, { @@ -187,7 +187,7 @@ "metadata": {}, "outputs": [], "source": [ - "spectra_dict = cubeviz.specviz.get_spectra()\n", + "spectra_dict = cubeviz.specviz.get_spectra(apply_slider_redshift=True)\n", "spectra_dict" ] }, diff --git a/notebooks/ImvizDitheredExample.ipynb b/notebooks/ImvizDitheredExample.ipynb index 8af6aac55c..52c75d05cf 100644 --- a/notebooks/ImvizDitheredExample.ipynb +++ b/notebooks/ImvizDitheredExample.ipynb @@ -301,7 +301,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = imviz.app.get_data_from_viewer('imviz-0', 'acs_47tuc_1[SCI,1]')" + "data = imviz.get_data('acs_47tuc_1[SCI,1]')" ] }, { diff --git a/notebooks/ImvizExample.ipynb b/notebooks/ImvizExample.ipynb index b39014d4c5..1841efb1bd 100644 --- a/notebooks/ImvizExample.ipynb +++ b/notebooks/ImvizExample.ipynb @@ -348,7 +348,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = imviz.app.get_data_from_viewer('imviz-0', 'jw02727-o002_t062_nircam_clear-f090w_i2d[DATA]')" + "data = imviz.get_data('jw02727-o002_t062_nircam_clear-f090w_i2d[DATA]')" ] }, { diff --git a/notebooks/concepts/cubeviz_data_interactions.ipynb b/notebooks/concepts/cubeviz_data_interactions.ipynb index 862ba92047..fd6b6d0a2f 100644 --- a/notebooks/concepts/cubeviz_data_interactions.ipynb +++ b/notebooks/concepts/cubeviz_data_interactions.ipynb @@ -149,7 +149,7 @@ "source": [ "from specutils import Spectrum1D\n", "\n", - "spec_data = app.get_data_from_viewer('spectrum-viewer')\n", + "spec_data = app.get_viewer('spectrum-viewer').data()\n", "\n", "# The returned data from `get_data` is in list format, as it's \n", "# possible for there to be several data plotted in the viewer\n", @@ -170,7 +170,7 @@ "metadata": {}, "outputs": [], "source": [ - "spec = app.get_data_from_viewer('spectrum-viewer', '6de4c8ee5659e87a302e3de595074ba5[FLUX]')\n", + "spec = app._jdaviz_helper.get_data('6de4c8ee5659e87a302e3de595074ba5[FLUX]', function='sum')\n", "spec" ] }, @@ -267,7 +267,7 @@ "import matplotlib.pyplot as plt\n", "\n", "# The returned data object is a `CCData` class to represent the 2D nature of the image data.\n", - "image_data = app.get_data_from_viewer('flux-viewer')\n", + "image_data = app.get_viewer('flux-viewer').data()[0]\n", "\n", "f, ax = plt.subplots()\n", "\n", diff --git a/notebooks/concepts/cubeviz_ndarray_gif.ipynb b/notebooks/concepts/cubeviz_ndarray_gif.ipynb index e49026182c..f8dfc86423 100644 --- a/notebooks/concepts/cubeviz_ndarray_gif.ipynb +++ b/notebooks/concepts/cubeviz_ndarray_gif.ipynb @@ -102,8 +102,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = cubeviz.app.get_data_from_viewer('flux-viewer', 'ordered')\n", - "orig_cube = data.get_object(statistic=None)\n", + "orig_cube = cubeviz.get_data('ordered')\n", "orig_cube.shape # Input was (8, 5, 10) # x, y, z" ] }, @@ -124,7 +123,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_2 = cubeviz.app.get_data_from_viewer('flux-viewer', 'roundtrip_test')\n", + "data_2 = cubeviz.get_data('roundtrip_test')\n", "data_2.shape" ] }, diff --git a/notebooks/concepts/default_programmatic_viewers_from_blank.ipynb b/notebooks/concepts/default_programmatic_viewers_from_blank.ipynb index 08b20d5f7d..3b885fa201 100644 --- a/notebooks/concepts/default_programmatic_viewers_from_blank.ipynb +++ b/notebooks/concepts/default_programmatic_viewers_from_blank.ipynb @@ -280,7 +280,7 @@ "outputs": [], "source": [ "# can I access the data back out? \n", - "app.get_data_from_viewer('image-viewer')" + "app.get_viewer('image-viewer').data()" ] }, { diff --git a/notebooks/concepts/imviz_roman_asdf.ipynb b/notebooks/concepts/imviz_roman_asdf.ipynb index 818cd22d0d..7d03ba9886 100644 --- a/notebooks/concepts/imviz_roman_asdf.ipynb +++ b/notebooks/concepts/imviz_roman_asdf.ipynb @@ -202,7 +202,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = imviz.app.get_data_from_viewer('imviz-0', imviz.app.data_collection[0].label)" + "data = imviz.get_data(imviz.app.data_collection[0].label)" ] }, { diff --git a/notebooks/concepts/mosviz_concept.ipynb b/notebooks/concepts/mosviz_concept.ipynb index c5372c5d78..d5426ce912 100644 --- a/notebooks/concepts/mosviz_concept.ipynb +++ b/notebooks/concepts/mosviz_concept.ipynb @@ -637,7 +637,7 @@ "source": [ "# Assuming the user has done something with a glue plugin that adds another spectrum to the spectrum view\n", "\n", - "currspec = mosviz.app.get_data_from_viewer('spectrum-viewer', 'spectrum-added-by-plugin')\n", + "currspec = mosviz.get_data('spectrum-added-by-plugin')\n", "\n", "... do something with currspec ..." ] From 1688ec16c5c3660dbb90bea2e36c0b6619d105e7 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jun 2023 18:25:13 -0400 Subject: [PATCH 126/283] Retain Mosviz get_data behavior and fix change log --- CHANGES.rst | 5 ++++- jdaviz/configs/mosviz/plugins/viewers.py | 2 +- jdaviz/configs/mosviz/tests/test_data_loading.py | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 55d25bf0d6..a8b8611b0d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -55,10 +55,13 @@ Imviz Mosviz ^^^^^^ +- Added new ``statistic`` keyword to ``mosviz.get_viewer("spectrum-2d-viewer").data()`` + to allow user to collapse 2D spectrum to 1D. [#2242] + Specviz ^^^^^^^ -* Re-enabled unit conversion support. [#2127] +- Re-enabled unit conversion support. [#2127] Specviz2d ^^^^^^^^^ diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py index c4cd8e1760..c5ad804655 100644 --- a/jdaviz/configs/mosviz/plugins/viewers.py +++ b/jdaviz/configs/mosviz/plugins/viewers.py @@ -43,7 +43,7 @@ def __init__(self, *args, **kwargs): self.figure.fig_margin = {'left': 0, 'bottom': 0, 'top': 0, 'right': 0} def data(self, cls=None): - return [layer_state.layer # .get_object(cls=cls or self.default_class) + return [layer_state.layer.get_object(cls=cls or self.default_class) for layer_state in self.state.layers if hasattr(layer_state, 'layer') and isinstance(layer_state.layer, BaseData)] diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index 83b12651d3..67d73ffe1c 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -2,9 +2,9 @@ from zipfile import ZipFile -from glue.core import Data import numpy as np import pytest +from astropy.nddata import CCDData from specutils import Spectrum1D from jdaviz.utils import PRIHDR_KEY @@ -48,11 +48,11 @@ def test_load_image(mosviz_helper, mos_image): table.widget_table.vue_on_row_clicked(0) data = mosviz_helper.app.get_viewer(mosviz_helper._default_image_viewer_reference_name - ).data() + ).data(cls=CCDData) assert len(data) == 1 dataval = data[0] - assert isinstance(dataval, Data) + assert isinstance(dataval, CCDData) assert dataval.shape == (55, 55) From 06f2ed022721bee6604200d283dd66792dd15d17 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:23:29 -0400 Subject: [PATCH 127/283] Undo bad diff --- jdaviz/tests/test_app.py | 286 +++++++++++++++++++-------------------- 1 file changed, 143 insertions(+), 143 deletions(-) diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py index 69dd0d1eaa..c72d4fee27 100644 --- a/jdaviz/tests/test_app.py +++ b/jdaviz/tests/test_app.py @@ -1,143 +1,143 @@ -import pytest - -from jdaviz import Application, Specviz -from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth - - -# This applies to all viz but testing with Imviz should be enough. -def test_viewer_calling_app(imviz_helper): - viewer = imviz_helper.default_viewer - assert viewer.session.jdaviz_app is imviz_helper.app - - -def test_get_tray_item_from_name(): - app = Application(configuration='default') - plg = app.get_tray_item_from_name('g-gaussian-smooth') - assert isinstance(plg, GaussianSmooth) - - with pytest.raises(KeyError, match='not found in app'): - app.get_tray_item_from_name('imviz-compass') - - -def test_nonstandard_specviz_viewer_name(spectrum1d): - config = {'settings': {'configuration': 'nonstandard', - 'data': {'parser': 'specviz-spectrum1d-parser'}, - 'visible': {'menu_bar': False, - 'toolbar': True, - 'tray': True, - 'tab_headers': False}, - 'context': {'notebook': {'max_height': '750px'}}}, - 'toolbar': ['g-data-tools', 'g-subset-tools'], - 'tray': ['g-metadata-viewer', - 'g-plot-options', - 'g-subset-plugin', - 'g-gaussian-smooth', - 'g-model-fitting', - 'g-unit-conversion', - 'g-line-list', - 'specviz-line-analysis', - 'g-export-plot'], - 'viewer_area': [{'container': 'col', - 'children': [{'container': 'row', - 'viewers': [{'name': 'H', - 'plot': 'specviz-profile-viewer', - 'reference': 'h'}, - {'name': 'K', - 'plot': 'specviz-profile-viewer', - 'reference': 'k'}]}]}]} - - class Customviz(Specviz): - _default_configuration = config - _default_spectrum_viewer_reference_name = 'h' - - viz = Customviz() - assert viz.app.get_viewer_reference_names() == ['h', 'k'] - - viz.load_spectrum(spectrum1d, data_label='example label') - with pytest.raises(ValueError): - viz.get_data("non-existent label") - - -def test_duplicate_data_labels(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test") - specviz_helper.load_spectrum(spectrum1d, data_label="test") - dc = specviz_helper.app.data_collection - assert dc[0].label == "test" - assert dc[1].label == "test (1)" - specviz_helper.load_spectrum(spectrum1d, data_label="test_1") - specviz_helper.load_spectrum(spectrum1d, data_label="test") - assert dc[2].label == "test_1" - assert dc[3].label == "test (2)" - - -def test_duplicate_data_labels_with_brackets(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") - dc = specviz_helper.app.data_collection - assert len(dc) == 2 - assert dc[0].label == "test[test]" - assert dc[1].label == "test[test] (1)" - - -def test_return_data_label_is_none(specviz_helper): - data_label = specviz_helper.app.return_data_label(None) - assert data_label == "Unknown" - - -def test_return_data_label_is_image(specviz_helper): - data_label = specviz_helper.app.return_data_label("data/path/test.jpg") - assert data_label == "test[jpg]" - - -def test_hdulist_with_filename(cubeviz_helper, image_cube_hdu_obj): - image_cube_hdu_obj.file_name = "test" - data_label = cubeviz_helper.app.return_data_label(image_cube_hdu_obj) - assert data_label == "test[HDU object]" - - -def test_file_path_not_image(imviz_helper, tmp_path): - path = tmp_path / "myimage.fits" - path.touch() - data_label = imviz_helper.app.return_data_label(str(path)) - assert data_label == "myimage" - - -def test_unique_name_variations(specviz_helper, spectrum1d): - data_label = specviz_helper.app.return_unique_name(None) - assert data_label == "Unknown" - - specviz_helper.load_spectrum(spectrum1d, data_label="test[flux]") - data_label = specviz_helper.app.return_data_label("test[flux]", ext="flux") - assert data_label == "test[flux][flux]" - - data_label = specviz_helper.app.return_data_label("test", ext="flux") - assert data_label == "test[flux] (1)" - - -def test_substring_in_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="M31") - specviz_helper.load_spectrum(spectrum1d, data_label="M32") - data_label = specviz_helper.app.return_data_label("M") - assert data_label == "M" - - -@pytest.mark.parametrize('data_label', ('111111', 'aaaaa', '///(#$@)', - 'two spaces repeating', - 'word42word42word two spaces')) -def test_edge_cases(specviz_helper, spectrum1d, data_label): - dc = specviz_helper.app.data_collection - - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - assert dc[1].label == f"{data_label} (1)" - - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - assert dc[2].label == f"{data_label} (2)" - - -def test_case_that_used_to_break_return_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break (1)") - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break") - dc = specviz_helper.app.data_collection - assert dc[0].label == "this used to break (1)" - assert dc[1].label == "this used to break (2)" +import pytest + +from jdaviz import Application, Specviz +from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth + + +# This applies to all viz but testing with Imviz should be enough. +def test_viewer_calling_app(imviz_helper): + viewer = imviz_helper.default_viewer + assert viewer.session.jdaviz_app is imviz_helper.app + + +def test_get_tray_item_from_name(): + app = Application(configuration='default') + plg = app.get_tray_item_from_name('g-gaussian-smooth') + assert isinstance(plg, GaussianSmooth) + + with pytest.raises(KeyError, match='not found in app'): + app.get_tray_item_from_name('imviz-compass') + + +def test_nonstandard_specviz_viewer_name(spectrum1d): + config = {'settings': {'configuration': 'nonstandard', + 'data': {'parser': 'specviz-spectrum1d-parser'}, + 'visible': {'menu_bar': False, + 'toolbar': True, + 'tray': True, + 'tab_headers': False}, + 'context': {'notebook': {'max_height': '750px'}}}, + 'toolbar': ['g-data-tools', 'g-subset-tools'], + 'tray': ['g-metadata-viewer', + 'g-plot-options', + 'g-subset-plugin', + 'g-gaussian-smooth', + 'g-model-fitting', + 'g-unit-conversion', + 'g-line-list', + 'specviz-line-analysis', + 'g-export-plot'], + 'viewer_area': [{'container': 'col', + 'children': [{'container': 'row', + 'viewers': [{'name': 'H', + 'plot': 'specviz-profile-viewer', + 'reference': 'h'}, + {'name': 'K', + 'plot': 'specviz-profile-viewer', + 'reference': 'k'}]}]}]} + + class Customviz(Specviz): + _default_configuration = config + _default_spectrum_viewer_reference_name = 'h' + + viz = Customviz() + assert viz.app.get_viewer_reference_names() == ['h', 'k'] + + viz.load_spectrum(spectrum1d, data_label='example label') + with pytest.raises(ValueError): + viz.get_data("non-existent label") + + +def test_duplicate_data_labels(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_spectrum(spectrum1d, data_label="test") + dc = specviz_helper.app.data_collection + assert dc[0].label == "test" + assert dc[1].label == "test (1)" + specviz_helper.load_spectrum(spectrum1d, data_label="test_1") + specviz_helper.load_spectrum(spectrum1d, data_label="test") + assert dc[2].label == "test_1" + assert dc[3].label == "test (2)" + + +def test_duplicate_data_labels_with_brackets(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") + specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") + dc = specviz_helper.app.data_collection + assert len(dc) == 2 + assert dc[0].label == "test[test]" + assert dc[1].label == "test[test] (1)" + + +def test_return_data_label_is_none(specviz_helper): + data_label = specviz_helper.app.return_data_label(None) + assert data_label == "Unknown" + + +def test_return_data_label_is_image(specviz_helper): + data_label = specviz_helper.app.return_data_label("data/path/test.jpg") + assert data_label == "test[jpg]" + + +def test_hdulist_with_filename(cubeviz_helper, image_cube_hdu_obj): + image_cube_hdu_obj.file_name = "test" + data_label = cubeviz_helper.app.return_data_label(image_cube_hdu_obj) + assert data_label == "test[HDU object]" + + +def test_file_path_not_image(imviz_helper, tmp_path): + path = tmp_path / "myimage.fits" + path.touch() + data_label = imviz_helper.app.return_data_label(str(path)) + assert data_label == "myimage" + + +def test_unique_name_variations(specviz_helper, spectrum1d): + data_label = specviz_helper.app.return_unique_name(None) + assert data_label == "Unknown" + + specviz_helper.load_spectrum(spectrum1d, data_label="test[flux]") + data_label = specviz_helper.app.return_data_label("test[flux]", ext="flux") + assert data_label == "test[flux][flux]" + + data_label = specviz_helper.app.return_data_label("test", ext="flux") + assert data_label == "test[flux] (1)" + + +def test_substring_in_label(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="M31") + specviz_helper.load_spectrum(spectrum1d, data_label="M32") + data_label = specviz_helper.app.return_data_label("M") + assert data_label == "M" + + +@pytest.mark.parametrize('data_label', ('111111', 'aaaaa', '///(#$@)', + 'two spaces repeating', + 'word42word42word two spaces')) +def test_edge_cases(specviz_helper, spectrum1d, data_label): + dc = specviz_helper.app.data_collection + + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + assert dc[1].label == f"{data_label} (1)" + + specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + assert dc[2].label == f"{data_label} (2)" + + +def test_case_that_used_to_break_return_label(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d, data_label="this used to break (1)") + specviz_helper.load_spectrum(spectrum1d, data_label="this used to break") + dc = specviz_helper.app.data_collection + assert dc[0].label == "this used to break (1)" + assert dc[1].label == "this used to break (2)" From a160c5fca21d3de3dc97dc026ffbb6e99e9b03b0 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Jun 2023 13:47:07 -0400 Subject: [PATCH 128/283] MNT: Add .mailmap so git shortlog -es gives sane listing. --- .mailmap | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..3e8f9e57a9 --- /dev/null +++ b/.mailmap @@ -0,0 +1,25 @@ +Brett Graham +Brett M. Morris +Camilla Pacifici +Craig Jones +Duy Nguyen +Duy Nguyen +Duy Nguyen Duy Tuong Nguyen +Duy Nguyen Duy Tuong Nguyen +Duy Nguyen duytnguyendtn +Ivo Busko Ivo +Ivo Busko busko +Ivo Busko busko@stsci.edu +Jennifer Kotler +Jennifer Kotler +Jesse Averbukh +Josh Soref <2119212+jsoref@users.noreply.github.com> +Kyle Conroy Kyle +Mario Buikhuizen +Nicholas Earl +Nicholas Earl nmearl +Ori Fox Ori +P. L. Lim <2090236+pllim@users.noreply.github.com> +Patrick Ogle PatrickOgle +Ricky O'Steen <39831871+rosteen@users.noreply.github.com> +Ricky O'Steen <39831871+rosteen@users.noreply.github.com> rosteen <39831871+rosteen@users.noreply.github.com> From d5fbd700850fb509e67b95afec6188b4a008a6e8 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 23 Jun 2023 07:21:28 -0700 Subject: [PATCH 129/283] DOC: Add warning about surface brightness in Simple Aperture Photometry plugin (#2261) * DOC: Add warning about surface brightness in Simple Aperture Photometry plugin. I am beginning to think the Simple in plugin name no longer applies. * DOC: Improve verbiage. Co-authored-by: Camilla Pacifici --------- Co-authored-by: Camilla Pacifici --- docs/imviz/plugins.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index 4ae367b37e..a69819d66f 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -202,6 +202,15 @@ an interactively selected region. A typical workflow is as follows: in display data unit. Otherwise, it is only informational. If this field is not applicable for you, leave it at 0. **This field resets every time Data selection changes if auto-population not possible.** + + .. warning:: + + If your data is in surface brightness units and pixels on the image + have varying sky area, you should first convert your data from + surface brightness to flux units before using this plugin. + This is because, for performance reasons, the plugin multiplies + by the area after the aperture sum is calculated. + 7. If you also want photometry result in the unit of counts, you can enter a conversion factor in the :guilabel:`Counts conversion factor` field. The value must be in the unit of display data unit per counts. This is used to convert linear From bc6cdee595ae972c72b79bb5f51bd76533dbd788 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 29 Jun 2023 08:11:48 -0700 Subject: [PATCH 130/283] FEAT: Annulus draw tool for Imviz (#2240) * FEAT: Annulus draw tool for Imviz. TODO: Need to fix icon. TST: Add tests. [ci skip] [rtd skip] * Pull in #2204 into this PR and bump upstream pins * Changed function name upstream in glue-astronomy during review process * Proper annulus icon from J. Kotler Co-authored-by: Jennifer Kotler * Avoid error traceback with bad annulus radii * Disable recentering for annulus * Fix test failure because not sure why spectral region is using Imviz centering method but okay. --------- Co-authored-by: Jennifer Kotler Co-authored-by: Ricky O'Steen --- CHANGES.rst | 3 +- jdaviz/app.py | 28 +------- .../plugins/subset_plugin/subset_plugin.py | 67 +++++++++---------- jdaviz/configs/imviz/plugins/viewers.py | 2 +- jdaviz/core/tools.py | 6 +- jdaviz/data/icons/select_annulus.svg | 12 ++++ jdaviz/tests/test_subsets.py | 27 +++++--- pyproject.toml | 4 +- 8 files changed, 74 insertions(+), 75 deletions(-) create mode 100644 jdaviz/data/icons/select_annulus.svg diff --git a/CHANGES.rst b/CHANGES.rst index a8b8611b0d..3b6639f105 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,8 @@ Imviz - Added the ability to load DS9 region files (``.reg``) using the ``IMPORT DATA`` button. However, this only works after loading at least one image into Imviz. [#2201] -- Added support for new ``CircularAnnulusROI`` subset from glue. [#2201] +- Added support for new ``CircularAnnulusROI`` subset from glue, including + a new draw tool. [#2201, #2240] Mosviz ^^^^^^ diff --git a/jdaviz/app.py b/jdaviz/app.py index f8d16356be..1bdd8df36f 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -12,11 +12,8 @@ from astropy import units as u from astropy.nddata import CCDData, NDData from astropy.io import fits -from astropy.coordinates import Angle from astropy.time import Time from astropy.utils.decorators import deprecated -from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion - from echo import CallbackProperty, DictCallbackProperty, ListCallbackProperty from ipygoldenlayout import GoldenLayout from ipysplitpanes import SplitPanes @@ -39,9 +36,9 @@ from glue.core.state_objects import State from glue.core.subset import (Subset, RangeSubsetState, RoiSubsetState, CompositeSubsetState, InvertState) -from glue.core.roi import CircularROI, EllipticalROI, RectangularROI from glue.core.units import unit_converter from glue_astronomy.spectral_coordinates import SpectralCoordinates +from glue_astronomy.translators.regions import roi_subset_state_to_region from glue_jupyter.app import JupyterApplication from glue_jupyter.common.toolbar_vuetify import read_icon from glue_jupyter.state_traitlets_helpers import GlueState @@ -1055,27 +1052,8 @@ def _get_range_subset_bounds(self, subset_state, return spec_region def _get_roi_subset_definition(self, subset_state): - _around_decimals = 6 - roi = subset_state.roi - roi_as_region = None - if isinstance(roi, CircularROI): - x, y = roi.get_center() - r = roi.radius - roi_as_region = CirclePixelRegion(PixCoord(x, y), r) - - elif isinstance(roi, RectangularROI): - theta = np.around(np.degrees(roi.theta), decimals=_around_decimals) - roi_as_region = RectanglePixelRegion(PixCoord(roi.center()[0], roi.center()[1]), - roi.width(), roi.height(), Angle(theta, "deg")) - - elif isinstance(roi, EllipticalROI): - xc = roi.xc - yc = roi.yc - rx = roi.radius_x - ry = roi.radius_y - theta = np.around(np.degrees(roi.theta), decimals=_around_decimals) - roi_as_region = EllipsePixelRegion(PixCoord(xc, yc), rx * 2, ry * 2, Angle(theta, "deg")) # noqa: E501 - + # TODO: Imviz: Return sky region if link type is WCS. + roi_as_region = roi_subset_state_to_region(subset_state) return [{"name": subset_state.roi.__class__.__name__, "glue_state": subset_state.__class__.__name__, "region": roi_as_region, diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 5bd4fb22b3..12d881ee44 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -7,8 +7,8 @@ from glue.core.message import EditSubsetMessage, SubsetUpdateMessage from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode, ReplaceMode, XorMode) -from glue.core.roi import CircularROI, EllipticalROI, RectangularROI -from glue.core.subset import RoiSubsetState, RangeSubsetState, CompositeSubsetState +from glue.core.roi import CircularROI, CircularAnnulusROI, EllipticalROI, RectangularROI +from glue.core.subset import RoiSubsetState, RangeSubsetState from glue.icons import icon_path from glue_jupyter.widgets.subset_mode_vuetify import SelectionModeMenu from glue_jupyter.common.toolbar_vuetify import read_icon @@ -149,7 +149,11 @@ def _unpack_get_subsets_for_ui(self): _around_decimals = 6 # Avoid 30 degrees from coming back as 29.999999999999996 if not subset_information: return - if len(subset_information) == 1: + if ((len(subset_information) == 1) and + (isinstance(subset_information[0]["subset_state"], RangeSubsetState) or + (isinstance(subset_information[0]["subset_state"], RoiSubsetState) and + isinstance(subset_information[0]["subset_state"].roi, + (CircularROI, RectangularROI, EllipticalROI))))): self.is_centerable = True else: self.is_centerable = False @@ -161,7 +165,7 @@ def _unpack_get_subsets_for_ui(self): glue_state = spec["glue_state"] if isinstance(subset_state, RoiSubsetState): if isinstance(subset_state.roi, CircularROI): - x, y = subset_state.roi.get_center() + x, y = subset_state.roi.center() r = subset_state.roi.radius subset_definition = [{"name": "X Center", "att": "xc", "value": x, "orig": x}, {"name": "Y Center", "att": "yc", "value": y, "orig": y}, @@ -178,8 +182,7 @@ def _unpack_get_subsets_for_ui(self): {"name": "Angle", "att": "theta", "value": theta, "orig": theta}) elif isinstance(subset_state.roi, EllipticalROI): - xc = subset_state.roi.xc - yc = subset_state.roi.yc + xc, yc = subset_state.roi.center() rx = subset_state.roi.radius_x ry = subset_state.roi.radius_y theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) @@ -190,6 +193,17 @@ def _unpack_get_subsets_for_ui(self): {"name": "Y Radius", "att": "radius_y", "value": ry, "orig": ry}, {"name": "Angle", "att": "theta", "value": theta, "orig": theta}] + elif isinstance(subset_state.roi, CircularAnnulusROI): + x, y = subset_state.roi.center() + inner_r = subset_state.roi.inner_radius + outer_r = subset_state.roi.outer_radius + subset_definition = [{"name": "X Center", "att": "xc", "value": x, "orig": x}, + {"name": "Y Center", "att": "yc", "value": y, "orig": y}, + {"name": "Inner radius", "att": "inner_radius", + "value": inner_r, "orig": inner_r}, + {"name": "Outer radius", "att": "outer_radius", + "value": outer_r, "orig": outer_r}] + subset_type = subset_state.roi.__class__.__name__ elif isinstance(subset_state, RangeSubsetState): @@ -303,6 +317,7 @@ def _check_input(self): reason = "" for index, sub in enumerate(self.subset_definitions): lo = hi = xmin = xmax = ymin = ymax = None + inner_radius = outer_radius = None for d_att in sub: if d_att["att"] == "lo": lo = d_att["value"] @@ -320,6 +335,10 @@ def _check_input(self): ymin = d_att["value"] elif d_att["att"] == "ymax": ymax = d_att["value"] + elif d_att["att"] == "outer_radius": + outer_radius = d_att["value"] + elif d_att["att"] == "inner_radius": + inner_radius = d_att["value"] if lo and hi and hi <= lo: status = False @@ -329,6 +348,10 @@ def _check_input(self): status = False reason = "Failed to update Subset: width and length must be positive scalars" break + elif inner_radius and outer_radius and inner_radius >= outer_radius: + status = False + reason = "Failed to update Subset: inner radius must be less than outer radius" + break return status, reason @@ -375,39 +398,13 @@ def get_center(self): depending on the Subset type, if applicable. If Subset is not centerable, this returns `None`. - Raises - ------ - NotImplementedError - Subset type is not supported. - """ # Composite region cannot be centered. if not self.is_centerable: # no-op return subset_state = self.subset_select.selected_subset_state - - if isinstance(subset_state, RoiSubsetState): - sbst_obj = subset_state.roi - if isinstance(sbst_obj, (CircularROI, EllipticalROI)): - cen = sbst_obj.get_center() - elif isinstance(sbst_obj, RectangularROI): - cen = sbst_obj.center() - else: # pragma: no cover - raise NotImplementedError( - f'Getting center of {sbst_obj.__class__} is not supported') - - elif isinstance(subset_state, RangeSubsetState): - cen = (subset_state.hi - subset_state.lo) * 0.5 + subset_state.lo - - elif isinstance(subset_state, CompositeSubsetState): - cen = None - - else: # pragma: no cover - raise NotImplementedError( - f'Getting center of {subset_state.__class__} is not supported') - - return cen + return subset_state.center() def set_center(self, new_cen, update=False): """Set the desired center for the selected Subset, if applicable. @@ -439,7 +436,7 @@ def set_center(self, new_cen, update=False): if isinstance(subset_state, RoiSubsetState): x, y = new_cen sbst_obj = subset_state.roi - if isinstance(sbst_obj, (CircularROI, EllipticalROI)): + if isinstance(sbst_obj, (CircularROI, CircularAnnulusROI, EllipticalROI)): self._set_value_in_subset_definition(0, "X Center", "value", x) self._set_value_in_subset_definition(0, "Y Center", "value", y) elif isinstance(sbst_obj, RectangularROI): @@ -454,7 +451,7 @@ def set_center(self, new_cen, update=False): raise NotImplementedError(f'Recentering of {sbst_obj.__class__} is not supported') elif isinstance(subset_state, RangeSubsetState): - dx = new_cen - ((subset_state.hi - subset_state.lo) * 0.5 + subset_state.lo) + dx = new_cen - subset_state.center() self._set_value_in_subset_definition(0, "Lower bound", "value", subset_state.lo + dx) self._set_value_in_subset_definition(0, "Upper bound", "value", subset_state.hi + dx) diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py index 46467b92a4..958d076fd1 100644 --- a/jdaviz/configs/imviz/plugins/viewers.py +++ b/jdaviz/configs/imviz/plugins/viewers.py @@ -24,7 +24,7 @@ class ImvizImageView(JdavizViewerMixin, BqplotImageView, AstrowidgetsImageViewer ['jdaviz:homezoom', 'jdaviz:prevzoom'], ['jdaviz:boxzoommatch', 'jdaviz:boxzoom'], ['jdaviz:panzoommatch', 'jdaviz:imagepanzoom'], - ['bqplot:circle', 'bqplot:rectangle', 'bqplot:ellipse', + ['bqplot:circle', 'bqplot:rectangle', 'bqplot:ellipse', 'bqplot:circannulus', 'jdaviz:singlepixelregion'], ['jdaviz:blinkonce', 'jdaviz:contrastbias'], ['jdaviz:sidebar_plot', 'jdaviz:sidebar_export', 'jdaviz:sidebar_compass'] diff --git a/jdaviz/core/tools.py b/jdaviz/core/tools.py index f86618d77e..f6dea95d86 100644 --- a/jdaviz/core/tools.py +++ b/jdaviz/core/tools.py @@ -9,8 +9,9 @@ HomeTool, BqplotPanZoomMode, BqplotPanZoomXMode, BqplotPanZoomYMode, BqplotRectangleMode, BqplotCircleMode, - BqplotEllipseMode, BqplotXRangeMode, - BqplotYRangeMode, BqplotSelectionTool, + BqplotEllipseMode, BqplotCircularAnnulusMode, + BqplotXRangeMode, BqplotYRangeMode, + BqplotSelectionTool, INTERACT_COLOR) from bqplot.interacts import BrushSelector, BrushIntervalSelector @@ -25,6 +26,7 @@ BqplotRectangleMode.icon = os.path.join(ICON_DIR, 'select_xy.svg') BqplotCircleMode.icon = os.path.join(ICON_DIR, 'select_circle.svg') BqplotEllipseMode.icon = os.path.join(ICON_DIR, 'select_ellipse.svg') +BqplotCircularAnnulusMode.icon = os.path.join(ICON_DIR, 'select_annulus.svg') BqplotXRangeMode.icon = os.path.join(ICON_DIR, 'select_x.svg') BqplotYRangeMode.icon = os.path.join(ICON_DIR, 'select_y.svg') diff --git a/jdaviz/data/icons/select_annulus.svg b/jdaviz/data/icons/select_annulus.svg new file mode 100644 index 0000000000..86863e1456 --- /dev/null +++ b/jdaviz/data/icons/select_annulus.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 8d4066bdf7..3d06573f56 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -3,9 +3,10 @@ from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from glue.core import Data -from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, XRangeROI +from glue.core.roi import CircularROI, CircularAnnulusROI, EllipticalROI, RectangularROI, XRangeROI from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode, XorMode -from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion +from regions import (PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion, + CircleAnnulusPixelRegion) from numpy.testing import assert_allclose from specutils import SpectralRegion, Spectrum1D @@ -316,7 +317,7 @@ def test_composite_region_from_subset_3d(cubeviz_helper): 'subset_state': reg[-1]['subset_state']} cubeviz_helper.app.session.edit_subset_mode.mode = OrMode - viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) + viewer.apply_roi(EllipticalROI(xc=30, yc=30, radius_x=3, radius_y=6)) reg = cubeviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), width=6, height=12, angle=0.0 * u.deg) @@ -368,7 +369,7 @@ def test_composite_region_with_consecutive_and_not_states(cubeviz_helper): 'subset_state': reg[-1]['subset_state']} cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode - viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) + viewer.apply_roi(EllipticalROI(xc=30, yc=30, radius_x=3, radius_y=6)) reg = cubeviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), width=6, height=12, angle=0.0 * u.deg) @@ -413,7 +414,7 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): arr = np.ones((10, 10)) data_label = 'image-data' - viewer = imviz_helper.app.get_viewer('imviz-0') + viewer = imviz_helper.default_viewer imviz_helper.load_data(arr, data_label=data_label, show_in_viewer=True) viewer.apply_roi(CircularROI(xc=5, yc=5, radius=2)) reg = imviz_helper.app.get_subsets("Subset 1") @@ -422,7 +423,7 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): 'subset_state': reg[-1]['subset_state']} imviz_helper.app.session.edit_subset_mode.mode = AndNotMode - viewer.apply_roi(RectangularROI(2, 4, 2, 4)) + viewer.apply_roi(RectangularROI(xmin=2, xmax=4, ymin=2, ymax=4)) reg = imviz_helper.app.get_subsets("Subset 1") rectangle1 = RectanglePixelRegion(center=PixCoord(x=3, y=3), width=2, height=2, angle=0.0 * u.deg) @@ -430,17 +431,25 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): 'subset_state': reg[-1]['subset_state']} imviz_helper.app.session.edit_subset_mode.mode = AndNotMode - viewer.apply_roi(EllipticalROI(3, 3, 3, 6)) + viewer.apply_roi(EllipticalROI(xc=3, yc=3, radius_x=3, radius_y=6)) reg = imviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=3, y=3), width=6, height=12, angle=0.0 * u.deg) assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1, 'subset_state': reg[-1]['subset_state']} + imviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(CircularAnnulusROI(xc=5, yc=5, inner_radius=2.5, outer_radius=5)) + reg = imviz_helper.app.get_subsets("Subset 1") + ann1 = CircleAnnulusPixelRegion(center=PixCoord(x=5, y=5), inner_radius=2.5, outer_radius=5) + assert reg[-1] == {'name': 'CircularAnnulusROI', 'glue_state': 'OrState', 'region': ann1, + 'subset_state': reg[-1]['subset_state']} + subset_plugin = imviz_helper.app.get_tray_item_from_name('g-subset-plugin') assert subset_plugin.subset_selected == "Subset 1" - assert subset_plugin.subset_types == ['CircularROI', 'RectangularROI', 'EllipticalROI'] - assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'AndNotState'] + assert subset_plugin.subset_types == ['CircularROI', 'RectangularROI', 'EllipticalROI', + 'CircularAnnulusROI'] + assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'AndNotState', 'OrState'] def test_with_invalid_subset_name(cubeviz_helper): diff --git a/pyproject.toml b/pyproject.toml index 44e46a75a0..e06a9f7943 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "bqplot>=0.12.37", "bqplot-image-gl>=1.4.11", "glue-core>=1.11", - "glue-jupyter>=0.16.3", + "glue-jupyter>=0.17", "echo>=0.5.0", "ipykernel>=6.19.4", "ipyvue>=1.6", @@ -26,7 +26,7 @@ dependencies = [ "specutils>=1.9", "specreduce>=1.3.0,<1.4.0", "photutils>=1.4", - "glue-astronomy>=0.9", + "glue-astronomy>=0.10", "asteval>=0.9.23", "idna", "vispy>=0.6.5", From 55a19f4fe455a02e09e910a77c7bfee4c3e6480f Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 29 Jun 2023 08:20:31 -0700 Subject: [PATCH 131/283] MNT: Temporarily pin voila<0.5 (#2269) * MNT: Temp pin voila<0.5 because voila-template is incompatible, see #2268 * TST: Disable voila dev in test matrix --- pyproject.toml | 2 +- tox.ini | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e06a9f7943..3d1023b832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "ipysplitpanes>=0.1.0", "ipygoldenlayout>=0.3.0", "ipywidgets>=8.0.6", - "voila>=0.4", + "voila>=0.4,<0.5", "pyyaml>=5.4.1", "specutils>=1.9", "specreduce>=1.3.0,<1.4.0", diff --git a/tox.ini b/tox.ini index c101fc241b..d212680cbd 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,8 @@ deps = devdeps: git+https://github.com/spacetelescope/stdatamodels.git devdeps: git+https://github.com/bqplot/bqplot.git@0.12.x devdeps: git+https://github.com/glue-viz/glue.git - devdeps: git+https://github.com/voila-dashboards/voila.git + # FIXME: https://github.com/spacetelescope/jdaviz/pull/2268 + #devdeps: git+https://github.com/voila-dashboards/voila.git devdeps: git+https://github.com/glue-viz/bqplot-image-gl.git devdeps: git+https://github.com/glue-viz/glue-jupyter.git devdeps: git+https://github.com/glue-viz/glue-astronomy.git From a9f13e9cad6de60e5a96611651bf8207e8dd76a7 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:37:46 -0700 Subject: [PATCH 132/283] TST: Ignore DeprecationWarning from asteval (#2274) * TST: Ignore DeprecationWarning from asteval. For example https://github.com/newville/asteval/issues/120 * Also ignore FutureWarning from asteval --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3d1023b832..08a722738e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,8 @@ filterwarnings = [ "ignore::DeprecationWarning:bqscales", "ignore::DeprecationWarning:traittypes", "ignore::DeprecationWarning:voila", + "ignore::DeprecationWarning:asteval", + "ignore::FutureWarning:asteval", "ignore:::specutils.spectra.spectrum1d", ] From 40188835f30f6f2a7bb17a23f05eb553bdfe70eb Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 30 Jun 2023 17:12:13 -0400 Subject: [PATCH 133/283] Add doc for windows latency issue --- docs/known_bugs.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/known_bugs.rst b/docs/known_bugs.rst index 086f959a35..37112b2901 100644 --- a/docs/known_bugs.rst +++ b/docs/known_bugs.rst @@ -130,6 +130,14 @@ the side of the cell output to collapse or expand the scrollable window. This has the unintended consequence of changing the contrast of the image displayed in the Cubeviz cube viewer. +On Windows OS, latency increases exponentially with number of subsets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This seems to be an issue with Windows OS specifically, although we are still +investigating exactly what causes it. +See `Issue #2263 `_ for +updates on this topic. + .. _known_issues_imviz: Imviz From 2c03ef14320bb1c0205518e8ce849b3ff3c38885 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 29 Jun 2023 10:34:11 -0400 Subject: [PATCH 134/283] Deprecate load_spectrum --- jdaviz/configs/specviz/helper.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 3a7c843bcd..11c4a875a7 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -1,7 +1,7 @@ import warnings from astropy import units as u -from astropy.utils.decorators import deprecated_renamed_argument +from astropy.utils.decorators import deprecated_renamed_argument, deprecated from regions.core.core import Region from glue.core.subset_group import GroupedSubset from specutils import SpectralRegion, Spectrum1D @@ -41,11 +41,35 @@ def __init__(self, *args, **kwargs): self.app.hub.subscribe(self, RedshiftMessage, handler=self._redshift_listener) + @deprecated(since="3.6", alternative="specviz.load_data") def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, concat_by_file=False): """ Loads a data file or `~specutils.Spectrum1D` object into Specviz. + Parameters + ---------- + data : str, `~specutils.Spectrum1D`, or `~specutils.SpectrumList` + Spectrum1D, SpectrumList, or path to compatible data file. + data_label : str + The Glue data label found in the ``DataCollection``. + format : str + Loader format specification used to indicate data format in + `~specutils.Spectrum1D.read` io method. + show_in_viewer : bool + Show data in viewer(s). + concat_by_file : bool + If True and there is more than one available extension, concatenate + the extensions within each spectrum file passed to the parser and + add a concatenated spectrum to the data collection. + """ + self.load_data(data, data_label, format, show_in_viewer, concat_by_file) + + def load_data(self, data, data_label=None, format=None, show_in_viewer=True, + concat_by_file=False): + """ + Loads a data file or `~specutils.Spectrum1D` object into Specviz. + Parameters ---------- data : str, `~specutils.Spectrum1D`, or `~specutils.SpectrumList` From eb623653a112ae15b3a47e48689682dd887a60a6 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 29 Jun 2023 12:23:55 -0400 Subject: [PATCH 135/283] Update tests and docs from load_spectrum --- docs/cubeviz/import_data.rst | 4 +-- docs/specviz/export_data.rst | 2 +- docs/specviz/import_data.rst | 20 +++++------ .../line_lists/tests/test_line_lists.py | 8 ++--- .../model_fitting/tests/test_plugin.py | 18 +++++----- .../subset_plugin/tests/test_subset_plugin.py | 2 +- jdaviz/configs/specviz/helper.py | 2 +- .../line_analysis/tests/test_line_analysis.py | 26 +++++++------- .../line_analysis/tests/test_lineflux.py | 4 +-- .../tests/test_unit_conversion.py | 8 ++--- jdaviz/configs/specviz/tests/test_helper.py | 34 +++++++++---------- jdaviz/core/data_formats.py | 7 ++-- jdaviz/core/tests/test_data_menu.py | 4 +-- jdaviz/core/tests/test_helpers.py | 4 +-- jdaviz/core/tests/test_template_mixin.py | 2 +- jdaviz/tests/test_app.py | 30 ++++++++-------- jdaviz/tests/test_subsets.py | 10 +++--- 17 files changed, 91 insertions(+), 94 deletions(-) diff --git a/docs/cubeviz/import_data.rst b/docs/cubeviz/import_data.rst index 993ad8969f..b421dd3199 100644 --- a/docs/cubeviz/import_data.rst +++ b/docs/cubeviz/import_data.rst @@ -74,7 +74,7 @@ Importing data via the API Alternatively, users who work in a coding environment like a Jupyter notebook can access the Cubeviz helper class API. Using this API, users can -load data into the application through code with the :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` +load data into the application through code with the :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` method, which takes as input a :class:`~specutils.Spectrum1D` object. FITS Files @@ -162,7 +162,7 @@ object, you can load it into Cubeviz as follows: # Create your spectrum1 spec3d = Spectrum1D(data, wcs=my_wcs) cubeviz = Cubeviz() - cubeviz.load_spectrum(spec3d, data_label='My Cube') + cubeviz.load_data(spec3d, data_label='My Cube') cubeviz.show() There is no plan to natively load such objects until ``datamodels`` diff --git a/docs/specviz/export_data.rst b/docs/specviz/export_data.rst index e6d322acb6..5f7111bad0 100644 --- a/docs/specviz/export_data.rst +++ b/docs/specviz/export_data.rst @@ -45,7 +45,7 @@ spectrum containing only your subset by running: spec = specviz.get_data(spectral_subset='Subset 1') subset_spec = Spectrum1D(flux=spec.flux[~spec.mask], spectral_axis=spec.spectral_axis[~spec.mask]) - specviz.load_spectrum(subset_spec) + specviz.load_data(subset_spec) .. seealso:: diff --git a/docs/specviz/import_data.rst b/docs/specviz/import_data.rst index e30aa19f02..e1e10303b2 100644 --- a/docs/specviz/import_data.rst +++ b/docs/specviz/import_data.rst @@ -51,7 +51,7 @@ Importing data via the API Alternatively, users who work in a coding environment like a Jupyter notebook can access the Specviz helper class API. Using this API, users can load data into the application through code with the -:py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` +:py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` method, which takes as input a :class:`~specutils.Spectrum1D` object. FITS Files @@ -64,15 +64,15 @@ The example below loads a FITS file into Specviz: from specutils import Spectrum1D spec1d = Spectrum1D.read("/path/to/data/file") specviz = Specviz() - specviz.load_spectrum(spec1d, data_label="my_spec") + specviz.load_data(spec1d, data_label="my_spec") specviz.show() You can also pass the path to a file that `~specutils.Spectrum1D` understands directly to the -:py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` method: +:py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` method: .. code-block:: python - specviz.load_spectrum("path/to/data/file") + specviz.load_data("path/to/data/file") Creating Your Own Array ----------------------- @@ -90,7 +90,7 @@ You can create your own array to load into Specviz: wavelength = np.arange(5100, 5300) * u.AA spec1d = Spectrum1D(spectral_axis=wavelength, flux=flux) specviz = Specviz() - specviz.load_spectrum(spec1d, data_label="my_spec") + specviz.load_data(spec1d, data_label="my_spec") specviz.show() JWST datamodels @@ -111,7 +111,7 @@ object, you can load it into Specviz as follows: spec1d = Spectrum1D(flux=flux, spectral_axis=wave) specviz = Specviz() - specviz.load_spectrum(spec1d, data_label="MultiSpecModel") + specviz.load_data(spec1d, data_label="MultiSpecModel") specviz.show() There is no plan to natively load such objects until ``datamodels`` @@ -122,7 +122,7 @@ is separated from the ``jwst`` pipeline package. Importing a SpectrumList ------------------------ -The :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` also accepts +The :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` also accepts a `~specutils.SpectrumList` object, in which case it will both load the individual `~specutils.Spectrum1D` objects in the list and additionally attempt to stitch together the spectra into a single data object so that @@ -132,7 +132,7 @@ they can be manipulated and analyzed in the application as a single entity: from specutils import SpectrumList spec_list = SpectrumList([spec1d_1, spec1d_2]) - specviz.load_spectrum(spec_list) + specviz.load_data(spec_list) specviz.show() In the screenshot below, the combined spectrum is plotted in gray, and one of @@ -145,13 +145,13 @@ end of the red region in the screenshot below: .. image:: img/spectrumlist_combined.png This functionality is also available in limited instances by providing a directory path -to the :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` method. Note +to the :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` method. Note that the ``read`` method of :class:`~specutils.SpectrumList` is only set up to handle directory input in limited cases, for example JWST MIRI MRS data, and will throw an error in other cases. In cases that it does work, only files in the directory level specified will be read, with no recursion into deeper folders. -The :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_spectrum` method also takes +The :py:meth:`~jdaviz.configs.specviz.helper.Specviz.load_data` method also takes an optional keyword argument ``concat_by_file``. When set to ``True``, the spectra loaded in the :class:`~specutils.SpectrumList` will be concatenated together into one combined spectrum per loaded file, which may be useful for MIRI observations, for example. diff --git a/jdaviz/configs/default/plugins/line_lists/tests/test_line_lists.py b/jdaviz/configs/default/plugins/line_lists/tests/test_line_lists.py index 2af3cae334..e85959f49f 100644 --- a/jdaviz/configs/default/plugins/line_lists/tests/test_line_lists.py +++ b/jdaviz/configs/default/plugins/line_lists/tests/test_line_lists.py @@ -12,7 +12,7 @@ def test_line_lists(specviz_helper): spec = Spectrum1D(flux=np.random.rand(100)*u.Jy, spectral_axis=np.arange(6000, 7000, 10)*u.AA) - specviz_helper.load_spectrum(spec) + specviz_helper.load_data(spec) lt = QTable() lt['linename'] = ['O III', 'Halpha'] @@ -51,7 +51,7 @@ def test_redshift(specviz_helper, spectrum1d): assert plg._obj.disabled_msg label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) assert not plg._obj.disabled_msg @@ -102,7 +102,7 @@ def test_redshift(specviz_helper, spectrum1d): def test_load_available_preset_lists(specviz_helper, spectrum1d): """ Loads all available line lists and checks the medium requirement """ label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) # Check to make sure we got our line lists available_linelists = get_available_linelists() @@ -125,7 +125,7 @@ def test_load_available_preset_lists(specviz_helper, spectrum1d): def test_line_identify(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) lt = QTable() lt['linename'] = ['O III', 'Halpha'] diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py index 42232520bc..4c9d89e072 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py @@ -20,7 +20,7 @@ def test_default_model_labels(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) modelfit_plugin = specviz_helper.plugins['Model Fitting'] # By default, the spectral region should be the entire spectrum assert modelfit_plugin._obj.spectral_subset_selected == "Entire Spectrum" @@ -46,7 +46,7 @@ def test_default_model_labels(specviz_helper, spectrum1d): def test_custom_model_labels(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) modelfit_plugin = specviz_helper.plugins['Model Fitting'] for i, model in enumerate(MODELS): @@ -68,7 +68,7 @@ def test_register_model_with_uncertainty_weighting(specviz_helper, spectrum1d): spectrum1d.uncertainty = StdDevUncertainty(spectrum1d.flux * 0.1) with warnings.catch_warnings(): warnings.simplefilter('ignore') - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) modelfit_plugin = specviz_helper.plugins['Model Fitting'] # Test registering a simple linear fit @@ -105,7 +105,7 @@ def test_register_model_uncertainty_is_none(specviz_helper, spectrum1d): spectrum1d.uncertainty = None with warnings.catch_warnings(): warnings.simplefilter('ignore') - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) modelfit_plugin = specviz_helper.plugins['Model Fitting'] # Test registering a simple linear fit @@ -157,7 +157,7 @@ def test_register_cube_model(cubeviz_helper, spectrum1d_cube): def test_user_api(specviz_helper, spectrum1d): with warnings.catch_warnings(): warnings.simplefilter('ignore') - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) p = specviz_helper.plugins['Model Fitting'] # even though the default label is set to C, adding Linear1D should default to its automatic @@ -195,7 +195,7 @@ def test_user_api(specviz_helper, spectrum1d): def test_fit_gaussian_with_fixed_mean(specviz_helper, spectrum1d): with warnings.catch_warnings(): warnings.simplefilter('ignore') - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) modelfit_plugin = specviz_helper.plugins['Model Fitting'] modelfit_plugin.create_model_component('Gaussian1D', 'G') @@ -217,7 +217,7 @@ def test_fit_gaussian_with_fixed_mean(specviz_helper, spectrum1d): def test_reestimate_parameters(specviz_helper, spectrum1d): with warnings.catch_warnings(): warnings.simplefilter('ignore') - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) mf = specviz_helper.plugins['Model Fitting'] mf.create_model_component('Gaussian1D', 'G') @@ -327,12 +327,12 @@ def test_subset_masks(cubeviz_helper, spectrum1d_cube_larger): def test_invalid_subset(specviz_helper, spectrum1d): # 6000-8000 - specviz_helper.load_spectrum(spectrum1d, data_label="right_spectrum") + specviz_helper.load_data(spectrum1d, data_label="right_spectrum") # 5000-7000 sp2 = Spectrum1D(spectral_axis=spectrum1d.spectral_axis - 1000*spectrum1d.spectral_axis.unit, flux=spectrum1d.flux * 1.25) - specviz_helper.load_spectrum(sp2, data_label="left_spectrum") + specviz_helper.load_data(sp2, data_label="left_spectrum") # apply subset that overlaps on left_spectrum, but not right_spectrum # NOTE: using a subset that overlaps the right_spectrum (reference) results in errors when diff --git a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py index b867e62489..3d0b4e094a 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py @@ -6,7 +6,7 @@ @pytest.mark.filterwarnings('ignore') def test_plugin(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) p = specviz_helper.plugins['Subset Tools'] # regression test for https://github.com/spacetelescope/jdaviz/issues/1693 diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 11c4a875a7..54d5b0cda7 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -66,7 +66,7 @@ def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, self.load_data(data, data_label, format, show_in_viewer, concat_by_file) def load_data(self, data, data_label=None, format=None, show_in_viewer=True, - concat_by_file=False): + concat_by_file=False): """ Loads a data file or `~specutils.Spectrum1D` object into Specviz. diff --git a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py index 34812a2453..682d6040fc 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_line_analysis.py @@ -14,7 +14,7 @@ def test_plugin(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -79,7 +79,7 @@ def test_spatial_subset(cubeviz_helper, image_cube_hdu_obj): def test_user_api(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) sv = specviz_helper.app.get_viewer('spectrum-viewer') sv.apply_roi(XRangeROI(6500, 7400)) @@ -109,7 +109,7 @@ def test_user_api(specviz_helper, spectrum1d): def test_line_identify(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) lt = QTable() lt['linename'] = ['O III', 'Halpha'] @@ -180,7 +180,7 @@ def test_coerce_unit(): def test_continuum_surrounding_spectral_subset(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -207,7 +207,7 @@ def test_continuum_surrounding_spectral_subset(specviz_helper, spectrum1d): def test_continuum_spectral_same_value(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -234,7 +234,7 @@ def test_continuum_spectral_same_value(specviz_helper, spectrum1d): def test_continuum_surrounding_invalid_width(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -259,7 +259,7 @@ def test_continuum_surrounding_invalid_width(specviz_helper, spectrum1d): def test_continuum_subset_spectral_entire(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -286,7 +286,7 @@ def test_continuum_subset_spectral_entire(specviz_helper, spectrum1d): def test_continuum_subset_spectral_subset2(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -319,7 +319,7 @@ def test_continuum_subset_spectral_subset2(specviz_helper, spectrum1d): def test_continuum_surrounding_no_right(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -347,7 +347,7 @@ def test_continuum_surrounding_no_right(specviz_helper, spectrum1d): def test_continuum_surrounding_no_left(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -375,7 +375,7 @@ def test_continuum_surrounding_no_left(specviz_helper, spectrum1d): def test_subset_changed(specviz_helper, spectrum1d): label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + specviz_helper.load_data(spectrum1d, data_label=label) plugin = specviz_helper.app.get_tray_item_from_name('specviz-line-analysis') plugin.open_in_tray() @@ -406,12 +406,12 @@ def test_subset_changed(specviz_helper, spectrum1d): def test_invalid_subset(specviz_helper, spectrum1d): # 6000-8000 - specviz_helper.load_spectrum(spectrum1d, data_label="right_spectrum") + specviz_helper.load_data(spectrum1d, data_label="right_spectrum") # 5000-7000 sp2 = Spectrum1D(spectral_axis=spectrum1d.spectral_axis - 1000*spectrum1d.spectral_axis.unit, flux=spectrum1d.flux * 1.25) - specviz_helper.load_spectrum(sp2, data_label="left_spectrum") + specviz_helper.load_data(sp2, data_label="left_spectrum") # apply subset that overlaps on left_spectrum, but not right_spectrum # NOTE: using a subset that overlaps the right_spectrum (reference) results in errors when diff --git a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py index 7ec76cd0aa..3e1b07b9ce 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py @@ -99,7 +99,7 @@ def test_unit_gaussian(specviz_helper, test_case): Test an Area 1 Gaussian and ensure the result returns in W/m2 Test provided by Patrick Ogle ''' - specviz_helper.load_spectrum(test_case) + specviz_helper.load_data(test_case) lineflux_result = _calculate_line_flux(specviz_helper) assert_quantity_allclose(float(lineflux_result['result']) * u.Unit(lineflux_result['unit']), @@ -117,7 +117,7 @@ def test_unit_gaussian_mixed_units_per_steradian(specviz_helper): flx_wave = _gauss_with_unity_area(lam_a.value, mn, sig)*1E3*u.erg/u.s/u.cm**2/u.Angstrom/u.sr fl_wave = Spectrum1D(spectral_axis=lam_a, flux=flx_wave) - specviz_helper.load_spectrum(fl_wave) + specviz_helper.load_data(fl_wave) lineflux_result = _calculate_line_flux(specviz_helper) assert_quantity_allclose(float(lineflux_result['result']) * u.Unit(lineflux_result['unit']), 1*u.Unit('W/(m2sr)')) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py index 245c19c124..d69e93b35f 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py @@ -10,7 +10,7 @@ ("micron", "fail", "micron", "Jy")]) def test_value_error_exception(specviz_helper, spectrum1d, new_spectral_axis, new_flux, expected_spectral_axis, expected_flux): - specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + specviz_helper.load_data(spectrum1d, data_label="Test 1D Spectrum") viewer = specviz_helper.app.get_viewer("spectrum-viewer") plg = specviz_helper.plugins["Unit Conversion"] @@ -34,7 +34,7 @@ def test_value_error_exception(specviz_helper, spectrum1d, new_spectral_axis, ne def test_conv_wave_only(specviz_helper, spectrum1d, uncert): if uncert is False: spectrum1d.uncertainty = None - specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + specviz_helper.load_data(spectrum1d, data_label="Test 1D Spectrum") viewer = specviz_helper.app.get_viewer("spectrum-viewer") plg = specviz_helper.plugins["Unit Conversion"] @@ -50,7 +50,7 @@ def test_conv_wave_only(specviz_helper, spectrum1d, uncert): def test_conv_flux_only(specviz_helper, spectrum1d, uncert): if uncert is False: spectrum1d.uncertainty = None - specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + specviz_helper.load_data(spectrum1d, data_label="Test 1D Spectrum") viewer = specviz_helper.app.get_viewer("spectrum-viewer") plg = specviz_helper.plugins["Unit Conversion"] @@ -66,7 +66,7 @@ def test_conv_flux_only(specviz_helper, spectrum1d, uncert): def test_conv_wave_flux(specviz_helper, spectrum1d, uncert): if uncert is False: spectrum1d.uncertainty = None - specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + specviz_helper.load_data(spectrum1d, data_label="Test 1D Spectrum") viewer = specviz_helper.app.get_viewer("spectrum-viewer") plg = specviz_helper.plugins["Unit Conversion"] diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index bade4d68aa..68ae8870c6 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -23,7 +23,7 @@ def setup_class(self, specviz_helper, spectrum1d, multi_order_spectrum_list): self.multi_order_spectrum_list = multi_order_spectrum_list self.label = "Test 1D Spectrum" - self.spec_app.load_spectrum(spectrum1d, data_label=self.label) + self.spec_app.load_data(spectrum1d, data_label=self.label) def test_load_spectrum1d(self): # starts with a single loaded spectrum1d object: @@ -38,7 +38,7 @@ def test_load_spectrum1d(self): def test_load_spectrum_list_no_labels(self): # now load three more spectra from a SpectrumList, without labels - self.spec_app.load_spectrum(self.spec_list) + self.spec_app.load_data(self.spec_list) assert len(self.spec_app.app.data_collection) == 4 for i in (1, 2, 3): assert "specviz_data" in self.spec_app.app.data_collection[i].label @@ -46,24 +46,24 @@ def test_load_spectrum_list_no_labels(self): def test_load_spectrum_list_with_labels(self): # now load three more spectra from a SpectrumList, with labels: labels = ["List test 1", "List test 2", "List test 3"] - self.spec_app.load_spectrum(self.spec_list, data_label=labels) + self.spec_app.load_data(self.spec_list, data_label=labels) assert len(self.spec_app.app.data_collection) == 4 def test_load_multi_order_spectrum_list(self): assert len(self.spec_app.app.data_collection) == 1 # now load ten spectral orders from a SpectrumList: - self.spec_app.load_spectrum(self.multi_order_spectrum_list) + self.spec_app.load_data(self.multi_order_spectrum_list) assert len(self.spec_app.app.data_collection) == 11 def test_mismatched_label_length(self): with pytest.raises(ValueError, match='Length'): labels = ["List test 1", "List test 2"] - self.spec_app.load_spectrum(self.spec_list, data_label=labels) + self.spec_app.load_data(self.spec_list, data_label=labels) def test_load_spectrum_collection(self): with pytest.raises(TypeError): collection = SpectrumCollection([1]*u.AA) - self.spec_app.load_spectrum(collection) + self.spec_app.load_data(collection) def test_get_spectra(self): with pytest.warns(UserWarning, match='Applying the value from the redshift slider'): @@ -245,15 +245,15 @@ def test_get_spectra_no_spectra_label_redshift_error(specviz_helper, spectrum1d) def test_add_spectrum_after_subset(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_data(spectrum1d, data_label="test") specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6200, 7000)) new_spec = specviz_helper.get_spectra(apply_slider_redshift=True)["test"]*0.9 - specviz_helper.load_spectrum(new_spec, data_label="test2") + specviz_helper.load_data(new_spec, data_label="test2") def test_get_spectral_regions_unit(specviz_helper, spectrum1d): # Ensure units we put in are the same as the units we get out - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6200, 7000)) subsets = specviz_helper.get_spectral_regions() @@ -275,7 +275,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): # If the reference (visible) data changes via unit conversion, # check that the region's units convert too - specviz_helper.load_spectrum(spectrum1d) # Originally Angstrom + specviz_helper.load_data(spectrum1d) # Originally Angstrom # Also check coordinates info panel. # x=0 -> 6000 A, x=1 -> 6222.222 A @@ -322,7 +322,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): def test_subset_default_thickness(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) sv = specviz_helper.app.get_viewer('spectrum-viewer') sv.toolbar.active_tool = sv.toolbar.tools['bqplot:xrange'] @@ -350,7 +350,7 @@ def test_load_spectrum_list_directory(tmpdir, specviz_helper): # Load two NIRISS x1d files from FITS. They have 19 and 20 EXTRACT1D # extensions per file, for a total of 39 spectra to load: with pytest.warns(UserWarning, match='SRCTYPE is missing or UNKNOWN in JWST x1d loader'): - specviz_helper.load_spectrum(data_path) + specviz_helper.load_data(data_path) # NOTE: the length was 3 before specutils 1.9 (https://github.com/astropy/specutils/pull/982) expected_len = 39 @@ -377,12 +377,12 @@ def test_load_spectrum_list_directory_concat(tmpdir, specviz_helper): # spectra common to each file into one "Combined" spectrum to load per file. # Now the total is (19 EXTRACT 1D + 1 Combined) + (20 EXTRACT 1D + 1 Combined) = 41. with pytest.warns(UserWarning, match='SRCTYPE is missing or UNKNOWN in JWST x1d loader'): - specviz_helper.load_spectrum(data_path, concat_by_file=True) + specviz_helper.load_data(data_path, concat_by_file=True) assert len(specviz_helper.app.data_collection) == 41 def test_plot_uncertainties(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) specviz_viewer = specviz_helper.app.get_viewer("spectrum-viewer") @@ -416,7 +416,7 @@ def test_plugin_user_apis(specviz_helper): def test_data_label_as_posarg(specviz_helper, spectrum1d): # Passing in data_label keyword as posarg. - specviz_helper.load_spectrum(spectrum1d, 'my_spec') + specviz_helper.load_data(spectrum1d, 'my_spec') assert specviz_helper.app.data_collection[0].label == 'my_spec' @@ -431,8 +431,8 @@ def test_spectra_partial_overlap(specviz_helper): flux_2 = ([60] * wave_2.size) * u.nJy sp_2 = Spectrum1D(flux=flux_2, spectral_axis=wave_2) - specviz_helper.load_spectrum(sp_1, data_label='left') - specviz_helper.load_spectrum(sp_2, data_label='right') + specviz_helper.load_data(sp_1, data_label='left') + specviz_helper.load_data(sp_2, data_label='right') # Test mouseover outside of left but in range for right. # Should show right spectrum even when mouse is near left flux. diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index c26104c663..6ede5130a9 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -293,7 +293,7 @@ def open(filename, show=True, **kwargs): show : bool Determines whether to immediately show the application - All other arguments are interpreted as load_data/load_spectrum arguments for + All other arguments are interpreted as load_data arguments for the autoidentified configuration class Returns @@ -312,10 +312,7 @@ def open(filename, show=True, **kwargs): # Load data data = hdul if (hdul is not None) else filename - if helper_str == "specviz": - viz_helper.load_spectrum(data, **kwargs) - else: - viz_helper.load_data(data, **kwargs) + viz_helper.load_data(data, **kwargs) # Display app if show: diff --git a/jdaviz/core/tests/test_data_menu.py b/jdaviz/core/tests/test_data_menu.py index f092c2994c..40cf1c0c41 100644 --- a/jdaviz/core/tests/test_data_menu.py +++ b/jdaviz/core/tests/test_data_menu.py @@ -29,11 +29,11 @@ def test_data_menu_toggles(specviz_helper, spectrum1d): # load 2 data entries - specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_data(spectrum1d, data_label="test") app = specviz_helper.app sv = app.get_viewer('spectrum-viewer') new_spec = specviz_helper.get_spectra(apply_slider_redshift=True)["test"]*0.9 - specviz_helper.load_spectrum(new_spec, data_label="test2") + specviz_helper.load_data(new_spec, data_label="test2") # check that both are enabled in the data menu selected_data_items = app._viewer_item_by_id('specviz-0')['selected_data_items'] diff --git a/jdaviz/core/tests/test_helpers.py b/jdaviz/core/tests/test_helpers.py index 6b32860f30..d651a2260c 100644 --- a/jdaviz/core/tests/test_helpers.py +++ b/jdaviz/core/tests/test_helpers.py @@ -37,8 +37,8 @@ def setup_class(self, specviz_helper, spectrum1d, multi_order_spectrum_list): self.spec2 = spectrum1d._copy(spectral_axis=spectrum1d.spectral_axis+1000*u.AA) self.label2 = "Test 1D Spectrum 2" - self.spec_app.load_spectrum(spectrum1d, data_label=self.label) - self.spec_app.load_spectrum(self.spec2, data_label=self.label2) + self.spec_app.load_data(spectrum1d, data_label=self.label) + self.spec_app.load_data(self.spec2, data_label=self.label2) # Add 3 subsets to cover different parts of spec and spec2 self.spec_app.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6000, 6500)) diff --git a/jdaviz/core/tests/test_template_mixin.py b/jdaviz/core/tests/test_template_mixin.py index 3523896965..3e251f2d82 100644 --- a/jdaviz/core/tests/test_template_mixin.py +++ b/jdaviz/core/tests/test_template_mixin.py @@ -11,7 +11,7 @@ def test_spectralsubsetselect(specviz_helper, spectrum1d): mask = spectrum1d.flux < spectrum1d.flux.mean() spectrum1d.mask = mask - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) sv = specviz_helper.app.get_viewer('spectrum-viewer') # create a "Subset 1" entry sv.apply_roi(XRangeROI(6500, 7400)) diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py index c72d4fee27..612c108fe4 100644 --- a/jdaviz/tests/test_app.py +++ b/jdaviz/tests/test_app.py @@ -53,26 +53,26 @@ class Customviz(Specviz): viz = Customviz() assert viz.app.get_viewer_reference_names() == ['h', 'k'] - viz.load_spectrum(spectrum1d, data_label='example label') + viz.load_data(spectrum1d, data_label='example label') with pytest.raises(ValueError): viz.get_data("non-existent label") def test_duplicate_data_labels(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test") - specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_data(spectrum1d, data_label="test") + specviz_helper.load_data(spectrum1d, data_label="test") dc = specviz_helper.app.data_collection assert dc[0].label == "test" assert dc[1].label == "test (1)" - specviz_helper.load_spectrum(spectrum1d, data_label="test_1") - specviz_helper.load_spectrum(spectrum1d, data_label="test") + specviz_helper.load_data(spectrum1d, data_label="test_1") + specviz_helper.load_data(spectrum1d, data_label="test") assert dc[2].label == "test_1" assert dc[3].label == "test (2)" def test_duplicate_data_labels_with_brackets(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") - specviz_helper.load_spectrum(spectrum1d, data_label="test[test]") + specviz_helper.load_data(spectrum1d, data_label="test[test]") + specviz_helper.load_data(spectrum1d, data_label="test[test]") dc = specviz_helper.app.data_collection assert len(dc) == 2 assert dc[0].label == "test[test]" @@ -106,7 +106,7 @@ def test_unique_name_variations(specviz_helper, spectrum1d): data_label = specviz_helper.app.return_unique_name(None) assert data_label == "Unknown" - specviz_helper.load_spectrum(spectrum1d, data_label="test[flux]") + specviz_helper.load_data(spectrum1d, data_label="test[flux]") data_label = specviz_helper.app.return_data_label("test[flux]", ext="flux") assert data_label == "test[flux][flux]" @@ -115,8 +115,8 @@ def test_unique_name_variations(specviz_helper, spectrum1d): def test_substring_in_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="M31") - specviz_helper.load_spectrum(spectrum1d, data_label="M32") + specviz_helper.load_data(spectrum1d, data_label="M31") + specviz_helper.load_data(spectrum1d, data_label="M32") data_label = specviz_helper.app.return_data_label("M") assert data_label == "M" @@ -127,17 +127,17 @@ def test_substring_in_label(specviz_helper, spectrum1d): def test_edge_cases(specviz_helper, spectrum1d, data_label): dc = specviz_helper.app.data_collection - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + specviz_helper.load_data(spectrum1d, data_label=data_label) + specviz_helper.load_data(spectrum1d, data_label=data_label) assert dc[1].label == f"{data_label} (1)" - specviz_helper.load_spectrum(spectrum1d, data_label=data_label) + specviz_helper.load_data(spectrum1d, data_label=data_label) assert dc[2].label == f"{data_label} (2)" def test_case_that_used_to_break_return_label(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break (1)") - specviz_helper.load_spectrum(spectrum1d, data_label="this used to break") + specviz_helper.load_data(spectrum1d, data_label="this used to break (1)") + specviz_helper.load_data(spectrum1d, data_label="this used to break") dc = specviz_helper.app.data_collection assert dc[0].label == "this used to break (1)" assert dc[1].label == "this used to break (2)" diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 3d06573f56..2c9acbcd80 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -459,7 +459,7 @@ def test_with_invalid_subset_name(cubeviz_helper): def test_composite_region_from_subset_2d(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) viewer.apply_roi(XRangeROI(6000, 7000)) reg = specviz_helper.app.get_subsets("Subset 1", simplify_spectral=False) @@ -505,7 +505,7 @@ def test_composite_region_from_subset_2d(specviz_helper, spectrum1d): def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) viewer.apply_roi(XRangeROI(6200, 6800)) @@ -554,7 +554,7 @@ def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): def test_edit_composite_spectral_with_xor(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) viewer.apply_roi(XRangeROI(6400, 6600)) @@ -574,7 +574,7 @@ def test_edit_composite_spectral_with_xor(specviz_helper, spectrum1d): def test_overlapping_spectral_regions(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) viewer.apply_roi(XRangeROI(6400, 7400)) @@ -593,7 +593,7 @@ def test_overlapping_spectral_regions(specviz_helper, spectrum1d): def test_only_overlapping_spectral_regions(specviz_helper, spectrum1d): - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) viewer.apply_roi(XRangeROI(6400, 6600)) From 91d6978b7f15f187f124777d0f7a34110e221fa1 Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Mon, 3 Jul 2023 10:12:05 -0400 Subject: [PATCH 136/283] Docstring suggestions Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- jdaviz/configs/specviz/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 54d5b0cda7..e2066a9435 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -41,7 +41,7 @@ def __init__(self, *args, **kwargs): self.app.hub.subscribe(self, RedshiftMessage, handler=self._redshift_listener) - @deprecated(since="3.6", alternative="specviz.load_data") + @deprecated(since="3.6", alternative="load_data") def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, concat_by_file=False): """ @@ -68,7 +68,7 @@ def load_spectrum(self, data, data_label=None, format=None, show_in_viewer=True, def load_data(self, data, data_label=None, format=None, show_in_viewer=True, concat_by_file=False): """ - Loads a data file or `~specutils.Spectrum1D` object into Specviz. + Load data into Specviz. Parameters ---------- From 40fd9015bd349467f7ce387c2507cef554be7354 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 3 Jul 2023 10:20:20 -0400 Subject: [PATCH 137/283] Changelog --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3b6639f105..ef8590659e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ New Features - Plots within plugins can now be popped-out into their own windows. [#2254] +- Replace specviz.load_spectrum with specviz.load_data. [#2273] + Cubeviz ^^^^^^^ From a569123d924f32d1d8b0868cc27c2e0fa57768ec Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 3 Jul 2023 10:25:37 -0400 Subject: [PATCH 138/283] Changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ef8590659e..2ac6ecbfc2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,7 +11,7 @@ New Features - Plots within plugins can now be popped-out into their own windows. [#2254] -- Replace specviz.load_spectrum with specviz.load_data. [#2273] +- The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] Cubeviz ^^^^^^^ From 24ff31420bd3181e54b97318730173b0b019766a Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 3 Jul 2023 07:43:28 -0700 Subject: [PATCH 139/283] Button to export Cubeviz movie (#2264) * WIP: Backend API to write movie file but got stuck with bqplot not cooperating after first frame. [ci skip] [rtd skip] * fix: bqplot stuck after the first frame of the movie We have to run the loop outside the main thread; otherwise, the processing of messages from the frontend is blocked, causing the message with the first image to never be received. The "save_image" method can only save the next image after the previous image is received. * Fix typo, this works now from the API. [ci skip] [rtd skip] * Add frontend and tests * Add user doc * DOC: Add note about standalone app saving file into weird places. * Address some review comments * Expose FPS and fix test * DOC: Baby Shark roundtrip as promised. * DOC: Ellie said more shark! * Vue.js style improvements [ci skip] [rtd skip] Co-authored-by: Kyle Conroy * Display pls install msg in plugin * Address review comments * Fix test * Fix path resolution in standalone app * Improve frontend validation and access. Co-authored-by: Kyle Conroy * Disable video for spectrum-viewer in GUI and add comment to Slice. * Add tooltip to kill switch but it only shows when activated * Rename kill with something less scary * Hide stop button since disabling is not obvious enough, also moar tooltip * Disable the whole movie menu for spectrum viewer in Cubeviz [ci skip] [rtd skip] Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> * Fix RTD warnings about invalid prop and remove unnecessary frontend check now that Ricky's suggestion is accepted. --------- Co-authored-by: Mario Buikhuizen Co-authored-by: Kyle Conroy Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> --- CHANGES.rst | 6 + docs/cubeviz/plugins.rst | 25 ++ .../plugins/moment_maps/moment_maps.py | 23 +- .../moment_maps/tests/test_moment_maps.py | 4 + jdaviz/configs/cubeviz/plugins/slice/slice.py | 2 +- .../plugins/tests/test_export_plots.py | 93 ++++++++ .../plugins/export_plot/export_plot.py | 225 ++++++++++++++++++ .../plugins/export_plot/export_plot.vue | 100 ++++++++ jdaviz/conftest.py | 2 +- notebooks/concepts/cubeviz_ndarray_gif.ipynb | 173 +++++++++++++- pyproject.toml | 3 + tox.ini | 3 +- 12 files changed, 641 insertions(+), 18 deletions(-) create mode 100644 jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py diff --git a/CHANGES.rst b/CHANGES.rst index 3b6639f105..2e25db76e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,9 @@ New Features Cubeviz ^^^^^^^ +- Added the ability to export cube slices to video. User will need to install + ``opencv-python`` separately or use ``[all]`` specifier when installing Jdaviz. [#2264] + Imviz ^^^^^ @@ -77,6 +80,9 @@ Bug Fixes Cubeviz ^^^^^^^ +- Moment Map plugin now writes FITS file to working directory if no path provided + in standalone mode. [#2264] + Imviz ^^^^^ diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index abea089de8..174a2079b4 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -274,3 +274,28 @@ Export Plot =========== This plugin allows exporting the plot in a given viewer to various image formats. + +.. _cubeviz-export-video: + +Movie +----- + +.. note:: + + For MPEG-4, this feature needs ``opencv-python`` to be installed; + see [opencv-python on PyPI](https://pypi.org/project/opencv-python/). + +Expand the "Export to video" section, then enter the desired starting and +ending slice indices (inclusive), the frame rate in frames per second (FPS), +and the filename. +If a path is not given, the file will be saved to current working +directory. Any existing file with the same name will be silently replaced. + +When you are ready, click the :guilabel:`Export to MP4` button. +The movie will be recorded at the given FPS. While recording is in progress, +it is highly recommended that you leave the app alone until it is done. + +While recording, there is an option to interrupt the recording when something +goes wrong (e.g., it is taking too long or you realized you entered the wrong inputs). +Click on the stop icon next to the :guilabel:`Export to MP4` button to interrupt it. +Doing so will result in no output video. diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py index 48c9e88675..4cfb013839 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py @@ -137,20 +137,29 @@ def _write_moment_to_fits(self, overwrite=False, *args): if self.moment is None or not self.filename: # pragma: no cover return - path = Path(self.filename).resolve() - if path.exists(): + # Make sure file does not end up in weird places in standalone mode. + path = os.path.dirname(self.filename) + if path and not os.path.exists(path): + raise ValueError(f"Invalid path={path}") + elif (not path or path.startswith("..")) and os.environ.get("JDAVIZ_START_DIR", ""): # noqa: E501 # pragma: no cover + filename = Path(os.environ["JDAVIZ_START_DIR"]) / self.filename + else: + filename = Path(self.filename).resolve() + + if filename.exists(): if overwrite: # Try to delete the file - path.unlink() - if path.exists(): + filename.unlink() + if filename.exists(): # Warn the user if the file still exists - raise FileExistsError(f"Unable to delete {path}. Check user permissions.") + raise FileExistsError(f"Unable to delete {filename}. Check user permissions.") else: self.overwrite_warn = True return - self.moment.write(str(path)) + filename = str(filename) + self.moment.write(filename) # Let the user know where we saved the file. self.hub.broadcast(SnackbarMessage( - f"Moment map saved to {os.path.abspath(self.filename)}", sender=self, color="success")) + f"Moment map saved to {os.path.abspath(filename)}", sender=self, color="success")) diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index 4408d6f065..8ad373d3e4 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -131,3 +131,7 @@ def test_write_momentmap(cubeviz_helper, spectrum1d_cube, tmp_path): sky = w.pixel_to_world(0, 0) assert_allclose(sky.ra.deg, 204.9998877673) assert_allclose(sky.dec.deg, 27.0001) + + plugin._obj.filename = "fake_path/test_file.fits" + with pytest.raises(ValueError, match="Invalid path"): + plugin._obj.vue_save_as_fits() diff --git a/jdaviz/configs/cubeviz/plugins/slice/slice.py b/jdaviz/configs/cubeviz/plugins/slice/slice.py index 9eb559b028..17b912691c 100644 --- a/jdaviz/configs/cubeviz/plugins/slice/slice.py +++ b/jdaviz/configs/cubeviz/plugins/slice/slice.py @@ -122,7 +122,7 @@ def _watch_viewer(self, viewer, watch=True): def _on_data_added(self, msg): if isinstance(msg.viewer, BqplotImageView): if len(msg.data.shape) == 3: - self.max_value = msg.data.shape[-1] - 1 + self.max_value = msg.data.shape[-1] - 1 # Same as i_end in Export Plot plugin self._watch_viewer(msg.viewer, True) msg.viewer.state.slices = (0, 0, int(self.slice)) diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py b/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py new file mode 100644 index 0000000000..31426e1831 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py @@ -0,0 +1,93 @@ +import os + +import pytest + +from jdaviz.configs.default.plugins.export_plot.export_plot import HAS_OPENCV + + +# TODO: Remove skip when https://github.com/bqplot/bqplot/pull/1397/files#r726500097 is resolved. +@pytest.mark.skip(reason="Cannot test due to async JS callback") +# @pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed") +def test_export_movie(cubeviz_helper, spectrum1d_cube, tmp_path): + orig_path = os.getcwd() + os.chdir(tmp_path) + try: + cubeviz_helper.load_data(spectrum1d_cube, data_label="test") + plugin = cubeviz_helper.plugins["Export Plot"] + assert plugin.i_start == 0 + assert plugin.i_end == 1 + assert plugin.movie_filename == "mymovie.mp4" + + plugin._obj.vue_save_movie("mp4") + assert os.path.isfile("mymovie.mp4"), tmp_path + finally: + os.chdir(orig_path) + + +@pytest.mark.skipif(HAS_OPENCV, reason="opencv-python is installed") +def test_no_opencv(cubeviz_helper, spectrum1d_cube): + cubeviz_helper.load_data(spectrum1d_cube, data_label="test") + plugin = cubeviz_helper.plugins["Export Plot"] + assert plugin._obj.movie_msg != "" + with pytest.raises(ImportError, match="Please install opencv-python"): + plugin.save_movie() + + +@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed") +def test_export_movie_not_cubeviz(imviz_helper): + plugin = imviz_helper.plugins["Export Plot"] + + with pytest.raises(NotImplementedError, match="save_movie is not available for config"): + plugin._obj.save_movie() + + # Also not available via plugin public API. + with pytest.raises(AttributeError): + plugin.save_movie() + + +@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed") +def test_export_movie_cubeviz_exceptions(cubeviz_helper, spectrum1d_cube): + cubeviz_helper.load_data(spectrum1d_cube, data_label="test") + cubeviz_helper.default_viewer.shape = (100, 100) + cubeviz_helper.app.get_viewer("uncert-viewer").shape = (100, 100) + plugin = cubeviz_helper.plugins["Export Plot"] + assert plugin._obj.movie_msg == "" + assert plugin.i_start == 0 + assert plugin.i_end == 1 + assert plugin.movie_filename == "mymovie.mp4" + + with pytest.raises(NotImplementedError, match="filetype"): + plugin.save_movie(filetype="gif") + + with pytest.raises(NotImplementedError, match="filetype"): + plugin.save_movie(filename="mymovie.gif", filetype=None) + + with pytest.raises(ValueError, match="No frames to write"): + plugin.save_movie(i_start=0, i_end=0) + + with pytest.raises(ValueError, match="Invalid frame rate"): + plugin.save_movie(fps=0) + + plugin.movie_filename = "fake_path/mymovie.mp4" + with pytest.raises(ValueError, match="Invalid path"): + plugin.save_movie() + + plugin.movie_filename = "mymovie.mp4" + plugin.viewer = 'spectrum-viewer' + with pytest.raises(TypeError, match=r"Movie for.*is not supported"): + plugin.save_movie() + + plugin.movie_filename = "" + plugin.viewer = 'uncert-viewer' + with pytest.raises(ValueError, match="Invalid filename"): + plugin.save_movie() + + +@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed") +def test_export_movie_cubeviz_empty(cubeviz_helper): + plugin = cubeviz_helper.plugins["Export Plot"] + assert plugin.i_start == 0 + assert plugin.i_end == 0 + + with pytest.raises(ValueError, match="Selected viewer has no display shape"): + plugin.save_movie(i_start=0, i_end=1) diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.py b/jdaviz/configs/default/plugins/export_plot/export_plot.py index 3b1522e907..4c1572ff43 100644 --- a/jdaviz/configs/default/plugins/export_plot/export_plot.py +++ b/jdaviz/configs/default/plugins/export_plot/export_plot.py @@ -1,9 +1,23 @@ import os +from glue_jupyter.bqplot.image import BqplotImageView +from traitlets import Any, Bool, Unicode + +from jdaviz.core.custom_traitlets import FloatHandleEmpty, IntHandleEmpty +from jdaviz.core.events import AddDataMessage, SnackbarMessage from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import PluginTemplateMixin, ViewerSelectMixin from jdaviz.core.user_api import PluginUserApi +try: + import cv2 +except ImportError: + HAS_OPENCV = False +else: + import threading + import time + HAS_OPENCV = True + __all__ = ['ExportViewer'] @@ -20,16 +34,46 @@ class ExportViewer(PluginTemplateMixin, ViewerSelectMixin): * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show` * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray` * :meth:`save_figure` + * :meth:`save_movie` (Cubeviz only) + * `i_start` (Cubeviz only) + * `i_end` (Cubeviz only) + * `movie_fps` (Cubeviz only) + * `movie_filename` (Cubeviz only) """ template_file = __file__, "export_plot.vue" + # For Cubeviz movie. + i_start = IntHandleEmpty(0).tag(sync=True) + i_end = IntHandleEmpty(0).tag(sync=True) + movie_fps = FloatHandleEmpty(5.0).tag(sync=True) + movie_filename = Any("mymovie.mp4").tag(sync=True) + movie_msg = Unicode("").tag(sync=True) + movie_recording = Bool(False).tag(sync=True) + movie_interrupt = Bool(False).tag(sync=True) + @property def user_api(self): + if self.config == "cubeviz": + return PluginUserApi(self, expose=('viewer', 'save_figure', 'save_movie', 'i_start', + 'i_end', 'movie_fps', 'movie_filename')) return PluginUserApi(self, expose=('viewer', 'save_figure')) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if self.config == "cubeviz": + if HAS_OPENCV: + self.session.hub.subscribe(self, AddDataMessage, handler=self._on_cubeviz_data_added) # noqa: E501 + else: + # NOTE: HTML tags do not work here. + self.movie_msg = 'Please install opencv-python to use this feature.' + + def _on_cubeviz_data_added(self, msg): + # NOTE: This needs revising if we allow loading more than one cube. + if isinstance(msg.viewer, BqplotImageView): + if len(msg.data.shape) == 3: + self.i_end = msg.data.shape[-1] - 1 # Same as max_value in Slice plugin + def save_figure(self, filename=None, filetype=None): """ Save the figure to an image with a provided filename or through an interactive save dialog. @@ -74,3 +118,184 @@ def vue_save_figure(self, filetype): the bqplot.Figure save methods. """ self.save_figure(filetype=filetype) + + def _save_movie(self, i_start, i_end, fps, filename, rm_temp_files): + # NOTE: All the stuff here has to be in the same thread but + # separate from main app thread to work. + + viewer = self.viewer.selected_obj + slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj + orig_slice = slice_plg.slice + temp_png_files = [] + i = i_start + video = None + + # TODO: Expose to users? + i_step = 1 # Need n_frames check if we allow tweaking + + try: + self.movie_recording = True + + while i <= i_end: + if self.movie_interrupt: + break + + slice_plg._on_slider_updated({'new': i}) + cur_pngfile = f"._cubeviz_movie_frame_{i}.png" + self.save_figure(filename=cur_pngfile, filetype="png") + temp_png_files.append(cur_pngfile) + i += i_step + + # Wait for the roundtrip to the frontend to complete. + while viewer.figure._upload_png_callback is not None: + time.sleep(0.05) + + if not self.movie_interrupt: + # Grab frame size. + frame_shape = cv2.imread(temp_png_files[0]).shape + frame_size = (frame_shape[1], frame_shape[0]) + + video = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'mp4v'), fps, frame_size, True) # noqa: E501 + for cur_pngfile in temp_png_files: + video.write(cv2.imread(cur_pngfile)) + finally: + cv2.destroyAllWindows() + if video: + video.release() + slice_plg._on_slider_updated({'new': orig_slice}) + self.movie_recording = False + + if rm_temp_files or self.movie_interrupt: + for cur_pngfile in temp_png_files: + if os.path.exists(cur_pngfile): + os.remove(cur_pngfile) + + if self.movie_interrupt: + if os.path.exists(filename): + os.remove(filename) + self.movie_interrupt = False + + def save_movie(self, i_start=None, i_end=None, fps=None, filename=None, filetype=None, + rm_temp_files=True): + """Save selected slices as a movie. + + This method creates a PNG file per frame (``._cubeviz_movie_frame_.png``) + in the working directory before stitching all the frames into a movie. + Please make sure you have sufficient memory for this operation. + PNG files are deleted after the movie is created unless otherwise specified. + If another PNG file with the same name already exists, it will be silently replaced. + + Parameters + ---------- + i_start, i_end : int or `None` + Slices to record; each slice will be a frame in the movie. + If not given, it is obtained from plugin inputs. + Unlike Python indexing, ``i_end`` is inclusive. + Wrapping and reverse indexing are not supported. + + fps : float or `None` + Frame rate in frames per second (FPS). + If not given, it is obtained from plugin inputs. + + filename : str or `None` + Filename for the movie to be recorded. Include path if necessary. + If not given, it is obtained from plugin inputs. + If another file with the same name already exists, it will be silently replaced. + + filetype : {'mp4', `None`} + Currently only MPEG-4 is supported. This keyword is reserved for future support + of other format(s). + + rm_temp_files : bool + Remove temporary PNG files after movie creation. Default is `True`. + + Returns + ------- + out_filename : str + The absolute path to the actual output file. + + """ + if self.config != "cubeviz": + raise NotImplementedError(f"save_movie is not available for config={self.config}") + + if not HAS_OPENCV: + raise ImportError("Please install opencv-python to save cube as movie.") + + if filetype is None: + if filename is not None and '.' in filename: + filetype = filename.split('.')[-1] + else: + # default to MPEG-4 + filetype = "mp4" + + if filetype != "mp4": + raise NotImplementedError(f"filetype={filetype} not supported") + + viewer = self.viewer.selected_obj + if not isinstance(viewer, BqplotImageView): # Profile viewer in glue-jupyter cannot do this + raise TypeError(f"Movie for {viewer.__class__.__name__} is not supported.") + if viewer.shape is None: + raise ValueError("Selected viewer has no display shape.") + + if fps is None: + fps = float(self.movie_fps) + if fps <= 0: + raise ValueError("Invalid frame rate, must be positive non-zero value.") + + if filename is None: + if self.movie_filename: + filename = self.movie_filename + else: + raise ValueError("Invalid filename.") + + # Make sure file does not end up in weird places in standalone mode. + path = os.path.dirname(filename) + if path and not os.path.exists(path): + raise ValueError(f"Invalid path={path}") + elif (not path or path.startswith("..")) and os.environ.get("JDAVIZ_START_DIR", ""): # noqa: E501 # pragma: no cover + filename = os.path.join(os.environ["JDAVIZ_START_DIR"], filename) + + if i_start is None: + i_start = int(self.i_start) + + if i_end is None: + i_end = int(self.i_end) + + # No wrapping. Forward only. + slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj + if i_start < 0: # pragma: no cover + i_start = 0 + if i_end > slice_plg.max_value: # pragma: no cover + i_end = slice_plg.max_value + if i_end <= i_start: + raise ValueError(f"No frames to write: i_start={i_start}, i_end={i_end}") + + threading.Thread( + target=lambda: self._save_movie(i_start, i_end, fps, filename, rm_temp_files) + ).start() + + return os.path.abspath(filename) + + def vue_save_movie(self, filetype): # pragma: no cover + """ + Callback for save movie events in the front end viewer toolbars. Uses + the bqplot.Figure save methods. + """ + try: + filename = self.save_movie(filetype=filetype) + except Exception as err: # pragma: no cover + self.hub.broadcast(SnackbarMessage( + f"Error saving {self.movie_filename}: {err!r}", sender=self, color="error")) + else: + # Let the user know where we saved the file. + # NOTE: Because of threading, this will be emitted even as movie as recording. + self.hub.broadcast(SnackbarMessage( + f"Movie being saved to {filename} for slices {self.i_start} to {self.i_end}, " + f"inclusive, at {self.movie_fps} FPS.", + sender=self, color="success")) + + def vue_interrupt_recording(self, *args): # pragma: no cover + self.movie_interrupt = True + self.hub.broadcast(SnackbarMessage( + f"Movie recording interrupted by user, {self.movie_filename} will be deleted.", + sender=self, color="warning")) diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.vue b/jdaviz/configs/default/plugins/export_plot/export_plot.vue index 065242935a..67408e4451 100644 --- a/jdaviz/configs/default/plugins/export_plot/export_plot.vue +++ b/jdaviz/configs/default/plugins/export_plot/export_plot.vue @@ -17,6 +17,7 @@ Export to PNG @@ -25,11 +26,110 @@ Export to SVG + + + + + + Export to Video + + + + + {{ movie_msg }} + + + + + + + + + + + + + + + + + + + + + + + + Start movie recording + +
+ + + Interrupt recording and delete movie file + +
+
+
+
+
+
diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index d56ecf77c0..3f5b9d7f2e 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -180,7 +180,7 @@ def multi_order_spectrum_list(spectrum1d, spectral_orders=10): def _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(2, 2, 4), with_uncerts=False): - + # nz=2 nx=2 ny=4 flux = np.arange(np.prod(shape)).reshape(shape) * fluxunit wcs_dict = {"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CTYPE3": "WAVE-LOG", "CRVAL1": 205, "CRVAL2": 27, "CRVAL3": 4.622e-7, diff --git a/notebooks/concepts/cubeviz_ndarray_gif.ipynb b/notebooks/concepts/cubeviz_ndarray_gif.ipynb index f8dfc86423..2ea0432750 100644 --- a/notebooks/concepts/cubeviz_ndarray_gif.ipynb +++ b/notebooks/concepts/cubeviz_ndarray_gif.ipynb @@ -17,7 +17,9 @@ "cell_type": "code", "execution_count": null, "id": "0adf0be6", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from jdaviz import Cubeviz" @@ -141,7 +143,9 @@ "cell_type": "code", "execution_count": null, "id": "fab04442", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "filename = 'baby_shark.gif'" @@ -151,7 +155,9 @@ "cell_type": "code", "execution_count": null, "id": "c89c8d5f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "cubeviz2 = Cubeviz()" @@ -161,7 +167,9 @@ "cell_type": "code", "execution_count": null, "id": "137fdde0", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "cubeviz2.load_data(filename)" @@ -172,17 +180,168 @@ "execution_count": null, "id": "ab4eca75", "metadata": { - "scrolled": false + "tags": [] }, "outputs": [], "source": [ "cubeviz2.show()" ] }, + { + "cell_type": "markdown", + "id": "b288fe1c", + "metadata": {}, + "source": [ + "### But what about roundtripping?\n", + "\n", + "Well, sort of..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22902812", + "metadata": {}, + "outputs": [], + "source": [ + "export_plg = cubeviz2.plugins[\"Export Plot\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b78bbf5a", + "metadata": {}, + "outputs": [], + "source": [ + "# Or you can use the GUI.\n", + "export_plg.save_movie(0, 131, fps=10, filename=\"baby_shark_roundtrip.mp4\")" + ] + }, + { + "cell_type": "markdown", + "id": "4c00ffed", + "metadata": {}, + "source": [ + "See the rendered movie at https://www.youtube.com/watch?v=n8czt1ZQUNk" + ] + }, + { + "cell_type": "markdown", + "id": "baec2fe1", + "metadata": {}, + "source": [ + "### More shark!\n", + "\n", + "Ellie says “more sharkâ€! See the demo at https://www.youtube.com/watch?v=ZTHJfSdmnBA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c68bed4b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "cubeviz2.app.add_data_to_viewer(\"uncert-viewer\", \"baby_shark\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cd9ecd1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_plg = cubeviz2.plugins[\"Plot Options\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e34310f2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_plg.viewer = \"flux-viewer\"\n", + "plot_plg.image_color_mode = \"Monochromatic\"\n", + "plot_plg.image_color = \"Red\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "252fe8be", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_plg.viewer = \"uncert-viewer\"\n", + "plot_plg.image_color_mode = \"Monochromatic\"\n", + "plot_plg.image_color = \"Blue\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82111003", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "slice_plg = cubeviz2.plugins[\"Slice\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41de986e-c051-4021-a51e-bbdb6b478505", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "slice_plg.slice = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cf924c0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# More shark!\n", + "slice_plg._obj.vue_play_start_stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b212b4fd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Run again to stop.\n", + "slice_plg._obj.vue_play_start_stop()" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "4d1af33f", + "id": "03f27eb1-531f-4509-ba7c-e5a3661a58eb", "metadata": {}, "outputs": [], "source": [] @@ -204,7 +363,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.0" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index 08a722738e..246618afa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,9 @@ mosviz = "jdaviz.configs.mosviz" imviz = "jdaviz.configs.imviz" [project.optional-dependencies] +all = [ + "opencv-python", +] test = [ "pytest", "pytest-astropy", diff --git a/tox.ini b/tox.ini index d212680cbd..2ee43453a7 100644 --- a/tox.ini +++ b/tox.ini @@ -65,8 +65,7 @@ deps = extras = test romandeps: roman - # Uncomment when we have all again in setup.cfg - #alldeps: all + alldeps: all commands = devdeps: pip install -U -i https://pypi.anaconda.org/astropy/simple astropy --pre From d3d251374039582b84119e8a7f757241907d574f Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 15 Jun 2023 13:30:57 -0400 Subject: [PATCH 140/283] Add functional but ugly launcher --- jdaviz/core/launcher.py | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 jdaviz/core/launcher.py diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py new file mode 100644 index 0000000000..8878408b42 --- /dev/null +++ b/jdaviz/core/launcher.py @@ -0,0 +1,53 @@ +import ipyvuetify as v +from ipywidgets import jslink +from traitlets import Dict + +from jdaviz import configs as jdaviz_configs +from jdaviz.core.data_formats import open as jdaviz_open + + +def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d']): + main = v.Sheet() + main.add_traits(_metadata=Dict(default_value={'mount_id': 'content'}).tag(sync=True)) + main.children = [] + + # Create Intro Row + intro_row = v.Row() + welcome_text = v.Html(tag='h1', attributes={'title': 'a title'}, + children=['Welcome to Jdaviz']) + #links = + intro_row.children = [welcome_text] + + # Filepath row + filepath_row = v.Row() + text_field = v.TextField(label="URI or File Path", v_model=None) + + def load_file(filepath): + print(filepath) + if filepath: + helper = jdaviz_open(filepath, show=False) + main.children = [helper.app] + + open_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[v.Icon(children=["mdi-upload"])]) + open_data_btn.on_event('click', lambda btn, event, data: load_file(btn.value)) + jslink((text_field, 'v_model'), (open_data_btn, 'value')) + + filepath_row.children = [text_field, open_data_btn] + + # Config buttons + def create_config(config): + viz_class = getattr(jdaviz_configs, config.capitalize()) + main.children = [viz_class().app] + + btns = [] + for config in configs: + config_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[config.capitalize()]) + config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0])) + btns.append(config_btn) + + # Create button row + btn_row = v.Row() + btn_row.children = btns + main.children = [intro_row, filepath_row, btn_row] + + return main \ No newline at end of file From cefb4863fb74c83ced370e837faac654f45698e9 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 15 Jun 2023 13:31:28 -0400 Subject: [PATCH 141/283] Use launcher notebook if neither config nor filepath is specified --- jdaviz/cli.py | 7 +++++- jdaviz/jdaviz_cli_launcher.ipynb | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 jdaviz/jdaviz_cli_launcher.ipynb diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 4f569e7f00..fd400adc4d 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -60,7 +60,12 @@ def main(filepaths=None, layout='default', instrument=None, browser='default', else: file_list = [] - with open(JDAVIZ_DIR / "jdaviz_cli.ipynb") as f: + if not filepaths and not layout: + notebook = "jdaviz_cli_launcher.ipynb" + else: + notebook = "jdaviz_cli.ipynb" + + with open(JDAVIZ_DIR / notebook) as f: notebook_template = f.read() start_dir = os.path.abspath('.') diff --git a/jdaviz/jdaviz_cli_launcher.ipynb b/jdaviz/jdaviz_cli_launcher.ipynb new file mode 100644 index 0000000000..f847be1796 --- /dev/null +++ b/jdaviz/jdaviz_cli_launcher.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from jdaviz.core import show_launcher\n", + "\n", + "show_launcher()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.10.10 ('envmain': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + }, + "vscode": { + "interpreter": { + "hash": "f917e879dca01012f092e44ceeb72fc316d3b188a12a493299dc2bd49905dadb" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From e652be8ed5c4bd3c5848c72f69f51baef344c56e Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 15 Jun 2023 13:36:26 -0400 Subject: [PATCH 142/283] Codestyle --- jdaviz/core/launcher.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index 8878408b42..8b4a05c0ba 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -15,7 +15,6 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d'] intro_row = v.Row() welcome_text = v.Html(tag='h1', attributes={'title': 'a title'}, children=['Welcome to Jdaviz']) - #links = intro_row.children = [welcome_text] # Filepath row @@ -27,11 +26,12 @@ def load_file(filepath): if filepath: helper = jdaviz_open(filepath, show=False) main.children = [helper.app] - - open_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[v.Icon(children=["mdi-upload"])]) + + open_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", + children=[v.Icon(children=["mdi-upload"])]) open_data_btn.on_event('click', lambda btn, event, data: load_file(btn.value)) jslink((text_field, 'v_model'), (open_data_btn, 'value')) - + filepath_row.children = [text_field, open_data_btn] # Config buttons @@ -41,7 +41,8 @@ def create_config(config): btns = [] for config in configs: - config_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[config.capitalize()]) + config_btn = v.Btn(class_="ma-2", outlined=True, color="primary", + children=[config.capitalize()]) config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0])) btns.append(config_btn) @@ -50,4 +51,4 @@ def create_config(config): btn_row.children = btns main.children = [intro_row, filepath_row, btn_row] - return main \ No newline at end of file + return main From f3bb66db33543fc77a6cc6cfdb249550df1fa976 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Thu, 15 Jun 2023 13:51:06 -0400 Subject: [PATCH 143/283] Add margins on left and right to avoid cutoff in notebook --- jdaviz/core/launcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index 8b4a05c0ba..66fa74c2fa 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -7,7 +7,7 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d']): - main = v.Sheet() + main = v.Sheet(class_="mx-4") main.add_traits(_metadata=Dict(default_value={'mount_id': 'content'}).tag(sync=True)) main.children = [] From dff234be3c0b913cef057605acd14ef4f79e39ad Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 16 Jun 2023 09:29:49 -0400 Subject: [PATCH 144/283] Support launcher from cli --- jdaviz/cli.py | 7 ++++--- jdaviz/jdaviz_cli_launcher.ipynb | 2 +- pyproject.toml | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/jdaviz/cli.py b/jdaviz/cli.py index fd400adc4d..614587b065 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -60,7 +60,8 @@ def main(filepaths=None, layout='default', instrument=None, browser='default', else: file_list = [] - if not filepaths and not layout: + print(f'{file_list}, {layout}') + if len(file_list) == 0 and layout is '': notebook = "jdaviz_cli_launcher.ipynb" else: notebook = "jdaviz_cli.ipynb" @@ -116,8 +117,8 @@ def _main(config=None): 'loaded from FILENAME.') filepaths_nargs = '*' if config is None: - parser.add_argument('layout', choices=['cubeviz', 'specviz', 'specviz2d', - 'mosviz', 'imviz'], + parser.add_argument('--layout', default='', choices=['cubeviz', 'specviz', 'specviz2d', + 'mosviz', 'imviz'], help='Configuration to use.') if (config == "mosviz") or ("mosviz" in sys.argv): filepaths_nargs = 1 diff --git a/jdaviz/jdaviz_cli_launcher.ipynb b/jdaviz/jdaviz_cli_launcher.ipynb index f847be1796..a13d733066 100644 --- a/jdaviz/jdaviz_cli_launcher.ipynb +++ b/jdaviz/jdaviz_cli_launcher.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "from jdaviz.core import show_launcher\n", + "from jdaviz.core.launcher import show_launcher\n", "\n", "show_launcher()" ] diff --git a/pyproject.toml b/pyproject.toml index 246618afa1..585361e9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ jdaviz = [ "configs/*/*/*/*.vue", "configs/*/*.yaml", "jdaviz_cli.ipynb", + "jdaviz_cli_launcher.ipynb", ] "jdaviz.configs.imviz.tests" = [ "data/*", From ceb79b0a3a06ac3bbbca09b1e58bd6b2f0156bb0 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 16 Jun 2023 09:35:06 -0400 Subject: [PATCH 145/283] Codestyle --- jdaviz/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 614587b065..4dd79709f6 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -61,7 +61,7 @@ def main(filepaths=None, layout='default', instrument=None, browser='default', file_list = [] print(f'{file_list}, {layout}') - if len(file_list) == 0 and layout is '': + if len(file_list) == 0 and layout == '': notebook = "jdaviz_cli_launcher.ipynb" else: notebook = "jdaviz_cli.ipynb" From b980750ba74c5cadfe65b055f10b9a548a828585 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 16 Jun 2023 09:52:56 -0400 Subject: [PATCH 146/283] Remove URI from path text until implemented --- jdaviz/cli.py | 1 - jdaviz/core/launcher.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 4dd79709f6..b088cf3522 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -60,7 +60,6 @@ def main(filepaths=None, layout='default', instrument=None, browser='default', else: file_list = [] - print(f'{file_list}, {layout}') if len(file_list) == 0 and layout == '': notebook = "jdaviz_cli_launcher.ipynb" else: diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index 66fa74c2fa..cb8a965fcc 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -19,10 +19,9 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d'] # Filepath row filepath_row = v.Row() - text_field = v.TextField(label="URI or File Path", v_model=None) + text_field = v.TextField(label="File Path", v_model=None) def load_file(filepath): - print(filepath) if filepath: helper = jdaviz_open(filepath, show=False) main.children = [helper.app] From bfa8a418545881402a7b834a5e0cdea695936ce9 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 16 Jun 2023 10:52:00 -0400 Subject: [PATCH 147/283] Fix standalone bug --- jdaviz/core/launcher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index cb8a965fcc..7b6188687b 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -7,8 +7,7 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d']): - main = v.Sheet(class_="mx-4") - main.add_traits(_metadata=Dict(default_value={'mount_id': 'content'}).tag(sync=True)) + main = v.Sheet(class_="mx-4", _metadata={'mount_id': 'content'}) main.children = [] # Create Intro Row From 73c4b6fe3ab4fd7e7565deaef61eb2aeca765932 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 26 Jun 2023 15:57:22 -0400 Subject: [PATCH 148/283] Specify --layout= as new required cli syntax --- docs/cubeviz/import_data.rst | 2 +- docs/imviz/import_data.rst | 2 +- docs/mosviz/import_data.rst | 4 ++-- docs/mosviz/index.rst | 2 +- docs/specviz/import_data.rst | 2 +- docs/specviz2d/import_data.rst | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/cubeviz/import_data.rst b/docs/cubeviz/import_data.rst index b421dd3199..555ae61e84 100644 --- a/docs/cubeviz/import_data.rst +++ b/docs/cubeviz/import_data.rst @@ -50,7 +50,7 @@ a data product is optional: .. code-block:: bash - jdaviz cubeviz /my/directory/cube.fits + jdaviz --layout=cubeviz /my/directory/cube.fits .. _cubeviz-import-gui: diff --git a/docs/imviz/import_data.rst b/docs/imviz/import_data.rst index 6d23609109..0ad3995623 100644 --- a/docs/imviz/import_data.rst +++ b/docs/imviz/import_data.rst @@ -25,7 +25,7 @@ Multiple data files may be provided: .. code-block:: bash - jdaviz imviz /my/image/data1.fits /my/image/data2.fits + jdaviz --layout=imviz /my/image/data1.fits /my/image/data2.fits .. _imviz-import-gui: diff --git a/docs/mosviz/import_data.rst b/docs/mosviz/import_data.rst index ed0f513af2..fd0b7ab026 100644 --- a/docs/mosviz/import_data.rst +++ b/docs/mosviz/import_data.rst @@ -41,13 +41,13 @@ Similarly, an instrument keyword can be specified by the command line. For NIRSp .. code-block:: bash - jdaviz mosviz /path/to/my/data --instrument=nirspec + jdaviz --layout=mosviz /path/to/my/data --instrument=nirspec and for NIRISS: .. code-block:: bash - jdaviz mosviz /path/to/my/data --instrument=niriss + jdaviz --layout=mosviz /path/to/my/data --instrument=niriss Specifying a data directory and an instrument are required to start Mosviz from the command line. If a directory is entered without specifying an instrument, Mosviz will diff --git a/docs/mosviz/index.rst b/docs/mosviz/index.rst index 64adb21405..58d747f8bb 100644 --- a/docs/mosviz/index.rst +++ b/docs/mosviz/index.rst @@ -30,7 +30,7 @@ To load a sample `NIRISS Nirspec Data Set Date: Mon, 26 Jun 2023 15:59:10 -0400 Subject: [PATCH 149/283] Remove unused import Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- jdaviz/core/launcher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index 7b6188687b..f079b72582 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -1,6 +1,5 @@ import ipyvuetify as v from ipywidgets import jslink -from traitlets import Dict from jdaviz import configs as jdaviz_configs from jdaviz.core.data_formats import open as jdaviz_open From 507a52866b16d38ae220a5666c3d2875ca77f5bf Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 26 Jun 2023 16:00:14 -0400 Subject: [PATCH 150/283] Update readme to show required layout flag --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4719445abb..3a6ccd47ed 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ from a terminal, type: .. code-block:: bash jdaviz --help - jdaviz specviz /path/to/data/spectral_file + jdaviz --layout=specviz /path/to/data/spectral_file For more information on the command line interfaces for each tool, see the `Jdaviz docs `_. From 47b50a544934551210002b08a1db1e18c482c3b8 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 3 Jul 2023 10:08:55 -0400 Subject: [PATCH 151/283] Changelog --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d1191cabec..74598f7998 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,8 @@ New Features - The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] +- Add launcher to select config and require --layout argument when launching standalone [#2257] + Cubeviz ^^^^^^^ From 0cd7a8084a9eee7c9be01f91cdd0207bce33f91d Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Mon, 3 Jul 2023 10:32:43 -0400 Subject: [PATCH 152/283] Changelog Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 74598f7998..55bd5b9c69 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ New Features - The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] -- Add launcher to select config and require --layout argument when launching standalone [#2257] +- Add launcher to select config and require --layout argument when launching standalone. [#2257] Cubeviz ^^^^^^^ From 294c8d67ba38d9df3c0f0915e988374032b8a90d Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Mon, 3 Jul 2023 10:44:26 -0400 Subject: [PATCH 153/283] Changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 55bd5b9c69..929526c549 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ New Features - The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] -- Add launcher to select config and require --layout argument when launching standalone. [#2257] +- Add first-pass launcher to select config and auto-identify data. [#2257] Cubeviz ^^^^^^^ From 918834478600af7a8fd32135bb3b34242e166662 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Mon, 3 Jul 2023 16:07:01 -0400 Subject: [PATCH 154/283] bugfix for non-stddev nduncertainties --- .../tests/test_unit_conversion.py | 33 +++++++++++++++++++ jdaviz/configs/specviz/plugins/viewers.py | 13 ++++++++ jdaviz/core/helpers.py | 20 +++++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py index d69e93b35f..b7ff19ab43 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py @@ -1,5 +1,12 @@ import pytest +import numpy as np +import astropy from astropy import units as u +from astropy.nddata import InverseVariance +from specutils import Spectrum1D +from astropy.utils.introspection import minversion + +ASTROPY_LT_5_3 = not minversion(astropy, "5.3") # On failure, should not crash; essentially a no-op. @@ -89,3 +96,29 @@ def test_conv_no_data(specviz_helper): with pytest.raises(ValueError, match="no valid unit choices"): plg.flux_unit = "erg / (s cm2 Angstrom)" assert len(specviz_helper.app.data_collection) == 0 + + +@pytest.mark.skipif(ASTROPY_LT_5_3, reason='this feature relies on astropy v5.3+') +def test_non_stddev_uncertainty(specviz_helper): + flux = np.ones(10) * u.Jy + stddev = 0.1 + var = stddev ** 2 + inv_var = np.ones(len(flux)) / var + wavelength = np.linspace(1, 5, len(flux)) * u.um + spec = Spectrum1D( + flux, + uncertainty=InverseVariance(inv_var), + spectral_axis=wavelength + ) + + specviz_helper.load_data(spec) + + po = specviz_helper.plugins['Plot Options'] + po.uncertainty_visible = True + + # check that the stddev uncertainties are drawn: + viewer = specviz_helper.app.get_viewer('spectrum-viewer') + np.testing.assert_allclose( + np.abs(viewer.figure.marks[-1].y - viewer.figure.marks[-1].y.mean(0)), + stddev + ) diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 2d5dc13b23..195da46a22 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -3,6 +3,7 @@ import numpy as np from astropy import table from astropy import units as u +from astropy.nddata import StdDevUncertainty, VarianceUncertainty, InverseVariance from glue.core import BaseData from glue.core.subset import Subset from glue.config import data_translator @@ -21,6 +22,13 @@ __all__ = ['SpecvizProfileView'] +uncertainty_str_to_cls_mapping = { + "std": StdDevUncertainty, + "var": VarianceUncertainty, + "ivar": InverseVariance +} + + @viewer_registry("specviz-profile-viewer", label="Profile 1D (Specviz)") class SpecvizProfileView(JdavizViewerMixin, BqplotProfileView): # categories: zoom resets, zoom, pan, subset, select tools, shortcuts @@ -447,6 +455,11 @@ def _plot_uncertainties(self): if "uncertainty" in comps: # noqa error = np.array(lyr['uncertainty'].data) + # ensure that the uncertainties are represented as stddev: + uncertainty_type_str = lyr.meta.get('uncertainty_type', 'stddev') + uncert_cls = uncertainty_str_to_cls_mapping[uncertainty_type_str] + error = uncert_cls(error).represent_as(StdDevUncertainty).array + data_obj = lyr.data.get_object() data_x = data_obj.spectral_axis.value data_y = data_obj.flux.value diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index 7a021d6421..d0d9dbf096 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -14,7 +14,7 @@ import numpy as np import astropy.units as u from astropy.wcs.wcsapi import BaseHighLevelWCS -from astropy.nddata import CCDData +from astropy.nddata import CCDData, StdDevUncertainty from regions.core.core import Region from glue.core import HubListener from glue.core.edit_subset_mode import NewMode @@ -428,7 +428,23 @@ def _handle_display_units(data, use_display_units): flux_unit = self.app._get_display_unit('flux') # TODO: any other attributes (meta, wcs, etc)? # TODO: implement uncertainty.to upstream - new_uncert = data.uncertainty.__class__(data.uncertainty.quantity.to(flux_unit)) if data.uncertainty is not None else None # noqa + uncertainty = data.uncertainty + if uncertainty is not None: + # convert the uncertainties to StdDevUncertainties, since + # that is assumed in a few places in jdaviz: + if uncertainty.unit is None: + uncertainty = uncertainty.unit = data.flux.unit + if hasattr(uncertainty, 'represent_as'): + new_uncert = uncertainty.represent_as( + StdDevUncertainty + ).quantity.to(flux_unit) + else: + # if not specified as NDUncertainty, assume stddev: + new_uncert = uncertainty.quantity.to(flux_unit) + new_uncert = StdDevUncertainty(new_uncert, unit=flux_unit) + else: + new_uncert = None + data = Spectrum1D(spectral_axis=data.spectral_axis.to(spectral_unit, u.spectral()), flux=data.flux.to(flux_unit, From a55cdc9de849d3e9e35da1694c9d93c1fd57da04 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Mon, 3 Jul 2023 16:15:31 -0400 Subject: [PATCH 155/283] Changelog entry added --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 929526c549..2987d4b212 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -123,6 +123,8 @@ Mosviz Specviz ^^^^^^^ +- Removed assumption that NDUncertainty's given to Specviz are always standard deviation type [#2283] + Specviz2d ^^^^^^^^^ From d872f9c1185564c1962435aa69b97a1438d201cd Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Mon, 3 Jul 2023 17:00:31 -0400 Subject: [PATCH 156/283] Update jdaviz/core/helpers.py Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> --- jdaviz/core/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index d0d9dbf096..faafa0afd0 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -433,7 +433,7 @@ def _handle_display_units(data, use_display_units): # convert the uncertainties to StdDevUncertainties, since # that is assumed in a few places in jdaviz: if uncertainty.unit is None: - uncertainty = uncertainty.unit = data.flux.unit + uncertainty.unit = data.flux.unit if hasattr(uncertainty, 'represent_as'): new_uncert = uncertainty.represent_as( StdDevUncertainty From 46be84fdafb75f936338e41cd2ceaeb4b8eedff8 Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Wed, 5 Jul 2023 23:11:40 +0200 Subject: [PATCH 157/283] POC: Attempt to package jdaviz in Pyinstaller (take 2) (#1960) * feat: pyinstaller * codesign osx * debug: try without codesign * does not need arguments * use hooks * also download the app * use branch of pyinstaller * better hooks * pin to 5.11 Otherwise we need to do at least: + rm -rf standalone/dist/jdaviz.app/Contents/MacOS/jedi/third_party/typeshed/stdlib/ + rm -rf standalone/dist/jdaviz.app/Contents/MacOS/**/*.dist-info * fix: maintain symlinks by zipping, upload-artifact does not support it * GHA logic? * code sign on gha * run notary tool on gha * fix: redo codesign after modifications * fix: reorder zipping and notary step * remove invalid symlink * make sure the program executes * fix path of entitlements file * add comment for hint with the osx fix * gpt assisted way of running the log tool on failure * fix syntax error * fix path * comments for the future * fix uuid parsing and re-zip the app after notary step * fix: use ditto instead of zip from https://developer.apple.com/forums/thread/116831 zip might cause issues with utf8 or metadata. * comments for the future * fix uuid for notary step * upload different artifact for osx * cleanup spec file and hooks, no more __pycache__ files should be included * ci: do not cancel on failure * fix: mistune 3.0 needs this * make dmg instead of zip for osx * BUG: Fix mouseover behavior in Cubeviz spectrum viewer when spatial subset is present. Co-authored-by: Duy Nguyen * Remove change log from #2258 because the bug only affects unreleased code [ci skip] [rtd skip] * Deprecate get_subsets_from_viewer * Missing region index * Fix subset args * Remove get_data_from_viewer from imviz viewer tests * Remove get_data_from_viewer from mosviz data loading test and sub hardcoded viewer ref names * Mosviz test update image truth class * Fix incorrect viewer ref * Set Mos2Dviewer data statistic to None by default * Non-existent data check * Properly deprecate getters * Properly check for valueError on non-existent label * Update Specviz get_data_from_viewer test * Rename "subset_to_apply" to "spectral_subset * Codestyle * Fix docs wording Co-authored-by: Jesse Averbukh * Catch missed code, fix bug * Retain Mosviz get_data behavior and fix change log * Undo bad diff * MNT: Add .mailmap so git shortlog -es gives sane listing. * DOC: Add warning about surface brightness in Simple Aperture Photometry plugin (#2261) * DOC: Add warning about surface brightness in Simple Aperture Photometry plugin. I am beginning to think the Simple in plugin name no longer applies. * DOC: Improve verbiage. Co-authored-by: Camilla Pacifici --------- Co-authored-by: Camilla Pacifici * FEAT: Annulus draw tool for Imviz (#2240) * FEAT: Annulus draw tool for Imviz. TODO: Need to fix icon. TST: Add tests. [ci skip] [rtd skip] * Pull in #2204 into this PR and bump upstream pins * Changed function name upstream in glue-astronomy during review process * Proper annulus icon from J. Kotler Co-authored-by: Jennifer Kotler * Avoid error traceback with bad annulus radii * Disable recentering for annulus * Fix test failure because not sure why spectral region is using Imviz centering method but okay. --------- Co-authored-by: Jennifer Kotler Co-authored-by: Ricky O'Steen * MNT: Temporarily pin voila<0.5 (#2269) * MNT: Temp pin voila<0.5 because voila-template is incompatible, see #2268 * TST: Disable voila dev in test matrix * TST: Ignore DeprecationWarning from asteval (#2274) * TST: Ignore DeprecationWarning from asteval. For example https://github.com/newville/asteval/issues/120 * Also ignore FutureWarning from asteval * Add doc for windows latency issue * Deprecate load_spectrum * Update tests and docs from load_spectrum * Docstring suggestions Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> * Changelog * Changelog * Button to export Cubeviz movie (#2264) * WIP: Backend API to write movie file but got stuck with bqplot not cooperating after first frame. [ci skip] [rtd skip] * fix: bqplot stuck after the first frame of the movie We have to run the loop outside the main thread; otherwise, the processing of messages from the frontend is blocked, causing the message with the first image to never be received. The "save_image" method can only save the next image after the previous image is received. * Fix typo, this works now from the API. [ci skip] [rtd skip] * Add frontend and tests * Add user doc * DOC: Add note about standalone app saving file into weird places. * Address some review comments * Expose FPS and fix test * DOC: Baby Shark roundtrip as promised. * DOC: Ellie said more shark! * Vue.js style improvements [ci skip] [rtd skip] Co-authored-by: Kyle Conroy * Display pls install msg in plugin * Address review comments * Fix test * Fix path resolution in standalone app * Improve frontend validation and access. Co-authored-by: Kyle Conroy * Disable video for spectrum-viewer in GUI and add comment to Slice. * Add tooltip to kill switch but it only shows when activated * Rename kill with something less scary * Hide stop button since disabling is not obvious enough, also moar tooltip * Disable the whole movie menu for spectrum viewer in Cubeviz [ci skip] [rtd skip] Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> * Fix RTD warnings about invalid prop and remove unnecessary frontend check now that Ricky's suggestion is accepted. --------- Co-authored-by: Mario Buikhuizen Co-authored-by: Kyle Conroy Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> * Add functional but ugly launcher * Use launcher notebook if neither config nor filepath is specified * Codestyle * Add margins on left and right to avoid cutoff in notebook * Support launcher from cli * Codestyle * Remove URI from path text until implemented * Fix standalone bug * Specify --layout= as new required cli syntax * Remove unused import Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> * Update readme to show required layout flag * Changelog * Changelog Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> * Changelog * Update .github/workflows/standalone.yml Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> * Update .github/workflows/standalone.yml * Update to use config launcher * Moved changelog entry to new section --------- Co-authored-by: Duy Nguyen Co-authored-by: Pey Lian Lim <2090236+pllim@users.noreply.github.com> Co-authored-by: Duy Tuong Nguyen Co-authored-by: Jesse Averbukh Co-authored-by: Camilla Pacifici Co-authored-by: Jennifer Kotler Co-authored-by: Ricky O'Steen Co-authored-by: Mario Buikhuizen Co-authored-by: Kyle Conroy Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> --- .github/workflows/standalone.yml | 141 +++++++++++++++++++++ CHANGES.rst | 4 + jdaviz/_astropy_init.py | 23 ++++ jdaviz/cli.py | 27 ++++ jdaviz/configs/cubeviz/cubeviz.ipynb | 41 ++++++ jdaviz/configs/default/default.ipynb | 50 ++++++++ jdaviz/configs/imviz/imviz.ipynb | 41 ++++++ jdaviz/configs/mosviz/mosviz.ipynb | 46 +++++++ jdaviz/configs/specviz/specviz.ipynb | 41 ++++++ jdaviz/core/tools.py | 2 +- setup.cfg | 158 ++++++++++++++++++++++++ standalone/entitlements.plist | 13 ++ standalone/hooks/hook-bqplot.py | 3 + standalone/hooks/hook-debugpy.py | 8 ++ standalone/hooks/hook-glue.py | 4 + standalone/hooks/hook-glue_jupyter.py | 4 + standalone/hooks/hook-ipypopout.py | 3 + standalone/hooks/hook-jdaviz.py | 4 + standalone/hooks/hook-jupyter_client.py | 3 + standalone/hooks/hook-mistune.py | 3 + standalone/hooks/hook-photutils.py | 5 + standalone/hooks/hook-regions.py | 5 + standalone/hooks/hook-skimage.py | 12 ++ standalone/jdaviz-cli-entrypoint.py | 24 ++++ standalone/jdaviz.spec | 78 ++++++++++++ standalone/test.py | 12 ++ 26 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/standalone.yml create mode 100644 jdaviz/_astropy_init.py create mode 100644 jdaviz/configs/cubeviz/cubeviz.ipynb create mode 100644 jdaviz/configs/default/default.ipynb create mode 100644 jdaviz/configs/imviz/imviz.ipynb create mode 100644 jdaviz/configs/mosviz/mosviz.ipynb create mode 100644 jdaviz/configs/specviz/specviz.ipynb create mode 100644 setup.cfg create mode 100644 standalone/entitlements.plist create mode 100644 standalone/hooks/hook-bqplot.py create mode 100644 standalone/hooks/hook-debugpy.py create mode 100644 standalone/hooks/hook-glue.py create mode 100644 standalone/hooks/hook-glue_jupyter.py create mode 100644 standalone/hooks/hook-ipypopout.py create mode 100644 standalone/hooks/hook-jdaviz.py create mode 100644 standalone/hooks/hook-jupyter_client.py create mode 100644 standalone/hooks/hook-mistune.py create mode 100644 standalone/hooks/hook-photutils.py create mode 100644 standalone/hooks/hook-regions.py create mode 100644 standalone/hooks/hook-skimage.py create mode 100644 standalone/jdaviz-cli-entrypoint.py create mode 100644 standalone/jdaviz.spec create mode 100644 standalone/test.py diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml new file mode 100644 index 0000000000..3368e4e7f5 --- /dev/null +++ b/.github/workflows/standalone.yml @@ -0,0 +1,141 @@ +name: Build standalone + +on: + push: + branches: + - main + - 'v*' + tags: + - 'v*' + +defaults: + run: + shell: bash {0} + + +jobs: + build_binary: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ubuntu, windows, macos] + steps: + # osx signing based on https://melatonin.dev/blog/how-to-code-sign-and-notarize-macos-audio-plugins-in-ci/ + - name: Import Certificates (macOS) + uses: apple-actions/import-codesign-certs@v1 + if: ${{ matrix.os == 'macos' }} + with: + p12-file-base64: ${{ secrets.DEV_ID_APP_CERT }} + p12-password: ${{ secrets.DEV_ID_APP_PASSWORD }} + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install jdaviz + run: pip install . + + - name: Install pyinstaller + run: pip install pyinstaller==5.11 + + - name: Create standalone binary + env: + DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }} + run: (cd standalone; pyinstaller ./jdaviz.spec) + + - name: Remove invalid files for OSX + # hopefully we can improve this in the future + # by using good hooks + # i think the issue is that we have a . in the name, there are many + # google hits on pyqt having the same issue + # and we might be able to remove it after https://github.com/pyinstaller/pyinstaller/pull/7619 + # is released (pyinstaller 5.13 probably) + if: ${{ matrix.os == 'macos' }} + run: | + rm -rf standalone/dist/jdaviz.app/Contents/MacOS/skimage/.dylibs + rm -rf standalone/dist/jdaviz.app/Contents/Resources/skimage/.dylibs + + - name: Codesign (OSX) + if: ${{ matrix.os == 'macos' }} + run: | + cd standalone/dist + codesign --deep --force --options=runtime --entitlements ../entitlements.plist --sign ${{ secrets.DEVELOPER_ID_APPLICATION }} --timestamp jdaviz.app + + - name: Create dmg (OSX) + # if we do not call always() GHA will && with success() + if: ${{ always() && (matrix.os == 'macos') }} + # it seems ditto (not zip) should be used in combination with notarization + # see https://developer.apple.com/forums/thread/116831 + # but dmg also works + # see https://github.com/glue-viz/glue-standalone-apps/blob/main/.github/workflows/build_stable.yml + run: | + rm -rf standalone/dist/jdaviz + hdiutil create -volname "Jdaviz" -srcfolder standalone/dist -ov -format UDZO standalone/dist/jdaviz.dmg + + - name: Notary step + stapling (OSX) + if: ${{ matrix.os == 'macos' }} + run: | + output=$(xcrun notarytool submit standalone/dist/jdaviz.dmg --apple-id ${{ secrets.NOTARIZATION_USERNAME }} --team-id ${{ secrets.TEAM_ID }} --wait --password ${{ secrets.NOTARIZATION_PASSWORD }}) || true + echo "$output" + uuid=$(echo "$output" | awk -F '[ :]+' '/id:/ {print $3; exit}') + echo "UUID: $uuid" + if [[ $output == *"status: Accepted"* ]]; then + echo "Great, notarization succeeded, staple it!" + xcrun stapler staple standalone/dist/jdaviz.dmg + else + echo "Log output for failed notarization: $uuid" + xcrun notarytool log --apple-id ${{ secrets.NOTARIZATION_USERNAME }} --team-id ${{ secrets.TEAM_ID }} --password ${{ secrets.NOTARIZATION_PASSWORD }} $uuid || true + fi + + - name: Validate app (OSX) + if: ${{ matrix.os == 'macos' }} + run: | + spctl -a -vvv -t execute standalone/dist/jdaviz.app + + - name: Run jdaviz cmd in background + if: ${{ matrix.os == 'macos' }} + run: ./standalone/dist/jdaviz.app/Contents/MacOS/jdaviz-cli imviz& + + - name: Run jdaviz cmd in background + if: ${{ matrix.os != 'macos' }} + run: ./standalone/dist/jdaviz/jdaviz-cli imviz& + + - name: Install playwright + run: (pip install playwright; playwright install chromium) + + - name: Install pytest + run: pip install pytest-playwright + + - name: Wait for Voila to get online + uses: ifaxity/wait-on-action@v1 + with: + resource: tcp:8866 + timeout: 60000 + + - name: Test standalone + run: (cd standalone; touch pytest.ini; JUPYTER_PLATFORM_DIRS=1 pytest test.py --video=on) + + - name: Upload Test artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results-${{ matrix.os }} + path: standalone/test-results + + - name: Upload jdaviz standalone (non-OSX) + if: ${{ always() && (matrix.os != 'macos') }} + uses: actions/upload-artifact@v3 + with: + name: jdaviz-standlone-${{ matrix.os }} + path: | + standalone/dist/jdaviz + + - name: Upload jdaviz standalone (OSX) + if: ${{ always() && (matrix.os == 'macos') }} + uses: actions/upload-artifact@v3 + with: + name: jdaviz-standlone-${{ matrix.os }} + path: standalone/dist/jdaviz.dmg diff --git a/CHANGES.rst b/CHANGES.rst index 929526c549..2593d792d8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -105,6 +105,10 @@ Other Changes and Additions - Gaussian smooth plugin excludes results from the gaussian smooth plugin from the input dataset dropdown. [#2239] +- CLI launchers no longer require data to be specified [#1960] + +- Added direct launchers for each config (e.g. ``specviz``) [#1960] + 3.5.1 (unreleased) ================== diff --git a/jdaviz/_astropy_init.py b/jdaviz/_astropy_init.py new file mode 100644 index 0000000000..95ea9ea7d6 --- /dev/null +++ b/jdaviz/_astropy_init.py @@ -0,0 +1,23 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +import os +import sys + +if not hasattr(sys.modules["__main__"], "__file__"): + # this happens under pyinstaller when running jdaviz cli + # which triggers an error in astropy, so we set it to the + # executable path of the cli executable + sys.modules["__main__"].__file__ = sys.executable + + +from astropy.tests.runner import TestRunner + +__all__ = ['__version__', 'test'] + +try: + from .version import version as __version__ +except ImportError: + __version__ = '' + +# Create the test function for self test +test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) +test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) diff --git a/jdaviz/cli.py b/jdaviz/cli.py index b088cf3522..9fd87e1f96 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -1,5 +1,6 @@ # Command-line interface for jdaviz +import inspect import os import pathlib import sys @@ -10,9 +11,11 @@ from jdaviz import __version__ from jdaviz.app import _verbosity_levels +from jdaviz import configs __all__ = ['main'] +CONFIGS_DIR = str(pathlib.Path(inspect.getfile(configs)).parent) JDAVIZ_DIR = pathlib.Path(__file__).parent.resolve() DEFAULT_VERBOSITY = 'warning' DEFAULT_HISTORY_VERBOSITY = 'info' @@ -151,3 +154,27 @@ def _main(config=None): main(filepaths=args.filepaths, layout=layout, instrument=args.instrument, browser=args.browser, theme=args.theme, verbosity=args.verbosity, history_verbosity=args.history_verbosity, hotreload=args.hotreload) + + +def _specviz(): + _main(config='specviz') + + +def _specviz2d(): + _main(config='specviz2d') + + +def _imviz(): + _main(config='imviz') + + +def _cubeviz(): + _main(config='cubeviz') + + +def _mosviz(): + _main(config='mosviz') + + +if __name__ == "__main__": + _main() diff --git a/jdaviz/configs/cubeviz/cubeviz.ipynb b/jdaviz/configs/cubeviz/cubeviz.ipynb new file mode 100644 index 0000000000..886d36661c --- /dev/null +++ b/jdaviz/configs/cubeviz/cubeviz.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# PREFIX\n", + "from jdaviz import Cubeviz\n", + "\n", + "cubeviz = Cubeviz(verbosity='JDAVIZ_VERBOSITY', history_verbosity='JDAVIZ_HISTORY_VERBOSITY')\n", + "data_path = 'DATA_FILENAME'\n", + "if data_path:\n", + " cubeviz.load_data('DATA_FILENAME')\n", + "cubeviz.app" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6-final" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/jdaviz/configs/default/default.ipynb b/jdaviz/configs/default/default.ipynb new file mode 100644 index 0000000000..b9d09d2ba8 --- /dev/null +++ b/jdaviz/configs/default/default.ipynb @@ -0,0 +1,50 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "from jdaviz.app import Application\n", + "app = Application(configuration='default')\n", + "app.verbosity = 'JDAVIZ_VERBOSITY'\n", + "app.history_verbosity = 'JDAVIZ_HISTORY_VERBOSITY'\n", + "data_path = 'DATA_FILENAME'\n", + "if data_path:\n", + " app.load_data('DATA_FILENAME')\n", + "app" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/jdaviz/configs/imviz/imviz.ipynb b/jdaviz/configs/imviz/imviz.ipynb new file mode 100644 index 0000000000..51c6fff42a --- /dev/null +++ b/jdaviz/configs/imviz/imviz.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# PREFIX\n", + "from jdaviz import Imviz\n", + "\n", + "imviz = Imviz(verbosity='JDAVIZ_VERBOSITY', history_verbosity='JDAVIZ_HISTORY_VERBOSITY')\n", + "data_path = 'DATA_FILENAME'\n", + "if data_path:\n", + " imviz.load_data('DATA_FILENAME')\n", + "imviz.app" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/jdaviz/configs/mosviz/mosviz.ipynb b/jdaviz/configs/mosviz/mosviz.ipynb new file mode 100644 index 0000000000..e9350d166f --- /dev/null +++ b/jdaviz/configs/mosviz/mosviz.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# PREFIX\n", + "import pathlib\n", + "from jdaviz import Mosviz\n", + "\n", + "data_path = pathlib.Path('DATA_FILENAME')\n", + "\n", + "mosviz = Mosviz(verbosity='JDAVIZ_VERBOSITY', history_verbosity='JDAVIZ_HISTORY_VERBOSITY')\n", + "data_path = 'DATA_FILENAME'\n", + "if data_path:\n", + " mosviz.load_data(directory=data_path, instrument='INSTRUMENT')\n", + "mosviz.app" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/jdaviz/configs/specviz/specviz.ipynb b/jdaviz/configs/specviz/specviz.ipynb new file mode 100644 index 0000000000..1f9f5fcd23 --- /dev/null +++ b/jdaviz/configs/specviz/specviz.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# PREFIX\n", + "from jdaviz import Specviz\n", + "\n", + "specviz = Specviz(verbosity='JDAVIZ_VERBOSITY', history_verbosity='JDAVIZ_HISTORY_VERBOSITY')\n", + "data_path = 'DATA_FILENAME'\n", + "if data_path:\n", + " specviz.load_data(data_path)\n", + "specviz.app" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6-final" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/jdaviz/core/tools.py b/jdaviz/core/tools.py index f6dea95d86..33aa315f20 100644 --- a/jdaviz/core/tools.py +++ b/jdaviz/core/tools.py @@ -19,7 +19,7 @@ __all__ = [] -ICON_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'icons') +ICON_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'data', 'icons')) # Override icons for built-in tools from glue-jupyter diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..479de7bc31 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,158 @@ +[metadata] +name = jdaviz +author = JDADF Developers +author_email = rosteen@stsci.edu +license = BSD 3-Clause +license_file = LICENSE.rst +url = https://jdaviz.readthedocs.io/en/latest/ +description = Astronomical data analysis development leveraging the Jupyter platform +long_description = file: README.rst +long_description_content_type = text/x-rst +edit_on_github = True +github_project = spacetelescope/jdaviz + +[options] +zip_safe = False +packages = find: +include_package_data = True +python_requires = >=3.8 +setup_requires = setuptools_scm +install_requires = + packaging + astropy>=4.3 + matplotlib + traitlets>=5.0.5 + bqplot>=0.12.36 + bqplot-image-gl>=1.4.11 + glue-core>=1.6.0 + glue-jupyter>=0.15.0 + echo>=0.5.0 + ipykernel>=6.19.4 + ipyvue>=1.6 + ipyvuetify>=1.7.0 + ipysplitpanes>=0.1.0 + ipygoldenlayout>=0.3.0 + ipywidgets>=8 + voila>=0.4 + pyyaml>=5.4.1 + specutils>=1.9 + specreduce>=1.3.0,<1.4.0 + photutils>=1.4 + glue-astronomy>=0.5.1 + asteval>=0.9.23 + idna + # vispy is an indirect dependency, but older vispy's don't play nice with jdaviz install + vispy>=0.6.5 + asdf>=2.14.3 + gwcs>=0.16.1 + regions>=0.6 + scikit-image + sidecar>=0.5.2 + ipypopout>=0.0.11 + astroquery + +[options.extras_require] +test = + pytest + pytest-astropy + pytest-tornasync +docs = + sphinx-rtd-theme + sphinx-astropy + +[options.package_data] +jdaviz = + data/* + data/*/* + *.vue + components/*.vue + configs/*/*/*/*.vue + configs/*/*.yaml + configs/*/*.ipynb +jdaviz.configs.imviz.tests = data/* + +[options.entry_points] +console_scripts = + jdaviz = jdaviz.cli:_main + specviz = jdaviz.cli:_specviz + specviz2d = jdaviz.cli:_specviz2d + imviz = jdaviz.cli:_imviz + cubeviz = jdaviz.cli:_cubeviz + mosviz = jdaviz.cli:_mosviz +gui_scripts = +jdaviz_plugins = + default = jdaviz.configs.default + cubeviz = jdaviz.configs.cubeviz + specviz = jdaviz.configs.specviz + mosviz = jdaviz.configs.mosviz + imviz = jdaviz.configs.imviz + +[tool:pytest] +testpaths = "jdaviz" "docs" +astropy_header = true +doctest_plus = enabled +text_file_format = rst +addopts = --doctest-rst --import-mode=append +filterwarnings = + error + ignore:numpy\.ufunc size changed:RuntimeWarning + ignore:numpy\.ndarray size changed:RuntimeWarning + ignore:Numpy has detected that you:DeprecationWarning + ignore:distutils Version classes are deprecated:DeprecationWarning + ignore:Passing unrecognized arguments to super:DeprecationWarning + ignore:.*With traitlets 4\.1, metadata should be set using the \.tag\(\) method:DeprecationWarning + ignore:Widget.* is deprecated:DeprecationWarning + ignore:.*np\.bool8.*is a deprecated alias for:DeprecationWarning + ignore:.*np\.uint0.*is a deprecated alias for:DeprecationWarning + ignore:.*np\.int0.*is a deprecated alias for:DeprecationWarning + ignore:zmq\.eventloop\.ioloop is deprecated in pyzmq:DeprecationWarning + ignore::DeprecationWarning:glue + ignore::DeprecationWarning:bqplot + ignore::DeprecationWarning:bqplot_image_gl + ignore::DeprecationWarning:bqscales + ignore::DeprecationWarning:traittypes + ignore::DeprecationWarning:voila + ignore:::specutils.spectra.spectrum1d + +[flake8] +max-line-length = 100 +# E123: closing bracket does not match indentation of opening bracket's line +# E126: continuation line over-indented for hanging indent +# E226: missing whitespace around arithmetic operator +# E402: Module level import not at top of file +# W503: line break before binary operator +# W504: line break after binary operator +ignore = E123,E126,E226,E402,W503,W504 + +[coverage:run] +omit = + jdaviz/_astropy_init* + jdaviz/conftest.py + jdaviz/*setup_package* + jdaviz/tests/* + jdaviz/*/tests/* + jdaviz/extern/* + jdaviz/version* + */jdaviz/_astropy_init* + */jdaviz/conftest.py + */jdaviz/*setup_package* + */jdaviz/tests/* + */jdaviz/*/tests/* + */jdaviz/extern/* + */jdaviz/version* + +[coverage:report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + # Don't complain about packages we have installed + except ImportError + # Don't complain if tests don't hit assertions + raise AssertionError + raise NotImplementedError + # Don't complain about script hooks + def main\(.*\): + # Ignore branches that don't pertain to this version of Python + pragma: py{ignore_python_version} + # Don't complain about IPython completion helper + def _ipython_key_completions_ diff --git a/standalone/entitlements.plist b/standalone/entitlements.plist new file mode 100644 index 0000000000..2ac1f14b34 --- /dev/null +++ b/standalone/entitlements.plist @@ -0,0 +1,13 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/standalone/hooks/hook-bqplot.py b/standalone/hooks/hook-bqplot.py new file mode 100644 index 0000000000..613900b9eb --- /dev/null +++ b/standalone/hooks/hook-bqplot.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files('bqplot') diff --git a/standalone/hooks/hook-debugpy.py b/standalone/hooks/hook-debugpy.py new file mode 100644 index 0000000000..5b7d0813ff --- /dev/null +++ b/standalone/hooks/hook-debugpy.py @@ -0,0 +1,8 @@ +from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs + +datas = collect_data_files("debugpy") +# we are picking up debugpy/_vendored/pydevd/pydevd_attach_to_process/attach_linux_amd64.dylib +datas = filter(lambda x: not x[0].endswith('.dylib'), datas) +# binaries = collect_dynamic_libs('omp') + +# breakpoint() \ No newline at end of file diff --git a/standalone/hooks/hook-glue.py b/standalone/hooks/hook-glue.py new file mode 100644 index 0000000000..4941c0920b --- /dev/null +++ b/standalone/hooks/hook-glue.py @@ -0,0 +1,4 @@ +from PyInstaller.utils.hooks import collect_data_files, copy_metadata + +datas = collect_data_files('glue') +datas += copy_metadata('glue-core') diff --git a/standalone/hooks/hook-glue_jupyter.py b/standalone/hooks/hook-glue_jupyter.py new file mode 100644 index 0000000000..98baed00c2 --- /dev/null +++ b/standalone/hooks/hook-glue_jupyter.py @@ -0,0 +1,4 @@ +from PyInstaller.utils.hooks import collect_data_files, copy_metadata + +datas = collect_data_files('glue_jupyter') +datas += copy_metadata('glue_jupyter') diff --git a/standalone/hooks/hook-ipypopout.py b/standalone/hooks/hook-ipypopout.py new file mode 100644 index 0000000000..55422b9aa5 --- /dev/null +++ b/standalone/hooks/hook-ipypopout.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files('ipypopout') diff --git a/standalone/hooks/hook-jdaviz.py b/standalone/hooks/hook-jdaviz.py new file mode 100644 index 0000000000..c7abff2b5e --- /dev/null +++ b/standalone/hooks/hook-jdaviz.py @@ -0,0 +1,4 @@ +from PyInstaller.utils.hooks import collect_data_files, copy_metadata + +datas = collect_data_files('jdaviz') +datas += copy_metadata('jdaviz') diff --git a/standalone/hooks/hook-jupyter_client.py b/standalone/hooks/hook-jupyter_client.py new file mode 100644 index 0000000000..6ecd059447 --- /dev/null +++ b/standalone/hooks/hook-jupyter_client.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +hiddenimports = collect_submodules("jupyter_client") diff --git a/standalone/hooks/hook-mistune.py b/standalone/hooks/hook-mistune.py new file mode 100644 index 0000000000..ac80b6f9e8 --- /dev/null +++ b/standalone/hooks/hook-mistune.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_submodules + +hiddenimports = collect_submodules("mistune") diff --git a/standalone/hooks/hook-photutils.py b/standalone/hooks/hook-photutils.py new file mode 100644 index 0000000000..31bc609d12 --- /dev/null +++ b/standalone/hooks/hook-photutils.py @@ -0,0 +1,5 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +hiddenimports = collect_submodules("photutils") +# for CITATION.rst +datas = collect_data_files('photutils') diff --git a/standalone/hooks/hook-regions.py b/standalone/hooks/hook-regions.py new file mode 100644 index 0000000000..9373dbae9a --- /dev/null +++ b/standalone/hooks/hook-regions.py @@ -0,0 +1,5 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +hiddenimports = collect_submodules("regions") +# for CITATION.rst +datas = collect_data_files('regions') diff --git a/standalone/hooks/hook-skimage.py b/standalone/hooks/hook-skimage.py new file mode 100644 index 0000000000..3f383b4319 --- /dev/null +++ b/standalone/hooks/hook-skimage.py @@ -0,0 +1,12 @@ +from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs + +datas = collect_data_files("skimage", includes=["*.pyi"]) +# osx does not like the .dylib directory with signing +# [('.../site-packages/skimage/.dylibs/libomp.dylib', 'skimage/.dylibs')] +binaries = collect_dynamic_libs('skimage') +if binaries and binaries[0][0].endswith('.dylib'): + assert len(binaries) == 1 + assert binaries[0][0].endswith('.dylibs/libomp.dylib') + binaries = [ + (binaries[0][0], 'skimage'), + ] diff --git a/standalone/jdaviz-cli-entrypoint.py b/standalone/jdaviz-cli-entrypoint.py new file mode 100644 index 0000000000..f77f124eef --- /dev/null +++ b/standalone/jdaviz-cli-entrypoint.py @@ -0,0 +1,24 @@ +import sys + + +def start_as_kernel(): + # similar to https://github.com/astrofrog/voila-qt-app/blob/master/voila_demo.py + import sys + + from ipykernel import kernelapp as app + app.launch_new_instance() + sys.argv = [app.__file__, sys.argv[3:]] + + +if __name__ == "__main__": + # When voila starts a kernel under pyinstaller, it will use sys.executable + # (which is this entry point again) + # if called like [sys.argv[0], "-m", "ipykernel_launcher", ...] + if len(sys.argv) >= 3 and sys.argv[1] == "-m" and sys.argv[2] == "ipykernel_launcher": + # it is important that we do not import jdaviz top level + # as that would cause it to import ipywidgets before the kernel is started + start_as_kernel() + else: + import jdaviz.cli + # should change this to _main, but now it doesn't need arguments + jdaviz.cli.main(layout="") diff --git a/standalone/jdaviz.spec b/standalone/jdaviz.spec new file mode 100644 index 0000000000..45768f2db4 --- /dev/null +++ b/standalone/jdaviz.spec @@ -0,0 +1,78 @@ +# -*- mode: python ; coding: utf-8 -*- +import sys +from pathlib import Path +import os + +from PyInstaller.building.build_main import Analysis +from PyInstaller.building.api import COLLECT, EXE, PYZ +from PyInstaller.building.osx import BUNDLE + +import jdaviz +codesign_identity = os.environ.get("DEVELOPER_ID_APPLICATION") + +# this copies over the nbextensions enabling json and the js assets +# for all the widgets +datas = [ + (Path(sys.prefix) / "share" / "jupyter", "./share/jupyter"), + (Path(sys.prefix) / "etc" / "jupyter", "./etc/jupyter"), +] + +block_cipher = None + + +a = Analysis( + ["jdaviz-cli-entrypoint.py"], + pathex=[], + binaries=[], + datas=datas, + hiddenimports=[], + hookspath=["hooks"], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + # executable name: dist/jdaviz/jdaviz-cli + # note: cannot be called jdaviz, because there is a directory called jdaviz + name="jdaviz-cli", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=codesign_identity, + entitlements_file="entitlements.plist", +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + # directory name: dist/jdaviz + name="jdaviz", +) +app = BUNDLE( + exe, + coll, + name="jdaviz.app", + icon=None, + entitlements_file="entitlements.plist", + bundle_identifier="edu.stsci.jdaviz", + version=jdaviz.__version__, +) diff --git a/standalone/test.py b/standalone/test.py new file mode 100644 index 0000000000..fb3276a7fa --- /dev/null +++ b/standalone/test.py @@ -0,0 +1,12 @@ +import re + +from playwright.sync_api import Page, expect + + +def test_voila_basics(page: Page): + page.goto("http://localhost:8866/") + + # basic voila is loaded + page.locator("body.theme-light").wait_for() + # when jdaviz is loaded (button at the top left) + page.locator("text=Import Data").wait_for() From 0b3ea9d7b4231cf9d054f6c8ef5833f3ba3cae6f Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 6 Jul 2023 08:58:49 -0400 Subject: [PATCH 158/283] replace all hacky traitlet replacements with send_state --- .../default/plugins/line_lists/line_lists.py | 47 ++++++++----------- .../plugins/model_fitting/model_fitting.py | 10 +--- .../plugins/slit_overlay/slit_overlay.py | 5 +- 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/jdaviz/configs/default/plugins/line_lists/line_lists.py b/jdaviz/configs/default/plugins/line_lists/line_lists.py index d324ea420c..f27b3eaad8 100644 --- a/jdaviz/configs/default/plugins/line_lists/line_lists.py +++ b/jdaviz/configs/default/plugins/line_lists/line_lists.py @@ -513,10 +513,8 @@ def _list_from_notebook(self, msg): list_contents[row["listname"]]["lines"].append(temp_dict) tmp_names_rest.append(row["name_rest"]) - self.loaded_lists = [] - self.loaded_lists = loaded_lists - self.list_contents = {} - self.list_contents = list_contents + self.send_state('loaded_lists') + self.send_state('list_contents') self._viewer.plot_spectral_lines(tmp_names_rest) self.update_line_mark_dict() @@ -654,9 +652,9 @@ def vue_show_all_in_list(self, listname): for line in lc[listname]["lines"]: line["show"] = True self._viewer.spectral_lines.loc[line["name_rest"]]["show"] = True - # Trick traitlets into updating - self.list_contents = {} + self.list_contents = lc + self.send_state('list_contents') self._viewer.plot_spectral_lines() self.update_line_mark_dict() @@ -665,14 +663,12 @@ def vue_hide_all_in_list(self, listname): """ Toggle all lines in list to be hidden """ - lc = self.list_contents name_rests = [] - for line in lc[listname]["lines"]: + for line in self.list_contents[listname]["lines"]: line["show"] = False name_rests.append(line["name_rest"]) - # Trick traitlets into updating - self.list_contents = {} - self.list_contents = lc + + self.send_state('list_contents') self._viewer.erase_spectral_lines(name_rest=name_rests) self.update_line_mark_dict() @@ -686,14 +682,12 @@ def vue_plot_all_lines(self, event): sender=self, color="error") self.hub.broadcast(warn_message) return - lc = self.list_contents - for listname in lc: - for line in lc[listname]["lines"]: + for listname in self.list_contents: + for line in self.list_contents[listname]["lines"]: line["show"] = True self._viewer.spectral_lines["show"] = True - # Trick traitlets into updating - self.list_contents = {} - self.list_contents = lc + + self.send_state('list_contents') self._viewer.plot_spectral_lines() self.update_line_mark_dict() @@ -707,13 +701,11 @@ def vue_erase_all_lines(self, event): sender=self, color="error") self.hub.broadcast(warn_message) return - lc = self.list_contents - for listname in lc: - for line in lc[listname]["lines"]: + for listname in self.list_contents: + for line in self.list_contents[listname]["lines"]: line["show"] = False - # Trick traitlets into updating - self.list_contents = {} - self.list_contents = lc + + self.send_state('list_contents') self._viewer.erase_spectral_lines() self.update_line_mark_dict() @@ -798,11 +790,10 @@ def vue_set_color(self, data): color = data['color'] if "listname" in data: listname = data["listname"] - # force a copy so that the change is picked up by traitlets - lc = self.list_contents[listname].copy() - lc["color"] = color - for line in lc["lines"]: + self.list_contents[listname]["color"] = color + + for line in self.list_contents[listname]["lines"]: line["colors"] = color # Update the astropy table entry name_rest = line["name_rest"] @@ -811,7 +802,7 @@ def vue_set_color(self, data): if name_rest in self.line_mark_dict: self.line_mark_dict[name_rest].colors = [color] - self.list_contents = {**self.list_contents, listname: lc} + self.send_state('list_contents') elif "linename" in data: pass diff --git a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py index e7ccc53395..66a92c49b3 100644 --- a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py @@ -241,10 +241,7 @@ def _update_parameters_from_fit(self): m["parameters"] = temp_params - # Trick traitlets into updating the displayed values - component_models = self.component_models - self.component_models = [] - self.component_models = component_models + self.send_state('component_models') def _update_parameters_from_QM(self): """ @@ -283,10 +280,7 @@ def _update_parameters_from_QM(self): temp_params += temp_param m["parameters"] = temp_params - # Trick traitlets into updating the displayed values - component_models = self.component_models - self.component_models = [] - self.component_models = component_models + self.send_state('component_models') def _update_initialized_parameters(self): # If the user changes a parameter value, we need to change it in the diff --git a/jdaviz/configs/mosviz/plugins/slit_overlay/slit_overlay.py b/jdaviz/configs/mosviz/plugins/slit_overlay/slit_overlay.py index 400e72a523..3ec0734111 100644 --- a/jdaviz/configs/mosviz/plugins/slit_overlay/slit_overlay.py +++ b/jdaviz/configs/mosviz/plugins/slit_overlay/slit_overlay.py @@ -152,7 +152,6 @@ def remove_slit_overlay(self): # We need to do the following instead of just removing directly on # the marks otherwise traitlets doesn't register a change in the # marks. - marks = image_figure.marks.copy() - marks.remove(self._slit_overlay_mark) - image_figure.marks = marks + image_figure.marks.remove(self._slit_overlay_mark) + image_figure.send_state('marks') self._slit_overlay_mark = None From 94021238b8e6d3b07fa0675f8c7109540a0e079f Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 6 Jul 2023 08:44:47 -0700 Subject: [PATCH 159/283] Remove annulus from aperture photometry plugin (#2276) * Remove annulus from aperture photometry plugin. Use the draw tool. [ci skip] [rtd skip] * Add change log --- CHANGES.rst | 5 ++ docs/imviz/plugins.rst | 9 ++- .../aper_phot_simple/aper_phot_simple.py | 39 +---------- .../aper_phot_simple/aper_phot_simple.vue | 24 ------- .../imviz/tests/test_simple_aper_phot.py | 66 ++++++++----------- 5 files changed, 42 insertions(+), 101 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2593d792d8..9bb6c63a69 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -60,6 +60,11 @@ Cubeviz Imviz ^^^^^ +- Simple Aperture Photometry plugin: Custom annulus background options are removed. + Please draw/load annulus as you would with other region shapes, then select it + in the plugin from Subset dropdown for the background. Using annulus region as + aperture is not supported. [#2276] + Mosviz ^^^^^^ diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index a69819d66f..922061dbaa 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -181,15 +181,18 @@ an interactively selected region. A typical workflow is as follows: object of interest using its center of mass, if you wish. Depending on the object, it may take several iterations for re-centering to converge, or it may never converge at all. + + .. note:: + + You cannot use annulus region as aperture (an exception will be thrown) + but you may use it for background (see below). + 5. If you want to subtract background before performing photometry, you have the following 3 options. Otherwise if your image is already background subtracted, choose "Manual" and leave the background set at 0: * Manual: Enter the background value in the :guilabel:`Background value` field. This value must be in the same unit as display data, if applicable. - * Annulus: Enter its inner radius and width in the :guilabel:`Annulus inner radius` - and :guilabel:`Annulus width`, respectively. Median of the pixels within - the annulus region will be used but the annulus shape will not be shown on display. * Subset: Define a region for background calculation (median) using Subset draw tool and select that region using the :guilabel:`Background` dropdown menu. Only regions created with the :guilabel:`replace` option are acceptable as background regions diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index 5091df49d5..9b078450a3 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -14,11 +14,8 @@ from packaging.version import Version from photutils.aperture import (ApertureStats, CircularAperture, EllipticalAperture, RectangularAperture) -from regions import (CircleAnnulusPixelRegion, CirclePixelRegion, EllipsePixelRegion, - RectanglePixelRegion) from traitlets import Any, Bool, Integer, List, Unicode, observe -from jdaviz.core.custom_traitlets import FloatHandleEmpty from jdaviz.core.events import SnackbarMessage, LinkUpdatedMessage from jdaviz.core.region_translators import regions2aperture, _get_region_from_spatial_subset from jdaviz.core.registries import tray_registry @@ -38,8 +35,6 @@ class SimpleAperturePhotometry(PluginTemplateMixin, DatasetSelectMixin, TableMix bg_subset_items = List().tag(sync=True) bg_subset_selected = Unicode("").tag(sync=True) background_value = Any(0).tag(sync=True) - bg_annulus_inner_r = FloatHandleEmpty(0).tag(sync=True) - bg_annulus_width = FloatHandleEmpty(10).tag(sync=True) pixel_area = Any(0).tag(sync=True) counts_factor = Any(0).tag(sync=True) flux_scaling = Any(0).tag(sync=True) @@ -66,7 +61,7 @@ def __init__(self, *args, **kwargs): 'bg_subset_items', 'bg_subset_selected', default_text='Manual', - manual_options=['Manual', 'Annulus'], + manual_options=['Manual'], allowed_type='spatial') headers = ['xcenter', 'ycenter', 'sky_center', @@ -161,7 +156,7 @@ def _on_link_update(self, msg): if self.dataset_selected == '' or self.subset_selected == '': return - # Force background auto-calculation (including annulus) to update when linking has changed. + # Force background auto-calculation to update when linking has changed. self._subset_selected_changed() @observe('subset_selected') @@ -173,18 +168,6 @@ def _subset_selected_changed(self, event={}): try: self._selected_subset = _get_region_from_spatial_subset(self, subset_selected) self._selected_subset.meta['label'] = subset_selected - - if isinstance(self._selected_subset, CirclePixelRegion): - self.bg_annulus_inner_r = self._selected_subset.radius - elif isinstance(self._selected_subset, EllipsePixelRegion): - self.bg_annulus_inner_r = max(self._selected_subset.width, - self._selected_subset.height) * 0.5 - elif isinstance(self._selected_subset, RectanglePixelRegion): - self.bg_annulus_inner_r = np.sqrt(self._selected_subset.width ** 2 + - self._selected_subset.height ** 2) * 0.5 - else: # pragma: no cover - raise TypeError(f'Unsupported region shape: {self._selected_subset.__class__}') - self.subset_area = int(np.ceil(self._selected_subset.area)) except Exception as e: @@ -206,30 +189,12 @@ def _calc_bg_subset_median(self, reg): # photutils/background/_utils.py --> nanmedian() return np.nanmedian(img_stat) # Naturally in data unit - @observe('bg_annulus_inner_r', 'bg_annulus_width') - def _bg_annulus_updated(self, *args): - if self.bg_subset_selected != 'Annulus': - return - - try: - inner_r = float(self.bg_annulus_inner_r) - reg = CircleAnnulusPixelRegion( - self._selected_subset.center, inner_radius=inner_r, - outer_radius=inner_r + float(self.bg_annulus_width)) - self.background_value = self._calc_bg_subset_median(reg) - - except Exception: # Error snackbar suppressed to prevent excessive queue. - self.background_value = 0 - @observe('bg_subset_selected') def _bg_subset_selected_changed(self, event={}): bg_subset_selected = event.get('new', self.bg_subset_selected) if bg_subset_selected == 'Manual': # we'll later access the user's self.background_value directly return - if bg_subset_selected == 'Annulus': - self._bg_annulus_updated() - return try: reg = _get_region_from_spatial_subset(self, bg_subset_selected) diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue index f4b0ca21f0..defadb323a 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue @@ -36,30 +36,6 @@ - - - - - - - - - - Date: Thu, 6 Jul 2023 14:10:08 -0400 Subject: [PATCH 160/283] update style guide for consistency between plugins (#2286) * update style guide for consistence between plugins --- docs/dev/ui_style_guide.rst | 21 ++++++--- jdaviz/components/plugin_add_results.vue | 3 +- .../plugins/export_plot/export_plot.vue | 44 +++++++++---------- .../plugins/subset_plugin/subset_plugin.vue | 12 ++--- .../spectral_extraction.vue | 8 ++-- 5 files changed, 49 insertions(+), 39 deletions(-) diff --git a/docs/dev/ui_style_guide.rst b/docs/dev/ui_style_guide.rst index af2edf7960..ca43d7bb2f 100644 --- a/docs/dev/ui_style_guide.rst +++ b/docs/dev/ui_style_guide.rst @@ -14,28 +14,37 @@ try to adhere to the following principles: ``j-tray-plugin`` stylesheet (``jdaviz/components/tray_plugin.vue``). * Each item should be wrapped in a ``v-row``, but avoid any unnecessary additional wrapping-components (``v-card-*``, ``v-container``, etc). -* Only use ``v-col`` components (within the ````) if multiple +* Only use ``v-col`` components (within a ````) if multiple components are necessary in a single row. Always emphasize readability at the default/minimum width of the plugin tray, rather than using columns that result in a ton of text overflow. -* Action buttons should have ``color="primary"`` if it loads something into the plugin, or - ``color="accent"`` if applying something to the viewers/apps/data. +* Use ```` to align content to the right (such as action buttons). +* Action buttons should use ```` with ``color="accent"`` if applying something + to the viewers/apps/data, and ``color="primary"`` otherwise. * To remove vertical padding from rows (i.e., two successive buttons stacked vertically), use ````. -* Use ```` to align content to the right (such as action buttons). * Use new ``Header Text`` to separate content within a plugin (instead of nested cards, ``v-card-subtitle``, etc). +* Plugin settings should use ```` immediately at the top of the plugin. + Optional "sections" of a plugin or editing dynamically-created components should use + ```` to make most use of horizontal and vertical space. * Number entries should use a ```` component *unless* requiring support for scientific notation (in which case ```` can be used with stripping invalid characters and type-casting in python). To handle emptying the input component without raising a traceback, - use an ``IntHandleEmpty`` traitlet instead, along with form-validation (see below) and/or - checks on the python-side to handle the case of an empty string. + use an ``IntHandleEmpty`` or ``FloatHandleEmpty`` traitlet instead, along with form-validation + (see below) and/or checks on the python-side to handle the case of an empty string. * Use form validation wherever possible, and disable action buttons if the relevant validation does not pass. This is preferred to raising errors through snackbars after pressing an action button. To do this, wrap the relevant section in a ````, create a ``form_valid_section_name = Bool(False).tag(sync=True)`` in the python class for the plugin, add rules to any relevant inputs, and set ``:disabled="!form_valid_section_name"`` to any action buttons. +* When validation requires checking multiple inputs simultaneously, the validation error message + can be displayed as its own element using `` + WARNING: warning message``. + This should be positioned where the error makes most sense in the flow of the input elements + and/or immediately above the action button to explain why it is disabled. + For warnings/errors that require more attention, use a ```` instead. * Select input elements should default whenever possible (not start as empty), and self-hide if only one valid option. Whenever possible, inputs should use form validation rules with red text explaining the error and disabling action buttons. When one selection/check makes others diff --git a/jdaviz/components/plugin_add_results.vue b/jdaviz/components/plugin_add_results.vue index 2660f84d7d..693da081ee 100644 --- a/jdaviz/components/plugin_add_results.vue +++ b/jdaviz/components/plugin_add_results.vue @@ -55,7 +55,8 @@ {{action_label}}{{label_overwrite ? ' (Overwrite)' : ''}} diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.vue b/jdaviz/configs/default/plugins/export_plot/export_plot.vue index 67408e4451..2344453128 100644 --- a/jdaviz/configs/default/plugins/export_plot/export_plot.vue +++ b/jdaviz/configs/default/plugins/export_plot/export_plot.vue @@ -12,26 +12,26 @@ />
- - + Export to PNG - - + + Export to SVG - - + @@ -95,9 +95,25 @@ + + + Interrupt recording and delete movie file + + diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue index 638385be9a..70f1b1c05c 100644 --- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue @@ -159,7 +159,7 @@
- + Export Trace @@ -273,7 +273,7 @@ - + Export Background Image @@ -297,7 +297,7 @@ - + Export Background Spectrum @@ -321,7 +321,7 @@ - + Export Background-Subtracted Image From f99c213a3fa98d0bfb047c05dae7d55d1df361cc Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 7 Jul 2023 14:18:57 -0400 Subject: [PATCH 161/283] subset plugin components to use filter instead of allowed_type (#2287) * subset plugin components to use filter instead of allowed_type * implement is_not_aperture and is_not_composite filters which is then used by aperture photometry's aperture * test coverage for exclusion in aperture dropdown --- CHANGES.rst | 2 +- .../plugins/model_fitting/model_fitting.py | 2 +- .../aper_phot_simple/aper_phot_simple.py | 4 +- .../aper_phot_simple/aper_phot_simple.vue | 4 +- .../imviz/tests/test_simple_aper_phot.py | 4 ++ .../plugins/line_analysis/line_analysis.py | 4 +- jdaviz/core/template_mixin.py | 57 ++++++++++++------- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9bb6c63a69..484bde6a45 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -63,7 +63,7 @@ Imviz - Simple Aperture Photometry plugin: Custom annulus background options are removed. Please draw/load annulus as you would with other region shapes, then select it in the plugin from Subset dropdown for the background. Using annulus region as - aperture is not supported. [#2276] + aperture is not supported. [#2276, #2287] Mosviz ^^^^^^ diff --git a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py index 66a92c49b3..4b2bd3ebd6 100644 --- a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py @@ -132,7 +132,7 @@ def __init__(self, *args, **kwargs): 'spatial_subset_items', 'spatial_subset_selected', default_text='Entire Cube', - allowed_type='spatial') + filters=['is_spatial']) else: self.spatial_subset = None diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index 9b078450a3..b8e0d77f62 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -55,14 +55,14 @@ def __init__(self, *args, **kwargs): 'subset_items', 'subset_selected', default_text=None, - allowed_type='spatial') + filters=['is_spatial', 'is_not_composite', 'is_not_annulus']) self.bg_subset = SubsetSelect(self, 'bg_subset_items', 'bg_subset_selected', default_text='Manual', manual_options=['Manual'], - allowed_type='spatial') + filters=['is_spatial', 'is_not_composite']) headers = ['xcenter', 'ycenter', 'sky_center', 'sum', 'sum_aper_area', diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue index defadb323a..62c50113da 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue @@ -18,7 +18,7 @@ :selected.sync="subset_selected" :show_if_single_entry="true" label="Aperture" - hint="Select aperture region for photometry." + hint="Select aperture region for photometry (cannot be an annulus or composite subset)." />
@@ -27,7 +27,7 @@ :selected.sync="bg_subset_selected" :show_if_single_entry="true" label="Background" - hint="Select subset region for background calculation." + hint="Select subset region for background calculation (cannot be a composite subset)." /> diff --git a/jdaviz/configs/imviz/tests/test_simple_aper_phot.py b/jdaviz/configs/imviz/tests/test_simple_aper_phot.py index 14c6e02c7a..121a77ed24 100644 --- a/jdaviz/configs/imviz/tests/test_simple_aper_phot.py +++ b/jdaviz/configs/imviz/tests/test_simple_aper_phot.py @@ -224,6 +224,10 @@ def test_annulus_background(imviz_helper): PixCoord(x=20.5, y=37.5), inner_radius=20.5, outer_radius=30.5) imviz_helper.load_regions([annulus_2]) + # Subset 4 (annulus) should be available for the background but not the aperture + assert 'Subset 4' not in phot_plugin.subset.choices + assert 'Subset 4' in phot_plugin.bg_subset.choices + phot_plugin.subset_selected = 'Subset 3' phot_plugin.bg_subset_selected = 'Subset 4' diff --git a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py index 5365e7cb4a..7aa9ecf360 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py @@ -130,7 +130,7 @@ def __init__(self, *args, **kwargs): 'continuum_subset_items', 'continuum_subset_selected', default_text='Surrounding', - allowed_type='spectral') + filters=['is_spectral']) # when accessing the selected data, access the spectrum-viewer version self.dataset._viewers = [self._default_spectrum_viewer_reference_name] @@ -142,7 +142,7 @@ def __init__(self, *args, **kwargs): 'spatial_subset_items', 'spatial_subset_selected', default_text=SPATIAL_DEFAULT_TEXT, - allowed_type='spatial') + filters=['is_spatial']) else: self.spatial_subset = None diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 1b8fece455..ca06dbadad 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -14,6 +14,7 @@ SubsetCreateMessage, SubsetDeleteMessage, SubsetUpdateMessage) +from glue.core.roi import CircularAnnulusROI from glue.core.subset import RoiSubsetState from glue_jupyter.bqplot.image import BqplotImageView from glue_jupyter.widgets.linked_dropdown import get_choices as _get_glue_choices @@ -745,7 +746,7 @@ class LayerSelect(SelectPluginComponent): """ def __init__(self, plugin, items, selected, viewer, multiselect=None, - default_text=None, manual_options=[], allowed_type=None, + default_text=None, manual_options=[], default_mode='first'): """ Parameters @@ -937,8 +938,7 @@ class SubsetSelect(SelectPluginComponent): * create (empty) traitlets in the plugin * register with all the automatic logic in the plugin's init by passing the string names - of the respective traitlets. Pass ``allowed_type='spectral'`` or ``allowed_type='spatial'`` - to only support spectral or spatial subsets, respectively. + of the respective traitlets. * use component in plugin template (see below) * refer to properties above based on the interally stored reference to the instantiated object of this component @@ -956,7 +956,7 @@ class SubsetSelect(SelectPluginComponent): """ def __init__(self, plugin, items, selected, selected_has_subregions=None, - viewers=None, default_text=None, manual_options=[], allowed_type=None, + viewers=None, default_text=None, manual_options=[], filters=[], default_mode='default_text'): """ Parameters @@ -975,27 +975,23 @@ def __init__(self, plugin, items, selected, selected_has_subregions=None, default_text : str or None the text to show for no selection. If not provided or None, no entry will be provided in the dropdown for no selection. - manual_options: list + manual_options : list list of options to provide that are not automatically populated by subsets. If ``default`` text is provided but not in ``manual_options`` it will still be included as the first item in the list. - allowed_type : str or None - whether to filter to 'spatial' or 'spectral' types of subsets. If not provided or None, - will include both entries. + filters : list + list of strings (for built-in filters) or callables to filter to only valid options. """ super().__init__(plugin, items=items, selected=selected, + filters=filters, selected_has_subregions=selected_has_subregions, viewers=viewers, default_text=default_text, manual_options=manual_options, default_mode=default_mode) - if allowed_type not in [None, 'spatial', 'spectral']: - raise ValueError("allowed_type must be None, 'spatial', or 'spectral'") - self._allowed_type = allowed_type - if selected_has_subregions is not None: self.selected_has_subregions = False @@ -1029,14 +1025,29 @@ def _delete_subset(self, subset): if self.selected not in self.labels: self._apply_default_selection() - def _update_subset(self, subset, attribute=None): - if self._allowed_type is not None and _subset_type(subset) != self._allowed_type: - return + def _is_valid_item(self, subset): + def is_spectral(subset): + return _subset_type(subset) == 'spectral' + def is_spatial(subset): + return _subset_type(subset) == 'spatial' + + def is_not_composite(subset): + return not hasattr(subset.subset_state, 'state1') + + def is_not_annulus(subset): + # this will be considered "not an annulus" if it is composite, even + # if that composite subset contains an annulus + return (not is_not_composite(subset) + or not isinstance(subset.subset_state.roi, CircularAnnulusROI)) + + return super()._is_valid_item(subset, locals()) + + def _update_subset(self, subset, attribute=None): if subset.label not in self.labels: # NOTE: this logic will need to be revisited if generic renaming of subsets is added # see https://github.com/spacetelescope/jdaviz/pull/1175#discussion_r829372470 - if subset.label.startswith('Subset'): + if subset.label.startswith('Subset') and self._is_valid_item(subset): # NOTE: += will not trigger traitlet update self.items = self.items + [self._subset_to_dict(subset)] # noqa else: @@ -1084,10 +1095,12 @@ def selected_subset_state(self): @property def selected_subset_mask(self): get_data_kwargs = {'data_label': self.plugin.dataset.selected} - if self._allowed_type: - get_data_kwargs[f'{self._allowed_type}_subset'] = self.selected + if 'is_spectral' in self.filters: + get_data_kwargs['spectral_subset'] = self.selected + elif 'is_spatial' in self.filters: + get_data_kwargs['spatial_subset'] = self.selected - if self.app.config == 'cubeviz' and self._allowed_type == 'spectral': + if self.app.config == 'cubeviz' and 'is_spectral' in self.filters: viewer_ref = getattr(self.plugin, '_default_spectrum_viewer_reference_name', self.viewers[0].reference_id) @@ -1143,7 +1156,7 @@ def __init__(self, *args, **kwargs): 'spectral_subset_selected_has_subregions', viewers=[spectrum_viewer], default_text='Entire Spectrum', - allowed_type='spectral') + filters=['is_spectral']) class SpatialSubsetSelectMixin(VuetifyTemplate, HubListener): @@ -1180,7 +1193,7 @@ def __init__(self, *args, **kwargs): 'spatial_subset_selected', 'spatial_subset_selected_has_subregions', default_text='No Subset', - allowed_type='spatial') + filters=['is_spatial']) class DatasetSpectralSubsetValidMixin(VuetifyTemplate, HubListener): @@ -1460,6 +1473,8 @@ def __init__(self, plugin, items, selected, the name of the items traitlet defined in ``plugin`` selected : str the name of the selected traitlet defined in ``plugin`` + filters : list + list of strings (for built-in filters) or callables to filter to only valid options. default_text : str or None the text to show for no selection. If not provided or None, no entry will be provided in the dropdown for no selection. From 683d989cc3cd3a3cb6cc73b2026c47651cb34b73 Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Fri, 7 Jul 2023 15:03:05 -0400 Subject: [PATCH 162/283] Jdaviz Launcher: Identify compatible configs and request user to select config (#2267) * Prep launch method without autodetect * Identify config before launching * Codestyle * Support "empty" filepaths * Filepath fallback when autoconfig doesn't need to open file * Move open to launcher module * Missing list encapsulation * Changelog * Combine changelogs --- CHANGES.rst | 3 +- jdaviz/__init__.py | 2 +- jdaviz/cli.py | 4 +- jdaviz/core/data_formats.py | 70 ++++--------------- jdaviz/core/launcher.py | 129 +++++++++++++++++++++++++++++------- 5 files changed, 124 insertions(+), 84 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 484bde6a45..19194b37fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,8 @@ New Features - The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] -- Add first-pass launcher to select config and auto-identify data. [#2257] +- Add launcher to select and identify compatible configurations, + and require --layout argument when launching standalone. [#2257, #2267] Cubeviz ^^^^^^^ diff --git a/jdaviz/__init__.py b/jdaviz/__init__.py index fdddfd2e84..8553c6671e 100644 --- a/jdaviz/__init__.py +++ b/jdaviz/__init__.py @@ -19,7 +19,7 @@ from jdaviz.configs.cubeviz import Cubeviz # noqa: F401 from jdaviz.configs.imviz import Imviz # noqa: F401 from jdaviz.utils import enable_hot_reloading # noqa: F401 -from jdaviz.core.data_formats import open # noqa: F401 +from jdaviz.core.launcher import open # noqa: F401 # Clean up namespace. del os diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 9fd87e1f96..755dd825ab 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -19,6 +19,7 @@ JDAVIZ_DIR = pathlib.Path(__file__).parent.resolve() DEFAULT_VERBOSITY = 'warning' DEFAULT_HISTORY_VERBOSITY = 'info' +ALL_JDAVIZ_CONFIGS = ['cubeviz', 'specviz', 'specviz2d', 'mosviz', 'imviz'] def main(filepaths=None, layout='default', instrument=None, browser='default', @@ -119,8 +120,7 @@ def _main(config=None): 'loaded from FILENAME.') filepaths_nargs = '*' if config is None: - parser.add_argument('--layout', default='', choices=['cubeviz', 'specviz', 'specviz2d', - 'mosviz', 'imviz'], + parser.add_argument('--layout', default='', choices=ALL_JDAVIZ_CONFIGS, help='Configuration to use.') if (config == "mosviz") or ("mosviz" in sys.argv): filepaths_nargs = 1 diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 6ede5130a9..32f464f704 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -11,8 +11,6 @@ from stdatamodels import asdf_in_fits from jdaviz.core.config import list_configurations -from jdaviz import configs as jdaviz_configs -from jdaviz.cli import DEFAULT_VERBOSITY, DEFAULT_HISTORY_VERBOSITY __all__ = [ 'guess_dimensionality', @@ -156,8 +154,8 @@ def identify_helper(filename, ext=1): Returns ------- - helper_name : str - Name of the best-guess helper for ``filename``. + helper_name : list of str + Name of the best-guess compatible helpers for ``filename``. Fits HDUList : astropy.io.fits.HDUList The HDUList of the file opened to identify the helper @@ -172,7 +170,7 @@ def identify_helper(filename, ext=1): if filename.lower().endswith('asdf'): # ASDF files are only supported in jdaviz for # Roman WFI 2D images, so suggest imviz: - return ('imviz', None) + return (['imviz'], None) # Must use memmap=False to force close all handles and allow file overwrite hdul = fits.open(filename, memmap=False) @@ -208,10 +206,10 @@ def identify_helper(filename, ext=1): # could be 2D spectrum or 2D image. break tie with WCS: if has_spectral_axis: if n_axes > 1: - return ('specviz2d', hdul) - return ('specviz', hdul) + return (['specviz2d'], hdul) + return (['specviz'], hdul) elif not isinstance(data, fits.BinTableHDU): - return ('imviz', hdul) + return (['imviz'], hdul) # Ensure specviz is chosen when ``data`` is a table or recarray # and there's a "known" spectral column name: @@ -237,7 +235,7 @@ def identify_helper(filename, ext=1): # if at least one spectral column is found: if sum(found_spectral_columns): - return ('specviz', hdul) + return (['specviz'], hdul) # If the data could be spectral: for cls in [Spectrum1D, SpectrumList]: @@ -247,10 +245,10 @@ def identify_helper(filename, ext=1): # first catch known JWST spectrum types: if (n_axes == 3 and recognized_spectrum_format.find('s3d') > -1): - return ('cubeviz', hdul) + return (['cubeviz'], hdul) elif (n_axes == 2 and recognized_spectrum_format.find('x1d') > -1): - return ('specviz', hdul) + return (['specviz'], hdul) # we intentionally don't choose specviz2d for # data recognized as 's2d' as we did with the cases above, @@ -260,62 +258,22 @@ def identify_helper(filename, ext=1): # Use WCS to break the tie below: elif n_axes == 2: if has_spectral_axis: - return ('specviz2d', hdul) - return ('imviz', hdul) + return (['specviz2d'], hdul) + return (['imviz'], hdul) elif n_axes == 1: - return ('specviz', hdul) + return (['specviz'], hdul) try: # try using the specutils registry: valid_format, config = identify_data(filename) - return (config, hdul) + return ([config], hdul) except ValueError: # if file type not recognized: pass if n_axes == 2 and not has_spectral_axis: # at this point, non-spectral 2D data are likely images: - return ('imviz', hdul) + return (['imviz'], hdul) raise ValueError(f"No helper could be auto-identified for {filename}.") - - -def open(filename, show=True, **kwargs): - ''' - Automatically detect the correct configuration based on a given file, - load the data, and display the configuration - - Parameters - ---------- - filename : str (path-like) - Name for a local data file. - show : bool - Determines whether to immediately show the application - - All other arguments are interpreted as load_data arguments for - the autoidentified configuration class - - Returns - ------- - Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper - The autoidentified ConfigHelper for the given data - ''' - # Identify the correct config - helper_str, hdul = identify_helper(filename) - viz_class = getattr(jdaviz_configs, helper_str.capitalize()) - - # Create config instance - verbosity = kwargs.pop('verbosity', DEFAULT_VERBOSITY) - history_verbosity = kwargs.pop('history_verbosity', DEFAULT_HISTORY_VERBOSITY) - viz_helper = viz_class(verbosity=verbosity, history_verbosity=history_verbosity) - - # Load data - data = hdul if (hdul is not None) else filename - viz_helper.load_data(data, **kwargs) - - # Display app - if show: - viz_helper.show() - - return viz_helper diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index f079b72582..fc2fd7b093 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -2,7 +2,76 @@ from ipywidgets import jslink from jdaviz import configs as jdaviz_configs -from jdaviz.core.data_formats import open as jdaviz_open +from jdaviz.cli import DEFAULT_VERBOSITY, DEFAULT_HISTORY_VERBOSITY, ALL_JDAVIZ_CONFIGS +from jdaviz.core.data_formats import identify_helper + + +def open(filename, show=True, **kwargs): + ''' + Automatically detect the correct configuration based on a given file, + load the data, and display the configuration + + Parameters + ---------- + filename : str (path-like) + Name for a local data file. + show : bool + Determines whether to immediately show the application + + All other arguments are interpreted as load_data/load_spectrum arguments for + the autoidentified configuration class + + Returns + ------- + Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper + The autoidentified ConfigHelper for the given data + ''' + # Identify the correct config + compatible_helpers, hdul = identify_helper(filename) + if len(compatible_helpers) > 1: + raise NotImplementedError(f"Multiple helpers provided: {compatible_helpers}." + "Unsure which to launch") + else: + return _launch_config_with_data(compatible_helpers[0], hdul, show, **kwargs) + + +def _launch_config_with_data(config, data=None, show=True, **kwargs): + ''' + Launch jdaviz with a specific, known configuration and data + + Parameters + ---------- + config : str (path-like) + Name for a local data file. + data : str or any Jdaviz-compatible data + A filepath or Jdaviz-compatible data object (such as Spectrum1D or CCDData) + show : bool + Determines whether to immediately show the application + + All other arguments are interpreted as load_data/load_spectrum arguments for + the autoidentified configuration class + + Returns + ------- + Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper + The loaded ConfigHelper with data loaded + ''' + viz_class = getattr(jdaviz_configs, config.capitalize()) + + # Create config instance + verbosity = kwargs.pop('verbosity', DEFAULT_VERBOSITY) + history_verbosity = kwargs.pop('history_verbosity', DEFAULT_HISTORY_VERBOSITY) + viz_helper = viz_class(verbosity=verbosity, history_verbosity=history_verbosity) + + # Load data + if data not in (None, ''): + viz_helper.load_data(data, **kwargs) + + # Display app + if show: + viz_helper.show() + + return viz_helper def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d']): @@ -15,37 +84,49 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d'] children=['Welcome to Jdaviz']) intro_row.children = [welcome_text] - # Filepath row - filepath_row = v.Row() - text_field = v.TextField(label="File Path", v_model=None) - - def load_file(filepath): - if filepath: - helper = jdaviz_open(filepath, show=False) - main.children = [helper.app] - - open_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", - children=[v.Icon(children=["mdi-upload"])]) - open_data_btn.on_event('click', lambda btn, event, data: load_file(btn.value)) - jslink((text_field, 'v_model'), (open_data_btn, 'value')) - - filepath_row.children = [text_field, open_data_btn] - # Config buttons - def create_config(config): - viz_class = getattr(jdaviz_configs, config.capitalize()) - main.children = [viz_class().app] + def create_config(config, data=None): + helper = _launch_config_with_data(config, data, show=False) + main.children = [helper.app] - btns = [] + btns = {} + loaded_data = None for config in configs: config_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[config.capitalize()]) - config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0])) - btns.append(config_btn) + config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0], + loaded_data)) + btns[config] = config_btn # Create button row btn_row = v.Row() - btn_row.children = btns + btn_row.children = list(btns.values()) + + # Filepath row + filepath_row = v.Row() + text_field = v.TextField(label="File Path", v_model=None) + + def enable_compatible_configs(filepath): + nonlocal loaded_data + if filepath in (None, ''): + compatible_helpers = ALL_JDAVIZ_CONFIGS + loaded_data = None + else: + compatible_helpers, loaded_data = identify_helper(filepath) + if len(compatible_helpers) > 0 and loaded_data is None: + loaded_data = filepath + + for config, btn in btns.items(): + btn.disabled = not (config in compatible_helpers) + + id_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", + children=[v.Icon(children=["mdi-magnify"])]) + id_data_btn.on_event('click', lambda btn, event, data: enable_compatible_configs(btn.value)) + jslink((text_field, 'v_model'), (id_data_btn, 'value')) + + filepath_row.children = [text_field, id_data_btn] + + # Create Launcher main.children = [intro_row, filepath_row, btn_row] return main From 885996895114de874c3c91a38142f252091dec2c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 10 Jul 2023 12:12:28 -0400 Subject: [PATCH 163/283] nested toolbar logic to hide inapplicable tools (#2284) * nested toolbar logic to hide inapplicable tools and handle reassignment of primary/active accordingly * test coverage for case of removing active tool * address comments from @pllim --- CHANGES.rst | 2 + docs/imviz/displayimages.rst | 21 +++--- jdaviz/components/toolbar_nested.py | 86 ++++++++++++++++++++++-- jdaviz/components/toolbar_nested.vue | 6 +- jdaviz/configs/imviz/plugins/tools.py | 3 + jdaviz/configs/imviz/tests/test_tools.py | 29 ++++++++ jdaviz/core/tools.py | 7 ++ 7 files changed, 136 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 19194b37fb..d7f6d67152 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,8 @@ New Features - Add launcher to select and identify compatible configurations, and require --layout argument when launching standalone. [#2257, #2267] +- Viewer toolbar items hide themselves when they are not applicable. [#2284] + Cubeviz ^^^^^^^ diff --git a/docs/imviz/displayimages.rst b/docs/imviz/displayimages.rst index ec8b2fb357..752d00a450 100644 --- a/docs/imviz/displayimages.rst +++ b/docs/imviz/displayimages.rst @@ -85,27 +85,27 @@ are saved when beginning a zoom selection or when activating a pan/zoom tool. Box Zoom and Linked Box Zoom ============================ -Linked Box Zoom is an Imviz-specific feature that allows the user to zoom -images in multiple different viewers simultaneously, not unlike +Linked Box Zoom is an Imviz-specific feature enabled only when there are multiple viewers that +allows the user to zoom images in multiple different viewers simultaneously, not unlike :ref:`imviz_pan_zoom`. Single-viewer Box Zoom is also available and is used in a similar way as in -other Jdaviz tools. To access this option, right-click on the Linked Box Zoom button -and left-click on the second option down to select it. +other Jdaviz tools. To access this option when there are multiple viewers, +right-click on the Linked Box Zoom button and left-click on the second option down to select it. .. _imviz_pan_zoom: Pan/Zoom and Linked Pan/Zoom ============================ -Linked Pan/Zoom is an Imviz-specific feature that allows the user to pan and zoom -images in multiple different viewers simultaneously. This works by matching images +Linked Pan/Zoom is an Imviz-specific feature enabled only when there are multiple viewers that +allows the user to pan and zoom images in multiple different viewers simultaneously. This works by matching images based on the way they are linked together. Images are linked by pixels on load time, but you can re-link them via WCS using :ref:`imviz-link-control`. Single-viewer Pan/Zoom is also available and is used in a similar way as in -other Jdaviz tools. To access this option, right-click on the Linked Pan/Zoom button -and left-click on the second option down to select it. +other Jdaviz tools. To access this option when there are multiple viewers, right-click on the +Linked Pan/Zoom button and left-click on the second option down to select it. When in either of these modes, clicking on the image will recenter the image to the location under cursor. @@ -219,8 +219,9 @@ Blinking Blinking is an Imviz-specific functionality that allows a user to quickly switch between viewing two or more images, as long as they are linked (see :ref:`imviz_pan_zoom` for -more on linking behavior). This can be done by selecting the |icon-blink| icon and -then left-clicking on the image to blink forward; right-clicking would blink backwards. +more on linking behavior). This can be done by selecting the |icon-blink| icon (only available if +there are more than one image loaded in the viewer) and then left-clicking on the image to blink +forward; right-clicking would blink backwards. You can also blink forward by pressing the "b" key on your keyboard while moused over the image. If you press Shift + "b" ("B"), you may blink backwards. diff --git a/jdaviz/components/toolbar_nested.py b/jdaviz/components/toolbar_nested.py index 90e742ff1e..67bdbec019 100644 --- a/jdaviz/components/toolbar_nested.py +++ b/jdaviz/components/toolbar_nested.py @@ -2,14 +2,19 @@ import traitlets from glue.config import viewer_tool +from glue.core import HubListener from glue.icons import icon_path from glue.viewers.common.tool import CheckableTool from glue_jupyter.common.toolbar_vuetify import BasicJupyterToolbar, read_icon +from jdaviz.core.events import (AddDataMessage, RemoveDataMessage, + ViewerAddedMessage, ViewerRemovedMessage, + SpectralMarksChangedMessage) + __all__ = ['NestedJupyterToolbar'] -class NestedJupyterToolbar(BasicJupyterToolbar): +class NestedJupyterToolbar(BasicJupyterToolbar, HubListener): template_file = (__file__, 'toolbar_nested.vue') # defined in BasicJupyterToolbar: @@ -28,6 +33,8 @@ class NestedJupyterToolbar(BasicJupyterToolbar): def __init__(self, viewer, tools_nested, default_tool_priority=[]): super().__init__(viewer) + self.viewer = viewer + self._max_menu_ind = len(tools_nested) # iterate through the nested list. The first item in each entry # is treated as the default primary tool of that subcategory, @@ -43,7 +50,12 @@ def __init__(self, viewer, tools_nested, default_tool_priority=[]): self.add_tool(tool, menu_ind=menu_ind, has_suboptions=len(subtools) > 1, - primary=i == 0) + primary=i == 0, + visible=True) + + # handle logic for tool visibilities (which will also handle setting the primary + # to something other than the first entry, if necessary) + self._update_tool_visibilities() # default_tool_priority allows falling back on an existing tool # if its the primary tool. If no items in default_tool_priority @@ -53,13 +65,76 @@ def __init__(self, viewer, tools_nested, default_tool_priority=[]): self.default_tool_priority = default_tool_priority self._handle_default_tool() + for msg in (AddDataMessage, RemoveDataMessage, ViewerAddedMessage, ViewerRemovedMessage, + SpectralMarksChangedMessage): + self.viewer.hub.subscribe(self, msg, + handler=lambda _: self._update_tool_visibilities()) + + def _is_visible(self, tool_id): + # tools can optionally implement self.is_visible(). If not NotImplementedError + # the tool will always be visible + if hasattr(self.tools[tool_id], 'is_visible'): + return self.tools[tool_id].is_visible() + return True + + def _update_tool_visibilities(self): + needs_deactivate_active = False + for menu_ind in range(self._max_menu_ind): + has_primary = False + n_visible = 0 + primary_id = None + if self.active_tool_id: + current_primary_active = self.tools_data[self.active_tool_id]['menu_ind'] == menu_ind # noqa + else: + current_primary_active = False + for tool_id, info in self.tools_data.items(): + if info['menu_ind'] != menu_ind: + continue + visible = self._is_visible(tool_id) + if visible: + n_visible += 1 + + if tool_id == self.active_tool_id: + # then the primary was already set by which tool is active + if visible: + # then keep this as primary + primary = True + else: + # then the currently active tool is being removed, so we need to deactivate + # the tool and allow the default logic to be triggered + primary = False + needs_deactivate_active = True + elif current_primary_active and self._is_visible(self.active_tool_id): + # then we are keeping the previous primary + primary = False + else: + # then there is no primary already in this submenu, so the first visible + # entry will be selected as primary + primary = visible and not has_primary + + if primary: + primary_id = tool_id + has_primary = True + + self.tools_data[tool_id] = {**info, + 'primary': primary, + 'visible': visible} + if primary_id: + self.tools_data[primary_id] = {**self.tools_data[primary_id], + 'has_suboptions': n_visible > 1} + + # mutation to dictionary needs to be manually sent to update the UI + self.send_state("tools_data") + if needs_deactivate_active: + self.active_tool_id = None + def _handle_default_tool(self): # default to the first item in the default_tool_priority list that is currently # already primary for tool_id in self.default_tool_priority: if tool_id not in self.tools_data: continue - if self.tools_data[tool_id]['primary']: + if self.tools_data[tool_id]['primary'] and self.tools_data[tool_id]['visible']: self.active_tool_id = tool_id break @@ -75,7 +150,7 @@ def _on_change_v_model(self, event): if event['old'] is not None: self.active_tool_id = event['old'] - def add_tool(self, tool, menu_ind, has_suboptions=True, primary=False): + def add_tool(self, tool, menu_ind, has_suboptions=True, primary=False, visible=True): # NOTE: this method is essentially copied from glue-jupyter's BasicJupyterToolbar, # but we need extra values in the tools_data dictionary. We could call super(), # but then that would create tools_data twice, which would then cause @@ -92,7 +167,8 @@ def add_tool(self, tool, menu_ind, has_suboptions=True, primary=False): 'img': read_icon(path, 'svg+xml'), 'menu_ind': menu_ind, 'has_suboptions': has_suboptions, - 'primary': primary + 'primary': primary, + 'visible': visible } } diff --git a/jdaviz/components/toolbar_nested.vue b/jdaviz/components/toolbar_nested.vue index 99fba72465..ff06e7979f 100644 --- a/jdaviz/components/toolbar_nested.vue +++ b/jdaviz/components/toolbar_nested.vue @@ -1,7 +1,7 @@