From 037628f792cf1d72b4dfcdb8a25be0135b2bf70b Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 2 Oct 2020 11:24:13 +0100 Subject: [PATCH 001/116] Updated access to AbstractData subclasses in Chart --- src/main/java/pulse/ui/components/Chart.java | 23 ++++++++++++------- src/main/resources/images/splash.png | Bin 87697 -> 61618 bytes 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index f7da5ca4..5de38f46 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -36,6 +36,7 @@ import pulse.AbstractData; import pulse.HeatingCurve; +import pulse.input.ExperimentalData; import pulse.input.IndexRange; import pulse.tasks.SearchTask; @@ -215,7 +216,7 @@ public void plot(SearchTask task, boolean extendedCurve) { } - public void plotSingle(AbstractData curve) { + public void plotSingle(HeatingCurve curve) { requireNonNull(curve); var plot = chart.getXYPlot(); @@ -228,13 +229,19 @@ public void plotSingle(AbstractData curve) { plot.getRenderer(4).setSeriesPaint(0, black); } - public XYSeries series(AbstractData curve, String title, boolean extendedCurve) { + public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { + final int realCount = curve.getAlteredSignalData().size(); + final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); + return series(curve, title, startTime, realCount, extendedCurve); + } + + public XYSeries series(ExperimentalData curve, String title, boolean extendedCurve) { + return series(curve, title, 0, curve.actualNumPoints(), extendedCurve); + } + + private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, boolean extendedCurve) { var series = new XYSeries(title); - - final int realCount = curve.actualNumPoints(); - double startTime = 0; - if (curve instanceof HeatingCurve) - startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); + int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); for (var i = 0; i < iStart && extendedCurve; i++) @@ -242,7 +249,7 @@ public XYSeries series(AbstractData curve, String title, boolean extendedCurve) for (var i = iStart; i < realCount; i++) series.add(factor * curve.timeAt(i), curve.signalAt(i)); - + return series; } diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index 8250c4760f8ca0df5e68399e18cd32f277426220..5f49895c5526fe2842e4129a3568702cfbe976c2 100644 GIT binary patch literal 61618 zcmXt8V{~RqvyN>$Z|r1Z+s?$v#I|kQnAo;$Yhv5Bo!oQge0S}&|J3e&x>nUwUDaJX zTtQA89tH;n2nYyXQbI%#2nfU#2nZMk3gRD$UAE&65D*rhhqAhpqP{D@&cW8i+{zf> z+Of^tr5`&-{(tzt?)UvDJjNmv;^O;=4Gh4bJr6NU*{O#9{Dm(9f!N? zc%-=N;^*75&lcb}`ccB`jo4X+`_Eg%zF0y@N&dllFQY22FTvXB!0(`!4BQe1l89|z z1&>mG+2c>oYn$in!R$CzF;%a_WjEcNoGJN&D~z#KpVzTz z7+w)#AMH{7P-Gj9>2sJ!q9ZqhL)KyRdB@5k98WDt_whGq_N9e+xoE9wH@vb5Dj#fV zx2yG>_o|uJdxT6JP)t~_!xa9j9KMf4Lf31>uz&!KBUGAUbd@$Z;k0?(DPp4C?t2q? zKYpLB)8}n8j3OzBOK3;KfPv{{vf|*HrsQlMiYaCtU2SBo&1+aB;wx)7a#E%pX$cIe z8`fbQ5PK_9vmbY<4w*)P)S*d$pnOdXOKM^XHQ|-|SQuL})WugFfi}E&2x~3&nPkg$jjml)zUzT0b1fC$ERVg~2 z=UuVLn>%8Gmhe-Acxm> zu~`mh<|R4yWlb42-7XVVEt@_gDV1FfZ}ZcHMs%GG7&$Vhoo{58c+O2eTEtLtGMpra zjF3z^$-ppN)TZ6RnFUNGnXd%qFp+R`Nw_V6Okyzi(6cr#2^}?Q?^T_gmOaTWFScop zQk%Xcjxh_LnJx15%|njoV=EP~=t?;!-3{|n^I5$a4yGn6T1;<>o@NaPj?ydVQQ-mBlIp^t5z=WoFl{ylRjFQ*J-3~i2$7ZU)qT$NMVeg z;C2Q?S|vJe`iw!);zj_s_$!c-4RCS@s5Ygy6npdob% z6p^jh@VCL-01jgGqb&vT$$4kOx7_M7qI05cfki*h4B#fzV!Aij>h^J(P6yW>x&&lc z<;Sk(Wgn@&rc7I(t*2kv%aSINc3;kqjcM#k0HGk8EIfp{ntOa80MEn7wVOZUZFxlD6|p9~`sP5B&`s)4mW*u~J6Vvd8ZLkG+@Qg%OP^|pj1#rNIBcC+=p;PzO9wy}HVckO&p zW2gIfzsL!4f(JfjdH7&M-!t62Fc6-_Z&SU6)lU`;3PAdXp6i8I?ZN|<1iH~8rmd%4 zz=$Ez7qJjKQAr(K(8gPsEqJpr|Jqz1EFIhN~rZs5$-rtwbt2?|CH zGVQ}2`*N)xF=eK6I?b}sr=M}X&o#j#&#vJ+j@Z0jXozn&30gTC(^!|>N1+-;x-b72 z?zR?%RO(-zq`iQKlGW@uTJfWfxwLjcNV)v%50#~57;=HDi^)CL5m2qqxYzndkT?@T_Q1M`;yP1s2NM4f_NQg}5oyZOFm) zGBcY73Ot`-VA?tVM-?LD6{#wtsrtGFW=}_0NNZU?me-7w7)bCY&d(rxN&-$+6-dGv*G?#kI4)J*=kZ>-=FWOrszJD|BUoX6=5-9@t(fRg5lXp?HnDSVnoW9P!qoc$G%?wJ zPT2c+-EgGPDMk_$gGbsHd5&^Q zRXgoqhi&GGuqv9h{54Mr&azMne;A7v&{2YB@$-a>`GjOTC||(bS(1BUbt^jY1f5#} z&02Bprco)nXc{4_z~WU%6u7rFi6&rxy?)BJ-J1t0jy!7EDHTSJ(1R?01|k!i)1MM7hDpE) zS}2TZeFqXuE^e5>Qd{0!TZc3WVtx|5U=RK)G|HhX*xq7?(MHxaP#v|cVK$bJJ4ru zaTytb5I+V+NOlyOt-*pZl51`*@&k-&GvE0sl=^cLUV8=(SS7U#$bKJLG(F>$xH20A zDQq84yrj^UKGdOinjkPU=83gURrtDiY9Q}OU>n+Iw|Wi;1ez|}KF1$MHk7!X3VR1l zmZqq{VwdND*(h=&7ye|3t^ff_k;oqngO?`0ns*C2O5TlJU zrTbQG{gS|owcrP`^&x0}Cs=*{+LYd`Tcu-?Kp*0Sw!pkWVmVmVXs%7S>TqPVv>(NU zeHTGprKrzcdC-*ZBp;$HJ4`%>u71EFcO3FGJ5mDne$K*jWxF1&i)Y;k>;y!KsrFz3 zPzKm&^xmweHF7GKm+@>fUU5t2Ctiw~N5#gy=;~R!ATtvDwaAqNqxj?^^^8ptadGp!cU{t!w3Q*QHA}INC1@p)dD`uRWnn7xnMzN4E?aEz{U^K>?<67)hI3SdgsOURf zf-(3&#vFs>G2w;1&k9Y;L>ReZ)Z}U??7$baSh=vh?bM*bK&P^YP$Gd)z#bYLSU|EB zRbi|)TU?zk2))<`V;H%$YLu2Fu!cD5oTT!k2H?hmqx*$Zeii=3Gk3x?xT)soe?(q? ziDVPWFgf$E1;968Q58Tq*qG&1YmtdspdS1-7ui+xY=r(PQ6N~l903}T8nGq0aq@;=s6*f!n(@@D=ce3#@bPad<~9M8r@+EX=VyxG$S}#%6%tt-Owu>WGMcv zDidKGvftDw{A=fwZX23I1=iWZm_)}PE-X8J-Up%U%&q% zG~Tda%wLwlB27-A9W@=PFTE&yjK)5o6?ulhJvcwJWywWJoDibw>qiE6?cGBtli6NP7$2tlFi$qpO-|r%C{HGG=AJP!d z%PnC01Dphsh_atRfsTfJ_98nVkNgcwqvl>oh@8OiZP3Otrbv`RK=^au2(;>2tXic; z>@g%VAGA&}p_yTV8u%*6fsH>|=x0xe+P8{Oj;x3VkkN#1hS&gT|iu-iyf z*irvDjXE_>Xg2b?X|uW(r%Ucs{!nNKvGJxdvZu2Osuf13jZm_zrK@`X&=--Bz!+2# zGfEm2Dw!4uLnLl-q|48Cg#I1QV%KJ-gJfuvWoWEr#IeEfNe3OkAuJhRf3+W{FK6cC zMY4x_qM2%m5(bNeqgVYt(>}?b({{}tR4x~m;qaykq!*dvlK>SLTKLG0$Fb`c+euIM zGw#OQdnIA+RsHZoKR)p*c;+`WOZ~=gwGkl#6$m=)h58WbP#N00qQ^c6J=8+VHY$W? z8eOw+VfX%_6C|@%$k@6tf#5ulvs+3;<*E~el-O2T#NlWx!HA%QTtfvhO^InpoWr$v za$sEqka83M2)*_>P$+P5Mtt{*PI(yW7hv%Hty6#^tRepG8J*cf+|o%xOX=o z)2MMXJpQcI?zol^(3lyq1s4*`aP`nskU|a;_T*U&4Szr*zJ@X9?0K*9!dxbMiQPK=^?}=E{Ed`V|0oy!_m|0hpJh={wM-pR5u`Ar$nP;mhEgDI3DO9|OT^4v3{{~8qq$PYpySpai_432)1$_rC0e8I?_ zzy689P)U!+x${VZR2&F}(P8#-nj}|Irrno2PC&6h&hpE{iK8dP4=v`vgpoC7G)+zG zFWzNKQa*_+VO+j|0(CHWfrNvgpuZFV)Xi$7&R>+EWn6G##d6WE4V43NP!oy~tv##* zLS~2y@HLrwQy+jQ{b@_wx-VyW9aeFdY1q?g|18}5k2^s9Nl$F*{JAvyoBJ4Q#0t&K z%9bU_z!?fu_T1FL&GDdm0h79*KN{_F5l2B_#=$W*sbtK;gSMD~%t~H(GVoTMa$G^? zLB<}fAIuMnYBf)tJh=7x@SYo^J-yCt)lHTfUr&lW*SXbeWeJa~va$lVcJXMW(H8BX;N@_wq8(TCn zmWgIcALI7c%z+OEbyPqhjszhkS}OQu4(@QXXQ_H;2v0vaVv<7_v{rwZ*H;>h;xF&- zgZxO0F+Sb9tYo=VMI3qnR~VqE&)E(_BVRa@?scekr8@qAYPU1Y4~(dVT$*H_auv74 z4E&GmEQ+-za+cWX1+0a)u%Ly^FC1iGCqL&T z*prYAp5iPi83LxSH^#W#tMO^8UYRSEbl`og#!jMcewXzjmb2DAL>0VZXZ0Xv_~2{E zm~xaTZZ0UZstMbH_XCN`f_tnVQ=HT$!qY{iW2Q$Ajzl4#Z#%jA^!@ph2DoSr<$|dO zuO(QM-nnZ%sc8%z^`J;VqkeYn@q2mV_sd-dd@M-D4|Tq0Y01$EFQTMQ0vyUoj8}`ck z4!~_J7yBb56CA=$@F7_oPu^Ql1(^Ky+R z8slK*1MXf7TTkSIVI~TR^4sHi8*{aw)K`%Zl6W>`)8<$BiljT5LN+w=&|AvHZB}zP z?*{P*i5z-ah>f{_pfwrvY6)mwrc74<2FZt7=>B{mCJ_<5QYcKGNeD9`wGT+$JIJHh2KB?!wQf|TAG zLKc3AF{m^-EPw;uk!EnIToppXXfa<2P~Z4Ev*$(V*G`})7@+%u3}9quR+3Lthiw5u zCw?v&CK!%!**d7|$@iGhmuWf!@FruM}ZYmsHKK01{A3V&D7To#9tt$ zth0d5BQjysDUI9D zktCqayb>w95COUbh~1rgBLu#!dTtMEK6wM1&QUH8n9*Zj@zHNWCLf@J_oP|Wde~D( zKl&=+r3b6A$I>Ag>*+prsBIQvY8dTEN^As=%$JH~lZK_^$DLj`w(-cPx7KXvcOz1A zMdOs?cz~f0v!m|iNl+yB9?yx}oN7lw1V;awY1IeFq@KDs7v3vK(!jkDT+fGY<{MA3 zP@g5_7tWgm%!NA?5M$i=TmaaP*q@&b?0nO47R>ck$3$|I;CO-z0HF6i3B#$i-!#UJO6u+OKwGf589?AjVh$9=$_ynPA=ZsfkYiQ=I77JgZ$0#tJu zHNSHjca>(9&W5E;`KM*Nu3v1~opQG8;_D0OteR3>{NOBdpChWb;xaQx3$BjhjBK$` z#D%iylCKS{w-4@8oW+s?)q2S3t1wqAeje)6?nNefYk zDk`8Tb-r1ZgF88+8-Y+AG&XKL7=glvB2%D`JT14j^TX3(&VDv({kUbMGfQ?>BRXhb zQM2Qa7V7P`V|}(t_D2%`rY?Rz|J7;oz82^u`%+>M(MoA1HQfvT!u)dldTi?GNM{{L zl&hBGyG;DrHj=P4xg8{7N#eV3tipBFK=bKS>l{sLz1al6V6|^!yh?!>REqo;DX>mRQ0y77wNOLoAP4#Bh%gc}D&MI1LbQ0kXFmou=Lg^;Hwl_8j*Z1L-9`Mx9p=*{4K9!Lza1 zm`>gIe$=U7=(PoEj46bdgpov)|Ll%)*4XVF(x_TcKhORM&g*Qh|KprKAfbmWEg={b zg)?5-D-@*~jfQHz7VG&Eisu`l#nd*gcc3nSPE9(3_yyV1!3vv*YNJSr;MBA7B#s7k z0W#Tc?Mt}sau0UH52~iQu4@g%oKv{Gu6Y1;vJF9By`J6)sdWVuDBo@*5pz&Ng$C36 zj_s|4fYk~fU}Kw+luQS7;2?hvol8??Nh#3j)&FKe@coO@NJ}%x68skhVxV`VzLEuZhVav$&D!uE+<#`zTq8q% z$2W7MxVUcHW5>sZSN$i~JE>t2L)~z{PK1Q!=_u&ILwJJ%_by)iANU)UrfcUiwe0*~ z5WOFT2x1%9|9vY&YBmo57ML(;zuKAR5_P>%lP!$;tH3`)VnEh}J$@FhhuLM+c5nD!lMgL|SpCXNCgJVz+!~7$vp`U~<@=i=iW3{&S zuR$P4uHbmZ{4fAKh^GrgS(*XP88^$;Bf34fzFrK(Ix)#B;mIozbmOzsQ2@UspBa#7 z0oVEp`k6u%InGu!{4*>+ z^oyUw{j~_(#A<}u+FYk@1uxjoM(t>dUPzCyl_-1p;hF@L-zId|Ga2# z8m9y2dx=Aw3=5syC0Xb~HW!)SIPwK$@=xuNznT!R^v=Mb`85JD*C#0s^mXP8R=ylh zB>)4vELA|{i!d87A9f@|k?;Gq z7qo)M}65i|FLRhN+^u z9fLC|`hNJMTr1RE{lDZwb_La(H@|S>O#AgEx0bCIw%{99!Xo7_Z7#)EKbH&Trxw&> zfxdgDa!~aRF1tFS^vePJkt`+mBCHPnf2!aD!j5{wZnlPuF5TiKhI-;|sT&dYPYh`( z8X?pI6%g{qUxO-i$b2`wS>U<@&h{L`j`1Mp*w&l>XX<7CDls`@lITbl+TSG)Jy6^~zs>d|{!;>8m?us&<6@u~Gob-R1bHfme`Z?iB zY(L_>;$DsDJ~K(7@25F*Y7Z;$E#73J$R3H>GvS$WmYBbR zCpqkPwU*E7zs7pt{Q}EWvE3IvqR4jkXqFq7DZ@qPl(R#*@|I{rsSs^2@|4&I$y(jFS zh1WjO`Zbf}zMIcK?IEY3I{5m?P=B^06HQd-zGTHBmuiImbp4+rTnAU`y5MviD2k+Q z`fQ^1N?8n%y<>W3iwad}^#<2d5?h|0p%^UEhyJHhdkh4zv8^K4H#w8*2Ry}YUi838 z=^mBcKKwz=Cp{S~Uu=($l4X_7e;cRRhVxgi4asZ8wc$yA+shf>pwgF*?=;Bf-A^bq z6v*&EGJ}=;+uT^R+uuF=&0n~)nHH_v{1xY-P}knsyZ6U&b(h!*F{ed;m30a45DV4xsl!}Vpd5zxa+7;U;J zcOsy`zhpTy;)-+IJLN-XnTbfi<0M%?+nkuXtqF+;qtU(7o_aEvPA6UGd8HM%;QXc8 z;3n{FV)wZ*41XtVg+6G^6@M&Uu+SNY)G!h-Kd}N_@G-Fi1;BB*02EmS0OHRQgx{Zl zvGhfa_>R6K7+awklx_3nJ1t0>4)_@x!z$Q`q1{E`GCfl6PH3<`Yafj!h{mVPUm9P* z+FDKSeav5r`>eor*fCf?Lh|RS?Jsdgnp;t+<(8UDsMK%48%_7|jrEs1yUz4pNx|-B zHI(AP=aJe9;$97r0QHAz8RmW<7ZO<;d-MX_PA%q&UC*fb-L0SD_xi9&qzY@=KNhc#86}dx+T6pnV-ya zy?|qD)?1mXH-W&$49Z{>I!yl<5P2*B04=+KEBX;G6 z+GWbfR{|rhAx?KPIFGWbE;}|S|C8-)LOJ$}lzgrKZ%i3VZa~-Kx+=9EbQE5D?v)mC z#0wEU$ca2ziOjl()urrDV+BARJ}6x$0!<*A2iB2$@c)qNdX4;X9YEgod!W{J?W%IksE8(+V* z-t|FEz87{eChorkiL2j}^XqXTA%nFYvRMgQAMpqNJ)}wXG_j&T%_2h0f)jNQi#p;B6RpLW)UwhzDfHr5sT+SUjLx{(9Mv)%i=t zAKW89b<{>sn8GVQ=IL5%xrTTn6ew%U_HQ!sN0TzZE7cG`^*jb-JUyk=HWUU*&>b$ZYqw)D?kcGR^DDY}6I*NJ6DxkP z3`N=uVA3&nt^W0*GP3Jedvp^03t0Wnr?ID8vc4ycu)5{ljy?VriY@<9yrnnC-Ra(R zC)_RRdi`R>S0D}wq5+QK4a@7x=$LkjgrU}22y81$MSynkyA zu?_fxjAamzrgNqZ{U|AtajE3O^;)q>*N72QjErOxAuWN$OL z!v{qz!#BIb#v!2HrC7cYQB3{?OOb82OmQJUXQ$JSyWL`d(Lmd$QsX=D9|NIfMNK<+ zKGJ4akOWO^7w!ZS0y zKeH~JW8*qr`S4m?*`|QvJ)54;w>%|reHA_;tp?$zhyNL+x(9{cH&PJSC+hr3PWC5JZ-m4k zcC4Q4x07SmqLE%`+|Q?t-z(G(}lV=0Dn7;Ur_#?t9kk|*-R6j zvLw6j+80N60AqgsuR#X2eFtpTgl8jagoPdzgPtEt)W1aGw=h?=Yc83MCl- zXO3g?EH0Se-k=&i9h^+6i3_pOp|m{6?z6hc;7XpyL|(`T5Aq~c-jLosb2}pQ`d`a- zI-k^d%w;@a4j;*3js#*MACtqW2d?MTf=1K(aGI~Mykc z=6ra!{@A0gy!Tnx;;O`~3&{x|?SZI3McyCOR)1FO#Jm<^zvOj^sx`xSDw-x?7AI~> zd2qdO_i=Ff%4*!SEp!^w{hQ?>f?8p(y<;z;U3bgu=4z~^gXz0~&ZDkLhyy*o^)HYN z{)j@&x+Ad3ht8~L_j|wf%&OqVVhf|S6~vov1}3vZQzi6>o(~nE(f5FN*teWM{^!6e zGyNe;D$OVBu!$r5CQ=`(%iK_%DDNu$@NOHww&__8cvYE$2w`r{a&F>3F!#Lssu=9`5_*IfPH_5K%b4jM=ZSkWe z3YqEuG1yyn?dsnIL6A+6@X<>T$BAxvC^?GB&y>-7S4$U0`OUNH0km|GD^F zIo=NCkm^Se2)b7&Dj{lgT=7)Zj0+1!uG#i)d5AY0`2i^_l>N$Fiy9|_s~|;u)|}9A zmSTpg^|$6Hxq1T;or=&!-FR12oX?i%9mfy8?)OBB|0vivV|%h!1JIUCJQGHGoVfBu zzMnn!N#o%Ta~k0+`SM0ir)*G~7lFC|@|83?UMKhXTWN}IJhFYQRUO#4LCKqvUZ#UY zqr;F)w?b1moVkpWgwoW58#%;~@>k-BH2N20>eOphruG{EgGXOG z-EYQ0ix^;3boImF2LG<<#I{>qKQdi-)T5v zx5I48WxU-n|Ma_l)x@6;q=OnJ=n-nd8V#^9v471+f&IsE`Zi{Ek!C*We=6uCDLJHI z0TLl>b;q9Yo07-%qn6^0AnRY^s;bhjGaY=rSsHcIuQP+kOmy)!yCW%dN z^dKshf6VlLz>WF?(^gE1r;{^cy@|5Ng?rqoeqmgSCb-B-566{!gQYQd`o>DX8rlU0 zB8#_TS0xG9`l3Il)!?HFVmy^L%p`V)%9Aqm)g1SSO>DlQ^jzZ3>zyf8D5-8jD>mQ}_$)9$f?5n~sID%u{|@5?T*JXpUKKKzjZ zaUrby4Q92jGQ6+>#Fon&T828JSCZ?81afA19?l7g(X9E)=%|Qr_fP!C`@GaowQUh^ z6;?tkO2heVF6wi`!DIj)oB{`BV%-32ntFmLB_25)X@?5DB2;NjHPP?EJyA{dgQLPk z3v$PYp)6c7*VUGv*;1;yp^YpTO8g^!c0&@lo)?^MxCFq?2d~ds)BP<+WkAdvjl zJiux`yB^iFD3qIDt4^D$-44&hLEk&LO5tEB+jgueaxsPKrv7XA6)$H6xNo%NTJ8kf zAA9go_zUl!5>hL*?RXtSZib8*&`J;`nW$U!dW+|_cjwD4D31t#tPbVKd%jmi;pWBJ zk946{x_t1z-v=F@Slmf9xwgf(2bS=SY1P{ikifhX7?q0c-i`O6%c?hfk|Nl@o>;zD z=x)hQ$*!S~ZeLoa$ubQjQDfR$rPpr}{MX1@J^SsKD7qCWF%6pXAfnWOpGnkG}csHG5QITIB}msP7dDoqik_b8xlfs z{p)x6^7~_N_1L0LnAcYKfCZl9%Rkh6LBzeX@$3|NW%*IxJWX-mR;3in6zh+9U$t5dF1*zMj~9XU-6Ahz_kGsK;}@0V=qFJy|% zM)=FKCGzl?mcMb8B@@aO+)nzj)eGeD)oqEAKm(`x=gLQ{cRVy|KLk5nedVQ&Z4>z} z68WOxj)@b{I4#AWnfKoT6*G%oXByS~EgJG`ngo$=xaYSrz6&yOqqOsYIKsYJbVbYus#+Q6 z-nw{$;hP0J>gqdbH4K?)_cP7K{Z)X#u%9Qg!;99!u&IBx=Q^JFp69borv&iQ?Q&6d z_XM?Q)(idttm_V0-X@QDT+^A&Sx8neCbkv%bSwe}Uxw^G=1tYO=2e@$N6XYW7Tj2l z5YLvDs_52NgSV#rTPmJ1%u3Pa;(hOT_h^T)y^wVbGG!LewQKCph#tN>kn07>$|cV^ zH`O3+en$c&2yg+s8y1VISLDudtkvuUn=x(Pg_Sz=@2l`EtQH zY8cx%S(g(QMRLeZj6y-13s?@a>lh1MY$l>ltOV&u9jyx7KS!sqk6F-rS+uC(2E3bu zhU7C3zRHxEP&nZ6SC?qTk5ttB^(T`|L+qQu4n(kqIhKu6TL!!VhuWigSpY5(CkATRB}shH1BGHh=DnrSS1K^AB2(^(*5F&p zEEnel6Zq&bb*K9`=<+veno4GZm{l|fdNMP-MmO4`@O4%RcwZL-GVL#^%AJ=5Gl04M z7WO=Ll8YNd+Y-+7z1wxsa@+f*g98_=W}lNTb{>d)(c;wJah(|7)E20+<{ByBd&d@ zqESs3p?DdAr+5&M?hn6IhgF~)KkJL1$2&G&oMrJkQm`V6a&Bi6VdsQmFT`c0LtjV+ zQ?jG(u>}jY1nWsro`tqv=_|SBpi8J;H?&)eU!=9x8A=SC8GV5%H(4Xg&h6lh57CPk ze6~Mcv0}V{K-WPoTdlUWMMa%?Wo7O0Mug3y<4|(=1vq$xe0&g!0s|yuQ|9ML=h@yC z1E%iW_OrC1b}coMR<(GzTaI2iKX1?% z*;t6y#rVxQtfd%z;5Q}ZlC#p}wW{)DW#DL<7xPU`U0&!>W8UL`Xb8F}kQ+Zwhjox5 zQF{{l4slc3N=&oH$Lz{>V*8rR_7WaL%7Y;TWo-l%WK-x*SaM8%LVXAG`=QCi8^rR$ zx~@crMcY2UTx1c87`q zjGaO*dpPi&W|;F6T1I6t#;8W*Kyy5Bd8yJVlO;GAuPQknUqG<)$vefr~vZOy)vXbSKmy zC|$U#`-s??P&Rp)Z1FPi;}PN_nqg;#`MPi}K+lQ7f81Fl^bdy#B&0k<>rs@Dj(2$! z%ekKC!z+(PMSdJ%{|xV56Z*$q-}0*Rajt)O8a_j75R$r(o+Q)@?OoX29PIVoI1L8s zab|A{J=Sj~&dv6Lm`n7*0E|2gb4E(`96gkA#g#rJHTmfx2NjZc*Y`|yY=-LlR+Gj> zYh)r)78=D>H9TvNZt<~k+0DO^*^D^b^UEnRf!4D*9AF2 zBs)V4`_`Iq3By+KSurU(D_SKjq1K0FD23MPt}@FRi#T(k=t!prPK`-OvE)CTV((-v zU*_bqjSaIJwqIwu?1lWEc=+5d;k)lfhFBb;(#rg@xLp!w_vDtcrBB0$UZ?S%;ai_^FQpg84igmxGKe`QJU0Q4(Qpxa?&D z4f^s*Es4hLLn!Km&>QLM>ES1X(&={lmn3h&fJqdw4SZ*N3V=dgfWRe65(+P->lS_G zsT#qe!%vH8iw$)7KwRknuG@X%BtHia(TzMJjH~?9Gmxh50sI(_C&=77iZD)pZ@B9O!kuc3!$|t?08ZTEDay~4UVfPS?~n~gfC+@ zdGIaw!{^c;P)(WY6DNcH6ABpOGKQ$S*{GbrXtf)|({0ZD&&nM>>MzZRIX#_Hr^@5o z36WieplGSMwZX#Ws1LF1Qy${siGS&UQgA*B4299e70t)mfMP@mUVi~S^Xm#Mraa#G zlfn^%&cJt_Mb9pJDolU2a%xd6!o9Ud){=lrwyDzXHYqYlKO%Sg)^KSW=YiHbdrVIRtyiMzi**5@d z4d=MTcLiA$+mKw<3RUBVc1clba3A4wQDxbg`8dAALhrA(!-sX#15p|=zcVpuV)^fBl}=3+ArVEYXl9We$Zl=Y#M-hOYv+zKzNFe%h-pvQy+HG*1f4caG(L z^TnD6FAHs zux18dJ02jI=eHP*xF9M;<%?W-XFID&+3oh>=bCjgS{}z;wER_nX)#j{w0BsO*UTW# z7};DqoD&`g&&%&xLN_;oNxbpGNF}zhCPkn{K(R7L`O_Ft-z*Q=TDvIEUe{7~(uH`n zsrBIw``|5IDYG%d&Z%L=k(UyWF$PzTDtBz2lt*;o*35}jN;8Pv#&i>za}V6v*;!eI z;#f&(d!hq)?Qjy&V)PMz6r9~v#-?Q5OQTE#%;4ODV4G%b zqkW=uED?IGOn+Jk;^hLKd32%N`4ZQve3#<+Bg>bLuArMyu744bjv1k8xmU-*`cD0_ z|M`8OcCFfc^l+~=6xATNv&PsrMPdWq8g0$%ibM2arQ4Kn-hGSc!)r?h%cizVgq3wv z0=OVyI51RL87Xxl6r>nGWBuiRuw8SnyX~6*&W)?+F}Y=GQcM=)N*ChR=sd%<_xg+b zWN#ixA{zrmF5?$J|ke_dT|GjNppbSoJ|Ul{=BU2AwY_!)NG9s0c= z3n1RSL9+<1-i{nS$zkqb{c?I(9}#{%%C;QxzD6$9lC9KRV=pYW7TF z)_48&A>=R~Oci@Ok~5f<5_`s4uAV2h8GL= zHKm;@Z+QGj!|8SurN#1y6NtDKYbIbbgj## zs*Z;{_C1XjSCtgm4znXfk&*1Mj*1)p&NJs9PXqK49|tCV`;)5tnH>}xdTMh%?g~k{ zQ!!v-Y(rUTo9->rHRa*%7yksq$1g>gdy%`vgpkoVXgCCGJ$bEbHE^CT3q(wrKU`Mn zC=Bs>yD?(@)@ucJ!<5AkN;NU&9KKM9UndqzRh#3M*Vaa3&0I-9hwclayk}ge(5v9? znOx2s^TTM&l>o05Jrv*tR;T*Nd{)+3Fzi4`$ECr(C~6rBUO>30q1Cp^7_M|nJzF^y zjJkDoiN^C;#mBHKmoASZWwlh*^PK74luc#7?vu^E-oikPSy8fHblUnij~J>P#nX|< zV7G(Tq43qF-&YUKZvJ~+Y)_?S+lv)Hv2s{-`9-3qi!)f=gM8`!lqkZbfn5ZWx)Ocg z=Y;h?UEhKB7mBb<;x{EB2u>UNtBz;?D~*1WDrV?wEs=pi%NYm1s}jl-PXdoTZH$+| z5ZTM0D%vCKhq6)8FX1csO8Oc^pUwK}3RI-JDXj#%8~I zjF>a3@#IhSl_e@Yn7MHl{82x~59`aw@Q<3MgUO^>s_nF5-5@1XeqE|H<`7v_;I6cm z#Ml?0$sz<@2s5Y#`Lz!Rwlt#9N8#`$P3N#jViy5BP0T`6WqeOorrAxRm(;q9!F*l6 zBCamvYJwVG_{F|X_XwY!5NT=dNtfMLPMgqF_^|h-x7aP#W@|$t*64A$kam1yyHmn$ zm+1;lzjHy42oovX0_}})OI+xLe+St5d7qpJ?ZT4GMGDkhFxMT3?U#;R1O5-GKvutS z+hd4GUKhP%X~?;sM?HEe@!x)kebM)^r(WD|f-_`pF)FP9QCHm~52(T>ZO=+7yu`p2 zcUrTjd|#LT>h#RRi{ZMALhk$cMEpkBb&HHhE@2Dmy^g2INKF~Pdf*v+ev5R!&7i@B zOgU~Sb0(FL8*CqFaV%nq1TVi`-mc`>B`4&dd&%RXg~j=EhwL{K%}7JL4s7SoP-H{O;8qG&;I<_}5w!O?X7D0K@as z2wIxD+6H#jCy4kgf|fufVtlw_9jjKXBN$5O)1N$_sRh1H+eiqgs*ci-XfuaO$u0kV z3FnM#4$^d;gN3E%HGf7-p3Jw#kKuoR_auw=d9AZTIH-8$FRw87E9aBpOPzPqXR}qH zVm(szy!M~awa5{(!RntW7_6~#@v?>^%GbA^p>edtUhn|%x+?q^e*=+Oc$k%tM5w+? z%%~BFiY<8WJ%N43tzCcC;m6}mxE8VUDb(Y)li0Ex-?^VfL~@S4(9RfyESiC;eF@!z ztyzRXL@TiCc9F;%eT=7d(<~>4zuz*KENL>_b$cRAOXs$k8GLJgEjgW>WfY>hIC{0? zQMG<6%U5sW?*D@kgL9ZQrho}U3m86p2t#vyxL$I$Wj&>mBN~;5>P7YGRqkb8wIq9g zfOaIz3F$pAId%whCop;vm*=fwVPngf=eJn9bQOu|v+;E@tmwK5{k`W=;j0%BhwtfS@ zd~PFQzl8we(K^2Kn$teYwzi~aNGr0<;!r%XdNV6 z9YU^r3b$bo_BHn+vJX)UH>0SsF_%$Q@h$zu+R7$YW-Wpn`*HZ zJw#&fGW=KEi^w0!QQfAMHrB-R5$j$-xji^{r$M+;{MFlu=Z{7lqlzZsX^KmWc;=?L z6bA(gL$_^72_{{9K9_HOoR{`E2d6~&=vX2JA-~N~KX~hySBuR#-L#h4~Z?$>-rwh1~Vl z2@&pKjPBOGr{#0=)#Lcizc;lB{L%brK_%y08%dc6vAz*^0z%bW@sbcjC9{TMl}zf- zW{*7n3iNw_KsU6{$)k|CTNWW7xfyZYuaSetbt+8y{;t7mio+`pVBPc^u)5}-mifbQ zN1uVU{GWLEv9>NJ@!)k>7k;yOoAxMTjW_{q`w#Zjce`yo0{hm|pwB$!1c_3L3LWN) zA3uXLMrUKmt~uYu5&Wd<4wXZX=DHuJZB(AkO`pCP{{v6*{2tZ4q_{Na^~z846W_Uo z|9AgZ3QLFb?f*WB^dpQZ&{{M1q$!LY>8#Si{HQ{Rf+CAtQl#(RM*5f^L zBi_b^of_Z%e13iTQObr*ui$?8YPURO_GvdGgQDfIx4^3{!~ORcaNqbN9K{@QBAkiJ z9D`JEkj)QnXCEU4Ko;%F8QFr!8wBMOo^s^ z>t=5H`K#<{I&$ft2S92LDLq!L+1EN@#bQ1tOzWFeOlc*>T$k>)wvm*YCtp}kQwROQ zidXRL@&i>GwCgZx%G8v}ZMR=FtY7sZfi}WUOPilvGnPO%%`j)^Fg{T%Xj|DWzIZVo z?Cae}@MWbj!)m`!&8*)?Wi)kOQ}3Ic0IO~{qJAw}_bi*eB{U414gI~)ju6PqZCUxmKvA?)MMZXbxSU)&u94}_cWuwJ|id(>`qSv~)4AXwi(VZl*exF5WL4meRP+kpU(2<`PtE!6<>BQybX^YG-{a&qT zEJlnQ%XcP^>_3s(kEMJE`Bb%XasNldq>Or5M=CH#pa4BjLU96*KJ+6f>qLH$o|{* zvN`Y6+?2{~XGz79#k&#sTdg@5&ZZ>2``$@UOU^uf7=QcoX7;3VGu?eHN_+Zj%rF z*w^sZzHyKs?u#lFo!yrx--P?Y%iUR4V@){+Y3p{^O6UkI_#^R0zkqJ4>HC);1gc;J zTE%+UCg6*2KqeXoY)5wz9*G2p|2WO1pI!IJBQrD8Lo{}BD%mF;)@*L-(ZXd%d422l z+(8qu9iTWY2Fyh2Bp`c85tm;&m#^G@3HN^ED*kZi1$^Snu?)=#P!V&9y6W&rb3a(j zvuhnv5g}m1;tkXW4op`wrWBLgSNog_(nn9Fpv|Q33n*4@-`j2i6`srU%c>}7vsYYt z{1}e&r&jQhbShX`*47!JiDVYww~wrC?UF(=+phF1iybw!y)I5Zn)F+sdoA)hHN~CQ z`1Wl^>+E)kH@o{mYY1l|({l$#%lacQY?p{aC7ii z@5FF;a`Q8>I>`tsZOO(Z>xdrdY$qJnY^m*hah(;RbbvmmE9fa2|{> zb&&tS_MwnG_5SX5dEevlTGm#@ThpK5xm-4-_^`=&2O7Gei3-tP+*zUEk&6lGmVr(PF#!gKKNU4y5?J@hJRWJUoZt6;!1u07=C%$_N-XE{Q z+p(%|J*b1nAS~5m&zA#aha0ylyw8kWjO7A@5do zW|Nvx#km82SEVI4m+JzykAlKHCQX~j_rHAupP!vaTy@+yWmQ!vnN-@1#aUP9ww@vK zT*-)DC^>e>S0XJgIC%&ai38u-uxc~QW6eW?kWjaJIhD=$%9vDLNtG5%SZRb*rPGlG>LWssKK8}cEpIgZoD`TU-dS$vU+&n zU!l^6^xOP)9rl5oA*kF@T{_V3#q&^)eHU-@62R*7V2Xo+D`Xb({dwrRyL+|e#WIIGOokuZ$aEi zq@?FA+PI7A)+iiJqpy^L{61b~X~>y8jk7`r7A;x6I^KQ1uK8TKNbuD2J6n(TYONW3 z+AM|!Q}2C<^f38t&uj=utgh;?CD}T@7G;Kn3Fp1Xy79ab^q7;8I@ZbfD2VUdf%nJ_cpKj9 zn+Nd)unH#ixHW~4*bS8?b8lDHb1saSb=*O#@pj&rRSUUq-bP%Tc3UGI4pI`LRpmVN z@JrOG&c)DHRlteU`)4A9rkWa7t=-1L#UJwgiwk++(U(CVR*K8wvO*OSoDv}X@b)?2OP zj3@nk`sCb{EH|dCTqYLkb_%Vjsgh{C`@TCVVePW5s5atGdmcsEAFbx0SvG$ue|~i} z@2uX#rphMDH*Dp_vcr!b2icSjn?6{9?<14KwR46bQ|Nv5dbF%?6IgB(w}-=yB{i~W z!hmaF>$o$~iy!JzW^#?dZD>N!)GzK`D+~3)|Dfkwg-Flu!poM}GcHEG^9M*{Ad-(T4NHAbyOIjnv9mgXckF#ynhy3vIHH7?!&T(?XHM1G!1LsKg zhlC&&jZxo_oJEyJl9m=|o$MQTZsPh!K4f~>YBw28jkvt@$`;06IJ`>>2d}<@cgiJ1 z4(hB7%E~^X>VHj5QR*9#8+NKS1i~Q#Jxohu5#z;otJ{rCOI1OXQjHLRF zedPKC2+7{cUBnYJ*tLBZ8Le5VHJLeij11cYBDbCExCd2bkhXo0_-P#rmBsN>2QmNM zngc0c5^)~iT*vK`vT(cc(7LgjhZo22`wx6sQ#?$`(DZ}mmXnSv;Ln@4lh)FKLIH;- z{xYAF?>L!AH@aGSc@voG@Ub4l`Uh{4?Zz0=vB z0BFdhv0NYcfc3)q=q$?81q=9fD8T2=9)i`9S5db&z2=Y8z=IFJO;f}^$P?`)T*iza zaRlcWCBJ?2nWVq}Sd-%BFMo_Pdw4|JDc{SJO%}y1+nYKPW=@8E)JjC9K}p zFR5ybvMlBi|8NI^{qY`-%jjuS3D^t(Pru^T$6jRK=El~9&?@0_{N-2jiOK#B&v0r5 z!zPZS{_TZiSo>{M^Tz+Z%NTmE85m#bmV=cFH4b!if~5TPd|O)BEC1E41>O z>w6yI+$sQNrDKKC2V4V-U^*0?fL;A8ZV%evXsJ7eje7S{n0*P@hc7BGd?Do6)6g5< zKxx_O^C~4s)a=Fg=&h)a{T_SloZfqmK%{4fZ(Fy7Owq0Gw3)wP9A7wV7oUA`H=}}` z5BL&IH0BVEIh0j5wkn$argP|R7fmdZ&(A)88krV7s*$R-PEtUE#`{Q}I!C1Oi5n;L z^}nq<=y{~jEPd%^Zd{nfNu%=_mLDNK*eo8jR@B!uv32J@UR|@7P|(+5Rfau96S-hY z5RPD5sW&zOYf{ExFyy) zo$l2+TzFzm_k@C0Y};8u{E(_uQ{LdVo3ulg%eJk1i60_|hq8i$ei&xNYz1pOwoMSRL7cl^SO8U z#hVr6wn=fLw8alDDDAL1g-Zq>zjhK|`q#SFiWUHS7QM-LYRkChjN_O*JROUcaiC=f zKnif0qAXj!h6kTqNz`t?IBLio%+IeHj-^xU)J?C8JL!7pM4LQgN9Huav$F781JGk) z`>~2AqgKuXZ4Wr>QGnX|EbjXEuqT|_wRb*xI>L5w&=dCKDS8!zF5d!OR{8r8bs2sG95`$wMU z7g>eea@GW9jLg9=4{TS*8|qoKbREBcZ4)_>Ks#ZV@Zijg&t-I$-EqD-4a28Q<)-D^ z`Pcd;?3U{a+sCd=oB7G=byS2hxp;ChV+Lmswgrui4XoR+o2S=RP#E;%|A?@Q3O`@H zelB@d>K&)GmYxz|0HJ7?RrojYrtjC~ei zfA>QV1BW?_n&*?PdGV7vuF5cXeEuSS|Es5XukJuv5zDs8XsG0$PrS+B5m-{Rc%e0( z)<^_{(%SC=C1MVjUV9N&PCxW{N>|%l^s!5j|9p-=uB;=gjnWSUd=#rD-e0z!_m?%- zpcXXmmX(J62lbt}3i0DFU(CoHUrMGB>Vs%mjnJycxz~h{9MNgv{?N$z#Ymq%YI*B= zTp2`L=uHdIH5FYHQ(4%PFG6a!OV?Lg;8kuV_V693eY=uxgMqZ>L{!OW55o~rIB^;e ze(hpzK5ZzCO>wFdUbmamaa^|5xePyUEZ_Rg5Y>`DFVG6 zR)FcF^AMKk^&X={TBj>#51VmQCvo45NvvMJj`_>Cu(R?&JH%aur<%{K2_dj7Nzi9E zw?kGMW5$o-?30F(DZo{SH%ZGMX8Q2Ht?c6^6d663U4(#@3pY?Mq0m*JHM3?GlcoBk zob2IVFGaJbaMg+?;%z+98jTz^li~ol-QEHf6cy!QWqCXkNGVd<L!OK$HVas*s{;Ihy;A(qy;E0D&*vo zj$?Xph=kjmxVy^>Jx?%f!Z_}lG>%ov*6`N*Ti8(S>!;Bm? zocrdEBj4ZLcT@_iaPiZJ-}?T47Ht1s8@uJV-?)l!C^^Z3!{u8kdhjfq9<-u`#+rNy zf!lromObDaSvxV}KRgAuvTDGt=mh%zvv=n4T~t^9f8Uw;ZrS%h7D5PwUG`lN1r^0z z>u#;J_IYgW&qrIet=&P{4pjgEAOJ~3K~$`d*4C|$wZ*#CT6YmQR20RHRdxu3HEaPA z$i95HnYs7($2SRt1PCN7f%&{%^J*pYow;-Fx#xcFx#ymPk<|mwy&oc6huI?%WomvQL?qXuaddJx z5t|VC--=3Pf-3bJa2cnzBuq(+Shjq%WBl;{a3_|V_HMgwWgT4 z4zneejos=y1)3(%bsb%tdK)nQK$xHY>Fs8^bJdt`4;&w+)ZQ&EaT3o?ZE3!yqmp|e z$`*Cn&rk?d$tL8E_2@kYw}I8iA)`~Frh+3eS(Y*o3tuDLtv~MZ*LKLPBy?9~f>fvW z&T41WEnU~q^|qUlQ&mQ~9wD&DN)DY-H(YM_={4R?pndAtZxI-V4jkX@0^QKj5A)8` zDYE9+FN4$p1n+c=b<%XSq?|}_XD5xjLxE;0>`g1$NN5f0F8SzfjJwmIBP z9aP1_8;z#v5ew$T+&OUIz<~pY+;tGaJDr2sg@#DXh}1Xj#FwQE>=kXy@~Z1-U3;Lb z;8DFxnjXwI{(!35-C?UqhuArA;J|?chun1(WIHES+4(0|;fw9m#0~{gg1&nps;ab2 zgjP%IhO#S;?u4!cwWEO0e;zB$`mtg78u>)W+ zsmni!Q-a)SA{Be%iZ=Cyh)FlQ8v5=HsBjy|U2GB}D!JX9 z$VSJS{|wf*(~nO4fjX2W+pU8RxpUyafddDf0mmWFoj|GfRR@qlAgW6s8~_qJpOmFU zRqsVgbxIICJ5-Ndx(y@dFnysJpt+hnaP1>_>1dz6gg$sOT4LIfi9hJawrrFX?NQ^V zOezi>IB?*=!O4tczb&ilXu7+7TXPchP&LXjJEf47Z9(yp2*EuSwr0&Ba?9#AetgGV z3V|%$ggN`)N8SMef5Y)xoGn=g4jede;E=m^6B|tj-9WU%b)yt$;Tlx1u5*&Rx=OS_ z#i_#&Y@l>&+n8Mko-(zhz9g)Db%MFz!DfB#8$DGgb&8O*?8Nv&O1)}ilKq8swdcaDsg zmF?v&w18oo%Lw|2Naq|%T()^|>FSG)yf zox-AlK*Pb?L z0_96a>AJH;>%f5n2M$hZ9Q!a+SvIdY+KlyS0aS#lu;U~1hRZ5 zD&RkKpH-Km>b7+FAriZ)lJKXmfV2;+<&VFoY-Evp-ZQ}gf^Ed+7&DNz&}TIp6WL*Vjs8Ud^7GSrHXvE`{Xd_BSH!;(8$ zu^Zijj{bC|#UK{XMqYFqT2lIYJt(2YZ{WEGLvUfXtL z)f#HIZRFv13UFr*PMA#=+pDGCl%XZZ4J_XiMwq5A(=_~KEu)@uM-+CVDUj3*S zjfQ43f6fxVUAG5+7$#hOF%!Eun*t6x5xG;EK{&k!nhIiz_K-4xRauTvU5=KL(P;!6 zUOXG>Dp2@NEzOa3q-w}P|1Pn2Yy>^6<8=ywRlXf<`PXQZu0G_w_KpqMVZHqWMYAxI zx;oak6GAiK^JiY-&Q%AQ)isT7F+PU&=)!p;@)RiJ;9O9#OW#9_&drrKk z$#B^`|C?udqTCI^Ft`2S27Wd+uH~#DBr~3VliwEj0Y$$HE@Ap5X-7ry1W;L8&h#BV zezcz*5HiT-9d)E-rx2sH?LkXtf60ThccW<@`lh<64LdsKpp%h1G=Y_xi%`{|;@dT$ zi<(Mg#okUs?vxdV#h)Xk>(ukROBb@Hl7rGF+rlnd8zHsFfDlO~2+#T_u5nYr8+}l% zvA3>6+3xlSAcz{w#05#2Jel+l=~9^FoaF<)}6diw4E7S$MX24JrUB8 zTO;E0`ZE9j@tig*Bp#gBw`4*F*nAiB~BST}N*Sqz#wdej`(* zAiB|OC%`OP&1Da6;)Q3gW2EQsve9%{wW^pTPc*-}?@G>y)3EJG`D+IbIuE%6ti&wB zu5_?Odt=&>r8^*hQ0L%3MQf28mto^N9W)s+vmb}1Z;+=T*|+twp7+Sdm{5$Jvv${XWYj8kA)l8_Kj&u;r-Foj5C=ovlu zzj|wo)Aklh-Gol$eP5bQ!R7dkMvR zYcNe4p&LZSCy?JG6>sBEIbgLQtT5{~?V`FafD$_1*m!#POeadTyx%aOY}X#PlvEQA zTL|4nbYc>Ha+5GxshVPFlA@yB?5(Q9v?QAD!WS1uuO4a5pD)0WVb*Ofp{mx8Wh->U zO-xb}eRC5L`+4X>6?<8-X)jt-8l%rhLCA>5bKS~9Dl9i62It{YVG1|yq^3TI5(Y6z z$qdL#LLIOLs%eUi8@5wk?MDfN=;T!T=Oz%YD`&-;QhZ&r7@Qp!Nw9DXQMYp&D~oH1 z&+5gX91nYnOW3-*8q-#|ywPOmWs@GYuey%VVB^X{_J(u@4(dr%sD_1Wi&?n7o;;tT zaM^lH!y-Gc7nw=!mS#kQip?9@SP`bw(h(}ml4a}gYLevq0puq}`b5WO!={~7RQs_d z=q?Y5>1p)NipI8@KX22jNP7kk&c~`Qqj2k9!meb-4N7Z8@fJQ^GbH=7J9}F1r<> zwNz#4uSF}_7}2?QOcWYmcOQ;&`<$4&Q$>AM^Zy@`f&t_5VBK0ku(zfb30ReTxbN-d z44aTdV9g5tGIuYrn&7v0=8_s?Anh=3{QD!GDX1Yc#!HINz%orrYC{xy(|P;u^T>~D zz7X}7Y~#*HKd0JolNjScS2mUPVK(@>@W#*2rPqFeTS8fUHvKbxHFqz)W4)yM46JaN zlG-qp>AAf8)A4li?6axL^z+QL_n5iUA|=K{tXs!2P4?D>S*s`W=6x5^x1l}3ic;=* z>=Sm{I;pW9qI5whWU{wDOjSxAul>(>QX4o*sB$mAe`g6vJxB7!pp=Le7qDj0N~V{^ z@_gS^{{DaOu{h`^$*oZr3=<057=y<0{Pn$XH4-Gdo);gV!6#*kXrG4!k49xx9ciP_ z$%=k6a}%;*dc!sS5-lNm3(&%4|FOc*hYUtHQ7 zL(@=5_O4mXFW%V1L(k1&+TD|hYEVJVuHY}vd_!Gy0(bxS1q@Agp@6-GOStFtjXeC^ zmrVPgNq7ZlLh{9HAM$)b4d)FR%&)HOhoNhLWY>y?-22WJ9(VzszI_6^08Nv8`r5~Q zwkym97o5lU$E2cbk)p`e+Y7nxh2=c;pKp2k_OtMK!o2zHEb26!yMA~Hqq3u$9Pvd< z7x3su+jw+V22Wm|hk1y@2f8Nk*nXaU`a_b=p3J|7q@n;>SIXUgo3~Pu?;BpHN`=dFh`Y^GHcNNQ91X$vzga_nBVH@U{`7uPyIZVfBxeGKHs5n`!BCxsK-XrTc6{U4LN6> z$0HLho_XX|7OO;l{gaD`@dz{>bXm#cfBT#&mzP^^x`6Y$#}Ek|u=(n}S-iHYfM3pt z=jrQmQC7sX8oGw2EZ%!#0g0oA@yDrsBC(>*R_|$#aM%CeT5JIkBeG-dQ z5t{xC_LBFom3C@s&0#FBwQ3=%vig{9$?o3{%Symp_BAb%=}8-F!AvZxeba;hlq;5S zMsJ7Mol0D8zZ_$l1|HpJ@t2Jia3ndYNeDpG1X2@(3JbaH#}{&5ej>i8D7-F>+Oj>& zEDlrZ%i)e|2H^9!aJh8cE*Dut$MXFl@mOUgyi^!O*CN|!D`$SgD!GsMzjliENMlP6{1^SE%k4BT!PY58aH+ndL6+r%DNC`3&Kb9O7jDvLAn<8ix< z$h|Iuz9YwS`{g6KHb0h7TXa4cWOMGB+%zr&m)nKg?ZTIq#jh?*0Zn6dc`cd%ojT?$ zDg{prkKQ_nM7JJ!r!S6MZ<#>Z#yv=m=h?GJ=U124@#I|-$c}a4aT|Et2II#Jq&%ds z3bwGvKDZLO44nr4jBe2M-v@o8rr|R5rf)Qi$bah^ton2z3xbO7{f99%KLMA^XwWA& z=U+UIxUi&R;e6JV?KgpI0?aTUkjHOt7))xcmy~#~v%}!v_>ns^vnNVcw!Z|G7K^oZ z379A6*>P*dJmmY&V(ZC~0VYquOL5Seu__$Ip7Rct)Qsv3%Rlu@*!j4eC%vUY6|4?q46Z)}aYI|4z$*x|kR z{X&5fN%ZdG+jq)VK+Wzg2w>cqy-7P@LRS*{_3T0oibV^yVG0mdkXft!=%zv60bQEK zlqnl7x%nyzr`^c+yX)9M&EBop3eKI7PpW86NR@5cPUqik8znDSu)Sn`i4bibJ z`)A>7)XG^%Vvmp1g$`vUsBjqJ-4}g^J6Aszb=~@b;c~3SDY2Z@S-Es6 z%a%0%XI&Gx1xVZG{A;H$F-J$r#*wWcA)P2UO^ifnP`9%ZfZiFNR!K=}VhmNbB57AS zeg!chlVTeJb{wfmE$^51zM?BahrsqS6tMZ*`TTK7%Y?ahC`6iD5ZJK?KiPPNi<@p4 z&4NFD#b@t-!gq7LWOYep&>4A57@R?rfX1C?Q;X;J9}VHomsazC&wW5zypQbc42BKv z$(X)LXf|yoTGHhr%iWSx2tBg3-6%)@o@y#J=ocN|qRNA9L+}1wVZ+Yjs}E8U9T(Tq z%OOO=saSPf*pjj(6+xS@9ZmO)R+~v~c06)R6}#*G2ojo_rG!rZWc`>QKyl!ZyVlo7 z2`N~)qj8l?Z`bNf09Hi_V#T*;XJ2u0(7Kumf^R>IdhcN@E$y^eIVma8-OY)Utgl7R zcpgheAGf^~i9qbx0pSqrNAApJbFo$X)mxLb1bUA`=?=L&RS-5UR@d#f*QRM?#YJ&u zziciX-IqamiA{tgqU;LcaW{_^77%Dy#6`KaR&AQw(5X|P%rH`cloF{FQs}sK;4q1F zLk-V&yWQYvsmr>@L!Y=fq`QG!Dln3|^W?8DVO2p9pD*6ZJ4-h3>9URdTJvz-nSJ@* zq`_pzMZD_iy$AEuPt#agu!UJmw)3yKtNC#LYNRiY8z&9q$_aglZKK*BZVN~V2!%`x zz~yqaY991x-w~J%(d^;s5X{Dw`|j3SsU3DQBwkQTVc7~B9iNL<38l~LLLmqqSVlz% ze8MSU=HLX8I|0=78Ar@_?;@@C)vmEk3%>aP*Tl+e{KQw7rE zys1-o>zppFELa*!Q>jC9iAIM`l+OhO@P}o@6LrAvLZ*dJfbe+G1ZbLuCIp5u37Usy zL<)$Cb_0r$Q^xZBeu;-na@%B56+|Z_F?M_s<0lU1{(3))S8Qb7;!PARTg#fQmHgw* zNqDq~XD&4}gLAVpxZt93)Rt8;f9ZPWF5AvGUoT;CwP4!!dbg#7^I;dXN`c#DU;~zA zwR*n{gxb6?Rzf2oMxd%Aac4E<)hOaxj(6K;XSIc^p?}hnPtFtXLI@09pkos-Tc4Tz zfiMDv*DwyNo(>!wKW3yLoHG!i`%sE@T~fGU#av|R&J!j$74j2&>ls47yBTxKMl6hW ze^+O@Pzo(E6FgD-CYQn$*wddPEcN5xCMl77_vKJDmwtn_V@+g7rM=&pIRZ1Qk7FrL zC4Ohym?kZ*BUy7Q2{Bp80Ib;&L~F&8mR8h~qJzjxC0c;yagn7%l&hLuB`xna{Q-(g zt0}Fw5E49DDF_6sx7Rn-L~VA-$d>2+?G}Qj0DsE@%@f7w(Sx}Ar=iuZbcUD{?wdp<3rQN;&cL~B)m@5~a7;oLi)dU{B2lL;5#H^`7NpWf( zoH)oOq(UTjMMOo)e#x?3g#Ps?mft!dhe(uE$cjBpEA{ZguTaw0?&oSsQkORh<@Gw2 z!a)mr7S|;g1$;NRkevr6>eZoO{cd8F&g8SYVMtJhmkS0)5q81K6-AK~u1%9WeEZ34 zE`0DE-rXtDfVkA|=mM5~TR>5;rK@IFVIi-7v4&065zk?$zLL*ATgdxsDk9sy`+3dK zG?J5|kVpbHloxO3-S_A7%^n*=YksfG>m}7C5Yj$m8k8fRY_hsBKv(SDTSl#F?rpLv zcJsor@F7o{HWR-M$wP(^r71uVE-&TYHk`^8!64;%sz2c>VoC-*S5&sWL58BMv)*Gj3O&5-XV7mP9ky#sHk{?{l}x( z{&qhrECuqjHy{)|e(ev32)*_+fqO2-UN{R|oSrKvAtid(T!b$M6$%iZ_P@wA^G{en zue24atfYZxmY}bikKF;ulMTy{zt2b~8K{F+5tE$8Rk>bL!^KQ{b2arrlaOf<3WZt! z%^Y4Ss3WFZH?GZfW2=S}t`|*UkXPfodEfEo+Dd|^TSbTglvV-1l5J>c<|}> zx&F7Wa{V8t^T4Z1*e-R1QiMe`-@kDpo{paJgdoZgM7jJFtg9ms3==#WMy#Okq|uxg zBiLTJl9}r(2{*|n!29ofOV};wci~y|ajGCXXv_R{d|3em22DbJ^jNzmOa<7>XJ8gC z!!>y9v2VM-j?jyLL4EcHmTjEgTB8&qBNvn;_~K)z?`H2KQzwe;HpE_HJ&>jo5MRV&t#U*g$qSZz+_WY7he1EIrv25^EJ-8`eKxCHoyCJGe3J|2T48y zW!qHLhbWNoym03fvW-a1Zp|0N?|w3YdmsOTf4%TN@A%yKTmorJs_MfO+R^;|C+8Bc z3q(d9f4?oje?PmFKmK(F@lh_ky1+CoD(l0PMRnzc?+?Lcb>NVeFzMp)e6{ieidHS) zt_{nGbwO2iJp-mp=FZIB{Gw>@5tt#thzrle4(8Arl zH-EW#B$+XEc&Zhp8Js&bnJ2df@XUChg$9hcU?M*rk;#!#Jc~GfcE@?#|L7;Y{NhYL ziuDq$LtP-mo?4Tk=bXiTlhUzmXY4sR*~p!Oa87^BE<@0Z3z6*%164*g_7^iT&KQNz zj$shLQUu?53HyWRF!kuuUTHL!553nw!f(BRHRCC4;{-1ywFnziRppo;zk-w<5ne0_ z4>%V)In%Kcoqa7Lp~uK!{G@;6NNH?pT-yy~VmeP6Md!E?bhLsIJ}l*Pm}-Ls2QU{vZksZ%iC{2iATC5IU2un8MJpWo#?0 zCT!XWO~(@xN008Q#OO3TXyH!I=C2Q2$L8&OsjUlQ+Y)pgZ%i!RbJB=2BKIo=>G=bA z`w!jOw6g-gKZKMDVYrBnPoQV_L|kfL?l(_zIuBnn3gJs>O6IK;oH1@F8N)OZbA|B3PH|*fxP!< zABwi`rMlLSWlInmF0YTolq9;R`;gWF#7==HBbSG+O~IFt++rq@nvd(Q8-<071Y_U# z4jMn4?65|xD?+GIkTw1sK1|=no|+IscafHza!B8WRDy|9M>9gX@fgkD>`qMQiHC1u zQ_*g!Y6D0Ky4y=~*ED*hdN8f#WVzRfA^fbrBE_RQTeS{OAh`q3bm|69B6Y_qg6+&f zW(yatWnUq87o%t99J3}{tG>g2^HEGK+Q|?JVE72ndJFsW*Rl2Z6E72`KzC!VSq8hd zp^Wy|9d;RR>PJj%h)#F9yt9+LjQo6#rpmLCkiw{8DNTf+HJBEpbV(znOIiz0l{&~G zSLq&l_3GA|bE~EOu^=ubg@Gw4Ep4D``D_dic|EdPx0$NBT=b|IdiRQH)yuYjTRaKL zj2xQWypalW`}880*2UwEPiNSW^aeuH_)baK*65Yry*Z&$P+wY!M3EA8WXX(`1f{v? z-o5o6whvN@%afML$b%#}kiyH5VSQRI68h!Se+#2FDt(%S#5Zy1Bcul7x)LAi8b>fz}-%b6qu#O$ah~P>Lz1*lWCPV7;lW41m!!YuK$oM~C9oB1-~SnwQ-r-^hN^@C12edPc_G`M zoP~YWATkrYSV2Ea=PhTYC5Rp{ie4GUA!D*#2K3E+6b2uqhyL4o)|KB!IPCO^1Bcvo z>>xLE>L&e=)PMgDvjdV2$*{uu;zRTw-qqpO$ol#dux!VHa}vW0SukfNs172WE*<}< z3nTd~9CUgVF1Y*Xb<6SabSQF$j^NF@5HEbQi|1dTLrvI5*9{V*-3%EtfV-~gjq5N| zf@$lste*e?AOJ~3K~zEEmV4UgU~q_?1BcvoJcNW`|1p@pc(j_zc4wNf0rSPXab0jF zTGwtJ)=pVnf&AbZY}@B(%E^un%1o*W28V2g9fEf93{wkwjXjeGhntv| zjZzAPK+_F89;3~vP@Fz>;NUQG*$H{80y8a(K>x7_yJJSF6xoDceg@Tn>BsG&jSvVq zT5>uK2rES0v^a6ucokUP`cgmO ze4>_rfz<(l@K(^q|K#t8dF(|*R7^YDr?7YKK;S|-6$(y|!5|dX=iP>#+|?0=gOgK7 zjfhj(GLP>;?#B1prcbD8ohVr0)?u_dnokf3jGKj<#Yk z=GUL&zTmRI?#Sd=B2ZSss_oV20&FdYp+hnc z8%TdjeScjUYd4m+NzGz1v*?!|jRK}U^e_Ha8p9WlUPYQNj*FTK4PJTTWggz-;)BPo zA=iEM33EeL<*Y0!YkLjZs3b=9O-0JiXHa&2^%+xM+{^`2FXWN$b=f=!71>7uTR+h>Yx`CwGAW_SW^zB!8MfvjWtPy%9Sx z)#>x&0RQeC{PNB9Zxw~nf`tO zx#I^jG}-MCJcryJ0X!b6zW)FzkKcja0h*%#RTHNEnJ0;P;IXEfqeqNz7`<{ArgA%4 za$2C4nu~5eHi@tuI5-Hb{J}hTbx$no;I>b9xv|u#cg9Dc*i{=QcnGRm9_plpR28HR z9mAi7fIxIwubscVf*0p+oNq%4fm?d`5N3t(d)KAleZ zoGG{`o_Cb(vN3`IOwA#8r#f^c@n7_FtghW1VRRN^pn9$14BRj^EExv4rPO-A_;b(?J;pmcM%=s>5xRpxg) z8o2bx_nX$fxWq<15xT&u?fW}`W`MrmJ&nIP@Et-#{?-=V*0e}__a~N?O^Xt4LI;-? zd5>xD)7^u;!{vgK+Dc+w0^MccazzfEHN_9MGDZ$3R|lsPxdUWuJk{6zjMT^PKdhYw^Ik`vPrm8BWQhXN%+RGxpWlm4W$beXi-zJ4RG&Msu$wkoz; z8pASDxn%NCE*g}M>wthRB_%tHHu0ZXYnivLlI@nppf1Tw9ygGyMrRXcXe~TS)qD3c z{iCIPTDXV85Dd?ZJpaOh>9}>#G`Vm7=v^*-Yd81*zni(J%N`zjVF5F1e9V93a$=5` zItd6?l{53R70fEw&C=>HU7}+cH!zP|E*MB=Ld!Cb(ze;TZUe7|nb?bDuih#{)&$h;*~JUf7xT%c3IZ-K=MC@6?H3Is(R*Y`zb(LRfLk2?cbZww z|NLSGpK9@Z^oJ|y7Ns|RlF(r5+}T|G+BVLaG=ZnD$VUoz{-GCmqB5QZf4lGX8Oe40=gqmixwZmtbPN|y9?cIYWaAc!y_+}kw|7?X#jbj~CMR>-72}xPFS*6G zxU|9)ELp|NUu|Z7Nj<)(D9#*^$4yhtAUCP`_Whm(Uoq()g&;nJ~8%8y3_lojUw-@nQNzfPu4=e57Q!f*UxCY&>sr?1Wisi@qwomW0u z&DWdD*bs!iDG5v*(U%)0<`L&H97A=YofeZXy=CIs;_8X}|Krs0{c*XDZ+3nwt0z%g zwxaLYglas*(4GqcArpK1cHHC729M{+$l=Cy*fXbNH`c#(u2b^Tm89m{pW~k})v*T0 z5rI9scxzQTiH4wDPvEAB-Eg;M&qpYx|LZ;e@#PlQ)w{WHcn>D#CQ!bun8gKK_-2cr z^M+?Pn*=|3<3pa9wUKo}FH=Y4F{x)VqH+&kuP9>nvJys)=}G*4KUn<5S3LN}a#rpR zan|U*T+%<4(5_v4zGM?$6_zt@bS_biCZ=RN^R@-a@0!M|@6KgKWte*1$E}n5a;UJf zK;FDb!J&E%><$!&@$Y`tT2WlPrv+Oa$7*tVSxcWm3XZJQn2X2-T|+qR9HbI$jU zdw<+L_P;f%YSv`cT+f;l30T=nrTf)4y%fnE=`U2EZwD2s?e|w~HTEVnYHsYBT=)T! zME(9woX_Sx9C?f$vs6Yp>&c|RTyOJj?aetUqp$Shl=_Z-66y)aSNe|SVq_}B!?vi%g9Z}9yi9->jU)q?!sIyh(`Q2&R!n24(UC-NHAWwo4s9G zPs*}f&}@t^ccTpNkENYG4cSD%&7VyNzH6->BB(smR8>?OH<>x?l=wNy+{iXra5^;3 z&$WrbPoCpMBgl5;hc=LqddcUzy$`SlDlq=8^PuL87$aY4%%mZVZSNRsIncoxK8v52 zR%9C}OsvqZ@mnq63N}1n6JrD}V&qW-5hQioO`}6^yb`M!valIV(XhgT44Q zCwOgW1jV$d5}Ujdl}86SSm{MT0(Umh&CuWb1HFi1S;W!7N!02pE57|FEH9$ONTYB7 z;~TZ}_Fda^xelhK6jymNYr9}-BO%W^N2-?qkGRW)%VYTIE+ zoHEAKifIGorwg$fj&v?EVlI$zz#q;~ueWQ(C$X24k8JQHDo{@v@w8Zim$aXKU0~R5 zZiDsS$pMJ(e}UIhTk_%}dAwXtL}J5qC=ganS;y0d3a2V}{M0;LFL#+`5jM4k>!%u# z=Kf;&v|d=&<`Wi!R;6yy{fE@SEbyVe`wA9YMtC=u^0iE0K-R*5o|iV z%j=ZL;`q2a;hcSd`!qk9(;n}bl{CS!q0k;>p()?ZWDH~^1Aw%c5gKH8jsoQ44W&%p z6XY^7(?dlTzJx`;CwY-4aSyHm||YK&y~Yto)1`_3X~d$)of zJlbx_OcL~gHJTIm0TU@s;QXESe#6s5)j7z@;r8T?-$H+n5Yd}e>??O zp45@D9Y*H@j$x(ixM!g%L;eQI6R<^#@+rhJ*nfZ>NTxqRIdr|IIS6ZtO`(G zTUM`*soWc+-#f={hvTrjI8|DI8Kun1mg`;!7)1DM7KyhOQlQJAWAixYp{heW`Da)l z#pC5^XrRBqjO2?JwhJ&CauC9}CXI5_%4 zL6ARPM)9#GBK36-t&{`321c#@;X3*xeqMKsrso=Fs_>}w$!-7P3jMR&4SnqB_P$J= zG#YQJ1iW`e(z#K;d%g}M!yLgn9L2%mF_9?sWm$7myE8spavT_jwTeo=d`v2tiHxx` zz3BwY3JbCIz}|Y2()lmN?w-?LGp&vYzAWT8PNC*2ApQh1zaf`8?FUeY$fF5A+g0at zcL}lmuGaa=Me&u9!pP&y{AOi<$IQ_Ie;M$>va{jhPl!=fb;RDaay$TfEFwWq6cGVw zY6R_ZpFi8;&F(GcCnE#Y+@E%hAGF-zScChz%Wusp#?qv6hvKcEkJW5HW-3MBGc9F# z%&4e2`7@$g)txE$TFFqeJF`32Vpi+u#TZN9z0Z}~z4Uw5>Vr0dYCtnL==^Y-l$+DAOeOGm0c1j?Hn#`+9XtQe>Xgb$6(A$3-ab{ARGPL{qai7 zy3a2hWX;f=hxEaoZHr$yXFwThCugfQhxnd5+gBO0%T{^ zhhrL@6F;nYQL)aekJj+&yr`-q9>ej%yxz-?oJ61&F%2(f8gzy>%6&{K`+fOv3Xvx+ zFNc3rG}{z`c2hD4EdQ-&ZAsK4e*2B}`3HGWd$J={6c;Mw7i1&>;_N$;nvcxP!nMvffb3$X4f2+I|i}+~NV>T_fADN? zVBKxC$RI}Yij3sO|HB_~@Q4?=le*Nkw=s2i^FDNUlASra0#b*uUPtx9J$KFGRzzKf zTrjX?w_)`~T%f)hxC~0S0-g{k1<6vZ?YK?$QKv1HOvEF7cX>%!iCNGA4!hCE-e0L3(eINgMvOT>cT|5TzLzBO8jkx*#Q68)3n^s5~GQ! z-dcmpZYtynYZSp>X-voZdgJP?8sF<{k#Kt|FLb9^E+Na}?;pe+tP}Wd<0y9SFv=$&=P}s;DlF+3 zs#Q$rTx~T;efd=%c^@c<+vUJ$aH|mZBqVKjsofY@{#6(J4&t4>*n#HrE=9+RVy!=! zCxx6iU!$uBq~lo!Ox1uj=gk@mD!}Yk2E0Rl`7uL|2=6}1H$yZzC+T#bKOT*K2}omS zd*Ui67GS+@SftKX(-`US`GN*uDoo){pdj6yB<=Ss#N_R!0=`1j9w6#BlSgeXJK~;< zX>~^q#Yf*s`AuO_!*PT-UA^a&Ki>y_Y&L&)b04Y(xR~2XW^tuF3lvP6F65VX6Zn$fzy6&hXB763`Jce&Z(mucGpkmwtzu#kd;Gr5*f@Z~fy9>Rdo%TpwyK z{tA0B^p_hEepWe13YKRj?I_PHRBU9wGFxYYMZ@{}#s9&QywdpK_F-J3KG+v`3h7C@ zP$P0DMddp`k<(mrlc3|ndE%nu7b$*GeWVU~Z_`EUI&|Us#R~V!#nzuJ4)K>f`~IaO z5hS#rIa-;`my==XscQD*oJ)`TCr>eUbgA7PjRHnGZJK?{iZ1|t&?ROd$4KC5le?wC zMTUBg5cwyQ#h~RT4fWxt3fuLPhWnoLi69I)RJSid$MF#l^T{#7LM-AOM=O_|scfem z3jx6~gEzK|%n09VY+?lkk@jTHN-I!&B{^+}KB9tDM70^x&&4`NF#JUnxx?-XGu!Dw@McRg?K|Hj`xKiIr&b@-!$LKhaO+ z#U)GWJFgYFd-g&RtfZtd& zU=CSfEpQn-9L%t^93!<6E-OaalA6a(fl%!%fgx6T*!fE=iKYA)4% zW@Jk(*iybCk8DneD}{zo0#A4!PlT)jSwitwFc&=uEu=bqtZ%a}%jh64wMmphOh|}D zqwzxDz>cO>Z~M+IR7P@lonp`R`m|GV*_U)#8I7xGrzA=vGNj{?%&6x(5?vabBRV`^ zUBT~kj(HH4xiDTd_$!ChS*KIe)M(7tHk_-Q_r>uhgEBURdWnvU4kEJZ&{pxA)gSw; z;o+0Z_=_p{Q;~)I29jV+BGND`XQnCRuMbtTjILxEQ4DEK`=Murw=(aDQ?=9=cqbfh zwTiCMRLiuz%=+!eR~a+Y&b%WN7eJ)Sl7b(}>5!Mx{60?0=K%L;^UD zJ0_iW>5~7JblpSDOLfi@IFtQ~GYqxjq+C|lbdD*{h=CIhFritj%&joqDxmw$m5}Qm zwsL6tLI=kJJ4)SN>gX8vR&DK%5i;4!*7#9F0`;uAQ?mHk4sA2??(W`Fv@Eyw6mQXz zn3fwqA(8Jjb-AUAJ^6^Yxa}SkA@>st>VGsQLZnmDKm#$VPVG%q2}ZLb$RnCAFDCkN zct7pGt$$6?J@1t}P&|b&&|uBgLV;3$V-i9a3E`*0Zm?5TE+AulM;>=qusIZhJuJ|i!;xezH{`TVGS7Rpb%wFd0-{1} z(^uOM+rTtm&XV2SI`H;PvD&uuPFiyWEzkiDTc0sCUG4$NDz6%m6if&O;*k8purtzj z#Yo}t98vX|F`_S-U1T4BEMjPNLU`6@H_)%z1v&tt;|9_GO)qB5v1yo4F;3#88Hfnv zk_4sipEBu74|cY_vF4YDJ6#uCg4Xl_fGrm|wHrIlY@2ICy_jU}cmD{ods1OA3hL$V zqw-VNoVSN$`7m z1D;C~gqV(Q^(QbgVo@eGcl8fxDSJ6jfGgKuS*lRyZ+cCzzLQOvzuchS4{x!p-L0@z zoKV(3l3m2YSZ$#ogK5fuNv%mJq|f`VFK~|&hc(xy8dk!|juP~+UaK%QWbB}(R1LW8np?-K2{5nEWOjfD3e_%CdNpbJ9WdsXZ zr**c)jOX;-9=22b+Fv4mzaWsb847+<=NIv~@;W^`yW##sSC^mdM9E%?NR1C5r*g>P z%FJ383a?7$`ED>~&Hhsl0F`5zejAQ|(8($b@kO>SCnu;t2QV> zl#50xZ+|SjJ=8GwRg^&5D^XRDZNtkc7O4{$gr}ALC_lHY!-0@l^33{Dg3b0(>lUF} zy4gMAvu#07%j`|46S1Fs=IOsk&J|igRWjlm^{-W8vsH78c_`Y9jB&bJ3At;%*bi!G zbJI0}ihqOKA#{s~Zao{(N|t z|2$@$&l~uOc6?5SK2~t}iYMoG^dT+X7H=f^jeFm-(qg~Rf$S*V_UfBTTvxenzp<&l zN}v!7FK5FwwYqTm`xnyDYAb23=hEi=4-riYZGYz#TlBU|>#wlFnu^1`xhJ~qO2VGY zC?6Yp#qvWdODR%_&qv!Ut2jP^^`#xdY1gaRigCj}KaAv=>!0NhZ;`>OqGQ00DGV1* zn=2PPGdkMhMJgh5>2|58Emw~rMMx{bTnUO-G*A7b)4@d0kwXkTZiZIwOE!FbYynl>KuOA z(&G%wxkKda2D}iKS+|XS$J4nKYh|t7VIL2AvYDPwQWC4Ot=2D4v$dg1&IFED!Kx^- z0FC!If>rnG_+}Bp_<)WlQKRWBZ-C)L2nb|^snDi~J)zpFm1siF&yAwRPNfxHTs;8J zrA-JXya=Jk7#E{j{X@r3>Gky`3SAS!nxm)ac<;3bRUVlkfR1U2KmU0hEnmkS-TF~> zL{efDN@MqhJ#!(Gdq_gBv}Dn4*QM~l?~!mBa8pcbt(qDN3X60!-K2?qm5U7zVXbDY__yDwYSc(KAHA0vWGQ2(yNW-0{NRFL4@ z{-NF52{iPtG-7%VwR~DNpr}*7sb5?uIBLSAVs&V`B}=l9ZN-q^m7b(7p74TL z&;vg{v0#N&*)6W|e8@hbaBY-=DCnHNrnbhu_;7C>Zkw~p!VIcG`PaFzIvW2(GJN%N z5Mrl)01RpyKPBQAB=EJ`{Yl6261fai5B6g@9B5a#-@@FSW%g{CXI4ImtiHM=HJJKB zPYlpW{6_G-x1c`VxZ+2JoE%AaI)czo*#uu;I?VZ(xjo1+3*a%tUUTG<#Dv*av6KFX z1pxNj#tR!Uy?KPzBSYA|RC@(G-Iwh3Pej*RM?jSPk%JDmUvDXKTJJJrc@fA53hc*= z-@j28+>!@BLe<54M;$W zyD^3zHp2BaE?i-@&uSxd8|hY3dD;8j4w@JLd(~875Q7bH;tEX&D2&jPyDw_wfL{Uu zq{MWZL8m7a|C{cU7K~xOOsaPi#B`FDT>s(?Ah3)PweNJv-}~J&Ia8~jbl~6l-ZT3% z4I5!+NNKeqMv4(@(epk$U^wA!(;A3RHo#1!Y5#BC_lADHN;EW~;P9FnxS$3YNR zHX-1uMuu@44G>~qpU4YZ<26X%Tk+ezqPm2pS1J{MKiv4{M*t6%$o%b==&ITfaop=E zbKDWNu#+>YK+S*aBlY7@mKcr3HM_!LCh z`$5!KJ8l@0%fHzvo&NQOC^bmyUvVPrRwBSj@+69>zeAPMb^W2`mat5|O~^CFj01okud0_hids!i0O|#@d_Zs{l*K?3LhLz6`H}FCGHYZqB7!Oi?1$OX1JA!%>7cP5lnjQ ziB(KRJ+yzc0`immYklQ}vIIAacuJdho$QRE#XW}&hUKP97})XUWuL`bdLB(<7@KvF z@ch4xK-xXw<<7-!6|h5XeTc5^n+@0g956(r+4IqRefO-d-7FZ_;oH4+N}*H&-{D62 zSFl}YIzdoB;Ks9zM6sA+Ge~LdUMw*lRcQ5@(Psh|pYMF+OfkcwXfQrP;&`kPji+ki zPx<~49k3sQiw`a$`spH>-3kXIT~^OYJjrqvAYx=f>G%&oY(6$q!aDiVP%5jY z>3`h&)MuWq_&Z`Xm-VjxP~BmRd1TYY?{i387N4)`c#|ELi1NcfuZw;N6cxeC0y!a#Q+J3 zpDRr_vOt0(_&?5k7tV%)Q#4Uw6mAzZ?jl&u;_{y>qO0MlhH21LQe>!j4(&0F4@XAh zyNszJj>44Dp5V~6AQH^{M-KluC!^Yd&8&>ej$x}gpz<{~Tb_5S#Vu=I*2DbOwGV?3 zOA=mXrG!Z8$c3`_4^JP)HJ5?oGYDn(F>fdSd8mJd)4!n*2A!7KVf29_I8 zy^P1{OM8wRH!}=4fK-GU)8dPt^5^*6pW&QQ>~c8GY)2dg*{oZMHRInVegkE1Q_akPBD@g%fV--sCt zt48L3oB4o4jwa4s;f&kL`StRE8T&riwyfC^I&pBf{Mn9m#_2M1?x<@Lq9!+;MF2!> zff+s$n3ff$pEP2%kZ^zLH3DBE<;|;QTaD&$@_+VoX(PUaP!P}LcEJ(5REcD!u*r{y z9k?;`p}Nr;&O?lzK94L(ij-EAD>kc87-1+?=Xz`irdCDPRKeE5@;+R?7Ly^pLGO`R z_xq?G(FpZltNHe_TWS0^L?h@Q=5d#2(FxfR8L?q;N&XKn3d@=t(VyDnvmdgSbq;bD zmh@+i->-7RjifbZNDnFtbrz6zWHH3dSe)aH&M9L_BwfZ*jKWa@Vq$z@5dz6^p|qsXTB_vAPXaS! zWc>Ef#Krg|#DwYgGh*({9-PdX?`@z-#C{1Tf$yG?X&9F0(_9;|>+3yz@$0uJ3`ymo zxxsX0>10K<+9})}vRc@@AYv8v|C%0eC})q2EPhgfKS@Jb1lIRoxVm`&`fD7&xG;y9 zgrJx>x1f;M`e*At@$i_?UenQqSX%JuZusGL=*j}z#3WE%I#yX5Sy}m*0GoCgyF;9q zV*LN_0329@hV#wtWx# z|C#0g9U9pDpJ{)u`2U&Z@+rR#oNB{nt60^q;j?NafY>jQWjM~%_dhB=|LjTw(=f!G zE&21koW;pi?0B$+@wL^>a1-E&1v^wpJbSsGxJ;=$8E&Ce-Vy_>S@xri2jX5C|O zhuKRjCsmvESU%e$nY{>?Ib(!$j2X<*AKmV~Mng%6_O-ZUom!~Z-0#`EkN(m56K@k| zTe%+mE5A+ON^-Z{#bgigqLAjHW<4<$x_!oCar`pM@2R;xxJk!uPl8-mFw`ck1O8}s z9a>q>C|vEf0iGv+H@((2>$WWTKDHc?SF`t_Vu$lc`{ezcf*inK_{xpGT!(_0q_>gY zRe5w>*p@=Vt=*7qBD*C#Yz(%jI`fUh%A{Qa#KU5KHlY(` z{kZ~9#A>O=b&;PovppPD7LS?!u%K3NK-?U#6h0R+IXC^wIa5=}P~YP7!wP8==S)WV zSgdpHl?ua+(mD}~NnaIU*qx9*8GJBSGWQpx*!vZX>WvZB^FgXU;_UfSmb{V2%GHfu z#;`9Rz%9wf2IO#U2O2rB)_;+iZ8us(@O3(aTn8=IbF^%K&A3O_(d!qjJzzF&nb)bq zXZiD)8C~=%3oeEw2Bo>)gNCd!Bw!Qwi+1m1rJ1@Vb;q;in5lJARn$mBf)R>fXytVb zZ1CLz^HF&>h~QhY{>9lQ`jeULpV(0rF8Q7TwD<0M2ufC0Gou!iW~9@#`lVYrEO9at ztq&NYI92qmFEpew@NpG41Iz&Hav;YuPW0sTZ2E`p{7rH-h$S8ulSOt;4jw=x2QAe2 zs!!}_t&rgU`pU3_fU{hefRFow8*X}AHsk4p4gHQ^@rL@Ja~|tome2(m*^{U73pbQ7 zdJtQB$61!@PuToV);MAB1q2p8EkTu`3k~ng+BcM(>1)K;ipo-7kS(yqVrwZ6T1&nNG=j- zZd+U8$_G{*-9+@x-cX*^@xZKCswhbA_N-4Fi@Q$=0ny1}5s{y7#uC^vjztF9JC4?Kz7rem%P#B~ zYW7EyCJ~_#HOT74Q+wU@&ihJdApudYYX3%}&^AC6Q%K{q_pQc!P zBbfPOg~VJ|dqtfISW@@Ioh)uW!8)UE}vg)r=sWzvWUpAUk;adXxynWKO zqxEP|jIN|`gy}?ALl80t{cR$UV)xD@yWvs7aN@9BCcx0<477qUa0F@NPRV?#nAe(qet!(4DW>U74=-JN1rSlmZ$w#m z>Euuy2^m6Uy0TT|z<5W+@_o_pB0q}k>v86a$?Sbs%NQYaacGAmc)s{t^)pg>Lu9`} zc*tfCe<`COyUIM2qVeC-z}_KGaVCYTy(0}NEoF?tb+5eY>ASQ)GHq*tC4HeES<>QS z5&sAaD1tPr+%~RP`B&O2{Qe~fl?Ow(i-&WC0n8z70#X!1l4K^0+)0hi=zS%LNL@Ci zl>?)omVTCeiijAA=`MdGE={@ZQRou8YTO+Y*6!ml#TrGa9EwPNQD4Ogn35fgV1 zW%*wDirqg{AA_{u-=#v$pyK5Fo$rERlj#gF-jn+=9B{EtUW$SlO-M|M^sX{?(Nkc1 zo*7tK$Oz>O!XTaKC=V8JM{X&S3yc5h7EhpjlvLXpPwR?I+7P}yNza!hfm#vnOGLt! zVPhO8iO6Q_i#eGrF^-vTwxx|sntB39nSWN5)ELy&Yv7PoUbrlhkgw(_j);lfcn)@aVJ)|J0 zCp7gL&dZ+G|G0zwIpjN|OG>qm{y@niVqXLm*So?epw4+B$3mZy*OCT$$>ToeIao)Q zJ7PwTSSMdnIjZjTbh57fLNuG3vGuIu!0LaG+SltJ zCZ@rv+X|RTYO?2VK3hz4bq@&6R#U|q2fYX3a4)x#geaMObr@q~f42$*UM99@6R)D* z$0Z@iK9vl9ALVx*Jm|2_m48%i&M7!&0;0OjkC70akiP<9wp=sHX2-X+Z6yuvkv4jdb(&?IC*y%oV_uJ=+Hm0nHJ6hfzzJWyDB@cy^bc0R2%VMb8L)3^>*^S zgf}5P&Y?0g zT}d&S@pq>hbEZ!NH=eM!>Fkk5I|Rl8KK=_-SBU*1JPV??y?{Hmdv$eIS$yojo39KE zdkZ)z&9bs3>Y^_y&s|m)y9?>CPjA1I6b*t!As7pC_MP^*1I5mGA?fw8mk^+=NFG%0 z7j8P60ma>oL{T9(k7uswo4}314R#W2T(-)nmrQk=^1gcK%g7C&(wo`grkVth4jWC^ zv~r!&O4)5n-k~jV*r!_i86^{{LbW0N-z}0V)axr-Uy&)Y75mRQq2)h!h3^O7nRz_8 z^>!*K&i^tT&l|(Npgw-2OzHIL3&qSfhX8gPOyhVcDUb!r-U@Rc^H!PgJ2QTkRQiYo zG?~hwuTn4S3;Bicuc|WRapOCi^)HjAK7SKIW{~gnXf&b=Vv0?L$wC~wYMmjjMbhM6 z-ND^T?Vd{g)`^BBG>4GxxeqG{55+Tclm6G;%3WLmqPIO;m>OjqY!kpt=7?@8sg-2p zt2~vYxuu;BE(!Z(?$&r%eD@{X9av%KnaX{WGwoG$Jr1-WONvLdC$!4dw2 z6ix`9OlRTlGPj^>J}Dgc(d(Txo2H?@gGJ)#W+fcVa>D^g>1%E0K^$0t^7Ms-&bBY^5{ z3GQHHstJHtoah+>j`E;6yoBpUL1~XlMR~m9Kolr^lH&{+t7*ve=p7$DGYwhM_?foO$sJ>D2P)@D{y#-<#6o<|9b93=gz;8UX%90Pq*IlF7L=h|b@yfw5 zYSm`QwZWvur4ls@^QDT>mgsawpN|@d2RZ5s!khHHz2CyzX>GrGN{)sWb8jf`k}3u( zTRWF;4JlrjVE_O=lBU@Ut@;cSFnK-q+R53UxXW5KTpB?)Ryr?m=|ICA zQJineazjhmmg)A-OBUk3K5&eGB=bkApN7fnWyGQ1$R!L!$ma8=s^zCYoeGb{p3q51 z*i{G&HmD!L(jr#WstoV$Qb<8t@B0@Pl69DvPu2Z)CZ)EGFqe6I|60xeoi9m=GJ=Za zWAm$FZcL7WnWc+dtzf^l4jzPYqU=RF*O_00?~Et-I=8-l%fNh<&uW)89OIIUdKR|tf$lwgpos^ zXKs&0f`w7z?TB}`OB`2hZ+^m?9q6?*?QOG(F{a>gSMvpXRn42MhQwBQejl9K(d6!9 zP+PMJSZMS#9y53alWMl>A(7C!Tqoi>Lj>23bO?HKYA471K;-C~OS2Z*2-(@{NEVVa zdWGSWw`}@`ucGbU?b`fprGBFsc;lb*JIhc#cj9_X1y4^1>^f_nXg3*T%eclocC&R6 zt283onpeZiWx#YcgW9&fI5`Sk!Z>uz;pJnp+m8Q;W3%o+?p1}e%pV}6MFjRfNH zCNU0hPf_xMwydT?FxZt6Uaa-pbR*s6b2oRUw$LuqkCR`_qVWmv z6|c(Y*@26#c7XyB!P!0{kJ=FknXOb{TXn|j-giQKUh08&Jz6t0#F(99?8)b1{rPjh ze{1(ZIO$zd>jos&!W`lH6RhGsr_{}pt^5rHYwF_@*q#D-s-R`sf(!fehEhHDePgvX z4s5Ybm7|fth3zZ-l+G&>dL1S*Xvb+fb}G|Kw#b-;m*{kR2-KB%-#%^dW7`#dXDpG0 zPJNb+&3Bp$4r(~ERS1qOZ24UC&{fnE(bAp#T9VK4DQ4LRuQs5GX|`+oV_z;)z|W4H zQFFs3lF}fZs63~%blisMW;2c3l=rcND6;aAemr(t@<*k$hGh5r0DN$m(MtDVC*t3w z4F0N+XC{kdZQWwuB#`E8!pDnWkf1Vuo9Fb5tX53M6MhZSjScmz!Q9OIQE(@reVk5v zxWN_&%&~jFS}Zow_SM)P*S|woHTbIl;P=+=!40+7#Ij>mmOW|w6!=amu&ZWshveO3+$%7HMJo@R=^gg?FL!VXI`Q}8{Fbk1s3Rkb=Y z)ZZd_+Myy3=Bm+=lqE&?Ush*JJ|uPA-7BM#?bS%2l`f7zj2P;VRiK_vo^;o&QDi`i z=Q>FOif93mqyqMfZjc?(25Sr%&GCa%>1p)spRjQfH=bbHupznJ*VLv*i;GDg5LI9z z3QaC7#C4;aMjJ2ejX)3}bH8yiq;-aTn0E3z(CrG2RGU5TI|ugi$Mnup-0M|cBAgTB z*xi9yFCyHBCP)&p(e37j;#!*ZAxJ6bb46?}kk3{CJrn#7hl*Jj=D zHQBRa8Og)_vNwZ1x0fH+V%6 z8}_f~Ic$lI1CobJbC@67spl*nr5)X8a-qJ{-16%a`9&Rp+3A7;U1ij89;q82F2_0 zen3h!ra$br12CstPvht1eaDN2MyLKo@txTR?s=O%so}p7nk}wrCBu$fzoj%4xx#mU zcn*D3o7UP(J>P#NY_DkX9r+Rl{Ov$mEQB+P(h*m%A+xkRFVWnLer|?khPxC#FWz|m zlW_OgKQj`*dMC4|ZY~0C5Y_6(3~VyNpF#Mgf37U*uvg*dp1fB zDC|ZmsqmuDMXV2vlDzEx%;X2}o}Py1$5XMq(h}b9M(>Zr*qQyB(9z_LS68xgb{;Hu z18mR9pBo&S3({NS@;RAEa6UQ2Uppka9`vzrsg!aBXRLWPy5gZwuGrpA?))D+_!?VL)XcRAWXP) zuGW_Z(BS#CW390q@))O2kkQ6Vf$d5h&_AD%-rk$PIa;=l)&laue^I5W zNo|C-`$P|iirSNmX@?YY!~eYW8vbr1KMosXghaMkIh@J%#E=t)^dpR|b6}~ha-h%E z0>t~^Oz(FSPMQ?H^AI*_aKc{yN094^MlMiT(;}ny0Ct)pWk_8*lTo|5o{dRV0q;3+DszV8)p`hmBhL!Q5lSL#(>S_1KTaTe3)=kNDOa}$lwWQQ8zb50s z-XtS=4#?C;25Jp^5oeJmbbceYl%XoQ-OWhSMlaA?+5&+-o?TgRV%&s`SC+we z(~P1M5a*NmIRMY!$r$vPUM_K=vA`eVU}G@go;k`(V>=?LJ>&L+tMwx!^sRbWzF*+u z6nxv;q1wvRXO71xrcYH~jwiBvv8ub#o>FzKR_wMc*iwtRDuxnrB%&}HLuUp$x~I+F z)St!{Hw$)%`zC<~G#VPS(g;*9E?@ zar8yyC8&bPU&OBpfH# zhVJl0YAi1uJ-JX|Rd`53P@dZWmSn*RZNFFD4GOy_Uc$cLXmt%Rk^7~&!nyuhiW3Mo zW||@3Onth5vmO9g9;C!9#= z_W!z-TiGau-&^#Fj`%AnZcnOM8Qd1dod$c@#yPCSQ&bc&@M9^!z;rVgJ8q>vGiJ){ zObbVBCU=QqI1bCkno|6PbJV zr;*8F(uYkj?U&8uszK+$4EPwCY546E9XNV~ckTqZlJNx{Z+`~`SF7DO*Ec|Pxs)Cf z17bUrIkRa{xY6fM)RoOlsR!phqNX;MP%vpxB^hMBp_cRyWGVF4*D)pFfAsB$KV zn(YmVombRGx5M7}e4`IuV9?;B^H~$oWF~;)`vpW?AWk&EttPkWW+PG@Xq+*7SdTEU z6!=syh-!_+4>vUoVjXS4EAQXV7EXr;0VLy|c_fYAL&hZVQ>;=Ai*s)~BWpXEu5Uhg20l_i-x;fc~OmSFo-lv~qvoVky zdQBmj&gs)7a7}?M1l=Gk{{Z*Un65?oe;PZ-;7kH`P0z&U#7^GWwlT@XwryJz+qP}ncw^f( zCU&yf@0_i(zqYo2c6E1k^{;;Fxv%Tai>Brpb-)fafM@6b^dB=cs8hCWSOlwiR@+Od z+F3hm*?EXajLSWdqG(9}z2al`9?dW68pMcoZU2dIU#%bG!Spk8eWqL(GV&#vGl}!` zWo1phz(Q_#5|kE@C|P0J;PtLi-O?<~Dr~WYMU`mjAJt{7_O&Got2c#rZ^B?kMKl{? zwr&-AXx3!~-@{e){rw#TU&ZbbJe-QTts?#1qtGI%01-2k*2hQ^$FkIfL={#x+Y&#RPGR#t!#iy%m>2{SnA`f-2;`_US-BOOYJUj0b-3wVB(JeaG z;SiYgHbl{R&nYFX$guPFj)(3cH2sa@GSOzE|7=kyuB&s){@(oJ-vd6M34EHn?2ZSP zzj!t92eUV8=gGeCtTkPTNW}qaMtt zR#w@Ds6G|1uHLd+l3H3hBuQ3Ka32CvoJm1f@2zwRP%xjNDd ztnr@prbUjPDg=6x%M?d{DhGFTJk6HRF#(__%7sE)^>1!G0lv2|usx%~ z$%HQp^j0%5hZ9TA98`o@b86X0Vg&rh3g;5vQJm>bMFAN3U1X8Xp5I1lP${yX6KS2dhzJnTI=m34_346!#}w>rOzl7@Awx1tBR_Y-%f9W|jrgFXxn%RyTud{NsSB z5ki-!IgS~n4jaK&MC7Hv8Dnb+kLnxkxEHNFFI8LOGbGLV$ z=is+E`NCq;*sLtb?;0~u2)sejpxDIpw~it0sPHU~=3ToJRUlvL*wqV5_TO;8P4UZ;b zV@kZ%X*RfOhFaK>6uEw`HV2`hkEn)!er)8ewD;C`4;tyB?3fO1gJcm58p4>b2H_mUjy(Q#0tS{Y)y6 zCuI*Zw8yqCSL!R!@IHEg+a35&r7`8v-GHXME7jR~XA99e#uhd=pw@?L6NRFm>lG6M z$GyO{Yl1%9deHu+*>bH zk)pF1STlWD`Zcx!RGg=3LBRp4ck}HSX^po4w(d(#&l?Wz<46Oi9)t;9*LYriszd8s zEugI0(jH9>qGxm+XnVa`$6$?aG$anW4nAipqHfiAv~b^uO0*IkN1D#ux?w>IK{Ur0 z42Omv*j80=-C;rf1onE+tl?JPM5OrppE5LfoMGQOjMEOvNVjg@3PjFYmS>zy9}Qrs zFTqeZ-^oEpzI-z{q^J<9(D~CmR!&2@{l|LUb4SAj)+5Sn%qh5(7_as+&B4J9g-)p| zq@o|T#f&qP@BvBZMh*PDs_tRH$~ES452zt|jo93|-F>4O&nmtZ?3^_xc|_RJG}dwt z9xOZVZk4`bD653o30BjACa%k*o>C41&6_B2E>>Ep^O2eN=wfKC2fD#ynAuRt(dJt| zz7mjSkS3yQ_>W8l5z2I|w>c7zFS4=uJdHIUoDBY=lc1V#@kfzWUo<_6$fnwO3676f zzl=3wUGzp{@SMErh@C*npARF#V zn_rF2X@9C;mzrZ=%zlRnj~%0BJ5yr~;zROAo-wOrF&a3C>#$spqa^@-B=R`b(QvJO z&0tAD8MW0Dw)5lSXIgZP?|{BFvap}E{b?kR{7cN1@F z!(4F|re!Q{XDJVMA$tWq3xXXnyX3L3`Jm85?|2=G`?u*tIsC_RCW-zZMH$lmNnwmK z#sbiGe-7d+K=01c${!?qc4RS@{24GVa?i83H>52+i~x-kS~r_YOed1ihA;f-i0PKS zqPT*Q6W91KJZcB$H%^Fqi-1D^2w6=s7qj8Dc|Wxd)t%n##mcOWw4dI;QI5H@Vv>7C z*ATaf26Krm_4Zfr)fX8p#XaG`LDt({_+5N-GSY-KnCw=hc-m7gSp;=XnUs@)~&3$DSbbM%%D)?LxN)Nko836u^jme|E zC$tZ(GbF{Z+MogXqC>&t2*e(A9qMiLh@bc$B?1k+d~y~1y6r{vsQt^EzKWay+CQYRbtUMP#E#o{8?n~iWu zFUu&U!4e1Em%JHjRHS?3n=!km{jx@egJ<6p!P)<*9g zoz?l<(}$>4G#n}8mh+tz8m-8Lak+cAq@E}T?Bx5rzMy_SY#Zt>K#yTeOwv@kN;f15 z#}P!;G&XC@ZHeZzL4IKjsxCkTvE>~wYYZY3HD%>nH<{@ez36+=Pqb93t+i?tZt_2C z9q<;EC6~3<8%fNMCHDGP8_#+pO;=2gC0Zzk^N|#FQh(3F+c}w9pb6+!)NG+_xk1yN zC8kqZuWv`76)|lR)X3cj-e;)9^jPRoz$smKt>22bo_B_OOW}i;C#PbMZO~wyb@6=O z0GDfo!!B)%vfO{YmXC!@JZv#N@{iGz&jkwhYz)b@4?Vd%Ih~xsb4<5SD z_(xTOuXjE+U5(T0RC2xu(z_ZzF}7_P-!yPtTZ8BLkIwnV2@>ZAaZhB&%kK!X;(6N} za7o~19!^O)RpeiwOTOQ0WVm`?dr3PXCSLdpfQ18 z35-a^2Sex{2F(BK^>;o)-xp%gu_a#>?8dLn0@Ju;k$B&?cX_{^9&_LL`-6tO9Q5#` zJKrSJtj|*~KmKoE{F^Sj^ZDKs_(Iy3l$92}8!alKJ;>Uro29?5j1H$4f5`H}9A$zP zHzq*+I}yECZ0(m`8eozQqo?C-3kc``V*ZH-jC~`=w=63^Qdk!>zZ88N&p61LfBdX} zN8@3R`TUT<`TWA@jo(H=%*j8f=MpejO%jjycSYG*%}~0mao;wJse0l!=~)Pk0CV#| z%|Hy(??KvGjV}D6nqCX=piys0hhWlo`rZyDyd`#V14ybOFpN^($VYvL0h)dXV#t^8jc(j6Y)q?*O+JF))ee_qAm&~Zv}H>|1pPxLa~La4VZ|OTMCo;dUJucN zpKL})uAEbuY+;8>R+;k!PO>Ey=xZ46Gg8uZAs zMgLiP9$+99%)|IOM;3_20X}h>2=8%8r4Ib6mIPFOLJ6cvRcW;9Nu3@+4QoY|gz^cW zFTG;=@-QIy6x_cju*6hOx(?I||8k^i%)je)=cGLIz{~(eVTJ`ds|qKnB1xEPWvVw8 zMZ(C7tGqlK#n#fO!w>5Y{ECA}UEF#7c zfA-X!L-J+NWMsj5=bP^oP!l00Qx;XYcT;^F+WI3FPe}X^dkM`l9uF%}9@G*NkxKPm z43xzY0r{z)Ayo;W9wG_Oe{qWn)rJPQXc>=;5fxFvFQb6J>nc3Tst4V< zdXn%5)&5h9G(i)!l?*yttjp7DGH}7oi<3xYwW~gPs?`^d&B6bz6nU6FH5@dyNKKfc zmPbw&WEX~Z6dE1*73%30Q=#!%F;v7N zqX~si;dfjZ&B5tiUWhKjTx}q>i$+TfOm)GajdAg`hXh&xICf#aEIyY=9CHEcBzsG& zPJ9nH%`NXxVTkR5(U$O3D+fsU8G#`Qtk9#UBV5#-<*$3C4HwQ78O+c<7=qmrq6(Ea z)q4?LIwYJ-iB79=7Kh{LR(1I(j>l}AGRmd9)?X)H-0nH+v`$aS#JDY18P|0pWYHN+IasOBGpPU zn*-j+bbraAA3PF-0AhRzQ_-OE?v*oRl&dYkdJ&Z}@1-2;NGLpcUv7y<(XG+YCiyJD zFeOLOAe-0uT*b#}(QL#1Rhnb^mUJMk{eex=Qz^*5Yr{dYp|#?!e{nXdaHIlrh+tcJZR`ngsj@fsaoIUO` z?%{BRmiDD*5fNR<`JIpeK=JO?d5CCO?wCx}p^sl+O$zYtW3m|j!|$X*6pW?b4?jV#4V5uKV^k z)gj9AOgE!gm}qF~mO&LU^lT_6u;CKzaM97qg8u!96nk^wWqbRQCMsk}xSI^v;Ugns zgF~~@_r2f00bCAG^>JAn3HMd|!;iwIVPPD@{yx<-TAmx7=9dB6W+%r}?VBNO-j`t1 zuSzAGd*dS-3`eA~DezcjVZTP#fN#H9f|XEg1$|6l+QOxl9&}GB+TGe zBQH)yFBUV}oJlgRnLUwt9H_DJ2rDjp*ki-4UH6|3v>XazW{T`V>5i~w)O-5`o8dB= zvT@j}9h~lrG~V1|W2=sJG>9-8#HGiD$?MZtDl9*X%6pUh~QbgX!IGcD@QtW zRselxXR>z89omgIRb1E2V$l>&Osh9yeD+tMxMoer=>Yub-l{)41VO*w!WAcKz;s7@F@RL&218P<4XwH-O?rPSuM_kc@X>jb6E{vs;rYo48&k|?)^X28 zjD*tE(9ZSIUF^xcZA!E1Ji*G^MKUUJ>h|aXY8k`v`f>W+%kW1)NhKLIe}s3zhuP~W zWh0kd)XRj=eD;v;6(JXuzwPlox!Z8cNWIAL4ZM@C^Zo1}3Bx&MuEvvqLTYbhe*=&% z{9oQ|{I-Fw*5`T@5CzazKFcxamtqdPYw*NA+Q9SA=68?vALYR{YYY1dP7)1s{fCE1 zsZEU7mgg+VEigvIGgl&}==OKOn3Gu>j(wmk5h-~@SHU}G=N+9&J54uK)p*c`m679| zfy`y*=rTDSVIE4iEA)PUe`?nQn7J72-SC(!lw#1NB!BMPMq15$IJeLJECWmFL@NE=Pr$pM^73!I*Znx@SLJ>k&^U=0$^^k{)` z(A30H&%|_VP#V(-5L5l2#XtcO{ec~IJnNd;x`R`ly*f0A zB#ya8`b6m4Gm6dU!!xcD1Qlc*>P%&yG9q~iO8*{AgE`_qb@px+3r_pL9 zqu{v4ZvUB!3!J0&F8HT};pme!C@0EGNY0>@T_Lbv92YLdTy7v~vL};>br+p~0wG`G zx`xprua+Ym3WP#eRPwV+^4@$$l6cC@QD(ktQNY1jF4jF7zY5|L6=wR!4|F^U5kaNc z3RJvWfqzdi8$azb=~HNhn9~0cTQVWaYz2)Gj*B%TUJI_%reIj-=G6%b6dv@?wkRkEFnoj&{B8CXEieRRkHRRXPU_jSr zM}U5bMud!iZKtgUS3%%Bc;~<8_`Gmfbd>uu#T;$ApMk!Ne_u6XVe<5I7Vj<2{f6@? z8v%r<0KQRinp&4wQUY&lYB@Hy5I+~qWO8#45SPHVzGrtA# zV$H>=3$F8&(O2TKyL-mZ?~vX(sZUcIo5}NrRKQl+U3*}7r^+_DUIp>lU_+Fpb}LXs z+T;a^H4gI5k;=U)*G-$_#4Pv@+*M=r(Izt(Dus~Kk?>veG`-y$uKepe`OEInC7eDB zk^th;ozR*tRrXzh@I-93>5Y|>HIiGNMB;7HdqwRbwC+EfOV0Gb)$~;tPWiE(OJT<< zN>r+2s^NMjvNc(7ucqoJ47t2xM&$R(g?N>{KUNjNVh;~sIwbxevo!ocy~<slMBn z$Gl>l?Cd=WEV_EIr(v4uvk}u>!g*U8=BF*)(&I8(E0%_q-=+`J!>i?mO9o_RWeIUk$ts4i!FYWiwq6Stv!2xQXhjA8iZ@0{3bsTane zma`QD1p&4LJ1)8~nfd5{edmG{FiQ1O`oAv(>F8F+Eo!viyK^Bmz&$$UL!_%ewY0bb zl3!X@puO1cz_iO#Ncb-itbw+scR|Ewo@)6_KYn*exm>>lSNjN;-mn3-ql4RZFt6}S zmM7S7Th}8dsMmX4F%y7rLo&ff*4ui$A6jyP1W$MiIANc|_(JPZM(0CP7Ve4Umj~o> z=e$~#MARNQ3tosAlGz#TYJ^DdmxJ1*ixIHU7X)z%f3_PpR+NEK`%o? z{Kv+|l668?8091f3;efXRm0P`;l^t%&>{xpyF#NoF!QAN?P;tn1!YIb2eXrkBzl** z0h!Uk>uosGiTusRt4H*ubRM|BTV~DK>2Oo{_(ypR;A6^u{z7P=*_GS%iE)A&#i~~m zqT%uXXwp2T(QY1!J|$fPy@3Nzew~nW8{7q zGA>_jAKwP6N51rJSqW#+@ zj7LYX#Sx9V%9+_%4Bh|<3bu7;@EoT^Z@TuEuqAxhuzroQ4xcqchfFIu-c>o~(ty6& zdQFEhV(>Nkis$4*Im~)DKh;7m=o25abvUIM+Zp0-hCg zwJO7E16c+)RM4-87Yo)hCEB-RqE>El8Ea3Ehb=ztb$#8c5TKH5m(u{-dZ1d7%203n0zhU9~@ zHbp!lF1j z(_)>>^G=hN6LXzV-)aHhM@b79pLd}^C4+k}owP>-Yoqcy5RUYWi z=fQ6)GiLJpLawq2CXT#$zQGprr?toaHa4PaEd zhKm>_vK9pfg;3N9h_ffb$CfRU2ZxP>`DKis*E(F%sv0k^-0Jj-?Sn^_T9U-`dj*X& zv&cT7T51*Vw_3v3uapU4)&n#YPwou)&!3o*Uh{!|4ng*nymsl}mfZrDEGgQH7WM;P z)1z$(P(B2+TZ;$Jbx!=~Y%Ysv0S4L$k zBd5O0wE2C2AR4KM>{oIv9qm7-3V(p`Tz8UE4Qn>tB|6h(ZnlOE7XTXNv3l544_Ac= zaPbCmEoiS+s`tzL`!`{m@ao-cvkg{Z6ksY5T)Hl~%SI@vX0Fser7@J-&NqE=;%jw{ zE2E;@m(+?FOFAOHGPw&Xsq0;7uA=*3X=l@(%_s+>KT%;y{IF#yC-hmIgGd zZEJIrgRW#;tXr-}^67xYYiSm{@VCXqzm5l+!f3qLhAK4WUE zy~l|lKcJltS8FbI8?I-DkF<_&BG!)1k|+<4rJ-R~U*E@!iM*&18p|(xH}H4E7V8;2?}jq%yDBM=|q&d76QJO z@SocBbxuM7BG|Om+08W02QiboYTeSABaA zqAt(TvG;bXe;_$F(~B`=W4e0Wxu+pCLRAFVuVMVWrx5#l>+4UWQ)LG8$iP4WH^1=n zm$pJyT`@!Y5~hZ^9~|uef+8&2YNU=w#-`vjz5LUY^*Wogvb|Fpm}QERNyUwbvNi%- z&D;|&cx--QFwJZ|&iebE&)z6qY2m5+YlhjgKvjnW1dR-Meokr$GNtey&#-iQZ1rn^ zE069jH&EVtu3x=i>BmRUw(3(uTzmDdOH=cpxcq1fx*6OLX-asE`#DeV71psXlMjn7 zH`)IwugNAN8YfPUtvR+K^C#D&w8Nd<3JY%8Rrra%R#{S{77lAycSmubg-LFdGH@i- zO)3oM#m(M2x~NHn=`XTrd-P}K)jr?x*p<7J`9hAUYWT@s@$n9qxXo2>V%q>`wo7r_g>i(XlX?eO*v;>58oex#BJ>7C$~fcaQvkz=MIF7Bm5#;~68Iw29yKGYb; zHLp}oDOE%n7;e_2XKZ}XBFg)7fS_L-q)H`1MKOIUc81p!Aygsu2TNfl?Fr4!Tzgt! z3UZi-(MltfO3e#I0-OsSU!ZrVU$kIsiu*P6jj?h!chahil@vY~t(scj=THF$+FMU+ zq*Rav9UdpT4|D+-7hWn{*ajagnpSyU*#eg7lH*r*TV(Nwv=z8g6q>acuUgwDOE5~t z@$VeN)%2RlxN%_T$?G?UW=97bB_^r2{PgVb1qqPlkG$Chb4qxP990tj=|~oH+;tej z>2`xpZfk6Jt3vjBI%Iz!rV!xAXwRKC(cdLPJre9_^+Tt^G&=FD(=);04YM4gF*(-V z91XU+{qHb{RM>VxpVfx-q!?7sFIhcydI0zx;Ngzh{=qipaNe2x(9w<)&Wjo;3xbAb zf}g93^TM6}oD0ACaMr9(TmhU7Q7`y8M{y$mWqFNaeq)U!pCN3Ct->*Y3+JdUb4jkC#Nl&tq4&sCYPqGVSc?7>t#1e8N1_{8V`;8Xk+ zW3w{=2WzRY*%IL74ft@|u{{^~3}8-ltPX2JprMs|=dor`(h6@OEe-2v9app9GetAq zI=qBS^~z!`Dd2FXE-36~R^#<#TCXll6_r~P^~t53%p#<-=a;5(F91QeiE7JmfXza8 zQbF=IqqB6`6ic3z?_Abk1#rESA3bsp2g8%pDG>7W`dttgEfkk?vFS4$bGqCs9{G0s z_3gwnhz=|^3iwU^ZZ%2Wz-)tmAfUiEiJQgvGYDd&_X0J!@ZUg+qS?6)9<;c`+V6rf zA8`SPIawB$GG!+E`N5m%Tt!n`If2Q^{o5|cg*rzchXXZ`mp=PoHnX8Z-cczl?VnMP zhy;nOa+&wMFzk$IY3HgkTa)D5f+I5sN(>gQNhvW&L7W&jvpGF`O+?z1B}ozz8i zSq_qck}(%h;fAr~jz%BgoZamwk9IDaQuI{e_YFybORE4hp>vxWAV^8#s}>5B!J}3> zwF#eXkzC6uF-Ckjw~fvg zZk#G+QKW&2iN|oLM;Cql-M9}tdB6D)*{(WnCK7xPhw;==RQ!A=cq0qtcXj;QTvhtE ztSRuimj6xH?nYiO#=2hD;5xOG16N1hS0m_Ca5~^qGoR|-jZ1w zp#{-}>svC#*JTp7UPdlwSe9u-uu^OxjOYY5H+G-Ji-`f(@G$Ok+ue&Z+v=Q7W-sVK zkJNQ>4-YQ6$fPKA(`yD49~|JZWHQ74z*ATk0lct=AvE9`3o*b`?ka5&*hZ5*7t8STEr*%XNTt{zg_k??LJ&wp2D-flchC`-KZp4Chgp@JfJk484 zYBWqSvBr{fM|KwT87r6gc)60oU-X;Aaf!P;8P1I1*AuCQHT+u5G4aMoZ~&)jTY{7z zHQ5!-?8Na0W6a57?SVM(AFuqO;WYlJ=8LMz&n9?S$|1LBrvvXSB~;;A^(Y z7i_tW1g$A|{4J5qzJY(S*JS0V^ovtJ(mT$*(d`rCf@{6um1@Sy+HAnCA&|MbXfXLi zpxEb~4;#xs6%W{UWNA`Y@wZ`HWca>EB!R6fmA{z(XHAvyZk`dyL=3=YR15e zNa47aT$HACTmzIT@oJyF6}?Q2FWfnvj~ALAWiY2c5Aakz7_;D=4n4dKdfKm-KCS6Q zm)P{(oLYL0J^i5`cO0+j*2+qL+?u!EcVAXam*w&Hl7LlBcFNo1f09?GGjw|cf6BsW zx;swpIY|GC z`%v~Ok@6gib+m@Pq;uLzuU30k(tXGDgoB3s?dbd@KuQz* zUb7OzaDa6ShxVg#fAlb+92DvhaSn{s?w>YRUfv&cj`mx(MQKKSl8{4*RH%4Qpu^ZdAZ8T{b+eTws8>6v#^Y!cZyZ4X#Zg&4T@4ho< z&YXGXnK_$qMR`dCSR7ao5D)}uDKTXb5C~il5YRSg$d8c#)9h#v5X>kKRShR)Lsueu zM>|ss>#syk?)G1azPeeMf`GWK{LDCTFz&z(elLVg^~3umoaF$;usI1TU7YN4f31NN zK11#f+P)3pXZn7yIrI`)WnIU~oSdOOky5o}be%0&?(Nm^yt?e>EBJUyPGQuOU+bA!C^z8cY1Av)hro{c3C(LWw+dR{C_R@M7;Bene>8e~!O{87 zW$%_5|NbIY#_(R7buh{OBd3n=#|blF`laGKuJwH@0;7o`^IDTqW#dKSL)+TNTy<>dNZ|!?ScGqo`?;}mzW;9~*J!7$*#(H|r<7`RXuls6b z{n=(4GYA(a&*?tT`PEcvENQlBv~Z^`e74L9o>f2XukqY;ftTT*N-7Qzpizt|DR#iH ztCxd!YwHG{fp`uTWZzl85f%`dbQkYpCf+bZTzr2xX6HDkKI>ed%8y7t8OIQ^T^s*OSE8zuu!$_m~etj>D7NE|>Hp_5N(KO5SeniJ$s=1>1 zQQ7$9Vj76XdsctV(011NoWCH?by<|CzPGohK&hE=vyqMfn;V_?c z)fx=QBHN3KdORFKHFrBsNY_Z)tv4K1%lhZhsK~>f!@A(dv6>pZfU|zd&h`!VGsxDYF z_MnlnB0X}Q72efR|1t|_C+x`ndaYLp;7NEZg@5Zo2hDRrrI~t{E*i@uPv{=I7dD{P zhUvIWDa|$eJO^6iE@ACq<2>l_+KhIx!a&zL`|9xyR-$Mv101o%WlKhN4?7hRh4JCgzj%cM0Ml9|yM!KVRPVY1Y8Qg-_- z%kRb;Tt!}swzyfMB!#1RzWYi=1L8VkZdB^$+}!;}P0rMpMqY@S)!Ou6%~I78V*{2s z*Vvq(6`ueV;*`!i!=8p*eu>qF3e}eq9kiCdrQ|3mR^WD}ZOHu0&DS_2q}s{_$*~iA zd=KpP2AOG7Sfl zBh}U)yBj2~DrhNFZNeKZETbL2P!lKD;Y;rw-7y@welgf_oC+Y*|NOVwK*nP{e)Nj;$zZuJZV)d;^G+j#O_ z)`PQIFpq9P(iA)zwDUyDXWk%eIb?j_os;7w1o%y7=SqK9g0ycR2^q=bM~ zik+PZc!5mDEd|~w`w?oJ1VKxaf}JbJD??_26_bEt@|YmDlL-PSvzY%@cF3y%ZgNRxCeG8#-r)E22yBo&*O z%%DzN+yNWf4)q;}a0G&l$e{4Ozn%uYv9b7?{A1x@nM9GgDJU`p&}RT=MxHv!RHBsO zDd;LqjkgIUBakN?NKT*YZbLgus9aCPyghKn|@8oaSRo^D&w(yw71UTnC~4T zvipMgwzdY^kwF|ypGS9oHmXE^5#28n14in;2t-T7Iwh2$FxptQaBwwN9=DNpT-}x_ zV^7@UZ$@b-dF}x`?OX&TWnuc{e zuoNavi{|V1j7@3a^G9>7_zy&R2 zDO9I5Y%ZUH^hxFv5@kGpMT5FqmU#MIZElb9i)`(@TVzZrDzg#4$!FJKIZeerB#wN2 z$T*ex1-ST4y(p($8wOHc0Tw6$palEWdUEe({9tPk{2)o9Zm@{kF^}*H?I$T5XzF-W z2c(L;@A;?X`JN;Opvtyg=E5v7Nf}SgMH|n{I1`24MKH(k{fZ0kQA>XoU40^ zXfDxJj8Zs>4LAAOT>|L5Y$Pu9w8S^NM=B2}tQewfG=Yk@ogc~Or zhlARW(FPDytYpG-`J>=^h)99_diYTgb6)hE)QOZoKoWmIO)Yv5vfxGbtwr&kprhW4 za>;T10dL%8yBSX})Wcei#qXbI4knOD$*lr0f387r_e%fIkniva$xSA>) z&TeW5CJs-s4nk?A>u0W16Xo97WF*?VwWwpCDnX)b6b16ndOc=)zuKA(mQ(m(6-?G? zmIq4H?aW2rux=sl$LYYF!mcBWnFjR=%P%WoKB0K!OW@2ChPvR?j|W=rq`Qqt#g%hu zHY2Js7qZ(>ghIuKUZ0H@e6EE}fL0-^7pxfBm4O?hZpG+E2_)!qG2Y6Dv4JKJAfh+^1lyNIGz4icM!U~K(IV4Vm5X|~4 zNQERXxgI7NR^m9^9U@@41NunYY zp04tq*w5sM=+JLF$Q$YzsMX5}m1U^E1KSN^N{F05aK-q00w}V`5wTMUK*0+0FoC8n zR49+el7p`$t}UE)M;n#l)0Q8uF#;Zr8S zz&T9$sUm17;r9|Hz_zgWLcTlG$nxIW0Bppt_!Z+LiZFI+yb^RQq304~<5f#n?2E$A?CQ$aFA}!BMOf{Gth7U-ut^O&G=Z z8|NsR%K*m=2^9NQ!y`L9y#UxN>CmWZ03V*JQx2HBY^GfhcfL7k?(q~A`XUG(1C?D5 z2z@zIrFx{i95qT06P%{DpJO=uD&r=*s@-j)lrlAwKXN`(agipAe3pPx5syi^;14XW zO_ste5m%cgw)t#f8_$p(B0P9;yj~)CmKJw69EqlBe<%sYDiY1&%tiPR|IUjLuCzUv znZ|+S{!5{EV`(;MJRnbaT5XtY4D?2^lbS^~;4@tcWQ?RHl&UqwVrZ~`W;%ug>4FUA z9n%U26?}6Ik%hrN4Np?fkGO4vfeqw7T z-Q#-Fe8@;1$LyRqko?u&K3&hIX8M3=u&hL^{7np=+%J(SvM3vUH{<|lmfcS&Aamh- zs03S2Jd;D+E0hgH{K z(ZcbD!Z~G^Ur9u6NR*RHL-E+@6Q#^-W3+guqyE?|8`tvgg2+Yk>hNjeazn32Hp367eCyV#K(4 z&p^wu2cNGgpdH{A_FXWp5M3v#F$7THsoGen6W(p-T)Px}GwyR!J%WP}IetvC!5-Bc zD{*1NBx8DePiRlXGKY!C@+0VIjo$v@rN(7ERI!uK8%iB z?D%p;3gzgr7;1PGC!y5%kXNYzMK7aobH4@KAR05sJ0Waa_nlBFP z<}m-IQ7=+y&nbINv0 zv91T$%=3o8yS01r@F*ONOR9iu{=A9rhfm9m_ZW77Ek@z44Z<)We$o)S9yY1>9-`Tn zVM3@tDxzJ1%YU^txiG;}fJ%L05)P=i!48z?$R}wgpo}2;R!iIVIg;L60u*8!SdS@_ z;LVq@NccXw^F!k2It%-wWqy;$e8^oadQQKKzS`1HrRxNhEq&dUvU}Z%n5wh|%xvba z#=9&7U>sFlBDHWDN8Xy26&J;hd$>XXkJ)SE`k^)q`h*aS~ zW0_%N88B)$r%Gma&<7UDW4VlU93gsf33TFk;fH(CAQFljl*s=U-%bf;>_99T=P0l4jDtVJUDTrceHJcX1a)!cA z%$q+KKi6Ya_VZ|iwO_unB{a?}Zey_<;(^6z$l@UO_fLUwUJQ>3b+&_jg(nh~Mj0bw z*Ep2`Cys4ext01%)G9Agx;9Cx#N|~eRh7LkiEtY0<-k02zO8ll^>-B^x&<3}DX zY4(6%H1C9l;w3v8!ICdUCu>FZ)GUIEdx6?diiOi+Ry&X(aElG^m-Tj_nI-Tu{PjCI zXuud1V-G{II3vhsbDs(?S)PTF44;_^+fpJW*zZIXSVPkU{9s)m9cxM46G6^17|^Yl z<&?BRevSZ|?a{!PQDkipKjOem=&9axgp5E7yLHHm&kEe56S+_#MS%t!Ska?DF*d3 zl%QDHm|id9&}hm$S53X^aJs-Ezq#P&sx9KE$%k~m4xa1C(qPuuza3f}I(IeMJ0Kt& zZ`QjA1h6f_y2ufBFj1iRJsM)I2ONUqkX16)r>15Ir9z`lu?B!BS63=^e?`K787)&U zrlG_scay<13_d_1#)m$4Je?*mU09$ASuj$7{1vR&7czufI#GHPU)dpR4n@r^o!Fg! zNMD>tycHk7AW6v@Tc>sU$}Q@dtl;A%U9BZt77^SoTfJ*_356|m{eEhH zxD(EzKDspP<=JCQP!}1VTWJPUt0==WPW&#VM0|B8%sh`|WU>{EO?1|1E%IZv!(E&k{08KvpXJ5pC%!W0UE9YRkyaP zj`0-qQKZ*kVqe-}S{K{{M~DT@_+7%2nb!!HjL6M?Z8E?cJUbg|Xsr1Rep!NLr%NN0 zvbLQTV>gB~W156Kp|jw=@(~ey=K>u;gzzjT?$mw)WUj^P z#tODsbzRtYKEp#p$L>BE>OHZsM2J}@1kZ2I;sep0$JetVp4s$eU z`6A!ZwVat+Zs0Q5$fy}&0<3y^Vh73tBkL*=D+7ogK!Ll5MTaEhZ;aISVV7IYgy(1c zXB(g*9b;@ijPa(NR%OY7Migx{E*%9>hCv|k!(%M_WYjR5m(H;u`sYS`qwj0Eg$m=E z@X%rXbz@96Km_2C-0D`l0-m9`Cb{zu4?W_jJWcKqBeXL(cw4*wu+))r1H4rc)XsSvg*c?Xn$3+@a z(>`lMg1b-Tj?}v}{_)13=;m>{f)9i*XA8d+dHmY`IIHen>pqs0mm}U}q`drF`SB~Cf@TV# zCe6==I|0uQ&!3wt8=f08{9H9xCg)3#=geV$uXZTo3tBBJq$c(~o{c9v;%*b-^2b-t zocF7pHXa|XaWayBFU*rhT6LeDdja1*#;+>d&6gY3gm1v3XEe6If`*HmfB#C}d|>Rl z^UtkW4+PNWy8Ya-We@**Unhc6@1(iS)843e7D-W_WAolY))2| zS}Wy#QvZEk%;6=+FtAbSe=?Mpi2mN1l`#zbJ1k2ZjPRerpZ|}-|NobND9RDdj~nX* zIfzHm8UTUmzKlk?Fz5K;BPvVg@Bq1lG_G(56$7xSNN7rV6tqx=i%FLLBa8xB+9l+3 ze1T!qy%Ycxw^tnTFS;Hm>UOy;8(6!(t5{)FyHWjrX90^VRvvW8%#qP8<}S0jk>-cW9T85UQyx#%5=K>*#?|rh2~Sw$V2I#A5{XpX%mhcF2o?Ef zL3$zMu@NwIg6N}-3LOG~&?Rg#jzF&POF+=0Eox#ZbmbF*W$-_VvX zAKQf>EYil~j;D{{`HS??`g<<3VCUwrhv=`RD_2AKK?pZMk~$9rHtaIGSbDM8a&eO7 zg7Pnhp5TUNF?xqul+=noTwR|(<`+~8@4kk@36M}B0(L!NBEZS6_{j!ikmX$x@B}4j zj)z+L#K@y)g>`5`VfH1<`~?9N?dT9(g0GvUUES~I7g$E{dqlktGwF7)VG>ekPX=zT z?&M-=-OFeLdEr!bNh0fzGPk(Y#TbC%42Lobpb21uf=%(??PM4@tZBC2+5SXC75$+Q z1?@>H$~6v8P{wkX=;#GTBr%k#w;$z9_V=gv7)4N@dYOEg_Y}dWw+Rf1G^{XU29Yv6 zDU-tiSv>!IOd?su8w=Rwc{HmpD&?9A)J<8+F4PV#*bW5*c63;yT^g>$|JjRyh%1LG ze;bD-C~w)Gy{o|+9{*ZI!7G&n+2h&%%}qrwd22IWVXUsvO;5FnpJ!FoY6GW}{2vPs16;TMD(ytJ(&JMtc>1$ou!-py^lVeB^* zort$AH~x?*>ntc!r0PDNk$q*J)4XMTRuo(D5ygF4*Me$s9@W=|iEc=;3 z`5q+D1b?EW8^TiTk6@)GqInb+EOjDopDgQhRud>aV&)BbOCT08xe-)+rtc^GgM=mF zQ@lYg5mqtFE%zC4MwMetnfzxcSq{A(lhNoT%f%cR{0bboKWl25+zScHN5O!aBq4>+ zAuV@bSTuwfhh;^1G}xW2&YaXP5>kH5j#LA6z&DsAb-*iy$un}xKRI_4`mvT~1$&Ss z8I>gR3p;`J{|OB44f#aCvSc1$U!LpqK0254CU6u^8kC3+K`}Uxlb}MY=F6B82!-iR zL{c3%7X=@W?Gw1tS(;Y9Jgjj-FWa~-l4aQ+%&n@Z{O<%4KIC!p(k~iAUzd(J3rfh{ z?-WtUBg@|!b&%fWB&0BnY3|uz5Ln5OQGSd%B@nFisVMzS zxkZACE|xmY`Q>66Dx5W2Qj>-sr9Hr;FtBK7wM_64K_b;B>yn%>U!yCWODVKb{SS-| z7b`9?DWr2LIG$21ZaMu-LRD7A zj3MWwxOxjsI(ly8cu9|DwEzrE!ukAXaTy|dL)_V%Wo4Zbu-(F+5;uu?uXq~SLR^_A zQe!31xG@iU++xWUTuwpCNGa1#y)&lKbBzjaO$)}1gv80j|Bz*@gWR zAMOet#xU)WB#7E6x*oU9-SB+`2bWt|yCuu;^TA{kp+1dap>T%0SN2K{C6=BGCSdf; zcY>d>Vo{U%YBD5D!>+g5 zhtLMgus!1Eh6Px>6<9k@6J;4Xu`x~R|31DU{_!}f?5(mZPyna3Q_{mka3i_?tB7gz zJ+gStRMgDWE7Q?sVU%n=Yv&6fsg`z5=nuYMaRejG3l8+9436A&33(53j@$~0k`R=l z@uK@CEfYqYSq5n6lkocZ-E6DO-S7`>wN575b>gIc7uTl!GjB0NQMJia9e4J=kCoqa zAWR=|@zNFL;Z1^Xbl>mADqpJUy8>! zAb-!5wefVPe?$U37VRH#nqk*k?85BhD@MIuNYRq-8LhLMl*xkPr~LA(6dU*yX@s9( z_hq9lChU9M?f3MR-jF!DSFaQ~58!Zs&>I4aea4p>&u7vNP<(s7N?4b+HMEP4!|=a* z^d4O{yjNIfa0TN6YLWxY8ZvA~4oiIoLL1wM*8;jnfoA% zXo#~8So!*(Lu;bN)bd4&Q>2{07+eG(68E5FV%X=%bg9RWf(fJ3ze-P-+MWDMa1yC! zj%fGUupO7;`O=t@x!5p_BA+doaW9`qVvAJP;MWgAzGhb9{s8Q00-2hjCq8yj#=cD7 z;fs|?md;&7om#!r_P@L@{V0!^+a>C0N58Q1+ULWd0=I2h*-fnmgXkWtFe%jHW9Bj* z*>XhX=%o^bw2+3hH6z*IEV7O0*3cAGzM7t<;cvx+O>Ui=J#0DY;+|0bd%waM2A`oz zLN0c6nHHqix{w5?12YdpdYODI(Nyp#EM&~yToq0+2k+nxx-mb`iFrz$@k=|tU=`8| ziq@Elx19ftfI!)6vDKju8d!UHk^24jn?NI*j0LE5CN%s~UKWd+-DNn>cTMECeo*L36U**U)zt}`P75voNrE{-{V4szMWWFJ;p8vnK zLoa0dAoma2gx}sNgLjN7oDX0Q4iV-AA@KWSg7C-?kj8ZJ<3LB7rPgA_w(@qz{I5_Y zw&dYV-zZd}_ct=_077a0Oe$}lX4=fRKMa-;AQ*Zj-N!DNU+j51%`kmx;W%f)(@%-F zDSCLN1cvM0bhPaPqcyo1{MWe)XAtqQ2A5A+RSN7+yNAI-*vQmg3blxjW*asK!ibrQ4@z#)a~dx z@!H-1idciU=L5IDhY$#Zr*|Hd?2ezVnpe-6*c$E}wHf=^Y`V!D z_x$o_v`J{@W$bLAq)lp;F-~hUFC0KaRvh@NcA)M*{9+t}7;gegIM82{$w{fxt~Nw` za)*ldbe?9OaEYEvB-2FKefcN*f7A^O%;r+ESp{bgixymW?8-`lN_>J8v4ibuhtooF zWE1p#44tU6?bvnZxQ)LZq3dwK%!~nhq`Ux44M+5zgdcfq{)9XcCmx>`yD&G~G7VlJ z1Q@onhFI?-KYJi4QIlZj7VTa5QZlf2Ed4Z-MIwwv(&-2}}Q~bW^xX-2XCnJ0qDfo}l z6j5LK%4ekyr8#|!g}>ADdRECC)@e@5kqlEe{4AD3Ml2qm$2fezR_j4=7PnK^#j1Bb zbT$cIE{6;rkQWB*?5fftMd@6S#Z>0*1^iiIARAveKIpl5@P85I4z)v?&;T2KsOvzT!6J!BBb9|wBf*d*0V6@5bhBLPB{hWP@UE@edb)#lq_bJ z_z!%>JzN&j$~As!1A_PKGH&a?d9Ovp8gqKVCR)wIprIS|!n0M9e%FrCbpdyHCbb8m z^iBgF%22Y*CbA4ZY*z|z-ByP|dZq3V&hWZshsFD2I2s3CeO^yI4!6{G&%3*$ZEZAp)6Ib6g^bY+8@) z3!1~^C`2g%rGOVImwoA$yIoxTK5OjEaRBrQ*s61Qr zN_PO>8%LuZr?5rV@MF2Y{|=rajWX{A-r4ZxRM#cv=#UYO{a>JBVsw`@D$gu=eP_Jo zj|J-RQ8vm4VSMzUK6deyeBvr0|>N6#<=hU1mZX8_6p>TCet8=BV<4Hq-aSb^QlWCDDE8b@Bxk2-p` z>#>QN&04K1;vbfw%G^QVm74&dH0Qb@KYq%kCuG&w6AolX4uPDBPD^e=(YbGf;1xu= z{lVZ)*e*Q}uzo^w7?-JF-fIktTzXr7NPV|k>-6=zv_;5>Kmv;#p{H1S6Ew7Y*)$x_ zPICe`R3sprA9KG@@^5`BHRpT!`cYQimj0tz3VrrhNaQ{YY#6-0~7*a)Tax%bJmb>M6+` zWayL~Af=^O3)OYuFLB_%%>Vgdj08Yz_mBOo*71wj(IC?On1}GA{9$f?yH2Vdoy+|j zfBT(zOj7HY&)-jcuvBn;I3@Bod>xp|>dBfZEgDO**2eYX! z4xrbtK0It&gx~0mSa-}JTPqP*RuD+qy^8@jd%&~3M1Szs4^9*$qk;zvsoHF9@j+Zd zAe5dBgjzsyzR)lzmA?I<;Yv$Pp$BG4IT;h@(_X+IWD$X4YVFhOIMBVw`faFnZwvwB zxr4hHld@A6U!Lcx0_F)x&jkJGqdQ+y_#6#h(Oe8L&yszxPRKOw0cox1G&ky(yh4zx zF65o2%nmz{W7~EpF$9z`h2`rwyLdqZN;te{g=xAY54F4w;aa!F|M(!RH-sCDtr>NB zqtwE(D|ADTGIJQYY29!A=^A2G5F~tT?0q{NuP1}oo-(&L#|QF2=7H5e z>Uyh$1hv>@tx}NmcvG_aI< z%0S%*+R>)j`{$A|Qtupe&5uqS^o`UZWe*^9^w68Xku5@S$>?0@e;WZ-!RUra?)g3F zgG^B;1IyPY-xeBSEK_=7^am@u;1Vq|UhT;K5gPZ9BJ~fTw&bg8*1yLd zZr*h%f8Q+n$^B;%^GuRzfGm9tti0`q#WHL!7GWCf)UKuY7@!LDjMQCW(g{qHid#!vjn+@TF#vlC#f|_HqbY0c_^I&ACDY!^`QQEx-fi_AW+k z$=grWnZ`4TE9?=1h{AOp5gb<8?N3oE!Z#aQx3m34sYCirklv;`!~p9Myj^~}Ufe+| zzWNR$TBohN#BI>NTY3RINX9aNJ#<7hCweX@VK#BmMNzSI4vB?VTBB^mKONVC>UYc7U>3pEdq>@j7lWREA34lgCY!Zc+QGd+hQ&o zS5=w*?Uxyf-hM(Dxpc`udPg6rDeRw6TR(myVtN=OS-PoP`kmXiyHb9ND{F+QFTCb~ z-g9Jj;jrCqgXLLWj4C|IuqH+*>txKi7F=dgi1$;992`oe3)XX>YNs~x((~P)Xj6p& zPm_Ex7z(%)pTQ@-6VC`B&lqXa`%y#skE@l-EP^t&hDzzf!x3>1y~D?*aGR;h%kz40 zq#%UWWlod3{5wwJ(lW^BH8q%Nx!T`S#TefN_jnK)W~wW| zN^H4Z4i0Kky+QTc0*Ii?%VPqsw}5G<7bk4^k5(Twejd)h{Ww_M@_gX}X-fruA>I2O ztONTR#!2<^tx;tLh@)?#%)CC2vc$vtG`3^Nt1jG?kN=>2&mDoUaTn!3(`FORM~kU zwA(F~X#cSKaPSV|di*J@giY#)aI3$;tOBae^LaQfr|}Q#V9`^eu)#9HJ7>pr!G80v zQGa1A;xcA;N$EJ&JKE%pt>#BgMBGA}N>5U9w=smn&qwVS6 zex9M|*Pvh@oR~Gy%JWY^FdK`ckaCqQ963u0-m8Le&h2c16*7Z|ncLh)l);p*rNegWpU=!g~RW zAbFOfGoRyZHdxtPhq~3e0_XLfA_#@blYeQGQkhttY^^El})Df5bmeE<~ z3D_Dt4v9?Xdb-NA#ys^zDyw};%rEIKMSJDXH%3X@B}Sj^)iLqb!2TFZ<1+v(~_}#YfP8h z;Tij-O(fe<)d$@Xs=Uh#SWjE`6}_3s-3pB@vo}$LUx08KQCp(z3bR_4#|lxaD<+BH zD|@jxj#-!Va3j=O|64U0ehCT&JznXOH#K2&KvN%3?Z>J1AE00HZ#iD9{vim#kv03< zTG<(5)Q)I^ekkqURk`#d%63Wyw+=;{Q{N#LJ2K08GM(~am6yIQ8Z*8x+F?dlq|9pU za8Yp9X14C!qoRfUuRE1yxDn6$%GID7Yq$e8uLzg{@vnJkonQLVDOG(Hh+MQU{w;GK zA?+)tk!<&KcBsd%U{Y`DSnfNfvjIT{!ss#pR5_8o6lz5ag6CeO@vG+lq|D*=Q;ACRN5^sBr0O=zqP7Xf5Z7mS^GN7!`C4QWge$9 zRu1U{n7UL~y82nvc`hUu`?A5y)U2=By zlN;-Dv0Il|YXx}L=6+XfE!rIQ>=s&jg>`VKZBO#ugCdeT{8f3VODroK)RG^h#6G|C3F|=GN?p--@Fnhg}3Db}sDXxfD+i!+0(&zoxEs2;sbrI@J3KEY-!Oq4@9>Ypy35Ha!>HEJ znjf+AX~aXssvPJ9i>om0cGb2kFBq=mp!7EkW~s9diyC)`6B4tXM8|;H%}=*M7$wI1;rEJBd_EUUXAND%E3SlSL(a=y8v6hJ7Qk6OO?n>H z`iaYQRyWNAp%M0sqvo0n?sZq*C$@hHMwRYw#^}3|^#g*nOi+G5a>qC*h)`g!08NzI zS$Ku$nV2-PT-|qv(l{-UgBYy@W%Z`Sa6?i$ttK71Y)?l&=NpVZ*1VeDQFp!%`dzI) znQdDPk(8`eZrA2-G-02xj2I|s1VW)7xPJp&Cn1kZrg8_x^5ZBkg0C~cFz^!Dz#icQ zhT)z36!=?q)#|N<=^{lhMzbB<>i10!-xg5>zv&Iz>7!%;|9cqWZ$1?{Wtv2e58KC= zvUB^`?TG7z2TnNq<4IXP($>m`pke!tN&i}Q{;hMzSQ)%Z<=ZiewP8E(oF%Y(>l=L< zt$;%HF*q_A=E4i>Y?N+0AoV+W+J4l4XINHXIDy;kXEDYXYM+~=%DV^`H=$~v$LYi* z>cI+h;IGQ?_%b3x`+UzXrBU~Nc~b497> z!tb2}!*d69IZ|tWVu#qiQQ#f*2Y(n5Uh9Dh(|gr^S+e+P|Gh{o#b8~MDf2vlNm8+= zEf&fM>Bx(-VTh1u>_=zl-!v0xq9fLNpk}Ec7!h6=v0zy(gca2P!;W(45iU*jwHMEQ z&`68JSi2F(oR+S|Kp03Z-<|m`d$aZrC?1tpnKYSY!Z5z$RdnFvme6W-_3+vc@HMO} zB%FDw{a^oN8PrB*sBBVd;^g;6;z0lGnXjT)^5ATAm=7qQy)Xt$njp?LCUT`M8>1LQ zB~e_45|DW8jXs3y={_c(-me**rnH6OaH4*3d+|hS3~cgl@jdV z6X27*)tF_GExxZQT>j7&VNVRB)42Bd8W>(Gb>AUU@~#$E7%>mY=GOq)`IWNgE_Yy8 zi|uNIz#r7~XuiKpd~Y6smCkXibdc$}OY~-Cb7#mC1Uwy|*7 zlPG35=9K)~6Ll1Ax!uR<9GHO66&@GQ*s{xEh8W!H7aDWq_+T5o%})S8X&L_;h%j~R zb#kUkh=0#K1L@M`x21(~g9?caUctCjotQG4zWug`OLg#B#yez*PfPdo1x(UzkBmmO z|u)upT95bEZ4xEZt~D1UWB@2mk1gRH5`JS9sBsiv7VvaY#9id zvOqZ{D~`s|zQ{I+E`8rFV*4O>QE3!R3=T(Do7T`|xacJ$+_(Hx?gC>vZK`vw2Oq4d z>5n=Phj%{s`%Mx^Bv-v-#qODyjv-1FVbQdKO38+oQ^Ut~N zy-(0{4(aS0C84#u!r%9rs!V4^nqgYNvL$)l3>Ba}SDv}qed1}`qRCgM`Uh`En?0T_ zD>ZaQOc{b~XrmN-Y?>sL0gs93ID*jct8p8PE-B1XBUf7c|(-`_B1`ZngE z7$Vx{r%t0Y`{HkniI71>rGhZo!n8Y5e%YMg*S}1c+Z*NWLe0F4_y@Kz_4adycn|ds zd5LzU5!-81p>k}@-tBMRFu!>Y?J5dZb*)XFRGkvnnNv5d)n~`*c80ZD;7*ZKz~OX$ z94KGy&Wn>lAtm7O2e>K?xa>k<3^R}{JyD+)An#YZOGniDo$5wn&SxZ%e z0N!aG_dv&Sh}H9upbhg$jy7}fo`qbJV?kM_c?GobH?&unV;di=o4C@RuFe&8@x8nHvFNPle8^#L%v98KwFfed~hm=!9A+?KfK+ zscBit9c;k}-`vl5mPOa&?U3c8QHMH78gMF@ABciAuZd4fc!{Je>$4+kmImkdRH&bj zjMF+eR|%*FV)~^h&jz}>L$L#*n~aqkj#D)|0L3-90#Uy=z&+3V1|K7=y(5ejjA}ze z?mR~rJHoksY7tzMx>3!v_!9bhZb5HuwggUYbcRPaXdMudq{gws-&Ok_o+8m6seRT+ zRm2@1L+BX3RM+3jk&JPu-NN+J#&f^Zd;`|Z!cXj$8=R*f+8+GyogLj+73{{Wv}&#Y ziQXxuM~jOQ<&N2ggBgG38+g-g8x?tRHWuZ`(dF0eSEV1k{vs{=e*mCBU%#qOBkesW zn<;4CfC+2LgK{?e%avC+n9(lS^%dNa$H7JFli9v@)Ai-+rFJf2E^7!_yS9~#8fn@^qg{OW zA0FaA-`}pF!B6qV!;kStKbYa5;hleEG|sN?J;onB-owPDG`}x0SAmh1$pMZ53Gh6Ky5!XJDPUmPW#<>e^eLlng%`u5>o z_;XDCGVIdJ!Rpwm*}HfHY}7C`$`NnJf?L&t-KIwH} zIT6R6e3HL-VUYLQjbIX4)^1$EW$Rj5+g49&Lx$RPf@A=36frrOXK-+YQ>XhmaQGCj zA0K7Xoj)iz`qb0>dG;gx)(s83uLV3&iAlZ7QP?m???xoXI>-*}CWs5OR%q~D7d2pP zfN$PJxb>4-j(@=oXE4p{aJg1|e>=v{{)dSNqEbZAe~9>*KZP5A6T9MSoggxs!yuW# zuKy^}$z5>taV#*~hf{bDfsM%YzD9EBAo+BoYB$WsXV-XwUH3oEU)hiFYgg2&j)RC? z#AKIef;2o7x>n`QHn|#0GK$ki!VLdPyRgZVZtn1ufKv% z-@J;I*|U$wFp;3HK0#f59jn)@;O3jIpfGrvCm!3y{cnz(`}g++_W$5H?q6{`cQ3#A zsiZFy&-1+-mTcL=Rjsr0>>V^WlGNp@u;GFyD$x|VlaG-c@5EPVRf9DKn`^~yzZ+A( zOcT|Mk<8(m*5gjShV}W1oFd{J;2puq=W)*gxBo|M<7)Ne>|Mp!##IE{?jr7d6*DzL zC61%yNGb^>hT0|}|Yv}Caz*%1@8|v3A zX1N|2rsI>C-u-*H=l=a18b8kqC}XH!v6Pm9(;S&t0BJSKP_?->KK1iA z^YN9bi(X-qYFf(2KHbV?8(!p3zI&YBbF4t{(GZV3c!2AEWjo6*a=iOOk^DLPGr^iG zcXIb8y#kmo=F#oY6HfGy>wJbF8n4Ji)f(`|;Fe!Uu=W-$!N1sPW+IJUyd9D?SW~%} z(RqkVC49&8#LxU0e(-ojCcFz)n8L2Q9<%BOY$8>WrKqv}@8N)oRf)(2K5up&?PkcGeWJ8^-6Hh#cZn^WAK-`Ie3;+=r+pkePtah(96LY! zVgBR4+Rpm)!Wet~ukGQf!Sl39qM48X!Uy@p%1e2E3R!ab4gC8%mr`?{-=uW<01qB0 zUi1@^pE}RV29qG0(o%zY^;kb|CaO_^bkD1VW2f-WS7dHzt-+;gu^aEe)V6ALU92F5 zS+oI?wUFbr@l{a6({2 zPQJL8UDw^rHQF&F3+NiG+j1qpeM#5g!s91;*xPmX<+H)kC2Z0$6qnMv^d4$w?|Fg- zW0I`A>MH*A$JVlJfjw%w{*%1cckYKFlU~l9*ELSx-|b*F^e|fOtL(!^OgN_9&BM04n0DA^ht=c)_T?+gR{-pbsxf~>MJ%Dyobc#5mJ47 zFfLZ7RN+ThroNFaJ2vx~&)ms>{dc!=_vRWB?;{iP`BOak=A<^vh{$>6k9W}?pZ$Kc zbnRkP8*&Ek_%*HE@pE_bzumR)f(C%m6TO^1&o=Sw`c>?h=D<9;Y9*J~o_kRHQa^_} zFK+L8eyVit6^SN6M%yS9HdGxSlRNnY$?;P}6{^*cOr-JaK7w7kQ^f=?WrCT=U>9%2 zrgE4nxYu|O#mHlZjuAimXSkz31n<;QvvOet*kwEM>pqN4Br7r=;@Fd#I7R04bC@Wv zKB;oa#-)7X=Rd~pd~gxjiybdM=J4K=3@U99k#m14aO7z3*^e8vS+=aJLr;J+@o;Eq z-gp_m^KU-F&u*zicPnDkbgD!Fn{^RBo`A6MyI+skl(~j+DakkQJfg1<5WM#yGH057^80_ z&*;PyWBC%r$PvXJtR)NsGMN;$xeSdBHMA~jVCB*#)-G?NHKQr%+{_vV*?X*T){h~R zZDVVj`gPiG>B%~-`N$1??z%?O0R!bB7J2l4SIFt+1i0ZbB|1)3b}j&~bbW%<@ZT0~j0NR@^|Mb(2;#TyldN zO#KpkrUiyhWAIgxH@Fxg7ZaS?MfBWX6W;ViZ1Xw=4zqBD*=1J{ul^w6zHj34Q?ouE z;!;d<;1HRvmnqe+QJ2h#VUywN+qd)7p_kZu5j&rJG|15|$4%>WAD4G44EOTt>&JL$ ze#_RdR&t6;szxVg86nXKVzr&aIp6hf1!?IiMiyMd?q^J7G^zLG8!%yM^RW6zn zApOET}}_S0zkgEgYQ4_29NF^W@4INW_VYkzoUakIy(6N zqcv=~Vk@7zWj*U_H0r8+w#PtwH{EAFh9Y3?`c`KD^hLwU9jjUK%x+GcqpVQzcn7=3 z*YUv_UYO_eoxF79+{YlApotqUYq+qW!KT@8!?k?j!z*aj;U4ek?ejJo!3^b=fi;)# zef{`#G&NN^_wSWTl2%vGD|0?h563%64jm>Ws>q}?NF-p{<=CY=z^I<{wA@U?G`4XS zHj&0u*&~1KJf<*;J@6pzhzdr|&VaDkCEM}KuEH8K`-@3D81G09A0;tzTs1r9j0Bt3 zFS^KLk)EO9Nz}taG|p>3c#Qw!&!6Tyd#5jG_^}aJ9B1E)FYyO|`aSM>b?7|D!Aa$_ zh5`qU4srI{kd16xldi}Ui(Rye>zCPc{@K3N%gYDzGyaS`cJKuIis!i_uUx~m&F|ep zVf|7*^|?Fw-8&RC{ELTUa%p>>F&0xS&CDNNisJK*pL9BL;S)+*)RSMO*! zcd+Us4(~n1@J!y=e4f|d=sEZHA``OtvXvKbKQ^qm_9{NHF012Me@sAAWA;3S8uJW| z&&2bIXKeg@D+CRV_1TMfUXPfZI?o8uHVTCeRW$+ej`YBOQpG`H$Lt<;353?*TDM`B zTn52BE1g=&LJ2m&)U{y})p7jg*}h7V$M+s0dg31kZQYD-*+^i6${c~| zCrJz*(Ww-3Y1HJ>)L!I6OH_)qvSA?y4HI2_?eCuAp%aDkRDGYRKRlvq_w)R>dygn^ zs6wTaT^uT${qw0|Z5zuf_C#%H+O~$x=cyM_>geFL(U}|2e)J?SojCUjW0PLWjaxF> zo_2}-+_t2F3)BpM9OZ1-avnkm^$Z3A(c)lXj0fvJrcw1 zBnJ;-XuTP$P zD@D}DEBhyBXk2>+_MPBxbna)m+RbaZETd6$snRRkSR0&sjGk`qVQ9KLIUeN5>GN!R z$h5NMVoqW4B__|a138^mF;ImK)xtZH{d-9cA0u|N|A}m^C2m@SZQBOHJlj+jCLxg_ z+VgFq7w*9io|yf>Axz_HT5wfYwc#DNbq78!5gdI2cknTMVN!RLnUqKoEW3=jVKt#; z*2je_#iU1%ks8>qzL+Z?6U7o2TU8}Y1PUk?=4fHywO6?BNTKpkqIYz?^aA%D)|Q^? z`M@7#@6of@b1{h)wyv!3bD&9a+0LcsJ{r=;9NBl0fteUkZk$)%?B|>{+HIDrFKl8Kk><9Jl{9Hom@EH!Jf%;uSBTZw2Dm^a|(+uFBcBk;M3TI3GA91F>RaY->9%5cFA^vRoCKQ_$%VBJ(%mhfL(qWCRIB- z-$OEsZCVW`1Yf;15u67%)`xlZtGJq0f-N5fs|8>)bJ1DM;!T(pS7CbJD4$0zHML;&773R49}M#t&y?&rYa4qiGi!0?4`k8l$_`~CggvhFh0=p&`#{Af4( z`p$kc36{07sR}+0GV9lHMa>DG9y|LWj!$*4Yj_j4H%{*z&JA?%(s|x|+oo0AxKe|7 zdY*%_TyyOTzTJL|v+auU@gR>r(ZL6PcGdJ8b?PU0{@D`@yK_C3)p7GR7a1f)Me^~v zKZD;pZ16-=1%}7+j7>$9A_qZ0G7*x^WofKUUSwftQP^;fffkOPA~|#f#x<@} zhgA9JME~g_#x81z7@8Y%TG_AwO&&VJiz62}SWJoyH(kdUZe2m^Io{}*ARv>=lF4Oh zThhwqS8U~{hfnjsx1Q(0<5L$@4N1IUd2y|IglgM@M$t_nx1+x4Y5EIp;8# z0cJ3PLr!82!yrnsNJ*9~Sq{&#to4&^?Q=Nx^V@yLZ`beHdmLn0>mW-yPAe-|G9^+H zGdWDd;p6~d26>P(&;UB#d#m=39*PMKB6K%Wea>m}FieN4->s_Ow|;@lrWTHut!&3s zp~#Y5cT!W|y{Jm&#zwh#ZG`r|3HnBTjQj1FYp5{YWYf=GCuY|)r!|96}DoF4Z<=d__>UKA9-?2t+DNWI~xE?0EIS-Kz?&&U4 zLX$X-N^Tbl1HF3>x?%vudbCWmh+Td~mamg11hgCA<9W|>z#;l4rI^-{CW zTGSM+eT$1jBCVL_(v7obxXNoeR1n)ZosoXdO~+59cZRQXW-|8sxvOhAl&h7pc;hZ@ z=5r5~$8_|Sv%u-cpW=_cb%hI)){6GQ@XQELeeX&BukZK7?204IOm6#`Biy=*cCRWF zA$CtMH)$yjW9=9DtFJuHpFY;c`LX2-4U2xB9iaQ-CBFLiKje@8y`A(Zw5 zfkQL6r@JwnwHe^5KHC5%y%cv@OJZ;KHwYpo+9iA#`C>9>BU*jcdG(h@IT% z2Bg9>)q!ha6f3(5^gZ~-!H@o*& zlB2iCL`*cr2XE!Kf-muDUAVA*QG27#tp_y>pNYy>s}N*gJ)p&dndbi_h#@T@;`SFO^2&(D~fc zyf)|~6z8Lzk`V?jo#W5D2RQ!WV|=tJ1D7@=EjGl$Gu?%Eq77vwH-|z1h7ic2rf3?$ zc5pI;fl=0s(eh!$`EMhG3&@C#F?${L;+Jp+&tu&7Yf;DAjIa12++Jix31VTA&7d|Y zz;PUm@eZulzJ~jrFJk0Z$Jb5*GpI-aky!+$n{A0OFv?pnN_WFVdva|8tsPTwiaa2vKGqqq3s0IseVu_Gaj zgP*``yc6M$`v1M=j_C8%8S?b%_IGNoSTc?5;41}mOVOBEWwzR>^;nXtE%FeXV3GK z))^KOa1Sd|GTD7_50BnaOU+t@RuqAdweIJ6C4*dk`UU=4-NSF)mZ#gYYOɱ*OO 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 From 953f0f0760ecf82dbe67a7b196114a17b8d75f19 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sat, 3 Oct 2020 14:14:05 +0100 Subject: [PATCH 002/116] Added histogram plotter --- .../pulse/search/statistics/AICStatistic.java | 12 ++-- .../search/statistics/AbsoluteDeviations.java | 15 ++++- .../statistics/AndersonDarlingTest.java | 2 +- .../java/pulse/search/statistics/KSTest.java | 2 +- .../search/statistics/RangePenalisedSSR.java | 2 +- .../search/statistics/ResidualStatistic.java | 4 +- .../pulse/search/statistics/SumOfSquares.java | 9 ++- .../java/pulse/ui/components/AuxPlotter.java | 57 ++++++++++++++++++ .../{AuxChart.java => PulseChart.java} | 56 ++++++----------- .../pulse/ui/components/ResidualsChart.java | 49 +++++++++++++++ .../ui/components/panels/ChartToolbar.java | 35 ++++++++--- .../java/pulse/ui/frames/AuxGraphFrame.java | 47 --------------- .../pulse/ui/frames/ExternalGraphFrame.java | 38 ++++++++++++ .../java/pulse/ui/frames/HistogramFrame.java | 37 ++++++++++++ .../pulse/ui/frames/InternalGraphFrame.java | 39 ++++++++++++ .../ui/frames/ProblemStatementFrame.java | 18 ++++-- .../pulse/ui/frames/TaskControlFrame.java | 6 +- .../resources/ResultFormatDescription.html | 17 ------ src/main/resources/images/pdf.png | Bin 0 -> 28126 bytes 19 files changed, 312 insertions(+), 133 deletions(-) create mode 100644 src/main/java/pulse/ui/components/AuxPlotter.java rename src/main/java/pulse/ui/components/{AuxChart.java => PulseChart.java} (67%) create mode 100644 src/main/java/pulse/ui/components/ResidualsChart.java delete mode 100644 src/main/java/pulse/ui/frames/AuxGraphFrame.java create mode 100644 src/main/java/pulse/ui/frames/ExternalGraphFrame.java create mode 100644 src/main/java/pulse/ui/frames/HistogramFrame.java create mode 100644 src/main/java/pulse/ui/frames/InternalGraphFrame.java delete mode 100644 src/main/resources/ResultFormatDescription.html create mode 100644 src/main/resources/images/pdf.png diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 9e6fe0bb..c045979d 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -1,5 +1,7 @@ package pulse.search.statistics; +import static java.lang.Math.PI; +import static java.lang.Math.log; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; @@ -14,14 +16,16 @@ public class AICStatistic extends SumOfSquares { private int kq; - private final static double PENALISATION_FACTOR = Math.log(2.0 * Math.PI) + 1.0; + private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); @Override public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); + kq = t.alteredParameters().size(); //number of variables super.evaluate(t); - double n = getResiduals().size(); - final double stat = n * Math.log((double)getStatistic().getValue()) + 2.0 * (kq + 1) + n * PENALISATION_FACTOR; + final double n = getResiduals().size(); //sample size + final double ssr = (double)getStatistic().getValue(); //sum of squared residuals divided by n + //TODO check formula! SSR = divided by n + final double stat = n * log(ssr) + 2.0 * (kq + 1) + n * PENALISATION_FACTOR; this.setStatistic(derive(OPTIMISER_STATISTIC, stat)); } diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 472c47d9..eb8ad3ae 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -1,13 +1,24 @@ package pulse.search.statistics; -import static java.lang.Math.*; +import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; import pulse.tasks.SearchTask; -public class AbsoluteDeviations extends ResidualStatistic { +/** + * A statistical optimality criterion relying on absolute deviations or the L1 norm condition. Similar to the least squares technique, + * it attempts to find a function which closely approximates a set of data. However, unlike the L2 norm, it is much more robust to + * data outliers. + * + */ +public class AbsoluteDeviations extends ResidualStatistic { + + /** + * Calculates the L1 norm statistic, which simply sums up the absolute values of residuals. + */ + @Override public void evaluate(SearchTask t) { calculateResiduals(t); diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index bcfd2aba..833580a9 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -16,7 +16,7 @@ public class AndersonDarlingTest extends NormalityTest { public boolean test(SearchTask task) { calculateResiduals(task); - double[] residuals = super.transformResiduals(task); + double[] residuals = super.transformResiduals(); var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); var testResult = GofStat.andersonDarling(residuals, nd); diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index 9496cbb8..7831d7fb 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -25,7 +25,7 @@ public boolean test(SearchTask task) { @Override public void evaluate(SearchTask t) { calculateResiduals(t); - residuals = transformResiduals(t); + residuals = transformResiduals(); final double sd = (new StandardDeviation()).evaluate(residuals); nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical diff --git a/src/main/java/pulse/search/statistics/RangePenalisedSSR.java b/src/main/java/pulse/search/statistics/RangePenalisedSSR.java index 2c62048b..3b8c8fd0 100644 --- a/src/main/java/pulse/search/statistics/RangePenalisedSSR.java +++ b/src/main/java/pulse/search/statistics/RangePenalisedSSR.java @@ -16,7 +16,7 @@ public void evaluate(SearchTask t) { final double n0 = t.getExperimentalCurve().actualNumPoints(); incrementStatistic( - (n0 - n) / n0 * (new StandardDeviation().evaluate(transformResiduals(t))) * PENALISATION_FACTOR); + (n0 - n) / n0 * (new StandardDeviation().evaluate(transformResiduals())) * PENALISATION_FACTOR); } @Override diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index ebb49011..da87effe 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -27,8 +27,8 @@ public ResidualStatistic() { setPrefix("Residuals"); } - public double[] transformResiduals(SearchTask task) { - return task.getResidualStatistic().getResiduals().stream().map(doubleArray -> doubleArray[1]) + public double[] transformResiduals() { + return getResiduals().stream().map(doubleArray -> doubleArray[1]) .mapToDouble(Double::doubleValue).toArray(); } diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index 3c9bdd25..af5da4ff 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -5,6 +5,11 @@ import pulse.tasks.SearchTask; +/** + * The standard optimality criterion of the L2 norm condition, or simply ordinary least squares. + * + */ + public class SumOfSquares extends ResidualStatistic { /** @@ -25,9 +30,7 @@ public class SumOfSquares extends ResidualStatistic { * baseline-subtracted temperature list. The value is interpolated using * the experimental time ti and the nearest * solution points to that time. The accuracy of this interpolation depends on - * the number of points. The boundaries of the summation are set by the - * {@code curve.getFittingStartIndex()} and {@code curve.getFittingEndIndex()} - * methods. + * the number of points. * * @param t The task containing the reference and calculated curves */ diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java new file mode 100644 index 00000000..57750040 --- /dev/null +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -0,0 +1,57 @@ +package pulse.ui.components; + +import static java.awt.Color.white; + +import java.awt.Font; + +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.XYPlot; + +public abstract class AuxPlotter { + + private ChartPanel chartPanel; + private JFreeChart chart; + private XYPlot plot; + + public AuxPlotter(String xLabel, String yLabel) { + createChart(xLabel, yLabel); + + plot = chart.getXYPlot(); + plot.setBackgroundPaint(white); + setFonts(); + + chart.removeLegend(); + chartPanel = new ChartPanel(chart); + } + + public void setFonts() { + var fontLabel = new Font("Arial", Font.PLAIN, 20); + var fontTicks = new Font("Arial", Font.PLAIN, 16); + var plot = getPlot(); + plot.getDomainAxis().setLabelFont(fontLabel); + plot.getDomainAxis().setTickLabelFont(fontTicks); + plot.getRangeAxis().setLabelFont(fontLabel); + plot.getRangeAxis().setTickLabelFont(fontTicks); + } + + public abstract void createChart(String xLabel, String yLabel); + public abstract void plot(T t); + + public ChartPanel getChartPanel() { + return chartPanel; + } + + public JFreeChart getChart() { + return chart; + } + + public XYPlot getPlot() { + return plot; + } + + public void setChart(JFreeChart chart) { + this.chart = chart; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/AuxChart.java b/src/main/java/pulse/ui/components/PulseChart.java similarity index 67% rename from src/main/java/pulse/ui/components/AuxChart.java rename to src/main/java/pulse/ui/components/PulseChart.java index 1ca5eb25..cd31f3e2 100644 --- a/src/main/java/pulse/ui/components/AuxChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -2,7 +2,6 @@ import static java.awt.Color.RED; import static java.awt.Color.black; -import static java.awt.Color.white; import static java.awt.Font.PLAIN; import static java.util.Objects.requireNonNull; import static org.jfree.chart.plot.PlotOrientation.VERTICAL; @@ -12,11 +11,8 @@ import java.awt.Font; import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.block.BlockBorder; -import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYDifferenceRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.chart.ui.RectangleAnchor; @@ -25,48 +21,33 @@ import org.jfree.data.xy.XYSeriesCollection; import pulse.problem.statements.Problem; +import pulse.problem.statements.Pulse; -public class AuxChart { - - private ChartPanel chartPanel; - private JFreeChart chart; - private XYPlot plot; - - private final static int NUM_PULSE_POINTS = 100; +public class PulseChart extends AuxPlotter { + private final static int NUM_PULSE_POINTS = 200; private final static double TO_MILLIS = 1E3; - public AuxChart() { - chart = ChartFactory.createScatterPlot("", "Time (ms)", "Laser Power (a. u.)", null, VERTICAL, true, true, - false); - - plot = chart.getXYPlot(); - plot.setBackgroundPaint(white); - setFonts(); + public PulseChart(String xLabel, String yLabel) { + super(xLabel,yLabel); setRenderer(); setLegendTitle(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); } private void setRenderer() { var rendererPulse = new XYDifferenceRenderer(new Color(0.0f, 0.2f, 0.8f, 0.1f), Color.red, false); rendererPulse.setSeriesPaint(0, RED); rendererPulse.setSeriesStroke(0, new BasicStroke(3.0f)); - plot.setRenderer(rendererPulse); + getPlot().setRenderer(rendererPulse); } - - private void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 16); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); + + @Override + public void createChart(String xLabel, String yLabel) { + setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); } private void setLegendTitle() { + var plot = getPlot(); var lt = new LegendTitle(plot); lt.setItemFont(new Font("Dialog", PLAIN, 16)); lt.setBackgroundPaint(new Color(200, 200, 255, 100)); @@ -77,16 +58,17 @@ private void setLegendTitle() { plot.addAnnotation(ta); } - public void plot(Problem problem) { - requireNonNull(problem); - + @Override + public void plot(Pulse pulse) { + requireNonNull(pulse); + + var problem = (Problem) pulse.getParent(); double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); var pulseDataset = new XYSeriesCollection(); - pulseDataset.addSeries(series(problem, startTime)); - plot.setDataset(0, pulseDataset); + getPlot().setDataset(0, pulseDataset); } private static XYSeries series(Problem problem, double startTime) { @@ -111,8 +93,4 @@ private static XYSeries series(Problem problem, double startTime) { return series; } - public ChartPanel getChartPanel() { - return chartPanel; - } - } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java new file mode 100644 index 00000000..26a0aa53 --- /dev/null +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -0,0 +1,49 @@ +package pulse.ui.components; + +import static java.util.Objects.requireNonNull; +import static org.jfree.chart.plot.PlotOrientation.VERTICAL; + +import org.jfree.chart.ChartFactory; +import org.jfree.data.statistics.HistogramDataset; +import org.jfree.data.statistics.HistogramType; + +import pulse.search.statistics.ResidualStatistic; + +public class ResidualsChart extends AuxPlotter { + + private int binCount; + + public ResidualsChart(String xLabel, String yLabel) { + super(xLabel, yLabel); + binCount = 32; + } + + @Override + public void createChart(String xLabel, String yLabel) { + setChart( ChartFactory.createHistogram("", xLabel, yLabel, null, VERTICAL, true, true, false) ); + } + + @Override + public void plot(ResidualStatistic stat) { + requireNonNull(stat); + + var pulseDataset = new HistogramDataset(); + pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); + + var residuals = stat.transformResiduals(); + + if(residuals.length > 0) + pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); + + getPlot().setDataset(0, pulseDataset); + } + + public int getBinCount() { + return binCount; + } + + public void setBinCount(int binCount) { + this.binCount = binCount; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 297ead37..3299d137 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -33,7 +33,9 @@ import pulse.input.Range; import pulse.tasks.TaskManager; +import pulse.ui.components.ResidualsChart; import pulse.ui.components.listeners.PlotRequestListener; +import pulse.ui.frames.HistogramFrame; @SuppressWarnings("serial") public class ChartToolbar extends JPanel { @@ -54,9 +56,30 @@ public void initComponents() { var upperLimitField = new JFormattedTextField(new NumberFormatter()); var limitRangeBtn = new JButton(); - var adiabaticSolutionBtn = new JToggleButton(); - var residualsBtn = new JToggleButton(); + var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE)); + var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE)); + var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE)); + pdfBtn.setToolTipText("Residuals Histogram"); + var instance = TaskManager.getManagerInstance(); + + var residualsChart = new ResidualsChart("Residual value", "Frequency"); + var chFrame = new HistogramFrame(residualsChart, 450, 450); + + pdfBtn.addActionListener(e -> { + + var task = instance.getSelectedTask(); + + if(task != null && task.getResidualStatistic() != null) { + + chFrame.setLocationRelativeTo(null); + chFrame.setVisible(true); + chFrame.plot(task.getResidualStatistic()); + + } + + } ); + var gbc = new GridBagConstraints(); gbc.fill = BOTH; gbc.weightx = 0.25; @@ -100,8 +123,6 @@ public void focusLost(FocusEvent e) { }; - var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(event -> { var t = instance.getSelectedTask(); var expCurve = t.getExperimentalCurve(); @@ -149,18 +170,16 @@ public void focusLost(FocusEvent e) { add(limitRangeBtn, gbc); adiabaticSolutionBtn.setToolTipText("Sanity check (original adiabatic solution)"); - adiabaticSolutionBtn.setIcon(loadIcon("parker.png", ICON_SIZE)); adiabaticSolutionBtn.addActionListener(e -> { getChart().setZeroApproximationShown(adiabaticSolutionBtn.isSelected()); notifyPlot(); }); - gbc.weightx = 0.125; + gbc.weightx = 0.08; add(adiabaticSolutionBtn, gbc); residualsBtn.setToolTipText("Plot residuals"); - residualsBtn.setIcon(loadIcon("residuals.png", ICON_SIZE)); residualsBtn.setSelected(true); residualsBtn.addActionListener(e -> { @@ -168,8 +187,8 @@ public void focusLost(FocusEvent e) { notifyPlot(); }); - gbc.weightx = 0.125; add(residualsBtn, gbc); + add(pdfBtn, gbc); } public void addPlotRequestListener(PlotRequestListener plotRequestListener) { diff --git a/src/main/java/pulse/ui/frames/AuxGraphFrame.java b/src/main/java/pulse/ui/frames/AuxGraphFrame.java deleted file mode 100644 index e0f8b82a..00000000 --- a/src/main/java/pulse/ui/frames/AuxGraphFrame.java +++ /dev/null @@ -1,47 +0,0 @@ -package pulse.ui.frames; - -import static java.awt.BorderLayout.CENTER; - -import javax.swing.JInternalFrame; - -import pulse.tasks.TaskManager; -import pulse.ui.components.AuxChart; - -@SuppressWarnings("serial") -public class AuxGraphFrame extends JInternalFrame { - - private static AuxChart chart; - private static AuxGraphFrame instance = new AuxGraphFrame(); - - private AuxGraphFrame() { - super("Laser Pulse", true, false, true, true); - initComponents(); - setVisible(true); - } - - private void initComponents() { - chart = new AuxChart(); - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - - chartPanel.setMaximumDrawHeight(2000); - chartPanel.setMaximumDrawWidth(2000); - chartPanel.setMinimumDrawWidth(10); - chartPanel.setMinimumDrawHeight(10); - } - - public void plot() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) - chart.plot(task.getProblem()); - } - - public static AuxChart getChart() { - return chart; - } - - public static AuxGraphFrame getInstance() { - return instance; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/ExternalGraphFrame.java b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java new file mode 100644 index 00000000..cc0ddc7c --- /dev/null +++ b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java @@ -0,0 +1,38 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.CENTER; + +import java.awt.Dimension; + +import javax.swing.JFrame; +import javax.swing.WindowConstants; + +import pulse.ui.components.AuxPlotter; + +@SuppressWarnings("serial") +public class ExternalGraphFrame extends JFrame { + + private AuxPlotter chart; + + public ExternalGraphFrame(String name, AuxPlotter chart, final int width, final int height) { + super(name); + this.chart = chart; + initComponents(chart); + this.setSize(new Dimension(width, height)); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java new file mode 100644 index 00000000..a9369cb1 --- /dev/null +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -0,0 +1,37 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.SOUTH; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JSlider; + +import pulse.search.statistics.ResidualStatistic; +import pulse.tasks.TaskManager; +import pulse.ui.components.AuxPlotter; +import pulse.ui.components.ResidualsChart; + +@SuppressWarnings("serial") +public class HistogramFrame extends ExternalGraphFrame { + + public HistogramFrame(AuxPlotter chart, int width, int height) { + super("Residuals PDF", chart, width, height); + this.getChart().getChartPanel().setBorder(BorderFactory.createRaisedSoftBevelBorder()); + var slider = new JSlider(8, 100, 20); + var panel = new JPanel(); + var info = new JLabel("Number of bins: " + ((ResidualsChart)chart).getBinCount()); + panel.add(info); + panel.add(new JSeparator()); + panel.add(slider); + panel.setBorder(BorderFactory.createRaisedSoftBevelBorder()); + getContentPane().add(panel, SOUTH); + slider.addChangeListener(e -> { + ((ResidualsChart)chart).setBinCount(slider.getValue()); + plot(TaskManager.getManagerInstance().getSelectedTask().getResidualStatistic() ); + info.setText("Number of bins: " + slider.getValue()); + }); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/InternalGraphFrame.java b/src/main/java/pulse/ui/frames/InternalGraphFrame.java new file mode 100644 index 00000000..b5751b4c --- /dev/null +++ b/src/main/java/pulse/ui/frames/InternalGraphFrame.java @@ -0,0 +1,39 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.CENTER; + +import javax.swing.JInternalFrame; + +import pulse.ui.components.AuxPlotter; + +@SuppressWarnings("serial") +public class InternalGraphFrame extends JInternalFrame { + + private AuxPlotter chart; + + public InternalGraphFrame(String name, AuxPlotter chart) { + super(name, true, false, true, true); + this.chart = chart; + initComponents(chart); + setVisible(true); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + + chartPanel.setMaximumDrawHeight(2000); + chartPanel.setMaximumDrawWidth(2000); + chartPanel.setMinimumDrawWidth(10); + chartPanel.setMinimumDrawHeight(10); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 5d56a91a..7724d100 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -48,23 +48,26 @@ import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; +import pulse.problem.statements.Pulse; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; import pulse.ui.components.PropertyHolderTable; +import pulse.ui.components.PulseChart; import pulse.ui.components.buttons.LoaderButton; import pulse.ui.components.controllers.ProblemListCellRenderer; import pulse.ui.components.panels.SettingsToolBar; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { - + + private InternalGraphFrame pulseFrame; + private PropertyHolderTable problemTable, schemeTable; private SchemeSelectionList schemeSelectionList; private ProblemList problemList; - + private final static int LIST_FONT_SIZE = 12; - private final static List knownProblems = instancesOf(Problem.class); /** @@ -141,6 +144,8 @@ public ProblemStatementFrame() { var instance = getManagerInstance(); + pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); + // simulate btn listener btnSimulate.addActionListener((ActionEvent arg0) -> { @@ -165,7 +170,7 @@ public ProblemStatementFrame() { e.printStackTrace(); } MainGraphFrame.getInstance().plot(); - AuxGraphFrame.getInstance().plot(); + pulseFrame.plot( instance.getSelectedTask().getProblem().getPulse() ); problemTable.updateTable(); schemeTable.updateTable(); }); @@ -507,4 +512,9 @@ public SchemeSelectionList() { } + + public InternalGraphFrame getPulseFrame() { + return pulseFrame; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 7c434da6..66c0457b 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -45,7 +45,6 @@ public class TaskControlFrame extends JFrame { private static PreviewFrame previewFrame; private static ResultFrame resultsFrame; private static MainGraphFrame graphFrame; - private static AuxGraphFrame auxGraphFrame; private static LogFrame logFrame; private static PulseMainMenu mainMenu; @@ -171,7 +170,6 @@ private void initComponents() { previewFrame = new PreviewFrame(); taskManagerFrame = new TaskManagerFrame(); graphFrame = MainGraphFrame.getInstance(); - auxGraphFrame = AuxGraphFrame.getInstance(); problemStatementFrame = new ProblemStatementFrame(); @@ -183,7 +181,7 @@ private void initComponents() { resizeQuadrants(); desktopPane.add(taskManagerFrame); - desktopPane.add(auxGraphFrame); + desktopPane.add(problemStatementFrame.getPulseFrame()); desktopPane.add(graphFrame); desktopPane.add(previewFrame); desktopPane.add(logFrame); @@ -247,7 +245,7 @@ private void doResize() { resizeQuadrants(); break; case PROBLEM: - resizeTriplet(problemStatementFrame, auxGraphFrame, graphFrame); + resizeTriplet(problemStatementFrame, problemStatementFrame.getPulseFrame(), graphFrame); break; case SEARCH: resizeFull(searchOptionsFrame); diff --git a/src/main/resources/ResultFormatDescription.html b/src/main/resources/ResultFormatDescription.html deleted file mode 100644 index c739093f..00000000 --- a/src/main/resources/ResultFormatDescription.html +++ /dev/null @@ -1,17 +0,0 @@ -

Please add or remove the following characters in the textfield below to include or remove specific numeric properties from the result table.

- - - - - - - - - - - - - - - -
D
 thermal diffusivity
S
 specific heat (constant pressure)
T
initial temperature
B
heat losses
M
maximum heating
N
diathermic coefficient
R
density
C
thermal conductivity
E
integral emissivity
U
baseline intercept
V
baseline slope
I
task identifier
K
Test statistic
\ No newline at end of file diff --git a/src/main/resources/images/pdf.png b/src/main/resources/images/pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..05328f67d331ad602574bd4a7bbc6ace23f7e5d6 GIT binary patch literal 28126 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 DiJgr= literal 0 HcmV?d00001 From 9ad85c9b39643dee89bd17040afdcf9ce0d0b3a8 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sat, 3 Oct 2020 21:26:31 +0100 Subject: [PATCH 003/116] Introduced the Bayesian Information Criterion (BIC) --- .../pulse/search/statistics/AICStatistic.java | 28 +++------------ .../pulse/search/statistics/BICStatistic.java | 22 ++++++++++++ .../statistics/ModelSelectionCriterion.java | 36 +++++++++++++++++++ 3 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 src/main/java/pulse/search/statistics/BICStatistic.java create mode 100644 src/main/java/pulse/search/statistics/ModelSelectionCriterion.java diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index c045979d..d0aea298 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -1,32 +1,16 @@ package pulse.search.statistics; -import static java.lang.Math.PI; -import static java.lang.Math.log; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; - /** * AIC algorithm: Banks, H. T., & Joyner, M. L. (2017). Applied Mathematics * Letters, 74, 33–45. doi:10.1016/j.aml.2017.05.005 * */ -public class AICStatistic extends SumOfSquares { - - private int kq; - private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); +public class AICStatistic extends ModelSelectionCriterion { @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of variables - super.evaluate(t); - final double n = getResiduals().size(); //sample size - final double ssr = (double)getStatistic().getValue(); //sum of squared residuals divided by n - //TODO check formula! SSR = divided by n - final double stat = n * log(ssr) + 2.0 * (kq + 1) + n * PENALISATION_FACTOR; - this.setStatistic(derive(OPTIMISER_STATISTIC, stat)); + public double penalisingTerm(final int kq, final int n) { + return 2.0 * (kq + 1); } @Override @@ -34,8 +18,4 @@ public String getDescriptor() { return "Akaike Information Criterion (AIC)"; } - public int getNumVariables() { - return kq; - } - -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java new file mode 100644 index 00000000..c762a7da --- /dev/null +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -0,0 +1,22 @@ +package pulse.search.statistics; + +import static java.lang.Math.log; + +/** + * Bayesian Information Criterion (BIC) algorithm formulated for the Gaussian distribution of residuals. + * + */ + +public class BICStatistic extends ModelSelectionCriterion { + + @Override + public double penalisingTerm(final int kq, final int n) { + return (kq + 1)*log(n); + } + + @Override + public String getDescriptor() { + return "Bayesian Information Criterion (BIC)"; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java new file mode 100644 index 00000000..de2b1a74 --- /dev/null +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -0,0 +1,36 @@ +package pulse.search.statistics; + +import static java.lang.Math.PI; +import static java.lang.Math.log; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; + +import pulse.tasks.SearchTask; + +public abstract class ModelSelectionCriterion extends SumOfSquares { + + private int kq; + private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); + + @Override + public void evaluate(SearchTask t) { + kq = t.alteredParameters().size(); //number of variables + super.evaluate(t); + final int n = getResiduals().size(); //sample size + final double ssr = (double)getStatistic().getValue(); //sum of squared residuals divided by n + final double stat = n * log(ssr) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; + this.setStatistic(derive(OPTIMISER_STATISTIC, stat)); + } + + public abstract double penalisingTerm(int k, int n); + + @Override + public String getDescriptor() { + return "Akaike Information Criterion (AIC)"; + } + + public int getNumVariables() { + return kq; + } + +} \ No newline at end of file From e7619576005c1f8d2c0edbdc2b61c80bbd354638 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 9 Oct 2020 11:35:10 +0100 Subject: [PATCH 004/116] Initial commit to introduce a set of changes. - Moved to WebLaF; - Minor changes to UI including Charts; - Model selector and refactoring of statistical toolkit. --- pom.xml | 5 + src/main/java/pulse/AbstractData.java | 7 + src/main/java/pulse/HeatingCurve.java | 7 + src/main/java/pulse/baseline/Baseline.java | 2 + .../java/pulse/baseline/FlatBaseline.java | 5 + .../java/pulse/baseline/LinearBaseline.java | 6 + .../pulse/baseline/SinusoidalBaseline.java | 14 +- .../pulse/input/InterpolationDataset.java | 2 +- src/main/java/pulse/input/Range.java | 15 +- .../pulse/problem/laser/DiscretePulse.java | 2 +- .../pulse/problem/laser/DiscretePulse2D.java | 2 +- .../laser/ExponentiallyModifiedGaussian.java | 14 + .../problem/laser/PulseTemporalShape.java | 2 + .../pulse/problem/laser/RectangularPulse.java | 5 + .../pulse/problem/laser/TrapezoidalPulse.java | 11 + .../problem/schemes/DifferenceScheme.java | 6 + .../problem/schemes/DistributedDetection.java | 4 +- .../schemes/rte/RadiativeTransferSolver.java | 2 +- .../rte/dom/DiscreteOrdinatesMethod.java | 2 +- .../schemes/rte/dom/Discretisation.java | 2 +- .../schemes/rte/dom/ODEIntegrator.java | 2 +- .../schemes/rte/dom/PhaseFunction.java | 2 +- .../NonscatteringAnalyticalDerivatives.java | 2 +- .../NonscatteringDiscreteDerivatives.java | 2 +- .../schemes/solvers/ADILinearisedSolver.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 2 +- .../solvers/ExplicitTranslucentSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 2 +- .../solvers/ImplicitDiathermicSolver.java | 7 +- .../solvers/ImplicitTranslucentSolver.java | 4 +- .../schemes/solvers/MixedCoupledSolver.java | 2 +- .../problem/statements/AdiabaticSolution.java | 1 + .../problem/statements/ClassicalProblem.java | 14 +- .../statements/ClassicalProblem2D.java | 17 +- .../problem/statements/CoreShellProblem.java | 17 +- .../problem/statements/DiathermicMedium.java | 71 ++--- .../problem/statements/NonlinearProblem.java | 12 +- .../statements/ParticipatingMedium.java | 13 +- .../statements/PenetrationProblem.java | 31 +- .../pulse/problem/statements/Problem.java | 21 +- .../java/pulse/problem/statements/Pulse.java | 10 + .../pulse/problem/statements/Pulse2D.java | 12 + .../AbsorptionModel.java | 6 +- .../BeerLambertAbsorption.java | 2 +- .../model/DiathermicProperties.java | 60 ++++ .../ExtendedThermalProperties.java | 2 +- .../{penetration => model}/Insulator.java | 2 +- .../{penetration => model}/SpectralRange.java | 2 +- .../{ => model}/ThermalProperties.java | 7 +- .../{ => model}/ThermoOpticalProperties.java | 2 +- .../problem/statements/package-info.java | 3 +- .../properties/NumericPropertyKeyword.java | 14 +- .../pulse/search/direction/ActiveFlags.java | 4 +- .../pulse/search/direction/PathOptimiser.java | 3 +- .../pulse/search/statistics/AICStatistic.java | 17 ++ .../search/statistics/AbsoluteDeviations.java | 21 +- .../pulse/search/statistics/BICStatistic.java | 17 ++ .../search/statistics/CorrelationTest.java | 2 +- .../statistics/ModelSelectionCriterion.java | 91 +++++- .../search/statistics/OptimiserStatistic.java | 26 ++ .../pulse/search/statistics/RSquaredTest.java | 7 +- .../search/statistics/RangePenalisedSSR.java | 27 -- .../search/statistics/ResidualStatistic.java | 18 +- .../pulse/search/statistics/SumOfSquares.java | 24 +- src/main/java/pulse/tasks/Calculation.java | 270 +++++++++++++++++ src/main/java/pulse/tasks/SearchTask.java | 274 ++++++----------- src/main/java/pulse/tasks/TaskManager.java | 11 +- .../tasks/listeners/TaskRepositoryEvent.java | 12 + .../listeners/TaskRepositoryListener.java | 2 +- .../tasks/listeners/TaskSelectionEvent.java | 2 +- src/main/java/pulse/tasks/logs/Log.java | 2 +- .../java/pulse/tasks/processing/Buffer.java | 2 +- src/main/java/pulse/ui/Launcher.java | 30 +- .../java/pulse/ui/components/AuxPlotter.java | 8 +- .../pulse/ui/components/CalculationTable.java | 90 ++++++ src/main/java/pulse/ui/components/Chart.java | 69 +++-- .../java/pulse/ui/components/DataLoader.java | 2 +- .../java/pulse/ui/components/LogPane.java | 4 +- .../java/pulse/ui/components/ProblemTree.java | 111 +++++++ .../ui/components/PropertyHolderTable.java | 4 - .../java/pulse/ui/components/PulseChart.java | 2 +- .../pulse/ui/components/PulseMainMenu.java | 16 +- .../java/pulse/ui/components/ResultTable.java | 7 - .../java/pulse/ui/components/TaskBox.java | 3 - .../pulse/ui/components/TaskPopupMenu.java | 47 +-- .../java/pulse/ui/components/TaskTable.java | 13 +- .../components/buttons/ExecutionButton.java | 2 +- .../ui/components/buttons/LoaderButton.java | 3 +- .../controllers/AccessibleTableRenderer.java | 64 ++-- .../controllers/NumericPropertyRenderer.java | 45 +-- .../controllers/ProblemListCellRenderer.java | 57 ---- .../controllers/SearchListRenderer.java | 12 +- .../controllers/TaskTableRenderer.java | 15 +- .../FrameVisibilityRequestListener.java | 3 +- .../listeners/ProblemSelectionEvent.java | 31 ++ .../listeners/ProblemSelectionListener.java | 7 + .../models/StoredCalculationTableModel.java | 51 ++++ .../ui/components/models/TaskTableModel.java | 6 +- .../ui/components/panels/ChartToolbar.java | 4 +- .../ui/components/panels/ModelToolbar.java | 53 ++++ .../ui/components/panels/SettingsToolBar.java | 6 - .../ui/components/panels/SystemPanel.java | 54 ++-- src/main/java/pulse/ui/frames/DataFrame.java | 5 - .../java/pulse/ui/frames/HistogramFrame.java | 2 +- .../pulse/ui/frames/ModelSelectionFrame.java | 38 +++ .../java/pulse/ui/frames/PreviewFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 280 +++++++----------- .../pulse/ui/frames/TaskControlFrame.java | 72 ++++- .../pulse/ui/frames/dialogs/ExportDialog.java | 4 +- .../frames/dialogs/FormattedInputDialog.java | 2 - .../ui/frames/dialogs/ResultChangeDialog.java | 2 - src/main/java/pulse/util/Group.java | 8 +- src/main/resources/NumericProperty.xml | 11 + src/main/resources/images/best_model.png | Bin 0 -> 35425 bytes src/main/resources/images/curves.png | Bin 0 -> 7912 bytes src/main/resources/images/log.png | Bin 0 -> 46922 bytes src/main/resources/images/pulse.png | Bin 0 -> 3231 bytes src/main/resources/images/stored.png | Bin 0 -> 27652 bytes src/main/resources/messages.properties | 1 + ...yticalNonscatteringTransferValidation.java | 2 +- ...screteNonscatteringTransferValidation.java | 2 +- .../repository/NonscatteringTestCase.java | 2 +- 122 files changed, 1669 insertions(+), 883 deletions(-) rename src/main/java/pulse/problem/statements/{penetration => model}/AbsorptionModel.java (91%) rename src/main/java/pulse/problem/statements/{penetration => model}/BeerLambertAbsorption.java (85%) create mode 100644 src/main/java/pulse/problem/statements/model/DiathermicProperties.java rename src/main/java/pulse/problem/statements/{ => model}/ExtendedThermalProperties.java (98%) rename src/main/java/pulse/problem/statements/{penetration => model}/Insulator.java (96%) rename src/main/java/pulse/problem/statements/{penetration => model}/SpectralRange.java (90%) rename src/main/java/pulse/problem/statements/{ => model}/ThermalProperties.java (98%) rename src/main/java/pulse/problem/statements/{ => model}/ThermoOpticalProperties.java (99%) create mode 100644 src/main/java/pulse/search/statistics/OptimiserStatistic.java delete mode 100644 src/main/java/pulse/search/statistics/RangePenalisedSSR.java create mode 100644 src/main/java/pulse/tasks/Calculation.java create mode 100644 src/main/java/pulse/ui/components/CalculationTable.java create mode 100644 src/main/java/pulse/ui/components/ProblemTree.java delete mode 100644 src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java create mode 100644 src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java create mode 100644 src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java create mode 100644 src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java create mode 100644 src/main/java/pulse/ui/components/panels/ModelToolbar.java create mode 100644 src/main/java/pulse/ui/frames/ModelSelectionFrame.java create mode 100644 src/main/resources/images/best_model.png create mode 100644 src/main/resources/images/curves.png create mode 100644 src/main/resources/images/log.png create mode 100644 src/main/resources/images/pulse.png create mode 100644 src/main/resources/images/stored.png diff --git a/pom.xml b/pom.xml index a73b8f46..05bddeee 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,11 @@ jfreechart 1.5.0 + + com.weblookandfeel + weblaf-ui + 1.2.13 + org.apache.commons commons-math3 diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index ebe20b10..0f4a7a48 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -42,6 +42,13 @@ protected AbstractData(List time, String name) { this.name = name; } + public AbstractData(AbstractData d) { + this.time = new ArrayList<>(d.time); + this.signal = new ArrayList<>(d.signal); + this.count = d.count; + this.name = d.name; + } + /** * Creates an {@code AbstractData} with the default number of points (set in the * corresponding XML file). diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 4fa1f019..1e77d276 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -50,6 +50,13 @@ public HeatingCurve() { adjustedSignal = new ArrayList((int)this.getNumPoints().getValue()); splineInterpolator = new SplineInterpolator(); } + + public HeatingCurve(HeatingCurve c) { + super(c); + this.adjustedSignal = new ArrayList<>(c.adjustedSignal); + this.startTime = c.startTime; + splineInterpolator = new SplineInterpolator(); + } /** * Creates a {@code HeatingCurve}, where the number of elements in the diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 37bb2796..f9f69788 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -27,6 +27,8 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { + public abstract Baseline copy(); + /** * Calculates the baseline at the given position. * diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index a556280f..73a2524f 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -145,4 +145,9 @@ public void assign(IndexedVector params) { } + @Override + public Baseline copy() { + return new FlatBaseline(this.intercept); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index 57f817f7..2c9d6b46 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -182,5 +182,11 @@ public List listedTypes() { list.add(getSlope()); return list; } + + @Override + public Baseline copy() { + return new LinearBaseline((double)this.getIntercept().getValue(), this.slope); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index a9ab943e..0db965e9 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -43,6 +43,7 @@ public class SinusoidalBaseline extends FlatBaseline { */ public SinusoidalBaseline() { + super(); setFrequency(def(BASELINE_FREQUENCY)); setAmplitude(def(BASELINE_AMPLITUDE)); setPhaseShift(def(BASELINE_PHASE_SHIFT)); @@ -82,7 +83,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { setAmplitude(property); break; default: - break; + super.set(type, property); } } @@ -168,5 +169,16 @@ public void assign(IndexedVector params) { } } + + @Override + public Baseline copy() { + var baseline = new SinusoidalBaseline(); + baseline.setIntercept(this.getIntercept()); + baseline.amplitude = this.amplitude; + baseline.frequency = this.frequency; + baseline.phaseShift = this.phaseShift; + return baseline; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 1a47d7d1..f140089c 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -13,7 +13,7 @@ import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; -import pulse.problem.statements.ThermalProperties; +import pulse.problem.statements.model.ThermalProperties; import pulse.properties.NumericPropertyKeyword; import pulse.util.ImmutableDataEntry; diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 45ce372a..9e37fd47 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -130,14 +130,13 @@ public Segment getSegment() { */ protected void updateMinimum(NumericProperty p) { - if (p != null) { + if (p == null) + return; - requireType(p, PULSE_WIDTH); + requireType(p, PULSE_WIDTH); + double pulseWidth = (double) p.getValue(); + segment.setMinimum(max(segment.getMinimum(), pulseWidth)); - double pulseWidth = (double) p.getValue(); - segment.setMinimum(max(segment.getMinimum(), pulseWidth)); - - } } @Override @@ -177,11 +176,11 @@ public void optimisationVector(IndexedVector[] output, List flags) { switch (output[0].getIndex(i)) { case UPPER_BOUND: output[0].set(i, segment.getMaximum()); - output[1].set(i, 0.25 * segment.getMaximum()); + output[1].set(i, 0.75 * segment.length()); break; case LOWER_BOUND: output[0].set(i, segment.getMinimum()); - output[1].set(i, 0.25 * segment.getMaximum()); + output[1].set(i, 0.75 * segment.length()); break; default: continue; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 9b57faa5..527a9804 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -18,7 +18,7 @@ public class DiscretePulse { private Pulse pulse; private double discretePulseWidth; private double timeFactor; - + /** * This creates a one-dimensional discrete pulse on a {@code grid}. *

diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 41815356..85f1f162 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -4,8 +4,8 @@ import pulse.problem.schemes.Grid2D; import pulse.problem.statements.ClassicalProblem2D; -import pulse.problem.statements.ExtendedThermalProperties; import pulse.problem.statements.Pulse2D; +import pulse.problem.statements.model.ExtendedThermalProperties; /** * The discrete pulse on a {@code Grid2D}. diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index 77f803b0..ee3baed4 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -57,6 +57,14 @@ public double integrand(double... vars) { }; } + + public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { + this.mu = another.mu; + this.sigma = another.sigma; + this.lambda = another.lambda; + this.norm = another.norm; + this.integrator = another.integrator; + } /** * This calls the superclass {@code init method} and sets the normalisation @@ -187,4 +195,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { firePropertyChanged(this, property); } + @Override + public PulseTemporalShape copy() { + // TODO Auto-generated method stub + return null; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index ed24ec38..ed96b635 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -28,6 +28,8 @@ public abstract class PulseTemporalShape extends PropertyHolder implements Refle public void init(DiscretePulse pulse) { width = pulse.getDiscreteWidth(); } + + public abstract PulseTemporalShape copy(); @Override public String getPrefix() { diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index fa336720..eec6885f 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -30,4 +30,9 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { // intentionally blak } + @Override + public PulseTemporalShape copy() { + return new RectangularPulse(); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index 570af2f9..36850248 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -37,6 +37,12 @@ public TrapezoidalPulse() { fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; h = height(); } + + public TrapezoidalPulse(TrapezoidalPulse another) { + this.rise = another.rise; + this.fall = another.fall; + this.h = another.h; + } @Override public void init(DiscretePulse pulse) { @@ -124,4 +130,9 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { firePropertyChanged(this, property); } + @Override + public PulseTemporalShape copy() { + return new TrapezoidalPulse(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 5bb773df..28aa5267 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -57,6 +57,12 @@ protected DifferenceScheme() { protected DifferenceScheme(NumericProperty timeLimit) { setTimeLimit(timeLimit); } + + public void initFrom(DifferenceScheme another) { + this.grid = grid.copy(); + this.timeLimit = another.timeLimit; + this.timeInterval = another.timeInterval; + } /** * Used to get a class of problems on which this difference scheme is diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index a32f06d5..010e2872 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -2,8 +2,8 @@ import java.util.stream.IntStream; -import pulse.problem.statements.penetration.AbsorptionModel; -import pulse.problem.statements.penetration.SpectralRange; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.SpectralRange; /** * An interface providing the ability to calculate the integral signal diff --git a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index 3ae192c0..8542ff4a 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -11,7 +11,7 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Descriptive; import pulse.util.PropertyHolder; import pulse.util.Reflexive; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index d16237a0..98eb4f38 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -7,7 +7,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index 2521c5dc..d718bf2c 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -9,7 +9,7 @@ import pulse.io.readers.QuadratureReader; import pulse.problem.schemes.rte.BlackbodySpectrum; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java index 21eda5c0..ed0fc40b 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -3,7 +3,7 @@ import pulse.problem.schemes.rte.BlackbodySpectrum; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.PropertyHolder; import pulse.util.Reflexive; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index addb751b..b5a6a034 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -1,7 +1,7 @@ package pulse.problem.schemes.rte.dom; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Reflexive; public abstract class PhaseFunction implements Reflexive { diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java index b4aa263a..c063a33e 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -10,7 +10,7 @@ import pulse.problem.schemes.rte.FluxesAndExplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * A solver of the radiative transfer equation for an absorbing-emitting medium diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java index 419c908e..fed6f271 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java @@ -4,7 +4,7 @@ import pulse.problem.schemes.rte.FluxesAndImplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * A solver of the radiative transfer equation for an absorbing-emitting medium diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index 90828d35..db6e1202 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -6,8 +6,8 @@ import pulse.problem.schemes.Grid2D; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.statements.ClassicalProblem2D; -import pulse.problem.statements.ExtendedThermalProperties; import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.properties.NumericProperty; /** diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 53ba210e..594f450a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -16,7 +16,7 @@ import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; public class ExplicitCoupledSolver extends ExplicitScheme implements Solver, FixedPointIterations { diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index 8a087e54..b1d32b7c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -1,14 +1,14 @@ package pulse.problem.schemes.solvers; import static pulse.problem.schemes.DistributedDetection.evaluateSignal; -import static pulse.problem.statements.penetration.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.LASER; import static pulse.ui.Messages.getString; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ExplicitScheme; import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; -import pulse.problem.statements.penetration.AbsorptionModel; +import pulse.problem.statements.model.AbsorptionModel; import pulse.properties.NumericProperty; public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver { diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 0de6f0f9..871ad37f 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -12,7 +12,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; public class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 729fd14a..f3515685 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -5,6 +5,7 @@ import pulse.problem.schemes.ImplicitScheme; import pulse.problem.statements.DiathermicMedium; import pulse.problem.statements.Problem; +import pulse.problem.statements.model.DiathermicProperties; import pulse.properties.NumericProperty; public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver { @@ -42,9 +43,11 @@ private void prepare(DiathermicMedium problem) { HX2_2TAU = HX2 / (2.0 * grid.getTimeStep()); /* Constants */ + + var properties = (DiathermicProperties)problem.getProperties(); - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - final double eta = (double) problem.getDiathermicCoefficient().getValue(); + final double Bi1 = (double) properties.getHeatLoss().getValue(); + final double eta = (double) properties.getDiathermicCoefficient().getValue(); z0 = 1.0 + HX2_2TAU + hx * Bi1 * (1.0 + eta); zN_1 = -hx * eta * Bi1; diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index 068defcf..f2c6cbe8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -1,7 +1,7 @@ package pulse.problem.schemes.solvers; import static pulse.problem.schemes.DistributedDetection.evaluateSignal; -import static pulse.problem.statements.penetration.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.LASER; import static pulse.ui.Messages.getString; import pulse.problem.schemes.DifferenceScheme; @@ -10,7 +10,7 @@ import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; -import pulse.problem.statements.penetration.AbsorptionModel; +import pulse.problem.statements.model.AbsorptionModel; import pulse.properties.NumericProperty; public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index d495ff4d..32f271c1 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -16,7 +16,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index 7e21603c..847cde5b 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -7,6 +7,7 @@ import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import pulse.HeatingCurve; +import pulse.problem.statements.model.ThermalProperties; public class AdiabaticSolution { diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index d079fb6f..e691c77d 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -2,6 +2,7 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; +import pulse.problem.statements.model.ThermalProperties; import pulse.ui.Messages; /** @@ -16,10 +17,10 @@ public ClassicalProblem() { super(); setPulse(new Pulse()); } - - public ClassicalProblem(Problem lp) { - super(lp); - setPulse(new Pulse(lp.getPulse())); + + public ClassicalProblem(Problem p) { + super(p); + setPulse(new Pulse(p.getPulse())); } @Override @@ -47,4 +48,9 @@ public boolean isReady() { return true; } + @Override + public Problem copy() { + return new ClassicalProblem(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 49cd8563..3a315962 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -17,6 +17,8 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.Grid2D; +import pulse.problem.statements.model.ExtendedThermalProperties; +import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; @@ -35,13 +37,13 @@ public ClassicalProblem2D() { setPulse( new Pulse2D() ); setComplexity(ProblemComplexity.MODERATE); } - - public ClassicalProblem2D(Problem lp2) { - super(lp2); - setPulse( new Pulse2D(lp2.getPulse()) ); + + public ClassicalProblem2D(Problem p) { + super(p); + setPulse( new Pulse2D(p.getPulse()) ); setComplexity(ProblemComplexity.MODERATE); } - + @Override public void initProperties() { setProperties( new ExtendedThermalProperties() ); @@ -140,4 +142,9 @@ public boolean isReady() { return true; } + @Override + public Problem copy() { + return new ClassicalProblem2D(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java index bde9c340..5e470e20 100644 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ b/src/main/java/pulse/problem/statements/CoreShellProblem.java @@ -11,6 +11,7 @@ import java.util.List; import pulse.math.IndexedVector; +import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -32,22 +33,6 @@ public CoreShellProblem() { setComplexity(ProblemComplexity.HIGH); } - public CoreShellProblem(Problem sdd) { - super(sdd); - tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); - tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); - coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - public CoreShellProblem(CoreShellProblem csp) { - super(csp); - tA = (double) csp.getCoatingAxialThickness().getValue(); - tR = (double) csp.getCoatingRadialThickness().getValue(); - coatingDiffusivity = (double) csp.getProperties().getDiffusivity().getValue(); - setComplexity(ProblemComplexity.HIGH); - } - @Override public String toString() { return Messages.getString("UniformlyCoatedSample.Descriptor"); diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 26e98e37..2f31ba73 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -2,9 +2,7 @@ import static java.lang.Math.tanh; import static pulse.math.MathUtils.atanh; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; @@ -13,10 +11,9 @@ import pulse.math.IndexedVector; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitDiathermicSolver; +import pulse.problem.statements.model.DiathermicProperties; +import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.ui.Messages; /** @@ -37,51 +34,36 @@ public class DiathermicMedium extends ClassicalProblem { - private double diathermicCoefficient; private final static int DEFAULT_CURVE_POINTS = 300; public DiathermicMedium() { - this(def(DIATHERMIC_COEFFICIENT)); - } - - public DiathermicMedium(NumericProperty diathermicCoefficient) { super(); - this.diathermicCoefficient = (double) (diathermicCoefficient.getValue()); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - } - - public DiathermicMedium(Problem sdd) { - super(sdd); - this.diathermicCoefficient = sdd instanceof DiathermicMedium ? ((DiathermicMedium) sdd).diathermicCoefficient - : (double) def(DIATHERMIC_COEFFICIENT).getValue(); getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); } - public NumericProperty getDiathermicCoefficient() { - return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); + public DiathermicMedium(Problem p) { + super(p); } - - public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { - requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); - this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); + + @Override + public void initProperties() { + setProperties(new DiathermicProperties()); } - + @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == DIATHERMIC_COEFFICIENT) { - diathermicCoefficient = ((Number) property.getValue()).doubleValue(); - } else { - super.set(type, property); - } + public void initProperties(ThermalProperties properties) { + setProperties(new DiathermicProperties(properties)); } @Override public void optimisationVector(IndexedVector[] output, List flags) { super.optimisationVector(output, flags); + var properties = (DiathermicProperties) this.getProperties(); for (int i = 0, size = output[0].dimension(); i < size; i++) { if (output[0].getIndex(i) == DIATHERMIC_COEFFICIENT) { - output[0].set(i, atanh(2.0 * diathermicCoefficient - 1.0)); + final double etta = (double) properties.getDiathermicCoefficient().getValue(); + output[0].set(i, atanh(2.0 * etta - 1.0)); output[1].set(i, 10.0); } } @@ -91,18 +73,19 @@ public void optimisationVector(IndexedVector[] output, List flags) { @Override public void assign(IndexedVector params) { super.assign(params); - var properties = this.getProperties(); - + var properties = (DiathermicProperties) this.getProperties(); + for (int i = 0, size = params.dimension(); i < size; i++) { switch (params.getIndex(i)) { case DIATHERMIC_COEFFICIENT: - diathermicCoefficient = 0.5 * (tanh(params.get(i)) + 1.0); + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, 0.5 * (tanh(params.get(i)) + 1.0))); break; case HEAT_LOSS: if (properties.areThermalPropertiesLoaded()) { properties.emissivity(); - final double emissivity = (double)properties.getEmissivity().getValue(); - diathermicCoefficient = emissivity / (2.0 - emissivity); + final double emissivity = (double) properties.getEmissivity().getValue(); + properties + .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); } break; default: @@ -111,21 +94,19 @@ public void assign(IndexedVector params) { } } - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(DIATHERMIC_COEFFICIENT)); - return list; - } - @Override public String toString() { return Messages.getString("DiathermicProblem.Descriptor"); } - + @Override public Class defaultScheme() { return ImplicitDiathermicSolver.class; } + @Override + public DiathermicMedium copy() { + return new DiathermicMedium(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index acb7534b..95a28fa9 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -29,11 +29,10 @@ public NonlinearProblem() { setPulse( new Pulse2D() ); setComplexity(ProblemComplexity.MODERATE); } - - public NonlinearProblem(Problem p) { + + public NonlinearProblem(NonlinearProblem p) { super(p); - setPulse( new Pulse2D(p.getPulse())); - setComplexity(ProblemComplexity.MODERATE); + setPulse( new Pulse2D((Pulse2D)p.getPulse()) ); } @Override @@ -114,5 +113,10 @@ public void assign(IndexedVector params) { public Class defaultScheme() { return ImplicitScheme.class; } + + @Override + public Problem copy() { + return new NonlinearProblem(this); + } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 1e6b0ef9..31217f22 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -12,6 +12,8 @@ import pulse.math.IndexedVector; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; +import pulse.problem.statements.model.ThermalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; @@ -25,12 +27,12 @@ public ParticipatingMedium() { getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); setComplexity(ProblemComplexity.HIGH); } - - public ParticipatingMedium(Problem p) { + + public ParticipatingMedium(ParticipatingMedium p) { super(p); setComplexity(ProblemComplexity.HIGH); } - + @Override public String toString() { return Messages.getString("ParticipatingMedium.Descriptor"); @@ -123,5 +125,10 @@ public void initProperties(ThermalProperties properties) { public void initProperties() { setProperties( new ThermoOpticalProperties() ); } + + @Override + public Problem copy() { + return new ParticipatingMedium(this); + } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index 0a28c71b..efbd8ea3 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -12,8 +12,8 @@ import pulse.math.IndexedVector; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; -import pulse.problem.statements.penetration.AbsorptionModel; -import pulse.problem.statements.penetration.BeerLambertAbsorption; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.Flag; import pulse.properties.Property; import pulse.ui.Messages; @@ -37,24 +37,10 @@ public PenetrationProblem() { instanceDescriptor.addListener(() -> initAbsorption()); absorption.setParent(this); } - - public PenetrationProblem(Problem sdd) { - super(sdd); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - if (sdd instanceof PenetrationProblem) { - PenetrationProblem tp = (PenetrationProblem) sdd; - setAbsorptionModel(tp.absorption); - } else { - initAbsorption(); - instanceDescriptor.addListener(() -> initAbsorption()); - } - absorption.setParent(this); - } - - public PenetrationProblem(PenetrationProblem tp) { - super(tp); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - setAbsorptionModel(tp.absorption); + + public PenetrationProblem(PenetrationProblem p) { + super(p); + initAbsorption(); } private void initAbsorption() { @@ -130,5 +116,10 @@ public Class defaultScheme() { public String toString() { return Messages.getString("DistributedProblem.Descriptor"); } + + @Override + public Problem copy() { + return new PenetrationProblem(this); + } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index b79b05eb..9efd0fca 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -25,6 +25,7 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.solvers.Solver; +import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -71,11 +72,9 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim */ protected Problem() { - super(); initProperties(); - curve = new HeatingCurve(); - curve.setParent(this); + setHeatingCurve( new HeatingCurve() ); instanceDescriptor.attemptUpdate(FlatBaseline.class.getSimpleName()); addListeners(); @@ -90,18 +89,23 @@ protected Problem() { */ public Problem(Problem p) { - super(); initProperties(p.getProperties().copy()); - this.curve = new HeatingCurve(); - this.curve.setParent(this); - this.curve.setNumPoints(p.getHeatingCurve().getNumPoints()); + setHeatingCurve( new HeatingCurve(p.getHeatingCurve()) ); + curve.setNumPoints(p.getHeatingCurve().getNumPoints()); instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); addListeners(); - initBaseline(); + this.baseline = p.getBaseline().copy(); } + + public abstract Problem copy(); + public void setHeatingCurve(HeatingCurve curve) { + this.curve = curve; + curve.setParent(this); + } + private void addListeners() { instanceDescriptor.addListener(() -> { initBaseline(); @@ -411,7 +415,6 @@ public InstanceDescriptor getBaselineDescriptor() { } private void initBaseline() { - // TODO var baseline = instanceDescriptor.newInstance(Baseline.class); setBaseline(baseline); parameterListChanged(); diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 9b533b47..62966949 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -70,6 +70,16 @@ public Pulse(Pulse p) { }); } + public Pulse copy() { + return new Pulse(this); + } + + public void initFrom(Pulse pulse) { + this.pulseWidth = pulse.pulseWidth; + this.laserEnergy = pulse.laserEnergy; + this.pulseShape = pulse.pulseShape; + } + public double evaluateAt(final double time) { return pulseShape.evaluateAt(time); } diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index ab08e385..0145240e 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -36,6 +36,18 @@ public Pulse2D(Pulse p) { super(p); this.spotDiameter = p instanceof Pulse2D ? ((Pulse2D) p).spotDiameter : (double) def(SPOT_DIAMETER).getValue(); } + + @Override + public void initFrom(Pulse pulse) { + super.initFrom(pulse); + if(pulse instanceof Pulse2D) + this.spotDiameter = ((Pulse2D) pulse).spotDiameter; + } + + @Override + public Pulse copy() { + return new Pulse2D(this); + } public NumericProperty getSpotDiameter() { return derive(SPOT_DIAMETER, spotDiameter); diff --git a/src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java similarity index 91% rename from src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java rename to src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 09614636..e00f2395 100644 --- a/src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -1,7 +1,7 @@ -package pulse.problem.statements.penetration; +package pulse.problem.statements.model; -import static pulse.problem.statements.penetration.SpectralRange.LASER; -import static pulse.problem.statements.penetration.SpectralRange.THERMAL; +import static pulse.problem.statements.model.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.THERMAL; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericPropertyKeyword.LASER_ABSORPTIVITY; import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; diff --git a/src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java similarity index 85% rename from src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java rename to src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 623e9303..b6350a92 100644 --- a/src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -1,4 +1,4 @@ -package pulse.problem.statements.penetration; +package pulse.problem.statements.model; public class BeerLambertAbsorption extends AbsorptionModel { diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java new file mode 100644 index 00000000..097eb1ac --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -0,0 +1,60 @@ +package pulse.problem.statements.model; + +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; + +import java.util.List; + +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.properties.Property; + +public class DiathermicProperties extends ThermalProperties { + + private double diathermicCoefficient; + + public DiathermicProperties() { + super(); + this.diathermicCoefficient = (double) def(DIATHERMIC_COEFFICIENT).getValue(); + } + + public DiathermicProperties(ThermalProperties p) { + super(p); + var property = p instanceof DiathermicProperties + ? ((DiathermicProperties) p).getDiathermicCoefficient() + : def(DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double)property.getValue(); + } + + public ThermalProperties copy() { + return new ThermalProperties(this); + } + + public NumericProperty getDiathermicCoefficient() { + return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); + } + + public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { + requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == DIATHERMIC_COEFFICIENT) { + diathermicCoefficient = ((Number) property.getValue()).doubleValue(); + } else { + super.set(type, property); + } + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(def(DIATHERMIC_COEFFICIENT)); + return list; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java similarity index 98% rename from src/main/java/pulse/problem/statements/ExtendedThermalProperties.java rename to src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index 85eda9c8..840cb632 100644 --- a/src/main/java/pulse/problem/statements/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -1,4 +1,4 @@ -package pulse.problem.statements; +package pulse.problem.statements.model; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; diff --git a/src/main/java/pulse/problem/statements/penetration/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java similarity index 96% rename from src/main/java/pulse/problem/statements/penetration/Insulator.java rename to src/main/java/pulse/problem/statements/model/Insulator.java index 5a7dd547..73062ba6 100644 --- a/src/main/java/pulse/problem/statements/penetration/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -1,4 +1,4 @@ -package pulse.problem.statements.penetration; +package pulse.problem.statements.model; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; diff --git a/src/main/java/pulse/problem/statements/penetration/SpectralRange.java b/src/main/java/pulse/problem/statements/model/SpectralRange.java similarity index 90% rename from src/main/java/pulse/problem/statements/penetration/SpectralRange.java rename to src/main/java/pulse/problem/statements/model/SpectralRange.java index 0d3f8aaf..7a54f6a0 100644 --- a/src/main/java/pulse/problem/statements/penetration/SpectralRange.java +++ b/src/main/java/pulse/problem/statements/model/SpectralRange.java @@ -1,4 +1,4 @@ -package pulse.problem.statements.penetration; +package pulse.problem.statements.model; import pulse.properties.NumericPropertyKeyword; diff --git a/src/main/java/pulse/problem/statements/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java similarity index 98% rename from src/main/java/pulse/problem/statements/ThermalProperties.java rename to src/main/java/pulse/problem/statements/model/ThermalProperties.java index 3b523a5d..65146f90 100644 --- a/src/main/java/pulse/problem/statements/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -1,4 +1,4 @@ -package pulse.problem.statements; +package pulse.problem.statements.model; import static java.lang.Math.PI; import static pulse.input.InterpolationDataset.getDataset; @@ -21,6 +21,7 @@ import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset.StandartType; +import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -231,12 +232,12 @@ public void emissivity() { setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); } - protected double maxBiot() { + public double maxBiot() { double lambda = thermalConductivity(); return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; } - protected double biot() { + public double biot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; } diff --git a/src/main/java/pulse/problem/statements/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java similarity index 99% rename from src/main/java/pulse/problem/statements/ThermoOpticalProperties.java rename to src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 9637cdf8..7077b288 100644 --- a/src/main/java/pulse/problem/statements/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -1,4 +1,4 @@ -package pulse.problem.statements; +package pulse.problem.statements.model; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; diff --git a/src/main/java/pulse/problem/statements/package-info.java b/src/main/java/pulse/problem/statements/package-info.java index c6685603..295cf5ba 100644 --- a/src/main/java/pulse/problem/statements/package-info.java +++ b/src/main/java/pulse/problem/statements/package-info.java @@ -1,7 +1,6 @@ /** * Introduces various problem statements for the heat conduction problem in the - * laser flash experiment. Note that currently only one problem statement is - * enabled, which is the {@code LinearisedProblem}. + * laser flash experiment. */ package pulse.problem.statements; \ No newline at end of file diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 0433e62c..424b5092 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -311,6 +311,12 @@ public enum NumericPropertyKeyword { OPTIMISER_STATISTIC, + /** + * Model selection criterion (AIC, BIC, etc.) + */ + + MODEL_CRITERION, + /** * Test statistic (e.g. normality test criterion). */ @@ -477,7 +483,13 @@ public enum NumericPropertyKeyword { * λ parameter for skewed normal distribution. */ - SKEW_LAMBDA; + SKEW_LAMBDA, + + /** + * A weight indicating how good a calculation model is. + */ + + MODEL_WEIGHT; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index b3b9edfa..0c7939d5 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -43,7 +43,7 @@ public static void listAvailableProperties(List list) { var t = TaskManager.getManagerInstance().getSelectedTask(); if (t != null) { - var p = t.getProblem(); + var p = t.getCurrentCalculation().getProblem(); if (p != null) { @@ -75,7 +75,7 @@ public static void listAvailableProperties(List list) { */ public static List activeParameters(SearchTask t) { - Problem p = t.getProblem(); + Problem p = t.getCurrentCalculation().getProblem(); var list = new ArrayList(); list.addAll(selectActiveAndListed(problemDependentFlags, p)); diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index 25423419..111ccda8 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -183,7 +183,8 @@ public static Vector gradient(SearchTask task) { var grad = new Vector(params.dimension()); boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dx = discreteGradient ? 2.0 * task.getScheme().getGrid().getXStep() : 2.0 * gradientResolution; + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + final double dx = discreteGradient ? 2.0 * dxGrid : 2.0 * gradientResolution; for (int i = 0; i < params.dimension(); i++) { final var shift = new Vector(params.dimension()); diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index d0aea298..6b4ed00a 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -8,6 +8,23 @@ public class AICStatistic extends ModelSelectionCriterion { + public AICStatistic(OptimiserStatistic os) { + super(os); + } + + public AICStatistic(AICStatistic another) { + super(another); + } + + public AICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new AICStatistic(this); + } + @Override public double penalisingTerm(final int kq, final int n) { return 2.0 * (kq + 1); diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index eb8ad3ae..20aaa740 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -13,7 +13,15 @@ * */ -public class AbsoluteDeviations extends ResidualStatistic { +public class AbsoluteDeviations extends OptimiserStatistic { + + public AbsoluteDeviations() { + super(); + } + + public AbsoluteDeviations(AbsoluteDeviations another) { + super(another); + } /** * Calculates the L1 norm statistic, which simply sums up the absolute values of residuals. @@ -25,10 +33,21 @@ public void evaluate(SearchTask t) { final double statistic = getResiduals().stream().map(r -> abs(r[1]) ).reduce(Double::sum).get() / getResiduals().size(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } + + @Override + public double variance() { + final double stat = (double)this.getStatistic().getValue(); + return stat*stat; + } @Override public String getDescriptor() { return "Absolute Deviations"; } + @Override + public OptimiserStatistic copy() { + return new AbsoluteDeviations(this); + } + } diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java index c762a7da..d62a4d5d 100644 --- a/src/main/java/pulse/search/statistics/BICStatistic.java +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -9,6 +9,23 @@ public class BICStatistic extends ModelSelectionCriterion { + public BICStatistic(BICStatistic another) { + super(another); + } + + public BICStatistic(OptimiserStatistic os) { + super(os); + } + + public BICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new BICStatistic(this); + } + @Override public double penalisingTerm(final int kq, final int n) { return (kq + 1)*log(n); diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index b001d411..b89cc3c2 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -16,7 +16,7 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv private static String selectedTestDescriptor; public CorrelationTest() { - //intentionall blank + //intentionally blank } public abstract double evaluate(double[] x, double[] y); diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index de2b1a74..a782a4ba 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -1,28 +1,72 @@ package pulse.search.statistics; import static java.lang.Math.PI; +import static java.lang.Math.exp; import static java.lang.Math.log; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.MODEL_CRITERION; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; +import java.util.List; + +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; -public abstract class ModelSelectionCriterion extends SumOfSquares { +/** + * An abstract superclass for the BIC and AIC statistics. + * + */ + +public abstract class ModelSelectionCriterion extends Statistic { + private static String selectedModelSelectionDescriptor; + private OptimiserStatistic os; private int kq; private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); - + private double criterion; + + public ModelSelectionCriterion(OptimiserStatistic os) { + super(); + setOptimiser(os); + } + + public ModelSelectionCriterion(ModelSelectionCriterion another) { + this.os = another.os.copy(); + this.kq = another.kq; + this.criterion = another.criterion; + } + @Override public void evaluate(SearchTask t) { kq = t.alteredParameters().size(); //number of variables - super.evaluate(t); - final int n = getResiduals().size(); //sample size - final double ssr = (double)getStatistic().getValue(); //sum of squared residuals divided by n - final double stat = n * log(ssr) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; - this.setStatistic(derive(OPTIMISER_STATISTIC, stat)); + os.evaluate(t); + final int n = os.getResiduals().size(); //sample size + criterion = n * log(os.variance()) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; } + /** + * The penalising term, which is different depending on implementation. + * @param k the number of model variables + * @param n the sample size + * @return the penalising term + */ + public abstract double penalisingTerm(int k, int n); + + public abstract ModelSelectionCriterion copy(); + + public NumericProperty weight(List all) { + final double sum = all.stream().map(criterion -> criterion.probability(all)).reduce( (a, b) -> a + b).get(); + return derive(MODEL_WEIGHT, probability(all)/sum); + } + + public double probability(List all) { + final double min = all.stream().map(criterion -> (double)criterion.getStatistic().getValue()).reduce( (a, b) -> a < b ? a : b).get(); + final double di = (double)this.getStatistic().getValue() - min; + return exp(-0.5*di); + } @Override public String getDescriptor() { @@ -33,4 +77,35 @@ public int getNumVariables() { return kq; } + public static String getSelectedCriterionDescriptor() { + return selectedModelSelectionDescriptor; + } + + public static void setSelectedCriterionDescriptor(String selectedTestDescriptor) { + ModelSelectionCriterion.selectedModelSelectionDescriptor = selectedTestDescriptor; + } + + public OptimiserStatistic getOptimiser() { + return os; + } + + public void setOptimiser(OptimiserStatistic os) { + this.os = os; + } + + public void setStatistic(NumericProperty p) { + requireType(p, MODEL_CRITERION); + this.criterion = (double)p.getValue(); + } + + public NumericProperty getStatistic() { + return derive(MODEL_CRITERION, criterion); + } + + @Override + public void set(NumericPropertyKeyword key, NumericProperty p) { + if(key == MODEL_CRITERION) + setStatistic(p); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/OptimiserStatistic.java b/src/main/java/pulse/search/statistics/OptimiserStatistic.java new file mode 100644 index 00000000..6822d0d3 --- /dev/null +++ b/src/main/java/pulse/search/statistics/OptimiserStatistic.java @@ -0,0 +1,26 @@ +package pulse.search.statistics; + +public abstract class OptimiserStatistic extends ResidualStatistic { + + private static String selectedOptimiserDescriptor; + + public OptimiserStatistic(OptimiserStatistic stat) { + super(stat); + } + + protected OptimiserStatistic() { + super(); + } + + public static String getSelectedOptimiserDescriptor() { + return selectedOptimiserDescriptor; + } + + public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { + OptimiserStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; + } + + public abstract OptimiserStatistic copy(); + public abstract double variance(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index 48822b5b..d7755915 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -22,16 +22,13 @@ public class RSquaredTest extends NormalityTest { public RSquaredTest() { super(); - } - - public RSquaredTest(SumOfSquares sos) { - this(); - this.sos = sos; + sos = new SumOfSquares(); } @Override public boolean test(SearchTask task) { evaluate(task); + sos = new SumOfSquares(); return getStatistic().compareTo(signifiance) > 0; } diff --git a/src/main/java/pulse/search/statistics/RangePenalisedSSR.java b/src/main/java/pulse/search/statistics/RangePenalisedSSR.java deleted file mode 100644 index 3b8c8fd0..00000000 --- a/src/main/java/pulse/search/statistics/RangePenalisedSSR.java +++ /dev/null @@ -1,27 +0,0 @@ -package pulse.search.statistics; - -import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; - -import pulse.tasks.SearchTask; - -public class RangePenalisedSSR extends SumOfSquares { - - private final static double PENALISATION_FACTOR = 0.5; - - @Override - public void evaluate(SearchTask t) { - super.evaluate(t); - - final double n = getResiduals().size(); - final double n0 = t.getExperimentalCurve().actualNumPoints(); - - incrementStatistic( - (n0 - n) / n0 * (new StandardDeviation().evaluate(transformResiduals())) * PENALISATION_FACTOR); - } - - @Override - public String getDescriptor() { - return "Range Penalised SSR"; - } - -} diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index da87effe..3da20f2e 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -19,21 +19,25 @@ public abstract class ResidualStatistic extends Statistic { private double statistic; private List residuals; - private static String selectedOptimiserDescriptor; public ResidualStatistic() { super(); residuals = new ArrayList<>(); setPrefix("Residuals"); } - + + public ResidualStatistic(ResidualStatistic another) { + this.statistic = another.statistic; + this.residuals = new ArrayList<>(another.residuals); + } + public double[] transformResiduals() { return getResiduals().stream().map(doubleArray -> doubleArray[1]) .mapToDouble(Double::doubleValue).toArray(); } public void calculateResiduals(SearchTask task) { - var estimate = task.getProblem().getHeatingCurve(); + var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); var reference = task.getExperimentalCurve(); residuals.clear(); @@ -74,14 +78,6 @@ public double residualLowerBound() { return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); } - public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { - ResidualStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; - } - - public static String getSelectedOptimiserDescriptor() { - return selectedOptimiserDescriptor; - } - public NumericProperty getStatistic() { return derive(OPTIMISER_STATISTIC, statistic); } diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index af5da4ff..573a1afc 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -10,8 +10,16 @@ * */ -public class SumOfSquares extends ResidualStatistic { - +public class SumOfSquares extends OptimiserStatistic { + + public SumOfSquares() { + super(); + } + + public SumOfSquares(SumOfSquares sos) { + super(sos); + } + /** * Calculates the sum of squared deviations using {@code curve} as reference. *

@@ -34,7 +42,7 @@ public class SumOfSquares extends ResidualStatistic { * * @param t The task containing the reference and calculated curves */ - + @Override public void evaluate(SearchTask t) { calculateResiduals(t); @@ -47,4 +55,14 @@ public String getDescriptor() { return "Ordinary least squares"; } + @Override + public double variance() { + return (double)getStatistic().getValue(); + } + + @Override + public OptimiserStatistic copy() { + return new SumOfSquares(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java new file mode 100644 index 00000000..cff1bbb4 --- /dev/null +++ b/src/main/java/pulse/tasks/Calculation.java @@ -0,0 +1,270 @@ +package pulse.tasks; + +import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; +import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; +import static pulse.tasks.logs.Status.FAILED; +import static pulse.tasks.logs.Status.INCOMPLETE; +import static pulse.util.Reflexive.instantiate; + +import java.util.List; +import java.util.stream.Collectors; + +import pulse.input.ExperimentalData; +import pulse.input.Metadata; +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.solvers.Solver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.Problem; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.statistics.ModelSelectionCriterion; +import pulse.search.statistics.OptimiserStatistic; +import pulse.tasks.logs.Details; +import pulse.tasks.logs.Status; +import pulse.ui.components.PropertyHolderTable; +import pulse.util.PropertyEvent; +import pulse.util.PropertyHolder; + +public class Calculation extends PropertyHolder implements Comparable { + + private Status status; + public final static double RELATIVE_TIME_MARGIN = 1.01; + + private Problem problem; + private DifferenceScheme scheme; + private ModelSelectionCriterion rs; + private OptimiserStatistic os; + + public Calculation() { + status = INCOMPLETE; + this.initOptimiser(); + } + + public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { + this(); + this.problem = problem; + this.scheme = scheme; + this.os = rs.getOptimiser(); + this.rs = rs; + problem.setParent(this); + scheme.setParent(this); + os.setParent(this); + rs.setParent(this); + } + + public Calculation copy() { + var status = this.status; + var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); + var p = nCalc.getProblem(); + p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); + nCalc.status = status; + return nCalc; + } + + public void clear() { + this.status = Status.INCOMPLETE; + this.problem = null; + this.scheme = null; + } + + /** + *

+ * After setting and adopting the {@code problem} by this {@code SearchTask}, + * this will attempt to change the parameters of that {@code problem} in + * accordance with the loaded {@code ExperimentalData} for this + * {@code SearchTask} (if not null). Later, if any changes to the properties of + * that {@code Problem} occur and if the source of that event is either the + * {@code Metadata} or the {@code PropertyHolderTable}, they will be accounted + * for by altering the parameters of the {@code problem} accordingly -- + * immediately after the former take place. + *

+ * + * @param problem a {@code Problem} + */ + + public void setProblem(Problem problem, ExperimentalData curve) { + this.problem = problem; + problem.setParent(this); + problem.removeHeatingCurveListeners(); + problem.retrieveData(curve); + + problem.getProperties().addListener((PropertyEvent event) -> { + var source = event.getSource(); + + if (source instanceof Metadata || source instanceof PropertyHolderTable ) { + + var property = event.getProperty(); + if(property instanceof NumericProperty && ((NumericProperty)property).isAutoAdjustable() ) + return; + + problem.estimateSignalRange(curve); + problem.getProperties().useTheoreticalEstimates(curve); + } + }); + + problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { + + var event = dataEvent.getType(); + + if (event == TIME_ORIGIN_CHANGED) { + var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); + } + + }); + + } + + /** + * Adopts the {@code scheme} by this {@code SearchTask} and updates the time + * limit of {@scheme} to match {@code ExperimentalData}. + * + * @param scheme the {@code DiffenceScheme}. + */ + + public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { + this.scheme = scheme; + + if (problem != null && scheme != null) { + scheme.setParent(this); + + var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); + + } + + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code Calculation}. + * + * @throws SolverException + */ + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void process() { + try { + ((Solver) scheme).solve(problem); + } catch (SolverException e) { + status = FAILED; + System.err.println("Solver of " + this + " has encountered an error. Details: "); + e.printStackTrace(); + } + } + + public Status getStatus() { + return status; + } + + public boolean setStatus(Status status, Details details) { + boolean done = false; + + if(this.status != status) { + this.status = status; + done = true; + } + else if(this.status.getDetails() != status.getDetails()){ + this.status.setDetails(status.getDetails()); + done = true; + } + + return done; + } + + public NumericProperty weight(List all) { + var result = def(MODEL_WEIGHT); + + if(rs instanceof ModelSelectionCriterion) { + var criterion = (ModelSelectionCriterion)rs; + + boolean condition = all.stream().allMatch(c -> c.getModelSelectionCriterion().getClass().equals(criterion.getClass())); + + if(condition) { + var list = all.stream().map(a -> (ModelSelectionCriterion)a.getModelSelectionCriterion()).collect(Collectors.toList()); + result = criterion.weight( list ); + } + + } + + return result; + } + + public void setModelSelectionCriterion(ModelSelectionCriterion rs) { + this.rs = rs; + rs.setParent(this); + } + + public ModelSelectionCriterion getModelSelectionCriterion() { + return rs; + } + + public void setOptimiserStatistic(OptimiserStatistic os) { + this.os = os; + os.setParent(this); + initModelCriterion(); + } + + public OptimiserStatistic getOptimiserStatistic() { + return os; + } + + public Problem getProblem() { + return problem; + } + + public void initOptimiser() { + this.setOptimiserStatistic( instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor() ) ); + this.initModelCriterion(); + } + + public void initModelCriterion() { + setModelSelectionCriterion( instantiate(ModelSelectionCriterion.class, ModelSelectionCriterion.getSelectedCriterionDescriptor() ) ); + rs.setOptimiser(os); + } + + public DifferenceScheme getScheme() { + return scheme; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public int compareTo(Calculation arg0) { + var s1 = arg0.getModelSelectionCriterion().getStatistic(); + return getModelSelectionCriterion().getStatistic().compareTo(s1); + } + + @Override + public boolean equals(Object o) { + if(o == this) + return true; + + if(o == null) + return false; + + if(! (o instanceof Calculation)) + return false; + + var c = (Calculation)o; + + if(os.getStatistic().equals(c.getOptimiserStatistic().getStatistic())) { + if(rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())) { + return true; + } + } + + return false; + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index a8afd60a..d03ec156 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -1,6 +1,5 @@ package pulse.tasks; -import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; import static pulse.search.direction.ActiveFlags.activeParameters; @@ -8,7 +7,6 @@ import static pulse.search.direction.PathOptimiser.getErrorTolerance; import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.search.direction.PathOptimiser.getLinearSolver; -import static pulse.search.statistics.ResidualStatistic.getSelectedOptimiserDescriptor; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; import static pulse.tasks.logs.Details.MISSING_BUFFER; @@ -37,22 +35,16 @@ import java.util.stream.Collectors; import pulse.input.ExperimentalData; -import pulse.input.Metadata; import pulse.math.IndexedVector; -import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; -import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.direction.Path; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; -import pulse.search.statistics.RSquaredTest; -import pulse.search.statistics.ResidualStatistic; -import pulse.search.statistics.SumOfSquares; import pulse.tasks.listeners.DataCollectionListener; import pulse.tasks.listeners.StatusChangeListener; +import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Details; @@ -62,9 +54,7 @@ import pulse.tasks.logs.Status; import pulse.tasks.processing.Buffer; import pulse.tasks.processing.CorrelationBuffer; -import pulse.ui.components.PropertyHolderTable; import pulse.util.Accessible; -import pulse.util.PropertyEvent; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -79,32 +69,28 @@ public class SearchTask extends Accessible implements Runnable { - private Problem problem; - private DifferenceScheme scheme; + private Calculation current; + private List stored; private ExperimentalData curve; - private ResidualStatistic rs; private Path path; private Buffer buffer; - private CorrelationBuffer correlationBuffer; private Log log; - private CorrelationTest correlationTest; - - private Identifier identifier; - private Status status = INCOMPLETE; + private CorrelationBuffer correlationBuffer; + private CorrelationTest correlationTest; private NormalityTest normalityTest; - - private final static double RELATIVE_TIME_MARGIN = 1.01; + + private Identifier identifier; /** * If {@code SearchTask} finishes, and its R2 value is lower * than this constant, the result will be considered {@code AMBIGUOUS}. */ - private List listeners = new CopyOnWriteArrayList(); - private List statusChangeListeners = new CopyOnWriteArrayList(); - + private List listeners = new CopyOnWriteArrayList<>(); + private List statusChangeListeners = new CopyOnWriteArrayList<>(); + /** *

* Creates a new {@code SearchTask} from {@code curve}. Generates a new @@ -117,6 +103,8 @@ public class SearchTask extends Accessible implements Runnable { */ public SearchTask(ExperimentalData curve) { + current = new Calculation(); + current.setParent(this); this.identifier = new Identifier(); this.curve = curve; curve.setParent(this); @@ -128,35 +116,60 @@ public SearchTask(ExperimentalData curve) { *

* Resets everything to default values (for a list of default values please see * the {@code .xml} document. Sets the status of this task to - * {@code INCOMPLETE}. + * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { + var scheme = current.getScheme(); + if (scheme != null) { + var curve = current.getProblem().getHeatingCurve(); + var startTime = (double) curve.getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + } + }); *

*/ public void clear() { + stored = new ArrayList(); curve.resetRanges(); buffer = new Buffer(); correlationBuffer.clear(); buffer.setParent(this); log = new Log(this); - initOptimiser(); initCorrelationTest(); initNormalityTest(); this.path = null; - this.problem = null; - this.scheme = null; - + current.clear(); + setStatus(INCOMPLETE); curve.addDataListener(dataEvent -> { + var scheme = current.getScheme(); if (scheme != null) { - var startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + var hcurve = current.getProblem().getHeatingCurve(); + var startTime = (double) hcurve.getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code SearchTask} and calculate the SSR value + * showing how well (or bad) the calculated solution describes the + * {@code ExperimentalData}. + * + * @return the value of SSR (sum of squared residuals). + * @throws SolverException + */ + + public double solveProblemAndCalculateDeviation() { + current.process(); + var rs = current.getOptimiserStatistic(); + rs.evaluate(this); + return (double) rs.getStatistic().getValue(); + } public List alteredParameters() { return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); @@ -180,7 +193,7 @@ public IndexedVector[] searchVector() { var array = new IndexedVector[] { optimisationVector, upperBound }; - problem.optimisationVector(array, flags); + current.getProblem().optimisationVector(array, flags); curve.getRange().optimisationVector(array, flags); return array; @@ -197,33 +210,10 @@ public IndexedVector[] searchVector() { */ public void assign(IndexedVector searchParameters) { - problem.assign(searchParameters); + current.getProblem().assign(searchParameters); curve.getRange().assign(searchParameters); } - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code SearchTask} and calculate the SSR value - * showing how well (or bad) the calculated solution describes the - * {@code ExperimentalData}. - * - * @return the value of SSR (sum of squared residuals). - * @throws SolverException - */ - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public double solveProblemAndCalculateDeviation() { - try { - ((Solver) scheme).solve(problem); - } catch (SolverException e) { - status = FAILED; - System.err.println("Solver of " + this + " has encountered an error. Details: "); - e.printStackTrace(); - } - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - /** *

* Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, will @@ -242,7 +232,7 @@ public void run() { /* check of status */ - switch (status) { + switch (current.getStatus()) { case READY: case QUEUED: setStatus(IN_PROGRESS); @@ -250,11 +240,11 @@ public void run() { default: return; } - + /* preparatory steps */ - getProblem().parameterListChanged(); // get updated list of parameters - solveProblemAndCalculateDeviation(); + current.getProblem().parameterListChanged(); // get updated list of parameters + current.process(); var pathSolver = getInstance(); @@ -278,13 +268,13 @@ public void run() { for (var i = 0; i < bufferSize; i++) { - if (status != IN_PROGRESS) + if (current.getStatus() != IN_PROGRESS) break outer; try { pathSolver.iteration(this); } catch (SolverException e) { - status = FAILED; + setStatus(FAILED); System.err.println(this + " failed during execution. Details: "); e.printStackTrace(); } @@ -305,7 +295,7 @@ public void run() { singleThreadExecutor.shutdown(); - if (status == IN_PROGRESS) + if (current.getStatus() == IN_PROGRESS) runChecks(); } @@ -331,9 +321,13 @@ private void runChecks() { if (properties.stream().anyMatch(np -> !np.validate())) setStatus(FAILED, PARAMETER_VALUES_NOT_SENSIBLE); - else + else { setStatus(DONE); - + current.getModelSelectionCriterion().evaluate(this); + storeCurrentCalculation(); + switchTo(current.copy()); + } + } } @@ -361,14 +355,6 @@ public String toString() { return getIdentifier().toString(); } - public Problem getProblem() { - return problem; - } - - public DifferenceScheme getScheme() { - return scheme; - } - public ExperimentalData getExperimentalCurve() { return curve; } @@ -377,77 +363,6 @@ public Path getPath() { return path; } - /** - *

- * After setting and adopting the {@code problem} by this {@code SearchTask}, - * this will attempt to change the parameters of that {@code problem} in - * accordance with the loaded {@code ExperimentalData} for this - * {@code SearchTask} (if not null). Later, if any changes to the properties of - * that {@code Problem} occur and if the source of that event is either the - * {@code Metadata} or the {@code PropertyHolderTable}, they will be accounted - * for by altering the parameters of the {@code problem} accordingly -- - * immediately after the former take place. - *

- * - * @param problem a {@code Problem} - */ - - public void setProblem(Problem problem) { - this.problem = problem; - problem.setParent(this); - problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); - - problem.getProperties().addListener((PropertyEvent event) -> { - var source = event.getSource(); - - if (source instanceof Metadata || source instanceof PropertyHolderTable ) { - - var property = event.getProperty(); - if(property instanceof NumericProperty && ((NumericProperty)property).isAutoAdjustable() ) - return; - - problem.estimateSignalRange(curve); - problem.getProperties().useTheoreticalEstimates(curve); - } - }); - - problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { - - var event = dataEvent.getType(); - - if (event == TIME_ORIGIN_CHANGED) { - var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); - } - - }); - - } - - /** - * Adopts the {@code scheme} by this {@code SearchTask} and updates the time - * limit of {@scheme} to match {@code ExperimentalData}. - * - * @param scheme the {@code DiffenceScheme}. - */ - - public void setScheme(DifferenceScheme scheme) { - this.scheme = scheme; - - if (problem != null && scheme != null) { - scheme.setParent(this); - - var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); - - } - - } - /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -461,19 +376,12 @@ public void setExperimentalCurve(ExperimentalData curve) { curve.setParent(this); } - - public Status getStatus() { - return status; - } - + public void setStatus(Status status, Details details) { - if (this.status != status) { - this.status = status; - status.setDetails(details); + if(current.setStatus(status, details)) notifyStatusListeners(new StateEntry(this, status)); - } } - + /** * Sets a new {@code status} to this {@code SearchTask} and informs the * listeners. @@ -505,17 +413,18 @@ public Status checkProblems() { */ public Status checkProblems(boolean updateStatus) { + var status = current.getStatus(); if (status == DONE) return status; var pathSolver = getInstance(); var s = INCOMPLETE; - if (problem == null) + if (current.getProblem() == null) s.setDetails(MISSING_PROBLEM_STATEMENT); - else if (!problem.isReady()) + else if (!current.getProblem().isReady()) s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); - else if (scheme == null) + else if (current.getScheme() == null) s.setDetails(MISSING_DIFFERENCE_SCHEME); else if (curve == null) s.setDetails(MISSING_HEATING_CURVE); @@ -578,7 +487,7 @@ public String describe() { */ public void terminate() { - switch (status) { + switch (current.getStatus()) { case IN_PROGRESS: case QUEUED: case READY: @@ -615,29 +524,11 @@ public NormalityTest getNormalityTest() { return normalityTest; } - public ResidualStatistic getResidualStatistic() { - return rs; - } - - public void setResidualStatistic(ResidualStatistic rs) { - this.rs = rs; - rs.setParent(this); - } - public void initNormalityTest() { normalityTest = instantiate(NormalityTest.class, NormalityTest.getSelectedTestDescriptor()); - - if (normalityTest instanceof RSquaredTest && rs instanceof SumOfSquares) - ((RSquaredTest) normalityTest).setSumOfSquares((SumOfSquares) rs); - normalityTest.setParent(this); } - public void initOptimiser() { - rs = instantiate(ResidualStatistic.class, getSelectedOptimiserDescriptor()); - rs.setParent(this); - } - public void initCorrelationTest() { correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); correlationTest.setParent(this); @@ -651,4 +542,35 @@ public CorrelationTest getCorrelationTest() { return correlationTest; } + public Calculation getCurrentCalculation() { + return current; + } + + public void storeCurrentCalculation() { + stored.add(current.copy()); + } + + public List getStoredCalculations() { + return this.stored; + } + + public void switchTo(Calculation calc) { + current = null; + this.current = calc.copy(); + current.setParent(this); + this.fireModelSelected(); + } + + public void switchToBestModel() { + var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); + this.switchTo(best.get()); + } + + private void fireModelSelected() { + var instance = TaskManager.getManagerInstance(); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MDOEL_SWITCH, this.getIdentifier()); + for(var l : instance.getTaskRepositoryListeners()) + l.onTaskListChanged(e); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index fe431cc0..8d87578a 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -128,7 +128,7 @@ public void execute(SearchTask t) { // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - if (t.getStatus() == DONE) { + if (t.getCurrentCalculation().getStatus() == DONE) { results.put(t, new Result(t, ResultFormat.getInstance())); } var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); @@ -158,7 +158,7 @@ public void notifyListeners(TaskRepositoryEvent e) { public void executeAll() { var queue = tasks.stream().filter(t -> { - switch (t.getStatus()) { + switch (t.getCurrentCalculation().getStatus()) { case DONE: case IN_PROGRESS: case EXECUTION_ERROR: @@ -188,7 +188,10 @@ public void executeAll() { */ public boolean isTaskQueueEmpty() { - return !tasks.stream().anyMatch(t -> t.getStatus() == QUEUED || t.getStatus() == IN_PROGRESS); + return !tasks.stream().anyMatch(t -> { + var status = t.getCurrentCalculation().getStatus(); + return status == QUEUED || status == IN_PROGRESS; + }); } /** @@ -535,7 +538,7 @@ public void removeResult(SearchTask t) { public void evaluate() { tasks.stream().forEach(t -> { - var properties = t.getProblem().getProperties(); + var properties = t.getCurrentCalculation().getProblem().getProperties(); InterpolationDataset.fill(properties); properties.useTheoreticalEstimates(t.getExperimentalCurve()); }); diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 6af6b77d..05b23c09 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -51,6 +51,18 @@ public enum State { */ TASK_RESET, + + /** + * An external request has been received to browse previous calculations. + */ + + TASK_BROWSING_REQUEST, + + /** + * The task has switched to a new model. + */ + + TASK_MDOEL_SWITCH, /** * The repository has been shut down/ diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java index cc3e173b..a5462427 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java @@ -2,4 +2,4 @@ public interface TaskRepositoryListener { public void onTaskListChanged(TaskRepositoryEvent e); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java index 3dd33ab7..b4149942 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java @@ -18,4 +18,4 @@ public void setSource(Object source) { this.source = source; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 09819e94..bd59d5ef 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -52,7 +52,7 @@ public Log(SearchTask task) { * Do these actions each time data has been collected for this task. */ - if (task.getStatus() != Status.INCOMPLETE && verbose) { + if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { logEntries.add(le); notifyListeners(le); } diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index c7782d23..d4699fa8 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -62,7 +62,7 @@ public void init() { */ public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getResidualStatistic().getStatistic().getValue(); + statistic[bufferElement] = (double) t.getCurrentCalculation().getModelSelectionCriterion().getStatistic().getValue(); data[bufferElement] = t.searchVector()[0]; } diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index f6da21f6..75783b8f 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -8,11 +8,7 @@ import static java.lang.System.err; import static java.lang.management.ManagementFactory.getPlatformMBeanServer; import static java.util.Objects.requireNonNull; -import static java.util.logging.Level.SEVERE; -import static java.util.logging.Logger.getLogger; import static javax.management.ObjectName.getInstance; -import static javax.swing.UIManager.getInstalledLookAndFeels; -import static javax.swing.UIManager.setLookAndFeel; import static pulse.ui.frames.TaskControlFrame.getInstance; import javax.management.Attribute; @@ -22,6 +18,10 @@ import javax.management.ObjectName; import javax.management.ReflectionException; import javax.swing.ImageIcon; +import javax.swing.UIManager; + +import com.alee.laf.WebLookAndFeel; +import com.alee.skin.dark.WebDarkSkin; /** *

@@ -46,25 +46,13 @@ private Launcher() { public static void main(String[] args) { splashScreen(); - /* Set the Nimbus Look and feel setting code. - /* - * If Nimbus (introduced in Java SE 6) is not available, stay with the default - * look and feel. For details see - * http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html - */ + WebLookAndFeel.install( WebDarkSkin.class); try { - for (var info : getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { - setLookAndFeel(info.getClassName()); - break; - } - } - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException - | javax.swing.UnsupportedLookAndFeelException ex) { - getLogger(Launcher.class.getName()).log(SEVERE, null, ex); + UIManager.setLookAndFeel( new WebLookAndFeel() ); + } catch( Exception ex ) { + System.err.println( "Failed to initialize LaF" ); } - // - + /* Create and display the form */ invokeLater(() -> { getInstance().setLocationRelativeTo(null); diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 57750040..7e379d03 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -1,9 +1,9 @@ package pulse.ui.components; -import static java.awt.Color.white; - import java.awt.Font; +import javax.swing.UIManager; + import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.XYPlot; @@ -16,9 +16,9 @@ public abstract class AuxPlotter { public AuxPlotter(String xLabel, String yLabel) { createChart(xLabel, yLabel); - + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot = chart.getXYPlot(); - plot.setBackgroundPaint(white); setFonts(); chart.removeLegend(); diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java new file mode 100644 index 00000000..9c73a693 --- /dev/null +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -0,0 +1,90 @@ +package pulse.ui.components; + +import static javax.swing.ListSelectionModel.SINGLE_SELECTION; +import static pulse.ui.frames.MainGraphFrame.getChart; + +import java.awt.Dimension; + +import javax.swing.JTable; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.TableCellRenderer; + +import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.ui.components.controllers.TaskTableRenderer; +import pulse.ui.components.models.StoredCalculationTableModel; + +@SuppressWarnings("serial") +public class CalculationTable extends JTable { + + private final static int ROW_HEIGHT = 70; + private final static int HEADER_HEIGHT = 30; + + private TaskTableRenderer taskTableRenderer; + + public CalculationTable() { + super(); + setDefaultEditor(Object.class, null); + taskTableRenderer = new TaskTableRenderer(); + this.setRowSelectionAllowed(true); + setRowHeight(ROW_HEIGHT); + + setFillsViewportHeight(true); + setSelectionMode(SINGLE_SELECTION); + setShowHorizontalLines(false); + + var model = new StoredCalculationTableModel(); + setModel(model); + + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); + + setAutoCreateRowSorter(false); + initListeners(); + + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener(e -> { + + if(e.getState() == TaskRepositoryEvent.State.TASK_MDOEL_SWITCH) { + var t = instance.getTask(e.getId()); + identifySelection(t); + } + + }); + } + + public void update(SearchTask t) { + ((StoredCalculationTableModel)getModel()).update(t); + identifySelection(t); + } + + public void identifySelection(SearchTask t) { + int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); + this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + } + + public void initListeners() { + + /* + * selection listener + */ + + var lsm = getSelectionModel(); + + lsm.addListSelectionListener((ListSelectionEvent e) -> { + var task = TaskManager.getManagerInstance().getSelectedTask(); + if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { + var id = lsm.getMinSelectionIndex(); + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + } + }); + + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + return taskTableRenderer; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 5de38f46..fdc5cd6c 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -7,7 +7,6 @@ import static java.awt.Color.GRAY; import static java.awt.Color.GREEN; import static java.awt.Color.black; -import static java.awt.Color.white; import static java.awt.Font.PLAIN; import static java.lang.String.format; import static java.util.Objects.requireNonNull; @@ -21,6 +20,8 @@ import java.awt.Font; import java.awt.Stroke; +import javax.swing.UIManager; + import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; @@ -38,6 +39,7 @@ import pulse.HeatingCurve; import pulse.input.ExperimentalData; import pulse.input.IndexRange; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; public class Chart { @@ -65,6 +67,7 @@ public Chart() { setFonts(); chart.removeLegend(); + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); chartPanel = new ChartPanel(chart); } @@ -78,7 +81,7 @@ private void setFonts() { } private void setBackgroundAndGrid() { - plot.setBackgroundPaint(white); + //plot.setBackgroundPaint(UIManager.getColor("Panel.background")); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(GRAY); @@ -121,11 +124,11 @@ private void setRenderers() { rendererOld.setSeriesStroke(0, new BasicStroke(2.0f, CAP_BUTT, JOIN_MITER, 2.0f, new float[] { 10f }, 0)); rendererOld.setSeriesShapesVisible(0, false); - plot.setRenderer(0, renderer); - plot.setRenderer(1, rendererLines); - plot.setRenderer(2, rendererOld); - plot.setRenderer(3, rendererResiduals); - plot.setRenderer(4, rendererClassic); + plot.setRenderer(0, rendererLines); + plot.setRenderer(1, rendererResiduals); + plot.setRenderer(2, rendererClassic); + plot.setRenderer(3, renderer); + } private void adjustAxisLabel(double maximum) { @@ -140,7 +143,7 @@ private void adjustAxisLabel(double maximum) { public void plot(SearchTask task, boolean extendedCurve) { requireNonNull(task); - + var plot = chart.getXYPlot(); for (int i = 0; i < 6; i++) @@ -156,7 +159,8 @@ public void plot(SearchTask task, boolean extendedCurve) { var rawDataset = new XYSeriesCollection(); rawDataset.addSeries(series(rawData, "Raw data (" + task.getIdentifier() + ")", extendedCurve)); - plot.setDataset(0, rawDataset); + plot.setDataset(3, rawDataset); + plot.getRenderer(3).setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); plot.clearDomainMarkers(); @@ -174,41 +178,43 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.addDomainMarker(upperMarker); plot.addDomainMarker(lowerMarker); - var problem = task.getProblem(); + var calc = task.getCurrentCalculation(); + var problem = calc.getProblem(); if (problem != null) { var solution = problem.getHeatingCurve(); - - if (solution != null && task.getScheme() != null && solution.actualNumPoints() > 0) { + var scheme = calc.getScheme(); + + if (solution != null && scheme != null && solution.actualNumPoints() > 0) { var solutionDataset = new XYSeriesCollection(); var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; solutionDataset.addSeries( - series(displayedCurve, "Solution with " + task.getScheme().getSimpleName(), extendedCurve)); - plot.setDataset(1, solutionDataset); + series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); + plot.setDataset(0, solutionDataset); /* * plot residuals */ - if (residualsShown) - if (task.getResidualStatistic().getResiduals() != null) { - var residualsDataset = new XYSeriesCollection(); - residualsDataset.addSeries(residuals(task)); - plot.setDataset(3, residualsDataset); + if (residualsShown) { + var residuals = calc.getOptimiserStatistic().getResiduals(); + if (residuals != null && residuals.size() > 0) { + var residualsDataset = new XYSeriesCollection(); + residualsDataset.addSeries(residuals(calc)); + plot.setDataset(1, residualsDataset); } + } } - plot.getRenderer().setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); - } if (zeroApproximationShown) { - var p = task.getProblem(); - var s = task.getScheme(); + var p = calc.getProblem(); + var s = calc.getScheme(); if (p != null && s != null) plotSingle(classicSolution(p, (double) (s.getTimeLimit().getValue()))); @@ -225,8 +231,8 @@ public void plotSingle(HeatingCurve curve) { classicDataset.addSeries(series(curve, curve.getName(), false)); - plot.setDataset(4, classicDataset); - plot.getRenderer(4).setSeriesPaint(0, black); + plot.setDataset(2, classicDataset); + plot.getRenderer(2).setSeriesPaint(0, black); } public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { @@ -253,17 +259,18 @@ private XYSeries series(AbstractData curve, String title, final double startTime return series; } - public XYSeries residuals(SearchTask task) { - var problem = task.getProblem(); + public XYSeries residuals(Calculation calc) { + var problem = calc.getProblem(); var baseline = problem.getBaseline(); + + var residuals = calc.getOptimiserStatistic().getResiduals(); + var size = residuals.size(); + final var span = problem.getHeatingCurve().maxAdjustedSignal() - baseline.valueAt(0); final var offset = baseline.valueAt(0) - span / 2.0; var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); - - var residuals = task.getResidualStatistic().getResiduals(); - var size = residuals.size(); - + for (var i = 0; i < size; i++) { series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); } diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index 3f1b7079..f252b9f1 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -106,7 +106,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = task.getProblem(); + var p = task.getCurrentCalculation().getProblem(); if (p != null) p.retrieveData(data); progressFrame.incrementProgress(); diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index 6a77c151..aeb57e5f 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -32,7 +32,7 @@ public class LogPane extends JEditorPane implements Descriptive { private ExecutorService updateExecutor = newSingleThreadExecutor(); - private final static boolean DEBUG = false; + private final static boolean DEBUG = true; private PrintStream outStream, errStream; @@ -122,7 +122,7 @@ public void printAll() { log.getLogEntries().stream().forEach(entry -> post(entry)); - if (task.getStatus() == DONE) + if (task.getCurrentCalculation().getStatus() == DONE) printTimeTaken(log); } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java new file mode 100644 index 00000000..a98aa6e5 --- /dev/null +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -0,0 +1,111 @@ +package pulse.ui.components; + +import static pulse.tasks.TaskManager.getManagerInstance; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import pulse.problem.statements.Problem; +import pulse.problem.statements.ProblemComplexity; +import pulse.ui.components.listeners.ProblemSelectionEvent; +import pulse.ui.components.listeners.ProblemSelectionListener; + +@SuppressWarnings("serial") +public class ProblemTree extends JTree { + + private List selectionListeners; + + public ProblemTree(List allProblems) { + super(); + var root = new DefaultMutableTreeNode("Problem Statements"); + + for (var c : ProblemComplexity.values()) { + var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); + + allProblems.stream().filter(p -> p.getComplexity() == c) + .forEach(pFiltered -> { + var node = new DefaultMutableTreeNode(pFiltered); + currentComplexity.add(node); + }); + + root.add(currentComplexity); + + } + + var model = (DefaultTreeModel)this.getModel(); + model.setRoot(root); + + for (int i = 0; i < getRowCount(); i++) { + expandRow(i); + } + + this.setRootVisible(false); + + selectionListeners = new ArrayList<>(); + this.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + + addListeners(); + } + + private void addListeners() { + var instance = getManagerInstance(); + + addTreeSelectionListener(e -> { + var object = ( (DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); + if(object instanceof Problem) + fireProblemSelection(new ProblemSelectionEvent((Problem)object, this)); + }); + + instance.addSelectionListener(e -> { + var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); + // select appropriate problem type from list + + setSelectedProblem(current); + fireProblemSelection(new ProblemSelectionEvent(current, instance)); + + }); + + } + + public void setSelectedProblem(Problem p) { + if(p == null) + return; + + var model = this.getModel(); + var root = model.getRoot(); + TreePath path = null; + + outer: for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { + var child = model.getChild(model.getRoot(), i); + + for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { + var node = (DefaultMutableTreeNode) model.getChild(child, j); + var problem = (Problem)node.getUserObject(); + if (p.getClass().equals(problem.getClass())) { + path = new TreePath(new Object[] { root, child, node }); + break outer; + } + } + + } + + this.setSelectionPath(path); + + } + + public void addProblemSelectionListener(ProblemSelectionListener l) { + selectionListeners.add(l); + } + + private void fireProblemSelection(ProblemSelectionEvent e) { + for (var l : selectionListeners) + l.onProblemSelected(e); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index de05925a..cfda35da 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -1,11 +1,9 @@ package pulse.ui.components; -import static java.awt.Font.BOLD; import static java.lang.Boolean.TRUE; import static javax.swing.SortOrder.ASCENDING; import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.event.ItemEvent; import java.util.ArrayList; import java.util.List; @@ -39,7 +37,6 @@ public class PropertyHolderTable extends JTable { private PropertyHolder propertyHolder; - private final static Font font = new Font(getString("PropertyHolderTable.FontName"), BOLD, 12); private final static int ROW_HEIGHT = 40; public PropertyHolderTable(PropertyHolder p) { @@ -55,7 +52,6 @@ public PropertyHolderTable(PropertyHolder p) { setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); setShowGrid(false); - setFont(font); setRowHeight(ROW_HEIGHT); var list = new ArrayList(); diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index cd31f3e2..6c6329db 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -50,7 +50,7 @@ private void setLegendTitle() { var plot = getPlot(); var lt = new LegendTitle(plot); lt.setItemFont(new Font("Dialog", PLAIN, 16)); - lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + //lt.setBackgroundPaint(new Color(200, 200, 255, 100)); lt.setFrame(new BlockBorder(black)); lt.setPosition(RectangleEdge.RIGHT); var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index abd92ec7..202b2db7 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; import static java.io.File.separator; import static javax.swing.JFileChooser.APPROVE_OPTION; import static javax.swing.JFileChooser.DIRECTORIES_ONLY; @@ -15,7 +14,7 @@ import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.search.statistics.CorrelationTest.setThreshold; import static pulse.search.statistics.NormalityTest.setStatisticalSignificance; -import static pulse.search.statistics.ResidualStatistic.setSelectedOptimiserDescriptor; +import static pulse.search.statistics.OptimiserStatistic.setSelectedOptimiserDescriptor; import static pulse.tasks.TaskManager.getManagerInstance; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; import static pulse.ui.Launcher.loadIcon; @@ -23,7 +22,6 @@ import static pulse.ui.components.DataLoader.loadMetadataDialog; import static pulse.util.Reflexive.allDescriptors; -import java.awt.Font; import java.io.File; import java.util.ArrayList; import java.util.List; @@ -39,7 +37,7 @@ import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; -import pulse.search.statistics.ResidualStatistic; +import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; @@ -154,11 +152,6 @@ private void initComponents() { modelSettingsItem.setEnabled(false); searchSettingsItem.setEnabled(false); - var menuFont = new Font("Arial", PLAIN, 18); - fileMenu.setFont(menuFont); - settingsMenu.setFont(menuFont); - infoMenu.setFont(menuFont); - fileMenu.add(loadDataItem); fileMenu.add(loadMetadataItem); fileMenu.add(new JSeparator()); @@ -227,8 +220,7 @@ private JMenu initAnalysisSubmenu() { item = null; - var set = allDescriptors(ResidualStatistic.class); - set.removeAll(allDescriptors(NormalityTest.class)); + var set = allDescriptors(OptimiserStatistic.class); for (var statisticName : set) { item = new JRadioButtonMenuItem(statisticName); @@ -239,7 +231,7 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); } }); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 3a9cd4de..9875390e 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; import static java.lang.Math.abs; import static java.util.stream.Collectors.toList; import static javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION; @@ -8,9 +7,7 @@ import static javax.swing.SwingConstants.TOP; import static javax.swing.SwingUtilities.invokeLater; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Comparator; @@ -39,8 +36,6 @@ @SuppressWarnings("serial") public class ResultTable extends JTable implements Descriptive { - private final static Font font = new Font(getString("ResultTable.FontName"), PLAIN, 12); - private final static int ROW_HEIGHT = 25; private final static int RESULTS_HEADER_HEIGHT = 30; @@ -62,8 +57,6 @@ public ResultTable(ResultFormat fmt) { setShowHorizontalLines(false); setFillsViewportHeight(true); - getTableHeader().setFont(font); - setSelectionMode(SINGLE_INTERVAL_SELECTION); setRowSelectionAllowed(false); setColumnSelectionAllowed(true); diff --git a/src/main/java/pulse/ui/components/TaskBox.java b/src/main/java/pulse/ui/components/TaskBox.java index 253844a7..416d9b12 100644 --- a/src/main/java/pulse/ui/components/TaskBox.java +++ b/src/main/java/pulse/ui/components/TaskBox.java @@ -17,8 +17,6 @@ @SuppressWarnings("serial") public class TaskBox extends JComboBox { - private final static int FONT_SIZE = 12; - public TaskBox() { super(); @@ -49,7 +47,6 @@ public TaskBox() { public void init() { setMaximumSize(new Dimension(32767, 24)); - setFont(getFont().deriveFont(FONT_SIZE)); setMinimumSize(new Dimension(250, 20)); setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ setBackground(WHITE); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 83a4095b..2f0fdff9 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; import static java.lang.System.err; import static java.lang.System.lineSeparator; import static javax.swing.JOptionPane.ERROR_MESSAGE; @@ -10,6 +9,7 @@ import static javax.swing.JOptionPane.showConfirmDialog; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; import static pulse.tasks.logs.Details.NONE; @@ -21,7 +21,6 @@ import static pulse.ui.frames.MainGraphFrame.getChart; import java.awt.Component; -import java.awt.Font; import java.awt.event.ActionEvent; import javax.swing.ImageIcon; @@ -33,12 +32,13 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.logs.Status; import pulse.tasks.processing.Result; @SuppressWarnings("serial") public class TaskPopupMenu extends JPopupMenu { - - private final static Font f = new Font(getString("TaskTable.FontName"), PLAIN, 16); //$NON-NLS-1$ + + private JMenuItem itemViewStored; private final static int ICON_SIZE = 24; @@ -48,18 +48,17 @@ public class TaskPopupMenu extends JPopupMenu { private static ImageIcon ICON_RUN = loadIcon("execute_single.png", ICON_SIZE); private static ImageIcon ICON_RESET = loadIcon("reset.png", ICON_SIZE); private static ImageIcon ICON_RESULT = loadIcon("result.png", ICON_SIZE); + private static ImageIcon ICON_STORED = loadIcon("stored.png", ICON_SIZE); public TaskPopupMenu() { var referenceWindow = getWindowAncestor(this); var itemChart = new JMenuItem(getString("TaskTablePopupMenu.ShowHeatingCurve"), ICON_GRAPH); //$NON-NLS-1$ itemChart.addActionListener(e -> plot(false)); - itemChart.setFont(f); var itemExtendedChart = new JMenuItem(getString("TaskTablePopupMenu.ShowExtendedHeatingCurve"), //$NON-NLS-1$ ICON_GRAPH); itemExtendedChart.addActionListener(e -> plot(true)); - itemExtendedChart.setFont(f); var instance = TaskManager.getManagerInstance(); @@ -75,8 +74,6 @@ public TaskPopupMenu() { t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); }); - itemShowMeta.setFont(f); - var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); instance.addSelectionListener(event -> { @@ -90,14 +87,12 @@ public TaskPopupMenu() { itemShowStatus.addActionListener((ActionEvent e) -> { var t = instance.getSelectedTask(); if (t != null) { - var d = t.getStatus().getDetails(); + var d = t.getCurrentCalculation().getStatus().getDetails(); showMessageDialog(getWindowAncestor((Component) e.getSource()), "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); } }); - itemShowStatus.setFont(f); - var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ itemExecute.addActionListener((ActionEvent e) -> { var t = instance.getSelectedTask(); @@ -112,34 +107,31 @@ public TaskPopupMenu() { + getString("TaskTablePopupMenu.AskToDelete"), getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); if (dialogResult == 0) { - instance.removeResult(t); - // t.storeCurrentSolution(); + // instance.removeResult(t); + instance.getSelectedTask().setStatus(Status.READY); instance.execute(instance.getSelectedTask()); } } else if (t.checkProblems() != READY) { showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ + t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ ERROR_MESSAGE); } else instance.execute(instance.getSelectedTask()); }); - itemExecute.setFont(f); var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); - itemReset.setFont(f); itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); - itemGenerateResult.setFont(f); itemGenerateResult.addActionListener((ActionEvent arg0) -> { var t = instance.getSelectedTask(); if (t == null) return; - if (t.getProblem() != null) { + if (t.getCurrentCalculation().getProblem() != null) { var r = new Result(t, getInstance()); instance.useResult(t, r); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); @@ -147,6 +139,13 @@ public TaskPopupMenu() { } }); + itemViewStored = new JMenuItem(getString("TaskTablePopupMenu.ViewStored"), ICON_STORED); + + itemViewStored.setEnabled(false); + + itemViewStored.addActionListener(arg0 -> instance.notifyListeners( + new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier()))); + add(itemShowMeta); add(itemShowStatus); add(new JSeparator()); @@ -155,6 +154,7 @@ public TaskPopupMenu() { add(new JSeparator()); add(itemReset); add(itemGenerateResult); + add(itemViewStored); add(new JSeparator()); add(itemExecute); @@ -169,7 +169,8 @@ public void plot(boolean extended) { getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - var statusDetails = t.getStatus().getDetails(); + var calc = t.getCurrentCalculation(); + var statusDetails = calc.getStatus().getDetails(); if (statusDetails == MISSING_HEATING_CURVE) { @@ -179,10 +180,10 @@ public void plot(boolean extended) { } else { - var scheme = (Solver) t.getScheme(); + var scheme = (Solver) calc.getScheme(); if (scheme != null) { try { - scheme.solve(t.getProblem()); + scheme.solve(calc.getProblem()); } catch (SolverException e) { err.println("Solver error for " + t + "Details: "); e.printStackTrace(); @@ -197,4 +198,8 @@ public void plot(boolean extended) { } + public JMenuItem getItemViewStored() { + return itemViewStored; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index a48e319d..a87cf185 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -44,8 +44,6 @@ public class TaskTable extends JTable { private Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); private Comparator statusComparator = (s1, s2) -> s1.compareTo(s2); - private final static int FONT_SIZE = 14; - public TaskTable() { super(); setDefaultEditor(Object.class, null); @@ -64,8 +62,6 @@ public TaskTable() { setTableHeader(th); - var font = getTableHeader().getFont().deriveFont(FONT_SIZE); - getTableHeader().setFont(font); getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); setAutoCreateRowSorter(true); @@ -92,7 +88,7 @@ public TaskTable() { public void initListeners() { var instance = TaskManager.getManagerInstance(); - + /* * mouse listener */ @@ -102,8 +98,11 @@ public void initListeners() { @Override public void mouseClicked(MouseEvent e) { - if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) + if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { + var task = instance.getSelectedTask(); + menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); menu.show(e.getComponent(), e.getX(), e.getY()); + } } @@ -115,7 +114,7 @@ public void mouseClicked(MouseEvent e) { var lsm = getSelectionModel(); var reference = this; - + lsm.addListSelectionListener((ListSelectionEvent e) -> { if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 4883968e..c28b7738 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -52,7 +52,7 @@ public ExecutionButton() { .filter((t) -> t.checkProblems() == INCOMPLETE).findFirst(); if (problematicTask.isPresent()) { var t = problematicTask.get(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), t + " is " + t.getStatus().getMessage(), + showMessageDialog(getWindowAncestor((Component) e.getSource()), t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", ERROR_MESSAGE); } else { instance.executeAll(); diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 84ea003d..43c222dc 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -1,6 +1,5 @@ package pulse.ui.components.buttons; -import static java.awt.Font.BOLD; import static java.awt.Toolkit.getDefaultToolkit; import static javax.swing.JFileChooser.APPROVE_OPTION; import static javax.swing.JOptionPane.ERROR_MESSAGE; @@ -43,7 +42,7 @@ public LoaderButton(String str) { public void init() { - setFont(getFont().deriveFont(BOLD, 14f)); + //setFont(getFont().deriveFont(BOLD, 14f)); addActionListener((ActionEvent arg0) -> { var fileChooser = new JFileChooser(); diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 134dca97..123202eb 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,16 +1,12 @@ package pulse.ui.components.controllers; -import static java.awt.Color.BLUE; -import static java.awt.Color.LIGHT_GRAY; import static java.awt.Color.RED; -import static java.awt.Color.WHITE; -import static java.awt.Font.BOLD; -import static java.awt.Font.ITALIC; import java.awt.Component; import javax.swing.JButton; import javax.swing.JTable; +import javax.swing.UIManager; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -18,13 +14,9 @@ import pulse.ui.components.buttons.IconCheckBox; import pulse.util.PropertyHolder; +@SuppressWarnings("serial") public class AccessibleTableRenderer extends NumericPropertyRenderer { - /** - * - */ - private static final long serialVersionUID = 2269077862064919825L; - public AccessibleTableRenderer() { super(); } @@ -33,44 +25,38 @@ public AccessibleTableRenderer() { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - - if (value instanceof NumericProperty) { - var renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - renderer.setForeground(BLUE); - return renderer; - } - - if (value instanceof Flag) { - var btn = new IconCheckBox((boolean) ((Property) value).getValue()); - btn.setHorizontalAlignment(CENTER); - if (isSelected) - btn.setBackground(LIGHT_BLUE); - else - btn.setBackground(WHITE); - return btn; + + var selectedBackground = UIManager.getColor("Table.selectionBackground"); + var deselectedBackground = UIManager.getColor("Table.bakground"); + Component renderer = null; + + if (value instanceof NumericProperty) + renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + else if (value instanceof Flag) { + renderer = new IconCheckBox((boolean) ((Property) value).getValue()); + ((IconCheckBox)renderer).setHorizontalAlignment(CENTER); } - if (value instanceof PropertyHolder) { - var button = initButton(value.toString()); - return button; + else if (value instanceof PropertyHolder) + renderer = initButton(value.toString()); + + else if (value instanceof Property) { + renderer = initTextField(value.toString(), table.isRowSelected(row)); + renderer.setForeground(RED); } - - if (value instanceof Property) { - var jtf = initTextField(value.toString(), table.isRowSelected(row)); - jtf.setForeground(RED); - jtf.setFont(jtf.getFont().deriveFont(BOLD)); - return jtf; + + if(renderer != null) { + renderer.setBackground(isSelected ? selectedBackground : deselectedBackground); + return renderer; } - - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + else + return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } private JButton initButton(String str) { var button = new JButton(str); - button.setContentAreaFilled(true); - button.setBackground(LIGHT_GRAY); - button.setFont(button.getFont().deriveFont(12.0f).deriveFont(ITALIC)); button.setToolTipText(str); return button; } diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index a615de3c..8c8f6073 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -1,15 +1,11 @@ package pulse.ui.components.controllers; -import static java.awt.Color.white; -import static java.awt.Font.PLAIN; -import static pulse.ui.Messages.getString; - -import java.awt.Color; import java.awt.Component; -import java.awt.Font; import javax.swing.JFormattedTextField; +import javax.swing.JLabel; import javax.swing.JTable; +import javax.swing.UIManager; import javax.swing.table.DefaultTableCellRenderer; import pulse.properties.NumericProperty; @@ -18,9 +14,6 @@ @SuppressWarnings("serial") public class NumericPropertyRenderer extends DefaultTableCellRenderer { - protected static final Color LIGHT_BLUE = new Color(175, 238, 238); - private final static Font font = new Font(getString("PropertyHolderTable.FontName"), PLAIN, 14); - public NumericPropertyRenderer() { super(); } @@ -30,26 +23,36 @@ public NumericPropertyRenderer() { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - if (value instanceof NumericProperty) - return initTextField(((Property) value).formattedOutput(), table.isRowSelected(row)); + Component result = null; - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof NumericProperty) { + var output = ((Property) value).formattedOutput(); + result = table.getEditorComponent() != null ? + initTextField(output, table.isRowSelected(row)) + : initLabel(output, table.isRowSelected(row)); + } else if(value instanceof Number) { + result = initLabel(value.toString(), table.isRowSelected(row)); + } else + result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + return result; } protected static JFormattedTextField initTextField(String text, boolean rowSelected) { var jtf = new JFormattedTextField(text); - jtf.setOpaque(true); - jtf.setBorder(null); jtf.setHorizontalAlignment(CENTER); - jtf.setFont(font); - - if (rowSelected) - jtf.setBackground(LIGHT_BLUE); - else - jtf.setBackground(white); - + jtf.setBackground( + rowSelected ? UIManager.getColor("Table.selectionBackground") : UIManager.getColor("Table.background")); return jtf; } + protected static JLabel initLabel(String text, boolean rowSelected) { + var lab = new JLabel(text); + lab.setHorizontalAlignment(CENTER); + lab.setBackground( + rowSelected ? UIManager.getColor("Table.selectionBackground") : UIManager.getColor("Table.background")); + return lab; + } + } diff --git a/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java deleted file mode 100644 index 06909a2e..00000000 --- a/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java +++ /dev/null @@ -1,57 +0,0 @@ -package pulse.ui.components.controllers; - -import static java.awt.Color.black; -import static java.awt.Font.BOLD; -import static javax.swing.BorderFactory.createTitledBorder; - -import java.awt.Color; -import java.awt.Component; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.JComponent; -import javax.swing.JList; - -import pulse.problem.statements.Problem; - -@SuppressWarnings("serial") -public class ProblemListCellRenderer extends DefaultListCellRenderer { - - public ProblemListCellRenderer() { - super(); - } - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - - var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - var complexity = ((Problem) value).getComplexity(); - var color = blend(renderer.getBackground(), complexity.getColor(), 15); - - if (isSelected) { - color = color.darker(); - renderer.setFont(renderer.getFont().deriveFont(BOLD)); - } - - renderer.setForeground(black); - renderer.setBackground(color); - - var border = createTitledBorder("Complexity: " + complexity); - border.setTitleColor(complexity.getColor().darker().darker()); - ((JComponent) renderer).setBorder(border); - return renderer; - - } - - private static Color blend(Color c0, Color c1, int alpha) { - double totalAlpha = c0.getAlpha() + c1.getAlpha(); - var weight0 = c0.getAlpha() / totalAlpha; - var weight1 = c1.getAlpha() / totalAlpha; - var r = weight0 * c0.getRed() + weight1 * c1.getRed(); - var g = weight0 * c0.getGreen() + weight1 * c1.getGreen(); - var b = weight0 * c0.getBlue() + weight1 * c1.getBlue(); - - return new Color((int) r, (int) g, (int) b, alpha); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index 208b47fe..893e28a8 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -9,24 +9,16 @@ import javax.swing.JComponent; import javax.swing.JList; +@SuppressWarnings("serial") public class SearchListRenderer extends DefaultListCellRenderer { - /** - * - */ - private static final long serialVersionUID = 1L; - @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - ((JComponent) renderer).setOpaque(true); - - if (isSelected) - renderer.setBackground(new Color(51, 102, 153, 210)); + renderer.setForeground(isSelected ? Color.DARK_GRAY : Color.white); return renderer; diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index b249e2c6..ff2b4ec4 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -10,6 +10,7 @@ import pulse.properties.Property; import pulse.tasks.Identifier; import pulse.tasks.logs.Status; +import pulse.util.PropertyHolder; @SuppressWarnings("serial") public class TaskTableRenderer extends NumericPropertyRenderer { @@ -27,17 +28,21 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); else if (value instanceof Identifier) - return initTextField("" + ((Property) value).getValue(), table.isRowSelected(row)); + return initLabel("" + ((Property) value).getValue(), table.isRowSelected(row)); else if (value instanceof Status) { - var jtf = initTextField(value.toString(), table.isRowSelected(row)); - jtf.setForeground(((Status) value).getColor()); - jtf.setFont(jtf.getFont().deriveFont(BOLD)); + var lab = initLabel(value.toString(), table.isRowSelected(row)); + lab.setForeground(((Status) value).getColor()); + lab.setFont(lab.getFont().deriveFont(BOLD)); - return jtf; + return lab; } + + else if(value instanceof PropertyHolder) { + return initLabel("" + ((PropertyHolder)value).describe(), table.isRowSelected(row)); + } return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); diff --git a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java index 8f14c00b..8415432f 100644 --- a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java @@ -3,7 +3,6 @@ public interface FrameVisibilityRequestListener { public void onProblemStatementShowRequest(); - public void onSearchSettingsShowRequest(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java new file mode 100644 index 00000000..93c7a045 --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java @@ -0,0 +1,31 @@ +package pulse.ui.components.listeners; + +import pulse.problem.statements.Problem; + +public class ProblemSelectionEvent { + + private Problem problem; + private Object source; + + public ProblemSelectionEvent(Problem problem, Object source) { + this.problem = problem; + this.source = source; + } + + public Problem getProblem() { + return problem; + } + + public void setProblem(Problem problem) { + this.problem = problem; + } + + public Object getSource() { + return source; + } + + public void setSource(Object source) { + this.source = source; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java new file mode 100644 index 00000000..8e975ed9 --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java @@ -0,0 +1,7 @@ +package pulse.ui.components.listeners; + +public interface ProblemSelectionListener { + + public void onProblemSelected(ProblemSelectionEvent e); + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java new file mode 100644 index 00000000..07fca65a --- /dev/null +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -0,0 +1,51 @@ +package pulse.ui.components.models; + +import static javax.swing.SwingUtilities.invokeLater; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; + +import javax.swing.table.DefaultTableModel; + +import pulse.tasks.Calculation; +import pulse.tasks.SearchTask; + +@SuppressWarnings("serial") +public class StoredCalculationTableModel extends DefaultTableModel { + + public static final int WEIGHT_COLUMN = 5; + public static final int STATUS_COLUMN = 4; + public static final int MODEL_STATISTIC_COLUMN = 3; + public static final int OPTIMISER_STATISTIC_COLUMN = 2; + public static final int BASELINE_COLUMN = 1; + public static final int PROBLEM_COLUMN = 0; + + public StoredCalculationTableModel() { + + super(new Object[][] {}, + new String[] { "Problem Statement", "Baseline", "Parameter count", + "Optimiser Statistic", "Model Selection Statistic", + def(MODEL_WEIGHT).getAbbreviation(true) }); + + } + + public void update(SearchTask t) { + super.setRowCount(0); + var list = t.getStoredCalculations(); + + for(Calculation c : list) { + var problem = c.getProblem(); + var baseline = c.getProblem().getBaseline(); + var optimiser = c.getOptimiserStatistic(); + var criterion = c.getModelSelectionCriterion(); + var parameters = c.getModelSelectionCriterion().getNumVariables(); + + var weight = c.weight(list); + + var data = new Object[] { problem, baseline, parameters, optimiser.getStatistic(), criterion.getStatistic(), weight }; + + invokeLater(() -> super.addRow(data)); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 2826bf58..5948d1c1 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -50,8 +50,8 @@ else if (e.getState() == TASK_ADDED) public void addTask(SearchTask t) { var temperature = t.getExperimentalCurve().getMetadata().numericProperty(TEST_TEMPERATURE); - var data = new Object[] { t.getIdentifier(), temperature, t.getResidualStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getStatus() }; + var data = new Object[] { t.getIdentifier(), temperature, t.getCurrentCalculation().getModelSelectionCriterion().getStatistic(), + t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus() }; invokeLater(() -> super.addRow(data)); @@ -62,7 +62,7 @@ public void addTask(SearchTask t) { }); t.addTaskListener((LogEntry e) -> { - setValueAt(t.getResidualStatistic().getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); + setValueAt(t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); }); } diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 3299d137..fc5483b9 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -70,11 +70,11 @@ public void initComponents() { var task = instance.getSelectedTask(); - if(task != null && task.getResidualStatistic() != null) { + if(task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { chFrame.setLocationRelativeTo(null); chFrame.setVisible(true); - chFrame.plot(task.getResidualStatistic()); + chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); } diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java new file mode 100644 index 00000000..f2027a08 --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -0,0 +1,53 @@ +package pulse.ui.components.panels; + +import static pulse.ui.Launcher.loadIcon; +import static pulse.util.Reflexive.allDescriptors; + +import java.awt.Dimension; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JToolBar; + +import pulse.search.statistics.ModelSelectionCriterion; +import pulse.tasks.TaskManager; + +@SuppressWarnings("serial") +public class ModelToolbar extends JToolBar { + + private final static int ICON_SIZE = 20; + + public ModelToolbar() { + super(); + setFloatable(false); + setRollover(true); + var set = allDescriptors(ModelSelectionCriterion.class); + var criterionSelection = new JComboBox<>(set.toArray(String[]::new)); + criterionSelection.addActionListener(e -> { + ModelSelectionCriterion.setSelectedCriterionDescriptor(criterionSelection.getSelectedItem().toString()); + } + ); + criterionSelection.setSelectedIndex(0); + + this.setBorder(BorderFactory.createEtchedBorder()); + + add(new JLabel("Model Selection Criterion: ")); + add(Box.createRigidArea(new Dimension(5,0))); + add(criterionSelection); + + var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE)); + bestSelection.setToolTipText("Select Best Model"); + add(Box.createRigidArea(new Dimension(15,0))); + add(bestSelection); + + bestSelection.addActionListener(e -> { + var t = TaskManager.getManagerInstance().getSelectedTask(); + t.switchToBestModel(); + }); + + } + +} diff --git a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java index 1a64814e..9ef76689 100644 --- a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java +++ b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java @@ -1,11 +1,9 @@ package pulse.ui.components.panels; -import static java.awt.Font.PLAIN; import static java.awt.GridBagConstraints.BOTH; import static javax.swing.Box.createHorizontalStrut; import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -25,8 +23,6 @@ public class SettingsToolBar extends JToolBar { private JCheckBox cbSingleStatement, cbHideDetails; - private Font f = new Font(getString("TaskSelectionToolBar.FontName"), PLAIN, 14); //$NON-NLS-1$ - public SettingsToolBar(PropertyHolderTable... tables) { super(); setFloatable(false); @@ -35,11 +31,9 @@ public SettingsToolBar(PropertyHolderTable... tables) { cbSingleStatement = new JCheckBox(getString("TaskSelectionToolBar.ApplyToAll")); //$NON-NLS-1$ cbSingleStatement.setSelected(TaskManager.getManagerInstance().isSingleStatement()); - cbSingleStatement.setFont(f); cbHideDetails = new JCheckBox(getString("TaskSelectionToolBar.Hide")); //$NON-NLS-1$ cbHideDetails.setSelected(true); - cbHideDetails.setFont(f); setLayout(new GridBagLayout()); diff --git a/src/main/java/pulse/ui/components/panels/SystemPanel.java b/src/main/java/pulse/ui/components/panels/SystemPanel.java index 485eecff..34395750 100644 --- a/src/main/java/pulse/ui/components/panels/SystemPanel.java +++ b/src/main/java/pulse/ui/components/panels/SystemPanel.java @@ -1,8 +1,6 @@ package pulse.ui.components.panels; -import static java.awt.Color.black; import static java.awt.Color.red; -import static java.awt.Color.yellow; import static java.lang.String.format; import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; import static java.util.concurrent.TimeUnit.SECONDS; @@ -13,11 +11,13 @@ import static pulse.ui.Launcher.getMemoryUsage; import static pulse.ui.Launcher.threadsAvailable; +import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.UIManager; @SuppressWarnings("serial") public class SystemPanel extends JPanel { @@ -63,7 +63,8 @@ private void startSystemMonitors() { coresLabel.setText(coresAvailable); var executor = newSingleThreadScheduledExecutor(); - + var defColor = UIManager.getColor("Label.foreground"); + Runnable periodicTask = () -> { var cpuUsage = cpuUsage(); var memoryUsage = getMemoryUsage(); @@ -71,26 +72,39 @@ private void startSystemMonitors() { cpuLabel.setText(cpuString); var memoryString = format("Memory usage: %3.1f%%", memoryUsage); memoryLabel.setText(memoryString); - if (cpuUsage > 75) { - cpuLabel.setForeground(red); - } else if (cpuUsage > 50) { - cpuLabel.setForeground(yellow); - } else { - cpuLabel.setForeground(black); - } - /* - * - */ - if (memoryUsage > 75) { - memoryLabel.setForeground(red); - } else if (memoryUsage > 50) { - memoryLabel.setForeground(yellow); - } else { - memoryLabel.setForeground(black); - } + + cpuLabel.setForeground(blend(defColor, red, (float)cpuUsage/100)); + memoryLabel.setForeground(blend(defColor, red, (float)memoryUsage/100)); + }; executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); } + + private Color blend( Color c1, Color c2, float ratio ) { + if ( ratio > 1f ) ratio = 1f; + else if ( ratio < 0f ) ratio = 0f; + float iRatio = 1.0f - ratio; + + int i1 = c1.getRGB(); + int i2 = c2.getRGB(); + + int a1 = (i1 >> 24 & 0xff); + int r1 = ((i1 & 0xff0000) >> 16); + int g1 = ((i1 & 0xff00) >> 8); + int b1 = (i1 & 0xff); + + int a2 = (i2 >> 24 & 0xff); + int r2 = ((i2 & 0xff0000) >> 16); + int g2 = ((i2 & 0xff00) >> 8); + int b2 = (i2 & 0xff); + + int a = (int)((a1 * iRatio) + (a2 * ratio)); + int r = (int)((r1 * iRatio) + (r2 * ratio)); + int g = (int)((g1 * iRatio) + (g2 * ratio)); + int b = (int)((b1 * iRatio) + (b2 * ratio)); + + return new Color( a << 24 | r << 16 | g << 8 | b ); + } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/DataFrame.java b/src/main/java/pulse/ui/frames/DataFrame.java index 34803b1d..dcc26c5f 100644 --- a/src/main/java/pulse/ui/frames/DataFrame.java +++ b/src/main/java/pulse/ui/frames/DataFrame.java @@ -2,13 +2,10 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.Color.BLACK; -import static java.awt.Font.PLAIN; import static java.awt.Window.Type.UTILITY; -import static pulse.ui.Messages.getString; import java.awt.BorderLayout; import java.awt.Component; -import java.awt.Font; import javax.swing.JFrame; import javax.swing.JPanel; @@ -24,7 +21,6 @@ public class DataFrame extends JFrame { private PropertyHolderTable dataTable; private Component ancestorFrame; private PropertyHolder dataObject; - private final static Font TABLE_FONT = new Font(getString("DataFrame.FontName"), PLAIN, 16); private final static int ROW_HEIGHT = 70; @Override @@ -64,7 +60,6 @@ public DataFrame(PropertyHolder dataObject, Component ancestor) { contentPane.add(scrollPane, CENTER); dataTable = new PropertyHolderTable(dataObject); - dataTable.setFont(TABLE_FONT); dataTable.setRowHeight(ROW_HEIGHT); setBounds(100, 100, 600, 450); diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java index a9369cb1..9e347d27 100644 --- a/src/main/java/pulse/ui/frames/HistogramFrame.java +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -29,7 +29,7 @@ public HistogramFrame(AuxPlotter chart, int width, int height getContentPane().add(panel, SOUTH); slider.addChangeListener(e -> { ((ResidualsChart)chart).setBinCount(slider.getValue()); - plot(TaskManager.getManagerInstance().getSelectedTask().getResidualStatistic() ); + plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic() ); info.setText("Number of bins: " + slider.getValue()); }); } diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java new file mode 100644 index 00000000..f9f4b0ca --- /dev/null +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -0,0 +1,38 @@ +package pulse.ui.frames; + +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JInternalFrame; +import javax.swing.JScrollPane; + +import pulse.tasks.TaskManager; +import pulse.ui.components.CalculationTable; +import pulse.ui.components.panels.ModelToolbar; + +@SuppressWarnings("serial") +public class ModelSelectionFrame extends JInternalFrame { + private CalculationTable table; + + public ModelSelectionFrame() { + super("Model Comparison", true, true, true, true); + table = new CalculationTable(); + getContentPane().add(new JScrollPane(table)); + setSize(new Dimension(400, 400)); + setTitle("Stored Calculations"); + getContentPane().add(new ModelToolbar(), BorderLayout.SOUTH); + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener(e-> { + if(e.getState() == TASK_BROWSING_REQUEST) + table.update(instance.getTask( e.getId() ) ); + }); + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + } + + public CalculationTable getTable() { + return table; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index e842d0b4..fa654899 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -7,7 +7,6 @@ import static java.awt.Color.BLUE; import static java.awt.Color.GRAY; import static java.awt.Color.RED; -import static java.awt.Color.white; import static org.jfree.chart.ChartFactory.createScatterPlot; import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -29,6 +28,7 @@ import javax.swing.JSeparator; import javax.swing.JToggleButton; import javax.swing.JToolBar; +import javax.swing.UIManager; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; @@ -201,7 +201,6 @@ private static ChartPanel createEmptyPanel() { plot.setRenderer(0, renderer); plot.setRenderer(1, rendererSpline); - plot.setBackgroundPaint(white); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(GRAY); @@ -220,6 +219,8 @@ private static ChartPanel createEmptyPanel() { cp.setMaximumDrawWidth(2000); cp.setMinimumDrawWidth(10); cp.setMinimumDrawHeight(10); + + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); return cp; } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 7724d100..f0c8bbd4 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -3,7 +3,6 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.NORTH; import static java.awt.BorderLayout.SOUTH; -import static java.awt.Font.BOLD; import static java.awt.Toolkit.getDefaultToolkit; import static java.lang.System.err; import static javax.swing.BorderFactory.createLineBorder; @@ -29,12 +28,9 @@ import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.List; import javax.swing.DefaultListModel; -import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JInternalFrame; import javax.swing.JList; @@ -43,6 +39,8 @@ import javax.swing.JToolBar; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreePath; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.Solver; @@ -50,24 +48,23 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; import pulse.tasks.SearchTask; -import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; +import pulse.ui.Launcher; +import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.PulseChart; import pulse.ui.components.buttons.LoaderButton; -import pulse.ui.components.controllers.ProblemListCellRenderer; import pulse.ui.components.panels.SettingsToolBar; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { - + private InternalGraphFrame pulseFrame; - + private PropertyHolderTable problemTable, schemeTable; private SchemeSelectionList schemeSelectionList; - private ProblemList problemList; - - private final static int LIST_FONT_SIZE = 12; + private ProblemTree problemList; + private final static List knownProblems = instancesOf(Problem.class); /** @@ -102,9 +99,45 @@ public ProblemStatementFrame() { * Problem selection list and scroller */ - problemList = new ProblemList(); + problemList = new ProblemTree(knownProblems); contentPane.add(new JScrollPane(problemList)); + var instance = getManagerInstance(); + + problemList.addProblemSelectionListener(e -> { + + var newlySelectedProblem = e.getProblem(); + + if (newlySelectedProblem == null) { + + ((DefaultTableModel) problemTable.getModel()).setRowCount(0); + + } + + else { + + var selectedTask = instance.getSelectedTask(); + + if (e.getSource() != instance) { + if (instance.isSingleStatement()) + instance.getTaskList().stream().forEach(t -> changeProblem(t, newlySelectedProblem)); + else + changeProblem(selectedTask, newlySelectedProblem); + } + + problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); + // after problem is selected for this task, show available difference schemes + var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + schemes.forEach(s -> defaultModel.addElement(s)); + selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); + schemeSelectionList.setToolTipText(null); + + } + + }); + /* * Scheme list and scroller */ @@ -127,7 +160,7 @@ public ProblemStatementFrame() { * Scheme details table and scroller */ - schemeTable = new PropertyHolderTable(null); + schemeTable = new PropertyHolderTable(null); var schemeDetailsScroller = new JScrollPane(schemeTable); contentPane.add(schemeDetailsScroller); @@ -140,37 +173,37 @@ public ProblemStatementFrame() { toolBar.setLayout(new GridLayout()); var btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ - btnSimulate.setFont(btnSimulate.getFont().deriveFont(BOLD, 14f)); - var instance = getManagerInstance(); - pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - + pulseFrame.setFrameIcon(Launcher.loadIcon("pulse.png", 20)); + pulseFrame.setVisible(false); + // simulate btn listener btnSimulate.addActionListener((ActionEvent arg0) -> { var t = instance.getSelectedTask(); if (t == null) return; + var calc = t.getCurrentCalculation(); if (t.checkProblems() == INCOMPLETE) { - var d = t.getStatus().getDetails(); + var d = calc.getStatus().getDetails(); if (d == MISSING_PROBLEM_STATEMENT || d == MISSING_DIFFERENCE_SCHEME || d == INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), t.getStatus().getMessage(), + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); return; } } try { - ((Solver) t.getScheme()).solve(t.getProblem()); + ((Solver) calc.getScheme()).solve(calc.getProblem()); } catch (SolverException e) { err.println("Solver of " + t + " has encountered an error. Details: "); e.printStackTrace(); } MainGraphFrame.getInstance().plot(); - pulseFrame.plot( instance.getSelectedTask().getProblem().getPulse() ); + pulseFrame.plot(calc.getProblem().getPulse()); problemTable.updateTable(); schemeTable.updateTable(); }); @@ -185,32 +218,38 @@ public ProblemStatementFrame() { btnLoadDensity.setDataType(DENSITY); toolBar.add(btnLoadDensity); - problemList.setSelectionModel(new DefaultListSelectionModel() { + problemList.setSelectionModel(new DefaultTreeSelectionModel() { @Override - public void setSelectionInterval(int index0, int index1) { - if (index0 != index1) - return; + public void setSelectionPath(TreePath path) { + var object = path.getLastPathComponent(); - var problem = knownProblems.get(index0); - var enabledFlag = problem.isEnabled(); + if (!(object instanceof Problem)) + super.setSelectionPath(path); - if (enabledFlag) { - super.setSelectionInterval(index0, index0); - problemList.ensureIndexIsVisible(index0); + else { - if (!problem.isReady()) { - var bred = new Color(1.0f, 0.0f, 0.0f, 0.35f); - btnLoadDensity.setBorder(createLineBorder(bred, 3)); - btnLoadCv.setBorder(createLineBorder(bred, 3)); - } else { - btnLoadDensity.setBorder(null); - btnLoadCv.setBorder(null); - } + var problem = (Problem) object; + var enabledFlag = problem.isEnabled(); - } else - showMessageDialog(null, "This problem statement is not currently supported. Please select another.", - "Feature not supported", WARNING_MESSAGE); + if (enabledFlag) { + super.setSelectionPath(path); + + if (!problem.isReady()) { + var bred = new Color(1.0f, 0.0f, 0.0f, 0.35f); + btnLoadDensity.setBorder(createLineBorder(bred, 3)); + btnLoadCv.setBorder(createLineBorder(bred, 3)); + } else { + btnLoadDensity.setBorder(null); + btnLoadCv.setBorder(null); + } + + } else + showMessageDialog(null, + "This problem statement is not currently supported. Please select another.", + "Feature not supported", WARNING_MESSAGE); + + } } @@ -228,9 +267,7 @@ public void setSelectionInterval(int index0, int index1) { * listeners */ - instance.addSelectionListener((TaskSelectionEvent e) -> - update(instance.getSelectedTask()) - ); + instance.addSelectionListener((TaskSelectionEvent e) -> update(instance.getSelectedTask())); // TODO getManagerInstance().addHierarchyListener(event -> { @@ -243,7 +280,7 @@ public void setSelectionInterval(int index0, int index1) { Problem p; for (var task : instance.getTaskList()) { - p = task.getProblem(); + p = task.getCurrentCalculation().getProblem(); if (p != null) p.updateProperty(event, event.getProperty()); } @@ -258,18 +295,16 @@ public void update() { private void update(SearchTask selectedTask) { - var selectedProblem = selectedTask == null ? null : selectedTask.getProblem(); - var selectedScheme = selectedTask == null ? null : selectedTask.getScheme(); + var calc = selectedTask.getCurrentCalculation(); + var selectedProblem = selectedTask == null ? null : calc.getProblem(); + var selectedScheme = selectedTask == null ? null : calc.getScheme(); // problem if (selectedProblem == null) problemList.clearSelection(); - else { - setSelectedElement(problemList, selectedProblem); - problemTable.setPropertyHolder(selectedProblem); - - } + else + problemList.setSelectedProblem(selectedProblem); // scheme @@ -283,41 +318,19 @@ private void update(SearchTask selectedTask) { } private void changeProblem(SearchTask task, Problem newProblem) { - var oldProblem = task.getProblem(); // stores previous information - - var problemClass = newProblem.getClass(); - Constructor problemCopyConstructor = null; - - try { - if (newProblem != null) { - problemCopyConstructor = newProblem.getClass().getConstructor(Problem.class); - } - } catch (NoSuchMethodException | SecurityException e) { - err.println(getString("ProblemStatementFrame.ConstructorAccessError") + problemClass); //$NON-NLS-1$ - e.printStackTrace(); - } - - Problem np = null; - - try { - if (problemCopyConstructor != null && oldProblem != null) - np = problemCopyConstructor.newInstance(oldProblem); - else - np = newProblem.getClass().getDeclaredConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - err.println(getString("ProblemStatementFrame.InvocationError") + problemCopyConstructor); //$NON-NLS-1$ - e.printStackTrace(); + var data = task.getExperimentalCurve(); + var calc = task.getCurrentCalculation(); + var oldProblem = calc.getProblem(); // stores previous information + var np = newProblem.copy(); + + if (oldProblem != null) { + np.initProperties(oldProblem.getProperties().copy()); + np.getPulse().initFrom(oldProblem.getPulse()); } - task.setProblem(np); // copies information from old problem to new problem type - - oldProblem = null; - problemCopyConstructor = null; - problemClass = null; + calc.setProblem(np, data); // copies information from old problem to new problem type task.checkProblems(); - } private static void selectDefaultScheme(JList list, Problem p) { @@ -341,21 +354,19 @@ private void changeScheme(SearchTask task, DifferenceScheme newScheme) { // TODO - if (task.getScheme() == null) { - task.setScheme(newScheme.copy()); - // task.getScheme().setTimeLimit( task.getTimeLimit() ); - } + var calc = task.getCurrentCalculation(); + var data = task.getExperimentalCurve(); + + if (calc.getScheme() == null) + calc.setScheme(newScheme.copy(), data); else { - var oldScheme = task.getScheme().copy(); // stores previous information - task.setScheme(null); - task.setScheme(newScheme.copy()); // assigns new problem type + var oldScheme = calc.getScheme().copy(); // stores previous information + calc.setScheme(newScheme.copy(), data); // assigns new problem type if (newScheme.getClass().getSimpleName().equals(oldScheme.getClass().getSimpleName())) - task.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type - // else - // task.getScheme().setTimeLimit( task.getTimeLimit() ); + calc.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type oldScheme = null; // deletes reference to old problem @@ -388,83 +399,6 @@ private void setSelectedElement(JList list, Object o) { } - /* - * ################## Problem List Class ################## - */ - - class ProblemList extends JList { - - public ProblemList() { - super(); - setFont(getFont().deriveFont(LIST_FONT_SIZE)); - this.setCellRenderer(new ProblemListCellRenderer()); - - var listModel = new DefaultListModel(); - for (var p : knownProblems) { - listModel.addElement(p); - } - - setModel(listModel); - setSelectionMode(SINGLE_SELECTION); - - var instance = getManagerInstance(); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - var newlySelectedProblem = getSelectedValue(); - if (newlySelectedProblem == null) { - ((DefaultTableModel) problemTable.getModel()).setRowCount(0); - return; - } - - if (instance.getSelectedTask() == null) { - instance.selectFirstTask(); - } - var selectedTask = instance.getSelectedTask(); - if (TaskManager.getManagerInstance().isSingleStatement()) { - instance.getTaskList().stream().forEach(t -> changeProblem(t, newlySelectedProblem)); - } else { - changeProblem(selectedTask, newlySelectedProblem); - } - listModel.set(listModel.indexOf(newlySelectedProblem), selectedTask.getProblem()); - problemTable.setPropertyHolder(instance.getSelectedTask().getProblem()); - // after problem is selected for this task, show available difference schemes - var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); - defaultModel.clear(); - var schemes = newlySelectedProblem.availableSolutions(); - schemes.forEach(s -> defaultModel.addElement(s)); - selectDefaultScheme(schemeSelectionList, selectedTask.getProblem()); - schemeSelectionList.setToolTipText(null); - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // select appropriate problem type from list - if (instance.getSelectedTask().getProblem() != null) { - for (var i = 0; i < getModel().getSize(); i++) { - var p = getModel().getElementAt(i); - if (instance.getSelectedTask().getProblem().getClass().equals(p.getClass())) { - setSelectedIndex(i); - break; - } - } - } - // then, select appropriate scheme type - if (instance.getSelectedTask().getScheme() != null) { - for (var i = 0; i < schemeSelectionList.getModel().getSize(); i++) { - if (instance.getSelectedTask().getScheme().getClass() - .equals(schemeSelectionList.getModel().getElementAt(i).getClass())) { - schemeSelectionList.setSelectedIndex(i); - break; - } - } - } - }); - - } - - } - /* * ########################### Scheme selection list class * ########################### @@ -475,7 +409,6 @@ class SchemeSelectionList extends JList { public SchemeSelectionList() { super(); - setFont(getFont().deriveFont(LIST_FONT_SIZE)); setSelectionMode(SINGLE_SELECTION); var m = new DefaultListModel(); setModel(m); @@ -498,8 +431,8 @@ public SchemeSelectionList() { } else { changeScheme(selectedTask, newScheme); } - schemeTable.setPropertyHolder(selectedTask.getScheme()); - if (selectedTask.getProblem().getComplexity() == HIGH) { + schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); + if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { showMessageDialog(null, "

" + "You have selected a " + "high-complexity problem statement. Calculations will take longer than usual. " + "You may track the progress of your task with the verbose logging option. Watch out for " @@ -512,7 +445,6 @@ public SchemeSelectionList() { } - public InternalGraphFrame getPulseFrame() { return pulseFrame; } diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 66c0457b..ccab28e7 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -5,6 +5,7 @@ import static javax.swing.JOptionPane.YES_NO_OPTION; import static javax.swing.JOptionPane.YES_OPTION; import static javax.swing.JOptionPane.showOptionDialog; +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; import static pulse.tasks.processing.ResultFormat.addResultFormatListener; import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; @@ -24,6 +25,7 @@ import javax.swing.event.InternalFrameEvent; import pulse.tasks.TaskManager; +import pulse.ui.Launcher; import pulse.ui.components.PulseMainMenu; import pulse.ui.components.listeners.FrameVisibilityRequestListener; import pulse.ui.components.listeners.TaskActionListener; @@ -32,22 +34,22 @@ @SuppressWarnings("serial") public class TaskControlFrame extends JFrame { - private static Mode mode = Mode.TASK; - private final static int HEIGHT = 730; private final static int WIDTH = 1035; private static TaskControlFrame instance = new TaskControlFrame(); - private static ProblemStatementFrame problemStatementFrame; - private static SearchOptionsFrame searchOptionsFrame; - private static TaskManagerFrame taskManagerFrame; - private static PreviewFrame previewFrame; - private static ResultFrame resultsFrame; - private static MainGraphFrame graphFrame; - private static LogFrame logFrame; + private Mode mode = Mode.TASK; + private ProblemStatementFrame problemStatementFrame; + private SearchOptionsFrame searchOptionsFrame; + private TaskManagerFrame taskManagerFrame; + private ModelSelectionFrame modelFrame; + private PreviewFrame previewFrame; + private ResultFrame resultsFrame; + private MainGraphFrame graphFrame; + private LogFrame logFrame; - private static PulseMainMenu mainMenu; + private PulseMainMenu mainMenu; public static TaskControlFrame getInstance() { return instance; @@ -121,6 +123,14 @@ public void onSearchSettingsShowRequest() { exit(0); }); + var manager = TaskManager.getManagerInstance(); + manager.addTaskRepositoryListener(e -> + { + if(e.getState() == TASK_BROWSING_REQUEST) + setModelSelectionFrameVisible(true); + } + ); + addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) .changeFormat(rfe.getResultFormat())); @@ -151,6 +161,7 @@ public void onGraphRequest() { } }); + } /** @@ -166,14 +177,23 @@ private void initComponents() { setJMenuBar(mainMenu); logFrame = new LogFrame(); + logFrame.setFrameIcon(Launcher.loadIcon("log.png", 20)); resultsFrame = new ResultFrame(); + resultsFrame.setFrameIcon(Launcher.loadIcon("result.png", 20)); previewFrame = new PreviewFrame(); + previewFrame.setFrameIcon(Launcher.loadIcon("preview.png", 20)); taskManagerFrame = new TaskManagerFrame(); + taskManagerFrame.setFrameIcon(Launcher.loadIcon("task_manager.png", 20)); graphFrame = MainGraphFrame.getInstance(); + graphFrame.setFrameIcon(Launcher.loadIcon("curves.png", 20)); problemStatementFrame = new ProblemStatementFrame(); + problemStatementFrame.setFrameIcon(Launcher.loadIcon("heat_problem.png", 20)); + modelFrame = new ModelSelectionFrame(); + modelFrame.setFrameIcon(Launcher.loadIcon("stored.png", 20)); searchOptionsFrame = new SearchOptionsFrame(); + searchOptionsFrame.setFrameIcon(Launcher.loadIcon("optimiser.png", 20)); /* * CONSTRAINT ADJUSTMENT @@ -188,6 +208,7 @@ private void initComponents() { desktopPane.add(resultsFrame); desktopPane.add(problemStatementFrame); desktopPane.add(searchOptionsFrame); + desktopPane.add(modelFrame); setDefaultResizeBehaviour(); @@ -237,6 +258,15 @@ public void internalFrameClosing(InternalFrameEvent e) { }); + modelFrame.addInternalFrameListener(new InternalFrameAdapter() { + + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setModelSelectionFrameVisible(false); + } + + }); + } private void doResize() { @@ -253,6 +283,11 @@ private void doResize() { case PREVIEW: resizeHalves(previewFrame, resultsFrame); break; + case MODEL_COMPARISON: + resizeTriplet(graphFrame, resultsFrame, modelFrame); + break; + default: + break; } } @@ -356,6 +391,7 @@ private void setPreviewFrameVisible(boolean show) { private void setProblemStatementFrameVisible(boolean show) { problemStatementFrame.setVisible(show); + problemStatementFrame.getPulseFrame().setVisible(show); graphFrame.setVisible(true); previewFrame.setVisible(false); @@ -383,9 +419,23 @@ private void setSearchOptionsFrameVisible(boolean show) { doResize(); } + private void setModelSelectionFrameVisible(boolean show) { + modelFrame.setVisible(show); + resultsFrame.setVisible(true); + graphFrame.setVisible(true); + + problemStatementFrame.setVisible(false); + previewFrame.setVisible(false); + taskManagerFrame.setVisible(!show); + logFrame.setVisible(!show); + + mode = show ? Mode.MODEL_COMPARISON : Mode.TASK; + doResize(); + } + private enum Mode { - TASK, PROBLEM, PREVIEW, SEARCH; + TASK, PROBLEM, PREVIEW, SEARCH, MODEL_COMPARISON; } diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index e0f15268..f83b3282 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -48,8 +48,8 @@ public class ExportDialog extends JDialog { private static Map, Boolean> exportSettings = new HashMap, Boolean>(); - private final static int HEIGHT = 160; - private final static int WIDTH = 650; + private final static int HEIGHT = 180; + private final static int WIDTH = 750; private static ProgressDialog progressFrame = new ProgressDialog(); diff --git a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java index 65961e39..148e35d8 100644 --- a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java @@ -36,7 +36,6 @@ @SuppressWarnings("serial") public class FormattedInputDialog extends JDialog { - private final static int FONT_SIZE = 14; private final static int WIDTH = 550; private final static int HEIGHT = 130; private JFormattedTextField ftf; @@ -133,7 +132,6 @@ public void actionPerformed(ActionEvent e) { } }); - inputTextField.setFont(inputTextField.getFont().deriveFont(FONT_SIZE)); inputTextField.setColumns(10); return inputTextField; } diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index c31af837..191623fd 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -146,7 +146,6 @@ private void initComponents() { MainToolbar.setRollover(true); MainToolbar.add(filler1); - cancelBtn.setFont(new java.awt.Font("Dialog", 1, 16)); // NOI18N cancelBtn.setText("Cancel"); cancelBtn.setFocusable(false); cancelBtn.setHorizontalTextPosition(SwingConstants.CENTER); @@ -154,7 +153,6 @@ private void initComponents() { MainToolbar.add(cancelBtn); MainToolbar.add(filler3); - commitBtn.setFont(new java.awt.Font("Dialog", 1, 16)); // NOI18N commitBtn.setText("Commit"); commitBtn.setFocusable(false); commitBtn.setHorizontalTextPosition(SwingConstants.CENTER); diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 95da82d6..8ed2540d 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -26,10 +26,8 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0) - continue; - - if (!Group.class.isAssignableFrom(m.getReturnType())) + if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) + || m.getReturnType().isAssignableFrom(getClass())) continue; Group a = null; @@ -68,7 +66,7 @@ public List subgroups() { public static Set contents(Group root) { var contents = root.subgroups().stream().filter(ph -> root.getParent() != ph).collect(Collectors.toSet()); - + for (var it = contents.iterator(); it.hasNext();) contents(it.next()).stream().forEach(a -> contents.add(a)); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 90884661..72cd8f0f 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,16 @@ + + + + 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/curves.png b/src/main/resources/images/curves.png new file mode 100644 index 0000000000000000000000000000000000000000..5b08ec35ea48fec4f75c781db0829a640fa9e56d GIT binary patch literal 7912 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/log.png b/src/main/resources/images/log.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7463e2b343a484d687ce45eed210ee70ce9b11 GIT binary patch literal 46922 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 E4`{F~iU0rr literal 0 HcmV?d00001 diff --git a/src/main/resources/images/stored.png b/src/main/resources/images/stored.png new file mode 100644 index 0000000000000000000000000000000000000000..5f46620c3c8e03f6fbe9b2ee59dc32f8391f8139 GIT binary patch literal 27652 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 Date: Fri, 9 Oct 2020 21:22:06 +0100 Subject: [PATCH 005/116] Some additional refactoring --- src/main/java/pulse/HeatingCurve.java | 2 + .../pulse/input/InterpolationDataset.java | 40 ++------- src/main/java/pulse/input/Range.java | 4 +- .../listeners/ExternalDatasetListener.java | 7 ++ .../statements/model/ThermalProperties.java | 37 ++++++++- .../statistics/ModelSelectionCriterion.java | 11 +-- src/main/java/pulse/tasks/Calculation.java | 17 +++- src/main/java/pulse/tasks/SearchTask.java | 31 ++++--- src/main/java/pulse/tasks/TaskManager.java | 2 - .../tasks/listeners/TaskRepositoryEvent.java | 8 +- .../java/pulse/tasks/processing/Buffer.java | 2 +- src/main/java/pulse/ui/Launcher.java | 10 --- .../pulse/ui/components/CalculationTable.java | 36 ++++++--- .../java/pulse/ui/components/ProblemTree.java | 70 ++++++++-------- .../pulse/ui/components/PulseMainMenu.java | 2 +- .../pulse/ui/components/TaskPopupMenu.java | 2 +- .../components/buttons/ExecutionButton.java | 6 +- .../ui/components/buttons/IconCheckBox.java | 2 +- .../ui/components/buttons/LoaderButton.java | 20 ++++- .../ui/components/models/TaskTableModel.java | 2 +- .../ui/components/panels/ChartToolbar.java | 2 +- .../ui/components/panels/LogToolbar.java | 2 +- .../ui/components/panels/ModelToolbar.java | 28 +++++-- .../ui/components/panels/ResultToolbar.java | 2 +- .../ui/components/panels/SystemPanel.java | 33 +------- .../ui/components/panels/TaskToolbar.java | 2 +- .../pulse/ui/frames/ModelSelectionFrame.java | 4 - .../java/pulse/ui/frames/PreviewFrame.java | 2 +- .../ui/frames/ProblemStatementFrame.java | 76 +++++++----------- .../pulse/ui/frames/TaskControlFrame.java | 25 +++--- src/main/java/pulse/util/ImageUtils.java | 51 ++++++++++++ src/main/resources/images/go_estimate.png | Bin 0 -> 17249 bytes 32 files changed, 309 insertions(+), 229 deletions(-) create mode 100644 src/main/java/pulse/input/listeners/ExternalDatasetListener.java create mode 100644 src/main/java/pulse/util/ImageUtils.java create mode 100644 src/main/resources/images/go_estimate.png diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 1e77d276..a43c09c2 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -56,6 +56,8 @@ public HeatingCurve(HeatingCurve c) { this.adjustedSignal = new ArrayList<>(c.adjustedSignal); this.startTime = c.startTime; splineInterpolator = new SplineInterpolator(); + if(c.splineInterpolation != null) + this.refreshInterpolation(); } /** diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index f140089c..87642d88 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -1,6 +1,5 @@ package pulse.input; -import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; @@ -13,7 +12,7 @@ import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; -import pulse.problem.statements.model.ThermalProperties; +import pulse.input.listeners.ExternalDatasetListener; import pulse.properties.NumericPropertyKeyword; import pulse.util.ImmutableDataEntry; @@ -28,9 +27,10 @@ public class InterpolationDataset { + private UnivariateFunction interpolation; private List> dataset; private static Map standartDatasets = new HashMap(); - private UnivariateFunction interpolation; + private static List listeners = new ArrayList<>();; /** * Creates an empty {@code InterpolationDataset}. @@ -107,35 +107,7 @@ public static InterpolationDataset getDataset(StandartType type) { public static void setDataset(InterpolationDataset dataset, StandartType type) { standartDatasets.put(type, dataset); - } - - /** - * Calculates some or all of the following properties: - * Cp, ρ, &labmda;, - * ε. - *

- * These properties will be calculated only if the necessary - * {@code InterpolationDataset}s were previously loaded by the - * {@code TaskManager}. - *

- */ - - public static void fill(ThermalProperties properties) { - final double testTemperature = (double)properties.getTestTemperature().getValue(); - var cpCurve = getDataset(StandartType.HEAT_CAPACITY); - - if (cpCurve != null) { - final double cp = cpCurve.interpolateAt(testTemperature); - properties.set(NumericPropertyKeyword.SPECIFIC_HEAT, derive(NumericPropertyKeyword.SPECIFIC_HEAT, cp)); - } - - var rhoCurve = getDataset(StandartType.DENSITY); - - if (rhoCurve != null) { - final double rho = rhoCurve.interpolateAt(testTemperature); - properties.set(NumericPropertyKeyword.DENSITY, derive(NumericPropertyKeyword.DENSITY, rho)); - } - + listeners.stream().forEach(l -> l.onDensityDataLoaded(type)); } public static List derivableProperties() { @@ -148,6 +120,10 @@ public static List derivableProperties() { list.add(CONDUCTIVITY); return list; } + + public static void addListener(ExternalDatasetListener l) { + listeners.add(l); + } public enum StandartType { diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 9e37fd47..521ffeee 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -176,11 +176,11 @@ public void optimisationVector(IndexedVector[] output, List flags) { switch (output[0].getIndex(i)) { case UPPER_BOUND: output[0].set(i, segment.getMaximum()); - output[1].set(i, 0.75 * segment.length()); + output[1].set(i, 0.25 * segment.length()); break; case LOWER_BOUND: output[0].set(i, segment.getMinimum()); - output[1].set(i, 0.75 * segment.length()); + output[1].set(i, 0.25 * segment.length()); break; default: continue; diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java new file mode 100644 index 00000000..d6fb473e --- /dev/null +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -0,0 +1,7 @@ +package pulse.input.listeners; + +import pulse.input.InterpolationDataset.StandartType; + +public interface ExternalDatasetListener { + public void onDensityDataLoaded(StandartType type); +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 65146f90..2c5cbbf3 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -20,6 +20,7 @@ import java.util.List; import pulse.input.ExperimentalData; +import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; @@ -61,6 +62,7 @@ public ThermalProperties() { signalHeight = (double) def(MAXTEMP).getValue(); T = (double) def(TEST_TEMPERATURE).getValue(); emissivity = (double) def(EMISSIVITY).getValue(); + initListeners(); } public ThermalProperties(ThermalProperties p) { @@ -70,6 +72,35 @@ public ThermalProperties(ThermalProperties p) { this.Bi = p.Bi; this.T = p.T; this.emissivity = p.emissivity; + initListeners(); + } + + /** + * Calculates some or all of the following properties: + * Cp, ρ, &labmda;, + * ε. + *

+ * These properties will be calculated only if the necessary + * {@code InterpolationDataset}s were previously loaded by the + * {@code TaskManager}. + *

+ */ + + private void initListeners() { + + InterpolationDataset.addListener(e -> { + if(getParent() == null) + return; + + if (e == StandartType.DENSITY) { + rho = InterpolationDataset.getDataset(StandartType.DENSITY).interpolateAt(T); + } + else if (e == StandartType.HEAT_CAPACITY) { + cP = InterpolationDataset.getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); + } + + }); + } public ThermalProperties copy() { @@ -223,7 +254,7 @@ public List listedTypes() { public final double thermalConductivity() { return a * cP * rho; } - + public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, thermalConductivity()); } @@ -290,12 +321,12 @@ public void setEmissivity(NumericProperty e) { this.emissivity = (double) e.getValue(); setHeatLoss(derive(HEAT_LOSS, biot())); } - + @Override public String getDescriptor() { return "Sample Thermo-Physical Properties"; } - + @Override public String toString() { return "Show Details..."; diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index a782a4ba..6c0f18fe 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -13,6 +13,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; +import pulse.util.PropertyEvent; /** * An abstract superclass for the BIC and AIC statistics. @@ -21,7 +22,6 @@ public abstract class ModelSelectionCriterion extends Statistic { - private static String selectedModelSelectionDescriptor; private OptimiserStatistic os; private int kq; private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); @@ -44,6 +44,7 @@ public void evaluate(SearchTask t) { os.evaluate(t); final int n = os.getResiduals().size(); //sample size criterion = n * log(os.variance()) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; + this.tellParent(new PropertyEvent(null, this, getStatistic())); } /** @@ -76,14 +77,6 @@ public String getDescriptor() { public int getNumVariables() { return kq; } - - public static String getSelectedCriterionDescriptor() { - return selectedModelSelectionDescriptor; - } - - public static void setSelectedCriterionDescriptor(String selectedTestDescriptor) { - ModelSelectionCriterion.selectedModelSelectionDescriptor = selectedTestDescriptor; - } public OptimiserStatistic getOptimiser() { return os; diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index cff1bbb4..c939afaf 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -20,11 +20,13 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.statistics.AICStatistic; import pulse.search.statistics.ModelSelectionCriterion; import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.logs.Details; import pulse.tasks.logs.Status; import pulse.ui.components.PropertyHolderTable; +import pulse.util.InstanceDescriptor; import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; @@ -38,9 +40,17 @@ public class Calculation extends PropertyHolder implements Comparable instanceDescriptor = new InstanceDescriptor<>( + "Model Selection Criterion", ModelSelectionCriterion.class); + + static { + instanceDescriptor.setSelectedDescriptor(AICStatistic.class.getSimpleName()); + } + public Calculation() { status = INCOMPLETE; this.initOptimiser(); + instanceDescriptor.addListener( () -> initModelCriterion()); } public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { @@ -225,8 +235,7 @@ public void initOptimiser() { } public void initModelCriterion() { - setModelSelectionCriterion( instantiate(ModelSelectionCriterion.class, ModelSelectionCriterion.getSelectedCriterionDescriptor() ) ); - rs.setOptimiser(os); + setModelSelectionCriterion(instanceDescriptor.newInstance(ModelSelectionCriterion.class, os)); } public DifferenceScheme getScheme() { @@ -267,4 +276,8 @@ public boolean equals(Object o) { } + public static InstanceDescriptor getModelSelectionDescriptor() { + return instanceDescriptor; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index d03ec156..c28d36d3 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import pulse.input.ExperimentalData; +import pulse.input.InterpolationDataset; import pulse.math.IndexedVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; @@ -110,6 +111,22 @@ public SearchTask(ExperimentalData curve) { curve.setParent(this); correlationBuffer = new CorrelationBuffer(); clear(); + + InterpolationDataset.addListener(e -> { + var p = current.getProblem().getProperties(); + if(p.areThermalPropertiesLoaded()) + p.useTheoreticalEstimates(curve); + }); + + curve.addDataListener(dataEvent -> { + var scheme = current.getScheme(); + if (scheme != null) { + var hcurve = current.getProblem().getHeatingCurve(); + var startTime = (double) hcurve.getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + } + }); + } /** @@ -141,17 +158,7 @@ public void clear() { this.path = null; current.clear(); - setStatus(INCOMPLETE); - - curve.addDataListener(dataEvent -> { - var scheme = current.getScheme(); - if (scheme != null) { - var hcurve = current.getProblem().getHeatingCurve(); - var startTime = (double) hcurve.getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); - } - }); - + setStatus(INCOMPLETE); } /** @@ -568,7 +575,7 @@ public void switchToBestModel() { private void fireModelSelected() { var instance = TaskManager.getManagerInstance(); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MDOEL_SWITCH, this.getIdentifier()); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); for(var l : instance.getTaskRepositoryListeners()) l.onTaskListChanged(e); } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 8d87578a..c300d0af 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -33,7 +33,6 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; -import pulse.input.InterpolationDataset; import pulse.properties.SampleName; import pulse.search.direction.PathOptimiser; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -539,7 +538,6 @@ public void removeResult(SearchTask t) { public void evaluate() { tasks.stream().forEach(t -> { var properties = t.getCurrentCalculation().getProblem().getProperties(); - InterpolationDataset.fill(properties); properties.useTheoreticalEstimates(t.getExperimentalCurve()); }); } diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 05b23c09..9ca0aba9 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -62,7 +62,13 @@ public enum State { * The task has switched to a new model. */ - TASK_MDOEL_SWITCH, + TASK_MODEL_SWITCH, + + /** + * The task changed its selection criterion. + */ + + TASK_CRITERION_SWITCH, /** * The repository has been shut down/ diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index d4699fa8..17e56ebe 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -62,7 +62,7 @@ public void init() { */ public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getCurrentCalculation().getModelSelectionCriterion().getStatistic().getValue(); + statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); data[bufferElement] = t.searchVector()[0]; } diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 75783b8f..5b02434f 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -1,7 +1,6 @@ package pulse.ui; import static java.awt.EventQueue.invokeLater; -import static java.awt.Image.SCALE_SMOOTH; import static java.awt.SplashScreen.getSplashScreen; import static java.lang.Integer.valueOf; import static java.lang.Runtime.getRuntime; @@ -17,7 +16,6 @@ import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ReflectionException; -import javax.swing.ImageIcon; import javax.swing.UIManager; import com.alee.laf.WebLookAndFeel; @@ -142,12 +140,4 @@ public static int threadsAvailable() { return number > 1 ? (number - 1) : 1; } - public static ImageIcon loadIcon(String path, int iconSize) { - var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a - // imageIcon - var image = imageIcon.getImage(); // transform it - var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way - return new ImageIcon(newimg); // transform it back - } - } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 9c73a693..2806cc2b 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -6,6 +6,7 @@ import java.awt.Dimension; import javax.swing.JTable; +import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.table.TableCellRenderer; @@ -36,31 +37,40 @@ public CalculationTable() { var model = new StoredCalculationTableModel(); setModel(model); - + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); setAutoCreateRowSorter(false); initListeners(); - + var instance = TaskManager.getManagerInstance(); instance.addTaskRepositoryListener(e -> { - - if(e.getState() == TaskRepositoryEvent.State.TASK_MDOEL_SWITCH) { + + if (e.getState() == TaskRepositoryEvent.State.TASK_MODEL_SWITCH) { var t = instance.getTask(e.getId()); identifySelection(t); } + else if(e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { + update(TaskManager.getManagerInstance().getSelectedTask()); + } + }); + } - + public void update(SearchTask t) { - ((StoredCalculationTableModel)getModel()).update(t); - identifySelection(t); + if (t != null) + SwingUtilities.invokeLater(() -> { + ((StoredCalculationTableModel) getModel()).update(t); + identifySelection(t); + }); } - + public void identifySelection(SearchTask t) { int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); - this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + if (modelIndex > -1) + this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); } public void initListeners() { @@ -74,9 +84,11 @@ public void initListeners() { lsm.addListSelectionListener((ListSelectionEvent e) -> { var task = TaskManager.getManagerInstance().getSelectedTask(); if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { - var id = lsm.getMinSelectionIndex(); - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); + var id = convertRowIndexToModel(this.getSelectedRow()); + if (id < task.getStoredCalculations().size()) { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + } } }); diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index a98aa6e5..cce01460 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -6,6 +6,7 @@ import java.util.List; import javax.swing.JTree; +import javax.swing.SwingUtilities; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; @@ -27,20 +28,19 @@ public ProblemTree(List allProblems) { for (var c : ProblemComplexity.values()) { var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); - - allProblems.stream().filter(p -> p.getComplexity() == c) - .forEach(pFiltered -> { - var node = new DefaultMutableTreeNode(pFiltered); - currentComplexity.add(node); - }); - + + allProblems.stream().filter(p -> p.getComplexity() == c).forEach(pFiltered -> { + var node = new DefaultMutableTreeNode(pFiltered); + currentComplexity.add(node); + }); + root.add(currentComplexity); - + } - - var model = (DefaultTreeModel)this.getModel(); + + var model = (DefaultTreeModel) this.getModel(); model.setRoot(root); - + for (int i = 0; i < getRowCount(); i++) { expandRow(i); } @@ -57,9 +57,9 @@ private void addListeners() { var instance = getManagerInstance(); addTreeSelectionListener(e -> { - var object = ( (DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); - if(object instanceof Problem) - fireProblemSelection(new ProblemSelectionEvent((Problem)object, this)); + var object = ((DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); + if (object instanceof Problem) + fireProblemSelection(new ProblemSelectionEvent((Problem) object, this)); }); instance.addSelectionListener(e -> { @@ -68,35 +68,39 @@ private void addListeners() { setSelectedProblem(current); fireProblemSelection(new ProblemSelectionEvent(current, instance)); - + }); } - + public void setSelectedProblem(Problem p) { - if(p == null) + if (p == null) return; - + var model = this.getModel(); var root = model.getRoot(); - TreePath path = null; - - outer: for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { - var child = model.getChild(model.getRoot(), i); - - for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { - var node = (DefaultMutableTreeNode) model.getChild(child, j); - var problem = (Problem)node.getUserObject(); - if (p.getClass().equals(problem.getClass())) { - path = new TreePath(new Object[] { root, child, node }); - break outer; + + SwingUtilities.invokeLater(() -> { + + TreePath path = null; + + outer: for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { + var child = model.getChild(model.getRoot(), i); + + for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { + var node = (DefaultMutableTreeNode) model.getChild(child, j); + var problem = (Problem) node.getUserObject(); + if (p.getClass().equals(problem.getClass())) { + path = new TreePath(new Object[] { root, child, node }); + break outer; + } } + } - } - - this.setSelectionPath(path); - + this.setSelectionPath(path); + + }); } public void addProblemSelectionListener(ProblemSelectionListener l) { diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 202b2db7..d8ccd4c5 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -17,9 +17,9 @@ import static pulse.search.statistics.OptimiserStatistic.setSelectedOptimiserDescriptor; import static pulse.tasks.TaskManager.getManagerInstance; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.components.DataLoader.loadDataDialog; import static pulse.ui.components.DataLoader.loadMetadataDialog; +import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.allDescriptors; import java.io.File; diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 2f0fdff9..e92e68a2 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -16,9 +16,9 @@ import static pulse.tasks.logs.Status.DONE; import static pulse.tasks.logs.Status.READY; import static pulse.tasks.processing.ResultFormat.getInstance; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Component; import java.awt.event.ActionEvent; diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index c28b7738..078fd607 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -7,6 +7,7 @@ import static pulse.ui.Messages.getString; import static pulse.ui.components.buttons.ExecutionButton.ExecutionState.EXECUTE; import static pulse.ui.components.buttons.ExecutionButton.ExecutionState.STOP; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Component; import java.awt.event.ActionEvent; @@ -16,7 +17,6 @@ import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; -import pulse.ui.Launcher; @SuppressWarnings("serial") public class ExecutionButton extends JButton { @@ -92,8 +92,8 @@ public ExecutionState getExecutionState() { } public enum ExecutionState { - EXECUTE("Execute All Tasks", Launcher.loadIcon("execute.png", 24)), - STOP("Terminate All Running Tasks", Launcher.loadIcon("stop.png", 24)); + EXECUTE("Execute All Tasks", loadIcon("execute.png", 24)), + STOP("Terminate All Running Tasks", loadIcon("stop.png", 24)); private String message; private ImageIcon icon; diff --git a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java index 6470e26e..6c11d32d 100644 --- a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java +++ b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java @@ -1,6 +1,6 @@ package pulse.ui.components.buttons; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; import javax.swing.ImageIcon; import javax.swing.JCheckBox; diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 43c222dc..65968958 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -13,16 +13,20 @@ import static pulse.ui.Messages.getString; import static pulse.ui.components.DataLoader.load; +import java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; +import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFileChooser; +import javax.swing.UIManager; import javax.swing.filechooser.FileNameExtensionFilter; import pulse.input.InterpolationDataset; +import pulse.util.ImageUtils; @SuppressWarnings("serial") public class LoaderButton extends JButton { @@ -30,6 +34,9 @@ public class LoaderButton extends JButton { private InterpolationDataset.StandartType dataType; private static File dir; + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.75f); + public LoaderButton() { super(); init(); @@ -42,7 +49,10 @@ public LoaderButton(String str) { public void init() { - //setFont(getFont().deriveFont(BOLD, 14f)); + InterpolationDataset.addListener(e -> { + if (dataType == e) + highlight(false); + }); addActionListener((ActionEvent arg0) -> { var fileChooser = new JFileChooser(); @@ -98,4 +108,12 @@ public void setDataType(InterpolationDataset.StandartType dataType) { this.dataType = dataType; } + public void highlight(boolean highlighted) { + setBorder(highlighted ? BorderFactory.createLineBorder(HIGHLIGHTED) : null ); + } + + public void highlightIfNeeded() { + highlight(InterpolationDataset.getDataset(dataType) == null); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 5948d1c1..766e6550 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -50,7 +50,7 @@ else if (e.getState() == TASK_ADDED) public void addTask(SearchTask t) { var temperature = t.getExperimentalCurve().getMetadata().numericProperty(TEST_TEMPERATURE); - var data = new Object[] { t.getIdentifier(), temperature, t.getCurrentCalculation().getModelSelectionCriterion().getStatistic(), + var data = new Object[] { t.getIdentifier(), temperature, t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus() }; invokeLater(() -> super.addRow(data)); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index fc5483b9..e78cf273 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -13,9 +13,9 @@ import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; +import static pulse.util.ImageUtils.loadIcon; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index b9a06c2a..443f5053 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -3,8 +3,8 @@ import static javax.swing.SwingConstants.CENTER; import static pulse.tasks.logs.Log.isVerbose; import static pulse.tasks.logs.Log.setVerbose; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; +import static pulse.util.ImageUtils.loadIcon; import java.awt.GridLayout; import java.util.ArrayList; diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index f2027a08..c4d1c2f1 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -1,7 +1,6 @@ package pulse.ui.components.panels; -import static pulse.ui.Launcher.loadIcon; -import static pulse.util.Reflexive.allDescriptors; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Dimension; @@ -12,8 +11,9 @@ import javax.swing.JLabel; import javax.swing.JToolBar; -import pulse.search.statistics.ModelSelectionCriterion; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; @SuppressWarnings("serial") public class ModelToolbar extends JToolBar { @@ -22,15 +22,15 @@ public class ModelToolbar extends JToolBar { public ModelToolbar() { super(); + setOpaque(false); setFloatable(false); setRollover(true); - var set = allDescriptors(ModelSelectionCriterion.class); + var set = Calculation.getModelSelectionDescriptor().getAllDescriptors(); var criterionSelection = new JComboBox<>(set.toArray(String[]::new)); - criterionSelection.addActionListener(e -> { - ModelSelectionCriterion.setSelectedCriterionDescriptor(criterionSelection.getSelectedItem().toString()); - } + criterionSelection.addActionListener(e -> + Calculation.getModelSelectionDescriptor().setSelectedDescriptor((String)criterionSelection.getSelectedItem()) ); - criterionSelection.setSelectedIndex(0); + criterionSelection.setSelectedItem(Calculation.getModelSelectionDescriptor().getValue()); this.setBorder(BorderFactory.createEtchedBorder()); @@ -38,6 +38,18 @@ public ModelToolbar() { add(Box.createRigidArea(new Dimension(5,0))); add(criterionSelection); + var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE)); + doCalc.setToolTipText("Re-calculate model weights"); + add(Box.createRigidArea(new Dimension(15,0))); + add(doCalc); + + doCalc.addActionListener(e -> { + var instance = TaskManager.getManagerInstance(); + var t = instance.getSelectedTask(); + t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().evaluate(t)); + instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); + }); + var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE)); bestSelection.setToolTipText("Select Best Model"); add(Box.createRigidArea(new Dimension(15,0))); diff --git a/src/main/java/pulse/ui/components/panels/ResultToolbar.java b/src/main/java/pulse/ui/components/panels/ResultToolbar.java index 877fa725..b657a201 100644 --- a/src/main/java/pulse/ui/components/panels/ResultToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ResultToolbar.java @@ -1,6 +1,6 @@ package pulse.ui.components.panels; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; import java.awt.GridLayout; import java.util.ArrayList; diff --git a/src/main/java/pulse/ui/components/panels/SystemPanel.java b/src/main/java/pulse/ui/components/panels/SystemPanel.java index 34395750..84117090 100644 --- a/src/main/java/pulse/ui/components/panels/SystemPanel.java +++ b/src/main/java/pulse/ui/components/panels/SystemPanel.java @@ -11,7 +11,6 @@ import static pulse.ui.Launcher.getMemoryUsage; import static pulse.ui.Launcher.threadsAvailable; -import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -19,6 +18,8 @@ import javax.swing.JPanel; import javax.swing.UIManager; +import pulse.util.ImageUtils; + @SuppressWarnings("serial") public class SystemPanel extends JPanel { @@ -73,38 +74,12 @@ private void startSystemMonitors() { var memoryString = format("Memory usage: %3.1f%%", memoryUsage); memoryLabel.setText(memoryString); - cpuLabel.setForeground(blend(defColor, red, (float)cpuUsage/100)); - memoryLabel.setForeground(blend(defColor, red, (float)memoryUsage/100)); + cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float)cpuUsage/100)); + memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float)memoryUsage/100)); }; executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); } - private Color blend( Color c1, Color c2, float ratio ) { - if ( ratio > 1f ) ratio = 1f; - else if ( ratio < 0f ) ratio = 0f; - float iRatio = 1.0f - ratio; - - int i1 = c1.getRGB(); - int i2 = c2.getRGB(); - - int a1 = (i1 >> 24 & 0xff); - int r1 = ((i1 & 0xff0000) >> 16); - int g1 = ((i1 & 0xff00) >> 8); - int b1 = (i1 & 0xff); - - int a2 = (i2 >> 24 & 0xff); - int r2 = ((i2 & 0xff0000) >> 16); - int g2 = ((i2 & 0xff00) >> 8); - int b2 = (i2 & 0xff); - - int a = (int)((a1 * iRatio) + (a2 * ratio)); - int r = (int)((r1 * iRatio) + (r2 * ratio)); - int g = (int)((g1 * iRatio) + (g2 * ratio)); - int b = (int)((b1 * iRatio) + (b2 * ratio)); - - return new Color( a << 24 | r << 16 | g << 8 | b ); - } - } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index b5e99025..d2df41f5 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -1,6 +1,6 @@ package pulse.ui.components.panels; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; import java.awt.GridLayout; import java.util.ArrayList; diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java index f9f4b0ca..ba88e853 100644 --- a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -31,8 +31,4 @@ public ModelSelectionFrame() { this.setDefaultCloseOperation(HIDE_ON_CLOSE); } - public CalculationTable getTable() { - return table; - } - } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index fa654899..92187481 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -11,7 +11,7 @@ import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; import java.awt.BasicStroke; import java.awt.BorderLayout; diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index f0c8bbd4..ca215ac7 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -5,7 +5,6 @@ import static java.awt.BorderLayout.SOUTH; import static java.awt.Toolkit.getDefaultToolkit; import static java.lang.System.err; -import static javax.swing.BorderFactory.createLineBorder; import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import static javax.swing.JOptionPane.WARNING_MESSAGE; @@ -21,10 +20,10 @@ import static pulse.tasks.logs.Details.MISSING_PROBLEM_STATEMENT; import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.ui.Messages.getString; +import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.instancesOf; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; @@ -39,6 +38,7 @@ import javax.swing.JToolBar; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; +import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; @@ -49,12 +49,12 @@ import pulse.problem.statements.Pulse; import pulse.tasks.SearchTask; import pulse.tasks.listeners.TaskSelectionEvent; -import pulse.ui.Launcher; import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.PulseChart; import pulse.ui.components.buttons.LoaderButton; import pulse.ui.components.panels.SettingsToolBar; +import pulse.ui.frames.TaskControlFrame.Mode; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { @@ -63,7 +63,7 @@ public class ProblemStatementFrame extends JInternalFrame { private PropertyHolderTable problemTable, schemeTable; private SchemeSelectionList schemeSelectionList; - private ProblemTree problemList; + private ProblemTree problemTree; private final static List knownProblems = instancesOf(Problem.class); @@ -93,18 +93,16 @@ public ProblemStatementFrame() { layout.setVgap(5); contentPane.setLayout(layout); - LoaderButton btnLoadCv, btnLoadDensity; - /* * Problem selection list and scroller */ - problemList = new ProblemTree(knownProblems); - contentPane.add(new JScrollPane(problemList)); + problemTree = new ProblemTree(knownProblems); + contentPane.add(new JScrollPane(problemTree)); var instance = getManagerInstance(); - problemList.addProblemSelectionListener(e -> { + problemTree.addProblemSelectionListener(e -> { var newlySelectedProblem = e.getProblem(); @@ -119,9 +117,9 @@ public ProblemStatementFrame() { var selectedTask = instance.getSelectedTask(); if (e.getSource() != instance) { - if (instance.isSingleStatement()) + if (instance.isSingleStatement()) instance.getTaskList().stream().forEach(t -> changeProblem(t, newlySelectedProblem)); - else + else changeProblem(selectedTask, newlySelectedProblem); } @@ -175,7 +173,7 @@ public ProblemStatementFrame() { var btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(Launcher.loadIcon("pulse.png", 20)); + pulseFrame.setFrameIcon(loadIcon("pulse.png", 20)); pulseFrame.setVisible(false); // simulate btn listener @@ -210,44 +208,40 @@ public ProblemStatementFrame() { toolBar.add(btnSimulate); - btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ + var btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ btnLoadCv.setDataType(HEAT_CAPACITY); toolBar.add(btnLoadCv); - btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ + var btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ btnLoadDensity.setDataType(DENSITY); toolBar.add(btnLoadDensity); - problemList.setSelectionModel(new DefaultTreeSelectionModel() { + problemTree.setSelectionModel(new DefaultTreeSelectionModel() { @Override public void setSelectionPath(TreePath path) { - var object = path.getLastPathComponent(); + var object = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (!(object instanceof Problem)) + if (!(object.getUserObject() instanceof Problem)) super.setSelectionPath(path); else { - var problem = (Problem) object; + var problem = (Problem) object.getUserObject(); var enabledFlag = problem.isEnabled(); if (enabledFlag) { super.setSelectionPath(path); - - if (!problem.isReady()) { - var bred = new Color(1.0f, 0.0f, 0.0f, 0.35f); - btnLoadDensity.setBorder(createLineBorder(bred, 3)); - btnLoadCv.setBorder(createLineBorder(bred, 3)); - } else { - btnLoadDensity.setBorder(null); - btnLoadCv.setBorder(null); + if(!problem.isReady()) { + btnLoadDensity.highlightIfNeeded(); + btnLoadCv.highlightIfNeeded(); } - - } else + } else { showMessageDialog(null, "This problem statement is not currently supported. Please select another.", "Feature not supported", WARNING_MESSAGE); + path = null; + } } @@ -271,19 +265,9 @@ public void setSelectionPath(TreePath path) { // TODO getManagerInstance().addHierarchyListener(event -> { - if (!(event.getSource() instanceof PropertyHolderTable)) - return; - - if (instance.isSingleStatement()) - return; - - Problem p; - - for (var task : instance.getTaskList()) { - p = task.getCurrentCalculation().getProblem(); - if (p != null) - p.updateProperty(event, event.getProperty()); - } + if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) + instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getProblem()).filter(p -> p != null) + .forEach(pp -> pp.updateProperty(event, event.getProperty())); }); @@ -302,9 +286,9 @@ private void update(SearchTask selectedTask) { // problem if (selectedProblem == null) - problemList.clearSelection(); + problemTree.clearSelection(); else - problemList.setSelectedProblem(selectedProblem); + problemTree.setSelectedProblem(selectedProblem); // scheme @@ -415,12 +399,14 @@ public SchemeSelectionList() { // scheme list listener addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) + if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) return; - if (!(getSelectedValue() instanceof DifferenceScheme)) { + + if (arg0.getValueIsAdjusting() || !(getSelectedValue() instanceof DifferenceScheme)) { ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); return; } + var instance = getManagerInstance(); var selectedTask = instance.getSelectedTask(); var newScheme = getSelectedValue(); diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index ccab28e7..ec63b40d 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -7,8 +7,8 @@ import static javax.swing.JOptionPane.showOptionDialog; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; import static pulse.tasks.processing.ResultFormat.addResultFormatListener; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Component; import java.awt.Dimension; @@ -25,7 +25,6 @@ import javax.swing.event.InternalFrameEvent; import pulse.tasks.TaskManager; -import pulse.ui.Launcher; import pulse.ui.components.PulseMainMenu; import pulse.ui.components.listeners.FrameVisibilityRequestListener; import pulse.ui.components.listeners.TaskActionListener; @@ -177,23 +176,23 @@ private void initComponents() { setJMenuBar(mainMenu); logFrame = new LogFrame(); - logFrame.setFrameIcon(Launcher.loadIcon("log.png", 20)); + logFrame.setFrameIcon(loadIcon("log.png", 20)); resultsFrame = new ResultFrame(); - resultsFrame.setFrameIcon(Launcher.loadIcon("result.png", 20)); + resultsFrame.setFrameIcon(loadIcon("result.png", 20)); previewFrame = new PreviewFrame(); - previewFrame.setFrameIcon(Launcher.loadIcon("preview.png", 20)); + previewFrame.setFrameIcon(loadIcon("preview.png", 20)); taskManagerFrame = new TaskManagerFrame(); - taskManagerFrame.setFrameIcon(Launcher.loadIcon("task_manager.png", 20)); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20)); graphFrame = MainGraphFrame.getInstance(); - graphFrame.setFrameIcon(Launcher.loadIcon("curves.png", 20)); + graphFrame.setFrameIcon(loadIcon("curves.png", 20)); problemStatementFrame = new ProblemStatementFrame(); - problemStatementFrame.setFrameIcon(Launcher.loadIcon("heat_problem.png", 20)); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20)); modelFrame = new ModelSelectionFrame(); - modelFrame.setFrameIcon(Launcher.loadIcon("stored.png", 20)); + modelFrame.setFrameIcon(loadIcon("stored.png", 20)); searchOptionsFrame = new SearchOptionsFrame(); - searchOptionsFrame.setFrameIcon(Launcher.loadIcon("optimiser.png", 20)); + searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20)); /* * CONSTRAINT ADJUSTMENT @@ -433,10 +432,14 @@ private void setModelSelectionFrameVisible(boolean show) { doResize(); } - private enum Mode { + public enum Mode { TASK, PROBLEM, PREVIEW, SEARCH, MODEL_COMPARISON; } + public Mode getMode() { + return mode; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/util/ImageUtils.java b/src/main/java/pulse/util/ImageUtils.java new file mode 100644 index 00000000..dd89effe --- /dev/null +++ b/src/main/java/pulse/util/ImageUtils.java @@ -0,0 +1,51 @@ +package pulse.util; + +import static java.awt.Image.SCALE_SMOOTH; + +import java.awt.Color; + +import javax.swing.ImageIcon; + +import pulse.ui.Launcher; + +public class ImageUtils { + + private ImageUtils() { + //intentionally blank + } + + public static ImageIcon loadIcon(String path, int iconSize) { + var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a + // imageIcon + var image = imageIcon.getImage(); // transform it + var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way + return new ImageIcon(newimg); // transform it back + } + + public static Color blend( Color c1, Color c2, float ratio ) { + if ( ratio > 1f ) ratio = 1f; + else if ( ratio < 0f ) ratio = 0f; + float iRatio = 1.0f - ratio; + + int i1 = c1.getRGB(); + int i2 = c2.getRGB(); + + int a1 = (i1 >> 24 & 0xff); + int r1 = ((i1 & 0xff0000) >> 16); + int g1 = ((i1 & 0xff00) >> 8); + int b1 = (i1 & 0xff); + + int a2 = (i2 >> 24 & 0xff); + int r2 = ((i2 & 0xff0000) >> 16); + int g2 = ((i2 & 0xff00) >> 8); + int b2 = (i2 & 0xff); + + int a = (int)((a1 * iRatio) + (a2 * ratio)); + int r = (int)((r1 * iRatio) + (r2 * ratio)); + int g = (int)((g1 * iRatio) + (g2 * ratio)); + int b = (int)((b1 * iRatio) + (b2 * ratio)); + + return new Color( a << 24 | r << 16 | g << 8 | b ); + } + +} \ No newline at end of file diff --git a/src/main/resources/images/go_estimate.png b/src/main/resources/images/go_estimate.png new file mode 100644 index 0000000000000000000000000000000000000000..6d07c6b5667a273dcf09f1606eb08fde391ea6f7 GIT binary patch literal 17249 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 literal 0 HcmV?d00001 From 16ee306efebd56ab71a40472c3232146338f741c Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sat, 10 Oct 2020 15:31:58 +0100 Subject: [PATCH 006/116] Finalised the model selection feature --- pom.xml | 2 +- .../java/pulse/io/export/ExportManager.java | 2 +- .../statements/model/ThermalProperties.java | 11 ++++ .../pulse/properties/NumericProperty.java | 9 ++- .../statistics/ModelSelectionCriterion.java | 5 +- src/main/java/pulse/tasks/Calculation.java | 13 ++++ src/main/java/pulse/tasks/SearchTask.java | 22 +++---- src/main/java/pulse/tasks/TaskManager.java | 64 ++++--------------- .../tasks/listeners/TaskRepositoryEvent.java | 6 ++ .../tasks/processing/AbstractResult.java | 25 ++++++++ .../java/pulse/tasks/processing/Result.java | 6 +- .../pulse/tasks/processing/ResultFormat.java | 17 +++++ .../java/pulse/ui/components/ResultTable.java | 64 ++++++++++++------- .../pulse/ui/components/TaskPopupMenu.java | 5 +- .../java/pulse/ui/components/TaskTable.java | 8 ++- .../controllers/TaskTableRenderer.java | 2 +- .../models/StoredCalculationTableModel.java | 1 - .../ui/components/panels/ModelToolbar.java | 2 +- src/main/resources/About.html | 4 +- src/main/resources/messages.properties | 2 +- 20 files changed, 163 insertions(+), 107 deletions(-) diff --git a/pom.xml b/pom.xml index 05bddeee..afbf0f0c 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 kotik-coder PULsE - 1.85 + 1.88 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 7562b475..330dd986 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -179,7 +179,7 @@ public static void exportCurrentTask(File directory) { public static void exportAllResults(File directory, Extension extension) { var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> instance.getResult(t)).filter(Objects::nonNull) + instance.getTaskList().stream().map(t -> t.getStoredCalculations() ).flatMap(x -> x.stream()).filter(Objects::nonNull) .forEach(r -> export(r, directory, extension)); } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 2c5cbbf3..50b8c1f1 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -63,6 +63,7 @@ public ThermalProperties() { T = (double) def(TEST_TEMPERATURE).getValue(); emissivity = (double) def(EMISSIVITY).getValue(); initListeners(); + fill(); } public ThermalProperties(ThermalProperties p) { @@ -73,6 +74,16 @@ public ThermalProperties(ThermalProperties p) { this.T = p.T; this.emissivity = p.emissivity; initListeners(); + fill(); + } + + private void fill() { + var rhoCurve = InterpolationDataset.getDataset(StandartType.DENSITY); + var cpCurve = InterpolationDataset.getDataset(StandartType.HEAT_CAPACITY); + if(rhoCurve != null) + rhoCurve.interpolateAt(T); + if(cpCurve != null) + cpCurve.interpolateAt(T); } /** diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 27939415..a06e8ce5 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -116,10 +116,13 @@ public boolean validate() { public void setValue(Number value) { - if (!validate()) - throw new IllegalArgumentException(printRangeAndNumber(this, value)); - + Number oldValue = this.value; this.value = value; + + if (!validate()) { + this.value = oldValue; + throw new IllegalArgumentException(printRangeAndNumber(this, value)); + } } diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 6c0f18fe..352ec1bb 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -41,7 +41,10 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { @Override public void evaluate(SearchTask t) { kq = t.alteredParameters().size(); //number of variables - os.evaluate(t); + calcCriterion(); + } + + public void calcCriterion() { final int n = os.getResiduals().size(); //sample size criterion = n * log(os.variance()) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; this.tellParent(new PropertyEvent(null, this, getStatistic())); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index c939afaf..3b3b8ff0 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -25,6 +25,7 @@ import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.logs.Details; import pulse.tasks.logs.Status; +import pulse.tasks.processing.Result; import pulse.ui.components.PropertyHolderTable; import pulse.util.InstanceDescriptor; import pulse.util.PropertyEvent; @@ -39,6 +40,7 @@ public class Calculation extends PropertyHolder implements Comparable instanceDescriptor = new InstanceDescriptor<>( "Model Selection Criterion", ModelSelectionCriterion.class); @@ -71,6 +73,8 @@ public Calculation copy() { var p = nCalc.getProblem(); p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); nCalc.status = status; + if(this.getResult() != null) + nCalc.setResult(new Result(this.getResult())); return nCalc; } @@ -280,4 +284,13 @@ public static InstanceDescriptor getModelSele return instanceDescriptor; } + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + result.setParent(this); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index c28d36d3..3400a7ba 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -329,10 +329,8 @@ private void runChecks() { if (properties.stream().anyMatch(np -> !np.validate())) setStatus(FAILED, PARAMETER_VALUES_NOT_SENSIBLE); else { - setStatus(DONE); current.getModelSelectionCriterion().evaluate(this); - storeCurrentCalculation(); - switchTo(current.copy()); + setStatus(DONE); } } @@ -553,29 +551,27 @@ public Calculation getCurrentCalculation() { return current; } - public void storeCurrentCalculation() { - stored.add(current.copy()); - } - public List getStoredCalculations() { return this.stored; } public void switchTo(Calculation calc) { - current = null; - this.current = calc.copy(); + current.setParent(null); + current = calc; current.setParent(this); - this.fireModelSelected(); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); + fireRepositoryEvent(e); } public void switchToBestModel() { var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); - this.switchTo(best.get()); + this.switchTo(best.get()); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); + fireRepositoryEvent(e); } - private void fireModelSelected() { + private void fireRepositoryEvent(TaskRepositoryEvent e) { var instance = TaskManager.getManagerInstance(); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); for(var l : instance.getTaskRepositoryListeners()) l.onTaskListChanged(e); } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index c300d0af..e55e69e4 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -16,15 +16,12 @@ import static pulse.tasks.logs.Status.DONE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.QUEUED; -import static pulse.tasks.logs.Status.READY; import static pulse.ui.Launcher.threadsAvailable; import static pulse.util.Group.contents; import java.io.File; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; @@ -62,7 +59,6 @@ public class TaskManager extends UpwardsNavigable { private List tasks; private SearchTask selectedTask; - private Map results; private boolean singleStatement = true; @@ -91,7 +87,6 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList(); - results = new HashMap(); taskPool = new ForkJoinPool(THREADS_AVAILABLE - 1); selectionListeners = new CopyOnWriteArrayList(); taskRepositoryListeners = new CopyOnWriteArrayList(); @@ -118,7 +113,6 @@ public static TaskManager getManagerInstance() { */ public void execute(SearchTask t) { - removeResult(t); // remove old result t.setStatus(QUEUED); // notify listeners computation is about to start // notify listeners @@ -127,11 +121,18 @@ public void execute(SearchTask t) { // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - if (t.getCurrentCalculation().getStatus() == DONE) { - results.put(t, new Result(t, ResultFormat.getInstance())); - } + var current = t.getCurrentCalculation(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - notifyListeners(e); + if (current.getStatus() == DONE) { + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + current.setParent(null); + t.getStoredCalculations().add(current.copy()); + current.setParent(t); + current.setResult(null); + } else + notifyListeners(e); }); } @@ -492,49 +493,6 @@ public String describe() { return tasks.size() > 0 ? getSampleName().toString() : DEFAULT_NAME; } - public Result getResult(SearchTask t) { - return results.get(t); - } - - /** - * Assigns {@code r} as the {@code Result} for {@code t}. - * - * @param t the {@code Result} - * @param r the {@code SearchTask}. - */ - - public void useResult(SearchTask t, Result r) { - results.put(t, r); - } - - /** - * Searches for a {@code Result} for a {@code SearchTask} with a specific - * {@code id}. - * - * @param id the {@code Identifier} of a {@code SearchTask} - * @return {@code null} if such {@code Result} cannot be found. Otherwise, - * returns the found {@code Result}. - */ - - public Result getResult(Identifier id) { - var optional = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); - return optional.isPresent() ? results.get(optional.get()) : null; - } - - /** - * Removes the results of the task {@code t} and sets its status to - * {@code READY}. - * - * @param t a {@code SearchTask} contained in the repository - */ - - public void removeResult(SearchTask t) { - if (!results.containsKey(t)) - return; - results.remove(t); - t.setStatus(READY); - } - public void evaluate() { tasks.stream().forEach(t -> { var properties = t.getCurrentCalculation().getProblem().getProperties(); diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 9ca0aba9..1e8550ce 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -69,6 +69,12 @@ public enum State { */ TASK_CRITERION_SWITCH, + + /** + * Indicates the task has discarded superfluous calculations. + */ + + BEST_MODEL_SELECTED, /** * The repository has been shut down/ diff --git a/src/main/java/pulse/tasks/processing/AbstractResult.java b/src/main/java/pulse/tasks/processing/AbstractResult.java index 86d2ac05..3cd8cca3 100644 --- a/src/main/java/pulse/tasks/processing/AbstractResult.java +++ b/src/main/java/pulse/tasks/processing/AbstractResult.java @@ -34,6 +34,11 @@ public AbstractResult(ResultFormat format) { this.format = format; properties = new ArrayList<>(format.size()); } + + public AbstractResult(AbstractResult r) { + this.properties = new ArrayList<>(r.getProperties()); + this.format = r.format; + } public ResultFormat getFormat() { return format; @@ -106,5 +111,25 @@ public static List filterProperties(AbstractResult result, Resu public static List filterProperties(AbstractResult result) { return filterProperties(result, result.format); } + + @Override + public boolean equals(Object o) { + if(o == this) + return true; + + if(o == null) + return false; + + if(! (o.getClass().equals(o.getClass()))) + return false; + + var another = (AbstractResult)o; + + if(!another.properties.containsAll(this.properties) || !this.properties.containsAll(another.properties)) + return false; + + return another.format.equals(this.format); + + } } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index dac042ed..7cf8bcbe 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -29,10 +29,14 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept if (task == null) throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); - setParent(task); + setParent(task.getCurrentCalculation()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); } + + public Result(Result r) { + super(r); + } } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultFormat.java b/src/main/java/pulse/tasks/processing/ResultFormat.java index 40e5ab15..55225dc0 100644 --- a/src/main/java/pulse/tasks/processing/ResultFormat.java +++ b/src/main/java/pulse/tasks/processing/ResultFormat.java @@ -152,5 +152,22 @@ public int indexOf(NumericPropertyKeyword key) { public static NumericPropertyKeyword[] getMinimalArray() { return minimalArray; } + + @Override + public boolean equals(Object o) { + if(o == this) + return true; + + if(o == null) + return false; + + if(! (o.getClass().equals(o.getClass()))) + return false; + + var another = (ResultFormat)o; + + return (another.nameMap.containsAll(this.nameMap)) && (this.nameMap.containsAll(another.nameMap)); + + } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 9875390e..235339c5 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -58,8 +58,8 @@ public ResultTable(ResultFormat fmt) { setFillsViewportHeight(true); setSelectionMode(SINGLE_INTERVAL_SELECTION); - setRowSelectionAllowed(false); - setColumnSelectionAllowed(true); + setRowSelectionAllowed(true); + setColumnSelectionAllowed(false); var headersSize = getPreferredSize(); headersSize.height = RESULTS_HEADER_HEIGHT; @@ -73,24 +73,9 @@ public ResultTable(ResultFormat fmt) { var instance = TaskManager.getManagerInstance(); instance.addSelectionListener((TaskSelectionEvent e) -> { - var id = instance.getSelectedTask().getIdentifier(); + var t = instance.getSelectedTask(); getSelectionModel().clearSelection(); - var results = ((ResultTableModel) getModel()).getResults(); - var jj = 0; - for (var r : results) { - if (!(r instanceof Result)) - continue; - var rid = r.identify(); - if (!rid.equals(id)) - continue; - jj = convertRowIndexToView(results.indexOf(r)); - - if (jj > -1) { - getSelectionModel().addSelectionInterval(jj, jj); - scrollToSelection(jj); - } - - } + select( t.getCurrentCalculation().getResult() ); }); /* @@ -99,10 +84,10 @@ public ResultTable(ResultFormat fmt) { */ TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { + var t = instance.getTask(e.getId()); switch (e.getState()) { case TASK_FINISHED: - var t = instance.getTask(e.getId()); - var r = instance.getResult(t); + var r = t.getCurrentCalculation().getResult(); invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); break; case TASK_REMOVED: @@ -110,6 +95,18 @@ public ResultTable(ResultFormat fmt) { ((ResultTableModel) getModel()).removeAll(e.getId()); getSelectionModel().clearSelection(); break; + case BEST_MODEL_SELECTED : + for(var c : t.getStoredCalculations()) + if(c.getResult() != null && c != t.getCurrentCalculation()) + ((ResultTableModel) getModel()).remove(c.getResult()); + this.select(t.getCurrentCalculation().getResult()); + break; + case TASK_MODEL_SWITCH : + var c = t.getCurrentCalculation(); + this.getSelectionModel().clearSelection(); + if(c != null && c.getResult() != null) + select(c.getResult()); + break; default: break; } @@ -252,6 +249,7 @@ public boolean hasEnoughElements(int elements) { public void deleteSelected() { + invokeLater(() -> { var rtm = (ResultTableModel) getModel(); var selection = getSelectedRows(); @@ -260,11 +258,27 @@ public void deleteSelected() { for (var i = selection.length - 1; i >= 0; i--) { rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); - } + }}); } + + public void select(Result r) { + invokeLater(() -> { + var results = ((ResultTableModel) getModel()).getResults(); + if(results.contains(r)) { + + int jj = convertRowIndexToView(results.indexOf(r)); + + if (jj > -1) { + getSelectionModel().addSelectionInterval(jj, jj); + scrollToSelection(jj); + } + + }}); + } public void undo() { + invokeLater(() -> { var dtm = (ResultTableModel) getModel(); for (var i = dtm.getRowCount() - 1; i >= 0; i--) { @@ -272,7 +286,9 @@ public void undo() { } var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> instance.getResult(t)).forEach(r -> dtm.addRow(r)); - } + instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getResult()).forEach(r -> dtm.addRow(r)); + }); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index e92e68a2..0e38fcff 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -131,9 +131,10 @@ public TaskPopupMenu() { var t = instance.getSelectedTask(); if (t == null) return; - if (t.getCurrentCalculation().getProblem() != null) { + var current = t.getCurrentCalculation(); + if (current != null) { var r = new Result(t, getInstance()); - instance.useResult(t, r); + current.setResult(r); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); instance.notifyListeners(e); } diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index a87cf185..6d48cd33 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -17,6 +17,7 @@ import javax.swing.JTable; import javax.swing.RowSorter; +import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; @@ -126,6 +127,7 @@ public void mouseClicked(MouseEvent e) { // simply ignore call if event is triggered by taskTable if (e.getSource() instanceof TaskTable) return; + var id = instance.getSelectedTask().getIdentifier(); Identifier idFromTable = null; int i = 0; @@ -136,8 +138,8 @@ public void mouseClicked(MouseEvent e) { if(i < getRowCount()) setRowSelectionInterval(i, i); clearSelection(); - }); - + }); + } @Override @@ -146,6 +148,7 @@ public TableCellRenderer getCellRenderer(int row, int column) { } public void removeSelectedRows() { + SwingUtilities.invokeLater(() -> { var rows = getSelectedRows(); Identifier id; @@ -157,6 +160,7 @@ public void removeSelectedRows() { } clearSelection(); + }); } private class TableHeader extends JTableHeader { diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index ff2b4ec4..bc99304e 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -41,7 +41,7 @@ else if (value instanceof Status) { } else if(value instanceof PropertyHolder) { - return initLabel("" + ((PropertyHolder)value).describe(), table.isRowSelected(row)); + return initLabel("" + ((PropertyHolder)value).getDescriptor(), table.isRowSelected(row)); } return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java index 07fca65a..64bd897d 100644 --- a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -25,7 +25,6 @@ public StoredCalculationTableModel() { new String[] { "Problem Statement", "Baseline", "Parameter count", "Optimiser Statistic", "Model Selection Statistic", def(MODEL_WEIGHT).getAbbreviation(true) }); - } public void update(SearchTask t) { diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index c4d1c2f1..ede552eb 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -46,7 +46,7 @@ public ModelToolbar() { doCalc.addActionListener(e -> { var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); - t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().evaluate(t)); + t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().calcCriterion() ); instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); }); diff --git a/src/main/resources/About.html b/src/main/resources/About.html index 5c08a188..a531f4b7 100644 --- a/src/main/resources/About.html +++ b/src/main/resources/About.html @@ -1,5 +1,5 @@
Processing Unit for Laser flash Experiments
-
(PULsE), v. 1.85
-
+
(PULsE), v. 1.88
+

Date of release: 10/10/2020
Lead developer: Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii

PULsE is an advanced software toolkit for processing data from laser flash experiments, allowing effective treatment of difficult cases where conditions may not be ideal for simpler analysis. PULsE analyses the heating curves from laser flash experiments and outputs the thermal properties of the sample.

This software is specifically tailored for use in the Materials Research Facility at UKAEA, and reads ASCII files generated by the Linseis LFA; it was initially designed to read custom file formats from the 'Kvant' laser flash analyser. For full specification regarding the file formats, please refer to manual.

\ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6c4051b1..88e518a2 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,4 +1,4 @@ -TaskControlFrame.SoftwareTitle=PULsE / v. 1.85 +TaskControlFrame.SoftwareTitle=PULsE / v. 1.88 TaskControlFrame.AboutDialog=About PULsE NumericProperty.XMLFile=/NumericProperty.xml NumericProperty.PlusMinus=\ ± From e31ddea9a29044e6693ef8de4b34f1b5578dda0f Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sat, 10 Oct 2020 15:49:54 +0100 Subject: [PATCH 007/116] Updated splash screen --- src/main/resources/images/splash.png | Bin 61618 -> 58378 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index 5f49895c5526fe2842e4129a3568702cfbe976c2..489632b00d00348ecd0260d755eaaaaa54b1bd3e 100644 GIT binary patch literal 58378 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 z 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 Date: Sat, 10 Oct 2020 22:06:45 +0100 Subject: [PATCH 008/116] Minor fixes --- src/main/java/pulse/tasks/SearchTask.java | 1 + src/main/java/pulse/tasks/TaskManager.java | 3 +- .../java/pulse/ui/components/ResultTable.java | 72 +++++++++++-------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 3400a7ba..77179868 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -383,6 +383,7 @@ public void setExperimentalCurve(ExperimentalData curve) { } public void setStatus(Status status, Details details) { + status.setDetails(details); if(current.setStatus(status, details)) notifyStatusListeners(new StateEntry(this, status)); } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index e55e69e4..6d33b58a 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -318,7 +318,8 @@ public void reset() { */ public SearchTask getTask(Identifier id) { - return tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst().get(); + var o = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); + return o.isPresent() ? o.get() : null; } /** diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 235339c5..049c62ac 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -2,7 +2,7 @@ import static java.lang.Math.abs; import static java.util.stream.Collectors.toList; -import static javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION; +import static javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION; import static javax.swing.SortOrder.ASCENDING; import static javax.swing.SwingConstants.TOP; import static javax.swing.SwingUtilities.invokeLater; @@ -22,6 +22,7 @@ import pulse.properties.NumericProperty; import pulse.properties.Property; +import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.listeners.TaskSelectionEvent; @@ -57,7 +58,7 @@ public ResultTable(ResultFormat fmt) { setShowHorizontalLines(false); setFillsViewportHeight(true); - setSelectionMode(SINGLE_INTERVAL_SELECTION); + setSelectionMode(MULTIPLE_INTERVAL_SELECTION); setRowSelectionAllowed(true); setColumnSelectionAllowed(false); @@ -75,7 +76,7 @@ public ResultTable(ResultFormat fmt) { instance.addSelectionListener((TaskSelectionEvent e) -> { var t = instance.getSelectedTask(); getSelectionModel().clearSelection(); - select( t.getCurrentCalculation().getResult() ); + select(t); }); /* @@ -95,16 +96,16 @@ public ResultTable(ResultFormat fmt) { ((ResultTableModel) getModel()).removeAll(e.getId()); getSelectionModel().clearSelection(); break; - case BEST_MODEL_SELECTED : - for(var c : t.getStoredCalculations()) - if(c.getResult() != null && c != t.getCurrentCalculation()) + case BEST_MODEL_SELECTED: + for (var c : t.getStoredCalculations()) + if (c.getResult() != null && c != t.getCurrentCalculation()) ((ResultTableModel) getModel()).remove(c.getResult()); this.select(t.getCurrentCalculation().getResult()); break; - case TASK_MODEL_SWITCH : + case TASK_MODEL_SWITCH: var c = t.getCurrentCalculation(); this.getSelectionModel().clearSelection(); - if(c != null && c.getResult() != null) + if (c != null && c.getResult() != null) select(c.getResult()); break; default: @@ -185,7 +186,8 @@ public void merge(double temperatureDelta) { var val = ((Number) ((Property) this.getValueAt(i, temperatureIndex)).getValue()); - var indices = group(val.doubleValue(), temperatureIndex, temperatureDelta); // get indices of results in table + var indices = group(val.doubleValue(), temperatureIndex, temperatureDelta); // get indices of results in + // table skipList.addAll(indices); // skip those indices if they refer to the same group if (indices.size() < 2) @@ -250,45 +252,53 @@ public boolean hasEnoughElements(int elements) { public void deleteSelected() { invokeLater(() -> { - var rtm = (ResultTableModel) getModel(); - var selection = getSelectedRows(); + var rtm = (ResultTableModel) getModel(); + var selection = getSelectedRows(); - if (selection.length < 0) - return; + if (selection.length < 0) + return; - for (var i = selection.length - 1; i >= 0; i--) { - rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); - }}); + for (var i = selection.length - 1; i >= 0; i--) { + rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); + } + }); } - - public void select(Result r) { - invokeLater(() -> { + + public void select(Result r) { var results = ((ResultTableModel) getModel()).getResults(); - if(results.contains(r)) { - + if (results.contains(r)) { + int jj = convertRowIndexToView(results.indexOf(r)); if (jj > -1) { getSelectionModel().addSelectionInterval(jj, jj); scrollToSelection(jj); } - - }}); + + } + ; + } + + public void select(SearchTask t) { + t.getStoredCalculations().stream().forEach(c -> { + if (c.getResult() != null) + select(c.getResult()); + }); } public void undo() { invokeLater(() -> { - var dtm = (ResultTableModel) getModel(); + var dtm = (ResultTableModel) getModel(); - for (var i = dtm.getRowCount() - 1; i >= 0; i--) { - dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); - } + for (var i = dtm.getRowCount() - 1; i >= 0; i--) { + dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); + } - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getResult()).forEach(r -> dtm.addRow(r)); - }); + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getResult()).forEach(r -> dtm.addRow(r)); + }); } - + } \ No newline at end of file From 968aa8d5469862a0e959e7e085de044acd565356 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sun, 11 Oct 2020 10:35:12 +0100 Subject: [PATCH 009/116] Minor corrections --- .../model/BeerLambertAbsorption.java | 4 - .../statements/model/ThermalProperties.java | 8 +- src/main/java/pulse/tasks/Calculation.java | 123 +++++++++--------- .../java/pulse/ui/components/ResultTable.java | 18 ++- .../pulse/ui/components/TaskPopupMenu.java | 3 +- .../ui/components/buttons/LoaderButton.java | 2 +- .../ui/frames/ProblemStatementFrame.java | 10 +- src/main/java/pulse/util/ImageUtils.java | 2 +- src/main/resources/messages.properties | 7 +- 9 files changed, 83 insertions(+), 94 deletions(-) diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index b6350a92..74c0ec4a 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,10 +2,6 @@ public class BeerLambertAbsorption extends AbsorptionModel { - public BeerLambertAbsorption() { - super(); - } - @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 50b8c1f1..ed48e9ba 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -78,8 +78,8 @@ public ThermalProperties(ThermalProperties p) { } private void fill() { - var rhoCurve = InterpolationDataset.getDataset(StandartType.DENSITY); - var cpCurve = InterpolationDataset.getDataset(StandartType.HEAT_CAPACITY); + var rhoCurve = getDataset(StandartType.DENSITY); + var cpCurve = getDataset(StandartType.HEAT_CAPACITY); if(rhoCurve != null) rhoCurve.interpolateAt(T); if(cpCurve != null) @@ -104,10 +104,10 @@ private void initListeners() { return; if (e == StandartType.DENSITY) { - rho = InterpolationDataset.getDataset(StandartType.DENSITY).interpolateAt(T); + rho = getDataset(StandartType.DENSITY).interpolateAt(T); } else if (e == StandartType.HEAT_CAPACITY) { - cP = InterpolationDataset.getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); + cP = getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); } }); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 3b3b8ff0..b1ac731e 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -35,13 +35,13 @@ public class Calculation extends PropertyHolder implements Comparable instanceDescriptor = new InstanceDescriptor<>( "Model Selection Criterion", ModelSelectionCriterion.class); @@ -52,9 +52,9 @@ public class Calculation extends PropertyHolder implements Comparable initModelCriterion()); + instanceDescriptor.addListener(() -> initModelCriterion()); } - + public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { this(); this.problem = problem; @@ -66,24 +66,24 @@ public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCrite os.setParent(this); rs.setParent(this); } - + public Calculation copy() { var status = this.status; var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); var p = nCalc.getProblem(); p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); nCalc.status = status; - if(this.getResult() != null) + if (this.getResult() != null) nCalc.setResult(new Result(this.getResult())); return nCalc; } - + public void clear() { - this.status = Status.INCOMPLETE; + this.status = INCOMPLETE; this.problem = null; this.scheme = null; } - + /** *

* After setting and adopting the {@code problem} by this {@code SearchTask}, @@ -107,18 +107,18 @@ public void setProblem(Problem problem, ExperimentalData curve) { problem.getProperties().addListener((PropertyEvent event) -> { var source = event.getSource(); - - if (source instanceof Metadata || source instanceof PropertyHolderTable ) { - + + if (source instanceof Metadata || source instanceof PropertyHolderTable) { + var property = event.getProperty(); - if(property instanceof NumericProperty && ((NumericProperty)property).isAutoAdjustable() ) + if (property instanceof NumericProperty && ((NumericProperty) property).isAutoAdjustable()) return; - + problem.estimateSignalRange(curve); problem.getProperties().useTheoreticalEstimates(curve); } }); - + problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { var event = dataEvent.getType(); @@ -150,11 +150,11 @@ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { - (double) problem.getHeatingCurve().getTimeShift().getValue(); scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); - + } } - + /** * This will use the current {@code DifferenceScheme} to solve the * {@code Problem} for this {@code Calculation}. @@ -172,72 +172,74 @@ public void process() { e.printStackTrace(); } } - + public Status getStatus() { return status; } - + public boolean setStatus(Status status, Details details) { boolean done = false; - - if(this.status != status) { + + if (this.status != status) { this.status = status; done = true; - } - else if(this.status.getDetails() != status.getDetails()){ + } else if (this.status.getDetails() != status.getDetails()) { this.status.setDetails(status.getDetails()); done = true; } - + return done; } - + public NumericProperty weight(List all) { var result = def(MODEL_WEIGHT); - - if(rs instanceof ModelSelectionCriterion) { - var criterion = (ModelSelectionCriterion)rs; - - boolean condition = all.stream().allMatch(c -> c.getModelSelectionCriterion().getClass().equals(criterion.getClass())); - - if(condition) { - var list = all.stream().map(a -> (ModelSelectionCriterion)a.getModelSelectionCriterion()).collect(Collectors.toList()); - result = criterion.weight( list ); + + if (rs instanceof ModelSelectionCriterion) { + var criterion = (ModelSelectionCriterion) rs; + + boolean condition = all.stream() + .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(criterion.getClass())); + + if (condition) { + var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) + .collect(Collectors.toList()); + result = criterion.weight(list); } - - } - + + } + return result; } - + public void setModelSelectionCriterion(ModelSelectionCriterion rs) { this.rs = rs; rs.setParent(this); } - + public ModelSelectionCriterion getModelSelectionCriterion() { return rs; } - + public void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; os.setParent(this); initModelCriterion(); } - + public OptimiserStatistic getOptimiserStatistic() { return os; } - + public Problem getProblem() { return problem; } - + public void initOptimiser() { - this.setOptimiserStatistic( instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor() ) ); + this.setOptimiserStatistic( + instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor())); this.initModelCriterion(); } - + public void initModelCriterion() { setModelSelectionCriterion(instanceDescriptor.newInstance(ModelSelectionCriterion.class, os)); } @@ -245,7 +247,7 @@ public void initModelCriterion() { public DifferenceScheme getScheme() { return scheme; } - + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { // intentionally left blank @@ -256,28 +258,23 @@ public int compareTo(Calculation arg0) { var s1 = arg0.getModelSelectionCriterion().getStatistic(); return getModelSelectionCriterion().getStatistic().compareTo(s1); } - + @Override public boolean equals(Object o) { - if(o == this) + if (o == this) return true; - - if(o == null) + + if (o == null) return false; - - if(! (o instanceof Calculation)) + + if (!(o instanceof Calculation)) return false; - - var c = (Calculation)o; - - if(os.getStatistic().equals(c.getOptimiserStatistic().getStatistic())) { - if(rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())) { - return true; - } - } - - return false; - + + var c = (Calculation) o; + + return (os.getStatistic().equals(c.getOptimiserStatistic().getStatistic()) + && rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())); + } public static InstanceDescriptor getModelSelectionDescriptor() { diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 049c62ac..8e47f610 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -277,7 +277,7 @@ public void select(Result r) { } } - ; + } public void select(SearchTask t) { @@ -288,17 +288,15 @@ public void select(SearchTask t) { } public void undo() { - invokeLater(() -> { - var dtm = (ResultTableModel) getModel(); - - for (var i = dtm.getRowCount() - 1; i >= 0; i--) { - dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); - } + var dtm = (ResultTableModel) getModel(); - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getResult()).forEach(r -> dtm.addRow(r)); - }); + for (var i = dtm.getRowCount() - 1; i >= 0; i--) { + dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); + } + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) + .forEach(c -> dtm.addRow(c.getResult())); } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 0e38fcff..1c3674b7 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -32,7 +32,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; -import pulse.tasks.logs.Status; import pulse.tasks.processing.Result; @SuppressWarnings("serial") @@ -108,7 +107,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); if (dialogResult == 0) { // instance.removeResult(t); - instance.getSelectedTask().setStatus(Status.READY); + instance.getSelectedTask().setStatus(READY); instance.execute(instance.getSelectedTask()); } } else if (t.checkProblems() != READY) { diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 65968958..c138c8e6 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -113,7 +113,7 @@ public void highlight(boolean highlighted) { } public void highlightIfNeeded() { - highlight(InterpolationDataset.getDataset(dataType) == null); + highlight(getDataset(dataType) == null); } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index ca215ac7..f1ce8f8b 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -238,8 +238,8 @@ public void setSelectionPath(TreePath path) { } } else { showMessageDialog(null, - "This problem statement is not currently supported. Please select another.", - "Feature not supported", WARNING_MESSAGE); + getString("problem.notsupportedmessage"), + getString("problem.notsupportedtitle"), WARNING_MESSAGE); path = null; } @@ -419,11 +419,7 @@ public SchemeSelectionList() { } schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, "

" + "You have selected a " - + "high-complexity problem statement. Calculations will take longer than usual. " - + "You may track the progress of your task with the verbose logging option. Watch out for " - + "timeouts as they typically may occur for multi-variate optimisation when the problem is ill-posed." - + "

", "High complexity", INFORMATION_MESSAGE); + showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); } }); diff --git a/src/main/java/pulse/util/ImageUtils.java b/src/main/java/pulse/util/ImageUtils.java index dd89effe..fd2b7556 100644 --- a/src/main/java/pulse/util/ImageUtils.java +++ b/src/main/java/pulse/util/ImageUtils.java @@ -22,7 +22,7 @@ public static ImageIcon loadIcon(String path, int iconSize) { return new ImageIcon(newimg); // transform it back } - public static Color blend( Color c1, Color c2, float ratio ) { + public static Color blend( final Color c1, final Color c2, float ratio ) { if ( ratio > 1f ) ratio = 1f; else if ( ratio < 0f ) ratio = 0f; float iRatio = 1.0f - ratio; diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 88e518a2..34230626 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -139,8 +139,8 @@ TaskTable.Temperature=Temperature,
T0 (K) TaskTablePopupMenu.11=Error TaskTablePopupMenu.12=Missing heat curve\! TaskTablePopupMenu.13=Error -TaskTablePopupMenu.AskToDelete=Do you want to delete the old result and execute it a second time? -TaskTablePopupMenu.DeleteTitle=Delete old result? +TaskTablePopupMenu.AskToDelete=Do you want to start a new calculation sequence? +TaskTablePopupMenu.DeleteTitle=Run again? TaskTablePopupMenu.EmptySelection=Selection empty\! TaskTablePopupMenu.EmptySelection2=Selection empty\! TaskTablePopupMenu.ErrorTitle=Error @@ -263,6 +263,9 @@ DifferenceScheme.2=Access locked to DifferenceScheme.3=Illegal argument to constructor method DifferenceScheme.4=Could not invoke constructor for Class XStep.Label=XStep +problem.notsupportedmessage=This problem statement is not currently supported. Please amend your choice. +problem.notsupportedtitle=Problem Not Supported +complexity.warning=

You have selected a high-complexity problem statement. Calculations will take longer than usual and are more likely to fail.

ExplicitScheme.2=Time interval too small: ExplicitScheme.3=Problem not supported or unknown: ExplicitScheme.4=Forward Time, Centred Space (FTCS) Scheme
  • Order of approximation O(h2 + τ)
  • Conditionally stable
  • Faster than other schemes
From 2f7e7d2606eb8f61cce63b5f5b361c6f1f69f246 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sun, 11 Oct 2020 11:44:24 +0100 Subject: [PATCH 010/116] Introduced dyed icons --- src/main/java/pulse/tasks/Calculation.java | 24 ++--- src/main/java/pulse/tasks/SearchTask.java | 8 +- src/main/java/pulse/tasks/TaskManager.java | 1 - .../pulse/ui/components/CalculationTable.java | 21 ++-- .../java/pulse/ui/components/ProblemTree.java | 8 +- .../java/pulse/ui/components/ResultTable.java | 14 +-- .../controllers/ProblemCellRenderer.java | 32 ++++++ .../ui/frames/ProblemStatementFrame.java | 3 +- .../pulse/ui/frames/TaskControlFrame.java | 17 +-- src/main/java/pulse/util/ImageUtils.java | 100 +++++++++++++----- 10 files changed, 151 insertions(+), 77 deletions(-) create mode 100644 src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index b1ac731e..b5826043 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -104,7 +104,10 @@ public void setProblem(Problem problem, ExperimentalData curve) { problem.setParent(this); problem.removeHeatingCurveListeners(); problem.retrieveData(curve); + addProblemListeners(problem, curve); + } + private void addProblemListeners(Problem problem, ExperimentalData curve) { problem.getProperties().addListener((PropertyEvent event) -> { var source = event.getSource(); @@ -130,7 +133,6 @@ public void setProblem(Problem problem, ExperimentalData curve) { } }); - } /** @@ -194,18 +196,13 @@ public boolean setStatus(Status status, Details details) { public NumericProperty weight(List all) { var result = def(MODEL_WEIGHT); - if (rs instanceof ModelSelectionCriterion) { - var criterion = (ModelSelectionCriterion) rs; - - boolean condition = all.stream() - .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(criterion.getClass())); - - if (condition) { - var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) - .collect(Collectors.toList()); - result = criterion.weight(list); - } + boolean condition = all.stream() + .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(rs.getClass())); + if (condition) { + var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) + .collect(Collectors.toList()); + result = rs.weight(list); } return result; @@ -287,7 +284,8 @@ public Result getResult() { public void setResult(Result result) { this.result = result; - result.setParent(this); + if(result != null) + result.setParent(this); } } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 77179868..3143e435 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -111,7 +111,10 @@ public SearchTask(ExperimentalData curve) { curve.setParent(this); correlationBuffer = new CorrelationBuffer(); clear(); - + addListeners(); + } + + private void addListeners() { InterpolationDataset.addListener(e -> { var p = current.getProblem().getProperties(); if(p.areThermalPropertiesLoaded()) @@ -126,7 +129,6 @@ public SearchTask(ExperimentalData curve) { scheme.setTimeLimit(derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); - } /** @@ -237,6 +239,8 @@ public void assign(IndexedVector searchParameters) { @Override public void run() { + current.setResult(null); + /* check of status */ switch (current.getStatus()) { diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 6d33b58a..3b8e6acb 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -130,7 +130,6 @@ public void execute(SearchTask t) { current.setParent(null); t.getStoredCalculations().add(current.copy()); current.setParent(t); - current.setResult(null); } else notifyListeners(e); }); diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 2806cc2b..d6d65433 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -50,13 +50,13 @@ public CalculationTable() { var t = instance.getTask(e.getId()); identifySelection(t); } - - else if(e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { + + else if (e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { update(TaskManager.getManagerInstance().getSelectedTask()); } }); - + } public void update(SearchTask t) { @@ -75,21 +75,16 @@ public void identifySelection(SearchTask t) { public void initListeners() { - /* - * selection listener - */ - var lsm = getSelectionModel(); lsm.addListSelectionListener((ListSelectionEvent e) -> { var task = TaskManager.getManagerInstance().getSelectedTask(); - if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { - var id = convertRowIndexToModel(this.getSelectedRow()); - if (id < task.getStoredCalculations().size()) { - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); - } + var id = convertRowIndexToModel(this.getSelectedRow()); + if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); } + }); } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index cce01460..657b08a0 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -14,6 +14,7 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.ProblemComplexity; +import pulse.ui.components.controllers.ProblemCellRenderer; import pulse.ui.components.listeners.ProblemSelectionEvent; import pulse.ui.components.listeners.ProblemSelectionListener; @@ -21,14 +22,15 @@ public class ProblemTree extends JTree { private List selectionListeners; - + public ProblemTree(List allProblems) { super(); + this.setCellRenderer(new ProblemCellRenderer()); var root = new DefaultMutableTreeNode("Problem Statements"); - + for (var c : ProblemComplexity.values()) { var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); - + allProblems.stream().filter(p -> p.getComplexity() == c).forEach(pFiltered -> { var node = new DefaultMutableTreeNode(pFiltered); currentComplexity.add(node); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 8e47f610..2a04c3a0 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -267,17 +267,11 @@ public void deleteSelected() { public void select(Result r) { var results = ((ResultTableModel) getModel()).getResults(); - if (results.contains(r)) { - - int jj = convertRowIndexToView(results.indexOf(r)); - - if (jj > -1) { - getSelectionModel().addSelectionInterval(jj, jj); - scrollToSelection(jj); - } - + int jj = convertRowIndexToView(results.indexOf(r)); + if (jj > -1) { + getSelectionModel().addSelectionInterval(jj, jj); + scrollToSelection(jj); } - } public void select(SearchTask t) { diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java new file mode 100644 index 00000000..daecbf52 --- /dev/null +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -0,0 +1,32 @@ +package pulse.ui.components.controllers; + +import java.awt.Component; + +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.UIManager; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; + +import com.alee.managers.icon.LazyIcon; + +import pulse.problem.statements.Problem; +import pulse.util.ImageUtils; + +@SuppressWarnings("serial") +public class ProblemCellRenderer extends DefaultTreeCellRenderer { + private static ImageIcon defaultIcon = (ImageIcon) ((LazyIcon) UIManager.getIcon("Tree.leafIcon")).getIcon(); + + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, + int row, boolean hasFocus) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + var object = ((DefaultMutableTreeNode)value).getUserObject(); + if (leaf && object instanceof Problem) { + var icon = ImageUtils.dye(defaultIcon, ((Problem)object).getComplexity().getColor()); + setIcon(icon); + } + return this; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index f1ce8f8b..33d7fdd7 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -24,6 +24,7 @@ import static pulse.util.Reflexive.instancesOf; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; @@ -173,7 +174,7 @@ public ProblemStatementFrame() { var btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(loadIcon("pulse.png", 20)); + pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); pulseFrame.setVisible(false); // simulate btn listener diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index ec63b40d..55b7f0f3 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -10,6 +10,7 @@ import static pulse.ui.Messages.getString; import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; @@ -176,23 +177,23 @@ private void initComponents() { setJMenuBar(mainMenu); logFrame = new LogFrame(); - logFrame.setFrameIcon(loadIcon("log.png", 20)); + logFrame.setFrameIcon(loadIcon("log.png", 20, Color.white)); resultsFrame = new ResultFrame(); - resultsFrame.setFrameIcon(loadIcon("result.png", 20)); + resultsFrame.setFrameIcon(loadIcon("result.png", 20, Color.white)); previewFrame = new PreviewFrame(); - previewFrame.setFrameIcon(loadIcon("preview.png", 20)); + previewFrame.setFrameIcon(loadIcon("preview.png", 20, Color.white)); taskManagerFrame = new TaskManagerFrame(); - taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20)); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20, Color.white)); graphFrame = MainGraphFrame.getInstance(); - graphFrame.setFrameIcon(loadIcon("curves.png", 20)); + graphFrame.setFrameIcon(loadIcon("curves.png", 20, Color.white)); problemStatementFrame = new ProblemStatementFrame(); - problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20)); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20, Color.white)); modelFrame = new ModelSelectionFrame(); - modelFrame.setFrameIcon(loadIcon("stored.png", 20)); + modelFrame.setFrameIcon(loadIcon("stored.png", 20, Color.white)); searchOptionsFrame = new SearchOptionsFrame(); - searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20)); + searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); /* * CONSTRAINT ADJUSTMENT diff --git a/src/main/java/pulse/util/ImageUtils.java b/src/main/java/pulse/util/ImageUtils.java index fd2b7556..a6c5270f 100644 --- a/src/main/java/pulse/util/ImageUtils.java +++ b/src/main/java/pulse/util/ImageUtils.java @@ -2,7 +2,11 @@ import static java.awt.Image.SCALE_SMOOTH; +import java.awt.AlphaComposite; import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import javax.swing.ImageIcon; @@ -11,9 +15,9 @@ public class ImageUtils { private ImageUtils() { - //intentionally blank + // intentionally blank } - + public static ImageIcon loadIcon(String path, int iconSize) { var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a // imageIcon @@ -22,30 +26,74 @@ public static ImageIcon loadIcon(String path, int iconSize) { return new ImageIcon(newimg); // transform it back } - public static Color blend( final Color c1, final Color c2, float ratio ) { - if ( ratio > 1f ) ratio = 1f; - else if ( ratio < 0f ) ratio = 0f; - float iRatio = 1.0f - ratio; - - int i1 = c1.getRGB(); - int i2 = c2.getRGB(); - - int a1 = (i1 >> 24 & 0xff); - int r1 = ((i1 & 0xff0000) >> 16); - int g1 = ((i1 & 0xff00) >> 8); - int b1 = (i1 & 0xff); - - int a2 = (i2 >> 24 & 0xff); - int r2 = ((i2 & 0xff0000) >> 16); - int g2 = ((i2 & 0xff00) >> 8); - int b2 = (i2 & 0xff); - - int a = (int)((a1 * iRatio) + (a2 * ratio)); - int r = (int)((r1 * iRatio) + (r2 * ratio)); - int g = (int)((g1 * iRatio) + (g2 * ratio)); - int b = (int)((b1 * iRatio) + (b2 * ratio)); - - return new Color( a << 24 | r << 16 | g << 8 | b ); + public static ImageIcon loadIcon(String path, int iconSize, Color clr) { + var icon = loadIcon(path, iconSize); + return dye(icon, clr); + } + + /** + * Credit to Marco13 + * (https://stackoverflow.com/questions/21382966/colorize-a-picture-in-java) + */ + + public static BufferedImage dye(BufferedImage image, Color color) { + int w = image.getWidth(); + int h = image.getHeight(); + BufferedImage dyed = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = dyed.createGraphics(); + g.drawImage(image, 0, 0, null); + g.setComposite(AlphaComposite.SrcAtop); + g.setColor(color); + g.fillRect(0, 0, w, h); + g.dispose(); + return dyed; } + /** + * Credit to Werner Kvalem Vesterås (https://stackoverflow.com/questions/15053214/converting-an-imageicon-to-a-bufferedimage) + */ + + public static ImageIcon dye(ImageIcon icon, Color color) { + var bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics g = bi.createGraphics(); + // paint the Icon to the BufferedImage. + icon.paintIcon(null, g, 0, 0); + g.dispose(); + var dyedImage = dye(bi, color); + return new ImageIcon(dyedImage); + } + + /** + * Credit to bmauter + * (https://stackoverflow.com/questions/19398238/how-to-mix-two-int-colors-correctly) + */ + + public static Color blend(final Color c1, final Color c2, float ratio) { + if (ratio > 1f) + ratio = 1f; + else if (ratio < 0f) + ratio = 0f; + float iRatio = 1.0f - ratio; + + int i1 = c1.getRGB(); + int i2 = c2.getRGB(); + + int a1 = (i1 >> 24 & 0xff); + int r1 = ((i1 & 0xff0000) >> 16); + int g1 = ((i1 & 0xff00) >> 8); + int b1 = (i1 & 0xff); + + int a2 = (i2 >> 24 & 0xff); + int r2 = ((i2 & 0xff0000) >> 16); + int g2 = ((i2 & 0xff00) >> 8); + int b2 = (i2 & 0xff); + + int a = (int) ((a1 * iRatio) + (a2 * ratio)); + int r = (int) ((r1 * iRatio) + (r2 * ratio)); + int g = (int) ((g1 * iRatio) + (g2 * ratio)); + int b = (int) ((b1 * iRatio) + (b2 * ratio)); + + return new Color(a << 24 | r << 16 | g << 8 | b); + } + } \ No newline at end of file From ac3d8bce11870dce5597e55784e2a877c4db6591 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sun, 11 Oct 2020 12:19:46 +0100 Subject: [PATCH 011/116] Fixed three things mainly -R-squared calculation (the method did not initiate a residual calculation previously) -Reset button -Problem statement frame auto-update on show --- src/main/java/pulse/search/statistics/RSquaredTest.java | 2 ++ src/main/java/pulse/tasks/SearchTask.java | 3 ++- src/main/java/pulse/ui/frames/ProblemStatementFrame.java | 2 ++ src/main/java/pulse/ui/frames/TaskControlFrame.java | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index d7755915..7bb32f41 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -53,6 +53,8 @@ public boolean test(SearchTask task) { public void evaluate(SearchTask t) { var reference = t.getExperimentalCurve(); + sos.evaluate(t); + final int start = reference.getIndexRange().getLowerBound(); final int end = reference.getIndexRange().getUpperBound(); diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 3143e435..18bc1bfb 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -160,7 +160,8 @@ public void clear() { this.path = null; current.clear(); - setStatus(INCOMPLETE); + this.checkProblems(); + this.notifyStatusListeners(new StateEntry(this, current.getStatus())); } /** diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 33d7fdd7..3e281cae 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -271,6 +271,8 @@ public void setSelectionPath(TreePath path) { .forEach(pp -> pp.updateProperty(event, event.getProperty())); }); + + } diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 55b7f0f3..65e30aa1 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -108,6 +108,7 @@ private void initListeners() { @Override public void onProblemStatementShowRequest() { + problemStatementFrame.update(); setProblemStatementFrameVisible(true); } From a1e15cd1dd75ef7b785d1f36817c4e843b340d39 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sun, 11 Oct 2020 13:03:29 +0100 Subject: [PATCH 012/116] Fixed table operations --- src/main/java/pulse/ui/components/ResultTable.java | 5 +++-- .../java/pulse/ui/components/models/ResultTableModel.java | 2 +- src/main/resources/NumericProperty.xml | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 2a04c3a0..b4d85b1b 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -267,8 +267,9 @@ public void deleteSelected() { public void select(Result r) { var results = ((ResultTableModel) getModel()).getResults(); - int jj = convertRowIndexToView(results.indexOf(r)); - if (jj > -1) { + int modelIndex = results.indexOf(r); + if (modelIndex > -1) { + int jj = convertRowIndexToView(modelIndex); getSelectionModel().addSelectionInterval(jj, jj); scrollToSelection(jj); } diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 4a3e0b0f..bb8417bb 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -105,7 +105,7 @@ public void removeAll(Identifier id) { if (!(result instanceof Result)) continue; - if (result.identify().equals(id)) { + if (id.equals(result.identify())) { results.remove(result); super.removeRow(i); } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 72cd8f0f..2e46515c 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -18,8 +18,8 @@

Date of release: 21/09/2020
Lead developer:
Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii

Date: Mon, 12 Oct 2020 11:38:05 +0100 Subject: [PATCH 013/116] Corrected a possible zero number of threads for single-thread machines --- src/main/java/pulse/tasks/TaskManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 3b8e6acb..d0016778 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -87,7 +87,8 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList(); - taskPool = new ForkJoinPool(THREADS_AVAILABLE - 1); + int threads = THREADS_AVAILABLE > 1 ? THREADS_AVAILABLE - 1 : 1; + taskPool = new ForkJoinPool(threads); selectionListeners = new CopyOnWriteArrayList(); taskRepositoryListeners = new CopyOnWriteArrayList(); this.addHierarchyListener(statementListener); From d98425ba7837421586d6d53f67ffd821632ea588 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 12 Oct 2020 11:40:17 +0100 Subject: [PATCH 014/116] Corrected THREADS_AVAILABLE --- src/main/java/pulse/tasks/TaskManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index d0016778..dff07137 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -87,8 +87,7 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList(); - int threads = THREADS_AVAILABLE > 1 ? THREADS_AVAILABLE - 1 : 1; - taskPool = new ForkJoinPool(threads); + taskPool = new ForkJoinPool(THREADS_AVAILABLE); selectionListeners = new CopyOnWriteArrayList(); taskRepositoryListeners = new CopyOnWriteArrayList(); this.addHierarchyListener(statementListener); From d6af14fe201fdf37ba29e48ada6d51ca8cea409d Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Tue, 13 Oct 2020 10:02:55 +0100 Subject: [PATCH 015/116] A few fixes: - A dedicated class for resource monitoring; - Fixed number of threads in exporting - Error logs now saved directly in a file in the working directory --- src/main/java/pulse/tasks/TaskManager.java | 5 +- src/main/java/pulse/ui/Launcher.java | 98 +++----------- .../java/pulse/ui/components/LogPane.java | 41 +----- .../ui/components/panels/ModelToolbar.java | 8 +- .../ui/components/panels/SystemPanel.java | 19 ++- .../pulse/ui/frames/dialogs/ExportDialog.java | 8 +- src/main/java/pulse/util/ResourceMonitor.java | 120 ++++++++++++++++++ 7 files changed, 160 insertions(+), 139 deletions(-) create mode 100644 src/main/java/pulse/util/ResourceMonitor.java diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index dff07137..3df2f751 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -16,7 +16,6 @@ import static pulse.tasks.logs.Status.DONE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.QUEUED; -import static pulse.ui.Launcher.threadsAvailable; import static pulse.util.Group.contents; import java.io.File; @@ -41,6 +40,7 @@ import pulse.util.Group; import pulse.util.HierarchyListener; import pulse.util.PropertyHolder; +import pulse.util.ResourceMonitor; import pulse.util.UpwardsNavigable; /** @@ -62,7 +62,6 @@ public class TaskManager extends UpwardsNavigable { private boolean singleStatement = true; - private final int THREADS_AVAILABLE = threadsAvailable(); private ForkJoinPool taskPool; private List selectionListeners; @@ -87,7 +86,7 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList(); - taskPool = new ForkJoinPool(THREADS_AVAILABLE); + taskPool = new ForkJoinPool(ResourceMonitor.getInstance().getThreadsAvailable()); selectionListeners = new CopyOnWriteArrayList(); taskRepositoryListeners = new CopyOnWriteArrayList(); this.addHierarchyListener(statementListener); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 5b02434f..c0f2403b 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -2,20 +2,17 @@ import static java.awt.EventQueue.invokeLater; import static java.awt.SplashScreen.getSplashScreen; -import static java.lang.Integer.valueOf; -import static java.lang.Runtime.getRuntime; import static java.lang.System.err; -import static java.lang.management.ManagementFactory.getPlatformMBeanServer; +import static java.lang.System.setErr; +import static java.time.LocalDateTime.now; +import static java.time.format.DateTimeFormatter.ISO_WEEK_DATE; import static java.util.Objects.requireNonNull; -import static javax.management.ObjectName.getInstance; import static pulse.ui.frames.TaskControlFrame.getInstance; -import javax.management.Attribute; -import javax.management.AttributeList; -import javax.management.InstanceNotFoundException; -import javax.management.MalformedObjectNameException; -import javax.management.ObjectName; -import javax.management.ReflectionException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; + import javax.swing.UIManager; import com.alee.laf.WebLookAndFeel; @@ -32,9 +29,11 @@ */ public class Launcher { + + private PrintStream errStream; private Launcher() { - // intentionally blank + arrangeErrorOutput(); } /** @@ -42,6 +41,7 @@ private Launcher() { */ public static void main(String[] args) { + new Launcher(); splashScreen(); WebLookAndFeel.install( WebDarkSkin.class); @@ -67,77 +67,19 @@ private static void splashScreen() { requireNonNull(g, "splash.createGraphics() returned null"); } } - - /** - *

- * This will calculate the ratio {@code totalMemory/maxMemory} using the - * standard {@code Runtime}. Note this memory usage depends on heap allocation - * for the JVM. - *

- * - * @return a value depicting the memory usage - */ - - public static double getMemoryUsage() { - double totalMemory = getRuntime().totalMemory(); - double maxMemory = getRuntime().maxMemory(); - return (totalMemory / maxMemory * 100); - } - - /** - *

- * This will calculate the CPU load for the machine running {@code PULsE}. Note - * this is rather code-intensive, so it is recommende to use only at certain - * time intervals. - *

- * - * @return a value depicting the CPU usage - */ - - public static double cpuUsage() { - - var mbs = getPlatformMBeanServer(); - ObjectName name = null; + + private void arrangeErrorOutput() { try { - name = getInstance("java.lang:type=OperatingSystem"); - } catch (MalformedObjectNameException | NullPointerException e1) { - err.println("Error while calculating CPU usage:"); - e1.printStackTrace(); - } - - AttributeList list = null; - try { - list = mbs.getAttributes(name, new String[] { "ProcessCpuLoad" }); - } catch (InstanceNotFoundException | ReflectionException e) { - err.println("Error while calculating CPU usage:"); + setErr( new PrintStream(new File("ErrorLog_" + now().format(ISO_WEEK_DATE) + ".log")) ); + } catch (FileNotFoundException e) { + System.err.println("Unable to set up error stream"); e.printStackTrace(); } - - if (list.isEmpty()) - return valueOf(null); - - var att = (Attribute) list.get(0); - var value = (double) att.getValue(); - - if (value < 0) - return valueOf(null); - - return (value * 100); } - - /** - * Finds the number of threads available for calculation. This will be used by - * the {@code TaskManager} when allocating the {@code ForkJoinPool} for running - * several tasks in parallel. - * - * @return the number of threads, which is greater or equal to the number of - * cores - * @see pulse.tasks.TaskManager - */ - - public static int threadsAvailable() { - var number = getRuntime().availableProcessors(); - return number > 1 ? (number - 1) : 1; + + @Override + public void finalize() { + errStream.close(); } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index aeb57e5f..72802e5b 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -1,9 +1,6 @@ package pulse.ui.components; -import static java.lang.String.valueOf; import static java.lang.System.err; -import static java.lang.System.setErr; -import static java.lang.System.setOut; import static java.time.temporal.ChronoUnit.MILLIS; import static java.time.temporal.ChronoUnit.SECONDS; import static java.util.concurrent.Executors.newSingleThreadExecutor; @@ -12,8 +9,6 @@ import static pulse.ui.Messages.getString; import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; import java.util.concurrent.ExecutorService; import javax.swing.JEditorPane; @@ -32,52 +27,26 @@ public class LogPane extends JEditorPane implements Descriptive { private ExecutorService updateExecutor = newSingleThreadExecutor(); - private final static boolean DEBUG = true; - - private PrintStream outStream, errStream; - public LogPane() { super(); setContentType("text/html"); setEditable(false); var c = (DefaultCaret) getCaret(); c.setUpdatePolicy(ALWAYS_UPDATE); - - OutputStream out = new OutputStream() { - @Override - public void write(final int b) throws IOException { - postError(valueOf((char) b)); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - postError(new String(b, off, len)); - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - }; - - if (!DEBUG) { - setOut(outStream = new PrintStream(out, true)); - setErr(errStream = new PrintStream(out, true)); - } - } private void post(LogEntry logEntry) { post(logEntry.toString()); } + /* private void postError(String text) { var sb = new StringBuilder(); sb.append(getString("DataLogEntry.FontTagError")); sb.append(text); sb.append(getString("DataLogEntry.FontTagClose")); post(sb.toString()); - } + }*/ private void post(String text) { @@ -153,12 +122,6 @@ public void clear() { } } - @Override - public void finalize() { - outStream.close(); - errStream.close(); - } - public ExecutorService getUpdateExecutor() { return updateExecutor; } diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index ede552eb..d3c19e5d 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -2,9 +2,9 @@ import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.Dimension; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComboBox; @@ -32,13 +32,11 @@ public ModelToolbar() { ); criterionSelection.setSelectedItem(Calculation.getModelSelectionDescriptor().getValue()); - this.setBorder(BorderFactory.createEtchedBorder()); - add(new JLabel("Model Selection Criterion: ")); add(Box.createRigidArea(new Dimension(5,0))); add(criterionSelection); - var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE)); + var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE, Color.WHITE)); doCalc.setToolTipText("Re-calculate model weights"); add(Box.createRigidArea(new Dimension(15,0))); add(doCalc); @@ -50,7 +48,7 @@ public ModelToolbar() { instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); }); - var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE)); + var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE, Color.RED)); bestSelection.setToolTipText("Select Best Model"); add(Box.createRigidArea(new Dimension(15,0))); add(bestSelection); diff --git a/src/main/java/pulse/ui/components/panels/SystemPanel.java b/src/main/java/pulse/ui/components/panels/SystemPanel.java index 84117090..d57df067 100644 --- a/src/main/java/pulse/ui/components/panels/SystemPanel.java +++ b/src/main/java/pulse/ui/components/panels/SystemPanel.java @@ -7,9 +7,6 @@ import static javax.swing.SwingConstants.CENTER; import static javax.swing.SwingConstants.LEFT; import static javax.swing.SwingConstants.RIGHT; -import static pulse.ui.Launcher.cpuUsage; -import static pulse.ui.Launcher.getMemoryUsage; -import static pulse.ui.Launcher.threadsAvailable; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -19,6 +16,7 @@ import javax.swing.UIManager; import pulse.util.ImageUtils; +import pulse.util.ResourceMonitor; @SuppressWarnings("serial") public class SystemPanel extends JPanel { @@ -60,22 +58,23 @@ private void initComponents() { } private void startSystemMonitors() { - var coresAvailable = format("{" + (threadsAvailable() + 1) + " cores}"); + var monitor = ResourceMonitor.getInstance(); + + var coresAvailable = format("{" + (monitor.getThreadsAvailable() + 1) + " cores}"); coresLabel.setText(coresAvailable); var executor = newSingleThreadScheduledExecutor(); var defColor = UIManager.getColor("Label.foreground"); Runnable periodicTask = () -> { - var cpuUsage = cpuUsage(); - var memoryUsage = getMemoryUsage(); - var cpuString = format("CPU usage: %3.1f%%", cpuUsage); + monitor.update(); + var cpuString = format("CPU usage: %3.1f%%", monitor.getCpuUsage()); cpuLabel.setText(cpuString); - var memoryString = format("Memory usage: %3.1f%%", memoryUsage); + var memoryString = format("Memory usage: %3.1f%%", monitor.getMemoryUsage()); memoryLabel.setText(memoryString); - cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float)cpuUsage/100)); - memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float)memoryUsage/100)); + cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float)monitor.getCpuUsage()/100)); + memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float)monitor.getMemoryUsage()/100)); }; diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index f83b3282..37d0fe93 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -11,7 +11,6 @@ import static pulse.io.export.ExportManager.exportAllResults; import static pulse.io.export.ExportManager.exportGroup; import static pulse.io.export.Extension.valueOf; -import static pulse.ui.Launcher.threadsAvailable; import java.awt.Dimension; import java.io.File; @@ -43,6 +42,7 @@ import pulse.tasks.TaskManager; import pulse.tasks.logs.Log; import pulse.tasks.processing.Result; +import pulse.util.ResourceMonitor; @SuppressWarnings("serial") public class ExportDialog extends JDialog { @@ -104,11 +104,11 @@ private void export(Extension extension) { if (subdirs.size() > 0 && !destination.exists()) destination.mkdirs(); - final var threads = threadsAvailable(); + var monitor = ResourceMonitor.getInstance(); if (createSubdirectories) { progressFrame.trackProgress(subdirs.size()); - var pool = newFixedThreadPool(threads - 1); + var pool = newFixedThreadPool( monitor.getThreadsAvailable() ); subdirs.stream().forEach(s -> { pool.submit(() -> { exportGroup(s, destination, extension); @@ -117,7 +117,7 @@ private void export(Extension extension) { }); } else { var groupped = instance.allGrouppedContents(); - var pool = newFixedThreadPool(threads - 1); + var pool = newFixedThreadPool( monitor.getThreadsAvailable() ); progressFrame.trackProgress(groupped.size()); groupped.stream().forEach(individual -> pool.submit(() -> { diff --git a/src/main/java/pulse/util/ResourceMonitor.java b/src/main/java/pulse/util/ResourceMonitor.java new file mode 100644 index 00000000..5ff072e1 --- /dev/null +++ b/src/main/java/pulse/util/ResourceMonitor.java @@ -0,0 +1,120 @@ +package pulse.util; + +import static java.lang.Runtime.getRuntime; +import static java.lang.System.err; +import static java.lang.management.ManagementFactory.getPlatformMBeanServer; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +/** + * Provides means of storage and methods for access to runtime system information, + * such as CPU usage, memory usage, an number of available threads. + * + */ + +public class ResourceMonitor { + + private double memoryUsage; + private double cpuUsage; + private int threadsAvailable; + + private static ResourceMonitor instance = new ResourceMonitor(); + + private ResourceMonitor() { + threadsAvailable(); + } + + public void update() { + cpuUsage(); + memoryUsage(); + } + + /** + *

+ * This will calculate the ratio {@code totalMemory/maxMemory} using the + * standard {@code Runtime}. Note this memory usage depends on heap allocation + * for the JVM. + *

+ * + */ + + public void memoryUsage() { + final double totalMemory = getRuntime().totalMemory(); + final double maxMemory = getRuntime().maxMemory(); + memoryUsage = (totalMemory / maxMemory * 100); + } + + /** + *

+ * This will calculate the CPU load for the machine running {@code PULsE}. Note + * this is rather code-intensive, so it is recommended for use only at certain + * time intervals. + *

+ * + */ + + public void cpuUsage() { + + var mbs = getPlatformMBeanServer(); + ObjectName name = null; + try { + name = ObjectName.getInstance("java.lang:type=OperatingSystem"); + } catch (MalformedObjectNameException | NullPointerException e1) { + err.println("Error while calculating CPU usage:"); + e1.printStackTrace(); + } + + AttributeList list = null; + try { + list = mbs.getAttributes(name, new String[] { "ProcessCpuLoad" }); + } catch (InstanceNotFoundException | ReflectionException e) { + err.println("Error while calculating CPU usage:"); + e.printStackTrace(); + } + + if (!list.isEmpty()) { + + var att = (Attribute) list.get(0); + var value = (double) att.getValue(); + + cpuUsage = value < 0 ? 0 : (value * 100); + + } + + } + + /** + * Finds the number of threads available for calculation. This will be used by + * the {@code TaskManager} when allocating the {@code ForkJoinPool} for running + * several tasks in parallel. The number of threads is greater or equal to the number of + * cores + * @see pulse.tasks.TaskManager + */ + + public void threadsAvailable() { + final int number = getRuntime().availableProcessors(); + threadsAvailable = number > 1 ? (number - 1) : 1; + } + + public double getCpuUsage() { + return cpuUsage; + } + + public int getThreadsAvailable() { + return threadsAvailable; + } + + public double getMemoryUsage() { + return memoryUsage; + } + + public static ResourceMonitor getInstance() { + return instance; + } + +} \ No newline at end of file From 3be4160837a754d2d3ad6a8054f9c61928738cd8 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Tue, 13 Oct 2020 10:54:55 +0100 Subject: [PATCH 016/116] Fixed error log file not appearing in the current directory --- src/main/java/pulse/ui/Launcher.java | 14 +++++++++++++- src/main/java/pulse/util/ResourceMonitor.java | 2 +- src/main/resources/About.html | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index c0f2403b..89ab860f 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -12,6 +12,8 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import javax.swing.UIManager; @@ -69,8 +71,18 @@ private static void splashScreen() { } private void arrangeErrorOutput() { + String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + String decodedPath = ""; try { - setErr( new PrintStream(new File("ErrorLog_" + now().format(ISO_WEEK_DATE) + ".log")) ); + decodedPath = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e1) { + System.err.println("Unsupported UTF-8 encoding. Details below."); + e1.printStackTrace(); + } + // + try { + var dir = new File(decodedPath).getParent(); + setErr( new PrintStream(new File(dir + File.separator + "ErrorLog_" + now().format(ISO_WEEK_DATE) + ".log")) ); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); diff --git a/src/main/java/pulse/util/ResourceMonitor.java b/src/main/java/pulse/util/ResourceMonitor.java index 5ff072e1..a78e3b0f 100644 --- a/src/main/java/pulse/util/ResourceMonitor.java +++ b/src/main/java/pulse/util/ResourceMonitor.java @@ -12,7 +12,7 @@ import javax.management.ReflectionException; /** - * Provides means of storage and methods for access to runtime system information, + * Provides unified means of storage and methods of access to runtime system information, * such as CPU usage, memory usage, an number of available threads. * */ diff --git a/src/main/resources/About.html b/src/main/resources/About.html index a531f4b7..bbf1eee2 100644 --- a/src/main/resources/About.html +++ b/src/main/resources/About.html @@ -1,5 +1,5 @@
Processing Unit for Laser flash Experiments
-
(PULsE), v. 1.88
-
+
(PULsE), v. 1.88b3
+

Date of release: 13/10/2020
Lead developer: Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii

PULsE is an advanced software toolkit for processing data from laser flash experiments, allowing effective treatment of difficult cases where conditions may not be ideal for simpler analysis. PULsE analyses the heating curves from laser flash experiments and outputs the thermal properties of the sample.

This software is specifically tailored for use in the Materials Research Facility at UKAEA, and reads ASCII files generated by the Linseis LFA; it was initially designed to read custom file formats from the 'Kvant' laser flash analyser. For full specification regarding the file formats, please refer to manual.

\ No newline at end of file From 22d203ce500eb09e6f682aff57ca69bb09038de1 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Tue, 13 Oct 2020 18:15:49 +0100 Subject: [PATCH 017/116] Added shutdown hook to delete emty error log on exit --- src/main/java/pulse/ui/Launcher.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 89ab860f..0cd0236b 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -5,7 +5,6 @@ import static java.lang.System.err; import static java.lang.System.setErr; import static java.time.LocalDateTime.now; -import static java.time.format.DateTimeFormatter.ISO_WEEK_DATE; import static java.util.Objects.requireNonNull; import static pulse.ui.frames.TaskControlFrame.getInstance; @@ -33,6 +32,7 @@ public class Launcher { private PrintStream errStream; + private File errorLog; private Launcher() { arrangeErrorOutput(); @@ -82,11 +82,23 @@ private void arrangeErrorOutput() { // try { var dir = new File(decodedPath).getParent(); - setErr( new PrintStream(new File(dir + File.separator + "ErrorLog_" + now().format(ISO_WEEK_DATE) + ".log")) ); + errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); + setErr( new PrintStream(errorLog) ); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); } + + /* + * Delete log file on program exit if empty + */ + + Runnable r = () -> { + if(errorLog != null && errorLog.exists() && errorLog.length() < 1) + errorLog.delete(); + }; + Runtime.getRuntime().addShutdownHook(new Thread(r)); + } @Override From c2183b239001f5868e692daf7af8e58e8f5ec00f Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 14 Oct 2020 15:43:53 +0100 Subject: [PATCH 018/116] Fixed a number of things and introduces better version control - Fixed incorrect plotting method call when trying to plot an incomplete curve; - Fixed ProblemStatementFrame button highlighted incorrectly - Introduced a Version.txt with current version info in the .jar file - The current version is now checked against the latest version available on the Internet - The 'About' dialog has been replaced with a link to the PULsE web-site. --- .../java/pulse/io/readers/ReaderManager.java | 21 ++ src/main/java/pulse/tasks/Calculation.java | 15 +- src/main/java/pulse/tasks/SearchTask.java | 60 +++--- src/main/java/pulse/tasks/logs/Status.java | 16 ++ src/main/java/pulse/ui/Launcher.java | 52 +++-- src/main/java/pulse/ui/Version.java | 66 +++++++ src/main/java/pulse/ui/components/Chart.java | 31 +-- .../pulse/ui/components/PulseMainMenu.java | 18 +- .../pulse/ui/components/TaskPopupMenu.java | 53 ++--- .../components/buttons/ExecutionButton.java | 13 +- .../ui/components/panels/ChartToolbar.java | 16 +- .../ui/components/panels/LogToolbar.java | 10 +- .../ui/components/panels/ProblemToolbar.java | 101 ++++++++++ .../ui/components/panels/ResultToolbar.java | 9 +- .../ui/components/panels/TaskToolbar.java | 11 +- .../ui/frames/ProblemStatementFrame.java | 182 ++++-------------- .../pulse/ui/frames/SearchOptionsFrame.java | 4 +- .../pulse/ui/frames/TaskControlFrame.java | 20 +- .../pulse/ui/frames/dialogs/AboutDialog.java | 64 ------ src/main/resources/About.html | 5 - src/main/resources/Version.txt | 1 + src/main/resources/images/clear.png | Bin 29219 -> 37672 bytes src/main/resources/images/graph.png | Bin 26005 -> 35409 bytes src/main/resources/messages.properties | 2 +- 24 files changed, 424 insertions(+), 346 deletions(-) create mode 100644 src/main/java/pulse/ui/Version.java create mode 100644 src/main/java/pulse/ui/components/panels/ProblemToolbar.java delete mode 100644 src/main/java/pulse/ui/frames/dialogs/AboutDialog.java delete mode 100644 src/main/resources/About.html create mode 100644 src/main/resources/Version.txt diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 44c87642..e8535467 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -11,8 +11,10 @@ import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import pulse.ui.Messages; +import pulse.ui.Version; import pulse.util.ReflexiveFinder; /** @@ -247,5 +249,24 @@ private static T readSpecific(AbstractReader reader, String location, Str } return result; } + + public static Version readVersion() { + var versionInfoFile = Version.class.getResource("/Version.txt"); + String versionLabel = ""; + long date = 0; + try { + date = versionInfoFile.openConnection().getLastModified(); + } catch (IOException e1) { + System.err.println("Could not connect to local version file!"); + e1.printStackTrace(); + } + try { + versionLabel = IOUtils.toString(versionInfoFile, "UTF-8"); + } catch (IOException e) { + System.err.println("Could not read current version!"); + e.printStackTrace(); + } + return new Version(versionLabel, date); + } } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index b5826043..5b73f63c 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -23,7 +23,6 @@ import pulse.search.statistics.AICStatistic; import pulse.search.statistics.ModelSelectionCriterion; import pulse.search.statistics.OptimiserStatistic; -import pulse.tasks.logs.Details; import pulse.tasks.logs.Status; import pulse.tasks.processing.Result; import pulse.ui.components.PropertyHolderTable; @@ -179,17 +178,9 @@ public Status getStatus() { return status; } - public boolean setStatus(Status status, Details details) { - boolean done = false; - - if (this.status != status) { - this.status = status; - done = true; - } else if (this.status.getDetails() != status.getDetails()) { - this.status.setDetails(status.getDetails()); - done = true; - } - + public boolean setStatus(Status status) { + boolean done = this.status != status; + this.status = status; return done; } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 18bc1bfb..89b402be 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; @@ -48,7 +49,6 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; -import pulse.tasks.logs.Details; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; @@ -160,8 +160,7 @@ public void clear() { this.path = null; current.clear(); - this.checkProblems(); - this.notifyStatusListeners(new StateEntry(this, current.getStatus())); + this.checkProblems(true); } /** @@ -314,8 +313,11 @@ public void run() { private void runChecks() { - if (!normalityTest.test(this)) // first, check if the residuals are normally-distributed - setStatus(FAILED, ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed + var status = FAILED; + status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + setStatus(status); + } else { @@ -323,16 +325,22 @@ private void runChecks() { // correlations notifyDataListeners(new CorrelationLogEntry(this)); - if (test) - setStatus(AMBIGUOUS, SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + if (test) { + var status = AMBIGUOUS; + status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + setStatus(status); + } else { // lastly, check if the parameter values estimated in this procedure are // reasonable var properties = alteredParameters(); - if (properties.stream().anyMatch(np -> !np.validate())) - setStatus(FAILED, PARAMETER_VALUES_NOT_SENSIBLE); + if (properties.stream().anyMatch(np -> !np.validate())) { + var status = FAILED; + status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + } else { current.getModelSelectionCriterion().evaluate(this); setStatus(DONE); @@ -387,25 +395,11 @@ public void setExperimentalCurve(ExperimentalData curve) { } - public void setStatus(Status status, Details details) { - status.setDetails(details); - if(current.setStatus(status, details)) - notifyStatusListeners(new StateEntry(this, status)); - } - - /** - * Sets a new {@code status} to this {@code SearchTask} and informs the - * listeners. - * - * @param status the new status - */ - public void setStatus(Status status) { - setStatus(status, Details.NONE); - } - - public Status checkProblems() { - return checkProblems(true); + Objects.requireNonNull(status); + boolean changed = current.setStatus(status); + if(changed) + notifyStatusListeners(new StateEntry(this, status)); } /** @@ -420,13 +414,13 @@ public Status checkProblems() { * exist. For the latter, additional details will be available using the * {@code status.getDetails()} method. *

- * @return the current status */ - public Status checkProblems(boolean updateStatus) { + public void checkProblems(boolean updateStatus) { var status = current.getStatus(); + if (status == DONE) - return status; + return; var pathSolver = getInstance(); var s = INCOMPLETE; @@ -447,11 +441,9 @@ else if (buffer == null) s.setDetails(MISSING_BUFFER); else s = READY; - - if (updateStatus) + + if(updateStatus) setStatus(s); - - return status; } public Identifier getIdentifier() { diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index e7670a0b..6092b3a2 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -103,6 +103,22 @@ static String parse(String str) { return sb.toString(); } + + public boolean checkProblemStatementSet() { + if(details == null) + return true; + + switch(details) { + case MISSING_DIFFERENCE_SCHEME : + case MISSING_HEATING_CURVE : + case MISSING_PROBLEM_STATEMENT : + case INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT : + return false; + default : + return true; + } + + } @Override public String toString() { diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 0cd0236b..38d995a8 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -14,6 +14,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import javax.swing.JOptionPane; import javax.swing.UIManager; import com.alee.laf.WebLookAndFeel; @@ -22,17 +23,18 @@ /** *

* This is the main class used to launch {@code PULsE} and start the GUI. In - * addition to providing the launcher methods, it also provides some - * functionality for accessing the System CPU and memory usage, as well as the - * number of available threads that can be used in calculation. + * addition to providing the launcher methods, it also redirects the System.err + * stream to an external file. An empty log file is deleted upon program exit + * via a shutdown hook. *

* */ public class Launcher { - + private PrintStream errStream; private File errorLog; + private final static boolean DEBUG = true; private Launcher() { arrangeErrorOutput(); @@ -46,17 +48,25 @@ public static void main(String[] args) { new Launcher(); splashScreen(); - WebLookAndFeel.install( WebDarkSkin.class); + WebLookAndFeel.install(WebDarkSkin.class); try { - UIManager.setLookAndFeel( new WebLookAndFeel() ); - } catch( Exception ex ) { - System.err.println( "Failed to initialize LaF" ); + UIManager.setLookAndFeel(new WebLookAndFeel()); + } catch (Exception ex) { + System.err.println("Failed to initialize LaF"); } - + + var newVersion = Version.getCurrentVersion().checkNewVersion(); + /* Create and display the form */ invokeLater(() -> { getInstance().setLocationRelativeTo(null); getInstance().setVisible(true); + + if (newVersion != null) { + JOptionPane.showMessageDialog(null, "A new version of this software is available: " + + newVersion.toString() + "
Please visit the PULsE website for more details."); + } + }); } @@ -69,10 +79,14 @@ private static void splashScreen() { requireNonNull(g, "splash.createGraphics() returned null"); } } - + private void arrangeErrorOutput() { + if (DEBUG) + return; + String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); String decodedPath = ""; + // try { decodedPath = URLDecoder.decode(path, "UTF-8"); } catch (UnsupportedEncodingException e1) { @@ -83,24 +97,30 @@ private void arrangeErrorOutput() { try { var dir = new File(decodedPath).getParent(); errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); - setErr( new PrintStream(errorLog) ); + setErr(new PrintStream(errorLog)); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); } - + + createShutdownHook(); + + } + + private void createShutdownHook() { + /* * Delete log file on program exit if empty */ - + Runnable r = () -> { - if(errorLog != null && errorLog.exists() && errorLog.length() < 1) + if (errorLog != null && errorLog.exists() && errorLog.length() < 1) errorLog.delete(); }; Runtime.getRuntime().addShutdownHook(new Thread(r)); - + } - + @Override public void finalize() { errStream.close(); diff --git a/src/main/java/pulse/ui/Version.java b/src/main/java/pulse/ui/Version.java new file mode 100644 index 00000000..1a1074e4 --- /dev/null +++ b/src/main/java/pulse/ui/Version.java @@ -0,0 +1,66 @@ +package pulse.ui; + +import static pulse.ui.Messages.getString; + +import java.io.IOException; +import java.net.URL; +import java.text.DateFormat; +import java.util.Date; + +import org.apache.commons.io.IOUtils; + +import pulse.io.readers.ReaderManager; + +public class Version { + + private long versionDate; + private String versionLabel; + private static Version currentVersion = ReaderManager.readVersion(); + + public Version(String label, long versionDate) { + this.versionLabel = label; + this.versionDate = versionDate; + } + + public Version checkNewVersion() { + + try { + var website = new URL("https://kotik-coder.github.io/Version.txt"); + var conn = website.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + long date = conn.getLastModified(); + + if (date == 0) + System.out.println("No remote version info found"); + + var label = IOUtils.toString(website, "UTF-8"); + + return Long.compare(date, versionDate) > 0 ? new Version(label, date) : null; + + } catch (IOException e) { + System.err.println( "Could not check for new version"); + e.printStackTrace(); + return null; + } + } + + public long getVersionDate() { + return versionDate; + } + + public String getVersionLabel() { + return versionLabel; + } + + public String toString() { + var fmt = DateFormat.getDateInstance(DateFormat.SHORT); + return getString("TaskControlFrame.SoftwareTitle") + " - " + versionLabel + " (" + fmt.format(new Date(versionDate)) + ")"; + } + + public static Version getCurrentVersion() { + return currentVersion; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index fdc5cd6c..95a6fa11 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -81,7 +81,7 @@ private void setFonts() { } private void setBackgroundAndGrid() { - //plot.setBackgroundPaint(UIManager.getColor("Panel.background")); + // plot.setBackgroundPaint(UIManager.getColor("Panel.background")); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(GRAY); @@ -128,7 +128,7 @@ private void setRenderers() { plot.setRenderer(1, rendererResiduals); plot.setRenderer(2, rendererClassic); plot.setRenderer(3, renderer); - + } private void adjustAxisLabel(double maximum) { @@ -143,7 +143,7 @@ private void adjustAxisLabel(double maximum) { public void plot(SearchTask task, boolean extendedCurve) { requireNonNull(task); - + var plot = chart.getXYPlot(); for (int i = 0; i < 6; i++) @@ -185,14 +185,14 @@ public void plot(SearchTask task, boolean extendedCurve) { var solution = problem.getHeatingCurve(); var scheme = calc.getScheme(); - - if (solution != null && scheme != null && solution.actualNumPoints() > 0) { + + if (solution != null && scheme != null && !solution.isIncomplete()) { var solutionDataset = new XYSeriesCollection(); var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; - solutionDataset.addSeries( - series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); + solutionDataset + .addSeries(series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); plot.setDataset(0, solutionDataset); /* @@ -202,7 +202,7 @@ public void plot(SearchTask task, boolean extendedCurve) { if (residualsShown) { var residuals = calc.getOptimiserStatistic().getResiduals(); if (residuals != null && residuals.size() > 0) { - var residualsDataset = new XYSeriesCollection(); + var residualsDataset = new XYSeriesCollection(); residualsDataset.addSeries(residuals(calc)); plot.setDataset(1, residualsDataset); } @@ -240,22 +240,23 @@ public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); return series(curve, title, startTime, realCount, extendedCurve); } - + public XYSeries series(ExperimentalData curve, String title, boolean extendedCurve) { return series(curve, title, 0, curve.actualNumPoints(), extendedCurve); } - - private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, boolean extendedCurve) { + + private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, + boolean extendedCurve) { var series = new XYSeries(title); - + int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); - + for (var i = 0; i < iStart && extendedCurve; i++) series.add(factor * curve.timeAt(i), curve.signalAt(i)); for (var i = iStart; i < realCount; i++) series.add(factor * curve.timeAt(i), curve.signalAt(i)); - + return series; } @@ -270,7 +271,7 @@ public XYSeries residuals(Calculation calc) { final var offset = baseline.valueAt(0) - span / 2.0; var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); - + for (var i = 0; i < size; i++) { series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index d8ccd4c5..de024d08 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -22,7 +22,11 @@ import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.allDescriptors; +import java.awt.Desktop; import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -42,7 +46,6 @@ import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; import pulse.ui.components.listeners.FrameVisibilityRequestListener; -import pulse.ui.frames.dialogs.AboutDialog; import pulse.ui.frames.dialogs.ExportDialog; import pulse.ui.frames.dialogs.FormattedInputDialog; import pulse.ui.frames.dialogs.ResultChangeDialog; @@ -133,7 +136,7 @@ private void initComponents() { loadIcon("inverse_problem.png", ICON_SIZE)); resultFormatItem = new JMenuItem("Change Result Format...", loadIcon("result_format.png", ICON_SIZE)); var infoMenu = new JMenu("Info"); - aboutItem = new JMenuItem("About..."); + aboutItem = new JMenuItem("PULsE Web-site"); var selectBuffer = new JMenuItem("Buffer size...", loadIcon("buffer.png", ICON_SIZE)); selectBuffer.addActionListener(e -> bufferDialog.setVisible(true)); @@ -318,10 +321,13 @@ private void assignMenuFunctions() { }); aboutItem.addActionListener(e -> { - var aboutDialog = new AboutDialog(); - aboutDialog.setLocationRelativeTo(getWindowAncestor(this)); - aboutDialog.setAlwaysOnTop(true); - aboutDialog.setVisible(true); + try { + Desktop.getDesktop().browse(new URL("https://kotik-coder.github.io/").toURI()); + } catch (IOException | URISyntaxException e1) { + System.err.println("Unable to open URL. Details: "); + e1.printStackTrace(); + } + }); } diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 1c3674b7..a0c4db2c 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -36,7 +36,7 @@ @SuppressWarnings("serial") public class TaskPopupMenu extends JPopupMenu { - + private JMenuItem itemViewStored; private final static int ICON_SIZE = 24; @@ -76,11 +76,9 @@ public TaskPopupMenu() { var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); instance.addSelectionListener(event -> { - var details = instance.getSelectedTask().checkProblems(false).getDetails(); - if ((details == null) || (details == NONE)) - itemShowStatus.setEnabled(false); - else - itemShowStatus.setEnabled(true); + instance.getSelectedTask().checkProblems(false); + var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); + itemShowStatus.setEnabled((details != null) & (details != NONE)); }); itemShowStatus.addActionListener((ActionEvent e) -> { @@ -99,26 +97,31 @@ public TaskPopupMenu() { showMessageDialog(getWindowAncestor((Component) e.getSource()), getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ - } else if (t.checkProblems() == DONE) { - var dialogButton = YES_NO_OPTION; - var dialogResult = showConfirmDialog(referenceWindow, - getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() - + getString("TaskTablePopupMenu.AskToDelete"), - getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); - if (dialogResult == 0) { - // instance.removeResult(t); - instance.getSelectedTask().setStatus(READY); + } else { + t.checkProblems(true); + var status = t.getCurrentCalculation().getStatus(); + + if (status == DONE) { + var dialogButton = YES_NO_OPTION; + var dialogResult = showConfirmDialog(referenceWindow, + getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() + + getString("TaskTablePopupMenu.AskToDelete"), + getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); + if (dialogResult == 0) { + // instance.removeResult(t); + instance.getSelectedTask().setStatus(READY); + instance.execute(instance.getSelectedTask()); + } + } else if (status != READY) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ + getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ + ERROR_MESSAGE); + } else instance.execute(instance.getSelectedTask()); - } - } else if (t.checkProblems() != READY) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ - getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ - ERROR_MESSAGE); - } else - instance.execute(instance.getSelectedTask()); - }); + } + }); var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); @@ -140,7 +143,7 @@ public TaskPopupMenu() { }); itemViewStored = new JMenuItem(getString("TaskTablePopupMenu.ViewStored"), ICON_STORED); - + itemViewStored.setEnabled(false); itemViewStored.addActionListener(arg0 -> instance.notifyListeners( diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 078fd607..f759e07a 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -27,7 +27,7 @@ public ExecutionButton() { super(); setIcon(state.getIcon()); setToolTipText(state.getMessage()); - + var instance = TaskManager.getManagerInstance(); this.addActionListener((ActionEvent e) -> { @@ -48,12 +48,15 @@ public ExecutionButton() { ERROR_MESSAGE); return; } - var problematicTask = instance.getTaskList().stream() - .filter((t) -> t.checkProblems() == INCOMPLETE).findFirst(); + var problematicTask = instance.getTaskList().stream().filter(t -> { + t.checkProblems(true); + return t.getCurrentCalculation().getStatus() == INCOMPLETE; + }).findFirst(); if (problematicTask.isPresent()) { var t = problematicTask.get(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), t + " is " + t.getCurrentCalculation().getStatus().getMessage(), - "Problems found", ERROR_MESSAGE); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", + ERROR_MESSAGE); } else { instance.executeAll(); } diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index e78cf273..db77e3ab 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -1,8 +1,8 @@ package pulse.ui.components.panels; import static java.awt.Color.GRAY; -import static java.awt.Color.black; import static java.awt.Color.gray; +import static java.awt.Color.white; import static java.awt.GridBagConstraints.BOTH; import static java.awt.Toolkit.getDefaultToolkit; import static java.lang.String.format; @@ -17,6 +17,7 @@ import static pulse.ui.frames.MainGraphFrame.getChart; import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.FocusEvent; @@ -26,9 +27,9 @@ import javax.swing.JButton; import javax.swing.JFormattedTextField; -import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.JToggleButton; +import javax.swing.JToolBar; import javax.swing.text.NumberFormatter; import pulse.input.Range; @@ -38,13 +39,14 @@ import pulse.ui.frames.HistogramFrame; @SuppressWarnings("serial") -public class ChartToolbar extends JPanel { +public class ChartToolbar extends JToolBar { private final static int ICON_SIZE = 16; private List listeners; public ChartToolbar() { super(); + setFloatable(false); listeners = new ArrayList<>(); initComponents(); } @@ -56,9 +58,9 @@ public void initComponents() { var upperLimitField = new JFormattedTextField(new NumberFormatter()); var limitRangeBtn = new JButton(); - var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE)); - var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE)); - var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE)); + var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE, Color.white)); + var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE, Color.white)); + var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE, Color.white)); pdfBtn.setToolTipText("Residuals Histogram"); var instance = TaskManager.getManagerInstance(); @@ -109,7 +111,7 @@ public void initComponents() { public void focusGained(FocusEvent e) { var src = (JTextField) e.getSource(); if (src.getText().length() > 0) - src.setForeground(black); + src.setForeground(white); } @Override diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index 443f5053..4646c453 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -1,28 +1,30 @@ package pulse.ui.components.panels; -import static javax.swing.SwingConstants.CENTER; import static pulse.tasks.logs.Log.isVerbose; import static pulse.tasks.logs.Log.setVerbose; import static pulse.ui.Messages.getString; import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridLayout; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; import javax.swing.JCheckBox; -import javax.swing.JPanel; +import javax.swing.JToolBar; import pulse.ui.components.listeners.LogExportListener; @SuppressWarnings("serial") -public class LogToolbar extends JPanel { +public class LogToolbar extends JToolBar { private final static int ICON_SIZE = 16; private List listeners; public LogToolbar() { + super(); + setFloatable(false); initComponents(); listeners = new ArrayList<>(); } @@ -30,7 +32,7 @@ public LogToolbar() { public void initComponents() { setLayout(new GridLayout()); - var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE)); + var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); saveLogBtn.setToolTipText("Save"); var verboseCheckBox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java new file mode 100644 index 00000000..f619a64c --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -0,0 +1,101 @@ +package pulse.ui.components.panels; + +import static java.awt.Toolkit.getDefaultToolkit; +import static java.lang.System.err; +import static javax.swing.JOptionPane.ERROR_MESSAGE; +import static javax.swing.JOptionPane.showMessageDialog; +import static javax.swing.SwingUtilities.getWindowAncestor; +import static pulse.input.InterpolationDataset.StandartType.DENSITY; +import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; +import static pulse.tasks.logs.Status.INCOMPLETE; +import static pulse.ui.Messages.getString; + +import java.awt.Component; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; + +import javax.swing.JButton; +import javax.swing.JToolBar; + +import pulse.problem.schemes.solvers.Solver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.TaskManager; +import pulse.ui.components.buttons.LoaderButton; +import pulse.ui.frames.MainGraphFrame; +import pulse.ui.frames.TaskControlFrame; + +@SuppressWarnings("serial") +public class ProblemToolbar extends JToolBar { + + private JButton btnSimulate; + private LoaderButton btnLoadCv; + private LoaderButton btnLoadDensity; + + public ProblemToolbar() { + super(); + setFloatable(false); + setLayout(new GridLayout()); + + btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ + add(btnSimulate); + + btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ + btnLoadCv.setDataType(HEAT_CAPACITY); + add(btnLoadCv); + + btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ + btnLoadDensity.setDataType(DENSITY); + add(btnLoadDensity); + + addListeners(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void addListeners() { + var instance = TaskManager.getManagerInstance(); + + // simulate btn listener + + btnSimulate.addActionListener((ActionEvent e) -> { + var t = instance.getSelectedTask(); + + if (t == null) + return; + + var calc = t.getCurrentCalculation(); + t.checkProblems(true); + var status = t.getCurrentCalculation().getStatus(); + + if (status == INCOMPLETE && !status.checkProblemStatementSet()) { + + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), + getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ + ERROR_MESSAGE); + + } else { + try { + ((Solver) calc.getScheme()).solve(calc.getProblem()); + } catch (SolverException se) { + err.println("Solver of " + t + " has encountered an error. Details: "); + se.printStackTrace(); + } + MainGraphFrame.getInstance().plot(); + TaskControlFrame.getInstance().getPulseFrame().plot(calc.getProblem().getPulse()); + } + }); + + } + + public void highlightButtons(boolean highlight) { + if(highlight) { + btnLoadDensity.highlightIfNeeded(); + btnLoadCv.highlightIfNeeded(); + } + else { + btnLoadDensity.highlight(false); + btnLoadCv.highlight(false); + } + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ResultToolbar.java b/src/main/java/pulse/ui/components/panels/ResultToolbar.java index b657a201..fc3c3590 100644 --- a/src/main/java/pulse/ui/components/panels/ResultToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ResultToolbar.java @@ -2,17 +2,18 @@ import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridLayout; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; -import javax.swing.JPanel; +import javax.swing.JToolBar; import pulse.ui.components.listeners.ResultRequestListener; @SuppressWarnings("serial") -public class ResultToolbar extends JPanel { +public class ResultToolbar extends JToolBar { private final static int ICON_SIZE = 16; @@ -25,6 +26,8 @@ public class ResultToolbar extends JPanel { private List listeners; public ResultToolbar() { + super(); + this.setFloatable(false); initComponents(); listeners = new ArrayList<>(); } @@ -34,7 +37,7 @@ public void initComponents() { mergeBtn = new JButton(loadIcon("merge.png", ICON_SIZE)); undoBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); previewBtn = new JButton(loadIcon("preview.png", ICON_SIZE)); - saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE)); + saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); setLayout(new GridLayout(5, 0)); deleteEntryBtn.setToolTipText("Delete Entry"); diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index d2df41f5..858f1038 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -1,5 +1,8 @@ package pulse.ui.components.panels; +import static java.awt.Color.black; +import static java.awt.Color.red; +import static pulse.util.ImageUtils.blend; import static pulse.util.ImageUtils.loadIcon; import java.awt.GridLayout; @@ -7,14 +10,14 @@ import java.util.List; import javax.swing.JButton; -import javax.swing.JPanel; +import javax.swing.JToolBar; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.ExecutionButton; import pulse.ui.components.listeners.TaskActionListener; @SuppressWarnings("serial") -public class TaskToolbar extends JPanel { +public class TaskToolbar extends JToolBar { private final static int ICON_SIZE = 16; @@ -27,6 +30,8 @@ public class TaskToolbar extends JPanel { private List listeners; public TaskToolbar() { + super(); + setFloatable(false); initComponents(); listeners = new ArrayList<>(); addButtonListeners(); @@ -35,7 +40,7 @@ public TaskToolbar() { private void initComponents() { removeBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); - clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE)); + clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE, blend(red, black, 0.5f))); resetBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); graphBtn = new JButton(loadIcon("graph.png", ICON_SIZE)); execBtn = new ExecutionButton(); diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 3e281cae..97bc86a5 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -3,40 +3,24 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.NORTH; import static java.awt.BorderLayout.SOUTH; -import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.System.err; -import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import static javax.swing.JOptionPane.WARNING_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.ListSelectionModel.SINGLE_SELECTION; -import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.problem.statements.ProblemComplexity.HIGH; import static pulse.tasks.TaskManager.getManagerInstance; -import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; -import static pulse.tasks.logs.Details.MISSING_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.ui.Messages.getString; -import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.instancesOf; import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; import java.awt.GridLayout; -import java.awt.event.ActionEvent; import java.util.List; import javax.swing.DefaultListModel; -import javax.swing.JButton; import javax.swing.JInternalFrame; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; -import javax.swing.JToolBar; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; import javax.swing.tree.DefaultMutableTreeNode; @@ -44,34 +28,29 @@ import javax.swing.tree.TreePath; import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.solvers.Solver; -import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; -import pulse.problem.statements.Pulse; import pulse.tasks.SearchTask; import pulse.tasks.listeners.TaskSelectionEvent; import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; -import pulse.ui.components.PulseChart; -import pulse.ui.components.buttons.LoaderButton; +import pulse.ui.components.panels.ProblemToolbar; import pulse.ui.components.panels.SettingsToolBar; import pulse.ui.frames.TaskControlFrame.Mode; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { - private InternalGraphFrame pulseFrame; - - private PropertyHolderTable problemTable, schemeTable; - private SchemeSelectionList schemeSelectionList; + private PropertyHolderTable problemTable; + private PropertyHolderTable schemeTable; + private JList schemeSelectionList; private ProblemTree problemTree; + private ProblemToolbar toolbar; private final static List knownProblems = instancesOf(Problem.class); /** * Create the frame. */ - @SuppressWarnings({ "unchecked", "rawtypes" }) public ProblemStatementFrame() { setResizable(true); setClosable(true); @@ -141,7 +120,34 @@ public ProblemStatementFrame() { * Scheme list and scroller */ - schemeSelectionList = new SchemeSelectionList(); + schemeSelectionList = new JList(); + schemeSelectionList.setSelectionMode(SINGLE_SELECTION); + schemeSelectionList.setModel(new DefaultListModel()); + + schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { + if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) + return; + + var selectedValue = schemeSelectionList.getSelectedValue(); + + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + return; + } + + var selectedTask = instance.getSelectedTask(); + var newScheme = selectedValue; + if (instance.isSingleStatement()) { + instance.getTaskList().stream().forEach(t -> changeScheme(t, newScheme)); + } else { + changeScheme(selectedTask, newScheme); + } + schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); + if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); + } + }); + schemeSelectionList.setToolTipText(getString("ProblemStatementFrame.PleaseSelect")); //$NON-NLS-1$ var schemeScroller = new JScrollPane(schemeSelectionList); @@ -163,59 +169,7 @@ public ProblemStatementFrame() { var schemeDetailsScroller = new JScrollPane(schemeTable); contentPane.add(schemeDetailsScroller); - /* - * Toolbar - */ - - var toolBar = new JToolBar(); - toolBar.setFloatable(false); - toolBar.setLayout(new GridLayout()); - - var btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ - - pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); - pulseFrame.setVisible(false); - - // simulate btn listener - - btnSimulate.addActionListener((ActionEvent arg0) -> { - var t = instance.getSelectedTask(); - if (t == null) - return; - var calc = t.getCurrentCalculation(); - if (t.checkProblems() == INCOMPLETE) { - var d = calc.getStatus().getDetails(); - if (d == MISSING_PROBLEM_STATEMENT || d == MISSING_DIFFERENCE_SCHEME - || d == INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), calc.getStatus().getMessage(), - getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ - ERROR_MESSAGE); - return; - } - } - try { - ((Solver) calc.getScheme()).solve(calc.getProblem()); - } catch (SolverException e) { - err.println("Solver of " + t + " has encountered an error. Details: "); - e.printStackTrace(); - } - MainGraphFrame.getInstance().plot(); - pulseFrame.plot(calc.getProblem().getPulse()); - problemTable.updateTable(); - schemeTable.updateTable(); - }); - - toolBar.add(btnSimulate); - - var btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ - btnLoadCv.setDataType(HEAT_CAPACITY); - toolBar.add(btnLoadCv); - - var btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ - btnLoadDensity.setDataType(DENSITY); - toolBar.add(btnLoadDensity); + toolbar = new ProblemToolbar(); problemTree.setSelectionModel(new DefaultTreeSelectionModel() { @@ -233,13 +187,9 @@ public void setSelectionPath(TreePath path) { if (enabledFlag) { super.setSelectionPath(path); - if(!problem.isReady()) { - btnLoadDensity.highlightIfNeeded(); - btnLoadCv.highlightIfNeeded(); - } + toolbar.highlightButtons(!problem.isReady()); } else { - showMessageDialog(null, - getString("problem.notsupportedmessage"), + showMessageDialog(null, getString("problem.notsupportedmessage"), getString("problem.notsupportedtitle"), WARNING_MESSAGE); path = null; } @@ -256,7 +206,7 @@ public void setSelectionPath(TreePath path) { getContentPane().add(new SettingsToolBar(problemTable, schemeTable), NORTH); getContentPane().add(contentPane, CENTER); - getContentPane().add(toolBar, SOUTH); + getContentPane().add(toolbar, SOUTH); /* * listeners @@ -268,11 +218,9 @@ public void setSelectionPath(TreePath path) { getManagerInstance().addHierarchyListener(event -> { if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getProblem()).filter(p -> p != null) - .forEach(pp -> pp.updateProperty(event, event.getProperty())); + .forEach(pp -> pp.updateProperty(event, event.getProperty())); }); - - } @@ -317,7 +265,9 @@ private void changeProblem(SearchTask task, Problem newProblem) { calc.setProblem(np, data); // copies information from old problem to new problem type - task.checkProblems(); + task.checkProblems(true); + toolbar.highlightButtons(!np.isReady()); + } private static void selectDefaultScheme(JList list, Problem p) { @@ -359,7 +309,7 @@ private void changeScheme(SearchTask task, DifferenceScheme newScheme) { } - task.checkProblems(); + task.checkProblems(true); } @@ -386,52 +336,4 @@ private void setSelectedElement(JList list, Object o) { } - /* - * ########################### Scheme selection list class - * ########################### - */ - - class SchemeSelectionList extends JList { - - public SchemeSelectionList() { - - super(); - setSelectionMode(SINGLE_SELECTION); - var m = new DefaultListModel(); - setModel(m); - // scheme list listener - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) - return; - - if (arg0.getValueIsAdjusting() || !(getSelectedValue() instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - return; - } - - var instance = getManagerInstance(); - var selectedTask = instance.getSelectedTask(); - var newScheme = getSelectedValue(); - if (newScheme == null) - return; - if (instance.isSingleStatement()) { - instance.getTaskList().stream().forEach(t -> changeScheme(t, newScheme)); - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); - if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); - } - }); - - } - - } - - public InternalGraphFrame getPulseFrame() { - return pulseFrame; - } - } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index bdf2a690..e58d61b4 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -153,7 +153,7 @@ public PathOptimiser getElementAt(int index) { setInstance(searchScheme); linearList.setEnabled(true); for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(); + t.checkProblems(true); } }); @@ -205,7 +205,7 @@ public LinearOptimiser getElementAt(int index) { pathTable.setPropertyHolder(pathSolver); pathTable.setEnabled(true); for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(); + t.checkProblems(true); } }); diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 65e30aa1..39f08889 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -25,7 +25,10 @@ import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; +import pulse.problem.statements.Pulse; import pulse.tasks.TaskManager; +import pulse.ui.Version; +import pulse.ui.components.PulseChart; import pulse.ui.components.PulseMainMenu; import pulse.ui.components.listeners.FrameVisibilityRequestListener; import pulse.ui.components.listeners.TaskActionListener; @@ -48,6 +51,7 @@ public class TaskControlFrame extends JFrame { private ResultFrame resultsFrame; private MainGraphFrame graphFrame; private LogFrame logFrame; + private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; @@ -60,7 +64,7 @@ public static TaskControlFrame getInstance() { */ private TaskControlFrame() { - setTitle(getString("TaskControlFrame.SoftwareTitle")); + setTitle(Version.getCurrentVersion().toString()); setPreferredSize(new Dimension(WIDTH, HEIGHT)); setExtendedState(getExtendedState() | MAXIMIZED_BOTH); initComponents(); @@ -196,13 +200,17 @@ private void initComponents() { searchOptionsFrame = new SearchOptionsFrame(); searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); + pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); + pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); + pulseFrame.setVisible(false); + /* * CONSTRAINT ADJUSTMENT */ resizeQuadrants(); desktopPane.add(taskManagerFrame); - desktopPane.add(problemStatementFrame.getPulseFrame()); + desktopPane.add(pulseFrame); desktopPane.add(graphFrame); desktopPane.add(previewFrame); desktopPane.add(logFrame); @@ -276,7 +284,7 @@ private void doResize() { resizeQuadrants(); break; case PROBLEM: - resizeTriplet(problemStatementFrame, problemStatementFrame.getPulseFrame(), graphFrame); + resizeTriplet(problemStatementFrame, pulseFrame, graphFrame); break; case SEARCH: resizeFull(searchOptionsFrame); @@ -392,7 +400,7 @@ private void setPreviewFrameVisible(boolean show) { private void setProblemStatementFrameVisible(boolean show) { problemStatementFrame.setVisible(show); - problemStatementFrame.getPulseFrame().setVisible(show); + pulseFrame.setVisible(show); graphFrame.setVisible(true); previewFrame.setVisible(false); @@ -444,4 +452,8 @@ public Mode getMode() { return mode; } + public InternalGraphFrame getPulseFrame() { + return pulseFrame; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java b/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java deleted file mode 100644 index 5ebbefe8..00000000 --- a/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java +++ /dev/null @@ -1,64 +0,0 @@ -package pulse.ui.frames.dialogs; - -import static java.awt.BorderLayout.CENTER; -import static java.lang.System.err; -import static javax.swing.UIManager.getColor; -import static pulse.ui.Messages.getString; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import javax.swing.JDialog; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.text.BadLocationException; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; - -@SuppressWarnings("serial") -public class AboutDialog extends JDialog { - - private final static int WIDTH = 570; - private final static int HEIGHT = 370; - - public AboutDialog() { - - setTitle(getString("TaskControlFrame.AboutDialog")); - setAlwaysOnTop(true); - setSize(WIDTH, HEIGHT); - - var reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/About.html"))); - var sb = new StringBuilder(); - String str; - - try { - while ((str = reader.readLine()) != null) - sb.append(str); - } catch (IOException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - var textPane = new JTextPane(); - textPane.setBackground(getColor("Panel.background")); - textPane.setEditable(false); - textPane.setContentType("text/html"); - - final var doc = (HTMLDocument) textPane.getDocument(); - final var kit = (HTMLEditorKit) textPane.getEditorKit(); - try { - kit.insertHTML(doc, doc.getLength(), sb.toString(), 0, 0, null); - } catch (BadLocationException e) { - err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ - e.printStackTrace(); - } catch (IOException e) { - err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ - e.printStackTrace(); - } - - getContentPane().add(new JScrollPane(textPane), CENTER); - - } - -} diff --git a/src/main/resources/About.html b/src/main/resources/About.html deleted file mode 100644 index bbf1eee2..00000000 --- a/src/main/resources/About.html +++ /dev/null @@ -1,5 +0,0 @@ -
Processing Unit for Laser flash Experiments
-
(PULsE), v. 1.88b3
-

Date of release: 13/10/2020
Lead developer: Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii

-

PULsE is an advanced software toolkit for processing data from laser flash experiments, allowing effective treatment of difficult cases where conditions may not be ideal for simpler analysis. PULsE analyses the heating curves from laser flash experiments and outputs the thermal properties of the sample.

-

This software is specifically tailored for use in the Materials Research Facility at UKAEA, and reads ASCII files generated by the Linseis LFA; it was initially designed to read custom file formats from the 'Kvant' laser flash analyser. For full specification regarding the file formats, please refer to manual.

\ No newline at end of file diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt new file mode 100644 index 00000000..92127072 --- /dev/null +++ b/src/main/resources/Version.txt @@ -0,0 +1 @@ +1.88R \ No newline at end of file diff --git a/src/main/resources/images/clear.png b/src/main/resources/images/clear.png index 57773041f71589d51fd5df5c00909d807ea63d26..eef771684ee4f487a2f3c211f3d79cbdca1c31fa 100644 GIT binary patch literal 37672 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 TZr(3O{mf5}iyXwWh0*^9sy3?? literal 29219 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 diff --git a/src/main/resources/images/graph.png b/src/main/resources/images/graph.png index 8ec2543ed16004ef5e6a478c68d7370181df593a..05c481b9ec583136bc6fdf81cff2a9f4e2c50537 100644 GIT binary patch literal 35409 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 RV+i=WtE8b=B5xA-KLFJbW#9k+ literal 26005 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 Q$E}Evj4bx&?mZFuUl=Uz(f|Me diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 34230626..89d58d23 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,4 +1,4 @@ -TaskControlFrame.SoftwareTitle=PULsE / v. 1.88 +TaskControlFrame.SoftwareTitle=PULsE TaskControlFrame.AboutDialog=About PULsE NumericProperty.XMLFile=/NumericProperty.xml NumericProperty.PlusMinus=\ ± From d94a0595ea1581645e8735eaeba74d93019cfa62 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 14 Oct 2020 15:45:27 +0100 Subject: [PATCH 019/116] debug = false --- src/main/java/pulse/ui/Launcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 38d995a8..1c5f1ad3 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -34,7 +34,7 @@ public class Launcher { private PrintStream errStream; private File errorLog; - private final static boolean DEBUG = true; + private final static boolean DEBUG = false; private Launcher() { arrangeErrorOutput(); From 27282a3026a09ae41b092d6835d370c8384e6512 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 16 Oct 2020 18:44:48 +0100 Subject: [PATCH 020/116] Corrected bug with file extension due to WebLaF file chooser --- src/main/java/pulse/io/export/Exporter.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index ff5bdccd..f90af465 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -101,19 +101,26 @@ public default void askToExport(T target, JFrame parentWindow, String fileTypeLa var workingDirectory = new File(System.getProperty("user.home")); fileChooser.setCurrentDirectory(workingDirectory); fileChooser.setMultiSelectionEnabled(true); - fileChooser.setSelectedFile(new File(target.describe())); - + + FileNameExtensionFilter choosable = null; + for (var s : getSupportedExtensions()) { - fileChooser.addChoosableFileFilter( - new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase())); + choosable = new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase()); + fileChooser.addChoosableFileFilter( choosable ); } fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.setFileFilter( choosable ); + fileChooser.setSelectedFile(new File(target.describe() + "." + choosable.getExtensions()[0])); int returnVal = fileChooser.showSaveDialog(parentWindow); if (returnVal == JFileChooser.APPROVE_OPTION) { var file = fileChooser.getSelectedFile(); var path = file.getPath(); + + if(! (fileChooser.getFileFilter() instanceof FileNameExtensionFilter) ) + return; + var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); var ext = currentFilter.getExtensions()[0]; From 93fdf080dd1a5df346b90c93b0e19654f25e566c Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Sun, 18 Oct 2020 14:32:04 +0100 Subject: [PATCH 021/116] Code simpification using Stream api --- src/main/java/pulse/tasks/processing/CorrelationBuffer.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 88d9049b..ae8a0cae 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -81,11 +81,7 @@ public boolean test(CorrelationTest t) { if (map == null) return false; - for (Double d : map.values()) { - if (t.compareToThreshold(d)) - return true; - } - return false; + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } public static void excludePair(ImmutablePair pair) { From 3b9537224f0cf716ed2e7d7320a50c018c02d517 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 23 Oct 2020 15:40:56 +0100 Subject: [PATCH 022/116] Initial implementation of different optimisers: - Levenber-Marquardt for OLS - SR1 Quasi-Newton - Ridge regression - Tidied up BFGS / Quasi-Newton Still in test mode though --- src/main/java/pulse/math/IndexedVector.java | 12 + src/main/java/pulse/math/linear/Matrices.java | 35 ++- .../pulse/math/linear/RectangularMatrix.java | 253 +++++++++++++++++ .../java/pulse/math/linear/SquareMatrix.java | 254 ++---------------- .../schemes/rte/dom/ButcherTableau.java | 2 +- .../pulse/problem/schemes/rte/dom/TRBDF2.java | 2 +- .../rte/exact/ChandrasekharsQuadrature.java | 4 +- ...ssianOptimiser.java => BFGSOptimiser.java} | 52 ++-- .../pulse/search/direction/ComplexPath.java | 18 +- .../direction/CompositePathOptimiser.java | 95 +++++++ .../search/direction/DirectionSolver.java | 21 ++ .../direction/HessianDirectionSolver.java | 46 ++++ .../pulse/search/direction/LMOptimiser.java | 253 +++++++++++++++++ .../java/pulse/search/direction/Path.java | 14 +- .../pulse/search/direction/PathOptimiser.java | 123 +++------ .../pulse/search/direction/SR1Optimiser.java | 106 ++++++++ .../direction/SteepestDescentOptimiser.java | 28 +- .../search/linear/GoldenSectionOptimiser.java | 4 +- .../pulse/search/linear/WolfeOptimiser.java | 10 +- .../statistics/RegularisedLeastSquares.java | 59 ++++ .../pulse/search/statistics/SumOfSquares.java | 2 +- src/main/java/pulse/tasks/Calculation.java | 13 +- src/main/java/pulse/tasks/SearchTask.java | 108 ++++---- src/main/java/pulse/tasks/TaskManager.java | 2 +- src/main/java/pulse/tasks/logs/Details.java | 18 +- src/main/java/pulse/ui/Launcher.java | 2 +- .../pulse/ui/components/PulseMainMenu.java | 27 +- .../pulse/ui/frames/SearchOptionsFrame.java | 93 +------ src/main/resources/NumericProperty.xml | 4 +- src/main/resources/messages.properties | 4 +- 30 files changed, 1101 insertions(+), 563 deletions(-) create mode 100644 src/main/java/pulse/math/linear/RectangularMatrix.java rename src/main/java/pulse/search/direction/{ApproximatedHessianOptimiser.java => BFGSOptimiser.java} (69%) create mode 100644 src/main/java/pulse/search/direction/CompositePathOptimiser.java create mode 100644 src/main/java/pulse/search/direction/DirectionSolver.java create mode 100644 src/main/java/pulse/search/direction/HessianDirectionSolver.java create mode 100644 src/main/java/pulse/search/direction/LMOptimiser.java create mode 100644 src/main/java/pulse/search/direction/SR1Optimiser.java create mode 100644 src/main/java/pulse/search/statistics/RegularisedLeastSquares.java diff --git a/src/main/java/pulse/math/IndexedVector.java b/src/main/java/pulse/math/IndexedVector.java index d77675e7..d96e0d6d 100644 --- a/src/main/java/pulse/math/IndexedVector.java +++ b/src/main/java/pulse/math/IndexedVector.java @@ -104,5 +104,17 @@ public List getIndices() { private void assign(List indices) { this.indices.addAll(indices); } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Indices: "); + for(var key : indices) { + sb.append(key + " ; "); + } + sb.append(System.lineSeparator()); + sb.append(" Values: " + super.toString()); + return sb.toString(); + } } \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/Matrices.java b/src/main/java/pulse/math/linear/Matrices.java index 13be56ae..c04b8d6f 100644 --- a/src/main/java/pulse/math/linear/Matrices.java +++ b/src/main/java/pulse/math/linear/Matrices.java @@ -1,8 +1,9 @@ package pulse.math.linear; /** - * A static factory class used to create matrices. The factory methods - * are invoked by classes outside this package instead of the constructors, which are package-private. + * A static factory class used to create matrices. The factory methods are + * invoked by classes outside this package instead of the constructors, which + * are package-private. * */ @@ -13,14 +14,19 @@ private Matrices() { } /** - * Creates a square matrix out of {@code data}. Depending on the data dimensions, - * this will either create a general-form {@code SquareMatrix} or one of the subclasses: - * {@code Matrix2}, {@code Matrix3} or {@code Matrix4}. + * Creates a square matrix out of {@code data}. Depending on the data + * dimensions, this will either create a general-form {@code SquareMatrix} or + * one of the subclasses: {@code Matrix2}, {@code Matrix3} or {@code Matrix4}. + * * @param data the input data * @return a {@code} SquareMatrix instance or one of its subclasses */ - - public static SquareMatrix createMatrix(double[][] data) { + + public static RectangularMatrix createMatrix(double[][] data) { + return data.length == data[0].length ? createSquareMatrix(data) : new RectangularMatrix(data); + } + + public static SquareMatrix createSquareMatrix(double[][] data) { int m = data.length; SquareMatrix result; @@ -42,20 +48,21 @@ public static SquareMatrix createMatrix(double[][] data) { return result; } - + /** - * Creates an identity matrix with its dimension equal to the argument + * Creates an identity matrix with its dimension equal to the argument + * * @param dimension the dimension * @return an identity matrix of the given dimension */ - + public static SquareMatrix createIdentityMatrix(int dimension) { var data = new double[dimension][dimension]; - - for(int i = 0; i < dimension; i++) + + for (int i = 0; i < dimension; i++) data[i][i] = 1.0; - - return createMatrix(data); + + return createSquareMatrix(data); } } \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/RectangularMatrix.java b/src/main/java/pulse/math/linear/RectangularMatrix.java new file mode 100644 index 00000000..81506d9e --- /dev/null +++ b/src/main/java/pulse/math/linear/RectangularMatrix.java @@ -0,0 +1,253 @@ +package pulse.math.linear; + +import static pulse.math.MathUtils.approximatelyEquals; +import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; +import static pulse.math.linear.ArithmeticOperations.SUM; +import static pulse.math.linear.Matrices.createMatrix; + +import pulse.ui.Messages; + +public class RectangularMatrix { + + protected final double[][] x; + + protected RectangularMatrix(double[][] args) { + int m = args.length; + int n = args[0].length; + + x = new double[m][n]; + + for (int i = 0; i < m; i++) + System.arraycopy(args[i], 0, x[i], 0, n); + + } + + protected RectangularMatrix(double[] data, int n) { + final int m = data.length / n; + x = new double[m][n]; + + for (int i = 0; i < m; i++) + System.arraycopy(data, i * n, x[i], 0, n); + + } + + /** + * Performs an element-wise summation if {@code this} and {@code m} have + * matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of summation + */ + + public RectangularMatrix sum(RectangularMatrix m) { + return performOperation(this, m, SUM); + } + + /** + * Performs an element-wise subtraction of {@code m} from {@code this} if these + * matrices have matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of subtraction + */ + + public RectangularMatrix subtract(RectangularMatrix m) { + return performOperation(this, m, DIFFERENCE); + } + + /** + *

+ * Performs {@code Matrix} multiplication. Checks whether the dimensions of each + * matrix are appropriate (number of columns in {@code this} matrix should be + * equal to the number of rows in {@code m}. + *

+ * + * @param m another {@code Matrix} suitable for multiplication + * @return a {@code Matrix}, which is the result of multiplying {@code this} by + * {@code m} + */ + + public RectangularMatrix multiply(RectangularMatrix m) { + if (this.x[0].length != m.x.length) + throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); + + final int mm = this.x.length; + final int nn = m.x[0].length; + + var y = new double[mm][nn]; + + for (int i = 0; i < mm; i++) { + for (int j = 0; j < nn; j++) { + for (int k = 0; k < this.x[0].length; k++) { + y[i][j] += this.x[i][k] * m.x[k][j]; + } + } + } + + return createMatrix(y); + + } + + /** + * Scales this {@code Matrix} by {@code f}, which results in element-wise + * multiplication by {@code f}. + * + * @param f a numeric value + * @return the scaled {@code Matrix} + */ + + public RectangularMatrix multiply(double f) { + double[][] y = new double[x.length][x[0].length]; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + y[i][j] = this.x[i][j] * f; + } + } + + return createMatrix(y); + + } + + /** + * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. + * + * @return a transposed {@code Matrix} + */ + + public RectangularMatrix transpose() { + int m = x.length; + int n = x[0].length; + double[][] y = new double[n][m]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + y[j][i] = x[i][j]; + } + } + + return createMatrix(y); + + } + + public double get(int m, int k) { + return x[m][k]; + } + + public double[][] getData() { + return x; + } + + private static RectangularMatrix performOperation(RectangularMatrix m1, RectangularMatrix m2, + ArithmeticOperation op) { + if (!m1.dimensionsMatch(m2)) + throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); + + double[][] y = new double[m1.x.length][m1.x[0].length]; + + for (int i = 0; i < y.length; i++) { + for (int j = 0; j < y[0].length; j++) { + y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); + } + } + + return createMatrix(y); + } + + /** + *

+ * Multiplies this {@code Matrix} by the vector {@code v}, which is represented + * by a n × 1 {@code Matrix}, where {@code n} is the + * dimension of {@code v}. Note {@code n} should be equal to the number of rows + * in this {@code Matrix}. + *

+ * + * @param v a {@code Vector}. + * @return the result of multiplication, which is a {@code Vector}. + */ + + public Vector multiply(Vector v) { + double[] r = new double[x.length]; + + if (x[0].length != v.dimension()) + throw new IllegalArgumentException( + "Cannot multiply a " + x.length + "x" + x[0].length + " matrix by a " + v.dimension() + " vector"); + + for (int i = 0; i < x.length; i++) { + for (int k = 0; k < x[0].length; k++) { + r[i] += x[i][k] * v.get(k); + } + } + + return new Vector(r); + } + + /** + * Prints out matrix dimensions and all the elements contained in it. + */ + + @Override + public String toString() { + int m = x.length; + int n = x[0].length; + final String f = Messages.getString("Math.DecimalFormat"); + + StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); + for (int i = 0; i < m; i++) { + sb.append(System.lineSeparator()); + for (int j = 0; j < n; j++) { + sb.append(" "); + sb.append(String.format(f, x[i][j])); + } + } + + return sb.toString(); + } + + /** + * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. if + * the number of rows is the same and the number of columns is the same + * + * @param m another {@code Matrix} + * @return {@code true} if the dimensions match, {@code false} otherwise. + */ + + public boolean dimensionsMatch(RectangularMatrix m) { + return (x.length == m.x.length) && (x[0].length == m.x[0].length); + } + + /** + * Checks whether {@code o} is a {@code SquareMatrix} with matching dimensions + * and all elements of which are (approximately) equal to the respective + * elements of {@code this} matrix}. + */ + + @Override + public boolean equals(Object o) { + if (!(o instanceof SquareMatrix)) + return false; + + if (o == this) + return true; + + var m = (SquareMatrix) o; + + if (!this.dimensionsMatch(m)) + return false; + + boolean result = true; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { + result = false; + break; + } + } + } + + return result; + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/SquareMatrix.java b/src/main/java/pulse/math/linear/SquareMatrix.java index 3551598c..52988845 100644 --- a/src/main/java/pulse/math/linear/SquareMatrix.java +++ b/src/main/java/pulse/math/linear/SquareMatrix.java @@ -1,15 +1,11 @@ package pulse.math.linear; import static org.ejml.dense.row.CommonOps_DDRM.invert; -import static pulse.math.MathUtils.approximatelyEquals; -import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; -import static pulse.math.linear.ArithmeticOperations.SUM; -import static pulse.math.linear.Matrices.createMatrix; +import static pulse.math.linear.Matrices.createSquareMatrix; import org.ejml.data.DMatrixRMaj; import org.ejml.dense.row.CommonOps_DDRM; - -import pulse.ui.Messages; +import org.ejml.dense.row.MatrixFeatures_DDRM; /** * The matrix class. @@ -28,9 +24,7 @@ * @see pulse.math.linear.Matrices */ -public class SquareMatrix { - - private final double[][] x; +public class SquareMatrix extends RectangularMatrix { /** * Constructs a {@code Matrix} with the elements copied from {@code args}. The @@ -40,145 +34,11 @@ public class SquareMatrix { */ protected SquareMatrix(double[][] args) { - int m = args.length; - int n = args[0].length; - - x = new double[m][n]; - - for (int i = 0; i < m; i++) - System.arraycopy(args[i], 0, x[i], 0, n); - + super(args); } private SquareMatrix(double[] data, int n) { - this.x = new double[n][n]; - - for (int i = 0; i < n; i++) - System.arraycopy(data, i * n, x[i], 0, n); - - } - - /** - * Performs an element-wise summation if {@code this} and {@code m} have - * matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of summation - */ - - public SquareMatrix sum(SquareMatrix m) { - return performOperation(this, m, SUM); - } - - /** - * Performs an element-wise subtraction of {@code m} from {@code this} if these - * matrices have matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of subtraction - */ - - public SquareMatrix subtract(SquareMatrix m) { - return performOperation(this, m, DIFFERENCE); - } - - /** - *

- * Performs {@code Matrix} multiplication. Checks whether the dimensions of each - * matrix are appropriate (number of columns in {@code this} matrix should be - * equal to the number of rows in {@code m}. - *

- * - * @param m another {@code Matrix} suitable for multiplication - * @return a {@code Matrix}, which is the result of multiplying {@code this} by - * {@code m} - */ - - public SquareMatrix multiply(SquareMatrix m) { - if (this.x[0].length != m.x.length) - throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); - - final int mm = this.x.length; - final int nn = m.x[0].length; - - var y = new double[mm][nn]; - - for (int i = 0; i < mm; i++) { - for (int j = 0; j < nn; j++) { - for (int k = 0; k < this.x[0].length; k++) { - y[i][j] += this.x[i][k] * m.x[k][j]; - } - } - } - - return createMatrix(y); - - } - - /** - * Scales this {@code Matrix} by {@code f}, which results in element-wise - * multiplication by {@code f}. - * - * @param f a numeric value - * @return the scaled {@code Matrix} - */ - - public SquareMatrix multiply(double f) { - double[][] y = new double[x.length][x[0].length]; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - y[i][j] = this.x[i][j] * f; - } - } - - return createMatrix(y); - - } - - /** - *

- * Multiplies this {@code Matrix} by the vector {@code v}, which is represented - * by a n × 1 {@code Matrix}, where {@code n} is the - * dimension of {@code v}. Note {@code n} should be equal to the number of rows - * in this {@code Matrix}. - *

- * - * @param v a {@code Vector}. - * @return the result of multiplication, which is a {@code Vector}. - */ - - public Vector multiply(Vector v) { - double[] r = new double[v.dimension()]; - - for (int i = 0; i < r.length; i++) { - for (int k = 0; k < r.length; k++) { - r[i] += x[i][k] * v.get(k); - } - } - - return new Vector(r); - } - - /** - * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. - * - * @return a transposed {@code Matrix} - */ - - public SquareMatrix transpose() { - int m = x.length; - int n = x[0].length; - double[][] y = new double[n][m]; - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - y[j][i] = x[i][j]; - } - } - - return createMatrix(y); - + super(data, n); } /** @@ -205,7 +65,16 @@ public SquareMatrix inverse() { invert(mx); return new SquareMatrix(mx.getData(), x.length); } - + + /** + * Checks if a matrix is positive definite. Uses EJML implementation. + * @return {@code true} is positive-definite + */ + + public boolean isPositiveDefinite() { + return MatrixFeatures_DDRM.isPositiveDefinite(new DMatrixRMaj(x)); + } + /** * Calculates the outer product of two vectors. * @@ -223,98 +92,11 @@ public static SquareMatrix outerProduct(Vector a, Vector b) { } } - return createMatrix(x); + return createSquareMatrix(x); } - - /** - * Checks whether {@code o} is a {@code SquareMatrix} with matching dimensions - * and all elements of which are (approximately) equal to the respective elements - * of {@code this} matrix}. - */ - @Override - public boolean equals(Object o) { - if (!(o instanceof SquareMatrix)) - return false; - - if (o == this) - return true; - - var m = (SquareMatrix) o; - - if(! this.hasSameDimensions(m) ) - return false; - - boolean result = true; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x.length; j++) { - if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { - result = false; - break; - } - } - } - - return result; - - } - - /** - * Prints out matrix dimensions and all the elements contained in it. - */ - - @Override - public String toString() { - int m = x.length; - int n = x[0].length; - final String f = Messages.getString("Math.DecimalFormat"); - - StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); - for (int i = 0; i < m; i++) { - sb.append(System.lineSeparator()); - for (int j = 0; j < n; j++) { - sb.append(" "); - sb.append(String.format(f, x[i][j])); - } - } - - return sb.toString(); - } - - /** - * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. if - * the number of rows is the same and the number of columns is the same - * - * @param m another {@code Matrix} - * @return {@code true} if the dimensions match, {@code false} otherwise. - */ - - public boolean hasSameDimensions(SquareMatrix m) { - return (x.length == m.x.length) && (x[0].length == m.x[0].length); - } - - public double get(int m, int k) { - return x[m][k]; - } - - public double[][] getData() { - return x; - } - - private static SquareMatrix performOperation(SquareMatrix m1, SquareMatrix m2, ArithmeticOperation op) { - if (!m1.hasSameDimensions(m2)) - throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); - - double[][] y = new double[m1.x.length][m1.x[0].length]; - - for (int i = 0; i < y.length; i++) { - for (int j = 0; j < y[0].length; j++) { - y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); - } - } - - return createMatrix(y); + public static SquareMatrix asSquareMatrix(RectangularMatrix m) { + return m.x.length == m.x[0].length ? new SquareMatrix(m.getData()) : null; } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java index a32c75c8..4ab5afe9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java @@ -34,7 +34,7 @@ public ButcherTableau(String name, double[][] coefs, double[] c, double[] b, dou this.name = name; this.fsal = fsal; - this.coefs = Matrices.createMatrix(coefs); + this.coefs = Matrices.createSquareMatrix(coefs); this.c = new Vector(c); this.b = new Vector(b); this.bHat = new Vector(bHat); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java index b84c5bfd..a97aebca 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java @@ -207,7 +207,7 @@ public Vector[] step(final int j, final double sign) { } - invA = (Matrices.createMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages + invA = (Matrices.createSquareMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages i2 = invA.multiply(new Vector(bVector)); // intensity vector at 2nd stage /* diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java index 2fb41182..a243cf64 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java @@ -120,7 +120,7 @@ private SquareMatrix xMatrix(final double[] roots) { } } - return Matrices.createMatrix(x); + return Matrices.createSquareMatrix(x); } /** @@ -221,7 +221,7 @@ private SquareMatrix momentMatrix() { moments[2 * m - 1] = moment(2 * m - 1); - return Matrices.createMatrix(data); + return Matrices.createSquareMatrix(data); } diff --git a/src/main/java/pulse/search/direction/ApproximatedHessianOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java similarity index 69% rename from src/main/java/pulse/search/direction/ApproximatedHessianOptimiser.java rename to src/main/java/pulse/search/direction/BFGSOptimiser.java index a0345a37..7bd8e2d4 100644 --- a/src/main/java/pulse/search/direction/ApproximatedHessianOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import static pulse.math.linear.SquareMatrix.asSquareMatrix; import static pulse.math.linear.SquareMatrix.outerProduct; import pulse.math.linear.SquareMatrix; @@ -28,26 +29,15 @@ * @see pulse.search.linear.WolfeOptimiser */ -public class ApproximatedHessianOptimiser extends PathOptimiser { +public class BFGSOptimiser extends CompositePathOptimiser { - private static ApproximatedHessianOptimiser instance = new ApproximatedHessianOptimiser(); + private static BFGSOptimiser instance = new BFGSOptimiser(); - private ApproximatedHessianOptimiser() { + private BFGSOptimiser() { super(); - } - - /** - * Uses an approximation of the Hessian matrix, containing the information on - * second derivatives, calculated with the BFGS formula in combination with the - * local value of the gradient to evaluate the direction of the minimum on - * {@code p}. Invokes {@code p.setDirection()}. - */ - - @Override - public Vector direction(Path p) { - Vector dir = (((ComplexPath) p).getHessian().inverse()).multiply(p.getGradient()).inverted(); - p.setDirection(dir); - return dir; + this.setSolver(new HessianDirectionSolver() { + //empty statement + }); } /** @@ -64,19 +54,20 @@ public Vector direction(Path p) { */ @Override - public void endOfStep(SearchTask task) throws SolverException { + public void prepare(SearchTask task) throws SolverException { var p = (ComplexPath) task.getPath(); - Vector dir = p.getDirection(); + Vector dir = p.getDirection(); //p[k] - final double minimumPoint = p.getMinimumPoint(); - final SquareMatrix prevHessian = p.getHessian(); - final Vector g0 = p.getGradient(); // g0 - Vector g1 = gradient(task); // g1 - - p.setHessian(hessian(g0, g1, dir, prevHessian, minimumPoint)); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 + final double minimumPoint = p.getMinimumPoint(); // alpha[k] + final SquareMatrix prevHessian = p.getHessian(); // B[k] + + final Vector g0 = p.getGradient(); // g[k] + final Vector g1 = gradient(task); // g[k+1] + var hessian = hessian(g0, g1, dir, prevHessian, minimumPoint); //B[k+1] + + p.setHessian(hessian); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 p.setGradient(g1); // set g1 as the new gradient for next step - } /** @@ -92,8 +83,11 @@ public void endOfStep(SearchTask task) throws SolverException { private SquareMatrix hessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevHessian, double alpha) { Vector y = g2.subtract(g1); // g[k+1] - g[k] - return prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) - .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS for Ge[k+1] + + var m = prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) + .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS formula + + return asSquareMatrix(m); } @Override @@ -108,7 +102,7 @@ public String toString() { * @return the single (static) instance of this class */ - public static ApproximatedHessianOptimiser getInstance() { + public static BFGSOptimiser getInstance() { return instance; } diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index 59e2042b..4b407492 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -1,9 +1,8 @@ package pulse.search.direction; import static pulse.math.linear.Matrices.createIdentityMatrix; -import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.gradient; +import pulse.math.linear.Matrices; import pulse.math.linear.SquareMatrix; import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.SearchTask; @@ -20,6 +19,7 @@ public class ComplexPath extends Path { private SquareMatrix hessian; + private SquareMatrix inverseHessian; protected ComplexPath(SearchTask task) { super(task); @@ -33,10 +33,10 @@ protected ComplexPath(SearchTask task) { */ @Override - public void reset(SearchTask task) { - setGradient(gradient(task)); + public void configure(SearchTask task) { + super.configure(task); hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); - setDirection(getInstance().direction(this)); + inverseHessian = Matrices.createIdentityMatrix(hessian.getData().length); } public SquareMatrix getHessian() { @@ -47,4 +47,12 @@ public void setHessian(SquareMatrix hes) { this.hessian = hes; } + public SquareMatrix getInverseHessian() { + return inverseHessian; + } + + public void setInverseHessian(SquareMatrix inverseHessian) { + this.inverseHessian = inverseHessian; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java new file mode 100644 index 00000000..0d0dde33 --- /dev/null +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -0,0 +1,95 @@ +package pulse.search.direction; + +import static pulse.properties.NumericProperties.compare; + +import java.util.List; + +import pulse.math.IndexedVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Property; +import pulse.search.linear.LinearOptimiser; +import pulse.search.linear.WolfeOptimiser; +import pulse.tasks.SearchTask; +import pulse.tasks.logs.Status; +import pulse.util.InstanceDescriptor; + +public abstract class CompositePathOptimiser extends PathOptimiser { + + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Linear Optimiser Selector", LinearOptimiser.class); + + private LinearOptimiser linearSolver; + + public CompositePathOptimiser() { + instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); + linearSolver = instanceDescriptor.newInstance(LinearOptimiser.class); + linearSolver.setParent(this); + instanceDescriptor.addListener(() -> initLinearOptimiser()); + } + + private void initLinearOptimiser() { + setLinearSolver(instanceDescriptor.newInstance(LinearOptimiser.class)); + } + + public boolean iteration(SearchTask task) throws SolverException { + var p = task.getPath(); // the previous path of the task + + /* + * Checks whether an iteration limit has been already reached + */ + + if (compare(p.getIteration(), getMaxIterations()) > 0) { + + task.setStatus(Status.TIMEOUT); + + } else { + + var parameters = task.searchVector()[0]; // current parameters + var dir = getSolver().direction(p); // find p[k] + + double step = linearSolver.linearStep(task); // find magnitude of step + p.setLinearStep(step); + + var candidateParams = parameters.sum(dir.multiply(step)); // new set of parameters determined through search + task.assign(new IndexedVector(candidateParams, parameters.getIndices())); // assign to this task + + prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] + p.incrementStep(); // increment the counter of successful steps + + task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + + } + + return true; + + } + + public LinearOptimiser getLinearSolver() { + return linearSolver; + } + + /** + * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this + * object as its parent. + * + * @param linearSearch a {@code LinearSolver} + */ + + public void setLinearSolver(LinearOptimiser linearSearch) { + this.linearSolver = linearSearch; + linearSolver.setParent(this); + super.parameterListChanged(); + } + + public InstanceDescriptor getLinearOptimiserDescriptor() { + return instanceDescriptor; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java new file mode 100644 index 00000000..d78fbf0a --- /dev/null +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -0,0 +1,21 @@ +package pulse.search.direction; + +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; + +public interface DirectionSolver { + + /** + * Finds the direction of the minimum using the previously calculated values + * stored in {@code p}. + * + * @param p a {@code Path} object + * @return a {@code Vector} pointing to the minimum direction for this + * {@code Path} + * @throws SolverException + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + + public Vector direction(Path p) throws SolverException; + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java new file mode 100644 index 00000000..92dff17b --- /dev/null +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -0,0 +1,46 @@ +package pulse.search.direction; + +import org.ejml.data.DMatrixRMaj; +import org.ejml.dense.row.CommonOps_DDRM; + +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; + +public interface HessianDirectionSolver extends DirectionSolver { + + /** + * Uses an approximation of the Hessian matrix, containing the information on + * second derivatives, calculated with the BFGS formula in combination with the + * local value of the gradient to evaluate the direction of the minimum on + * {@code p}. Invokes {@code p.setDirection()}. + * @throws SolverException + */ + + public default Vector direction(Path p) throws SolverException { + var cp = (ComplexPath) p; + final int dimg = p.getGradient().dimension(); + + Vector invGrad = p.getGradient().inverted(); + Vector result; + + // use linear solver for big matrices + if (dimg > 4) { + + var hess = new DMatrixRMaj(cp.getHessian().getData()); + var antigrad = new DMatrixRMaj(invGrad.getData()); + var dirv = new DMatrixRMaj(dimg, 1); + + if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { + throw new SolverException("Singular matrix!"); + } + + result = new Vector(dirv.getData()); + + } else // use fast inverse + result = cp.getHessian().inverse().multiply(invGrad); + + p.setDirection(result); + return result; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java new file mode 100644 index 00000000..9f7bdd2c --- /dev/null +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -0,0 +1,253 @@ +package pulse.search.direction; + +import static pulse.math.linear.SquareMatrix.asSquareMatrix; +import static pulse.properties.NumericProperties.compare; +import static pulse.properties.NumericProperties.isDiscrete; + +import pulse.math.IndexedVector; +import pulse.math.linear.Matrices; +import pulse.math.linear.RectangularMatrix; +import pulse.math.linear.SquareMatrix; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.statistics.OptimiserStatistic; +import pulse.search.statistics.ResidualStatistic; +import pulse.search.statistics.SumOfSquares; +import pulse.tasks.SearchTask; +import pulse.tasks.logs.Status; +import pulse.ui.Messages; + +public class LMOptimiser extends PathOptimiser { + + private static LMOptimiser instance = new LMOptimiser(); + private boolean computeJacobian; + + private final static double EPS = 1e-10; //for numerical comparison + + private LMOptimiser() { + super(); + this.setSolver(new HessianDirectionSolver() { + // see default implementation + }); + } + + @Override + public void reset() { + super.reset(); + computeJacobian = true; + } + + @Override + public boolean iteration(SearchTask task) throws SolverException { + var p = (LMPath) task.getPath(); // the previous path of the task + + /* + * Checks whether an iteration limit has been already reached + */ + + if (compare(p.getIteration(), getMaxIterations()) > 0) { + + task.setStatus(Status.TIMEOUT); + return true; + + } + + else { + + double initialCost = task.solveProblemAndCalculateCost(); + var parameters = task.searchVector()[0]; // get current search vector + + prepare(task); // do the preparatory step + + var candidateParams = parameters.sum(getSolver().direction(p)); + task.assign(new IndexedVector(candidateParams, parameters.getIndices())); // assign new parameters to this task + + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + + /* + * Delayed gratification + */ + + if (newCost > initialCost + EPS) { + p.setLambda(p.getLambda() * 2.0); + task.assign(parameters); // roll back if cost increased + computeJacobian = true; + } else { + p.setLambda(p.getLambda() / 3.0); + computeJacobian = false; + p.incrementStep(); // increment the counter of successful steps + } + + return newCost < initialCost + EPS; + + } + + } + + public RectangularMatrix jacobian(SearchTask task) throws SolverException { + + var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); + + final var params = task.searchVector()[0]; + + final int numPoints = residualCalculator.getResiduals().size(); + final int numParams = params.dimension(); + + var jacobian = new double[numPoints][numParams]; + + boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + final double dx = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + + for (int i = 0; i < numParams; i++) { + + final var shift = new Vector(numParams); + shift.set(i, 0.5 * dx); + + // + shift + task.assign(new IndexedVector(params.sum(shift), params.getIndices())); + task.solveProblemAndCalculateCost(); + var r1 = residualVector(residualCalculator); + + // - shift + task.assign(new IndexedVector(params.subtract(shift), params.getIndices())); + task.solveProblemAndCalculateCost(); + var r2 = residualVector(residualCalculator); + + for (int j = 0; j < numPoints; j++) { + + jacobian[j][i] = (r1[j] - r2[j]) / dx; + + } + + } + + // revert to original params + task.assign(params); + + return Matrices.createMatrix(jacobian); + + } + + private static double[] residualVector(ResidualStatistic rs) { + return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); + } + + @Override + public Path createPath(SearchTask t) { + computeJacobian = true; + return new LMPath(t); + } + + private Vector halfGradient(SearchTask task) { + var jacobian = ((LMPath) task.getPath()).getJacobian(); + var residuals = residualVector(task.getCurrentCalculation().getOptimiserStatistic()); + return jacobian.transpose().multiply(new Vector(residuals)); + } + + private SquareMatrix halfHessian(SearchTask task) { + var jacobian = ((LMPath) task.getPath()).getJacobian(); + return asSquareMatrix(jacobian.transpose().multiply(jacobian)); + } + + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (LMPath) task.getPath(); + + // Calculate the Jacobian -- if needed + if (computeJacobian) { + p.setJacobian(jacobian(task)); //J + p.setNonregularisedHessian(halfHessian(task)); //this is just J'J + } + + // the Jacobian is then used to calculate the 'gradient' + Vector g1 = halfGradient(task); // g1 + p.setGradient(g1); + + // the Hessian is then regularised by adding labmda*I + + var hessian = p.getNonregularisedHessian(); + var lambdaI = Matrices.createIdentityMatrix(hessian.getData().length).multiply(p.getLambda()); + var regularisedHessian = asSquareMatrix( hessian.sum( lambdaI ) ); //J'J + lambda I + + p.setHessian(regularisedHessian); //so this is the new Hessian + } + + /** + * This class uses a singleton pattern, meaning there is only instance of this + * class. + * + * @return the single (static) instance of this class + */ + + public static LMOptimiser getInstance() { + return instance; + } + + @Override + public String toString() { + return Messages.getString("LMOptimiser.Descriptor"); + } + + /* + * Path + */ + + class LMPath extends ComplexPath { + + private RectangularMatrix jacobian; + private SquareMatrix nonregularisedHessian; + private double lambda; + + public LMPath(SearchTask t) { + super(t); + } + + public RectangularMatrix getJacobian() { + return jacobian; + } + + public void setJacobian(RectangularMatrix jacobian) { + this.jacobian = jacobian; + } + + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + @Override + public void configure(SearchTask t) { + super.configure(t); + this.jacobian = null; + this.setHessian(null); + nonregularisedHessian = null; + this.lambda = 1.0; + } + + public SquareMatrix getNonregularisedHessian() { + return nonregularisedHessian; + } + + public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { + this.nonregularisedHessian = nonregularisedHessian; + } + + } + + /** + * The Levenberg-Marquardt optimiser will only accept ordinary least-squares + * as its objective function. Therefore, {@code os} should be an instance of + * {@code SumOfSquares}. + * @return {@code true} if {@code.getClass()} returns {@code SumOfSquares.class}, {@code false} otherwise + */ + + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return os.getClass().equals(SumOfSquares.class); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/Path.java b/src/main/java/pulse/search/direction/Path.java index 3ed91f32..c2142e75 100644 --- a/src/main/java/pulse/search/direction/Path.java +++ b/src/main/java/pulse/search/direction/Path.java @@ -4,6 +4,7 @@ import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; @@ -35,7 +36,7 @@ public class Path { private int iteration; protected Path(SearchTask t) { - reset(t); + configure(t); } /** @@ -46,10 +47,15 @@ protected Path(SearchTask t) { * @see pulse.search.direction.PathSolver.direction(Path) */ - public void reset(SearchTask t) { - this.gradient = PathOptimiser.gradient(t); - this.direction = PathOptimiser.getInstance().direction(this); + public void configure(SearchTask t) { + try { + this.gradient = PathOptimiser.getInstance().gradient(t); + } catch (SolverException e) { + System.err.println("Failed on gradient calculation while resetting optimiser..."); + e.printStackTrace(); + } minimumPoint = 0.0; + iteration = 0; } public Vector getDirection() { diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index 111ccda8..a267d305 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -1,6 +1,5 @@ package pulse.search.direction; -import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperties.isDiscrete; @@ -19,10 +18,9 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; -import pulse.search.linear.LinearOptimiser; +import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; -import pulse.tasks.logs.Status; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -44,11 +42,11 @@ public abstract class PathOptimiser extends PropertyHolder implements Reflexive { - private static int maxIterations; - private static double errorTolerance; - private static double gradientResolution; - - private static LinearOptimiser linearSolver; + private DirectionSolver solver; + + private int maxIterations; + private double errorTolerance; + private double gradientResolution; private static PathOptimiser instance; @@ -75,13 +73,13 @@ protected PathOptimiser() { * @see pulse.properties.Flag.defaultList() */ - public static void reset() { + public void reset() { maxIterations = (int) def(ITERATION_LIMIT).getValue(); errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); ActiveFlags.reset(); } - + /** *

* This method sets out the basic algorithm for estimating the minimum of the @@ -102,46 +100,8 @@ public static void reset() { * @see direction(Path) * @see pulse.search.linear.LinearOptimiser */ - - public double iteration(SearchTask task) throws SolverException { - var p = task.getPath(); // the previous path of the task - - /* - * Checks whether an iteration limit has been already reached - */ - - if (compare(p.getIteration(), getMaxIterations()) > 0) - task.setStatus(Status.TIMEOUT); - - var parameters = task.searchVector()[0]; // get current search vector - - var dir = direction(p); // find the direction to the global minimum - - double step = linearSolver.linearStep(task); // find how big the step needs to be to reach the minimum - p.setLinearStep(step); - - var newParams = parameters.sum(dir.multiply(step)); // this set of parameters supposedly corresponds to the - // minimum - task.assign(new IndexedVector(newParams, parameters.getIndices())); // assign new parameters to this task - - endOfStep(task); // compute gradients, Hessians, etc. with new parameters - - p.incrementStep(); // increment the counter of successful steps - - return task.solveProblemAndCalculateDeviation(); // calculate the sum of squared residuals - } - - /** - * Finds the direction of the minimum using the previously calculated values - * stored in {@code p}. - * - * @param p a {@code Path} object - * @return a {@code Vector} pointing to the minimum direction for this - * {@code Path} - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - - public abstract Vector direction(Path p); + + public abstract boolean iteration(SearchTask task) throws SolverException; /** * Defines a set of procedures to be run at the end of the search iteration. @@ -150,7 +110,7 @@ public double iteration(SearchTask task) throws SolverException { * @throws SolverException */ - public abstract void endOfStep(SearchTask task) throws SolverException; + public abstract void prepare(SearchTask task) throws SolverException; /** * Calculates the {@code Vector} gradient of the target function (the sum of @@ -177,24 +137,24 @@ public double iteration(SearchTask task) throws SolverException { * @throws SolverException */ - public static Vector gradient(SearchTask task) { + public Vector gradient(SearchTask task) throws SolverException { final var params = task.searchVector()[0]; var grad = new Vector(params.dimension()); boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - final double dx = discreteGradient ? 2.0 * dxGrid : 2.0 * gradientResolution; + final double dx = discreteGradient ? dxGrid : gradientResolution; for (int i = 0; i < params.dimension(); i++) { final var shift = new Vector(params.dimension()); shift.set(i, 0.5 * dx); task.assign(new IndexedVector( params.sum(shift) , params.getIndices())); - final double ss2 = task.solveProblemAndCalculateDeviation(); + final double ss2 = task.solveProblemAndCalculateCost(); task.assign(new IndexedVector( params.subtract(shift), params.getIndices())); - final double ss1 = task.solveProblemAndCalculateDeviation(); + final double ss1 = task.solveProblemAndCalculateCost(); grad.set(i, (ss2 - ss1) / dx); @@ -206,45 +166,28 @@ public static Vector gradient(SearchTask task) { } - public static LinearOptimiser getLinearSolver() { - return linearSolver; - } - - /** - * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this - * object as its parent. - * - * @param linearSearch a {@code LinearSolver} - */ - - public void setLinearSolver(LinearOptimiser linearSearch) { - PathOptimiser.linearSolver = linearSearch; - linearSolver.setParent(this); - super.parameterListChanged(); - } - - public static NumericProperty getErrorTolerance() { + public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); } - public static void setErrorTolerance(NumericProperty errorTolerance) { - PathOptimiser.errorTolerance = (double) errorTolerance.getValue(); + public void setErrorTolerance(NumericProperty errorTolerance) { + this.errorTolerance = (double) errorTolerance.getValue(); } - public static void setGradientResolution(NumericProperty resolution) { - PathOptimiser.gradientResolution = (double) resolution.getValue(); + public void setGradientResolution(NumericProperty resolution) { + this.gradientResolution = (double) resolution.getValue(); } - public static NumericProperty getGradientResolution() { + public NumericProperty getGradientResolution() { return derive(GRADIENT_RESOLUTION, gradientResolution); } - public static NumericProperty getMaxIterations() { + public NumericProperty getMaxIterations() { return derive(ITERATION_LIMIT, maxIterations); } - public static void setMaxIterations(NumericProperty maxIterations) { - PathOptimiser.maxIterations = (int) maxIterations.getValue(); + public void setMaxIterations(NumericProperty maxIterations) { + this.maxIterations = (int) maxIterations.getValue(); } @Override @@ -362,5 +305,23 @@ public static void setInstance(PathOptimiser selectedPathOptimiser) { PathOptimiser.instance = selectedPathOptimiser; selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); } + + protected DirectionSolver getSolver() { + return solver; + } + + protected void setSolver(DirectionSolver solver) { + this.solver = solver; + } + + /** + * Checks if this optimiser is compatible with the statistic passed to the method as its argument. + * By default, this will accept any {@code OptimiserStatistic}. + * @return {@code true}, if not specified otherwise by its subclass implementation. + */ + + public boolean compatibleWith(OptimiserStatistic os) { + return true; + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java new file mode 100644 index 00000000..37c2a74c --- /dev/null +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -0,0 +1,106 @@ +package pulse.search.direction; + +import static java.lang.Math.abs; +import static pulse.math.linear.SquareMatrix.asSquareMatrix; +import static pulse.math.linear.SquareMatrix.outerProduct; + +import pulse.math.linear.SquareMatrix; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.SearchTask; +import pulse.ui.Messages; + +public class SR1Optimiser extends CompositePathOptimiser { + + private static SR1Optimiser instance = new SR1Optimiser(); + + private final static double r = 1E-8; + + private SR1Optimiser() { + super(); + this.setSolver(path -> ((ComplexPath)path).getInverseHessian().multiply(path.getGradient().inverted())); + } + + /** + *

+ * Calculated the gradient at the end of this step. Invokes {@code hessian(...)} + * to calculate the Hessian matrix at the {@code k+1} step using the + * gk and + * gk+1 gradient values, the previously + * calculated Hessian matrix on step k, and the result of the linear + * search αk+1. + *

+ * + * @throws SolverException + */ + + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (ComplexPath) task.getPath(); + Vector dir = p.getDirection(); + + final double minimumPoint = p.getMinimumPoint(); + final Vector g0 = p.getGradient(); // g0 + final Vector g1 = gradient(task); // g1 + + /* + * Evaluate condition and update if needed + */ + + Vector y = g1.subtract(g0); // g[k+1] - g[k] + + final var dx = dir.multiply(minimumPoint); + final var m1 = y.subtract(p.getHessian().multiply(dx)); + + if(abs(dx.dot(m1)) > r*dx.length()*m1.length() ) { + + var m = p.getHessian().sum((outerProduct(m1, m1)).multiply(1. / m1.dot(dx))); + p.setHessian( asSquareMatrix(m) ); + p.setInverseHessian( inverseHessian(g0, g1, dir, p.getInverseHessian(), minimumPoint) ); + + } + + p.setGradient(g1); // set g1 as the new gradient for next step + + } + + private SquareMatrix inverseHessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevInvHessian, double alpha) { + Vector y = g2.subtract(g1); // g[k+1] - g[k] + + final var dx = dir.multiply(alpha); + final var m1 = dx.subtract(prevInvHessian.multiply(y)); + + var m = prevInvHessian.sum((outerProduct(m1, m1)).multiply(1. / m1.dot(y))); //SR1 formula + return asSquareMatrix(m); + } + + @Override + public String toString() { + return Messages.getString("SR1.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of this + * class. + * + * @return the single (static) instance of this class + */ + + public static SR1Optimiser getInstance() { + return instance; + } + + /** + * Creates a new {@code Path} instance for storing the gradient, direction, and + * minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + + @Override + public Path createPath(SearchTask t) { + return new ComplexPath(t); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 23e663d5..d02d7890 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -15,28 +15,20 @@ * page */ -public class SteepestDescentOptimiser extends PathOptimiser { +public class SteepestDescentOptimiser extends CompositePathOptimiser { private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); private SteepestDescentOptimiser() { super(); - } - - /** - *

- * The direction of the minimum at the iteration k is - * calculated simply as the the inverted gradient vector: - * pk = -gk. Invokes - * {@code p.setDirection()}. - *

- */ - - @Override - public Vector direction(Path p) { - Vector dir = p.getGradient().inverted(); // p_k = -g - p.setDirection(dir); - return dir; + //init gradient solver + this.setSolver( p -> { + + Vector dir = p.getGradient().inverted(); // p_k = -g + p.setDirection(dir); + return dir; + + }); } /** @@ -46,7 +38,7 @@ public Vector direction(Path p) { */ @Override - public void endOfStep(SearchTask task) throws SolverException { + public void prepare(SearchTask task) throws SolverException { task.getPath().setGradient(gradient(task)); } diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index 6d5c1897..6cba4146 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -63,11 +63,11 @@ public double linearStep(SearchTask task) throws SolverException { final var newParams1 = params[0].sum(direction.multiply(alpha)); // alpha task.assign(new IndexedVector(newParams1, params[0].getIndices())); - final double ss2 = task.solveProblemAndCalculateDeviation(); // f(alpha) + final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) final var newParams2 = params[0].sum(direction.multiply(one_minus_alpha)); // 1 - alpha task.assign(new IndexedVector(newParams2, params[0].getIndices())); - final double ss1 = task.solveProblemAndCalculateDeviation(); // f(1-alpha) + final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) task.assign(new IndexedVector(newParams2, params[0].getIndices())); // return to old position diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 5bd11959..fc087140 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -18,7 +18,7 @@ * {@code ApproximatedHessianSolver}. *

* - * @see pulse.search.direction.ApproximatedHessianOptimiser + * @see pulse.search.direction.BFGSOptimiser * @see Wikipedia * page */ @@ -78,10 +78,12 @@ public double linearStep(SearchTask task) throws SolverException { var params = task.searchVector(); Segment segment = domain(params[0], params[1], direction); - double ss1 = task.solveProblemAndCalculateDeviation(); + double ss1 = task.solveProblemAndCalculateCost(); double randomConfinedValue = 0; double g2p; + + var instance = PathOptimiser.getInstance(); for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { @@ -90,7 +92,7 @@ public double linearStep(SearchTask task) throws SolverException { final var newParams = params[0].sum(direction.multiply(randomConfinedValue)); task.assign(new IndexedVector(newParams, params[0].getIndices())); - final double ss2 = task.solveProblemAndCalculateDeviation(); + final double ss2 = task.solveProblemAndCalculateCost(); /** * Checks if the first Armijo inequality is not satisfied. In this case, it will @@ -102,7 +104,7 @@ public double linearStep(SearchTask task) throws SolverException { continue; } - final var g2 = PathOptimiser.gradient(task); + final var g2 = instance.gradient(task); g2p = g2.dot(direction); /** diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java new file mode 100644 index 00000000..aad0cfb7 --- /dev/null +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -0,0 +1,59 @@ +package pulse.search.statistics; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; + +import pulse.tasks.SearchTask; + +public class RegularisedLeastSquares extends OptimiserStatistic { + + private double lambda = 1e-4; + private SumOfSquares sos; + + public RegularisedLeastSquares() { + super(); + sos = new SumOfSquares(); + } + + public RegularisedLeastSquares(RegularisedLeastSquares rls) { + super(rls); + sos = new SumOfSquares(rls.sos); + this.lambda = rls.lambda; + } + + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + /* + * SSR with L2 regularisation + */ + + @Override + public void evaluate(SearchTask t) { + sos.evaluate(t); + final double ssr = (double)sos.getStatistic().getValue(); + final double statistic = ssr + lambda*t.searchVector()[0].lengthSq(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "L2 Regularised Least Squares"; + } + + @Override + public double variance() { + return (double)sos.getStatistic().getValue(); + } + + @Override + public OptimiserStatistic copy() { + return new RegularisedLeastSquares(this); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index 573a1afc..038ef48a 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -52,7 +52,7 @@ public void evaluate(SearchTask t) { @Override public String getDescriptor() { - return "Ordinary least squares"; + return "Ordinary Least Squares"; } @Override diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 5b73f63c..14847faa 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -5,7 +5,6 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import static pulse.tasks.logs.Status.FAILED; import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.util.Reflexive.instantiate; @@ -164,16 +163,10 @@ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { */ @SuppressWarnings({ "unchecked", "rawtypes" }) - public void process() { - try { - ((Solver) scheme).solve(problem); - } catch (SolverException e) { - status = FAILED; - System.err.println("Solver of " + this + " has encountered an error. Details: "); - e.printStackTrace(); - } + public void process() throws SolverException { + ((Solver) scheme).solve(problem); } - + public Status getStatus() { return status; } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 89b402be..d250f567 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -4,16 +4,15 @@ import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; import static pulse.search.direction.ActiveFlags.activeParameters; import static pulse.search.direction.ActiveFlags.getAllFlags; -import static pulse.search.direction.PathOptimiser.getErrorTolerance; import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.getLinearSolver; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; +import static pulse.tasks.logs.Details.INCOMPATIBLE_OPTIMISER; import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; +import static pulse.tasks.logs.Details.MAX_ITERATIONS_REACHED; import static pulse.tasks.logs.Details.MISSING_BUFFER; import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; -import static pulse.tasks.logs.Details.MISSING_LINEAR_SOLVER; -import static pulse.tasks.logs.Details.MISSING_PATH_SOLVER; +import static pulse.tasks.logs.Details.MISSING_OPTIMISER; import static pulse.tasks.logs.Details.MISSING_PROBLEM_STATEMENT; import static pulse.tasks.logs.Details.PARAMETER_VALUES_NOT_SENSIBLE; import static pulse.tasks.logs.Details.SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS; @@ -42,6 +41,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.direction.Path; +import pulse.search.direction.PathOptimiser; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -81,7 +81,7 @@ public class SearchTask extends Accessible implements Runnable { private CorrelationBuffer correlationBuffer; private CorrelationTest correlationTest; private NormalityTest normalityTest; - + private Identifier identifier; /** @@ -91,7 +91,7 @@ public class SearchTask extends Accessible implements Runnable { private List listeners = new CopyOnWriteArrayList<>(); private List statusChangeListeners = new CopyOnWriteArrayList<>(); - + /** *

* Creates a new {@code SearchTask} from {@code curve}. Generates a new @@ -113,11 +113,11 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - + private void addListeners() { InterpolationDataset.addListener(e -> { - var p = current.getProblem().getProperties(); - if(p.areThermalPropertiesLoaded()) + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) p.useTheoreticalEstimates(curve); }); @@ -126,7 +126,8 @@ private void addListeners() { if (scheme != null) { var hcurve = current.getProblem().getHeatingCurve(); var startTime = (double) hcurve.getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + scheme.setTimeLimit( + derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); } @@ -135,14 +136,11 @@ private void addListeners() { *

* Resets everything to default values (for a list of default values please see * the {@code .xml} document. Sets the status of this task to - * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { - var scheme = current.getScheme(); - if (scheme != null) { - var curve = current.getProblem().getHeatingCurve(); - var startTime = (double) curve.getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); - } - }); + * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { var scheme = + * current.getScheme(); if (scheme != null) { var curve = + * current.getProblem().getHeatingCurve(); var startTime = (double) + * curve.getTimeShift().getValue(); scheme.setTimeLimit(derive(TIME_LIMIT, + * RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); *

*/ @@ -159,10 +157,10 @@ public void clear() { this.path = null; current.clear(); - + this.checkProblems(true); } - + /** * This will use the current {@code DifferenceScheme} to solve the * {@code Problem} for this {@code SearchTask} and calculate the SSR value @@ -173,7 +171,7 @@ public void clear() { * @throws SolverException */ - public double solveProblemAndCalculateDeviation() { + public double solveProblemAndCalculateCost() throws SolverException { current.process(); var rs = current.getOptimiserStatistic(); rs.evaluate(this); @@ -240,7 +238,7 @@ public void assign(IndexedVector searchParameters) { public void run() { current.setResult(null); - + /* check of status */ switch (current.getStatus()) { @@ -251,17 +249,15 @@ public void run() { default: return; } - + /* preparatory steps */ current.getProblem().parameterListChanged(); // get updated list of parameters - current.process(); - - var pathSolver = getInstance(); - path = pathSolver.createPath(this); + var optimiser = getInstance(); - var errorTolerance = (double) getErrorTolerance().getValue(); + path = optimiser.createPath(this); + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); int bufferSize = (Integer) getSize().getValue(); buffer.init(); correlationBuffer.clear(); @@ -273,6 +269,15 @@ public void run() { List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); + try { + solveProblemAndCalculateCost(); + } catch (SolverException e1) { + System.err.println("Failed on first calculation. Details:"); + e1.printStackTrace(); + } + + final int maxIterations = (int)PathOptimiser.getInstance().getMaxIterations().getValue(); + outer: do { bufferFutures.clear(); @@ -282,13 +287,22 @@ public void run() { if (current.getStatus() != IN_PROGRESS) break outer; + int iter = 0; + try { - pathSolver.iteration(this); + for (boolean finished = false; !finished && iter < maxIterations; iter++) + finished = optimiser.iteration(this); } catch (SolverException e) { setStatus(FAILED); System.err.println(this + " failed during execution. Details: "); e.printStackTrace(); } + + if(iter >= maxIterations) { + var fail = FAILED; + fail.setDetails(MAX_ITERATIONS_REACHED); + setStatus(fail); + } final var j = i; @@ -329,8 +343,7 @@ private void runChecks() { var status = AMBIGUOUS; status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); setStatus(status); - } - else { + } else { // lastly, check if the parameter values estimated in this procedure are // reasonable @@ -340,12 +353,11 @@ private void runChecks() { var status = FAILED; status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); setStatus(status); - } - else { + } else { current.getModelSelectionCriterion().evaluate(this); setStatus(DONE); } - + } } @@ -394,11 +406,11 @@ public void setExperimentalCurve(ExperimentalData curve) { curve.setParent(this); } - + public void setStatus(Status status) { Objects.requireNonNull(status); boolean changed = current.setStatus(status); - if(changed) + if (changed) notifyStatusListeners(new StateEntry(this, status)); } @@ -418,7 +430,7 @@ public void setStatus(Status status) { public void checkProblems(boolean updateStatus) { var status = current.getStatus(); - + if (status == DONE) return; @@ -434,15 +446,15 @@ else if (current.getScheme() == null) else if (curve == null) s.setDetails(MISSING_HEATING_CURVE); else if (pathSolver == null) - s.setDetails(MISSING_PATH_SOLVER); - else if (getLinearSolver() == null) - s.setDetails(MISSING_LINEAR_SOLVER); + s.setDetails(MISSING_OPTIMISER); else if (buffer == null) s.setDetails(MISSING_BUFFER); + else if (!PathOptimiser.getInstance().compatibleWith(current.getOptimiserStatistic()) ) + s.setDetails(INCOMPATIBLE_OPTIMISER); else s = READY; - - if(updateStatus) + + if (updateStatus) setStatus(s); } @@ -548,11 +560,11 @@ public CorrelationTest getCorrelationTest() { public Calculation getCurrentCalculation() { return current; } - + public List getStoredCalculations() { return this.stored; } - + public void switchTo(Calculation calc) { current.setParent(null); current = calc; @@ -560,17 +572,17 @@ public void switchTo(Calculation calc) { var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); } - + public void switchToBestModel() { var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); - this.switchTo(best.get()); + this.switchTo(best.get()); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); fireRepositoryEvent(e); } - + private void fireRepositoryEvent(TaskRepositoryEvent e) { var instance = TaskManager.getManagerInstance(); - for(var l : instance.getTaskRepositoryListeners()) + for (var l : instance.getTaskRepositoryListeners()) l.onTaskListChanged(e); } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 3df2f751..ef376e41 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -304,7 +304,7 @@ public void reset() { notifyListeners(e); } - PathOptimiser.reset(); + PathOptimiser.getInstance().reset(); } diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 2c4f1434..251d605f 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -29,22 +29,22 @@ public enum Details { MISSING_HEATING_CURVE, /** - * No information can be found about the selected path solver. + * There is no information about the selected optimiser. */ - MISSING_LINEAR_SOLVER, - - /** - * There is no information about the selected path solver. - */ - - MISSING_PATH_SOLVER, + MISSING_OPTIMISER, /** * The buffer has not been created. */ MISSING_BUFFER, + + /** + * The optimisation statistic is not suported by the selected optimiser. + */ + + INCOMPATIBLE_OPTIMISER, /** * Some data is missing in the problem statement. Probably, the interpolation @@ -58,6 +58,8 @@ public enum Details { PARAMETER_VALUES_NOT_SENSIBLE, + MAX_ITERATIONS_REACHED, + ABNORMAL_DISTRIBUTION_OF_RESIDUALS; @Override diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 1c5f1ad3..38d995a8 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -34,7 +34,7 @@ public class Launcher { private PrintStream errStream; private File errorLog; - private final static boolean DEBUG = false; + private final static boolean DEBUG = true; private Launcher() { arrangeErrorOutput(); diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index de024d08..7d1d99a6 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -42,6 +42,7 @@ import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.search.statistics.OptimiserStatistic; +import pulse.search.statistics.SumOfSquares; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; @@ -184,10 +185,8 @@ private JMenu initAnalysisSubmenu() { var statisticItems = new ButtonGroup(); - JRadioButtonMenuItem item = null; - for (var statisticName : allDescriptors(NormalityTest.class)) { - item = new JRadioButtonMenuItem(statisticName); + var item = new JRadioButtonMenuItem(statisticName); statisticItems.add(item); statisticsSubMenu.add(item); item.addItemListener(e -> { @@ -221,16 +220,19 @@ private JMenu initAnalysisSubmenu() { var optimisersItems = new ButtonGroup(); - item = null; - var set = allDescriptors(OptimiserStatistic.class); + var defaultOptimiser = new SumOfSquares(); for (var statisticName : set) { - item = new JRadioButtonMenuItem(statisticName); + var item = new JRadioButtonMenuItem(statisticName); optimisersItems.add(item); optimisersSubMenu.add(item); + + if(statisticName.equalsIgnoreCase(defaultOptimiser.getDescriptor())) + item.setSelected(true); + item.addItemListener(e -> { - + if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); @@ -238,9 +240,18 @@ private JMenu initAnalysisSubmenu() { } }); + } - + + //for some reason it does not work without this line! optimisersSubMenu.getItem(0).setSelected(true); + + for(int i = 0, size = set.size(); i < size; i++) { + var item = optimisersSubMenu.getItem(i); + if(item.getText().equalsIgnoreCase(defaultOptimiser.getDescriptor())) + item.setSelected(true); + } + analysisSubMenu.add(optimisersSubMenu); // diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index e58d61b4..abb2dfaf 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -5,7 +5,6 @@ import static javax.swing.BorderFactory.createTitledBorder; import static javax.swing.ListSelectionModel.SINGLE_SELECTION; import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.getLinearSolver; import static pulse.search.direction.PathOptimiser.setInstance; import static pulse.ui.Messages.getString; import static pulse.util.Reflexive.instancesOf; @@ -22,10 +21,8 @@ import javax.swing.JScrollPane; import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; -import javax.swing.table.DefaultTableModel; import pulse.search.direction.PathOptimiser; -import pulse.search.linear.LinearOptimiser; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.controllers.SearchListRenderer; @@ -34,13 +31,10 @@ public class SearchOptionsFrame extends JInternalFrame { private PropertyHolderTable pathTable; - private JList linearList; private PathSolversList pathList; private final static Font font = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); - private final static List pathSolvers = instancesOf(PathOptimiser.class); - private final static List linearSolvers = instancesOf(LinearOptimiser.class); /** * Create the frame. @@ -61,12 +55,7 @@ public SearchOptionsFrame() { pathList = new PathSolversList(); var pathListScroller = new JScrollPane(pathList); - pathListScroller.setBorder(createTitledBorder("Select a Direction Search Method")); - - linearList = new LinearSearchList(); - linearList.setEnabled(false); - var linearListScroller = new JScrollPane(linearList); - linearListScroller.setBorder(createTitledBorder("Select a Line Search Method")); + pathListScroller.setBorder(createTitledBorder("Select an Optimiser")); pathTable = new PropertyHolderTable(null); @@ -83,10 +72,6 @@ public SearchOptionsFrame() { getContentPane().add(pathListScroller, gbc); gbc.gridy = 1; - - getContentPane().add(linearListScroller, gbc); - - gbc.gridy = 2; gbc.weighty = 0.6; var tableScroller = new JScrollPane(pathTable); @@ -99,22 +84,14 @@ public void update() { var selected = getInstance(); if (selected != null) { pathList.setSelectedIndex(pathSolvers.indexOf(selected)); - linearList.setSelectedIndex(linearSolvers.indexOf(getLinearSolver())); pathTable.updateTable(); } else { pathList.clearSelection(); - linearList.clearSelection(); - linearList.setEnabled(false); } } class PathSolversList extends JList { - /** - * - */ - private static final long serialVersionUID = 3662972578473909850L; - public PathSolversList() { super(); @@ -143,74 +120,18 @@ public PathOptimiser getElementAt(int index) { addListSelectionListener((ListSelectionEvent arg0) -> { if (arg0.getValueIsAdjusting()) return; - if (!(getSelectedValue() instanceof PathOptimiser)) { - ((DefaultTableModel) pathTable.getModel()).setRowCount(0); - return; - } - var searchScheme = getSelectedValue(); - if (searchScheme == null) - return; - setInstance(searchScheme); - linearList.setEnabled(true); - for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(true); - } - }); - - } - } - - class LinearSearchList extends JList { - - /** - * - */ - private static final long serialVersionUID = 5478023007473400159L; - - public LinearSearchList() { - - super(); - - setFont(font); - setSelectionMode(SINGLE_SELECTION); - setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -3560305247730025830L; - - @Override - public int getSize() { - return linearSolvers.size(); - } - - @Override - public LinearOptimiser getElementAt(int index) { - return linearSolvers.get(index); - } - }); - - this.setCellRenderer(new SearchListRenderer()); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - if (!(getSelectedValue() instanceof LinearOptimiser)) { - pathTable.setEnabled(false); - return; - } - var linearSolver = getSelectedValue(); - var pathSolver = getInstance(); - pathSolver.setLinearSolver(linearSolver); - pathTable.setPropertyHolder(pathSolver); - pathTable.setEnabled(true); + + var optimiser = getSelectedValue(); + + setInstance(optimiser); + pathTable.setPropertyHolder(optimiser); + for (var t : TaskManager.getManagerInstance().getTaskList()) { t.checkProblems(true); } }); } - } } \ No newline at end of file diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 2e46515c..1d5a90fe 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -395,7 +395,7 @@ Golden Section
(low accuracy, high speed) WolfeSolver.Descriptor=Wolfe Conditions
(high accuracy, low speed) -ApproximatedHessianSolver.Descriptor=Approximated Hessian Method
(high accuracy, low speed) +ApproximatedHessianSolver.Descriptor=BFGS (Quasi-Newton)
(high accuracy, low speed) +LMOptimiser.Descriptor=Levenberg-Marquardt
Damped least-squares solver. Does not require a linear search routine.
+SR1.Descriptor=SR1 (Quasi-Newton)
(Best for sparse problems) SteepestDescentSolver.Descriptor=Steepest Descent
(low accuracy, high speed) BufferSize.Label=BufferSize BufferSize.Descriptor=Buffer size From b9421ca8b8319578ae287fd3a2c0505ba18a221c Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Thu, 29 Oct 2020 11:19:34 +0000 Subject: [PATCH 023/116] Optional geodesic convergence for L-M solver --- .../java/pulse/math/linear/SquareMatrix.java | 18 ++ .../properties/NumericPropertyKeyword.java | 8 +- .../pulse/search/direction/BFGSOptimiser.java | 13 - .../pulse/search/direction/ComplexPath.java | 3 +- .../direction/CompositePathOptimiser.java | 14 + .../direction/HessianDirectionSolver.java | 24 +- .../pulse/search/direction/LMOptimiser.java | 251 ++++++++++++++---- .../pulse/search/direction/PathOptimiser.java | 26 ++ .../pulse/search/direction/SR1Optimiser.java | 13 - .../direction/SteepestDescentOptimiser.java | 1 + .../pulse/search/linear/WolfeOptimiser.java | 14 +- src/main/java/pulse/tasks/SearchTask.java | 7 +- src/main/resources/NumericProperty.xml | 7 +- 13 files changed, 300 insertions(+), 99 deletions(-) diff --git a/src/main/java/pulse/math/linear/SquareMatrix.java b/src/main/java/pulse/math/linear/SquareMatrix.java index 52988845..22032327 100644 --- a/src/main/java/pulse/math/linear/SquareMatrix.java +++ b/src/main/java/pulse/math/linear/SquareMatrix.java @@ -98,5 +98,23 @@ public static SquareMatrix outerProduct(Vector a, Vector b) { public static SquareMatrix asSquareMatrix(RectangularMatrix m) { return m.x.length == m.x[0].length ? new SquareMatrix(m.getData()) : null; } + + public int dimension() { + return getData().length; + } + + /** + * Creates a block-diagonal matrix from the diagonal of this matrix. + * @return diag(this) + */ + + public SquareMatrix blockDiagonal() { + final int dim = dimension(); + var data = getData(); + var diag = new double[dim][dim]; + for(int i = 0; i < dim; i++) + diag[i][i] = data[i][i]; + return new SquareMatrix(diag); + } } \ No newline at end of file diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 424b5092..1afaf682 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -489,7 +489,13 @@ public enum NumericPropertyKeyword { * A weight indicating how good a calculation model is. */ - MODEL_WEIGHT; + MODEL_WEIGHT, + + /** + * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg damping. A value of 1 gives pure Marquardt damping. + */ + + DAMPING_RATIO; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 7bd8e2d4..d089668d 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -106,17 +106,4 @@ public static BFGSOptimiser getInstance() { return instance; } - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ - - @Override - public Path createPath(SearchTask t) { - return new ComplexPath(t); - } - } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index 4b407492..8103fc69 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -2,7 +2,6 @@ import static pulse.math.linear.Matrices.createIdentityMatrix; -import pulse.math.linear.Matrices; import pulse.math.linear.SquareMatrix; import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.SearchTask; @@ -36,7 +35,7 @@ protected ComplexPath(SearchTask task) { public void configure(SearchTask task) { super.configure(task); hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); - inverseHessian = Matrices.createIdentityMatrix(hessian.getData().length); + inverseHessian = createIdentityMatrix(hessian.getData().length); } public SquareMatrix getHessian() { diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 0d0dde33..b64e00c0 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -91,5 +91,19 @@ public List listedTypes() { list.add(instanceDescriptor); return list; } + + /** + * Creates a new {@code Path} instance for storing the gradient, direction, and + * minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + + @Override + public Path createPath(SearchTask t) { + this.configure(t); + return new ComplexPath(t); + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index 92dff17b..1246f16c 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -13,21 +13,29 @@ public interface HessianDirectionSolver extends DirectionSolver { * second derivatives, calculated with the BFGS formula in combination with the * local value of the gradient to evaluate the direction of the minimum on * {@code p}. Invokes {@code p.setDirection()}. - * @throws SolverException + * + * @throws SolverException */ + @Override public default Vector direction(Path p) throws SolverException { var cp = (ComplexPath) p; - final int dimg = p.getGradient().dimension(); - Vector invGrad = p.getGradient().inverted(); - Vector result; + Vector invGrad = p.getGradient().inverted(); + var result = solve(cp, invGrad); + + p.setDirection(result); + return result; + } + public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { + final int dimg = cp.getGradient().dimension(); + Vector result; // use linear solver for big matrices if (dimg > 4) { var hess = new DMatrixRMaj(cp.getHessian().getData()); - var antigrad = new DMatrixRMaj(invGrad.getData()); + var antigrad = new DMatrixRMaj(rhs.getData()); var dirv = new DMatrixRMaj(dimg, 1); if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { @@ -37,10 +45,10 @@ public default Vector direction(Path p) throws SolverException { result = new Vector(dirv.getData()); } else // use fast inverse - result = cp.getHessian().inverse().multiply(invGrad); - - p.setDirection(result); + result = cp.getHessian().inverse().multiply(rhs); + return result; + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 9f7bdd2c..e2426ef9 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -2,7 +2,12 @@ import static pulse.math.linear.SquareMatrix.asSquareMatrix; import static pulse.properties.NumericProperties.compare; -import static pulse.properties.NumericProperties.isDiscrete; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.DAMPING_RATIO; + +import java.util.List; import pulse.math.IndexedVector; import pulse.math.linear.Matrices; @@ -10,6 +15,9 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.properties.Property; import pulse.search.statistics.OptimiserStatistic; import pulse.search.statistics.ResidualStatistic; import pulse.search.statistics.SumOfSquares; @@ -21,16 +29,21 @@ public class LMOptimiser extends PathOptimiser { private static LMOptimiser instance = new LMOptimiser(); private boolean computeJacobian; + + private final static double EPS = 1e-10; // for numerical comparison + private double dampingRatio; - private final static double EPS = 1e-10; //for numerical comparison + private double geodesicParameter = 0.1; + private boolean geodesicCorrection = false; private LMOptimiser() { super(); + dampingRatio = (double)def(DAMPING_RATIO).getValue(); this.setSolver(new HessianDirectionSolver() { // see default implementation }); } - + @Override public void reset() { super.reset(); @@ -54,16 +67,35 @@ public boolean iteration(SearchTask task) throws SolverException { else { - double initialCost = task.solveProblemAndCalculateCost(); - var parameters = task.searchVector()[0]; // get current search vector - + double initialCost = task.solveProblemAndCalculateCost(); + var parameters = task.searchVector()[0]; + p.setParameters(parameters); // store current parameters + prepare(task); // do the preparatory step - - var candidateParams = parameters.sum(getSolver().direction(p)); - task.assign(new IndexedVector(candidateParams, parameters.getIndices())); // assign new parameters to this task + + var lmDirection = getSolver().direction(p); - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + /* + * Geodesic acceleration + */ + var acceleration = p.getJacobian().transpose().multiply( directionalDerivative(task) ); // J' dr/dp + var correction = HessianDirectionSolver.solve(p, acceleration.inverted() ); // H^-1 J'dr/dp + + double newCost = Double.POSITIVE_INFINITY; + + /* + * Additional conditions imposed by geodesic acceleration. + */ + + if( !geodesicCorrection || correction.length() / lmDirection.length() <= geodesicParameter) { + var candidate = parameters.sum(lmDirection); + task.assign(new IndexedVector( + geodesicCorrection ? candidate.sum( correction.multiply(0.5) ) : candidate, + parameters.getIndices() ) ); // assign new parameters + newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + } + /* * Delayed gratification */ @@ -83,34 +115,66 @@ public boolean iteration(SearchTask task) throws SolverException { } } + + + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (LMPath) task.getPath(); + + //store residual vector at current parameters + p.setResidualVector( new Vector( residualVector(task.getCurrentCalculation().getOptimiserStatistic()) )); + // Calculate the Jacobian -- if needed + if (computeJacobian) { + p.setJacobian(jacobian(task)); // J + p.setNonregularisedHessian(halfHessian(p)); // this is just J'J + } + + // the Jacobian is then used to calculate the 'gradient' + Vector g1 = halfGradient(p); // g1 + p.setGradient(g1); + + // the Hessian is then regularised by adding labmda*I + + var hessian = p.getNonregularisedHessian(); + var damping = ( levenbergDamping(hessian).multiply(dampingRatio) + .sum(marquardtDamping(hessian).multiply(1.0 - dampingRatio)) + ) + .multiply(p.getLambda()); + var regularisedHessian = asSquareMatrix(hessian.sum(damping)); // J'J + lambda I + + p.setHessian(regularisedHessian); // so this is the new Hessian + } + + public RectangularMatrix jacobian(SearchTask task) throws SolverException { var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); - final var params = task.searchVector()[0]; + var p = ((LMPath) task.getPath()); - final int numPoints = residualCalculator.getResiduals().size(); + final var params = p.getParameters(); + final var indices = params.getIndices(); + + final int numPoints = p.getResidualVector().dimension(); final int numParams = params.dimension(); var jacobian = new double[numPoints][numParams]; - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - final double dx = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); - + final double dx = super.getGradientStep(); + for (int i = 0; i < numParams; i++) { final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); // + shift - task.assign(new IndexedVector(params.sum(shift), params.getIndices())); + task.assign(new IndexedVector(params.sum(shift), indices)); task.solveProblemAndCalculateCost(); var r1 = residualVector(residualCalculator); // - shift - task.assign(new IndexedVector(params.subtract(shift), params.getIndices())); + task.assign(new IndexedVector(params.subtract(shift), indices)); task.solveProblemAndCalculateCost(); var r2 = residualVector(residualCalculator); @@ -135,42 +199,87 @@ private static double[] residualVector(ResidualStatistic rs) { @Override public Path createPath(SearchTask t) { + this.configure(t); computeJacobian = true; return new LMPath(t); } - private Vector halfGradient(SearchTask task) { - var jacobian = ((LMPath) task.getPath()).getJacobian(); - var residuals = residualVector(task.getCurrentCalculation().getOptimiserStatistic()); + private Vector halfGradient(LMPath path) { + var jacobian = path.getJacobian(); + var residuals = path.getResidualVector(); return jacobian.transpose().multiply(new Vector(residuals)); } - private SquareMatrix halfHessian(SearchTask task) { - var jacobian = ((LMPath) task.getPath()).getJacobian(); + private SquareMatrix halfHessian(LMPath path) { + var jacobian = path.getJacobian(); return asSquareMatrix(jacobian.transpose().multiply(jacobian)); } - @Override - public void prepare(SearchTask task) throws SolverException { - var p = (LMPath) task.getPath(); + private Vector directionalDerivative(SearchTask t) throws SolverException { + var p = (LMPath) t.getPath(); - // Calculate the Jacobian -- if needed - if (computeJacobian) { - p.setJacobian(jacobian(task)); //J - p.setNonregularisedHessian(halfHessian(task)); //this is just J'J - } + var currentParameters = p.getParameters(); + final int numParams = currentParameters.dimension(); - // the Jacobian is then used to calculate the 'gradient' - Vector g1 = halfGradient(task); // g1 - p.setGradient(g1); + final var dir = p.getDirection(); + var shift = new Vector(numParams); + + final double h = 0.5*super.getGradientStep(); - // the Hessian is then regularised by adding labmda*I + //small shift in a previously calculated direction + for(int i = 0; i < numParams; i++) + shift.set(i, h * dir.get(i) ); - var hessian = p.getNonregularisedHessian(); - var lambdaI = Matrices.createIdentityMatrix(hessian.getData().length).multiply(p.getLambda()); - var regularisedHessian = asSquareMatrix( hessian.sum( lambdaI ) ); //J'J + lambda I + final var statistic = t.getCurrentCalculation().getOptimiserStatistic(); + + var currentResiduals = p.getResidualVector(); + + t.assign( new IndexedVector( currentParameters.sum(shift), currentParameters.getIndices() ) ); + t.solveProblemAndCalculateCost(); + var newResiduals = residualVector(statistic); + + t.assign(currentParameters); //shift back + + var diff = new double[newResiduals.length]; + var jacobian = p.getJacobian(); - p.setHessian(regularisedHessian); //so this is the new Hessian + for(int i = 0 ; i < newResiduals.length; i++) { + diff[i] = ( newResiduals[i] - currentResiduals.get(i) ) / h; + + double add = 0; + for(int j = 0; j < numParams; j++) + add += jacobian.get(i, j)*dir.get(j); + + diff[i] -= add; + diff[i] *= 2.0/h; + } + + return new Vector(diff); + } + + /* + * Additive damping strategy, where the scaling matrix is simply the identity matrix. + */ + + private SquareMatrix levenbergDamping(SquareMatrix hessian) { + return Matrices.createIdentityMatrix(hessian.getData().length); + } + + /* + * Multiplicative damping strategy, where the scaling matrix is equal to the 'hessian' block-diagonal matrix. + * Works best for badly scaled problems. However, this is also scale-invariant, + * which mean it increases the susceptibility to parameter evaporation. + */ + + private SquareMatrix marquardtDamping(SquareMatrix hessian) { + return hessian.blockDiagonal(); + } + + @Override + public List listedTypes() { + var list = super.listedTypes(); + list.add(def(DAMPING_RATIO)); + return list; } /** @@ -188,6 +297,38 @@ public static LMOptimiser getInstance() { public String toString() { return Messages.getString("LMOptimiser.Descriptor"); } + + /** + * The Levenberg-Marquardt optimiser will only accept ordinary least-squares as + * its objective function. Therefore, {@code os} should be an instance of + * {@code SumOfSquares}. + * + * @return {@code true} if {@code.getClass()} returns + * {@code SumOfSquares.class}, {@code false} otherwise + */ + + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return os.getClass().equals(SumOfSquares.class); + } + + public NumericProperty getDampingRatio() { + return derive(DAMPING_RATIO, dampingRatio); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if(type == DAMPING_RATIO) + setDampingRatio(property); + } + + public void setDampingRatio(NumericProperty dampingRatio) { + requireType(dampingRatio, DAMPING_RATIO); + this.dampingRatio = (double)dampingRatio.getValue(); + firePropertyChanged(this, dampingRatio); + } + /* * Path @@ -195,6 +336,8 @@ public String toString() { class LMPath extends ComplexPath { + private IndexedVector parameters; + private Vector residualVector; private RectangularMatrix jacobian; private SquareMatrix nonregularisedHessian; private double lambda; @@ -226,6 +369,7 @@ public void configure(SearchTask t) { this.setHessian(null); nonregularisedHessian = null; this.lambda = 1.0; + this.residualVector = null; } public SquareMatrix getNonregularisedHessian() { @@ -236,18 +380,23 @@ public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { this.nonregularisedHessian = nonregularisedHessian; } + public Vector getResidualVector() { + return residualVector; + } + + public void setResidualVector(Vector residualVector) { + this.residualVector = residualVector; + } + + public IndexedVector getParameters() { + return parameters; + } + + public void setParameters(IndexedVector parameters) { + this.parameters = parameters; + } + } - - /** - * The Levenberg-Marquardt optimiser will only accept ordinary least-squares - * as its objective function. Therefore, {@code os} should be an instance of - * {@code SumOfSquares}. - * @return {@code true} if {@code.getClass()} returns {@code SumOfSquares.class}, {@code false} otherwise - */ - - @Override - public boolean compatibleWith(OptimiserStatistic os) { - return os.getClass().equals(SumOfSquares.class); - } + } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index a267d305..72a4bf9a 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -3,6 +3,7 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperties.isDiscrete; +import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.ERROR_TOLERANCE; import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import static pulse.properties.NumericPropertyKeyword.ITERATION_LIMIT; @@ -46,7 +47,9 @@ public abstract class PathOptimiser extends PropertyHolder implements Reflexive private int maxIterations; private double errorTolerance; + private double gradientResolution; + private double gradientStep; private static PathOptimiser instance; @@ -165,17 +168,34 @@ public Vector gradient(SearchTask task) throws SolverException { return grad; } + + /** + * Checks whether a discrete property is being optimised and selects the gradient step + * best suited to the optimisation strategy. Should be called before creating the optimisation path. + * @param task the search task defining the search vector + */ + + public void configure(SearchTask task) { + var params = task.searchVector()[0]; + boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + } public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); } public void setErrorTolerance(NumericProperty errorTolerance) { + requireType(errorTolerance, ERROR_TOLERANCE); this.errorTolerance = (double) errorTolerance.getValue(); + firePropertyChanged(this, errorTolerance); } public void setGradientResolution(NumericProperty resolution) { + requireType(resolution, GRADIENT_RESOLUTION); this.gradientResolution = (double) resolution.getValue(); + firePropertyChanged(this, resolution); } public NumericProperty getGradientResolution() { @@ -187,7 +207,9 @@ public NumericProperty getMaxIterations() { } public void setMaxIterations(NumericProperty maxIterations) { + requireType(maxIterations, ITERATION_LIMIT); this.maxIterations = (int) maxIterations.getValue(); + firePropertyChanged(this, maxIterations); } @Override @@ -324,4 +346,8 @@ public boolean compatibleWith(OptimiserStatistic os) { return true; } + public double getGradientStep() { + return gradientStep; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index 37c2a74c..9266f742 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -89,18 +89,5 @@ public String toString() { public static SR1Optimiser getInstance() { return instance; } - - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ - - @Override - public Path createPath(SearchTask t) { - return new ComplexPath(t); - } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index d02d7890..7378d339 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -68,6 +68,7 @@ public static SteepestDescentOptimiser getInstance() { @Override public Path createPath(SearchTask t) { + this.configure(t); return new Path(t); } diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index fc087140..0848ea60 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -72,13 +72,13 @@ public double linearStep(SearchTask task) throws SolverException { final Vector direction = p.getDirection(); final Vector g1 = p.getGradient(); - final double G1P = g1.dot(direction); - final double G1P_ABS = abs(G1P); + final double G1P = g1.dot(direction); + final double G1P_ABS = abs(G1P); - var params = task.searchVector(); - Segment segment = domain(params[0], params[1], direction); + var params = task.searchVector(); + Segment segment = domain(params[0], params[1], direction); - double ss1 = task.solveProblemAndCalculateCost(); + double cost1 = task.solveProblemAndCalculateCost(); double randomConfinedValue = 0; double g2p; @@ -92,14 +92,14 @@ public double linearStep(SearchTask task) throws SolverException { final var newParams = params[0].sum(direction.multiply(randomConfinedValue)); task.assign(new IndexedVector(newParams, params[0].getIndices())); - final double ss2 = task.solveProblemAndCalculateCost(); + final double cost2 = task.solveProblemAndCalculateCost(); /** * Checks if the first Armijo inequality is not satisfied. In this case, it will * set the maximum of the search domain to the {@code randomConfinedValue}. */ - if (ss2 - ss1 > C1 * randomConfinedValue * G1P) { + if (cost2 - cost1 > C1 * randomConfinedValue * G1P) { segment.setMaximum(randomConfinedValue); continue; } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index d250f567..b06c4d8f 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -41,7 +41,6 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.direction.Path; -import pulse.search.direction.PathOptimiser; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -257,6 +256,8 @@ public void run() { var optimiser = getInstance(); path = optimiser.createPath(this); + optimiser.configure(this); + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); int bufferSize = (Integer) getSize().getValue(); buffer.init(); @@ -276,7 +277,7 @@ public void run() { e1.printStackTrace(); } - final int maxIterations = (int)PathOptimiser.getInstance().getMaxIterations().getValue(); + final int maxIterations = (int)getInstance().getMaxIterations().getValue(); outer: do { @@ -449,7 +450,7 @@ else if (pathSolver == null) s.setDetails(MISSING_OPTIMISER); else if (buffer == null) s.setDetails(MISSING_BUFFER); - else if (!PathOptimiser.getInstance().compatibleWith(current.getOptimiserStatistic()) ) + else if (!getInstance().compatibleWith(current.getOptimiserStatistic()) ) s.setDetails(INCOMPATIBLE_OPTIMISER); else s = READY; diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 1d5a90fe..58ddeb87 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,10 @@ + + Date: Mon, 23 Nov 2020 12:58:58 +0000 Subject: [PATCH 024/116] PSO - initial commit --- src/main/java/pulse/baseline/Baseline.java | 2 +- .../java/pulse/baseline/FlatBaseline.java | 19 +- .../java/pulse/baseline/LinearBaseline.java | 21 ++- .../pulse/baseline/SinusoidalBaseline.java | 31 ++-- src/main/java/pulse/input/Range.java | 26 +-- src/main/java/pulse/math/IndexedVector.java | 120 ------------- src/main/java/pulse/math/ParameterVector.java | 169 ++++++++++++++++++ src/main/java/pulse/math/linear/Vector.java | 31 ++++ .../pulse/math/transforms/AtanhTransform.java | 28 +++ .../transforms/BoundedParameterTransform.java | 21 +++ .../math/transforms/InvDiamTransform.java | 28 +++ .../math/transforms/InvLenSqTransform.java | 28 +++ .../math/transforms/InvLenTransform.java | 23 +++ .../transforms/StandardTransformations.java | 45 +++++ .../pulse/math/transforms/Transformable.java | 8 + .../statements/ClassicalProblem2D.java | 71 +++----- .../problem/statements/CoreShellProblem.java | 46 ++--- .../problem/statements/DiathermicMedium.java | 42 +++-- .../problem/statements/NonlinearProblem.java | 52 +++--- .../statements/ParticipatingMedium.java | 83 ++++----- .../statements/PenetrationProblem.java | 61 ++++--- .../pulse/problem/statements/Problem.java | 81 +++++---- src/main/java/pulse/search/Optimisable.java | 6 +- .../pulse/search/direction/BFGSOptimiser.java | 2 +- .../pulse/search/direction/ComplexPath.java | 2 +- .../direction/CompositePathOptimiser.java | 14 +- .../search/direction/DirectionSolver.java | 2 +- .../direction/GradientBasedOptimiser.java | 163 +++++++++++++++++ .../{Path.java => GradientGuidedPath.java} | 21 +-- .../direction/HessianDirectionSolver.java | 2 +- .../search/direction/IterativeState.java | 24 +++ .../pulse/search/direction/LMOptimiser.java | 108 +++-------- .../java/pulse/search/direction/LMPath.java | 71 ++++++++ .../pulse/search/direction/PathOptimiser.java | 130 ++------------ .../pulse/search/direction/SR1Optimiser.java | 2 +- .../direction/SteepestDescentOptimiser.java | 6 +- .../pulse/search/direction/pso/FIPSMover.java | 40 +++++ .../pulse/search/direction/pso/Mover.java | 7 + .../direction/pso/NeighbourhoodTopology.java | 7 + .../pulse/search/direction/pso/Particle.java | 84 +++++++++ .../search/direction/pso/ParticleState.java | 68 +++++++ .../direction/pso/ParticleSwarmOptimiser.java | 51 ++++++ .../direction/pso/StaticTopologies.java | 58 ++++++ .../search/direction/pso/SwarmState.java | 104 +++++++++++ .../search/linear/GoldenSectionOptimiser.java | 17 +- .../pulse/search/linear/LinearOptimiser.java | 30 ++-- .../pulse/search/linear/WolfeOptimiser.java | 17 +- .../statistics/RegularisedLeastSquares.java | 2 +- src/main/java/pulse/tasks/SearchTask.java | 27 ++- src/main/java/pulse/tasks/TaskManager.java | 2 + .../java/pulse/tasks/logs/DataLogEntry.java | 2 +- .../java/pulse/tasks/processing/Buffer.java | 18 +- .../tasks/processing/CorrelationBuffer.java | 8 +- 53 files changed, 1479 insertions(+), 652 deletions(-) delete mode 100644 src/main/java/pulse/math/IndexedVector.java create mode 100644 src/main/java/pulse/math/ParameterVector.java create mode 100644 src/main/java/pulse/math/transforms/AtanhTransform.java create mode 100644 src/main/java/pulse/math/transforms/BoundedParameterTransform.java create mode 100644 src/main/java/pulse/math/transforms/InvDiamTransform.java create mode 100644 src/main/java/pulse/math/transforms/InvLenSqTransform.java create mode 100644 src/main/java/pulse/math/transforms/InvLenTransform.java create mode 100644 src/main/java/pulse/math/transforms/StandardTransformations.java create mode 100644 src/main/java/pulse/math/transforms/Transformable.java create mode 100644 src/main/java/pulse/search/direction/GradientBasedOptimiser.java rename src/main/java/pulse/search/direction/{Path.java => GradientGuidedPath.java} (77%) create mode 100644 src/main/java/pulse/search/direction/IterativeState.java create mode 100644 src/main/java/pulse/search/direction/LMPath.java create mode 100644 src/main/java/pulse/search/direction/pso/FIPSMover.java create mode 100644 src/main/java/pulse/search/direction/pso/Mover.java create mode 100644 src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java create mode 100644 src/main/java/pulse/search/direction/pso/Particle.java create mode 100644 src/main/java/pulse/search/direction/pso/ParticleState.java create mode 100644 src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java create mode 100644 src/main/java/pulse/search/direction/pso/StaticTopologies.java create mode 100644 src/main/java/pulse/search/direction/pso/SwarmState.java diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index f9f69788..b24ccaf8 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -22,7 +22,7 @@ * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask - * @see pulse.math.IndexedVector + * @see pulse.math.ParameterVector */ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 73a2524f..5dd2bb57 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -9,7 +9,8 @@ import java.util.Arrays; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -122,12 +123,14 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { - for (int i = 0, size = output[0].dimension(); i < size; i++) { - - if (output[0].getIndex(i) == BASELINE_INTERCEPT) { - output[0].set(i, intercept); - output[1].set(i, 5); + public void optimisationVector(ParameterVector output, List flags) { + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == BASELINE_INTERCEPT) { + output.set(i, intercept); + output.setParameterBounds(i, new Segment(-10, 10)); } } @@ -135,7 +138,7 @@ public void optimisationVector(IndexedVector[] output, List flags) { } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { for (int i = 0, size = params.dimension(); i < size; i++) { if (params.getIndex(i) == BASELINE_INTERCEPT) diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index 2c9d6b46..35b90890 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -8,7 +8,8 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -26,7 +27,7 @@ * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask - * @see pulse.math.IndexedVector + * @see pulse.math.ParameterVector */ public class LinearBaseline extends FlatBaseline { @@ -134,14 +135,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - for (int i = 0, size = output[0].dimension(); i < size; i++) { + for (int i = 0, size = output.dimension(); i < size; i++) { - if (output[0].getIndex(i) == BASELINE_SLOPE) { - output[0].set(i, slope); - output[1].set(i, 1000); + var key = output.getIndex(i); + + if (key == BASELINE_SLOPE) { + output.set(i, slope); + output.setParameterBounds(i, new Segment(1E-4, 1E4)); } } @@ -159,12 +162,12 @@ public void optimisationVector(IndexedVector[] output, List flags) { */ @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { - if (params.getIndex(i) == BASELINE_SLOPE) + if (params.getIndex(i) == BASELINE_SLOPE) setSlope(derive(BASELINE_SLOPE, params.get(i))); } diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index 0db965e9..44911aba 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,7 +1,7 @@ package pulse.baseline; import static java.lang.Math.sin; -import static java.lang.Math.sqrt; +import static pulse.math.transforms.StandardTransformations.SQRT; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -11,7 +11,8 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -119,23 +120,26 @@ public void setPhaseShift(NumericProperty phaseShift) { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - for (int i = 0, size = output[0].dimension(); i < size; i++) { + for (int i = 0, size = output.dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + var key = output.getIndex(i); + + switch (key) { case BASELINE_FREQUENCY: - output[0].set(i, frequency); - output[1].set(i, 30); + output.set(i, frequency); + output.setParameterBounds(i, new Segment(0, 200)); break; case BASELINE_PHASE_SHIFT: - output[0].set(i, phaseShift); - output[1].set(i, 1.0); + output.set(i, phaseShift); + output.setParameterBounds(i, new Segment(-3.14, 3.14) ); break; case BASELINE_AMPLITUDE: - output[0].set(i, sqrt(amplitude)); - output[1].set(i, 1.0); + output.setTransform(i, SQRT); + output.set(i, amplitude); + output.setParameterBounds(i, new Segment( 0.0, 10.0 ) ); break; default: break; @@ -146,7 +150,7 @@ public void optimisationVector(IndexedVector[] output, List flags) { } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { @@ -159,8 +163,7 @@ public void assign(IndexedVector params) { setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.get(i))); break; case BASELINE_AMPLITUDE: - var p = params.get(i); - setAmplitude(derive(BASELINE_AMPLITUDE, p*p)); + setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i) )); break; default: break; diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 521ffeee..9a473a26 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -9,7 +9,7 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -168,23 +168,27 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { */ @Override - public void optimisationVector(IndexedVector[] output, List flags) { - int size = output[0].dimension(); + public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0; i < size; i++) { + double len = segment.length(); + var bounds = new Segment(-0.25 * len, 0.25 * len); + + for (int i = 0, size = output.dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + var key = output.getIndex(i); + + switch (key) { case UPPER_BOUND: - output[0].set(i, segment.getMaximum()); - output[1].set(i, 0.25 * segment.length()); + output.set(i, segment.getMaximum()); break; case LOWER_BOUND: - output[0].set(i, segment.getMinimum()); - output[1].set(i, 0.25 * segment.length()); + output.set(i, segment.getMinimum()); break; default: - continue; + continue; } + + output.setParameterBounds(i, bounds); } @@ -197,7 +201,7 @@ public void optimisationVector(IndexedVector[] output, List flags) { */ @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { NumericProperty p = null; diff --git a/src/main/java/pulse/math/IndexedVector.java b/src/main/java/pulse/math/IndexedVector.java deleted file mode 100644 index d96e0d6d..00000000 --- a/src/main/java/pulse/math/IndexedVector.java +++ /dev/null @@ -1,120 +0,0 @@ -package pulse.math; - -import java.util.ArrayList; -import java.util.List; - -import pulse.math.linear.Vector; -import pulse.properties.NumericPropertyKeyword; - -/** - * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific - * components of the vector. Used when constructing the optimisation vector. - */ - -public class IndexedVector extends Vector { - - private List indices; - - /** - * Constructs an {@code IndexedVector} with the specified list of keywords. - * - * @param indices a list of keywords - */ - - public IndexedVector(List indices) { - this(indices.size()); - assign(indices); - } - - /** - * Constructs an {@code IndexedVector} based on {@code v} and a list of keyword - * {@code indices} - * - * @param v the vector to be copied - * @param indices a list of keyword - */ - - public IndexedVector(Vector v, List indices) { - super(v); - this.indices = indices; - } - - private IndexedVector(final int n) { - super(n); - indices = new ArrayList<>(n); - } - - /** - * Finds the component of this vector that corresponds to {@code index} and sets - * its value to {@code x} - * - * @param index the keyword associated with a component of this - * {@code IndexedVector} - * @param x the new value of this component - */ - - public void set(NumericPropertyKeyword index, final double x) { - final int i = indexOf(index); - super.set(i, x); - } - - /** - * Retrieves the keyword associated with the {@code dataIndex} - * - * @param dataIndex an index pointing to a component of this vector - * @return a keyword describing this component - */ - - public NumericPropertyKeyword getIndex(final int dataIndex) { - return indices.get(dataIndex); - } - - /** - * Gets the data index that corresponds to the keyword {@code index} - * - * @param index a keyword-index of the component - * @return a numeric index associated with the original {@code Vector} - */ - - private int indexOf(NumericPropertyKeyword index) { - return indices.indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - - public double get(NumericPropertyKeyword index) { - return super.get(indexOf(index)); - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - - public List getIndices() { - return indices; - } - - private void assign(List indices) { - this.indices.addAll(indices); - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("Indices: "); - for(var key : indices) { - sb.append(key + " ; "); - } - sb.append(System.lineSeparator()); - sb.append(" Values: " + super.toString()); - return sb.toString(); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java new file mode 100644 index 00000000..d40350a4 --- /dev/null +++ b/src/main/java/pulse/math/ParameterVector.java @@ -0,0 +1,169 @@ +package pulse.math; + +import java.util.Arrays; +import java.util.List; + +import pulse.math.linear.Vector; +import pulse.math.transforms.Transformable; +import pulse.properties.NumericPropertyKeyword; + +/** + * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific + * components of the vector. Used when constructing the optimisation vector. + */ + +public class ParameterVector extends Vector { + + private NumericPropertyKeyword[] indices; + private Transformable[] transforms; + private Segment[] bounds; + + /** + * Constructs an {@code IndexedVector} with the specified list of keywords. + * + * @param indices a list of keywords + */ + + public ParameterVector(List indices) { + this(indices.size()); + assign(indices); + } + + /** + * Constructs an {@code IndexedVector} based on {@code v} and a list of keyword + * {@code indices} + * + * @param v the vector to be copied + * @param prototype the prototype of the parameter vector + */ + + public ParameterVector(ParameterVector proto, Vector v) { + super(v); + this.indices = new NumericPropertyKeyword[proto.indices.length]; + System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); + this.bounds = new Segment[proto.bounds.length]; + System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); + this.transforms = new Transformable[proto.transforms.length]; + System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); + + } + + public ParameterVector(ParameterVector v) { + this( v.dimension() ); + final int n = indices.length; + System.arraycopy(v.indices, 0, indices, 0, n); + System.arraycopy(v.transforms, 0, transforms, 0, n); + System.arraycopy(v.bounds, 0, bounds, 0, n); + } + + private ParameterVector(final int n) { + super(n); + indices = new NumericPropertyKeyword[n]; + transforms = new Transformable[n]; + bounds = new Segment[n]; + } + + /** + * Applies the corresponding transformation (defined by the respective {@code Transformable}) -- if present, + * and sets the result of this transformation to the ith component of this {@code ParameterVector}. + */ + + @Override + public void set(final int i, final double x) { + final double t = transforms[i] == null ? x : transforms[i].transform(x); + super.set(i, t); + } + + /** + * Retrieves the keyword associated with the {@code dataIndex} + * + * @param dataIndex an index pointing to a component of this vector + * @return a keyword describing this component + */ + + public NumericPropertyKeyword getIndex(final int dataIndex) { + return indices[dataIndex]; + } + + /** + * Gets the data index that corresponds to the keyword {@code index} + * + * @param index a keyword-index of the component + * @return a numeric index associated with the original {@code Vector} + */ + + private int indexOf(NumericPropertyKeyword index) { + return getIndices().indexOf(index); + } + + /** + * Gets the component at this {@code index} + * + * @param index a keyword-index of a component + * @return the respective component + */ + + public double getParameterValue(NumericPropertyKeyword index) { + return super.get(indexOf(index)); + } + + public double inverseTransform(final int i) { + return transforms[i].inverse( get(i) ); + } + + public Transformable getTransform(final int i) { + return transforms[i]; + } + + public void setTransform(final int i, Transformable transformable) { + transforms[i] = transformable; + } + + public Segment getParameterBounds(final int i) { + return bounds[i]; + } + + public Segment getTransformedBounds(final int i) { + return transforms[i] != null ? + new Segment( transforms[i].transform( bounds[i].getMinimum() ), + transforms[i].transform( bounds[i].getMaximum() ) ) : + getParameterBounds(i); + } + + public void setParameterBounds(int i, Segment segment) { + bounds[i] = segment; + } + + /** + * Gets the full list of indices recognised by this {@code IndexedVector}. + * + * @return the full list of {@code NumericPropertyKeyword} indices. + */ + + public List getIndices() { + return Arrays.asList(indices); + } + + private void assign(List indices) { + this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); + bounds = new Segment[this.indices.length]; + transforms = new Transformable[this.indices.length]; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Indices: "); + for(var key : indices) { + sb.append(key + " ; "); + } + sb.append(System.lineSeparator()); + sb.append(" Values: " + super.toString()); + return sb.toString(); + } + + public Segment[] getBounds() { + return bounds; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 909bf322..41101ac5 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -112,6 +112,37 @@ public Vector multiply(double f) { return factor; } + + /** + * Creates a vector with random coordinates confined within [min;max] + * @param n the vector dimension + * @param min upper bound for the random number generator + * @param max lower bound for the random generator generator + * @return the randomised vector + */ + + public static Vector random(int n, double min, double max) { + var v = new Vector(n); + for(int i = 0; i < n; i++) { + v.x[i] = min + Math.random()*(max - min); + } + return v; + } + + /** + * Component-wise vector multiplication + */ + + public Vector multComponents(Vector v) { + Vector nv = new Vector(this); + + for(int i = 0; i < x.length; i++) { + nv.x[i] *= v.x[i]; + } + + return nv; + + } /** * Calculates the scalar product of {@code this} and {@code v}. diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java new file mode 100644 index 00000000..d268735a --- /dev/null +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -0,0 +1,28 @@ +package pulse.math.transforms; + +import static java.lang.Math.tanh; +import static pulse.math.MathUtils.atanh; + +import pulse.math.Segment; + +/** + * Hyper-tangent parameter transform. + */ + +public class AtanhTransform extends BoundedParameterTransform { + + public AtanhTransform(Segment bounds) { + super(bounds); + } + + @Override + public double transform(double a) { + return atanh(2.0 * a / getBounds().getMaximum() - 1.0); + } + + @Override + public double inverse(double t) { + return 0.5 * getBounds().getMaximum() * (tanh(t) + 1.0); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java new file mode 100644 index 00000000..066eb5b8 --- /dev/null +++ b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java @@ -0,0 +1,21 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +public abstract class BoundedParameterTransform implements Transformable { + + private Segment bounds; + + public BoundedParameterTransform(Segment bounds) { + setBounds(bounds); + } + + public Segment getBounds() { + return bounds; + } + + public void setBounds(Segment bounds) { + this.bounds = bounds; + } + +} diff --git a/src/main/java/pulse/math/transforms/InvDiamTransform.java b/src/main/java/pulse/math/transforms/InvDiamTransform.java new file mode 100644 index 00000000..6afdb00a --- /dev/null +++ b/src/main/java/pulse/math/transforms/InvDiamTransform.java @@ -0,0 +1,28 @@ +package pulse.math.transforms; + +import pulse.problem.statements.model.ExtendedThermalProperties; + +/** + * A transform that simply divides the value by the squared length of the + * sample. + */ + +public class InvDiamTransform implements Transformable { + + private double d; + + public InvDiamTransform(ExtendedThermalProperties etp) { + d = (double) etp.getSampleDiameter().getValue(); + } + + @Override + public double transform(double value) { + return value / d; + } + + @Override + public double inverse(double t) { + return t * d; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java new file mode 100644 index 00000000..5fa4ac8d --- /dev/null +++ b/src/main/java/pulse/math/transforms/InvLenSqTransform.java @@ -0,0 +1,28 @@ +package pulse.math.transforms; + +import pulse.problem.statements.model.ThermalProperties; + +/** + * A transform that simply divides the value by the squared length of the + * sample. + */ + +public class InvLenSqTransform implements Transformable { + + private double l; + + public InvLenSqTransform(ThermalProperties tp) { + this.l = (double) tp.getSampleThickness().getValue(); + } + + @Override + public double transform(double value) { + return value / (l * l); + } + + @Override + public double inverse(double t) { + return t * (l * l); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java new file mode 100644 index 00000000..322c7849 --- /dev/null +++ b/src/main/java/pulse/math/transforms/InvLenTransform.java @@ -0,0 +1,23 @@ +package pulse.math.transforms; + +import pulse.problem.statements.model.ThermalProperties; + +public class InvLenTransform implements Transformable { + + private double l; + + public InvLenTransform(ThermalProperties tp) { + l = (double) tp.getSampleThickness().getValue(); + } + + @Override + public double transform(double value) { + return value / l; + } + + @Override + public double inverse(double t) { + return t * l; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java new file mode 100644 index 00000000..d8e9b6ed --- /dev/null +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -0,0 +1,45 @@ +package pulse.math.transforms; + +import static java.lang.Math.exp; +import static java.lang.Math.log; +import static java.lang.Math.sqrt; + +public class StandardTransformations { + + /** + * Logarithmic parameter transform. The parameter space is only bounded by positive numbers, so no bounding segment required. + */ + + public final static Transformable LOG = new Transformable() { + + @Override + public double transform(double a) { + return log(a); + } + + @Override + public double inverse(double t) { + return exp(t); + } + + }; + + public final static Transformable SQRT = new Transformable() { + + @Override + public double transform(double a) { + return sqrt(a); + } + + @Override + public double inverse(double t) { + return t*t; + } + + }; + + private StandardTransformations() { + //empty + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/Transformable.java b/src/main/java/pulse/math/transforms/Transformable.java new file mode 100644 index 00000000..032f1b3d --- /dev/null +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -0,0 +1,8 @@ +package pulse.math.transforms; + +public interface Transformable { + + public double transform(double value); + public double inverse(double t); + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 3a315962..f2e35af5 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -1,16 +1,13 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.InvDiamTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.laser.DiscretePulse2D; import pulse.problem.schemes.ADIScheme; @@ -20,7 +17,6 @@ import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; -import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; /** @@ -34,19 +30,19 @@ public class ClassicalProblem2D extends Problem { public ClassicalProblem2D() { super(); - setPulse( new Pulse2D() ); + setPulse(new Pulse2D()); setComplexity(ProblemComplexity.MODERATE); } - + public ClassicalProblem2D(Problem p) { super(p); - setPulse( new Pulse2D(p.getPulse()) ); + setPulse(new Pulse2D(p.getPulse())); setComplexity(ProblemComplexity.MODERATE); } @Override public void initProperties() { - setProperties( new ExtendedThermalProperties() ); + setProperties(new ExtendedThermalProperties()); } @Override @@ -70,66 +66,57 @@ public DiscretePulse discretePulseOn(Grid grid) { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (ExtendedThermalProperties) getProperties(); - double value; - final double d = (double)properties.getSampleDiameter().getValue(); - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { case FOV_OUTER: - value = (double)properties.getFOVOuter().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); + value = (double) properties.getFOVOuter().getValue(); break; case FOV_INNER: - value = (double)properties.getFOVInner().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); + value = (double) properties.getFOVInner().getValue(); break; - case SPOT_DIAMETER : + case SPOT_DIAMETER: value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); break; case HEAT_LOSS_SIDE: - final double Bi3 = (double)properties.getSideLosses().getValue(); - output[0].set(i, - properties.areThermalPropertiesLoaded() ? atanh(2.0 * Bi3 / properties.maxBiot() - 1.0) : log(Bi3)); - output[1].set(i, 2.0); - break; + final double Bi = (double) properties.getSideLosses().getValue(); + setHeatLossParameter(output, i, Bi); + continue; default: continue; } + + output.setTransform(i, new InvDiamTransform(properties)); + output.set(i, value); + output.setParameterBounds(i, new Segment(0.5 * value, 1.5 * value)); + } } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); var properties = (ExtendedThermalProperties) getProperties(); - NumericPropertyKeyword type; - - final double d = (double)properties.getSampleDiameter().getValue(); // TODO one-to-one mapping for FOV and SPOT_DIAMETER for (int i = 0, size = params.dimension(); i < size; i++) { - type = params.getIndex(i); + var type = params.getIndex(i); switch (type) { case FOV_OUTER: case FOV_INNER: - properties.set(type, derive(type, params.get(i) * d)); + case HEAT_LOSS_SIDE: + properties.set(type, derive(type, params.inverseTransform(i) )); break; case SPOT_DIAMETER: - var spotDiameter = derive(SPOT_DIAMETER, params.get(i) * d); - ((Pulse2D) getPulse()).setSpotDiameter(spotDiameter); - break; - case HEAT_LOSS_SIDE: - final double bi = properties.areThermalPropertiesLoaded() ? 0.5 * properties.maxBiot() * (tanh(params.get(i)) + 1.0) : exp(params.get(i)); - properties.setSideLosses( derive(HEAT_LOSS_SIDE, bi) ); + ((Pulse2D) getPulse()).setSpotDiameter( derive(SPOT_DIAMETER, params.inverseTransform(i) )); break; default: continue; diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java index 5e470e20..85335c7c 100644 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ b/src/main/java/pulse/problem/statements/CoreShellProblem.java @@ -1,6 +1,5 @@ package pulse.problem.statements; -import static java.lang.Math.pow; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.AXIAL_COATING_THICKNESS; @@ -10,7 +9,11 @@ import java.util.ArrayList; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.InvDiamTransform; +import pulse.math.transforms.InvLenSqTransform; +import pulse.math.transforms.InvLenTransform; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -104,24 +107,29 @@ public boolean isEnabled() { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + var bounds = new Segment(0.1, 1.0); + var properties = (ExtendedThermalProperties) this.getProperties(); + + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + switch (key) { case AXIAL_COATING_THICKNESS: - output[0].set(i, tA / (double)getProperties().getSampleThickness().getValue()); - output[1].set(i, 0.01); + output.setTransform(i, new InvLenTransform(properties) ); + output.set(i, tA); + output.setParameterBounds(i, bounds ); break; case RADIAL_COATING_THICKNESS: - final double d = (double)((ExtendedThermalProperties)getProperties()).getSampleDiameter().getValue(); - output[0].set(i, 2.0 * tR / d); - output[1].set(i, 0.01); + output.setTransform(i, new InvDiamTransform(properties) ); + output.set(i, tR); + output.setParameterBounds(i, bounds ); break; case COATING_DIFFUSIVITY: - double value = coatingDiffusivity / pow(tA + 2.0 * tR, -2); - output[0].set(i, value); - output[1].set(i, 0.75 * value); + output.setTransform(i, new InvLenSqTransform(properties) ); + output.set(i, coatingDiffusivity); + output.setParameterBounds( i, new Segment(0.5 * coatingDiffusivity, 1.5 * coatingDiffusivity) ); break; default: continue; @@ -131,21 +139,19 @@ public void optimisationVector(IndexedVector[] output, List flags) { } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); - + for (int i = 0, size = params.dimension(); i < size; i++) { switch (params.getIndex(i)) { case AXIAL_COATING_THICKNESS: - final double l = (double)((ExtendedThermalProperties)getProperties()).getSampleThickness().getValue(); - tA = params.get(i) * l; + tA = params.inverseTransform(i); break; case RADIAL_COATING_THICKNESS: - final double d = (double)((ExtendedThermalProperties)getProperties()).getSampleDiameter().getValue(); - tR = params.get(i) / (d / 2.0); + tR = params.inverseTransform(i); break; case COATING_DIFFUSIVITY: - coatingDiffusivity = params.get(i) * pow(tA + 2.0 * tR, 2); + coatingDiffusivity = params.inverseTransform(i); break; default: continue; diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 2f31ba73..f2516f9c 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -1,14 +1,14 @@ package pulse.problem.statements; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.AtanhTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitDiathermicSolver; import pulse.problem.statements.model.DiathermicProperties; @@ -44,41 +44,54 @@ public DiathermicMedium() { public DiathermicMedium(Problem p) { super(p); } - + @Override public void initProperties() { setProperties(new DiathermicProperties()); } - + @Override public void initProperties(ThermalProperties properties) { setProperties(new DiathermicProperties(properties)); } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = output[0].dimension(); i < size; i++) { - if (output[0].getIndex(i) == DIATHERMIC_COEFFICIENT) { + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == DIATHERMIC_COEFFICIENT) { + + var bounds = new Segment(1E-3, 1.0); final double etta = (double) properties.getDiathermicCoefficient().getValue(); - output[0].set(i, atanh(2.0 * etta - 1.0)); - output[1].set(i, 10.0); + + output.setTransform(i, new AtanhTransform(bounds)); + output.set(i, etta); + output.setParameterBounds(i, bounds); + } + } } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); var properties = (DiathermicProperties) this.getProperties(); for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { + + var key = params.getIndex(i); + + switch (key) { + case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, 0.5 * (tanh(params.get(i)) + 1.0))); + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); break; case HEAT_LOSS: if (properties.areThermalPropertiesLoaded()) { @@ -90,8 +103,11 @@ public void assign(IndexedVector params) { break; default: continue; + } + } + } @Override diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 95a28fa9..5dc7fec7 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,7 +1,5 @@ package pulse.problem.statements; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; @@ -14,7 +12,9 @@ import java.util.List; import pulse.input.ExperimentalData; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.AtanhTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.properties.Flag; @@ -26,15 +26,15 @@ public class NonlinearProblem extends ClassicalProblem { public NonlinearProblem() { super(); - setPulse( new Pulse2D() ); + setPulse(new Pulse2D()); setComplexity(ProblemComplexity.MODERATE); } - + public NonlinearProblem(NonlinearProblem p) { super(p); - setPulse( new Pulse2D((Pulse2D)p.getPulse()) ); + setPulse(new Pulse2D((Pulse2D) p.getPulse())); } - + @Override public boolean isReady() { return getProperties().areThermalPropertiesLoaded(); @@ -64,20 +64,27 @@ public String toString() { public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, getProperties().thermalConductivity()); } - + @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - int size = output[0].dimension(); + int size = output.dimension(); + var properties = getProperties(); for (int i = 0; i < size; i++) { - if (output[0].getIndex(i) == HEAT_LOSS) { - var properties = getProperties(); - final double Bi1 = (double)properties.getHeatLoss().getValue(); - output[0].set(i, atanh(2.0 * Bi1 / properties.maxBiot() - 1.0)); - output[1].set(i, 10.0); + var key = output.getIndex(i); + + if (key == HEAT_LOSS) { + + var bounds = new Segment(1e-5, properties.maxBiot()); + final double Bi1 = (double) properties.getHeatLoss().getValue(); + output.setTransform(i, new AtanhTransform(bounds)); + output.set(i, Bi1); + output.setParameterBounds(i, bounds); + } + } } @@ -93,16 +100,19 @@ public void optimisationVector(IndexedVector[] output, List flags) { */ @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); var p = getProperties(); - + for (int i = 0, size = params.dimension(); i < size; i++) { - if (params.getIndex(i) == HEAT_LOSS) { - final double heatLoss = 0.5 * p.maxBiot() * (tanh(params.get(i)) + 1.0); - p.setHeatLoss(derive(HEAT_LOSS, heatLoss)); + var key = params.getIndex(i); + + if (key == HEAT_LOSS) { + + p.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); p.emissivity(); + } } @@ -113,7 +123,7 @@ public void assign(IndexedVector params) { public Class defaultScheme() { return ImplicitScheme.class; } - + @Override public Problem copy() { return new NonlinearProblem(this); diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 31217f22..e35947ca 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,21 +1,20 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; +import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.AtanhTransform; +import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; -import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { @@ -39,72 +38,76 @@ public String toString() { } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (ThermoOpticalProperties)getProperties(); - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + Segment bounds; + double value = 0; + Transformable transform; + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { case PLANCK_NUMBER: - final double planckNumber = (double)properties.getPlanckNumber().getValue(); - output[0].set(i, atanh(2.0 * planckNumber / properties.maxNp() - 1.0)); - output[1].set(i, 1.0); + bounds = new Segment(1E-5, properties.maxNp() ); + value = (double)properties.getPlanckNumber().getValue(); + transform = new AtanhTransform(bounds); break; case OPTICAL_THICKNESS: - final double opticalThickness = (double)properties.getOpticalThickness().getValue(); - output[0].set(i, log(opticalThickness)); - output[1].set(i, 1.0); + value = (double)properties.getOpticalThickness().getValue(); + bounds = new Segment(1E-8, 1E5); + transform = LOG; break; case SCATTERING_ALBEDO: - final double scatteringAlbedo = (double)properties.getScatteringAlbedo().getValue(); - output[0].set(i, atanh(2.0 * scatteringAlbedo - 1.0)); - output[1].set(i, 1.0); + value = (double)properties.getScatteringAlbedo().getValue(); + bounds = new Segment(1e-5, 0.999); + transform = new AtanhTransform(bounds); break; case SCATTERING_ANISOTROPY: - final double scatteringAnisotropy = (double)properties.getScatteringAnisostropy().getValue(); - output[0].set(i, atanh(scatteringAnisotropy)); - output[1].set(i, 1.0); + value = (double)properties.getScatteringAnisostropy().getValue(); + bounds = new Segment(-0.999, 0.999); + transform = new AtanhTransform(bounds); break; default: continue; + } + + output.setTransform(i, transform); + output.set(i, value); + output.setParameterBounds(i, bounds); + } } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); var properties = (ThermoOpticalProperties)getProperties(); - NumericPropertyKeyword type; - for (int i = 0, size = params.dimension(); i < size; i++) { - type = params.getIndex(i); + + var type = params.getIndex(i); + switch (type) { - + case PLANCK_NUMBER: - var nP = derive(type, 0.5 * properties.maxNp() * (tanh(params.get(i)) + 1.0)); - properties.setPlanckNumber(nP); - break; + case SCATTERING_ALBEDO : + case SCATTERING_ANISOTROPY : case OPTICAL_THICKNESS: - var tau0 = derive(type, exp(params.get(i))); - properties.setOpticalThickness(tau0); - break; - case SCATTERING_ALBEDO: - var omega0 = derive(type, 0.5 * (tanh(params.get(i)) + 1.0)); - properties.setScatteringAlbedo(omega0); - break; - case SCATTERING_ANISOTROPY: - var anisotropy = derive(type, tanh(params.get(i))); - properties.setScatteringAnisotropy(anisotropy); + properties.set( type, derive( type, params.inverseTransform(i) ) ); break; case HEAT_LOSS: case DIFFUSIVITY: - getProperties().emissivity(); + properties.emissivity(); break; default: break; + } } diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index efbd8ea3..d2e96338 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -1,15 +1,13 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; +import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.LASER_ABSORPTIVITY; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; -import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; import pulse.problem.statements.model.AbsorptionModel; @@ -37,7 +35,7 @@ public PenetrationProblem() { instanceDescriptor.addListener(() -> initAbsorption()); absorption.setParent(this); } - + public PenetrationProblem(PenetrationProblem p) { super(p); initAbsorption(); @@ -68,42 +66,53 @@ public static InstanceDescriptor getAbsorptionSelecto } @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + double value = 0; + + switch(key) { case LASER_ABSORPTIVITY: - output[0].set(i, log((double) (absorption.getLaserAbsorptivity()).getValue())); - output[1].set(i, 2.0); + value = (double) (absorption.getLaserAbsorptivity()).getValue(); break; - case THERMAL_ABSORPTIVITY: - output[0].set(i, log((double) (absorption.getThermalAbsorptivity()).getValue())); - output[0].set(i, 2.0); + case THERMAL_ABSORPTIVITY : + value = (double) (absorption.getThermalAbsorptivity()).getValue(); break; - default: + default : continue; } + + //do this for the listed key values + output.setTransform(i, LOG); + output.set(i, value); + output.setParameterBounds(i, new Segment(1E-2, 1000.0)); + } } @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { super.assign(params); + double value; + for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case LASER_ABSORPTIVITY: - absorption.setLaserAbsorptivity(derive(LASER_ABSORPTIVITY, exp(params.get(i)))); + var key = params.getIndex(i); + + switch(key) { + case LASER_ABSORPTIVITY : + case THERMAL_ABSORPTIVITY : + value = params.inverseTransform(i); break; - case THERMAL_ABSORPTIVITY: - absorption.setThermalAbsorptivity( - derive(THERMAL_ABSORPTIVITY, exp(params.get(i)))); - break; - default: + default : continue; } + + absorption.set(key, derive(key, value) ); + } } @@ -111,12 +120,12 @@ public void assign(IndexedVector params) { public Class defaultScheme() { return ImplicitTranslucentSolver.class; } - + @Override public String toString() { return Messages.getString("DistributedProblem.Descriptor"); } - + @Override public Problem copy() { return new PenetrationProblem(this); diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 9efd0fca..b0d94043 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,10 +1,7 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; import static pulse.input.listeners.CurveEventType.RESCALED; -import static pulse.math.MathUtils.atanh; +import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -20,7 +17,10 @@ import pulse.baseline.Baseline; import pulse.baseline.FlatBaseline; import pulse.input.ExperimentalData; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.AtanhTransform; +import pulse.math.transforms.InvLenSqTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; @@ -74,7 +74,7 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim protected Problem() { initProperties(); - setHeatingCurve( new HeatingCurve() ); + setHeatingCurve(new HeatingCurve()); instanceDescriptor.attemptUpdate(FlatBaseline.class.getSimpleName()); addListeners(); @@ -91,21 +91,21 @@ protected Problem() { public Problem(Problem p) { initProperties(p.getProperties().copy()); - setHeatingCurve( new HeatingCurve(p.getHeatingCurve()) ); + setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); curve.setNumPoints(p.getHeatingCurve().getNumPoints()); instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); addListeners(); this.baseline = p.getBaseline().copy(); } - + public abstract Problem copy(); public void setHeatingCurve(HeatingCurve curve) { this.curve = curve; curve.setParent(this); } - + private void addListeners() { instanceDescriptor.addListener(() -> { initBaseline(); @@ -222,41 +222,57 @@ public void estimateSignalRange(ExperimentalData c) { */ @Override - public void optimisationVector(IndexedVector[] output, List flags) { + public void optimisationVector(ParameterVector output, List flags) { baseline.optimisationVector(output, flags); - for (int i = 0, size = output[0].dimension(); i < size; i++) { + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); - switch (output[0].getIndex(i)) { + switch (key) { case DIFFUSIVITY: - final double l = (double) properties.getSampleThickness().getValue(); final double a = (double) properties.getDiffusivity().getValue(); - final double prefactor = 1.0 / (l * l); - output[0].set(i, a * prefactor); - output[1].set(i, 0.45 * a * prefactor); + output.setTransform(i, new InvLenSqTransform(properties)); + output.setParameterBounds(i, new Segment(0.33 * a, 3.0 * a)); + output.set(i, a); break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output[0].set(i, signalHeight); - output[1].set(i, 0.5 * signalHeight); + output.set(i, signalHeight); + output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); break; case HEAT_LOSS: final double Bi = (double) properties.getHeatLoss().getValue(); - output[0].set(i, properties.areThermalPropertiesLoaded() ? atanh(2.0 * Bi / properties.maxBiot() - 1.0) - : log(Bi)); - output[1].set(i, 2.0); + setHeatLossParameter(output, i, Bi); break; case TIME_SHIFT: - output[0].set(i, (double) curve.getTimeShift().getValue()); - output[1].set(i, 0.025 * properties.timeFactor()); + output.set(i, (double) curve.getTimeShift().getValue()); + double magnitude = 0.25 * properties.timeFactor(); + output.setParameterBounds(i, new Segment(-magnitude, magnitude)); break; default: continue; } + } } + + protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { + Segment bounds; + if (properties.areThermalPropertiesLoaded()) { + bounds = new Segment(1e-5, properties.maxBiot()); + output.setTransform(i, new AtanhTransform(bounds) ); + } + else { + bounds = new Segment(1E-5, 2.0); + output.setTransform(i, LOG); + } + output.setParameterBounds(i, bounds); + output.setTransform(i, properties.areThermalPropertiesLoaded() ? new AtanhTransform(bounds) : LOG); + output.set(i, Bi); + } /** * Assigns parameter values of this {@code Problem} using the optimisation @@ -267,26 +283,25 @@ public void optimisationVector(IndexedVector[] output, List flags) { */ @Override - public void assign(IndexedVector params) { + public void assign(ParameterVector params) { baseline.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { + double value = params.get(i); + var key = params.getIndex(i); + + switch (key) { case DIFFUSIVITY: - final double l = (double) properties.getSampleThickness().getValue(); - properties.setDiffusivity(derive(DIFFUSIVITY, params.get(i) * (l * l))); + properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i) ) ); break; case MAXTEMP: - properties.setMaximumTemperature(derive(MAXTEMP, params.get(i))); + properties.setMaximumTemperature( derive(MAXTEMP, value) ); break; case HEAT_LOSS: - final double bi = properties.areThermalPropertiesLoaded() - ? 0.5 * properties.maxBiot() * (tanh(params.get(i)) + 1.0) - : exp(params.get(i)); - properties.setHeatLoss(derive(HEAT_LOSS, bi)); + properties.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i) ) ); break; case TIME_SHIFT: - curve.set(TIME_SHIFT, derive(TIME_SHIFT, params.get(i))); + curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); break; default: continue; diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 946afe49..7909db12 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -2,7 +2,7 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.properties.Flag; /** @@ -23,7 +23,7 @@ public interface Optimisable { * @see pulse.util.PropertyHolder.listedTypes() */ - public void assign(IndexedVector params); + public void assign(ParameterVector params); /** * Calculates the vector argument defined on Rn @@ -34,6 +34,6 @@ public interface Optimisable { * the search */ - public void optimisationVector(IndexedVector[] output, List flags); + public void optimisationVector(ParameterVector output, List flags); } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index d089668d..e525a468 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -55,7 +55,7 @@ private BFGSOptimiser() { @Override public void prepare(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getPath(); + var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); //p[k] final double minimumPoint = p.getMinimumPoint(); // alpha[k] diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index 8103fc69..f4ba4b0e 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -15,7 +15,7 @@ * */ -public class ComplexPath extends Path { +public class ComplexPath extends GradientGuidedPath { private SquareMatrix hessian; private SquareMatrix inverseHessian; diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index b64e00c0..81d0b866 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -4,7 +4,7 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Property; import pulse.search.linear.LinearOptimiser; @@ -13,7 +13,7 @@ import pulse.tasks.logs.Status; import pulse.util.InstanceDescriptor; -public abstract class CompositePathOptimiser extends PathOptimiser { +public abstract class CompositePathOptimiser extends GradientBasedOptimiser { private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( "Linear Optimiser Selector", LinearOptimiser.class); @@ -32,7 +32,7 @@ private void initLinearOptimiser() { } public boolean iteration(SearchTask task) throws SolverException { - var p = task.getPath(); // the previous path of the task + var p = (ComplexPath) task.getIterativeState(); // the previous path of the task /* * Checks whether an iteration limit has been already reached @@ -44,14 +44,14 @@ public boolean iteration(SearchTask task) throws SolverException { } else { - var parameters = task.searchVector()[0]; // current parameters + var parameters = task.searchVector(); // current parameters var dir = getSolver().direction(p); // find p[k] - + double step = linearSolver.linearStep(task); // find magnitude of step p.setLinearStep(step); var candidateParams = parameters.sum(dir.multiply(step)); // new set of parameters determined through search - task.assign(new IndexedVector(candidateParams, parameters.getIndices())); // assign to this task + task.assign(new ParameterVector(parameters, candidateParams)); // assign to this task prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] p.incrementStep(); // increment the counter of successful steps @@ -101,7 +101,7 @@ public List listedTypes() { */ @Override - public Path createPath(SearchTask t) { + public GradientGuidedPath initState(SearchTask t) { this.configure(t); return new ComplexPath(t); } diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java index d78fbf0a..c209fc8b 100644 --- a/src/main/java/pulse/search/direction/DirectionSolver.java +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -16,6 +16,6 @@ public interface DirectionSolver { * @see pulse.problem.statements.Problem.optimisationVector(List) */ - public Vector direction(Path p) throws SolverException; + public Vector direction(GradientGuidedPath p) throws SolverException; } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java new file mode 100644 index 00000000..db833731 --- /dev/null +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -0,0 +1,163 @@ +package pulse.search.direction; + +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperties.isDiscrete; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; + +import java.util.List; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.properties.Property; +import pulse.tasks.SearchTask; + +public abstract class GradientBasedOptimiser extends PathOptimiser { + + private double gradientResolution; + private double gradientStep; + + /** + * Abstract constructor that sets up the default + * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} for + * this {@code PathSolver}. In addition, sets up a list of search flags defined + * by the {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + + protected GradientBasedOptimiser() { + super(); + } + + /** + * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and + * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In addition, + * sets up a list of search flags defined by the {@code Flag.defaultList} + * method. + * + * @see pulse.properties.Flag.defaultList() + */ + + public void reset() { + super.reset(); + gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); + } + + /** + * Calculates the {@code Vector} gradient of the target function (the sum of + * squared residuals, SSR, for this {@code task}. + *

+ * If Δf(Δxi) is the change in the + * target function associated with the change of the parameter + * xi, the i-th component of the gradient + * is equal to gi = + * (Δf(Δxi)/Δxi). The + * accuracy of this calculation depends on the + * Δxi value, which is roughly the + * {@code GRADIENT_RESOLUTION}. Note however that instead of using a + * forward-difference scheme to calculate the gradient, this method utilises the + * central-difference calculation of the gradient, which significantly increases + * the overall accuracy of calculation. This means that to evaluate each + * component of this vector, the {@code Problem} associated with this + * {@code task} is solved twice (for xi ± + * Δxi). + *

+ * + * @param task a {@code SearchTask} that is being driven to the minimum of SSR + * @return the gradient of the target function + * @throws SolverException + */ + + public Vector gradient(SearchTask task) throws SolverException { + + final var params = task.searchVector(); + var grad = new Vector(params.dimension()); + + boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + final double dx = discreteGradient ? dxGrid : gradientResolution; + + for (int i = 0; i < params.dimension(); i++) { + final var shift = new Vector(params.dimension()); + shift.set(i, 0.5 * dx); + + task.assign(new ParameterVector( params, params.sum(shift) )); + final double ss2 = task.solveProblemAndCalculateCost(); + + task.assign(new ParameterVector( params, params.subtract(shift) )); + final double ss1 = task.solveProblemAndCalculateCost(); + + grad.set(i, (ss2 - ss1) / dx); + + } + + task.assign(params); + + return grad; + + } + + /** + * Checks whether a discrete property is being optimised and selects the gradient step + * best suited to the optimisation strategy. Should be called before creating the optimisation path. + * @param task the search task defining the search vector + */ + + public void configure(SearchTask task) { + var params = task.searchVector(); + boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + } + + public void setGradientResolution(NumericProperty resolution) { + requireType(resolution, GRADIENT_RESOLUTION); + this.gradientResolution = (double) resolution.getValue(); + firePropertyChanged(this, resolution); + } + + public NumericProperty getGradientResolution() { + return derive(GRADIENT_RESOLUTION, gradientResolution); + } + + /** + *

+ * The types of the listed parameters for this class include: + * GRADIENT_RESOLUTION, + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class + * are treated as separate listed parameters. + *

+ * + * @see pulse.properties.NumericPropertyKeyword + */ + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(def(GRADIENT_RESOLUTION)); + return list; + } + + /** + * The accepted types are: + * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. + */ + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if(type == GRADIENT_RESOLUTION) { + setGradientResolution(property); + } + } + + public double getGradientStep() { + return gradientStep; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/Path.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java similarity index 77% rename from src/main/java/pulse/search/direction/Path.java rename to src/main/java/pulse/search/direction/GradientGuidedPath.java index c2142e75..b62b5fb2 100644 --- a/src/main/java/pulse/search/direction/Path.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -1,11 +1,7 @@ package pulse.search.direction; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.ITERATION; - import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; /** @@ -28,14 +24,13 @@ * */ -public class Path { +public class GradientGuidedPath extends IterativeState { private Vector direction; private Vector gradient; private double minimumPoint; - private int iteration; - protected Path(SearchTask t) { + protected GradientGuidedPath(SearchTask t) { configure(t); } @@ -48,14 +43,14 @@ protected Path(SearchTask t) { */ public void configure(SearchTask t) { + super.reset(); try { - this.gradient = PathOptimiser.getInstance().gradient(t); + this.gradient = ( (GradientBasedOptimiser) PathOptimiser.getInstance() ).gradient(t); } catch (SolverException e) { System.err.println("Failed on gradient calculation while resetting optimiser..."); e.printStackTrace(); } minimumPoint = 0.0; - iteration = 0; } public Vector getDirection() { @@ -82,12 +77,4 @@ public void setLinearStep(double min) { minimumPoint = min; } - public NumericProperty getIteration() { - return derive(ITERATION, iteration); - } - - public void incrementStep() { - iteration++; - } - } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index 1246f16c..ee92b4c3 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -18,7 +18,7 @@ public interface HessianDirectionSolver extends DirectionSolver { */ @Override - public default Vector direction(Path p) throws SolverException { + public default Vector direction(GradientGuidedPath p) throws SolverException { var cp = (ComplexPath) p; Vector invGrad = p.getGradient().inverted(); diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java new file mode 100644 index 00000000..b8e756d6 --- /dev/null +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -0,0 +1,24 @@ +package pulse.search.direction; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.ITERATION; + +import pulse.properties.NumericProperty; + +public class IterativeState { + + private int iteration; + + public void reset() { + iteration = 0; + } + + public NumericProperty getIteration() { + return derive(ITERATION, iteration); + } + + public void incrementStep() { + iteration++; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index e2426ef9..8548071f 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -9,7 +9,7 @@ import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.linear.Matrices; import pulse.math.linear.RectangularMatrix; import pulse.math.linear.SquareMatrix; @@ -25,7 +25,7 @@ import pulse.tasks.logs.Status; import pulse.ui.Messages; -public class LMOptimiser extends PathOptimiser { +public class LMOptimiser extends GradientBasedOptimiser { private static LMOptimiser instance = new LMOptimiser(); private boolean computeJacobian; @@ -33,8 +33,10 @@ public class LMOptimiser extends PathOptimiser { private final static double EPS = 1e-10; // for numerical comparison private double dampingRatio; + /* private double geodesicParameter = 0.1; private boolean geodesicCorrection = false; + */ private LMOptimiser() { super(); @@ -52,7 +54,7 @@ public void reset() { @Override public boolean iteration(SearchTask task) throws SolverException { - var p = (LMPath) task.getPath(); // the previous path of the task + var p = (LMPath) task.getIterativeState(); // the previous path of the task /* * Checks whether an iteration limit has been already reached @@ -68,7 +70,8 @@ public boolean iteration(SearchTask task) throws SolverException { else { double initialCost = task.solveProblemAndCalculateCost(); - var parameters = task.searchVector()[0]; + var parameters = task.searchVector(); + p.setParameters(parameters); // store current parameters prepare(task); // do the preparatory step @@ -79,22 +82,24 @@ public boolean iteration(SearchTask task) throws SolverException { * Geodesic acceleration */ + /* var acceleration = p.getJacobian().transpose().multiply( directionalDerivative(task) ); // J' dr/dp var correction = HessianDirectionSolver.solve(p, acceleration.inverted() ); // H^-1 J'dr/dp double newCost = Double.POSITIVE_INFINITY; + */ /* * Additional conditions imposed by geodesic acceleration. */ - if( !geodesicCorrection || correction.length() / lmDirection.length() <= geodesicParameter) { + //if( !geodesicCorrection || correction.length() / lmDirection.length() <= geodesicParameter) { var candidate = parameters.sum(lmDirection); - task.assign(new IndexedVector( - geodesicCorrection ? candidate.sum( correction.multiply(0.5) ) : candidate, - parameters.getIndices() ) ); // assign new parameters - newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals - } + task.assign(new ParameterVector( + //geodesicCorrection ? candidate.sum( correction.multiply(0.5) ) : + parameters, candidate ) ); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + //} /* * Delayed gratification @@ -119,7 +124,7 @@ public boolean iteration(SearchTask task) throws SolverException { @Override public void prepare(SearchTask task) throws SolverException { - var p = (LMPath) task.getPath(); + var p = (LMPath) task.getIterativeState(); //store residual vector at current parameters p.setResidualVector( new Vector( residualVector(task.getCurrentCalculation().getOptimiserStatistic()) )); @@ -151,10 +156,9 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); - var p = ((LMPath) task.getPath()); + var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); - final var indices = params.getIndices(); final int numPoints = p.getResidualVector().dimension(); final int numParams = params.dimension(); @@ -169,12 +173,12 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { shift.set(i, 0.5 * dx); // + shift - task.assign(new IndexedVector(params.sum(shift), indices)); + task.assign(new ParameterVector( params, params.sum(shift) )); task.solveProblemAndCalculateCost(); var r1 = residualVector(residualCalculator); // - shift - task.assign(new IndexedVector(params.subtract(shift), indices)); + task.assign(new ParameterVector( params, params.subtract(shift) )); task.solveProblemAndCalculateCost(); var r2 = residualVector(residualCalculator); @@ -198,7 +202,7 @@ private static double[] residualVector(ResidualStatistic rs) { } @Override - public Path createPath(SearchTask t) { + public GradientGuidedPath initState(SearchTask t) { this.configure(t); computeJacobian = true; return new LMPath(t); @@ -215,6 +219,7 @@ private SquareMatrix halfHessian(LMPath path) { return asSquareMatrix(jacobian.transpose().multiply(jacobian)); } + /* private Vector directionalDerivative(SearchTask t) throws SolverException { var p = (LMPath) t.getPath(); @@ -256,6 +261,7 @@ private Vector directionalDerivative(SearchTask t) throws SolverException { return new Vector(diff); } + */ /* * Additive damping strategy, where the scaling matrix is simply the identity matrix. @@ -328,75 +334,5 @@ public void setDampingRatio(NumericProperty dampingRatio) { this.dampingRatio = (double)dampingRatio.getValue(); firePropertyChanged(this, dampingRatio); } - - - /* - * Path - */ - - class LMPath extends ComplexPath { - - private IndexedVector parameters; - private Vector residualVector; - private RectangularMatrix jacobian; - private SquareMatrix nonregularisedHessian; - private double lambda; - - public LMPath(SearchTask t) { - super(t); - } - - public RectangularMatrix getJacobian() { - return jacobian; - } - - public void setJacobian(RectangularMatrix jacobian) { - this.jacobian = jacobian; - } - - public double getLambda() { - return lambda; - } - - public void setLambda(double lambda) { - this.lambda = lambda; - } - - @Override - public void configure(SearchTask t) { - super.configure(t); - this.jacobian = null; - this.setHessian(null); - nonregularisedHessian = null; - this.lambda = 1.0; - this.residualVector = null; - } - - public SquareMatrix getNonregularisedHessian() { - return nonregularisedHessian; - } - - public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { - this.nonregularisedHessian = nonregularisedHessian; - } - - public Vector getResidualVector() { - return residualVector; - } - - public void setResidualVector(Vector residualVector) { - this.residualVector = residualVector; - } - - public IndexedVector getParameters() { - return parameters; - } - - public void setParameters(IndexedVector parameters) { - this.parameters = parameters; - } - - } - } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java new file mode 100644 index 00000000..1acc5fc7 --- /dev/null +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -0,0 +1,71 @@ +package pulse.search.direction; + +import pulse.math.ParameterVector; +import pulse.math.linear.RectangularMatrix; +import pulse.math.linear.SquareMatrix; +import pulse.math.linear.Vector; +import pulse.tasks.SearchTask; + +class LMPath extends ComplexPath { + + private ParameterVector parameters; + private Vector residualVector; + private RectangularMatrix jacobian; + private SquareMatrix nonregularisedHessian; + private double lambda; + + public LMPath(SearchTask t) { + super(t); + } + + @Override + public void configure(SearchTask t) { + super.configure(t); + this.jacobian = null; + this.setHessian(null); + nonregularisedHessian = null; + this.lambda = 1.0; + this.residualVector = null; + } + + public RectangularMatrix getJacobian() { + return jacobian; + } + + public void setJacobian(RectangularMatrix jacobian) { + this.jacobian = jacobian; + } + + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + public SquareMatrix getNonregularisedHessian() { + return nonregularisedHessian; + } + + public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { + this.nonregularisedHessian = nonregularisedHessian; + } + + public Vector getResidualVector() { + return residualVector; + } + + public void setResidualVector(Vector residualVector) { + this.residualVector = residualVector; + } + + public ParameterVector getParameters() { + return parameters; + } + + public void setParameters(ParameterVector parameters) { + this.parameters = parameters; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index 72a4bf9a..84ac428d 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -2,18 +2,14 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.isDiscrete; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.ERROR_TOLERANCE; -import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import static pulse.properties.NumericPropertyKeyword.ITERATION_LIMIT; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import pulse.math.IndexedVector; -import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -48,9 +44,6 @@ public abstract class PathOptimiser extends PropertyHolder implements Reflexive private int maxIterations; private double errorTolerance; - private double gradientResolution; - private double gradientStep; - private static PathOptimiser instance; /** @@ -79,7 +72,6 @@ protected PathOptimiser() { public void reset() { maxIterations = (int) def(ITERATION_LIMIT).getValue(); errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); ActiveFlags.reset(); } @@ -115,73 +107,6 @@ public void reset() { public abstract void prepare(SearchTask task) throws SolverException; - /** - * Calculates the {@code Vector} gradient of the target function (the sum of - * squared residuals, SSR, for this {@code task}. - *

- * If Δf(Δxi) is the change in the - * target function associated with the change of the parameter - * xi, the i-th component of the gradient - * is equal to gi = - * (Δf(Δxi)/Δxi). The - * accuracy of this calculation depends on the - * Δxi value, which is roughly the - * {@code GRADIENT_RESOLUTION}. Note however that instead of using a - * forward-difference scheme to calculate the gradient, this method utilises the - * central-difference calculation of the gradient, which significantly increases - * the overall accuracy of calculation. This means that to evaluate each - * component of this vector, the {@code Problem} associated with this - * {@code task} is solved twice (for xi ± - * Δxi). - *

- * - * @param task a {@code SearchTask} that is being driven to the minimum of SSR - * @return the gradient of the target function - * @throws SolverException - */ - - public Vector gradient(SearchTask task) throws SolverException { - - final var params = task.searchVector()[0]; - var grad = new Vector(params.dimension()); - - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - final double dx = discreteGradient ? dxGrid : gradientResolution; - - for (int i = 0; i < params.dimension(); i++) { - final var shift = new Vector(params.dimension()); - shift.set(i, 0.5 * dx); - - task.assign(new IndexedVector( params.sum(shift) , params.getIndices())); - final double ss2 = task.solveProblemAndCalculateCost(); - - task.assign(new IndexedVector( params.subtract(shift), params.getIndices())); - final double ss1 = task.solveProblemAndCalculateCost(); - - grad.set(i, (ss2 - ss1) / dx); - - } - - task.assign(params); - - return grad; - - } - - /** - * Checks whether a discrete property is being optimised and selects the gradient step - * best suited to the optimisation strategy. Should be called before creating the optimisation path. - * @param task the search task defining the search vector - */ - - public void configure(SearchTask task) { - var params = task.searchVector()[0]; - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); - } - public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); } @@ -192,16 +117,6 @@ public void setErrorTolerance(NumericProperty errorTolerance) { firePropertyChanged(this, errorTolerance); } - public void setGradientResolution(NumericProperty resolution) { - requireType(resolution, GRADIENT_RESOLUTION); - this.gradientResolution = (double) resolution.getValue(); - firePropertyChanged(this, resolution); - } - - public NumericProperty getGradientResolution() { - return derive(GRADIENT_RESOLUTION, gradientResolution); - } - public NumericProperty getMaxIterations() { return derive(ITERATION_LIMIT, maxIterations); } @@ -233,8 +148,7 @@ public List genericProperties() { /** *

* The types of the listed parameters for this class include: - * GRADIENT_RESOLUTION, - * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class * are treated as separate listed parameters. *

* @@ -244,7 +158,6 @@ public List genericProperties() { @Override public List listedTypes() { List list = new ArrayList(); - list.add(def(GRADIENT_RESOLUTION)); list.add(def(ERROR_TOLERANCE)); list.add(def(ITERATION_LIMIT)); @@ -261,24 +174,15 @@ public List data() { /** * The accepted types are: - * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. + * ERROR_TOLERANCE, ITERATION_LIMIT. */ @Override public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case GRADIENT_RESOLUTION: - setGradientResolution(property); - break; - case ERROR_TOLERANCE: + if(type == ERROR_TOLERANCE) setErrorTolerance(property); - break; - case ITERATION_LIMIT: - setMaxIterations(property); - break; - default: - break; - } + else if(type == ITERATION_LIMIT) + setMaxIterations(property); } /** @@ -290,7 +194,6 @@ public boolean ignoreSiblings() { return true; } - /** * Finds a {@code Flag} equivalent to {@code flag} in the {@code originalList} * and substitutes its value with {@code flag.getValue}. @@ -309,15 +212,6 @@ public void update(Property property) { } } - - /** - * Creates a new {@code Path} suitable for this {@code PathSolver} - * - * @param t the task, the optimisation path of which will be tracked - * @return a {@code Path} instance - */ - - public abstract Path createPath(SearchTask t); public static PathOptimiser getInstance() { return instance; @@ -345,9 +239,17 @@ protected void setSolver(DirectionSolver solver) { public boolean compatibleWith(OptimiserStatistic os) { return true; } + + + /** + * Creates a new {@code Path} suitable for this {@code PathSolver} + * + * @param t the task, the optimisation path of which will be tracked + * @return a {@code Path} instance + */ - public double getGradientStep() { - return gradientStep; - } + public abstract IterativeState initState(SearchTask t); + + } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index 9266f742..5b376242 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -36,7 +36,7 @@ private SR1Optimiser() { @Override public void prepare(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getPath(); + var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); final double minimumPoint = p.getMinimumPoint(); diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 7378d339..4d9550e9 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -39,7 +39,7 @@ private SteepestDescentOptimiser() { @Override public void prepare(SearchTask task) throws SolverException { - task.getPath().setGradient(gradient(task)); + ( (GradientGuidedPath) task.getIterativeState() ).setGradient(gradient(task)); } @Override @@ -67,9 +67,9 @@ public static SteepestDescentOptimiser getInstance() { */ @Override - public Path createPath(SearchTask t) { + public GradientGuidedPath initState(SearchTask t) { this.configure(t); - return new Path(t); + return new GradientGuidedPath(t); } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java new file mode 100644 index 00000000..5e4ef483 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -0,0 +1,40 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class FIPSMover implements Mover { + + private double chi; + private double phi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_PHI = 4.1; + + public FIPSMover() { + chi = DEFAULT_CHI; + phi = DEFAULT_PHI; + } + + @Override + public ParticleState attemptMove(Particle p, Particle[] neighbours) { + var current = p.getCurrentState(); + var pos = current.getPosition(); + + final int n = pos.dimension(); + var nsum = new Vector(n); + + for(var neighbour : neighbours) + nsum = nsum.sum( Vector.random(n, 0.0, phi).multComponents( neighbour.getCurrentState().getPosition().subtract(pos) ) ); + + nsum = nsum.multiply(1.0/neighbours.length); + + var newVelocity = ( current.getVelocity().sum(nsum) ).multiply(chi); + var newPosition = pos.sum(newVelocity); + + return new ParticleState( + new ParameterVector(pos, newPosition ), + new ParameterVector(pos, newVelocity ) ); + + } + +} diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java new file mode 100644 index 00000000..c086f608 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -0,0 +1,7 @@ +package pulse.search.direction.pso; + +public interface Mover { + + public ParticleState attemptMove(Particle p, Particle[] neighbours); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java new file mode 100644 index 00000000..3a6f4b4b --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java @@ -0,0 +1,7 @@ +package pulse.search.direction.pso; + +public interface NeighbourhoodTopology { + + public Particle[] neighbours(Particle p, SwarmState ss); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java new file mode 100644 index 00000000..1897784c --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -0,0 +1,84 @@ +/* %% + * + * JPSO + * + * Copyright 2006 Jeff Ridder + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.SearchTask; + +/** + * Class defining a particle - the basic unit of a swarm. + */ + +public class Particle { + + private int id; + + private ParticleState current; + private ParticleState pbest; + + private Particle[] neighbours; + + public Particle(ParameterVector pv, int id) { + this.id = id; + current = new ParticleState( pv, null ); + pbest = new ParticleState(current); + } + + public void adopt(ParticleState state) { + this.current = state; + } + + public void evaluate(SearchTask t) throws SolverException { + var params = t.searchVector(); + t.assign( current.getPosition() ); + current.setFitness( t.solveProblemAndCalculateCost() ); + t.assign( params ); + + if(current.isBetterThan(pbest)) + pbest = new ParticleState(current); + } + + /** + * Returns the current state (position, velocity, fitness) of the particle. + * + * @return current state. + */ + public ParticleState getCurrentState() { + return current; + } + + /** + * Returns the personal best state ever achieved by the particle. + * + * @return personal best state. + */ + public ParticleState getBestState() { + return pbest; + } + + public int getId() { + return id; + } + + public Particle[] getNeighbours() { + return neighbours; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java new file mode 100644 index 00000000..4fc1a56b --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -0,0 +1,68 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.tasks.SearchTask; + +public class ParticleState { + + private ParameterVector position; + private ParameterVector velocity; + private double fitness; + + public ParticleState(SearchTask t) { + randomise(t); + this.fitness = Double.MAX_VALUE; + } + + public ParticleState(ParticleState another) { + this.position = new ParameterVector(another.position); + if(another.velocity != null) + this.velocity = new ParameterVector(another.velocity); + this.fitness = another.fitness; + } + + public ParticleState(ParameterVector p, ParameterVector v) { + this.position = p; + this.velocity = v; + } + + public boolean isBetterThan(ParticleState s) { + return this.fitness < s.fitness; + } + + public void randomise(SearchTask task) { + + position = new ParameterVector( task.searchVector() ); + + for (int i = 0, n = position.dimension(); i < n; i++) { + + var bounds = position.getBounds(); + var t = position.getTransform(i); + + double max = t.transform( bounds[i].getMaximum() ); + double min = t.transform( bounds[i].getMinimum() ); + + double value = min + Math.random() * ( max - min ); + position.set(i, value); + + } + + } + + public ParameterVector getPosition() { + return this.position; + } + + public ParameterVector getVelocity() { + return this.velocity; + } + + public double getFitness() { + return this.fitness; + } + + protected void setFitness(double fitness) { + this.fitness = fitness; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java new file mode 100644 index 00000000..30288f07 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -0,0 +1,51 @@ +package pulse.search.direction.pso; + +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.tasks.SearchTask; + +public class ParticleSwarmOptimiser extends PathOptimiser { + + private SwarmState swarmState; + private Mover mover; + + public ParticleSwarmOptimiser() { + this.swarmState = new SwarmState(); + mover = new FIPSMover(); + } + + protected void moveParticles() { + var topology = swarmState.getNeighborhoodTopology(); + for (var p : swarmState.getParticles()) + p.adopt( mover.attemptMove( p, topology.neighbours(p, swarmState) ) ); + } + + /** + * Iterates the swarm. + * + * @param max_iterations max number of iterations to be computed by the swarm. + */ + + @Override + public boolean iteration(SearchTask task) throws SolverException { + this.prepare(task); + + moveParticles(); + swarmState.evaluate(task); + + return true; + } + + @Override + public void prepare(SearchTask task) throws SolverException { + swarmState.prepare(task); + } + + @Override + public IterativeState initState(SearchTask t) { + swarmState.create(); + return swarmState; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java new file mode 100644 index 00000000..29b99957 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -0,0 +1,58 @@ +package pulse.search.direction.pso; + +import java.util.Arrays; + +public class StaticTopologies { + + private StaticTopologies() { + //empty + } + + /** Global best + * + */ + + public final static NeighbourhoodTopology GLOBAL = (p, state) -> state.getParticles(); + + /** + * Ring topology (1D - lattice) + */ + + public final static NeighbourhoodTopology RING = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + return new Particle[] { ps[i > 0 ? i - 1 : ps.length - 1], + ps[i + 1 < ps.length ? i + 1 : 0] + }; + }; + + /** + * Von Neumann topology (square lattice) + * Condition: + if( ( ps.length & (ps.length - 1) ) != 0) + throw new IllegalArgumentException("Number of particles: " + ps.length + " is not power of 2"); + */ + + public final static NeighbourhoodTopology SQUARE = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + + final int latticeParameter = (int) Math.sqrt(ps.length); + + final int row = i % latticeParameter; + final int column = i - row*latticeParameter; + + final int above = column + (row > 0 ? + (row - 1)*latticeParameter : (latticeParameter - 1)*latticeParameter ); + + final int below = column + ( row + 1 < ps.length ? + latticeParameter*(row + 1) : 0 ); + + final int left = row*latticeParameter + ( column > 0 ? column - 1 : ps.length - 1 ); + final int right = row*latticeParameter + ( column + 1 < ps.length ? column + 1 : 0 ); + + return new Particle[] { ps[left], ps[right], ps[above], ps[below] }; + }; + + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java new file mode 100644 index 00000000..ec971c05 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -0,0 +1,104 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.direction.IterativeState; +import pulse.tasks.SearchTask; + +public class SwarmState extends IterativeState { + + private ParameterVector current; + + private Particle[] particles; + private NeighbourhoodTopology neighborhoodTopology; + + private Particle bestSoFar; + private int bestSoFarIndex; + + public SwarmState() { + this(16, StaticTopologies.GLOBAL); + } + + public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + this.particles = new Particle[numberOfParticles]; + this.bestSoFar = null; + this.bestSoFarIndex = -1; + } + + public void evaluate(SearchTask t) throws SolverException { + for (var p : particles) + p.evaluate(t); + } + + public void prepare(SearchTask t) { + current = t.searchVector(); + } + + public void create() { + for (int i = 0; i < particles.length; i++) + particles[i] = new Particle(current, i); + } + + /** + * Returns the best state achieved by any particle so far. + * + * @return State object. + */ + + public ParticleState bestSoFar() { + int bestIndex = 0; + double bestFitness = Double.MAX_VALUE; + + for (int i = 0; i < particles.length; i++) { + if (particles[i].getBestState().getFitness() < bestFitness) { + bestIndex = i; + bestFitness = particles[i].getBestState().getFitness(); + } + } + + this.bestSoFar = particles[bestIndex]; + this.bestSoFarIndex = bestIndex; + + return bestSoFar.getBestState(); + } + + public NeighbourhoodTopology getNeighborhoodTopology() { + return neighborhoodTopology; + } + + public void setNeighborhoodTopology(NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + } + + /** + * Returns the particles of the swarm. + * + * @return array of Particles. + */ + + public Particle[] getParticles() { + return particles; + } + + public void setParticles(Particle[] particles) { + this.particles = particles; + } + + public Particle getBestSoFar() { + return bestSoFar; + } + + public void setBestSoFar(Particle bestSoFar) { + this.bestSoFar = bestSoFar; + } + + public int getBestSoFarIndex() { + return bestSoFarIndex; + } + + public void setBestSoFarIndex(int bestSoFarIndex) { + this.bestSoFarIndex = bestSoFarIndex; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index 6cba4146..a0736e29 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -1,8 +1,9 @@ package pulse.search.linear; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.direction.GradientGuidedPath; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -51,9 +52,9 @@ public double linearStep(SearchTask task) throws SolverException { final double EPS = 1e-14; final var params = task.searchVector(); - final Vector direction = task.getPath().getDirection(); + final Vector direction = ( (GradientGuidedPath) task.getIterativeState() ).getDirection(); - var segment = domain(params[0], params[1], direction); + var segment = domain(params, direction); final double absError = searchResolution * PHI * segment.length(); @@ -61,15 +62,15 @@ public double linearStep(SearchTask task) throws SolverException { final double alpha = segment.getMinimum() + t; final double one_minus_alpha = segment.getMaximum() - t; - final var newParams1 = params[0].sum(direction.multiply(alpha)); // alpha - task.assign(new IndexedVector(newParams1, params[0].getIndices())); + final var newParams1 = params.sum(direction.multiply(alpha)); // alpha + task.assign(new ParameterVector(params, newParams1 )); final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) - final var newParams2 = params[0].sum(direction.multiply(one_minus_alpha)); // 1 - alpha - task.assign(new IndexedVector(newParams2, params[0].getIndices())); + final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + task.assign(new ParameterVector(params, newParams2)); final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) - task.assign(new IndexedVector(newParams2, params[0].getIndices())); // return to old position + task.assign(new ParameterVector(params, newParams2)); // return to old position if (ss2 - ss1 > EPS) segment.setMaximum(alpha); diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index e31ce2cc..00593f6c 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -1,7 +1,6 @@ package pulse.search.linear; import static java.lang.Math.abs; -import static java.lang.Math.min; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; @@ -10,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; @@ -32,7 +31,8 @@ public abstract class LinearOptimiser extends PropertyHolder implements Reflexive { protected static double searchResolution = (double) def(LINEAR_RESOLUTION).getValue(); - + private final static double EPS = 1E-15; + protected LinearOptimiser() { super(); } @@ -64,18 +64,16 @@ protected LinearOptimiser() { * less than unity. *

* - * @param x the current set of parameters - * @param bounds the bounds for x + * @param x the current set of parameter * @param p the result of the direction search with the {@code PathSolver} * @return a {@code Segment} defining the domain of this search * @see pulse.search.direction.PathSolver.direction(SearchTask) */ - public static Segment domain(IndexedVector x, IndexedVector bounds, Vector p) { - double alpha = Double.POSITIVE_INFINITY; - - final double EPS = 1E-15; - + public static Segment domain(ParameterVector x, Vector p) { + double alphaMax = Double.POSITIVE_INFINITY; + double alpha = 0.0; + for (int i = 0; i < x.dimension(); i++) { final double component = p.get(i); @@ -84,11 +82,19 @@ public static Segment domain(IndexedVector x, IndexedVector bounds, Vector p) { if (component < EPS && component > -EPS) continue; - alpha = min(alpha, abs(bounds.get(i) / component)); + var bound = x.getTransformedBounds(i); + + alpha = abs( + ( ( component > 0 ? bound.getMaximum() : bound.getMinimum() ) - x.get(i) ) + / component); + + if(Double.isFinite(alpha) && alpha < alphaMax) + alphaMax = alpha; } - return new Segment(0, alpha); + return new Segment(0.0, alphaMax); + } /** diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 0848ea60..715e594b 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -2,11 +2,12 @@ import static java.lang.Math.abs; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.search.direction.Path; +import pulse.search.direction.GradientBasedOptimiser; +import pulse.search.direction.GradientGuidedPath; import pulse.search.direction.PathOptimiser; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -67,7 +68,7 @@ private WolfeOptimiser() { @Override public double linearStep(SearchTask task) throws SolverException { - Path p = task.getPath(); + GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); final Vector direction = p.getDirection(); final Vector g1 = p.getGradient(); @@ -76,21 +77,21 @@ public double linearStep(SearchTask task) throws SolverException { final double G1P_ABS = abs(G1P); var params = task.searchVector(); - Segment segment = domain(params[0], params[1], direction); + Segment segment = domain(params, direction); double cost1 = task.solveProblemAndCalculateCost(); double randomConfinedValue = 0; double g2p; - var instance = PathOptimiser.getInstance(); + var instance = (GradientBasedOptimiser) PathOptimiser.getInstance(); for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { randomConfinedValue = segment.randomValue(); - final var newParams = params[0].sum(direction.multiply(randomConfinedValue)); - task.assign(new IndexedVector(newParams, params[0].getIndices())); + final var newParams = params.sum(direction.multiply(randomConfinedValue)); + task.assign(new ParameterVector(params, newParams)); final double cost2 = task.solveProblemAndCalculateCost(); @@ -123,7 +124,7 @@ public double linearStep(SearchTask task) throws SolverException { } - task.assign(params[0]); + task.assign(params); p.setGradient(g1); return randomConfinedValue; diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index aad0cfb7..7ab66433 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -37,7 +37,7 @@ public void setLambda(double lambda) { public void evaluate(SearchTask t) { sos.evaluate(t); final double ssr = (double)sos.getStatistic().getValue(); - final double statistic = ssr + lambda*t.searchVector()[0].lengthSq(); + final double statistic = ssr + lambda*t.searchVector().lengthSq(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index b06c4d8f..15122eeb 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -36,11 +36,11 @@ import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.search.direction.Path; +import pulse.search.direction.IterativeState; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -73,7 +73,7 @@ public class SearchTask extends Accessible implements Runnable { private List stored; private ExperimentalData curve; - private Path path; + private IterativeState path; private Buffer buffer; private Log log; @@ -191,19 +191,15 @@ public List alteredParameters() { * @see pulse.problem.statements.Problem.optimisationVector(List) */ - public IndexedVector[] searchVector() { + public ParameterVector searchVector() { var flags = getAllFlags(); var keywords = activeParameters(this); - var optimisationVector = new IndexedVector(keywords); - var upperBound = new IndexedVector(optimisationVector.getIndices()); + var optimisationVector = new ParameterVector(keywords); - var array = new IndexedVector[] { optimisationVector, upperBound }; - - current.getProblem().optimisationVector(array, flags); - curve.getRange().optimisationVector(array, flags); - - return array; + current.getProblem().optimisationVector(optimisationVector, flags); + curve.getRange().optimisationVector(optimisationVector, flags); + return optimisationVector; } /** @@ -215,7 +211,7 @@ public IndexedVector[] searchVector() { * @see pulse.problem.statements.Problem.assign(IndexedVector) */ - public void assign(IndexedVector searchParameters) { + public void assign(ParameterVector searchParameters) { current.getProblem().assign(searchParameters); curve.getRange().assign(searchParameters); } @@ -255,8 +251,7 @@ public void run() { var optimiser = getInstance(); - path = optimiser.createPath(this); - optimiser.configure(this); + path = optimiser.initState(this); var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); int bufferSize = (Integer) getSize().getValue(); @@ -390,7 +385,7 @@ public ExperimentalData getExperimentalCurve() { return curve; } - public Path getPath() { + public IterativeState getIterativeState() { return path; } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index ef376e41..b4ef1249 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -112,6 +112,7 @@ public static TaskManager getManagerInstance() { */ public void execute(SearchTask t) { + t.setStatus(QUEUED); // notify listeners computation is about to start // notify listeners @@ -132,6 +133,7 @@ public void execute(SearchTask t) { } else notifyListeners(e); }); + } /** diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 5b7d9ca3..196ee139 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -57,7 +57,7 @@ private void fill() throws IllegalAccessException, IllegalArgumentException, Inv entry = task.alteredParameters(); Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); - entry.add(0, task.getPath().getIteration()); + entry.add(0, task.getIterativeState().getIteration()); } public List getData() { diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index 17e56ebe..30298e3e 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -26,7 +26,7 @@ public class Buffer extends PropertyHolder { - private IndexedVector[] data; + private ParameterVector[] data; private double[] statistic; private static int size = (int) def(BUFFER_SIZE).getValue(); @@ -44,12 +44,12 @@ public Buffer() { * @return the data */ - public IndexedVector[] getData() { + public ParameterVector[] getData() { return data; } public void init() { - this.data = new IndexedVector[size]; + this.data = new ParameterVector[size]; statistic = new double[size]; } @@ -63,7 +63,7 @@ public void init() { public void fill(SearchTask t, int bufferElement) { statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); - data[bufferElement] = t.searchVector()[0]; + data[bufferElement] = t.searchVector(); } /** @@ -106,8 +106,8 @@ public double average(NumericPropertyKeyword index) { double av = 0; - for (IndexedVector v : data) { - av += v.get(index); + for (ParameterVector v : data) { + av += v.getParameterValue(index); } return av / data.length; @@ -146,8 +146,8 @@ public double variance(NumericPropertyKeyword index) { double sd = 0; double av = average(index); - for (IndexedVector v : data) { - final double s = v.get(index) - av; + for (ParameterVector v : data) { + final double s = v.getParameterValue(index) - av; sd += s*s; } diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index ae8a0cae..e4417488 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -8,7 +8,7 @@ import java.util.Set; import java.util.stream.Collectors; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.properties.NumericPropertyKeyword; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.EmptyCorrelationTest; @@ -18,7 +18,7 @@ public class CorrelationBuffer { - private List params; + private List params; private static Set> excludePairList; private static Set excludeSingleList; @@ -37,7 +37,7 @@ public CorrelationBuffer() { } public void inflate(SearchTask t) { - params.add(t.searchVector()[0]); + params.add(t.searchVector()); } public void clear() { @@ -53,7 +53,7 @@ public Map, Double> evaluate(CorrelationTe var indices = params.get(0).getIndices(); var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.get(index)).toArray())) + .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); From c44e39b717b9da38135082b5fd06954aa9e509ef Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 5 Mar 2021 13:59:21 +0100 Subject: [PATCH 025/116] Major Commit * Import Proteus files * Start implementing PSO optimiser * Numerical pulse --- src/main/java/pulse/input/Metadata.java | 11 ++ .../pulse/io/readers/NetzschCSVReader.java | 158 ++++++++++++++++++ .../io/readers/NetzschPulseCSVReader.java | 129 ++++++++++++++ .../pulse/io/readers/PulseDataReader.java | 12 ++ .../java/pulse/io/readers/ReaderManager.java | 14 ++ src/main/java/pulse/math/ParameterVector.java | 12 +- .../pulse/problem/laser/DiscretePulse.java | 10 +- .../laser/ExponentiallyModifiedGaussian.java | 35 +--- .../pulse/problem/laser/NumericPulse.java | 107 ++++++++++++ .../pulse/problem/laser/NumericPulseData.java | 45 +++++ .../problem/laser/PulseTemporalShape.java | 46 ++++- .../pulse/problem/laser/TrapezoidalPulse.java | 5 +- .../java/pulse/problem/statements/Pulse.java | 4 +- .../pulse/search/direction/pso/FIPSMover.java | 20 ++- .../pulse/search/direction/pso/Particle.java | 8 +- .../search/direction/pso/ParticleState.java | 33 ++-- .../direction/pso/ParticleSwarmOptimiser.java | 12 +- .../direction/pso/StaticTopologies.java | 7 +- .../search/direction/pso/SwarmState.java | 31 ++-- src/main/java/pulse/tasks/TaskManager.java | 17 +- .../java/pulse/ui/components/DataLoader.java | 43 ++++- .../java/pulse/ui/components/PulseChart.java | 2 +- .../pulse/ui/components/PulseMainMenu.java | 16 +- .../ui/frames/dialogs/ProgressDialog.java | 2 +- src/main/resources/messages.properties | 2 + 25 files changed, 685 insertions(+), 96 deletions(-) create mode 100644 src/main/java/pulse/io/readers/NetzschCSVReader.java create mode 100644 src/main/java/pulse/io/readers/NetzschPulseCSVReader.java create mode 100644 src/main/java/pulse/io/readers/PulseDataReader.java create mode 100644 src/main/java/pulse/problem/laser/NumericPulse.java create mode 100644 src/main/java/pulse/problem/laser/NumericPulseData.java diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index f2a03c69..2cf102c8 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.TreeSet; +import pulse.problem.laser.NumericPulseData; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; import pulse.properties.NumericProperty; @@ -47,6 +48,8 @@ public class Metadata extends PropertyHolder implements Reflexive { private InstanceDescriptor pulseDescriptor = new InstanceDescriptor( "Pulse Shape Selector", PulseTemporalShape.class); + + private NumericPulseData pulseData; /** * Creates a {@code Metadata} with the specified parameters and a default @@ -117,6 +120,14 @@ public SampleName getSampleName() { public void setSampleName(SampleName sampleName) { this.sampleName = sampleName; } + + public void setPulseData(NumericPulseData pulseData) { + this.pulseData = pulseData; + } + + public NumericPulseData getPulseData() { + return pulseData; + } /** * Searches the internal list of this class for a property with the {@code key} diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java new file mode 100644 index 00000000..b608a1ba --- /dev/null +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -0,0 +1,158 @@ +package pulse.io.readers; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import pulse.input.ExperimentalData; +import pulse.input.Metadata; +import pulse.input.Range; +import pulse.ui.Messages; + +public class NetzschCSVReader implements CurveReader { + + private static CurveReader instance = new NetzschCSVReader(); + private final static double TO_KELVIN = 273; + private final static double TO_SECONDS = 1E-3; + + private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; + private final static String SHOT_DATA = "Shot_data"; + private final static String DETECTOR = "DETECTOR"; + + private NetzschCSVReader() { + // intentionally blank + } + + /** + * @return The supported extension ({@code .csv}). + */ + + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } + + /** + *

+ * This will return a single {@code ExperimentalData}, which stores all the + * information available in the {@code file}, wrapped in a {@code List} object + * with the size of unity. In addition to the time-temperature data loaded + * directly into the {@code ExperimentalData} lists, a {@code Metadata} object + * will be created for the {@code ExperimentalData} and will store the test + * temperature declared in {@code file}. + * + * @param file a '{@code .dat}' file, which conforms to the respective format. + * @return a single {@code ExperimentalData} wrapped in a {@code List} with the + * size of unity. + */ + + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + ExperimentalData curve = new ExperimentalData(); + + String delims = "[#();/°Cx%^]+"; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + String[] shotID = reader.readLine().split(delims); + + int shotId = -1; + + //check if first entry makes sense + if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) + throw new IllegalArgumentException(file.getName() + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); + else + shotId = Integer.parseInt(shotID[shotID.length - 1]); + + String[] tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); + double sampleTemperature = parseDoubleWithComma( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; + + var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + curve.setMetadata(met); + + double time; + double temp; + + var detectorLabel = findLineByLabel(reader, DETECTOR, delims); + + if(detectorLabel == null) { + System.err.println("Skipping " + file.getName()); + return new ArrayList<>(); + } + + reader.readLine(); + + String[] tokens; + + for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { + tokens = line.split(delims); + + time = parseDoubleWithComma(tokens[0]) * TO_SECONDS; + temp = parseDoubleWithComma(tokens[1]); + curve.addPoint(time, temp); + } + curve.setRange(new Range(curve.getTimeSequence())); + + } + + return new ArrayList<>(Arrays.asList(curve)); + + } + + private double parseDoubleWithComma(String s) { + var format = NumberFormat.getInstance(Locale.GERMANY); + try { + return format.parse(s).doubleValue(); + } catch (ParseException e) { + System.out.println("Couldn't parse double from: " + s); + e.printStackTrace(); + } + return Double.NaN; + } + + private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { + + String line = ""; + String[] tokens; + + //find sample temperature + outer : for(line = reader.readLine(); line != null; line = reader.readLine()) { + + tokens = line.split(delims); + + for(String token : tokens) { + if(token.equalsIgnoreCase(label)) + break outer; + } + + } + + return line; + + } + + /** + * As this class uses the singleton pattern, only one instance is created using + * an empty no-argument constructor. + * + * @return the single instance of this class. + */ + + public static CurveReader getInstance() { + return instance; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java new file mode 100644 index 00000000..b6c259a7 --- /dev/null +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -0,0 +1,129 @@ +package pulse.io.readers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; +import java.util.Objects; + +import pulse.problem.laser.NumericPulseData; +import pulse.ui.Messages; + +public class NetzschPulseCSVReader implements PulseDataReader { + + private static PulseDataReader instance = new NetzschPulseCSVReader(); + private final static double TO_SECONDS = 1E-3; + + private final static String SHOT_DATA = "Shot_data"; + private final static String PULSE = "Laser_pulse_data"; + + private NetzschPulseCSVReader() { + // intentionally blank + } + + /** + * @return The supported extension ({@code .csv}). + */ + + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } + + @Override + public NumericPulseData read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + String delims = "[#();/°Cx%^]+"; + + NumericPulseData data; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + String[] shotID = reader.readLine().split(delims); + + int shotId = -1; + + //check if first entry makes sense + if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) + throw new IllegalArgumentException(file.getName() + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); + else + shotId = Integer.parseInt(shotID[shotID.length - 1]); + + data = new NumericPulseData(shotId); + + double time; + double power; + + var pulseLabel = findLineByLabel(reader, PULSE, delims); + + if(pulseLabel == null) { + System.err.println("Skipping " + file.getName()); + return null; + } + + reader.readLine(); + + String[] tokens; + + for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { + tokens = line.split(delims); + + time = parseDoubleWithComma(tokens[0]) * TO_SECONDS; + power = parseDoubleWithComma(tokens[1]); + data.addPoint(time, power); + } + + } + + return data; + + } + + private double parseDoubleWithComma(String s) { + var format = NumberFormat.getInstance(Locale.GERMANY); + try { + return format.parse(s).doubleValue(); + } catch (ParseException e) { + System.out.println("Couldn't parse double from: " + s); + e.printStackTrace(); + } + return Double.NaN; + } + + private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { + + String line = ""; + String[] tokens; + + //find sample temperature + outer : for(line = reader.readLine(); line != null; line = reader.readLine()) { + + tokens = line.split(delims); + + for(String token : tokens) { + if(token.equalsIgnoreCase(label)) + break outer; + } + + } + + return line; + + } + + /** + * As this class uses the singleton pattern, only one instance is created using + * an empty no-argument constructor. + * + * @return the single instance of this class. + */ + + public static PulseDataReader getInstance() { + return instance; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/io/readers/PulseDataReader.java b/src/main/java/pulse/io/readers/PulseDataReader.java new file mode 100644 index 00000000..dc012765 --- /dev/null +++ b/src/main/java/pulse/io/readers/PulseDataReader.java @@ -0,0 +1,12 @@ +package pulse.io.readers; + +import java.io.File; +import java.io.IOException; +import pulse.problem.laser.NumericPulseData; + +public interface PulseDataReader extends AbstractReader { + + @Override + public abstract NumericPulseData read(File file) throws IOException; + +} \ No newline at end of file diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index e8535467..b00b3775 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -41,6 +41,7 @@ public class ReaderManager { private static List allReaders = allReaders(); private static List allDataExtensions = supportedExtensions(ReaderManager.curveReaders()); + private static List allPulseExtensions = supportedExtensions(ReaderManager.pulseReaders()); private static List allDatasetExtensions = supportedExtensions(ReaderManager.datasetReaders()); private ReaderManager() { @@ -61,6 +62,10 @@ private static List supportedExtensions(List public static List getCurveExtensions() { return allDataExtensions; } + + public static List getPulseExtensions() { + return allPulseExtensions; + } /** * Returns a list of extensions recognised by the available @@ -112,6 +117,11 @@ public static List findCurveReaders(String pckgname) { .collect(Collectors.toList()); } + public static List findPulseReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof PulseDataReader).map(r -> (PulseDataReader) r) + .collect(Collectors.toList()); + } + /** * Finds all classes assignable from {@code CurveReader} within this * package. @@ -122,6 +132,10 @@ public static List findCurveReaders(String pckgname) { public static List curveReaders() { return findCurveReaders(ReaderManager.class.getPackage().getName()); } + + public static List pulseReaders() { + return findPulseReaders(ReaderManager.class.getPackage().getName()); + } /** * Finds all classes assignable from {@code DatasetReader} within the diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index d40350a4..0d4e5cab 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -50,7 +50,9 @@ public ParameterVector(ParameterVector proto, Vector v) { public ParameterVector(ParameterVector v) { this( v.dimension() ); - final int n = indices.length; + final int n = dimension(); + for(int i = 0; i < n; i++) + this.set(i, v.get(i)); System.arraycopy(v.indices, 0, indices, 0, n); System.arraycopy(v.transforms, 0, transforms, 0, n); System.arraycopy(v.bounds, 0, bounds, 0, n); @@ -70,7 +72,11 @@ private ParameterVector(final int n) { @Override public void set(final int i, final double x) { - final double t = transforms[i] == null ? x : transforms[i].transform(x); + set(i, x, false); + } + + public void set(final int i, final double x, boolean ignoreTransform) { + final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); super.set(i, t); } @@ -108,7 +114,7 @@ public double getParameterValue(NumericPropertyKeyword index) { } public double inverseTransform(final int i) { - return transforms[i].inverse( get(i) ); + return transforms[i] != null ? transforms[i].inverse( get(i) ) : get(i); } public Transformable getTransform(final int i) { diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 527a9804..cbec3bf2 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -3,6 +3,7 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; +import pulse.tasks.SearchTask; /** * A {@code DiscretePulse} is an object that acts as a medium between the @@ -37,12 +38,13 @@ public DiscretePulse(Problem problem, Grid grid) { this.pulse = problem.getPulse(); recalculate(); + + var data = ( (SearchTask) problem.specificAncestor(SearchTask.class) ).getExperimentalCurve(); - pulse.getPulseShape().init(this); + pulse.getPulseShape().init(data, this); pulse.addListener(e -> { recalculate(); - pulse.getPulseShape().init(this); - + pulse.getPulseShape().init(data, this); }); } @@ -55,7 +57,7 @@ public DiscretePulse(Problem problem, Grid grid) { */ public double laserPowerAt(double time) { - return pulse.evaluateAt(time); + return pulse.evaluateAt(time / timeFactor); } /** diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index ee3baed4..f80c37e0 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -6,16 +6,13 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA; import static pulse.properties.NumericPropertyKeyword.SKEW_MU; import static pulse.properties.NumericPropertyKeyword.SKEW_SIGMA; import java.util.List; -import pulse.math.FixedIntervalIntegrator; -import pulse.math.MidpointIntegrator; -import pulse.math.Segment; +import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -35,9 +32,6 @@ public class ExponentiallyModifiedGaussian extends PulseTemporalShape { private double lambda; private double norm; - private final static int DEFAULT_POINTS = 100; - private FixedIntervalIntegrator integrator; - /** * Creates an exponentially modified Gaussian with the default parameter values. */ @@ -47,23 +41,14 @@ public ExponentiallyModifiedGaussian() { lambda = (double) def(SKEW_LAMBDA).getValue(); sigma = (double) def(SKEW_SIGMA).getValue(); norm = 1.0; - integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), - derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { - - @Override - public double integrand(double... vars) { - return evaluateAt(vars[0]); - } - - }; } public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { + super(another); this.mu = another.mu; this.sigma = another.sigma; this.lambda = another.lambda; this.norm = another.norm; - this.integrator = another.integrator; } /** @@ -72,25 +57,13 @@ public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { */ @Override - public void init(DiscretePulse pulse) { - super.init(pulse); + public void init(ExperimentalData data, DiscretePulse pulse) { + super.init(data, pulse); norm = 1.0; // resets the normalisation factor to unity norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of // the area. } - /** - * Uses numeric integration (midpoint rule) to calculate the area of the pulse - * shape corresponding to the selected parameters. - * - * @return the area - */ - - private double area() { - integrator.setBounds(new Segment(0.0, getPulseWidth())); - return integrator.integrate(); - } - /** * Evaluates the laser power function. The error function is calculated using * the ApacheCommonsMath library tools. diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java new file mode 100644 index 00000000..d69f2b4e --- /dev/null +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -0,0 +1,107 @@ +package pulse.problem.laser; + +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; + +import pulse.input.ExperimentalData; +import pulse.problem.schemes.Grid; +import pulse.problem.statements.Problem; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.SearchTask; + +import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; +import static pulse.properties.NumericProperties.derive; + +public class NumericPulse extends PulseTemporalShape { + + private NumericPulseData pulseData; + private UnivariateFunction interpolation; + private double adjustedPulseWidth; + + public NumericPulse() { + //intentionally blank + } + + public NumericPulse(NumericPulse pulse) { + super(pulse); + this.pulseData = new NumericPulseData(pulseData); + } + + @Override + public void init(ExperimentalData data, DiscretePulse pulse) { + pulseData = data.getMetadata().getPulseData(); + + var problem = ((SearchTask)data.getParent()).getCurrentCalculation().getProblem(); + setPulseWidth(problem); + + double timeFactor = problem.getProperties().timeFactor(); + + super.init(data, pulse); + + doInterpolation( timeFactor ); + + normalise(problem); + } + + public void normalise(Problem problem) { + + final double EPS = 1E-2; + double timeFactor = problem.getProperties().timeFactor(); + + for( double area = area() ; Math.abs(area - 1.0) > EPS; area = area() ) { + pulseData.scale( 1.0 / area ); + doInterpolation( timeFactor ); + } + + } + + private void setPulseWidth(Problem problem) { + var timeSequence = pulseData.getTimeSequence(); + double pulseWidth = timeSequence.get( timeSequence.size() - 1 ); + + var pulseObject = problem.getPulse(); + pulseObject.setPulseWidth( derive(PULSE_WIDTH, pulseWidth) ); + + } + + private void doInterpolation(double timeFactor) { + var interpolator = new SplineInterpolator(); + + var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); + adjustedPulseWidth = timeList[timeList.length - 1]; + var powerList = pulseData.getSignalData(); + + interpolation = interpolator.interpolate(timeList, + powerList.stream().mapToDouble(d -> d).toArray()); + + } + + @Override + public double evaluateAt(double time) { + return time > adjustedPulseWidth ? 0.0 : interpolation.value(time); + } + + @Override + public PulseTemporalShape copy() { + return new NumericPulse(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // TODO Auto-generated method stub + } + + public NumericPulseData getData() { + return pulseData; + } + + public void setData(NumericPulseData pulseData) { + this.pulseData = pulseData; + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } + +} diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java new file mode 100644 index 00000000..ad388188 --- /dev/null +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -0,0 +1,45 @@ +package pulse.problem.laser; + +import pulse.AbstractData; + +public class NumericPulseData extends AbstractData { + + private int externalID; + + public NumericPulseData(int id) { + super(); + this.externalID = id; + } + + public NumericPulseData(NumericPulseData data) { + super(data); + this.externalID = data.externalID; + } + + @Override + public void addPoint(double time, double power) { + super.addPoint(time, power); + super.incrementCount(); + } + + /** + * Gets the external ID usually specified in the experimental files. Note this + * is not a {@code NumericProperty} + * + * @return an integer, representing the external ID + */ + + public int getExternalID() { + return externalID; + } + + public void scale(double factor) { + + var power = this.getSignalData(); + + for(int i = 0, size = power.size(); i < size; i++) + power.set(i, power.get(i) * factor ); + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index ed96b635..8697c726 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -1,5 +1,12 @@ package pulse.problem.laser; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; + +import pulse.input.ExperimentalData; +import pulse.math.FixedIntervalIntegrator; +import pulse.math.MidpointIntegrator; +import pulse.math.Segment; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -13,7 +20,43 @@ public abstract class PulseTemporalShape extends PropertyHolder implements Reflexive { private double width; + + private final static int DEFAULT_POINTS = 256; + private FixedIntervalIntegrator integrator; + + public PulseTemporalShape() { + //intentionlly blank + } + + public PulseTemporalShape(PulseTemporalShape another) { + this.integrator = another.integrator; + } + + public void initAreaIntegrator() { + integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), + derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { + @Override + public double integrand(double... vars) { + return evaluateAt(vars[0]); + } + + }; + } + + /** + * Uses numeric integration (midpoint rule) to calculate the area of the pulse + * shape corresponding to the selected parameters. + * + * @return the area + */ + + public double area() { + integrator.setBounds(new Segment(0.0, getPulseWidth())); + return integrator.integrate(); + } + + /** * This evaluates the dimensionless, discretised pulse function on a * {@code grid} needed to evaluate the heat source in the difference scheme. @@ -25,8 +68,9 @@ public abstract class PulseTemporalShape extends PropertyHolder implements Refle public abstract double evaluateAt(double time); - public void init(DiscretePulse pulse) { + public void init(ExperimentalData data, DiscretePulse pulse) { width = pulse.getDiscreteWidth(); + this.initAreaIntegrator(); } public abstract PulseTemporalShape copy(); diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index 36850248..fa88573b 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -8,6 +8,7 @@ import java.util.List; +import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -45,8 +46,8 @@ public TrapezoidalPulse(TrapezoidalPulse another) { } @Override - public void init(DiscretePulse pulse) { - super.init(pulse); + public void init(ExperimentalData data, DiscretePulse pulse) { + super.init(data, pulse); h = height(); } diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 62966949..8b63ce36 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -96,7 +96,9 @@ public NumericProperty getPulseWidth() { public void setPulseWidth(NumericProperty pulseWidth) { requireType(pulseWidth, PULSE_WIDTH); this.pulseWidth = (double) pulseWidth.getValue(); - firePropertyChanged(this, pulseWidth); + + if(pulseWidth.compareTo(this.getPulseWidth()) != 0) + firePropertyChanged(this, pulseWidth); } public NumericProperty getLaserEnergy() { diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index 5e4ef483..771fca56 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -17,19 +17,23 @@ public FIPSMover() { @Override public ParticleState attemptMove(Particle p, Particle[] neighbours) { - var current = p.getCurrentState(); - var pos = current.getPosition(); + var current = p.getCurrentState(); - final int n = pos.dimension(); - var nsum = new Vector(n); + var pos = current.getPosition(); - for(var neighbour : neighbours) - nsum = nsum.sum( Vector.random(n, 0.0, phi).multComponents( neighbour.getCurrentState().getPosition().subtract(pos) ) ); - - nsum = nsum.multiply(1.0/neighbours.length); + final int n = pos.dimension(); + var nsum = new Vector(n); + for(var neighbour : neighbours) { + var nPos = neighbour.getCurrentState().getPosition(); + nsum = nsum.sum( Vector.random(n, 0.0, phi).multComponents( nPos.subtract(pos) ) ); + } + + nsum = nsum.multiply(1.0/((double)neighbours.length)); + var newVelocity = ( current.getVelocity().sum(nsum) ).multiply(chi); var newPosition = pos.sum(newVelocity); + System.out.println(newPosition); return new ParticleState( new ParameterVector(pos, newPosition ), diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java index 1897784c..60a5cd71 100644 --- a/src/main/java/pulse/search/direction/pso/Particle.java +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -18,7 +18,6 @@ */ package pulse.search.direction.pso; -import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.SearchTask; @@ -35,12 +34,12 @@ public class Particle { private Particle[] neighbours; - public Particle(ParameterVector pv, int id) { + public Particle(ParticleState cur, int id) { this.id = id; - current = new ParticleState( pv, null ); + current = cur; pbest = new ParticleState(current); } - + public void adopt(ParticleState state) { this.current = state; } @@ -60,6 +59,7 @@ public void evaluate(SearchTask t) throws SolverException { * * @return current state. */ + public ParticleState getCurrentState() { return current; } diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index 4fc1a56b..c9fb52f6 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -1,7 +1,6 @@ package pulse.search.direction.pso; import pulse.math.ParameterVector; -import pulse.tasks.SearchTask; public class ParticleState { @@ -9,15 +8,20 @@ public class ParticleState { private ParameterVector velocity; private double fitness; - public ParticleState(SearchTask t) { - randomise(t); + public ParticleState(ParameterVector cur) { + randomise(cur); + this.velocity = new ParameterVector(cur); + + //set initial velocity to zero + for(int i = 0, n = velocity.dimension(); i < n; i++) + velocity.set(i, 0.0); + this.fitness = Double.MAX_VALUE; } public ParticleState(ParticleState another) { this.position = new ParameterVector(another.position); - if(another.velocity != null) - this.velocity = new ParameterVector(another.velocity); + this.velocity = new ParameterVector(another.velocity); this.fitness = another.fitness; } @@ -30,23 +34,22 @@ public boolean isBetterThan(ParticleState s) { return this.fitness < s.fitness; } - public void randomise(SearchTask task) { - - position = new ParameterVector( task.searchVector() ); - + public void randomise(ParameterVector pos) { + + this.position = new ParameterVector(pos); + for (int i = 0, n = position.dimension(); i < n; i++) { var bounds = position.getBounds(); - var t = position.getTransform(i); - double max = t.transform( bounds[i].getMaximum() ); - double min = t.transform( bounds[i].getMinimum() ); - + double max = bounds[i].getMaximum(); + double min = bounds[i].getMinimum(); + double value = min + Math.random() * ( max - min ); - position.set(i, value); + position.set(i, value ); } - + } public ParameterVector getPosition() { diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index 30288f07..ac6248a8 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -11,8 +11,8 @@ public class ParticleSwarmOptimiser extends PathOptimiser { private Mover mover; public ParticleSwarmOptimiser() { - this.swarmState = new SwarmState(); - mover = new FIPSMover(); + swarmState = new SwarmState(); + mover = new FIPSMover(); } protected void moveParticles() { @@ -31,9 +31,14 @@ protected void moveParticles() { public boolean iteration(SearchTask task) throws SolverException { this.prepare(task); - moveParticles(); swarmState.evaluate(task); + moveParticles(); + swarmState.incrementStep(); + + task.assign( swarmState.bestSoFar().getPosition() ); + task.solveProblemAndCalculateCost(); + return true; } @@ -44,6 +49,7 @@ public void prepare(SearchTask task) throws SolverException { @Override public IterativeState initState(SearchTask t) { + swarmState.prepare(t); swarmState.create(); return swarmState; } diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 29b99957..3ff0d0c8 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -4,9 +4,6 @@ public class StaticTopologies { - private StaticTopologies() { - //empty - } /** Global best * @@ -54,5 +51,9 @@ private StaticTopologies() { return new Particle[] { ps[left], ps[right], ps[above], ps[below] }; }; + private StaticTopologies() { + //empty + } + } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index ec971c05..afd784dc 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -1,13 +1,14 @@ package pulse.search.direction.pso; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; import pulse.search.direction.IterativeState; import pulse.tasks.SearchTask; public class SwarmState extends IterativeState { - private ParameterVector current; + private ParameterVector seed; private Particle[] particles; private NeighbourhoodTopology neighborhoodTopology; @@ -15,8 +16,10 @@ public class SwarmState extends IterativeState { private Particle bestSoFar; private int bestSoFarIndex; + private final static int DEFAULT_PARTICLES = 16; + public SwarmState() { - this(16, StaticTopologies.GLOBAL); + this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); } public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { @@ -32,12 +35,12 @@ public void evaluate(SearchTask t) throws SolverException { } public void prepare(SearchTask t) { - current = t.searchVector(); + seed = t.searchVector(); } public void create() { - for (int i = 0; i < particles.length; i++) - particles[i] = new Particle(current, i); + for (int i = 0; i < particles.length; i++) + particles[i] = new Particle(new ParticleState(seed), i); } /** @@ -48,17 +51,23 @@ public void create() { public ParticleState bestSoFar() { int bestIndex = 0; + + double fitness = 0; double bestFitness = Double.MAX_VALUE; - + for (int i = 0; i < particles.length; i++) { - if (particles[i].getBestState().getFitness() < bestFitness) { - bestIndex = i; - bestFitness = particles[i].getBestState().getFitness(); + + fitness = particles[i].getBestState().getFitness(); + + if (fitness < bestFitness) { + bestIndex = i; + bestFitness = fitness; } + } - this.bestSoFar = particles[bestIndex]; - this.bestSoFarIndex = bestIndex; + this.bestSoFar = particles[bestIndex]; + this.bestSoFarIndex = bestIndex; return bestSoFar.getBestState(); } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index b4ef1249..f3941d5a 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -133,7 +133,7 @@ public void execute(SearchTask t) { } else notifyListeners(e); }); - + } /** @@ -321,6 +321,20 @@ public SearchTask getTask(Identifier id) { var o = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); return o.isPresent() ? o.get() : null; } + + /** + * Finds a {@code SearchTask} using the external identifier specified in its metadata. + * + * @param externalId the external ID of the data. + * @return the {@code SearchTask} associated with this {@code Identifier}. + */ + + public SearchTask getTask(int externalId) { + var o = tasks.stream().filter(t -> + Integer.compare( t.getExperimentalCurve().getMetadata().getExternalID(), + externalId ) == 0 ).findFirst(); + return o.isPresent() ? o.get() : null; + } /** *

@@ -349,6 +363,7 @@ public void generateTask(File file) { public void generateTasks(List files) { requireNonNull(files, "Null list of files passed to generatesTasks(...)"); + var pool = Executors.newSingleThreadExecutor(); files.stream().forEach(f -> pool.submit(() -> generateTask(f))); pool.shutdown(); diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index f252b9f1..42ad45ec 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -1,7 +1,6 @@ package pulse.ui.components; -import static pulse.io.readers.ReaderManager.datasetReaders; -import static pulse.io.readers.ReaderManager.read; +import static pulse.io.readers.ReaderManager.*; import java.awt.Window; import java.io.File; @@ -19,6 +18,9 @@ import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; +import pulse.problem.laser.NumericPulse; +import pulse.problem.laser.NumericPulseData; +import pulse.problem.laser.RectangularPulse; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -60,7 +62,7 @@ private DataLoader() { public static void loadDataDialog() { var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), ReaderManager.getCurveExtensions()); - + if (files != null) { progressFrame.trackProgress(files.size()); TaskManager.getManagerInstance().generateTasks(files); @@ -123,6 +125,41 @@ public static void loadMetadataDialog() { instance.selectFirstTask(); } + + public static void loadPulseDialog() { + var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + ReaderManager.getPulseExtensions()); + + if (files != null) { + + var manager = TaskManager.getManagerInstance(); + + progressFrame.trackProgress(files.size()); + + //TODO replace with pool loading + for(var f : files) { + + NumericPulseData pulseData = read(pulseReaders(), f); + + if(pulseData != null) { + + var task = manager.getTask(pulseData.getExternalID()); + + if(task != null) { + + var metadata = task.getExperimentalCurve().getMetadata(); + metadata.setPulseData(pulseData); + metadata.getPulseDescriptor().setSelectedDescriptor(NumericPulse.class.getSimpleName()); + + } + + } + + } + + } + + } /** * Uses the {@code ReaderManager} to create an {@code InterpolationDataset} from diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index 6c6329db..32c86629 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -25,7 +25,7 @@ public class PulseChart extends AuxPlotter { - private final static int NUM_PULSE_POINTS = 200; + private final static int NUM_PULSE_POINTS = 600; private final static double TO_MILLIS = 1E3; public PulseChart(String xLabel, String yLabel) { diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 7d1d99a6..c601544c 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -17,8 +17,7 @@ import static pulse.search.statistics.OptimiserStatistic.setSelectedOptimiserDescriptor; import static pulse.tasks.TaskManager.getManagerInstance; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; -import static pulse.ui.components.DataLoader.loadDataDialog; -import static pulse.ui.components.DataLoader.loadMetadataDialog; +import static pulse.ui.components.DataLoader.*; import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.allDescriptors; @@ -65,6 +64,7 @@ public class PulseMainMenu extends JMenuBar { private static JMenuItem resultFormatItem; private static JMenuItem searchSettingsItem; private static JMenuItem loadMetadataItem; + private static JMenuItem loadPulseItem; private static JMenuItem modelSettingsItem; private static ExportDialog exportDialog = new ExportDialog(); @@ -127,6 +127,7 @@ private void initComponents() { fileMenu = new JMenu("File"); loadDataItem = new JMenuItem("Load Heating Curve(s)...", loadIcon("load.png", ICON_SIZE)); loadMetadataItem = new JMenuItem("Load Metadata...", loadIcon("metadata.png", ICON_SIZE)); + loadPulseItem = new JMenuItem("Load Pulse Measurement(s)...", loadIcon("pulse.png", ICON_SIZE)); exportCurrentItem = new JMenuItem("Export Current", loadIcon("save.png", ICON_SIZE)); exportAllItem = new JMenuItem("Export...", loadIcon("save.png", ICON_SIZE)); exitItem = new JMenuItem("Exit"); @@ -143,7 +144,8 @@ private void initComponents() { fileMenu.setMnemonic('f'); loadDataItem.setMnemonic('h'); - loadMetadataItem.setMnemonic('m'); + loadMetadataItem.setMnemonic('M'); + loadPulseItem.setMnemonic('P'); exportCurrentItem.setMnemonic('c'); exportAllItem.setMnemonic('e'); exitItem.setMnemonic('x'); @@ -151,6 +153,7 @@ private void initComponents() { settingsMenu.setMnemonic('s'); loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); exportCurrentItem.setEnabled(false); exportAllItem.setEnabled(false); modelSettingsItem.setEnabled(false); @@ -158,6 +161,7 @@ private void initComponents() { fileMenu.add(loadDataItem); fileMenu.add(loadMetadataItem); + fileMenu.add(loadPulseItem); fileMenu.add(new JSeparator()); fileMenu.add(exportCurrentItem); fileMenu.add(exportAllItem); @@ -296,8 +300,10 @@ private JMenu initAnalysisSubmenu() { private void assignMenuFunctions() { loadDataItem.addActionListener(e -> loadDataDialog()); loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); loadMetadataItem.addActionListener(e -> loadMetadataDialog()); - + loadPulseItem.addActionListener(e -> loadPulseDialog()); + modelSettingsItem.setEnabled(false); modelSettingsItem.addActionListener(e -> notifyProblem()); @@ -315,10 +321,12 @@ private void assignMenuFunctions() { getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { if (getManagerInstance().getTaskList().size() > 0) { loadMetadataItem.setEnabled(true); + loadPulseItem.setEnabled(true);; modelSettingsItem.setEnabled(true); searchSettingsItem.setEnabled(true); } else { loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); modelSettingsItem.setEnabled(false); searchSettingsItem.setEnabled(false); } diff --git a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java index 609b56d2..83deee7d 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java @@ -20,7 +20,7 @@ public class ProgressDialog extends JDialog implements PropertyChangeListener { public ProgressDialog() { super(); initComponents(); - setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + setDefaultCloseOperation(HIDE_ON_CLOSE); setTitle("Please wait..."); setPreferredSize(new Dimension(400, 75)); pack(); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index d9eb74d9..9d470fcc 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -53,6 +53,8 @@ Problem.7=Heating Curve DATReader.0=dat DATReader.1=No file selected when trying to load data\! DATReader.2=\ \t ; +NetzschCSVReader.0=csv +NetzschCSVReader.1=\ \t # ; LFRReader.0=lfr LFRReader.1=No file selected when trying to load data\! LFRReader.10=\ \t; From 0f7f91a226699967c09f1c19a4805e3ba6f6f4ab Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 8 Mar 2021 10:59:04 +0100 Subject: [PATCH 026/116] Improvements for pulse handling --- .../java/pulse/io/readers/NetzschCSVReader.java | 13 ++++++------- .../pulse/io/readers/NetzschPulseCSVReader.java | 11 +++++------ src/main/java/pulse/io/readers/PulseDataReader.java | 1 + .../java/pulse/problem/laser/DiscretePulse.java | 5 +++-- src/main/java/pulse/problem/laser/NumericPulse.java | 7 +++---- .../java/pulse/problem/schemes/MixedScheme.java | 2 +- .../schemes/solvers/ImplicitLinearisedSolver.java | 4 +--- .../problem/schemes/solvers/MixedCoupledSolver.java | 1 + .../schemes/solvers/MixedLinearisedSolver.java | 1 + src/main/java/pulse/problem/statements/Pulse.java | 4 ---- .../java/pulse/search/direction/pso/SwarmState.java | 1 - src/main/java/pulse/ui/components/DataLoader.java | 5 +++-- src/main/java/pulse/ui/components/PulseChart.java | 4 +++- .../java/pulse/ui/components/PulseMainMenu.java | 4 +++- 14 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index b608a1ba..5a936983 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -7,12 +7,9 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.text.NumberFormat; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.Objects; import pulse.input.ExperimentalData; @@ -63,7 +60,7 @@ public List read(File file) throws IOException { ExperimentalData curve = new ExperimentalData(); - String delims = "[#();/°Cx%^]+"; + String delims = "[#();,/°Cx%^]+"; try (BufferedReader reader = new BufferedReader(new FileReader(file))) { @@ -78,7 +75,7 @@ public List read(File file) throws IOException { shotId = Integer.parseInt(shotID[shotID.length - 1]); String[] tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); - double sampleTemperature = parseDoubleWithComma( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; + double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); curve.setMetadata(met); @@ -100,8 +97,8 @@ public List read(File file) throws IOException { for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - time = parseDoubleWithComma(tokens[0]) * TO_SECONDS; - temp = parseDoubleWithComma(tokens[1]); + time = Double.parseDouble(tokens[0]) * TO_SECONDS; + temp = Double.parseDouble(tokens[1]); curve.addPoint(time, temp); } curve.setRange(new Range(curve.getTimeSequence())); @@ -112,6 +109,7 @@ public List read(File file) throws IOException { } + /* private double parseDoubleWithComma(String s) { var format = NumberFormat.getInstance(Locale.GERMANY); try { @@ -122,6 +120,7 @@ private double parseDoubleWithComma(String s) { } return Double.NaN; } + */ private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index b6c259a7..481dacf3 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -4,9 +4,6 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.Locale; import java.util.Objects; import pulse.problem.laser.NumericPulseData; @@ -37,7 +34,7 @@ public String getSupportedExtension() { public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); - String delims = "[#();/°Cx%^]+"; + String delims = "[#(),;/°Cx%^]+"; NumericPulseData data; @@ -72,8 +69,8 @@ public NumericPulseData read(File file) throws IOException { for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - time = parseDoubleWithComma(tokens[0]) * TO_SECONDS; - power = parseDoubleWithComma(tokens[1]); + time = Double.parseDouble(tokens[0]) * TO_SECONDS; + power = Double.parseDouble(tokens[1]); data.addPoint(time, power); } @@ -83,6 +80,7 @@ public NumericPulseData read(File file) throws IOException { } + /* private double parseDoubleWithComma(String s) { var format = NumberFormat.getInstance(Locale.GERMANY); try { @@ -93,6 +91,7 @@ private double parseDoubleWithComma(String s) { } return Double.NaN; } + */ private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { diff --git a/src/main/java/pulse/io/readers/PulseDataReader.java b/src/main/java/pulse/io/readers/PulseDataReader.java index dc012765..a5e3f319 100644 --- a/src/main/java/pulse/io/readers/PulseDataReader.java +++ b/src/main/java/pulse/io/readers/PulseDataReader.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; + import pulse.problem.laser.NumericPulseData; public interface PulseDataReader extends AbstractReader { diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index cbec3bf2..68c88e42 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -33,8 +33,8 @@ public class DiscretePulse { */ public DiscretePulse(Problem problem, Grid grid) { - timeFactor = problem.getProperties().timeFactor(); this.grid = grid; + timeFactor = problem.getProperties().timeFactor(); this.pulse = problem.getPulse(); recalculate(); @@ -43,6 +43,7 @@ public DiscretePulse(Problem problem, Grid grid) { pulse.getPulseShape().init(data, this); pulse.addListener(e -> { + timeFactor = problem.getProperties().timeFactor(); recalculate(); pulse.getPulseShape().init(data, this); }); @@ -57,7 +58,7 @@ public DiscretePulse(Problem problem, Grid grid) { */ public double laserPowerAt(double time) { - return pulse.evaluateAt(time / timeFactor); + return pulse.getPulseShape().evaluateAt(time); } /** diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index d69f2b4e..4b1f85c4 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -1,18 +1,17 @@ package pulse.problem.laser; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; + import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; import pulse.input.ExperimentalData; -import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; -import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; -import static pulse.properties.NumericProperties.derive; - public class NumericPulse extends PulseTemporalShape { private NumericPulseData pulseData; diff --git a/src/main/java/pulse/problem/schemes/MixedScheme.java b/src/main/java/pulse/problem/schemes/MixedScheme.java index 976469b2..0b1a591d 100644 --- a/src/main/java/pulse/problem/schemes/MixedScheme.java +++ b/src/main/java/pulse/problem/schemes/MixedScheme.java @@ -60,7 +60,7 @@ public MixedScheme(NumericProperty N, NumericProperty timeFactor) { public MixedScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { super(N, timeFactor, timeLimit); } - + /** * Prints out the description of this problem type. * diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 5db66ff8..ad19bf89 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -55,8 +55,6 @@ public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver Date: Mon, 8 Mar 2021 15:17:53 +0100 Subject: [PATCH 027/116] Changed default number of digits after decimal point --- src/main/java/pulse/io/export/CurveExporter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/pulse/io/export/CurveExporter.java b/src/main/java/pulse/io/export/CurveExporter.java index 01e07968..a9b1b7ad 100644 --- a/src/main/java/pulse/io/export/CurveExporter.java +++ b/src/main/java/pulse/io/export/CurveExporter.java @@ -76,10 +76,10 @@ private void printHTML(AbstractData hc, FileOutputStream fos) { stream.print(""); t = hc.timeAt(i); - stream.printf("%.6f %n", t); + stream.printf("%.8f %n", t); stream.print("\t"); T = hc.signalAt(i); - stream.printf("%.6f %n", T); + stream.printf("%.8f %n", T); stream.println(""); } @@ -102,9 +102,9 @@ private void printCSV(AbstractData hc, FileOutputStream fos) { for (int i = 0; i < size; i++) { t = hc.timeAt(i); - stream.printf("%n%3.4f", t); + stream.printf("%n%3.8f", t); T = hc.signalAt(i); - stream.printf("\t%3.4f", T); + stream.printf("\t%3.8f", T); } } From 8117dd1ed4f74cf66d40411a95844a7bf7db9c15 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Thu, 18 Mar 2021 13:33:17 +0100 Subject: [PATCH 028/116] v 1.90 release --- pom.xml | 2 +- .../pulse/io/export/ResidualStatisticExporter.java | 6 +++--- .../direction/pso/ParticleSwarmOptimiser.java | 13 +++++++------ src/main/resources/NumericProperty.xml | 4 ++-- src/main/resources/Version.txt | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index afbf0f0c..873852c6 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 kotik-coder PULsE - 1.88 + 1.90 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index 9770c5da..f65d08d0 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -77,7 +77,7 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { for (int i = 0; i < residualsLength; i++) { double tr = residuals.get(i)[0]; double Tr = residuals.get(i)[1]; - stream.printf("%n%.6f%.6f", tr, Tr); + stream.printf("%n%.8f%.8f", tr, Tr); } stream.print(""); @@ -95,9 +95,9 @@ private void printCSV(ResidualStatistic hc, FileOutputStream fos) { double tr, Tr; for (int i = 0; i < residualsLength; i++) { tr = residuals.get(i)[0]; - stream.printf("%n%3.4f", tr); + stream.printf("%n%3.8f", tr); Tr = residuals.get(i)[1]; - stream.printf("\t%3.4f", Tr); + stream.printf("\t%3.8f", Tr); } } diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index ac6248a8..edbde3fe 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -1,11 +1,8 @@ package pulse.search.direction.pso; -import pulse.problem.schemes.solvers.SolverException; -import pulse.search.direction.IterativeState; -import pulse.search.direction.PathOptimiser; -import pulse.tasks.SearchTask; - -public class ParticleSwarmOptimiser extends PathOptimiser { +public class ParticleSwarmOptimiser + //extends PathOptimiser + { private SwarmState swarmState; private Mover mover; @@ -27,6 +24,7 @@ protected void moveParticles() { * @param max_iterations max number of iterations to be computed by the swarm. */ + /* @Override public boolean iteration(SearchTask task) throws SolverException { this.prepare(task); @@ -41,7 +39,9 @@ public boolean iteration(SearchTask task) throws SolverException { return true; } + */ + /* @Override public void prepare(SearchTask task) throws SolverException { swarmState.prepare(task); @@ -53,5 +53,6 @@ public IterativeState initState(SearchTask t) { swarmState.create(); return swarmState; } + */ } \ No newline at end of file diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 58ddeb87..58e4a47e 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -431,8 +431,8 @@ default-search-variable="false" /> Date: Thu, 18 Mar 2021 13:33:35 +0100 Subject: [PATCH 029/116] splash screen update --- src/main/resources/images/splash.png | Bin 58378 -> 59748 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index 489632b00d00348ecd0260d755eaaaaa54b1bd3e..32a1c51d6909c50994a44619e0af57eed56660b5 100644 GIT binary patch literal 59748 zcmYg$V|b-Yuyt(Pw#`X)Y)))rV%xTDPHatVPB^h`+sT)E&zy6=C&~Mx_fB_pRjsPk ztKV=1IdOOx92g)VAb3d$k?%l2V7Nd)z+zAkUn8^@sZu~dXyhKs>Q3JcT#4))Y)yYy znGiX-+nErVxcx8%0&-ia%J|{3_A5c`(+Is2Y#f*l5{hWjo%d+}5#ChFq}5oBFE@qG zB*HjA0B#eAXYKPnHu!V9Vjn};n&st;??%Ne+x^;fU~$pQr$jw>vj*^RoBH{_?^gQ$ z&T*^hMqh(kzd@*tXY1>|wMzi^wC{#>$?Dz#>5;3yby)F~{r>)JpMW=2vs=~$nell4 zbBY)s=iCkX=P`Inc>{3os9A&g$?L^UBy!AVqwLT{p8WJiP=k&3DQR-8`7~i)Ej{(B zEw)vR;f^RK&_#E-%RWR1dPhD&c-c2KQ}cjb$k;n|>Mi!$^)4hvwleD_D z=uIR#(^i(%#KxL<-sz82%EfKDWV+!5Cu|KjZTO8*p+t^G<@Lssx;~>CfdT`Gn@Gv9wtvVyFMc;i(MCP5~;i8_#1|1h;Eog9Jqeu zu(G6BlI-qa=jAmV(#0rSK8(9h?u@gE{+6kCx9OA2Vg9aeJ2vsh?BZi=t$R$%d{=Lu z{!{57pyc9{)X{(LozHgD_?&C%II&I*(?iB6|Mh}ela4OzsB$^9xIeYIvf-$9yjC^F zC0y;d+`?j+X(39EDcpt`v+Yh+MfZ2{NW_Cf{mH|)%ZaErIpOD822zi7{i-si7daz- z&?x)i2iN@^It-i2WLBxO-y4(}Da*mJWQOh5HS{sXrjZocq0O4^N8!<^`$c7j&SlvG zOhXH^9uwIjm)|Sr42A&~=R+58TJ`pB9$ef)26p08AJ2z|@ zca~l+uJNv_TtBzT4#)Wwjc4~;p(fTRsBVvpLcGoB0u8#%(pf%%^;M_j@+Be<$w|`L zCYA`cupLS`PpJ|go8T3uC|k?KPBAq-<3#6&ij-BexW+gu<*q}}JOPM!T^WuA4PL|r z1}~l_?TXL=R6?Ll(uQs*>Y{JrRrk$#KI~z$)D{#5AT zs5q~Qd3!ZM&eO0{udPJ7p&IFnu&>(Sp5-@pFFn7QhO4%N*1p^2OZCSx45;**CsBM~ zos?X-O@@lx#PYaHKC*VMTMGePyX36Zm4N%6>$%&BI^snr5@mQv*BTBfsJU z5+f;;g>Go3lenzPQ~=nzX083FZ?BcGRn5=>e-bZc+dil*wl57{DJ;-jc;|L_jW#BT8uo(Xc$aJBrp>)ZnS9vZq>GlL$s0!v#n1=<;=sAgV^F2a4lX7 z4jGS|o+vcRbin0YgtBk{sXfglsGhfNF+U%c?1NShCYjHx44UCT6Jr54Jxz{-bbNl| z0a4&jKAC*(he)>Ry)tPg^jfUrB>SWAVk!TySXq^1tLQUjT{3pggRBHJu?!S%+i^)x zcm9nRK8fUa{o_Cp9-H-AnSY=Jr4cdpq1X47rXDB%D;BL2|bW)qJZv!9Y#KJA6Eo~ zIm17)+ra3oL9h68)a{Wv?m^BJd~9jjF9|dWOZ|RQuQN{dr5%Jwl2p1_U<^&6q4>tz=QHO;dc@?>{NPRmn zik+SjK?K`gG(c@2T$zU=cYSD5GH6tRa~^$_fDdH!&tsB!?6sl|6pM<6?qft!xtaNe zgl;c4+2a~la?r{|z1F?!&3PObItppUK0(6Yr45Lci^cecenRn8qzCHlAZL7}KzfrAyz8dT_%SFbWI zvL`#R5UgJfG3-9aNl0jIsf)tUh-7HSTy54X+Ij`xT<|c`S6IdKV})3q41XZ&o0$Zn zxzFB7`#(kLgL>4{lTyp1tKLjr#^Ci#JGT>SYij9hio6Uc4KD}eaDaV+LyZYC^N<=& zsG=pzO;07VZ6rV9b+~FVY4U}wq1e$5?n(!O;ujprv$fB}dsvu;zgs{86kn2vOLMe$ zRsE8BJYWrM?P}=nk~KsKqj71_XA4l$XC�p+iVVnpzowx%J~DIk|00&aI=78_nwU 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 literal 58378 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 z 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 Date: Fri, 19 Mar 2021 12:03:08 +0100 Subject: [PATCH 030/116] debug false --- src/main/java/pulse/ui/Launcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 38d995a8..1c5f1ad3 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -34,7 +34,7 @@ public class Launcher { private PrintStream errStream; private File errorLog; - private final static boolean DEBUG = true; + private final static boolean DEBUG = false; private Launcher() { arrangeErrorOutput(); From a12fa550f19043df2a4906f43799ab0adaf5bf11 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 24 May 2021 16:54:53 +0200 Subject: [PATCH 031/116] Mostly Javadoc changes --- src/main/java/pulse/AbstractData.java | 53 ++++++++++--- src/main/java/pulse/HeatingCurve.java | 74 ++++++++++++++----- src/main/java/pulse/HeatingCurveListener.java | 2 +- src/main/java/pulse/baseline/Baseline.java | 4 +- .../java/pulse/baseline/FlatBaseline.java | 10 +-- .../pulse/baseline/SinusoidalBaseline.java | 5 ++ .../java/pulse/input/ExperimentalData.java | 18 ++--- src/main/java/pulse/input/IndexRange.java | 4 +- .../pulse/input/InterpolationDataset.java | 14 +++- src/main/java/pulse/input/Metadata.java | 10 +++ .../pulse/input/listeners/CurveEvent.java | 6 ++ .../pulse/input/listeners/CurveEventType.java | 7 +- .../pulse/input/listeners/DataEventType.java | 6 +- .../pulse/input/listeners/DataListener.java | 4 +- .../listeners/ExternalDatasetListener.java | 16 +++- .../pulse/input/listeners/package-info.java | 7 +- .../java/pulse/io/export/CurveExporter.java | 4 +- .../java/pulse/io/export/ExportManager.java | 6 +- src/main/java/pulse/io/export/Exporter.java | 5 +- .../java/pulse/io/export/RawDataExporter.java | 2 +- .../java/pulse/io/export/ResultExporter.java | 2 +- src/main/java/pulse/package-info.java | 2 +- .../statements/ClassicalProblem2D.java | 10 ++- src/main/java/pulse/ui/components/Chart.java | 2 +- 24 files changed, 191 insertions(+), 82 deletions(-) diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index 0f4a7a48..dd652164 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -42,6 +42,11 @@ protected AbstractData(List time, String name) { this.name = name; } + /** + * Copy constructor. Copies all data and assigns the same name to {@code this}. + * @param d another instance of this class + */ + public AbstractData(AbstractData d) { this.time = new ArrayList<>(d.time); this.signal = new ArrayList<>(d.signal); @@ -145,21 +150,27 @@ public double timeLimit() { } /** - * Retrieves the baseline-subtracted temperature corresponding to - * {@code index} in the respective {@code List}. + * Retrieves the signal value corresponding to the index {@code index}. Is overriden by + * subclasses. * * @param index the index of the element - * @return a double, respresenting the baseline-subtracted temperature at + * @return a double, representing the signal at * {@code index} */ public double signalAt(int index) { return signal.get(index); } + + /** + * Adds a time-signal pair to the lists. + * @param time the time value + * @param sgn the signal value at {@code time} + */ - public void addPoint(double time, double temperature) { + public void addPoint(double time, double sgn) { this.time.add(time); - this.signal.add(temperature); + this.signal.add(sgn); } protected void incrementCount() { @@ -179,20 +190,31 @@ public void setTimeAt(int index, double t) { } /** - * Sets the temperature {@code t} at the position {@code index} of the - * {@code temperature List}. + * Sets the signal {@code t} at the position {@code index} of the + * {@code signal List}. * * @param index the index - * @param t the new temperature value at this index + * @param t the new signal value at this index */ public void setSignalAt(int index, double t) { signal.set(index, t); } + /** + * Calculates the simple maximum signal. + * @return the maximum signal value + * @see java.util.Collections.max + */ + public double apparentMaximum() { return max(signal); } + + /** + * Checks if the time list is incomplete. + * @return {@code false} if the list with time values has less elements than initially declared, {@code true} otherwise. + */ public boolean isIncomplete() { return time.size() < count; @@ -225,6 +247,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == NUMPOINTS) setNumPoints(property); } + + /** + * Lists {@code NUM_POINTS} as an accessible property of this {@code PropertyHolder}. + */ @Override public List listedTypes() { @@ -232,7 +258,7 @@ public List listedTypes() { } /** - * Removes an element with the index {@code i} from the time-temperature lists. + * Removes a time-value pair that is presend under the index {@code i}. * * @param i the element to be removed */ @@ -242,6 +268,10 @@ public void remove(int i) { this.signal.remove(i); } + /** + * @return true + */ + @Override public boolean ignoreSiblings() { return true; @@ -255,6 +285,11 @@ public List getSignalData() { return signal; } + /** + * @return {@code true} only if {@code o} is an {@code AbstractData} containing all the elements + * of the time and signal lists of {@code this} object. + */ + @Override public boolean equals(Object o) { if (o == this) diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index a43c09c2..8d36cfef 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -25,7 +25,14 @@ /** * The {@code HeatingCurve} represents a time-temperature profile (a {@code AbstractData} instance) - * generated using a finite-difference calculation algorithm. + * generated using a calculation algorithm implemented by a {@code Problem}'s {@code Solver}. In addition + * to the time and signal lists defined in the super-class, it features baseline-corrected signal values + * stored in a separate list.The {@code HeatingCurve} may have {@code HeatingCurveListener}s to process + * simple events. To enable comparison with {@code ExperimentalData}, a {@code HeatingCurve} builds a + * spline interpolation of its time - baseline-adjusted signal values, thus representing a continuous curve, + * rather than just a collection of discrete data. + * @see pulse.HeatingCurveListener + * @see org.apache.commons.math3.analysis.interpolation.UnivariateInterpolation * */ @@ -45,12 +52,22 @@ protected HeatingCurve(List time, List signal, final double star this.startTime = startTime; } + /** + * Calls the super-constructor and initialises the baseline-corrected signal list. Creates a {@code SplineInterpolator} object. + */ + public HeatingCurve() { super(); adjustedSignal = new ArrayList((int)this.getNumPoints().getValue()); splineInterpolator = new SplineInterpolator(); } + /** + * Copy constructor. In addition to copying the data, also re-builds the splines. + * @param c another instance of this class + * @see refreshInterpolation() + */ + public HeatingCurve(HeatingCurve c) { super(c); this.adjustedSignal = new ArrayList<>(c.adjustedSignal); @@ -62,8 +79,8 @@ public HeatingCurve(HeatingCurve c) { /** * Creates a {@code HeatingCurve}, where the number of elements in the - * {@code time} and {@code temperature} collections are set to - * {@code count.getValue()}. + * {@code time}, {@code signal}, and {@code adjustedSignal} collections are set to + * {@code count.getValue()}. The time shift is initialized with a default value. * * @param count The {@code NumericProperty} that is derived from the * {@code NumericPropertyKeyword.NUMPOINTS}. @@ -86,7 +103,8 @@ public void clear() { } /** - * Retrieves the time from the stored list of values, adding the value of {@code startTime} to the result + * Retrieves the time from the stored list of values, adding the value of {@code startTime} to the result. + * @return time at {@code index} + startTime * */ @@ -96,11 +114,11 @@ public double timeAt(int index) { } /** - * Retrieves the baseline-subtracted temperature corresponding to + * Retrieves the baseline-corrected temperature corresponding to * {@code index} in the respective {@code List}. * * @param index the index of the element - * @return a double, respresenting the baseline-subtracted temperature at + * @return a double, representing the baseline-corrected temperature at * {@code index} */ @@ -115,15 +133,19 @@ public double signalAt(int index) { * where T is the current temperature value at this index. Finally. applies the * baseline to the scaled temperature values. *

- * This method is used in the DifferenceScheme classes when a dimensionless + * This method is used in the DifferenceScheme subclasses when a dimensionless * solution needs to be re-scaled to the given maximum temperature (usually * matching the {@code ExperimentalData}, but also used as a search variable by * the {@code SearchTask}. + *

+ * Triggers a {@code RESCALED} {@code CurveEvent}. + *

* * @param scale the scale * @see pulse.problem.schemes.DifferenceScheme * @see pulse.problem.statements.Problem * @see pulse.tasks.SearchTask + * @see pulse.input.listeners.CurveEvent */ public void scale(double scale) { @@ -171,10 +193,10 @@ private void refreshInterpolation() { } /** - * Retrieves the absolute maximum (in arbitrary untis) of the - * baseline-subtracted temperature list. + * Retrieves the simple maximum (in arbitrary units) of the + * baseline-corrected temperature list. * - * @return the absolute maximum of the baseline-adjusted temperature. + * @return the simple maximum of the baseline-adjusted temperature. */ public double maxAdjustedSignal() { @@ -182,13 +204,12 @@ public double maxAdjustedSignal() { } /** - * Subtracts the baseline values from each element of the {@code temperature} + * Adds the baseline value to each element of the {@code signal} * list. *

- * The baseline.valueAt(...) is explicitly invoked for all {@code time} values, - * and the result of subtracting the baseline value from the corresponding - * {@code temperature} is assigned to a position in the - * {@code baselineAdjustedTemperature} list. + * The {@code baseline.valueAt} method is explicitly invoked for all {@code time} values, + * and the result of adding the baseline value to the corresponding + * {@code signal} is assigned to a position in the {@code adjustedSignal} list. *

* * @param baseline the baseline. Note it may not specifically belong to this @@ -248,10 +269,8 @@ public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { } /** - * Provides general setter accessibility for the number of points of this - * {@code HeatingCurve}. + * Calls {@code super.set} and provides write access to the {@code TIME_SHIFT} property. * - * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} * @param property the property of the type * {@code NumericPropertyKeyword.NUMPOINTS} */ @@ -262,6 +281,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == TIME_SHIFT) setTimeShift(property); } + + /** + * Lists {@code TIME_SHIFT} and {@code NUM_POINTS}. + */ @Override public List listedTypes() { @@ -272,7 +295,7 @@ public List listedTypes() { /** * Removes an element with the index {@code i} from all three {@code List}s - * (time, temperature, and baseline-subtracted temperature). + * (time, signal, and baseline-corrected signal). * * @param i the element to be removed */ @@ -281,10 +304,21 @@ public void remove(int i) { super.remove(i); this.adjustedSignal.remove(i); } + + /** + * The time shift is the position of the 'zero-time'. + * @return a {@code TIME_SHIFT} property + */ public NumericProperty getTimeShift() { return derive(TIME_SHIFT, startTime); } + + /** + * Sets the time shift and triggers {@code TIME_ORIGIN_CHANGED} in {@code CurveEvent}. Triggers the + * {@code firePropertyChanged}. + * @param startTime the new start time value + */ public void setTimeShift(NumericProperty startTime) { requireType(startTime, TIME_SHIFT); @@ -298,7 +332,7 @@ public UnivariateFunction getSplineInterpolation() { return splineInterpolation; } - public List getAlteredSignalData() { + public List getBaselineCorrectedData() { return adjustedSignal; } diff --git a/src/main/java/pulse/HeatingCurveListener.java b/src/main/java/pulse/HeatingCurveListener.java index 3c0d6a18..bfa5bbe8 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -10,7 +10,7 @@ public interface HeatingCurveListener { /** - * Signals that the {@code HeatingCurve} has been rescaled. + * Signals that a {@code CurveEvent} has occurred. */ public void onCurveEvent(CurveEvent event); diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index b24ccaf8..fc225cb8 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -41,10 +41,10 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti /** * Calculates the baseline parameters based on input arguments. *

- * This will run a simple least-squares estimation of the parameters of this + * This usually runs a simple least-squares estimation of the parameters of this * baseline using the specified {@code data} within the time range * {@code rangeMin < t < rangeMax}. If no data is available, the method will NOT - * change the {@code intercept} and {@code slope} values. Upon completion, the + * change the baseline parameters. Upon completion, the * method will use the respective {@code set} methods of this class to update * the parameter values, triggering whatever events are associated with them. *

diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 5dd2bb57..8eea0bad 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -62,12 +62,8 @@ protected void doFit(List x, List y, int size) { } protected double mean(List x) { - double sum = 0.0; - final double len = x.size(); - for (int i = 0; i < len; i++) { - sum += x.get(i); - } - return sum / len; + double sum = x.stream().reduce( (a, b) -> a + b).get(); + return sum / x.size(); } /** @@ -121,7 +117,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { this.firePropertyChanged(this, property); } } - + @Override public void optimisationVector(ParameterVector output, List flags) { for (int i = 0, size = output.dimension(); i < size; i++) { diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index 44911aba..1b62a077 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -119,6 +119,11 @@ public void setPhaseShift(NumericProperty phaseShift) { firePropertyChanged(this, phaseShift); } + /** + * The optimisation vector can include the amplitude, frequency and phase shift of a sinusoid, and + * a baseline intercept value of the superclass. + */ + @Override public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 63c486ef..5291ec60 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -65,8 +65,8 @@ public class ExperimentalData extends AbstractData { /** * Constructs an {@code ExperimentalData} object using the superclass - * constructor and rejecting the responsibility for the {@code baseline}, making - * its parent {@code null}. The number of points is set to zero by default. + * constructor and creating a new list of data listeners. The number of points is set to zero by default, + * and a new {@code IndexRange} is initialized. * */ @@ -114,12 +114,8 @@ public String toString() { } /** - * Adds {@code time} and {@code temperature} to the respective {@code List}s. - *

- * Note that the {@code baselineAdjustedTemperature} will be the same as the - * corresponding {@code temperature}, i.e. no baseline subtraction is performed. - * Upon completion, the {@code count} variable will be incremented. - *

+ * Adds {@code time} and {@code temperature} to the respective {@code List}s. Increments the counter of points. + * Note that no baseline correction is performed. * * @param time the next time value * @param signal the next signal value @@ -189,8 +185,8 @@ public List runningAverage(int reductionFactor) { } /** - * Instead of returning the absolute maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the (absolute) maximum of the + * Instead of returning the simple maximum (which can be an outlier!) of the + * temperature, this overriden method calculates the maximum of the * {@code runningAverage} using the default reduction factor * {@value REDUCTION_FACTOR}. * @@ -213,7 +209,7 @@ public double maxAdjustedSignal() { * to the closest temperature value available for that curve is used to retrieve * the half-rise time (which also has the same index). If this fails, i.e. the * associated index is less than 1, this will print out a warning message and - * still return a value equal to the acquistion time divided by a fail-safe + * still return a value equal to the acquisition time divided by a fail-safe * factor {@value FAIL_SAFE_FACTOR}. *

* diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 5ebe0b26..cb817404 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -163,7 +163,7 @@ public boolean isValid() { *

* any integer greater than 0 and lesser than {@code in.size} that * matches the above criterion. If {@code of} is greater than the last - * elemennt of {@code in}, this will return the latter. Otherwise, if no + * element of {@code in}, this will return the latter. Otherwise, if no * element matching the criterion is found, returns 0. *

*/ @@ -187,7 +187,7 @@ public static int closestLeft(double of, List in) { *

* any integer greater than 0 and lesser than {@code in.size} that * matches the above criterion. If {@code of} is greater than the last - * elemennt of {@code in}, this will return the latter. Otherwise, if no + * element of {@code in}, this will return the latter. Otherwise, if no * element matching the criterion is found, returns 0. *

*/ diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 87642d88..2146532c 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -22,7 +22,8 @@ * 'value') and provides means to interpolate between the 'values' using the * 'keys'. This is used mainly to interpolate between available data for thermal * properties loaded in tabular representation, e.g. the density and specific - * heat tables. + * heat tables. Features a static list of {@code ExternalDatasetListener}s. + * @see pulse.input.listeners.ExternalDatasetListener */ public class InterpolationDataset { @@ -99,7 +100,7 @@ public static InterpolationDataset getDataset(StandartType type) { /** * Puts a datset specified by {@code type} into the static hash map of this - * class, using {@code type} as key + * class, using {@code type} as key. Triggers {@code onDensityDataLoaded} * * @param dataset a dataset to be appended to the static hash map * @param type the dataset type @@ -107,8 +108,15 @@ public static InterpolationDataset getDataset(StandartType type) { public static void setDataset(InterpolationDataset dataset, StandartType type) { standartDatasets.put(type, dataset); - listeners.stream().forEach(l -> l.onDensityDataLoaded(type)); + listeners.stream().forEach(l -> l.onDataLoaded(type)); } + + /** + * Creates a list of property keywords that can be derived with help of the loaded data. + * For example, if heat capacity and density data is available, the returned list will contain + * {@code CONDUCTIVITY}. + * @return + */ public static List derivableProperties() { var list = new ArrayList(); diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index 2cf102c8..de0c8d5f 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -125,6 +125,11 @@ public void setPulseData(NumericPulseData pulseData) { this.pulseData = pulseData; } + /** + * If a Numerical Pulse has been loaded (for example, when importing from Proteus), this will return + * an object describing this data. + */ + public NumericPulseData getPulseData() { return pulseData; } @@ -179,6 +184,11 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } + /** + * The listed types include {@code TEST_TEMPERATURE}, {@code THICKNESS}, {@code DIAMETER}, {@code PULSE_WIDTH}, {@code SPOT_DIAMETER}, + * {@code LASER_ENERGY}, {@code DETECTOR_GAIN}, {@code DETECTOR_IRIS}, sample name and the types listed by the pulse descriptor. + */ + @Override public List listedTypes() { List list = new ArrayList<>(9); diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index feeadad0..c7f35001 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -2,6 +2,12 @@ import pulse.AbstractData; +/** + * A {@code CurveEvent} is associated with an {@code AbstractData} object. + * @see pulse.AbstractData + * + */ + public class CurveEvent { private CurveEventType type; diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 3d4f7de2..06173cce 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -2,11 +2,14 @@ public enum CurveEventType { + /** + * Indicates the curve signal values have been re-scaled. + */ + RESCALED, /** - *

- * Signal a time shift between the time sequences of a {@code HeatingCurve} and + * Indicates a new time shift is introduced between the time sequences of a {@code HeatingCurve} and * its linked {@code ExperimentalData}. Triggered either when manually changing * the time origin of the solution (i.e., shifting it relative to the * experimental data points) or by the search procedure. diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index 5106b4bf..9e6ef953 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -1,11 +1,7 @@ package pulse.input.listeners; /** - *

- * This is an enum type that is used to store some information about the type of - * the {@code DataEvent}s occurring with a {@code HeatingCurve} object. This is - * currently limited to two types of events. - *

+ * An event that is associated with an {@code ExperimentalData} object. * */ diff --git a/src/main/java/pulse/input/listeners/DataListener.java b/src/main/java/pulse/input/listeners/DataListener.java index 44839288..14af8220 100644 --- a/src/main/java/pulse/input/listeners/DataListener.java +++ b/src/main/java/pulse/input/listeners/DataListener.java @@ -2,7 +2,7 @@ /** * A listener interface, which is used to listen to {@code DataEvent}s occurring - * with an {@code HeatingCurve object}. + * with an {@code ExperimentalData} object. * */ @@ -10,7 +10,7 @@ public interface DataListener { /** * Triggered when a certain {@code DataEvent} specified by its - * {@code DataEventType} is initiated from within the {@code HeatingCurve} + * {@code DataEventType} is initiated from within the {@code ExperimentalData} * object. * * @param e the event object. diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java index d6fb473e..5279f846 100644 --- a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -2,6 +2,20 @@ import pulse.input.InterpolationDataset.StandartType; +/** + * A listener associated with the {@code InterpolationDataset} static repository of interpolations. + * + */ + public interface ExternalDatasetListener { - public void onDensityDataLoaded(StandartType type); + + + /** + * Triggered when a data {@code type} has been loaded. + * @param type a type of the dataset, for which an interpolation is created. + */ + + public void onDataLoaded(StandartType type); + + } \ No newline at end of file diff --git a/src/main/java/pulse/input/listeners/package-info.java b/src/main/java/pulse/input/listeners/package-info.java index 64db8c6d..e332df25 100644 --- a/src/main/java/pulse/input/listeners/package-info.java +++ b/src/main/java/pulse/input/listeners/package-info.java @@ -1,8 +1,9 @@ /** - * Package contains listeners and associated types which are used to track any + * Package contains listeners and event types which are used to track any * runtime changes with the internal data structures defined in - * {@code pulse.input}. This currently only includes a listener for - * {@code ExperimentalData}, since the latter can be truncated during runtime. + * {@code pulse} and {@code pulse.input}. + * @see pulse + * @see pulse.input */ package pulse.input.listeners; \ No newline at end of file diff --git a/src/main/java/pulse/io/export/CurveExporter.java b/src/main/java/pulse/io/export/CurveExporter.java index a9b1b7ad..60835fd5 100644 --- a/src/main/java/pulse/io/export/CurveExporter.java +++ b/src/main/java/pulse/io/export/CurveExporter.java @@ -10,7 +10,7 @@ import pulse.AbstractData; /** - * A singleton exporter allows writing the data contained in a heating curve in + * A singleton exporter allows writing the data contained in a {@code AbstractData} object in * a two-column format to create files conforming to either csv or html * extension. The first column always represents the time sequence, which may be * shifted if the associated property of the heating curve is non-zero. The @@ -121,7 +121,7 @@ public static CurveExporter getInstance() { } /** - * @return the {@code HeatingCurve} class. + * @return the {@code AbstractData} class. */ @Override diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 330dd986..15ada634 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -46,16 +46,16 @@ public static Exporter findExporter(T target) { /** * Finds an exporter that can work with {@code target}. *

- * Searches through available instances of the Exporter class contained in this + * Searches through available instances of the {@code Exporter} class contained in this * package and checks if any of those have their target set to the argument of - * this method, return the first occurrence. If nothing matches exactly the same + * this method, then returns the first occurrence. If nothing matches exactly the same * class as specified, searches for exporters of any classes assignable from * {@code target}. *

* * @param an instance of {@code Descriptive} * @param target the target glass - * @return an intancce of the Exporter class that can work worth the type T, + * @return an instance of the Exporter class that can work worth the type T, * null if nothing has been found */ diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index f90af465..80c2d91d 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -21,10 +21,9 @@ public interface Exporter extends Reflexive { /** - * Gets the default export extension. If not overriden, will return - * {@code Extension.CSV}. + * Gets the default export extension. * - * @return the default export extension + * @return {@code Extension.CSV} by default */ public static Extension getDefaultExportExtension() { diff --git a/src/main/java/pulse/io/export/RawDataExporter.java b/src/main/java/pulse/io/export/RawDataExporter.java index 11a8452e..4ffb0812 100644 --- a/src/main/java/pulse/io/export/RawDataExporter.java +++ b/src/main/java/pulse/io/export/RawDataExporter.java @@ -7,7 +7,7 @@ /** * A wrapper singleton class that is made specifically to handle export requests * of {@code ExperimentalData}. Does exactly the same as the - * {@code HeatingCurveExporter}, except that its target is specifically set to + * {@code CurveExporter}, except that its target is specifically set to * {@code ExperimentalData}. * * @see pulse.ui.frames.dialogs.ExportDialog diff --git a/src/main/java/pulse/io/export/ResultExporter.java b/src/main/java/pulse/io/export/ResultExporter.java index 4c2f9ce0..889c21fc 100644 --- a/src/main/java/pulse/io/export/ResultExporter.java +++ b/src/main/java/pulse/io/export/ResultExporter.java @@ -10,7 +10,7 @@ import pulse.ui.Messages; /** - * Provides export capabilities for instances of the {@code Result} class both + * Provides export capabilities, for instances, of the {@code Result} class both * in the {@code csv} and {@code html} formats. * */ diff --git a/src/main/java/pulse/package-info.java b/src/main/java/pulse/package-info.java index 139f9d4e..8a374e66 100644 --- a/src/main/java/pulse/package-info.java +++ b/src/main/java/pulse/package-info.java @@ -1,6 +1,6 @@ /** * Contains some of the most frequently used classes, which did not seem to fit - * in any other packages. Currently consists of {@code HeatingCurve} extensively + * in any other packages. Currently consists of three classes extensively * used in other packages. */ diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index f2e35af5..026bd3ec 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -1,7 +1,7 @@ package pulse.problem.statements; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; +import static pulse.properties.NumericPropertyKeyword.*; import java.util.List; @@ -112,9 +112,15 @@ public void assign(ParameterVector params) { switch (type) { case FOV_OUTER: case FOV_INNER: - case HEAT_LOSS_SIDE: + case HEAT_LOSS_SIDE: //comment this when locking side + (rear-front) properties.set(type, derive(type, params.inverseTransform(i) )); break; + //UNCOMMENT TO MAKE HEAT LOSS LOCKED + /* + case HEAT_LOSS: + properties.set(HEAT_LOSS_SIDE, derive(HEAT_LOSS_SIDE, params.inverseTransform(i) )); + break; + */ case SPOT_DIAMETER: ((Pulse2D) getPulse()).setSpotDiameter( derive(SPOT_DIAMETER, params.inverseTransform(i) )); break; diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 95a6fa11..6287fcc8 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -236,7 +236,7 @@ public void plotSingle(HeatingCurve curve) { } public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { - final int realCount = curve.getAlteredSignalData().size(); + final int realCount = curve.getBaselineCorrectedData().size(); final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); return series(curve, title, startTime, realCount, extendedCurve); } From 5e9b12d5c2d41c7d542d7340d6f4855f779468c3 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 24 May 2021 21:45:49 +0200 Subject: [PATCH 032/116] Proteus Reader update --- .../pulse/input/listeners/CurveEvent.java | 12 +- .../pulse/input/listeners/CurveEventType.java | 8 +- .../java/pulse/input/listeners/DataEvent.java | 12 +- .../pulse/input/listeners/DataEventType.java | 2 +- .../pulse/io/readers/NetzschCSVReader.java | 114 ++++++++++++------ .../io/readers/NetzschPulseCSVReader.java | 54 +-------- .../statements/ClassicalProblem2D.java | 2 +- 7 files changed, 99 insertions(+), 105 deletions(-) diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index c7f35001..db990f12 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -1,17 +1,17 @@ package pulse.input.listeners; -import pulse.AbstractData; +import pulse.HeatingCurve; /** - * A {@code CurveEvent} is associated with an {@code AbstractData} object. - * @see pulse.AbstractData + * A {@code CurveEvent} is associated with an {@code HeatingCurve} object. + * @see pulse.HeatingCurve * */ public class CurveEvent { private CurveEventType type; - private AbstractData data; + private HeatingCurve data; /** * Constructs a {@code CurveEvent} object, combining the {@code type} and @@ -21,7 +21,7 @@ public class CurveEvent { * @param data the source of the event */ - public CurveEvent(CurveEventType type, AbstractData data) { + public CurveEvent(CurveEventType type, HeatingCurve data) { this.type = type; this.data = data; } @@ -43,7 +43,7 @@ public CurveEventType getType() { * @return the associated data */ - public AbstractData getData() { + public HeatingCurve getData() { return data; } diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 06173cce..38da9b01 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -1,9 +1,15 @@ package pulse.input.listeners; +/** + * An event type associated with an {@code HeatingCurve} object. + * + */ + public enum CurveEventType { /** - * Indicates the curve signal values have been re-scaled. + * Indicates the curve signal values have been re-scaled. This means that + * each signal value has been multiplied by a single number. */ RESCALED, diff --git a/src/main/java/pulse/input/listeners/DataEvent.java b/src/main/java/pulse/input/listeners/DataEvent.java index 90fca6da..569ff56d 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -1,17 +1,17 @@ package pulse.input.listeners; -import pulse.AbstractData; +import pulse.input.ExperimentalData; /** * A {@code DataEvent} is used to track changes happening with a - * {@code HeatingCurve}. + * {@code ExperimentalData}. * */ public class DataEvent { private DataEventType type; - private AbstractData data; + private ExperimentalData data; /** * Constructs a {@code DataEvent} object, combining the {@code type} and @@ -21,7 +21,7 @@ public class DataEvent { * @param data the source of the event */ - public DataEvent(DataEventType type, AbstractData data) { + public DataEvent(DataEventType type, ExperimentalData data) { this.type = type; this.data = data; } @@ -37,13 +37,13 @@ public DataEventType getType() { } /** - * Used to get the {@code HeatingCurve} object that has undergone certain + * Used to get the {@code ExperimentalData} object that has undergone certain * changes specified by this event type. * * @return the associated data */ - public AbstractData getData() { + public ExperimentalData getData() { return data; } diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index 9e6ef953..e1480c0b 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -1,7 +1,7 @@ package pulse.input.listeners; /** - * An event that is associated with an {@code ExperimentalData} object. + * An event type that is associated with an {@code ExperimentalData} object. * */ diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 5a936983..99ce8567 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -12,20 +12,36 @@ import java.util.List; import java.util.Objects; +import pulse.AbstractData; import pulse.input.ExperimentalData; import pulse.input.Metadata; import pulse.input.Range; import pulse.ui.Messages; +/** + * Reads the .CSV files exported from Proteus LFA Analysis software. To load Proteus measurements in PULsE, + * the detector signal needs to be imported first, followed by the pulse data. + *

+ * Note that by default the decimal separator is assumed to be a point ("."). + *

+ */ + public class NetzschCSVReader implements CurveReader { - private static CurveReader instance = new NetzschCSVReader(); + private static NetzschCSVReader instance = new NetzschCSVReader(); + private final static double TO_KELVIN = 273; - private final static double TO_SECONDS = 1E-3; + protected final static double TO_SECONDS = 1E-3; private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; - private final static String SHOT_DATA = "Shot_data"; + protected final static String SHOT_DATA = "Shot_data"; private final static String DETECTOR = "DETECTOR"; + + /** + * Note comma is included a delimiter character here. + */ + + public final static String delims = "[#();,/°Cx%^]+"; private NetzschCSVReader() { // intentionally blank @@ -41,17 +57,21 @@ public String getSupportedExtension() { } /** + * Reads {@code file}, assuming that it contains data generated by Proteus with the detector signal. *

- * This will return a single {@code ExperimentalData}, which stores all the - * information available in the {@code file}, wrapped in a {@code List} object - * with the size of unity. In addition to the time-temperature data loaded - * directly into the {@code ExperimentalData} lists, a {@code Metadata} object - * will be created for the {@code ExperimentalData} and will store the test - * temperature declared in {@code file}. + * This will throw an {@code IllegalArgumentException} if the first entry in this file does not contain the + * {@value SHOT_DATA} string. If this is found, then an ID is extracted from the file, which will then be used + * to associate a pulse with the newly create {@code ExperimentalData} (this requires another reader. + * When the ID is identified, the file is searched for the keyword {@value SAMPLE_TEMPERATURE} to determine + * the baseline temperature of the shot. Then the method proceeds to search for the {@code DETECTOR} keyword, + * marking the beginning of the experimental time-signal sequence. If, for example, the file only contains + * the pulse data, the method will return an empty list and print an error message in the log, saying that the file + * was skipped. Otherwise, the time-signal sequence will be read, taking care to convert the time (in milliseconds + * by default) to second (used by default in PULsE). + *

+ * @return a list containing either zero elements, if the procedure failed, or one element, corresponding to + * the stored shot data. * - * @param file a '{@code .dat}' file, which conforms to the respective format. - * @return a single {@code ExperimentalData} wrapped in a {@code List} with the - * size of unity. */ @Override @@ -60,28 +80,16 @@ public List read(File file) throws IOException { ExperimentalData curve = new ExperimentalData(); - String delims = "[#();,/°Cx%^]+"; - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String[] shotID = reader.readLine().split(delims); - - int shotId = -1; - - //check if first entry makes sense - if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) - throw new IllegalArgumentException(file.getName() + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); - else - shotId = Integer.parseInt(shotID[shotID.length - 1]); + int shotId = determineShotID(reader, file); String[] tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; - var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); - curve.setMetadata(met); - - double time; - double temp; + /* + * Finds the detector keyword. + */ var detectorLabel = findLineByLabel(reader, DETECTOR, delims); @@ -90,17 +98,11 @@ public List read(File file) throws IOException { return new ArrayList<>(); } - reader.readLine(); + reader.readLine(); + populate(curve, reader); - String[] tokens; - - for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { - tokens = line.split(delims); - - time = Double.parseDouble(tokens[0]) * TO_SECONDS; - temp = Double.parseDouble(tokens[1]); - curve.addPoint(time, temp); - } + var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); } @@ -109,6 +111,37 @@ public List read(File file) throws IOException { } + protected static void populate(AbstractData data, BufferedReader reader) throws IOException { + double time; + double power; + String[] tokens; + + for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { + tokens = line.split(delims); + + time = Double.parseDouble(tokens[0]) * NetzschCSVReader.TO_SECONDS; + power = Double.parseDouble(tokens[1]); + data.addPoint(time, power); + } + + } + + protected static int determineShotID(BufferedReader reader, File file) throws IOException { + String[] shotID = reader.readLine().split(delims); + + int shotId = -1; + + //check if first entry makes sense + if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) + throw new IllegalArgumentException(file.getName() + + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); + else + shotId = Integer.parseInt(shotID[shotID.length - 1]); + + return shotId; + + } + /* private double parseDoubleWithComma(String s) { var format = NumberFormat.getInstance(Locale.GERMANY); @@ -122,12 +155,12 @@ private double parseDoubleWithComma(String s) { } */ - private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { String line = ""; String[] tokens; - //find sample temperature + //find keyword outer : for(line = reader.readLine(); line != null; line = reader.readLine()) { tokens = line.split(delims); @@ -142,6 +175,7 @@ private String findLineByLabel(BufferedReader reader, String label, String delim return line; } + /** * As this class uses the singleton pattern, only one instance is created using diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 481dacf3..34ce139e 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -12,9 +12,7 @@ public class NetzschPulseCSVReader implements PulseDataReader { private static PulseDataReader instance = new NetzschPulseCSVReader(); - private final static double TO_SECONDS = 1E-3; - private final static String SHOT_DATA = "Shot_data"; private final static String PULSE = "Laser_pulse_data"; private NetzschPulseCSVReader() { @@ -34,28 +32,14 @@ public String getSupportedExtension() { public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); - String delims = "[#(),;/°Cx%^]+"; - NumericPulseData data; try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String[] shotID = reader.readLine().split(delims); - - int shotId = -1; - - //check if first entry makes sense - if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) - throw new IllegalArgumentException(file.getName() + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); - else - shotId = Integer.parseInt(shotID[shotID.length - 1]); - + int shotId = NetzschCSVReader.determineShotID(reader, file); data = new NumericPulseData(shotId); - - double time; - double power; - - var pulseLabel = findLineByLabel(reader, PULSE, delims); + + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.delims); if(pulseLabel == null) { System.err.println("Skipping " + file.getName()); @@ -63,16 +47,7 @@ public NumericPulseData read(File file) throws IOException { } reader.readLine(); - - String[] tokens; - - for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { - tokens = line.split(delims); - - time = Double.parseDouble(tokens[0]) * TO_SECONDS; - power = Double.parseDouble(tokens[1]); - data.addPoint(time, power); - } + NetzschCSVReader.populate(data, reader); } @@ -92,27 +67,6 @@ private double parseDoubleWithComma(String s) { return Double.NaN; } */ - - private String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { - - String line = ""; - String[] tokens; - - //find sample temperature - outer : for(line = reader.readLine(); line != null; line = reader.readLine()) { - - tokens = line.split(delims); - - for(String token : tokens) { - if(token.equalsIgnoreCase(label)) - break outer; - } - - } - - return line; - - } /** * As this class uses the singleton pattern, only one instance is created using diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 026bd3ec..143c01b5 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -1,7 +1,7 @@ package pulse.problem.statements; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.*; +import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; import java.util.List; From 25c064293e51d96edc1dc744f6d766ffaae72c36 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 24 May 2021 21:54:18 +0200 Subject: [PATCH 033/116] Additional changes to the reader --- .../pulse/io/readers/NetzschPulseCSVReader.java | 17 +++++++++++++++++ .../java/pulse/io/readers/PulseDataReader.java | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 34ce139e..94137fe5 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -9,6 +9,14 @@ import pulse.problem.laser.NumericPulseData; import pulse.ui.Messages; +/** + * Reads numeric pulse data generated by the Proteus LFA Analysis export tool. + * The data must have a decimal point separator and should follow, in general, + * the same rules set by the NetzschPulseCSVReader + * @see pulse.io.reader.NetzschCSVReader + * + */ + public class NetzschPulseCSVReader implements PulseDataReader { private static PulseDataReader instance = new NetzschPulseCSVReader(); @@ -27,6 +35,15 @@ private NetzschPulseCSVReader() { public String getSupportedExtension() { return Messages.getString("NetzschCSVReader.0"); } + + /** + * This performs a basic check, finding the shot ID, which is then passed to a + * new {@code NumericPulseData} object. The latter is populated using the + * time-power sequence stored in this file. If the {@value PULSE} keyword is not found, + * the method will display an error. + * @see pulse.io.readers.NetzschCSVReader.read() + * @return a new {@code NumericPulseData} object encapsulating the contents of {@code file} + */ @Override public NumericPulseData read(File file) throws IOException { diff --git a/src/main/java/pulse/io/readers/PulseDataReader.java b/src/main/java/pulse/io/readers/PulseDataReader.java index a5e3f319..7841e4a4 100644 --- a/src/main/java/pulse/io/readers/PulseDataReader.java +++ b/src/main/java/pulse/io/readers/PulseDataReader.java @@ -5,8 +5,18 @@ import pulse.problem.laser.NumericPulseData; +/** + * A reader for importing numeric pulse data -- if available. + * + */ + public interface PulseDataReader extends AbstractReader { + + /** + * Converts the ASCII file to a {@code NumericPulseData} object. + */ + @Override public abstract NumericPulseData read(File file) throws IOException; From aaf7e8eeb445a5c1dbd0f87e21351a6082744247 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Tue, 25 May 2021 10:17:29 +0200 Subject: [PATCH 034/116] Improved truncation & time limit adjustment --- .../java/pulse/input/ExperimentalData.java | 18 +++++++++++++++--- .../problem/schemes/DifferenceScheme.java | 13 ++++++++----- .../statements/model/ThermalProperties.java | 2 +- src/main/java/pulse/tasks/SearchTask.java | 5 +++++ src/main/java/pulse/tasks/TaskManager.java | 13 ++++++++++--- .../java/pulse/ui/components/DataLoader.java | 19 +++---------------- 6 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 5291ec60..05010f3b 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -159,7 +159,7 @@ public List runningAverage(int reductionFactor) { int start = indexRange.getLowerBound(); int end = indexRange.getUpperBound(); - + int step = (end - start) / (count / reductionFactor); double av = 0; @@ -177,9 +177,9 @@ public List runningAverage(int reductionFactor) { av /= step; crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); - + } - + return crudeAverage; } @@ -411,5 +411,17 @@ private void doSetRange() { if (metadata != null) range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); } + + /** + * Retrieves the + * + * @see pulse.problem.schemes.DifferenceScheme + * @return a double, equal to the last element of the {@code time List}. + */ + + @Override + public double timeLimit() { + return timeAt(indexRange.getUpperBound()); + } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 28aa5267..0e3544da 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -97,16 +97,18 @@ public void copyFrom(DifferenceScheme df) { /** *

* Contains preparatory steps to ensure smooth running of the solver. This - * includes creating a {@code DiscretePulse} object and calculating the - * {@code timeInterval}. The latter determines the real-time calculation of a - * {@code HeatingCurve} based on the numerical solution of {@code problem}; it - * thus takes into account the difference between the scheme timestep and the - * {@code HeatingCurve} point spacing. All subclasses of + * includes creating a {@code DiscretePulse} object and adjusting the grid of this + * scheme to match the {@code DiscretePulse} created for this {@code problem}. + * Finally, a heating curve is cleared from the previously calculated values. + *

+ *

+ * All subclasses of * {@code DifferenceScheme} should override and explicitly call this superclass * method where appropriate. *

* * @param problem the heat problem to be solved + * @see pulse.problem.schemes.Grid.adjustTo() */ protected void prepare(Problem problem) { @@ -305,6 +307,7 @@ public NumericProperty getTimeLimit() { public void setTimeLimit(NumericProperty timeLimit) { requireType(timeLimit, TIME_LIMIT); this.timeLimit = (double) timeLimit.getValue(); + firePropertyChanged(this, timeLimit); } @Override diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index ed48e9ba..77d27aaa 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -52,7 +52,7 @@ public class ThermalProperties extends PropertyHolder { * Chem. Eng. Sci. 199 (2019) 546-551
*/ - public final double PARKERS_COEFFICIENT = 0.1370; // in mm + public final double PARKERS_COEFFICIENT = 0.1388; // in mm public ThermalProperties() { super(); diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 15122eeb..eb1aa8c8 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -119,6 +119,11 @@ private void addListeners() { if (p.areThermalPropertiesLoaded()) p.useTheoreticalEstimates(curve); }); + + /** + * Sets the difference scheme's time limit to the upper bound of the range of {@code ExperimentalData} + * multiplied by a safety margin {@value Calculation.RELATIVE_TIME_MARGIN}. + */ curve.addDataListener(dataEvent -> { var scheme = current.getScheme(); diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index f3941d5a..c449e9b4 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -239,7 +239,9 @@ public boolean dataNeedsTruncation() { */ public void truncateData() { - tasks.stream().forEach(t -> t.getExperimentalCurve().truncate()); + tasks.stream().forEach(t -> + t.getExperimentalCurve().truncate() + ); } private void fireTaskSelected(Object source) { @@ -373,8 +375,12 @@ public void generateTasks(List files) { System.err.println("Failed to load all tasks within 2 minutes. Details:"); e.printStackTrace(); } - + selectFirstTask(); + + // check if the data loaded needs truncation + if (instance.dataNeedsTruncation()) + this.truncateData(); } @@ -512,7 +518,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { var properties = t.getCurrentCalculation().getProblem().getProperties(); - properties.useTheoreticalEstimates(t.getExperimentalCurve()); + var c = t.getExperimentalCurve(); + properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index cc1c367a..0261cf47 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -4,7 +4,6 @@ import static pulse.io.readers.ReaderManager.pulseReaders; import static pulse.io.readers.ReaderManager.read; -import java.awt.Window; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -64,9 +63,11 @@ public static void loadDataDialog() { var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), ReaderManager.getCurveExtensions()); + var instance = TaskManager.getManagerInstance(); + if (files != null) { progressFrame.trackProgress(files.size()); - TaskManager.getManagerInstance().generateTasks(files); + instance.generateTasks(files); } } @@ -116,10 +117,6 @@ public static void loadMetadataDialog() { } - // check if the data loaded needs truncation - if (instance.dataNeedsTruncation()) - truncateDataDialog(progressFrame); - progressFrame.incrementProgress(); // select first of the generated task @@ -179,16 +176,6 @@ public static void load(StandartType type, File f) throws IOException { TaskManager.getManagerInstance().evaluate(); } - private static void truncateDataDialog(Window frame) { - Object[] options = { "Truncate", "Do not change" }; - int answer = JOptionPane.showOptionDialog(frame, - ("The acquisition time for some experiments appears to be too long.\nIf time resolution is low, the model estimates will be biased.\n\nIt is recommended to allow PULSE to truncate this data.\n\nWould you like to proceed? "), //$NON-NLS-1$ - "Potential Problem with Data", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, - options[0]); - if (answer == 0) - TaskManager.getManagerInstance().truncateData(); - } - private static List userInput(String descriptor, List extensions) { JFileChooser fileChooser = new JFileChooser(); From fb64ec9c6121488f3dc1a567aeae19640768017a Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 26 May 2021 21:48:27 +0200 Subject: [PATCH 035/116] Fixed two problems -Error in the LMOptimiser where the computeJacobian was not thread-safe. This has been transferred to LMPath. - Fixed NetzschCSVReader, so that it now reads sample thickness --- pom.xml | 2 +- .../pulse/io/readers/NetzschCSVReader.java | 20 ++- src/main/java/pulse/math/ParameterVector.java | 47 ++++++ .../pulse/math/transforms/AtanhTransform.java | 17 ++- .../transforms/BoundedParameterTransform.java | 6 + .../math/transforms/InvLenTransform.java | 5 + .../transforms/StandardTransformations.java | 5 + .../pulse/math/transforms/Transformable.java | 16 ++ .../laser/ExponentiallyModifiedGaussian.java | 10 +- .../pulse/problem/laser/NumericPulse.java | 36 +++++ .../pulse/problem/laser/NumericPulseData.java | 27 +++- .../problem/laser/PulseTemporalShape.java | 17 ++- .../pulse/problem/laser/TrapezoidalPulse.java | 4 + .../problem/schemes/BlockMatrixAlgorithm.java | 2 +- .../schemes/CoupledImplicitScheme.java | 1 - .../problem/schemes/FixedPointIterations.java | 26 ++++ .../search/direction/IterativeState.java | 13 ++ .../pulse/search/direction/LMOptimiser.java | 143 +++++++----------- .../java/pulse/search/direction/LMPath.java | 14 +- src/main/java/pulse/tasks/TaskManager.java | 13 +- src/main/resources/Version.txt | 2 +- 21 files changed, 305 insertions(+), 121 deletions(-) diff --git a/pom.xml b/pom.xml index 873852c6..f36f267d 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 kotik-coder PULsE - 1.90 + 1.91 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 99ce8567..3a113a7a 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -16,6 +16,7 @@ import pulse.input.ExperimentalData; import pulse.input.Metadata; import pulse.input.Range; +import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; /** @@ -32,13 +33,15 @@ public class NetzschCSVReader implements CurveReader { private final static double TO_KELVIN = 273; protected final static double TO_SECONDS = 1E-3; + private final static double TO_METRES = 1E-3; private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; - protected final static String SHOT_DATA = "Shot_data"; + private final static String SHOT_DATA = "Shot_data"; private final static String DETECTOR = "DETECTOR"; + private final static String THICKNESS = "Thickness_RT"; /** - * Note comma is included a delimiter character here. + * Note comma is included as a delimiter character here. */ public final static String delims = "[#();,/°Cx%^]+"; @@ -62,8 +65,8 @@ public String getSupportedExtension() { * This will throw an {@code IllegalArgumentException} if the first entry in this file does not contain the * {@value SHOT_DATA} string. If this is found, then an ID is extracted from the file, which will then be used * to associate a pulse with the newly create {@code ExperimentalData} (this requires another reader. - * When the ID is identified, the file is searched for the keyword {@value SAMPLE_TEMPERATURE} to determine - * the baseline temperature of the shot. Then the method proceeds to search for the {@code DETECTOR} keyword, + * When the ID is identified, the file is searched for the keywords {@value THICKNESS} and {@value SAMPLE_TEMPERATURE} to + * determine the sample thickness and baseline temperature of the shot. Then the method proceeds to search for the {@code DETECTOR} keyword, * marking the beginning of the experimental time-signal sequence. If, for example, the file only contains * the pulse data, the method will return an empty list and print an error message in the log, saying that the file * was skipped. Otherwise, the time-signal sequence will be read, taking care to convert the time (in milliseconds @@ -84,8 +87,11 @@ public List read(File file) throws IOException { int shotId = determineShotID(reader, file); - String[] tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); - double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; + var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); + final double thickness = Double.parseDouble( tempTokens[tempTokens.length - 1] ) * TO_METRES; + + tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); + final double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; /* * Finds the detector keyword. @@ -102,6 +108,8 @@ public List read(File file) throws IOException { populate(curve, reader); var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); + curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 0d4e5cab..d815ab4f 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -48,6 +48,11 @@ public ParameterVector(ParameterVector proto, Vector v) { } + /** + * Copy constructor + * @param v another vector + */ + public ParameterVector(ParameterVector v) { this( v.dimension() ); final int n = dimension(); @@ -57,6 +62,11 @@ public ParameterVector(ParameterVector v) { System.arraycopy(v.transforms, 0, transforms, 0, n); System.arraycopy(v.bounds, 0, bounds, 0, n); } + + /** + * Creates an empty ParameterVector with a dimension of {@code n} + * @param n dimension + */ private ParameterVector(final int n) { super(n); @@ -75,6 +85,14 @@ public void set(final int i, final double x) { set(i, x, false); } + /** + * Sets the i-component of this vector to {@code x} or its corresponding transform, + * if the latter is defined and {@code ignoreTransform} is {@code false}. + * @param i index of the value and its transform + * @param x the non-transformed value, which needs to be assigned to the i-th component + * @param ignoreTransform if {@code} false, will ignore exiting transform. + */ + public void set(final int i, final double x, boolean ignoreTransform) { final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); super.set(i, t); @@ -113,10 +131,22 @@ public double getParameterValue(NumericPropertyKeyword index) { return super.get(indexOf(index)); } + /** + * Performs an inverse transform corresponding to the index {@code i} of this vector. + * @param i the index of the transform + * @return the inverse transform of {@code get(i) } if the transform is defined, {@code get(i)} otherwise. + */ + public double inverseTransform(final int i) { return transforms[i] != null ? transforms[i].inverse( get(i) ) : get(i); } + /** + * Gets the transformable of the i-th component + * @param i index of the component + * @return the corresponding {@code Transforamble} + */ + public Transformable getTransform(final int i) { return transforms[i]; } @@ -129,6 +159,12 @@ public Segment getParameterBounds(final int i) { return bounds[i]; } + /** + * If transform of {@code i} is not null, applies the transformation to the component bounds + * @param i the index of the component + * @return the transformed bounds + */ + public Segment getTransformedBounds(final int i) { return transforms[i] != null ? new Segment( transforms[i].transform( bounds[i].getMinimum() ), @@ -136,6 +172,12 @@ public Segment getTransformedBounds(final int i) { getParameterBounds(i); } + /** + * Sets the bounds of i-th component of this vector. + * @param i the index of the component + * @param segment new parameter bounds + */ + public void setParameterBounds(int i, Segment segment) { bounds[i] = segment; } @@ -149,6 +191,11 @@ public void setParameterBounds(int i, Segment segment) { public List getIndices() { return Arrays.asList(indices); } + + /** + * This will assign a new list of indices to this vector + * @param indices a list of indices + */ private void assign(List indices) { this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java index d268735a..213da185 100644 --- a/src/main/java/pulse/math/transforms/AtanhTransform.java +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -6,19 +6,34 @@ import pulse.math.Segment; /** - * Hyper-tangent parameter transform. + * Hyper-tangent parameter transform allowing to set an upper bound for a parameter. */ public class AtanhTransform extends BoundedParameterTransform { + /** + * Only the upper bound of the argument is used. + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public AtanhTransform(Segment bounds) { super(bounds); } + + /** + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ @Override public double transform(double a) { return atanh(2.0 * a / getBounds().getMaximum() - 1.0); } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ @Override public double inverse(double t) { diff --git a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java index 066eb5b8..7ad677df 100644 --- a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java +++ b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java @@ -2,6 +2,12 @@ import pulse.math.Segment; +/** + * An abstract {@code Transformable} where the bounds of the parameter is manually set. + * Subclasses can be bounded from either on or both sides. + * + */ + public abstract class BoundedParameterTransform implements Transformable { private Segment bounds; diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java index 322c7849..28c1d658 100644 --- a/src/main/java/pulse/math/transforms/InvLenTransform.java +++ b/src/main/java/pulse/math/transforms/InvLenTransform.java @@ -2,6 +2,11 @@ import pulse.problem.statements.model.ThermalProperties; +/** + * A transform that simply divides the value by the length of the + * sample. + */ + public class InvLenTransform implements Transformable { private double l; diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index d8e9b6ed..b7b96721 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -4,6 +4,11 @@ import static java.lang.Math.log; import static java.lang.Math.sqrt; +/** + * A utility class containing standard mathematical transforms and their inverses for non-bounded parameters. + * + */ + public class StandardTransformations { /** diff --git a/src/main/java/pulse/math/transforms/Transformable.java b/src/main/java/pulse/math/transforms/Transformable.java index 032f1b3d..0116a784 100644 --- a/src/main/java/pulse/math/transforms/Transformable.java +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -1,8 +1,24 @@ package pulse.math.transforms; +/** + * An interface for performing reversible one-to-one mapping of the model parameters. + * + */ + public interface Transformable { + /** + * Performs the selected transform with {@code value} + * @param value a double representing the parameter value + * @return the results, such that {@code inverse( transform(value) ) = value} + */ + public double transform(double value); + + /** + * Inverses the transform. + */ + public double inverse(double t); } \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index f80c37e0..6e625322 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -59,7 +59,6 @@ public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { @Override public void init(ExperimentalData data, DiscretePulse pulse) { super.init(data, pulse); - norm = 1.0; // resets the normalisation factor to unity norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of // the area. } @@ -83,6 +82,12 @@ public double evaluateAt(double time) { * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); } + + /** + * @see pulse.properties.NumericPropertyKeyword.SKEW_MU + * @see pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA + * @see pulse.properties.NumericPropertyKeyword.SKEW_SIGMA + */ @Override public List listedTypes() { @@ -170,8 +175,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public PulseTemporalShape copy() { - // TODO Auto-generated method stub - return null; + return new ExponentiallyModifiedGaussian(this); } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 4b1f85c4..88539768 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -12,6 +12,13 @@ import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; +/** + * A numeric pulse is given by a set of discrete {@code NumericPulseData} measured + * independelty using a pulse diode. + * @see pulse.problem.laser.NumericPulseData + * + */ + public class NumericPulse extends PulseTemporalShape { private NumericPulseData pulseData; @@ -22,11 +29,24 @@ public NumericPulse() { //intentionally blank } + /** + * Copy constructor + * @param pulse another numeric pulse, the data of which will be copied + */ + public NumericPulse(NumericPulse pulse) { super(pulse); this.pulseData = new NumericPulseData(pulseData); } + /** + * Defines the pulse width as the last element of the time sequence contained in {@code NumericPulseData}. + * Calls {@code super.init}, then interpolates the input pulse using spline functions and normalises the + * output. + * @see normalise() + * + */ + @Override public void init(ExperimentalData data, DiscretePulse pulse) { pulseData = data.getMetadata().getPulseData(); @@ -43,6 +63,13 @@ public void init(ExperimentalData data, DiscretePulse pulse) { normalise(problem); } + /** + * Checks that the area of the pulse curve is unity (within a small error margin). + * If this is {@code false}, re-scales the numeric data using {@code 1/area} as the scaling factor. + * @param problem defines the {@code timeFactor} needed for re-building the interpolation + * @see pulse.problem.laser.NumericPulseData.scale() + */ + public void normalise(Problem problem) { final double EPS = 1E-2; @@ -76,6 +103,11 @@ private void doInterpolation(double timeFactor) { } + /** + * If the argument is less than the pulse width, uses the spline function to interpolated the pulse + * function at {@code time}. Otherwise returns zero. + */ + @Override public double evaluateAt(double time) { return time > adjustedPulseWidth ? 0.0 : interpolation.value(time); @@ -86,6 +118,10 @@ public PulseTemporalShape copy() { return new NumericPulse(); } + /** + * Does not define any property. + */ + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { // TODO Auto-generated method stub diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index ad388188..d6d2d51f 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -2,20 +2,40 @@ import pulse.AbstractData; +/** + * An instance of the {@code AbstractData} class, which also declares an {@code externalID}. + * Use to store numeric data of the pulse for each measurement imported from an external source. + * + */ + public class NumericPulseData extends AbstractData { private int externalID; + /** + * Stores {@code id} and calls super-constructor + * @param id an external ID defined in the imported file + */ + public NumericPulseData(int id) { super(); this.externalID = id; } + /** + * Copies everything, including the id number. + * @param data another object + */ + public NumericPulseData(NumericPulseData data) { super(data); this.externalID = data.externalID; } - + + /** + * Adds a data point to the internal storage and increments counter. + */ + @Override public void addPoint(double time, double power) { super.addPoint(time, power); @@ -33,6 +53,11 @@ public int getExternalID() { return externalID; } + /** + * Uniformly scales the values of the pulse power by {@code factor}. + * @param factor the scaling factor + */ + public void scale(double factor) { var power = this.getSignalData(); diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index 8697c726..fa927f8e 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -13,7 +13,8 @@ /** * An abstract time-dependent pulse shape. Declares the abstract method to * calculate the laser power function at a given moment of time. This generally - * utilises a discrete pulse width. + * utilises a discrete pulse width. By default, uses a midpoint-rule numeric integrator + * to calculate the pulse integral. * */ @@ -32,6 +33,13 @@ public PulseTemporalShape(PulseTemporalShape another) { this.integrator = another.integrator; } + /** + * Creates a new midpoint-integrator using the number of segments equal to {@value DEFAULT_POINTS}. + * The integrand function is specified by the {@code evaluateAt} method of this class. + * @see pulse.math.MidpointIntegrator + * @see evaluateAt() + */ + public void initAreaIntegrator() { integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { @@ -46,7 +54,7 @@ public double integrand(double... vars) { /** * Uses numeric integration (midpoint rule) to calculate the area of the pulse - * shape corresponding to the selected parameters. + * shape corresponding to the selected parameters. The integration bounds are non-negative. * * @return the area */ @@ -68,6 +76,11 @@ public double area() { public abstract double evaluateAt(double time); + /** + * Stores the pulse width from {@code pulse} and initialises area integration. + * @param pulse the discrete pulse containing the pulse width + */ + public void init(ExperimentalData data, DiscretePulse pulse) { width = pulse.getDiscreteWidth(); this.initAreaIntegrator(); diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index fa88573b..4da228bf 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -45,6 +45,10 @@ public TrapezoidalPulse(TrapezoidalPulse another) { this.h = another.h; } + /** + * Calculates the height of the trapez after calling the super-class method. + */ + @Override public void init(ExperimentalData data, DiscretePulse pulse) { super.init(data, pulse); diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index a1530d34..037483bc 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -20,7 +20,7 @@ public BlockMatrixAlgorithm(Grid grid) { p = new double[gamma.length - 1]; q = new double[gamma.length - 1]; } - + @Override public void sweep(double[] V) { final int N = V.length - 1; diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 06ff68bd..681ff79e 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -45,7 +45,6 @@ public void iteration(final int m) { super.timeStep(m); } - @Override public void finaliseIteration(double[] V) { setCalculationStatus( coupling.getRadiativeTransferEquation().compute(V) ); } diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index a093e67f..d98a3bb3 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -2,8 +2,24 @@ import static java.lang.Math.abs; +/** + * @see Wiki page + * + */ + public interface FixedPointIterations { + /** + * Performs iterations until the convergence criterion is satisfied. + * The latter consists in having a difference two consequent iterations of V + * less than the specified error. At the end of each iteration, calls {@code finaliseIteration()}. + * @param V the calculation array + * @param error used in the convergence criterion + * @param m time step + * @see finaliseIteration() + * @see iteration() + */ + public default void doIterations(double[] V, final double error, final int m) { final int N = V.length - 1; @@ -18,8 +34,18 @@ public default void doIterations(double[] V, final double error, final int m) { } } + /** + * Performs an iteration at time {@code m} + * @param m time step + */ + public void iteration(final int m); + /** + * Finalises the current iteration. By default, does nothing. + * @param V the current iteration + */ + public default void finaliseIteration(double[] V) { // do nothing } diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index b8e756d6..34e1cc14 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -8,6 +8,7 @@ public class IterativeState { private int iteration; + private int failedAttempts; public void reset() { iteration = 0; @@ -20,5 +21,17 @@ public NumericProperty getIteration() { public void incrementStep() { iteration++; } + + public int getFailedAttempts() { + return failedAttempts; + } + + public void resetFailedAttempts() { + failedAttempts = 0; + } + + public void incrementFailedAttempts() { + failedAttempts++; + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 8548071f..53cb769b 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -25,19 +25,26 @@ import pulse.tasks.logs.Status; import pulse.ui.Messages; +/** + * Given an objective function equal to the sum of squared residuals, + * iteratively approaches the minimum of this function by applying + * the Levenberg-Marquardt formulas. + * + */ + public class LMOptimiser extends GradientBasedOptimiser { private static LMOptimiser instance = new LMOptimiser(); - private boolean computeJacobian; private final static double EPS = 1e-10; // for numerical comparison private double dampingRatio; - /* - private double geodesicParameter = 0.1; - private boolean geodesicCorrection = false; - */ - + /** + * Maximum number of consequent failed iterations that can be rejected. + */ + + public final static int MAX_FAILED_ATTEMPTS = 10; + private LMOptimiser() { super(); dampingRatio = (double)def(DAMPING_RATIO).getValue(); @@ -46,16 +53,12 @@ private LMOptimiser() { }); } - @Override - public void reset() { - super.reset(); - computeJacobian = true; - } - @Override public boolean iteration(SearchTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); // the previous path of the task + boolean accept = true; //accept the step by default + /* * Checks whether an iteration limit has been already reached */ @@ -63,7 +66,6 @@ public boolean iteration(SearchTask task) throws SolverException { if (compare(p.getIteration(), getMaxIterations()) > 0) { task.setStatus(Status.TIMEOUT); - return true; } @@ -78,28 +80,10 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); - /* - * Geodesic acceleration - */ - - /* - var acceleration = p.getJacobian().transpose().multiply( directionalDerivative(task) ); // J' dr/dp - var correction = HessianDirectionSolver.solve(p, acceleration.inverted() ); // H^-1 J'dr/dp - - double newCost = Double.POSITIVE_INFINITY; - */ - - /* - * Additional conditions imposed by geodesic acceleration. - */ - - //if( !geodesicCorrection || correction.length() / lmDirection.length() <= geodesicParameter) { - var candidate = parameters.sum(lmDirection); - task.assign(new ParameterVector( - //geodesicCorrection ? candidate.sum( correction.multiply(0.5) ) : - parameters, candidate ) ); // assign new parameters - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals - //} + var candidate = parameters.sum(lmDirection); + task.assign(new ParameterVector( + parameters, candidate ) ); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals /* * Delayed gratification @@ -108,20 +92,27 @@ public boolean iteration(SearchTask task) throws SolverException { if (newCost > initialCost + EPS) { p.setLambda(p.getLambda() * 2.0); task.assign(parameters); // roll back if cost increased - computeJacobian = true; + p.setComputeJacobian(true); + p.incrementFailedAttempts(); + accept = p.getFailedAttempts() > MAX_FAILED_ATTEMPTS; } else { + p.resetFailedAttempts(); p.setLambda(p.getLambda() / 3.0); - computeJacobian = false; + p.setComputeJacobian(false); p.incrementStep(); // increment the counter of successful steps } - return newCost < initialCost + EPS; - } + + return accept; //either accept or reject this step } + /** + * Calculates the Jacobian, if needed, evaluates the gradient and the Hessian matrix. + */ + @Override public void prepare(SearchTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); @@ -130,7 +121,7 @@ public void prepare(SearchTask task) throws SolverException { p.setResidualVector( new Vector( residualVector(task.getCurrentCalculation().getOptimiserStatistic()) )); // Calculate the Jacobian -- if needed - if (computeJacobian) { + if (p.isComputeJacobian()) { p.setJacobian(jacobian(task)); // J p.setNonregularisedHessian(halfHessian(p)); // this is just J'J } @@ -138,19 +129,38 @@ public void prepare(SearchTask task) throws SolverException { // the Jacobian is then used to calculate the 'gradient' Vector g1 = halfGradient(p); // g1 p.setGradient(g1); - + // the Hessian is then regularised by adding labmda*I - + var hessian = p.getNonregularisedHessian(); var damping = ( levenbergDamping(hessian).multiply(dampingRatio) .sum(marquardtDamping(hessian).multiply(1.0 - dampingRatio)) ) .multiply(p.getLambda()); var regularisedHessian = asSquareMatrix(hessian.sum(damping)); // J'J + lambda I - - p.setHessian(regularisedHessian); // so this is the new Hessian + + p.setHessian(regularisedHessian); // so this is the new Hessian + } + /** + *

+ * Calculates the Jacobian of the model function given as a discrete set of time-signal values. + * The elements of the Jacobian are calculated using central differences from two residual vectors + * evaluated by shifting the search vector slightly to the right or left of each search parameter. + *

+ *

+ * This is also equivalent to calculating the difference of the model values when performing the shift, + * when taking the model values at the time points of the reference dataset. Because of a different + * discretisation of the model, it is easier to substitute these with the residuals, which had already + * been interpolated at the reference time values. + *

+ * @param task the task being optimised + * @return the jacobian matrix + * @throws SolverException + * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() + */ + public RectangularMatrix jacobian(SearchTask task) throws SolverException { @@ -204,7 +214,6 @@ private static double[] residualVector(ResidualStatistic rs) { @Override public GradientGuidedPath initState(SearchTask t) { this.configure(t); - computeJacobian = true; return new LMPath(t); } @@ -218,50 +227,6 @@ private SquareMatrix halfHessian(LMPath path) { var jacobian = path.getJacobian(); return asSquareMatrix(jacobian.transpose().multiply(jacobian)); } - - /* - private Vector directionalDerivative(SearchTask t) throws SolverException { - var p = (LMPath) t.getPath(); - - var currentParameters = p.getParameters(); - final int numParams = currentParameters.dimension(); - - final var dir = p.getDirection(); - var shift = new Vector(numParams); - - final double h = 0.5*super.getGradientStep(); - - //small shift in a previously calculated direction - for(int i = 0; i < numParams; i++) - shift.set(i, h * dir.get(i) ); - - final var statistic = t.getCurrentCalculation().getOptimiserStatistic(); - - var currentResiduals = p.getResidualVector(); - - t.assign( new IndexedVector( currentParameters.sum(shift), currentParameters.getIndices() ) ); - t.solveProblemAndCalculateCost(); - var newResiduals = residualVector(statistic); - - t.assign(currentParameters); //shift back - - var diff = new double[newResiduals.length]; - var jacobian = p.getJacobian(); - - for(int i = 0 ; i < newResiduals.length; i++) { - diff[i] = ( newResiduals[i] - currentResiduals.get(i) ) / h; - - double add = 0; - for(int j = 0; j < numParams; j++) - add += jacobian.get(i, j)*dir.get(j); - - diff[i] -= add; - diff[i] *= 2.0/h; - } - - return new Vector(diff); - } - */ /* * Additive damping strategy, where the scaling matrix is simply the identity matrix. diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 1acc5fc7..dff3617f 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -13,6 +13,7 @@ class LMPath extends ComplexPath { private RectangularMatrix jacobian; private SquareMatrix nonregularisedHessian; private double lambda; + private boolean computeJacobian; public LMPath(SearchTask t) { super(t); @@ -21,11 +22,8 @@ public LMPath(SearchTask t) { @Override public void configure(SearchTask t) { super.configure(t); - this.jacobian = null; - this.setHessian(null); - nonregularisedHessian = null; this.lambda = 1.0; - this.residualVector = null; + computeJacobian = true; } public RectangularMatrix getJacobian() { @@ -68,4 +66,12 @@ public void setParameters(ParameterVector parameters) { this.parameters = parameters; } + public boolean isComputeJacobian() { + return computeJacobian; + } + + public void setComputeJacobian(boolean computeJacobian) { + this.computeJacobian = computeJacobian; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index c449e9b4..d909b537 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -1,6 +1,5 @@ package pulse.tasks; -import static java.lang.System.gc; import static java.time.LocalDateTime.now; import static java.time.format.DateTimeFormatter.ISO_WEEK_DATE; import static java.util.Objects.requireNonNull; @@ -24,7 +23,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; @@ -112,7 +110,6 @@ public static TaskManager getManagerInstance() { */ public void execute(SearchTask t) { - t.setStatus(QUEUED); // notify listeners computation is about to start // notify listeners @@ -168,14 +165,8 @@ public void executeAll() { } }).collect(toList()); - try { - taskPool.submit(() -> queue.parallelStream().forEach(t -> execute(t))).get(); - } catch (InterruptedException | ExecutionException e) { - System.err.println("Execution exception while running multiple tasks"); - e.printStackTrace(); - } - - gc(); + for(SearchTask t : queue) + taskPool.submit(() -> execute(t)); } diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 622bdff5..57a845df 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.90R \ No newline at end of file +1.91 \ No newline at end of file From 9d21ab2f9a50e3f69c5a92b51c7d561a1b2655b7 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 26 May 2021 22:01:30 +0200 Subject: [PATCH 036/116] Splash update --- src/main/resources/images/Splash.png | Bin 0 -> 93670 bytes src/main/resources/images/splash.png | Bin 59748 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/images/Splash.png delete mode 100644 src/main/resources/images/splash.png diff --git a/src/main/resources/images/Splash.png b/src/main/resources/images/Splash.png new file mode 100644 index 0000000000000000000000000000000000000000..c755ddc03be30edbbdfd394eabdba278f7174100 GIT binary patch literal 93670 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¢a((yrv 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png deleted file mode 100644 index 32a1c51d6909c50994a44619e0af57eed56660b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59748 zcmYg$V|b-Yuyt(Pw#`X)Y)))rV%xTDPHatVPB^h`+sT)E&zy6=C&~Mx_fB_pRjsPk ztKV=1IdOOx92g)VAb3d$k?%l2V7Nd)z+zAkUn8^@sZu~dXyhKs>Q3JcT#4))Y)yYy znGiX-+nErVxcx8%0&-ia%J|{3_A5c`(+Is2Y#f*l5{hWjo%d+}5#ChFq}5oBFE@qG zB*HjA0B#eAXYKPnHu!V9Vjn};n&st;??%Ne+x^;fU~$pQr$jw>vj*^RoBH{_?^gQ$ z&T*^hMqh(kzd@*tXY1>|wMzi^wC{#>$?Dz#>5;3yby)F~{r>)JpMW=2vs=~$nell4 zbBY)s=iCkX=P`Inc>{3os9A&g$?L^UBy!AVqwLT{p8WJiP=k&3DQR-8`7~i)Ej{(B zEw)vR;f^RK&_#E-%RWR1dPhD&c-c2KQ}cjb$k;n|>Mi!$^)4hvwleD_D z=uIR#(^i(%#KxL<-sz82%EfKDWV+!5Cu|KjZTO8*p+t^G<@Lssx;~>CfdT`Gn@Gv9wtvVyFMc;i(MCP5~;i8_#1|1h;Eog9Jqeu zu(G6BlI-qa=jAmV(#0rSK8(9h?u@gE{+6kCx9OA2Vg9aeJ2vsh?BZi=t$R$%d{=Lu z{!{57pyc9{)X{(LozHgD_?&C%II&I*(?iB6|Mh}ela4OzsB$^9xIeYIvf-$9yjC^F zC0y;d+`?j+X(39EDcpt`v+Yh+MfZ2{NW_Cf{mH|)%ZaErIpOD822zi7{i-si7daz- z&?x)i2iN@^It-i2WLBxO-y4(}Da*mJWQOh5HS{sXrjZocq0O4^N8!<^`$c7j&SlvG zOhXH^9uwIjm)|Sr42A&~=R+58TJ`pB9$ef)26p08AJ2z|@ zca~l+uJNv_TtBzT4#)Wwjc4~;p(fTRsBVvpLcGoB0u8#%(pf%%^;M_j@+Be<$w|`L zCYA`cupLS`PpJ|go8T3uC|k?KPBAq-<3#6&ij-BexW+gu<*q}}JOPM!T^WuA4PL|r z1}~l_?TXL=R6?Ll(uQs*>Y{JrRrk$#KI~z$)D{#5AT zs5q~Qd3!ZM&eO0{udPJ7p&IFnu&>(Sp5-@pFFn7QhO4%N*1p^2OZCSx45;**CsBM~ zos?X-O@@lx#PYaHKC*VMTMGePyX36Zm4N%6>$%&BI^snr5@mQv*BTBfsJU z5+f;;g>Go3lenzPQ~=nzX083FZ?BcGRn5=>e-bZc+dil*wl57{DJ;-jc;|L_jW#BT8uo(Xc$aJBrp>)ZnS9vZq>GlL$s0!v#n1=<;=sAgV^F2a4lX7 z4jGS|o+vcRbin0YgtBk{sXfglsGhfNF+U%c?1NShCYjHx44UCT6Jr54Jxz{-bbNl| z0a4&jKAC*(he)>Ry)tPg^jfUrB>SWAVk!TySXq^1tLQUjT{3pggRBHJu?!S%+i^)x zcm9nRK8fUa{o_Cp9-H-AnSY=Jr4cdpq1X47rXDB%D;BL2|bW)qJZv!9Y#KJA6Eo~ zIm17)+ra3oL9h68)a{Wv?m^BJd~9jjF9|dWOZ|RQuQN{dr5%Jwl2p1_U<^&6q4>tz=QHO;dc@?>{NPRmn zik+SjK?K`gG(c@2T$zU=cYSD5GH6tRa~^$_fDdH!&tsB!?6sl|6pM<6?qft!xtaNe zgl;c4+2a~la?r{|z1F?!&3PObItppUK0(6Yr45Lci^cecenRn8qzCHlAZL7}KzfrAyz8dT_%SFbWI zvL`#R5UgJfG3-9aNl0jIsf)tUh-7HSTy54X+Ij`xT<|c`S6IdKV})3q41XZ&o0$Zn zxzFB7`#(kLgL>4{lTyp1tKLjr#^Ci#JGT>SYij9hio6Uc4KD}eaDaV+LyZYC^N<=& zsG=pzO;07VZ6rV9b+~FVY4U}wq1e$5?n(!O;ujprv$fB}dsvu;zgs{86kn2vOLMe$ zRsE8BJYWrM?P}=nk~KsKqj71_XA4l$XC�p+iVVnpzowx%J~DIk|00&aI=78_nwU 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 From 0fe630cef7c2e96eb7e209e515db3fc6a81d6ae4 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 26 May 2021 22:06:16 +0200 Subject: [PATCH 037/116] Splash update --- .../resources/images/{Splash.png => splash.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/images/{Splash.png => splash.png} (100%) diff --git a/src/main/resources/images/Splash.png b/src/main/resources/images/splash.png similarity index 100% rename from src/main/resources/images/Splash.png rename to src/main/resources/images/splash.png From d49ee09ab90d559c778cdfde526889b514a35a0e Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Mon, 31 May 2021 20:33:36 +0200 Subject: [PATCH 038/116] Fixed improper treatment of Bi = 0 curves --- pom.xml | 2 +- .../java/pulse/problem/statements/Problem.java | 15 ++++++--------- .../java/pulse/search/direction/LMOptimiser.java | 5 ++--- src/main/resources/NumericProperty.xml | 4 ++-- src/main/resources/Version.txt | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index f36f267d..ffb5c0f7 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 kotik-coder PULsE - 1.91 + 1.91R PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index b0d94043..0705b85b 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -260,17 +260,14 @@ public void optimisationVector(ParameterVector output, List flags) { } protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { - Segment bounds; - if (properties.areThermalPropertiesLoaded()) { - bounds = new Segment(1e-5, properties.maxBiot()); + if(output.getTransform(i) == null) { + final double min = (double) def(HEAT_LOSS).getMinimum(); + final double max = (double) def(HEAT_LOSS).getMaximum(); + var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); + output.setTransform(i, new AtanhTransform(bounds) ); + output.setParameterBounds(i, bounds); } - else { - bounds = new Segment(1E-5, 2.0); - output.setTransform(i, LOG); - } - output.setParameterBounds(i, bounds); - output.setTransform(i, properties.areThermalPropertiesLoaded() ? new AtanhTransform(bounds) : LOG); output.set(i, Bi); } diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 53cb769b..f85dca30 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -89,12 +89,12 @@ public boolean iteration(SearchTask task) throws SolverException { * Delayed gratification */ - if (newCost > initialCost + EPS) { + if (newCost > initialCost + EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS) { p.setLambda(p.getLambda() * 2.0); task.assign(parameters); // roll back if cost increased p.setComputeJacobian(true); p.incrementFailedAttempts(); - accept = p.getFailedAttempts() > MAX_FAILED_ATTEMPTS; + accept = false; } else { p.resetFailedAttempts(); p.setLambda(p.getLambda() / 3.0); @@ -108,7 +108,6 @@ public boolean iteration(SearchTask task) throws SolverException { } - /** * Calculates the Jacobian, if needed, evaluates the gradient and the Hessian matrix. */ diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 58e4a47e..8e546b12 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -438,11 +438,11 @@ auto-adjustable="true" descriptor="Heat loss (side surface), Bi<sub>s</sub>" dimensionfactor="1.0" keyword="HEAT_LOSS_SIDE" maximum="10.0" - minimum="1e-10" value="0.1" primitive-type="double" discreet="false" + minimum="0.0" value="0.0" primitive-type="double" discreet="false" default-search-variable="true" /> Date: Wed, 2 Jun 2021 19:15:14 +0200 Subject: [PATCH 039/116] Fixed initial guess of Bi --- pom.xml | 4 ++-- src/main/resources/NumericProperty.xml | 4 ++-- src/main/resources/Version.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index ffb5c0f7..6887d17c 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 kotik-coder PULsE - 1.91R + 1.91FR PULsE Processing Unit for Laser flash Experiments @@ -122,4 +122,4 @@ UTF-8 UTF-8 - \ No newline at end of file + diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 8e546b12..941230b9 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -438,11 +438,11 @@ auto-adjustable="true" descriptor="Heat loss (side surface), Bi<sub>s</sub>" dimensionfactor="1.0" keyword="HEAT_LOSS_SIDE" maximum="10.0" - minimum="0.0" value="0.0" primitive-type="double" discreet="false" + minimum="0.0" value="1E-3" primitive-type="double" discreet="false" default-search-variable="true" /> Date: Thu, 3 Jun 2021 15:12:19 +0200 Subject: [PATCH 040/116] Fixes & Improvements - Fixed JUnit test not running - Updated .pom - Fixed JUnit test criterion for fluxes and derivatives. Now a linear regression is built and its coefficient of determination is checked. - Improved logging. Now important notes are showed in a pop-up window prior to being written in the log. Exceptions that are written in the log also trigger a ShowMessageDialog. --- pom.xml | 142 +++++++++--------- .../java/pulse/input/ExperimentalData.java | 2 +- .../pulse/problem/statements/Problem.java | 1 - src/main/java/pulse/ui/Launcher.java | 26 +++- .../pulse/ui/frames/SearchOptionsFrame.java | 11 +- src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 4 +- .../AnalyticalNonscatteringTestCase.java} | 16 +- .../DiscreteNonscatteringTestCase.java} | 14 +- .../NonscatteringSetup.java} | 39 ++++- .../ProfileLoader.java} | 6 +- .../QuadratureTest.java} | 22 +-- 12 files changed, 170 insertions(+), 115 deletions(-) rename src/test/java/{repository/AnalyticalNonscatteringTransferValidation.java => test/AnalyticalNonscatteringTestCase.java} (87%) rename src/test/java/{repository/DiscreteNonscatteringTransferValidation.java => test/DiscreteNonscatteringTestCase.java} (87%) rename src/test/java/{repository/NonscatteringTestCase.java => test/NonscatteringSetup.java} (71%) rename src/test/java/{repository/TestProfileLoader.java => test/ProfileLoader.java} (93%) rename src/test/java/{repository/QuadratureValidation.java => test/QuadratureTest.java} (88%) diff --git a/pom.xml b/pom.xml index 6887d17c..374577b9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,73 @@ 4.0.0 + kotik-coder PULsE - 1.91FR + 1.91FM PULsE Processing Unit for Laser flash Experiments + + + al + Artem Lunev + alounev@list.ru + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + org.jfree + jfreechart + 1.5.0 + + + com.weblookandfeel + weblaf-ui + 1.2.13 + + + org.apache.commons + commons-math3 + 3.6.1 + + + ca.umontreal.iro.simul + ssj + 3.3.1 + + + commons-io + commons-io + 2.6 + + + colt + colt + 1.2.0 + + + org.ejml + ejml-all + 0.39 + + + org.junit.jupiter + junit-jupiter + 5.7.0-M1 + test + + + + + src/main/java + src/test maven-compiler-plugin @@ -49,75 +111,19 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.0 - - - org.junit.platform - junit-platform-surefire-provider - 1.2.0 - - - - - src/test/java/ - - - + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.0 + - - - org.jfree - jfreechart - 1.5.0 - - - com.weblookandfeel - weblaf-ui - 1.2.13 - - - org.apache.commons - commons-math3 - 3.6.1 - - - ca.umontreal.iro.simul - ssj - 3.3.1 - - - commons-io - commons-io - 2.6 - - - colt - colt - 1.2.0 - - - org.ejml - ejml-all - 0.39 - - - org.junit.jupiter - junit-jupiter-engine - 5.2.0 - test - - - org.junit.platform - junit-platform-runner - 1.2.0 - test - - - + UTF-8 UTF-8 diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 05010f3b..0cad7510 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -228,7 +228,7 @@ public double halfRiseTime() { degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); if (index < 1) { - System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); + System.out.println(Messages.getString("ExperimentalData.HalfRiseError")); return max(getTimeSequence()) / FAIL_SAFE_FACTOR; } diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 0705b85b..fa150187 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,7 +1,6 @@ package pulse.problem.statements; import static pulse.input.listeners.CurveEventType.RESCALED; -import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 1c5f1ad3..137b2079 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -38,6 +38,7 @@ public class Launcher { private Launcher() { arrangeErrorOutput(); + arrangeMessages(); } /** @@ -66,7 +67,7 @@ public static void main(String[] args) { JOptionPane.showMessageDialog(null, "A new version of this software is available: " + newVersion.toString() + "
Please visit the PULsE website for more details."); } - + }); } @@ -97,7 +98,17 @@ private void arrangeErrorOutput() { try { var dir = new File(decodedPath).getParent(); errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); - setErr(new PrintStream(errorLog)); + setErr(new PrintStream(errorLog) { + + @Override + public void println(String str) { + super.println(str); + JOptionPane.showMessageDialog(null, "An exception has occurred. " + + "Please check the stored log!", "Exception", JOptionPane.ERROR_MESSAGE); + } + + } + ); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); @@ -106,6 +117,17 @@ private void arrangeErrorOutput() { createShutdownHook(); } + + private void arrangeMessages() { + System.setOut( new PrintStream(System.out) { + + @Override + public void println(String str) { + JOptionPane.showMessageDialog(null, Messages.getString("TextWrap.0") + str + Messages.getString("TextWrap.0")); + } + + }); + } private void createShutdownHook() { diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index abb2dfaf..b491c0bf 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -82,12 +82,11 @@ public SearchOptionsFrame() { public void update() { var selected = getInstance(); - if (selected != null) { - pathList.setSelectedIndex(pathSolvers.indexOf(selected)); - pathTable.updateTable(); - } else { - pathList.clearSelection(); - } + if (selected == null) + TaskManager.getManagerInstance().selectFirstTask(); + + pathList.setSelectedIndex(pathSolvers.indexOf(selected)); + pathTable.updateTable(); } class PathSolversList extends JList { diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index a7fe3445..afec0bcc 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.91FR \ No newline at end of file +1.91FM \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 9d470fcc..b71d01ab 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -279,4 +279,6 @@ ImplicitScheme.4=Fully Implicit Scheme

-MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
\ No newline at end of file +MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
+TextWrap.0=

+TextWrap.1=

\ No newline at end of file diff --git a/src/test/java/repository/AnalyticalNonscatteringTransferValidation.java b/src/test/java/test/AnalyticalNonscatteringTestCase.java similarity index 87% rename from src/test/java/repository/AnalyticalNonscatteringTransferValidation.java rename to src/test/java/test/AnalyticalNonscatteringTestCase.java index 4118cc43..ec790dd4 100644 --- a/src/test/java/repository/AnalyticalNonscatteringTransferValidation.java +++ b/src/test/java/test/AnalyticalNonscatteringTestCase.java @@ -1,10 +1,10 @@ -package repository; +package test; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.ProfileLoader.loadTestProfileDense; import java.util.List; @@ -19,10 +19,10 @@ import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; import pulse.problem.statements.model.ThermoOpticalProperties; -class AnalyticalNonscatteringTransferValidation { +class AnalyticalNonscatteringTestCase { private static List testProfile; - private static NonscatteringTestCase testCase; + private static NonscatteringSetup testCase; private NonscatteringRadiativeTransfer chand; private RadiativeTransferSolver dom; @@ -31,7 +31,7 @@ class AnalyticalNonscatteringTransferValidation { @BeforeAll static void setUpBeforeClass() throws Exception { testProfile = loadTestProfileDense(); - testCase = new NonscatteringTestCase(testProfile.size(), 10.0); + testCase = new NonscatteringSetup(testProfile.size(), 10.0); } @BeforeEach @@ -47,7 +47,7 @@ void testFluxesLowThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 2)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 0.1)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-1)); } @Test @@ -55,7 +55,7 @@ void testFluxesMediumThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 3)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 1.5)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-2, 1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-2)); } @Test @@ -63,7 +63,7 @@ void testFluxesHighThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 6)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 100.0)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-1, Double.POSITIVE_INFINITY)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-2)); } } \ No newline at end of file diff --git a/src/test/java/repository/DiscreteNonscatteringTransferValidation.java b/src/test/java/test/DiscreteNonscatteringTestCase.java similarity index 87% rename from src/test/java/repository/DiscreteNonscatteringTransferValidation.java rename to src/test/java/test/DiscreteNonscatteringTestCase.java index 77d8018a..550edce5 100644 --- a/src/test/java/repository/DiscreteNonscatteringTransferValidation.java +++ b/src/test/java/test/DiscreteNonscatteringTestCase.java @@ -1,10 +1,10 @@ -package repository; +package test; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.ProfileLoader.loadTestProfileDense; import java.util.List; @@ -19,10 +19,10 @@ import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; import pulse.problem.statements.model.ThermoOpticalProperties; -class DiscreteNonscatteringTransferValidation { +class DiscreteNonscatteringTestCase { private static List testProfile; - private static NonscatteringTestCase testCase; + private static NonscatteringSetup testCase; private NonscatteringRadiativeTransfer newton; private RadiativeTransferSolver dom; @@ -30,7 +30,7 @@ class DiscreteNonscatteringTransferValidation { @BeforeAll private static void setUpBeforeClass() { testProfile = loadTestProfileDense(); - testCase = new NonscatteringTestCase(testProfile.size(), 10.0); + testCase = new NonscatteringSetup(testProfile.size(), 10.0); } @BeforeEach @@ -48,14 +48,14 @@ void setUp() throws Exception { void testFluxesLowThickness() { var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 0.1)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-3, 1e-1)); } @Test void testFluxesMediumThickness() { var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 1.5)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-3, 1e-1)); } } \ No newline at end of file diff --git a/src/test/java/repository/NonscatteringTestCase.java b/src/test/java/test/NonscatteringSetup.java similarity index 71% rename from src/test/java/repository/NonscatteringTestCase.java rename to src/test/java/test/NonscatteringSetup.java index 00cdb810..d56f41cd 100644 --- a/src/test/java/repository/NonscatteringTestCase.java +++ b/src/test/java/test/NonscatteringSetup.java @@ -1,4 +1,4 @@ -package repository; +package test; import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; @@ -11,6 +11,8 @@ import java.util.List; +import org.apache.commons.math3.stat.regression.SimpleRegression; + import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.solvers.ImplicitCoupledSolver; @@ -18,12 +20,12 @@ import pulse.problem.statements.Pulse2D; import pulse.problem.statements.model.ThermoOpticalProperties; -public class NonscatteringTestCase { +public class NonscatteringSetup { private ParticipatingMedium testProblem; private DifferenceScheme testScheme; - public NonscatteringTestCase(final int testProfileSize, final double maxHeating) { + public NonscatteringSetup(final int testProfileSize, final double maxHeating) { testProblem = new ParticipatingMedium(); var properties = (ThermoOpticalProperties)testProblem.getProperties(); properties.setSpecificHeat(derive(SPECIFIC_HEAT, 300.0)); @@ -76,12 +78,35 @@ public boolean testFluxesAndDerivatives(List testProfile, RadiativeTrans var fluxes1 = solver1.getFluxes(); var fluxes2 = solver2.getFluxes(); + final int n = testProfile.size(); + double[][] test1 = new double[n][2]; + double[][] test2 = new double[n][2]; + + System.out.printf("%12s ; %12s ; %12s ; %12s %n", "Flux (Solver1)", "Flux (Solver2)", "Derivative (Solver1)", "Derivative (Solver2)"); for (int i = 1, size = testProfile.size(); i < size - 1 && pass; i++) { - pass &= approximatelyEquals(fluxes1.getFlux(i), fluxes2.getFlux(i), margin1); - pass &= approximatelyEquals(fluxes1.fluxDerivative(i), fluxes2.fluxDerivative(i), margin2); - System.out.printf("%1.4e ; % 1.4e %1.4e ; % 1.4e %n", fluxes1.getFlux(i), fluxes2.getFlux(i), - fluxes1.fluxDerivative(i), fluxes2.fluxDerivative(i)); + test1[i][0] = fluxes1.getFlux(i); + test1[i][1] = fluxes2.getFlux(i); + + test2[i][0] = fluxes1.fluxDerivative(i); + test2[i][1] = fluxes2.fluxDerivative(i); + + System.out.printf("%1.4e ; %1.4e %1.4e ; %1.4e %n", test1[i][0], test1[i][1], test2[i][0], test2[i][1]); } + + + var regression1 = new SimpleRegression(); + regression1.addData(test1); + double rsq1 = regression1.getRSquare(); + System.out.println("R-Squared of linear regression for fluxes: " + rsq1); + + var regression2 = new SimpleRegression(); + regression2.addData(test2); + double rsq2 = regression2.getRSquare(); + System.out.println("R-Squared of linear regression for derivatives: " + rsq2); + + pass &= approximatelyEquals(rsq1, 1.0, margin1); + pass &= approximatelyEquals(rsq2, 1.0, margin2); + System.out.println(pass ? "SUCCESS" : "FAILED"); return pass; } diff --git a/src/test/java/repository/TestProfileLoader.java b/src/test/java/test/ProfileLoader.java similarity index 93% rename from src/test/java/repository/TestProfileLoader.java rename to src/test/java/test/ProfileLoader.java index f1d80aa4..63488cbd 100644 --- a/src/test/java/repository/TestProfileLoader.java +++ b/src/test/java/test/ProfileLoader.java @@ -1,4 +1,4 @@ -package repository; +package test; import java.io.File; import java.io.FileNotFoundException; @@ -9,9 +9,9 @@ import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; -public class TestProfileLoader { +public class ProfileLoader { - private TestProfileLoader() { + private ProfileLoader() { //intentionally blank } diff --git a/src/test/java/repository/QuadratureValidation.java b/src/test/java/test/QuadratureTest.java similarity index 88% rename from src/test/java/repository/QuadratureValidation.java rename to src/test/java/test/QuadratureTest.java index 5a933e77..baa71ea3 100644 --- a/src/test/java/repository/QuadratureValidation.java +++ b/src/test/java/test/QuadratureTest.java @@ -1,12 +1,12 @@ -package repository; +package test; import static java.lang.Math.pow; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import static repository.NonscatteringTestCase.approximatelyEquals; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.NonscatteringSetup.approximatelyEquals; +import static test.ProfileLoader.loadTestProfileDense; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,7 @@ import pulse.problem.schemes.rte.exact.NewtonCotesQuadrature; import pulse.problem.statements.ParticipatingMedium; -class QuadratureValidation { +class QuadratureTest { private static ParticipatingMedium problem; private static List testProfile; @@ -41,7 +41,7 @@ class QuadratureValidation { @BeforeAll static void setUpBeforeClass() throws Exception { testProfile = loadTestProfileDense(); - problem = new NonscatteringTestCase(testProfile.size(), 0.1).getTestProblem(); + problem = new NonscatteringSetup(testProfile.size(), 0.1).getTestProblem(); var tempArray = testProfile.stream().mapToDouble(d -> d).toArray(); @@ -99,19 +99,21 @@ private boolean test(final double margin) { System.out.printf("%nSegments: %6d. Result = %3.4f", quad2.getIntegrator().getIntegrationSegments().getValue(), value); } + System.out.println(); + return approximatelyEquals(list.get(list.size() - 1), list2.get(list2.size() - 1), margin); } @Test void testFirstOrderConvergence() { - System.out.printf("%n%nFirst-order test"); + System.out.printf("%n%nQuadratures: First-order test"); prepareFirstOrder(quad1, quad2); - assertTrue( test(1E-3) ); + assertTrue( test(1E-2) ); } @Test void testSecondOrderConvergence() { - System.out.printf("%n%nSecond-order test"); + System.out.printf("%n%nQuadratures: Second-order test"); prepareSecondOrder(quad1, quad2); assertTrue( test(1E-3) ); } @@ -121,7 +123,7 @@ void testChandrasekhars() { prepareFirstOrder(quad1); quad1.setQuadraturePoints(derive(QUADRATURE_POINTS, 8)); final double result = quad1.integrate(); - assertTrue(approximatelyEquals(result, 1961.617, 1E-6)); + assertTrue(approximatelyEquals(result, 1961.617, 1E-4)); } @Test @@ -129,7 +131,7 @@ void testNewtonCotes() { prepareFirstOrder(quad2); quad2.getIntegrator().setIntegrationSegments(derive(INTEGRATION_SEGMENTS, 4096)); final double result = quad2.integrate(); - assertTrue(approximatelyEquals(result, 1962.45, 1E-6)); + assertTrue(approximatelyEquals(result, 1965.20, 1E-4)); } } \ No newline at end of file From a4790ab3aba176872e7dc503a2910d05e44569c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Jun 2021 13:13:17 +0000 Subject: [PATCH 041/116] Bump commons-io from 2.6 to 2.7 Bumps commons-io from 2.6 to 2.7. Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 374577b9..2151e49a 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ commons-io commons-io - 2.6 + 2.7 colt From 093ea66109c6ec44d253b23ee7937469cec84667 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 11 Jun 2021 10:48:30 +0200 Subject: [PATCH 042/116] Fixed half-time Fixed half-time determination for heating curves with high Bi numbers. --- .../java/pulse/input/ExperimentalData.java | 13 +++++++--- .../pulse/search/statistics/AICStatistic.java | 4 +++ .../statistics/AndersonDarlingTest.java | 12 +++++++++ .../pulse/search/statistics/BICStatistic.java | 6 +++++ .../java/pulse/search/statistics/KSTest.java | 7 ++++- .../statistics/ModelSelectionCriterion.java | 26 +++++++++++++++++-- .../search/statistics/NormalityTest.java | 11 ++++++++ .../search/statistics/OptimiserStatistic.java | 6 +++++ .../search/statistics/PearsonCorrelation.java | 6 +++++ .../statistics/RegularisedLeastSquares.java | 16 +++++++++++- .../search/statistics/ResidualStatistic.java | 25 ++++++++++++++++++ .../statistics/SpearmansCorrelationTest.java | 2 +- .../pulse/search/statistics/Statistic.java | 6 +++++ .../pulse/search/statistics/SumOfSquares.java | 8 ++---- .../java/pulse/tasks/processing/Buffer.java | 16 +++++++++--- 15 files changed, 146 insertions(+), 18 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 0cad7510..9cd5f4d2 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; import pulse.AbstractData; -import pulse.baseline.FlatBaseline; +import pulse.baseline.LinearBaseline; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataListener; import pulse.properties.NumericProperty; @@ -219,11 +219,18 @@ public double maxAdjustedSignal() { public double halfRiseTime() { var degraded = runningAverage(REDUCTION_FACTOR); double max = (max(degraded, pointComparator)).getY(); - var baseline = new FlatBaseline(); + var baseline = new LinearBaseline(); baseline.fitTo(this); double halfMax = (max + baseline.valueAt(0)) / 2.0; - + + int cutoffIndex = degraded.size() - 1; + + for(int i = cutoffIndex; i > 0 && degraded.get(i).getY() < halfMax; i--) + cutoffIndex--; + + degraded = degraded.subList(0, cutoffIndex); + int index = IndexRange.closestLeft(halfMax, degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 6b4ed00a..6605fd67 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -25,6 +25,10 @@ public ModelSelectionCriterion copy() { return new AICStatistic(this); } + /** + * @return the AIC penalising term. + */ + @Override public double penalisingTerm(final int kq, final int n) { return 2.0 * (kq + 1); diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 833580a9..de6fc0b3 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -10,8 +10,20 @@ import umontreal.ssj.gof.GofStat; import umontreal.ssj.probdist.NormalDist; +/** + * The Anderson-Darling normality test. In this variant of the test, the mean and the variance + * are assumed to be known. + * + */ + public class AndersonDarlingTest extends NormalityTest { + /** + * This uses the SSJ statistical library to calculate the Anderson-Darling test + * with the input parameters formed by the {@code task} residuals and a normal distribution + * with zero mean and variance equal to the residuals variance. + */ + @Override public boolean test(SearchTask task) { calculateResiduals(task); diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java index d62a4d5d..9077eea5 100644 --- a/src/main/java/pulse/search/statistics/BICStatistic.java +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -4,6 +4,8 @@ /** * Bayesian Information Criterion (BIC) algorithm formulated for the Gaussian distribution of residuals. + * This is used in model selection. BIC values are always negative. The absolute BIC value is meaningless, + * it is only used as a comparative statistic. * */ @@ -26,6 +28,10 @@ public ModelSelectionCriterion copy() { return new BICStatistic(this); } + /** + * @return the BIC penalising term + */ + @Override public double penalisingTerm(final int kq, final int n) { return (kq + 1)*log(n); diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index 7831d7fb..dd416bef 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -10,6 +10,11 @@ import pulse.tasks.SearchTask; +/** + * The Kolmogorov-Smirnov normality test as implemented in {@code ApacheCommonsMath}. + * + */ + public class KSTest extends NormalityTest { private double[] residuals; @@ -21,7 +26,7 @@ public boolean test(SearchTask task) { setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); return significanceTest(); } - + @Override public void evaluate(SearchTask t) { calculateResiduals(t); diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 352ec1bb..aa35220a 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -23,7 +23,7 @@ public abstract class ModelSelectionCriterion extends Statistic { private OptimiserStatistic os; - private int kq; + private int kq; //the number of parameters (dimensionality of the search vector) private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); private double criterion; @@ -40,10 +40,16 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { @Override public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of variables + kq = t.alteredParameters().size(); //number of parameters calcCriterion(); } + /** + * This calculates either the AIC or BIC statistic, which only differ + * by the penalising term. + * @see penalisingTerm() + */ + public void calcCriterion() { final int n = os.getResiduals().size(); //sample size criterion = n * log(os.variance()) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; @@ -61,11 +67,27 @@ public void calcCriterion() { public abstract ModelSelectionCriterion copy(); + /** + * Calculates the weight (in the Akaike sense) when comparing the model associated + * with this statistic with other models represented by statistics of the same type. + * @param the selection statistics of the same type as this one + * @return a {@code NumericProperty} of the {@code MODEL_WEIGHT} type, which is the probability + * this model is the best one. + */ + public NumericProperty weight(List all) { + if(all.stream().anyMatch(s -> s.getClass() != this.getClass())) + throw new IllegalArgumentException("Cannot mix different model selection criteria!"); final double sum = all.stream().map(criterion -> criterion.probability(all)).reduce( (a, b) -> a + b).get(); return derive(MODEL_WEIGHT, probability(all)/sum); } + /** + * Calculates the probability that this model is the best among {@code all} others. + * @param all statistics from models that will be compared with this one + * @return the probability, which is a decimal value within the [0,1] range. + */ + public double probability(List all) { final double min = all.stream().map(criterion -> (double)criterion.getStatistic().getValue()).reduce( (a, b) -> a < b ? a : b).get(); final double di = (double)this.getStatistic().getValue() - min; diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index fa311532..59b9aff0 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -11,6 +11,17 @@ import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; +/** + * A normality test is invoked after a task finishes, to validate its result. + * It may be used as an acceptance criterion for tasks. + * + * For the test to pass, the model residuals need be distributed according + * to a (0, σ) normal distribution, where σ is the variance of the model residuals. As + * this is the pre-requisite for optimizers based on the ordinary least-square statistic, the normality + * test can also be used to estimate if a fit 'failed' or 'succeeded' in describing the data. + * + */ + public abstract class NormalityTest extends ResidualStatistic { private double statistic; diff --git a/src/main/java/pulse/search/statistics/OptimiserStatistic.java b/src/main/java/pulse/search/statistics/OptimiserStatistic.java index 6822d0d3..14506925 100644 --- a/src/main/java/pulse/search/statistics/OptimiserStatistic.java +++ b/src/main/java/pulse/search/statistics/OptimiserStatistic.java @@ -1,5 +1,11 @@ package pulse.search.statistics; +/** + * An Optimiser statistic is simply the objective function that is calculated + * by the Optimiser. + * + */ + public abstract class OptimiserStatistic extends ResidualStatistic { private static String selectedOptimiserDescriptor; diff --git a/src/main/java/pulse/search/statistics/PearsonCorrelation.java b/src/main/java/pulse/search/statistics/PearsonCorrelation.java index 1de6b0db..64819e9b 100644 --- a/src/main/java/pulse/search/statistics/PearsonCorrelation.java +++ b/src/main/java/pulse/search/statistics/PearsonCorrelation.java @@ -2,6 +2,12 @@ import org.apache.commons.math3.stat.correlation.PearsonsCorrelation; +/** + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Pearson Correlation. + * + */ + + public class PearsonCorrelation extends CorrelationTest { @Override diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index 7ab66433..1b070b60 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -5,6 +5,13 @@ import pulse.tasks.SearchTask; +/** + * This is an experimental feature. The objective function here is equal to the ordinary least-square (OLS) + * plus a penalising term proportional to the squared length of a search vector. This way, search vectors + * of lower dimensionality are favoured. + * + */ + public class RegularisedLeastSquares extends OptimiserStatistic { private double lambda = 1e-4; @@ -20,6 +27,11 @@ public RegularisedLeastSquares(RegularisedLeastSquares rls) { sos = new SumOfSquares(rls.sos); this.lambda = rls.lambda; } + + /** + * The lambda is the regularisation strength. + * @return the lambda factor. + */ public double getLambda() { return lambda; @@ -30,7 +42,9 @@ public void setLambda(double lambda) { } /* - * SSR with L2 regularisation + * OLS with L2 regularisation. The penalisation term is equal to {@code lambda} times the + * L2 norm of the search vector. + * @see pulse.search.statistics.SumOfSquares */ @Override diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 3da20f2e..9ef09e92 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -15,6 +15,15 @@ import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; +/** + * An abstract statistic (= a numeric value resulting from a statistical procedure) that operates with model residuals. + * The list of residuals is stored in a field value for objects of this class. Each {@code SearchTask} will have at + * least two {@code ResidualStatistic}s associated with its {@code Calculation}s. + * @see pulse.tasks.SearchTask + * @see pulse.tasks.Calculation + * + */ + public abstract class ResidualStatistic extends Statistic { private double statistic; @@ -35,6 +44,22 @@ public double[] transformResiduals() { return getResiduals().stream().map(doubleArray -> doubleArray[1]) .mapToDouble(Double::doubleValue).toArray(); } + + /** + * This will calculate the residuals for the {@code task} using the time sequence defined + * by the {@code ExperimentalData} object. The residuals are calculated between the model, + * which was previously used to populate the {@code HeatingCurve} and the experimental data. + * The temperature value of the model at the reference time is ti. + * and unknown a priori. Therefore, it needs to be interpolated based on the discrete dataset + * generated by the solver. The interpolation is currently done using natural cubic splines, + * which are re-constructed each time a new solution is generated. Therefore, calling this method + * does not involve expensive calculation of the spline coefficents. The residuals are calculated + * only for the range that is specified by the {@code ExperimentalData} reference. The output of this method + * is stored in the field of the {@code residuals} object. + * @param task the optimisation task + * @see pulse.input.ExperimentalData + * @see pulse.HeatingCurve + */ public void calculateResiduals(SearchTask task) { var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); diff --git a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java index f4b8adb4..0e6ae516 100644 --- a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java @@ -3,7 +3,7 @@ import org.apache.commons.math3.stat.correlation.SpearmansCorrelation; /** - * Wrapper CorrelationTest class for ApacheCommonsMath Spearmans Correlation. + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Spearmans Correlation. * */ diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 0c170752..6f84cca7 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -5,6 +5,12 @@ import pulse.util.PropertyHolder; import pulse.util.Reflexive; +/** + * A statistic is an abstract class that hosts the {@code evaluate} method + * to validate the results of a {@code SearchTask}. + * + */ + public abstract class Statistic extends PropertyHolder implements Reflexive { public abstract void evaluate(SearchTask t); diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index 038ef48a..d88de831 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -33,14 +33,10 @@ public SumOfSquares(SumOfSquares sos) { * reference's time list, which generally does not match to that of this * heating curve. The * T(ti) - * is the interpolated value for this heating curve at the reference time. The - * temperature value is interpolated using two nearest elements of the - * baseline-subtracted temperature list. The value is interpolated using - * the experimental time ti and the nearest - * solution points to that time. The accuracy of this interpolation depends on - * the number of points. + * is the interpolated value. * * @param t The task containing the reference and calculated curves + * @see calculateResiduals() */ @Override diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index 30298e3e..61f65c3e 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -19,7 +19,7 @@ /** * A {@code Buffer} is used to estimate the convergence of the reverse problem * solution, by comparing the variance of the properties to a pre-specified - * error tolerance. + * error tolerance. * * @see pulse.tasks.SearchTask.run() */ @@ -47,6 +47,10 @@ public Buffer() { public ParameterVector[] getData() { return data; } + + /* + * Re-inits the storage. + */ public void init() { this.data = new ParameterVector[size]; @@ -55,7 +59,7 @@ public void init() { /** * (Over)writes a buffer cell corresponding to the {@code bufferElement} with - * the current set of parameters of {@code SearchTask}. + * the current set of parameters of {@code SearchTask} and the search statistic. * * @param t the {@code SearchTask} * @param bufferElement the {@code bufferElement} which will be written over @@ -115,13 +119,12 @@ public double average(NumericPropertyKeyword index) { } /** - * Calculated the average statistic value + * Calculates the average statistic value * * @return the mean statistic value. */ public double averageStatistic() { - double av = 0; for (double ss : statistic) { @@ -177,6 +180,11 @@ public static void setSize(NumericProperty newSize) { Buffer.size = ((Number) newSize.getValue()).intValue(); } + /* + * Sets the buffer size + * @param type @code{BUFFER_SIZE} + */ + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == BUFFER_SIZE) From 8fb90b3f4d4a11bf24585ebacc0263b0955dd9aa Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 11 Jun 2021 11:04:09 +0200 Subject: [PATCH 043/116] Fixed Proteus Netzsch import Included import of diameter --- src/main/java/pulse/input/ExperimentalData.java | 4 ++-- src/main/java/pulse/io/readers/NetzschCSVReader.java | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 9cd5f4d2..9ae0d8ad 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; import pulse.AbstractData; -import pulse.baseline.LinearBaseline; +import pulse.baseline.FlatBaseline; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataListener; import pulse.properties.NumericProperty; @@ -219,7 +219,7 @@ public double maxAdjustedSignal() { public double halfRiseTime() { var degraded = runningAverage(REDUCTION_FACTOR); double max = (max(degraded, pointComparator)).getY(); - var baseline = new LinearBaseline(); + var baseline = new FlatBaseline(); baseline.fitTo(this); double halfMax = (max + baseline.valueAt(0)) / 2.0; diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 3a113a7a..3b4c31c3 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -39,7 +39,8 @@ public class NetzschCSVReader implements CurveReader { private final static String SHOT_DATA = "Shot_data"; private final static String DETECTOR = "DETECTOR"; private final static String THICKNESS = "Thickness_RT"; - + private final static String DIAMETER = "Diameter"; + /** * Note comma is included as a delimiter character here. */ @@ -90,6 +91,9 @@ public List read(File file) throws IOException { var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); final double thickness = Double.parseDouble( tempTokens[tempTokens.length - 1] ) * TO_METRES; + tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); + final double diameter = Double.parseDouble( tempTokens[tempTokens.length - 1] ) * TO_METRES; + tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); final double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; @@ -109,6 +113,9 @@ public List read(File file) throws IOException { var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); + met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); + met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, 0.85*diameter)); + met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94*diameter)); curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); From 0f2f95bd0c67c0942a27d3e3d83dc652854762c6 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 11 Jun 2021 21:07:47 +0200 Subject: [PATCH 044/116] 1.91FM_01 Better error handling in SearchTask: now instead of freezing on nonsensical values, an exception is thrown by the assign() method in ThermalProperties if the numeric properties in the ParameterVector do not pass the validate() check, and is handled by setting the status of the task to failed and breaking the main loop of the search. Better handling of very small pulses where calculations could previously start with a pulse width = 0 (less than the grid time step) Better calculation of the maximum temperature rise for the heating curves (baseline correction) --- pom.xml | 2 +- .../java/pulse/input/ExperimentalData.java | 22 ++++++++----------- src/main/java/pulse/math/ParameterVector.java | 10 +++++++++ .../pulse/problem/laser/DiscretePulse.java | 2 +- .../solvers/ImplicitLinearisedSolver.java | 1 + .../statements/ClassicalProblem2D.java | 3 ++- .../problem/statements/CoreShellProblem.java | 3 ++- .../problem/statements/DiathermicMedium.java | 3 ++- .../problem/statements/NonlinearProblem.java | 4 +++- .../statements/ParticipatingMedium.java | 3 ++- .../statements/PenetrationProblem.java | 3 ++- .../pulse/problem/statements/Problem.java | 11 ++++++++-- src/main/java/pulse/search/Optimisable.java | 4 +++- src/main/java/pulse/tasks/SearchTask.java | 18 +++++++++++---- src/main/java/pulse/ui/Launcher.java | 6 ++--- .../ui/components/panels/ProblemToolbar.java | 9 ++++---- src/main/resources/NumericProperty.xml | 6 ++--- src/main/resources/Version.txt | 2 +- 18 files changed, 72 insertions(+), 40 deletions(-) diff --git a/pom.xml b/pom.xml index 2151e49a..53dd7b1b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.91FM + 1.91FM_01 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 9ae0d8ad..8af92946 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -157,8 +157,8 @@ public List runningAverage(int reductionFactor) { List crudeAverage = new ArrayList<>(count / reductionFactor); - int start = indexRange.getLowerBound(); - int end = indexRange.getUpperBound(); + int start = indexRange.getLowerBound(); + int end = indexRange.getUpperBound(); int step = (end - start) / (count / reductionFactor); double av = 0; @@ -193,9 +193,9 @@ public List runningAverage(int reductionFactor) { * @see pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) */ - public double maxAdjustedSignal() { + public Point2D maxAdjustedSignal() { var degraded = runningAverage(REDUCTION_FACTOR); - return (max(degraded, pointComparator)).getY(); + return max(degraded, pointComparator); } /** @@ -217,18 +217,14 @@ public double maxAdjustedSignal() { */ public double halfRiseTime() { - var degraded = runningAverage(REDUCTION_FACTOR); - double max = (max(degraded, pointComparator)).getY(); - var baseline = new FlatBaseline(); + var degraded = runningAverage(REDUCTION_FACTOR); + var max = (max(degraded, pointComparator)); + var baseline = new FlatBaseline(); baseline.fitTo(this); - double halfMax = (max + baseline.valueAt(0)) / 2.0; - - int cutoffIndex = degraded.size() - 1; - - for(int i = cutoffIndex; i > 0 && degraded.get(i).getY() < halfMax; i--) - cutoffIndex--; + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + int cutoffIndex = degraded.indexOf(max); degraded = degraded.subList(0, cutoffIndex); int index = IndexRange.closestLeft(halfMax, diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index d815ab4f..e8e42667 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -5,6 +5,7 @@ import pulse.math.linear.Vector; import pulse.math.transforms.Transformable; +import pulse.properties.NumericProperties; import pulse.properties.NumericPropertyKeyword; /** @@ -214,6 +215,15 @@ public String toString() { sb.append(" Values: " + super.toString()); return sb.toString(); } + + public boolean validate() { + for(int i = 0; i < this.dimension(); i++) { + if( !NumericProperties.derive(this.getIndex(i), inverseTransform(i)).validate() ) { + return false; + } + } + return true; + } public Segment[] getBounds() { return bounds; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 68c88e42..d3d511d3 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -70,7 +70,7 @@ public double laserPowerAt(double time) { public void recalculate() { final double width = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - discretePulseWidth = grid.gridTime(width, timeFactor); + discretePulseWidth = Math.max( grid.gridTime(width, timeFactor), grid.getTimeStep() ); } /** diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index ad19bf89..a24f47a5 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -116,6 +116,7 @@ public double evalRightBoundary(final int m, final double alphaN, final double b return (HH * getPreviousSolution()[N] + 2. * tau * betaN) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); } + @Override public DifferenceScheme copy() { var grid = getGrid(); diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 143c01b5..73a7c107 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -14,6 +14,7 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.Grid2D; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; @@ -102,7 +103,7 @@ public void optimisationVector(ParameterVector output, List flags) { } @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (ExtendedThermalProperties) getProperties(); diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java index 85335c7c..b30f53d7 100644 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ b/src/main/java/pulse/problem/statements/CoreShellProblem.java @@ -14,6 +14,7 @@ import pulse.math.transforms.InvDiamTransform; import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.InvLenTransform; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -139,7 +140,7 @@ public void optimisationVector(ParameterVector output, List flags) { } @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index f2516f9c..ee2e31ad 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -11,6 +11,7 @@ import pulse.math.transforms.AtanhTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitDiathermicSolver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.DiathermicProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; @@ -80,7 +81,7 @@ public void optimisationVector(ParameterVector output, List flags) { } @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (DiathermicProperties) this.getProperties(); diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 5dc7fec7..1b0f7b1d 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -17,6 +17,7 @@ import pulse.math.transforms.AtanhTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.Property; @@ -96,11 +97,12 @@ public void optimisationVector(ParameterVector output, List flags) { * * @param params the optimisation vector, containing a similar set of parameters * to this {@code Problem} + * @throws SolverException * @see listedTypes() */ @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); var p = getProperties(); diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index e35947ca..155f68e7 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -12,6 +12,7 @@ import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; @@ -85,7 +86,7 @@ public void optimisationVector(ParameterVector output, List flags) { } @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (ThermoOpticalProperties)getProperties(); diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index d2e96338..20361b6e 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -10,6 +10,7 @@ import pulse.math.Segment; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.AbsorptionModel; import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.Flag; @@ -94,7 +95,7 @@ public void optimisationVector(ParameterVector output, List flags) { } @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { super.assign(params); double value; diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index fa150187..6990cb50 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -24,6 +24,7 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.solvers.Solver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -199,7 +200,8 @@ public void retrieveData(ExperimentalData c) { */ public void estimateSignalRange(ExperimentalData c) { - final double signalHeight = c.maxAdjustedSignal() - baseline.valueAt(0); + var maxPoint = c.maxAdjustedSignal(); + final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); } @@ -279,7 +281,12 @@ protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { */ @Override - public void assign(ParameterVector params) { + public void assign(ParameterVector params) throws SolverException { + + if(!params.validate()) { + throw new SolverException("Parameter values not sensible"); + } + baseline.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 7909db12..5512f3a0 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -3,6 +3,7 @@ import java.util.List; import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; /** @@ -20,10 +21,11 @@ public interface Optimisable { * * @param params the optimisation vector, containing a similar set of parameters * to this {@code Problem} + * @throws SolverException if {@code params} contains invalid parameter values * @see pulse.util.PropertyHolder.listedTypes() */ - public void assign(ParameterVector params); + public void assign(ParameterVector params) throws SolverException; /** * Calculates the vector argument defined on Rn diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index eb1aa8c8..f6077cd4 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -48,6 +48,7 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; +import pulse.tasks.logs.Details; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; @@ -217,7 +218,14 @@ public ParameterVector searchVector() { */ public void assign(ParameterVector searchParameters) { - current.getProblem().assign(searchParameters); + try { + current.getProblem().assign(searchParameters); + } catch (SolverException e) { + var status = FAILED; + status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + e.printStackTrace(); + } curve.getRange().assign(searchParameters); } @@ -282,7 +290,7 @@ public void run() { outer: do { bufferFutures.clear(); - + for (var i = 0; i < bufferSize; i++) { if (current.getStatus() != IN_PROGRESS) @@ -291,12 +299,14 @@ public void run() { int iter = 0; try { - for (boolean finished = false; !finished && iter < maxIterations; iter++) - finished = optimiser.iteration(this); + for (boolean finished = false; !finished && iter < maxIterations; iter++) { + finished = optimiser.iteration(this); + } } catch (SolverException e) { setStatus(FAILED); System.err.println(this + " failed during execution. Details: "); e.printStackTrace(); + break outer; } if(iter >= maxIterations) { diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 137b2079..f3528731 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -37,7 +37,8 @@ public class Launcher { private final static boolean DEBUG = false; private Launcher() { - arrangeErrorOutput(); + if(!DEBUG) + arrangeErrorOutput(); arrangeMessages(); } @@ -82,9 +83,6 @@ private static void splashScreen() { } private void arrangeErrorOutput() { - if (DEBUG) - return; - String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); String decodedPath = ""; // diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index f619a64c..fb1191af 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -57,12 +57,13 @@ private void addListeners() { // simulate btn listener btnSimulate.addActionListener((ActionEvent e) -> { + if (instance.getSelectedTask() == null) + instance.selectFirstTask(); + var t = instance.getSelectedTask(); - - if (t == null) - return; - + var calc = t.getCurrentCalculation(); + t.checkProblems(true); var status = t.getCurrentCalculation().getStatus(); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 941230b9..a4158e4f 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -346,7 +346,7 @@ abbreviation="<i>t</i><sub>lim</sub>" auto-adjustable="true" descriptor="End time, <i>t</i><sub>lim</sub> (s)" - dimensionfactor="1.0" keyword="TIME_LIMIT" maximum="20.0" + dimensionfactor="1.0" keyword="TIME_LIMIT" maximum="100.0" minimum="1.0E-6" value="1.0" primitive-type="double" discreet="true" default-search-variable="false" /> Date: Sun, 13 Jun 2021 14:05:18 +0200 Subject: [PATCH 045/116] Fixed calculation range handling - Fixed calculation limit not updating on increase of calculation range - Fixed data event handling related to changes in the range. Removed the TRUNCATED type. - Fixed copy constructor of NumericProperty not making exact copies of the property, e.g. discrete state was ignored - Allowed optimizers to select a larger gradient step for properties marked discrete - The gradient step is now relative, rather than absolute --- .../java/pulse/input/ExperimentalData.java | 11 ++++---- src/main/java/pulse/input/Range.java | 17 +++++++++---- .../pulse/input/listeners/DataEventType.java | 6 ++--- .../java/pulse/io/export/XMLConverter.java | 6 ++--- .../pulse/problem/statements/Problem.java | 7 +----- .../pulse/properties/NumericProperty.java | 25 +++++++++++-------- .../direction/GradientBasedOptimiser.java | 10 +++++--- .../pulse/search/direction/LMOptimiser.java | 13 +++++++--- .../search/statistics/ResidualStatistic.java | 8 +++--- src/main/java/pulse/tasks/SearchTask.java | 2 +- .../ui/components/panels/ChartToolbar.java | 19 ++++++++++---- src/main/resources/NumericProperty.xml | 12 ++++----- 12 files changed, 79 insertions(+), 57 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 8af92946..3c1a9be6 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -2,7 +2,6 @@ import static java.lang.Double.valueOf; import static java.util.Collections.max; -import static pulse.input.listeners.DataEventType.TRUNCATED; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; @@ -18,6 +17,7 @@ import pulse.AbstractData; import pulse.baseline.FlatBaseline; import pulse.input.listeners.DataEvent; +import pulse.input.listeners.DataEventType; import pulse.input.listeners.DataListener; import pulse.properties.NumericProperty; import pulse.ui.Messages; @@ -301,10 +301,7 @@ public void truncate() { final double halfMaximum = halfRiseTime(); final double cutoff = CUTOFF_FACTOR * halfMaximum; - this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); - this.indexRange.set(getTimeSequence(), range); - - fireDataChanged(new DataEvent(TRUNCATED, this)); + this.range.setUpperBound(derive(UPPER_BOUND, cutoff));; } /** @@ -407,8 +404,10 @@ private void doSetRange() { indexRange.set(time, range); addHierarchyListener(l -> { - if (l.getSource() == range) + if (l.getSource() == range) { indexRange.set(time, range); + this.fireDataChanged(new DataEvent(DataEventType.RANGE_CHANGED, this)); + } }); if (metadata != null) diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 9a473a26..d4106833 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -11,6 +11,7 @@ import pulse.math.ParameterVector; import pulse.math.Segment; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -170,8 +171,8 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public void optimisationVector(ParameterVector output, List flags) { - double len = segment.length(); - var bounds = new Segment(-0.25 * len, 0.25 * len); + var curve = (ExperimentalData)this.getParent(); + Segment bounds; for (int i = 0, size = output.dimension(); i < size; i++) { @@ -180,9 +181,12 @@ public void optimisationVector(ParameterVector output, List flags) { switch (key) { case UPPER_BOUND: output.set(i, segment.getMaximum()); + var seq = curve.getTimeSequence(); + bounds = new Segment( 1.1 * curve.maxAdjustedSignal().getX(), seq.get(seq.size() - 1) ); break; case LOWER_BOUND: output.set(i, segment.getMinimum()); + bounds = new Segment( 0.0, 0.35 * curve.halfRiseTime() ); break; default: continue; @@ -198,11 +202,14 @@ public void optimisationVector(ParameterVector output, List flags) { * Tries to assign the upper and lower bound based on {@code params}. * * @param params an {@code IndexedVector} which may contain the bounds. + * @throws SolverException */ @Override - public void assign(ParameterVector params) { - + public void assign(ParameterVector params) throws SolverException { + if(!params.validate()) + throw new SolverException("Parameter values not sensible"); + NumericProperty p = null; for (int i = 0, size = params.dimension(); i < size; i++) { @@ -219,7 +226,7 @@ public void assign(ParameterVector params) { default: continue; } - + } } diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index e1480c0b..e3d48138 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -8,13 +8,13 @@ public enum DataEventType { /** *

- * The {@code TRUNCATED} {@code DataEventType} indicates the range of the - * {@code ExperimentalData} has been truncated. Note this means that only the + * The {@code RANGE_CHANGED} {@code DataEventType} indicates the range of the + * {@code ExperimentalData} has either been truncated or extended. Note this means that only the * range is affected and not the data itself. * * @see pulse.input.ExperimentalData.truncate() */ - TRUNCATED + RANGE_CHANGED } \ No newline at end of file diff --git a/src/main/java/pulse/io/export/XMLConverter.java b/src/main/java/pulse/io/export/XMLConverter.java index 0cc40769..cd2b3fe3 100644 --- a/src/main/java/pulse/io/export/XMLConverter.java +++ b/src/main/java/pulse/io/export/XMLConverter.java @@ -177,8 +177,8 @@ public static List readXML(InputStream inputStream) Element eElement = (Element) nNode; NumericPropertyKeyword keyword = NumericPropertyKeyword.valueOf(eElement.getAttribute("keyword")); boolean autoAdjustable = Boolean.valueOf(eElement.getAttribute("auto-adjustable")); - boolean discreet = Boolean.valueOf(eElement.getAttribute("discreet")); - String descriptor = eElement.getAttribute("descriptor"); + boolean discrete = Boolean.valueOf(eElement.getAttribute("discreet")); + String descriptor = eElement.getAttribute("descriptor"); String abbreviation = eElement.getAttribute("abbreviation"); boolean defSearch = Boolean.valueOf(eElement.getAttribute("default-search-variable")); @@ -200,7 +200,7 @@ public static List readXML(InputStream inputStream) np.setDescriptor(descriptor); np.setAbbreviation(abbreviation); np.setAutoAdjustable(autoAdjustable); - np.setDiscreet(discreet); + np.setDiscrete(discrete); np.setDefaultSearchVariable(defSearch); properties.add(np); } diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 6990cb50..c2b9024c 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -281,12 +281,7 @@ protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { */ @Override - public void assign(ParameterVector params) throws SolverException { - - if(!params.validate()) { - throw new SolverException("Parameter values not sensible"); - } - + public void assign(ParameterVector params) throws SolverException { baseline.assign(params); for (int i = 0, size = params.dimension(); i < size; i++) { diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index a06e8ce5..f12c962f 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -38,7 +38,7 @@ public class NumericProperty implements Property, Comparable { private NumericPropertyKeyword type; private boolean autoAdjustable; - private boolean discreet; + private boolean discrete; private boolean defaultSearchVariable; /** @@ -82,14 +82,17 @@ public NumericProperty(NumericPropertyKeyword type, Number... params) { */ public NumericProperty(NumericProperty num) { - this.value = num.value; - this.descriptor = num.descriptor; - this.abbreviation = num.abbreviation; - this.minimum = num.minimum; - this.maximum = num.maximum; - this.type = num.type; + this.value = num.value; + this.descriptor = num.descriptor; + this.abbreviation = num.abbreviation; + this.minimum = num.minimum; + this.maximum = num.maximum; + this.type = num.type; + this.discrete = num.discrete; this.dimensionFactor = num.dimensionFactor; - this.autoAdjustable = num.autoAdjustable; + this.autoAdjustable = num.autoAdjustable; + this.error = num.error; + this.defaultSearchVariable = num.defaultSearchVariable; } public NumericPropertyKeyword getType() { @@ -270,11 +273,11 @@ public int compareTo(NumericProperty arg0) { } public boolean isDiscrete() { - return discreet; + return discrete; } - public void setDiscreet(boolean discreet) { - this.discreet = discreet; + public void setDiscrete(boolean discrete) { + this.discrete = discrete; } @Override diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index db833731..16e8aa94 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -11,6 +11,7 @@ import pulse.math.ParameterVector; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -78,11 +79,14 @@ public Vector gradient(SearchTask task) throws SolverException { final var params = task.searchVector(); var grad = new Vector(params.dimension()); - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - final double dx = discreteGradient ? dxGrid : gradientResolution; + final double resolutionHigh = (double) getGradientResolution().getValue(); + final double resolutionLow = 5E-2; //TODO + for (int i = 0; i < params.dimension(); i++) { + boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); + double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); + final var shift = new Vector(params.dimension()); shift.set(i, 0.5 * dx); diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index f85dca30..1c219f2f 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -15,6 +15,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -174,10 +175,14 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { var jacobian = new double[numPoints][numParams]; - final double dx = super.getGradientStep(); + final double resolutionHigh = super.getGradientStep(); + final double resolutionLow = 1E-2; //TODO for (int i = 0; i < numParams; i++) { + boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); + double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); + final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); @@ -191,7 +196,7 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { task.solveProblemAndCalculateCost(); var r2 = residualVector(residualCalculator); - for (int j = 0; j < numPoints; j++) { + for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { jacobian[j][i] = (r1[j] - r2[j]) / dx; @@ -217,8 +222,8 @@ public GradientGuidedPath initState(SearchTask t) { } private Vector halfGradient(LMPath path) { - var jacobian = path.getJacobian(); - var residuals = path.getResidualVector(); + var jacobian = path.getJacobian(); + var residuals = path.getResidualVector(); return jacobian.transpose().multiply(new Vector(residuals)); } diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 9ef09e92..22ae8660 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -71,11 +71,11 @@ public void calculateResiduals(SearchTask task) { var s = estimate.getSplineInterpolation(); - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); - - double interpolated; + int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); + int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); + double interpolated; + for (int i = startIndex; i <= endIndex; i++) { /* * find the point on the calculated heating curve which has the closest time diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index f6077cd4..f9e25d83 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -220,13 +220,13 @@ public ParameterVector searchVector() { public void assign(ParameterVector searchParameters) { try { current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); } catch (SolverException e) { var status = FAILED; status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); setStatus(status); e.printStackTrace(); } - curve.getRange().assign(searchParameters); } /** diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index db77e3ab..00232b70 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -12,6 +12,9 @@ import static javax.swing.JOptionPane.showConfirmDialog; import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; +import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; @@ -234,18 +237,24 @@ private void validateRange(double a, double b) { sb.append(getString("RangeSelectionFrame.ConfirmationMessage1")); sb.append("


"); sb.append(getString("RangeSelectionFrame.ConfirmationMessage2")); - sb.append(expCurve.getEffectiveStartTime()); + sb.append(format("%3.6f", expCurve.getEffectiveStartTime())); sb.append(" to "); - sb.append(expCurve.getEffectiveEndTime()); + sb.append(format("%3.6f", expCurve.getEffectiveEndTime())); sb.append("

"); sb.append(getString("RangeSelectionFrame.ConfirmationMessage3")); - sb.append(format("%3.4f", a) + " to " + format("%3.4f", b)); + sb.append(format("%3.6f", a) + " to " + format("%3.6f", b)); sb.append(""); var dialogResult = showConfirmDialog(getWindowAncestor(this), sb.toString(), "Confirm chocie", YES_NO_OPTION); - if (dialogResult == YES_OPTION) - expCurve.setRange(new Range(a, b)); + if (dialogResult == YES_OPTION) { + if(expCurve.getRange() == null) + expCurve.setRange(new Range(a, b)); + else { + expCurve.getRange().setLowerBound(derive(LOWER_BOUND, a)); + expCurve.getRange().setUpperBound(derive(UPPER_BOUND, b)); + } + } } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index a4158e4f..a049c5eb 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -257,7 +257,7 @@ + minimum="1" primitive-type="int" value="256" discreet="false"> + default-search-variable="false" /> + default-search-variable="true" > @@ -504,7 +504,7 @@ abbreviation="<i>t</i><sub>max</sub>" auto-adjustable="false" descriptor="Upper bound, <i>t</i><sub>max</sub>" - dimensionfactor="1" discreet="false" keyword="UPPER_BOUND" + dimensionfactor="1" discreet="true" keyword="UPPER_BOUND" maximum="100" minimum="-100" primitive-type="double" value="1.0" default-search-variable="false"> From b39c01305b58bc881d39c8715d79f53fff67d9c1 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Wed, 16 Jun 2021 20:21:33 +0200 Subject: [PATCH 046/116] Meta version update --- pom.xml | 2 +- src/main/java/pulse/search/statistics/ResidualStatistic.java | 3 +-- src/main/resources/Version.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 53dd7b1b..71d2b643 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.91FM_01 + 1.91FM_02 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 22ae8660..7129ff81 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -41,8 +41,7 @@ public ResidualStatistic(ResidualStatistic another) { } public double[] transformResiduals() { - return getResiduals().stream().map(doubleArray -> doubleArray[1]) - .mapToDouble(Double::doubleValue).toArray(); + return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); } /** diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index b7c44c94..e0b8d5a9 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.91FM_01 \ No newline at end of file +1.91FM_02 \ No newline at end of file From 4b6f2caa52ade5353c0eb396a4b00cdc9bf69c72 Mon Sep 17 00:00:00 2001 From: Artem Lunev Date: Fri, 26 Nov 2021 20:03:30 +0100 Subject: [PATCH 047/116] Changed Biot number maximal values --- src/main/resources/NumericProperty.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index a049c5eb..a32fd5cd 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -437,12 +437,12 @@ Date: Fri, 26 Nov 2021 21:01:32 +0100 Subject: [PATCH 048/116] Increased max biot to 100 --- src/main/resources/NumericProperty.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index a32fd5cd..860690d7 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -437,12 +437,12 @@ Date: Mon, 29 Nov 2021 12:26:02 +0100 Subject: [PATCH 049/116] Changes to XML format: - Added HEAT_LOSS_COMBINED and ABSORPTIVITY_COMBINED - Added tag for excluding superfluous optimisation flags - Increased Biot maximal value to 100.0 - Changed autoAdjustable to visible for easier interpretation --- src/main/resources/NumericProperty.xml | 208 +++++++++++++++---------- 1 file changed, 122 insertions(+), 86 deletions(-) diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 860690d7..6daaac70 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,56 +1,56 @@ - - - - - - - - - @@ -206,62 +206,79 @@ - - + + + THERMAL_ABSORPTIVITY + LASER_ABSORPTIVITY + + + default-search-variable="false"> + + COMBINED_ABSORPTIVITY + + discreet="false" default-search-variable="false"> + + COMBINED_ABSORPTIVITY + - + + + HEAT_LOSS + HEAT_LOSS_SIDE + + - + + HEAT_LOSS_COMBINED + + + + + HEAT_LOSS_COMBINED + - - - - From efdef6aee7b59eec1f56536e89d35b8afeb8b377 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 12:49:55 +0100 Subject: [PATCH 050/116] Changes to 2D Problem Statement: - Added support to HEAT_LOSS_COMBINED - Biot number transform changed from ATANH to ABS (atanh caused problems) - Problem now had hideDetails set to true --- .../transforms/StandardTransformations.java | 14 + .../statements/ClassicalProblem2D.java | 15 +- .../pulse/problem/statements/Problem.java | 8 +- .../model/ExtendedThermalProperties.java | 270 ++++++++++-------- .../statements/model/ThermalProperties.java | 20 +- 5 files changed, 182 insertions(+), 145 deletions(-) diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index b7b96721..08604df5 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -42,6 +42,20 @@ public double inverse(double t) { } }; + + public final static Transformable ABS = new Transformable() { + + @Override + public double transform(double a) { + return Math.abs(a); + } + + @Override + public double inverse(double t) { + return transform(t); + } + + }; private StandardTransformations() { //empty diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 73a7c107..a80101aa 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -18,6 +18,8 @@ import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; /** @@ -90,6 +92,10 @@ public void optimisationVector(ParameterVector output, List flags) { final double Bi = (double) properties.getSideLosses().getValue(); setHeatLossParameter(output, i, Bi); continue; + case HEAT_LOSS_COMBINED: + final double combined = (double) properties.getHeatLoss().getValue(); + setHeatLossParameter(output, i, combined); + continue; default: continue; } @@ -113,15 +119,10 @@ public void assign(ParameterVector params) throws SolverException { switch (type) { case FOV_OUTER: case FOV_INNER: - case HEAT_LOSS_SIDE: //comment this when locking side + (rear-front) + case HEAT_LOSS_SIDE: + case HEAT_LOSS_COMBINED: properties.set(type, derive(type, params.inverseTransform(i) )); break; - //UNCOMMENT TO MAKE HEAT LOSS LOCKED - /* - case HEAT_LOSS: - properties.set(HEAT_LOSS_SIDE, derive(HEAT_LOSS_SIDE, params.inverseTransform(i) )); - break; - */ case SPOT_DIAMETER: ((Pulse2D) getPulse()).setSpotDiameter( derive(SPOT_DIAMETER, params.inverseTransform(i) )); break; diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index c2b9024c..0f6f63ed 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -18,8 +18,8 @@ import pulse.input.ExperimentalData; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.AtanhTransform; import pulse.math.transforms.InvLenSqTransform; +import pulse.math.transforms.StandardTransformations; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; @@ -260,13 +260,13 @@ public void optimisationVector(ParameterVector output, List flags) { } + //TODO remove atanh transform and replace with abs protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { if(output.getTransform(i) == null) { final double min = (double) def(HEAT_LOSS).getMinimum(); final double max = (double) def(HEAT_LOSS).getMaximum(); - var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); - - output.setTransform(i, new AtanhTransform(bounds) ); + var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); + output.setTransform(i, StandardTransformations.ABS ); output.setParameterBounds(i, bounds); } output.set(i, Bi); diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index 840cb632..40c38962 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -1,6 +1,5 @@ package pulse.problem.statements.model; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.DIAMETER; @@ -12,133 +11,154 @@ import java.util.List; import pulse.input.ExperimentalData; +import pulse.properties.NumericProperties; +import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_COMBINED; import pulse.properties.Property; public class ExtendedThermalProperties extends ThermalProperties { - private double d; - private double Bi3; - private double fovOuter; - private double fovInner; - - public ExtendedThermalProperties() { - super(); - Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); - d = (double) def(DIAMETER).getValue(); - fovOuter = (double) def(FOV_OUTER).getValue(); - fovInner = (double) def(FOV_INNER).getValue(); - defaultValues(); - } - - public ExtendedThermalProperties(ThermalProperties sdd) { - super(sdd); - defaultValues(); - } - - private void defaultValues() { - Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); - d = (double) def(DIAMETER).getValue(); - fovOuter = (double) def(FOV_OUTER).getValue(); - fovInner = (double) def(FOV_INNER).getValue(); - } - - public ExtendedThermalProperties(ExtendedThermalProperties sdd) { - super(sdd); - this.d = sdd.d; - this.Bi3 = sdd.Bi3; - this.fovOuter = sdd.fovOuter; - this.fovInner = sdd.fovInner; - } - - @Override - public ThermalProperties copy() { - return new ExtendedThermalProperties(this); - } - - @Override - public void useTheoreticalEstimates(ExperimentalData c) { - super.useTheoreticalEstimates(c); - if (areThermalPropertiesLoaded()) - Bi3 = biot(); - } - - public NumericProperty getSampleDiameter() { - return derive(DIAMETER, d); - } - - public void setSampleDiameter(NumericProperty d) { - requireType(d, DIAMETER); - this.d = (double) d.getValue(); - firePropertyChanged(this, d); - } - - public NumericProperty getSideLosses() { - return derive(HEAT_LOSS_SIDE, Bi3); - } - - public void setSideLosses(NumericProperty bi3) { - requireType(bi3, HEAT_LOSS_SIDE); - this.Bi3 = (double) bi3.getValue(); - firePropertyChanged(this, bi3); - } - - public NumericProperty getFOVOuter() { - return derive(FOV_OUTER, fovOuter); - } - - public void setFOVOuter(NumericProperty fovOuter) { - requireType(fovOuter, FOV_OUTER); - this.fovOuter = (double) fovOuter.getValue(); - firePropertyChanged(this, fovOuter); - } - - public NumericProperty getFOVInner() { - return derive(FOV_INNER, fovInner); - } - - public void setFOVInner(NumericProperty fovInner) { - requireType(fovInner, FOV_INNER); - this.fovInner = (double) fovInner.getValue(); - firePropertyChanged(this, fovInner); - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.addAll(super.listedTypes()); - list.add(def(HEAT_LOSS_SIDE)); - list.add(def(DIAMETER)); - list.add(def(FOV_OUTER)); - list.add(def(FOV_INNER)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - switch (type) { - case FOV_OUTER: - setFOVOuter(property); - break; - case FOV_INNER: - setFOVInner(property); - break; - case DIAMETER: - setSampleDiameter(property); - break; - case HEAT_LOSS_SIDE: - setSideLosses(property); - break; - default: - break; - } - } - - @Override - public String getDescriptor() { - return "Sample Thermo-Physical Properties (2D)"; - } - -} \ No newline at end of file + private double d; + private double Bi3; + private double fovOuter; + private double fovInner; + + public ExtendedThermalProperties() { + super(); + Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); + d = (double) def(DIAMETER).getValue(); + fovOuter = (double) def(FOV_OUTER).getValue(); + fovInner = (double) def(FOV_INNER).getValue(); + defaultValues(); + } + + public ExtendedThermalProperties(ThermalProperties sdd) { + super(sdd); + defaultValues(); + } + + private void defaultValues() { + Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); + d = (double) def(DIAMETER).getValue(); + fovOuter = (double) def(FOV_OUTER).getValue(); + fovInner = (double) def(FOV_INNER).getValue(); + } + + public ExtendedThermalProperties(ExtendedThermalProperties sdd) { + super(sdd); + this.d = sdd.d; + this.Bi3 = sdd.Bi3; + this.fovOuter = sdd.fovOuter; + this.fovInner = sdd.fovInner; + } + + @Override + public ThermalProperties copy() { + return new ExtendedThermalProperties(this); + } + + @Override + public void useTheoreticalEstimates(ExperimentalData c) { + super.useTheoreticalEstimates(c); + if (areThermalPropertiesLoaded()) { + Bi3 = biot(); + } + } + + public NumericProperty getSampleDiameter() { + return derive(DIAMETER, d); + } + + public void setSampleDiameter(NumericProperty d) { + requireType(d, DIAMETER); + this.d = (double) d.getValue(); + firePropertyChanged(this, d); + } + + public NumericProperty getSideLosses() { + return derive(HEAT_LOSS_SIDE, Bi3); + } + + public void setSideLosses(NumericProperty bi3) { + requireType(bi3, HEAT_LOSS_SIDE); + this.Bi3 = (double) bi3.getValue(); + firePropertyChanged(this, bi3); + } + + public NumericProperty getCombinedLosses() { + return derive(HEAT_LOSS_COMBINED, (double) this.getHeatLoss().getValue()); + } + + public void setCombinedLosses(NumericProperty bic) { + requireType(bic, HEAT_LOSS_COMBINED); + double value = (double) bic.getValue(); + setSideLosses(NumericProperties.derive(HEAT_LOSS_SIDE, value)); + setHeatLoss(NumericProperties.derive(HEAT_LOSS, value)); + } + + public NumericProperty getFOVOuter() { + return derive(FOV_OUTER, fovOuter); + } + + public void setFOVOuter(NumericProperty fovOuter) { + requireType(fovOuter, FOV_OUTER); + this.fovOuter = (double) fovOuter.getValue(); + firePropertyChanged(this, fovOuter); + } + + public NumericProperty getFOVInner() { + return derive(FOV_INNER, fovInner); + } + + public void setFOVInner(NumericProperty fovInner) { + requireType(fovInner, FOV_INNER); + this.fovInner = (double) fovInner.getValue(); + firePropertyChanged(this, fovInner); + } + + @Override + public List listedTypes() { + List list = new ArrayList<>(); + list.addAll(super.listedTypes()); + list.add(def(HEAT_LOSS_SIDE)); + list.add(def(HEAT_LOSS_COMBINED)); + list.add(def(DIAMETER)); + list.add(def(FOV_OUTER)); + list.add(def(FOV_INNER)); + return list; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + switch (type) { + case FOV_OUTER: + setFOVOuter(property); + break; + case FOV_INNER: + setFOVInner(property); + break; + case DIAMETER: + setSampleDiameter(property); + break; + case HEAT_LOSS_SIDE: + setSideLosses(property); + break; + //extracts the value from the combined heat loss and sets the facial and side heat losses + case HEAT_LOSS_COMBINED: + setCombinedLosses(property); + break; + default: + break; + } + } + + @Override + public String getDescriptor() { + return "Sample Thermo-Physical Properties (2D)"; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 77d27aaa..0e53c5c9 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -6,15 +6,7 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; -import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; +import static pulse.properties.NumericPropertyKeyword.*; import java.util.ArrayList; import java.util.List; @@ -117,6 +109,16 @@ else if (e == StandartType.HEAT_CAPACITY) { public ThermalProperties copy() { return new ThermalProperties(this); } + + /** + * Hides optimiser directives + * @return true + */ + + @Override + public boolean areDetailsHidden() { + return true; + } /** * Used to change the parameter values of this {@code Problem}. It is only From 47a232cbf8fde1285299a6bf01c474c4d0a08167 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:10:35 +0100 Subject: [PATCH 051/116] Changes to property accessors and problem statements: - Added support for COMBINED_ABSORPTIVITY in PenetrationProblem - Replaced ATANH transform for absorptivity to ABS - Added firePropertyChanged after each change to the InstanceDescriptor or DiscreteSelector - Added missing call to super-constructor from Insulator --- .../rte/dom/DiscreteOrdinatesMethod.java | 287 +++++----- .../schemes/rte/dom/Discretisation.java | 509 +++++++++--------- .../schemes/rte/dom/ExplicitRungeKutta.java | 279 +++++----- .../exact/NonscatteringRadiativeTransfer.java | 1 + .../statements/PenetrationProblem.java | 235 ++++---- .../statements/model/AbsorptionModel.java | 14 + .../problem/statements/model/Insulator.java | 74 +-- 7 files changed, 706 insertions(+), 693 deletions(-) diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index 98eb4f38..92031b5e 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -20,147 +20,150 @@ * solve to RTE. * */ - public class DiscreteOrdinatesMethod extends RadiativeTransferSolver { - private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( - "Integrator selector", AdaptiveIntegrator.class); - private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( - "Iterative solver selector", IterativeSolver.class); - private InstanceDescriptor phaseFunctionSelector = new InstanceDescriptor( - "Phase function selector", PhaseFunction.class); - - private AdaptiveIntegrator integrator; - private IterativeSolver iterativeSolver; - - /** - * Constructs a discrete ordinates solver using the parameters (emissivity, - * scattering albedo and optical thickness) declared by the {@code problem} - * object. - * - * @param problem the coupled problem statement - * @param grid the heat problem grid - */ - - public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { - super(); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - - var discrete = new Discretisation(problem); - - integratorDescriptor.setSelectedDescriptor(TRBDF2.class.getSimpleName()); - setIntegrator(integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)); - - iterativeSolverSelector.setSelectedDescriptor(FixedIterations.class.getSimpleName()); - setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); - - phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); - phaseFunctionSelector.addListener(() -> initPhaseFunction(problem, discrete)); - initPhaseFunction(problem, discrete); - - init(problem, grid); - - integratorDescriptor.addListener(() -> setIntegrator( - integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete))); - - iterativeSolverSelector - .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); - - } - - @Override - public RTECalculationStatus compute(double[] tempArray) { - integrator.getEmissionFunction().setInterpolation(interpolateTemperatureProfile(tempArray)); - - var status = iterativeSolver.doIterations(integrator); - - if (status == RTECalculationStatus.NORMAL) - fluxesAndDerivatives(tempArray.length); - - fireStatusUpdate(status); - return status; - } - - private void fluxesAndDerivatives(final int nExclusive) { - final var interpolation = integrator.getHermiteInterpolator().interpolateOnExternalGrid(nExclusive, integrator); - - final double DOUBLE_PI = 2.0 * Math.PI; - final var discrete = integrator.getDiscretisation(); - var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); - - for (int i = 0; i < nExclusive; i++) { - fluxes.setFlux(i, DOUBLE_PI * discrete.firstMoment(interpolation[0], i)); - fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); - } - } - - @Override - public String getDescriptor() { - return "Discrete Ordinates Method (DOM)"; - } - - @Override - public void init(ParticipatingMedium problem, Grid grid) { - super.init(problem, grid); - initPhaseFunction(problem, integrator.getDiscretisation()); - integrator.init(problem); - integrator.getPhaseFunction().init(problem); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(integratorDescriptor); - list.add(iterativeSolverSelector); - list.add(phaseFunctionSelector); - return list; - } - - public AdaptiveIntegrator getIntegrator() { - return integrator; - } - - public InstanceDescriptor getIntegratorDescriptor() { - return integratorDescriptor; - } - - public void setIntegrator(AdaptiveIntegrator integrator) { - this.integrator = integrator; - integrator.setParent(this); - } - - public IterativeSolver getIterativeSolver() { - return iterativeSolver; - } - - public InstanceDescriptor getIterativeSolverSelector() { - return iterativeSolverSelector; - } - - public void setIterativeSolver(IterativeSolver solver) { - this.iterativeSolver = solver; - solver.setParent(this); - } - - public InstanceDescriptor getPhaseFunctionSelector() { - return phaseFunctionSelector; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + integrator.toString() + " ; " + iterativeSolver.toString(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - private void initPhaseFunction(ParticipatingMedium problem, Discretisation discrete) { - var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, problem, discrete); - integrator.setPhaseFunction(pf); - pf.init(problem); - } - -} \ No newline at end of file + private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( + "Integrator selector", AdaptiveIntegrator.class); + private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( + "Iterative solver selector", IterativeSolver.class); + private InstanceDescriptor phaseFunctionSelector = new InstanceDescriptor( + "Phase function selector", PhaseFunction.class); + + private AdaptiveIntegrator integrator; + private IterativeSolver iterativeSolver; + + /** + * Constructs a discrete ordinates solver using the parameters (emissivity, + * scattering albedo and optical thickness) declared by the {@code problem} + * object. + * + * @param problem the coupled problem statement + * @param grid the heat problem grid + */ + public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { + super(); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + + var discrete = new Discretisation(problem); + + integratorDescriptor.setSelectedDescriptor(TRBDF2.class.getSimpleName()); + setIntegrator(integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)); + + iterativeSolverSelector.setSelectedDescriptor(FixedIterations.class.getSimpleName()); + setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); + + phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); + phaseFunctionSelector.addListener(() -> initPhaseFunction(problem, discrete)); + initPhaseFunction(problem, discrete); + + init(problem, grid); + + integratorDescriptor.addListener(() -> setIntegrator( + integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)) + ); + + iterativeSolverSelector + .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); + + } + + @Override + public RTECalculationStatus compute(double[] tempArray) { + integrator.getEmissionFunction().setInterpolation(interpolateTemperatureProfile(tempArray)); + + var status = iterativeSolver.doIterations(integrator); + + if (status == RTECalculationStatus.NORMAL) { + fluxesAndDerivatives(tempArray.length); + } + + fireStatusUpdate(status); + return status; + } + + private void fluxesAndDerivatives(final int nExclusive) { + final var interpolation = integrator.getHermiteInterpolator().interpolateOnExternalGrid(nExclusive, integrator); + + final double DOUBLE_PI = 2.0 * Math.PI; + final var discrete = integrator.getDiscretisation(); + var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); + + for (int i = 0; i < nExclusive; i++) { + fluxes.setFlux(i, DOUBLE_PI * discrete.firstMoment(interpolation[0], i)); + fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); + } + } + + @Override + public String getDescriptor() { + return "Discrete Ordinates Method (DOM)"; + } + + @Override + public void init(ParticipatingMedium problem, Grid grid) { + super.init(problem, grid); + initPhaseFunction(problem, integrator.getDiscretisation()); + integrator.init(problem); + integrator.getPhaseFunction().init(problem); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(integratorDescriptor); + list.add(iterativeSolverSelector); + list.add(phaseFunctionSelector); + return list; + } + + public AdaptiveIntegrator getIntegrator() { + return integrator; + } + + public InstanceDescriptor getIntegratorDescriptor() { + return integratorDescriptor; + } + + public void setIntegrator(AdaptiveIntegrator integrator) { + this.integrator = integrator; + integrator.setParent(this); + firePropertyChanged(this, integratorDescriptor); + } + + public IterativeSolver getIterativeSolver() { + return iterativeSolver; + } + + public InstanceDescriptor getIterativeSolverSelector() { + return iterativeSolverSelector; + } + + public void setIterativeSolver(IterativeSolver solver) { + this.iterativeSolver = solver; + solver.setParent(this); + firePropertyChanged(this, iterativeSolverSelector); + } + + public InstanceDescriptor getPhaseFunctionSelector() { + return phaseFunctionSelector; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + integrator.toString() + " ; " + iterativeSolver.toString(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + private void initPhaseFunction(ParticipatingMedium problem, Discretisation discrete) { + var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, problem, discrete); + integrator.setPhaseFunction(pf); + pf.init(problem); + firePropertyChanged(this, phaseFunctionSelector); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index d718bf2c..a754a3eb 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -24,265 +24,258 @@ * and intensities based on the discrete ordinates method. * */ - public class Discretisation extends PropertyHolder { - private DiscreteQuantities quantities; - private StretchedGrid grid; - private OrdinateSet ordinates; - private DiscreteSelector quadSelector; - - private double emissivity; - private double boundaryFluxFactor; - - /** - * Constructs a {@code DiscreteIntensities} with the default {@code OrdinateSet} - * and a new uniform grid. - * - * @param problem the problem statement - */ - - public Discretisation(ParticipatingMedium problem) { - - quadSelector = new DiscreteSelector<>(QuadratureReader.getInstance(), "/quadratures/", - "Quadratures.list"); - quadSelector.setDefaultSelection(OrdinateSet.DEFAULT_SET); - ordinates = quadSelector.getDefaultSelection(); - quadSelector.addListener(() -> { - ordinates = (OrdinateSet)quadSelector.getValue(); - quantities.init(grid.getDensity(), ordinates.getTotalNodes()); - }); - - var properties = (ThermoOpticalProperties) problem.getProperties(); - setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); - quantities = new DiscreteQuantities(grid.getDensity(), ordinates.getTotalNodes()); - setEmissivity((double) problem.getProperties().getEmissivity().getValue()); - } - - /** - * Calculates the incident radiation iwi - * Iij, which is the zeroth moment of the intensities. The - * calculation uses the symmetry of the quadrature weights for positive and - * negativ nodes. - * - * @param j spatial index - * @return the incident radiation at {@code j} - */ - - public double incidentRadation(final int j) { - double integral = 0; - - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - // uses symmetry - for (int i = nStart; i < nHalf; i++) { - integral += ordinates.getWeight(i) - * (quantities.getIntensity(j, i) + quantities.getIntensity(j, i + nHalf)); - } - - if (ordinates.hasZeroNode()) - integral += ordinates.getWeight(0) * quantities.getIntensity(j, 0); - - return integral; - } - - /** - * Calculates the incident radiation iwi - * Iij, by performing simple summation for node points - * between {@code startInclusive} and {@code endExclusive}. - * - * @param j spatial index - * @param startInclusive lower bound for summation - * @param endExclusive upper bound (exclusive) for summation - * @return the partial incident radiation at {@code j} - * @see pulse.problem.schemes.rte.dom.LinearAnisotropicPF - */ - - public double incidentRadiation(final int j, final int startInclusive, final int endExclusive) { - double integral = 0; - - for (int i = startInclusive; i < endExclusive; i++) { - integral += ordinates.getWeight(i) * quantities.getIntensity(j, i); - } - - return integral; - - } - - /** - * Calculates the first moment - * iwiμiextij, - * which can be applied e.g. for flux or flux derivative calculation. The - * calculation uses the symmetry of the quadrature weights for positive and - * negativ nodes. - * - * @param j spatial index - * @return the first moment at {@code j} - */ - - public double firstMoment(final double[][] ext, final int j) { - double integral = 0; - - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - // uses symmetry - for (int i = nStart; i < nHalf; i++) { - integral += ordinates.getWeight(i) * (ext[j][i] - ext[j][i + nHalf]) * ordinates.getNode(i); - } - - return integral; - } - - /** - * Calculates the net flux at {@code j}. - * - * @param j the spatial coordinate - * @return the flux - * @see firstMoment(double[][],int) - */ - - public double flux(final int j) { - return firstMoment(quantities.getIntensities(), j); - } - - /** - * Calculates the partial flux by performing a simple summation bounded by the - * arguments. - * - * @param j the spatial index - * @param startInclusive node index lower bound - * @param endExclusive node index upper bound (exclusive) - * @return the partial flux - */ - - public double flux(final int j, final int startInclusive, final int endExclusive) { - double integral = 0; - - for (int i = startInclusive; i < endExclusive; i++) { - integral += ordinates.getWeight(i) * quantities.getIntensity(j, i) * ordinates.getNode(i); - } - - return integral; - } - - /** - * Calculates the flux at the left boundary using an alternative formula. - * - * @param emissionFunction the emission function - * @return the net flux at the left boundary - */ - - public double fluxLeft(final BlackbodySpectrum emissionFunction) { - final int nHalf = ordinates.getFirstNegativeNode(); - return emissivity * PI * (emissionFunction.radianceAt(0.0) + 2.0 * flux(0, nHalf, ordinates.getTotalNodes())); - } - - /** - * Calculates the flux at the right boundary using an alternative formula. - * - * @param emissionFunction the emission function - * @return the net flux at the right boundary - */ - - public double fluxRight(final BlackbodySpectrum emissionFunction) { - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - return -emissivity * PI - * (emissionFunction.radianceAt(grid.getDimension()) - 2.0 * flux(grid.getDensity(), nStart, nHalf)); - } - - /** - * Calculates the reflected intensity (positive angles, first half of indices) - * at the left boundary (τ = 0). - * - * @param ef the emission function - */ - - public void intensitiesLeftBoundary(final BlackbodySpectrum ef) { - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - for (int i = nStart; i < nHalf; i++) { - // for positive streams - quantities.setIntensity(0, i, ef.radianceAt(0.0) - boundaryFluxFactor * fluxLeft(ef)); - } - - } - - /** - * Calculates the reflected intensity (negative angles, second half of indices) - * at the right boundary (τ = τ0). - * - * @param ef the emission function - */ - - public void intensitiesRightBoundary(final BlackbodySpectrum ef) { - - final int N = grid.getDensity(); - final int nHalf = ordinates.getFirstNegativeNode(); - final double tau0 = grid.getDimension(); - - for (int i = nHalf; i < ordinates.getTotalNodes(); i++) { - // for negative streams - quantities.setIntensity(N, i, ef.radianceAt(tau0) + boundaryFluxFactor * fluxRight(ef)); - } - - } - - protected void setEmissivity(double emissivity) { - this.emissivity = emissivity; - boundaryFluxFactor = (1.0 - emissivity) / (emissivity * PI); - } - - public OrdinateSet getOrdinates() { - return ordinates; - } - - public void setOrdinateSet(OrdinateSet set) { - this.ordinates = set; - quantities.init(grid.getDensity(), ordinates.getTotalNodes()); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(quadSelector)); - } - - @Override - public String getDescriptor() { - return "Discretisation"; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("( "); - sb.append("Quadrature: " + ordinates.getName() + " ; "); - sb.append("Grid: " + grid.toString()); - return sb.toString(); - } - - public StretchedGrid getGrid() { - return grid; - } - - public void setGrid(StretchedGrid grid) { - this.grid = grid; - this.grid.setParent(this); - } - - public DiscreteQuantities getQuantities() { - return quantities; - } - - public DiscreteSelector getQuadratureSelector() { - return quadSelector; - } + private DiscreteQuantities quantities; + private StretchedGrid grid; + private OrdinateSet ordinates; + private DiscreteSelector quadSelector; + + private double emissivity; + private double boundaryFluxFactor; + + /** + * Constructs a {@code DiscreteIntensities} with the default + * {@code OrdinateSet} and a new uniform grid. + * + * @param problem the problem statement + */ + public Discretisation(ParticipatingMedium problem) { + + quadSelector = new DiscreteSelector<>(QuadratureReader.getInstance(), "/quadratures/", + "Quadratures.list"); + quadSelector.setDefaultSelection(OrdinateSet.DEFAULT_SET); + ordinates = quadSelector.getDefaultSelection(); + quadSelector.addListener(() -> { + ordinates = (OrdinateSet) quadSelector.getValue(); + quantities.init(grid.getDensity(), ordinates.getTotalNodes()); + this.firePropertyChanged(this, quadSelector); + }); + + var properties = (ThermoOpticalProperties) problem.getProperties(); + setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); + quantities = new DiscreteQuantities(grid.getDensity(), ordinates.getTotalNodes()); + setEmissivity((double) problem.getProperties().getEmissivity().getValue()); + } + + /** + * Calculates the incident radiation + * iwi + * Iij, which is the zeroth moment of the intensities. + * The calculation uses the symmetry of the quadrature weights for positive + * and negativ nodes. + * + * @param j spatial index + * @return the incident radiation at {@code j} + */ + public double incidentRadation(final int j) { + double integral = 0; + + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + // uses symmetry + for (int i = nStart; i < nHalf; i++) { + integral += ordinates.getWeight(i) + * (quantities.getIntensity(j, i) + quantities.getIntensity(j, i + nHalf)); + } + + if (ordinates.hasZeroNode()) { + integral += ordinates.getWeight(0) * quantities.getIntensity(j, 0); + } + + return integral; + } + + /** + * Calculates the incident radiation + * iwi + * Iij, by performing simple summation for node points + * between {@code startInclusive} and {@code endExclusive}. + * + * @param j spatial index + * @param startInclusive lower bound for summation + * @param endExclusive upper bound (exclusive) for summation + * @return the partial incident radiation at {@code j} + * @see pulse.problem.schemes.rte.dom.LinearAnisotropicPF + */ + public double incidentRadiation(final int j, final int startInclusive, final int endExclusive) { + double integral = 0; + + for (int i = startInclusive; i < endExclusive; i++) { + integral += ordinates.getWeight(i) * quantities.getIntensity(j, i); + } + + return integral; + + } + + /** + * Calculates the first moment + * iwiμiextij, + * which can be applied e.g. for flux or flux derivative calculation. The + * calculation uses the symmetry of the quadrature weights for positive and + * negativ nodes. + * + * @param j spatial index + * @return the first moment at {@code j} + */ + public double firstMoment(final double[][] ext, final int j) { + double integral = 0; + + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + // uses symmetry + for (int i = nStart; i < nHalf; i++) { + integral += ordinates.getWeight(i) * (ext[j][i] - ext[j][i + nHalf]) * ordinates.getNode(i); + } + + return integral; + } + + /** + * Calculates the net flux at {@code j}. + * + * @param j the spatial coordinate + * @return the flux + * @see firstMoment(double[][],int) + */ + public double flux(final int j) { + return firstMoment(quantities.getIntensities(), j); + } + + /** + * Calculates the partial flux by performing a simple summation bounded by + * the arguments. + * + * @param j the spatial index + * @param startInclusive node index lower bound + * @param endExclusive node index upper bound (exclusive) + * @return the partial flux + */ + public double flux(final int j, final int startInclusive, final int endExclusive) { + double integral = 0; + + for (int i = startInclusive; i < endExclusive; i++) { + integral += ordinates.getWeight(i) * quantities.getIntensity(j, i) * ordinates.getNode(i); + } + + return integral; + } + + /** + * Calculates the flux at the left boundary using an alternative formula. + * + * @param emissionFunction the emission function + * @return the net flux at the left boundary + */ + public double fluxLeft(final BlackbodySpectrum emissionFunction) { + final int nHalf = ordinates.getFirstNegativeNode(); + return emissivity * PI * (emissionFunction.radianceAt(0.0) + 2.0 * flux(0, nHalf, ordinates.getTotalNodes())); + } + + /** + * Calculates the flux at the right boundary using an alternative formula. + * + * @param emissionFunction the emission function + * @return the net flux at the right boundary + */ + public double fluxRight(final BlackbodySpectrum emissionFunction) { + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + return -emissivity * PI + * (emissionFunction.radianceAt(grid.getDimension()) - 2.0 * flux(grid.getDensity(), nStart, nHalf)); + } + + /** + * Calculates the reflected intensity (positive angles, first half of + * indices) at the left boundary (τ = 0). + * + * @param ef the emission function + */ + public void intensitiesLeftBoundary(final BlackbodySpectrum ef) { + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + for (int i = nStart; i < nHalf; i++) { + // for positive streams + quantities.setIntensity(0, i, ef.radianceAt(0.0) - boundaryFluxFactor * fluxLeft(ef)); + } + + } + + /** + * Calculates the reflected intensity (negative angles, second half of + * indices) at the right boundary (τ = τ0). + * + * @param ef the emission function + */ + public void intensitiesRightBoundary(final BlackbodySpectrum ef) { + + final int N = grid.getDensity(); + final int nHalf = ordinates.getFirstNegativeNode(); + final double tau0 = grid.getDimension(); + + for (int i = nHalf; i < ordinates.getTotalNodes(); i++) { + // for negative streams + quantities.setIntensity(N, i, ef.radianceAt(tau0) + boundaryFluxFactor * fluxRight(ef)); + } + + } + + protected void setEmissivity(double emissivity) { + this.emissivity = emissivity; + boundaryFluxFactor = (1.0 - emissivity) / (emissivity * PI); + } + + public OrdinateSet getOrdinates() { + return ordinates; + } + + public void setOrdinateSet(OrdinateSet set) { + this.ordinates = set; + quantities.init(grid.getDensity(), ordinates.getTotalNodes()); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public List listedTypes() { + return new ArrayList(Arrays.asList(quadSelector)); + } + + @Override + public String getDescriptor() { + return "Discretisation"; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("( "); + sb.append("Quadrature: " + ordinates.getName() + " ; "); + sb.append("Grid: " + grid.toString()); + return sb.toString(); + } + + public StretchedGrid getGrid() { + return grid; + } + + public void setGrid(StretchedGrid grid) { + this.grid = grid; + this.grid.setParent(this); + } + + public DiscreteQuantities getQuantities() { + return quantities; + } + + public DiscreteSelector getQuadratureSelector() { + return quadSelector; + } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java index f8ad0f10..246338a5 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java @@ -8,193 +8,186 @@ import pulse.util.DiscreteSelector; /** - * Explicit Runge-Kutta integrator with Hermite interpolation for the solution of one-dimensional radiative transfer problems. - * + * Explicit Runge-Kutta integrator with Hermite interpolation for the solution + * of one-dimensional radiative transfer problems. + * * @author Artem Lunev, Vadim Zborovskii * */ - public class ExplicitRungeKutta extends AdaptiveIntegrator { - private ButcherTableau tableau; - private DiscreteSelector tableauSelector; + private ButcherTableau tableau; + private DiscreteSelector tableauSelector; - public ExplicitRungeKutta(Discretisation intensities) { - super(intensities); - - tableauSelector = new DiscreteSelector<>(ButcherTableauReader.getInstance(), "/solvers/", - "Solvers.list"); - tableauSelector.setDefaultSelection(ButcherTableau.DEFAULT_TABLEAU); - tableau = tableauSelector.getDefaultSelection(); - tableauSelector.addListener(() -> tableau = (ButcherTableau)tableauSelector.getValue()); - } + public ExplicitRungeKutta(Discretisation intensities) { + super(intensities); - @Override - public Vector[] step(final int j, final double sign) { + tableauSelector = new DiscreteSelector<>(ButcherTableauReader.getInstance(), "/solvers/", + "Solvers.list"); + tableauSelector.setDefaultSelection(ButcherTableau.DEFAULT_TABLEAU); + tableau = tableauSelector.getDefaultSelection(); + tableauSelector.addListener(() -> setButcherTableau((ButcherTableau) tableauSelector.getValue())); + } - var quantities = getDiscretisation().getQuantities(); - - final var grid = getDiscretisation().getGrid(); - final var ordinates = getDiscretisation().getOrdinates(); + @Override + public Vector[] step(final int j, final double sign) { - final double h = grid.step(j, sign); - final double hSigned = h * sign; - final double t = grid.getNode(j); + var quantities = getDiscretisation().getQuantities(); - final int nPositiveStart = ordinates.getFirstPositiveNode(); - final int nNegativeStart = ordinates.getFirstNegativeNode(); + final var grid = getDiscretisation().getGrid(); + final var ordinates = getDiscretisation().getOrdinates(); - var hermite = getHermiteInterpolator(); - - hermite.a = t; - hermite.bMinusA = hSigned; + final double h = grid.step(j, sign); + final double hSigned = h * sign; + final double t = grid.getNode(j); - /* - * Indices of outward (n1 to n2) and inward (> n3) intensities - */ + final int nPositiveStart = ordinates.getFirstPositiveNode(); + final int nNegativeStart = ordinates.getFirstNegativeNode(); - final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index (e.g. 0) or first - // negative (n/2) - final int n2 = sign > 0 ? nNegativeStart : ordinates.getTotalNodes(); // either first negative index (n/2) or - // n - final int n3 = ordinates.getTotalNodes() - n2; // either nNegativeStart or 0 - final int nH = n2 - n1; + var hermite = getHermiteInterpolator(); - var error = new double[nH]; - var iOutward = new double[nH]; - var iInward = new double[nH]; + hermite.a = t; + hermite.bMinusA = hSigned; - int stages = tableau.numberOfStages(); + /* + * Indices of outward (n1 to n2) and inward (> n3) intensities + */ + final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index (e.g. 0) or first + // negative (n/2) + final int n2 = sign > 0 ? nNegativeStart : ordinates.getTotalNodes(); // either first negative index (n/2) or + // n + final int n3 = ordinates.getTotalNodes() - n2; // either nNegativeStart or 0 + final int nH = n2 - n1; - var q = new double[nH][stages]; // first index - cosine node, second index - stage + var error = new double[nH]; + var iOutward = new double[nH]; + var iInward = new double[nH]; - double bDotQ; - double sum; + int stages = tableau.numberOfStages(); - int increment = (int) (1 * sign); + var q = new double[nH][stages]; // first index - cosine node, second index - stage - /* - * RK Explicit (Embedded) - */ + double bDotQ; + double sum; - /* - * First stage - */ + int increment = (int) (1 * sign); - if (tableau.isFSAL() && ! isFirstRun() ) { // if FSAL + /* + * RK Explicit (Embedded) + */ - for (int l = n1; l < n2; l++) { - q[l - n1][0] = quantities.getQLast(l - n1); // assume first stage is the last stage of last step - } + /* + * First stage + */ + if (tableau.isFSAL() && !isFirstRun()) { // if FSAL - } else { // if not FSAL or on first run + for (int l = n1; l < n2; l++) { + q[l - n1][0] = quantities.getQLast(l - n1); // assume first stage is the last stage of last step + } - for (int l = n1; l < n2; l++) { - q[l - n1][0] = derivative(l, j, t, quantities.getIntensity(j, l)); - } + } else { // if not FSAL or on first run - setFirstRun(false); + for (int l = n1; l < n2; l++) { + q[l - n1][0] = derivative(l, j, t, quantities.getIntensity(j, l)); + } - } + setFirstRun(false); - // in any case + } - for (int l = n1; l < n2; l++) { - quantities.setDerivative(j, l, q[l - n1][0]); // store derivative for inward intensities - error[l - n1] = (tableau.getInterpolator().get(0) - tableau.getEstimator().get(0)) * q[l - n1][0] * hSigned; - } + // in any case + for (int l = n1; l < n2; l++) { + quantities.setDerivative(j, l, q[l - n1][0]); // store derivative for inward intensities + error[l - n1] = (tableau.getInterpolator().get(0) - tableau.getEstimator().get(0)) * q[l - n1][0] * hSigned; + } - /* + /* * Next stages - */ - - for (int m = 1; m < stages; m++) { // <------- STAGES (1...s) + */ + for (int m = 1; m < stages; m++) { // <------- STAGES (1...s) - /* + /* * Calculate interpolated (OUTWARD and INWARD) intensities at each stage from m * = 1 onwards - */ - - double tm = t + hSigned * tableau.getC().get(m); // interpolation point for stage m + */ + double tm = t + hSigned * tableau.getC().get(m); // interpolation point for stage m - for (int l = n1; l < n2; l++) { // find unknown intensities (sum over the outward intensities) + for (int l = n1; l < n2; l++) { // find unknown intensities (sum over the outward intensities) - /* + /* * OUTWARD - */ + */ + sum = tableau.getMatrix().get(m, 0) * q[l - n1][0]; + for (int k = 1; k < m; k++) { + sum += tableau.getMatrix().get(m, k) * q[l - n1][k]; + } - sum = tableau.getMatrix().get(m, 0) * q[l - n1][0]; - for (int k = 1; k < m; k++) - sum += tableau.getMatrix().get(m, k) * q[l - n1][k]; + iOutward[l - n1] = quantities.getIntensity(j, l) + hSigned * sum; // outward intensities are simply + // found from the + // RK explicit expressions - iOutward[l - n1] = quantities.getIntensity(j, l) + hSigned * sum; // outward intensities are simply - // found from the - // RK explicit expressions - - /* + /* * INWARD - */ - - hermite.y0 = quantities.getIntensity(j, l + n3); - hermite.y1 = quantities.getIntensity(j + increment, l + n3); - hermite.d0 = quantities.getDerivative(j, l + n3); - hermite.d1 = quantities.getDerivative(j + increment, l + n3); + */ + hermite.y0 = quantities.getIntensity(j, l + n3); + hermite.y1 = quantities.getIntensity(j + increment, l + n3); + hermite.d0 = quantities.getDerivative(j, l + n3); + hermite.d1 = quantities.getDerivative(j + increment, l + n3); - iInward[l - n1] = hermite.interpolate(tm); // inward intensities are interpolated with - // Hermite polynomials + iInward[l - n1] = hermite.interpolate(tm); // inward intensities are interpolated with + // Hermite polynomials - } + } - /* + /* * Derivatives and associated errors at stage m - */ - - for (int l = n1; l < n2; l++) { - q[l - n1][m] = derivative(l, tm, iOutward, iInward, n1, n2); - quantities.setQLast(l - n1, q[l - n1][m]); - error[l - n1] += (tableau.getInterpolator().get(m) - tableau.getEstimator().get(m)) * q[l - n1][m] - * hSigned; - } + */ + for (int l = n1; l < n2; l++) { + q[l - n1][m] = derivative(l, tm, iOutward, iInward, n1, n2); + quantities.setQLast(l - n1, q[l - n1][m]); + error[l - n1] += (tableau.getInterpolator().get(m) - tableau.getEstimator().get(m)) * q[l - n1][m] + * hSigned; + } - } + } - double[] Is = new double[nH]; + double[] Is = new double[nH]; - /* + /* * Value at next step - */ - - for (int l = 0; l < nH; l++) { - bDotQ = tableau.getInterpolator().dot(new Vector(q[l])); - Is[l] = quantities.getIntensity(j, l + n1) + bDotQ * hSigned; - } - - return new Vector[] { new Vector(Is), new Vector(error) }; - - } - - public ButcherTableau getButcherTableau() { - return tableau; - } - - public void setButcherTableau(ButcherTableau coef) { - this.tableau = coef; - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(tableauSelector); - return list; - } - - @Override - public String toString() { - return super.toString() + " ; " + tableau; - } - - public DiscreteSelector getTableauSelector() { - return tableauSelector; - } - -} \ No newline at end of file + */ + for (int l = 0; l < nH; l++) { + bDotQ = tableau.getInterpolator().dot(new Vector(q[l])); + Is[l] = quantities.getIntensity(j, l + n1) + bDotQ * hSigned; + } + + return new Vector[]{new Vector(Is), new Vector(error)}; + + } + + public ButcherTableau getButcherTableau() { + return tableau; + } + + public void setButcherTableau(ButcherTableau coef) { + this.tableau = coef; + this.firePropertyChanged(this, tableauSelector); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(tableauSelector); + return list; + } + + @Override + public String toString() { + return super.toString() + " ; " + tableau; + } + + public DiscreteSelector getTableauSelector() { + return tableauSelector; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java index b95aa910..b5c7db2e 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -239,6 +239,7 @@ private double integrateSecondOrder(double a, double b) { private void initQuadrature() { setQuadrature(instanceDescriptor.newInstance(CompositionProduct.class)); + firePropertyChanged(this, instanceDescriptor); } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index 20361b6e..ac748524 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -8,6 +8,7 @@ import pulse.math.ParameterVector; import pulse.math.Segment; +import static pulse.math.transforms.StandardTransformations.ABS; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; import pulse.problem.schemes.solvers.SolverException; @@ -19,117 +20,123 @@ import pulse.util.InstanceDescriptor; public class PenetrationProblem extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - - private static InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Absorption model selector", AbsorptionModel.class); - - static { - instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); - } - - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); - - public PenetrationProblem() { - super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - instanceDescriptor.addListener(() -> initAbsorption()); - absorption.setParent(this); - } - - public PenetrationProblem(PenetrationProblem p) { - super(p); - initAbsorption(); - } - - private void initAbsorption() { - setAbsorptionModel(instanceDescriptor.newInstance(AbsorptionModel.class)); - } - - public AbsorptionModel getAbsorptionModel() { - return absorption; - } - - public void setAbsorptionModel(AbsorptionModel model) { - this.absorption = model; - this.absorption.setParent(this); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(instanceDescriptor); - return list; - } - - public static InstanceDescriptor getAbsorptionSelector() { - return instanceDescriptor; - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); - double value = 0; - - switch(key) { - case LASER_ABSORPTIVITY: - value = (double) (absorption.getLaserAbsorptivity()).getValue(); - break; - case THERMAL_ABSORPTIVITY : - value = (double) (absorption.getThermalAbsorptivity()).getValue(); - break; - default : - continue; - } - - //do this for the listed key values - output.setTransform(i, LOG); - output.set(i, value); - output.setParameterBounds(i, new Segment(1E-2, 1000.0)); - - } - - } - - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - - double value; - - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); - - switch(key) { - case LASER_ABSORPTIVITY : - case THERMAL_ABSORPTIVITY : - value = params.inverseTransform(i); - break; - default : - continue; - } - - absorption.set(key, derive(key, value) ); - - } - } - - @Override - public Class defaultScheme() { - return ImplicitTranslucentSolver.class; - } - - @Override - public String toString() { - return Messages.getString("DistributedProblem.Descriptor"); - } - - @Override - public Problem copy() { - return new PenetrationProblem(this); - } - -} \ No newline at end of file + + private final static int DEFAULT_CURVE_POINTS = 300; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "Absorption Model Selector", AbsorptionModel.class); + + private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); + + public PenetrationProblem() { + super(); + getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); + instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); + instanceDescriptor.addListener(() -> initAbsorption()); + absorption.setParent(this); + } + + public PenetrationProblem(PenetrationProblem p) { + super(p); + instanceDescriptor.setSelectedDescriptor((String)p.getAbsorptionSelector().getValue()); + instanceDescriptor.addListener(() -> initAbsorption()); + initAbsorption(); + } + + private void initAbsorption() { + setAbsorptionModel(instanceDescriptor.newInstance(AbsorptionModel.class)); + firePropertyChanged(this, instanceDescriptor); + } + + public AbsorptionModel getAbsorptionModel() { + return absorption; + } + + public void setAbsorptionModel(AbsorptionModel model) { + this.absorption = model; + this.absorption.setParent(this); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + public InstanceDescriptor getAbsorptionSelector() { + return instanceDescriptor; + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + double value = 0; + + switch (key) { + case LASER_ABSORPTIVITY: + value = (double) (absorption.getLaserAbsorptivity()).getValue(); + break; + case THERMAL_ABSORPTIVITY: + value = (double) (absorption.getThermalAbsorptivity()).getValue(); + break; + case COMBINED_ABSORPTIVITY: + value = (double) (absorption.getCombinedAbsorptivity()).getValue(); + break; + default: + continue; + } + + //do this for the listed key values + output.setTransform(i, ABS); + output.set(i, value); + output.setParameterBounds(i, new Segment(1E-2, 1000.0)); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + + double value; + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = params.getIndex(i); + + switch (key) { + case LASER_ABSORPTIVITY: + case THERMAL_ABSORPTIVITY: + case COMBINED_ABSORPTIVITY: + value = params.inverseTransform(i); + break; + default: + continue; + } + + absorption.set(key, derive(key, value)); + + } + } + + @Override + public Class defaultScheme() { + return ImplicitTranslucentSolver.class; + } + + @Override + public String toString() { + return Messages.getString("DistributedProblem.Descriptor"); + } + + @Override + public Problem copy() { + return new PenetrationProblem(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index e00f2395..23421c60 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -12,6 +12,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.COMBINED_ABSORPTIVITY; import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -36,6 +37,10 @@ public NumericProperty getLaserAbsorptivity() { public NumericProperty getThermalAbsorptivity() { return absorptionMap.get(THERMAL); } + + public NumericProperty getCombinedAbsorptivity() { + return getThermalAbsorptivity(); + } public NumericProperty getAbsorptivity(SpectralRange spectrum) { return absorptionMap.get(spectrum); @@ -52,6 +57,11 @@ public void setLaserAbsorptivity(NumericProperty a) { public void setThermalAbsorptivity(NumericProperty a) { absorptionMap.put(THERMAL, a); } + + public void setCombinedAbsorptivity(NumericProperty a) { + setThermalAbsorptivity(a); + setLaserAbsorptivity(a); + } @Override public void set(NumericPropertyKeyword type, NumericProperty property) { @@ -63,6 +73,9 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { case THERMAL_ABSORPTIVITY: absorptionMap.put(THERMAL, property); break; + case COMBINED_ABSORPTIVITY: + setCombinedAbsorptivity(property); + break; default: break; } @@ -79,6 +92,7 @@ public List listedTypes() { List list = super.listedTypes(); list.add(def(LASER_ABSORPTIVITY)); list.add(def(THERMAL_ABSORPTIVITY)); + list.add(def(COMBINED_ABSORPTIVITY)); return list; } diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 73062ba6..9c554b29 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -12,39 +12,41 @@ public class Insulator extends AbsorptionModel { - protected double R; - - public Insulator() { - R = (double) def(REFLECTANCE).getValue(); - } - - @Override - public double absorption(SpectralRange spectrum, double x) { - double a = (double) (this.getAbsorptivity(spectrum).getValue()); - return a * (Math.exp(-a * x) - R * Math.exp(-a * (2.0 - x))) / (1.0 - R * R * Math.exp(-2.0 * a)); - } - - public NumericProperty getReflectance() { - return derive(REFLECTANCE, R); - } - - public void setReflectance(NumericProperty a) { - this.R = (double) a.getValue(); - - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == REFLECTANCE) - R = ((Number) property.getValue()).doubleValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(REFLECTANCE)); - return list; - } - -} \ No newline at end of file + private double R; + + public Insulator() { + super(); + R = (double) def(REFLECTANCE).getValue(); + } + + @Override + public double absorption(SpectralRange spectrum, double x) { + double a = (double) (this.getAbsorptivity(spectrum).getValue()); + return a * (Math.exp(-a * x) - R * Math.exp(-a * (2.0 - x))) / (1.0 - R * R * Math.exp(-2.0 * a)); + } + + public NumericProperty getReflectance() { + return derive(REFLECTANCE, R); + } + + public void setReflectance(NumericProperty a) { + NumericProperty.requireType(a, REFLECTANCE); + this.R = (double) a.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == REFLECTANCE) { + setReflectance(property); + } + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(def(REFLECTANCE)); + return list; + } + +} From 5ed35d30c54470d86e80d9e690d7b12a0f16d7aa Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:17:51 +0100 Subject: [PATCH 052/116] Changes to handling Flags Added support to exclude lists for the optimiser Fixed wrong filtering of flags, which should not depend on property visibility Introduced keywords for combined heat losses and absorptivities --- src/main/java/pulse/properties/Flag.java | 327 +++++----- .../pulse/properties/NumericProperty.java | 583 +++++++++--------- .../properties/NumericPropertyKeyword.java | 19 +- .../pulse/search/direction/ActiveFlags.java | 169 ++--- .../pulse/search/direction/PathOptimiser.java | 424 +++++++------ 5 files changed, 770 insertions(+), 752 deletions(-) diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index dcfd6d3f..2433f4eb 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -16,172 +16,167 @@ * with a short abbreviation describing the type of this flag (this is usually * defined by the corresponding {@code NumericProperty}). */ - public class Flag implements Property { - private NumericPropertyKeyword index; - private boolean value; - private String descriptor; - - /** - * Creates a {@code Flag} with the type {@code type}. The default {@code value} - * is set to {@code false}. - * - * @param type the {@code NumericPropertyKeyword} associated with this - * {@code Flag} - */ - - public Flag(NumericPropertyKeyword type) { - this.index = type; - value = false; - } - - /** - * Creates a {@code Flag} with the following pre-specified parameters: type - * {@code type}, short description {@code abbreviations}, and {@code value}. - * - * @param property the {@code NumericProperty} parameter containing the {@code NumericPropertyKeyword} identifier - * @param value the {@code boolean} value of this {@code flag} - */ - - public Flag(NumericProperty property, boolean value) { - this.index = property.getType(); - this.descriptor = property.getDescriptor(true); - this.value = value; - } - - /** - * A static method for converting enabled flags to a {@code List} of - * {@code NumericPropertyKeyword}s. Each keyword in this list corresponds to an - * enabled flag in the {@code flags} {@code List}. - * - * @param flags the list of flags that needs to be analysed - * @return a list of {@code NumericPropertyKeyword}s corresponding to enabled - * {@code flag}s. - */ - - public static List convert(List flags) { - var filtered = flags.stream().filter(flag -> (boolean) flag.getValue()); - return filtered.map(flag -> flag.getType()).collect(Collectors.toList()); - } - - /** - * The default list of {@code Flag}s used in finding the reverse solution of the - * heat conduction problem contains: + private NumericPropertyKeyword index; + private boolean value; + private String descriptor; + + /** + * Creates a {@code Flag} with the type {@code type}. The default + * {@code value} is set to {@code false}. + * + * @param type the {@code NumericPropertyKeyword} associated with this + * {@code Flag} + */ + public Flag(NumericPropertyKeyword type) { + this.index = type; + value = false; + } + + /** + * Creates a {@code Flag} with the following pre-specified parameters: type + * {@code type}, short description {@code abbreviations}, and {@code value}. + * + * @param property the {@code NumericProperty} parameter containing the + * {@code NumericPropertyKeyword} identifier + * @param value the {@code boolean} value of this {@code flag} + */ + public Flag(NumericProperty property, boolean value) { + this.index = property.getType(); + this.descriptor = property.getDescriptor(true); + this.value = value; + } + + /** + * A static method for converting enabled flags to a {@code List} of + * {@code NumericPropertyKeyword}s. Each keyword in this list corresponds to + * an enabled flag in the {@code flags} {@code List}. + * + * @param flags the list of flags that needs to be analysed + * @return a list of {@code NumericPropertyKeyword}s corresponding to + * enabled {@code flag}s. + */ + public static List convert(List flags) { + var filtered = flags.stream().filter(flag -> (boolean) flag.getValue()); + return filtered.map(flag -> flag.getType()).collect(Collectors.toList()); + } + + /** + * The default list of {@code Flag}s used in finding the reverse solution of + * the heat conduction problem contains: * DIFFUSIVITY (true), HEAT_LOSS (true), MAXTEMP (true), BASELINE_INTERCEPT (false), BASELINE_SLOPE (false) . - * - * @return a {@code List} of default {@code Flag}s - */ - - public static List allProblemDependentFlags() { - return defaultList().stream().filter(p -> p.isAutoAdjustable()).map(p -> new Flag(p, p.isDefaultSearchVariable())) - .collect(Collectors.toList()); - } - - public static List allProblemIndependentFlags() { - List flags = new ArrayList<>(); - flags.add(new Flag(def(TIME_SHIFT), false)); - flags.add(new Flag(def(LOWER_BOUND), false)); - flags.add(new Flag(def(UPPER_BOUND), false)); - return flags; - } - - /** - * Returns the type of this {@code Flag}. - * - * @return a {@code NumericPropertyKeyword} representing the type of this - * {@code Flag}. - */ - - public NumericPropertyKeyword getType() { - return index; - } - - /** - * Creates a new {@code Flag} object based on this {@code Flag}, but with a - * different {@code value}. - * - * @param value either {@code true} or {@code false} - * @return a {@code Flag} that replicates the {@code type} and - * {@code abbreviation} of this {@code Flag}, but sets a new - * {@code value} - */ - - public Flag derive(boolean value) { - return new Flag(def(index), value); - } - - /** - * Creates a short description for the GUI. - */ - - @Override - public String getDescriptor(boolean addHtmlTags) { - return addHtmlTags ? "Search for " + descriptor + "" : "Search for " + descriptor; - } - - /** - * The value for this {@code Property} is a {@code boolean}. - */ - - @Override - public Object getValue() { - return value; - } - - public void setValue(boolean value) { - this.value = (boolean) value; - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": " + index.name(); - } - - public String abbreviation(boolean addHtmlTags) { - return addHtmlTags ? "" + descriptor + "" : descriptor; - } - - public void setAbbreviation(String abbreviation) { - this.descriptor = abbreviation; - } - - @Override - public Object identifier() { - return index; - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (o == null) - return false; - - if (!(o instanceof Flag)) - return false; - - Flag f = (Flag) o; - - if (f.getType() == this.getType()) { - if (f.getValue().equals(this.getValue())) { - return true; - } - } - - return false; - - } - - @Override - public boolean attemptUpdate(Object value) { - // TODO Auto-generated method stub - return false; - } - - public static List selectActive(List flags) { - return flags.stream().filter(flag -> (boolean) flag.getValue()).collect(Collectors.toList()); - } - -} \ No newline at end of file + * + * @return a {@code List} of default {@code Flag}s + */ + public static List allProblemDependentFlags() { + return defaultList().stream() + .map(p -> new Flag(p, p.isDefaultSearchVariable())) + .collect(Collectors.toList()); + } + + public static List allProblemIndependentFlags() { + List flags = new ArrayList<>(); + flags.add(new Flag(def(TIME_SHIFT), false)); + flags.add(new Flag(def(LOWER_BOUND), false)); + flags.add(new Flag(def(UPPER_BOUND), false)); + return flags; + } + + /** + * Returns the type of this {@code Flag}. + * + * @return a {@code NumericPropertyKeyword} representing the type of this + * {@code Flag}. + */ + public NumericPropertyKeyword getType() { + return index; + } + + /** + * Creates a new {@code Flag} object based on this {@code Flag}, but with a + * different {@code value}. + * + * @param value either {@code true} or {@code false} + * @return a {@code Flag} that replicates the {@code type} and + * {@code abbreviation} of this {@code Flag}, but sets a new {@code value} + */ + public Flag derive(boolean value) { + return new Flag(def(index), value); + } + + /** + * Creates a short description for the GUI. + */ + @Override + public String getDescriptor(boolean addHtmlTags) { + return addHtmlTags ? "Search for " + descriptor + "" : "Search for " + descriptor; + } + + /** + * The value for this {@code Property} is a {@code boolean}. + */ + @Override + public Object getValue() { + return value; + } + + public void setValue(boolean value) { + this.value = (boolean) value; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ": " + index.name(); + } + + public String abbreviation(boolean addHtmlTags) { + return addHtmlTags ? "" + descriptor + "" : descriptor; + } + + public void setAbbreviation(String abbreviation) { + this.descriptor = abbreviation; + } + + @Override + public Object identifier() { + return index; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o instanceof Flag)) { + return false; + } + + Flag f = (Flag) o; + + if (f.getType() == this.getType()) { + if (f.getValue().equals(this.getValue())) { + return true; + } + } + + return false; + + } + + @Override + public boolean attemptUpdate(Object value) { + // TODO Auto-generated method stub + return false; + } + + public static List selectActive(List flags) { + return flags.stream().filter(flag -> (boolean) flag.getValue()).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index f12c962f..30f4c04a 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -17,293 +17,306 @@ * {@code NuemricPropertyKeyword}. The latter is used to link with a repository * of default {@code NumericPropert}ies loaded from an {@code .xml} file. *

- * + * * @see pulse.properties.NumericPropertyKeyword * @see pulse.io.export.XMLConverter */ - public class NumericProperty implements Property, Comparable { - private Number value; - - private Number minimum; - private Number maximum; - - private String descriptor; - private String abbreviation; - - private Number dimensionFactor; - private Number error; - - private NumericPropertyKeyword type; - - private boolean autoAdjustable; - private boolean discrete; - private boolean defaultSearchVariable; - - /** - * Creates a {@code NumericProperty} based on {@pattern} and assigns - * {@code value} as its value. - * - * @param value the {@code} for the {@NumericProperty} that is otherwise equal - * to {@code pattern} - * @param pattern a valid {@code NumericProperty} - */ - - protected NumericProperty(Number value, NumericProperty pattern) { - this(pattern); - this.value = value; - } - - /** - * Constructor used by {@code XMLConverter} to create a {@code NumericProperty} - * with fully specified set of parameters - * - * @param type the type of this {@code NumericProperty}, set by one - * of the {@code NumericPropertyKeyword} constants - * @param params the numeric parameters in the following order: value, minimum, maximum, dimension factor. - * @see pulse.io.export.XMLConverter - */ - - public NumericProperty(NumericPropertyKeyword type, Number... params) { - if(params.length != 4) - throw new IllegalArgumentException("Input array must be of length 4. Received: " + params.length); - - this.type = type; - this.value = params[0]; - this.dimensionFactor = params[3]; - setDomain(params[1], params[2]); - } - - /** - * A copy constructor for {@code NumericProperty} - * - * @param num another {@code NumericProperty} that is going to be replicated - */ - - public NumericProperty(NumericProperty num) { - this.value = num.value; - this.descriptor = num.descriptor; - this.abbreviation = num.abbreviation; - this.minimum = num.minimum; - this.maximum = num.maximum; - this.type = num.type; - this.discrete = num.discrete; - this.dimensionFactor = num.dimensionFactor; - this.autoAdjustable = num.autoAdjustable; - this.error = num.error; - this.defaultSearchVariable = num.defaultSearchVariable; - } - - public NumericPropertyKeyword getType() { - return type; - } - - @Override - public Object getValue() { - return value; - } - - public boolean validate() { - return isValueSensible(this, value); - } - - /** - * Sets the {@code value} of this {@code NumericProperty} -- if and only if the - * new {@code value} is confined within the definition domain for this - * {@code NumericProperty}. Checks whether - * - * @param value the value to be set to {@code this property} - * @see NumericProperties.isValueSensible(NumericProperty,Number) - */ - - public void setValue(Number value) { - - Number oldValue = this.value; - this.value = value; - - if (!validate()) { - this.value = oldValue; - throw new IllegalArgumentException(printRangeAndNumber(this, value)); - } - - } - - /** - * Sets the definition domain for this {@code NumericProperty}. - * - * @param minimum the minimum value - * @param maximum the maximum value - * @throws IllegalArgumentException if any two of - * {@code minimum, maximum, or this.value} have - * different primitive types (e.g. a - * {@code double} and an {@code int}). - */ - - public void setDomain(Number minimum, Number maximum) throws IllegalArgumentException { - var minClass = minimum.getClass(); - var maxClass = maximum.getClass(); - if (!minClass.equals(maxClass)) - throw new IllegalArgumentException( - "Types of minimum and maximum do not match: " + minClass + " and " + maxClass); - if (!minClass.equals(value.getClass())) - throw new IllegalArgumentException("Interrupted attempt of setting " + minClass.getSimpleName() - + " boundaries to a " + value.getClass().getSimpleName() + " property"); - this.minimum = minimum; - this.maximum = maximum; - } - - public Number getMinimum() { - return minimum; - } - - public Number getMaximum() { - return maximum; - } - - /** - * Prints out the {@code type} and {@code value} of this - * {@code NumericProperty}. - */ - - @Override - public String toString() { - return (type + " = " + formattedValueAndError(this, false)); - } - - /** - * Calls {@code formattedValue(true)}. - * - * @see NumericProperties.formattedValueAndError(boolean) - */ - - @Override - public String formattedOutput() { - return formattedValueAndError(this, true); - } - - public String valueOutput() { - return numberFormat(this, true).format(valueInCurrentUnits()); - } - - public String errorOutput() { - return numberFormat(this, true).format(errorInCurrentUnits()); - } - - public Number valueInCurrentUnits() { - return value instanceof Double ? (double) value * dimensionFactor.doubleValue() : (int) value; - } - - public double errorInCurrentUnits() { - return error == null ? 0.0 : (double) error * dimensionFactor.doubleValue(); - } - - public Number getDimensionFactor() { - return dimensionFactor; - } - - public void setDimensionFactor(Number dimensionFactor) { - this.dimensionFactor = dimensionFactor; - } - - public void setAutoAdjustable(boolean autoAdjustable) { - this.autoAdjustable = autoAdjustable; - } - - public boolean isAutoAdjustable() { - return autoAdjustable; - } - - public Number getError() { - return error; - } - - public void setError(Number error) { - this.error = error; - } - - @Override - public String getDescriptor(boolean addHtmlTag) { - return addHtmlTag ? "" + descriptor + "" : descriptor; - } - - public void setDescriptor(String descriptor) { - this.descriptor = descriptor; - } - - public String getAbbreviation(boolean addHtmlTags) { - return addHtmlTags ? "" + abbreviation + "" : abbreviation; - } - - public void setAbbreviation(String abbreviation) { - this.abbreviation = abbreviation; - } - - /** - * The {@code Object} o is considered to be equal to this - * {@code NumericProperty} if (a) it is of the same class; (b) its value is the - * same as for this {@code NumericProperty}, and (c) if it is specified by the - * same {@code NumericPropertyKeyword}. - */ - - @Override - public boolean equals(Object o) { - if (o == null) - return false; - - if (o == this) - return true; - - if (!(o instanceof NumericProperty)) - return false; - - NumericProperty onp = (NumericProperty) o; - - if (onp.getType() != this.getType()) - return false; - - return compare(this, onp) == 0; - - } - - @Override - public int compareTo(NumericProperty arg0) { - final int result = this.getType().compareTo(arg0.getType()); - return result != 0 ? result : compare(this, arg0); - } - - public boolean isDiscrete() { - return discrete; - } - - public void setDiscrete(boolean discrete) { - this.discrete = discrete; - } - - @Override - public boolean attemptUpdate(Object value) { - if (!(value instanceof Number)) - return false; - - if (!(derive(this.getType(), (Number) value).validate())) - return false; - - this.value = (Number) value; - return true; - - } - - public static void requireType(NumericProperty property, NumericPropertyKeyword type) { - if (property.getType() != type) - throw new IllegalArgumentException("Illegal type: " + property.getType()); - } - - public boolean isDefaultSearchVariable() { - return defaultSearchVariable; - } - - public void setDefaultSearchVariable(boolean defaultSearchVariable) { - this.defaultSearchVariable = defaultSearchVariable; - } - -} \ No newline at end of file + private Number value; + + private Number minimum; + private Number maximum; + + private String descriptor; + private String abbreviation; + + private Number dimensionFactor; + private Number error; + + private NumericPropertyKeyword type; + private NumericPropertyKeyword[] excludes; + + private boolean autoAdjustable; + private boolean discrete; + private boolean defaultSearchVariable; + + /** + * Creates a {@code NumericProperty} based on { + * + * @pattern} and assigns {@code value} as its value. + * + * @param value the {@code} for the { + * @NumericProperty} that is otherwise equal to {@code pattern} + * @param pattern a valid {@code NumericProperty} + */ + protected NumericProperty(Number value, NumericProperty pattern) { + this(pattern); + this.value = value; + } + + /** + * Constructor used by {@code XMLConverter} to create a + * {@code NumericProperty} with fully specified set of parameters + * + * @param type the type of this {@code NumericProperty}, set by one of the + * {@code NumericPropertyKeyword} constants + * @param params the numeric parameters in the following order: value, + * minimum, maximum, dimension factor. + * @see pulse.io.export.XMLConverter + */ + public NumericProperty(NumericPropertyKeyword type, Number... params) { + if (params.length != 4) { + throw new IllegalArgumentException("Input array must be of length 4. Received: " + params.length); + } + + this.type = type; + this.value = params[0]; + this.dimensionFactor = params[3]; + this.excludes = new NumericPropertyKeyword[0]; + setDomain(params[1], params[2]); + } + + /** + * A copy constructor for {@code NumericProperty} + * + * @param num another {@code NumericProperty} that is going to be replicated + */ + public NumericProperty(NumericProperty num) { + this.value = num.value; + this.descriptor = num.descriptor; + this.abbreviation = num.abbreviation; + this.minimum = num.minimum; + this.maximum = num.maximum; + this.type = num.type; + this.discrete = num.discrete; + this.dimensionFactor = num.dimensionFactor; + this.autoAdjustable = num.autoAdjustable; + this.error = num.error; + this.defaultSearchVariable = num.defaultSearchVariable; + this.excludes = num.excludes; + } + + public NumericPropertyKeyword[] getExcludeKeywords() { + return excludes; + } + + public void setExcludeKeywords(NumericPropertyKeyword[] keys) { + this.excludes = keys; + } + + public NumericPropertyKeyword getType() { + return type; + } + + @Override + public Object getValue() { + return value; + } + + public boolean validate() { + return isValueSensible(this, value); + } + + /** + * Sets the {@code value} of this {@code NumericProperty} -- if and only if + * the new {@code value} is confined within the definition domain for this + * {@code NumericProperty}. Checks whether + * + * @param value the value to be set to {@code this property} + * @see NumericProperties.isValueSensible(NumericProperty,Number) + */ + public void setValue(Number value) { + + Number oldValue = this.value; + this.value = value; + + if (!validate()) { + this.value = oldValue; + throw new IllegalArgumentException(printRangeAndNumber(this, value)); + } + + } + + /** + * Sets the definition domain for this {@code NumericProperty}. + * + * @param minimum the minimum value + * @param maximum the maximum value + * @throws IllegalArgumentException if any two of + * {@code minimum, maximum, or this.value} have different primitive types + * (e.g. a {@code double} and an {@code int}). + */ + public void setDomain(Number minimum, Number maximum) throws IllegalArgumentException { + var minClass = minimum.getClass(); + var maxClass = maximum.getClass(); + if (!minClass.equals(maxClass)) { + throw new IllegalArgumentException( + "Types of minimum and maximum do not match: " + minClass + " and " + maxClass); + } + if (!minClass.equals(value.getClass())) { + throw new IllegalArgumentException("Interrupted attempt of setting " + minClass.getSimpleName() + + " boundaries to a " + value.getClass().getSimpleName() + " property"); + } + this.minimum = minimum; + this.maximum = maximum; + } + + public Number getMinimum() { + return minimum; + } + + public Number getMaximum() { + return maximum; + } + + /** + * Prints out the {@code type} and {@code value} of this + * {@code NumericProperty}. + */ + @Override + public String toString() { + return (type + " = " + formattedValueAndError(this, false)); + } + + /** + * Calls {@code formattedValue(true)}. + * + * @see NumericProperties.formattedValueAndError(boolean) + */ + @Override + public String formattedOutput() { + return formattedValueAndError(this, true); + } + + public String valueOutput() { + return numberFormat(this, true).format(valueInCurrentUnits()); + } + + public String errorOutput() { + return numberFormat(this, true).format(errorInCurrentUnits()); + } + + public Number valueInCurrentUnits() { + return value instanceof Double ? (double) value * dimensionFactor.doubleValue() : (int) value; + } + + public double errorInCurrentUnits() { + return error == null ? 0.0 : (double) error * dimensionFactor.doubleValue(); + } + + public Number getDimensionFactor() { + return dimensionFactor; + } + + public void setDimensionFactor(Number dimensionFactor) { + this.dimensionFactor = dimensionFactor; + } + + public void setVisibleByDefault(boolean autoAdjustable) { + this.autoAdjustable = autoAdjustable; + } + + public boolean isVisibleByDefault() { + return autoAdjustable; + } + + public Number getError() { + return error; + } + + public void setError(Number error) { + this.error = error; + } + + @Override + public String getDescriptor(boolean addHtmlTag) { + return addHtmlTag ? "" + descriptor + "" : descriptor; + } + + public void setDescriptor(String descriptor) { + this.descriptor = descriptor; + } + + public String getAbbreviation(boolean addHtmlTags) { + return addHtmlTags ? "" + abbreviation + "" : abbreviation; + } + + public void setAbbreviation(String abbreviation) { + this.abbreviation = abbreviation; + } + + /** + * The {@code Object} o is considered to be equal to this + * {@code NumericProperty} if (a) it is of the same class; (b) its value is + * the same as for this {@code NumericProperty}, and (c) if it is specified + * by the same {@code NumericPropertyKeyword}. + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!(o instanceof NumericProperty)) { + return false; + } + + NumericProperty onp = (NumericProperty) o; + + if (onp.getType() != this.getType()) { + return false; + } + + return compare(this, onp) == 0; + + } + + @Override + public int compareTo(NumericProperty arg0) { + final int result = this.getType().compareTo(arg0.getType()); + return result != 0 ? result : compare(this, arg0); + } + + public boolean isDiscrete() { + return discrete; + } + + public void setDiscrete(boolean discrete) { + this.discrete = discrete; + } + + @Override + public boolean attemptUpdate(Object value) { + if (!(value instanceof Number)) { + return false; + } + + if (!(derive(this.getType(), (Number) value).validate())) { + return false; + } + + this.value = (Number) value; + return true; + + } + + public static void requireType(NumericProperty property, NumericPropertyKeyword type) { + if (property.getType() != type) { + throw new IllegalArgumentException("Illegal type: " + property.getType()); + } + } + + public boolean isDefaultSearchVariable() { + return defaultSearchVariable; + } + + public void setDefaultSearchVariable(boolean defaultSearchVariable) { + this.defaultSearchVariable = defaultSearchVariable; + } + +} diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 1afaf682..fbb71212 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -225,12 +225,21 @@ public enum NumericPropertyKeyword { HEAT_LOSS_SIDE, /** - * A general keyword for the coefficient of heat losses. Indicates primarily - * those on the front and rear faces. + * The coefficient of heat losses from the front and rear surfaces of the sample + * (1D and 2D problems). */ HEAT_LOSS, + + /** + * A directive for the optimiser to maintain equal heat losses on all + * surfaces of the sample. Note that the dimensionless heat losses, + * i.e. Biot numbers, will differ due to different areas of the side and + * front/rear surfaces. + */ + HEAT_LOSS_COMBINED, + /** * Search iteration. */ @@ -261,6 +270,12 @@ public enum NumericPropertyKeyword { THERMAL_ABSORPTIVITY, + /** + * A directive to the optimiser informing both front and rear side absorptivities must be equal. + */ + + COMBINED_ABSORPTIVITY, + /** * Reflectance of the sample (0 < R ≤ 1). */ diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 0c7939d5..61004b9f 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -18,86 +18,89 @@ public class ActiveFlags { - private static List problemIndependentFlags = allProblemIndependentFlags(); - private static List problemDependentFlags = allProblemDependentFlags(); - - private ActiveFlags() { - //empty constructor - } - - public static void reset() { - problemDependentFlags = allProblemDependentFlags(); - problemIndependentFlags = allProblemIndependentFlags(); - } - - public static List getAllFlags() { - var newList = new ArrayList(); - newList.addAll(problemDependentFlags); - newList.addAll(problemIndependentFlags); - return newList; - } - - public static void listAvailableProperties(List list) { - list.addAll(problemIndependentFlags); - - var t = TaskManager.getManagerInstance().getSelectedTask(); - - if (t != null) { - var p = t.getCurrentCalculation().getProblem(); - - if (p != null) { - - var params = p.listedTypes().stream().filter(pp -> pp instanceof NumericProperty) - .map(pMap -> ((NumericProperty) pMap).getType()).collect(Collectors.toList()); - - NumericPropertyKeyword key; - - for (Flag property : problemDependentFlags) { - key = property.getType(); - if (params.contains(key)) - list.add(property); - - } - - } - } else { - for (Flag property : problemDependentFlags) { - list.add(property); - } - } - } - - /** - * Finds what properties are being altered in the search - * - * @return a {@code List} of property types represented by - * {@code NumericPropertyKeyword}s - */ - - public static List activeParameters(SearchTask t) { - Problem p = t.getCurrentCalculation().getProblem(); - - var list = new ArrayList(); - list.addAll(selectActiveAndListed(problemDependentFlags, p)); - list.addAll(selectActiveTypes(problemIndependentFlags)); - return list; - } - - public static List selectActiveAndListed(List flags, Problem listed) { - return selectActiveTypes(flags).stream().filter(type -> listed.isListedNumericType(type)) - .collect(Collectors.toList()); - } - - public static List selectActiveTypes(List flags) { - return selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); - } - - public static List getProblemIndependentFlags() { - return problemIndependentFlags; - } - - public static List getProblemDependentFlags() { - return problemDependentFlags; - } - -} \ No newline at end of file + private static List problemIndependentFlags = allProblemIndependentFlags(); + private static List problemDependentFlags = allProblemDependentFlags(); + + private ActiveFlags() { + //empty constructor + } + + public static void reset() { + problemDependentFlags = allProblemDependentFlags(); + problemIndependentFlags = allProblemIndependentFlags(); + } + + public static List getAllFlags() { + var newList = new ArrayList(); + newList.addAll(problemDependentFlags); + newList.addAll(problemIndependentFlags); + return newList; + } + + public static void listAvailableProperties(List list) { + list.addAll(problemIndependentFlags); + + var t = TaskManager.getManagerInstance().getSelectedTask(); + + if (t != null) { + var p = t.getCurrentCalculation().getProblem(); + + if (p != null) { + + var params = p.listedTypes().stream().filter(pp + -> pp instanceof NumericProperty) + .map(pMap -> ((NumericProperty) pMap) + .getType()).collect( + Collectors.toList()); + + NumericPropertyKeyword key; + + for (Flag property : problemDependentFlags) { + key = property.getType(); + if (params.contains(key)) { + list.add(property); + } + + } + + } + } else { + for (Flag property : problemDependentFlags) { + list.add(property); + } + } + } + + /** + * Finds what properties are being altered in the search + * + * @return a {@code List} of property types represented by + * {@code NumericPropertyKeyword}s + */ + public static List activeParameters(SearchTask t) { + Problem p = t.getCurrentCalculation().getProblem(); + + var list = new ArrayList(); + list.addAll(selectActiveAndListed(problemDependentFlags, p)); + list.addAll(selectActiveTypes(problemIndependentFlags)); + return list; + } + + public static List selectActiveAndListed(List flags, Problem listed) { + return selectActiveTypes(flags).stream().filter(type -> listed.isListedNumericType(type)) + .collect(Collectors.toList()); + } + + public static List selectActiveTypes(List flags) { + return selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); + } + + public static List getProblemIndependentFlags() { + return problemIndependentFlags; + } + + public static List getProblemDependentFlags() { + return problemDependentFlags; + } + +} diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index 84ac428d..c5039b3d 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -32,224 +32,216 @@ * class is closely linked with another abstract search class, the * {@code LinearSolver}. *

- * + * * @see pulse.search.tasks.SearchTask.run() * @see pulse.search.linear.LinearOptimiser */ - public abstract class PathOptimiser extends PropertyHolder implements Reflexive { - private DirectionSolver solver; - - private int maxIterations; - private double errorTolerance; - - private static PathOptimiser instance; - - /** - * Abstract constructor that sets up the default - * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} for - * this {@code PathSolver}. In addition, sets up a list of search flags defined - * by the {@code Flag.defaultList} method. - * - * @see pulse.properties.Flag.defaultList() - */ - - protected PathOptimiser() { - super(); - reset(); - } - - /** - * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and - * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In addition, - * sets up a list of search flags defined by the {@code Flag.defaultList} - * method. - * - * @see pulse.properties.Flag.defaultList() - */ - - public void reset() { - maxIterations = (int) def(ITERATION_LIMIT).getValue(); - errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); - ActiveFlags.reset(); - } - - /** - *

- * This method sets out the basic algorithm for estimating the minimum of the - * target function, which is defined as the sum of squared residuals (SSR), or - * the deviations of the model solution (a {@code DifferenceScheme} used to - * solve the {@code Problem} for this {@code task}) from the empirical values - * (the {@code ExperimentalData}). The algorithm will go through the following - * steps: (1) find the direction, which points to the minimum, using the - * concrete {@code direction} method; (2) estimate the magnitude of the step to - * reach the minimum using the {@code LinearSolver}; (3) assign a new set of - * parameters to the {@code SearchTask}; (4) calculate the new SSR value. - *

- *

- * - * @param task a {@code SearchTask} that needs to be driven to a minimum of SSR. - * @return the SSR value with the newly found parameters. - * @throws SolverException - * @see direction(Path) - * @see pulse.search.linear.LinearOptimiser - */ - - public abstract boolean iteration(SearchTask task) throws SolverException; - - /** - * Defines a set of procedures to be run at the end of the search iteration. - * - * @param task the {@code SearchTask} undergoing optimisation - * @throws SolverException - */ - - public abstract void prepare(SearchTask task) throws SolverException; - - public NumericProperty getErrorTolerance() { - return derive(ERROR_TOLERANCE, errorTolerance); - } - - public void setErrorTolerance(NumericProperty errorTolerance) { - requireType(errorTolerance, ERROR_TOLERANCE); - this.errorTolerance = (double) errorTolerance.getValue(); - firePropertyChanged(this, errorTolerance); - } - - public NumericProperty getMaxIterations() { - return derive(ITERATION_LIMIT, maxIterations); - } - - public void setMaxIterations(NumericProperty maxIterations) { - requireType(maxIterations, ITERATION_LIMIT); - this.maxIterations = (int) maxIterations.getValue(); - firePropertyChanged(this, maxIterations); - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * This method has been overriden to account for each individual flag in the - * {@code List} set out by this class. - */ - - @Override - public List genericProperties() { - var original = super.genericProperties(); - original.addAll(ActiveFlags.getProblemDependentFlags()); - original.addAll(ActiveFlags.getProblemIndependentFlags()); - return original; - } - - /** - *

- * The types of the listed parameters for this class include: - * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class - * are treated as separate listed parameters. - *

- * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - List list = new ArrayList(); - list.add(def(ERROR_TOLERANCE)); - list.add(def(ITERATION_LIMIT)); - - ActiveFlags.listAvailableProperties(list); - - return list; - } - - @Override - public List data() { - var list = listedTypes(); - return super.data().stream().filter(p -> list.contains(p)).collect(Collectors.toList()); - } - - /** - * The accepted types are: - * ERROR_TOLERANCE, ITERATION_LIMIT. - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if(type == ERROR_TOLERANCE) - setErrorTolerance(property); - else if(type == ITERATION_LIMIT) - setMaxIterations(property); - } - - /** - * @return {@code false} for {@code PathSolver} - */ - - @Override - public boolean ignoreSiblings() { - return true; - } - - /** - * Finds a {@code Flag} equivalent to {@code flag} in the {@code originalList} - * and substitutes its value with {@code flag.getValue}. - */ - - @Override - public void update(Property property) { - if(! (property instanceof Flag) ) - super.update(property); - else { - var flag = (Flag) property; - var optional = ActiveFlags.getAllFlags().stream().filter(f -> f.getType() == flag.getType()).findFirst(); - - if (optional.isPresent()) - optional.get().setValue((boolean) flag.getValue()); - - } - } - - public static PathOptimiser getInstance() { - return instance; - } - - public static void setInstance(PathOptimiser selectedPathOptimiser) { - PathOptimiser.instance = selectedPathOptimiser; - selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); - } - - protected DirectionSolver getSolver() { - return solver; - } - - protected void setSolver(DirectionSolver solver) { - this.solver = solver; - } - - /** - * Checks if this optimiser is compatible with the statistic passed to the method as its argument. - * By default, this will accept any {@code OptimiserStatistic}. - * @return {@code true}, if not specified otherwise by its subclass implementation. - */ - - public boolean compatibleWith(OptimiserStatistic os) { - return true; - } - - - /** - * Creates a new {@code Path} suitable for this {@code PathSolver} - * - * @param t the task, the optimisation path of which will be tracked - * @return a {@code Path} instance - */ - - public abstract IterativeState initState(SearchTask t); - - - -} \ No newline at end of file + private DirectionSolver solver; + + private int maxIterations; + private double errorTolerance; + + private static PathOptimiser instance; + + /** + * Abstract constructor that sets up the default + * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} + * for this {@code PathSolver}. In addition, sets up a list of search flags + * defined by the {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + protected PathOptimiser() { + super(); + reset(); + } + + /** + * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and + * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In + * addition, sets up a list of search flags defined by the + * {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + public void reset() { + maxIterations = (int) def(ITERATION_LIMIT).getValue(); + errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); + ActiveFlags.reset(); + } + + /** + *

+ * This method sets out the basic algorithm for estimating the minimum of + * the target function, which is defined as the sum of squared residuals + * (SSR), or the deviations of the model solution (a + * {@code DifferenceScheme} used to solve the {@code Problem} for this + * {@code task}) from the empirical values (the {@code ExperimentalData}). + * The algorithm will go through the following steps: (1) find the + * direction, which points to the minimum, using the concrete + * {@code direction} method; (2) estimate the magnitude of the step to reach + * the minimum using the {@code LinearSolver}; (3) assign a new set of + * parameters to the {@code SearchTask}; (4) calculate the new SSR value. + *

+ *

+ * + * @param task a {@code SearchTask} that needs to be driven to a minimum of + * SSR. + * @return the SSR value with the newly found parameters. + * @throws SolverException + * @see direction(Path) + * @see pulse.search.linear.LinearOptimiser + */ + public abstract boolean iteration(SearchTask task) throws SolverException; + + /** + * Defines a set of procedures to be run at the end of the search iteration. + * + * @param task the {@code SearchTask} undergoing optimisation + * @throws SolverException + */ + public abstract void prepare(SearchTask task) throws SolverException; + + public NumericProperty getErrorTolerance() { + return derive(ERROR_TOLERANCE, errorTolerance); + } + + public void setErrorTolerance(NumericProperty errorTolerance) { + requireType(errorTolerance, ERROR_TOLERANCE); + this.errorTolerance = (double) errorTolerance.getValue(); + firePropertyChanged(this, errorTolerance); + } + + public NumericProperty getMaxIterations() { + return derive(ITERATION_LIMIT, maxIterations); + } + + public void setMaxIterations(NumericProperty maxIterations) { + requireType(maxIterations, ITERATION_LIMIT); + this.maxIterations = (int) maxIterations.getValue(); + firePropertyChanged(this, maxIterations); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * This method has been overriden to account for each individual flag in the + * {@code List} set out by this class. + */ + @Override + public List genericProperties() { + var original = super.genericProperties(); + original.addAll(ActiveFlags.getProblemDependentFlags()); + original.addAll(ActiveFlags.getProblemIndependentFlags()); + return original; + } + + /** + *

+ * The types of the listed parameters for this class include: + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in + * this class are treated as separate listed parameters. + *

+ * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public List listedTypes() { + List list = new ArrayList(); + list.add(def(ERROR_TOLERANCE)); + list.add(def(ITERATION_LIMIT)); + + ActiveFlags.listAvailableProperties(list); + + return list; + } + + @Override + public List data() { + var list = listedTypes(); + return super.data().stream().filter(p -> list.contains(p)).collect(Collectors.toList()); + } + + /** + * The accepted types are: ERROR_TOLERANCE, ITERATION_LIMIT. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == ERROR_TOLERANCE) { + setErrorTolerance(property); + } else if (type == ITERATION_LIMIT) { + setMaxIterations(property); + } + } + + /** + * @return {@code false} for {@code PathSolver} + */ + @Override + public boolean ignoreSiblings() { + return true; + } + + /** + * Finds a {@code Flag} equivalent to {@code flag} in the + * {@code originalList} and substitutes its value with + * {@code flag.getValue}. + */ + @Override + public void update(Property property) { + if (!(property instanceof Flag)) { + super.update(property); + } else { + var flag = (Flag) property; + var optional = ActiveFlags.getAllFlags().stream().filter(f -> f.getType() == flag.getType()).findFirst(); + + if (optional.isPresent()) { + optional.get().setValue((boolean) flag.getValue()); + } + + } + } + + public static PathOptimiser getInstance() { + return instance; + } + + public static void setInstance(PathOptimiser selectedPathOptimiser) { + PathOptimiser.instance = selectedPathOptimiser; + selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); + } + + protected DirectionSolver getSolver() { + return solver; + } + + protected void setSolver(DirectionSolver solver) { + this.solver = solver; + } + + /** + * Checks if this optimiser is compatible with the statistic passed to the + * method as its argument. By default, this will accept any + * {@code OptimiserStatistic}. + * + * @return {@code true}, if not specified otherwise by its subclass + * implementation. + */ + public boolean compatibleWith(OptimiserStatistic os) { + return true; + } + + /** + * Creates a new {@code Path} suitable for this {@code PathSolver} + * + * @param t the task, the optimisation path of which will be tracked + * @return a {@code Path} instance + */ + public abstract IterativeState initState(SearchTask t); + +} From f98b62edc597ad6befd427ba82811d065619a8d4 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:19:41 +0100 Subject: [PATCH 053/116] LM Optimiser changes - Fixed wrong acceptance of new parameters which are equal to previous set --- .../pulse/search/direction/LMOptimiser.java | 506 +++++++++--------- 1 file changed, 248 insertions(+), 258 deletions(-) diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 1c219f2f..d488b876 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -28,280 +28,270 @@ /** * Given an objective function equal to the sum of squared residuals, - * iteratively approaches the minimum of this function by applying - * the Levenberg-Marquardt formulas. + * iteratively approaches the minimum of this function by applying the + * Levenberg-Marquardt formulas. * */ - public class LMOptimiser extends GradientBasedOptimiser { - private static LMOptimiser instance = new LMOptimiser(); - - private final static double EPS = 1e-10; // for numerical comparison - private double dampingRatio; - - /** - * Maximum number of consequent failed iterations that can be rejected. - */ - - public final static int MAX_FAILED_ATTEMPTS = 10; - - private LMOptimiser() { - super(); - dampingRatio = (double)def(DAMPING_RATIO).getValue(); - this.setSolver(new HessianDirectionSolver() { - // see default implementation - }); - } - - @Override - public boolean iteration(SearchTask task) throws SolverException { - var p = (LMPath) task.getIterativeState(); // the previous path of the task - - boolean accept = true; //accept the step by default - - /* - * Checks whether an iteration limit has been already reached - */ + private static LMOptimiser instance = new LMOptimiser(); + + private final static double EPS = 1e-10; // for numerical comparison + private double dampingRatio; - if (compare(p.getIteration(), getMaxIterations()) > 0) { + /** + * Maximum number of consequent failed iterations that can be rejected. + */ + public final static int MAX_FAILED_ATTEMPTS = 10; - task.setStatus(Status.TIMEOUT); + private LMOptimiser() { + super(); + dampingRatio = (double) def(DAMPING_RATIO).getValue(); + this.setSolver(new HessianDirectionSolver() { + // see default implementation + }); + } - } + @Override + public boolean iteration(SearchTask task) throws SolverException { + var p = (LMPath) task.getIterativeState(); // the previous path of the task + + boolean accept = true; //accept the step by default + + /* + * Checks whether an iteration limit has been already reached + */ + if (compare(p.getIteration(), getMaxIterations()) > 0) { - else { + task.setStatus(Status.TIMEOUT); - double initialCost = task.solveProblemAndCalculateCost(); - var parameters = task.searchVector(); + } else { - p.setParameters(parameters); // store current parameters + double initialCost = task.solveProblemAndCalculateCost(); + var parameters = task.searchVector(); - prepare(task); // do the preparatory step + p.setParameters(parameters); // store current parameters - var lmDirection = getSolver().direction(p); - - var candidate = parameters.sum(lmDirection); - task.assign(new ParameterVector( - parameters, candidate ) ); // assign new parameters - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + prepare(task); // do the preparatory step - /* + var lmDirection = getSolver().direction(p); + + var candidate = parameters.sum(lmDirection); + task.assign(new ParameterVector( + parameters, candidate)); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + + /* * Delayed gratification - */ - - if (newCost > initialCost + EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS) { - p.setLambda(p.getLambda() * 2.0); - task.assign(parameters); // roll back if cost increased - p.setComputeJacobian(true); - p.incrementFailedAttempts(); - accept = false; - } else { - p.resetFailedAttempts(); - p.setLambda(p.getLambda() / 3.0); - p.setComputeJacobian(false); - p.incrementStep(); // increment the counter of successful steps - } - - } - - return accept; //either accept or reject this step - - } - - /** - * Calculates the Jacobian, if needed, evaluates the gradient and the Hessian matrix. - */ - - @Override - public void prepare(SearchTask task) throws SolverException { - var p = (LMPath) task.getIterativeState(); - - //store residual vector at current parameters - p.setResidualVector( new Vector( residualVector(task.getCurrentCalculation().getOptimiserStatistic()) )); - - // Calculate the Jacobian -- if needed - if (p.isComputeJacobian()) { - p.setJacobian(jacobian(task)); // J - p.setNonregularisedHessian(halfHessian(p)); // this is just J'J - } - - // the Jacobian is then used to calculate the 'gradient' - Vector g1 = halfGradient(p); // g1 - p.setGradient(g1); - - // the Hessian is then regularised by adding labmda*I - - var hessian = p.getNonregularisedHessian(); - var damping = ( levenbergDamping(hessian).multiply(dampingRatio) - .sum(marquardtDamping(hessian).multiply(1.0 - dampingRatio)) - ) - .multiply(p.getLambda()); - var regularisedHessian = asSquareMatrix(hessian.sum(damping)); // J'J + lambda I - - p.setHessian(regularisedHessian); // so this is the new Hessian - - } - - /** - *

- * Calculates the Jacobian of the model function given as a discrete set of time-signal values. - * The elements of the Jacobian are calculated using central differences from two residual vectors - * evaluated by shifting the search vector slightly to the right or left of each search parameter. - *

- *

- * This is also equivalent to calculating the difference of the model values when performing the shift, - * when taking the model values at the time points of the reference dataset. Because of a different - * discretisation of the model, it is easier to substitute these with the residuals, which had already - * been interpolated at the reference time values. - *

- * @param task the task being optimised - * @return the jacobian matrix - * @throws SolverException - * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() - */ - - - public RectangularMatrix jacobian(SearchTask task) throws SolverException { - - var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); - - var p = ((LMPath) task.getIterativeState()); - - final var params = p.getParameters(); - - final int numPoints = p.getResidualVector().dimension(); - final int numParams = params.dimension(); - - var jacobian = new double[numPoints][numParams]; - - final double resolutionHigh = super.getGradientStep(); - final double resolutionLow = 1E-2; //TODO - - for (int i = 0; i < numParams; i++) { - - boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); - double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); - - final var shift = new Vector(numParams); - shift.set(i, 0.5 * dx); - - // + shift - task.assign(new ParameterVector( params, params.sum(shift) )); - task.solveProblemAndCalculateCost(); - var r1 = residualVector(residualCalculator); - - // - shift - task.assign(new ParameterVector( params, params.subtract(shift) )); - task.solveProblemAndCalculateCost(); - var r2 = residualVector(residualCalculator); - - for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { - - jacobian[j][i] = (r1[j] - r2[j]) / dx; - - } - - } - - // revert to original params - task.assign(params); - - return Matrices.createMatrix(jacobian); - - } - - private static double[] residualVector(ResidualStatistic rs) { - return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); - } - - @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); - return new LMPath(t); - } - - private Vector halfGradient(LMPath path) { - var jacobian = path.getJacobian(); - var residuals = path.getResidualVector(); - return jacobian.transpose().multiply(new Vector(residuals)); - } - - private SquareMatrix halfHessian(LMPath path) { - var jacobian = path.getJacobian(); - return asSquareMatrix(jacobian.transpose().multiply(jacobian)); - } - - /* + */ + if (newCost > initialCost - EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS) { + p.setLambda(p.getLambda() * 2.0); + task.assign(parameters); // roll back if cost increased + p.setComputeJacobian(true); + p.incrementFailedAttempts(); + accept = false; + } else { + p.resetFailedAttempts(); + p.setLambda(p.getLambda() / 3.0); + p.setComputeJacobian(false); + p.incrementStep(); // increment the counter of successful steps + } + + } + + return accept; //either accept or reject this step + + } + + /** + * Calculates the Jacobian, if needed, evaluates the gradient and the + * Hessian matrix. + */ + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (LMPath) task.getIterativeState(); + + //store residual vector at current parameters + p.setResidualVector(new Vector(residualVector(task.getCurrentCalculation().getOptimiserStatistic()))); + + // Calculate the Jacobian -- if needed + if (p.isComputeJacobian()) { + p.setJacobian(jacobian(task)); // J + p.setNonregularisedHessian(halfHessian(p)); // this is just J'J + } + + // the Jacobian is then used to calculate the 'gradient' + Vector g1 = halfGradient(p); // g1 + p.setGradient(g1); + + // the Hessian is then regularised by adding labmda*I + var hessian = p.getNonregularisedHessian(); + var damping = (levenbergDamping(hessian).multiply(dampingRatio) + .sum(marquardtDamping(hessian).multiply(1.0 - dampingRatio))) + .multiply(p.getLambda()); + var regularisedHessian = asSquareMatrix(hessian.sum(damping)); // J'J + lambda I + + p.setHessian(regularisedHessian); // so this is the new Hessian + + } + + /** + *

+ * Calculates the Jacobian of the model function given as a discrete set of + * time-signal values. The elements of the Jacobian are calculated using + * central differences from two residual vectors evaluated by shifting the + * search vector slightly to the right or left of each search parameter. + *

+ *

+ * This is also equivalent to calculating the difference of the model values + * when performing the shift, when taking the model values at the time + * points of the reference dataset. Because of a different discretisation of + * the model, it is easier to substitute these with the residuals, which had + * already been interpolated at the reference time values. + *

+ * + * @param task the task being optimised + * @return the jacobian matrix + * @throws SolverException + * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() + */ + public RectangularMatrix jacobian(SearchTask task) throws SolverException { + + var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); + + var p = ((LMPath) task.getIterativeState()); + + final var params = p.getParameters(); + + final int numPoints = p.getResidualVector().dimension(); + final int numParams = params.dimension(); + + var jacobian = new double[numPoints][numParams]; + + final double resolutionHigh = super.getGradientStep(); + final double resolutionLow = 1E-2; //TODO + + for (int i = 0; i < numParams; i++) { + + boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); + double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); + + final var shift = new Vector(numParams); + shift.set(i, 0.5 * dx); + + // + shift + task.assign(new ParameterVector(params, params.sum(shift))); + task.solveProblemAndCalculateCost(); + var r1 = residualVector(residualCalculator); + + // - shift + task.assign(new ParameterVector(params, params.subtract(shift))); + task.solveProblemAndCalculateCost(); + var r2 = residualVector(residualCalculator); + + for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { + + jacobian[j][i] = (r1[j] - r2[j]) / dx; + + } + + } + + // revert to original params + task.assign(params); + + return Matrices.createMatrix(jacobian); + + } + + private static double[] residualVector(ResidualStatistic rs) { + return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); + } + + @Override + public GradientGuidedPath initState(SearchTask t) { + this.configure(t); + return new LMPath(t); + } + + private Vector halfGradient(LMPath path) { + var jacobian = path.getJacobian(); + var residuals = path.getResidualVector(); + return jacobian.transpose().multiply(new Vector(residuals)); + } + + private SquareMatrix halfHessian(LMPath path) { + var jacobian = path.getJacobian(); + return asSquareMatrix(jacobian.transpose().multiply(jacobian)); + } + + /* * Additive damping strategy, where the scaling matrix is simply the identity matrix. - */ + */ + private SquareMatrix levenbergDamping(SquareMatrix hessian) { + return Matrices.createIdentityMatrix(hessian.getData().length); + } - private SquareMatrix levenbergDamping(SquareMatrix hessian) { - return Matrices.createIdentityMatrix(hessian.getData().length); - } - - /* + /* * Multiplicative damping strategy, where the scaling matrix is equal to the 'hessian' block-diagonal matrix. * Works best for badly scaled problems. However, this is also scale-invariant, * which mean it increases the susceptibility to parameter evaporation. - */ - - private SquareMatrix marquardtDamping(SquareMatrix hessian) { - return hessian.blockDiagonal(); - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(DAMPING_RATIO)); - return list; - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static LMOptimiser getInstance() { - return instance; - } - - @Override - public String toString() { - return Messages.getString("LMOptimiser.Descriptor"); - } - - /** - * The Levenberg-Marquardt optimiser will only accept ordinary least-squares as - * its objective function. Therefore, {@code os} should be an instance of - * {@code SumOfSquares}. - * - * @return {@code true} if {@code.getClass()} returns - * {@code SumOfSquares.class}, {@code false} otherwise - */ - - @Override - public boolean compatibleWith(OptimiserStatistic os) { - return os.getClass().equals(SumOfSquares.class); - } - - public NumericProperty getDampingRatio() { - return derive(DAMPING_RATIO, dampingRatio); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if(type == DAMPING_RATIO) - setDampingRatio(property); - } - - public void setDampingRatio(NumericProperty dampingRatio) { - requireType(dampingRatio, DAMPING_RATIO); - this.dampingRatio = (double)dampingRatio.getValue(); - firePropertyChanged(this, dampingRatio); - } - -} \ No newline at end of file + */ + private SquareMatrix marquardtDamping(SquareMatrix hessian) { + return hessian.blockDiagonal(); + } + + @Override + public List listedTypes() { + var list = super.listedTypes(); + list.add(def(DAMPING_RATIO)); + return list; + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static LMOptimiser getInstance() { + return instance; + } + + @Override + public String toString() { + return Messages.getString("LMOptimiser.Descriptor"); + } + + /** + * The Levenberg-Marquardt optimiser will only accept ordinary least-squares + * as its objective function. Therefore, {@code os} should be an instance of + * {@code SumOfSquares}. + * + * @return {@code true} if {@code.getClass()} returns + * {@code SumOfSquares.class}, {@code false} otherwise + */ + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return os.getClass().equals(SumOfSquares.class); + } + + public NumericProperty getDampingRatio() { + return derive(DAMPING_RATIO, dampingRatio); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == DAMPING_RATIO) { + setDampingRatio(property); + } + } + + public void setDampingRatio(NumericProperty dampingRatio) { + requireType(dampingRatio, DAMPING_RATIO); + this.dampingRatio = (double) dampingRatio.getValue(); + firePropertyChanged(this, dampingRatio); + } + +} From cc1f3d712e3736860f8abe71a4407f7ad41656ab Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:22:31 +0100 Subject: [PATCH 054/116] General changes: - Formatting - Added firePropertyChanged after setting model selection criteria --- src/main/java/pulse/tasks/Calculation.java | 490 +++++++++++---------- 1 file changed, 248 insertions(+), 242 deletions(-) diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 14847faa..b893e377 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -31,245 +31,251 @@ public class Calculation extends PropertyHolder implements Comparable { - private Status status; - public final static double RELATIVE_TIME_MARGIN = 1.01; - - private Problem problem; - private DifferenceScheme scheme; - private ModelSelectionCriterion rs; - private OptimiserStatistic os; - private Result result; - - private static InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Model Selection Criterion", ModelSelectionCriterion.class); - - static { - instanceDescriptor.setSelectedDescriptor(AICStatistic.class.getSimpleName()); - } - - public Calculation() { - status = INCOMPLETE; - this.initOptimiser(); - instanceDescriptor.addListener(() -> initModelCriterion()); - } - - public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { - this(); - this.problem = problem; - this.scheme = scheme; - this.os = rs.getOptimiser(); - this.rs = rs; - problem.setParent(this); - scheme.setParent(this); - os.setParent(this); - rs.setParent(this); - } - - public Calculation copy() { - var status = this.status; - var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); - var p = nCalc.getProblem(); - p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); - nCalc.status = status; - if (this.getResult() != null) - nCalc.setResult(new Result(this.getResult())); - return nCalc; - } - - public void clear() { - this.status = INCOMPLETE; - this.problem = null; - this.scheme = null; - } - - /** - *

- * After setting and adopting the {@code problem} by this {@code SearchTask}, - * this will attempt to change the parameters of that {@code problem} in - * accordance with the loaded {@code ExperimentalData} for this - * {@code SearchTask} (if not null). Later, if any changes to the properties of - * that {@code Problem} occur and if the source of that event is either the - * {@code Metadata} or the {@code PropertyHolderTable}, they will be accounted - * for by altering the parameters of the {@code problem} accordingly -- - * immediately after the former take place. - *

- * - * @param problem a {@code Problem} - */ - - public void setProblem(Problem problem, ExperimentalData curve) { - this.problem = problem; - problem.setParent(this); - problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); - addProblemListeners(problem, curve); - } - - private void addProblemListeners(Problem problem, ExperimentalData curve) { - problem.getProperties().addListener((PropertyEvent event) -> { - var source = event.getSource(); - - if (source instanceof Metadata || source instanceof PropertyHolderTable) { - - var property = event.getProperty(); - if (property instanceof NumericProperty && ((NumericProperty) property).isAutoAdjustable()) - return; - - problem.estimateSignalRange(curve); - problem.getProperties().useTheoreticalEstimates(curve); - } - }); - - problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { - - var event = dataEvent.getType(); - - if (event == TIME_ORIGIN_CHANGED) { - var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); - } - - }); - } - - /** - * Adopts the {@code scheme} by this {@code SearchTask} and updates the time - * limit of {@scheme} to match {@code ExperimentalData}. - * - * @param scheme the {@code DiffenceScheme}. - */ - - public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { - this.scheme = scheme; - - if (problem != null && scheme != null) { - scheme.setParent(this); - - var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); - - } - - } - - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code Calculation}. - * - * @throws SolverException - */ - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void process() throws SolverException { - ((Solver) scheme).solve(problem); - } - - public Status getStatus() { - return status; - } - - public boolean setStatus(Status status) { - boolean done = this.status != status; - this.status = status; - return done; - } - - public NumericProperty weight(List all) { - var result = def(MODEL_WEIGHT); - - boolean condition = all.stream() - .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(rs.getClass())); - - if (condition) { - var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) - .collect(Collectors.toList()); - result = rs.weight(list); - } - - return result; - } - - public void setModelSelectionCriterion(ModelSelectionCriterion rs) { - this.rs = rs; - rs.setParent(this); - } - - public ModelSelectionCriterion getModelSelectionCriterion() { - return rs; - } - - public void setOptimiserStatistic(OptimiserStatistic os) { - this.os = os; - os.setParent(this); - initModelCriterion(); - } - - public OptimiserStatistic getOptimiserStatistic() { - return os; - } - - public Problem getProblem() { - return problem; - } - - public void initOptimiser() { - this.setOptimiserStatistic( - instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor())); - this.initModelCriterion(); - } - - public void initModelCriterion() { - setModelSelectionCriterion(instanceDescriptor.newInstance(ModelSelectionCriterion.class, os)); - } - - public DifferenceScheme getScheme() { - return scheme; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - @Override - public int compareTo(Calculation arg0) { - var s1 = arg0.getModelSelectionCriterion().getStatistic(); - return getModelSelectionCriterion().getStatistic().compareTo(s1); - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (o == null) - return false; - - if (!(o instanceof Calculation)) - return false; - - var c = (Calculation) o; - - return (os.getStatistic().equals(c.getOptimiserStatistic().getStatistic()) - && rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())); - - } - - public static InstanceDescriptor getModelSelectionDescriptor() { - return instanceDescriptor; - } - - public Result getResult() { - return result; - } - - public void setResult(Result result) { - this.result = result; - if(result != null) - result.setParent(this); - } - -} \ No newline at end of file + private Status status; + public final static double RELATIVE_TIME_MARGIN = 1.01; + + private Problem problem; + private DifferenceScheme scheme; + private ModelSelectionCriterion rs; + private OptimiserStatistic os; + private Result result; + + private static InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( + "Model Selection Criterion", ModelSelectionCriterion.class); + + static { + instanceDescriptor.setSelectedDescriptor(AICStatistic.class.getSimpleName()); + } + + public Calculation() { + status = INCOMPLETE; + this.initOptimiser(); + instanceDescriptor.addListener(() -> initModelCriterion()); + } + + public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { + this(); + this.problem = problem; + this.scheme = scheme; + this.os = rs.getOptimiser(); + this.rs = rs; + problem.setParent(this); + scheme.setParent(this); + os.setParent(this); + rs.setParent(this); + } + + public Calculation copy() { + var status = this.status; + var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); + var p = nCalc.getProblem(); + p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); + nCalc.status = status; + if (this.getResult() != null) { + nCalc.setResult(new Result(this.getResult())); + } + return nCalc; + } + + public void clear() { + this.status = INCOMPLETE; + this.problem = null; + this.scheme = null; + } + + /** + *

+ * After setting and adopting the {@code problem} by this + * {@code SearchTask}, this will attempt to change the parameters of that + * {@code problem} in accordance with the loaded {@code ExperimentalData} + * for this {@code SearchTask} (if not null). Later, if any changes to the + * properties of that {@code Problem} occur and if the source of that event + * is either the {@code Metadata} or the {@code PropertyHolderTable}, they + * will be accounted for by altering the parameters of the {@code problem} + * accordingly -- immediately after the former take place. + *

+ * + * @param problem a {@code Problem} + */ + public void setProblem(Problem problem, ExperimentalData curve) { + this.problem = problem; + problem.setParent(this); + problem.removeHeatingCurveListeners(); + problem.retrieveData(curve); + addProblemListeners(problem, curve); + } + + private void addProblemListeners(Problem problem, ExperimentalData curve) { + problem.getProperties().addListener((PropertyEvent event) -> { + var source = event.getSource(); + + if (source instanceof Metadata || source instanceof PropertyHolderTable) { + + var property = event.getProperty(); + if (property instanceof NumericProperty && ((NumericProperty) property).isVisibleByDefault()) { + return; + } + + problem.estimateSignalRange(curve); + problem.getProperties().useTheoreticalEstimates(curve); + } + }); + + problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { + + var event = dataEvent.getType(); + + if (event == TIME_ORIGIN_CHANGED) { + var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); + } + + }); + } + + /** + * Adopts the {@code scheme} by this {@code SearchTask} and updates the time + * limit of { + * + * @scheme} to match {@code ExperimentalData}. + * + * @param scheme the {@code DiffenceScheme}. + */ + public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { + this.scheme = scheme; + + if (problem != null && scheme != null) { + scheme.setParent(this); + + var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); + + } + + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code Calculation}. + * + * @throws SolverException + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public void process() throws SolverException { + ((Solver) scheme).solve(problem); + } + + public Status getStatus() { + return status; + } + + public boolean setStatus(Status status) { + boolean done = this.status != status; + this.status = status; + return done; + } + + public NumericProperty weight(List all) { + var result = def(MODEL_WEIGHT); + + boolean condition = all.stream() + .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(rs.getClass())); + + if (condition) { + var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) + .collect(Collectors.toList()); + result = rs.weight(list); + } + + return result; + } + + public void setModelSelectionCriterion(ModelSelectionCriterion rs) { + this.rs = rs; + rs.setParent(this); + firePropertyChanged(this, instanceDescriptor); + } + + public ModelSelectionCriterion getModelSelectionCriterion() { + return rs; + } + + public void setOptimiserStatistic(OptimiserStatistic os) { + this.os = os; + os.setParent(this); + initModelCriterion(); + } + + public OptimiserStatistic getOptimiserStatistic() { + return os; + } + + public Problem getProblem() { + return problem; + } + + public void initOptimiser() { + this.setOptimiserStatistic( + instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor())); + this.initModelCriterion(); + } + + public void initModelCriterion() { + setModelSelectionCriterion(instanceDescriptor.newInstance(ModelSelectionCriterion.class, os)); + } + + public DifferenceScheme getScheme() { + return scheme; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public int compareTo(Calculation arg0) { + var s1 = arg0.getModelSelectionCriterion().getStatistic(); + return getModelSelectionCriterion().getStatistic().compareTo(s1); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o instanceof Calculation)) { + return false; + } + + var c = (Calculation) o; + + return (os.getStatistic().equals(c.getOptimiserStatistic().getStatistic()) + && rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())); + + } + + public static InstanceDescriptor getModelSelectionDescriptor() { + return instanceDescriptor; + } + + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + if (result != null) { + result.setParent(this); + } + } + +} From e093d2b032f511abca5c5827107fe09e48dba33d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:23:45 +0100 Subject: [PATCH 055/116] Removed correlation test for MAXTEMP and HEAT_LOSS_COMBINED --- src/main/java/pulse/tasks/processing/CorrelationBuffer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index e4417488..dc01df99 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -30,6 +30,7 @@ public class CorrelationBuffer { excludePair(NumericPropertyKeyword.HEAT_LOSS, NumericPropertyKeyword.MAXTEMP); excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_INTERCEPT); excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_SLOPE); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.HEAT_LOSS_COMBINED); } public CorrelationBuffer() { From 4fb0d89b3dad1c6c78b2ad9ceb42b54049a71c20 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:25:23 +0100 Subject: [PATCH 056/116] Prevents Flags for being displayed in PropertyHolderTables --- src/main/java/pulse/ui/components/PropertyHolderTable.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index cfda35da..6dba661b 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -92,7 +92,9 @@ private Object[][] dataArray(PropertyHolder p) { return null; List dataList = new ArrayList<>(); - var data = p.data().stream().map(property -> new Object[] { property.getDescriptor(true), property }) + //ignore flags + var data = p.data().stream().filter(property -> !(property instanceof Flag)) + .map(property -> new Object[] { property.getDescriptor(true), property }) .collect(Collectors.toList()); dataList.addAll(data); @@ -152,8 +154,7 @@ public TableCellEditor getCellEditor(int row, int column) { new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); if (value instanceof InstanceDescriptor) { - var inst = new InstanceCellEditor((InstanceDescriptor) value); - return inst; + return new InstanceCellEditor((InstanceDescriptor) value); } if (value instanceof DiscreteSelector) { From f246935e0a0be7f7cc043e010765e3548ac869f3 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:26:33 +0100 Subject: [PATCH 057/116] Changed PopupMenuListener to PopupMenuAdapter --- .../controllers/InstanceCellEditor.java | 68 +++++++++---------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 9b6397a3..988a7094 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -1,5 +1,6 @@ package pulse.ui.components.controllers; +import com.alee.utils.swing.PopupMenuAdapter; import java.awt.Component; import java.awt.event.ItemEvent; @@ -14,49 +15,42 @@ @SuppressWarnings("serial") public class InstanceCellEditor extends DefaultCellEditor { - private InstanceDescriptor descriptor; - private JComboBox combobox; + private InstanceDescriptor descriptor; + private JComboBox combobox; - public InstanceCellEditor(InstanceDescriptor value) { - super(new JComboBox(((InstanceDescriptor) value).getAllDescriptors().toArray())); - this.descriptor = value; - } + public InstanceCellEditor(InstanceDescriptor value) { + super(new JComboBox(((InstanceDescriptor) value).getAllDescriptors().toArray())); + this.descriptor = value; + } - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - combobox = new JComboBox<>(((InstanceDescriptor) value).getAllDescriptors().toArray()); - combobox.setSelectedItem(descriptor.getValue()); + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + combobox = new JComboBox<>(((InstanceDescriptor) value).getAllDescriptors().toArray()); + combobox.setSelectedItem(descriptor.getValue()); - combobox.addItemListener(e -> { - if (e.getStateChange() == ItemEvent.SELECTED) - descriptor.attemptUpdate(e.getItem()); - }); + combobox.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + descriptor.attemptUpdate(e.getItem()); + } + }); - combobox.addPopupMenuListener(new PopupMenuListener() { + combobox.addPopupMenuListener(new PopupMenuAdapter() { - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - // - } + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + fireEditingCanceled(); + } - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { - fireEditingCanceled(); - } + } + ); - @Override - public void popupMenuCanceled(PopupMenuEvent e) { - // - } - }); + return combobox; + } - return combobox; - } + @Override + public Object getCellEditorValue() { + descriptor.setSelectedDescriptor((String) combobox.getSelectedItem()); + return descriptor; + } - @Override - public Object getCellEditorValue() { - descriptor.setSelectedDescriptor((String) combobox.getSelectedItem()); - return descriptor; - } - -} \ No newline at end of file +} From d073b7d1f7517eb74aa83342450048e37bf26e25 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:31:48 +0100 Subject: [PATCH 058/116] UI Changes: - Removed wrong foreground in KeywordListRenderer - Added ActiveFlagsListModel (previously ResultListModel) -> can now be used for ResultFormatDialog and SearchOptionsFrame - The list of checkboxes is now removed from SearchOptionsFrame, replaced by a DoubleListPanel --- .../controllers/KeywordListRenderer.java | 35 ++--- .../models/ActiveFlagsListModel.java | 92 ++++++++++++ .../components/models/ParameterListModel.java | 18 ++- .../ui/components/panels/DoubleListPanel.java | 136 ++++++++++++++++++ .../pulse/ui/frames/SearchOptionsFrame.java | 104 ++++++++++++-- .../ui/frames/dialogs/ResultChangeDialog.java | 102 +++---------- 6 files changed, 364 insertions(+), 123 deletions(-) create mode 100644 src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java create mode 100644 src/main/java/pulse/ui/components/panels/DoubleListPanel.java diff --git a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java b/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java index 1ab97360..8e8232e1 100644 --- a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java @@ -1,7 +1,5 @@ package pulse.ui.components.controllers; -import static java.awt.Color.black; -import static java.awt.Font.BOLD; import static pulse.properties.NumericProperties.def; import java.awt.Component; @@ -13,27 +11,24 @@ public class KeywordListRenderer extends DefaultListCellRenderer { - /** - * - */ - private static final long serialVersionUID = 1L; + /** + * + */ + private static final long serialVersionUID = 1L; - public KeywordListRenderer() { - super(); - } + public KeywordListRenderer() { + super(); + } - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { - var renderer = super.getListCellRendererComponent(list, - (def((NumericPropertyKeyword) value).getDescriptor(true)), index, cellHasFocus, cellHasFocus); + var renderer = super.getListCellRendererComponent(list, + (def((NumericPropertyKeyword) value).getDescriptor(true)), index, cellHasFocus, cellHasFocus); - renderer.setForeground(black); - if (isSelected) - renderer.setFont(renderer.getFont().deriveFont(BOLD)); - return renderer; + return renderer; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java b/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java new file mode 100644 index 00000000..215d75b2 --- /dev/null +++ b/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java @@ -0,0 +1,92 @@ +package pulse.ui.components.models; + +import static pulse.tasks.processing.ResultFormat.getMinimalArray; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.DefaultListModel; + +import pulse.properties.NumericPropertyKeyword; + +public class ActiveFlagsListModel extends DefaultListModel { + + /** + * + */ + private static final long serialVersionUID = 1L; + private List elements = new ArrayList(); + private final List referenceList; + private NumericPropertyKeyword[] mandatorySelection; + + public ActiveFlagsListModel(List keys, NumericPropertyKeyword[] mandatorySelection) { + super(); + this.mandatorySelection = mandatorySelection; + referenceList = keys; + update(); + } + + public void update() { + update(referenceList); + } + + public void update(List keys) { + elements.clear(); + elements.addAll(keys); + } + + @Override + public int getSize() { + return elements.size(); + } + + @Override + public NumericPropertyKeyword getElementAt(int i) { + return elements.get(i); + } + + @Override + public void addElement(NumericPropertyKeyword key) { + elements.add(key); + var size = this.getSize(); + this.fireIntervalAdded(key, size, size); + } + + @Override + public boolean removeElement(Object obj) { + if (!(obj instanceof NumericPropertyKeyword)) { + return false; + } + + var key = (NumericPropertyKeyword) obj; + + if (!elements.contains(key)) { + return false; + } + + for (var keyMin : mandatorySelection) { + if (key == keyMin) { + return false; + } + } + var index = elements.indexOf(key); + elements.remove(key); + this.fireIntervalRemoved(key, index, index); + return true; + } + + @Override + public boolean contains(Object obj) { + if (!(obj instanceof NumericPropertyKeyword)) { + return false; + } + + var key = (NumericPropertyKeyword) obj; + return elements.contains(key); + } + + public List getData() { + return elements; + } + +} diff --git a/src/main/java/pulse/ui/components/models/ParameterListModel.java b/src/main/java/pulse/ui/components/models/ParameterListModel.java index e4e169fe..0a353b1f 100644 --- a/src/main/java/pulse/ui/components/models/ParameterListModel.java +++ b/src/main/java/pulse/ui/components/models/ParameterListModel.java @@ -22,21 +22,25 @@ public class ParameterListModel extends AbstractListModel elements = new ArrayList(); - - public ParameterListModel() { + private boolean extendedList; + + public ParameterListModel(boolean extendedList) { super(); + this.extendedList = extendedList; update(); } - + public void update() { elements.clear(); var list = new ArrayList(); listAvailableProperties(list); list.stream().forEach(property -> elements.add(((Flag) property).getType())); - elements.add(OPTIMISER_STATISTIC); - elements.add(TEST_STATISTIC); - elements.add(IDENTIFIER); - elements.addAll(InterpolationDataset.derivableProperties()); + if(extendedList) { + elements.add(OPTIMISER_STATISTIC); + elements.add(TEST_STATISTIC); + elements.add(IDENTIFIER); + elements.addAll(InterpolationDataset.derivableProperties()); + } } @Override diff --git a/src/main/java/pulse/ui/components/panels/DoubleListPanel.java b/src/main/java/pulse/ui/components/panels/DoubleListPanel.java new file mode 100644 index 00000000..c8fcc653 --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/DoubleListPanel.java @@ -0,0 +1,136 @@ +/* + * Copyright 2021 kotik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.ui.components.panels; + +import java.awt.GridBagConstraints; +import static java.awt.GridBagConstraints.BOTH; +import static javax.swing.BorderFactory.createTitledBorder; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.SwingConstants; +import javax.swing.JPanel; +import pulse.properties.NumericProperties; +import pulse.properties.NumericPropertyKeyword; + +public class DoubleListPanel extends JPanel { + + private javax.swing.JButton moveLeftBtn; + private javax.swing.JButton moveRightBtn; + + public DoubleListPanel(JList leftList, String titleLeft, JList rightList, String titleRight) { + + super(); + initComponents(leftList, titleLeft, rightList, titleRight); + + moveRightBtn.addActionListener(e -> { + + var key = leftList.getSelectedValue(); + var model = (DefaultListModel) rightList.getModel(); + + if (key != null) { + if (!model.contains(key)) { + model.addElement((NumericPropertyKeyword) key); + var excluded = NumericProperties.def( + (NumericPropertyKeyword) key) + .getExcludeKeywords(); + + for (var aKey : excluded) { + if (model.contains(aKey)) { + model.removeElement(aKey); + } + } + + } + } + + }); + + moveLeftBtn.addActionListener(e -> { + + var key = rightList.getSelectedValue(); + var model = (DefaultListModel) rightList.getModel(); + + if (key != null) { + model.removeElement(key); + } + + }); + + } + + public void initComponents(JList leftList, String titleLeft, JList rightList, String titleRight) { + var leftScroller = new javax.swing.JScrollPane(); + var rightScroller = new javax.swing.JScrollPane(); + var moveToolbar = new javax.swing.JToolBar(); + moveRightBtn = new javax.swing.JButton(); + moveLeftBtn = new javax.swing.JButton(); + + setPreferredSize(new java.awt.Dimension(650, 400)); + setLayout(new java.awt.GridBagLayout()); + + var borderLeft = createTitledBorder(titleLeft); + leftScroller.setBorder(borderLeft); + borderLeft.setTitleColor(java.awt.Color.WHITE); + + leftList.setFixedCellHeight(50); + + leftScroller.setViewportView(leftList); + + var gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(8, 5, 8, 5); + add(leftScroller, gridBagConstraints); + + var borderRight = createTitledBorder(titleRight); + rightScroller.setBorder(borderRight); + borderRight.setTitleColor(java.awt.Color.WHITE); + + rightList.setFixedCellHeight(50); + rightScroller.setViewportView(rightList); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 5, 9, 5); + add(rightScroller, gridBagConstraints); + + moveToolbar.setFloatable(false); + moveToolbar.setOrientation(SwingConstants.HORIZONTAL); + moveToolbar.setRollover(true); + + moveRightBtn.setText("\u25BA"); + moveRightBtn.setFocusable(false); + moveRightBtn.setHorizontalTextPosition(SwingConstants.CENTER); + moveRightBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + + moveLeftBtn.setText("\u25C4"); + moveLeftBtn.setFocusable(false); + moveLeftBtn.setHorizontalTextPosition(SwingConstants.CENTER); + moveLeftBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + + moveToolbar.add(moveLeftBtn); + moveToolbar.add(moveRightBtn); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + add(moveToolbar, gridBagConstraints); + } + +} diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index b491c0bf..30fbf26e 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -9,6 +9,8 @@ import static pulse.ui.Messages.getString; import static pulse.util.Reflexive.instancesOf; +import java.util.stream.Collectors; + import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -20,22 +22,38 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.border.EmptyBorder; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionEvent; +import pulse.properties.Flag; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import pulse.search.direction.ActiveFlags; +import pulse.search.direction.LMOptimiser; import pulse.search.direction.PathOptimiser; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; +import pulse.ui.components.controllers.KeywordListRenderer; import pulse.ui.components.controllers.SearchListRenderer; +import pulse.ui.components.models.ParameterListModel; +import pulse.ui.components.models.ActiveFlagsListModel; +import pulse.ui.components.panels.DoubleListPanel; @SuppressWarnings("serial") public class SearchOptionsFrame extends JInternalFrame { private PropertyHolderTable pathTable; + private JList leftList; + private JList rightList; private PathSolversList pathList; private final static Font font = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); private final static List pathSolvers = instancesOf(PathOptimiser.class); - + + private NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{DIFFUSIVITY, MAXTEMP}; + /** * Create the frame. */ @@ -58,7 +76,7 @@ public SearchOptionsFrame() { pathListScroller.setBorder(createTitledBorder("Select an Optimiser")); pathTable = new PropertyHolderTable(null); - + getContentPane().setLayout(new GridBagLayout()); var gbc = new GridBagConstraints(); @@ -67,25 +85,91 @@ public SearchOptionsFrame() { gbc.gridy = 0; gbc.gridx = 0; gbc.weightx = 1.0; - gbc.weighty = 0.2; - + gbc.weighty = 0.3; + + leftList = new javax.swing.JList(); + leftList.setModel(new ParameterListModel(false)); + leftList.setCellRenderer(new KeywordListRenderer()); + + rightList = new javax.swing.JList(); + rightList.setCellRenderer(new KeywordListRenderer()); + + var mainContainer = new DoubleListPanel(leftList, "All Parameters", rightList, "Optimised Parameters"); + getContentPane().add(pathListScroller, gbc); gbc.gridy = 1; - gbc.weighty = 0.6; + gbc.weighty = 0.45; + + getContentPane().add(mainContainer, gbc); + gbc.gridy = 2; + gbc.weighty = 0.25; + var tableScroller = new JScrollPane(pathTable); - tableScroller.setBorder(createTitledBorder("Select search variables and settings")); + tableScroller.setBorder( + createTitledBorder("Select search variables and settings")); getContentPane().add(tableScroller, gbc); } public void update() { - var selected = getInstance(); - if (selected == null) - TaskManager.getManagerInstance().selectFirstTask(); - + var selected = PathOptimiser.getInstance(); + /* + Select Levenberg-Marquardt as default optimiser + */ + if (selected == null) + pathList.setSelectedValue(LMOptimiser.getInstance(), closable); + pathList.setSelectedIndex(pathSolvers.indexOf(selected)); + ((ParameterListModel)leftList.getModel()).update(); + + var rightListModel = rightList.getModel(); + var activeTask = TaskManager.getManagerInstance().getSelectedTask(); + + //model for the flags list already created + if(rightListModel instanceof ActiveFlagsListModel) { + var searchKeys = ActiveFlags.activeParameters(activeTask); + ((ActiveFlagsListModel)rightListModel).update(searchKeys); + } + //Create a new model for the flags list + else { + if(activeTask != null + && activeTask.getCurrentCalculation() != null + && activeTask.getCurrentCalculation().getProblem() != null) { + var searchKeys = ActiveFlags.activeParameters(activeTask); + rightList.setModel(new ActiveFlagsListModel(searchKeys, mandatorySelection)); + + /* + Add listener to this + */ + rightList.getModel().addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent arg0) { + updateFlag(arg0, true); + } + + @Override + public void intervalRemoved(ListDataEvent arg0) { + updateFlag(arg0, false); + } + + private void updateFlag(ListDataEvent arg0, boolean value) { + var source = (NumericPropertyKeyword)arg0.getSource(); + var flag = new Flag(source); + flag.setValue(value); + PathOptimiser.getInstance().update(flag); + + } + + @Override + public void contentsChanged(ListDataEvent arg0) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }); + + } + } pathTable.updateTable(); } diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index 191623fd..a321983c 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -13,9 +13,11 @@ import javax.swing.SwingConstants; import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.processing.ResultFormat; import pulse.ui.components.controllers.KeywordListRenderer; import pulse.ui.components.models.ParameterListModel; -import pulse.ui.components.models.ResultListModel; +import pulse.ui.components.models.ActiveFlagsListModel; +import pulse.ui.components.panels.DoubleListPanel; public class ResultChangeDialog extends JDialog { /** @@ -32,51 +34,21 @@ public ResultChangeDialog() { setSize(WIDTH, HEIGHT); initComponents(); - - var model = (ResultListModel) rightList.getModel(); - - moveRightBtn.addActionListener(e -> { - - var key = leftList.getSelectedValue(); - - if (key != null) - if (!model.contains(key)) - model.add(key); - - }); - - moveLeftBtn.addActionListener(e -> { - - var key = rightList.getSelectedValue(); - - if (key != null) - model.remove(key); - - }); - + var model = (ActiveFlagsListModel)rightList.getModel(); commitBtn.addActionListener(e -> generateFormat(model.getData())); cancelBtn.addActionListener(e -> this.setVisible(false)); - } @Override public void setVisible(boolean value) { super.setVisible(value); - ((ResultListModel) rightList.getModel()).update(); + ((ActiveFlagsListModel) rightList.getModel()).update(); ((ParameterListModel) leftList.getModel()).update(); } private void initComponents() { java.awt.GridBagConstraints gridBagConstraints; - MainContainer = new javax.swing.JPanel(); - leftScroller = new javax.swing.JScrollPane(); - leftList = new javax.swing.JList<>(); - rightScroller = new javax.swing.JScrollPane(); - rightList = new javax.swing.JList<>(); - moveToolbar = new javax.swing.JToolBar(); - moveRightBtn = new javax.swing.JButton(); - moveLeftBtn = new javax.swing.JButton(); MainToolbar = new javax.swing.JToolBar(); filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0)); @@ -89,56 +61,19 @@ private void initComponents() { setDefaultCloseOperation(HIDE_ON_CLOSE); - MainContainer.setPreferredSize(new java.awt.Dimension(650, 400)); - MainContainer.setLayout(new java.awt.GridBagLayout()); - - leftScroller.setBorder(createTitledBorder("Available properties")); - leftList.setModel(new ParameterListModel()); + leftList = new javax.swing.JList<>(); + leftList.setModel(new ParameterListModel(true)); leftList.setCellRenderer(new KeywordListRenderer()); - leftList.setFixedCellHeight(50); - leftScroller.setViewportView(leftList); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.fill = BOTH; - gridBagConstraints.weightx = 0.5; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(8, 5, 8, 5); - MainContainer.add(leftScroller, gridBagConstraints); - - rightScroller.setBorder(createTitledBorder("Printed output")); - - rightList.setModel(new ResultListModel()); + + rightList = new javax.swing.JList<>(); + rightList.setModel(new ActiveFlagsListModel( + ResultFormat.getInstance().getKeywords(), + ResultFormat.getMinimalArray())); rightList.setCellRenderer(new KeywordListRenderer()); - rightList.setFixedCellHeight(50); - rightScroller.setViewportView(rightList); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 2; - gridBagConstraints.fill = GridBagConstraints.BOTH; - gridBagConstraints.weightx = 0.5; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(9, 5, 9, 5); - MainContainer.add(rightScroller, gridBagConstraints); - - moveToolbar.setFloatable(false); - moveToolbar.setOrientation(VERTICAL); - moveToolbar.setRollover(true); - - moveRightBtn.setText(">>"); - moveRightBtn.setFocusable(false); - moveRightBtn.setHorizontalTextPosition(SwingConstants.CENTER); - moveRightBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - moveToolbar.add(moveRightBtn); - - moveLeftBtn.setText("<<"); - moveLeftBtn.setFocusable(false); - moveLeftBtn.setHorizontalTextPosition(SwingConstants.CENTER); - moveLeftBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - moveToolbar.add(moveLeftBtn); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 1; - MainContainer.add(moveToolbar, gridBagConstraints); + + MainContainer = new DoubleListPanel + (leftList, "All Parameters", + rightList, "Output"); getContentPane().add(MainContainer, BorderLayout.CENTER); @@ -172,12 +107,7 @@ private void initComponents() { private javax.swing.Box.Filler filler1; private javax.swing.Box.Filler filler2; private javax.swing.Box.Filler filler3; - private javax.swing.JButton moveRightBtn; - private javax.swing.JButton moveLeftBtn; - private javax.swing.JToolBar moveToolbar; private javax.swing.JList leftList; - private javax.swing.JScrollPane leftScroller; private javax.swing.JList rightList; - private javax.swing.JScrollPane rightScroller; } \ No newline at end of file From 144627df76810a2148cc860900c79e5679d8fcfc Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:34:12 +0100 Subject: [PATCH 059/116] Changed wrong display criterion for numericData() and fixed formatting --- src/main/java/pulse/util/PropertyHolder.java | 523 +++++++++---------- 1 file changed, 260 insertions(+), 263 deletions(-) diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index efe780e2..34410ad3 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -15,270 +15,267 @@ * properties of the {@code Accessible}. * */ - public abstract class PropertyHolder extends Accessible { - private List parameters = listedTypes(); - private List listeners; - private String prefix; - - /** - *

- * By default, this will search the children of this {@code PropertyHolder} to - * collect the types of their listed parameters recursively. Note this method is - * used only to retrieve the type and not the data! - *

- * - * @return a list of {@code Property} instances, which have been explicitly - * marked as a listed parameter for this {@code PropertyHolder}. - */ - - public List listedTypes() { - - List properties = new ArrayList<>(); - - for (var accessible : accessibleChildren()) { - if (accessible instanceof PropertyHolder) { - properties.addAll(((PropertyHolder) accessible).listedTypes()); - } - } - - return properties; - } - - public PropertyHolder() { - this.listeners = new ArrayList<>(); - } - - /** - * Checks whether {@code p} belongs to the list of parameters for this - * {@code PropertyHolder}, i.e. if the associated - * {@code NumericPropertyKeyword}s match. - * - * @param p the {@code NumericProperty} of a certain type. - * @return {@code true} if {@code p} is listed. - * @see listedParameters() - */ - - private boolean isListedNumericType(NumericProperty p) { - return isListedNumericType(p.getType()); - } - - public boolean isListedNumericType(NumericPropertyKeyword p) { - if (p == null) - return false; - - return parameters.stream().filter(pr -> pr instanceof NumericProperty) - .anyMatch(param -> ((NumericProperty) param).getType() == p); - - } - - /** - * Checks whether {@code p} belongs to the list of parameters for this - * {@code PropertyHolder}, i.e. if the properties are of the same class. - * - * @param p the {@code Property}. - * @return {@code true} if {@code p} is listed. - * @see listedParameters() - */ - - private boolean isListedGenericType(Property p) { - if (p == null) - return false; - - if (parameters.contains(null)) - parameters = listedTypes(); - - return parameters.stream().anyMatch(param -> param.getClass().equals(p.getClass())); - - } - - /** - * Checks whether {@code p}, which is either a generic or a numeric property, is - * listed as as parameter for this {@code PropertyHolder}. - * - * @param p the {@code Property} - * @return {@code true} if {@code p} is listed, {@code false} otherwise. - */ - - public boolean isListedParameter(Property p) { - return p instanceof NumericProperty ? isListedNumericType((NumericProperty) p) : isListedGenericType(p); - } - - /** - * Lists all data contained in this {@code PropertyHolder}. The data objects - * must satisfy the following conditions: (a) they must be explicitly listed; - * (b) the corresponding property must not be auto-adjustable if the details - * need to remain hidden. - * - * @return a list of data, which combines generic and numeric properties. - */ - - public List data() { - var numeric = numericData(); - var all = genericProperties().stream().filter(p -> isListedGenericType(p)).collect(toList()); - - all.addAll(numeric); - return all; - } - - /** - * Lists all numeric data contained in this {@code PropertyHolder}. The data - * objects must satisfy the following conditions: (a) they must be explicitly - * listed; (b) the corresponding property must not be auto-adjustable if the - * details need to remain hidden. - * - * @return a list of {@code Property} data. - * @see areDetailsHidden() - * @see pulse.properties.NumericProperty.isAutoAdjustable() - * @see isListedNumericType(NumericProperty) - */ - - public List numericData() { - return numericProperties().stream() - .filter(p -> (isListedNumericType(p) && (areDetailsHidden() ? !p.isAutoAdjustable() : true))) - .collect(toList()); - } - - /** - *

- * Attempts to update an {@code updatedProperty} similar to one found in this - * {@code PropertyHolder}. The call originator is declared to be the - * {@code sourceComponent}. If the originator is not the parent of this - * {@code UpwardsNavigable}, this object will tell their parent about this - * behavior. The update is done by calling the superclass method - * {@code update(Property} -- if and only if a property similar to - * {@code updatedProperty} exists and its value is not equal to the - * {@code updatedProperty}. When the update happens, this will pass the - * corresponding {@code PropertyEvent} to the available listeners. - *

- * - * @param sourceComponent the originator of the change. - * @param updatedProperty the updated property that will be assigned to this - * {@code PropertyHolder}. - * @see pulse.util.Accessible.update(Property) - */ - - public boolean updateProperty(Object sourceComponent, Property updatedProperty) { - var existing = property(updatedProperty); - - if (existing == null) { - - return accessibleChildren().stream().filter(a -> a instanceof PropertyHolder) - .anyMatch(c -> ((PropertyHolder) c).updateProperty(sourceComponent, updatedProperty)); - } - - if (existing.equals(updatedProperty)) - return false; - - update(updatedProperty); - firePropertyChanged(sourceComponent, updatedProperty); - - return true; - } - - public void firePropertyChanged(Object source, Property property) { - var event = new PropertyEvent(source, this, property); - listeners.forEach(l -> l.onPropertyChanged(event)); - - /* + private List parameters = listedTypes(); + private List listeners; + private String prefix; + + /** + *

+ * By default, this will search the children of this {@code PropertyHolder} + * to collect the types of their listed parameters recursively. Note this + * method is used only to retrieve the type and not the data! + *

+ * + * @return a list of {@code Property} instances, which have been explicitly + * marked as a listed parameter for this {@code PropertyHolder}. + */ + public List listedTypes() { + + List properties = new ArrayList<>(); + + for (var accessible : accessibleChildren()) { + if (accessible instanceof PropertyHolder) { + properties.addAll(((PropertyHolder) accessible).listedTypes()); + } + } + + return properties; + } + + public PropertyHolder() { + this.listeners = new ArrayList<>(); + } + + /** + * Checks whether {@code p} belongs to the list of parameters for this + * {@code PropertyHolder}, i.e. if the associated + * {@code NumericPropertyKeyword}s match. + * + * @param p the {@code NumericProperty} of a certain type. + * @return {@code true} if {@code p} is listed. + * @see listedParameters() + */ + private boolean isListedNumericType(NumericProperty p) { + return isListedNumericType(p.getType()); + } + + public boolean isListedNumericType(NumericPropertyKeyword p) { + if (p == null) { + return false; + } + + return parameters.stream().filter(pr -> pr instanceof NumericProperty) + .anyMatch(param -> ((NumericProperty) param).getType() == p); + + } + + /** + * Checks whether {@code p} belongs to the list of parameters for this + * {@code PropertyHolder}, i.e. if the properties are of the same class. + * + * @param p the {@code Property}. + * @return {@code true} if {@code p} is listed. + * @see listedParameters() + */ + private boolean isListedGenericType(Property p) { + if (p == null) { + return false; + } + + if (parameters.contains(null)) { + parameters = listedTypes(); + } + + return parameters.stream().anyMatch(param -> param.getClass().equals(p.getClass())); + + } + + /** + * Checks whether {@code p}, which is either a generic or a numeric + * property, is listed as as parameter for this {@code PropertyHolder}. + * + * @param p the {@code Property} + * @return {@code true} if {@code p} is listed, {@code false} otherwise. + */ + public boolean isListedParameter(Property p) { + return p instanceof NumericProperty ? isListedNumericType((NumericProperty) p) : isListedGenericType(p); + } + + /** + * Lists all data contained in this {@code PropertyHolder}. The data objects + * must satisfy the following conditions: (a) they must be explicitly + * listed; (b) the corresponding property must not be auto-adjustable if the + * details need to remain hidden. + * + * @return a list of data, which combines generic and numeric properties. + */ + public List data() { + var numeric = numericData(); + var all = genericProperties().stream().filter(p -> isListedGenericType(p)).collect(toList()); + + all.addAll(numeric); + return all; + } + + /** + * Lists all numeric data contained in this {@code PropertyHolder}. The data + * objects must satisfy the following conditions: (a) they must be + * explicitly listed; (b) the corresponding property must not be + * auto-adjustable if the details need to remain hidden. + * + * @return a list of {@code Property} data. + * @see areDetailsHidden() + * @see pulse.properties.NumericProperty.isAutoAdjustable() + * @see isListedNumericType(NumericProperty) + */ + public List numericData() { + return numericProperties().stream() + .filter(p -> (isListedNumericType(p) + && (areDetailsHidden() ? p.isVisibleByDefault() : true))) + .collect(toList()); + } + + /** + *

+ * Attempts to update an {@code updatedProperty} similar to one found in + * this {@code PropertyHolder}. The call originator is declared to be the + * {@code sourceComponent}. If the originator is not the parent of this + * {@code UpwardsNavigable}, this object will tell their parent about this + * behaviour. The update is done by calling the superclass method + * {@code update(Property} -- if and only if a property similar to + * {@code updatedProperty} exists and its value is not equal to the + * {@code updatedProperty}. When the update happens, this will pass the + * corresponding {@code PropertyEvent} to the available listeners. + *

+ * + * @param sourceComponent the originator of the change. + * @param updatedProperty the updated property that will be assigned to this + * {@code PropertyHolder}. + * @see pulse.util.Accessible.update(Property) + */ + public boolean updateProperty(Object sourceComponent, Property updatedProperty) { + var existing = property(updatedProperty); + + if (existing == null) { + + return accessibleChildren().stream().filter(a -> a instanceof PropertyHolder) + .anyMatch(c -> ((PropertyHolder) c).updateProperty(sourceComponent, updatedProperty)); + } + + if (existing.equals(updatedProperty)) { + return false; + } + + update(updatedProperty); + firePropertyChanged(sourceComponent, updatedProperty); + + return true; + } + + public void firePropertyChanged(Object source, Property property) { + var event = new PropertyEvent(source, this, property); + listeners.forEach(l -> l.onPropertyChanged(event)); + + /* * If the changes are triggered by an external GUI component (such as * PropertyHolderTable), inform parents about this - */ - - if (source != getParent()) - tellParent(event); - - } - - /** - * This method will update this {@code PropertyHolder} with all properties that - * are contained in a different {@code propertyHolder}, if they also are present - * in the former. - * - * @param sourceComponent the source of the change - * @param propertyHolder another {@code PropertyHolder} - * @see updateProperty(Object, Property) - */ - - public void updateProperties(Object sourceComponent, PropertyHolder propertyHolder) { - propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); - } - - public void removeHeatingCurveListeners() { - this.listeners.clear(); - } - - public void addListener(PropertyHolderListener l) { - this.listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - /** - * By default, this is set to {@code false}. If the overriding subclass sets - * this to {@code true}, only those {@code NumericPropert}ies that have the - * {@code autoAdjustable} flag set {@code false} will be shown. - * - * @return {@code true} if the auto-adjustable numeric properties need to stay - * hidden, {@code false} otherwise. - * @see pulse.properties.NumericProperty.isAutoAdjustable() - */ - - public boolean areDetailsHidden() { - return false; - } - - public void parameterListChanged() { - this.parameters = listedTypes(); - } - - /** - * Should {@code Accessible}s that belong to this {@code PropertyHolder} be - * ignored when this {@code PropertyHolder} is displayed in a table? - * - * @return {@code false} by default - * @see pulse.ui.components.PropertyHolderTable - */ - - public boolean ignoreSiblings() { - return false; - } - - @Override - public String describe() { - if (prefix == null) - return super.describe(); - - var id = identify(); - - if (id == null) - return super.describe(); - - if (!prefix.trim().isEmpty()) - return prefix + "_" + id.getValue(); - else - return describe() + "_" + id.getValue(); - } - - public String getPrefix() { - return prefix; - } - - /** - * If not null, will return the prefix, otherwise calls the superclass method. - * - * @return the descriptor - */ - - public String getDescriptor() { - return prefix != null ? getPrefix() : super.getDescriptor(); - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - -} \ No newline at end of file + */ + if (source != getParent()) { + tellParent(event); + } + + } + + /** + * This method will update this {@code PropertyHolder} with all properties + * that are contained in a different {@code propertyHolder}, if they also + * are present in the former. + * + * @param sourceComponent the source of the change + * @param propertyHolder another {@code PropertyHolder} + * @see updateProperty(Object, Property) + */ + public void updateProperties(Object sourceComponent, PropertyHolder propertyHolder) { + propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); + } + + public void removeHeatingCurveListeners() { + this.listeners.clear(); + } + + public void addListener(PropertyHolderListener l) { + this.listeners.add(l); + } + + public List getListeners() { + return listeners; + } + + /** + * By default, this is set to {@code false}. If the overriding subclass sets + * this to {@code true}, only those {@code NumericPropert}ies that have the + * {@code autoAdjustable} flag set {@code false} will be shown. + * + * @return {@code true} if the auto-adjustable numeric properties need to + * stay hidden, {@code false} otherwise. + * @see pulse.properties.NumericProperty.isAutoAdjustable() + */ + public boolean areDetailsHidden() { + return false; + } + + public void parameterListChanged() { + this.parameters = listedTypes(); + } + + /** + * Should {@code Accessible}s that belong to this {@code PropertyHolder} be + * ignored when this {@code PropertyHolder} is displayed in a table? + * + * @return {@code false} by default + * @see pulse.ui.components.PropertyHolderTable + */ + public boolean ignoreSiblings() { + return false; + } + + @Override + public String describe() { + if (prefix == null) { + return super.describe(); + } + + var id = identify(); + + if (id == null) { + return super.describe(); + } + + if (!prefix.trim().isEmpty()) { + return prefix + "_" + id.getValue(); + } else { + return describe() + "_" + id.getValue(); + } + } + + public String getPrefix() { + return prefix; + } + + /** + * If not null, will return the prefix, otherwise calls the superclass + * method. + * + * @return the descriptor + */ + public String getDescriptor() { + return prefix != null ? getPrefix() : super.getDescriptor(); + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + +} From 1a5dd9a6cb98155799c86832274b31353a1fb19b Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:35:55 +0100 Subject: [PATCH 060/116] Added support for non-string objects in attemptUpdate() --- .../java/pulse/util/InstanceDescriptor.java | 204 +++++++++--------- 1 file changed, 103 insertions(+), 101 deletions(-) diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 79652b4f..54ee4dbf 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -13,105 +13,107 @@ public class InstanceDescriptor implements Property { - private String selectedDescriptor = ""; - private Set allDescriptors; - private String generalDescriptor; - private int hashCode; + private String selectedDescriptor = ""; + private Set allDescriptors; + private String generalDescriptor; + private int hashCode; + + private List listeners; + + private static Map, Set> nameMap = new HashMap<>(); + + public InstanceDescriptor(String generalDescriptor, Class c, Object... arguments) { + if (nameMap.get(c) == null) { + nameMap.put(c, allSubclassesNames(c)); + } + this.hashCode = c.hashCode(); + allDescriptors = nameMap.get(c); + selectedDescriptor = allDescriptors.iterator().next(); + this.generalDescriptor = generalDescriptor; + listeners = new ArrayList(); + } + + public InstanceDescriptor(Class c, Object... arguments) { + this(c.getSimpleName(), c, arguments); + } + + public K newInstance(Class c, Object... arguments) { + return instancesOf(c, arguments).stream().filter(r -> getValue().equals(r.getClass().getSimpleName())).findAny() + .get(); + } + + @Override + public Object getValue() { + return selectedDescriptor; + } + + @Override + public boolean attemptUpdate(Object object) { + var string = object.toString(); + + if (selectedDescriptor.equals(string) || !allDescriptors.contains(string)) { + return false; + } + + this.selectedDescriptor = string; + listeners.stream().forEach(l -> l.onDescriptorChanged()); + return true; + } + + public void setSelectedDescriptor(String selectedDescriptor) { + attemptUpdate(selectedDescriptor); + } + + @Override + public Object identifier() { + return hashCode; + } + + @Override + public String getDescriptor(boolean addHtmlTags) { + return generalDescriptor; + } + + public Set getAllDescriptors() { + return allDescriptors; + } + + @Override + public String toString() { + return selectedDescriptor; + } + + public void addListener(DescriptorChangeListener l) { + this.listeners.add(l); + } + + public List getListeners() { + return listeners; + } + + @Override + public boolean equals(Object o) { + + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!(o instanceof InstanceDescriptor)) { + return false; + } + + var descriptor = (InstanceDescriptor) o; + + if (!allDescriptors.containsAll(descriptor.allDescriptors)) { + return false; + } + + return selectedDescriptor.equals(descriptor.selectedDescriptor); + + } - private List listeners; - - private static Map, Set> nameMap = new HashMap<>(); - - public InstanceDescriptor(String generalDescriptor, Class c, Object... arguments) { - if (nameMap.get(c) == null) - nameMap.put(c, allSubclassesNames(c)); - this.hashCode = c.hashCode(); - allDescriptors = nameMap.get(c); - selectedDescriptor = allDescriptors.iterator().next(); - this.generalDescriptor = generalDescriptor; - listeners = new ArrayList(); - } - - public InstanceDescriptor(Class c, Object... arguments) { - this(c.getSimpleName(), c, arguments); - } - - public K newInstance(Class c, Object... arguments) { - return instancesOf(c, arguments).stream().filter(r -> getValue().equals(r.getClass().getSimpleName())).findAny() - .get(); - } - - @Override - public Object getValue() { - return selectedDescriptor; - } - - @Override - public boolean attemptUpdate(Object object) { - if (!(object instanceof String)) - return false; - - if (selectedDescriptor.equals(object)) - return false; - - if (!allDescriptors.contains(object)) - return false; - - this.selectedDescriptor = (String) object; - listeners.stream().forEach(l -> l.onDescriptorChanged()); - return true; - } - - public void setSelectedDescriptor(String selectedDescriptor) { - attemptUpdate(selectedDescriptor); - } - - @Override - public Object identifier() { - return hashCode; - } - - @Override - public String getDescriptor(boolean addHtmlTags) { - return generalDescriptor; - } - - public Set getAllDescriptors() { - return allDescriptors; - } - - @Override - public String toString() { - return selectedDescriptor; - } - - public void addListener(DescriptorChangeListener l) { - this.listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - @Override - public boolean equals(Object o) { - - if (o == null) - return false; - - if (o == this) - return true; - - if (!(o instanceof InstanceDescriptor)) - return false; - - var descriptor = (InstanceDescriptor) o; - - if (!allDescriptors.containsAll(descriptor.allDescriptors)) - return false; - - return selectedDescriptor.equals(descriptor.selectedDescriptor); - - } - -} \ No newline at end of file +} From a340eb89ed727d1bed1c2fbca4a5cd9e99cac8f4 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:36:55 +0100 Subject: [PATCH 061/116] Minor formatting changes --- src/main/java/pulse/util/Accessible.java | 476 ++++++++++++----------- 1 file changed, 240 insertions(+), 236 deletions(-) diff --git a/src/main/java/pulse/util/Accessible.java b/src/main/java/pulse/util/Accessible.java index 0ee0047e..b9cdf8c2 100644 --- a/src/main/java/pulse/util/Accessible.java +++ b/src/main/java/pulse/util/Accessible.java @@ -25,248 +25,252 @@ *

* */ - public abstract class Accessible extends Group { - /** - *

- * Searches for a {@code Property} in this {@code Accessible} that looks - * {@code similar} to the argument. Determines whether the {@code similar} is a - * {@code NumericProperty} or a generic property and calls the suitable method - * in this class. - *

- * - * @param similar a generic or a numeric {@code Property} - * @return the matching property of this {@code Accessible} - */ - - public Property property(Property similar) { - if (similar instanceof NumericProperty) - return numericProperty(((NumericProperty) similar).getType()); - else - return genericProperty(similar); - } - - /** - * Tries to access the property getter methods in this {@code Accessible}, which - * should be declared as no-argument methods with a specific return type. - * - * @return This will return a unique {@code Set} containing all - * instances of {@code NumericProperty} belonging to this - * {@code Accessible}. This set will not contain any duplicate elements - * by definition. - * @see pulse.properties.NumericProperty.equal(Object) - */ - - public Set numericProperties() { - Set fields = new TreeSet<>(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { - - if (m.getParameterCount() > 0) - continue; - - if (NumericProperty.class.isAssignableFrom(m.getReturnType())) + /** + *

+ * Searches for a {@code Property} in this {@code Accessible} that looks + * {@code similar} to the argument. Determines whether the {@code similar} + * is a {@code NumericProperty} or a generic property and calls the suitable + * method in this class. + *

+ * + * @param similar a generic or a numeric {@code Property} + * @return the matching property of this {@code Accessible} + */ + public Property property(Property similar) { + if (similar instanceof NumericProperty) { + return numericProperty(((NumericProperty) similar).getType()); + } else { + return genericProperty(similar); + } + } + + /** + * Tries to access the property getter methods in this {@code Accessible}, + * which should be declared as no-argument methods with a specific return + * type. + * + * @return This will return a unique {@code Set} containing + * all instances of {@code NumericProperty} belonging to this + * {@code Accessible}. This set will not contain any duplicate elements by + * definition. + * @see pulse.properties.NumericProperty.equal(Object) + */ + public Set numericProperties() { + Set fields = new TreeSet<>(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + + if (m.getParameterCount() > 0) { + continue; + } + + if (NumericProperty.class.isAssignableFrom(m.getReturnType())) try { - var obj = m.invoke(this); - if (obj != null) - fields.add((NumericProperty) m.invoke(this)); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - err.println("Error invoking method " + m); - e.printStackTrace(); - } - - } - - return fields; - - } - - /** - * Tries to access the property getter methods in this {@code Accessible}, which - * should be declared as no-argument methods with a specific return type. - * - * @return This will return a {@code List} containing all properties - * belonging to this {@code Accessible}, which are not assignable from - * the {@code NumericProperty} class. - */ - - public List genericProperties() { - List fields = new ArrayList<>(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { - - if (m.getParameterCount() > 0) - continue; - - if (Property.class.isAssignableFrom(m.getReturnType()) - && !NumericProperty.class.isAssignableFrom(m.getReturnType())) + var obj = m.invoke(this); + if (obj != null) { + fields.add((NumericProperty) m.invoke(this)); + } + } catch (IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + err.println("Error invoking method " + m); + e.printStackTrace(); + } + + } + + return fields; + + } + + /** + * Tries to access the property getter methods in this {@code Accessible}, + * which should be declared as no-argument methods with a specific return + * type. + * + * @return This will return a {@code List} containing all + * properties belonging to this {@code Accessible}, which are not assignable + * from the {@code NumericProperty} class. + */ + public List genericProperties() { + List fields = new ArrayList<>(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + + if (m.getParameterCount() > 0) { + continue; + } + + if (Property.class.isAssignableFrom(m.getReturnType()) + && !NumericProperty.class.isAssignableFrom(m.getReturnType())) try { - fields.add((Property) m.invoke(this)); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - err.println("Error invoking method " + m); - e.printStackTrace(); - } - - } - /* + fields.add((Property) m.invoke(this)); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + err.println("Error invoking method " + m); + e.printStackTrace(); + } + + } + /* * Get access to the properties of accessibles contained in this accessible - */ + */ // for (var a : accessibleChildren()) { // fields.addAll(a.genericProperties()); // } - - return fields; - - } - - /** - *

- * Recursively searches for a {@code NumericProperty} from the unique set of - * numeric properties in this {@code Accessible} by comparing its - * {@code NumericPropertyKeyword} to {@code type}. This will search for this - * property through the children of this object, through the children of their - * children, etc. - *

- * - * @param type the type of the {@code NumericProperty}. - * @return the respective {@code NumericProperty}, or {@code null} if nothing is - * found. - * @see numericProperties() - */ - - public NumericProperty numericProperty(NumericPropertyKeyword type) { - - var match = numericProperties().stream().filter(p -> p.getType() == type).findFirst(); - - if (match.isPresent()) - return match.get(); - - NumericProperty property = null; - - for (var accessible : accessibleChildren()) { - property = accessible.numericProperty(type); - if (property != null) - break; - } - - return property; - - } - - /** - *

- * Recursively searches for a {@code Property} from the non-unique list of - * generic properties in this {@code Accessible} by comparing its class to - * {@code sameClass.getClass()}. This will search for this property through the - * children of this object, through the children of their children, etc. - *

- * - * @param sameClass the class identifying this {@code Property}. - * @return the respective {@code Property}, or {@code null} if nothing is found. - * @see genericProperties() - */ - - public Property genericProperty(Property sameClass) { - - var match = genericProperties().stream().filter(p -> p.identifier().equals(sameClass.identifier())) - .collect(Collectors.toList()); - - Property result = null; - - switch (match.size()) { - case 0: - - break; - // just one matching element found - case 1: - result = match.get(0); - break; - // several possible matches found; use other criteria - default: - throw new IllegalArgumentException("Too many matches found: " + sameClass + " : " + match.size()); - } - - return result; - - } - - /** - *

- * An abstract method, which must be overriden to gain access over setting the - * values of all relevant (selected by the programmer) {@code NumericPropert}ies - * in subclasses of {@code Accessible}. Typically this involves a {@code switch} - * statement that goes through the different options for the {@code type} and - * invokes different {@code set(...)} methods to update the matching - * {@code NumericProperty} with {@code property}. - *

- * - * @param type the type, which must be equal by definition to - * {@code property.getType()}. - * @param property the property, which contains new information. - */ - - public abstract void set(NumericPropertyKeyword type, NumericProperty property); - - /** - * Runs recursive search for a property in this {@code Accessible} object with - * the same identifier as {@code property} and sets its value to the value of - * the {@code property} parameter.If {@code property} is a - * {@code NumericProperty}, uses its {@code NumericPropertyKeyword} for - * identification. For generic properties, calls {@code attemptUpdate}. - * - * @param property the {@code Property}, which will update a similar property of - * this {@code Accessible}. - * @see Property.attemptUpdate(Property) - */ - - public void update(Property property) { - - if (property instanceof NumericProperty) - update((NumericProperty) property); - else { - var p = genericProperty(property); - - if (p == null) - accessibleChildren().stream().forEach(c -> c.update(property)); - else - p.attemptUpdate(property.getValue()); - } - - } - - /** - * Set a NumericProperty contained in this Accessible or any of its accessible - * childern, using the NumericPropertyKeyword of the argument as identifier and - * its value. - * - * @param p a NumericProperty - * @see Accessible.accessibleChildren() - */ - - public void update(NumericProperty p) { - this.set(p.getType(), p); - for (var a : accessibleChildren()) - a.update(p); - } - - /** - *

- * Selects only those {@code Accessible}s, the parent of which is {@code this}. - * Note that all {@code Accessible}s are required to explicitly adopt children - * by calling the {@code setParent()} method. - *

- * - * @return a {@code List} of children that this {@code Accessible} has adopted. - * @see children - */ - - public List accessibleChildren() { - return children().stream().filter(group -> group instanceof Accessible).map(acGroup -> (Accessible) acGroup) - .collect(toList()); - } - -} \ No newline at end of file + return fields; + + } + + /** + *

+ * Recursively searches for a {@code NumericProperty} from the unique set of + * numeric properties in this {@code Accessible} by comparing its + * {@code NumericPropertyKeyword} to {@code type}. This will search for this + * property through the children of this object, through the children of + * their children, etc. + *

+ * + * @param type the type of the {@code NumericProperty}. + * @return the respective {@code NumericProperty}, or {@code null} if + * nothing is found. + * @see numericProperties() + */ + public NumericProperty numericProperty(NumericPropertyKeyword type) { + + var match = numericProperties().stream().filter(p -> p.getType() == type).findFirst(); + + if (match.isPresent()) { + return match.get(); + } + + NumericProperty property = null; + + for (var accessible : accessibleChildren()) { + property = accessible.numericProperty(type); + if (property != null) { + break; + } + } + + return property; + + } + + /** + *

+ * Recursively searches for a {@code Property} from the non-unique list of + * generic properties in this {@code Accessible} by comparing its class to + * {@code sameClass.getClass()}. This will search for this property through + * the children of this object, through the children of their children, etc. + *

+ * + * @param sameClass the class identifying this {@code Property}. + * @return the respective {@code Property}, or {@code null} if nothing is + * found. + * @see genericProperties() + */ + public Property genericProperty(Property sameClass) { + + var match = genericProperties().stream().filter(p -> p.identifier().equals(sameClass.identifier())) + .collect(Collectors.toList()); + + Property result = null; + + switch (match.size()) { + case 0: + + break; + // just one matching element found + case 1: + result = match.get(0); + break; + // several possible matches found; use other criteria + default: + throw new IllegalArgumentException("Too many matches found: " + sameClass + " : " + match.size()); + } + + return result; + + } + + /** + *

+ * An abstract method, which must be overriden to gain access over setting + * the values of all relevant (selected by the programmer) + * {@code NumericPropert}ies in subclasses of {@code Accessible}. Typically + * this involves a {@code switch} statement that goes through the different + * options for the {@code type} and invokes different {@code set(...)} + * methods to update the matching {@code NumericProperty} with + * {@code property}. + *

+ * + * @param type the type, which must be equal by definition to + * {@code property.getType()}. + * @param property the property, which contains new information. + */ + public abstract void set(NumericPropertyKeyword type, NumericProperty property); + + /** + * Runs recursive search for a property in this {@code Accessible} object + * with the same identifier as {@code property} and sets its value to the + * value of the {@code property} parameter.If {@code property} is a + * {@code NumericProperty}, uses its {@code NumericPropertyKeyword} for + * identification. For generic properties, calls {@code attemptUpdate}. + * + * @param property the {@code Property}, which will update a similar + * property of this {@code Accessible}. + * @see Property.attemptUpdate(Property) + */ + public void update(Property property) { + + if (property instanceof NumericProperty) { + update((NumericProperty) property); + } else { + var p = genericProperty(property); + + if (p == null) { + accessibleChildren().stream().forEach(c -> c.update(property)); + } else { + p.attemptUpdate(property.getValue()); + } + } + + } + + /** + * Set a NumericProperty contained in this Accessible or any of its + * accessible childern, using the NumericPropertyKeyword of the argument as + * identifier and its value. + * + * @param p a NumericProperty + * @see Accessible.accessibleChildren() + */ + public void update(NumericProperty p) { + this.set(p.getType(), p); + for (var a : accessibleChildren()) { + a.update(p); + } + } + + /** + *

+ * Selects only those {@code Accessible}s, the parent of which is + * {@code this}. Note that all {@code Accessible}s are required to + * explicitly adopt children by calling the {@code setParent()} method. + *

+ * + * @return a {@code List} of children that this {@code Accessible} has + * adopted. + * @see children + */ + public List accessibleChildren() { + return children().stream().filter(group -> group instanceof Accessible).map(acGroup -> (Accessible) acGroup) + .collect(toList()); + } + +} From 0bdf7a7e5110e90ffb34f4a5a2f6e8f7bf585ee1 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:39:18 +0100 Subject: [PATCH 062/116] Several general changes: - Added more details to SolverException when being thrown - Removed ResultListModel.java - Intoduced support to new entitites in the XML file --- src/main/java/pulse/input/Range.java | 2 +- .../java/pulse/io/export/XMLConverter.java | 400 +++++++++--------- .../ui/components/models/ResultListModel.java | 69 --- 3 files changed, 210 insertions(+), 261 deletions(-) delete mode 100644 src/main/java/pulse/ui/components/models/ResultListModel.java diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index d4106833..166f9867 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -208,7 +208,7 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { if(!params.validate()) - throw new SolverException("Parameter values not sensible"); + throw new SolverException("Parameter values not sensible: " + params); NumericProperty p = null; diff --git a/src/main/java/pulse/io/export/XMLConverter.java b/src/main/java/pulse/io/export/XMLConverter.java index cd2b3fe3..73b91e43 100644 --- a/src/main/java/pulse/io/export/XMLConverter.java +++ b/src/main/java/pulse/io/export/XMLConverter.java @@ -35,196 +35,214 @@ * information XML file in the resource folder. * */ - public class XMLConverter { - private XMLConverter() { - } - - private static void toXML(NumericProperty np, Document doc, Element rootElement) { - Element property = doc.createElement(np.getClass().getSimpleName()); - rootElement.appendChild(property); - - Attr keyword = doc.createAttribute("keyword"); - keyword.setValue(np.getType().toString()); - property.setAttributeNode(keyword); - - Attr descriptor = doc.createAttribute("descriptor"); - descriptor.setValue(np.getDescriptor(false)); - property.setAttributeNode(descriptor); - - Attr abbreviation = doc.createAttribute("abbreviation"); - abbreviation.setValue(np.getAbbreviation(false)); - property.setAttributeNode(abbreviation); - - Attr value = doc.createAttribute("value"); - value.setValue(np.getValue().toString()); - property.setAttributeNode(value); - - Attr minimum = doc.createAttribute("minimum"); - minimum.setValue(np.getMinimum().toString()); - property.setAttributeNode(minimum); - - Attr maximum = doc.createAttribute("maximum"); - maximum.setValue(np.getMaximum().toString()); - property.setAttributeNode(maximum); - - Attr dim = doc.createAttribute("dimensionfactor"); - dim.setValue(np.getDimensionFactor().toString()); - property.setAttributeNode(dim); - - Attr autoAdj = doc.createAttribute("auto-adjustable"); - autoAdj.setValue(np.isAutoAdjustable() + ""); - property.setAttributeNode(autoAdj); - - Attr primitiveType = doc.createAttribute("primitive-type"); - primitiveType.setValue(np.getValue() instanceof Double ? "double" : "int"); - property.setAttributeNode(primitiveType); - - Attr defSearch = doc.createAttribute("default-search-variable"); - primitiveType.setValue(np.isDefaultSearchVariable() + ""); - property.setAttributeNode(defSearch); - - } - - /** - * Utility method that creates an {@code .xml} file listing all public final - * static instances of {@code NumericProperty} found in the - * {@code NumericProperty} class. - */ - - public static void writeXML() throws ParserConfigurationException, TransformerException { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - Document doc = dBuilder.newDocument(); - - Element rootElement = doc.createElement("NumericProperties"); - doc.appendChild(rootElement); - - List properties = new ArrayList<>(); - - int modifiers; - - /** - * Reads all final static {@code NumericProperty} constants in the - * {@code NumericProperty} class - */ - - for (Field field : NumericProperty.class.getDeclaredFields()) { - - modifiers = field.getModifiers(); - - // filter only public final static NumericProperties - if ((Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) - && field.getType().equals(NumericProperty.class)) { - - NumericProperty value = null; - try { - value = (NumericProperty) field.get(null); - } catch (IllegalArgumentException | IllegalAccessException e) { - System.out.println("Unable to access field: " + field); - e.printStackTrace(); - } - if (value != null) - properties.add(value); - - } - - } - - properties.stream().forEach(p -> XMLConverter.toXML(p, doc, rootElement)); - - // write the content into xml file - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - DOMSource source = new DOMSource(doc); - StreamResult result = new StreamResult(new File(NumericProperty.class.getSimpleName() + ".xml")); - transformer.transform(source, result); - - // Output to console for testing - StreamResult consoleResult = new StreamResult(System.out); - transformer.transform(source, consoleResult); - - } - - /** - * Utility method used to read {@code NumericProperty} constants from - * {@code xml} files. - * - * @param inputStream the input stream used to read data from. - * @return a list of {@code NumericProperty} objects with their attributes - * specified in the {@code xml} file. - */ - - public static List readXML(InputStream inputStream) - throws ParserConfigurationException, SAXException, IOException { - - List properties = new ArrayList<>(); - - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - Document doc = dBuilder.parse(inputStream); - - doc.getDocumentElement().normalize(); - NodeList nList = doc.getElementsByTagName(NumericProperty.class.getSimpleName()); - - for (int temp = 0; temp < nList.getLength(); temp++) { - Node nNode = nList.item(temp); - - if (nNode.getNodeType() == Node.ELEMENT_NODE) { - Element eElement = (Element) nNode; - NumericPropertyKeyword keyword = NumericPropertyKeyword.valueOf(eElement.getAttribute("keyword")); - boolean autoAdjustable = Boolean.valueOf(eElement.getAttribute("auto-adjustable")); - boolean discrete = Boolean.valueOf(eElement.getAttribute("discreet")); - String descriptor = eElement.getAttribute("descriptor"); - String abbreviation = eElement.getAttribute("abbreviation"); - boolean defSearch = Boolean.valueOf(eElement.getAttribute("default-search-variable")); - - Number value, minimum, maximum, dimensionFactor; - - if (eElement.getAttribute("primitive-type").equalsIgnoreCase("double")) { - value = Double.valueOf(eElement.getAttribute("value")); - minimum = Double.valueOf(eElement.getAttribute("minimum")); - maximum = Double.valueOf(eElement.getAttribute("maximum")); - dimensionFactor = Double.valueOf(eElement.getAttribute("dimensionfactor")); - } else { - value = Integer.valueOf(eElement.getAttribute("value")); - minimum = Integer.valueOf(eElement.getAttribute("minimum")); - maximum = Integer.valueOf(eElement.getAttribute("maximum")); - dimensionFactor = Integer.valueOf(eElement.getAttribute("dimensionfactor")); - } - - var np = new NumericProperty(keyword, value, minimum, maximum, dimensionFactor); - np.setDescriptor(descriptor); - np.setAbbreviation(abbreviation); - np.setAutoAdjustable(autoAdjustable); - np.setDiscrete(discrete); - np.setDefaultSearchVariable(defSearch); - properties.add(np); - } - } - - return properties; - - } - - /** - * The default XML file is specific in the 'messages.properties' text file in - * the {@code pulse.ui} package - * - * @return a list of default instances of {@code NumericProperty}. - */ - - public static List readDefaultXML() { - try { - return readXML(NumericProperty.class.getResourceAsStream(Messages.getString("NumericProperty.XMLFile"))); - } catch (ParserConfigurationException | SAXException | IOException e) { - System.err.println("Unable to read list of default numeric properties"); - e.printStackTrace(); - } - return null; - } - -} \ No newline at end of file + private XMLConverter() { + } + + private static void toXML(NumericProperty np, Document doc, Element rootElement) { + Element property = doc.createElement(np.getClass().getSimpleName()); + rootElement.appendChild(property); + + Attr keyword = doc.createAttribute("keyword"); + keyword.setValue(np.getType().toString()); + property.setAttributeNode(keyword); + + Attr descriptor = doc.createAttribute("descriptor"); + descriptor.setValue(np.getDescriptor(false)); + property.setAttributeNode(descriptor); + + Attr abbreviation = doc.createAttribute("abbreviation"); + abbreviation.setValue(np.getAbbreviation(false)); + property.setAttributeNode(abbreviation); + + Attr value = doc.createAttribute("value"); + value.setValue(np.getValue().toString()); + property.setAttributeNode(value); + + Attr minimum = doc.createAttribute("minimum"); + minimum.setValue(np.getMinimum().toString()); + property.setAttributeNode(minimum); + + Attr maximum = doc.createAttribute("maximum"); + maximum.setValue(np.getMaximum().toString()); + property.setAttributeNode(maximum); + + Attr dim = doc.createAttribute("dimensionfactor"); + dim.setValue(np.getDimensionFactor().toString()); + property.setAttributeNode(dim); + + Attr autoAdj = doc.createAttribute("visible"); + autoAdj.setValue(np.isVisibleByDefault() + ""); + property.setAttributeNode(autoAdj); + + Attr primitiveType = doc.createAttribute("primitive-type"); + primitiveType.setValue(np.getValue() instanceof Double ? "double" : "int"); + property.setAttributeNode(primitiveType); + + Attr defSearch = doc.createAttribute("default-search-variable"); + primitiveType.setValue(np.isDefaultSearchVariable() + ""); + property.setAttributeNode(defSearch); + + } + + /** + * Utility method that creates an {@code .xml} file listing all public final + * static instances of {@code NumericProperty} found in the + * {@code NumericProperty} class. + * + * @throws javax.xml.parsers.ParserConfigurationException + * @throws javax.xml.transform.TransformerException + */ + public static void writeXML() throws ParserConfigurationException, TransformerException { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.newDocument(); + + Element rootElement = doc.createElement("NumericProperties"); + doc.appendChild(rootElement); + + List properties = new ArrayList<>(); + + int modifiers; + + /** + * Reads all final static {@code NumericProperty} constants in the + * {@code NumericProperty} class + */ + for (Field field : NumericProperty.class.getDeclaredFields()) { + + modifiers = field.getModifiers(); + + // filter only public final static NumericProperties + if ((Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) + && field.getType().equals(NumericProperty.class)) { + + NumericProperty value = null; + try { + value = (NumericProperty) field.get(null); + } catch (IllegalArgumentException | IllegalAccessException e) { + System.out.println("Unable to access field: " + field); + e.printStackTrace(); + } + if (value != null) { + properties.add(value); + } + + } + + } + + properties.stream().forEach(p -> XMLConverter.toXML(p, doc, rootElement)); + + // write the content into xml file + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(new File(NumericProperty.class.getSimpleName() + ".xml")); + transformer.transform(source, result); + + // Output to console for testing + StreamResult consoleResult = new StreamResult(System.out); + transformer.transform(source, consoleResult); + + } + + /** + * Utility method used to read {@code NumericProperty} constants from + * {@code xml} files. + * + * @param inputStream the input stream used to read data from. + * @return a list of {@code NumericProperty} objects with their attributes + * specified in the {@code xml} file. + * @throws javax.xml.parsers.ParserConfigurationException + * @throws org.xml.sax.SAXException + * @throws java.io.IOException + */ + public static List readXML(InputStream inputStream) + throws ParserConfigurationException, SAXException, IOException { + + List properties = new ArrayList<>(); + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(inputStream); + + doc.getDocumentElement().normalize(); + NodeList nList = doc.getElementsByTagName(NumericProperty.class.getSimpleName()); + + for (int temp = 0; temp < nList.getLength(); temp++) { + Node nNode = nList.item(temp); + + if (nNode.getNodeType() == Node.ELEMENT_NODE) { + Element eElement = (Element) nNode; + NumericPropertyKeyword keyword = NumericPropertyKeyword.valueOf(eElement.getAttribute("keyword")); + boolean visible = Boolean.valueOf(eElement.getAttribute("visible")); + boolean discrete = Boolean.valueOf(eElement.getAttribute("discreet")); + String descriptor = eElement.getAttribute("descriptor"); + String abbreviation = eElement.getAttribute("abbreviation"); + boolean defSearch = Boolean.valueOf(eElement.getAttribute("default-search-variable")); + + Number value, minimum, maximum, dimensionFactor; + + if (eElement.getAttribute("primitive-type").equalsIgnoreCase("double")) { + value = Double.valueOf(eElement.getAttribute("value")); + minimum = Double.valueOf(eElement.getAttribute("minimum")); + maximum = Double.valueOf(eElement.getAttribute("maximum")); + dimensionFactor = Double.valueOf(eElement.getAttribute("dimensionfactor")); + } else { + value = Integer.valueOf(eElement.getAttribute("value")); + minimum = Integer.valueOf(eElement.getAttribute("minimum")); + maximum = Integer.valueOf(eElement.getAttribute("maximum")); + dimensionFactor = Integer.valueOf(eElement.getAttribute("dimensionfactor")); + } + + NodeList excludeList = eElement.getElementsByTagName("excludes"); + + var np = new NumericProperty(keyword, value, minimum, maximum, dimensionFactor); + + if (excludeList.getLength() > 0) { + var excludeKeywords = ((Element) excludeList.item(0)).getElementsByTagName("keyword"); + NumericPropertyKeyword[] array = new NumericPropertyKeyword[excludeKeywords.getLength()]; + + for (int i = 0; i < excludeKeywords.getLength(); i++) { + String textValue = excludeKeywords.item(i).getChildNodes().item(0).getNodeValue(); + array[i] = NumericPropertyKeyword.valueOf(textValue); + } + + np.setExcludeKeywords(array); + + } + + np.setDescriptor(descriptor); + np.setAbbreviation(abbreviation); + np.setVisibleByDefault(visible); + np.setDiscrete(discrete); + np.setDefaultSearchVariable(defSearch); + properties.add(np); + } + } + + return properties; + + } + + /** + * The default XML file is specific in the 'messages.properties' text file + * in the {@code pulse.ui} package + * + * @return a list of default instances of {@code NumericProperty}. + */ + public static List readDefaultXML() { + try { + return readXML(NumericProperty.class.getResourceAsStream(Messages.getString("NumericProperty.XMLFile"))); + } catch (ParserConfigurationException | SAXException | IOException e) { + System.err.println("Unable to read list of default numeric properties"); + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/main/java/pulse/ui/components/models/ResultListModel.java b/src/main/java/pulse/ui/components/models/ResultListModel.java deleted file mode 100644 index 42dc1857..00000000 --- a/src/main/java/pulse/ui/components/models/ResultListModel.java +++ /dev/null @@ -1,69 +0,0 @@ -package pulse.ui.components.models; - -import static pulse.tasks.processing.ResultFormat.getInstance; -import static pulse.tasks.processing.ResultFormat.getMinimalArray; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.AbstractListModel; - -import pulse.properties.NumericPropertyKeyword; - -public class ResultListModel extends AbstractListModel { - - /** - * - */ - private static final long serialVersionUID = 1L; - private List elements = new ArrayList(); - - public ResultListModel() { - super(); - update(); - } - - public void update() { - elements.clear(); - elements.addAll(getInstance().getKeywords()); - } - - @Override - public int getSize() { - return elements.size(); - } - - @Override - public NumericPropertyKeyword getElementAt(int i) { - return elements.get(i); - } - - public void add(NumericPropertyKeyword key) { - elements.add(key); - var size = this.getSize(); - this.fireContentsChanged(this, size - 1, size); - } - - public void remove(NumericPropertyKeyword key) { - if (!elements.contains(key)) - return; - - for (var keyMin : getMinimalArray()) { - if (key == keyMin) - return; - } - var index = elements.indexOf(key); - elements.remove(key); - this.fireContentsChanged(this, index - 1, index); - - } - - public boolean contains(NumericPropertyKeyword key) { - return elements.contains(key); - } - - public List getData() { - return elements; - } - -} \ No newline at end of file From 071551a4372927d5aeb92b4da3c0b58f55c42320 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 13:47:51 +0100 Subject: [PATCH 063/116] Version 1.92 update --- src/main/resources/Version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index e0b8d5a9..7484b4f1 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.91FM_02 \ No newline at end of file +1.92 \ No newline at end of file From c3b3415f851f61378b98d9ff05126dcef6fa64f5 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 17:00:57 +0100 Subject: [PATCH 064/116] Changed splash screen --- .classpath | 34 + .metadata/.lock | 0 .metadata/.log | 62 + .../.org.eclipse.egit.core.cmp/.location | Bin 0 -> 152 bytes .../.root/.indexes/history.version | 1 + .../.root/.indexes/properties.index | Bin 0 -> 57 bytes .../.root/.indexes/properties.version | 1 + .../org.eclipse.core.resources/.root/1.tree | Bin 0 -> 189 bytes .../org.eclipse.core.resources/.root/2.tree | Bin 0 -> 189 bytes .../.safetable/org.eclipse.core.resources | Bin 0 -> 428 bytes .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.ui.prefs | 9 + .../.settings/org.eclipse.m2e.discovery.prefs | 2 + .../.settings/org.eclipse.ui.browser.prefs | 2 + .../.settings/org.eclipse.ui.ide.prefs | 4 + .../.settings/org.eclipse.ui.prefs | 2 + .../.settings/org.eclipse.ui.workbench.prefs | 10 + .../.settings/org.eclipse.urischeme.prefs | 2 + .../org.eclipse.e4.workbench/workbench.xmi | 2197 +++++++++++++++++ .../.org.eclipse.egit.core.cmp/.project | 11 + .../assumedExternalFilesCache | Bin 0 -> 4 bytes .../org.eclipse.jdt.core/externalFilesCache | Bin 0 -> 4 bytes .../org.eclipse.jdt.core/javaLikeNames.txt | 1 + .../org.eclipse.jdt.core/nonChainingJarsCache | Bin 0 -> 4 bytes .../variablesAndContainers.dat | Bin 0 -> 110 bytes .../org.eclipse.jdt.ui/OpenTypeHistory.xml | 2 + .../QualifiedTypeNameHistory.xml | 2 + .../org.eclipse.jdt.ui/dialog_settings.xml | 10 + .../0.log | 2 + .../logback.1.16.1.20210603-1006.xml | 43 + .../org.eclipse.oomph.setup/workspace.setup | 6 + .../org.eclipse.tips.ide/dialog_settings.xml | 3 + .../.plugins/org.eclipse.ui.intro/introstate | 2 + .../dialog_settings.xml | 5 + .../org.eclipse.ui.workbench/workingsets.xml | 6 + .metadata/version.ini | 3 + .project | 23 + .settings/org.eclipse.core.resources.prefs | 4 + .settings/org.eclipse.jdt.core.prefs | 8 + nb-configuration.xml | 18 + 40 files changed, 2477 insertions(+) create mode 100644 .classpath create mode 100644 .metadata/.lock create mode 100644 .metadata/.log create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/1.tree create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/2.tree create mode 100644 .metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs create mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs create mode 100644 .metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi create mode 100644 .metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project create mode 100644 .metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache create mode 100644 .metadata/.plugins/org.eclipse.jdt.core/externalFilesCache create mode 100644 .metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt create mode 100644 .metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache create mode 100644 .metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat create mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml create mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml create mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml create mode 100644 .metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log create mode 100644 .metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml create mode 100644 .metadata/.plugins/org.eclipse.oomph.setup/workspace.setup create mode 100644 .metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml create mode 100644 .metadata/.plugins/org.eclipse.ui.intro/introstate create mode 100644 .metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml create mode 100644 .metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml create mode 100644 .metadata/version.ini create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 nb-configuration.xml diff --git a/.classpath b/.classpath new file mode 100644 index 00000000..72280d57 --- /dev/null +++ b/.classpath @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.lock b/.metadata/.lock new file mode 100644 index 00000000..e69de29b diff --git a/.metadata/.log b/.metadata/.log new file mode 100644 index 00000000..72fedfd7 --- /dev/null +++ b/.metadata/.log @@ -0,0 +1,62 @@ +!SESSION 2021-11-26 20:20:51.678 ----------------------------------------------- +eclipse.buildId=4.21.0.I20210906-0500 +java.version=11.0.11 +java.vendor=Ubuntu +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=en_GB +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product + +!ENTRY org.eclipse.oomph.p2.core 2 0 2021-11-26 20:20:54.554 +!MESSAGE Failed to register the thread safe credentials providers: 'java.util.Map org.eclipse.core.internal.runtime.AdapterManager.getFactories()' + +!ENTRY org.eclipse.jface 2 0 2021-11-26 20:21:22.643 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2021-11-26 20:21:22.643 +!MESSAGE A conflict occurred for CTRL+SHIFT+T: +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, + Open a type in a Java editor, + Category(org.eclipse.ui.category.navigate,Navigate,null,true), + org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@25216999, + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.lsp4e.symbolinworkspace,Go to Symbol in Workspace, + , + Category(org.eclipse.lsp4e.category,Language Servers,null,true), + org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@77e6761f, + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +!SESSION 2021-11-27 22:13:57.597 ----------------------------------------------- +eclipse.buildId=4.21.0.I20210906-0500 +java.version=11.0.11 +java.vendor=Ubuntu +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=en_GB +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product + +!ENTRY org.eclipse.oomph.p2.core 2 0 2021-11-27 22:13:59.112 +!MESSAGE Failed to register the thread safe credentials providers: 'java.util.Map org.eclipse.core.internal.runtime.AdapterManager.getFactories()' + +!ENTRY org.eclipse.jface 2 0 2021-11-27 22:14:07.571 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2021-11-27 22:14:07.571 +!MESSAGE A conflict occurred for CTRL+SHIFT+T: +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, + Open a type in a Java editor, + Category(org.eclipse.ui.category.navigate,Navigate,null,true), + org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@7c3c3d67, + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.lsp4e.symbolinworkspace,Go to Symbol in Workspace, + , + Category(org.eclipse.lsp4e.category,Language Servers,null,true), + org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@1e258d3b, + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location new file mode 100644 index 0000000000000000000000000000000000000000..034a670db4c7db94e49dc1c0f1cc4c7ca8b33158 GIT binary patch literal 152 zcmZ?R*xjhShe1S2b=vdAllRFnWP}EJ>g%Uv=A>HbXXNLm>SyPdWM=E9XO`#}r55BD zXO`p_RqE^Irj{h8B$g!V>lNgbrf23A>*p7x>!l{=WEK>s>ZJme>m}zGrRwWp71m46 WEnq+a2O@rd2D>Bhb5Hf|@?!vzwl&!R literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version new file mode 100644 index 00000000..25cb955b --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index new file mode 100644 index 0000000000000000000000000000000000000000..5897676125d2fa218cd63bb0f4f65a8641b628c6 GIT binary patch literal 57 zcmZQ%U|?WmVAN+|WMUA>FG|--P0q6s8!dday3KqU$+!a!BEu^ayxL5+vm3g$90aOf2j6s8!dday3KqU$+!a!BEu^ayxL5+vm3g$90aOf2jr7#VVgiq7A2Ns=I6!d7p3c^Cg)@p6sPKCrIhF;=NF~g8k!kf z7?_xwnHn0I8gM1&q$U=*fb^lLPDxEFO^2vYOUx-w#ib5zDnymOUP)1Es;v>%fr#Iq S!M+Im+*7@~{FuYA^c4UgV3%D0 literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..dffc6b51 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +version=1 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..d5dcab7e --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,9 @@ +content_assist_proposals_background=255,255,255 +content_assist_proposals_foreground=48,48,48 +eclipse.preferences.version=1 +org.eclipse.jdt.ui.formatterprofiles.version=21 +spelling_locale=en_GB +spelling_locale_initialized=true +typefilter_migrated=true +useAnnotationsPrefPage=true +useQuickDiffPrefPage=true diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs new file mode 100644 index 00000000..67b1d96c --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +org.eclipse.m2e.discovery.pref.projects= diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs new file mode 100644 index 00000000..56ea599b --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +internalWebBrowserHistory=https\://www.eclipse.org/setups/donate/?scope\=Eclipse%20IDE%20for%20Java%20Developers%20(includes%20Incubating%20components)&version\=4.21.0.20210910-1200&campaign\=2021-06|*| diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs new file mode 100644 index 00000000..82aeac55 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +platformState=1637954452401 +quickStart=false +tipsAndTricks=true diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs new file mode 100644 index 00000000..08076f23 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +showIntro=false diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs new file mode 100644 index 00000000..ad0baeae --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs @@ -0,0 +1,10 @@ +//org.eclipse.ui.commands/state/org.eclipse.ui.navigator.resources.nested.changeProjectPresentation/org.eclipse.ui.commands.radioState=false +PLUGINS_NOT_ACTIVATED_ON_STARTUP=;org.eclipse.m2e.discovery; +eclipse.preferences.version=1 +org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_BG_END=255,255,255 +org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_BG_START=255,255,255 +org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_TEXT_COLOR=16,16,16 +org.eclipse.ui.workbench.ACTIVE_TAB_BG_END=255,255,255 +org.eclipse.ui.workbench.ACTIVE_TAB_BG_START=255,255,255 +org.eclipse.ui.workbench.INACTIVE_TAB_BG_END=246,245,244 +org.eclipse.ui.workbench.INACTIVE_TAB_BG_START=246,245,244 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs new file mode 100644 index 00000000..855d634b --- /dev/null +++ b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +processedSchemes=,eclipse+command,eclipse+mpc diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi new file mode 100644 index 00000000..79ea9ac1 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi @@ -0,0 +1,2197 @@ + + + + activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration + + + + + + + + topLevel + shellMaximized + + + + + persp.actionSet:org.eclipse.ui.cheatsheets.actionSet + persp.actionSet:org.eclipse.search.searchActionSet + persp.actionSet:org.eclipse.text.quicksearch.actionSet + persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo + persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet + persp.actionSet:org.eclipse.ui.actionSet.keyBindings + persp.actionSet:org.eclipse.ui.actionSet.openFiles + persp.actionSet:org.eclipse.debug.ui.launchActionSet + persp.actionSet:org.eclipse.jdt.ui.JavaActionSet + persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet + persp.actionSet:org.eclipse.ui.NavigateActionSet + persp.viewSC:org.eclipse.jdt.ui.PackageExplorer + persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy + persp.viewSC:org.eclipse.jdt.ui.SourceView + persp.viewSC:org.eclipse.jdt.ui.JavadocView + persp.viewSC:org.eclipse.search.ui.views.SearchView + persp.viewSC:org.eclipse.ui.console.ConsoleView + persp.viewSC:org.eclipse.ui.views.ContentOutline + persp.viewSC:org.eclipse.ui.views.ProblemView + persp.viewSC:org.eclipse.ui.views.ResourceNavigator + persp.viewSC:org.eclipse.ui.views.TaskList + persp.viewSC:org.eclipse.ui.views.ProgressView + persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer + persp.viewSC:org.eclipse.ui.texteditor.TemplatesView + persp.viewSC:org.eclipse.pde.runtime.LogView + persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard + persp.newWizSC:org.eclipse.ui.wizards.new.folder + persp.newWizSC:org.eclipse.ui.wizards.new.file + persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard + persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective + persp.perspSC:org.eclipse.debug.ui.DebugPerspective + persp.showIn:org.eclipse.jdt.ui.PackageExplorer + persp.showIn:org.eclipse.team.ui.GenericHistoryView + persp.showIn:org.eclipse.ui.navigator.ProjectExplorer + persp.actionSet:org.eclipse.debug.ui.breakpointActionSet + persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet + persp.showIn:org.eclipse.egit.ui.RepositoriesView + persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet + persp.showIn:org.eclipse.eclemma.ui.CoverageView + persp.viewSC:org.eclipse.tm.terminal.view.ui.TerminalsView + persp.showIn:org.eclipse.tm.terminal.view.ui.TerminalsView + persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard + persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet + persp.viewSC:org.eclipse.ant.ui.views.AntView + + + + org.eclipse.e4.primaryNavigationStack + active + + View + categoryTag:Java + + + View + categoryTag:Java + + + View + categoryTag:General + + + View + categoryTag:Java + + + + + View + categoryTag:Git + + + + + + + + org.eclipse.e4.secondaryNavigationStack + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Ant + + + + + org.eclipse.e4.secondaryDataStack + + View + categoryTag:General + + + View + categoryTag:Java + + + View + categoryTag:Java + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Terminal + + + + + + + + + View + categoryTag:Help + + + View + categoryTag:General + + + View + categoryTag:Help + + + + + + + View + categoryTag:Help + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Help + + + + org.eclipse.e4.primaryDataStack + EditorStack + + + + + + + View + categoryTag:Java + active + activeOnClose + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Git + + + + + View + categoryTag:Terminal + + + + + View + categoryTag:Java + + + + + View + categoryTag:Ant + + + + toolbarSeparator + + + + Draggable + + + + toolbarSeparator + + + + Draggable + + + toolbarSeparator + + + + Draggable + + + Draggable + + + Draggable + + + toolbarSeparator + + + + Draggable + + + + toolbarSeparator + + + + toolbarSeparator + + + + Draggable + + + stretch + SHOW_RESTORE_MENU + + + Draggable + HIDEABLE + SHOW_RESTORE_MENU + + + + + stretch + + + Draggable + + + Draggable + + + + + TrimStack + Draggable + + + + + + + + + + + + + + + + + + platform:gtk + + + + + + platform:gtk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + platform:gtk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Editor + removeOnHide + + + + + View + categoryTag:Ant + + + + + View + categoryTag:Gradle + + + + + View + categoryTag:Gradle + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Java + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + NoRestore + + + + + View + categoryTag:Git + + + + + View + categoryTag:Help + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Maven + + + + + View + categoryTag:Maven + + + + + View + categoryTag:Oomph + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Version Control (Team) + + + + + View + categoryTag:Version Control (Team) + + + View + categoryTag:Help + + + + + View + categoryTag:Terminal + + + + + View + categoryTag:Other + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Help + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:XML + + + + + View + categoryTag:XML + + + + glue + move_after:PerspectiveSpacer + SHOW_RESTORE_MENU + + + move_after:Spacer Glue + HIDEABLE + SHOW_RESTORE_MENU + + + glue + move_after:SearchField + SHOW_RESTORE_MENU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project b/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project new file mode 100644 index 00000000..3c108561 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project @@ -0,0 +1,11 @@ + + + .org.eclipse.egit.core.cmp + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache new file mode 100644 index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4 GIT binary patch literal 4 LcmZQzU|;|M00aO5 literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache new file mode 100644 index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4 GIT binary patch literal 4 LcmZQzU|;|M00aO5 literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt new file mode 100644 index 00000000..85863977 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt @@ -0,0 +1 @@ +java \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache new file mode 100644 index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4 GIT binary patch literal 4 LcmZQzU|;|M00aO5 literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat new file mode 100644 index 0000000000000000000000000000000000000000..0edae4b20855dcd5c83bdac184b9ed16afb1b634 GIT binary patch literal 110 zcmZQzU|?c^05&ki?iJ)3@8jvj2;?y`aD#ZkLC!(`{vjX{CI&9AP(RO*cn^PHSC9ZR e16Tu435dtSzz2~A^5IHY8Q6V|;)7fR{22i=Q4xRu literal 0 HcmV?d00001 diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml new file mode 100644 index 00000000..a4ee3cbc --- /dev/null +++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml @@ -0,0 +1,2 @@ + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml new file mode 100644 index 00000000..9e390f50 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml @@ -0,0 +1,2 @@ + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml new file mode 100644 index 00000000..365b96be --- /dev/null +++ b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml @@ -0,0 +1,10 @@ + +
+
+ + + + + +
+
diff --git a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log new file mode 100644 index 00000000..a7532364 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log @@ -0,0 +1,2 @@ +2021-11-26 20:21:25,753 [Worker-0: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is not available. Remote download required. +2021-11-27 22:14:09,721 [Worker-0: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update. diff --git a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml new file mode 100644 index 00000000..e33758c3 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml @@ -0,0 +1,43 @@ + + + + %date [%thread] %-5level %logger{35} - %msg%n + + + OFF + + + + + ${org.eclipse.m2e.log.dir}/0.log + + ${org.eclipse.m2e.log.dir}/%i.log + 1 + 10 + + + 100MB + + + %date [%thread] %-5level %logger{35} - %msg%n + + + + + + WARN + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup new file mode 100644 index 00000000..1f73e14c --- /dev/null +++ b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup @@ -0,0 +1,6 @@ + + diff --git a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml new file mode 100644 index 00000000..5ca0b776 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml @@ -0,0 +1,3 @@ + +
+
diff --git a/.metadata/.plugins/org.eclipse.ui.intro/introstate b/.metadata/.plugins/org.eclipse.ui.intro/introstate new file mode 100644 index 00000000..02f134f0 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.ui.intro/introstate @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml new file mode 100644 index 00000000..5b583c4b --- /dev/null +++ b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml @@ -0,0 +1,5 @@ + +
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml new file mode 100644 index 00000000..bb8757e7 --- /dev/null +++ b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.metadata/version.ini b/.metadata/version.ini new file mode 100644 index 00000000..f155edd3 --- /dev/null +++ b/.metadata/version.ini @@ -0,0 +1,3 @@ +#Sat Nov 27 22:14:05 CET 2021 +org.eclipse.core.runtime=2 +org.eclipse.platform=4.21.0.v20210906-0500 diff --git a/.project b/.project new file mode 100644 index 00000000..293d250b --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + repository + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..abdea9ac --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b5490a03 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/nb-configuration.xml b/nb-configuration.xml new file mode 100644 index 00000000..e767c48f --- /dev/null +++ b/nb-configuration.xml @@ -0,0 +1,18 @@ + + + + + + apache20 + + From d2963c122a1aa945fb2d357c058e387eef8003a9 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 17:09:43 +0100 Subject: [PATCH 065/116] Revert "Changed splash screen" This reverts commit c3b3415f851f61378b98d9ff05126dcef6fa64f5. --- .classpath | 34 - .metadata/.lock | 0 .metadata/.log | 62 - .../.org.eclipse.egit.core.cmp/.location | Bin 152 -> 0 bytes .../.root/.indexes/history.version | 1 - .../.root/.indexes/properties.index | Bin 57 -> 0 bytes .../.root/.indexes/properties.version | 1 - .../org.eclipse.core.resources/.root/1.tree | Bin 189 -> 0 bytes .../org.eclipse.core.resources/.root/2.tree | Bin 189 -> 0 bytes .../.safetable/org.eclipse.core.resources | Bin 428 -> 0 bytes .../org.eclipse.core.resources.prefs | 2 - .../.settings/org.eclipse.jdt.ui.prefs | 9 - .../.settings/org.eclipse.m2e.discovery.prefs | 2 - .../.settings/org.eclipse.ui.browser.prefs | 2 - .../.settings/org.eclipse.ui.ide.prefs | 4 - .../.settings/org.eclipse.ui.prefs | 2 - .../.settings/org.eclipse.ui.workbench.prefs | 10 - .../.settings/org.eclipse.urischeme.prefs | 2 - .../org.eclipse.e4.workbench/workbench.xmi | 2197 ----------------- .../.org.eclipse.egit.core.cmp/.project | 11 - .../assumedExternalFilesCache | Bin 4 -> 0 bytes .../org.eclipse.jdt.core/externalFilesCache | Bin 4 -> 0 bytes .../org.eclipse.jdt.core/javaLikeNames.txt | 1 - .../org.eclipse.jdt.core/nonChainingJarsCache | Bin 4 -> 0 bytes .../variablesAndContainers.dat | Bin 110 -> 0 bytes .../org.eclipse.jdt.ui/OpenTypeHistory.xml | 2 - .../QualifiedTypeNameHistory.xml | 2 - .../org.eclipse.jdt.ui/dialog_settings.xml | 10 - .../0.log | 2 - .../logback.1.16.1.20210603-1006.xml | 43 - .../org.eclipse.oomph.setup/workspace.setup | 6 - .../org.eclipse.tips.ide/dialog_settings.xml | 3 - .../.plugins/org.eclipse.ui.intro/introstate | 2 - .../dialog_settings.xml | 5 - .../org.eclipse.ui.workbench/workingsets.xml | 6 - .metadata/version.ini | 3 - .project | 23 - .settings/org.eclipse.core.resources.prefs | 4 - .settings/org.eclipse.jdt.core.prefs | 8 - nb-configuration.xml | 18 - 40 files changed, 2477 deletions(-) delete mode 100644 .classpath delete mode 100644 .metadata/.lock delete mode 100644 .metadata/.log delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/1.tree delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.root/2.tree delete mode 100644 .metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs delete mode 100644 .metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs delete mode 100644 .metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi delete mode 100644 .metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project delete mode 100644 .metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache delete mode 100644 .metadata/.plugins/org.eclipse.jdt.core/externalFilesCache delete mode 100644 .metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt delete mode 100644 .metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache delete mode 100644 .metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat delete mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml delete mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml delete mode 100644 .metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml delete mode 100644 .metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log delete mode 100644 .metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml delete mode 100644 .metadata/.plugins/org.eclipse.oomph.setup/workspace.setup delete mode 100644 .metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml delete mode 100644 .metadata/.plugins/org.eclipse.ui.intro/introstate delete mode 100644 .metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml delete mode 100644 .metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml delete mode 100644 .metadata/version.ini delete mode 100644 .project delete mode 100644 .settings/org.eclipse.core.resources.prefs delete mode 100644 .settings/org.eclipse.jdt.core.prefs delete mode 100644 nb-configuration.xml diff --git a/.classpath b/.classpath deleted file mode 100644 index 72280d57..00000000 --- a/.classpath +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.metadata/.lock b/.metadata/.lock deleted file mode 100644 index e69de29b..00000000 diff --git a/.metadata/.log b/.metadata/.log deleted file mode 100644 index 72fedfd7..00000000 --- a/.metadata/.log +++ /dev/null @@ -1,62 +0,0 @@ -!SESSION 2021-11-26 20:20:51.678 ----------------------------------------------- -eclipse.buildId=4.21.0.I20210906-0500 -java.version=11.0.11 -java.vendor=Ubuntu -BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=en_GB -Framework arguments: -product org.eclipse.epp.package.java.product -Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product - -!ENTRY org.eclipse.oomph.p2.core 2 0 2021-11-26 20:20:54.554 -!MESSAGE Failed to register the thread safe credentials providers: 'java.util.Map org.eclipse.core.internal.runtime.AdapterManager.getFactories()' - -!ENTRY org.eclipse.jface 2 0 2021-11-26 20:21:22.643 -!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. -!SUBENTRY 1 org.eclipse.jface 2 0 2021-11-26 20:21:22.643 -!MESSAGE A conflict occurred for CTRL+SHIFT+T: -Binding(CTRL+SHIFT+T, - ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, - Open a type in a Java editor, - Category(org.eclipse.ui.category.navigate,Navigate,null,true), - org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@25216999, - ,,true),null), - org.eclipse.ui.defaultAcceleratorConfiguration, - org.eclipse.ui.contexts.window,,,system) -Binding(CTRL+SHIFT+T, - ParameterizedCommand(Command(org.eclipse.lsp4e.symbolinworkspace,Go to Symbol in Workspace, - , - Category(org.eclipse.lsp4e.category,Language Servers,null,true), - org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@77e6761f, - ,,true),null), - org.eclipse.ui.defaultAcceleratorConfiguration, - org.eclipse.ui.contexts.window,,,system) -!SESSION 2021-11-27 22:13:57.597 ----------------------------------------------- -eclipse.buildId=4.21.0.I20210906-0500 -java.version=11.0.11 -java.vendor=Ubuntu -BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=en_GB -Framework arguments: -product org.eclipse.epp.package.java.product -Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product - -!ENTRY org.eclipse.oomph.p2.core 2 0 2021-11-27 22:13:59.112 -!MESSAGE Failed to register the thread safe credentials providers: 'java.util.Map org.eclipse.core.internal.runtime.AdapterManager.getFactories()' - -!ENTRY org.eclipse.jface 2 0 2021-11-27 22:14:07.571 -!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. -!SUBENTRY 1 org.eclipse.jface 2 0 2021-11-27 22:14:07.571 -!MESSAGE A conflict occurred for CTRL+SHIFT+T: -Binding(CTRL+SHIFT+T, - ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, - Open a type in a Java editor, - Category(org.eclipse.ui.category.navigate,Navigate,null,true), - org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@7c3c3d67, - ,,true),null), - org.eclipse.ui.defaultAcceleratorConfiguration, - org.eclipse.ui.contexts.window,,,system) -Binding(CTRL+SHIFT+T, - ParameterizedCommand(Command(org.eclipse.lsp4e.symbolinworkspace,Go to Symbol in Workspace, - , - Category(org.eclipse.lsp4e.category,Language Servers,null,true), - org.eclipse.ui.internal.WorkbenchHandlerServiceHandler@1e258d3b, - ,,true),null), - org.eclipse.ui.defaultAcceleratorConfiguration, - org.eclipse.ui.contexts.window,,,system) diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location deleted file mode 100644 index 034a670db4c7db94e49dc1c0f1cc4c7ca8b33158..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152 zcmZ?R*xjhShe1S2b=vdAllRFnWP}EJ>g%Uv=A>HbXXNLm>SyPdWM=E9XO`#}r55BD zXO`p_RqE^Irj{h8B$g!V>lNgbrf23A>*p7x>!l{=WEK>s>ZJme>m}zGrRwWp71m46 WEnq+a2O@rd2D>Bhb5Hf|@?!vzwl&!R diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version deleted file mode 100644 index 25cb955b..00000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index deleted file mode 100644 index 5897676125d2fa218cd63bb0f4f65a8641b628c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57 zcmZQ%U|?WmVAN+|WMUA>FG|--P0q6s8!dday3KqU$+!a!BEu^ayxL5+vm3g$90aOf2j6s8!dday3KqU$+!a!BEu^ayxL5+vm3g$90aOf2jr7#VVgiq7A2Ns=I6!d7p3c^Cg)@p6sPKCrIhF;=NF~g8k!kf z7?_xwnHn0I8gM1&q$U=*fb^lLPDxEFO^2vYOUx-w#ib5zDnymOUP)1Es;v>%fr#Iq S!M+Im+*7@~{FuYA^c4UgV3%D0 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index dffc6b51..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -version=1 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index d5dcab7e..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,9 +0,0 @@ -content_assist_proposals_background=255,255,255 -content_assist_proposals_foreground=48,48,48 -eclipse.preferences.version=1 -org.eclipse.jdt.ui.formatterprofiles.version=21 -spelling_locale=en_GB -spelling_locale_initialized=true -typefilter_migrated=true -useAnnotationsPrefPage=true -useQuickDiffPrefPage=true diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs deleted file mode 100644 index 67b1d96c..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.m2e.discovery.pref.projects= diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs deleted file mode 100644 index 56ea599b..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.browser.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -internalWebBrowserHistory=https\://www.eclipse.org/setups/donate/?scope\=Eclipse%20IDE%20for%20Java%20Developers%20(includes%20Incubating%20components)&version\=4.21.0.20210910-1200&campaign\=2021-06|*| diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs deleted file mode 100644 index 82aeac55..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -platformState=1637954452401 -quickStart=false -tipsAndTricks=true diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs deleted file mode 100644 index 08076f23..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -showIntro=false diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs deleted file mode 100644 index ad0baeae..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs +++ /dev/null @@ -1,10 +0,0 @@ -//org.eclipse.ui.commands/state/org.eclipse.ui.navigator.resources.nested.changeProjectPresentation/org.eclipse.ui.commands.radioState=false -PLUGINS_NOT_ACTIVATED_ON_STARTUP=;org.eclipse.m2e.discovery; -eclipse.preferences.version=1 -org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_BG_END=255,255,255 -org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_BG_START=255,255,255 -org.eclipse.ui.workbench.ACTIVE_NOFOCUS_TAB_TEXT_COLOR=16,16,16 -org.eclipse.ui.workbench.ACTIVE_TAB_BG_END=255,255,255 -org.eclipse.ui.workbench.ACTIVE_TAB_BG_START=255,255,255 -org.eclipse.ui.workbench.INACTIVE_TAB_BG_END=246,245,244 -org.eclipse.ui.workbench.INACTIVE_TAB_BG_START=246,245,244 diff --git a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs b/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs deleted file mode 100644 index 855d634b..00000000 --- a/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.urischeme.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -processedSchemes=,eclipse+command,eclipse+mpc diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi deleted file mode 100644 index 79ea9ac1..00000000 --- a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi +++ /dev/null @@ -1,2197 +0,0 @@ - - - - activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration - - - - - - - - topLevel - shellMaximized - - - - - persp.actionSet:org.eclipse.ui.cheatsheets.actionSet - persp.actionSet:org.eclipse.search.searchActionSet - persp.actionSet:org.eclipse.text.quicksearch.actionSet - persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation - persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation - persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo - persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet - persp.actionSet:org.eclipse.ui.actionSet.keyBindings - persp.actionSet:org.eclipse.ui.actionSet.openFiles - persp.actionSet:org.eclipse.debug.ui.launchActionSet - persp.actionSet:org.eclipse.jdt.ui.JavaActionSet - persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet - persp.actionSet:org.eclipse.ui.NavigateActionSet - persp.viewSC:org.eclipse.jdt.ui.PackageExplorer - persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy - persp.viewSC:org.eclipse.jdt.ui.SourceView - persp.viewSC:org.eclipse.jdt.ui.JavadocView - persp.viewSC:org.eclipse.search.ui.views.SearchView - persp.viewSC:org.eclipse.ui.console.ConsoleView - persp.viewSC:org.eclipse.ui.views.ContentOutline - persp.viewSC:org.eclipse.ui.views.ProblemView - persp.viewSC:org.eclipse.ui.views.ResourceNavigator - persp.viewSC:org.eclipse.ui.views.TaskList - persp.viewSC:org.eclipse.ui.views.ProgressView - persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer - persp.viewSC:org.eclipse.ui.texteditor.TemplatesView - persp.viewSC:org.eclipse.pde.runtime.LogView - persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard - persp.newWizSC:org.eclipse.ui.wizards.new.folder - persp.newWizSC:org.eclipse.ui.wizards.new.file - persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard - persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective - persp.perspSC:org.eclipse.debug.ui.DebugPerspective - persp.showIn:org.eclipse.jdt.ui.PackageExplorer - persp.showIn:org.eclipse.team.ui.GenericHistoryView - persp.showIn:org.eclipse.ui.navigator.ProjectExplorer - persp.actionSet:org.eclipse.debug.ui.breakpointActionSet - persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet - persp.showIn:org.eclipse.egit.ui.RepositoriesView - persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet - persp.showIn:org.eclipse.eclemma.ui.CoverageView - persp.viewSC:org.eclipse.tm.terminal.view.ui.TerminalsView - persp.showIn:org.eclipse.tm.terminal.view.ui.TerminalsView - persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard - persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet - persp.viewSC:org.eclipse.ant.ui.views.AntView - - - - org.eclipse.e4.primaryNavigationStack - active - - View - categoryTag:Java - - - View - categoryTag:Java - - - View - categoryTag:General - - - View - categoryTag:Java - - - - - View - categoryTag:Git - - - - - - - - org.eclipse.e4.secondaryNavigationStack - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:Ant - - - - - org.eclipse.e4.secondaryDataStack - - View - categoryTag:General - - - View - categoryTag:Java - - - View - categoryTag:Java - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:Terminal - - - - - - - - - View - categoryTag:Help - - - View - categoryTag:General - - - View - categoryTag:Help - - - - - - - View - categoryTag:Help - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Help - - - - org.eclipse.e4.primaryDataStack - EditorStack - - - - - - - View - categoryTag:Java - active - activeOnClose - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Git - - - - - View - categoryTag:Terminal - - - - - View - categoryTag:Java - - - - - View - categoryTag:Ant - - - - toolbarSeparator - - - - Draggable - - - - toolbarSeparator - - - - Draggable - - - toolbarSeparator - - - - Draggable - - - Draggable - - - Draggable - - - toolbarSeparator - - - - Draggable - - - - toolbarSeparator - - - - toolbarSeparator - - - - Draggable - - - stretch - SHOW_RESTORE_MENU - - - Draggable - HIDEABLE - SHOW_RESTORE_MENU - - - - - stretch - - - Draggable - - - Draggable - - - - - TrimStack - Draggable - - - - - - - - - - - - - - - - - - platform:gtk - - - - - - platform:gtk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - platform:gtk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Editor - removeOnHide - - - - - View - categoryTag:Ant - - - - - View - categoryTag:Gradle - - - - - View - categoryTag:Gradle - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Java - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - NoRestore - - - - - View - categoryTag:Git - - - - - View - categoryTag:Help - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Maven - - - - - View - categoryTag:Maven - - - - - View - categoryTag:Oomph - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Version Control (Team) - - - - - View - categoryTag:Version Control (Team) - - - View - categoryTag:Help - - - - - View - categoryTag:Terminal - - - - - View - categoryTag:Other - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Help - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:XML - - - - - View - categoryTag:XML - - - - glue - move_after:PerspectiveSpacer - SHOW_RESTORE_MENU - - - move_after:Spacer Glue - HIDEABLE - SHOW_RESTORE_MENU - - - glue - move_after:SearchField - SHOW_RESTORE_MENU - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project b/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project deleted file mode 100644 index 3c108561..00000000 --- a/.metadata/.plugins/org.eclipse.egit.core/.org.eclipse.egit.core.cmp/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - .org.eclipse.egit.core.cmp - - - - - - - - diff --git a/.metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache deleted file mode 100644 index 593f4708db84ac8fd0f5cc47c634f38c013fe9e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZQzU|;|M00aO5 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache deleted file mode 100644 index 593f4708db84ac8fd0f5cc47c634f38c013fe9e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZQzU|;|M00aO5 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt deleted file mode 100644 index 85863977..00000000 --- a/.metadata/.plugins/org.eclipse.jdt.core/javaLikeNames.txt +++ /dev/null @@ -1 +0,0 @@ -java \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache deleted file mode 100644 index 593f4708db84ac8fd0f5cc47c634f38c013fe9e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZQzU|;|M00aO5 diff --git a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat deleted file mode 100644 index 0edae4b20855dcd5c83bdac184b9ed16afb1b634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110 zcmZQzU|?c^05&ki?iJ)3@8jvj2;?y`aD#ZkLC!(`{vjX{CI&9AP(RO*cn^PHSC9ZR e16Tu435dtSzz2~A^5IHY8Q6V|;)7fR{22i=Q4xRu diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml deleted file mode 100644 index a4ee3cbc..00000000 --- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml deleted file mode 100644 index 9e390f50..00000000 --- a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml deleted file mode 100644 index 365b96be..00000000 --- a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml +++ /dev/null @@ -1,10 +0,0 @@ - -
-
- - - - - -
-
diff --git a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log deleted file mode 100644 index a7532364..00000000 --- a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/0.log +++ /dev/null @@ -1,2 +0,0 @@ -2021-11-26 20:21:25,753 [Worker-0: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is not available. Remote download required. -2021-11-27 22:14:09,721 [Worker-0: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update. diff --git a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml b/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml deleted file mode 100644 index e33758c3..00000000 --- a/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.16.1.20210603-1006.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - %date [%thread] %-5level %logger{35} - %msg%n - - - OFF - - - - - ${org.eclipse.m2e.log.dir}/0.log - - ${org.eclipse.m2e.log.dir}/%i.log - 1 - 10 - - - 100MB - - - %date [%thread] %-5level %logger{35} - %msg%n - - - - - - WARN - - - - - - - - - - - - - - - diff --git a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup deleted file mode 100644 index 1f73e14c..00000000 --- a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml deleted file mode 100644 index 5ca0b776..00000000 --- a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/.metadata/.plugins/org.eclipse.ui.intro/introstate b/.metadata/.plugins/org.eclipse.ui.intro/introstate deleted file mode 100644 index 02f134f0..00000000 --- a/.metadata/.plugins/org.eclipse.ui.intro/introstate +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml deleted file mode 100644 index 5b583c4b..00000000 --- a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - -
-
-
-
diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml deleted file mode 100644 index bb8757e7..00000000 --- a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.metadata/version.ini b/.metadata/version.ini deleted file mode 100644 index f155edd3..00000000 --- a/.metadata/version.ini +++ /dev/null @@ -1,3 +0,0 @@ -#Sat Nov 27 22:14:05 CET 2021 -org.eclipse.core.runtime=2 -org.eclipse.platform=4.21.0.v20210906-0500 diff --git a/.project b/.project deleted file mode 100644 index 293d250b..00000000 --- a/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - repository - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index abdea9ac..00000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index b5490a03..00000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,8 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 diff --git a/nb-configuration.xml b/nb-configuration.xml deleted file mode 100644 index e767c48f..00000000 --- a/nb-configuration.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - apache20 - - From ba53fdf02965e9a49bc9757b993a049b5712b2c0 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 17:11:19 +0100 Subject: [PATCH 066/116] Added Splash Screen (fixed) --- src/main/resources/images/splash.png | Bin 93670 -> 67732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index c755ddc03be30edbbdfd394eabdba278f7174100..5ff58dda6d92d62620fd1509a033c87bcc3ade32 100644 GIT binary patch literal 67732 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⪚;tzjW9a?17Kh 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

BXiAHr-U#3O90kFIz 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¢a((yrv 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 From f953fa855137a065020a03596862c98b477e72ed Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 29 Nov 2021 17:15:35 +0100 Subject: [PATCH 067/116] pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 71d2b643..68c0076b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.91FM_02 + 1.92 PULsE Processing Unit for Laser flash Experiments From 99f6100c5686aff71282b34cc742f38a8402f703 Mon Sep 17 00:00:00 2001 From: Artem Lunev <53570328+kotik-coder@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:23:14 +0100 Subject: [PATCH 068/116] Update README.md Removed obsolete links --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 306ecaf5..f61d167c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # PULsE -[![Build Status](https://travis-ci.com/kotik-coder/PULsE.svg?branch=releases)](https://travis-ci.com/kotik-coder/PULsE) -[![Code Quality](https://www.code-inspector.com/project/4377/score/svg)](https://frontend.code-inspector.com/public/project/4377/PULsE/dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1c354f5fe4d0435d8935f11cf2368d8e)](https://app.codacy.com/manual/kotik-coder/PULsE?utm_source=github.com&utm_medium=referral&utm_content=kotik-coder/PULsE&utm_campaign=Badge_Grade_Dashboard) [![Maintainability](https://api.codeclimate.com/v1/badges/bbbb695c6ffa3fbcb7e9/maintainability)](https://codeclimate.com/github/kotik-coder/PULsE/maintainability) [![codebeat badge](https://codebeat.co/badges/de6c9956-9737-4cba-ad1e-455140160792)](https://codebeat.co/projects/github-com-kotik-coder-pulse-development) From 2d8e5ede405887c3774b66a2058caa2255588b7a Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Dec 2021 21:45:53 +0100 Subject: [PATCH 069/116] Minor changes including formatting and import re-arrangement --- src/main/java/pulse/AbstractData.java | 577 ++++++------ src/main/java/pulse/HeatingCurve.java | 649 ++++++------- src/main/java/pulse/HeatingCurveListener.java | 12 +- .../java/pulse/baseline/package-info.java | 3 +- src/main/java/pulse/input/IndexRange.java | 411 ++++----- .../pulse/input/listeners/CurveEvent.java | 75 +- .../pulse/input/listeners/CurveEventType.java | 33 +- .../java/pulse/input/listeners/DataEvent.java | 70 +- .../pulse/input/listeners/DataEventType.java | 21 +- .../pulse/input/listeners/DataListener.java | 20 +- .../listeners/ExternalDatasetListener.java | 20 +- .../pulse/input/listeners/package-info.java | 6 +- src/main/java/pulse/input/package-info.java | 3 +- .../java/pulse/io/export/CurveExporter.java | 229 +++-- .../java/pulse/io/export/ExportManager.java | 380 ++++---- src/main/java/pulse/io/export/Extension.java | 50 +- .../java/pulse/io/export/LogExporter.java | 115 ++- .../java/pulse/io/export/LogPaneExporter.java | 113 ++- .../pulse/io/export/MetadataExporter.java | 189 ++-- .../java/pulse/io/export/RawDataExporter.java | 91 +- .../io/export/ResidualStatisticExporter.java | 187 ++-- .../java/pulse/io/export/ResultExporter.java | 170 ++-- .../java/pulse/io/export/package-info.java | 5 +- .../pulse/io/readers/AbstractHandler.java | 96 +- .../pulse/io/readers/AbstractPopulator.java | 23 +- .../java/pulse/io/readers/AbstractReader.java | 27 +- .../io/readers/ButcherTableauReader.java | 158 ++-- .../java/pulse/io/readers/CurveReader.java | 77 +- src/main/java/pulse/io/readers/DATReader.java | 135 ++- .../java/pulse/io/readers/DatasetReader.java | 28 +- .../pulse/io/readers/ExpressionParser.java | 197 ++-- src/main/java/pulse/io/readers/LFRReader.java | 314 ++++--- .../pulse/io/readers/MetaFilePopulator.java | 219 +++-- .../pulse/io/readers/NetzschCSVReader.java | 323 +++---- .../io/readers/NetzschPulseCSVReader.java | 126 ++- .../pulse/io/readers/PulseDataReader.java | 15 +- .../pulse/io/readers/QuadratureReader.java | 115 ++- .../java/pulse/io/readers/ReaderManager.java | 494 +++++----- src/main/java/pulse/io/readers/TBLReader.java | 133 ++- .../java/pulse/io/readers/package-info.java | 3 +- .../java/pulse/math/AbstractIntegrator.java | 107 ++- .../pulse/math/FixedIntervalIntegrator.java | 196 ++-- .../pulse/math/FunctionWithInterpolation.java | 215 +++-- src/main/java/pulse/math/LegendrePoly.java | 293 +++--- src/main/java/pulse/math/MathUtils.java | 321 ++++--- .../java/pulse/math/MidpointIntegrator.java | 60 +- src/main/java/pulse/math/ParameterVector.java | 447 ++++----- src/main/java/pulse/math/Segment.java | 257 +++--- .../java/pulse/math/SimpsonIntegrator.java | 68 +- .../math/linear/ArithmeticOperation.java | 21 +- .../math/linear/ArithmeticOperations.java | 66 +- src/main/java/pulse/math/linear/Matrices.java | 95 +- src/main/java/pulse/math/linear/Matrix2.java | 69 +- src/main/java/pulse/math/linear/Matrix3.java | 74 +- src/main/java/pulse/math/linear/Matrix4.java | 95 +- .../pulse/math/linear/RectangularMatrix.java | 483 +++++----- .../java/pulse/math/linear/SquareMatrix.java | 164 ++-- src/main/java/pulse/math/linear/Vector.java | 605 ++++++------- .../java/pulse/math/linear/package-info.java | 3 +- src/main/java/pulse/math/package-info.java | 3 +- .../pulse/math/transforms/AtanhTransform.java | 58 +- .../transforms/BoundedParameterTransform.java | 29 +- .../math/transforms/InvDiamTransform.java | 31 +- .../math/transforms/InvLenSqTransform.java | 31 +- .../math/transforms/InvLenTransform.java | 34 +- .../transforms/StandardTransformations.java | 108 +-- .../pulse/math/transforms/Transformable.java | 34 +- src/main/java/pulse/package-info.java | 3 +- .../pulse/problem/laser/DiscretePulse.java | 175 ++-- .../pulse/problem/laser/DiscretePulse2D.java | 112 ++- .../laser/ExponentiallyModifiedGaussian.java | 300 +++--- .../pulse/problem/laser/NumericPulseData.java | 117 ++- .../problem/laser/PulseTemporalShape.java | 179 ++-- .../pulse/problem/laser/RectangularPulse.java | 44 +- .../pulse/problem/laser/TrapezoidalPulse.java | 239 +++-- .../pulse/problem/laser/package-info.java | 3 +- .../java/pulse/problem/schemes/ADIScheme.java | 93 +- .../problem/schemes/BlockMatrixAlgorithm.java | 142 +-- .../problem/schemes/DistributedDetection.java | 52 +- .../pulse/problem/schemes/ExplicitScheme.java | 135 ++- .../problem/schemes/FixedPointIterations.java | 89 +- .../java/pulse/problem/schemes/Grid2D.java | 188 ++-- .../pulse/problem/schemes/ImplicitScheme.java | 180 ++-- .../pulse/problem/schemes/MixedScheme.java | 99 +- .../problem/schemes/OneDimensionalScheme.java | 84 +- .../java/pulse/problem/schemes/Partition.java | 120 +-- .../schemes/RadiativeTransferCoupling.java | 130 +-- .../schemes/TridiagonalMatrixAlgorithm.java | 221 ++--- .../pulse/problem/schemes/package-info.java | 5 +- .../schemes/rte/BlackbodySpectrum.java | 141 ++- .../schemes/rte/DerivativeCalculator.java | 108 ++- .../pulse/problem/schemes/rte/Fluxes.java | 128 ++- .../rte/FluxesAndExplicitDerivatives.java | 126 +-- .../rte/FluxesAndImplicitDerivatives.java | 84 +- .../schemes/rte/RTECalculationListener.java | 16 +- .../schemes/rte/RTECalculationStatus.java | 42 +- .../schemes/rte/RadiativeTransferSolver.java | 223 +++-- .../schemes/rte/dom/ButcherTableau.java | 169 ++-- .../rte/dom/CompositeGaussianQuadrature.java | 152 ++-- .../schemes/rte/dom/DiscreteQuantities.java | 178 ++-- .../schemes/rte/dom/Discretisation.java | 6 +- .../schemes/rte/dom/FixedIterations.java | 58 +- .../schemes/rte/dom/HenyeyGreensteinPF.java | 55 +- .../schemes/rte/dom/HermiteInterpolator.java | 215 ++--- .../schemes/rte/dom/IterativeSolver.java | 178 ++-- .../schemes/rte/dom/LinearAnisotropicPF.java | 51 +- .../schemes/rte/dom/ODEIntegrator.java | 190 ++-- .../problem/schemes/rte/dom/OrdinateSet.java | 153 ++-- .../schemes/rte/dom/PhaseFunction.java | 130 +-- .../schemes/rte/dom/StretchedGrid.java | 273 +++--- .../rte/dom/SuccessiveOverrelaxation.java | 168 ++-- .../pulse/problem/schemes/rte/dom/TRBDF2.java | 336 ++++--- .../problem/schemes/rte/dom/package-info.java | 3 +- .../schemes/rte/exact/CompositionProduct.java | 125 +-- .../rte/exact/ExponentialIntegral.java | 108 ++- .../rte/exact/ExponentialIntegrals.java | 77 +- .../NonscatteringAnalyticalDerivatives.java | 138 ++- .../NonscatteringDiscreteDerivatives.java | 25 +- .../exact/NonscatteringRadiativeTransfer.java | 410 ++++----- .../schemes/rte/exact/package-info.java | 5 +- .../problem/schemes/rte/package-info.java | 3 +- .../schemes/solvers/ADILayeredSolver.java | 143 ++- .../schemes/solvers/ADILinearisedSolver.java | 474 +++++----- .../solvers/ExplicitCoupledSolver.java | 268 +++--- .../solvers/ExplicitLinearisedSolver.java | 96 +- .../solvers/ExplicitNonlinearSolver.java | 239 +++-- .../solvers/ExplicitTranslucentSolver.java | 150 ++- .../solvers/ImplicitCoupledSolver.java | 167 ++-- .../solvers/ImplicitDiathermicSolver.java | 190 ++-- .../solvers/ImplicitLinearisedSolver.java | 174 ++-- .../solvers/ImplicitNonlinearSolver.java | 267 +++--- .../solvers/ImplicitTranslucentSolver.java | 220 +++-- .../solvers/MixedLinearisedSolver.java | 220 +++-- .../pulse/problem/schemes/solvers/Solver.java | 22 +- .../schemes/solvers/SolverException.java | 8 +- .../problem/schemes/solvers/package-info.java | 5 +- .../problem/statements/AdiabaticSolution.java | 195 ++-- .../problem/statements/ClassicalProblem.java | 83 +- .../statements/ClassicalProblem2D.java | 225 +++-- .../problem/statements/CoreShellProblem.java | 275 +++--- .../problem/statements/DiathermicMedium.java | 137 ++- .../statements/ParticipatingMedium.java | 233 ++--- .../statements/PenetrationProblem.java | 6 +- .../problem/statements/ProblemComplexity.java | 18 +- .../statements/model/SpectralRange.java | 28 +- .../problem/statements/package-info.java | 3 +- .../properties/NumericPropertyKeyword.java | 857 +++++++----------- .../java/pulse/properties/SampleName.java | 82 +- .../java/pulse/properties/package-info.java | 3 +- src/main/java/pulse/search/Optimisable.java | 52 +- .../pulse/search/direction/BFGSOptimiser.java | 155 ++-- .../pulse/search/direction/ComplexPath.java | 77 +- .../search/direction/DirectionSolver.java | 25 +- .../search/direction/GradientGuidedPath.java | 88 +- .../direction/HessianDirectionSolver.java | 89 +- .../java/pulse/search/direction/LMPath.java | 126 ++- .../pulse/search/direction/SR1Optimiser.java | 154 ++-- .../direction/SteepestDescentOptimiser.java | 98 +- .../pulse/search/direction/package-info.java | 3 +- .../pulse/search/direction/pso/FIPSMover.java | 70 +- .../pulse/search/direction/pso/Mover.java | 6 +- .../direction/pso/NeighbourhoodTopology.java | 6 +- .../pulse/search/direction/pso/Particle.java | 101 +-- .../search/direction/pso/ParticleState.java | 133 +-- .../direction/pso/ParticleSwarmOptimiser.java | 60 +- .../direction/pso/StaticTopologies.java | 101 +-- .../search/direction/pso/SwarmState.java | 206 ++--- .../search/linear/GoldenSectionOptimiser.java | 169 ++-- .../pulse/search/linear/LinearOptimiser.java | 232 ++--- .../pulse/search/linear/WolfeOptimiser.java | 230 +++-- .../pulse/search/linear/package-info.java | 3 +- .../pulse/search/statistics/AICStatistic.java | 66 +- .../search/statistics/AbsoluteDeviations.java | 78 +- .../statistics/AndersonDarlingTest.java | 67 +- .../pulse/search/statistics/BICStatistic.java | 71 +- .../search/statistics/CorrelationTest.java | 57 +- .../statistics/EmptyCorrelationTest.java | 18 +- .../pulse/search/statistics/EmptyTest.java | 33 +- .../java/pulse/search/statistics/KSTest.java | 60 +- .../statistics/ModelSelectionCriterion.java | 215 ++--- .../search/statistics/NormalityTest.java | 139 +-- .../search/statistics/OptimiserStatistic.java | 50 +- .../search/statistics/PearsonCorrelation.java | 23 +- .../pulse/search/statistics/RSquaredTest.java | 160 ++-- .../statistics/RegularisedLeastSquares.java | 109 ++- .../search/statistics/ResidualStatistic.java | 179 ++-- .../statistics/SpearmansCorrelationTest.java | 24 +- .../pulse/search/statistics/Statistic.java | 15 +- .../pulse/search/statistics/SumOfSquares.java | 99 +- .../pulse/search/statistics/package-info.java | 3 +- src/main/java/pulse/tasks/Identifier.java | 91 +- .../listeners/DataCollectionListener.java | 5 +- .../tasks/listeners/LogEntryListener.java | 6 +- .../tasks/listeners/ResultFormatEvent.java | 14 +- .../tasks/listeners/ResultFormatListener.java | 2 +- .../tasks/listeners/StatusChangeListener.java | 3 +- .../tasks/listeners/TaskRepositoryEvent.java | 144 ++- .../listeners/TaskRepositoryListener.java | 5 +- .../tasks/listeners/TaskSelectionEvent.java | 24 +- .../listeners/TaskSelectionListener.java | 2 +- .../pulse/tasks/listeners/package-info.java | 3 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 84 +- .../java/pulse/tasks/logs/DataLogEntry.java | 148 ++- src/main/java/pulse/tasks/logs/Details.java | 105 +-- src/main/java/pulse/tasks/logs/Log.java | 358 ++++---- src/main/java/pulse/tasks/logs/LogEntry.java | 54 +- .../java/pulse/tasks/logs/StateEntry.java | 73 +- src/main/java/pulse/tasks/logs/Status.java | 232 +++-- .../java/pulse/tasks/logs/package-info.java | 3 +- src/main/java/pulse/tasks/package-info.java | 3 +- .../tasks/processing/AbstractResult.java | 227 +++-- .../java/pulse/tasks/processing/Buffer.java | 343 ++++--- .../tasks/processing/CorrelationBuffer.java | 165 ++-- .../java/pulse/tasks/processing/Result.java | 45 +- .../pulse/tasks/processing/ResultFormat.java | 290 +++--- .../pulse/tasks/processing/package-info.java | 3 +- src/main/java/pulse/ui/Version.java | 105 +-- .../java/pulse/ui/components/AuxPlotter.java | 91 +- .../pulse/ui/components/CalculationTable.java | 114 +-- .../java/pulse/ui/components/LogPane.java | 154 ++-- .../java/pulse/ui/components/ProblemTree.java | 142 +-- .../ui/components/PropertyHolderTable.java | 299 +++--- .../java/pulse/ui/components/PulseChart.java | 142 +-- .../pulse/ui/components/PulseMainMenu.java | 639 ++++++------- .../java/pulse/ui/components/TaskBox.java | 71 +- .../pulse/ui/components/TaskPopupMenu.java | 341 +++---- .../components/buttons/ExecutionButton.java | 182 ++-- .../ui/components/buttons/IconCheckBox.java | 54 +- .../ui/components/buttons/LoaderButton.java | 174 ++-- .../ui/components/buttons/package-info.java | 2 +- .../components/controllers/ButtonEditor.java | 83 +- .../components/controllers/ConfirmAction.java | 2 +- .../controllers/InstanceCellEditor.java | 1 - .../components/controllers/NumberEditor.java | 336 +++---- .../NumericPropertyComparator.java | 18 +- .../controllers/ProblemCellRenderer.java | 31 +- .../controllers/SearchListRenderer.java | 18 +- .../components/controllers/package-info.java | 2 +- .../listeners/ExitRequestListener.java | 2 +- .../FrameVisibilityRequestListener.java | 7 +- .../listeners/LogExportListener.java | 2 +- .../listeners/PlotRequestListener.java | 2 +- .../PreviewFrameCreationListener.java | 2 +- .../listeners/ProblemSelectionEvent.java | 50 +- .../listeners/ProblemSelectionListener.java | 6 +- .../components/listeners/ResultListener.java | 2 +- .../listeners/ResultRequestListener.java | 10 +- .../listeners/TaskActionListener.java | 8 +- .../ui/components/listeners/package-info.java | 2 +- .../models/StoredCalculationTableModel.java | 72 +- .../ui/components/models/TaskBoxModel.java | 158 ++-- .../ui/components/models/TaskTableModel.java | 118 +-- .../ui/components/models/package-info.java | 2 +- .../pulse/ui/components/package-info.java | 3 +- .../ui/components/panels/LogToolbar.java | 54 +- .../ui/components/panels/ModelToolbar.java | 82 +- .../ui/components/panels/OpacitySlider.java | 46 +- .../ui/components/panels/ResultToolbar.java | 150 +-- .../ui/components/panels/SettingsToolBar.java | 82 +- .../ui/components/panels/SystemPanel.java | 122 +-- .../ui/components/panels/TaskToolbar.java | 202 ++--- .../ui/components/panels/package-info.java | 2 +- src/main/java/pulse/ui/frames/DataFrame.java | 98 +- .../pulse/ui/frames/ExternalGraphFrame.java | 48 +- .../java/pulse/ui/frames/HistogramFrame.java | 36 +- .../pulse/ui/frames/InternalGraphFrame.java | 56 +- src/main/java/pulse/ui/frames/LogFrame.java | 120 +-- .../java/pulse/ui/frames/MainGraphFrame.java | 86 +- .../pulse/ui/frames/ModelSelectionFrame.java | 38 +- .../java/pulse/ui/frames/ResultFrame.java | 201 ++-- .../pulse/ui/frames/TaskControlFrame.java | 769 ++++++++-------- .../pulse/ui/frames/TaskManagerFrame.java | 166 ++-- .../ui/frames/dialogs/ProgressDialog.java | 142 +-- .../pulse/ui/frames/dialogs/package-info.java | 2 +- .../java/pulse/ui/frames/package-info.java | 3 +- src/main/java/pulse/ui/package-info.java | 3 +- src/main/java/pulse/util/Descriptive.java | 26 +- .../pulse/util/DescriptorChangeListener.java | 4 +- .../java/pulse/util/DiscreteSelector.java | 140 +-- src/main/java/pulse/util/Group.java | 223 +++-- .../java/pulse/util/HierarchyListener.java | 20 +- src/main/java/pulse/util/ImageUtils.java | 165 ++-- .../java/pulse/util/ImmutableDataEntry.java | 81 +- src/main/java/pulse/util/ImmutablePair.java | 63 +- src/main/java/pulse/util/PropertyEvent.java | 97 +- .../pulse/util/PropertyHolderListener.java | 17 +- src/main/java/pulse/util/Reflexive.java | 99 +- src/main/java/pulse/util/ReflexiveFinder.java | 416 ++++----- src/main/java/pulse/util/ResourceMonitor.java | 201 ++-- .../java/pulse/util/UpwardsNavigable.java | 196 ++-- src/main/java/pulse/util/package-info.java | 3 +- 291 files changed, 17373 insertions(+), 17866 deletions(-) diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index dd652164..f1f5ab84 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -17,7 +18,8 @@ import pulse.util.PropertyHolder; /** - * A named collection of time and temperature values, with user-adjustable number of entries. + * A named collection of time and temperature values, with user-adjustable + * number of entries. *

* The notion of temperature is loosely used here, and this can represent just * the detector signal in mV. Unless explicitly specified otherwise, the unit of @@ -26,293 +28,290 @@ *

* */ - public abstract class AbstractData extends PropertyHolder { - private int count; - - private List time; - private List signal; - - private String name; - - protected AbstractData(List time, String name) { - this.time = time; - this.count = time.size(); - this.name = name; - } - - /** - * Copy constructor. Copies all data and assigns the same name to {@code this}. - * @param d another instance of this class - */ - - public AbstractData(AbstractData d) { - this.time = new ArrayList<>(d.time); - this.signal = new ArrayList<>(d.signal); - this.count = d.count; - this.name = d.name; - } - - /** - * Creates an {@code AbstractData} with the default number of points (set in the - * corresponding XML file). - */ - - public AbstractData() { - this(def(NUMPOINTS)); - } - - /** - * Creates a {@code AbstractData}, where the number of elements in the - * {@code time} and {@code temperature} collections are set to - * {@code count.getValue()}. - *

- * - * @param count The {@code NumericProperty} that is derived from the - * {@code NumericPropertyKeyword.NUMPOINTS}. - */ - - public AbstractData(NumericProperty count) { - setNumPoints(count); - time = new ArrayList<>(this.count); - signal = new ArrayList<>(this.count); - } - - /** - * The actual number of points, explicitly calculated as the size of the internal lists. - * @return an integer size equal to the real number of elements (pairs) - */ - - public int actualNumPoints() { - return time.size(); - } - - /** - * Clears all elements from the three {@code List} objects, thus releasing - * memory. - */ - - public void clear() { - this.time.clear(); - this.signal.clear(); - } - - /** - * Getter method providing accessibility to the {@code count NumericProperty}. - * - * @return a {@code NumericProperty} derived from - * {@code NumericPropertyKeyword.NUMPOINTS} with the value of - * {@code count} - */ - - public NumericProperty getNumPoints() { - return derive(NUMPOINTS, count); - } - - /** - * Sets the number of points for this baseline. - *

- * The {@code List} data objects, containing time, temperature, and - * baseline-subtracted temperature are filled with zeroes. - * - * @param c - */ - - public void setNumPoints(NumericProperty c) { - requireType(c, NUMPOINTS); - this.count = (int) c.getValue(); - firePropertyChanged(this, c); - } - - /** - * Retrieves an element from the {@code time List} specified by {@code index} - * - * @param index the index of the element to be returned - * @return a time value corresponding to {@code index} - */ - - public double timeAt(int index) { - return time.get(index); - } - - /** - * Retrieves the last element of the {@code time List}. This is used e.g. by the - * {@code DifferenceScheme} to set the calculation limit for the - * finite-difference scheme. - * - * @see pulse.problem.schemes.DifferenceScheme - * @return a double, equal to the last element of the {@code time List}. - */ - - public double timeLimit() { - return timeAt(time.size() - 1); - } - - /** - * Retrieves the signal value corresponding to the index {@code index}. Is overriden by - * subclasses. - * - * @param index the index of the element - * @return a double, representing the signal at - * {@code index} - */ - - public double signalAt(int index) { - return signal.get(index); - } - - /** - * Adds a time-signal pair to the lists. - * @param time the time value - * @param sgn the signal value at {@code time} - */ - - public void addPoint(double time, double sgn) { - this.time.add(time); - this.signal.add(sgn); - } - - protected void incrementCount() { - count++; - } - - /** - * Sets the time {@code t} at the position {@code index} of the - * {@code time List}. - * - * @param index the index - * @param t the new time value at this index - */ - - public void setTimeAt(int index, double t) { - time.set(index, t); - } - - /** - * Sets the signal {@code t} at the position {@code index} of the - * {@code signal List}. - * - * @param index the index - * @param t the new signal value at this index - */ - - public void setSignalAt(int index, double t) { - signal.set(index, t); - } - - /** - * Calculates the simple maximum signal. - * @return the maximum signal value - * @see java.util.Collections.max - */ - - public double apparentMaximum() { - return max(signal); - } - - /** - * Checks if the time list is incomplete. - * @return {@code false} if the list with time values has less elements than initially declared, {@code true} otherwise. - */ - - public boolean isIncomplete() { - return time.size() < count; - } - - @Override - public String toString() { - return name != null ? name : getClass().getSimpleName() + " (" + getNumPoints() + ")"; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Provides general setter accessibility for the number of points of this - * {@code AbstractData}. - * - * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} - * @param property the property of the type - * {@code NumericPropertyKeyword.NUMPOINTS} - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NUMPOINTS) - setNumPoints(property); - } - - /** - * Lists {@code NUM_POINTS} as an accessible property of this {@code PropertyHolder}. - */ - - @Override - public List listedTypes() { - return new ArrayList<>(Arrays.asList(getNumPoints())); - } - - /** - * Removes a time-value pair that is presend under the index {@code i}. - * - * @param i the element to be removed - */ - - public void remove(int i) { - this.time.remove(i); - this.signal.remove(i); - } - - /** - * @return true - */ - - @Override - public boolean ignoreSiblings() { - return true; - } - - public List getTimeSequence() { - return time; - } - - public List getSignalData() { - return signal; - } - - /** - * @return {@code true} only if {@code o} is an {@code AbstractData} containing all the elements - * of the time and signal lists of {@code this} object. - */ - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof AbstractData)) - return false; - - var other = (AbstractData) o; - - final double EPS = 1e-8; - - if (abs(count - (Integer) other.getNumPoints().getValue()) > EPS) - return false; - - if (signal.hashCode() != other.signal.hashCode()) - return false; - - if (time.hashCode() != other.time.hashCode()) - return false; - - return time.containsAll(other.time) && signal.containsAll(other.signal); - - } - -} \ No newline at end of file + private int count; + + private List time; + private List signal; + + private String name; + + protected AbstractData(List time, String name) { + this.time = time; + this.count = time.size(); + this.name = name; + } + + /** + * Copy constructor. Copies all data and assigns the same name to + * {@code this}. + * + * @param d another instance of this class + */ + public AbstractData(AbstractData d) { + this.time = new ArrayList<>(d.time); + this.signal = new ArrayList<>(d.signal); + this.count = d.count; + this.name = d.name; + } + + /** + * Creates an {@code AbstractData} with the default number of points (set in + * the corresponding XML file). + */ + public AbstractData() { + this(def(NUMPOINTS)); + } + + /** + * Creates a {@code AbstractData}, where the number of elements in the + * {@code time} and {@code temperature} collections are set to + * {@code count.getValue()}. + *

+ * + * @param count The {@code NumericProperty} that is derived from the + * {@code NumericPropertyKeyword.NUMPOINTS}. + */ + public AbstractData(NumericProperty count) { + setNumPoints(count); + time = new ArrayList<>(this.count); + signal = new ArrayList<>(this.count); + } + + /** + * The actual number of points, explicitly calculated as the size of the + * internal lists. + * + * @return an integer size equal to the real number of elements (pairs) + */ + public int actualNumPoints() { + return time.size(); + } + + /** + * Clears all elements from the three {@code List} objects, thus releasing + * memory. + */ + public void clear() { + this.time.clear(); + this.signal.clear(); + } + + /** + * Getter method providing accessibility to the + * {@code count NumericProperty}. + * + * @return a {@code NumericProperty} derived from + * {@code NumericPropertyKeyword.NUMPOINTS} with the value of {@code count} + */ + public NumericProperty getNumPoints() { + return derive(NUMPOINTS, count); + } + + /** + * Sets the number of points for this baseline. + *

+ * The {@code List} data objects, containing time, temperature, and + * baseline-subtracted temperature are filled with zeroes. + * + * @param c + */ + public void setNumPoints(NumericProperty c) { + requireType(c, NUMPOINTS); + this.count = (int) c.getValue(); + firePropertyChanged(this, c); + } + + /** + * Retrieves an element from the {@code time List} specified by + * {@code index} + * + * @param index the index of the element to be returned + * @return a time value corresponding to {@code index} + */ + public double timeAt(int index) { + return time.get(index); + } + + /** + * Retrieves the last element of the {@code time List}. This is used e.g. by + * the {@code DifferenceScheme} to set the calculation limit for the + * finite-difference scheme. + * + * @see pulse.problem.schemes.DifferenceScheme + * @return a double, equal to the last element of the {@code time List}. + */ + public double timeLimit() { + return timeAt(time.size() - 1); + } + + /** + * Retrieves the signal value corresponding to the index {@code index}. Is + * overriden by subclasses. + * + * @param index the index of the element + * @return a double, representing the signal at {@code index} + */ + public double signalAt(int index) { + return signal.get(index); + } + + /** + * Adds a time-signal pair to the lists. + * + * @param time the time value + * @param sgn the signal value at {@code time} + */ + public void addPoint(double time, double sgn) { + this.time.add(time); + this.signal.add(sgn); + } + + protected void incrementCount() { + count++; + } + + /** + * Sets the time {@code t} at the position {@code index} of the + * {@code time List}. + * + * @param index the index + * @param t the new time value at this index + */ + public void setTimeAt(int index, double t) { + time.set(index, t); + } + + /** + * Sets the signal {@code t} at the position {@code index} of the + * {@code signal List}. + * + * @param index the index + * @param t the new signal value at this index + */ + public void setSignalAt(int index, double t) { + signal.set(index, t); + } + + /** + * Calculates the simple maximum signal. + * + * @return the maximum signal value + * @see java.util.Collections.max + */ + public double apparentMaximum() { + return max(signal); + } + + /** + * Checks if the time list is incomplete. + * + * @return {@code false} if the list with time values has less elements than + * initially declared, {@code true} otherwise. + */ + public boolean isIncomplete() { + return time.size() < count; + } + + @Override + public String toString() { + return name != null ? name : getClass().getSimpleName() + " (" + getNumPoints() + ")"; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Provides general setter accessibility for the number of points of this + * {@code AbstractData}. + * + * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} + * @param property the property of the type + * {@code NumericPropertyKeyword.NUMPOINTS} + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NUMPOINTS) { + setNumPoints(property); + } + } + + /** + * Lists {@code NUM_POINTS} as an accessible property of this + * {@code PropertyHolder}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NUMPOINTS); + return set; + } + + /** + * Removes a time-value pair that is present under the index {@code i}. + * + * @param i the element to be removed + */ + public void remove(int i) { + this.time.remove(i); + this.signal.remove(i); + } + + /** + * @return true + */ + @Override + public boolean ignoreSiblings() { + return true; + } + + public List getTimeSequence() { + return time; + } + + public List getSignalData() { + return signal; + } + + /** + * @return {@code true} only if {@code o} is an {@code AbstractData} + * containing all the elements of the time and signal lists of {@code this} + * object. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof AbstractData)) { + return false; + } + + var other = (AbstractData) o; + + final double EPS = 1e-8; + + if (abs(count - (Integer) other.getNumPoints().getValue()) > EPS) { + return false; + } + + if (signal.hashCode() != other.signal.hashCode()) { + return false; + } + + if (time.hashCode() != other.time.hashCode()) { + return false; + } + + return time.containsAll(other.time) && signal.containsAll(other.signal); + + } + +} diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 8d36cfef..81112127 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; @@ -24,337 +25,345 @@ import pulse.properties.Property; /** - * The {@code HeatingCurve} represents a time-temperature profile (a {@code AbstractData} instance) - * generated using a calculation algorithm implemented by a {@code Problem}'s {@code Solver}. In addition - * to the time and signal lists defined in the super-class, it features baseline-corrected signal values - * stored in a separate list.The {@code HeatingCurve} may have {@code HeatingCurveListener}s to process - * simple events. To enable comparison with {@code ExperimentalData}, a {@code HeatingCurve} builds a - * spline interpolation of its time - baseline-adjusted signal values, thus representing a continuous curve, - * rather than just a collection of discrete data. - * @see pulse.HeatingCurveListener + * The {@code HeatingCurve} represents a time-temperature profile (a + * {@code AbstractData} instance) generated using a calculation algorithm + * implemented by a {@code Problem}'s {@code Solver}. In addition to the time + * and signal lists defined in the super-class, it features baseline-corrected + * signal values stored in a separate list.The {@code HeatingCurve} may have + * {@code HeatingCurveListener}s to process simple events. To enable comparison + * with {@code ExperimentalData}, a {@code HeatingCurve} builds a spline + * interpolation of its time - baseline-adjusted signal values, thus + * representing a continuous curve, rather than just a collection of discrete + * data. + * + * @see pulse.HeatingCurveListener * @see org.apache.commons.math3.analysis.interpolation.UnivariateInterpolation * */ - public class HeatingCurve extends AbstractData { - private List adjustedSignal; - private double startTime; - - private List listeners = new ArrayList(); - - private UnivariateInterpolator splineInterpolator; - private UnivariateFunction splineInterpolation; - - protected HeatingCurve(List time, List signal, final double startTime, String name) { - super(time, name); - this.adjustedSignal = signal; - this.startTime = startTime; - } - - /** - * Calls the super-constructor and initialises the baseline-corrected signal list. Creates a {@code SplineInterpolator} object. - */ - - public HeatingCurve() { - super(); - adjustedSignal = new ArrayList((int)this.getNumPoints().getValue()); - splineInterpolator = new SplineInterpolator(); - } - - /** - * Copy constructor. In addition to copying the data, also re-builds the splines. - * @param c another instance of this class - * @see refreshInterpolation() - */ - - public HeatingCurve(HeatingCurve c) { - super(c); - this.adjustedSignal = new ArrayList<>(c.adjustedSignal); - this.startTime = c.startTime; - splineInterpolator = new SplineInterpolator(); - if(c.splineInterpolation != null) - this.refreshInterpolation(); - } - - /** - * Creates a {@code HeatingCurve}, where the number of elements in the - * {@code time}, {@code signal}, and {@code adjustedSignal} collections are set to - * {@code count.getValue()}. The time shift is initialized with a default value. - * - * @param count The {@code NumericProperty} that is derived from the - * {@code NumericPropertyKeyword.NUMPOINTS}. - */ - - public HeatingCurve(NumericProperty count) { - super(count); - setPrefix("Solution"); - - adjustedSignal = new ArrayList<>((int)count.getValue()); - startTime = (double) def(TIME_SHIFT).getValue(); - - splineInterpolator = new SplineInterpolator(); - } - - @Override - public void clear() { - super.clear(); - this.adjustedSignal.clear(); - } - - /** - * Retrieves the time from the stored list of values, adding the value of {@code startTime} to the result. - * @return time at {@code index} + startTime - * - */ - - @Override - public double timeAt(int index) { - return super.timeAt(index) + startTime; - } - - /** - * Retrieves the baseline-corrected temperature corresponding to - * {@code index} in the respective {@code List}. - * - * @param index the index of the element - * @return a double, representing the baseline-corrected temperature at - * {@code index} - */ - - public double signalAt(int index) { - return adjustedSignal.get(index); - } - - /** - * Scales the temperature values by a factor of {@code scale}. - *

- * This is done by manually setting each temperature value to {@code T*scale}, - * where T is the current temperature value at this index. Finally. applies the - * baseline to the scaled temperature values. - *

- * This method is used in the DifferenceScheme subclasses when a dimensionless - * solution needs to be re-scaled to the given maximum temperature (usually - * matching the {@code ExperimentalData}, but also used as a search variable by - * the {@code SearchTask}. - *

- * Triggers a {@code RESCALED} {@code CurveEvent}. - *

- * - * @param scale the scale - * @see pulse.problem.schemes.DifferenceScheme - * @see pulse.problem.statements.Problem - * @see pulse.tasks.SearchTask - * @see pulse.input.listeners.CurveEvent - */ - - public void scale(double scale) { - var signal = getSignalData(); - final int count = this.actualNumPoints(); - for (int i = 0; i < count; i++) - signal.set(i, signal.get(i) * scale); - var dataEvent = new CurveEvent(RESCALED, this); - fireCurveEvent(dataEvent); - } - - private void refreshInterpolation() { - - /* + private List adjustedSignal; + private double startTime; + + private List listeners = new ArrayList(); + + private UnivariateInterpolator splineInterpolator; + private UnivariateFunction splineInterpolation; + + protected HeatingCurve(List time, List signal, final double startTime, String name) { + super(time, name); + this.adjustedSignal = signal; + this.startTime = startTime; + } + + /** + * Calls the super-constructor and initialises the baseline-corrected signal + * list. Creates a {@code SplineInterpolator} object. + */ + public HeatingCurve() { + super(); + adjustedSignal = new ArrayList((int) this.getNumPoints().getValue()); + splineInterpolator = new SplineInterpolator(); + } + + /** + * Copy constructor. In addition to copying the data, also re-builds the + * splines. + * + * @param c another instance of this class + * @see refreshInterpolation() + */ + public HeatingCurve(HeatingCurve c) { + super(c); + this.adjustedSignal = new ArrayList<>(c.adjustedSignal); + this.startTime = c.startTime; + splineInterpolator = new SplineInterpolator(); + if (c.splineInterpolation != null) { + this.refreshInterpolation(); + } + } + + /** + * Creates a {@code HeatingCurve}, where the number of elements in the + * {@code time}, {@code signal}, and {@code adjustedSignal} collections are + * set to {@code count.getValue()}. The time shift is initialized with a + * default value. + * + * @param count The {@code NumericProperty} that is derived from the + * {@code NumericPropertyKeyword.NUMPOINTS}. + */ + public HeatingCurve(NumericProperty count) { + super(count); + setPrefix("Solution"); + + adjustedSignal = new ArrayList<>((int) count.getValue()); + startTime = (double) def(TIME_SHIFT).getValue(); + + splineInterpolator = new SplineInterpolator(); + } + + @Override + public void clear() { + super.clear(); + this.adjustedSignal.clear(); + } + + /** + * Retrieves the time from the stored list of values, adding the value of + * {@code startTime} to the result. + * + * @return time at {@code index} + startTime + * + */ + @Override + public double timeAt(int index) { + return super.timeAt(index) + startTime; + } + + /** + * Retrieves the baseline-corrected temperature corresponding to + * {@code index} in the respective {@code List}. + * + * @param index the index of the element + * @return a double, representing the baseline-corrected temperature at + * {@code index} + */ + public double signalAt(int index) { + return adjustedSignal.get(index); + } + + /** + * Scales the temperature values by a factor of {@code scale}. + *

+ * This is done by manually setting each temperature value to + * {@code T*scale}, where T is the current temperature value at this index. + * Finally. applies the baseline to the scaled temperature values. + *

+ * This method is used in the DifferenceScheme subclasses when a + * dimensionless solution needs to be re-scaled to the given maximum + * temperature (usually matching the {@code ExperimentalData}, but also used + * as a search variable by the {@code SearchTask}. + *

+ * Triggers a {@code RESCALED} {@code CurveEvent}. + *

+ * + * @param scale the scale + * @see pulse.problem.schemes.DifferenceScheme + * @see pulse.problem.statements.Problem + * @see pulse.tasks.SearchTask + * @see pulse.input.listeners.CurveEvent + */ + public void scale(double scale) { + var signal = getSignalData(); + final int count = this.actualNumPoints(); + for (int i = 0; i < count; i++) { + signal.set(i, signal.get(i) * scale); + } + var dataEvent = new CurveEvent(RESCALED, this); + fireCurveEvent(dataEvent); + } + + private void refreshInterpolation() { + + /* * Prepare extended time array - */ + */ + var time = this.getTimeSequence(); + var timeExtended = new double[time.size() + 1]; - var time = this.getTimeSequence(); - var timeExtended = new double[time.size() + 1]; + for (int i = 1; i < timeExtended.length; i++) { + timeExtended[i] = timeAt(i - 1); + } - for (int i = 1; i < timeExtended.length; i++) - timeExtended[i] = timeAt(i - 1); + final double dt = timeExtended[2] - timeExtended[1]; + timeExtended[0] = timeExtended[1] - dt; // extrapolate linearly - final double dt = timeExtended[2] - timeExtended[1]; - timeExtended[0] = timeExtended[1] - dt; // extrapolate linearly - - /* + /* * Prepare extended signal array - */ - - var adjustedSignalExtended = new double[adjustedSignal.size() + 1]; + */ + var adjustedSignalExtended = new double[adjustedSignal.size() + 1]; - for (int i = 1; i < timeExtended.length; i++) - adjustedSignalExtended[i] = signalAt(i - 1); + for (int i = 1; i < timeExtended.length; i++) { + adjustedSignalExtended[i] = signalAt(i - 1); + } - final double alpha = -1.0; - adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate - // linearly + final double alpha = -1.0; + adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate + // linearly - /* + /* * Submit to spline interpolation - */ - - splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); - } - - /** - * Retrieves the simple maximum (in arbitrary units) of the - * baseline-corrected temperature list. - * - * @return the simple maximum of the baseline-adjusted temperature. - */ - - public double maxAdjustedSignal() { - return max(adjustedSignal); - } - - /** - * Adds the baseline value to each element of the {@code signal} - * list. - *

- * The {@code baseline.valueAt} method is explicitly invoked for all {@code time} values, - * and the result of adding the baseline value to the corresponding - * {@code signal} is assigned to a position in the {@code adjustedSignal} list. - *

- * - * @param baseline the baseline. Note it may not specifically belong to this - * heating curve. - */ - - public void apply(Baseline baseline) { - var time = this.getTimeSequence(); - var signal = this.getSignalData(); - adjustedSignal.clear(); - for (int i = 0, size = time.size(); i < size; i++) - adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); - - if (time.get(0) > -startTime) { - time.add(0, -startTime); - adjustedSignal.add(0, baseline.valueAt(-startTime)); - } - - refreshInterpolation(); - } - - /** - * This creates a new {@code HeatingCurve} to match the time boundaries of the - * {@code data}. - *

- * Curves derived in this way are called extended and are used primarily - * to visually inspect how the calculated baseline correlates with the - * {@code data} at times {@code t < 0}. This method is not used in any - * calculation and is introduced primarily because the search for the reverse - * solution of the heat problems only regards time value at - * t0, whereas in reality it may - * not be consistent with the experimental baseline value at {@code t < 0}. - *

- * - * @param data the experimental data, with a time range broader than the time - * range of this {@code HeatingCurve}. - * @return a new {@code HeatingCurve}, extended to match the time limits of - * {@code data} - */ - - public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { - - int dataStartIndex = data.getIndexRange().getLowerBound(); - - if (dataStartIndex < 1) // no extension required - return this; - - var baselineTime = data.getTimeSequence().stream().filter(t -> t < 0).collect(toList()); - var baselineSignal = baselineTime.stream().map(bTime -> baseline.valueAt(bTime)).collect(toList()); - - var time = this.getTimeSequence(); - - baselineTime.addAll(time); - baselineSignal.addAll(adjustedSignal); - - return new HeatingCurve(baselineTime, baselineSignal, startTime, getName()); - } - - /** - * Calls {@code super.set} and provides write access to the {@code TIME_SHIFT} property. - * - * @param property the property of the type - * {@code NumericPropertyKeyword.NUMPOINTS} - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == TIME_SHIFT) - setTimeShift(property); - } - - /** - * Lists {@code TIME_SHIFT} and {@code NUM_POINTS}. - */ - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(TIME_SHIFT)); - return list; - } - - /** - * Removes an element with the index {@code i} from all three {@code List}s - * (time, signal, and baseline-corrected signal). - * - * @param i the element to be removed - */ - - public void remove(int i) { - super.remove(i); - this.adjustedSignal.remove(i); - } - - /** - * The time shift is the position of the 'zero-time'. - * @return a {@code TIME_SHIFT} property - */ - - public NumericProperty getTimeShift() { - return derive(TIME_SHIFT, startTime); - } - - /** - * Sets the time shift and triggers {@code TIME_ORIGIN_CHANGED} in {@code CurveEvent}. Triggers the - * {@code firePropertyChanged}. - * @param startTime the new start time value - */ - - public void setTimeShift(NumericProperty startTime) { - requireType(startTime, TIME_SHIFT); - this.startTime = (double) startTime.getValue(); - var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED, this); - fireCurveEvent(dataEvent); - firePropertyChanged(this, startTime); - } - - public UnivariateFunction getSplineInterpolation() { - return splineInterpolation; - } - - public List getBaselineCorrectedData() { - return adjustedSignal; - } - - public void addHeatingCurveListener(HeatingCurveListener l) { - this.listeners.add(l); - } - - public void removeHeatingCurveListeners() { - listeners.clear(); - } - - private void fireCurveEvent(CurveEvent event) { - for (HeatingCurveListener l : listeners) - l.onCurveEvent(event); - } - - @Override - public boolean equals(Object o) { - if(! (o instanceof HeatingCurve )) - return false; - - return super.equals(o) && adjustedSignal.containsAll( ((HeatingCurve)o).adjustedSignal); - } - -} \ No newline at end of file + */ + splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); + } + + /** + * Retrieves the simple maximum (in arbitrary units) of the + * baseline-corrected temperature list. + * + * @return the simple maximum of the baseline-adjusted temperature. + */ + public double maxAdjustedSignal() { + return max(adjustedSignal); + } + + /** + * Adds the baseline value to each element of the {@code signal} list. + *

+ * The {@code baseline.valueAt} method is explicitly invoked for all + * {@code time} values, and the result of adding the baseline value to the + * corresponding {@code signal} is assigned to a position in the + * {@code adjustedSignal} list. + *

+ * + * @param baseline the baseline. Note it may not specifically belong to this + * heating curve. + */ + public void apply(Baseline baseline) { + var time = this.getTimeSequence(); + var signal = this.getSignalData(); + adjustedSignal.clear(); + for (int i = 0, size = time.size(); i < size; i++) { + adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); + } + + if (time.get(0) > -startTime) { + time.add(0, -startTime); + adjustedSignal.add(0, baseline.valueAt(-startTime)); + } + + refreshInterpolation(); + } + + /** + * This creates a new {@code HeatingCurve} to match the time boundaries of + * the {@code data}. + *

+ * Curves derived in this way are called extended and are used + * primarily to visually inspect how the calculated baseline correlates with + * the {@code data} at times {@code t < 0}. This method is not used in any + * calculation and is introduced primarily because the search for the + * reverse solution of the heat problems only regards time value at + * t0, whereas in reality it + * may not be consistent with the experimental baseline value at + * {@code t < 0}. + *

+ * + * @param data the experimental data, with a time range broader than the + * time range of this {@code HeatingCurve}. + * @return a new {@code HeatingCurve}, extended to match the time limits of + * {@code data} + */ + public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { + + int dataStartIndex = data.getIndexRange().getLowerBound(); + + if (dataStartIndex < 1) // no extension required + { + return this; + } + + var baselineTime = data.getTimeSequence().stream().filter(t -> t < 0).collect(toList()); + var baselineSignal = baselineTime.stream().map(bTime -> baseline.valueAt(bTime)).collect(toList()); + + var time = this.getTimeSequence(); + + baselineTime.addAll(time); + baselineSignal.addAll(adjustedSignal); + + return new HeatingCurve(baselineTime, baselineSignal, startTime, getName()); + } + + /** + * Calls {@code super.set} and provides write access to the + * {@code TIME_SHIFT} property. + * + * @param property the property of the type + * {@code NumericPropertyKeyword.NUMPOINTS} + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == TIME_SHIFT) { + setTimeShift(property); + } + } + + /** + * @return {@code TIME_SHIFT} and {@code NUM_POINTS}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TIME_SHIFT); + return set; + } + + /** + * Removes an element with the index {@code i} from all three {@code List}s + * (time, signal, and baseline-corrected signal). + * + * @param i the element to be removed + */ + @Override + public void remove(int i) { + super.remove(i); + this.adjustedSignal.remove(i); + } + + /** + * The time shift is the position of the 'zero-time'. + * + * @return a {@code TIME_SHIFT} property + */ + public NumericProperty getTimeShift() { + return derive(TIME_SHIFT, startTime); + } + + /** + * Sets the time shift and triggers {@code TIME_ORIGIN_CHANGED} in + * {@code CurveEvent}. Triggers the {@code firePropertyChanged}. + * + * @param startTime the new start time value + */ + public void setTimeShift(NumericProperty startTime) { + requireType(startTime, TIME_SHIFT); + this.startTime = (double) startTime.getValue(); + var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED, this); + fireCurveEvent(dataEvent); + firePropertyChanged(this, startTime); + } + + public UnivariateFunction getSplineInterpolation() { + return splineInterpolation; + } + + public List getBaselineCorrectedData() { + return adjustedSignal; + } + + public void addHeatingCurveListener(HeatingCurveListener l) { + this.listeners.add(l); + } + + @Override + public void removeHeatingCurveListeners() { + listeners.clear(); + } + + private void fireCurveEvent(CurveEvent event) { + for (HeatingCurveListener l : listeners) { + l.onCurveEvent(event); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof HeatingCurve)) { + return false; + } + + return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); + } + +} diff --git a/src/main/java/pulse/HeatingCurveListener.java b/src/main/java/pulse/HeatingCurveListener.java index bfa5bbe8..dd2219fe 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -6,13 +6,11 @@ * An interface used to listen to data events related to {@code HeatingCurve}. * */ - public interface HeatingCurveListener { - /** - * Signals that a {@code CurveEvent} has occurred. - */ - - public void onCurveEvent(CurveEvent event); + /** + * Signals that a {@code CurveEvent} has occurred. + */ + public void onCurveEvent(CurveEvent event); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/baseline/package-info.java b/src/main/java/pulse/baseline/package-info.java index 20542d43..103a7c31 100644 --- a/src/main/java/pulse/baseline/package-info.java +++ b/src/main/java/pulse/baseline/package-info.java @@ -2,5 +2,4 @@ * Contains classes for describing and evaluating the baseline signal of a * {@code HeatingCurve} or its subclasses. */ - -package pulse.baseline; \ No newline at end of file +package pulse.baseline; diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index cb817404..742d77e6 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -11,218 +11,209 @@ * Essentially, an object of this class contains an ordered pair representing * the associated indices. Works in conjunction with the {@code Range} class. *

- * + * * @see pulse.input.Range * */ - public class IndexRange { - private int iStart; - private int iEnd; - - /** - * Construct an empty index range where the start index is set to -1 and the end - * index is set to 0. - */ - - public IndexRange() { - iStart = -1; - iEnd = 0; - } - - /** - * Constructs a new index range for {@code data} based on the dimensional - * {@code range}. - * - * @param data the list to be analysed - * @param range the range object used to define the index range - * - * @see set - */ - - public IndexRange(List data, Range range) { - set(data, range); - } - - /** - * Resets the index range by effectively treating the {@code data} list as - * bounded by its first and last elements, assuming that {@code data} is sorted - * in ascending order. Because of this last assumption, the visibility of this - * method has been set to protected. - * - * @param data a list sorted in ascending order - */ - - protected void reset(List data) { - requireNonNull(data); - int size = data.size(); - - if (size > 0) { - setLowerBound(data, data.get(0)); - setUpperBound(data, data.get(size - 1)); - } - - } - - /** - * Sets the start index by conducting a primitive binary search using - * {@code closest(...)} to find an element in {@code data} either matching or - * being as close as possible to {@code a} (if {@code a} is non-negative) or - * zero. - * - * @param data the list to process - * @param a an element representing the lower bound (not necessarily - * contained in {@code data}). - * @see closestLeft - * @see closestRight - */ - - public void setLowerBound(List data, double a) { - iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); - } - - /** - * Sets the end index by conducting a primitive binary search using - * {@code closest(...)} to find an element in {@code data} either matching or - * being as close as possible to {@code b}. For the above operation, the list is - * searched through from its last to first element (i.e., in reverse order). - * - * @param data the list to process - * @param b an element representing the upper bound (not necessarily - * contained in {@code data}). - * @see closestLeft - * @see closestRight - */ - - public void setUpperBound(List data, double b) { - iEnd = closestRight(b, data); - } - - /** - * Sets the bounds of this index range using the minimum and maximum values of - * the segment specified in the {@code range} object. If the minimum bound is - * negative, it will be ignored and replaced by 0.0. - * - * @param data the data list to be processed - * @param range a range with minimum and maximum values - * @see setLowerBound - * @see setUpperBound - */ - - public void set(List data, Range range) { - var segment = range.getSegment(); - setLowerBound(data, Math.max(0.0, segment.getMinimum())); - setUpperBound(data, segment.getMaximum()); - } - - /** - * Gets the integer value representing the index of the lower bound previously - * set by looking at a certain unspecified data list. - * - * @return the start index - */ - - public int getLowerBound() { - return iStart; - } - - /** - * Gets the integer value representing the index of the upper bound previously - * set by looking at a certain unspecified data list. - * - * @return the end index - */ - - public int getUpperBound() { - return iEnd; - } - - /** - * Checks if this index range is viable. - * - * @return {@code true} if the upper bound is positive and greater than the - * lower bound, {@code false} otherwise. - */ - - public boolean isValid() { - return (iStart < iEnd && iEnd > 0); - } - - /** - * Searches through the elements contained in the the second argument of this - * method to find an element belonging to {@code in} most closely resembling the - * first argument. The search is completed once {@code of} lies between any two - * adjacent elements of {@code in}. The result is then the index of the - * preceding element. - * - * @param of an element which will be compared against - * @param in a list of data presumably containing an element similar to - * {@code of} - * @return - *

- * any integer greater than 0 and lesser than {@code in.size} that - * matches the above criterion. If {@code of} is greater than the last - * element of {@code in}, this will return the latter. Otherwise, if no - * element matching the criterion is found, returns 0. - *

- */ - - public static int closestLeft(double of, List in) { - return closest(of, in, false); - } - - /** - * Searches through the elements contained in the the second argument of this - * method to find an element belonging to {@code in} most closely resembling the - * first argument. The search utilises a reverse order, i.e. it starts from the - * last element and goes to the first. The search is completed once {@code of} - * lies between any two adjacent elements of {@code in}. The result is then the - * index of the preceding element. - * - * @param of an element which will be compared against - * @param in a list of data presumably containing an element similar to - * {@code of} - * @return - *

- * any integer greater than 0 and lesser than {@code in.size} that - * matches the above criterion. If {@code of} is greater than the last - * element of {@code in}, this will return the latter. Otherwise, if no - * element matching the criterion is found, returns 0. - *

- */ - - public static int closestRight(double of, List in) { - return closest(of, in, true); - } - - private static int closest(double of, List in, boolean reverseOrder) { - int sizeMinusOne = in.size() - 1; - - if (of > in.get(sizeMinusOne)) - return sizeMinusOne; - - int start = reverseOrder ? sizeMinusOne - 1 : 0; - int increment = reverseOrder ? -1 : 1; - - for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { - - if (between(of, in.get(i), in.get(i + 1))) - return i; - - } - - return 0; - - } - - private static boolean between(double x, double minValueInclusive, double maxValueInclusive) { - return (x >= minValueInclusive && x <= maxValueInclusive); - } - - @Override - public String toString() { - return "Index range: from " + iStart + " to " + iEnd; - } - -} \ No newline at end of file + private int iStart; + private int iEnd; + + /** + * Construct an empty index range where the start index is set to -1 and the + * end index is set to 0. + */ + public IndexRange() { + iStart = -1; + iEnd = 0; + } + + /** + * Constructs a new index range for {@code data} based on the dimensional + * {@code range}. + * + * @param data the list to be analysed + * @param range the range object used to define the index range + * + * @see set + */ + public IndexRange(List data, Range range) { + set(data, range); + } + + /** + * Resets the index range by effectively treating the {@code data} list as + * bounded by its first and last elements, assuming that {@code data} is + * sorted in ascending order. Because of this last assumption, the + * visibility of this method has been set to protected. + * + * @param data a list sorted in ascending order + */ + protected void reset(List data) { + requireNonNull(data); + int size = data.size(); + + if (size > 0) { + setLowerBound(data, data.get(0)); + setUpperBound(data, data.get(size - 1)); + } + + } + + /** + * Sets the start index by conducting a primitive binary search using + * {@code closest(...)} to find an element in {@code data} either matching + * or being as close as possible to {@code a} (if {@code a} is non-negative) + * or zero. + * + * @param data the list to process + * @param a an element representing the lower bound (not necessarily + * contained in {@code data}). + * @see closestLeft + * @see closestRight + */ + public void setLowerBound(List data, double a) { + iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); + } + + /** + * Sets the end index by conducting a primitive binary search using + * {@code closest(...)} to find an element in {@code data} either matching + * or being as close as possible to {@code b}. For the above operation, the + * list is searched through from its last to first element (i.e., in reverse + * order). + * + * @param data the list to process + * @param b an element representing the upper bound (not necessarily + * contained in {@code data}). + * @see closestLeft + * @see closestRight + */ + public void setUpperBound(List data, double b) { + iEnd = closestRight(b, data); + } + + /** + * Sets the bounds of this index range using the minimum and maximum values + * of the segment specified in the {@code range} object. If the minimum + * bound is negative, it will be ignored and replaced by 0.0. + * + * @param data the data list to be processed + * @param range a range with minimum and maximum values + * @see setLowerBound + * @see setUpperBound + */ + public void set(List data, Range range) { + var segment = range.getSegment(); + setLowerBound(data, Math.max(0.0, segment.getMinimum())); + setUpperBound(data, segment.getMaximum()); + } + + /** + * Gets the integer value representing the index of the lower bound + * previously set by looking at a certain unspecified data list. + * + * @return the start index + */ + public int getLowerBound() { + return iStart; + } + + /** + * Gets the integer value representing the index of the upper bound + * previously set by looking at a certain unspecified data list. + * + * @return the end index + */ + public int getUpperBound() { + return iEnd; + } + + /** + * Checks if this index range is viable. + * + * @return {@code true} if the upper bound is positive and greater than the + * lower bound, {@code false} otherwise. + */ + public boolean isValid() { + return (iStart < iEnd && iEnd > 0); + } + + /** + * Searches through the elements contained in the the second argument of + * this method to find an element belonging to {@code in} most closely + * resembling the first argument. The search is completed once {@code of} + * lies between any two adjacent elements of {@code in}. The result is then + * the index of the preceding element. + * + * @param of an element which will be compared against + * @param in a list of data presumably containing an element similar to + * {@code of} + * @return + *

+ * any integer greater than 0 and lesser than {@code in.size} that matches + * the above criterion. If {@code of} is greater than the last element of + * {@code in}, this will return the latter. Otherwise, if no element + * matching the criterion is found, returns 0. + *

+ */ + public static int closestLeft(double of, List in) { + return closest(of, in, false); + } + + /** + * Searches through the elements contained in the the second argument of + * this method to find an element belonging to {@code in} most closely + * resembling the first argument. The search utilises a reverse order, i.e. + * it starts from the last element and goes to the first. The search is + * completed once {@code of} lies between any two adjacent elements of + * {@code in}. The result is then the index of the preceding element. + * + * @param of an element which will be compared against + * @param in a list of data presumably containing an element similar to + * {@code of} + * @return + *

+ * any integer greater than 0 and lesser than {@code in.size} that matches + * the above criterion. If {@code of} is greater than the last element of + * {@code in}, this will return the latter. Otherwise, if no element + * matching the criterion is found, returns 0. + *

+ */ + public static int closestRight(double of, List in) { + return closest(of, in, true); + } + + private static int closest(double of, List in, boolean reverseOrder) { + int sizeMinusOne = Math.max( in.size() - 1, 0); //has to be non-negative + + if (of > in.get(sizeMinusOne)) { + return sizeMinusOne; + } + + int start = reverseOrder ? sizeMinusOne - 1 : 0; + int increment = reverseOrder ? -1 : 1; + + for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { + + if (between(of, in.get(i), in.get(i + 1))) { + return i; + } + + } + + return 0; + + } + + private static boolean between(double x, double minValueInclusive, double maxValueInclusive) { + return (x >= minValueInclusive && x <= maxValueInclusive); + } + + @Override + public String toString() { + return "Index range: from " + iStart + " to " + iEnd; + } + +} diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index db990f12..a0b69f9b 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -4,47 +4,44 @@ /** * A {@code CurveEvent} is associated with an {@code HeatingCurve} object. + * * @see pulse.HeatingCurve * */ - public class CurveEvent { - private CurveEventType type; - private HeatingCurve data; - - /** - * Constructs a {@code CurveEvent} object, combining the {@code type} and - * associated {@code data} - * - * @param type the type of this event - * @param data the source of the event - */ - - public CurveEvent(CurveEventType type, HeatingCurve data) { - this.type = type; - this.data = data; - } - - /** - * Used to get the type of this event. - * - * @return the type of this event - */ - - public CurveEventType getType() { - return type; - } - - /** - * Used to get the {@code HeatingCurve} object that has undergone certain - * changes specified by this event type. - * - * @return the associated data - */ - - public HeatingCurve getData() { - return data; - } - -} \ No newline at end of file + private CurveEventType type; + private HeatingCurve data; + + /** + * Constructs a {@code CurveEvent} object, combining the {@code type} and + * associated {@code data} + * + * @param type the type of this event + * @param data the source of the event + */ + public CurveEvent(CurveEventType type, HeatingCurve data) { + this.type = type; + this.data = data; + } + + /** + * Used to get the type of this event. + * + * @return the type of this event + */ + public CurveEventType getType() { + return type; + } + + /** + * Used to get the {@code HeatingCurve} object that has undergone certain + * changes specified by this event type. + * + * @return the associated data + */ + public HeatingCurve getData() { + return data; + } + +} diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 38da9b01..65b364e2 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -4,23 +4,20 @@ * An event type associated with an {@code HeatingCurve} object. * */ - public enum CurveEventType { - /** - * Indicates the curve signal values have been re-scaled. This means that - * each signal value has been multiplied by a single number. - */ - - RESCALED, - - /** - * Indicates a new time shift is introduced between the time sequences of a {@code HeatingCurve} and - * its linked {@code ExperimentalData}. Triggered either when manually changing - * the time origin of the solution (i.e., shifting it relative to the - * experimental data points) or by the search procedure. - */ - - TIME_ORIGIN_CHANGED; - -} \ No newline at end of file + /** + * Indicates the curve signal values have been re-scaled. This means that + * each signal value has been multiplied by a single number. + */ + RESCALED, + /** + * Indicates a new time shift is introduced between the time sequences of a + * {@code HeatingCurve} and its linked {@code ExperimentalData}. Triggered + * either when manually changing the time origin of the solution (i.e., + * shifting it relative to the experimental data points) or by the search + * procedure. + */ + TIME_ORIGIN_CHANGED; + +} diff --git a/src/main/java/pulse/input/listeners/DataEvent.java b/src/main/java/pulse/input/listeners/DataEvent.java index 569ff56d..f2b8ca4e 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -7,44 +7,40 @@ * {@code ExperimentalData}. * */ - public class DataEvent { - private DataEventType type; - private ExperimentalData data; - - /** - * Constructs a {@code DataEvent} object, combining the {@code type} and - * associated {@code data} - * - * @param type the type of this event - * @param data the source of the event - */ - - public DataEvent(DataEventType type, ExperimentalData data) { - this.type = type; - this.data = data; - } - - /** - * Used to get the type of this event. - * - * @return the type of this event - */ - - public DataEventType getType() { - return type; - } - - /** - * Used to get the {@code ExperimentalData} object that has undergone certain - * changes specified by this event type. - * - * @return the associated data - */ - - public ExperimentalData getData() { - return data; - } + private DataEventType type; + private ExperimentalData data; + + /** + * Constructs a {@code DataEvent} object, combining the {@code type} and + * associated {@code data} + * + * @param type the type of this event + * @param data the source of the event + */ + public DataEvent(DataEventType type, ExperimentalData data) { + this.type = type; + this.data = data; + } + + /** + * Used to get the type of this event. + * + * @return the type of this event + */ + public DataEventType getType() { + return type; + } + + /** + * Used to get the {@code ExperimentalData} object that has undergone + * certain changes specified by this event type. + * + * @return the associated data + */ + public ExperimentalData getData() { + return data; + } } diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index e3d48138..0423e378 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -4,17 +4,16 @@ * An event type that is associated with an {@code ExperimentalData} object. * */ - public enum DataEventType { - /** - *

- * The {@code RANGE_CHANGED} {@code DataEventType} indicates the range of the - * {@code ExperimentalData} has either been truncated or extended. Note this means that only the - * range is affected and not the data itself. - * - * @see pulse.input.ExperimentalData.truncate() - */ + /** + *

+ * The {@code RANGE_CHANGED} {@code DataEventType} indicates the range of + * the {@code ExperimentalData} has either been truncated or extended. Note + * this means that only the range is affected and not the data itself. + * + * @see pulse.input.ExperimentalData.truncate() + */ - RANGE_CHANGED + RANGE_CHANGED -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/DataListener.java b/src/main/java/pulse/input/listeners/DataListener.java index 14af8220..cb1f3d4c 100644 --- a/src/main/java/pulse/input/listeners/DataListener.java +++ b/src/main/java/pulse/input/listeners/DataListener.java @@ -5,17 +5,15 @@ * with an {@code ExperimentalData} object. * */ - public interface DataListener { - /** - * Triggered when a certain {@code DataEvent} specified by its - * {@code DataEventType} is initiated from within the {@code ExperimentalData} - * object. - * - * @param e the event object. - */ - - public void onDataChanged(DataEvent e); + /** + * Triggered when a certain {@code DataEvent} specified by its + * {@code DataEventType} is initiated from within the + * {@code ExperimentalData} object. + * + * @param e the event object. + */ + public void onDataChanged(DataEvent e); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java index 5279f846..1bb85392 100644 --- a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -3,19 +3,17 @@ import pulse.input.InterpolationDataset.StandartType; /** - * A listener associated with the {@code InterpolationDataset} static repository of interpolations. + * A listener associated with the {@code InterpolationDataset} static repository + * of interpolations. * */ - public interface ExternalDatasetListener { - - - /** - * Triggered when a data {@code type} has been loaded. - * @param type a type of the dataset, for which an interpolation is created. - */ - - public void onDataLoaded(StandartType type); + /** + * Triggered when a data {@code type} has been loaded. + * + * @param type a type of the dataset, for which an interpolation is created. + */ + public void onDataLoaded(StandartType type); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/package-info.java b/src/main/java/pulse/input/listeners/package-info.java index e332df25..af353d56 100644 --- a/src/main/java/pulse/input/listeners/package-info.java +++ b/src/main/java/pulse/input/listeners/package-info.java @@ -2,8 +2,8 @@ * Package contains listeners and event types which are used to track any * runtime changes with the internal data structures defined in * {@code pulse} and {@code pulse.input}. + * * @see pulse - * @see pulse.input + * @see pulse.input */ - -package pulse.input.listeners; \ No newline at end of file +package pulse.input.listeners; diff --git a/src/main/java/pulse/input/package-info.java b/src/main/java/pulse/input/package-info.java index 31eb4608..6590dcad 100644 --- a/src/main/java/pulse/input/package-info.java +++ b/src/main/java/pulse/input/package-info.java @@ -4,5 +4,4 @@ * metadata, and property curves (e.g. specific heat and density); (b) are used * by the {@code TaskManager} or any affiliated class. */ - -package pulse.input; \ No newline at end of file +package pulse.input; diff --git a/src/main/java/pulse/io/export/CurveExporter.java b/src/main/java/pulse/io/export/CurveExporter.java index 60835fd5..81636051 100644 --- a/src/main/java/pulse/io/export/CurveExporter.java +++ b/src/main/java/pulse/io/export/CurveExporter.java @@ -10,123 +10,120 @@ import pulse.AbstractData; /** - * A singleton exporter allows writing the data contained in a {@code AbstractData} object in - * a two-column format to create files conforming to either csv or html - * extension. The first column always represents the time sequence, which may be - * shifted if the associated property of the heating curve is non-zero. The - * second column represents the baseline-adjusted signal. + * A singleton exporter allows writing the data contained in a + * {@code AbstractData} object in a two-column format to create files conforming + * to either csv or html extension. The first column always represents the time + * sequence, which may be shifted if the associated property of the heating + * curve is non-zero. The second column represents the baseline-adjusted signal. * */ - public class CurveExporter implements Exporter { - private static CurveExporter instance = new CurveExporter(); - - private CurveExporter() { - // Intentionally blank - } - - @Override - public void printToStream(AbstractData hc, FileOutputStream fos, Extension extension) { - if (hc.actualNumPoints() < 1) - return; - - switch (extension) { - case HTML: - printHTML(hc, fos); - break; - case CSV: - printCSV(hc, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - /** - * Currently {@code html} and {@code csv} extensions are supported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printHTML(AbstractData hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(getString("ResultTableExporter.style")); - stream.print("Time-temperature profile"); - stream.print(""); - - final String TIME_LABEL = getString("HeatingCurve.6"); - final String TEMPERATURE_LABEL = getString("HeatingCurve.7"); - - stream.print("" + TIME_LABEL + "\t"); - stream.print("" + TEMPERATURE_LABEL + "\t"); - - stream.print(""); - - double t; - double T; - - final int size = hc.actualNumPoints(); - - for (int i = 0; i < size; i++) { - stream.print(""); - - stream.print(""); - t = hc.timeAt(i); - stream.printf("%.8f %n", t); - stream.print("\t"); - T = hc.signalAt(i); - stream.printf("%.8f %n", T); - - stream.println(""); - } - - stream.print(""); - } - - } - - private void printCSV(AbstractData hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - final String TIME_LABEL = getString("HeatingCurve.6"); - final String TEMPERATURE_LABEL = hc.getPrefix(); - stream.print(TIME_LABEL + "\t" + TEMPERATURE_LABEL + "\t"); - - double t; - double T; - - final int size = hc.actualNumPoints(); - - for (int i = 0; i < size; i++) { - t = hc.timeAt(i); - stream.printf("%n%3.8f", t); - T = hc.signalAt(i); - stream.printf("\t%3.8f", T); - } - } - - } - - /** - * Returns the single instance of this subclass. - * - * @return an instance of {@code HeatingCurveExporter}. - */ - - public static CurveExporter getInstance() { - return instance; - } - - /** - * @return the {@code AbstractData} class. - */ - - @Override - public Class target() { - return AbstractData.class; - } - -} \ No newline at end of file + private static CurveExporter instance = new CurveExporter(); + + private CurveExporter() { + // Intentionally blank + } + + @Override + public void printToStream(AbstractData hc, FileOutputStream fos, Extension extension) { + if (hc.actualNumPoints() < 1) { + return; + } + + switch (extension) { + case HTML: + printHTML(hc, fos); + break; + case CSV: + printCSV(hc, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + /** + * Currently {@code html} and {@code csv} extensions are supported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + private void printHTML(AbstractData hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(getString("ResultTableExporter.style")); + stream.print("Time-temperature profile"); + stream.print(""); + + final String TIME_LABEL = getString("HeatingCurve.6"); + final String TEMPERATURE_LABEL = getString("HeatingCurve.7"); + + stream.print("" + TIME_LABEL + "\t"); + stream.print("" + TEMPERATURE_LABEL + "\t"); + + stream.print(""); + + double t; + double T; + + final int size = hc.actualNumPoints(); + + for (int i = 0; i < size; i++) { + stream.print(""); + + stream.print(""); + t = hc.timeAt(i); + stream.printf("%.8f %n", t); + stream.print("\t"); + T = hc.signalAt(i); + stream.printf("%.8f %n", T); + + stream.println(""); + } + + stream.print(""); + } + + } + + private void printCSV(AbstractData hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + final String TIME_LABEL = getString("HeatingCurve.6"); + final String TEMPERATURE_LABEL = hc.getPrefix(); + stream.print(TIME_LABEL + "\t" + TEMPERATURE_LABEL + "\t"); + + double t; + double T; + + final int size = hc.actualNumPoints(); + + for (int i = 0; i < size; i++) { + t = hc.timeAt(i); + stream.printf("%n%3.8f", t); + T = hc.signalAt(i); + stream.printf("\t%3.8f", T); + } + } + + } + + /** + * Returns the single instance of this subclass. + * + * @return an instance of {@code HeatingCurveExporter}. + */ + public static CurveExporter getInstance() { + return instance; + } + + /** + * @return the {@code AbstractData} class. + */ + @Override + public Class target() { + return AbstractData.class; + } + +} diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 15ada634..ec8c81a1 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -20,199 +20,191 @@ * suitable for a given target and shortcuts for export operations. * */ - public class ExportManager { - private ExportManager() { - // intentionally blank - } - - /** - * Finds a suitable exporter for a non-null {@code target} by calling - * {@code findExporter(target.getClass())}. - * - * @param an instance of {@code Descriptive} - * @param target the exported target - * @return an exporter that works for {@code target} - * @see findExporter - */ - - @SuppressWarnings("unchecked") - public static Exporter findExporter(T target) { - Objects.requireNonNull(target); - return (Exporter) findExporter(target.getClass()); - } - - /** - * Finds an exporter that can work with {@code target}. - *

- * Searches through available instances of the {@code Exporter} class contained in this - * package and checks if any of those have their target set to the argument of - * this method, then returns the first occurrence. If nothing matches exactly the same - * class as specified, searches for exporters of any classes assignable from - * {@code target}. - *

- * - * @param an instance of {@code Descriptive} - * @param target the target glass - * @return an instance of the Exporter class that can work worth the type T, - * null if nothing has been found - */ - - @SuppressWarnings({ "unchecked" }) - public static Exporter findExporter(Class target) { - var allExporters = instancesOf(Exporter.class); - var exporter = allExporters.stream().filter(e -> e.target() == target).findFirst(); - - if (exporter.isPresent()) - return exporter.get(); - else { - exporter = allExporters.stream().filter(e -> e.target().isAssignableFrom(target)).findFirst(); - return exporter.isPresent() ? exporter.get() : null; - } - } - - /** - * Finds an exporter matching to {@code target} and allows the user to select - * the location of export. - * - * @param a {@code Descriptive} type - * @param target the target to be exported - * @param parentWindow a frame to which the file chooser dialog will be - * attached - * @param fileTypeLabel a brief description of the exported file types - * @see findExporter - * @see pulse.io.export.Exporter.askToExport() - * @throws IllegalArgumentException if no exporter can be found - */ - - public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var exporter = findExporter(target); - if (exporter != null) - exporter.askToExport(target, parentWindow, fileTypeLabel); - else - throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); - } - - /** - * Attempts to export the given {@code target} to the {@code directory} by - * saving the contents in a file with the given {@code Extension}. - *

- * The file is formatted according to the inherent format, i.e. if it is an - * {@code Extension.HTML} file, it will contain HTML tags, etc. If - * {@code extension} is not present in the list of supported extension of an - * exporter matching {@code target}, this will revert to the first supported - * extension. This method will not have any result if no exporter has been found - * fot {@code target}. - *

- * - * @param the target type - * @param target the exported target - * @param directory a pre-selected directory - * @param extension the desired extension - */ - - public static void export(T target, File directory, Extension extension) { - var exporter = findExporter(target); - - if (exporter != null) { - var supportedExtensions = exporter.getSupportedExtensions(); - - if (supportedExtensions.length > 0) { - var confirmedExtension = asList(supportedExtensions).contains(extension) ? extension - : supportedExtensions[0]; - exporter.export(target, directory, confirmedExtension); - } - - } - } - - /** - * This will invoke {@code exportGroup} on each task listed by the - * {@code TaskManager}. - * - * @param directory a pre-selected directory - * @param extension the desired extension - * @see exportGroup - * @see pulse.tasks.TaskManager - */ - - public static void exportAllTasks(File directory, Extension extension) { - TaskManager.getManagerInstance().getTaskList().stream().forEach(t -> exportGroup(t, directory, extension)); - } - - /** - * Exports the currently selected task as a group of objects. - * - * @param directory a pre-selected directory - * @param extension the desired extension - * @see exportGroup - * @see pulse.tasks.TaskManager.getSelectedTask() - */ - - public static void exportCurrentTask(File directory, Extension extension) { - exportGroup(TaskManager.getManagerInstance().getSelectedTask(), directory, extension); - } - - /** - * Exports the currently selected task as a group of objects using the default - * export extension. - * - * @param directory a pre-selected directory - * @see exportGroup - * @see pulse.tasks.TaskManager.getSelectedTask() - */ - - public static void exportCurrentTask(File directory) { - exportCurrentTask(directory, getDefaultExportExtension()); - } - - /** - * Exports all results generated previously during task execution for all tasks - * listed by the TaskManager, provided those tasks had the respective result - * assigned to them. - * - * @param directory a pre-selected directory - * @param extension the desired extension - */ - - public static void exportAllResults(File directory, Extension extension) { - - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> t.getStoredCalculations() ).flatMap(x -> x.stream()).filter(Objects::nonNull) - .forEach(r -> export(r, directory, extension)); - - } - - /** - * Fully exports {@code group} and all its contents to the root - * {@code directory} requesting the files to be saved with the - * {@code extension}. - *

- * If an {@code Exporter} exists that accepts the {@code group} as its argument, - * this will create files in the root {@code directory} in accordance to the - * root {@code Exporter} rules. All contents of the {@code group} will then be - * processed in a similar manner and the output will be stored in an internal - * directory, the name of which conforms to the respective description. Note - * this method is NOT recursive and it calls the {@code export} method of the - * {@code ExportManager}. - *

- * - * @param group a group - * @param directory a pre-selected root directory - * @param extension the desired extension - * @throws IllegalArgumentException if {@code directory} is not a directory - */ - - public static void exportGroup(Group group, File directory, Extension extension) { - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var internalDirectory = new File(directory + separator + group.describe() + separator); - internalDirectory.mkdirs(); - - export(group, directory, extension); - contents(group).stream().forEach(internalHolder -> export(internalHolder, internalDirectory, extension)); - } - -} \ No newline at end of file + private ExportManager() { + // intentionally blank + } + + /** + * Finds a suitable exporter for a non-null {@code target} by calling + * {@code findExporter(target.getClass())}. + * + * @param an instance of {@code Descriptive} + * @param target the exported target + * @return an exporter that works for {@code target} + * @see findExporter + */ + @SuppressWarnings("unchecked") + public static Exporter findExporter(T target) { + Objects.requireNonNull(target); + return (Exporter) findExporter(target.getClass()); + } + + /** + * Finds an exporter that can work with {@code target}. + *

+ * Searches through available instances of the {@code Exporter} class + * contained in this package and checks if any of those have their target + * set to the argument of this method, then returns the first occurrence. If + * nothing matches exactly the same class as specified, searches for + * exporters of any classes assignable from {@code target}. + *

+ * + * @param an instance of {@code Descriptive} + * @param target the target glass + * @return an instance of the Exporter class that can work worth the type T, + * null if nothing has been found + */ + @SuppressWarnings({"unchecked"}) + public static Exporter findExporter(Class target) { + var allExporters = instancesOf(Exporter.class); + var exporter = allExporters.stream().filter(e -> e.target() == target).findFirst(); + + if (exporter.isPresent()) { + return exporter.get(); + } else { + exporter = allExporters.stream().filter(e -> e.target().isAssignableFrom(target)).findFirst(); + return exporter.isPresent() ? exporter.get() : null; + } + } + + /** + * Finds an exporter matching to {@code target} and allows the user to + * select the location of export. + * + * @param a {@code Descriptive} type + * @param target the target to be exported + * @param parentWindow a frame to which the file chooser dialog will be + * attached + * @param fileTypeLabel a brief description of the exported file types + * @see findExporter + * @see pulse.io.export.Exporter.askToExport() + * @throws IllegalArgumentException if no exporter can be found + */ + public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { + var exporter = findExporter(target); + if (exporter != null) { + exporter.askToExport(target, parentWindow, fileTypeLabel); + } else { + throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); + } + } + + /** + * Attempts to export the given {@code target} to the {@code directory} by + * saving the contents in a file with the given {@code Extension}. + *

+ * The file is formatted according to the inherent format, i.e. if it is an + * {@code Extension.HTML} file, it will contain HTML tags, etc. If + * {@code extension} is not present in the list of supported extension of an + * exporter matching {@code target}, this will revert to the first supported + * extension. This method will not have any result if no exporter has been + * found fot {@code target}. + *

+ * + * @param the target type + * @param target the exported target + * @param directory a pre-selected directory + * @param extension the desired extension + */ + public static void export(T target, File directory, Extension extension) { + var exporter = findExporter(target); + + if (exporter != null) { + var supportedExtensions = exporter.getSupportedExtensions(); + + if (supportedExtensions.length > 0) { + var confirmedExtension = asList(supportedExtensions).contains(extension) ? extension + : supportedExtensions[0]; + exporter.export(target, directory, confirmedExtension); + } + + } + } + + /** + * This will invoke {@code exportGroup} on each task listed by the + * {@code TaskManager}. + * + * @param directory a pre-selected directory + * @param extension the desired extension + * @see exportGroup + * @see pulse.tasks.TaskManager + */ + public static void exportAllTasks(File directory, Extension extension) { + TaskManager.getManagerInstance().getTaskList().stream().forEach(t -> exportGroup(t, directory, extension)); + } + + /** + * Exports the currently selected task as a group of objects. + * + * @param directory a pre-selected directory + * @param extension the desired extension + * @see exportGroup + * @see pulse.tasks.TaskManager.getSelectedTask() + */ + public static void exportCurrentTask(File directory, Extension extension) { + exportGroup(TaskManager.getManagerInstance().getSelectedTask(), directory, extension); + } + + /** + * Exports the currently selected task as a group of objects using the + * default export extension. + * + * @param directory a pre-selected directory + * @see exportGroup + * @see pulse.tasks.TaskManager.getSelectedTask() + */ + public static void exportCurrentTask(File directory) { + exportCurrentTask(directory, getDefaultExportExtension()); + } + + /** + * Exports all results generated previously during task execution for all + * tasks listed by the TaskManager, provided those tasks had the respective + * result assigned to them. + * + * @param directory a pre-selected directory + * @param extension the desired extension + */ + public static void exportAllResults(File directory, Extension extension) { + + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(x -> x.stream()).filter(Objects::nonNull) + .forEach(r -> export(r, directory, extension)); + + } + + /** + * Fully exports {@code group} and all its contents to the root + * {@code directory} requesting the files to be saved with the + * {@code extension}. + *

+ * If an {@code Exporter} exists that accepts the {@code group} as its + * argument, this will create files in the root {@code directory} in + * accordance to the root {@code Exporter} rules. All contents of the + * {@code group} will then be processed in a similar manner and the output + * will be stored in an internal directory, the name of which conforms to + * the respective description. Note this method is NOT recursive and it + * calls the {@code export} method of the {@code ExportManager}. + *

+ * + * @param group a group + * @param directory a pre-selected root directory + * @param extension the desired extension + * @throws IllegalArgumentException if {@code directory} is not a directory + */ + public static void exportGroup(Group group, File directory, Extension extension) { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + var internalDirectory = new File(directory + separator + group.describe() + separator); + internalDirectory.mkdirs(); + + export(group, directory, extension); + contents(group).stream().forEach(internalHolder -> export(internalHolder, internalDirectory, extension)); + } + +} diff --git a/src/main/java/pulse/io/export/Extension.java b/src/main/java/pulse/io/export/Extension.java index 33c86bfb..c0cff319 100644 --- a/src/main/java/pulse/io/export/Extension.java +++ b/src/main/java/pulse/io/export/Extension.java @@ -6,32 +6,28 @@ * class are responsible for observing adherence to that format. * */ - public enum Extension { - /** - * The result will be a document with html tags that can be viewed in any web - * browser. Useful for complex formatting, but not for data manipulation, as the - * tags and special symbols do not allow simple parsing through the file. - */ - - HTML, - - /** - * The result will be a tab-delimited CSV document. Useful for data - * manipulations and plotting with external tools, e.g. gnuplot or LaTeX. - */ - - CSV; - - /** - * This will return the lower-case string with the name of the extension (e.g., - * html or csv). - */ - - @Override - public String toString() { - return super.toString().toLowerCase(); - } - -} \ No newline at end of file + /** + * The result will be a document with html tags that can be viewed in any + * web browser. Useful for complex formatting, but not for data + * manipulation, as the tags and special symbols do not allow simple parsing + * through the file. + */ + HTML, + /** + * The result will be a tab-delimited CSV document. Useful for data + * manipulations and plotting with external tools, e.g. gnuplot or LaTeX. + */ + CSV; + + /** + * This will return the lower-case string with the name of the extension + * (e.g., html or csv). + */ + @Override + public String toString() { + return super.toString().toLowerCase(); + } + +} diff --git a/src/main/java/pulse/io/export/LogExporter.java b/src/main/java/pulse/io/export/LogExporter.java index 9dfb1349..1f37b628 100644 --- a/src/main/java/pulse/io/export/LogExporter.java +++ b/src/main/java/pulse/io/export/LogExporter.java @@ -14,65 +14,60 @@ * supported. * */ - public class LogExporter implements Exporter { - private static LogExporter instance = new LogExporter(); - - private LogExporter() { - // intentionally blank - } - - /** - * Gets the only static instance of this subclass. - * - * @return an instance of{@code LogExporter}. - */ - - public static LogExporter getInstance() { - return instance; - } - - /** - * Prints all the data contained in this {@code Log} using {@code fos}. By - * default, this will output all data in an {@code html} format. Note this - * implementation ignores the {@code extension} parameter. After execution, the - * stream is explicitly closed. - * - * @param log a log to be exported - * @param fos an output stream - * @param extension the desired extension - * @see pulse.tasks.Log.toString() - */ - - @Override - public void printToStream(Log log, FileOutputStream fos, Extension extension) { - var stream = new PrintStream(fos); - stream.print(log.toString()); - try { - fos.close(); - } catch (IOException e) { - System.err.println("Unable to close stream"); - e.printStackTrace(); - } - } - - /** - * @return {@code Log.class}. - */ - - @Override - public Class target() { - return Log.class; - } - - /** - * Only html is currently supported by this exporter. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file + private static LogExporter instance = new LogExporter(); + + private LogExporter() { + // intentionally blank + } + + /** + * Gets the only static instance of this subclass. + * + * @return an instance of{@code LogExporter}. + */ + public static LogExporter getInstance() { + return instance; + } + + /** + * Prints all the data contained in this {@code Log} using {@code fos}. By + * default, this will output all data in an {@code html} format. Note this + * implementation ignores the {@code extension} parameter. After execution, + * the stream is explicitly closed. + * + * @param log a log to be exported + * @param fos an output stream + * @param extension the desired extension + * @see pulse.tasks.Log.toString() + */ + @Override + public void printToStream(Log log, FileOutputStream fos, Extension extension) { + var stream = new PrintStream(fos); + stream.print(log.toString()); + try { + fos.close(); + } catch (IOException e) { + System.err.println("Unable to close stream"); + e.printStackTrace(); + } + } + + /** + * @return {@code Log.class}. + */ + @Override + public Class target() { + return Log.class; + } + + /** + * Only html is currently supported by this exporter. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/LogPaneExporter.java b/src/main/java/pulse/io/export/LogPaneExporter.java index f801c2e7..ade0e0fb 100644 --- a/src/main/java/pulse/io/export/LogPaneExporter.java +++ b/src/main/java/pulse/io/export/LogPaneExporter.java @@ -15,64 +15,59 @@ * of a {@code LogPane} currently being displayed to the user. * */ - public class LogPaneExporter implements Exporter { - private static LogPaneExporter instance = new LogPaneExporter(); - - private LogPaneExporter() { - // intentionally blank - } - - /** - * This will write all contents of {@code pane}, which are accessed using an - * {@code HTMLEditorKit} directly to {@code fos}. The {@code extension} argument - * is ignored. After exporting, the stream is explicitly closed. - */ - - @Override - public void printToStream(LogPane pane, FileOutputStream fos, Extension extension) { - var kit = (HTMLEditorKit) pane.getEditorKit(); - try { - kit.write(fos, pane.getDocument(), 0, pane.getDocument().getLength()); - } catch (IOException | BadLocationException e) { - System.err.println("Could not export the log pane!"); - e.printStackTrace(); - } - try { - fos.close(); - } catch (IOException e) { - System.err.println("Unable to close stream"); - e.printStackTrace(); - } - } - - /** - * Gets the only static instance of this subclass. - * - * @return an instance of{@code LogPaneExporter}. - */ - - public static LogPaneExporter getInstance() { - return instance; - } - - /** - * @return {@code LogPane.class}. - */ - - @Override - public Class target() { - return LogPane.class; - } - - /** - * Only html is currently supported by this exporter. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file + private static LogPaneExporter instance = new LogPaneExporter(); + + private LogPaneExporter() { + // intentionally blank + } + + /** + * This will write all contents of {@code pane}, which are accessed using an + * {@code HTMLEditorKit} directly to {@code fos}. The {@code extension} + * argument is ignored. After exporting, the stream is explicitly closed. + */ + @Override + public void printToStream(LogPane pane, FileOutputStream fos, Extension extension) { + var kit = (HTMLEditorKit) pane.getEditorKit(); + try { + kit.write(fos, pane.getDocument(), 0, pane.getDocument().getLength()); + } catch (IOException | BadLocationException e) { + System.err.println("Could not export the log pane!"); + e.printStackTrace(); + } + try { + fos.close(); + } catch (IOException e) { + System.err.println("Unable to close stream"); + e.printStackTrace(); + } + } + + /** + * Gets the only static instance of this subclass. + * + * @return an instance of{@code LogPaneExporter}. + */ + public static LogPaneExporter getInstance() { + return instance; + } + + /** + * @return {@code LogPane.class}. + */ + @Override + public Class target() { + return LogPane.class; + } + + /** + * Only html is currently supported by this exporter. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/MetadataExporter.java b/src/main/java/pulse/io/export/MetadataExporter.java index 4af47a23..9851c854 100644 --- a/src/main/java/pulse/io/export/MetadataExporter.java +++ b/src/main/java/pulse/io/export/MetadataExporter.java @@ -13,102 +13,97 @@ * A singleton class used to export {@code Metadata} objects in a html format. * */ - public class MetadataExporter implements Exporter { - private static MetadataExporter instance = new MetadataExporter(); - - private MetadataExporter() { - // intentionally left blank - } - - /** - * Retrieves the single instance of this class. - * - * @return a single instance of {@code MetadataExporter}. - */ - - public static MetadataExporter getInstance() { - return instance; - } - - /** - * Prints the metadata content in html format in two columns, where the first - * column forms the description of the entry and the second column gives its - * value. Extension is ignored, as only html is supported. - */ - - @Override - public void printToStream(Metadata metadata, FileOutputStream fos, Extension extension) { - printHTML(metadata, fos); - } - - private void printHTML(Metadata meta, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("Metadata table"); - stream.print(""); - - final String METADATA_LABEL = "Metadata"; - final String VALUE_LABEL = "Value"; - - stream.print(""); - stream.print(""); - stream.print(METADATA_LABEL + "\t"); - stream.print(""); - stream.print(""); - stream.print(VALUE_LABEL + "\t"); - stream.print(""); - - stream.print(""); - - var data = meta.data(); - - data.forEach(entry -> { - stream.print(""); - - stream.print(""); - stream.print(entry.getDescriptor(false)); - stream.print(""); - stream.print(entry.formattedOutput()); - // possible error typecast property -> object - stream.print(""); - - stream.println(""); - }); - - stream.print(""); - stream.print(""); - } - } - - /** - * Ignores metadata whose external IDs are negative, otherwise calls the - * superclass method. - */ - - @Override - public void export(Metadata metadata, File file, Extension extension) { - if (metadata.getExternalID() > -1) - Exporter.super.export(metadata, file, extension); - } - - /** - * @return {@code Metadata.class} - */ - - @Override - public Class target() { - return Metadata.class; - } - - /** - * @return a single-element array containing {@code Extension.HTML} - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file + private static MetadataExporter instance = new MetadataExporter(); + + private MetadataExporter() { + // intentionally left blank + } + + /** + * Retrieves the single instance of this class. + * + * @return a single instance of {@code MetadataExporter}. + */ + public static MetadataExporter getInstance() { + return instance; + } + + /** + * Prints the metadata content in html format in two columns, where the + * first column forms the description of the entry and the second column + * gives its value. Extension is ignored, as only html is supported. + */ + @Override + public void printToStream(Metadata metadata, FileOutputStream fos, Extension extension) { + printHTML(metadata, fos); + } + + private void printHTML(Metadata meta, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("Metadata table"); + stream.print(""); + + final String METADATA_LABEL = "Metadata"; + final String VALUE_LABEL = "Value"; + + stream.print(""); + stream.print(""); + stream.print(METADATA_LABEL + "\t"); + stream.print(""); + stream.print(""); + stream.print(VALUE_LABEL + "\t"); + stream.print(""); + + stream.print(""); + + var data = meta.data(); + + data.forEach(entry -> { + stream.print(""); + + stream.print(""); + stream.print(entry.getDescriptor(false)); + stream.print(""); + stream.print(entry.formattedOutput()); + // possible error typecast property -> object + stream.print(""); + + stream.println(""); + }); + + stream.print(""); + stream.print(""); + } + } + + /** + * Ignores metadata whose external IDs are negative, otherwise calls the + * superclass method. + */ + @Override + public void export(Metadata metadata, File file, Extension extension) { + if (metadata.getExternalID() > -1) { + Exporter.super.export(metadata, file, extension); + } + } + + /** + * @return {@code Metadata.class} + */ + @Override + public Class target() { + return Metadata.class; + } + + /** + * @return a single-element array containing {@code Extension.HTML} + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/RawDataExporter.java b/src/main/java/pulse/io/export/RawDataExporter.java index 4ffb0812..1ebeb2c4 100644 --- a/src/main/java/pulse/io/export/RawDataExporter.java +++ b/src/main/java/pulse/io/export/RawDataExporter.java @@ -9,56 +9,51 @@ * of {@code ExperimentalData}. Does exactly the same as the * {@code CurveExporter}, except that its target is specifically set to * {@code ExperimentalData}. - * + * * @see pulse.ui.frames.dialogs.ExportDialog * */ - public class RawDataExporter implements Exporter { - private static RawDataExporter instance = new RawDataExporter(); - private static CurveExporter hcExporter = CurveExporter.getInstance(); - - private RawDataExporter() { - // intentionally left blank - } - - /** - * Retrieves the single static instance of this class - * - * @return an instance of {@code RawDataExporter}. - */ - - public static RawDataExporter getInstance() { - return instance; - } - - /** - * @return {@code ExperimentalData.class} - */ - - @Override - public Class target() { - return ExperimentalData.class; - } - - /** - * Invokes the {@code printToStream(...)} method of the - * {@code HeatingCurveExporter} instance. - */ - - @Override - public void printToStream(ExperimentalData target, FileOutputStream fos, Extension extension) { - hcExporter.printToStream(target, fos, extension); - } - - /** - * Currently {@code html} and {@code csv} extensions are supported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { Extension.HTML, Extension.CSV }; - } - -} \ No newline at end of file + private static RawDataExporter instance = new RawDataExporter(); + private static CurveExporter hcExporter = CurveExporter.getInstance(); + + private RawDataExporter() { + // intentionally left blank + } + + /** + * Retrieves the single static instance of this class + * + * @return an instance of {@code RawDataExporter}. + */ + public static RawDataExporter getInstance() { + return instance; + } + + /** + * @return {@code ExperimentalData.class} + */ + @Override + public Class target() { + return ExperimentalData.class; + } + + /** + * Invokes the {@code printToStream(...)} method of the + * {@code HeatingCurveExporter} instance. + */ + @Override + public void printToStream(ExperimentalData target, FileOutputStream fos, Extension extension) { + hcExporter.printToStream(target, fos, extension); + } + + /** + * Currently {@code html} and {@code csv} extensions are supported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{Extension.HTML, Extension.CSV}; + } + +} diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index f65d08d0..1de1e4b6 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -14,103 +14,98 @@ * in time. Implements both the csv and html formats. * */ - public class ResidualStatisticExporter implements Exporter { - private static ResidualStatisticExporter instance = new ResidualStatisticExporter(); - - private ResidualStatisticExporter() { - // intentionally left blank - } - - /** - * @return {@code ResidualStatistic.class} - */ - - @Override - public Class target() { - return ResidualStatistic.class; - } - - /** - * Prints the residuals in a two-column format in a {@code html} or {@code csv} - * file (accepts both extensions). - */ - - @Override - public void printToStream(ResidualStatistic rs, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(rs, fos); - break; - case CSV: - printCSV(rs, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - /** - * The supported extensions for exporting the data contained in this object. - * Currently include {@code .html} and {@code .csv}. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printHTML(ResidualStatistic hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - var residuals = hc.getResiduals(); - int residualsLength = residuals == null ? 0 : residuals.size(); - stream.print(getString("ResultTableExporter.style")); - stream.print("Time profile of residuals"); - stream.print(""); - final String TIME_LABEL = getString("HeatingCurve.6"); - final String RESIDUAL_LABEL = "Residual"; - stream.print("" + TIME_LABEL + "\t"); - stream.print("" + RESIDUAL_LABEL + "\t"); - stream.print(""); - - for (int i = 0; i < residualsLength; i++) { - double tr = residuals.get(i)[0]; - double Tr = residuals.get(i)[1]; - stream.printf("%n%.8f%.8f", tr, Tr); - } - - stream.print(""); - } - - } - - private void printCSV(ResidualStatistic hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - var residuals = hc.getResiduals(); - int residualsLength = residuals == null ? 0 : residuals.size(); - final String TIME_LABEL = getString("HeatingCurve.6"); - final String RESIDUAL_LABEL = "Residual"; - stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); - double tr, Tr; - for (int i = 0; i < residualsLength; i++) { - tr = residuals.get(i)[0]; - stream.printf("%n%3.8f", tr); - Tr = residuals.get(i)[1]; - stream.printf("\t%3.8f", Tr); - } - } - - } - - /** - * Retrieves the single instance of this class. - * - * @return a single instance of {@code ResidualStatisticExporter}. - */ - - public static ResidualStatisticExporter getInstance() { - return instance; - } + private static ResidualStatisticExporter instance = new ResidualStatisticExporter(); + + private ResidualStatisticExporter() { + // intentionally left blank + } + + /** + * @return {@code ResidualStatistic.class} + */ + @Override + public Class target() { + return ResidualStatistic.class; + } + + /** + * Prints the residuals in a two-column format in a {@code html} or + * {@code csv} file (accepts both extensions). + */ + @Override + public void printToStream(ResidualStatistic rs, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(rs, fos); + break; + case CSV: + printCSV(rs, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + /** + * The supported extensions for exporting the data contained in this object. + * Currently include {@code .html} and {@code .csv}. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + private void printHTML(ResidualStatistic hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + var residuals = hc.getResiduals(); + int residualsLength = residuals == null ? 0 : residuals.size(); + stream.print(getString("ResultTableExporter.style")); + stream.print("Time profile of residuals"); + stream.print(""); + final String TIME_LABEL = getString("HeatingCurve.6"); + final String RESIDUAL_LABEL = "Residual"; + stream.print("" + TIME_LABEL + "\t"); + stream.print("" + RESIDUAL_LABEL + "\t"); + stream.print(""); + + for (int i = 0; i < residualsLength; i++) { + double tr = residuals.get(i)[0]; + double Tr = residuals.get(i)[1]; + stream.printf("%n%.8f%.8f", tr, Tr); + } + + stream.print(""); + } + + } + + private void printCSV(ResidualStatistic hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + var residuals = hc.getResiduals(); + int residualsLength = residuals == null ? 0 : residuals.size(); + final String TIME_LABEL = getString("HeatingCurve.6"); + final String RESIDUAL_LABEL = "Residual"; + stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); + double tr, Tr; + for (int i = 0; i < residualsLength; i++) { + tr = residuals.get(i)[0]; + stream.printf("%n%3.8f", tr); + Tr = residuals.get(i)[1]; + stream.printf("\t%3.8f", Tr); + } + } + + } + + /** + * Retrieves the single instance of this class. + * + * @return a single instance of {@code ResidualStatisticExporter}. + */ + public static ResidualStatisticExporter getInstance() { + return instance; + } } diff --git a/src/main/java/pulse/io/export/ResultExporter.java b/src/main/java/pulse/io/export/ResultExporter.java index 889c21fc..4babbbb5 100644 --- a/src/main/java/pulse/io/export/ResultExporter.java +++ b/src/main/java/pulse/io/export/ResultExporter.java @@ -14,92 +14,88 @@ * in the {@code csv} and {@code html} formats. * */ - public class ResultExporter implements Exporter { - private static ResultExporter instance = new ResultExporter(); - - private ResultExporter() { - // intentionally blank - } - - /** - * Prints the data of this {@code Result} with {@code fos} either in a - * {@code html} or a {@code csv} file format. - */ - - @Override - public void printToStream(Result result, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(result, fos); - break; - case CSV: - printCSV(result, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - private void printHTML(Result result, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("Calculated parameters"); - - for (var p : result.getProperties()) { - stream.print(""); - stream.print(""); - - stream.print(p.getDescriptor(true)); - stream.print(""); - stream.print(p.formattedOutput()); - - stream.print(""); - stream.println(""); - } - - stream.print(""); - } - } - - /** - * Currently the supported extensions include {@code .html} and {@code .csv}. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printCSV(Result result, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print("(Results)"); - - for (var p : result.getProperties()) { - stream.printf("%n%-24.12s", p.getType()); - stream.printf("\t%-24.12s", p.formattedOutput()); - } - } - } - - /** - * @return {@code Result.class} - */ - - @Override - public Class target() { - return Result.class; - } - - /** - * Returns the single static instance of this class. - * - * @return instance an instance of this class. - */ - - public static ResultExporter getInstance() { - return instance; - } - -} \ No newline at end of file + private static ResultExporter instance = new ResultExporter(); + + private ResultExporter() { + // intentionally blank + } + + /** + * Prints the data of this {@code Result} with {@code fos} either in a + * {@code html} or a {@code csv} file format. + */ + @Override + public void printToStream(Result result, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(result, fos); + break; + case CSV: + printCSV(result, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + private void printHTML(Result result, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("Calculated parameters"); + + for (var p : result.getProperties()) { + stream.print(""); + stream.print(""); + + stream.print(p.getDescriptor(true)); + stream.print(""); + stream.print(p.formattedOutput()); + + stream.print(""); + stream.println(""); + } + + stream.print(""); + } + } + + /** + * Currently the supported extensions include {@code .html} and + * {@code .csv}. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + private void printCSV(Result result, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print("(Results)"); + + for (var p : result.getProperties()) { + stream.printf("%n%-24.12s", p.getType()); + stream.printf("\t%-24.12s", p.formattedOutput()); + } + } + } + + /** + * @return {@code Result.class} + */ + @Override + public Class target() { + return Result.class; + } + + /** + * Returns the single static instance of this class. + * + * @return instance an instance of this class. + */ + public static ResultExporter getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/io/export/package-info.java b/src/main/java/pulse/io/export/package-info.java index f3e455dc..f241cb52 100644 --- a/src/main/java/pulse/io/export/package-info.java +++ b/src/main/java/pulse/io/export/package-info.java @@ -2,7 +2,6 @@ * Package contains the PULsE export API, which currently consists of different * exporter classes, an export manager, an XML converter and a MassExporter * class. - * + * */ - -package pulse.io.export; \ No newline at end of file +package pulse.io.export; diff --git a/src/main/java/pulse/io/readers/AbstractHandler.java b/src/main/java/pulse/io/readers/AbstractHandler.java index 1e96ec45..c5419967 100644 --- a/src/main/java/pulse/io/readers/AbstractHandler.java +++ b/src/main/java/pulse/io/readers/AbstractHandler.java @@ -11,56 +11,52 @@ * pre-set extension. * */ - public interface AbstractHandler extends Reflexive { - /** - * Retrieves the supported extension of files, which this - * {@code AbstractHandler} is able to process. - * - * @return a {@code String} (usually, lower-case) containing the supported - * extension. - */ - - public String getSupportedExtension(); - - /** - * Checks if the file suffix for {@code file} matches the {@code extension}. - * - * @param file the {@code File} to process - * @param extension a String, which needs to be checked against the suffix of - * {@code File} - * @return {@code false} if {@code file} is a directory or if it has a suffix - * different from {@code extension}. True otherwise. - */ - - public static boolean extensionsMatch(File file, String extension) { - if (file.isDirectory()) - return false; - - String name = file.getName(); - - /* + /** + * Retrieves the supported extension of files, which this + * {@code AbstractHandler} is able to process. + * + * @return a {@code String} (usually, lower-case) containing the supported + * extension. + */ + public String getSupportedExtension(); + + /** + * Checks if the file suffix for {@code file} matches the {@code extension}. + * + * @param file the {@code File} to process + * @param extension a String, which needs to be checked against the suffix + * of {@code File} + * @return {@code false} if {@code file} is a directory or if it has a + * suffix different from {@code extension}. True otherwise. + */ + public static boolean extensionsMatch(File file, String extension) { + if (file.isDirectory()) { + return false; + } + + String name = file.getName(); + + /* * The below code is based on string helper function by Gili Tzabari - */ - - int suffixLength = extension.length(); - return name.regionMatches(true, name.length() - suffixLength, extension, 0, suffixLength); - - } - - /** - * Invokes {@code extensionMatch} with the second argument set as - * {@code getSupportedExtension()}. - * - * @param file the file to be checked - * @return {@code true} if extensions match, false otherwise. - * @see extensionsMatch - * @see getSupportedExtension - */ - - public default boolean isExtensionSupported(File file) { - return extensionsMatch(file, getSupportedExtension()); - } - -} \ No newline at end of file + */ + int suffixLength = extension.length(); + return name.regionMatches(true, name.length() - suffixLength, extension, 0, suffixLength); + + } + + /** + * Invokes {@code extensionMatch} with the second argument set as + * {@code getSupportedExtension()}. + * + * @param file the file to be checked + * @return {@code true} if extensions match, false otherwise. + * @see extensionsMatch + * @see getSupportedExtension + */ + public default boolean isExtensionSupported(File file) { + return extensionsMatch(file, getSupportedExtension()); + } + +} diff --git a/src/main/java/pulse/io/readers/AbstractPopulator.java b/src/main/java/pulse/io/readers/AbstractPopulator.java index e4b138b5..484eed25 100644 --- a/src/main/java/pulse/io/readers/AbstractPopulator.java +++ b/src/main/java/pulse/io/readers/AbstractPopulator.java @@ -10,18 +10,17 @@ * latter does not change the internal structure of an object. * */ - public interface AbstractPopulator extends AbstractHandler { - /** - * Tries to populate {@code t} from data contained in {@code f}. - * - * @param f a file presumably containing data that can be converted to the - * internal format of {@code t}. - * @param t a {@code T} object which can potentially be populated by {@code f}. - * @throws IOException if an exception occurs during processing {@code f}. - */ - - public void populate(File f, T t) throws IOException; + /** + * Tries to populate {@code t} from data contained in {@code f}. + * + * @param f a file presumably containing data that can be converted to the + * internal format of {@code t}. + * @param t a {@code T} object which can potentially be populated by + * {@code f}. + * @throws IOException if an exception occurs during processing {@code f}. + */ + public void populate(File f, T t) throws IOException; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/AbstractReader.java b/src/main/java/pulse/io/readers/AbstractReader.java index f3e1bd71..a557fcbe 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -14,20 +14,19 @@ * using the reader. *

*/ - public interface AbstractReader extends AbstractHandler { - /** - * Reads {@code f} to translate its contents to one of the immutable structures - * of {@code T}. Usually this involves reading arrays and collections and - * pasting their data into existing structural elements. This does not change - * the internal structure of the object. - * - * @param f a file which has readable content - * @return a {@code T} object created by reading all information from {@code f}. - * @throws IOException - */ - - public T read(File f) throws IOException; + /** + * Reads {@code f} to translate its contents to one of the immutable + * structures of {@code T}. Usually this involves reading arrays and + * collections and pasting their data into existing structural elements. + * This does not change the internal structure of the object. + * + * @param f a file which has readable content + * @return a {@code T} object created by reading all information from + * {@code f}. + * @throws IOException + */ + public T read(File f) throws IOException; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/ButcherTableauReader.java b/src/main/java/pulse/io/readers/ButcherTableauReader.java index 7e6e14d2..bea5d870 100644 --- a/src/main/java/pulse/io/readers/ButcherTableauReader.java +++ b/src/main/java/pulse/io/readers/ButcherTableauReader.java @@ -18,111 +18,115 @@ * {@code jar}). The coefficients are used by an explicit Runge-Kutta solver. * */ - public class ButcherTableauReader implements AbstractReader { - private final static String SUPPORTED_EXTENSION = "rk"; - private static ButcherTableauReader instance = new ButcherTableauReader(); - - private ButcherTableauReader() { - // intentionally blank - } - - /** - * Reads the Butcher tableau stored in {@code file}. The file contents should be - * arranged as follows: first row contains specific keywords (e.g. FSAL), second - * and subsequent rows contain the matrix coefficients (the matrix is assumed to - * be quadratic), so the number of columns should be equal to the number of - * rows; the three final rows correspond to {@code c}, {@code b} and {@code b^} - * vectors. Consistency should be maintained between the corresponding - * dimensions. - */ + private final static String SUPPORTED_EXTENSION = "rk"; + private static ButcherTableauReader instance = new ButcherTableauReader(); - @Override - public ButcherTableau read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + private ButcherTableauReader() { + // intentionally blank + } - // ignore extension! + /** + * Reads the Butcher tableau stored in {@code file}. The file contents + * should be arranged as follows: first row contains specific keywords (e.g. + * FSAL), second and subsequent rows contain the matrix coefficients (the + * matrix is assumed to be quadratic), so the number of columns should be + * equal to the number of rows; the three final rows correspond to + * {@code c}, {@code b} and {@code b^} vectors. Consistency should be + * maintained between the corresponding dimensions. + */ + @Override + public ButcherTableau read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - String name = file.getName().split("\\.")[0]; - ButcherTableau bt = null; + // ignore extension! + String name = file.getName().split("\\.")[0]; + ButcherTableau bt = null; - String delims = Messages.getString("}{,\t "); + String delims = Messages.getString("}{,\t "); - try (var fr = new FileReader(file); BufferedReader reader = new BufferedReader(fr)) { - // first line with declarations (e.g. FSAL, etc.) - var tokenizer = new StringTokenizer(reader.readLine()); + try (var fr = new FileReader(file); BufferedReader reader = new BufferedReader(fr)) { + // first line with declarations (e.g. FSAL, etc.) + var tokenizer = new StringTokenizer(reader.readLine()); - boolean fsal = false; + boolean fsal = false; - while (tokenizer.hasMoreTokens()) - if (tokenizer.nextToken(delims).equalsIgnoreCase("FSAL")) - fsal = true; + while (tokenizer.hasMoreTokens()) { + if (tokenizer.nextToken(delims).equalsIgnoreCase("FSAL")) { + fsal = true; + } + } - var aMatrix = readMatrix(reader, delims); - var v = readVectors(reader, delims, aMatrix.length); + var aMatrix = readMatrix(reader, delims); + var v = readVectors(reader, delims, aMatrix.length); - bt = new ButcherTableau(name, aMatrix, v[0], v[1], v[2], fsal); + bt = new ButcherTableau(name, aMatrix, v[0], v[1], v[2], fsal); - reader.close(); - } + reader.close(); + } - return bt; + return bt; - } + } - private double[][] readMatrix(BufferedReader reader, String delims) throws IOException { - List lineDouble = new ArrayList<>(); - int lineno = 0; - int dimension = 1; + private double[][] readMatrix(BufferedReader reader, String delims) throws IOException { + List lineDouble = new ArrayList<>(); + int lineno = 0; + int dimension = 1; - StringTokenizer tokenizer; + StringTokenizer tokenizer; - for (String line = ""; lineno < dimension; lineno++) { - line = reader.readLine(); - tokenizer = new StringTokenizer(line); + for (String line = ""; lineno < dimension; lineno++) { + line = reader.readLine(); + tokenizer = new StringTokenizer(line); - while (tokenizer.hasMoreTokens()) - lineDouble.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - if (lineno == 0) - dimension = lineDouble.size(); - } + while (tokenizer.hasMoreTokens()) { + lineDouble.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + } + if (lineno == 0) { + dimension = lineDouble.size(); + } + } - double[][] aMatrix = new double[dimension][dimension]; + double[][] aMatrix = new double[dimension][dimension]; - for (int i = 0; i < dimension; i++) - for (int j = 0; j < dimension; j++) - aMatrix[i][j] = lineDouble.get(i * dimension + j); + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + aMatrix[i][j] = lineDouble.get(i * dimension + j); + } + } - return aMatrix; - } + return aMatrix; + } - private double[][] readVectors(BufferedReader reader, String delims, int dimension) throws IOException { - var v = new double[3][dimension]; + private double[][] readVectors(BufferedReader reader, String delims, int dimension) throws IOException { + var v = new double[3][dimension]; - int lineno = 0; - StringTokenizer tokenizer; + int lineno = 0; + StringTokenizer tokenizer; - for (String line = ""; lineno < 3 && line != null; lineno++) { - line = reader.readLine(); - tokenizer = new StringTokenizer(line); + for (String line = ""; lineno < 3 && line != null; lineno++) { + line = reader.readLine(); + tokenizer = new StringTokenizer(line); - for (int i = 0; i < dimension && tokenizer.hasMoreTokens(); i++) - v[lineno][i] = (ExpressionParser.evaluate(tokenizer.nextToken(delims))); + for (int i = 0; i < dimension && tokenizer.hasMoreTokens(); i++) { + v[lineno][i] = (ExpressionParser.evaluate(tokenizer.nextToken(delims))); + } - } + } - return v; + return v; - } + } - @Override - public String getSupportedExtension() { - return SUPPORTED_EXTENSION; - } + @Override + public String getSupportedExtension() { + return SUPPORTED_EXTENSION; + } - public static ButcherTableauReader getInstance() { - return instance; - } + public static ButcherTableauReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/CurveReader.java b/src/main/java/pulse/io/readers/CurveReader.java index a928414c..41586f07 100644 --- a/src/main/java/pulse/io/readers/CurveReader.java +++ b/src/main/java/pulse/io/readers/CurveReader.java @@ -17,45 +17,42 @@ * {@code ExperimentalData} objects. *

*/ - public interface CurveReader extends AbstractReader> { - /** - * Basic operation for reading the {@code file} and converting this to a - * {@code List} of {@code ExperimentalData} objects. - *

- * - * @param file a {@code File} which has either all information encoded in - * its contents or provides {@code URI} links to other files, - * each containing the necessary information. - * @return - *

- * a {@code List} of {@code ExperimentalData} objects associated with - * this {@code file}. In case if {@code file} contains only one - * {@code ExperimentalData}, i.e. if the data is only presented for one - * heating curve taken at a specific temperature after a single laser - * shot, the size of the {@code List} will be equal to unity. - *

- * @throws IOException if something goes wrong with reading the file - */ - - @Override - public abstract List read(File file) throws IOException; - - /** - * Sorts the {@code List} of {@code ExperimentalData} according to their - * external IDs (if any). - * - * @param array an unsorted list of {@code ExperimentalData} - * @return the same list after sorting - * @see pulse.input.Metadata.getExternalID() - */ - - public static List sort(List array) { - Comparator externalIdComparator = (ExperimentalData e1, ExperimentalData e2) -> Integer - .valueOf(e1.getMetadata().getExternalID()).compareTo(Integer.valueOf(e2.getMetadata().getExternalID())); - - return array.stream().sorted(externalIdComparator).collect(Collectors.toList()); - } - -} \ No newline at end of file + /** + * Basic operation for reading the {@code file} and converting this to a + * {@code List} of {@code ExperimentalData} objects. + *

+ * + * @param file a {@code File} which has either all information + * encoded in its contents or provides {@code URI} links to other + * files, each containing the necessary information. + * @return + *

+ * a {@code List} of {@code ExperimentalData} objects associated with this + * {@code file}. In case if {@code file} contains only one + * {@code ExperimentalData}, i.e. if the data is only presented for one + * heating curve taken at a specific temperature after a single laser shot, + * the size of the {@code List} will be equal to unity. + *

+ * @throws IOException if something goes wrong with reading the file + */ + @Override + public abstract List read(File file) throws IOException; + + /** + * Sorts the {@code List} of {@code ExperimentalData} according to their + * external IDs (if any). + * + * @param array an unsorted list of {@code ExperimentalData} + * @return the same list after sorting + * @see pulse.input.Metadata.getExternalID() + */ + public static List sort(List array) { + Comparator externalIdComparator = (ExperimentalData e1, ExperimentalData e2) -> Integer + .valueOf(e1.getMetadata().getExternalID()).compareTo(Integer.valueOf(e2.getMetadata().getExternalID())); + + return array.stream().sorted(externalIdComparator).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/pulse/io/readers/DATReader.java b/src/main/java/pulse/io/readers/DATReader.java index 4fd25c5a..31a515ae 100644 --- a/src/main/java/pulse/io/readers/DATReader.java +++ b/src/main/java/pulse/io/readers/DATReader.java @@ -33,74 +33,71 @@ * an absolute scale, according to NIST recommendations. *

*/ - public class DATReader implements CurveReader { - private static CurveReader instance = new DATReader(); - private final static double CONVERSION_TO_KELVIN = 273.15; - - private DATReader() { - // intentionally blank - } - - /** - * @return a {@code String} equal to {.dat} - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("DATReader.0"); //$NON-NLS-1$ - } - - /** - *

- * This will return a single {@code ExperimentalData}, which stores all the - * information available in the {@code file}, wrapped in a {@code List} object - * with the size of unity. In addition to the time-temperature data loaded - * directly into the {@code ExperimentalData} lists, a {@code Metadata} object - * will be created for the {@code ExperimentalData} and will store the test - * temperature declared in {@code file}. - * - * @param file a '{@code .dat}' file, which conforms to the respective format. - * @return a single {@code ExperimentalData} wrapped in a {@code List} with the - * size of unity. - */ - - @Override - public List read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("DATReader.1")); - - ExperimentalData curve = new ExperimentalData(); - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - double T = Double.parseDouble(reader.readLine()) + CONVERSION_TO_KELVIN; - Metadata met = new Metadata(derive(TEST_TEMPERATURE, T), -1); - curve.setMetadata(met); - double time, temp; - String delims = Messages.getString("DATReader.2"); //$NON-NLS-1$ - StringTokenizer tokenizer; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line, delims); - time = Double.parseDouble(tokenizer.nextToken()); - temp = Double.parseDouble(tokenizer.nextToken()); - curve.addPoint(time, temp); - } - curve.setRange(new Range(curve.getTimeSequence())); - } - - return new ArrayList<>(Arrays.asList(curve)); - - } - - /** - * As this class uses the singleton pattern, only one instance is created using - * an empty no-argument constructor. - * - * @return the single instance of this class. - */ - - public static CurveReader getInstance() { - return instance; - } - -} \ No newline at end of file + private static CurveReader instance = new DATReader(); + private final static double CONVERSION_TO_KELVIN = 273.15; + + private DATReader() { + // intentionally blank + } + + /** + * @return a {@code String} equal to {.dat} + */ + @Override + public String getSupportedExtension() { + return Messages.getString("DATReader.0"); //$NON-NLS-1$ + } + + /** + *

+ * This will return a single {@code ExperimentalData}, which stores all the + * information available in the {@code file}, wrapped in a {@code List} + * object with the size of unity. In addition to the time-temperature data + * loaded directly into the {@code ExperimentalData} lists, a + * {@code Metadata} object will be created for the {@code ExperimentalData} + * and will store the test temperature declared in {@code file}. + * + * @param file a '{@code .dat}' file, which conforms to the respective + * format. + * @return a single {@code ExperimentalData} wrapped in a {@code List} with + * the size of unity. + */ + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + ExperimentalData curve = new ExperimentalData(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + double T = Double.parseDouble(reader.readLine()) + CONVERSION_TO_KELVIN; + Metadata met = new Metadata(derive(TEST_TEMPERATURE, T), -1); + curve.setMetadata(met); + double time, temp; + String delims = Messages.getString("DATReader.2"); //$NON-NLS-1$ + StringTokenizer tokenizer; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line, delims); + time = Double.parseDouble(tokenizer.nextToken()); + temp = Double.parseDouble(tokenizer.nextToken()); + curve.addPoint(time, temp); + } + curve.setRange(new Range(curve.getTimeSequence())); + } + + return new ArrayList<>(Arrays.asList(curve)); + + } + + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/io/readers/DatasetReader.java b/src/main/java/pulse/io/readers/DatasetReader.java index 7d320f66..59c4a3bb 100644 --- a/src/main/java/pulse/io/readers/DatasetReader.java +++ b/src/main/java/pulse/io/readers/DatasetReader.java @@ -10,22 +10,20 @@ * with an interpolation algorithm. * */ - public interface DatasetReader extends AbstractReader { - /** - * Creates an {@code InterpolationDataset} using the dataset stored in the - * {@code file}. - * - * @param file a file with a supported extension containing the information - * needed to create an {@code InterpolationDataset}. - * @return an {@code InterpolationDataset}, which not only stores the - * information contained in {@code file}, but also provides means of - * interpolation. - * @throws IOException if something goes wrong with reading the {@code file} - */ - - @Override - public abstract InterpolationDataset read(File file) throws IOException; + /** + * Creates an {@code InterpolationDataset} using the dataset stored in the + * {@code file}. + * + * @param file a file with a supported extension containing the information + * needed to create an {@code InterpolationDataset}. + * @return an {@code InterpolationDataset}, which not only stores the + * information contained in {@code file}, but also provides means of + * interpolation. + * @throws IOException if something goes wrong with reading the {@code file} + */ + @Override + public abstract InterpolationDataset read(File file) throws IOException; } diff --git a/src/main/java/pulse/io/readers/ExpressionParser.java b/src/main/java/pulse/io/readers/ExpressionParser.java index 6cc0114b..4179bb51 100644 --- a/src/main/java/pulse/io/readers/ExpressionParser.java +++ b/src/main/java/pulse/io/readers/ExpressionParser.java @@ -3,114 +3,119 @@ /** * Original source: * https://stackoverflow.com/questions/3422673/how-to-evaluate-a-math-expression-given-in-string-form#3423360 - * + * * @author Romeo Sierra * */ - public class ExpressionParser { - private ExpressionParser() { - } - - public static double evaluate(final String str) { - return new Object() { - int pos = -1, ch; - - void nextChar() { - ch = (++pos < str.length()) ? str.charAt(pos) : -1; - } - - boolean eat(int charToEat) { - while (ch == ' ') - nextChar(); - if (ch == charToEat) { - nextChar(); - return true; - } - return false; - } + private ExpressionParser() { + } - double parse() { - nextChar(); - double x = parseExpression(); - if (pos < str.length()) - throw new RuntimeException("Unexpected: " + (char) ch); - return x; - } + public static double evaluate(final String str) { + return new Object() { + int pos = -1, ch; - // Grammar: - // expression = term | expression `+` term | expression `-` term - // term = factor | term `*` factor | term `/` factor - // factor = `+` factor | `-` factor | `(` expression `)` - // | number | functionName factor | factor `^` factor + void nextChar() { + ch = (++pos < str.length()) ? str.charAt(pos) : -1; + } - double parseExpression() { - double x = parseTerm(); - for (;;) { - if (eat('+')) - x += parseTerm(); // addition - else if (eat('-')) - x -= parseTerm(); // subtraction - else - return x; - } - } + boolean eat(int charToEat) { + while (ch == ' ') { + nextChar(); + } + if (ch == charToEat) { + nextChar(); + return true; + } + return false; + } - double parseTerm() { - double x = parseFactor(); - for (;;) { - if (eat('*')) - x *= parseFactor(); // multiplication - else if (eat('/')) - x /= parseFactor(); // division - else if (eat('^')) - x = Math.pow(x, parseFactor()); // exponentiation -> Moved in to here. So the problem is fixed - else - return x; - } - } + double parse() { + nextChar(); + double x = parseExpression(); + if (pos < str.length()) { + throw new RuntimeException("Unexpected: " + (char) ch); + } + return x; + } - double parseFactor() { - if (eat('+')) - return parseFactor(); // unary plus - if (eat('-')) - return -parseFactor(); // unary minus + // Grammar: + // expression = term | expression `+` term | expression `-` term + // term = factor | term `*` factor | term `/` factor + // factor = `+` factor | `-` factor | `(` expression `)` + // | number | functionName factor | factor `^` factor + double parseExpression() { + double x = parseTerm(); + for (;;) { + if (eat('+')) { + x += parseTerm(); // addition + } else if (eat('-')) { + x -= parseTerm(); // subtraction + } else { + return x; + } + } + } - double x; - int startPos = this.pos; - if (eat('(')) { // parentheses - x = parseExpression(); - eat(')'); - } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers - while ((ch >= '0' && ch <= '9') || ch == '.') - nextChar(); - x = Double.parseDouble(str.substring(startPos, this.pos)); - } else if (ch >= 'a' && ch <= 'z') { // functions - while (ch >= 'a' && ch <= 'z') - nextChar(); - String func = str.substring(startPos, this.pos); - x = parseFactor(); - if (func.equals("sqrt")) - x = Math.sqrt(x); - else if (func.equals("sin")) - x = Math.sin(Math.toRadians(x)); - else if (func.equals("cos")) - x = Math.cos(Math.toRadians(x)); - else if (func.equals("tan")) - x = Math.tan(Math.toRadians(x)); - else - throw new RuntimeException("Unknown function: " + func); - } else { - throw new RuntimeException("Unexpected: " + (char) ch); - } + double parseTerm() { + double x = parseFactor(); + for (;;) { + if (eat('*')) { + x *= parseFactor(); // multiplication + } else if (eat('/')) { + x /= parseFactor(); // division + } else if (eat('^')) { + x = Math.pow(x, parseFactor()); // exponentiation -> Moved in to here. So the problem is fixed + } else { + return x; + } + } + } - // if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation -> This is - // causing a bit of problem + double parseFactor() { + if (eat('+')) { + return parseFactor(); // unary plus + } + if (eat('-')) { + return -parseFactor(); // unary minus + } + double x; + int startPos = this.pos; + if (eat('(')) { // parentheses + x = parseExpression(); + eat(')'); + } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers + while ((ch >= '0' && ch <= '9') || ch == '.') { + nextChar(); + } + x = Double.parseDouble(str.substring(startPos, this.pos)); + } else if (ch >= 'a' && ch <= 'z') { // functions + while (ch >= 'a' && ch <= 'z') { + nextChar(); + } + String func = str.substring(startPos, this.pos); + x = parseFactor(); + if (func.equals("sqrt")) { + x = Math.sqrt(x); + } else if (func.equals("sin")) { + x = Math.sin(Math.toRadians(x)); + } else if (func.equals("cos")) { + x = Math.cos(Math.toRadians(x)); + } else if (func.equals("tan")) { + x = Math.tan(Math.toRadians(x)); + } else { + throw new RuntimeException("Unknown function: " + func); + } + } else { + throw new RuntimeException("Unexpected: " + (char) ch); + } - return x; - } - }.parse(); - } + // if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation -> This is + // causing a bit of problem + return x; + } + }.parse(); + } } diff --git a/src/main/java/pulse/io/readers/LFRReader.java b/src/main/java/pulse/io/readers/LFRReader.java index 52cf6116..c193ab1f 100644 --- a/src/main/java/pulse/io/readers/LFRReader.java +++ b/src/main/java/pulse/io/readers/LFRReader.java @@ -30,7 +30,7 @@ * Linseis software), test temperatures, and other variables. The individual * ASCII files encoded in ASCII represent tab-delimited time-temperature data. *

- * + * *

* {@code PULsE} currently accepts the formats of only those files output by * Linseis LFA systems that are in ASCII formats, so results from other systems @@ -42,182 +42,178 @@ * format). This should be done for any shot or curve you wish to analyse in * {@code PULsE}. *

- * + * *

* After all shots have been recorded, click “Severals†in the Linseis analysis * window and select all exported heating curve {@code .txt} files for the * experiment. Clicking “Ok†and “Save†on the following windows will create a * {@code .lfr} file with file locations and data for all the heating curves. * Save this in the same folder as the {@code .txt} files. - * + * */ - public class LFRReader implements CurveReader { - private static CurveReader instance = new LFRReader(); - private final static double TO_KELVIN = 273; - private final static double TO_SECONDS = 1E-3; - - private LFRReader() { - // intentionally blank - } - - /** - * @return The supported extension ({@code .lfr}). - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("LFRReader.0"); - } - - /** - * Reads through the {@code file}, identifies the names of other files with - * individual heating curves, theirs external IDs and test temperatures (in - * degrees Celsius, later converted to Kelvin). - *

- * Creates a {@code List} of {@code ExperimentalData} objects with the size - * equal to the number of individual entries in the master-file. Searches for - * the individual files listed in the namelist and stored in the same directory - * where the master-file has been found previously. Upon finding the individual - * files, invokes {@code readSingleCurve} on each of them sequentially and - * stores the {@code ExperimentalData} in a list. Finally, invokes the - * {@code sort} method on that list to sort it. - *

- * - * @param file the master-file with {@code .lfr} suffix - * @return a {@code List} of @code ExperimentalData}, containing all information - * stored in both the master file and linked individual files. - * @see sort - * @see readSingleCurve - */ - - @Override - public List read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("LFRReader.1")); - - String stringSplitter = Messages.getString("LFRReader.3"); - - final String directory = file.getAbsoluteFile().getParent(); - final Map fileMap; - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - fileMap = fileMap(reader, stringSplitter); - } - - return sort(convertToData(directory, stringSplitter, fileMap)); - - } - - private Map fileMap(BufferedReader reader, String stringSplitter) throws IOException { - - String delims = Messages.getString("LFRReader.2"); - StringTokenizer tokenizer; - - // skip two first lines - reader.readLine(); - reader.readLine(); - - var fileTempMap = new HashMap(); - - String tmp; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - int id = Integer.parseInt(tokenizer.nextToken(delims)); // id - - tmp = tokenizer.nextToken(delims).split(stringSplitter)[0]; // write file names without extensions - - tokenizer.nextToken(delims); // sample id - var temperature = derive(TEST_TEMPERATURE, parseDouble(tokenizer.nextToken()) + TO_KELVIN); // test - // temperature - - fileTempMap.put(tmp, new Metadata(temperature, id)); // assign metadata object with external id and - // temperature - - } - - return fileTempMap; - - } - - private List convertToData(String directory, String stringSplitter, Map map) - throws IOException { - List curves = new ArrayList<>(); - var filenames = map.keySet(); - - for (File f : new File(directory).listFiles()) { - - var name = f.getName().split(stringSplitter)[0]; - - if (filenames.contains(name)) - curves.add(readSingleCurve(f, map.get(name))); - - } - - return curves; - - } - - /** - * Creates a single {@code ExperimentalData} object with the time-temperature - * information retrieved from {@code file} and using the previously generated - * {@code Metadata} object, containing the external ID and the test temperature - * of this heating curve. - *

- * The time in Linseis files is usually stored in [ms], hence the time values - * are multiplied by {@code 1E-3} to adhere to the {@code PULsE} format. The - * signal rise is recorded in [mV], hence it represents a relative scale, which - * however is functionally linked to the temperature rise. {@code PULsE} does - * not establish this functional relation. Instead, it uses the signal values in - * the dimensionless problem formulation. - *

- * - * @param file the file with a data just enough for a single - * {@code ExperimentalData} object - * @param metadata the previously loaded {@code Metadata} which includes the - * external ID and the test temperature - * @return an {@code ExperimentalData} object - * @throws IOException - */ + private static CurveReader instance = new LFRReader(); + private final static double TO_KELVIN = 273; + private final static double TO_SECONDS = 1E-3; - public ExperimentalData readSingleCurve(File file, Metadata metadata) throws IOException { - Objects.requireNonNull(file, Messages.getString("LFRReader.9")); + private LFRReader() { + // intentionally blank + } - var curve = new ExperimentalData(); - curve.setMetadata(metadata); - curve.clear(); + /** + * @return The supported extension ({@code .lfr}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("LFRReader.0"); + } + + /** + * Reads through the {@code file}, identifies the names of other files with + * individual heating curves, theirs external IDs and test temperatures (in + * degrees Celsius, later converted to Kelvin). + *

+ * Creates a {@code List} of {@code ExperimentalData} objects with the size + * equal to the number of individual entries in the master-file. Searches + * for the individual files listed in the namelist and stored in the same + * directory where the master-file has been found previously. Upon finding + * the individual files, invokes {@code readSingleCurve} on each of them + * sequentially and stores the {@code ExperimentalData} in a list. Finally, + * invokes the {@code sort} method on that list to sort it. + *

+ * + * @param file the master-file with {@code .lfr} suffix + * @return a {@code List} of @code ExperimentalData}, containing all + * information stored in both the master file and linked individual files. + * @see sort + * @see readSingleCurve + */ + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("LFRReader.1")); + + String stringSplitter = Messages.getString("LFRReader.3"); + + final String directory = file.getAbsoluteFile().getParent(); + final Map fileMap; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + fileMap = fileMap(reader, stringSplitter); + } + + return sort(convertToData(directory, stringSplitter, fileMap)); + + } + + private Map fileMap(BufferedReader reader, String stringSplitter) throws IOException { + + String delims = Messages.getString("LFRReader.2"); + StringTokenizer tokenizer; + + // skip two first lines + reader.readLine(); + reader.readLine(); + + var fileTempMap = new HashMap(); + + String tmp; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + int id = Integer.parseInt(tokenizer.nextToken(delims)); // id + + tmp = tokenizer.nextToken(delims).split(stringSplitter)[0]; // write file names without extensions + + tokenizer.nextToken(delims); // sample id + var temperature = derive(TEST_TEMPERATURE, parseDouble(tokenizer.nextToken()) + TO_KELVIN); // test + // temperature + + fileTempMap.put(tmp, new Metadata(temperature, id)); // assign metadata object with external id and + // temperature + + } + + return fileTempMap; + + } + + private List convertToData(String directory, String stringSplitter, Map map) + throws IOException { + List curves = new ArrayList<>(); + var filenames = map.keySet(); + + for (File f : new File(directory).listFiles()) { + + var name = f.getName().split(stringSplitter)[0]; + + if (filenames.contains(name)) { + curves.add(readSingleCurve(f, map.get(name))); + } + + } + + return curves; + + } + + /** + * Creates a single {@code ExperimentalData} object with the + * time-temperature information retrieved from {@code file} and using the + * previously generated {@code Metadata} object, containing the external ID + * and the test temperature of this heating curve. + *

+ * The time in Linseis files is usually stored in [ms], hence the time + * values are multiplied by {@code 1E-3} to adhere to the {@code PULsE} + * format. The signal rise is recorded in [mV], hence it represents a + * relative scale, which however is functionally linked to the temperature + * rise. {@code PULsE} does not establish this functional relation. Instead, + * it uses the signal values in the dimensionless problem formulation. + *

+ * + * @param file the file with a data just enough for a single + * {@code ExperimentalData} object + * @param metadata the previously loaded {@code Metadata} which includes the + * external ID and the test temperature + * @return an {@code ExperimentalData} object + * @throws IOException + */ + public ExperimentalData readSingleCurve(File file, Metadata metadata) throws IOException { + Objects.requireNonNull(file, Messages.getString("LFRReader.9")); - String delims = Messages.getString("LFRReader.10"); - StringTokenizer tokenizer; + var curve = new ExperimentalData(); + curve.setMetadata(metadata); + curve.clear(); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - reader.readLine(); // skip first line - double time, temp; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); + String delims = Messages.getString("LFRReader.10"); + StringTokenizer tokenizer; - time = parseDouble(tokenizer.nextToken(delims)) * TO_SECONDS; - temp = parseDouble(tokenizer.nextToken(delims)); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + reader.readLine(); // skip first line + double time, temp; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); - curve.addPoint(time, temp); + time = parseDouble(tokenizer.nextToken(delims)) * TO_SECONDS; + temp = parseDouble(tokenizer.nextToken(delims)); - } - curve.setRange(new Range(curve.getTimeSequence())); - } + curve.addPoint(time, temp); - return curve; + } + curve.setRange(new Range(curve.getTimeSequence())); + } - } + return curve; - /** - * Retrieves the single instance of this class. As this class uses a singleton - * pattern, there is only one such instance. - * - * @return the single instance of this class. - */ + } - public static CurveReader getInstance() { - return instance; - } + /** + * Retrieves the single instance of this class. As this class uses a + * singleton pattern, there is only one such instance. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/MetaFilePopulator.java b/src/main/java/pulse/io/readers/MetaFilePopulator.java index 59a9c8a0..e1f53a71 100644 --- a/src/main/java/pulse/io/readers/MetaFilePopulator.java +++ b/src/main/java/pulse/io/readers/MetaFilePopulator.java @@ -51,17 +51,17 @@ *

* The full list of keywords for the {@code .met} files are listed in the * {@code NumericPropertyKeyword} enum. - * + * *

* An example content of a valid {@code .met} file is provided below. *

- * + * *
  * 
- * Thickness	2.034 						
- * Diameter	9.88 			
- * Spot_Diameter	10.0 						
- *							
+ * Thickness	2.034
+ * Diameter	9.88
+ * Spot_Diameter	10.0
+ *
  * Test_Temperature	Pulse_Width	Spot_Diameter	Laser_Energy	Detector_Gain	TemporalShape	Detector_Iris
  * 200	200	5	2	31.81	50	TrapezoidalPulse	1
  * 201	196	5	2	31.81	100	TrapezoidalPulse	1
@@ -76,154 +76,151 @@
  * 210	400	5	2	31.81	10	TrapezoidalPulse	1
  * 
  * 
- * + * * @see pulse.properties.NumericPropertyKeyword * @see pulse.problem.laser.PulseTemporalShape */ - public class MetaFilePopulator implements AbstractPopulator { - private static MetaFilePopulator instance = new MetaFilePopulator(); - private final static double TO_KELVIN = 273; - - private MetaFilePopulator() { - // intentionally blank - } - - /** - * Gets the single instance of this class. - * - * @return a static instance of {@code MetaFilePopulator}. - */ - - public static MetaFilePopulator getInstance() { - return instance; - } - - @Override - public void populate(File file, Metadata met) throws IOException { - Objects.requireNonNull(file, Messages.getString("MetaFileReader.1")); //$NON-NLS-1$ - Map metaFormat = new HashMap<>(); - metaFormat.put(0, "ID"); // id must always be the first entry in the current row - - List tokens = new LinkedList<>(); + private static MetaFilePopulator instance = new MetaFilePopulator(); + private final static double TO_KELVIN = 273; - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + private MetaFilePopulator() { + // intentionally blank + } - for (String line = reader.readLine(); line != null; line = reader.readLine()) { + /** + * Gets the single instance of this class. + * + * @return a static instance of {@code MetaFilePopulator}. + */ + public static MetaFilePopulator getInstance() { + return instance; + } - tokens.clear(); - for (StringTokenizer st = new StringTokenizer(line); st.hasMoreTokens();) - tokens.add(st.nextToken()); + @Override + public void populate(File file, Metadata met) throws IOException { + Objects.requireNonNull(file, Messages.getString("MetaFileReader.1")); //$NON-NLS-1$ + Map metaFormat = new HashMap<>(); + metaFormat.put(0, "ID"); // id must always be the first entry in the current row - int size = tokens.size(); + List tokens = new LinkedList<>(); - if (size == 2) - processPair(tokens, met); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - else if (size > 2) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { - if (tokens.get(0).equalsIgnoreCase(metaFormat.get(0))) { + tokens.clear(); + for (StringTokenizer st = new StringTokenizer(line); st.hasMoreTokens();) { + tokens.add(st.nextToken()); + } - for (int i = 1; i < size; i++) - metaFormat.put(i, tokens.get(i)); + int size = tokens.size(); - } + if (size == 2) { + processPair(tokens, met); + } else if (size > 2) { - else if (Integer.compare(Integer.valueOf(tokens.get(0)), met.getExternalID()) == 0) { + if (tokens.get(0).equalsIgnoreCase(metaFormat.get(0))) { - processList(tokens, met, metaFormat); + for (int i = 1; i < size; i++) { + metaFormat.put(i, tokens.get(i)); + } - } + } else if (Integer.compare(Integer.valueOf(tokens.get(0)), met.getExternalID()) == 0) { - } + processList(tokens, met, metaFormat); - } + } - } - } + } - private void processPair(List tokens, Metadata met) { - List> val = new ArrayList<>(); - var entry = new ImmutableDataEntry<>(tokens.get(0), tokens.get(1)); - val.add(entry); + } - try { - translate(val, met); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Error changing property in Metadata object. Details below."); - e.printStackTrace(); - } - } + } + } - private void processList(List tokens, Metadata met, Map metaFormat) { - int size = tokens.size(); - List> values = new ArrayList<>(size); + private void processPair(List tokens, Metadata met) { + List> val = new ArrayList<>(); + var entry = new ImmutableDataEntry<>(tokens.get(0), tokens.get(1)); + val.add(entry); - for (int i = 1; i < size; i++) - values.add(new ImmutableDataEntry<>(metaFormat.get(i), tokens.get(i))); + try { + translate(val, met); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Error changing property in Metadata object. Details below."); + e.printStackTrace(); + } + } - try { - translate(values, met); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Error changing property in Metadata object. Details below."); - e.printStackTrace(); - } - } + private void processList(List tokens, Metadata met, Map metaFormat) { + int size = tokens.size(); + List> values = new ArrayList<>(size); - private void translate(List> data, Metadata met) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (int i = 1; i < size; i++) { + values.add(new ImmutableDataEntry<>(metaFormat.get(i), tokens.get(i))); + } - for (var dataEntry : data) { + try { + translate(values, met); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Error changing property in Metadata object. Details below."); + e.printStackTrace(); + } + } - var optional = findAny(dataEntry.getKey()); + private void translate(List> data, Metadata met) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - // numeric properties - if (optional.isPresent()) { - var key = optional.get(); + for (var dataEntry : data) { - double value = Double.valueOf(dataEntry.getValue()); - if (key == TEST_TEMPERATURE) - value += TO_KELVIN; + var optional = findAny(dataEntry.getKey()); - var proto = def(key); - value /= proto.getDimensionFactor().doubleValue(); + // numeric properties + if (optional.isPresent()) { + var key = optional.get(); - if (isValueSensible(proto, value)) { - proto.setValue(value); - met.set(key, proto); - } + double value = Double.valueOf(dataEntry.getValue()); + if (key == TEST_TEMPERATURE) { + value += TO_KELVIN; + } - } + var proto = def(key); + value /= proto.getDimensionFactor().doubleValue(); - // generic properties - else { + if (isValueSensible(proto, value)) { + proto.setValue(value); + met.set(key, proto); + } - for (Property genericEntry : met.genericProperties()) { + } // generic properties + else { - if (genericEntry instanceof InstanceDescriptor - || dataEntry.getKey().equalsIgnoreCase(genericEntry.getClass().getSimpleName())) { + for (Property genericEntry : met.genericProperties()) { - if (genericEntry.attemptUpdate(dataEntry.getValue())) - met.updateProperty(instance, genericEntry); + if (genericEntry instanceof InstanceDescriptor + || dataEntry.getKey().equalsIgnoreCase(genericEntry.getClass().getSimpleName())) { - } + if (genericEntry.attemptUpdate(dataEntry.getValue())) { + met.updateProperty(instance, genericEntry); + } - } + } - } + } - } + } - } + } - /** - * @return {@code .met}, an internal PULsE meta-file format. - */ + } - @Override - public String getSupportedExtension() { - return Messages.getString("MetaFileReader.0"); //$NON-NLS-1$ - } + /** + * @return {@code .met}, an internal PULsE meta-file format. + */ + @Override + public String getSupportedExtension() { + return Messages.getString("MetaFileReader.0"); //$NON-NLS-1$ + } } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 3b4c31c3..d3320e3f 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -20,144 +20,148 @@ import pulse.ui.Messages; /** - * Reads the .CSV files exported from Proteus LFA Analysis software. To load Proteus measurements in PULsE, - * the detector signal needs to be imported first, followed by the pulse data. + * Reads the .CSV files exported from Proteus LFA Analysis software. To load + * Proteus measurements in PULsE, the detector signal needs to be imported + * first, followed by the pulse data. *

* Note that by default the decimal separator is assumed to be a point ("."). *

*/ - public class NetzschCSVReader implements CurveReader { - private static NetzschCSVReader instance = new NetzschCSVReader(); - - private final static double TO_KELVIN = 273; - protected final static double TO_SECONDS = 1E-3; - private final static double TO_METRES = 1E-3; - - private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; - private final static String SHOT_DATA = "Shot_data"; - private final static String DETECTOR = "DETECTOR"; - private final static String THICKNESS = "Thickness_RT"; - private final static String DIAMETER = "Diameter"; - - /** - * Note comma is included as a delimiter character here. - */ - - public final static String delims = "[#();,/°Cx%^]+"; - - private NetzschCSVReader() { - // intentionally blank - } + private static NetzschCSVReader instance = new NetzschCSVReader(); + + private final static double TO_KELVIN = 273; + protected final static double TO_SECONDS = 1E-3; + private final static double TO_METRES = 1E-3; + + private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; + private final static String SHOT_DATA = "Shot_data"; + private final static String DETECTOR = "DETECTOR"; + private final static String THICKNESS = "Thickness_RT"; + private final static String DIAMETER = "Diameter"; + + /** + * Note comma is included as a delimiter character here. + */ + public final static String delims = "[#();,/°Cx%^]+"; + + private NetzschCSVReader() { + // intentionally blank + } + + /** + * @return The supported extension ({@code .csv}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } + + /** + * Reads {@code file}, assuming that it contains data generated by Proteus + * with the detector signal. + *

+ * This will throw an {@code IllegalArgumentException} if the first entry in + * this file does not contain the {@value SHOT_DATA} string. If this is + * found, then an ID is extracted from the file, which will then be used to + * associate a pulse with the newly create {@code ExperimentalData} (this + * requires another reader. When the ID is identified, the file is searched + * for the keywords {@value THICKNESS} and {@value SAMPLE_TEMPERATURE} to + * determine the sample thickness and baseline temperature of the shot. Then + * the method proceeds to search for the {@code DETECTOR} keyword, marking + * the beginning of the experimental time-signal sequence. If, for example, + * the file only contains the pulse data, the method will return an empty + * list and print an error message in the log, saying that the file was + * skipped. Otherwise, the time-signal sequence will be read, taking care to + * convert the time (in milliseconds by default) to second (used by default + * in PULsE). + *

+ * + * @return a list containing either zero elements, if the procedure failed, + * or one element, corresponding to the stored shot data. + * + */ + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + ExperimentalData curve = new ExperimentalData(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + int shotId = determineShotID(reader, file); + + var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); + final double thickness = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + + tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); + final double diameter = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + + tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); + final double sampleTemperature = Double.parseDouble(tempTokens[tempTokens.length - 1]) + TO_KELVIN; + + /* + * Finds the detector keyword. + */ + var detectorLabel = findLineByLabel(reader, DETECTOR, delims); - /** - * @return The supported extension ({@code .csv}). - */ + if (detectorLabel == null) { + System.err.println("Skipping " + file.getName()); + return new ArrayList<>(); + } - @Override - public String getSupportedExtension() { - return Messages.getString("NetzschCSVReader.0"); - } + reader.readLine(); + populate(curve, reader); - /** - * Reads {@code file}, assuming that it contains data generated by Proteus with the detector signal. - *

- * This will throw an {@code IllegalArgumentException} if the first entry in this file does not contain the - * {@value SHOT_DATA} string. If this is found, then an ID is extracted from the file, which will then be used - * to associate a pulse with the newly create {@code ExperimentalData} (this requires another reader. - * When the ID is identified, the file is searched for the keywords {@value THICKNESS} and {@value SAMPLE_TEMPERATURE} to - * determine the sample thickness and baseline temperature of the shot. Then the method proceeds to search for the {@code DETECTOR} keyword, - * marking the beginning of the experimental time-signal sequence. If, for example, the file only contains - * the pulse data, the method will return an empty list and print an error message in the log, saying that the file - * was skipped. Otherwise, the time-signal sequence will be read, taking care to convert the time (in milliseconds - * by default) to second (used by default in PULsE). - *

- * @return a list containing either zero elements, if the procedure failed, or one element, corresponding to - * the stored shot data. - * - */ - - @Override - public List read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("DATReader.1")); - - ExperimentalData curve = new ExperimentalData(); - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - - int shotId = determineShotID(reader, file); - - var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); - final double thickness = Double.parseDouble( tempTokens[tempTokens.length - 1] ) * TO_METRES; - - tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); - final double diameter = Double.parseDouble( tempTokens[tempTokens.length - 1] ) * TO_METRES; - - tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); - final double sampleTemperature = Double.parseDouble( tempTokens[tempTokens.length - 1] ) + TO_KELVIN; - - /* - * Finds the detector keyword. - */ - - var detectorLabel = findLineByLabel(reader, DETECTOR, delims); - - if(detectorLabel == null) { - System.err.println("Skipping " + file.getName()); - return new ArrayList<>(); - } - - reader.readLine(); - populate(curve, reader); - - var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); - met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); - met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); - met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, 0.85*diameter)); - met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94*diameter)); - - curve.setMetadata(met); - curve.setRange(new Range(curve.getTimeSequence())); - - } + var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); + met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); + met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, 0.85 * diameter)); + met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94 * diameter)); - return new ArrayList<>(Arrays.asList(curve)); + curve.setMetadata(met); + curve.setRange(new Range(curve.getTimeSequence())); - } - - protected static void populate(AbstractData data, BufferedReader reader) throws IOException { - double time; - double power; - String[] tokens; - - for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { - tokens = line.split(delims); - - time = Double.parseDouble(tokens[0]) * NetzschCSVReader.TO_SECONDS; - power = Double.parseDouble(tokens[1]); - data.addPoint(time, power); - } - - } - - protected static int determineShotID(BufferedReader reader, File file) throws IOException { - String[] shotID = reader.readLine().split(delims); - - int shotId = -1; - - //check if first entry makes sense - if(!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) - throw new IllegalArgumentException(file.getName() - + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); - else - shotId = Integer.parseInt(shotID[shotID.length - 1]); - - return shotId; - - } - - /* + } + + return new ArrayList<>(Arrays.asList(curve)); + + } + + protected static void populate(AbstractData data, BufferedReader reader) throws IOException { + double time; + double power; + String[] tokens; + + for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { + tokens = line.split(delims); + + time = Double.parseDouble(tokens[0]) * NetzschCSVReader.TO_SECONDS; + power = Double.parseDouble(tokens[1]); + data.addPoint(time, power); + } + + } + + protected static int determineShotID(BufferedReader reader, File file) throws IOException { + String[] shotID = reader.readLine().split(delims); + + int shotId = -1; + + //check if first entry makes sense + if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { + throw new IllegalArgumentException(file.getName() + + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); + } else { + shotId = Integer.parseInt(shotID[shotID.length - 1]); + } + + return shotId; + + } + + /* private double parseDoubleWithComma(String s) { var format = NumberFormat.getInstance(Locale.GERMANY); try { @@ -168,39 +172,38 @@ private double parseDoubleWithComma(String s) { } return Double.NaN; } - */ - - protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { - - String line = ""; - String[] tokens; - - //find keyword - outer : for(line = reader.readLine(); line != null; line = reader.readLine()) { - - tokens = line.split(delims); - - for(String token : tokens) { - if(token.equalsIgnoreCase(label)) - break outer; - } - - } - - return line; - - } - + */ + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { - /** - * As this class uses the singleton pattern, only one instance is created using - * an empty no-argument constructor. - * - * @return the single instance of this class. - */ + String line = ""; + String[] tokens; - public static CurveReader getInstance() { - return instance; - } + //find keyword + outer: + for (line = reader.readLine(); line != null; line = reader.readLine()) { + + tokens = line.split(delims); + + for (String token : tokens) { + if (token.equalsIgnoreCase(label)) { + break outer; + } + } + + } + + return line; + + } + + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 94137fe5..2b67fe4c 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -13,66 +13,66 @@ * Reads numeric pulse data generated by the Proteus LFA Analysis export tool. * The data must have a decimal point separator and should follow, in general, * the same rules set by the NetzschPulseCSVReader + * * @see pulse.io.reader.NetzschCSVReader * */ - public class NetzschPulseCSVReader implements PulseDataReader { - private static PulseDataReader instance = new NetzschPulseCSVReader(); - - private final static String PULSE = "Laser_pulse_data"; - - private NetzschPulseCSVReader() { - // intentionally blank - } + private static PulseDataReader instance = new NetzschPulseCSVReader(); - /** - * @return The supported extension ({@code .csv}). - */ + private final static String PULSE = "Laser_pulse_data"; - @Override - public String getSupportedExtension() { - return Messages.getString("NetzschCSVReader.0"); - } - - /** - * This performs a basic check, finding the shot ID, which is then passed to a - * new {@code NumericPulseData} object. The latter is populated using the - * time-power sequence stored in this file. If the {@value PULSE} keyword is not found, - * the method will display an error. - * @see pulse.io.readers.NetzschCSVReader.read() - * @return a new {@code NumericPulseData} object encapsulating the contents of {@code file} - */ - - @Override - public NumericPulseData read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("DATReader.1")); - - NumericPulseData data; - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - - int shotId = NetzschCSVReader.determineShotID(reader, file); - data = new NumericPulseData(shotId); - - var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.delims); - - if(pulseLabel == null) { - System.err.println("Skipping " + file.getName()); - return null; - } - - reader.readLine(); - NetzschCSVReader.populate(data, reader); - - } + private NetzschPulseCSVReader() { + // intentionally blank + } - return data; + /** + * @return The supported extension ({@code .csv}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } - } - - /* + /** + * This performs a basic check, finding the shot ID, which is then passed to + * a new {@code NumericPulseData} object. The latter is populated using the + * time-power sequence stored in this file. If the {@value PULSE} keyword is + * not found, the method will display an error. + * + * @see pulse.io.readers.NetzschCSVReader.read() + * @return a new {@code NumericPulseData} object encapsulating the contents + * of {@code file} + */ + @Override + public NumericPulseData read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + NumericPulseData data; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + int shotId = NetzschCSVReader.determineShotID(reader, file); + data = new NumericPulseData(shotId); + + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.delims); + + if (pulseLabel == null) { + System.err.println("Skipping " + file.getName()); + return null; + } + + reader.readLine(); + NetzschCSVReader.populate(data, reader); + + } + + return data; + + } + + /* private double parseDoubleWithComma(String s) { var format = NumberFormat.getInstance(Locale.GERMANY); try { @@ -83,17 +83,15 @@ private double parseDoubleWithComma(String s) { } return Double.NaN; } - */ + */ + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static PulseDataReader getInstance() { + return instance; + } - /** - * As this class uses the singleton pattern, only one instance is created using - * an empty no-argument constructor. - * - * @return the single instance of this class. - */ - - public static PulseDataReader getInstance() { - return instance; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/PulseDataReader.java b/src/main/java/pulse/io/readers/PulseDataReader.java index 7841e4a4..b478cf62 100644 --- a/src/main/java/pulse/io/readers/PulseDataReader.java +++ b/src/main/java/pulse/io/readers/PulseDataReader.java @@ -9,15 +9,12 @@ * A reader for importing numeric pulse data -- if available. * */ - public interface PulseDataReader extends AbstractReader { - - /** - * Converts the ASCII file to a {@code NumericPulseData} object. - */ - - @Override - public abstract NumericPulseData read(File file) throws IOException; + /** + * Converts the ASCII file to a {@code NumericPulseData} object. + */ + @Override + public abstract NumericPulseData read(File file) throws IOException; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/QuadratureReader.java b/src/main/java/pulse/io/readers/QuadratureReader.java index caba2913..fbcd7520 100644 --- a/src/main/java/pulse/io/readers/QuadratureReader.java +++ b/src/main/java/pulse/io/readers/QuadratureReader.java @@ -18,84 +18,81 @@ * associated resource folder. * */ - public class QuadratureReader implements AbstractReader { - private final static String SUPPORTED_EXTENSION = "quad"; - - private static QuadratureReader instance = new QuadratureReader(); - - private QuadratureReader() { - // intentionally blank - } - - /** - * Reads an ordinate set. Scans the first line for any keywords and then treats - * any subsequent lines as consisting of two tokens, which correspond to the - * quadrature node and weight. Ignores all other information. - */ - - @Override - public OrdinateSet read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + private final static String SUPPORTED_EXTENSION = "quad"; - // ignore extension! + private static QuadratureReader instance = new QuadratureReader(); - String name = file.getName().split("\\.")[0]; + private QuadratureReader() { + // intentionally blank + } - OrdinateSet set = null; + /** + * Reads an ordinate set. Scans the first line for any keywords and then + * treats any subsequent lines as consisting of two tokens, which correspond + * to the quadrature node and weight. Ignores all other information. + */ + @Override + public OrdinateSet read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - String delims = Messages.getString("}{,\t "); - StringTokenizer tokenizer; + // ignore extension! + String name = file.getName().split("\\.")[0]; - List nodes = new ArrayList<>(); - List weights = new ArrayList<>(); + OrdinateSet set = null; - String line = ""; + String delims = Messages.getString("}{,\t "); + StringTokenizer tokenizer; - try (var fr = new FileReader(file); var reader = new BufferedReader(fr)) { + List nodes = new ArrayList<>(); + List weights = new ArrayList<>(); - // first line with declarations (e.g. IGNORE, etc.) - tokenizer = new StringTokenizer(reader.readLine()); + String line = ""; - while (tokenizer.hasMoreTokens()) - if (tokenizer.nextToken(delims).equalsIgnoreCase("IGNORE")) - return null; + try (var fr = new FileReader(file); var reader = new BufferedReader(fr)) { - for (line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - nodes.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - weights.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - } + // first line with declarations (e.g. IGNORE, etc.) + tokenizer = new StringTokenizer(reader.readLine()); - set = new OrdinateSet(name, nodes.stream().mapToDouble(d -> d).toArray(), - weights.stream().mapToDouble(d -> d).toArray()); + while (tokenizer.hasMoreTokens()) { + if (tokenizer.nextToken(delims).equalsIgnoreCase("IGNORE")) { + return null; + } + } - reader.close(); + for (line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + nodes.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + weights.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + } - } + set = new OrdinateSet(name, nodes.stream().mapToDouble(d -> d).toArray(), + weights.stream().mapToDouble(d -> d).toArray()); - return set; + reader.close(); - } + } - /** - * @return {@code quad} - */ + return set; - @Override - public String getSupportedExtension() { - return SUPPORTED_EXTENSION; - } + } - /** - * Returns the single instance of this class. - * - * @return an instance of {@code QuadratureReader}. - */ + /** + * @return {@code quad} + */ + @Override + public String getSupportedExtension() { + return SUPPORTED_EXTENSION; + } - public static QuadratureReader getInstance() { - return instance; - } + /** + * Returns the single instance of this class. + * + * @return an instance of {@code QuadratureReader}. + */ + public static QuadratureReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index b00b3775..83c72eef 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -29,258 +29,254 @@ *

* This class heavily relies on the stream API from the Java SDK. *

- * + * * @see pulse.util.Reflexive * @see pulse.io.readers.CurveReader * @see pulse.io.readers.DatasetReader */ - public class ReaderManager { - @SuppressWarnings("rawtypes") - private static List allReaders = allReaders(); - - private static List allDataExtensions = supportedExtensions(ReaderManager.curveReaders()); - private static List allPulseExtensions = supportedExtensions(ReaderManager.pulseReaders()); - private static List allDatasetExtensions = supportedExtensions(ReaderManager.datasetReaders()); - - private ReaderManager() { - // intentionally blank - } - - private static List supportedExtensions(List> readers) { - return readers.stream().map(reader -> reader.getSupportedExtension()).collect(Collectors.toList()); - } - - /** - * Returns a list of extensions recognised by the available - * {@code CurveReader}s. - * - * @return a {@code List} of {@String} objects representing file extensions - */ - - public static List getCurveExtensions() { - return allDataExtensions; - } - - public static List getPulseExtensions() { - return allPulseExtensions; - } - - /** - * Returns a list of extensions recognised by the available - * {@code DatasetReader}s. - * - * @return a {@code List} of {@String} objects representing file extensions - */ - - public static List getDatasetExtensions() { - return allDatasetExtensions; - } - - /** - * Finds all classes assignable from {@code AbstractReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package for the classes to be searched in - * @return a list of {@code AbstractReader}s in {@code pckgnamge} - */ - - @SuppressWarnings("rawtypes") - private static List allReaders(String pckgname) { - return ReflexiveFinder.simpleInstances(pckgname).stream().filter(ref -> ref instanceof AbstractReader) - .map(reflexive -> (AbstractReader) reflexive).collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code AbstractReader} within this - * package. - * - * @return a list of {@code AbstractReader}s in this package - */ - - @SuppressWarnings("rawtypes") - private static List allReaders() { - return allReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Finds all classes assignable from {@code CurveReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package to conduct search in. - * @return a list of {@code CurveReader}s in {@code pckgname} - */ - - public static List findCurveReaders(String pckgname) { - return allReaders.stream().filter(reader -> reader instanceof CurveReader).map(r -> (CurveReader) r) - .collect(Collectors.toList()); - } - - public static List findPulseReaders(String pckgname) { - return allReaders.stream().filter(reader -> reader instanceof PulseDataReader).map(r -> (PulseDataReader) r) - .collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code CurveReader} within this - * package. - * - * @return a list of {@code CurveReader}s in this package - */ - - public static List curveReaders() { - return findCurveReaders(ReaderManager.class.getPackage().getName()); - } - - public static List pulseReaders() { - return findPulseReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Finds all classes assignable from {@code DatasetReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package to conduct search in. - * @return a list of {@code DatasetReader}s in {@code pckgname} - */ - - public static List findDatasetReaders(String pckgname) { - return allReaders.stream().filter(reader -> reader instanceof DatasetReader).map(r -> (DatasetReader) r) - .collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code DatasetReader} within this - * package. - * - * @return a list of {@code DatasetReader}s in this package - */ - - public static List datasetReaders() { - return findDatasetReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Attempts to find a {@code DatasetReader} for processing {@code file}. - * - * @param file the target file supposedly containing data for an - * {@code InterpolationDataset}. - * @return an {@code InterpolationDataset} extracted from {@file} using the - * first available {@code DatasetReader} from the list - * @throws IOException if the reader has been found, but an error - * occurred when reading the file - * @throws IllegalArgumentException if the file has an unsupported extension - */ - - public static T read(List> readers, File file) { - Objects.requireNonNull(readers); - - var optional = readers.stream() - .filter(reader -> AbstractHandler.extensionsMatch(file, reader.getSupportedExtension())).findFirst(); - - if (!optional.isPresent()) - throw new IllegalArgumentException(Messages.getString("ReaderManager.1") + file.getName()); - - T result = null; - - try { - result = optional.get().read(file); - } catch (IOException e) { - System.err.println("Error reading " + file + " with reader: " + optional.get()); - e.printStackTrace(); - } - - return result; - - } - - /** - * Obtains a set of files in {@code directory} and attemps to convert each file - * to {@code T} using {@code readers}. - * - * @param a type recognised by {@code readers} - * @param readers a list of {@code AbstractReader}s capable of processing - * {@code T} - * @param directory a directory - * @return the set of converted {@code T} objects - * @throws IllegalArgumentException if second argument is not a directory - */ - - public static Set readDirectory(List> readers, File directory) - throws IllegalArgumentException { - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var list = new HashSet(); - - for(File f : directory.listFiles()) - list.add( read(readers, f) ); - - return list; - } - - /** - * This method is specifically introduced to handle multiple files in a - * resources folder enclosed within the {@code jar} archive. A list of files is - * required to be included in the same location, which is scanned and each entry - * is added to a temporary list of names. A combination of these names with the - * relative {@code location} allows reading separate files and collating the - * result in a unique {@code Set}. - * - * @param a type recognised by the {@code reader} - * @param reader the reader specifically targetted at {@code T} - * @param location the relative location of files - * @param listName the name of the list-file - * @return a unique {@code Set} of {@code T} - */ - - public static Set load(AbstractReader reader, String location, String listName) { - - var stream = ReaderManager.class.getResourceAsStream(location + listName); - var names = new ArrayList(); - - try (Scanner s = new Scanner(stream)) { - while (s.hasNext()) - names.add(s.next()); - } - - return names.stream().map(name -> readSpecific(reader, location, name)).map(obj -> (T) obj) - .collect(Collectors.toSet()); - - } - - private static T readSpecific(AbstractReader reader, String location, String name) { - T result = null; - try { - var f = File.createTempFile(name, ".tmp"); - f.deleteOnExit(); - FileUtils.copyInputStreamToFile(ReaderManager.class.getResourceAsStream(location + name), f); - result = reader.read(f); - } catch (IOException e) { - System.err.println("Unable to read: " + name); - e.printStackTrace(); - } - return result; - } - - public static Version readVersion() { - var versionInfoFile = Version.class.getResource("/Version.txt"); - String versionLabel = ""; - long date = 0; - try { - date = versionInfoFile.openConnection().getLastModified(); - } catch (IOException e1) { - System.err.println("Could not connect to local version file!"); - e1.printStackTrace(); - } - try { - versionLabel = IOUtils.toString(versionInfoFile, "UTF-8"); - } catch (IOException e) { - System.err.println("Could not read current version!"); - e.printStackTrace(); - } - return new Version(versionLabel, date); - } - -} \ No newline at end of file + @SuppressWarnings("rawtypes") + private static List allReaders = allReaders(); + + private static List allDataExtensions = supportedExtensions(ReaderManager.curveReaders()); + private static List allPulseExtensions = supportedExtensions(ReaderManager.pulseReaders()); + private static List allDatasetExtensions = supportedExtensions(ReaderManager.datasetReaders()); + + private ReaderManager() { + // intentionally blank + } + + private static List supportedExtensions(List> readers) { + return readers.stream().map(reader -> reader.getSupportedExtension()).collect(Collectors.toList()); + } + + /** + * Returns a list of extensions recognised by the available + * {@code CurveReader}s. + * + * @return a {@code List} of { + * @String} objects representing file extensions + */ + public static List getCurveExtensions() { + return allDataExtensions; + } + + public static List getPulseExtensions() { + return allPulseExtensions; + } + + /** + * Returns a list of extensions recognised by the available + * {@code DatasetReader}s. + * + * @return a {@code List} of { + * @String} objects representing file extensions + */ + public static List getDatasetExtensions() { + return allDatasetExtensions; + } + + /** + * Finds all classes assignable from {@code AbstractReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package for the classes to be searched in + * @return a list of {@code AbstractReader}s in {@code pckgnamge} + */ + @SuppressWarnings("rawtypes") + private static List allReaders(String pckgname) { + return ReflexiveFinder.simpleInstances(pckgname).stream().filter(ref -> ref instanceof AbstractReader) + .map(reflexive -> (AbstractReader) reflexive).collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code AbstractReader} within + * this + * package. + * + * @return a list of {@code AbstractReader}s in this package + */ + @SuppressWarnings("rawtypes") + private static List allReaders() { + return allReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Finds all classes assignable from {@code CurveReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package to conduct search in. + * @return a list of {@code CurveReader}s in {@code pckgname} + */ + public static List findCurveReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof CurveReader).map(r -> (CurveReader) r) + .collect(Collectors.toList()); + } + + public static List findPulseReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof PulseDataReader).map(r -> (PulseDataReader) r) + .collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code CurveReader} within this + * package. + * + * @return a list of {@code CurveReader}s in this package + */ + public static List curveReaders() { + return findCurveReaders(ReaderManager.class.getPackage().getName()); + } + + public static List pulseReaders() { + return findPulseReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Finds all classes assignable from {@code DatasetReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package to conduct search in. + * @return a list of {@code DatasetReader}s in {@code pckgname} + */ + public static List findDatasetReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof DatasetReader).map(r -> (DatasetReader) r) + .collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code DatasetReader} within + * this + * package. + * + * @return a list of {@code DatasetReader}s in this package + */ + public static List datasetReaders() { + return findDatasetReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Attempts to find a {@code DatasetReader} for processing {@code file}. + * + * @param file the target file supposedly containing data for an + * {@code InterpolationDataset}. + * @return an {@code InterpolationDataset} extracted from { + * @file} using the first available {@code DatasetReader} from the list + * @throws IOException if the reader has been found, but an error occurred + * when reading the file + * @throws IllegalArgumentException if the file has an unsupported extension + */ + public static T read(List> readers, File file) { + Objects.requireNonNull(readers); + + var optional = readers.stream() + .filter(reader -> AbstractHandler.extensionsMatch(file, reader.getSupportedExtension())).findFirst(); + + if (!optional.isPresent()) { + throw new IllegalArgumentException(Messages.getString("ReaderManager.1") + file.getName()); + } + + T result = null; + + try { + result = optional.get().read(file); + } catch (IOException e) { + System.err.println("Error reading " + file + " with reader: " + optional.get()); + e.printStackTrace(); + } + + return result; + + } + + /** + * Obtains a set of files in {@code directory} and attemps to convert each + * file to {@code T} using {@code readers}. + * + * @param a type recognised by {@code readers} + * @param readers a list of {@code AbstractReader}s capable of processing + * {@code T} + * @param directory a directory + * @return the set of converted {@code T} objects + * @throws IllegalArgumentException if second argument is not a directory + */ + public static Set readDirectory(List> readers, File directory) + throws IllegalArgumentException { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + var list = new HashSet(); + + for (File f : directory.listFiles()) { + list.add(read(readers, f)); + } + + return list; + } + + /** + * This method is specifically introduced to handle multiple files in a + * resources folder enclosed within the {@code jar} archive. A list of files + * is required to be included in the same location, which is scanned and + * each entry is added to a temporary list of names. A combination of these + * names with the relative {@code location} allows reading separate files + * and collating the result in a unique {@code Set}. + * + * @param a type recognised by the {@code reader} + * @param reader the reader specifically targetted at {@code T} + * @param location the relative location of files + * @param listName the name of the list-file + * @return a unique {@code Set} of {@code T} + */ + public static Set load(AbstractReader reader, String location, String listName) { + + var stream = ReaderManager.class.getResourceAsStream(location + listName); + var names = new ArrayList(); + + try (Scanner s = new Scanner(stream)) { + while (s.hasNext()) { + names.add(s.next()); + } + } + + return names.stream().map(name -> readSpecific(reader, location, name)).map(obj -> (T) obj) + .collect(Collectors.toSet()); + + } + + private static T readSpecific(AbstractReader reader, String location, String name) { + T result = null; + try { + var f = File.createTempFile(name, ".tmp"); + f.deleteOnExit(); + FileUtils.copyInputStreamToFile(ReaderManager.class.getResourceAsStream(location + name), f); + result = reader.read(f); + } catch (IOException e) { + System.err.println("Unable to read: " + name); + e.printStackTrace(); + } + return result; + } + + public static Version readVersion() { + var versionInfoFile = Version.class.getResource("/Version.txt"); + String versionLabel = ""; + long date = 0; + try { + date = versionInfoFile.openConnection().getLastModified(); + } catch (IOException e1) { + System.err.println("Could not connect to local version file!"); + e1.printStackTrace(); + } + try { + versionLabel = IOUtils.toString(versionInfoFile, "UTF-8"); + } catch (IOException e) { + System.err.println("Could not read current version!"); + e.printStackTrace(); + } + return new Version(versionLabel, date); + } + +} diff --git a/src/main/java/pulse/io/readers/TBLReader.java b/src/main/java/pulse/io/readers/TBLReader.java index c067397f..65cae937 100644 --- a/src/main/java/pulse/io/readers/TBLReader.java +++ b/src/main/java/pulse/io/readers/TBLReader.java @@ -27,7 +27,7 @@ *

* Below is an example of a valid {@code .tbl} file: *

- * + * *
  * 
  * -273	11000.00
@@ -43,73 +43,72 @@
  * 450	10814.93
  * 500	10798.58
  * 550	10782.14
- 
+ * 
  * 
*/ - public class TBLReader implements DatasetReader { - private static DatasetReader instance = new TBLReader(); - - private TBLReader() { - // intentionally blank - } - - /** - * @return a String equal to '{@code tbl}' - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("TBLReader.0"); - } - - /** - * As this class is built using a singleton pattern, only one instance exists. - * - * @return the static instance of this class - */ - - public static DatasetReader getInstance() { - return instance; - } - - /** - * Reads through a {@code file} with {@code .tbl extension}, converting each row - * into an {@code ImmutableDataEntry}, which is then added to a - * newly created {@code InterpolationDataset}. Upon completion, the - * {@code doInterpolation()} method of {@code InterpolationDataset} is invoked. - * - * @see pulse.input.InterpolationDataset.doInterpolation() - * @param file a {@code File} with {@code tbl} extension - */ - - @Override - public InterpolationDataset read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - - if (!isExtensionSupported(file)) - throw new IllegalArgumentException("Extension not supported: " + file.getName()); - - var curve = new InterpolationDataset(); - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String delims = Messages.getString("TBLReader.2"); - StringTokenizer tokenizer; - - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - curve.add(new ImmutableDataEntry<>(parse(tokenizer, delims), parse(tokenizer, delims))); - } - } - - curve.doInterpolation(); - return curve; - - } - - private static Double parse(StringTokenizer tokenizer, String delims) { - return Double.parseDouble(tokenizer.nextToken(delims)); - } - -} \ No newline at end of file + private static DatasetReader instance = new TBLReader(); + + private TBLReader() { + // intentionally blank + } + + /** + * @return a String equal to '{@code tbl}' + */ + @Override + public String getSupportedExtension() { + return Messages.getString("TBLReader.0"); + } + + /** + * As this class is built using a singleton pattern, only one instance + * exists. + * + * @return the static instance of this class + */ + public static DatasetReader getInstance() { + return instance; + } + + /** + * Reads through a {@code file} with {@code .tbl extension}, converting each + * row into an {@code ImmutableDataEntry}, which is then + * added to a newly created {@code InterpolationDataset}. Upon completion, + * the {@code doInterpolation()} method of {@code InterpolationDataset} is + * invoked. + * + * @see pulse.input.InterpolationDataset.doInterpolation() + * @param file a {@code File} with {@code tbl} extension + */ + @Override + public InterpolationDataset read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + + if (!isExtensionSupported(file)) { + throw new IllegalArgumentException("Extension not supported: " + file.getName()); + } + + var curve = new InterpolationDataset(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String delims = Messages.getString("TBLReader.2"); + StringTokenizer tokenizer; + + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + curve.add(new ImmutableDataEntry<>(parse(tokenizer, delims), parse(tokenizer, delims))); + } + } + + curve.doInterpolation(); + return curve; + + } + + private static Double parse(StringTokenizer tokenizer, String delims) { + return Double.parseDouble(tokenizer.nextToken(delims)); + } + +} diff --git a/src/main/java/pulse/io/readers/package-info.java b/src/main/java/pulse/io/readers/package-info.java index f3566804..b94086f9 100644 --- a/src/main/java/pulse/io/readers/package-info.java +++ b/src/main/java/pulse/io/readers/package-info.java @@ -8,5 +8,4 @@ * {@code AbstractReader} and place it into this package (otherwise, the * {@code ReaderManager} won't know where the class is). */ - -package pulse.io.readers; \ No newline at end of file +package pulse.io.readers; diff --git a/src/main/java/pulse/math/AbstractIntegrator.java b/src/main/java/pulse/math/AbstractIntegrator.java index 1992f3a6..1fadb3aa 100644 --- a/src/main/java/pulse/math/AbstractIntegrator.java +++ b/src/main/java/pulse/math/AbstractIntegrator.java @@ -10,61 +10,56 @@ * or more variables and the other to actually do the integration. * */ - public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive { - private Segment integrationBounds; - - /** - * Creates an {@code AbstractIntegrator} with the specified integration bounds. - * - * @param bounds the integration bounds. - */ - - public AbstractIntegrator(Segment bounds) { - setBounds(bounds); - } - - /** - * Calculates the definite integral within the specified integration bounds. - * - * @return the value of the integral - */ - - public abstract double integrate(); - - /** - * Calculates the integrand function. - * - * @param vars one or more variables - * @return the value of the integrand at the specified variable values. - */ - - public abstract double integrand(double... vars); - - /** - * Retrieves the integration bounds - * - * @return the integration bounds. - */ - - public Segment getBounds() { - return integrationBounds; - } - - /** - * Simply sets the integration bounds to {@code bounds} - * - * @param bounds the new integration bounds. - */ - - public void setBounds(Segment bounds) { - this.integrationBounds = bounds; - } - - @Override - public String getPrefix() { - return "Integrator"; - } - -} \ No newline at end of file + private Segment integrationBounds; + + /** + * Creates an {@code AbstractIntegrator} with the specified integration + * bounds. + * + * @param bounds the integration bounds. + */ + public AbstractIntegrator(Segment bounds) { + setBounds(bounds); + } + + /** + * Calculates the definite integral within the specified integration bounds. + * + * @return the value of the integral + */ + public abstract double integrate(); + + /** + * Calculates the integrand function. + * + * @param vars one or more variables + * @return the value of the integrand at the specified variable values. + */ + public abstract double integrand(double... vars); + + /** + * Retrieves the integration bounds + * + * @return the integration bounds. + */ + public Segment getBounds() { + return integrationBounds; + } + + /** + * Simply sets the integration bounds to {@code bounds} + * + * @param bounds the new integration bounds. + */ + public void setBounds(Segment bounds) { + this.integrationBounds = bounds; + } + + @Override + public String getPrefix() { + return "Integrator"; + } + +} diff --git a/src/main/java/pulse/math/FixedIntervalIntegrator.java b/src/main/java/pulse/math/FixedIntervalIntegrator.java index 8a54bf29..8a857986 100644 --- a/src/main/java/pulse/math/FixedIntervalIntegrator.java +++ b/src/main/java/pulse/math/FixedIntervalIntegrator.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -21,105 +22,100 @@ * such partitioning times the integration weights. * */ - public abstract class FixedIntervalIntegrator extends AbstractIntegrator { - private int integrationSegments; - - /** - * Creates a {@code FixedIntervalIntegrator} with the specified integration - * bounds and a default number of integration segments. - * - * @param bounds the integration bounds - */ - - public FixedIntervalIntegrator(Segment bounds) { - this(bounds, def(INTEGRATION_SEGMENTS)); - } - - /** - * Creates a {@code FixedIntervalIntegrator} with the specified integration - * bounds number of integration segments. - * - * @param bounds the integration bounds - * @param segments number of integration segments - */ - - public FixedIntervalIntegrator(Segment bounds, NumericProperty segments) { - super(bounds); - setIntegrationSegments(segments); - } - - /** - * Retrieves the number of integration segments. - * - * @return the number of integration segments. - */ - - public NumericProperty getIntegrationSegments() { - return derive(INTEGRATION_SEGMENTS, integrationSegments); - } - - /** - * Sets the number of integration segments and re-evaluates the integration step - * size. - * - * @param integrationSegments a property of the {@code INTEGRATION_SEGMENTS} - * type - */ - - public void setIntegrationSegments(NumericProperty integrationSegments) { - requireType(integrationSegments, INTEGRATION_SEGMENTS); - this.integrationSegments = (int) integrationSegments.getValue(); - } - - /** - * Sets the bounds to the argument and re-evaluates the integration step size. - * - * @param bounds the integration bounds - */ - - @Override - public void setBounds(Segment bounds) { - super.setBounds(bounds); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == INTEGRATION_SEGMENTS) { - setIntegrationSegments(property); - firePropertyChanged(this, property); - } - } - - /** - * The listed property is {@code INTEGRATION_SEGMENTS}. - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(INTEGRATION_SEGMENTS))); - } - - /** - * Retrieves the step size equal to the integration range length divided by the - * number of integration segments. - * - * @return the integration step size. - */ - - public double stepSize() { - return getBounds().length() / (double) this.integrationSegments; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " ; " + getIntegrationSegments(); - } - - @Override - public String getPrefix() { - return "Fixed step integrator"; - } - -} \ No newline at end of file + private int integrationSegments; + + /** + * Creates a {@code FixedIntervalIntegrator} with the specified integration + * bounds and a default number of integration segments. + * + * @param bounds the integration bounds + */ + public FixedIntervalIntegrator(Segment bounds) { + this(bounds, def(INTEGRATION_SEGMENTS)); + } + + /** + * Creates a {@code FixedIntervalIntegrator} with the specified integration + * bounds number of integration segments. + * + * @param bounds the integration bounds + * @param segments number of integration segments + */ + public FixedIntervalIntegrator(Segment bounds, NumericProperty segments) { + super(bounds); + setIntegrationSegments(segments); + } + + /** + * Retrieves the number of integration segments. + * + * @return the number of integration segments. + */ + public NumericProperty getIntegrationSegments() { + return derive(INTEGRATION_SEGMENTS, integrationSegments); + } + + /** + * Sets the number of integration segments and re-evaluates the integration + * step size. + * + * @param integrationSegments a property of the {@code INTEGRATION_SEGMENTS} + * type + */ + public void setIntegrationSegments(NumericProperty integrationSegments) { + requireType(integrationSegments, INTEGRATION_SEGMENTS); + this.integrationSegments = (int) integrationSegments.getValue(); + } + + /** + * Sets the bounds to the argument and re-evaluates the integration step + * size. + * + * @param bounds the integration bounds + */ + @Override + public void setBounds(Segment bounds) { + super.setBounds(bounds); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == INTEGRATION_SEGMENTS) { + setIntegrationSegments(property); + firePropertyChanged(this, property); + } + } + + /** + * The listed property is {@code INTEGRATION_SEGMENTS}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(INTEGRATION_SEGMENTS); + return set; + } + + /** + * Retrieves the step size equal to the integration range length divided by + * the number of integration segments. + * + * @return the integration step size. + */ + public double stepSize() { + return getBounds().length() / (double) this.integrationSegments; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " ; " + getIntegrationSegments(); + } + + @Override + public String getPrefix() { + return "Fixed step integrator"; + } + +} diff --git a/src/main/java/pulse/math/FunctionWithInterpolation.java b/src/main/java/pulse/math/FunctionWithInterpolation.java index f720e7a1..9cbf3d45 100644 --- a/src/main/java/pulse/math/FunctionWithInterpolation.java +++ b/src/main/java/pulse/math/FunctionWithInterpolation.java @@ -8,115 +8,110 @@ * interpolation. * */ - public abstract class FunctionWithInterpolation { - private Segment tBounds; - private int lookupTableSize; - private UnivariateFunction interpolation; - - public final static int NUM_PARTITIONS = 8192; - - /** - * Constructs a {@code FunctionWithInterpolation} by tabulating the function - * values within the {@code parameterBounds} at discrete nodes, the number of - * which is given by the second argument. After having done this, creates a - * {@code SplineInterpolation}, which can be invoked in future to calculate - * intermediate function values without loss of accuracy. - * - * @param parameterBounds the calculation bounds. - * @param lookupTableSize a size of the table for discrete function - * calculations. - */ - - public FunctionWithInterpolation(Segment parameterBounds, int lookupTableSize) { - this.tBounds = parameterBounds; - this.lookupTableSize = lookupTableSize; - init(); - } - - /** - * Creates a {@code FunctionWithInterpolation} using the {@code parameterBounds} - * and a default number of points {@value NUM_PARTITIONS}. - * - * @param parameterBounds the calculation bounds. - */ - - public FunctionWithInterpolation(Segment parameterBounds) { - this(parameterBounds, NUM_PARTITIONS); - } - - /** - * Performs the calculation at {@code t}. - * - * @param t the value of the independent variable - * @return the function value at {@code t} - */ - - public abstract double evaluate(double t); - - /** - * Uses the stored interpolation function to calculate values at any {@code t} - * within the parameter bounds. Note: If {@code t} is not contained - * within the parameter bounds, the method will return 0.0. - * - * @param t the value of the independent variable - * @return will return the interpolated value or 0.0 - */ - - public double valueAt(double t) { - return tBounds.contains(t) ? interpolation.value(t) : 0.0; - } - - /** - * Retrieves the parameter bounds. - * - * @return the parameter bounds. - */ - - public Segment getParameterBounds() { - return tBounds; - } - - /** - * Sets the parameter bounds to {@code parameterBounds}. The interpolation will - * then be re-calculated. - * - * @param parameterBounds the new parameter bounds. - */ - - public void setParameterBounds(Segment parameterBounds) { - this.tBounds = parameterBounds; - init(); - } - - private void init() { - var lookupTable = generateTable(); - interpolate(lookupTable); - } - - private double[] generateTable() { - var lookupTable = new double[lookupTableSize]; - final double delta = tBounds.length() / (lookupTableSize - 1); - - double t = tBounds.getMinimum(); - for (int i = 0; i < lookupTableSize; i++) { - lookupTable[i] = evaluate(t); - t += delta; - } - - return lookupTable; - } - - private void interpolate(double[] lookupTable) { - var tArray = new double[lookupTableSize]; - final double delta = tBounds.length() / (lookupTableSize - 1); - - for (int i = 0; i < tArray.length; i++) - tArray[i] = tBounds.getMinimum() + i * delta; - - var splineInterpolation = new SplineInterpolator(); - interpolation = splineInterpolation.interpolate(tArray, lookupTable); - } - -} \ No newline at end of file + private Segment tBounds; + private int lookupTableSize; + private UnivariateFunction interpolation; + + public final static int NUM_PARTITIONS = 8192; + + /** + * Constructs a {@code FunctionWithInterpolation} by tabulating the function + * values within the {@code parameterBounds} at discrete nodes, the number + * of which is given by the second argument. After having done this, creates + * a {@code SplineInterpolation}, which can be invoked in future to + * calculate intermediate function values without loss of accuracy. + * + * @param parameterBounds the calculation bounds. + * @param lookupTableSize a size of the table for discrete function + * calculations. + */ + public FunctionWithInterpolation(Segment parameterBounds, int lookupTableSize) { + this.tBounds = parameterBounds; + this.lookupTableSize = lookupTableSize; + init(); + } + + /** + * Creates a {@code FunctionWithInterpolation} using the + * {@code parameterBounds} and a default number of points + * {@value NUM_PARTITIONS}. + * + * @param parameterBounds the calculation bounds. + */ + public FunctionWithInterpolation(Segment parameterBounds) { + this(parameterBounds, NUM_PARTITIONS); + } + + /** + * Performs the calculation at {@code t}. + * + * @param t the value of the independent variable + * @return the function value at {@code t} + */ + public abstract double evaluate(double t); + + /** + * Uses the stored interpolation function to calculate values at any + * {@code t} within the parameter bounds. Note: If {@code t} is not + * contained within the parameter bounds, the method will return 0.0. + * + * @param t the value of the independent variable + * @return will return the interpolated value or 0.0 + */ + public double valueAt(double t) { + return tBounds.contains(t) ? interpolation.value(t) : 0.0; + } + + /** + * Retrieves the parameter bounds. + * + * @return the parameter bounds. + */ + public Segment getParameterBounds() { + return tBounds; + } + + /** + * Sets the parameter bounds to {@code parameterBounds}. The interpolation + * will then be re-calculated. + * + * @param parameterBounds the new parameter bounds. + */ + public void setParameterBounds(Segment parameterBounds) { + this.tBounds = parameterBounds; + init(); + } + + private void init() { + var lookupTable = generateTable(); + interpolate(lookupTable); + } + + private double[] generateTable() { + var lookupTable = new double[lookupTableSize]; + final double delta = tBounds.length() / (lookupTableSize - 1); + + double t = tBounds.getMinimum(); + for (int i = 0; i < lookupTableSize; i++) { + lookupTable[i] = evaluate(t); + t += delta; + } + + return lookupTable; + } + + private void interpolate(double[] lookupTable) { + var tArray = new double[lookupTableSize]; + final double delta = tBounds.length() / (lookupTableSize - 1); + + for (int i = 0; i < tArray.length; i++) { + tArray[i] = tBounds.getMinimum() + i * delta; + } + + var splineInterpolation = new SplineInterpolator(); + interpolation = splineInterpolation.interpolate(tArray, lookupTable); + } + +} diff --git a/src/main/java/pulse/math/LegendrePoly.java b/src/main/java/pulse/math/LegendrePoly.java index dc611532..b8095072 100644 --- a/src/main/java/pulse/math/LegendrePoly.java +++ b/src/main/java/pulse/math/LegendrePoly.java @@ -16,157 +16,150 @@ * point, and the roots of the polynomial (with the help of a * {@code LaguerreSolver}. *

- * + * * @see org.apache.commons.math3.analysis.solvers.LaguerreSolver - * @see
Wiki page + * @see Wiki + * page */ - public class LegendrePoly { - private double[] c; - private int n; - - private LaguerreSolver solver; - - /** - * Creates a Legendre polynomial of the order {@code n}. The coefficients of the - * polynomial are immediately calculated. - * - * @param n the order of the polynomial. - */ - - public LegendrePoly(final int n) { - this.n = n; - c = new double[n + 1]; - var solverError = (double) def(LAGUERRE_SOLVER_ERROR).getValue(); - solver = new LaguerreSolver(solverError); - coefficients(); - } - - /** - * Fast calculation of binomial coefficient Cmk = - * m!/(k!(m-k)!) - * - * @param m integer. - * @param k integer, k ≤ m. - * @return binomial coefficient Cmk. - */ - - public static long binomial(int m, int k) { - int k1 = m - k; - k = k > k1 ? k1 : k; - - long c = 1; - - // Calculate value of Binomial Coefficient in bottom up manner - for (int i = 1; i < k + 1; i++, m--) { - c = c / i * m + c % i * m / i; // split c * n / i into (c / i * i + c % i) * n / i - } - - return c; - - } - - /** - * Calculates the generalised binomial coefficient. - * - * @param k integer. - * @param alpha a double value - * @return the generalised binomial coefficient Cαk. - */ - - public static double generalisedBinomial(final double alpha, final int k) { - - double c = 1; - - for (int i = 0; i < k; i++) { - c *= (alpha - i) / (k - i); - } - - return c; - - } - - /** - * This will generate the coefficients for the Legendre polynomial, arranged in - * order of significance (from x0 to xn). The coeffients - * will then be stored in a double array for further use. - */ - - public void coefficients() { - - long intFactor = fastPowInt(2, n); - - for (int i = 0; i < c.length; i++) { - c[i] = intFactor * binomial(n, i) * generalisedBinomial((n + i - 1) * 0.5, n); - } - - } - - /** - * Calculates the derivative of this Legendre polynomial. The coefficients are - * assumed to be known. - * - * @param x a real value. - * @return the derivative at {@code x}. - */ - - public double derivative(final double x) { - double d = 0; - - for (int i = 1; i < c.length; i++) { - d += i * c[i] * fastPowLoop(x, i - 1); - } - - return d; - - } - - /** - * @return the order of this polynomial. - */ - - public int getOrder() { - return n; - } - - /** - * Calculates the value of this Legendre polynomial at {@code x} - * - * @param x a real value. - * @return the value of the Legendre polynomial at {@code x}. - */ - - public double poly(final double x) { - double poly = 0; - - for (int i = 0; i < c.length; i++) { - poly += c[i] * fastPowLoop(x, i); - } - - return poly; - - } - - /** - * Uses a {@code LaguerreSolver} to calculate the roots of this polynomial. All - * coefficients are assumed to be known. - * - * @return the real roots of the Legendre polynomial. - */ - - public double[] roots() { - var complexRoots = solver.solveAllComplex(c, 1.0); - var roots = new double[n]; - - // the last roots is always zero, so we have n non-zero roots in total - // in case of even n, the first n/2 roots are positive and the rest are negative - for (int i = 0; i < n; i++) { - roots[i] = complexRoots[i].getReal(); - } - - return roots; - - } - -} \ No newline at end of file + private double[] c; + private int n; + + private LaguerreSolver solver; + + /** + * Creates a Legendre polynomial of the order {@code n}. The coefficients of + * the polynomial are immediately calculated. + * + * @param n the order of the polynomial. + */ + public LegendrePoly(final int n) { + this.n = n; + c = new double[n + 1]; + var solverError = (double) def(LAGUERRE_SOLVER_ERROR).getValue(); + solver = new LaguerreSolver(solverError); + coefficients(); + } + + /** + * Fast calculation of binomial coefficient Cmk = + * m!/(k!(m-k)!) + * + * @param m integer. + * @param k integer, k ≤ m. + * @return binomial coefficient Cmk. + */ + public static long binomial(int m, int k) { + int k1 = m - k; + k = k > k1 ? k1 : k; + + long c = 1; + + // Calculate value of Binomial Coefficient in bottom up manner + for (int i = 1; i < k + 1; i++, m--) { + c = c / i * m + c % i * m / i; // split c * n / i into (c / i * i + c % i) * n / i + } + + return c; + + } + + /** + * Calculates the generalised binomial coefficient. + * + * @param k integer. + * @param alpha a double value + * @return the generalised binomial coefficient + * Cαk. + */ + public static double generalisedBinomial(final double alpha, final int k) { + + double c = 1; + + for (int i = 0; i < k; i++) { + c *= (alpha - i) / (k - i); + } + + return c; + + } + + /** + * This will generate the coefficients for the Legendre polynomial, arranged + * in order of significance (from x0 to xn). The + * coeffients will then be stored in a double array for further use. + */ + public void coefficients() { + + long intFactor = fastPowInt(2, n); + + for (int i = 0; i < c.length; i++) { + c[i] = intFactor * binomial(n, i) * generalisedBinomial((n + i - 1) * 0.5, n); + } + + } + + /** + * Calculates the derivative of this Legendre polynomial. The coefficients + * are assumed to be known. + * + * @param x a real value. + * @return the derivative at {@code x}. + */ + public double derivative(final double x) { + double d = 0; + + for (int i = 1; i < c.length; i++) { + d += i * c[i] * fastPowLoop(x, i - 1); + } + + return d; + + } + + /** + * @return the order of this polynomial. + */ + public int getOrder() { + return n; + } + + /** + * Calculates the value of this Legendre polynomial at {@code x} + * + * @param x a real value. + * @return the value of the Legendre polynomial at {@code x}. + */ + public double poly(final double x) { + double poly = 0; + + for (int i = 0; i < c.length; i++) { + poly += c[i] * fastPowLoop(x, i); + } + + return poly; + + } + + /** + * Uses a {@code LaguerreSolver} to calculate the roots of this polynomial. + * All coefficients are assumed to be known. + * + * @return the real roots of the Legendre polynomial. + */ + public double[] roots() { + var complexRoots = solver.solveAllComplex(c, 1.0); + var roots = new double[n]; + + // the last roots is always zero, so we have n non-zero roots in total + // in case of even n, the first n/2 roots are positive and the rest are negative + for (int i = 0; i < n; i++) { + roots[i] = complexRoots[i].getReal(); + } + + return roots; + + } + +} diff --git a/src/main/java/pulse/math/MathUtils.java b/src/main/java/pulse/math/MathUtils.java index 65eec0cc..248f39d3 100644 --- a/src/main/java/pulse/math/MathUtils.java +++ b/src/main/java/pulse/math/MathUtils.java @@ -10,170 +10,161 @@ * https://martin.ankerl.com * */ - public class MathUtils { - public final static double EQUALS_TOLERANCE = 1E-5; - - private MathUtils() { - // intentionally blank - } - - /** - * Checks if two numbers are approximately equal by comparing the modulus of - * their difference to {@value EQUALS_TOLERANCE}. - * - * @param a a number - * @param b another number - * @return {@code true} if numbers are approximately equal, {@code false} - * otherwise - */ - - public static boolean approximatelyEquals(final double a, final double b) { - return Math.abs(a - b) < EQUALS_TOLERANCE; - } - - /** - * A method for the approximate calculation of the hyperbolic tangent. - * - * @param a the argument of {@code tanh(a)}. - * @return the approximate {@code tanh(a)} value. - * @see MathUtils.fastExp(double) - */ - - public static double fastTanh(final double a) { - final double e2x = fastExp(2.0 * a); - return (e2x - 1.0) / (e2x + 1.0); - } - - /** - * Calculate the hyperbolic arctangent (exact). - * - * @param a the argument of {@code atanh(a)} - * @return the exact value of the {@code atanh(a)}. - */ - - public static double atanh(final double a) { - return 0.5 * log((1 + a) / (1 - a)); - } - - /** - * Rapid calculation of {@code b}-th power of {@code a}, which is simply a - * repeated multiplication, in case of positive {@code b}, or a repeated - * division. - * - * @param a the base . - * @param b the exponent. - * @return the exponentiation result. - */ - - public static double fastPowLoop(final double a, final int b) { - double re = 1; - // if positive (i < b is never satisfied for negative b's) - for (int i = 0; i < b; i++) { - re *= a; - } - // if negative - for (int i = 0; i < -b; i++) { - re /= a; - } - return re; - } - - /** - * Rapid calculation of (-1)n using a ternary conditional operator. - * - * @param n a positive integer number - * @return the result of exponentiation. - */ - - public static int fastPowSgn(int n) { - return n % 2 == 0 ? 1 : -1; - } - - /** - * Rapid exponentiation where the base and the exponent are integer value. Uses - * bitwise shiftiing. - * - * @param x the base - * @param y the exponent - * @return result of the exponentiation - */ - - public static long fastPowInt(long x, int y) { - long result = 1; - while (y > 0) { - if ((y & 1) == 0) { - x *= x; - y >>>= 1; - } else { - result *= x; - y--; - } - } - return result; - } - - /** - * Approximate calculation of {@code exp(val)}. - * - * @param val the argument of the exponent. - * @return the result. - */ - - public static double fastExp(double val) { - final long tmp = (long) (1512775 * val + 1072632447); - return longBitsToDouble(tmp << 32); - } - - /** - * A highly-approximate calculation of {@code ln(val)}. - * - * @param val the argument of the natural logarithm. - * @return the result. - */ - - public static double fastLog(double val) { - final double x = (doubleToLongBits(val) >> 32); - return (x - 1072632447) / 1512775; - } - - /** - * Approximate calculation of ab. - * - * @param a the base (real) - * @param b the exponent (real) - * @return the result of calculation - */ - - public static double fastPowGeneral(final double a, final double b) { - final long tmp = doubleToLongBits(a); - final long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L; - return longBitsToDouble(tmp2); - } - - /** - * A fast bitwise calculation of the absolute value. Arguably faster than - * {@code Math.abs}. - * - * @param a the argument - * @return the calculation result - */ - - public static double fastAbs(final double a) { - return longBitsToDouble((doubleToLongBits(a) << 1) >>> 1); - } - - /** - * Performs linear extrapolation according to the rule y(xStar) = y1 + (xStar - x1)/(x2 - x1) * (y2 - y1) - * @param p1 point 1 (x1, y1) - * @param p2 point 2 (x2, y2) - * @param xStar interpolation x coordinate - * @return - */ - - public static double linearExtrapolation(final double[] p1, final double[] p2, final double xStar) { - return p1[1] + (xStar - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]); - } - -} \ No newline at end of file + public final static double EQUALS_TOLERANCE = 1E-5; + + private MathUtils() { + // intentionally blank + } + + /** + * Checks if two numbers are approximately equal by comparing the modulus of + * their difference to {@value EQUALS_TOLERANCE}. + * + * @param a a number + * @param b another number + * @return {@code true} if numbers are approximately equal, {@code false} + * otherwise + */ + public static boolean approximatelyEquals(final double a, final double b) { + return Math.abs(a - b) < EQUALS_TOLERANCE; + } + + /** + * A method for the approximate calculation of the hyperbolic tangent. + * + * @param a the argument of {@code tanh(a)}. + * @return the approximate {@code tanh(a)} value. + * @see MathUtils.fastExp(double) + */ + public static double fastTanh(final double a) { + final double e2x = fastExp(2.0 * a); + return (e2x - 1.0) / (e2x + 1.0); + } + + /** + * Calculate the hyperbolic arctangent (exact). + * + * @param a the argument of {@code atanh(a)} + * @return the exact value of the {@code atanh(a)}. + */ + public static double atanh(final double a) { + return 0.5 * log((1 + a) / (1 - a)); + } + + /** + * Rapid calculation of {@code b}-th power of {@code a}, which is simply a + * repeated multiplication, in case of positive {@code b}, or a repeated + * division. + * + * @param a the base . + * @param b the exponent. + * @return the exponentiation result. + */ + public static double fastPowLoop(final double a, final int b) { + double re = 1; + // if positive (i < b is never satisfied for negative b's) + for (int i = 0; i < b; i++) { + re *= a; + } + // if negative + for (int i = 0; i < -b; i++) { + re /= a; + } + return re; + } + + /** + * Rapid calculation of (-1)n using a ternary conditional + * operator. + * + * @param n a positive integer number + * @return the result of exponentiation. + */ + public static int fastPowSgn(int n) { + return n % 2 == 0 ? 1 : -1; + } + + /** + * Rapid exponentiation where the base and the exponent are integer value. + * Uses bitwise shiftiing. + * + * @param x the base + * @param y the exponent + * @return result of the exponentiation + */ + public static long fastPowInt(long x, int y) { + long result = 1; + while (y > 0) { + if ((y & 1) == 0) { + x *= x; + y >>>= 1; + } else { + result *= x; + y--; + } + } + return result; + } + + /** + * Approximate calculation of {@code exp(val)}. + * + * @param val the argument of the exponent. + * @return the result. + */ + public static double fastExp(double val) { + final long tmp = (long) (1512775 * val + 1072632447); + return longBitsToDouble(tmp << 32); + } + + /** + * A highly-approximate calculation of {@code ln(val)}. + * + * @param val the argument of the natural logarithm. + * @return the result. + */ + public static double fastLog(double val) { + final double x = (doubleToLongBits(val) >> 32); + return (x - 1072632447) / 1512775; + } + + /** + * Approximate calculation of ab. + * + * @param a the base (real) + * @param b the exponent (real) + * @return the result of calculation + */ + public static double fastPowGeneral(final double a, final double b) { + final long tmp = doubleToLongBits(a); + final long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L; + return longBitsToDouble(tmp2); + } + + /** + * A fast bitwise calculation of the absolute value. Arguably faster than + * {@code Math.abs}. + * + * @param a the argument + * @return the calculation result + */ + public static double fastAbs(final double a) { + return longBitsToDouble((doubleToLongBits(a) << 1) >>> 1); + } + + /** + * Performs linear extrapolation according to the rule y(xStar) = y1 + + * (xStar - x1)/(x2 - x1) * (y2 - y1) + * + * @param p1 point 1 (x1, y1) + * @param p2 point 2 (x2, y2) + * @param xStar interpolation x coordinate + * @return + */ + public static double linearExtrapolation(final double[] p1, final double[] p2, final double xStar) { + return p1[1] + (xStar - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]); + } + +} diff --git a/src/main/java/pulse/math/MidpointIntegrator.java b/src/main/java/pulse/math/MidpointIntegrator.java index 1a4f0579..cc66763b 100644 --- a/src/main/java/pulse/math/MidpointIntegrator.java +++ b/src/main/java/pulse/math/MidpointIntegrator.java @@ -5,39 +5,37 @@ /** * Implements the midpoint integration scheme for the evaluation of definite * integrals. - * + * * @see Wiki page * */ - public abstract class MidpointIntegrator extends FixedIntervalIntegrator { - public MidpointIntegrator(Segment bounds, NumericProperty segments) { - super(bounds, segments); - } - - public MidpointIntegrator(Segment bounds) { - super(bounds); - } - - /** - * Performs the integration according to the midpoint scheme. This scheme should - * be used when the function is not well-defined at either of the integration - * bounds. - */ - - @Override - public double integrate() { - final double a = getBounds().getMinimum(); - - final int points = (int) getIntegrationSegments().getValue(); - - double sum = 0; - double h = stepSize(); - for (int i = 0; i < points; i++) { - sum += integrand(a + (i + 0.5) * h) * h; - } - return sum; - } - -} \ No newline at end of file + public MidpointIntegrator(Segment bounds, NumericProperty segments) { + super(bounds, segments); + } + + public MidpointIntegrator(Segment bounds) { + super(bounds); + } + + /** + * Performs the integration according to the midpoint scheme. This scheme + * should be used when the function is not well-defined at either of the + * integration bounds. + */ + @Override + public double integrate() { + final double a = getBounds().getMinimum(); + + final int points = (int) getIntegrationSegments().getValue(); + + double sum = 0; + double h = stepSize(); + for (int i = 0; i < points; i++) { + sum += integrand(a + (i + 0.5) * h) * h; + } + return sum; + } + +} diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index e8e42667..22242cc7 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,232 +1,247 @@ package pulse.math; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import pulse.math.linear.Vector; import pulse.math.transforms.Transformable; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperties; +import static pulse.properties.NumericProperties.def; +import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; /** * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific * components of the vector. Used when constructing the optimisation vector. */ - public class ParameterVector extends Vector { - private NumericPropertyKeyword[] indices; - private Transformable[] transforms; - private Segment[] bounds; - - /** - * Constructs an {@code IndexedVector} with the specified list of keywords. - * - * @param indices a list of keywords - */ - - public ParameterVector(List indices) { - this(indices.size()); - assign(indices); - } - - /** - * Constructs an {@code IndexedVector} based on {@code v} and a list of keyword - * {@code indices} - * - * @param v the vector to be copied - * @param prototype the prototype of the parameter vector - */ - - public ParameterVector(ParameterVector proto, Vector v) { - super(v); - this.indices = new NumericPropertyKeyword[proto.indices.length]; - System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); - this.bounds = new Segment[proto.bounds.length]; - System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); - this.transforms = new Transformable[proto.transforms.length]; - System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); - - } - - /** - * Copy constructor - * @param v another vector - */ - - public ParameterVector(ParameterVector v) { - this( v.dimension() ); - final int n = dimension(); - for(int i = 0; i < n; i++) - this.set(i, v.get(i)); - System.arraycopy(v.indices, 0, indices, 0, n); - System.arraycopy(v.transforms, 0, transforms, 0, n); - System.arraycopy(v.bounds, 0, bounds, 0, n); - } - - /** - * Creates an empty ParameterVector with a dimension of {@code n} - * @param n dimension - */ - - private ParameterVector(final int n) { - super(n); - indices = new NumericPropertyKeyword[n]; - transforms = new Transformable[n]; - bounds = new Segment[n]; - } - - /** - * Applies the corresponding transformation (defined by the respective {@code Transformable}) -- if present, - * and sets the result of this transformation to the ith component of this {@code ParameterVector}. - */ - - @Override - public void set(final int i, final double x) { - set(i, x, false); - } - - /** - * Sets the i-component of this vector to {@code x} or its corresponding transform, - * if the latter is defined and {@code ignoreTransform} is {@code false}. - * @param i index of the value and its transform - * @param x the non-transformed value, which needs to be assigned to the i-th component - * @param ignoreTransform if {@code} false, will ignore exiting transform. - */ - - public void set(final int i, final double x, boolean ignoreTransform) { - final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); - super.set(i, t); - } - - /** - * Retrieves the keyword associated with the {@code dataIndex} - * - * @param dataIndex an index pointing to a component of this vector - * @return a keyword describing this component - */ - - public NumericPropertyKeyword getIndex(final int dataIndex) { - return indices[dataIndex]; - } - - /** - * Gets the data index that corresponds to the keyword {@code index} - * - * @param index a keyword-index of the component - * @return a numeric index associated with the original {@code Vector} - */ - - private int indexOf(NumericPropertyKeyword index) { - return getIndices().indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - - public double getParameterValue(NumericPropertyKeyword index) { - return super.get(indexOf(index)); - } - - /** - * Performs an inverse transform corresponding to the index {@code i} of this vector. - * @param i the index of the transform - * @return the inverse transform of {@code get(i) } if the transform is defined, {@code get(i)} otherwise. - */ - - public double inverseTransform(final int i) { - return transforms[i] != null ? transforms[i].inverse( get(i) ) : get(i); - } - - /** - * Gets the transformable of the i-th component - * @param i index of the component - * @return the corresponding {@code Transforamble} - */ - - public Transformable getTransform(final int i) { - return transforms[i]; - } - - public void setTransform(final int i, Transformable transformable) { - transforms[i] = transformable; - } - - public Segment getParameterBounds(final int i) { - return bounds[i]; - } - - /** - * If transform of {@code i} is not null, applies the transformation to the component bounds - * @param i the index of the component - * @return the transformed bounds - */ - - public Segment getTransformedBounds(final int i) { - return transforms[i] != null ? - new Segment( transforms[i].transform( bounds[i].getMinimum() ), - transforms[i].transform( bounds[i].getMaximum() ) ) : - getParameterBounds(i); - } - - /** - * Sets the bounds of i-th component of this vector. - * @param i the index of the component - * @param segment new parameter bounds - */ - - public void setParameterBounds(int i, Segment segment) { - bounds[i] = segment; - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - - public List getIndices() { - return Arrays.asList(indices); - } - - /** - * This will assign a new list of indices to this vector - * @param indices a list of indices - */ - - private void assign(List indices) { - this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); - bounds = new Segment[this.indices.length]; - transforms = new Transformable[this.indices.length]; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("Indices: "); - for(var key : indices) { - sb.append(key + " ; "); - } - sb.append(System.lineSeparator()); - sb.append(" Values: " + super.toString()); - return sb.toString(); - } - - public boolean validate() { - for(int i = 0; i < this.dimension(); i++) { - if( !NumericProperties.derive(this.getIndex(i), inverseTransform(i)).validate() ) { - return false; - } - } - return true; - } - - public Segment[] getBounds() { - return bounds; - } - -} \ No newline at end of file + private NumericPropertyKeyword[] indices; + private Transformable[] transforms; + private Segment[] bounds; + + /** + * Constructs an {@code IndexedVector} with the specified list of keywords. + * + * @param indices a list of keywords + */ + public ParameterVector(List indices) { + this(indices.size()); + assign(indices); + } + + /** + * Constructs an {@code IndexedVector} based on {@code v} and a list of + * keyword {@code indices} + * + * @param v the vector to be copied + * @param prototype the prototype of the parameter vector + */ + public ParameterVector(ParameterVector proto, Vector v) { + super(v); + this.indices = new NumericPropertyKeyword[proto.indices.length]; + System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); + this.bounds = new Segment[proto.bounds.length]; + System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); + this.transforms = new Transformable[proto.transforms.length]; + System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); + + } + + /** + * Copy constructor + * + * @param v another vector + */ + public ParameterVector(ParameterVector v) { + this(v.dimension()); + final int n = dimension(); + for (int i = 0; i < n; i++) { + this.set(i, v.get(i)); + } + System.arraycopy(v.indices, 0, indices, 0, n); + System.arraycopy(v.transforms, 0, transforms, 0, n); + System.arraycopy(v.bounds, 0, bounds, 0, n); + } + + /** + * Creates an empty ParameterVector with a dimension of {@code n} + * + * @param n dimension + */ + private ParameterVector(final int n) { + super(n); + indices = new NumericPropertyKeyword[n]; + transforms = new Transformable[n]; + bounds = new Segment[n]; + } + + /** + * Applies the corresponding transformation (defined by the respective + * {@code Transformable}) -- if present, and sets the result of this + * transformation to the ith component of this + * {@code ParameterVector}. + */ + @Override + public void set(final int i, final double x) { + set(i, x, false); + } + + /** + * Sets the i-component of this vector to {@code x} or + * its corresponding transform, if the latter is defined and + * {@code ignoreTransform} is {@code false}. + * + * @param i index of the value and its transform + * @param x the non-transformed value, which needs to be assigned to the + * i-th component + * @param ignoreTransform if {@code} false, will ignore exiting transform. + */ + public void set(final int i, final double x, boolean ignoreTransform) { + final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); + super.set(i, t); + } + + /** + * Retrieves the keyword associated with the {@code dataIndex} + * + * @param dataIndex an index pointing to a component of this vector + * @return a keyword describing this component + */ + public NumericPropertyKeyword getIndex(final int dataIndex) { + return indices[dataIndex]; + } + + /** + * Gets the data index that corresponds to the keyword {@code index} + * + * @param index a keyword-index of the component + * @return a numeric index associated with the original {@code Vector} + */ + private int indexOf(NumericPropertyKeyword index) { + return getIndices().indexOf(index); + } + + /** + * Gets the component at this {@code index} + * + * @param index a keyword-index of a component + * @return the respective component + */ + public double getParameterValue(NumericPropertyKeyword index) { + return super.get(indexOf(index)); + } + + /** + * Performs an inverse transform corresponding to the index {@code i} of + * this vector. + * + * @param i the index of the transform + * @return the inverse transform of {@code get(i) } if the transform is + * defined, {@code get(i)} otherwise. + */ + public double inverseTransform(final int i) { + return transforms[i] != null ? transforms[i].inverse(get(i)) : get(i); + } + + /** + * Gets the transformable of the i-th component + * + * @param i index of the component + * @return the corresponding {@code Transforamble} + */ + public Transformable getTransform(final int i) { + return transforms[i]; + } + + public void setTransform(final int i, Transformable transformable) { + transforms[i] = transformable; + } + + public Segment getParameterBounds(final int i) { + return bounds[i]; + } + + /** + * If transform of {@code i} is not null, applies the transformation to the + * component bounds + * + * @param i the index of the component + * @return the transformed bounds + */ + public Segment getTransformedBounds(final int i) { + return transforms[i] != null + ? new Segment(transforms[i].transform(bounds[i].getMinimum()), + transforms[i].transform(bounds[i].getMaximum())) + : getParameterBounds(i); + } + + /** + * Sets the bounds of i-th component of this vector. + * + * @param i the index of the component + * @param segment new parameter bounds + */ + public void setParameterBounds(int i, Segment segment) { + bounds[i] = segment; + } + + /** + * Gets the full list of indices recognised by this {@code IndexedVector}. + * + * @return the full list of {@code NumericPropertyKeyword} indices. + */ + public List getIndices() { + return Arrays.asList(indices); + } + + /** + * This will assign a new list of indices to this vector + * + * @param indices a list of indices + */ + private void assign(List indices) { + this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); + bounds = new Segment[this.indices.length]; + transforms = new Transformable[this.indices.length]; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Indices: "); + for (var key : indices) { + sb.append(key).append(" ; "); + } + sb.append(System.lineSeparator()); + sb.append(" Values: ").append(super.toString()); + return sb.toString(); + } + + /** + * Finds any elements of this vector which do not pass sanity checks. + * @return a list of malformed numeric properties + * @see pulse.properties.NumericProperties.isValueSensible() + */ + + public List findMalformedElements() { + var list = new ArrayList(); + + for (int i = 0; i < dimension(); i++) { + var property = def(getIndex(i)); + boolean sensible = NumericProperties.isValueSensible(property, get(i)); + if (!sensible) { + list.add(property); + } + } + + return list; + } + + public Segment[] getBounds() { + return bounds; + } + +} diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index 8a89042b..5d0e1b49 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -7,138 +7,127 @@ * that {@code a < b}. * */ - public class Segment { - private double a; - private double b; - - /** - * Creates a {@code Segment} bounded by {@code a} and {@code b}. - * - * @param a any value - * @param b either {@code b > a} or {@code b < a} - */ - - public Segment(double a, double b) { - this.a = a < b ? a : b; - this.b = b > a ? b : a; - } - - /** - * Copies {@code segment} - * - * @param segment a {@code Segment} - */ - - public Segment(Segment segment) { - this.a = segment.a; - this.b = segment.b; - } - - /** - * Gets the {@code a} value for this {@code Segment} - * - * @return the lower end of this {@code Segment} - */ - - public double getMinimum() { - return a; - } - - /** - * Gets the {@code b} value for this {@code Segment} - * - * @return the upper end of this {@code Segment} - */ - - public double getMaximum() { - return b; - } - - /** - * Calculates the length {@code (b - a)} - * - * @return the length value - */ - - public double length() { - return (b - a); - } - - /** - * Calculates the squared length - * - * @return the squared length value - * @see length() - */ - - public double lengthSq() { - return Math.pow(b - a, 2); - } - - /** - * Sets the minimum value to {@code a}. Note it does not prevent against: - * {@code a >= max}. - * - * @param a a value, which should satisfy {@code a < max} - */ - - public void setMinimum(double a) { - this.a = a; - } - - /** - * Sets the maximum value to {@code b}. Note it does not prevent against: - * {@code b <= min}. - * - * @param b a value, which should satisfy {@code b > min} - */ - - public void setMaximum(double b) { - this.b = b; - } - - /** - * Calculates the middle point of this {@code Segment}. - * - * @return the mean - */ - - public double mean() { - return (a + b) * 0.5; - } - - /** - * Calculates a random value confined in the interval between the two ends of - * this {@code Segment}. - * - * @return a confined random value. - */ - - public double randomValue() { - return (new Random()).nextDouble() * length() + getMinimum(); - } - - /** - * Checks whether {@code x} is contained inside this {@code Segment}. - * - * @param x a value. - * @return {@code true} if min ≤ x ≤ max. - */ - - public boolean contains(double x) { - return x >= a ? (x <= b ? true : false) : false; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("["); - sb.append(a); - sb.append(" ; "); - sb.append(b); - sb.append("]"); - return sb.toString(); - } - -} \ No newline at end of file + + private double a; + private double b; + + /** + * Creates a {@code Segment} bounded by {@code a} and {@code b}. + * + * @param a any value + * @param b either {@code b > a} or {@code b < a} + */ + public Segment(double a, double b) { + this.a = a < b ? a : b; + this.b = b > a ? b : a; + } + + /** + * Copies {@code segment} + * + * @param segment a {@code Segment} + */ + public Segment(Segment segment) { + this.a = segment.a; + this.b = segment.b; + } + + /** + * Gets the {@code a} value for this {@code Segment} + * + * @return the lower end of this {@code Segment} + */ + public double getMinimum() { + return a; + } + + /** + * Gets the {@code b} value for this {@code Segment} + * + * @return the upper end of this {@code Segment} + */ + public double getMaximum() { + return b; + } + + /** + * Calculates the length {@code (b - a)} + * + * @return the length value + */ + public double length() { + return (b - a); + } + + /** + * Calculates the squared length + * + * @return the squared length value + * @see length() + */ + public double lengthSq() { + return Math.pow(b - a, 2); + } + + /** + * Sets the minimum value to {@code a}. Note it does not prevent against: + * {@code a >= max}. + * + * @param a a value, which should satisfy {@code a < max} + */ + public void setMinimum(double a) { + this.a = a; + } + + /** + * Sets the maximum value to {@code b}. Note it does not prevent against: + * {@code b <= min}. + * + * @param b a value, which should satisfy {@code b > min} + */ + public void setMaximum(double b) { + this.b = b; + } + + /** + * Calculates the middle point of this {@code Segment}. + * + * @return the mean + */ + public double mean() { + return (a + b) * 0.5; + } + + /** + * Calculates a random value confined in the interval between the two ends + * of this {@code Segment}. + * + * @return a confined random value. + */ + public double randomValue() { + return (new Random()).nextDouble() * length() + getMinimum(); + } + + /** + * Checks whether {@code x} is contained inside this {@code Segment}. + * + * @param x a value. + * @return {@code true} if min ≤ x ≤ max. + */ + public boolean contains(double x) { + return x >= a ? (x <= b ? true : false) : false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(a); + sb.append(" ; "); + sb.append(b); + sb.append("]"); + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/math/SimpsonIntegrator.java b/src/main/java/pulse/math/SimpsonIntegrator.java index 04db7241..55ddda7b 100644 --- a/src/main/java/pulse/math/SimpsonIntegrator.java +++ b/src/main/java/pulse/math/SimpsonIntegrator.java @@ -5,53 +5,51 @@ /** * Implements the Simpson's integration rule for the evaluation of definite * integrals. - * + * * @see Wiki page * */ - public abstract class SimpsonIntegrator extends FixedIntervalIntegrator { - public SimpsonIntegrator(Segment bounds) { - super(bounds); - } - - public SimpsonIntegrator(Segment bounds, NumericProperty segments) { - super(bounds, segments); - } + public SimpsonIntegrator(Segment bounds) { + super(bounds); + } - /** - * Performs the integration with the Simpson's rule. Based on: - * https://introcs.cs.princeton.edu/java/93integration/SimpsonsRule.java.html - */ + public SimpsonIntegrator(Segment bounds, NumericProperty segments) { + super(bounds, segments); + } - @Override - public double integrate() { - double rmin = getBounds().getMinimum(); - double rmax = getBounds().getMaximum(); + /** + * Performs the integration with the Simpson's rule. Based on: + * https://introcs.cs.princeton.edu/java/93integration/SimpsonsRule.java.html + */ + @Override + public double integrate() { + double rmin = getBounds().getMinimum(); + double rmax = getBounds().getMaximum(); - // 1/3 terms - double sum = integrand(rmin) + integrand(rmax); + // 1/3 terms + double sum = integrand(rmin) + integrand(rmax); - double x = 0; - double y = 0; + double x = 0; + double y = 0; - int integrationSegments = (int) getIntegrationSegments().getValue(); - double h = stepSize(); + int integrationSegments = (int) getIntegrationSegments().getValue(); + double h = stepSize(); - // 4/3 terms - for (int i = 1; i < integrationSegments; i += 2) { - x += integrand(rmin + h * i); - } + // 4/3 terms + for (int i = 1; i < integrationSegments; i += 2) { + x += integrand(rmin + h * i); + } - // 2/3 terms - for (int i = 2; i < integrationSegments; i += 2) { - y += integrand(rmin + h * i); - } + // 2/3 terms + for (int i = 2; i < integrationSegments; i += 2) { + y += integrand(rmin + h * i); + } - sum += x * 4.0 + y * 2.0; + sum += x * 4.0 + y * 2.0; - return sum * h / 3.0; - } + return sum * h / 3.0; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/linear/ArithmeticOperation.java b/src/main/java/pulse/math/linear/ArithmeticOperation.java index bc7112f7..10b60c15 100644 --- a/src/main/java/pulse/math/linear/ArithmeticOperation.java +++ b/src/main/java/pulse/math/linear/ArithmeticOperation.java @@ -4,16 +4,15 @@ * A basic wrapper interface for binary arithmetic operations. * */ - interface ArithmeticOperation { - /** - * Calculates the result of the binary operation. - * @param x a number - * @param y another number - * @return the result of arithmetic operation - */ - - double evaluate(double x, double y); - -} \ No newline at end of file + /** + * Calculates the result of the binary operation. + * + * @param x a number + * @param y another number + * @return the result of arithmetic operation + */ + double evaluate(double x, double y); + +} diff --git a/src/main/java/pulse/math/linear/ArithmeticOperations.java b/src/main/java/pulse/math/linear/ArithmeticOperations.java index a5d4d0d1..bc11a0aa 100644 --- a/src/main/java/pulse/math/linear/ArithmeticOperations.java +++ b/src/main/java/pulse/math/linear/ArithmeticOperations.java @@ -6,41 +6,35 @@ * Manages available arithmetic operations for use within the package. * */ - class ArithmeticOperations { - /** - * Sum of two numbers. - */ - - public final static ArithmeticOperation SUM = (x, y) -> x + y; - - /** - * Difference of two numbers. - */ - - public final static ArithmeticOperation DIFFERENCE = (x, y) -> x - y; - - /** - * Product of two numbers. - */ - - public final static ArithmeticOperation PRODUCT = (x, y) -> x * y; - - /** - * The result of division of one number by another number. - */ - - public final static ArithmeticOperation DIVISION = (x, y) -> x - y; - - /** - * The squared difference of two numbers. - */ - - public final static ArithmeticOperation DIFF_SQUARED = (x, y) -> fastPowLoop(x*x - y*y, 2); - - private ArithmeticOperations() { - //intentionally blank - } - -} \ No newline at end of file + /** + * Sum of two numbers. + */ + public final static ArithmeticOperation SUM = (x, y) -> x + y; + + /** + * Difference of two numbers. + */ + public final static ArithmeticOperation DIFFERENCE = (x, y) -> x - y; + + /** + * Product of two numbers. + */ + public final static ArithmeticOperation PRODUCT = (x, y) -> x * y; + + /** + * The result of division of one number by another number. + */ + public final static ArithmeticOperation DIVISION = (x, y) -> x - y; + + /** + * The squared difference of two numbers. + */ + public final static ArithmeticOperation DIFF_SQUARED = (x, y) -> fastPowLoop(x * x - y * y, 2); + + private ArithmeticOperations() { + //intentionally blank + } + +} diff --git a/src/main/java/pulse/math/linear/Matrices.java b/src/main/java/pulse/math/linear/Matrices.java index c04b8d6f..30b69c53 100644 --- a/src/main/java/pulse/math/linear/Matrices.java +++ b/src/main/java/pulse/math/linear/Matrices.java @@ -6,63 +6,62 @@ * are package-private. * */ - public class Matrices { - private Matrices() { - // intentionally blank - } - - /** - * Creates a square matrix out of {@code data}. Depending on the data - * dimensions, this will either create a general-form {@code SquareMatrix} or - * one of the subclasses: {@code Matrix2}, {@code Matrix3} or {@code Matrix4}. - * - * @param data the input data - * @return a {@code} SquareMatrix instance or one of its subclasses - */ - - public static RectangularMatrix createMatrix(double[][] data) { - return data.length == data[0].length ? createSquareMatrix(data) : new RectangularMatrix(data); - } + private Matrices() { + // intentionally blank + } - public static SquareMatrix createSquareMatrix(double[][] data) { - int m = data.length; + /** + * Creates a square matrix out of {@code data}. Depending on the data + * dimensions, this will either create a general-form {@code SquareMatrix} + * or one of the subclasses: {@code Matrix2}, {@code Matrix3} or + * {@code Matrix4}. + * + * @param data the input data + * @return a {@code} SquareMatrix instance or one of its subclasses + */ + public static RectangularMatrix createMatrix(double[][] data) { + return data.length == data[0].length ? createSquareMatrix(data) : new RectangularMatrix(data); + } - SquareMatrix result; + public static SquareMatrix createSquareMatrix(double[][] data) { + int m = data.length; - switch (m) { - case 2: - result = new Matrix2(data); - break; - case 3: - result = new Matrix3(data); - break; - case 4: - result = new Matrix4(data); - break; - default: - result = new SquareMatrix(data); - } + SquareMatrix result; - return result; + switch (m) { + case 2: + result = new Matrix2(data); + break; + case 3: + result = new Matrix3(data); + break; + case 4: + result = new Matrix4(data); + break; + default: + result = new SquareMatrix(data); + } - } + return result; - /** - * Creates an identity matrix with its dimension equal to the argument - * - * @param dimension the dimension - * @return an identity matrix of the given dimension - */ + } - public static SquareMatrix createIdentityMatrix(int dimension) { - var data = new double[dimension][dimension]; + /** + * Creates an identity matrix with its dimension equal to the argument + * + * @param dimension the dimension + * @return an identity matrix of the given dimension + */ + public static SquareMatrix createIdentityMatrix(int dimension) { + var data = new double[dimension][dimension]; - for (int i = 0; i < dimension; i++) - data[i][i] = 1.0; + for (int i = 0; i < dimension; i++) { + data[i][i] = 1.0; + } - return createSquareMatrix(data); - } + return createSquareMatrix(data); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/linear/Matrix2.java b/src/main/java/pulse/math/linear/Matrix2.java index 4794f842..aa579847 100644 --- a/src/main/java/pulse/math/linear/Matrix2.java +++ b/src/main/java/pulse/math/linear/Matrix2.java @@ -4,40 +4,39 @@ * A square 2-by-2 matrix. * */ - class Matrix2 extends SquareMatrix { - protected Matrix2(double[][] args) { - super(args); - } - - /** - * Fast (in terms of required floating point operations) calculation of the matrix inverse. - */ - - @Override - public SquareMatrix inverse() { - double[][] mx = new double[2][2]; - final var x = this.getData(); - - final double det = det(); - - mx[0][0] = x[1][1] / det; - mx[0][1] = -x[0][1] / det; - mx[1][0] = -x[1][0] / det; - mx[1][1] = x[0][0] / det; - - return new SquareMatrix(mx); - } - - /** - * Fast (in terms of required floating point operations) calculation of the determinant. - */ - - @Override - public double det() { - var x = getData(); - return x[0][0] * x[1][1] - x[1][0] * x[0][1]; - } - -} \ No newline at end of file + protected Matrix2(double[][] args) { + super(args); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * matrix inverse. + */ + @Override + public SquareMatrix inverse() { + double[][] mx = new double[2][2]; + final var x = this.getData(); + + final double det = det(); + + mx[0][0] = x[1][1] / det; + mx[0][1] = -x[0][1] / det; + mx[1][0] = -x[1][0] / det; + mx[1][1] = x[0][0] / det; + + return new SquareMatrix(mx); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * determinant. + */ + @Override + public double det() { + var x = getData(); + return x[0][0] * x[1][1] - x[1][0] * x[0][1]; + } + +} diff --git a/src/main/java/pulse/math/linear/Matrix3.java b/src/main/java/pulse/math/linear/Matrix3.java index e6e41a8e..09e799f6 100644 --- a/src/main/java/pulse/math/linear/Matrix3.java +++ b/src/main/java/pulse/math/linear/Matrix3.java @@ -4,45 +4,43 @@ * A 3-by-3 matrix class. * */ - class Matrix3 extends SquareMatrix { - protected Matrix3(double[][] args) { - super(args); - } - - /** - * Fast (in terms of required floating point operations) calculation of the - * matrix inverse. - */ - - @Override - public SquareMatrix inverse() { - - double[][] minv = new double[3][3]; - final var x = getData(); - - // computes the inverse of a matrix m - final double invdet = 1.0 / det(); - - minv[0][0] = (x[1][1] * x[2][2] - x[2][1] * x[1][2]) * invdet; - minv[0][1] = (x[0][2] * x[2][1] - x[0][1] * x[2][2]) * invdet; - minv[0][2] = (x[0][1] * x[1][2] - x[0][2] * x[1][1]) * invdet; - minv[1][0] = (x[1][2] * x[2][0] - x[1][0] * x[2][2]) * invdet; - minv[1][1] = (x[0][0] * x[2][2] - x[0][2] * x[2][0]) * invdet; - minv[1][2] = (x[1][0] * x[0][2] - x[0][0] * x[1][2]) * invdet; - minv[2][0] = (x[1][0] * x[2][1] - x[2][0] * x[1][1]) * invdet; - minv[2][1] = (x[2][0] * x[0][1] - x[0][0] * x[2][1]) * invdet; - minv[2][2] = (x[0][0] * x[1][1] - x[1][0] * x[0][1]) * invdet; - - return new SquareMatrix(minv); - } - - @Override - public double det() { - var x = getData(); - return x[0][0] * (x[1][1] * x[2][2] - x[2][1] * x[1][2]) - x[0][1] * (x[1][0] * x[2][2] - x[1][2] * x[2][0]) - + x[0][2] * (x[1][0] * x[2][1] - x[1][1] * x[2][0]); - } + protected Matrix3(double[][] args) { + super(args); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * matrix inverse. + */ + @Override + public SquareMatrix inverse() { + + double[][] minv = new double[3][3]; + final var x = getData(); + + // computes the inverse of a matrix m + final double invdet = 1.0 / det(); + + minv[0][0] = (x[1][1] * x[2][2] - x[2][1] * x[1][2]) * invdet; + minv[0][1] = (x[0][2] * x[2][1] - x[0][1] * x[2][2]) * invdet; + minv[0][2] = (x[0][1] * x[1][2] - x[0][2] * x[1][1]) * invdet; + minv[1][0] = (x[1][2] * x[2][0] - x[1][0] * x[2][2]) * invdet; + minv[1][1] = (x[0][0] * x[2][2] - x[0][2] * x[2][0]) * invdet; + minv[1][2] = (x[1][0] * x[0][2] - x[0][0] * x[1][2]) * invdet; + minv[2][0] = (x[1][0] * x[2][1] - x[2][0] * x[1][1]) * invdet; + minv[2][1] = (x[2][0] * x[0][1] - x[0][0] * x[2][1]) * invdet; + minv[2][2] = (x[0][0] * x[1][1] - x[1][0] * x[0][1]) * invdet; + + return new SquareMatrix(minv); + } + + @Override + public double det() { + var x = getData(); + return x[0][0] * (x[1][1] * x[2][2] - x[2][1] * x[1][2]) - x[0][1] * (x[1][0] * x[2][2] - x[1][2] * x[2][0]) + + x[0][2] * (x[1][0] * x[2][1] - x[1][1] * x[2][0]); + } } diff --git a/src/main/java/pulse/math/linear/Matrix4.java b/src/main/java/pulse/math/linear/Matrix4.java index cb3fc392..970a7a9a 100644 --- a/src/main/java/pulse/math/linear/Matrix4.java +++ b/src/main/java/pulse/math/linear/Matrix4.java @@ -4,64 +4,61 @@ * A 4-by-4 matrix. * */ - class Matrix4 extends SquareMatrix { - protected Matrix4(double[][] args) { - super(args); - } - - /** - * Fast inverse procedure for 4x4 matrix. Credit to Robin Hilliard. - * - * @return inverse of a 4x4 matrix - */ - - @Override - public SquareMatrix inverse() { - final var x = getData(); - var mx = new double[4][4]; + protected Matrix4(double[][] args) { + super(args); + } + + /** + * Fast inverse procedure for 4x4 matrix. Credit to Robin Hilliard. + * + * @return inverse of a 4x4 matrix + */ + @Override + public SquareMatrix inverse() { + final var x = getData(); + var mx = new double[4][4]; - final double s0 = x[0][0] * x[1][1] - x[1][0] * x[0][1]; - final double s1 = x[0][0] * x[1][2] - x[1][0] * x[0][2]; - final double s2 = x[0][0] * x[1][3] - x[1][0] * x[0][3]; - final double s3 = x[0][1] * x[1][2] - x[1][1] * x[0][2]; - final double s4 = x[0][1] * x[1][3] - x[1][1] * x[0][3]; - final double s5 = x[0][2] * x[1][3] - x[1][2] * x[0][3]; + final double s0 = x[0][0] * x[1][1] - x[1][0] * x[0][1]; + final double s1 = x[0][0] * x[1][2] - x[1][0] * x[0][2]; + final double s2 = x[0][0] * x[1][3] - x[1][0] * x[0][3]; + final double s3 = x[0][1] * x[1][2] - x[1][1] * x[0][2]; + final double s4 = x[0][1] * x[1][3] - x[1][1] * x[0][3]; + final double s5 = x[0][2] * x[1][3] - x[1][2] * x[0][3]; - final double c5 = x[2][2] * x[3][3] - x[3][2] * x[2][3]; - final double c4 = x[2][1] * x[3][3] - x[3][1] * x[2][3]; - final double c3 = x[2][1] * x[3][2] - x[3][1] * x[2][2]; - final double c2 = x[2][0] * x[3][3] - x[3][0] * x[2][3]; - final double c1 = x[2][0] * x[3][2] - x[3][0] * x[2][2]; - final double c0 = x[2][0] * x[3][1] - x[3][0] * x[2][1]; + final double c5 = x[2][2] * x[3][3] - x[3][2] * x[2][3]; + final double c4 = x[2][1] * x[3][3] - x[3][1] * x[2][3]; + final double c3 = x[2][1] * x[3][2] - x[3][1] * x[2][2]; + final double c2 = x[2][0] * x[3][3] - x[3][0] * x[2][3]; + final double c1 = x[2][0] * x[3][2] - x[3][0] * x[2][2]; + final double c0 = x[2][0] * x[3][1] - x[3][0] * x[2][1]; - // Should check for 0 determinant + // Should check for 0 determinant + final double invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); - final double invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); + mx[0][0] = (x[1][1] * c5 - x[1][2] * c4 + x[1][3] * c3) * invdet; + mx[0][1] = (-x[0][1] * c5 + x[0][2] * c4 - x[0][3] * c3) * invdet; + mx[0][2] = (x[3][1] * s5 - x[3][2] * s4 + x[3][3] * s3) * invdet; + mx[0][3] = (-x[2][1] * s5 + x[2][2] * s4 - x[2][3] * s3) * invdet; - mx[0][0] = (x[1][1] * c5 - x[1][2] * c4 + x[1][3] * c3) * invdet; - mx[0][1] = (-x[0][1] * c5 + x[0][2] * c4 - x[0][3] * c3) * invdet; - mx[0][2] = (x[3][1] * s5 - x[3][2] * s4 + x[3][3] * s3) * invdet; - mx[0][3] = (-x[2][1] * s5 + x[2][2] * s4 - x[2][3] * s3) * invdet; + mx[1][0] = (-x[1][0] * c5 + x[1][2] * c2 - x[1][3] * c1) * invdet; + mx[1][1] = (x[0][0] * c5 - x[0][2] * c2 + x[0][3] * c1) * invdet; + mx[1][2] = (-x[3][0] * s5 + x[3][2] * s2 - x[3][3] * s1) * invdet; + mx[1][3] = (x[2][0] * s5 - x[2][2] * s2 + x[2][3] * s1) * invdet; - mx[1][0] = (-x[1][0] * c5 + x[1][2] * c2 - x[1][3] * c1) * invdet; - mx[1][1] = (x[0][0] * c5 - x[0][2] * c2 + x[0][3] * c1) * invdet; - mx[1][2] = (-x[3][0] * s5 + x[3][2] * s2 - x[3][3] * s1) * invdet; - mx[1][3] = (x[2][0] * s5 - x[2][2] * s2 + x[2][3] * s1) * invdet; + mx[2][0] = (x[1][0] * c4 - x[1][1] * c2 + x[1][3] * c0) * invdet; + mx[2][1] = (-x[0][0] * c4 + x[0][1] * c2 - x[0][3] * c0) * invdet; + mx[2][2] = (x[3][0] * s4 - x[3][1] * s2 + x[3][3] * s0) * invdet; + mx[2][3] = (-x[2][0] * s4 + x[2][1] * s2 - x[2][3] * s0) * invdet; - mx[2][0] = (x[1][0] * c4 - x[1][1] * c2 + x[1][3] * c0) * invdet; - mx[2][1] = (-x[0][0] * c4 + x[0][1] * c2 - x[0][3] * c0) * invdet; - mx[2][2] = (x[3][0] * s4 - x[3][1] * s2 + x[3][3] * s0) * invdet; - mx[2][3] = (-x[2][0] * s4 + x[2][1] * s2 - x[2][3] * s0) * invdet; + mx[3][0] = (-x[1][0] * c3 + x[1][1] * c1 - x[1][2] * c0) * invdet; + mx[3][1] = (x[0][0] * c3 - x[0][1] * c1 + x[0][2] * c0) * invdet; + mx[3][2] = (-x[3][0] * s3 + x[3][1] * s1 - x[3][2] * s0) * invdet; + mx[3][3] = (x[2][0] * s3 - x[2][1] * s1 + x[2][2] * s0) * invdet; - mx[3][0] = (-x[1][0] * c3 + x[1][1] * c1 - x[1][2] * c0) * invdet; - mx[3][1] = (x[0][0] * c3 - x[0][1] * c1 + x[0][2] * c0) * invdet; - mx[3][2] = (-x[3][0] * s3 + x[3][1] * s1 - x[3][2] * s0) * invdet; - mx[3][3] = (x[2][0] * s3 - x[2][1] * s1 + x[2][2] * s0) * invdet; + return new SquareMatrix(mx); - return new SquareMatrix(mx); + } - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/linear/RectangularMatrix.java b/src/main/java/pulse/math/linear/RectangularMatrix.java index 81506d9e..8f325dc7 100644 --- a/src/main/java/pulse/math/linear/RectangularMatrix.java +++ b/src/main/java/pulse/math/linear/RectangularMatrix.java @@ -9,245 +9,244 @@ public class RectangularMatrix { - protected final double[][] x; - - protected RectangularMatrix(double[][] args) { - int m = args.length; - int n = args[0].length; - - x = new double[m][n]; - - for (int i = 0; i < m; i++) - System.arraycopy(args[i], 0, x[i], 0, n); - - } - - protected RectangularMatrix(double[] data, int n) { - final int m = data.length / n; - x = new double[m][n]; - - for (int i = 0; i < m; i++) - System.arraycopy(data, i * n, x[i], 0, n); - - } - - /** - * Performs an element-wise summation if {@code this} and {@code m} have - * matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of summation - */ - - public RectangularMatrix sum(RectangularMatrix m) { - return performOperation(this, m, SUM); - } - - /** - * Performs an element-wise subtraction of {@code m} from {@code this} if these - * matrices have matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of subtraction - */ - - public RectangularMatrix subtract(RectangularMatrix m) { - return performOperation(this, m, DIFFERENCE); - } - - /** - *

- * Performs {@code Matrix} multiplication. Checks whether the dimensions of each - * matrix are appropriate (number of columns in {@code this} matrix should be - * equal to the number of rows in {@code m}. - *

- * - * @param m another {@code Matrix} suitable for multiplication - * @return a {@code Matrix}, which is the result of multiplying {@code this} by - * {@code m} - */ - - public RectangularMatrix multiply(RectangularMatrix m) { - if (this.x[0].length != m.x.length) - throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); - - final int mm = this.x.length; - final int nn = m.x[0].length; - - var y = new double[mm][nn]; - - for (int i = 0; i < mm; i++) { - for (int j = 0; j < nn; j++) { - for (int k = 0; k < this.x[0].length; k++) { - y[i][j] += this.x[i][k] * m.x[k][j]; - } - } - } - - return createMatrix(y); - - } - - /** - * Scales this {@code Matrix} by {@code f}, which results in element-wise - * multiplication by {@code f}. - * - * @param f a numeric value - * @return the scaled {@code Matrix} - */ - - public RectangularMatrix multiply(double f) { - double[][] y = new double[x.length][x[0].length]; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - y[i][j] = this.x[i][j] * f; - } - } - - return createMatrix(y); - - } - - /** - * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. - * - * @return a transposed {@code Matrix} - */ - - public RectangularMatrix transpose() { - int m = x.length; - int n = x[0].length; - double[][] y = new double[n][m]; - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - y[j][i] = x[i][j]; - } - } - - return createMatrix(y); - - } - - public double get(int m, int k) { - return x[m][k]; - } - - public double[][] getData() { - return x; - } - - private static RectangularMatrix performOperation(RectangularMatrix m1, RectangularMatrix m2, - ArithmeticOperation op) { - if (!m1.dimensionsMatch(m2)) - throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); - - double[][] y = new double[m1.x.length][m1.x[0].length]; - - for (int i = 0; i < y.length; i++) { - for (int j = 0; j < y[0].length; j++) { - y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); - } - } - - return createMatrix(y); - } - - /** - *

- * Multiplies this {@code Matrix} by the vector {@code v}, which is represented - * by a n × 1 {@code Matrix}, where {@code n} is the - * dimension of {@code v}. Note {@code n} should be equal to the number of rows - * in this {@code Matrix}. - *

- * - * @param v a {@code Vector}. - * @return the result of multiplication, which is a {@code Vector}. - */ - - public Vector multiply(Vector v) { - double[] r = new double[x.length]; - - if (x[0].length != v.dimension()) - throw new IllegalArgumentException( - "Cannot multiply a " + x.length + "x" + x[0].length + " matrix by a " + v.dimension() + " vector"); - - for (int i = 0; i < x.length; i++) { - for (int k = 0; k < x[0].length; k++) { - r[i] += x[i][k] * v.get(k); - } - } - - return new Vector(r); - } - - /** - * Prints out matrix dimensions and all the elements contained in it. - */ - - @Override - public String toString() { - int m = x.length; - int n = x[0].length; - final String f = Messages.getString("Math.DecimalFormat"); - - StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); - for (int i = 0; i < m; i++) { - sb.append(System.lineSeparator()); - for (int j = 0; j < n; j++) { - sb.append(" "); - sb.append(String.format(f, x[i][j])); - } - } - - return sb.toString(); - } - - /** - * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. if - * the number of rows is the same and the number of columns is the same - * - * @param m another {@code Matrix} - * @return {@code true} if the dimensions match, {@code false} otherwise. - */ - - public boolean dimensionsMatch(RectangularMatrix m) { - return (x.length == m.x.length) && (x[0].length == m.x[0].length); - } - - /** - * Checks whether {@code o} is a {@code SquareMatrix} with matching dimensions - * and all elements of which are (approximately) equal to the respective - * elements of {@code this} matrix}. - */ - - @Override - public boolean equals(Object o) { - if (!(o instanceof SquareMatrix)) - return false; - - if (o == this) - return true; - - var m = (SquareMatrix) o; - - if (!this.dimensionsMatch(m)) - return false; - - boolean result = true; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { - result = false; - break; - } - } - } - - return result; - - } - -} \ No newline at end of file + protected final double[][] x; + + protected RectangularMatrix(double[][] args) { + int m = args.length; + int n = args[0].length; + + x = new double[m][n]; + + for (int i = 0; i < m; i++) { + System.arraycopy(args[i], 0, x[i], 0, n); + } + + } + + protected RectangularMatrix(double[] data, int n) { + final int m = data.length / n; + x = new double[m][n]; + + for (int i = 0; i < m; i++) { + System.arraycopy(data, i * n, x[i], 0, n); + } + + } + + /** + * Performs an element-wise summation if {@code this} and {@code m} have + * matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of summation + */ + public RectangularMatrix sum(RectangularMatrix m) { + return performOperation(this, m, SUM); + } + + /** + * Performs an element-wise subtraction of {@code m} from {@code this} if + * these matrices have matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of subtraction + */ + public RectangularMatrix subtract(RectangularMatrix m) { + return performOperation(this, m, DIFFERENCE); + } + + /** + *

+ * Performs {@code Matrix} multiplication. Checks whether the dimensions of + * each matrix are appropriate (number of columns in {@code this} matrix + * should be equal to the number of rows in {@code m}. + *

+ * + * @param m another {@code Matrix} suitable for multiplication + * @return a {@code Matrix}, which is the result of multiplying {@code this} + * by {@code m} + */ + public RectangularMatrix multiply(RectangularMatrix m) { + if (this.x[0].length != m.x.length) { + throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); + } + + final int mm = this.x.length; + final int nn = m.x[0].length; + + var y = new double[mm][nn]; + + for (int i = 0; i < mm; i++) { + for (int j = 0; j < nn; j++) { + for (int k = 0; k < this.x[0].length; k++) { + y[i][j] += this.x[i][k] * m.x[k][j]; + } + } + } + + return createMatrix(y); + + } + + /** + * Scales this {@code Matrix} by {@code f}, which results in element-wise + * multiplication by {@code f}. + * + * @param f a numeric value + * @return the scaled {@code Matrix} + */ + public RectangularMatrix multiply(double f) { + double[][] y = new double[x.length][x[0].length]; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + y[i][j] = this.x[i][j] * f; + } + } + + return createMatrix(y); + + } + + /** + * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. + * + * @return a transposed {@code Matrix} + */ + public RectangularMatrix transpose() { + int m = x.length; + int n = x[0].length; + double[][] y = new double[n][m]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + y[j][i] = x[i][j]; + } + } + + return createMatrix(y); + + } + + public double get(int m, int k) { + return x[m][k]; + } + + public double[][] getData() { + return x; + } + + private static RectangularMatrix performOperation(RectangularMatrix m1, RectangularMatrix m2, + ArithmeticOperation op) { + if (!m1.dimensionsMatch(m2)) { + throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); + } + + double[][] y = new double[m1.x.length][m1.x[0].length]; + + for (int i = 0; i < y.length; i++) { + for (int j = 0; j < y[0].length; j++) { + y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); + } + } + + return createMatrix(y); + } + + /** + *

+ * Multiplies this {@code Matrix} by the vector {@code v}, which is + * represented by a n × 1 {@code Matrix}, where + * {@code n} is the dimension of {@code v}. Note {@code n} should be equal + * to the number of rows in this {@code Matrix}. + *

+ * + * @param v a {@code Vector}. + * @return the result of multiplication, which is a {@code Vector}. + */ + public Vector multiply(Vector v) { + double[] r = new double[x.length]; + + if (x[0].length != v.dimension()) { + throw new IllegalArgumentException( + "Cannot multiply a " + x.length + "x" + x[0].length + " matrix by a " + v.dimension() + " vector"); + } + + for (int i = 0; i < x.length; i++) { + for (int k = 0; k < x[0].length; k++) { + r[i] += x[i][k] * v.get(k); + } + } + + return new Vector(r); + } + + /** + * Prints out matrix dimensions and all the elements contained in it. + */ + @Override + public String toString() { + int m = x.length; + int n = x[0].length; + final String f = Messages.getString("Math.DecimalFormat"); + + StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); + for (int i = 0; i < m; i++) { + sb.append(System.lineSeparator()); + for (int j = 0; j < n; j++) { + sb.append(" "); + sb.append(String.format(f, x[i][j])); + } + } + + return sb.toString(); + } + + /** + * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. + * if the number of rows is the same and the number of columns is the same + * + * @param m another {@code Matrix} + * @return {@code true} if the dimensions match, {@code false} otherwise. + */ + public boolean dimensionsMatch(RectangularMatrix m) { + return (x.length == m.x.length) && (x[0].length == m.x[0].length); + } + + /** + * Checks whether {@code o} is a {@code SquareMatrix} with matching + * dimensions and all elements of which are (approximately) equal to the + * respective elements of {@code this} matrix}. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof SquareMatrix)) { + return false; + } + + if (o == this) { + return true; + } + + var m = (SquareMatrix) o; + + if (!this.dimensionsMatch(m)) { + return false; + } + + boolean result = true; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { + result = false; + break; + } + } + } + + return result; + + } + +} diff --git a/src/main/java/pulse/math/linear/SquareMatrix.java b/src/main/java/pulse/math/linear/SquareMatrix.java index 22032327..cb037b50 100644 --- a/src/main/java/pulse/math/linear/SquareMatrix.java +++ b/src/main/java/pulse/math/linear/SquareMatrix.java @@ -20,101 +20,97 @@ * {@code pulse.math} package, the user needs to invoke the factory class * methods {@code Matrices} instead. *

- * + * * @see pulse.math.linear.Matrices */ - public class SquareMatrix extends RectangularMatrix { - /** - * Constructs a {@code Matrix} with the elements copied from {@code args}. The - * elements are copied by invoking System.arraycopy(...). - * - * @param args a two-dimensional double array - */ + /** + * Constructs a {@code Matrix} with the elements copied from {@code args}. + * The elements are copied by invoking System.arraycopy(...). + * + * @param args a two-dimensional double array + */ + protected SquareMatrix(double[][] args) { + super(args); + } + + private SquareMatrix(double[] data, int n) { + super(data, n); + } - protected SquareMatrix(double[][] args) { - super(args); - } + /** + * Calculates the determinant for an n-by-n square matrix. The + * determinant is calculated using the EJML library. + * + * @return a double, representing the determinant + */ + public double det() { + var mx = new DMatrixRMaj(x); + return CommonOps_DDRM.det(mx); + } - private SquareMatrix(double[] data, int n) { - super(data, n); - } + /** + * Conducts matrix inversion with the procedural EJML approach. Can be + * overriden by subclasses to boost performance. + * + * @return the inverted {@Code Matrix}. + */ + public SquareMatrix inverse() { + var mx = new DMatrixRMaj(x); + invert(mx); + return new SquareMatrix(mx.getData(), x.length); + } - /** - * Calculates the determinant for an n-by-n square matrix. The - * determinant is calculated using the EJML library. - * - * @return a double, representing the determinant - */ + /** + * Checks if a matrix is positive definite. Uses EJML implementation. + * + * @return {@code true} is positive-definite + */ + public boolean isPositiveDefinite() { + return MatrixFeatures_DDRM.isPositiveDefinite(new DMatrixRMaj(x)); + } - public double det() { - var mx = new DMatrixRMaj(x); - return CommonOps_DDRM.det(mx); - } + /** + * Calculates the outer product of two vectors. + * + * @param a a Vector + * @param b a Vector + * @return the outer product of {@code a} and {@code b} + */ + public static SquareMatrix outerProduct(Vector a, Vector b) { + double[][] x = new double[a.dimension()][b.dimension()]; - /** - * Conducts matrix inversion with the procedural EJML approach. Can be overriden - * by subclasses to boost performance. - * - * @return the inverted {@Code Matrix}. - */ + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + x[i][j] = a.get(i) * b.get(j); + } + } - public SquareMatrix inverse() { - var mx = new DMatrixRMaj(x); - invert(mx); - return new SquareMatrix(mx.getData(), x.length); - } - - /** - * Checks if a matrix is positive definite. Uses EJML implementation. - * @return {@code true} is positive-definite - */ - - public boolean isPositiveDefinite() { - return MatrixFeatures_DDRM.isPositiveDefinite(new DMatrixRMaj(x)); - } - - /** - * Calculates the outer product of two vectors. - * - * @param a a Vector - * @param b a Vector - * @return the outer product of {@code a} and {@code b} - */ + return createSquareMatrix(x); + } - public static SquareMatrix outerProduct(Vector a, Vector b) { - double[][] x = new double[a.dimension()][b.dimension()]; + public static SquareMatrix asSquareMatrix(RectangularMatrix m) { + return m.x.length == m.x[0].length ? new SquareMatrix(m.getData()) : null; + } - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - x[i][j] = a.get(i) * b.get(j); - } - } + public int dimension() { + return getData().length; + } - return createSquareMatrix(x); - } - - public static SquareMatrix asSquareMatrix(RectangularMatrix m) { - return m.x.length == m.x[0].length ? new SquareMatrix(m.getData()) : null; - } - - public int dimension() { - return getData().length; - } - - /** - * Creates a block-diagonal matrix from the diagonal of this matrix. - * @return diag(this) - */ - - public SquareMatrix blockDiagonal() { - final int dim = dimension(); - var data = getData(); - var diag = new double[dim][dim]; - for(int i = 0; i < dim; i++) - diag[i][i] = data[i][i]; - return new SquareMatrix(diag); - } + /** + * Creates a block-diagonal matrix from the diagonal of this matrix. + * + * @return diag(this) + */ + public SquareMatrix blockDiagonal() { + final int dim = dimension(); + var data = getData(); + var diag = new double[dim][dim]; + for (int i = 0; i < dim; i++) { + diag[i][i] = data[i][i]; + } + return new SquareMatrix(diag); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 41101ac5..3b493829 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -11,315 +11,304 @@ /** *

- * This is a general class for {@code Vector} operations useful for - * optimisers and ODE solvers. + * This is a general class for {@code Vector} operations useful for optimisers + * and ODE solvers. *

*/ - public class Vector { - private double[] x; - - /** - * Constructs a new vector specified by the argument array - * @param x an array of double - */ - - public Vector(double[] x) { - this.x = new double[x.length]; - System.arraycopy(x, 0, this.x, 0, x.length); - } - - /** - * Creates a zero {@code Vector}. - * - * @param n the dimension of the {@code Vector}. - */ - - public Vector(int n) { - x = new double[n]; - } - - /** - * Copy constructor. - * - * @param v The vector to be copied - */ - - public Vector(Vector v) { - this(v.x); - } - - /** - * Creates a new {@code Vector} based on {@code this} one, all elements of which - * are inverted, i.e. bi = -ai. - * - * @return a generalised inversion of {@code this Vector}. - */ - - public Vector inverted() { - return performOperation(new Vector(dimension()), this, DIFFERENCE); - } - - /** - * The dimension is simply the number of elements in a {@code Vector} - * - * @return the integer dimension - */ - - public int dimension() { - return x.length; - } - - /** - * Performs an element-wise summation of {@code this} and {@code v}. - * - * @param v another {@code Vector} with the same number of elements. - * @return the result of the summation. - */ - - public Vector sum(Vector v) { - return performOperation(this, v, SUM); - } - - /** - * Performs an element-wise subtraction of {@code v} from {@code this}. - * - * @param v another {@code Vector} with the same number of elements. - * @return the result of subtracting {@code v} from {@code this}. - * @throws IllegalArgumentException f the dimension of {@code this} and - * {@code v} are different. - */ - - public Vector subtract(Vector v) { - return performOperation(this, v, DIFFERENCE); - } - - /** - * Performs an element-wise multiplication by {@code f}. - * - * @param f a double value. - * @return a new {@code Vector}, all elements of which will be multiplied by - * {@code f}. - */ - - public Vector multiply(double f) { - Vector factor = new Vector(this); - - for (int i = 0; i < x.length; i++) { - factor.x[i] *= f; - } - - return factor; - } - - /** - * Creates a vector with random coordinates confined within [min;max] - * @param n the vector dimension - * @param min upper bound for the random number generator - * @param max lower bound for the random generator generator - * @return the randomised vector - */ - - public static Vector random(int n, double min, double max) { - var v = new Vector(n); - for(int i = 0; i < n; i++) { - v.x[i] = min + Math.random()*(max - min); - } - return v; - } - - /** - * Component-wise vector multiplication - */ - - public Vector multComponents(Vector v) { - Vector nv = new Vector(this); - - for(int i = 0; i < x.length; i++) { - nv.x[i] *= v.x[i]; - } - - return nv; - - } - - /** - * Calculates the scalar product of {@code this} and {@code v}. - * - * @param v another {@code Vector} with the same dimension. - * @return the dot product of {@code this} and {@code v}. - */ - - public double dot(Vector v) { - return reduce(this, v, PRODUCT); - } - - /** - * Calculates the length, which is represented by the square-root of the squared - * length. - * - * @return the calculated length. - * @see lengthSq() - */ - - public double length() { - return sqrt(lengthSq()); - } - - /** - * The squared length of this vector is the dot product of this vector by - * itself. - * - * @return the squared length. - */ - - public double lengthSq() { - return this.dot(this); - } - - /** - * Performs normalisation, e.g. scalar multiplication of {@code this} by the - * multiplicative inverse of {@code this Vector}'s length. - * - * @return a normalised {@code Vector} obtained from {@code this}. - */ - - public Vector normalise() { - return this.multiply(1.0 / length() ); - } - - /** - * Calculates the squared distance from {@code this Vector} to {@code v} (which - * is the squared length of the connecting {@code Vector}). - * - * @param v another {@code Vector}. - * @return the squared length of the connecting {@code Vector}. - * @throws IllegalArgumentException f the dimension of {@code this} and - * {@code v} are different. - */ - - public double distanceToSq(Vector v) throws IllegalArgumentException { - return reduce(this, v, DIFF_SQUARED); - } - - /** - * Gets the component of {@code this Vector} specified by {@code index} - * - * @param index the index of the component - * @return a double value, representing the value of the component - */ - - public double get(int index) { - return x[index]; - } - - /** - * Sets the component of {@code this Vector} specified by {@code index} to - * {@code value}. - * - * @param index the index of the component. - * @param value a new value that will replace the old one. - */ - - public void set(int index, double value) { - x[index] = value; - } - - /** - * Defines the string representation of the current instance of the class. - * - * @return the string-equivalent of this object containing all it's field - * values. - */ - - @Override - public String toString() { - final String f = Messages.getString("Math.DecimalFormat"); //$NON-NLS-1$ - StringBuilder sb = new StringBuilder().append("("); //$NON-NLS-1$ - for (double c : x) { - sb.append(String.format(f, c) + " "); //$NON-NLS-1$ - } - sb.append(")"); //$NON-NLS-1$ - return sb.toString(); - } - - /** - * Checks if o is logically equivalent to an instance of this - * class. - * - * @param o An object to compare with this vector. - * @return true if o equals this. - */ - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof Vector)) - return false; - - Vector v = (Vector) o; - - if (v.x.length != x.length) - return false; - - for (int i = 0; i < x.length; i++) { - if (Double.doubleToLongBits(this.x[i]) != Double.doubleToLongBits(v.x[i])) - return false; - } - - return true; - - } - - /** - * Determines the maximum absolute value of the vector components. - * - * @return a component having the maximum absolute value. - */ - - public double maxAbsComponent() { - double max = abs(x[0]); - double abs = 0; - for (int i = 1; i < x.length; i++) { - abs = abs(x[i]); - max = abs > max ? abs : max; - } - return max; - } - - public double[] getData() { - return x; - } - - private static void checkup(Vector v1, Vector v2) { - if (v1.x.length != v2.x.length) - throw new IllegalArgumentException( - Messages.getString("Vector.DimensionError1") + v1.x.length + " != " + v2.x.length); - } - - private static Vector performOperation(Vector v1, Vector v2, ArithmeticOperation op) { - checkup(v1, v2); - - Vector result = new Vector(v2.x.length); - - for (int i = 0; i < v2.x.length; i++) - result.x[i] = op.evaluate(v1.get(i), v2.get(i)); - - return result; - } - - private static double reduce(Vector v1, Vector v2, ArithmeticOperation op) { - checkup(v1, v2); - - double result = 0; - - for (int i = 0; i < v2.x.length; i++) - result += op.evaluate(v1.get(i), v2.get(i)); - - return result; - } - -} \ No newline at end of file + private double[] x; + + /** + * Constructs a new vector specified by the argument array + * + * @param x an array of double + */ + public Vector(double[] x) { + this.x = new double[x.length]; + System.arraycopy(x, 0, this.x, 0, x.length); + } + + /** + * Creates a zero {@code Vector}. + * + * @param n the dimension of the {@code Vector}. + */ + public Vector(int n) { + x = new double[n]; + } + + /** + * Copy constructor. + * + * @param v The vector to be copied + */ + public Vector(Vector v) { + this(v.x); + } + + /** + * Creates a new {@code Vector} based on {@code this} one, all elements of + * which are inverted, i.e. bi = + * -ai. + * + * @return a generalised inversion of {@code this Vector}. + */ + public Vector inverted() { + return performOperation(new Vector(dimension()), this, DIFFERENCE); + } + + /** + * The dimension is simply the number of elements in a {@code Vector} + * + * @return the integer dimension + */ + public int dimension() { + return x.length; + } + + /** + * Performs an element-wise summation of {@code this} and {@code v}. + * + * @param v another {@code Vector} with the same number of elements. + * @return the result of the summation. + */ + public Vector sum(Vector v) { + return performOperation(this, v, SUM); + } + + /** + * Performs an element-wise subtraction of {@code v} from {@code this}. + * + * @param v another {@code Vector} with the same number of elements. + * @return the result of subtracting {@code v} from {@code this}. + * @throws IllegalArgumentException f the dimension of {@code this} and + * {@code v} are different. + */ + public Vector subtract(Vector v) { + return performOperation(this, v, DIFFERENCE); + } + + /** + * Performs an element-wise multiplication by {@code f}. + * + * @param f a double value. + * @return a new {@code Vector}, all elements of which will be multiplied by + * {@code f}. + */ + public Vector multiply(double f) { + Vector factor = new Vector(this); + + for (int i = 0; i < x.length; i++) { + factor.x[i] *= f; + } + + return factor; + } + + /** + * Creates a vector with random coordinates confined within [min;max] + * + * @param n the vector dimension + * @param min upper bound for the random number generator + * @param max lower bound for the random generator generator + * @return the randomised vector + */ + public static Vector random(int n, double min, double max) { + var v = new Vector(n); + for (int i = 0; i < n; i++) { + v.x[i] = min + Math.random() * (max - min); + } + return v; + } + + /** + * Component-wise vector multiplication + */ + public Vector multComponents(Vector v) { + Vector nv = new Vector(this); + + for (int i = 0; i < x.length; i++) { + nv.x[i] *= v.x[i]; + } + + return nv; + + } + + /** + * Calculates the scalar product of {@code this} and {@code v}. + * + * @param v another {@code Vector} with the same dimension. + * @return the dot product of {@code this} and {@code v}. + */ + public double dot(Vector v) { + return reduce(this, v, PRODUCT); + } + + /** + * Calculates the length, which is represented by the square-root of the + * squared length. + * + * @return the calculated length. + * @see lengthSq() + */ + public double length() { + return sqrt(lengthSq()); + } + + /** + * The squared length of this vector is the dot product of this vector by + * itself. + * + * @return the squared length. + */ + public double lengthSq() { + return this.dot(this); + } + + /** + * Performs normalisation, e.g. scalar multiplication of {@code this} by the + * multiplicative inverse of {@code this Vector}'s length. + * + * @return a normalised {@code Vector} obtained from {@code this}. + */ + public Vector normalise() { + return this.multiply(1.0 / length()); + } + + /** + * Calculates the squared distance from {@code this Vector} to {@code v} + * (which is the squared length of the connecting {@code Vector}). + * + * @param v another {@code Vector}. + * @return the squared length of the connecting {@code Vector}. + * @throws IllegalArgumentException f the dimension of {@code this} and + * {@code v} are different. + */ + public double distanceToSq(Vector v) throws IllegalArgumentException { + return reduce(this, v, DIFF_SQUARED); + } + + /** + * Gets the component of {@code this Vector} specified by {@code index} + * + * @param index the index of the component + * @return a double value, representing the value of the component + */ + public double get(int index) { + return x[index]; + } + + /** + * Sets the component of {@code this Vector} specified by {@code index} to + * {@code value}. + * + * @param index the index of the component. + * @param value a new value that will replace the old one. + */ + public void set(int index, double value) { + x[index] = value; + } + + /** + * Defines the string representation of the current instance of the class. + * + * @return the string-equivalent of this object containing all it's field + * values. + */ + @Override + public String toString() { + final String f = Messages.getString("Math.DecimalFormat"); //$NON-NLS-1$ + StringBuilder sb = new StringBuilder().append("("); //$NON-NLS-1$ + for (double c : x) { + sb.append(String.format(f, c) + " "); //$NON-NLS-1$ + } + sb.append(")"); //$NON-NLS-1$ + return sb.toString(); + } + + /** + * Checks if o is logically equivalent to an instance of this + * class. + * + * @param o An object to compare with this vector. + * @return true if o equals this. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Vector)) { + return false; + } + + Vector v = (Vector) o; + + if (v.x.length != x.length) { + return false; + } + + for (int i = 0; i < x.length; i++) { + if (Double.doubleToLongBits(this.x[i]) != Double.doubleToLongBits(v.x[i])) { + return false; + } + } + + return true; + + } + + /** + * Determines the maximum absolute value of the vector components. + * + * @return a component having the maximum absolute value. + */ + public double maxAbsComponent() { + double max = abs(x[0]); + double abs = 0; + for (int i = 1; i < x.length; i++) { + abs = abs(x[i]); + max = abs > max ? abs : max; + } + return max; + } + + public double[] getData() { + return x; + } + + private static void checkup(Vector v1, Vector v2) { + if (v1.x.length != v2.x.length) { + throw new IllegalArgumentException( + Messages.getString("Vector.DimensionError1") + v1.x.length + " != " + v2.x.length); + } + } + + private static Vector performOperation(Vector v1, Vector v2, ArithmeticOperation op) { + checkup(v1, v2); + + Vector result = new Vector(v2.x.length); + + for (int i = 0; i < v2.x.length; i++) { + result.x[i] = op.evaluate(v1.get(i), v2.get(i)); + } + + return result; + } + + private static double reduce(Vector v1, Vector v2, ArithmeticOperation op) { + checkup(v1, v2); + + double result = 0; + + for (int i = 0; i < v2.x.length; i++) { + result += op.evaluate(v1.get(i), v2.get(i)); + } + + return result; + } + +} diff --git a/src/main/java/pulse/math/linear/package-info.java b/src/main/java/pulse/math/linear/package-info.java index 690dcd93..f875da47 100644 --- a/src/main/java/pulse/math/linear/package-info.java +++ b/src/main/java/pulse/math/linear/package-info.java @@ -1,5 +1,4 @@ /** * A linear algebra package based mostly on the EJML library. */ - -package pulse.math.linear; \ No newline at end of file +package pulse.math.linear; diff --git a/src/main/java/pulse/math/package-info.java b/src/main/java/pulse/math/package-info.java index 525d4ae4..28763b06 100644 --- a/src/main/java/pulse/math/package-info.java +++ b/src/main/java/pulse/math/package-info.java @@ -3,5 +3,4 @@ * (a {@code Vector}) of the minimum, including operations with vector and * matrices. */ - -package pulse.math; \ No newline at end of file +package pulse.math; diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java index 213da185..b65577e9 100644 --- a/src/main/java/pulse/math/transforms/AtanhTransform.java +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -6,38 +6,36 @@ import pulse.math.Segment; /** - * Hyper-tangent parameter transform allowing to set an upper bound for a parameter. + * Hyper-tangent parameter transform allowing to set an upper bound for a + * parameter. */ - public class AtanhTransform extends BoundedParameterTransform { - /** - * Only the upper bound of the argument is used. - * @param bounds the {@code bounda.getMaximum()} is used in the transforms - */ - - public AtanhTransform(Segment bounds) { - super(bounds); - } - - /** - * @see pulse.math.MathUtils.atanh() - * @see pulse.math.Segment.getBounds() - */ + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public AtanhTransform(Segment bounds) { + super(bounds); + } + + /** + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + return atanh(2.0 * a / getBounds().getMaximum() - 1.0); + } - @Override - public double transform(double a) { - return atanh(2.0 * a / getBounds().getMaximum() - 1.0); - } - - /** - * @see pulse.math.MathUtils.tanh() - * @see pulse.math.Segment.getBounds() - */ + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return 0.5 * getBounds().getMaximum() * (tanh(t) + 1.0); + } - @Override - public double inverse(double t) { - return 0.5 * getBounds().getMaximum() * (tanh(t) + 1.0); - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java index 7ad677df..be12e6ea 100644 --- a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java +++ b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java @@ -3,25 +3,24 @@ import pulse.math.Segment; /** - * An abstract {@code Transformable} where the bounds of the parameter is manually set. - * Subclasses can be bounded from either on or both sides. + * An abstract {@code Transformable} where the bounds of the parameter is + * manually set. Subclasses can be bounded from either on or both sides. * */ - public abstract class BoundedParameterTransform implements Transformable { - private Segment bounds; + private Segment bounds; + + public BoundedParameterTransform(Segment bounds) { + setBounds(bounds); + } + + public Segment getBounds() { + return bounds; + } - public BoundedParameterTransform(Segment bounds) { - setBounds(bounds); - } - - public Segment getBounds() { - return bounds; - } + public void setBounds(Segment bounds) { + this.bounds = bounds; + } - public void setBounds(Segment bounds) { - this.bounds = bounds; - } - } diff --git a/src/main/java/pulse/math/transforms/InvDiamTransform.java b/src/main/java/pulse/math/transforms/InvDiamTransform.java index 6afdb00a..c8810814 100644 --- a/src/main/java/pulse/math/transforms/InvDiamTransform.java +++ b/src/main/java/pulse/math/transforms/InvDiamTransform.java @@ -6,23 +6,22 @@ * A transform that simply divides the value by the squared length of the * sample. */ - public class InvDiamTransform implements Transformable { - private double d; - - public InvDiamTransform(ExtendedThermalProperties etp) { - d = (double) etp.getSampleDiameter().getValue(); - } - - @Override - public double transform(double value) { - return value / d; - } + private double d; + + public InvDiamTransform(ExtendedThermalProperties etp) { + d = (double) etp.getSampleDiameter().getValue(); + } + + @Override + public double transform(double value) { + return value / d; + } - @Override - public double inverse(double t) { - return t * d; - } + @Override + public double inverse(double t) { + return t * d; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java index 5fa4ac8d..2a849135 100644 --- a/src/main/java/pulse/math/transforms/InvLenSqTransform.java +++ b/src/main/java/pulse/math/transforms/InvLenSqTransform.java @@ -6,23 +6,22 @@ * A transform that simply divides the value by the squared length of the * sample. */ - public class InvLenSqTransform implements Transformable { - private double l; - - public InvLenSqTransform(ThermalProperties tp) { - this.l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return value / (l * l); - } + private double l; + + public InvLenSqTransform(ThermalProperties tp) { + this.l = (double) tp.getSampleThickness().getValue(); + } + + @Override + public double transform(double value) { + return value / (l * l); + } - @Override - public double inverse(double t) { - return t * (l * l); - } + @Override + public double inverse(double t) { + return t * (l * l); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java index 28c1d658..571bdd81 100644 --- a/src/main/java/pulse/math/transforms/InvLenTransform.java +++ b/src/main/java/pulse/math/transforms/InvLenTransform.java @@ -3,26 +3,24 @@ import pulse.problem.statements.model.ThermalProperties; /** - * A transform that simply divides the value by the length of the - * sample. + * A transform that simply divides the value by the length of the sample. */ - public class InvLenTransform implements Transformable { - private double l; - - public InvLenTransform(ThermalProperties tp) { - l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return value / l; - } + private double l; + + public InvLenTransform(ThermalProperties tp) { + l = (double) tp.getSampleThickness().getValue(); + } + + @Override + public double transform(double value) { + return value / l; + } - @Override - public double inverse(double t) { - return t * l; - } + @Override + public double inverse(double t) { + return t * l; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index 08604df5..a8206fdb 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -5,60 +5,60 @@ import static java.lang.Math.sqrt; /** - * A utility class containing standard mathematical transforms and their inverses for non-bounded parameters. + * A utility class containing standard mathematical transforms and their + * inverses for non-bounded parameters. * */ - public class StandardTransformations { - - /** - * Logarithmic parameter transform. The parameter space is only bounded by positive numbers, so no bounding segment required. - */ - - public final static Transformable LOG = new Transformable() { - - @Override - public double transform(double a) { - return log(a); - } - - @Override - public double inverse(double t) { - return exp(t); - } - - }; - - public final static Transformable SQRT = new Transformable() { - - @Override - public double transform(double a) { - return sqrt(a); - } - - @Override - public double inverse(double t) { - return t*t; - } - - }; - - public final static Transformable ABS = new Transformable() { - - @Override - public double transform(double a) { - return Math.abs(a); - } - - @Override - public double inverse(double t) { - return transform(t); - } - - }; - - private StandardTransformations() { - //empty - } - -} \ No newline at end of file + + /** + * Logarithmic parameter transform. The parameter space is only bounded by + * positive numbers, so no bounding segment required. + */ + public final static Transformable LOG = new Transformable() { + + @Override + public double transform(double a) { + return log(a); + } + + @Override + public double inverse(double t) { + return exp(t); + } + + }; + + public final static Transformable SQRT = new Transformable() { + + @Override + public double transform(double a) { + return sqrt(a); + } + + @Override + public double inverse(double t) { + return t * t; + } + + }; + + public final static Transformable ABS = new Transformable() { + + @Override + public double transform(double a) { + return Math.abs(a); + } + + @Override + public double inverse(double t) { + return transform(t); + } + + }; + + private StandardTransformations() { + //empty + } + +} diff --git a/src/main/java/pulse/math/transforms/Transformable.java b/src/main/java/pulse/math/transforms/Transformable.java index 0116a784..88e9d13c 100644 --- a/src/main/java/pulse/math/transforms/Transformable.java +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -1,24 +1,24 @@ package pulse.math.transforms; /** - * An interface for performing reversible one-to-one mapping of the model parameters. + * An interface for performing reversible one-to-one mapping of the model + * parameters. * */ - public interface Transformable { - /** - * Performs the selected transform with {@code value} - * @param value a double representing the parameter value - * @return the results, such that {@code inverse( transform(value) ) = value} - */ - - public double transform(double value); - - /** - * Inverses the transform. - */ - - public double inverse(double t); - -} \ No newline at end of file + /** + * Performs the selected transform with {@code value} + * + * @param value a double representing the parameter value + * @return the results, such that + * {@code inverse( transform(value) ) = value} + */ + public double transform(double value); + + /** + * Inverses the transform. + */ + public double inverse(double t); + +} diff --git a/src/main/java/pulse/package-info.java b/src/main/java/pulse/package-info.java index 8a374e66..5017a1ad 100644 --- a/src/main/java/pulse/package-info.java +++ b/src/main/java/pulse/package-info.java @@ -3,5 +3,4 @@ * in any other packages. Currently consists of three classes extensively * used in other packages. */ - -package pulse; \ No newline at end of file +package pulse; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index d3d511d3..dd397e24 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -9,98 +9,91 @@ * A {@code DiscretePulse} is an object that acts as a medium between the * physical {@code Pulse} and the respective {@code DifferenceScheme} used to * process the solution of a {@code Problem}. - * + * * @see pulse.problem.statements.Pulse */ - public class DiscretePulse { - private Grid grid; - private Pulse pulse; - private double discretePulseWidth; - private double timeFactor; - - /** - * This creates a one-dimensional discrete pulse on a {@code grid}. - *

- * The dimensional factor is taken from the {@code problem}, while the discrete - * pulse width (a multiplier of the {@code grid} parameter {@code tau} is - * calculated using the {@code gridTime} method. - *

- * - * @param problem the problem, used to extract the dimensional time factor - * @param grid a grid used to discretise the {@code pulse} - */ - - public DiscretePulse(Problem problem, Grid grid) { - this.grid = grid; - timeFactor = problem.getProperties().timeFactor(); - this.pulse = problem.getPulse(); - - recalculate(); - - var data = ( (SearchTask) problem.specificAncestor(SearchTask.class) ).getExperimentalCurve(); - - pulse.getPulseShape().init(data, this); - pulse.addListener(e -> { - timeFactor = problem.getProperties().timeFactor(); - recalculate(); - pulse.getPulseShape().init(data, this); - }); - } - - /** - * Uses the {@code PulseTemporalShape} of the {@code Pulse} object to calculate - * the laser power at the specified moment of {@code time}. - * - * @param time the time argument - * @return the laser power at the specified moment of {@code time} - */ - - public double laserPowerAt(double time) { - return pulse.getPulseShape().evaluateAt(time); - } - - /** - * Recalculates the {@code discretePulseWidth} by calling {@code gridTime} on - * the physical pulse width and {@code timeFactor}. - * - * @see pulse.problem.schemes.Grid.gridTime(double,double) - */ - - public void recalculate() { - final double width = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - discretePulseWidth = Math.max( grid.gridTime(width, timeFactor), grid.getTimeStep() ); - } - - /** - * Gets the discrete pulse width defined by {@code DiscretePulse}. - * - * @return a double, representing the discrete pulse width. - */ - - public double getDiscreteWidth() { - return discretePulseWidth; - } - - /** - * Gets the physical {@code Pulse} - * - * @return the {@code Pulse} object - */ - - public Pulse getPulse() { - return pulse; - } - - /** - * Gets the {@code Grid} object used to construct this {@code DiscretePulse} - * - * @return the {@code Grid} object. - */ - - public Grid getGrid() { - return grid; - } - -} \ No newline at end of file + private Grid grid; + private Pulse pulse; + private double discretePulseWidth; + private double timeFactor; + + /** + * This creates a one-dimensional discrete pulse on a {@code grid}. + *

+ * The dimensional factor is taken from the {@code problem}, while the + * discrete pulse width (a multiplier of the {@code grid} parameter + * {@code tau} is calculated using the {@code gridTime} method. + *

+ * + * @param problem the problem, used to extract the dimensional time factor + * @param grid a grid used to discretise the {@code pulse} + */ + public DiscretePulse(Problem problem, Grid grid) { + this.grid = grid; + timeFactor = problem.getProperties().timeFactor(); + this.pulse = problem.getPulse(); + + recalculate(); + + var data = ((SearchTask) problem.specificAncestor(SearchTask.class)).getExperimentalCurve(); + + pulse.getPulseShape().init(data, this); + pulse.addListener(e -> { + timeFactor = problem.getProperties().timeFactor(); + recalculate(); + pulse.getPulseShape().init(data, this); + }); + } + + /** + * Uses the {@code PulseTemporalShape} of the {@code Pulse} object to + * calculate the laser power at the specified moment of {@code time}. + * + * @param time the time argument + * @return the laser power at the specified moment of {@code time} + */ + public double laserPowerAt(double time) { + return pulse.getPulseShape().evaluateAt(time); + } + + /** + * Recalculates the {@code discretePulseWidth} by calling {@code gridTime} + * on the physical pulse width and {@code timeFactor}. + * + * @see pulse.problem.schemes.Grid.gridTime(double,double) + */ + public void recalculate() { + final double width = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); + discretePulseWidth = Math.max(grid.gridTime(width, timeFactor), grid.getTimeStep()); + } + + /** + * Gets the discrete pulse width defined by {@code DiscretePulse}. + * + * @return a double, representing the discrete pulse width. + */ + public double getDiscreteWidth() { + return discretePulseWidth; + } + + /** + * Gets the physical {@code Pulse} + * + * @return the {@code Pulse} object + */ + public Pulse getPulse() { + return pulse; + } + + /** + * Gets the {@code Grid} object used to construct this {@code DiscretePulse} + * + * @return the {@code Grid} object. + */ + public Grid getGrid() { + return grid; + } + +} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 85f1f162..96ad965e 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -16,69 +16,65 @@ *

* */ - public class DiscretePulse2D extends DiscretePulse { - private double discretePulseSpot; - private double coordFactor; - - /** - * The constructor for {@code DiscretePulse2D}. - *

- * Calls the constructor of the superclass, after which calculates the - * {@code discretePulseSpot} using the {@code gridRadialDistance} method of this - * class. The dimension factor is defined as the sample diameter. - *

- * - * @param problem a two-dimensional problem - * @param grid the two-dimensional grid - */ - - public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { - super(problem, grid); - var properties = (ExtendedThermalProperties)problem.getProperties(); - coordFactor = (double) properties.getSampleDiameter().getValue() / 2.0; - var pulse = (Pulse2D)problem.getPulse(); - discretePulseSpot = grid.gridRadialDistance((double) pulse.getSpotDiameter().getValue() / 2.0, coordFactor); - - } + private double discretePulseSpot; + private double coordFactor; - /** - * This calculates the dimensionless, discretised pulse function at a - * dimensionless radial coordinate {@code coord}. - *

- * It uses a Heaviside function to determine whether the {@code radialCoord} - * lies within the {@code 0 <= radialCoord <= discretePulseSpot} interval. It - * uses the {@code time} parameter to determine the discrete pulse function - * using {@code evaluateAt(time)}. - * - * @param time the time for calculation - * @param radialCoord - the radial coordinate [length dimension] - * @return the pulse function at {@code time} and {@code coord}, or 0 if - * {@code coord > spotDiameter}. - * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) - */ + /** + * The constructor for {@code DiscretePulse2D}. + *

+ * Calls the constructor of the superclass, after which calculates the + * {@code discretePulseSpot} using the {@code gridRadialDistance} method of + * this class. The dimension factor is defined as the sample diameter. + *

+ * + * @param problem a two-dimensional problem + * @param grid the two-dimensional grid + */ + public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { + super(problem, grid); + var properties = (ExtendedThermalProperties) problem.getProperties(); + coordFactor = (double) properties.getSampleDiameter().getValue() / 2.0; + var pulse = (Pulse2D) problem.getPulse(); + discretePulseSpot = grid.gridRadialDistance((double) pulse.getSpotDiameter().getValue() / 2.0, coordFactor); - public double evaluateAt(double time, double radialCoord) { - return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); - } + } - /** - * Calls the superclass method, then calculates the {@code discretePulseSpot} - * using the {@code gridRadialDistance} method. - * - * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) - */ + /** + * This calculates the dimensionless, discretised pulse function at a + * dimensionless radial coordinate {@code coord}. + *

+ * It uses a Heaviside function to determine whether the {@code radialCoord} + * lies within the {@code 0 <= radialCoord <= discretePulseSpot} interval. + * It uses the {@code time} parameter to determine the discrete pulse + * function using {@code evaluateAt(time)}. + * + * @param time the time for calculation + * @param radialCoord - the radial coordinate [length dimension] + * @return the pulse function at {@code time} and {@code coord}, or 0 if + * {@code coord > spotDiameter}. + * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) + */ + public double evaluateAt(double time, double radialCoord) { + return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); + } - @Override - public void recalculate() { - super.recalculate(); - final double radius = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue() / 2.0; - discretePulseSpot = ((Grid2D) getGrid()).gridRadialDistance(radius, coordFactor); - } + /** + * Calls the superclass method, then calculates the + * {@code discretePulseSpot} using the {@code gridRadialDistance} method. + * + * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) + */ + @Override + public void recalculate() { + super.recalculate(); + final double radius = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue() / 2.0; + discretePulseSpot = ((Grid2D) getGrid()).gridRadialDistance(radius, coordFactor); + } - public double getDiscretePulseSpot() { - return discretePulseSpot; - } + public double getDiscretePulseSpot() { + return discretePulseSpot; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index 6e625322..21c39744 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -11,171 +11,163 @@ import static pulse.properties.NumericPropertyKeyword.SKEW_SIGMA; import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import pulse.properties.Property; /** * Represents the exponentially modified Gaussian function, which is given by * three independent parameters (μ, σ and λ). - * + * * @see Wikipedia page * */ - public class ExponentiallyModifiedGaussian extends PulseTemporalShape { - private double mu; - private double sigma; - private double lambda; - private double norm; - - /** - * Creates an exponentially modified Gaussian with the default parameter values. - */ - - public ExponentiallyModifiedGaussian() { - mu = (double) def(SKEW_MU).getValue(); - lambda = (double) def(SKEW_LAMBDA).getValue(); - sigma = (double) def(SKEW_SIGMA).getValue(); - norm = 1.0; - } - - public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { - super(another); - this.mu = another.mu; - this.sigma = another.sigma; - this.lambda = another.lambda; - this.norm = another.norm; - } - - /** - * This calls the superclass {@code init method} and sets the normalisation - * factor to 1/∫Φ(Fo)dFo. - */ - - @Override - public void init(ExperimentalData data, DiscretePulse pulse) { - super.init(data, pulse); - norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of - // the area. - } - - /** - * Evaluates the laser power function. The error function is calculated using - * the ApacheCommonsMath library tools. - * - * @see https://tinyurl.com/ExpModifiedGaussian - * @param time is measured from the 'start' of laser pulse - */ - - @Override - public double evaluateAt(double time) { - final var reducedTime = time / getPulseWidth(); - - final double lambdaHalf = 0.5 * lambda; - final double sigmaSq = sigma * sigma; - - return norm * lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) - * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); - - } - - /** - * @see pulse.properties.NumericPropertyKeyword.SKEW_MU - * @see pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA - * @see pulse.properties.NumericPropertyKeyword.SKEW_SIGMA - */ - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(SKEW_MU)); - list.add(def(SKEW_LAMBDA)); - list.add(def(SKEW_SIGMA)); - return list; - } - - /** - * @return the μ parameter - */ - - public NumericProperty getMu() { - return derive(SKEW_MU, mu); - } - - /** - * @return the σ parameter - */ - - public NumericProperty getSigma() { - return derive(SKEW_SIGMA, sigma); - } - - /** - * @return the λ parameter - */ - - public NumericProperty getLambda() { - return derive(SKEW_LAMBDA, lambda); - } - - /** - * Sets the {@code SKEW_LAMBDA} parameter - * - * @param p the λ parameter - */ - - public void setLambda(NumericProperty p) { - requireType(p, SKEW_LAMBDA); - this.lambda = (double) p.getValue(); - } - - /** - * Sets the {@code SKEW_MU} parameter - * - * @param p the μ parameter - */ - - public void setMu(NumericProperty p) { - requireType(p, SKEW_MU); - this.mu = (double) p.getValue(); - } - - /** - * Sets the {@code SKEW_SIGMA} parameter - * - * @param p the σ parameter - */ - - public void setSigma(NumericProperty p) { - requireType(p, SKEW_SIGMA); - this.sigma = (double) p.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case SKEW_MU: - setMu(property); - break; - case SKEW_LAMBDA: - setLambda(property); - break; - case SKEW_SIGMA: - setSigma(property); - break; - default: - break; - } - firePropertyChanged(this, property); - } - - @Override - public PulseTemporalShape copy() { - return new ExponentiallyModifiedGaussian(this); - } - -} \ No newline at end of file + private double mu; + private double sigma; + private double lambda; + private double norm; + + /** + * Creates an exponentially modified Gaussian with the default parameter + * values. + */ + public ExponentiallyModifiedGaussian() { + mu = (double) def(SKEW_MU).getValue(); + lambda = (double) def(SKEW_LAMBDA).getValue(); + sigma = (double) def(SKEW_SIGMA).getValue(); + norm = 1.0; + } + + public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { + super(another); + this.mu = another.mu; + this.sigma = another.sigma; + this.lambda = another.lambda; + this.norm = another.norm; + } + + /** + * This calls the superclass {@code init method} and sets the normalisation + * factor to 1/∫Φ(Fo)dFo. + */ + @Override + public void init(ExperimentalData data, DiscretePulse pulse) { + super.init(data, pulse); + norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of + // the area. + } + + /** + * Evaluates the laser power function. The error function is calculated + * using the ApacheCommonsMath library tools. + * + * @see https://tinyurl.com/ExpModifiedGaussian + * @param time is measured from the 'start' of laser pulse + */ + @Override + public double evaluateAt(double time) { + final var reducedTime = time / getPulseWidth(); + + final double lambdaHalf = 0.5 * lambda; + final double sigmaSq = sigma * sigma; + + return norm * lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) + * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); + + } + + /** + * @see pulse.properties.NumericPropertyKeyword.SKEW_MU + * @see pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA + * @see pulse.properties.NumericPropertyKeyword.SKEW_SIGMA + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SKEW_MU); + set.add(SKEW_LAMBDA); + set.add(SKEW_SIGMA); + return set; + } + + /** + * @return the μ parameter + */ + public NumericProperty getMu() { + return derive(SKEW_MU, mu); + } + + /** + * @return the σ parameter + */ + public NumericProperty getSigma() { + return derive(SKEW_SIGMA, sigma); + } + + /** + * @return the λ parameter + */ + public NumericProperty getLambda() { + return derive(SKEW_LAMBDA, lambda); + } + + /** + * Sets the {@code SKEW_LAMBDA} parameter + * + * @param p the λ parameter + */ + public void setLambda(NumericProperty p) { + requireType(p, SKEW_LAMBDA); + this.lambda = (double) p.getValue(); + } + + /** + * Sets the {@code SKEW_MU} parameter + * + * @param p the μ parameter + */ + public void setMu(NumericProperty p) { + requireType(p, SKEW_MU); + this.mu = (double) p.getValue(); + } + + /** + * Sets the {@code SKEW_SIGMA} parameter + * + * @param p the σ parameter + */ + public void setSigma(NumericProperty p) { + requireType(p, SKEW_SIGMA); + this.sigma = (double) p.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case SKEW_MU: + setMu(property); + break; + case SKEW_LAMBDA: + setLambda(property); + break; + case SKEW_SIGMA: + setSigma(property); + break; + default: + break; + } + firePropertyChanged(this, property); + } + + @Override + public PulseTemporalShape copy() { + return new ExponentiallyModifiedGaussian(this); + } + +} diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index d6d2d51f..0a0493ef 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -3,68 +3,67 @@ import pulse.AbstractData; /** - * An instance of the {@code AbstractData} class, which also declares an {@code externalID}. - * Use to store numeric data of the pulse for each measurement imported from an external source. + * An instance of the {@code AbstractData} class, which also declares an + * {@code externalID}. Use to store numeric data of the pulse for each + * measurement imported from an external source. * */ - public class NumericPulseData extends AbstractData { - private int externalID; - - /** - * Stores {@code id} and calls super-constructor - * @param id an external ID defined in the imported file - */ - - public NumericPulseData(int id) { - super(); - this.externalID = id; - } - - /** - * Copies everything, including the id number. - * @param data another object - */ - - public NumericPulseData(NumericPulseData data) { - super(data); - this.externalID = data.externalID; - } - - /** - * Adds a data point to the internal storage and increments counter. - */ - - @Override - public void addPoint(double time, double power) { - super.addPoint(time, power); - super.incrementCount(); - } - - /** - * Gets the external ID usually specified in the experimental files. Note this - * is not a {@code NumericProperty} - * - * @return an integer, representing the external ID - */ + private int externalID; + + /** + * Stores {@code id} and calls super-constructor + * + * @param id an external ID defined in the imported file + */ + public NumericPulseData(int id) { + super(); + this.externalID = id; + } + + /** + * Copies everything, including the id number. + * + * @param data another object + */ + public NumericPulseData(NumericPulseData data) { + super(data); + this.externalID = data.externalID; + } + + /** + * Adds a data point to the internal storage and increments counter. + */ + @Override + public void addPoint(double time, double power) { + super.addPoint(time, power); + super.incrementCount(); + } + + /** + * Gets the external ID usually specified in the experimental files. Note + * this is not a {@code NumericProperty} + * + * @return an integer, representing the external ID + */ + public int getExternalID() { + return externalID; + } + + /** + * Uniformly scales the values of the pulse power by {@code factor}. + * + * @param factor the scaling factor + */ + public void scale(double factor) { + + var power = this.getSignalData(); + + for (int i = 0, size = power.size(); i < size; i++) { + power.set(i, power.get(i) * factor); + } - public int getExternalID() { - return externalID; - } - - /** - * Uniformly scales the values of the pulse power by {@code factor}. - * @param factor the scaling factor - */ - - public void scale(double factor) { - - var power = this.getSignalData(); - - for(int i = 0, size = power.size(); i < size; i++) - power.set(i, power.get(i) * factor ); - - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index fa927f8e..57019513 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -13,97 +13,96 @@ /** * An abstract time-dependent pulse shape. Declares the abstract method to * calculate the laser power function at a given moment of time. This generally - * utilises a discrete pulse width. By default, uses a midpoint-rule numeric integrator - * to calculate the pulse integral. + * utilises a discrete pulse width. By default, uses a midpoint-rule numeric + * integrator to calculate the pulse integral. * */ - public abstract class PulseTemporalShape extends PropertyHolder implements Reflexive { - private double width; - - private final static int DEFAULT_POINTS = 256; - private FixedIntervalIntegrator integrator; - - public PulseTemporalShape() { - //intentionlly blank - } - - public PulseTemporalShape(PulseTemporalShape another) { - this.integrator = another.integrator; - } - - /** - * Creates a new midpoint-integrator using the number of segments equal to {@value DEFAULT_POINTS}. - * The integrand function is specified by the {@code evaluateAt} method of this class. - * @see pulse.math.MidpointIntegrator - * @see evaluateAt() - */ - - public void initAreaIntegrator() { - integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), - derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { - - @Override - public double integrand(double... vars) { - return evaluateAt(vars[0]); - } - - }; - } - - /** - * Uses numeric integration (midpoint rule) to calculate the area of the pulse - * shape corresponding to the selected parameters. The integration bounds are non-negative. - * - * @return the area - */ - - public double area() { - integrator.setBounds(new Segment(0.0, getPulseWidth())); - return integrator.integrate(); - } - - - /** - * This evaluates the dimensionless, discretised pulse function on a - * {@code grid} needed to evaluate the heat source in the difference scheme. - * - * @param time the dimensionless time (a multiplier of {@code tau}), at which - * calculation should be performed - * @return a double value, representing the pulse function at {@code time} - */ - - public abstract double evaluateAt(double time); - - /** - * Stores the pulse width from {@code pulse} and initialises area integration. - * @param pulse the discrete pulse containing the pulse width - */ - - public void init(ExperimentalData data, DiscretePulse pulse) { - width = pulse.getDiscreteWidth(); - this.initAreaIntegrator(); - } - - public abstract PulseTemporalShape copy(); - - @Override - public String getPrefix() { - return "Pulse temporal shape"; - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - public double getPulseWidth() { - return width; - } - - public void setPulseWidth(double width) { - this.width = width; - } - -} \ No newline at end of file + private double width; + + private final static int DEFAULT_POINTS = 256; + private FixedIntervalIntegrator integrator; + + public PulseTemporalShape() { + //intentionlly blank + } + + public PulseTemporalShape(PulseTemporalShape another) { + this.integrator = another.integrator; + } + + /** + * Creates a new midpoint-integrator using the number of segments equal to + * {@value DEFAULT_POINTS}. The integrand function is specified by the + * {@code evaluateAt} method of this class. + * + * @see pulse.math.MidpointIntegrator + * @see evaluateAt() + */ + public void initAreaIntegrator() { + integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), + derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { + + @Override + public double integrand(double... vars) { + return evaluateAt(vars[0]); + } + + }; + } + + /** + * Uses numeric integration (midpoint rule) to calculate the area of the + * pulse shape corresponding to the selected parameters. The integration + * bounds are non-negative. + * + * @return the area + */ + public double area() { + integrator.setBounds(new Segment(0.0, getPulseWidth())); + return integrator.integrate(); + } + + /** + * This evaluates the dimensionless, discretised pulse function on a + * {@code grid} needed to evaluate the heat source in the difference scheme. + * + * @param time the dimensionless time (a multiplier of {@code tau}), at + * which calculation should be performed + * @return a double value, representing the pulse function at {@code time} + */ + public abstract double evaluateAt(double time); + + /** + * Stores the pulse width from {@code pulse} and initialises area + * integration. + * + * @param pulse the discrete pulse containing the pulse width + */ + public void init(ExperimentalData data, DiscretePulse pulse) { + width = pulse.getDiscreteWidth(); + this.initAreaIntegrator(); + } + + public abstract PulseTemporalShape copy(); + + @Override + public String getPrefix() { + return "Pulse temporal shape"; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public double getPulseWidth() { + return width; + } + + public void setPulseWidth(double width) { + this.width = width; + } + +} diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index eec6885f..39369a21 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -9,30 +9,28 @@ * The simplest pulse shape defined as 0.5*(1 + * sgn(tpulse - t)), where sgn(...) * is the signum function, pulse is the pulse width. - * + * * @see java.lang.Math.signum(double) */ - public class RectangularPulse extends PulseTemporalShape { - /** - * @param time the time measured from the start of the laser pulse. - */ - - @Override - public double evaluateAt(double time) { - var width = getPulseWidth(); - return 0.5 / width * (1 + signum(width - time)); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally blak - } - - @Override - public PulseTemporalShape copy() { - return new RectangularPulse(); - } - -} \ No newline at end of file + /** + * @param time the time measured from the start of the laser pulse. + */ + @Override + public double evaluateAt(double time) { + var width = getPulseWidth(); + return 0.5 / width * (1 + signum(width - time)); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally blak + } + + @Override + public PulseTemporalShape copy() { + return new RectangularPulse(); + } + +} diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index 4da228bf..82bb8542 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -7,10 +7,12 @@ import static pulse.properties.NumericPropertyKeyword.TRAPEZOIDAL_RISE_PERCENTAGE; import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import pulse.properties.Property; /** @@ -18,126 +20,121 @@ * segment, and a fall segment. The rise and fall ratios can be changed. * */ - public class TrapezoidalPulse extends PulseTemporalShape { - private double rise; - private double fall; - private double h; - - /** - * Constructs a trapezoidal pulse using a default segmentation principle. The - * reader is referred to the {@code .xml} file containing the default values of - * {@code TRAPEZOIDAL_RISE_PERCENTAGE} and {@code TRAPEZOIDAL_FALL_PERCENTAGE}. - * The maximum laser power is adjusted to ensure the area of the shape is equal - * to unity. - */ - - public TrapezoidalPulse() { - rise = (int) def(TRAPEZOIDAL_RISE_PERCENTAGE).getValue() / 100.0; - fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; - h = height(); - } - - public TrapezoidalPulse(TrapezoidalPulse another) { - this.rise = another.rise; - this.fall = another.fall; - this.h = another.h; - } - - /** - * Calculates the height of the trapez after calling the super-class method. - */ - - @Override - public void init(ExperimentalData data, DiscretePulse pulse) { - super.init(data, pulse); - h = height(); - } - - /** - * Calculates the height of the trapezium which under current segmentation will - * yield an area of unity. - * - * @return the calculated height of the constant segmment - */ - - private double height() { - return 2.0 / (getPulseWidth() * (2.0 - rise - fall)); - } - - /** - * Calculates power using a piecewise function, which corresponds to either a - * linearly changing, a constant laser power or zero. - * - * @param time the time measured from the start of the laser pulse. - */ - - @Override - public double evaluateAt(double time) { - final var reducedTime = time / getPulseWidth(); - - double result = 0; - - if (reducedTime < rise) { // triangular - result = reducedTime * h / rise; - } else if (reducedTime < 1.0 - fall) { // rectangular - result = h; - } else if (reducedTime < 1.0) { // triangular - final var t2 = (reducedTime - (1.0 - fall)); - result = (fall - t2) * h / fall; - } - - return result; - - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(TRAPEZOIDAL_RISE_PERCENTAGE)); - list.add(def(TRAPEZOIDAL_FALL_PERCENTAGE)); - return list; - } - - public NumericProperty getTrapezoidalRise() { - return derive(TRAPEZOIDAL_RISE_PERCENTAGE, (int) (rise * 100)); - } - - public NumericProperty getTrapezoidalFall() { - return derive(TRAPEZOIDAL_FALL_PERCENTAGE, (int) (fall * 100)); - } - - public void setTrapezoidalRise(NumericProperty p) { - requireType(p, TRAPEZOIDAL_RISE_PERCENTAGE); - this.rise = (int) p.getValue() / 100.0; - h = height(); - } - - public void setTrapezoidalFall(NumericProperty p) { - requireType(p, TRAPEZOIDAL_FALL_PERCENTAGE); - this.fall = (int) p.getValue() / 100.0; - h = height(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case TRAPEZOIDAL_RISE_PERCENTAGE: - setTrapezoidalRise(property); - break; - case TRAPEZOIDAL_FALL_PERCENTAGE: - setTrapezoidalFall(property); - break; - default: - break; - } - firePropertyChanged(this, property); - } - - @Override - public PulseTemporalShape copy() { - return new TrapezoidalPulse(this); - } - -} \ No newline at end of file + private double rise; + private double fall; + private double h; + + /** + * Constructs a trapezoidal pulse using a default segmentation principle. + * The reader is referred to the {@code .xml} file containing the default + * values of {@code TRAPEZOIDAL_RISE_PERCENTAGE} and + * {@code TRAPEZOIDAL_FALL_PERCENTAGE}. The maximum laser power is adjusted + * to ensure the area of the shape is equal to unity. + */ + public TrapezoidalPulse() { + rise = (int) def(TRAPEZOIDAL_RISE_PERCENTAGE).getValue() / 100.0; + fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; + h = height(); + } + + public TrapezoidalPulse(TrapezoidalPulse another) { + this.rise = another.rise; + this.fall = another.fall; + this.h = another.h; + } + + /** + * Calculates the height of the trapez after calling the super-class method. + */ + @Override + public void init(ExperimentalData data, DiscretePulse pulse) { + super.init(data, pulse); + h = height(); + } + + /** + * Calculates the height of the trapezium which under current segmentation + * will yield an area of unity. + * + * @return the calculated height of the constant segmment + */ + private double height() { + return 2.0 / (getPulseWidth() * (2.0 - rise - fall)); + } + + /** + * Calculates power using a piecewise function, which corresponds to either + * a linearly changing, a constant laser power or zero. + * + * @param time the time measured from the start of the laser pulse. + */ + @Override + public double evaluateAt(double time) { + final var reducedTime = time / getPulseWidth(); + + double result = 0; + + if (reducedTime < rise) { // triangular + result = reducedTime * h / rise; + } else if (reducedTime < 1.0 - fall) { // rectangular + result = h; + } else if (reducedTime < 1.0) { // triangular + final var t2 = (reducedTime - (1.0 - fall)); + result = (fall - t2) * h / fall; + } + + return result; + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TRAPEZOIDAL_RISE_PERCENTAGE); + set.add(TRAPEZOIDAL_FALL_PERCENTAGE); + return set; + } + + public NumericProperty getTrapezoidalRise() { + return derive(TRAPEZOIDAL_RISE_PERCENTAGE, (int) (rise * 100)); + } + + public NumericProperty getTrapezoidalFall() { + return derive(TRAPEZOIDAL_FALL_PERCENTAGE, (int) (fall * 100)); + } + + public void setTrapezoidalRise(NumericProperty p) { + requireType(p, TRAPEZOIDAL_RISE_PERCENTAGE); + this.rise = (int) p.getValue() / 100.0; + h = height(); + } + + public void setTrapezoidalFall(NumericProperty p) { + requireType(p, TRAPEZOIDAL_FALL_PERCENTAGE); + this.fall = (int) p.getValue() / 100.0; + h = height(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case TRAPEZOIDAL_RISE_PERCENTAGE: + setTrapezoidalRise(property); + break; + case TRAPEZOIDAL_FALL_PERCENTAGE: + setTrapezoidalFall(property); + break; + default: + break; + } + firePropertyChanged(this, property); + } + + @Override + public PulseTemporalShape copy() { + return new TrapezoidalPulse(this); + } + +} diff --git a/src/main/java/pulse/problem/laser/package-info.java b/src/main/java/pulse/problem/laser/package-info.java index 21573e60..3e4cf687 100644 --- a/src/main/java/pulse/problem/laser/package-info.java +++ b/src/main/java/pulse/problem/laser/package-info.java @@ -1,5 +1,4 @@ /** * This package deals with discrete laser pulse representation and their various temporal shapes. */ - -package pulse.problem.laser; \ No newline at end of file +package pulse.problem.laser; diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index 75e989c1..bad5501c 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -12,54 +12,49 @@ * needed to solve a {@code Problem}. * */ - public abstract class ADIScheme extends DifferenceScheme { - /** - * Creates a new {@code ADIScheme} with default values of grid density and time - * factor. - */ - - public ADIScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); - } - - /** - * Creates an {@code ADIScheme} with the specified arguments. This creates an - * associated {@code Grid2D} object. - * - * @param N the grid density - * @param timeFactor the time factor (τF) - */ - - public ADIScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid2D(N, timeFactor)); - } - - /** - * Creates an {@code ADIScheme} with the specified arguments. This creates an - * associated {@code Grid2D} object. - * - * @param N the grid density - * @param timeFactor the time factor (τF) - * @param timeLimit a custom time limit (tlim) - */ - - public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - setTimeLimit(timeLimit); - setGrid(new Grid2D(N, timeFactor)); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ADIScheme.4"); - } - -} \ No newline at end of file + /** + * Creates a new {@code ADIScheme} with default values of grid density and + * time factor. + */ + public ADIScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); + } + + /** + * Creates an {@code ADIScheme} with the specified arguments. This creates + * an associated {@code Grid2D} object. + * + * @param N the grid density + * @param timeFactor the time factor (τF) + */ + public ADIScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid2D(N, timeFactor)); + } + + /** + * Creates an {@code ADIScheme} with the specified arguments. This creates + * an associated {@code Grid2D} object. + * + * @param N the grid density + * @param timeFactor the time factor (τF) + * @param timeLimit a custom time limit (tlim) + */ + public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + setTimeLimit(timeLimit); + setGrid(new Grid2D(N, timeFactor)); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ADIScheme.4"); + } + +} diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index 037483bc..0c108abc 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -1,79 +1,81 @@ package pulse.problem.schemes; /** - * A modification of the algorithm for solving a system of linear equations, where the first and last equation contains references - * to the last and first elements of the solution, respectively. The corresponding matrix is composed of an inner tridiagonal block - * and a border formed by an extra row and column. This block system is solved using the Sherman-Morrison-Woodbury identity and the - * Thomas algorithm for the main block. + * A modification of the algorithm for solving a system of linear equations, + * where the first and last equation contains references to the last and first + * elements of the solution, respectively. The corresponding matrix is composed + * of an inner tridiagonal block and a border formed by an extra row and column. + * This block system is solved using the Sherman-Morrison-Woodbury identity and + * the Thomas algorithm for the main block. * */ - public class BlockMatrixAlgorithm extends TridiagonalMatrixAlgorithm { - private double[] gamma; - private double[] p; - private double[] q; - - public BlockMatrixAlgorithm(Grid grid) { - super(grid); - gamma = new double[getAlpha().length]; - p = new double[gamma.length - 1]; - q = new double[gamma.length - 1]; - } - - @Override - public void sweep(double[] V) { - final int N = V.length - 1; - for (int j = N - 1; j >= 0; j--) - V[j] = p[j] + V[N] * q[j]; - } - - @Override - public void evaluateBeta(final double[] U) { - super.evaluateBeta(U); - final int N = getGrid().getGridDensityValue(); - var alpha = getAlpha(); - var beta = getBeta(); - - p[N - 1] = beta[N]; - q[N - 1] = alpha[N] + gamma[N]; - - for (int i = N - 2; i >= 0; i--) { - p[i] = alpha[i + 1] * p[i + 1] + beta[i + 1]; - q[i] = alpha[i + 1] * q[i + 1] + gamma[i + 1]; - } - } - - @Override - public void evaluateBeta(final double[] U, final int start, final int endExclusive) { - var alpha = getAlpha(); - var grid = getGrid(); - final double HX2_TAU = grid.getXStep() * grid.getXStep() / getGrid().getTimeStep(); - - final double a = getCoefA(); - final double b = getCoefB(); - - for (int i = start; i < endExclusive; i++) { - setBeta(i, beta(U[i - 1] * HX2_TAU, phi(i - 1), i)); - setGamma(i, a * gamma[i - 1] / (b - a * alpha[i - 1])); - } - - } - - public double[] getP() { - return p; - } - - public double[] getQ() { - return q; - } - - public void setGamma(final int i, final double g) { - this.gamma[i] = g; - } - - public double[] getGamma() { - return gamma; - } + private double[] gamma; + private double[] p; + private double[] q; + + public BlockMatrixAlgorithm(Grid grid) { + super(grid); + gamma = new double[getAlpha().length]; + p = new double[gamma.length - 1]; + q = new double[gamma.length - 1]; + } + + @Override + public void sweep(double[] V) { + final int N = V.length - 1; + for (int j = N - 1; j >= 0; j--) { + V[j] = p[j] + V[N] * q[j]; + } + } + + @Override + public void evaluateBeta(final double[] U) { + super.evaluateBeta(U); + final int N = getGrid().getGridDensityValue(); + var alpha = getAlpha(); + var beta = getBeta(); + + p[N - 1] = beta[N]; + q[N - 1] = alpha[N] + gamma[N]; + + for (int i = N - 2; i >= 0; i--) { + p[i] = alpha[i + 1] * p[i + 1] + beta[i + 1]; + q[i] = alpha[i + 1] * q[i + 1] + gamma[i + 1]; + } + } + + @Override + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + var alpha = getAlpha(); + var grid = getGrid(); + final double HX2_TAU = grid.getXStep() * grid.getXStep() / getGrid().getTimeStep(); + + final double a = getCoefA(); + final double b = getCoefB(); + + for (int i = start; i < endExclusive; i++) { + setBeta(i, beta(U[i - 1] * HX2_TAU, phi(i - 1), i)); + setGamma(i, a * gamma[i - 1] / (b - a * alpha[i - 1])); + } + + } + + public double[] getP() { + return p; + } + + public double[] getQ() { + return q; + } + + public void setGamma(final int i, final double g) { + this.gamma[i] = g; + } + + public double[] getGamma() { + return gamma; + } } diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index 010e2872..2ef708df 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -6,33 +6,33 @@ import pulse.problem.statements.model.SpectralRange; /** - * An interface providing the ability to calculate the integral signal - * out from a finite-depth material layer. The depth is governed by - * the current {@code AbsorptionModel}. + * An interface providing the ability to calculate the integral signal out from + * a finite-depth material layer. The depth is governed by the current + * {@code AbsorptionModel}. * */ - public class DistributedDetection { - /** - * Calculates the effective signal registered by the detector, which takes into account - * a distributed emission pattern. The emissivity is assumed equal to the average absorptivity - * in the thermal region of the spectrum, as per the Kirchhoff's law. - * @param absorption the absorption model - * @param V the current time-temperature profile - * @return the effective detector signal (arbitrary units) - */ - - public static double evaluateSignal(final AbsorptionModel absorption, final Grid grid, final double[] V) { - final double hx = grid.getXStep(); - final int N = grid.getGridDensityValue(); - - double signal = IntStream.range(0, N) - .mapToDouble(i -> V[N - i] * absorption.absorption(SpectralRange.THERMAL, i * hx) - + V[N - 1 - i] * absorption.absorption(SpectralRange.THERMAL, (i + 1) * hx)) - .reduce((a, b) -> a + b).getAsDouble(); - - return signal * 0.5 * hx; - } - -} \ No newline at end of file + /** + * Calculates the effective signal registered by the detector, which takes + * into account a distributed emission pattern. The emissivity is assumed + * equal to the average absorptivity in the thermal region of the spectrum, + * as per the Kirchhoff's law. + * + * @param absorption the absorption model + * @param V the current time-temperature profile + * @return the effective detector signal (arbitrary units) + */ + public static double evaluateSignal(final AbsorptionModel absorption, final Grid grid, final double[] V) { + final double hx = grid.getXStep(); + final int N = grid.getGridDensityValue(); + + double signal = IntStream.range(0, N) + .mapToDouble(i -> V[N - i] * absorption.absorption(SpectralRange.THERMAL, i * hx) + + V[N - 1 - i] * absorption.absorption(SpectralRange.THERMAL, (i + 1) * hx)) + .reduce((a, b) -> a + b).getAsDouble(); + + return signal * 0.5 * hx; + } + +} diff --git a/src/main/java/pulse/problem/schemes/ExplicitScheme.java b/src/main/java/pulse/problem/schemes/ExplicitScheme.java index e5fb12a4..70a47ba3 100644 --- a/src/main/java/pulse/problem/schemes/ExplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ExplicitScheme.java @@ -12,85 +12,80 @@ * This class provides the necessary framework to enable a simple explicit * finite-difference scheme (also called the forward-time centred space scheme) * for solving the one-dimensional heat conduction problem. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem * */ - public abstract class ExplicitScheme extends OneDimensionalScheme { - /** - * Constructs a default explicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public ExplicitScheme() { - this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); - } - - /** - * Constructs an explicit scheme on a one-dimensional grid that is specified by - * the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ + /** + * Constructs a default explicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public ExplicitScheme() { + this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); + } - public ExplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - } + /** + * Constructs an explicit scheme on a one-dimensional grid that is specified + * by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public ExplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + } - /** - *

- * Constructs an explicit scheme on a one-dimensional grid that is specified by - * the values {@code N} and {@code timeFactor}. Sets the time limit of this - * scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ + /** + *

+ * Constructs an explicit scheme on a one-dimensional grid that is specified + * by the values {@code N} and {@code timeFactor}. Sets the time limit of + * this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public ExplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(timeLimit); + setGrid(new Grid(N, timeFactor)); + } - public ExplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(timeLimit); - setGrid(new Grid(N, timeFactor)); - } - - /** - * Uses the explicit finite-difference representation of the heat equation to calculate the grid-function everywhere - * except for the boundaries. This will update the current solution using the solution from previous time step. - */ - - public void explicitSolution() { - var grid = getGrid(); - var U = getPreviousSolution(); - final double TAU_HH = grid.getTimeStep()/(fastPowLoop(grid.getXStep(), 2)); - for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) - setSolutionAt(i, U[i] + TAU_HH * (U[i + 1] - 2. * U[i] + U[i - 1]) + phi(i) ); - } - - public double phi(final int i) { - return 0; - } + /** + * Uses the explicit finite-difference representation of the heat equation + * to calculate the grid-function everywhere except for the boundaries. This + * will update the current solution using the solution from previous time + * step. + */ + public void explicitSolution() { + var grid = getGrid(); + var U = getPreviousSolution(); + final double TAU_HH = grid.getTimeStep() / (fastPowLoop(grid.getXStep(), 2)); + for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { + setSolutionAt(i, U[i] + TAU_HH * (U[i + 1] - 2. * U[i] + U[i - 1]) + phi(i)); + } + } - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ + public double phi(final int i) { + return 0; + } - @Override - public String toString() { - return getString("ExplicitScheme.4"); - } + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ExplicitScheme.4"); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index d98a3bb3..221fe505 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -3,51 +3,52 @@ import static java.lang.Math.abs; /** - * @see Wiki page + * @see Wiki + * page * */ - public interface FixedPointIterations { - /** - * Performs iterations until the convergence criterion is satisfied. - * The latter consists in having a difference two consequent iterations of V - * less than the specified error. At the end of each iteration, calls {@code finaliseIteration()}. - * @param V the calculation array - * @param error used in the convergence criterion - * @param m time step - * @see finaliseIteration() - * @see iteration() - */ - - public default void doIterations(double[] V, final double error, final int m) { - - final int N = V.length - 1; - - for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error - || abs(V[N] - V_N) > error; finaliseIteration(V)) { - - V_N = V[N]; - V_0 = V[0]; - iteration(m); - - } - } - - /** - * Performs an iteration at time {@code m} - * @param m time step - */ - - public void iteration(final int m); - - /** - * Finalises the current iteration. By default, does nothing. - * @param V the current iteration - */ - - public default void finaliseIteration(double[] V) { - // do nothing - } - -} \ No newline at end of file + /** + * Performs iterations until the convergence criterion is satisfied. The + * latter consists in having a difference two consequent iterations of V + * less than the specified error. At the end of each iteration, calls + * {@code finaliseIteration()}. + * + * @param V the calculation array + * @param error used in the convergence criterion + * @param m time step + * @see finaliseIteration() + * @see iteration() + */ + public default void doIterations(double[] V, final double error, final int m) { + + final int N = V.length - 1; + + for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error + || abs(V[N] - V_N) > error; finaliseIteration(V)) { + + V_N = V[N]; + V_0 = V[0]; + iteration(m); + + } + } + + /** + * Performs an iteration at time {@code m} + * + * @param m time step + */ + public void iteration(final int m); + + /** + * Finalises the current iteration. By default, does nothing. + * + * @param V the current iteration + */ + public default void finaliseIteration(double[] V) { + // do nothing + } + +} diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index 0f1fc76c..e70710aa 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -17,101 +17,97 @@ * dimensions for interpreting the laser flash experiments. *

*/ - public class Grid2D extends Grid { - private double hy; - - protected Grid2D() { - super(); - } - - /** - * Creates a {@code Grid2D} where the radial and axial spatial steps are equal - * to the inverse {@code gridDensity}. Otherwise, calls the superclass - * constructor. - * - * @param gridDensity the grid density - * @param timeFactor the {@code τF} factor - */ - - public Grid2D(NumericProperty gridDensity, NumericProperty timeFactor) { - super(gridDensity, timeFactor); - hy = 1.0 / getGridDensityValue(); - } - - @Override - public Grid2D copy() { - return new Grid2D(getGridDensity(), getTimeFactor()); - } - - @Override - public void setTimeFactor(NumericProperty timeFactor) { - super.setTimeFactor(timeFactor); - setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); - } - - /** - * Calls the {@code adjustTo} method from superclass, then adjusts the - * {@code gridDensity} of the {@code grid} if - * {@code discretePulseSpot < (Grid2D)grid.hy}. - * - * @param pulse the discrete puls representation - */ - - @Override - public void adjustTo(DiscretePulse pulse) { - super.adjustTo(pulse); - if (pulse instanceof DiscretePulse2D) - adjustTo((DiscretePulse2D) pulse); - } - - private void adjustTo(DiscretePulse2D pulse) { - final int GRID_DENSITY_INCREMENT = 5; - - for (final var factor = 1.05; factor * hy > pulse.getDiscretePulseSpot(); pulse.recalculate()) { - int N = getGridDensityValue(); - setGridDensityValue(N + GRID_DENSITY_INCREMENT); - hy = 1. / N; - setXStep(1. / N); - } - - } - - /** - * Sets the value of the {@code gridDensity}. Automatically recalculates the - * {@code hx} an {@code hy} values. - */ - - @Override - public void setGridDensity(NumericProperty gridDensity) { - super.setGridDensity(gridDensity); - hy = getXStep(); - } - - /** - * The dimensionless radial distance on this {@code Grid2D}, which is the - * {@code radial/lengthFactor} rounded up to a factor of the coordinate step - * {@code hy}. - * - * @param radial the distance along the radial direction - * @param lengthFactor a factor which has the dimension of length - * @return a double representing the radial distance on the finite grid - */ - - public double gridRadialDistance(double radial, double lengthFactor) { - return rint((radial / lengthFactor) / hy) * hy; - } - - @Override - public String toString() { - var sb = new StringBuilder(super.toString()); - sb.append("hy=" + format("%3.3f", hy)); - return sb.toString(); - } - - public double getYStep() { - return hy; - } - -} \ No newline at end of file + private double hy; + + protected Grid2D() { + super(); + } + + /** + * Creates a {@code Grid2D} where the radial and axial spatial steps are + * equal to the inverse {@code gridDensity}. Otherwise, calls the superclass + * constructor. + * + * @param gridDensity the grid density + * @param timeFactor the {@code τF} factor + */ + public Grid2D(NumericProperty gridDensity, NumericProperty timeFactor) { + super(gridDensity, timeFactor); + hy = 1.0 / getGridDensityValue(); + } + + @Override + public Grid2D copy() { + return new Grid2D(getGridDensity(), getTimeFactor()); + } + + @Override + public void setTimeFactor(NumericProperty timeFactor) { + super.setTimeFactor(timeFactor); + setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); + } + + /** + * Calls the {@code adjustTo} method from superclass, then adjusts the + * {@code gridDensity} of the {@code grid} if + * {@code discretePulseSpot < (Grid2D)grid.hy}. + * + * @param pulse the discrete puls representation + */ + @Override + public void adjustTo(DiscretePulse pulse) { + super.adjustTo(pulse); + if (pulse instanceof DiscretePulse2D) { + adjustTo((DiscretePulse2D) pulse); + } + } + + private void adjustTo(DiscretePulse2D pulse) { + final int GRID_DENSITY_INCREMENT = 5; + + for (final var factor = 1.05; factor * hy > pulse.getDiscretePulseSpot(); pulse.recalculate()) { + int N = getGridDensityValue(); + setGridDensityValue(N + GRID_DENSITY_INCREMENT); + hy = 1. / N; + setXStep(1. / N); + } + + } + + /** + * Sets the value of the {@code gridDensity}. Automatically recalculates the + * {@code hx} an {@code hy} values. + */ + @Override + public void setGridDensity(NumericProperty gridDensity) { + super.setGridDensity(gridDensity); + hy = getXStep(); + } + + /** + * The dimensionless radial distance on this {@code Grid2D}, which is the + * {@code radial/lengthFactor} rounded up to a factor of the coordinate step + * {@code hy}. + * + * @param radial the distance along the radial direction + * @param lengthFactor a factor which has the dimension of length + * @return a double representing the radial distance on the finite grid + */ + public double gridRadialDistance(double radial, double lengthFactor) { + return rint((radial / lengthFactor) / hy) * hy; + } + + @Override + public String toString() { + var sb = new StringBuilder(super.toString()); + sb.append("hy=" + format("%3.3f", hy)); + return sb.toString(); + } + + public double getYStep() { + return hy; + } + +} diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index af2324c6..f7b73bbe 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -11,100 +11,94 @@ /** * An abstract implicit finite-difference scheme for solving one-dimensional * heat conduction problems. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem */ - public abstract class ImplicitScheme extends OneDimensionalScheme { - - private TridiagonalMatrixAlgorithm tridiagonal; - - /** - * Constructs a default fully-implicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public ImplicitScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.25)); - } - - /** - * Constructs a fully-implicit scheme on a one-dimensional grid that is - * specified by the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ - - public ImplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - } - - /** - *

- * Constructs a fully-implicit scheme on a one-dimensional grid that is - * specified by the values {@code N} and {@code timeFactor}. Sets the time limit - * of this scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ - - public ImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(timeLimit); - setGrid(new Grid(N, timeFactor)); - } - - @Override - protected void prepare(Problem problem) { - super.prepare(problem); - tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); - } - - @Override - public void timeStep(final int m) { - leftBoundary(m); - final var V = getCurrentSolution(); - final int N = V.length - 1; - setSolutionAt(N, evalRightBoundary(m, tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N]) ); - tridiagonal.sweep(V); - } - - public void leftBoundary(final int m) { - tridiagonal.setBeta( 1, firstBeta(m) ); - tridiagonal.evaluateBeta(getPreviousSolution()); - } - - public abstract double evalRightBoundary(final int m, final double alphaN, final double betaN); - public abstract double firstBeta(final int m); - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ImplicitScheme.4"); - } - - public TridiagonalMatrixAlgorithm getTridiagonalMatrixAlgorithm() { - return tridiagonal; - } - - public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal) { - this.tridiagonal = tridiagonal; - } - -} \ No newline at end of file + + private TridiagonalMatrixAlgorithm tridiagonal; + + /** + * Constructs a default fully-implicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public ImplicitScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.25)); + } + + /** + * Constructs a fully-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public ImplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + } + + /** + *

+ * Constructs a fully-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. Sets the time + * limit of this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public ImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(timeLimit); + setGrid(new Grid(N, timeFactor)); + } + + @Override + protected void prepare(Problem problem) { + super.prepare(problem); + tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); + } + + @Override + public void timeStep(final int m) { + leftBoundary(m); + final var V = getCurrentSolution(); + final int N = V.length - 1; + setSolutionAt(N, evalRightBoundary(m, tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); + tridiagonal.sweep(V); + } + + public void leftBoundary(final int m) { + tridiagonal.setBeta(1, firstBeta(m)); + tridiagonal.evaluateBeta(getPreviousSolution()); + } + + public abstract double evalRightBoundary(final int m, final double alphaN, final double betaN); + + public abstract double firstBeta(final int m); + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + public TridiagonalMatrixAlgorithm getTridiagonalMatrixAlgorithm() { + return tridiagonal; + } + + public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal) { + this.tridiagonal = tridiagonal; + } + +} diff --git a/src/main/java/pulse/problem/schemes/MixedScheme.java b/src/main/java/pulse/problem/schemes/MixedScheme.java index 0b1a591d..a02bfe3a 100644 --- a/src/main/java/pulse/problem/schemes/MixedScheme.java +++ b/src/main/java/pulse/problem/schemes/MixedScheme.java @@ -10,66 +10,59 @@ /** * An abstraction describing a weighted semi-implicit finite-difference scheme * for solving the one-dimensional heat conduction problem. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem * */ - public abstract class MixedScheme extends ImplicitScheme { - /** - * Constructs a default semi-implicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public MixedScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); - } - - /** - * Constructs a semi-implicit scheme on a one-dimensional grid that is specified - * by the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ - - public MixedScheme(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } + /** + * Constructs a default semi-implicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public MixedScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); + } - /** - *

- * Constructs a semi-implicit scheme on a one-dimensional grid that is specified - * by the values {@code N} and {@code timeFactor}. Sets the time limit of this - * scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ + /** + * Constructs a semi-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public MixedScheme(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } - public MixedScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ + /** + *

+ * Constructs a semi-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. Sets the time + * limit of this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public MixedScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - @Override - public String toString() { - return getString("MixedScheme.4"); - } + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("MixedScheme.4"); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index 12a7f756..d8992045 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -5,45 +5,45 @@ public abstract class OneDimensionalScheme extends DifferenceScheme { - private double[] U; - private double[] V; - - protected OneDimensionalScheme() { - super(); - } - - protected OneDimensionalScheme(NumericProperty timeLimit) { - super(timeLimit); - } - - @Override - protected void prepare(Problem problem) { - super.prepare(problem); - final int N = (int) getGrid().getGridDensity().getValue(); - U = new double[N + 1]; - V = new double[N + 1]; - } - - @Override - public double signal() { - return V[ V.length - 1 ]; - } - - @Override - public void finaliseStep() { - System.arraycopy(V, 0, U, 0, V.length); - } - - public double[] getPreviousSolution() { - return U; - } - - public double[] getCurrentSolution() { - return V; - } - - public void setSolutionAt(final int i, final double v) { - this.V[i] = v; - } - -} \ No newline at end of file + private double[] U; + private double[] V; + + protected OneDimensionalScheme() { + super(); + } + + protected OneDimensionalScheme(NumericProperty timeLimit) { + super(timeLimit); + } + + @Override + protected void prepare(Problem problem) { + super.prepare(problem); + final int N = (int) getGrid().getGridDensity().getValue(); + U = new double[N + 1]; + V = new double[N + 1]; + } + + @Override + public double signal() { + return V[V.length - 1]; + } + + @Override + public void finaliseStep() { + System.arraycopy(V, 0, U, 0, V.length); + } + + public double[] getPreviousSolution() { + return U; + } + + public double[] getCurrentSolution() { + return V; + } + + public void setSolutionAt(final int i, final double v) { + this.V[i] = v; + } + +} diff --git a/src/main/java/pulse/problem/schemes/Partition.java b/src/main/java/pulse/problem/schemes/Partition.java index 8fe29b6d..563fb9f8 100644 --- a/src/main/java/pulse/problem/schemes/Partition.java +++ b/src/main/java/pulse/problem/schemes/Partition.java @@ -4,63 +4,63 @@ public class Partition { - private int density; - private double multiplier; - private double shift; - - public Partition(int value, double multiplier, double shift) { - this.setDensity(value); - this.setShift(shift); - this.setGridMultiplier(multiplier); - } - - public double evaluate() { - return multiplier / (density * (1.0 + shift)); - } - - public int getDensity() { - return density; - } - - public void setDensity(int density) { - this.density = density; - } - - public double getGridMultiplier() { - return multiplier; - } - - public void setGridMultiplier(double multiplier) { - this.multiplier = multiplier; - } - - public double getShift() { - return shift; - } - - public void setShift(double shift) { - this.shift = shift; - } - - public enum Location { - - FRONT_Y, REAR_Y, SIDE_Y, SIDE_X, CORE_X, CORE_Y; - - public NumericPropertyKeyword densityKeyword() { - switch (this) { - case FRONT_Y: - case REAR_Y: - case SIDE_Y: - case SIDE_X: - return NumericPropertyKeyword.SHELL_GRID_DENSITY; - case CORE_X: - case CORE_Y: - return NumericPropertyKeyword.GRID_DENSITY; - default: - throw new IllegalArgumentException("Type not recognized: " + this); - } - } - - } - -} \ No newline at end of file + private int density; + private double multiplier; + private double shift; + + public Partition(int value, double multiplier, double shift) { + this.setDensity(value); + this.setShift(shift); + this.setGridMultiplier(multiplier); + } + + public double evaluate() { + return multiplier / (density * (1.0 + shift)); + } + + public int getDensity() { + return density; + } + + public void setDensity(int density) { + this.density = density; + } + + public double getGridMultiplier() { + return multiplier; + } + + public void setGridMultiplier(double multiplier) { + this.multiplier = multiplier; + } + + public double getShift() { + return shift; + } + + public void setShift(double shift) { + this.shift = shift; + } + + public enum Location { + + FRONT_Y, REAR_Y, SIDE_Y, SIDE_X, CORE_X, CORE_Y; + + public NumericPropertyKeyword densityKeyword() { + switch (this) { + case FRONT_Y: + case REAR_Y: + case SIDE_Y: + case SIDE_X: + return NumericPropertyKeyword.SHELL_GRID_DENSITY; + case CORE_X: + case CORE_Y: + return NumericPropertyKeyword.GRID_DENSITY; + default: + throw new IllegalArgumentException("Type not recognized: " + this); + } + } + + } + +} diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index 8208c939..e3e6a8a8 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -14,67 +14,69 @@ import pulse.util.PropertyHolder; public class RadiativeTransferCoupling extends PropertyHolder { - - private RadiativeTransferSolver rte; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "RTE Solver Selector", RadiativeTransferSolver.class); - - public RadiativeTransferCoupling() { - instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); - instanceDescriptor.addListener(() -> firePropertyChanged(this, instanceDescriptor)); - super.parameterListChanged(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - //intentionally blank - } - - public void init(ParticipatingMedium problem, Grid grid) { - - if (rte == null) { - newRTE(problem, grid); - instanceDescriptor.addListener(() -> { - newRTE(problem, grid); - rte.init(problem, grid); - }); - - } - else - rte.init(problem, grid); - - } - - private void newRTE(ParticipatingMedium problem, Grid grid) { - rte = instanceDescriptor.newInstance(RadiativeTransferSolver.class, problem, grid); - rte.setParent(this); - } - - public InstanceDescriptor getInstanceDescriptor() { - return instanceDescriptor; - } - - public RadiativeTransferSolver getRadiativeTransferEquation() { - return rte; - } - - public void setRadiativeTransferEquation(RadiativeTransferSolver solver) { - this.rte = solver; - } - - @Override - public String toString() { - return instanceDescriptor.toString(); - } - - @Override - public String getPrefix() { - return "RTE Coupling"; - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(instanceDescriptor)); - } - -} \ No newline at end of file + + private RadiativeTransferSolver rte; + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "RTE Solver Selector", RadiativeTransferSolver.class); + + public RadiativeTransferCoupling() { + instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); + instanceDescriptor.addListener(() -> firePropertyChanged(this, instanceDescriptor)); + super.parameterListChanged(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + //intentionally blank + } + + public void init(ParticipatingMedium problem, Grid grid) { + + if (rte == null) { + newRTE(problem, grid); + instanceDescriptor.addListener(() -> { + newRTE(problem, grid); + rte.init(problem, grid); + }); + + } else { + rte.init(problem, grid); + } + + } + + private void newRTE(ParticipatingMedium problem, Grid grid) { + rte = instanceDescriptor.newInstance(RadiativeTransferSolver.class, problem, grid); + rte.setParent(this); + } + + public InstanceDescriptor getInstanceDescriptor() { + return instanceDescriptor; + } + + public RadiativeTransferSolver getRadiativeTransferEquation() { + return rte; + } + + public void setRadiativeTransferEquation(RadiativeTransferSolver solver) { + this.rte = solver; + } + + @Override + public String toString() { + return instanceDescriptor.toString(); + } + + @Override + public String getPrefix() { + return "RTE Coupling"; + } + + @Override + public List listedTypes() { + var list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + +} diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 01c3c8cb..7426317d 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -1,116 +1,119 @@ package pulse.problem.schemes; /** - * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving systems of linear equations. - * Applicable to such systems where the forming matrix has a tridiagonal form. + * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving + * systems of linear equations. Applicable to such systems where the forming + * matrix has a tridiagonal form. * */ - public class TridiagonalMatrixAlgorithm { - private Grid grid; - - private double a; - private double b; - private double c; - - private double[] alpha; - private double[] beta; - - public TridiagonalMatrixAlgorithm(Grid grid) { - this.grid = grid; - final int N = grid.getGridDensityValue(); - alpha = new double[N + 2]; - beta = new double[N + 2]; - } - - /** - * Calculates the solution {@code V} using the tridiagonal matrix algorithm. - * This performs a backwards sweep from {@code N - 1} to {@code 0} where {@code N} - * is the grid density value. The coefficients {@code alpha} and {@code beta} - * should have been precalculated - * @param V the array containing the {@code N}th value previously calculated from the respective boundary condition - */ - - public void sweep(double[] V) { - for (int j = grid.getGridDensityValue() - 1; j >= 0; j--) - V[j] = alpha[j + 1] * V[j + 1] + beta[j + 1]; - } - - /** - * Calculates the {@code alpha} coefficients as part of the tridiagonal matrix algorithm. - */ - - public void evaluateAlpha() { - for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { - alpha[i + 1] = c / (b - a * alpha[i]); - } - } - - public void evaluateBeta(final double[] U) { - evaluateBeta(U, 2, grid.getGridDensityValue() + 1); - } - - /** - * Calculates the {@code beta} coefficients as part of the tridiagonal matrix algorithm. - */ - - public void evaluateBeta(final double[] U, final int start, final int endExclusive) { - final double tau = grid.getTimeStep(); - for (int i = start; i < endExclusive; i++) - beta[i] = beta(U[i - 1] / tau, phi(i - 1), i); - } - - public double beta(final double f, final double phi, final int i) { - return (f + phi + a * beta[i - 1]) / (b - a * alpha[i - 1]); - } - - public double phi(int i) { - return 0; - } - - public void setAlpha(final int i, final double alpha) { - this.alpha[i] = alpha; - } - - public void setBeta(final int i, final double beta) { - this.beta[i] = beta; - } - - public double[] getAlpha() { - return alpha; - } - - public double[] getBeta() { - return beta; - } - - public void setCoefA(double a) { - this.a = a; - } - - public void setCoefB(double b) { - this.b = b; - } - - public void setCoefC(double c) { - this.c = c; - } - - protected double getCoefA() { - return a; - } - - protected double getCoefB() { - return b; - } - - protected double getCoefC() { - return c; - } - - public Grid getGrid() { - return grid; - } - -} \ No newline at end of file + private Grid grid; + + private double a; + private double b; + private double c; + + private double[] alpha; + private double[] beta; + + public TridiagonalMatrixAlgorithm(Grid grid) { + this.grid = grid; + final int N = grid.getGridDensityValue(); + alpha = new double[N + 2]; + beta = new double[N + 2]; + } + + /** + * Calculates the solution {@code V} using the tridiagonal matrix algorithm. + * This performs a backwards sweep from {@code N - 1} to {@code 0} where + * {@code N} is the grid density value. The coefficients {@code alpha} and + * {@code beta} should have been precalculated + * + * @param V the array containing the {@code N}th value previously calculated + * from the respective boundary condition + */ + public void sweep(double[] V) { + for (int j = grid.getGridDensityValue() - 1; j >= 0; j--) { + V[j] = alpha[j + 1] * V[j + 1] + beta[j + 1]; + } + } + + /** + * Calculates the {@code alpha} coefficients as part of the tridiagonal + * matrix algorithm. + */ + public void evaluateAlpha() { + for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { + alpha[i + 1] = c / (b - a * alpha[i]); + } + } + + public void evaluateBeta(final double[] U) { + evaluateBeta(U, 2, grid.getGridDensityValue() + 1); + } + + /** + * Calculates the {@code beta} coefficients as part of the tridiagonal + * matrix algorithm. + */ + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + final double tau = grid.getTimeStep(); + for (int i = start; i < endExclusive; i++) { + beta[i] = beta(U[i - 1] / tau, phi(i - 1), i); + } + } + + public double beta(final double f, final double phi, final int i) { + return (f + phi + a * beta[i - 1]) / (b - a * alpha[i - 1]); + } + + public double phi(int i) { + return 0; + } + + public void setAlpha(final int i, final double alpha) { + this.alpha[i] = alpha; + } + + public void setBeta(final int i, final double beta) { + this.beta[i] = beta; + } + + public double[] getAlpha() { + return alpha; + } + + public double[] getBeta() { + return beta; + } + + public void setCoefA(double a) { + this.a = a; + } + + public void setCoefB(double b) { + this.b = b; + } + + public void setCoefC(double c) { + this.c = c; + } + + protected double getCoefA() { + return a; + } + + protected double getCoefB() { + return b; + } + + protected double getCoefC() { + return c; + } + + public Grid getGrid() { + return grid; + } + +} diff --git a/src/main/java/pulse/problem/schemes/package-info.java b/src/main/java/pulse/problem/schemes/package-info.java index 5ab514ab..2b28afa7 100644 --- a/src/main/java/pulse/problem/schemes/package-info.java +++ b/src/main/java/pulse/problem/schemes/package-info.java @@ -3,8 +3,7 @@ * PULsE, including the definition of {@code Grid}s, which determine the * partitioning rules for space and time variables. Specific implementation of * the difference schemes may be found separately in a different package. - * + * * @see pulse.problem.schemes.solvers.Solver */ - -package pulse.problem.schemes; \ No newline at end of file +package pulse.problem.schemes; diff --git a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java index 7aaa1782..a0e78806 100644 --- a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java +++ b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java @@ -14,77 +14,74 @@ * {@code SplineInterpolator}. * */ - public class BlackbodySpectrum { - private double reductionFactor; - private UnivariateFunction interpolation; - - /** - * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor - * δTm/T0, which - * is needed for calculations of the maximum heating. Note the interpolation - * needs to be set - * @param p a problem statement - */ - - public BlackbodySpectrum(NonlinearProblem p) { - final double maxHeating = p.getProperties().maximumHeating((Pulse2D)p.getPulse()); - reductionFactor = maxHeating / ((double) p.getProperties().getTestTemperature().getValue()); - } - - /** - * Calculates the spectral radiance, which is equal to the spectral power - * divided by π, at the given coordinate. - * - * @param x the geometric coordinate at which calculation should be performed - * @return the spectral radiance at {@code x} - */ - - public double radianceAt(double x) { - return radiance(interpolation.value(x)); - } - - /** - * Calculates the emissive power at the given coordinate. This is equal to - * 0.25 T0Tm [1 - * +δTm /T0 θ (x) - * ]4, where θ is the reduced temperature. - * - * @param x the geometric coordinate inside the sample - * @return the local emissive power value - */ - - public double powerAt(double x) { - return emissivePower(interpolation.value(x)); - } - - /** - * Sets a new function for the spatial temperature profile. The function is - * generally constructed using a {@code SplineInterpolator} - * - * @param interpolation - */ - - public void setInterpolation(UnivariateFunction interpolation) { - this.interpolation = interpolation; - } - - public UnivariateFunction getInterpolation() { - return interpolation; - } - - @Override - public String toString() { - return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; - } - - private double emissivePower(double reducedTemperature) { - return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); - } - - private double radiance(double reducedTemperature) { - return emissivePower(reducedTemperature) / Math.PI; - } - -} \ No newline at end of file + private double reductionFactor; + private UnivariateFunction interpolation; + + /** + * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor + * δTm/T0, which is + * needed for calculations of the maximum heating. Note the interpolation + * needs to be set + * + * @param p a problem statement + */ + public BlackbodySpectrum(NonlinearProblem p) { + final double maxHeating = p.getProperties().maximumHeating((Pulse2D) p.getPulse()); + reductionFactor = maxHeating / ((double) p.getProperties().getTestTemperature().getValue()); + } + + /** + * Calculates the spectral radiance, which is equal to the spectral power + * divided by π, at the given coordinate. + * + * @param x the geometric coordinate at which calculation should be + * performed + * @return the spectral radiance at {@code x} + */ + public double radianceAt(double x) { + return radiance(interpolation.value(x)); + } + + /** + * Calculates the emissive power at the given coordinate. This is equal to + * 0.25 T0Tm [1 + * +δTm /T0 θ (x) + * ]4, where θ is the reduced temperature. + * + * @param x the geometric coordinate inside the sample + * @return the local emissive power value + */ + public double powerAt(double x) { + return emissivePower(interpolation.value(x)); + } + + /** + * Sets a new function for the spatial temperature profile. The function is + * generally constructed using a {@code SplineInterpolator} + * + * @param interpolation + */ + public void setInterpolation(UnivariateFunction interpolation) { + this.interpolation = interpolation; + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } + + @Override + public String toString() { + return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; + } + + private double emissivePower(double reducedTemperature) { + return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); + } + + private double radiance(double reducedTemperature) { + return emissivePower(reducedTemperature) / Math.PI; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java index 69b32644..8ad2b582 100644 --- a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java +++ b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java @@ -1,63 +1,59 @@ package pulse.problem.schemes.rte; /** - * This is basically a coupling interface between a {@code Solver} and a {@code RadiativeTransferSolver}. + * This is basically a coupling interface between a {@code Solver} and a + * {@code RadiativeTransferSolver}. * */ - public interface DerivativeCalculator { - /** - * Calculates the average value of the flux derivatives at the {@code uIndex} - * grid point on the current and previous timesteps. - * - * @param uIndex the grid point index - * @return the time-averaged value of the flux derivative at {@code uIndex} - */ - - public double meanFluxDerivative(int uIndex); - - /** - * Calculates the average value of the flux derivatives at the first grid point - * on the current and previous timesteps. - * - * @return the time-averaged value of the flux derivative at the front surface - */ - - public double meanFluxDerivativeFront(); - - /** - * Calculates the average value of the flux derivatives at the last grid point - * on the current and previous timesteps. - * - * @return the time-averaged value of the flux derivative at the rear surface - */ - - public double meanFluxDerivativeRear(); - - /** - * Calculates the flux derivative at the {@code uIndex} grid point. - * - * @param uIndex the grid point index - * @return the value of the flux derivative at {@code uIndex} - */ - - public double fluxDerivative(int uIndex); - - /** - * Calculates the flux derivative at the front surface. - * - * @return the value of the flux derivative at the front surface - */ - - public double fluxDerivativeFront(); - - /** - * Calculates the flux derivative at the rear surface. - * - * @return the value of the flux derivative at the rear surface - */ - - public double fluxDerivativeRear(); - -} \ No newline at end of file + /** + * Calculates the average value of the flux derivatives at the + * {@code uIndex} grid point on the current and previous timesteps. + * + * @param uIndex the grid point index + * @return the time-averaged value of the flux derivative at {@code uIndex} + */ + public double meanFluxDerivative(int uIndex); + + /** + * Calculates the average value of the flux derivatives at the first grid + * point on the current and previous timesteps. + * + * @return the time-averaged value of the flux derivative at the front + * surface + */ + public double meanFluxDerivativeFront(); + + /** + * Calculates the average value of the flux derivatives at the last grid + * point on the current and previous timesteps. + * + * @return the time-averaged value of the flux derivative at the rear + * surface + */ + public double meanFluxDerivativeRear(); + + /** + * Calculates the flux derivative at the {@code uIndex} grid point. + * + * @param uIndex the grid point index + * @return the value of the flux derivative at {@code uIndex} + */ + public double fluxDerivative(int uIndex); + + /** + * Calculates the flux derivative at the front surface. + * + * @return the value of the flux derivative at the front surface + */ + public double fluxDerivativeFront(); + + /** + * Calculates the flux derivative at the rear surface. + * + * @return the value of the flux derivative at the rear surface + */ + public double fluxDerivativeRear(); + +} diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index a086a2b2..0dff5461 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -4,77 +4,73 @@ public abstract class Fluxes implements DerivativeCalculator { - private int N; - private double opticalThickness; - private double[] fluxes; - private double[] storedFluxes; + private int N; + private double opticalThickness; + private double[] fluxes; + private double[] storedFluxes; - public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { - setOpticalThickness(opticalThickness); - setDensity(gridDensity); - } - - /** - * Stores all currently calculated fluxes in a separate array. - */ + public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { + setOpticalThickness(opticalThickness); + setDensity(gridDensity); + } - public void store() { - System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results - } - - /** - * Retrieves the currently calculated flux at the {@code i} grid point - * - * @param i the index of the grid point - * @return the flux value at the specified grid point - */ + /** + * Stores all currently calculated fluxes in a separate array. + */ + public void store() { + System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results + } - public double getFlux(int i) { - return fluxes[i]; - } + /** + * Retrieves the currently calculated flux at the {@code i} grid point + * + * @param i the index of the grid point + * @return the flux value at the specified grid point + */ + public double getFlux(int i) { + return fluxes[i]; + } - /** - * Sets the flux at the {@code i} grid point - * - * @param i the index of the grid point - */ + /** + * Sets the flux at the {@code i} grid point + * + * @param i the index of the grid point + */ + public void setFlux(int i, double value) { + this.fluxes[i] = value; + } - public void setFlux(int i, double value) { - this.fluxes[i] = value; - } + /** + * Retrieves the previously calculated flux at the {@code i} grid point. + * + * @param i the index of the grid point + * @return the previous flux value at the specified grid point + * @see store() + */ + public double getStoredFlux(int i) { + return storedFluxes[i]; + } - /** - * Retrieves the previously calculated flux at the {@code i} grid point. - * - * @param i the index of the grid point - * @return the previous flux value at the specified grid point - * @see store() - */ + public double getOpticalGridStep() { + return opticalThickness / ((double) N); + } - public double getStoredFlux(int i) { - return storedFluxes[i]; - } - - public double getOpticalGridStep() { - return opticalThickness/((double)N); - } - - public int getDensity() { - return N; - } + public int getDensity() { + return N; + } - public double getOpticalThickness() { - return opticalThickness; - } - - public void setDensity(NumericProperty gridDensity) { - this.N = (int)gridDensity.getValue(); - fluxes = new double[N + 1]; - storedFluxes = new double[N + 1]; - } - - public void setOpticalThickness(NumericProperty opticalThickness) { - this.opticalThickness = (double)opticalThickness.getValue(); - } - -} \ No newline at end of file + public double getOpticalThickness() { + return opticalThickness; + } + + public void setDensity(NumericProperty gridDensity) { + this.N = (int) gridDensity.getValue(); + fluxes = new double[N + 1]; + storedFluxes = new double[N + 1]; + } + + public void setOpticalThickness(NumericProperty opticalThickness) { + this.opticalThickness = (double) opticalThickness.getValue(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 9351579a..0d164037 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -4,66 +4,66 @@ public class FluxesAndExplicitDerivatives extends Fluxes { - private double fd[]; - private double fdStored[]; - - public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { - super(gridDensity, opticalThickness); - } - - @Override - public void setDensity(NumericProperty gridDensity) { - super.setDensity(gridDensity); - fd = new double[getDensity() + 1]; - fdStored = new double[getDensity() + 1]; - } - - @Override - public double fluxDerivative(int index) { - return fd[index]; - } - - @Override - public double fluxDerivativeRear() { - return fd[getDensity()]; - } - - @Override - public double fluxDerivativeFront() { - return fd[0]; - } - - @Override - public void store() { - super.store(); - System.arraycopy(fd, 0, fdStored, 0, fd.length); // store previous results - } - - @Override - public double meanFluxDerivative(int uIndex) { - return 0.5 * (fd[uIndex] + fdStored[uIndex]); - } - - @Override - public double meanFluxDerivativeFront() { - return 0.5 * (fd[0] + fdStored[0]); - } - - @Override - public double meanFluxDerivativeRear() { - return 0.5 * (fd[getDensity()] + fdStored[getDensity()]); - } - - public double getStoredFluxDerivative(int index) { - return fdStored[index]; - } - - public double getFluxDerivative(int i) { - return fd[i]; - } - - public void setFluxDerivative(int i, double f) { - fd[i] = f; - } - -} \ No newline at end of file + private double fd[]; + private double fdStored[]; + + public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { + super(gridDensity, opticalThickness); + } + + @Override + public void setDensity(NumericProperty gridDensity) { + super.setDensity(gridDensity); + fd = new double[getDensity() + 1]; + fdStored = new double[getDensity() + 1]; + } + + @Override + public double fluxDerivative(int index) { + return fd[index]; + } + + @Override + public double fluxDerivativeRear() { + return fd[getDensity()]; + } + + @Override + public double fluxDerivativeFront() { + return fd[0]; + } + + @Override + public void store() { + super.store(); + System.arraycopy(fd, 0, fdStored, 0, fd.length); // store previous results + } + + @Override + public double meanFluxDerivative(int uIndex) { + return 0.5 * (fd[uIndex] + fdStored[uIndex]); + } + + @Override + public double meanFluxDerivativeFront() { + return 0.5 * (fd[0] + fdStored[0]); + } + + @Override + public double meanFluxDerivativeRear() { + return 0.5 * (fd[getDensity()] + fdStored[getDensity()]); + } + + public double getStoredFluxDerivative(int index) { + return fdStored[index]; + } + + public double getFluxDerivative(int i) { + return fd[i]; + } + + public void setFluxDerivative(int i, double f) { + fd[i] = f; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java index dc9e6f05..b2fd4e32 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java @@ -3,45 +3,45 @@ import pulse.properties.NumericProperty; public class FluxesAndImplicitDerivatives extends Fluxes { - - public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { - super(gridDensity, opticalThickness); - } - - @Override - public double meanFluxDerivative(int uIndex) { - double f = (getFlux(uIndex - 1) - getFlux(uIndex + 1)) - + (getStoredFlux(uIndex - 1) - getStoredFlux(uIndex + 1)); - return f * 0.25 / getOpticalGridStep(); - } - - @Override - public double meanFluxDerivativeFront() { - double f = (getFlux(0) - getFlux(1)) + (getStoredFlux(0) - getStoredFlux(1)); - return f * 0.5 / getOpticalGridStep(); - } - - @Override - public double meanFluxDerivativeRear() { - final int N = this.getDensity(); - double f = (getFlux(N - 1) - getFlux(N)) + (getStoredFlux(N - 1) - getStoredFlux(N)); - return f * 0.5 / getOpticalGridStep(); - } - - @Override - public double fluxDerivative(int uIndex) { - return (getFlux(uIndex - 1) - getFlux(uIndex + 1)) * 0.5 / getOpticalGridStep(); - } - - @Override - public double fluxDerivativeFront() { - return (getFlux(0) - getFlux(1)) / getOpticalGridStep(); - } - - @Override - public double fluxDerivativeRear() { - final int N = this.getDensity(); - return (getFlux(N - 1) - getFlux(N)) / getOpticalGridStep(); - } - -} \ No newline at end of file + + public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { + super(gridDensity, opticalThickness); + } + + @Override + public double meanFluxDerivative(int uIndex) { + double f = (getFlux(uIndex - 1) - getFlux(uIndex + 1)) + + (getStoredFlux(uIndex - 1) - getStoredFlux(uIndex + 1)); + return f * 0.25 / getOpticalGridStep(); + } + + @Override + public double meanFluxDerivativeFront() { + double f = (getFlux(0) - getFlux(1)) + (getStoredFlux(0) - getStoredFlux(1)); + return f * 0.5 / getOpticalGridStep(); + } + + @Override + public double meanFluxDerivativeRear() { + final int N = this.getDensity(); + double f = (getFlux(N - 1) - getFlux(N)) + (getStoredFlux(N - 1) - getStoredFlux(N)); + return f * 0.5 / getOpticalGridStep(); + } + + @Override + public double fluxDerivative(int uIndex) { + return (getFlux(uIndex - 1) - getFlux(uIndex + 1)) * 0.5 / getOpticalGridStep(); + } + + @Override + public double fluxDerivativeFront() { + return (getFlux(0) - getFlux(1)) / getOpticalGridStep(); + } + + @Override + public double fluxDerivativeRear() { + final int N = this.getDensity(); + return (getFlux(N - 1) - getFlux(N)) / getOpticalGridStep(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java index 607b5334..71da5f96 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java @@ -5,15 +5,13 @@ * subclasses. * */ - public interface RTECalculationListener { - /** - * Invoked when a sub-step of the RTE solution has finished. - * - * @param status the status of the completed step - */ - - public void onStatusUpdate(RTECalculationStatus status); + /** + * Invoked when a sub-step of the RTE solution has finished. + * + * @param status the status of the completed step + */ + public void onStatusUpdate(RTECalculationStatus status); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index b656dcb4..06d7b40d 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -4,30 +4,22 @@ * A measure of health for radiative transfer calculations. * */ - public enum RTECalculationStatus { - /** - * The current calculation step finished normally. - */ - - NORMAL, - - /** - * The integrator took too long to finish. - */ - - INTEGRATOR_TIMEOUT, - - /** - * The iterative solver took too long to finish. - */ - - ITERATION_LIMIT_REACHED, - - /** - * The grid density required to reach the error threshold was too large. - */ - - GRID_TOO_LARGE; -} \ No newline at end of file + /** + * The current calculation step finished normally. + */ + NORMAL, + /** + * The integrator took too long to finish. + */ + INTEGRATOR_TIMEOUT, + /** + * The iterative solver took too long to finish. + */ + ITERATION_LIMIT_REACHED, + /** + * The grid density required to reach the error threshold was too large. + */ + GRID_TOO_LARGE; +} diff --git a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index 8542ff4a..98dd9030 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -24,118 +24,115 @@ * steps with listeners. * */ - public abstract class RadiativeTransferSolver extends PropertyHolder implements Reflexive, Descriptive { - private Fluxes fluxes; - private List rteListeners; - - /** - * Dummy constructor. - * - */ - - public RadiativeTransferSolver() { - rteListeners = new ArrayList<>(); - } - - /** - * Launches a calculation of the radiative transfer equation. - * - * @param temperatureArray the input temperature profile - * @return the status of calculation - */ - - public abstract RTECalculationStatus compute(double[] temperatureArray); - - /** - * Retrieves the parameters from {@code p} and {@code grid} needed to run the - * calculations. Resets the flux arrays. - * - * @param p the problem statement - * @param grid the grid - */ - - public void init(ParticipatingMedium p, Grid grid) { - if (fluxes != null) { - fluxes.setDensity(grid.getGridDensity()); - var properties = (ThermoOpticalProperties)p.getProperties(); - fluxes.setOpticalThickness(properties.getOpticalThickness()); - } - } - - /** - * Performs interpolation with natural cubic splines using the input arguments. - * - * @param tempArray an array of data defined on a previously initialised grid. - * @return a {@code UnivariateFunction} generated with a - * {@code SplineInterpolator} - */ - - public UnivariateFunction interpolateTemperatureProfile(final double[] tempArray) { - var xArray = new double[tempArray.length + 2]; - IntStream.range(0, xArray.length).forEach(i -> xArray[i] = opticalCoordinateAt(i - 1)); - - var tarray = new double[tempArray.length + 2]; - System.arraycopy(tempArray, 0, tarray, 1, tempArray.length - 1); - - final double[] p1 = new double[] { xArray[1], tempArray[0] }; - final double[] p2 = new double[] { xArray[2], tempArray[1] }; - tarray[0] = linearExtrapolation(p1, p2, xArray[0]); - - final double[] p3 = new double[] { xArray[xArray.length - 2], tempArray[tempArray.length - 1] }; - final double[] p4 = new double[] { xArray[xArray.length - 3], tempArray[tempArray.length - 2] }; - tarray[tarray.length - 1] = linearExtrapolation(p3, p4, xArray[xArray.length - 1]); - - return (new SplineInterpolator()).interpolate(xArray, tarray); - } - - /** - * Retrieves the optical coordinate corresponding to the grid index {@code i} - * - * @param i the external grid index - * @return τ0/N i - */ - - public double opticalCoordinateAt(final int i) { - return fluxes.getOpticalGridStep() * i; - } - - @Override - public boolean ignoreSiblings() { - return true; - } - - @Override - public String getPrefix() { - return "Radiative Transfer Solver"; - } - - public List getRTEListeners() { - return rteListeners; - } - - /** - * Adds a listener that can listen to status updates. - * - * @param listener a listener to track the calculation progress - */ - - public void addRTEListener(RTECalculationListener listener) { - rteListeners.add(listener); - } - - public void fireStatusUpdate(RTECalculationStatus status) { - for (RTECalculationListener l : getRTEListeners()) - l.onStatusUpdate(status); - } - - public Fluxes getFluxes() { - return fluxes; - } - - public void setFluxes(Fluxes fluxes) { - this.fluxes = fluxes; - } - -} \ No newline at end of file + private Fluxes fluxes; + private List rteListeners; + + /** + * Dummy constructor. + * + */ + public RadiativeTransferSolver() { + rteListeners = new ArrayList<>(); + } + + /** + * Launches a calculation of the radiative transfer equation. + * + * @param temperatureArray the input temperature profile + * @return the status of calculation + */ + public abstract RTECalculationStatus compute(double[] temperatureArray); + + /** + * Retrieves the parameters from {@code p} and {@code grid} needed to run + * the calculations. Resets the flux arrays. + * + * @param p the problem statement + * @param grid the grid + */ + public void init(ParticipatingMedium p, Grid grid) { + if (fluxes != null) { + fluxes.setDensity(grid.getGridDensity()); + var properties = (ThermoOpticalProperties) p.getProperties(); + fluxes.setOpticalThickness(properties.getOpticalThickness()); + } + } + + /** + * Performs interpolation with natural cubic splines using the input + * arguments. + * + * @param tempArray an array of data defined on a previously initialised + * grid. + * @return a {@code UnivariateFunction} generated with a + * {@code SplineInterpolator} + */ + public UnivariateFunction interpolateTemperatureProfile(final double[] tempArray) { + var xArray = new double[tempArray.length + 2]; + IntStream.range(0, xArray.length).forEach(i -> xArray[i] = opticalCoordinateAt(i - 1)); + + var tarray = new double[tempArray.length + 2]; + System.arraycopy(tempArray, 0, tarray, 1, tempArray.length - 1); + + final double[] p1 = new double[]{xArray[1], tempArray[0]}; + final double[] p2 = new double[]{xArray[2], tempArray[1]}; + tarray[0] = linearExtrapolation(p1, p2, xArray[0]); + + final double[] p3 = new double[]{xArray[xArray.length - 2], tempArray[tempArray.length - 1]}; + final double[] p4 = new double[]{xArray[xArray.length - 3], tempArray[tempArray.length - 2]}; + tarray[tarray.length - 1] = linearExtrapolation(p3, p4, xArray[xArray.length - 1]); + + return (new SplineInterpolator()).interpolate(xArray, tarray); + } + + /** + * Retrieves the optical coordinate corresponding to the grid index + * {@code i} + * + * @param i the external grid index + * @return τ0/N i + */ + public double opticalCoordinateAt(final int i) { + return fluxes.getOpticalGridStep() * i; + } + + @Override + public boolean ignoreSiblings() { + return true; + } + + @Override + public String getPrefix() { + return "Radiative Transfer Solver"; + } + + public List getRTEListeners() { + return rteListeners; + } + + /** + * Adds a listener that can listen to status updates. + * + * @param listener a listener to track the calculation progress + */ + public void addRTEListener(RTECalculationListener listener) { + rteListeners.add(listener); + } + + public void fireStatusUpdate(RTECalculationStatus status) { + for (RTECalculationListener l : getRTEListeners()) { + l.onStatusUpdate(status); + } + } + + public Fluxes getFluxes() { + return fluxes; + } + + public void setFluxes(Fluxes fluxes) { + this.fluxes = fluxes; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java index 4ab5afe9..9c557e3d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java @@ -6,124 +6,125 @@ import pulse.util.Descriptive; /** - * The Butcher tableau coefficients used by the explicit Runge-Kutta solvers. Variable - * names correspond to the standard notations. + * The Butcher tableau coefficients used by the explicit Runge-Kutta solvers. + * Variable names correspond to the standard notations. * */ - public class ButcherTableau implements Descriptive { - private Vector b; - private Vector bHat; - private Vector c; - private SquareMatrix coefs; + private Vector b; + private Vector bHat; + private Vector c; + private SquareMatrix coefs; + + private boolean fsal; + private String name; - private boolean fsal; - private String name; + public final static String DEFAULT_TABLEAU = "BS23"; - public final static String DEFAULT_TABLEAU = "BS23"; + public ButcherTableau(String name, double[][] coefs, double[] c, double[] b, double[] bHat, boolean fsal) { - public ButcherTableau(String name, double[][] coefs, double[] c, double[] b, double[] bHat, boolean fsal) { + if (c.length != b.length || c.length != bHat.length) { + throw new IllegalArgumentException("Check dimensions of the input vectors"); + } - if (c.length != b.length || c.length != bHat.length) - throw new IllegalArgumentException("Check dimensions of the input vectors"); + if (coefs.length != coefs[0].length || coefs.length != c.length) { + throw new IllegalArgumentException("Check dimensions of the input matrix array"); + } - if (coefs.length != coefs[0].length || coefs.length != c.length) - throw new IllegalArgumentException("Check dimensions of the input matrix array"); + this.name = name; + this.fsal = fsal; - this.name = name; - this.fsal = fsal; + this.coefs = Matrices.createSquareMatrix(coefs); + this.c = new Vector(c); + this.b = new Vector(b); + this.bHat = new Vector(bHat); + } - this.coefs = Matrices.createSquareMatrix(coefs); - this.c = new Vector(c); - this.b = new Vector(b); - this.bHat = new Vector(bHat); - } + public int numberOfStages() { + return b.dimension(); + } - public int numberOfStages() { - return b.dimension(); - } + public SquareMatrix getMatrix() { + return coefs; + } - public SquareMatrix getMatrix() { - return coefs; - } + public void setMatrix(SquareMatrix coefs) { + this.coefs = coefs; + } - public void setMatrix(SquareMatrix coefs) { - this.coefs = coefs; - } + public Vector getEstimator() { + return bHat; + } - public Vector getEstimator() { - return bHat; - } + public void setEstimator(Vector bHat) { + this.bHat = bHat; + } - public void setEstimator(Vector bHat) { - this.bHat = bHat; - } + public Vector getInterpolator() { + return b; + } - public Vector getInterpolator() { - return b; - } + public void setInterpolator(Vector b) { + this.b = b; + } - public void setInterpolator(Vector b) { - this.b = b; - } + public Vector getC() { + return c; + } - public Vector getC() { - return c; - } + public void setC(Vector c) { + this.c = c; + } - public void setC(Vector c) { - this.c = c; - } + public boolean isFSAL() { + return fsal; + } - public boolean isFSAL() { - return fsal; - } + @Override + public String toString() { + return name; + } - @Override - public String toString() { - return name; - } - - public String printTableau() { + public String printTableau() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); - for (int i = 0; i < b.dimension(); i++) { + for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%n%3.8f | ", c.get(i))); + sb.append(String.format("%n%3.8f | ", c.get(i))); - for (int j = 0; j < b.dimension(); j++) { - sb.append(String.format("%3.8f ", coefs.get(i, j))); - } + for (int j = 0; j < b.dimension(); j++) { + sb.append(String.format("%3.8f ", coefs.get(i, j))); + } - } + } - sb.append(System.lineSeparator()); + sb.append(System.lineSeparator()); - for (int i = 0; i < b.dimension() + 1; i++) { - sb.append(String.format("%-12s", "-")); - } + for (int i = 0; i < b.dimension() + 1; i++) { + sb.append(String.format("%-12s", "-")); + } - sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); + sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); - for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%3.8f ", b.get(i))); - } + for (int i = 0; i < b.dimension(); i++) { + sb.append(String.format("%3.8f ", b.get(i))); + } - sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); + sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); - for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%3.8f ", bHat.get(i))); - } + for (int i = 0; i < b.dimension(); i++) { + sb.append(String.format("%3.8f ", bHat.get(i))); + } - return sb.toString(); + return sb.toString(); - } + } - @Override - public String describe() { - return "Butcher tableau"; - } + @Override + public String describe() { + return "Butcher tableau"; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java index 1d49c2bf..a242ef40 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java @@ -4,84 +4,84 @@ import pulse.math.MathUtils; /** - * A composite Gaussian quadrature for numerical evaluation of the scattering integral in - * one-dimensional heat transfer. + * A composite Gaussian quadrature for numerical evaluation of the scattering + * integral in one-dimensional heat transfer. + * * @author Teymur Aliev, Vadim Zborovskii, Artem Lunev * */ - public class CompositeGaussianQuadrature { - private LegendrePoly poly; - - private double[] roots; - - private double[] nodes; - private double[] weights; - - private int n; - - /** - * Constructs a composite Gaussian quadrature for an even {@code n} - * @param n an even integer - */ - - public CompositeGaussianQuadrature(final int n) { - if(n % 2 != 0) - throw new IllegalArgumentException(n + " is odd. Even number expected."); - this.n = n; - poly = new LegendrePoly(n / 2); - roots = poly.roots(); - nodes(); - weights(); - } - - private void nodes() { - - nodes = new double[n]; - weights = new double[n]; - - for (int i = 0; i < n / 2; i++) { - - nodes[i] = 0.5 * (1.0 + roots[i]); - nodes[i + n / 2] = -0.5 * (1.0 + roots[i]); - - } - - } - - /** - * Calculates the Gaussian weights. Uses the formula by Abramowitz & Stegun - * (Abramowitz & Stegun 1972, p. 887)) - */ - - private void weights() { - double denominator = 1; - - for (int i = 0; i < n / 2; i++) { - denominator = (1 - roots[i] * roots[i]) * MathUtils.fastPowLoop(poly.derivative(roots[i]), 2); - weights[i] = 1.0 / denominator; - weights[i + n / 2] = weights[i]; - } - - } - - /** - * The weights of the composite quadrature. - * @return the weights - */ - - public double[] getWeights() { - return weights; - } - - /** - * The cosine nodes of the composite quadrature. - * @return the cosine nodes - */ - - public double[] getNodes() { - return nodes; - } - -} \ No newline at end of file + private LegendrePoly poly; + + private double[] roots; + + private double[] nodes; + private double[] weights; + + private int n; + + /** + * Constructs a composite Gaussian quadrature for an even {@code n} + * + * @param n an even integer + */ + public CompositeGaussianQuadrature(final int n) { + if (n % 2 != 0) { + throw new IllegalArgumentException(n + " is odd. Even number expected."); + } + this.n = n; + poly = new LegendrePoly(n / 2); + roots = poly.roots(); + nodes(); + weights(); + } + + private void nodes() { + + nodes = new double[n]; + weights = new double[n]; + + for (int i = 0; i < n / 2; i++) { + + nodes[i] = 0.5 * (1.0 + roots[i]); + nodes[i + n / 2] = -0.5 * (1.0 + roots[i]); + + } + + } + + /** + * Calculates the Gaussian weights. Uses the formula by Abramowitz & Stegun + * (Abramowitz & Stegun 1972, p. 887)) + */ + private void weights() { + double denominator = 1; + + for (int i = 0; i < n / 2; i++) { + denominator = (1 - roots[i] * roots[i]) * MathUtils.fastPowLoop(poly.derivative(roots[i]), 2); + weights[i] = 1.0 / denominator; + weights[i + n / 2] = weights[i]; + } + + } + + /** + * The weights of the composite quadrature. + * + * @return the weights + */ + public double[] getWeights() { + return weights; + } + + /** + * The cosine nodes of the composite quadrature. + * + * @return the cosine nodes + */ + public double[] getNodes() { + return nodes; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index cb965541..bdcf5d3d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -1,96 +1,96 @@ package pulse.problem.schemes.rte.dom; /** - * Defines the main quantities calculated within the discrete ordinates method. This - * includes the various intensity and flux arrays used internally by the integrators. + * Defines the main quantities calculated within the discrete ordinates method. + * This includes the various intensity and flux arrays used internally by the + * integrators. */ - class DiscreteQuantities { - private double[][] I; - private double[][] Ik; - private double[][] f; - private double[][] fk; - private double[] qLast; - - /** - * Constructs a set of quantities based on the specified - * spatial and angular discretisation. - * @param gridDensity the DOM grid density - * @param ordinates the number of angular nodes - */ - - public DiscreteQuantities(int gridDensity, int ordinates) { - init(gridDensity, ordinates); - } - - public void init(int gridDensity, int ordinates) { - I = new double[gridDensity + 1][ordinates]; - f = new double[gridDensity + 1][ordinates]; - Ik = new double[gridDensity + 1][ordinates]; - fk = new double[gridDensity + 1][ordinates]; - qLast = new double[ordinates]; - } - - public void store() { - final int n = I.length; - final int m = I[0].length; - - Ik = new double[n][m]; - fk = new double[n][m]; - - /* + private double[][] I; + private double[][] Ik; + private double[][] f; + private double[][] fk; + private double[] qLast; + + /** + * Constructs a set of quantities based on the specified spatial and angular + * discretisation. + * + * @param gridDensity the DOM grid density + * @param ordinates the number of angular nodes + */ + public DiscreteQuantities(int gridDensity, int ordinates) { + init(gridDensity, ordinates); + } + + public void init(int gridDensity, int ordinates) { + I = new double[gridDensity + 1][ordinates]; + f = new double[gridDensity + 1][ordinates]; + Ik = new double[gridDensity + 1][ordinates]; + fk = new double[gridDensity + 1][ordinates]; + qLast = new double[ordinates]; + } + + public void store() { + final int n = I.length; + final int m = I[0].length; + + Ik = new double[n][m]; + fk = new double[n][m]; + + /* * store k-th components - */ - for (int j = 0; j < Ik.length; j++) { - System.arraycopy(I[j], 0, Ik[j], 0, Ik[0].length); - System.arraycopy(f[j], 0, fk[j], 0, fk[0].length); - } - - } - - public double[][] getIntensities() { - return I; - } - - public double[][] getDerivatives() { - return f; - } - - public double getQLast(int i) { - return qLast[i]; - } - - public void setQLast(int i, double q) { - this.qLast[i] = q; - } - - public double getDerivative(int i, int j) { - return f[i][j]; - } - - public void setDerivative(int i, int j, double f) { - this.f[i][j] = f; - } - - public double getStoredIntensity(final int i, final int j) { - return Ik[i][j]; - } - - public double getStoredDerivative(final int i, final int j) { - return fk[i][j]; - } - - public void setStoredDerivative(final int i, final int j, final double f) { - this.f[i][j] = f; - } - - public double getIntensity(int i, int j) { - return I[i][j]; - } - - public void setIntensity(int i, int j, double value) { - I[i][j] = value; - } - -} \ No newline at end of file + */ + for (int j = 0; j < Ik.length; j++) { + System.arraycopy(I[j], 0, Ik[j], 0, Ik[0].length); + System.arraycopy(f[j], 0, fk[j], 0, fk[0].length); + } + + } + + public double[][] getIntensities() { + return I; + } + + public double[][] getDerivatives() { + return f; + } + + public double getQLast(int i) { + return qLast[i]; + } + + public void setQLast(int i, double q) { + this.qLast[i] = q; + } + + public double getDerivative(int i, int j) { + return f[i][j]; + } + + public void setDerivative(int i, int j, double f) { + this.f[i][j] = f; + } + + public double getStoredIntensity(final int i, final int j) { + return Ik[i][j]; + } + + public double getStoredDerivative(final int i, final int j) { + return fk[i][j]; + } + + public void setStoredDerivative(final int i, final int j, final double f) { + this.f[i][j] = f; + } + + public double getIntensity(int i, int j) { + return I[i][j]; + } + + public void setIntensity(int i, int j, double value) { + I[i][j] = value; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index a754a3eb..20776d3f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -245,7 +245,9 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public List listedTypes() { - return new ArrayList(Arrays.asList(quadSelector)); + var list = super.listedTypes(); + list.add(quadSelector); + return list; } @Override @@ -278,4 +280,4 @@ public DiscreteSelector getQuadratureSelector() { return quadSelector; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java index 3ec1b198..08cea6dc 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java @@ -6,41 +6,41 @@ public class FixedIterations extends IterativeSolver { - @Override - public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { + @Override + public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { - var discrete = integrator.getDiscretisation(); - double relativeError = 100; + var discrete = integrator.getDiscretisation(); + double relativeError = 100; - double qld = 0; - double qrd = 0; - double qsum; + double qld = 0; + double qrd = 0; + double qsum; - int iterations = 0; + int iterations = 0; - RTECalculationStatus status = RTECalculationStatus.NORMAL; - final var ef = integrator.getEmissionFunction(); + RTECalculationStatus status = RTECalculationStatus.NORMAL; + final var ef = integrator.getEmissionFunction(); - for (double ql = 1e8, qr = ql; relativeError > getIterationError() - && status == RTECalculationStatus.NORMAL; status = sanityCheck(status, ++iterations)) { - // do the integration - status = integrator.integrate(); + for (double ql = 1e8, qr = ql; relativeError > getIterationError() + && status == RTECalculationStatus.NORMAL; status = sanityCheck(status, ++iterations)) { + // do the integration + status = integrator.integrate(); - // get the difference in boundary heat fluxes - qld = discrete.fluxLeft(ef); - qrd = discrete.fluxRight(ef); - qsum = abs(qld - ql) + abs(qrd - qr); - - // if the integrator attempted rescaling, last iteration is not valid anymore - relativeError = integrator.wasRescaled() ? Double.POSITIVE_INFINITY : qsum / (abs(ql) + abs(qr)); - - //use previous iteration - ql = qld; - qr = qrd; - } + // get the difference in boundary heat fluxes + qld = discrete.fluxLeft(ef); + qrd = discrete.fluxRight(ef); + qsum = abs(qld - ql) + abs(qrd - qr); - return status; + // if the integrator attempted rescaling, last iteration is not valid anymore + relativeError = integrator.wasRescaled() ? Double.POSITIVE_INFINITY : qsum / (abs(ql) + abs(qr)); - } + //use previous iteration + ql = qld; + qr = qrd; + } -} \ No newline at end of file + return status; + + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java index 053e9452..03c4477f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java @@ -8,33 +8,32 @@ * The single-parameter Henyey-Greenstein scattering phase function. * */ - public class HenyeyGreensteinPF extends PhaseFunction { - private double a1; - private double a2; - private double b1; - - public HenyeyGreensteinPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); - } - - @Override - public void init(ParticipatingMedium problem) { - super.init(problem); - final double anisotropy = getAnisotropyFactor(); - b1 = 2.0 * anisotropy; - final double aSq = anisotropy * anisotropy; - a1 = 1.0 - aSq; - a2 = 1.0 + aSq; - } - - @Override - public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - final double theta = ordinates.getNode(k) * ordinates.getNode(i); - final double f = a2 - b1 * theta; - return a1 / (f * sqrt(f)); - } - -} \ No newline at end of file + private double a1; + private double a2; + private double b1; + + public HenyeyGreensteinPF(ParticipatingMedium medium, Discretisation intensities) { + super(medium, intensities); + } + + @Override + public void init(ParticipatingMedium problem) { + super.init(problem); + final double anisotropy = getAnisotropyFactor(); + b1 = 2.0 * anisotropy; + final double aSq = anisotropy * anisotropy; + a1 = 1.0 - aSq; + a2 = 1.0 + aSq; + } + + @Override + public double function(final int i, final int k) { + final var ordinates = getDiscreteIntensities().getOrdinates(); + final double theta = ordinates.getNode(k) * ordinates.getNode(i); + final double f = a2 - b1 * theta; + return a1 / (f * sqrt(f)); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java index 18d8f0a7..1157850a 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java @@ -1,130 +1,131 @@ package pulse.problem.schemes.rte.dom; /** - * A globally C1 Hermite interpolator used to interpolate intensities and derivatives - * in discrete ordinates method when solving the radiative transfer equation with a Runge-Kutta - * solver (either implicit or explicit). + * A globally C1 Hermite interpolator used to interpolate intensities + * and derivatives in discrete ordinates method when solving the radiative + * transfer equation with a Runge-Kutta solver (either implicit or explicit). + * * @author Vadim Zborovskii, Artem Lunev * */ - public class HermiteInterpolator { - protected double y1; - protected double y0; - protected double d1; - protected double d0; - - protected double a; - protected double bMinusA; - - public void clear() { - y1 = 0; - y0 = 0; - d1 = 0; - d0 = 0; - a = 0; - bMinusA = 0; - } - - /** - * Interpolates the function at {@code x} - * @param x the value within the specified bounds - * @return the interpolated value - */ - - public double interpolate(final double x) { - final double t = (x - a) / bMinusA; - final double tMinusOne = (t - 1.0); - return t * t * (3.0 - 2.0 * t) * y1 + tMinusOne * tMinusOne * (1.0 + 2.0 * t) * y0 - + (t * t * tMinusOne * d1 + tMinusOne * tMinusOne * t * d0) * bMinusA; - } - - /** - * Calculates the derivative of the interpolant at {@code x} - * @param x the value within the specified bounds - * @return the derivative of the interpolant - */ - - public double derivative(final double x) { - final double t = (x - a) / bMinusA; - final double tt1 = t * (t - 1.0); - - return 6.0 * tt1 * (y0 - y1) / bMinusA + (t * (3.0 * t - 2.0) * d1 + (3.0 * t * t - 4.0 * t + 1.0) * d0); - } - - @Override - public String toString() { - return String.format("%n (%3.6f ; %3.6f), (%3.6f ; %3.6f), (%3.6f, %3.6f)", y0, y1, d0, d1, a, (bMinusA - a)); - } - - /** - * Interpolates intensities and their derivatives w.r.t. tau on EXTERNAL grid - * points of the heat problem solver based on the derivatives on INTERNAL grid - * points of DOM solver. - * @param externalGridSize the number of points in the external grid - * @param integrator the adaptive integrator - * @return a three-dimensional array containing the interpolated intensities and derivatives - */ - - public double[][][] interpolateOnExternalGrid(final int externalGridSize, AdaptiveIntegrator integrator) { - final var discrete = integrator.getDiscretisation(); - final var intensities = discrete.getQuantities().getIntensities(); - final var internalGrid = discrete.getGrid(); - final var derivatives = discrete.getQuantities().getDerivatives(); - final int total = discrete.getOrdinates().getTotalNodes(); - - var iExt = new double[2][externalGridSize][total]; - double t; - - final double hx = internalGrid.getDimension() / (externalGridSize - 1.0); - final int n = internalGrid.getDensity() + 1; - - /* + protected double y1; + protected double y0; + protected double d1; + protected double d0; + + protected double a; + protected double bMinusA; + + public void clear() { + y1 = 0; + y0 = 0; + d1 = 0; + d0 = 0; + a = 0; + bMinusA = 0; + } + + /** + * Interpolates the function at {@code x} + * + * @param x the value within the specified bounds + * @return the interpolated value + */ + public double interpolate(final double x) { + final double t = (x - a) / bMinusA; + final double tMinusOne = (t - 1.0); + return t * t * (3.0 - 2.0 * t) * y1 + tMinusOne * tMinusOne * (1.0 + 2.0 * t) * y0 + + (t * t * tMinusOne * d1 + tMinusOne * tMinusOne * t * d0) * bMinusA; + } + + /** + * Calculates the derivative of the interpolant at {@code x} + * + * @param x the value within the specified bounds + * @return the derivative of the interpolant + */ + public double derivative(final double x) { + final double t = (x - a) / bMinusA; + final double tt1 = t * (t - 1.0); + + return 6.0 * tt1 * (y0 - y1) / bMinusA + (t * (3.0 * t - 2.0) * d1 + (3.0 * t * t - 4.0 * t + 1.0) * d0); + } + + @Override + public String toString() { + return String.format("%n (%3.6f ; %3.6f), (%3.6f ; %3.6f), (%3.6f, %3.6f)", y0, y1, d0, d1, a, (bMinusA - a)); + } + + /** + * Interpolates intensities and their derivatives w.r.t. tau on EXTERNAL + * grid points of the heat problem solver based on the derivatives on + * INTERNAL grid points of DOM solver. + * + * @param externalGridSize the number of points in the external grid + * @param integrator the adaptive integrator + * @return a three-dimensional array containing the interpolated intensities + * and derivatives + */ + public double[][][] interpolateOnExternalGrid(final int externalGridSize, AdaptiveIntegrator integrator) { + final var discrete = integrator.getDiscretisation(); + final var intensities = discrete.getQuantities().getIntensities(); + final var internalGrid = discrete.getGrid(); + final var derivatives = discrete.getQuantities().getDerivatives(); + final int total = discrete.getOrdinates().getTotalNodes(); + + var iExt = new double[2][externalGridSize][total]; + double t; + + final double hx = internalGrid.getDimension() / (externalGridSize - 1.0); + final int n = internalGrid.getDensity() + 1; + + /* * Loop through the external grid points - */ - outer: for (int i = 0; i < iExt[0].length; i++) { - t = i * hx; + */ + outer: + for (int i = 0; i < iExt[0].length; i++) { + t = i * hx; - // loops through nodes sorted in ascending order - for (int j = 0; j < n; j++) { + // loops through nodes sorted in ascending order + for (int j = 0; j < n; j++) { - /* + /* * if node is greater than t, then the associated function can be interpolated * between points f_i and f_i-1, since t lies between nodes n_i and n_i-1 - */ - if (internalGrid.getNode(j) > t) { // nearest points on internal grid have been found -> j - 1 and j + */ + if (internalGrid.getNode(j) > t) { // nearest points on internal grid have been found -> j - 1 and j - a = internalGrid.getNode(j - 1); - bMinusA = internalGrid.stepLeft(j); + a = internalGrid.getNode(j - 1); + bMinusA = internalGrid.stepLeft(j); - /* + /* * Loops through ordinate set - */ - - for (int k = 0; k < total; k++) { - y0 = intensities[j - 1][k]; - y1 = intensities[j][k]; - d0 = derivatives[j - 1][k]; - d1 = derivatives[j][k]; - iExt[0][i][k] = interpolate(t); // intensity - iExt[1][i][k] = derivative(t); // derivative - } + */ + for (int k = 0; k < total; k++) { + y0 = intensities[j - 1][k]; + y1 = intensities[j][k]; + d0 = derivatives[j - 1][k]; + d1 = derivatives[j][k]; + iExt[0][i][k] = interpolate(t); // intensity + iExt[1][i][k] = derivative(t); // derivative + } - continue outer; // move to next point t + continue outer; // move to next point t - } + } - } + } - for (int k = 0; k < total; k++) { - iExt[0][i][k] = intensities[internalGrid.getDensity()][k]; - iExt[1][i][k] = derivatives[internalGrid.getDensity()][k]; - } + for (int k = 0; k < total; k++) { + iExt[0][i][k] = intensities[internalGrid.getDensity()][k]; + iExt[1][i][k] = derivatives[internalGrid.getDensity()][k]; + } - } + } - return iExt; - } + return iExt; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java index 86a1f672..66cf0f03 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java @@ -8,102 +8,108 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; /** - * Used to iteratively solve the radiative transfer problem.

This is necessary since the latter can - * only be solved separately for rays travelling in the positive or negative hemispheres. The problem - * lies in the fact that the intensities of incoming and outward rays are coupled. The only way to - * solve the coupled set of two Cauchy problems is iterative in nature.

- *

This abstract class only defines the basic functionaly, its implementation is defined by the - * subclasses.

+ * Used to iteratively solve the radiative transfer problem. + *

+ * This is necessary since the latter can only be solved separately for rays + * travelling in the positive or negative hemispheres. The problem lies in the + * fact that the intensities of incoming and outward rays are coupled. The only + * way to solve the coupled set of two Cauchy problems is iterative in + * nature.

+ *

+ * This abstract class only defines the basic functionaly, its implementation is + * defined by the subclasses.

* */ - public abstract class IterativeSolver extends PropertyHolder implements Reflexive { - private double iterationError; - private int maxIterations; - - /** - * Constructs an {@code IterativeSolver} with the default thresholds for - * iteration error and number of iterations. - */ - - public IterativeSolver() { - iterationError = (double) def(DOM_ITERATION_ERROR).getValue(); - maxIterations = (int) def(RTE_MAX_ITERATIONS).getValue(); - } - - /** - * De-facto solves the radiative transfer problem iteratively. - * @param integrator the integerator embedded in the iterative approach - * @return a calculation status - */ - - public abstract RTECalculationStatus doIterations(AdaptiveIntegrator integrator); - - protected RTECalculationStatus sanityCheck(RTECalculationStatus status, int iterations) { - return iterations < maxIterations ? status : ITERATION_LIMIT_REACHED; - } - - public NumericProperty getIterationErrorTolerance() { - return derive(DOM_ITERATION_ERROR, this.iterationError); - } - - public void setIterationErrorTolerance(NumericProperty e) { - if (e.getType() != DOM_ITERATION_ERROR) - throw new IllegalArgumentException("Illegal type: " + e.getType()); - this.iterationError = (double) e.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case DOM_ITERATION_ERROR: - setIterationErrorTolerance(property); - break; - case RTE_MAX_ITERATIONS: - setMaxIterations(property); - break; - default: - return; - } - - firePropertyChanged(this, property); - - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(DOM_ITERATION_ERROR)); - list.add(def(RTE_MAX_ITERATIONS)); - return list; - } - - public NumericProperty getMaxIterations() { - return derive(RTE_MAX_ITERATIONS, maxIterations); - } - - public void setMaxIterations(NumericProperty iterations) { - if (iterations.getType() == RTE_MAX_ITERATIONS) - this.maxIterations = (int) iterations.getValue(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + this.getIterationErrorTolerance(); - } - - public double getIterationError() { - return iterationError; - } - -} \ No newline at end of file + private double iterationError; + private int maxIterations; + + /** + * Constructs an {@code IterativeSolver} with the default thresholds for + * iteration error and number of iterations. + */ + public IterativeSolver() { + iterationError = (double) def(DOM_ITERATION_ERROR).getValue(); + maxIterations = (int) def(RTE_MAX_ITERATIONS).getValue(); + } + + /** + * De-facto solves the radiative transfer problem iteratively. + * + * @param integrator the integerator embedded in the iterative approach + * @return a calculation status + */ + public abstract RTECalculationStatus doIterations(AdaptiveIntegrator integrator); + + protected RTECalculationStatus sanityCheck(RTECalculationStatus status, int iterations) { + return iterations < maxIterations ? status : ITERATION_LIMIT_REACHED; + } + + public NumericProperty getIterationErrorTolerance() { + return derive(DOM_ITERATION_ERROR, this.iterationError); + } + + public void setIterationErrorTolerance(NumericProperty e) { + if (e.getType() != DOM_ITERATION_ERROR) { + throw new IllegalArgumentException("Illegal type: " + e.getType()); + } + this.iterationError = (double) e.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case DOM_ITERATION_ERROR: + setIterationErrorTolerance(property); + break; + case RTE_MAX_ITERATIONS: + setMaxIterations(property); + break; + default: + return; + } + + firePropertyChanged(this, property); + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DOM_ITERATION_ERROR); + set.add(RTE_MAX_ITERATIONS); + return set; + } + + public NumericProperty getMaxIterations() { + return derive(RTE_MAX_ITERATIONS, maxIterations); + } + + public void setMaxIterations(NumericProperty iterations) { + if (iterations.getType() == RTE_MAX_ITERATIONS) { + this.maxIterations = (int) iterations.getValue(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + this.getIterationErrorTolerance(); + } + + public double getIterationError() { + return iterationError; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java index dbecad8d..dc277010 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java @@ -6,31 +6,30 @@ * The linear-anisotropic scattering phase function. * */ - public class LinearAnisotropicPF extends PhaseFunction { - private double g; - - public LinearAnisotropicPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); - } - - @Override - public void init(ParticipatingMedium medium) { - super.init(medium); - g = 3.0 * getAnisotropyFactor(); - } - - @Override - public double partialSum(final int i, final int j, final int n1, final int n2Exclusive) { - final var intensities = getDiscreteIntensities(); - return intensities.incidentRadiation(j, n1, n2Exclusive) + g * intensities.getOrdinates().getNode(i) * intensities.flux(j, n1, n2Exclusive); - } - - @Override - public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - return 1.0 + g * ordinates.getNode(i) * ordinates.getNode(k); - } - -} \ No newline at end of file + private double g; + + public LinearAnisotropicPF(ParticipatingMedium medium, Discretisation intensities) { + super(medium, intensities); + } + + @Override + public void init(ParticipatingMedium medium) { + super.init(medium); + g = 3.0 * getAnisotropyFactor(); + } + + @Override + public double partialSum(final int i, final int j, final int n1, final int n2Exclusive) { + final var intensities = getDiscreteIntensities(); + return intensities.incidentRadiation(j, n1, n2Exclusive) + g * intensities.getOrdinates().getNode(i) * intensities.flux(j, n1, n2Exclusive); + } + + @Override + public double function(final int i, final int k) { + final var ordinates = getDiscreteIntensities().getOrdinates(); + return 1.0 + g * ordinates.getNode(i) * ordinates.getNode(k); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java index ed0fc40b..4c6680a8 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -9,130 +9,130 @@ public abstract class ODEIntegrator extends PropertyHolder implements Reflexive { - private Discretisation discretisation; - private PhaseFunction pf; - private BlackbodySpectrum spectrum; + private Discretisation discretisation; + private PhaseFunction pf; + private BlackbodySpectrum spectrum; - public ODEIntegrator(Discretisation intensities) { - setDiscretisation(intensities); - } + public ODEIntegrator(Discretisation intensities) { + setDiscretisation(intensities); + } - public abstract RTECalculationStatus integrate(); + public abstract RTECalculationStatus integrate(); - protected void init(ParticipatingMedium problem) { - discretisation.setEmissivity((double)problem.getProperties().getEmissivity().getValue()); - var properties = (ThermoOpticalProperties)problem.getProperties(); - discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); - setEmissionFunction(new BlackbodySpectrum(problem)); - } + protected void init(ParticipatingMedium problem) { + discretisation.setEmissivity((double) problem.getProperties().getEmissivity().getValue()); + var properties = (ThermoOpticalProperties) problem.getProperties(); + discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); + setEmissionFunction(new BlackbodySpectrum(problem)); + } - protected void treatZeroIndex() { - var ordinates = discretisation.getOrdinates(); + protected void treatZeroIndex() { + var ordinates = discretisation.getOrdinates(); - if (ordinates.hasZeroNode()) { + if (ordinates.hasZeroNode()) { - var grid = discretisation.getGrid(); - var quantities = discretisation.getQuantities(); - double denominator = 0; - final double halfAlbedo = pf.getHalfAlbedo(); + var grid = discretisation.getGrid(); + var quantities = discretisation.getQuantities(); + double denominator = 0; + final double halfAlbedo = pf.getHalfAlbedo(); - // loop through the spatial indices - for (int j = 0; j < grid.getDensity() + 1; j++) { + // loop through the spatial indices + for (int j = 0; j < grid.getDensity() + 1; j++) { - // solve I_k = S_k for mu[k] = 0 - denominator = 1.0 - halfAlbedo * ordinates.getWeight(0) * pf.function(0, 0); - quantities.setIntensity(j, 0, - (emission(grid.getNode(j)) + halfAlbedo * pf.sumExcludingIndex(0, j, 0)) / denominator); + // solve I_k = S_k for mu[k] = 0 + denominator = 1.0 - halfAlbedo * ordinates.getWeight(0) * pf.function(0, 0); + quantities.setIntensity(j, 0, + (emission(grid.getNode(j)) + halfAlbedo * pf.sumExcludingIndex(0, j, 0)) / denominator); - } - } + } + } - } + } - public double derivative(int i, int j, double t, double I) { - return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, j, t, I) - I); - } + public double derivative(int i, int j, double t, double I) { + return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, j, t, I) - I); + } - public double derivative(int i, double t, double[] out, double[] in, int l1, int l2) { - return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, out, in, t, l1, l2) - out[i - l1]); - } + public double derivative(int i, double t, double[] out, double[] in, int l1, int l2) { + return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, out, in, t, l1, l2) - out[i - l1]); + } - public double partial(int i, double t, double[] inward, int l1, int l2) { - return (emission(t) + pf.getHalfAlbedo() * pf.inwardPartialSum(i, inward, l1, l2)) - / discretisation.getOrdinates().getNode(i); - } + public double partial(int i, double t, double[] inward, int l1, int l2) { + return (emission(t) + pf.getHalfAlbedo() * pf.inwardPartialSum(i, inward, l1, l2)) + / discretisation.getOrdinates().getNode(i); + } - public double partial(int i, int j, double t, int l1, int l2) { - return (emission(t) + pf.getHalfAlbedo() * pf.partialSum(i, j, l1, l2)) - / discretisation.getOrdinates().getNode(i); - } + public double partial(int i, int j, double t, int l1, int l2) { + return (emission(t) + pf.getHalfAlbedo() * pf.partialSum(i, j, l1, l2)) + / discretisation.getOrdinates().getNode(i); + } - public double source(int i, int j, double t, double I) { - return emission(t) + pf.getHalfAlbedo() - * (pf.sumExcludingIndex(i, j, i) + pf.function(i, i) * discretisation.getOrdinates().getWeight(i) * I); - } + public double source(int i, int j, double t, double I) { + return emission(t) + pf.getHalfAlbedo() + * (pf.sumExcludingIndex(i, j, i) + pf.function(i, i) * discretisation.getOrdinates().getWeight(i) * I); + } - public double source(final int i, final double[] iOut, final double[] iIn, final double t, final int l1, - final int l2) { + public double source(final int i, final double[] iOut, final double[] iIn, final double t, final int l1, + final int l2) { - double sumOut = 0; - final var ordinates = discretisation.getOrdinates(); + double sumOut = 0; + final var ordinates = discretisation.getOrdinates(); - for (int l = l1; l < l2; l++) { - // sum over the OUTWARD intensities iOut - sumOut += iOut[l - l1] * ordinates.getWeight(l) * pf.function(i, l); - } + for (int l = l1; l < l2; l++) { + // sum over the OUTWARD intensities iOut + sumOut += iOut[l - l1] * ordinates.getWeight(l) * pf.function(i, l); + } - double sumIn = 0; + double sumIn = 0; - for (int start = ordinates.getTotalNodes() - l2, l = start, end = ordinates.getTotalNodes() - - l1; l < end; l++) { - // sum over the INWARD - // intensities iIn - sumIn += iIn[l - start] * ordinates.getWeight(l) * pf.function(i, l); - } + for (int start = ordinates.getTotalNodes() - l2, l = start, end = ordinates.getTotalNodes() + - l1; l < end; l++) { + // sum over the INWARD + // intensities iIn + sumIn += iIn[l - start] * ordinates.getWeight(l) * pf.function(i, l); + } - return emission(t) + pf.getHalfAlbedo() * (sumIn + sumOut); // contains sum over the incoming rays + return emission(t) + pf.getHalfAlbedo() * (sumIn + sumOut); // contains sum over the incoming rays - } + } - public double emission(double t) { - return (1.0 - 2.0 * pf.getHalfAlbedo()) * spectrum.radianceAt(t); - } + public double emission(double t) { + return (1.0 - 2.0 * pf.getHalfAlbedo()) * spectrum.radianceAt(t); + } - public PhaseFunction getPhaseFunction() { - return pf; - } + public PhaseFunction getPhaseFunction() { + return pf; + } - protected void setPhaseFunction(PhaseFunction pf) { - this.pf = pf; - } + protected void setPhaseFunction(PhaseFunction pf) { + this.pf = pf; + } - @Override - public String getDescriptor() { - return "Numeric integrator"; - } + @Override + public String getDescriptor() { + return "Numeric integrator"; + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); + } - public Discretisation getDiscretisation() { - return discretisation; - } + public Discretisation getDiscretisation() { + return discretisation; + } - public void setDiscretisation(Discretisation discretisation) { - this.discretisation = discretisation; - discretisation.setParent(this); - } + public void setDiscretisation(Discretisation discretisation) { + this.discretisation = discretisation; + discretisation.setParent(this); + } - public BlackbodySpectrum getEmissionFunction() { - return spectrum; - } + public BlackbodySpectrum getEmissionFunction() { + return spectrum; + } - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.spectrum = emissionFunction; - } + public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.spectrum = emissionFunction; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java index 60734ee6..f6c922b5 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java @@ -7,107 +7,108 @@ import pulse.util.Descriptive; /** - * A fixed set of discrete cosine nodes and weights for the angular discretisation of a radiative - * transfer equation. + * A fixed set of discrete cosine nodes and weights for the angular + * discretisation of a radiative transfer equation. * */ - public class OrdinateSet implements Descriptive { - private double[] mu; - private double[] w; + private double[] mu; + private double[] w; + + private int firstPositiveNode; + private int firstNegativeNode; + private int totalNodes; + + public final static String DEFAULT_SET = "G8M"; + private String name; - private int firstPositiveNode; - private int firstNegativeNode; - private int totalNodes; + public OrdinateSet(String name, double[] mu, double[] w) { + if (mu.length != w.length) { + throw new IllegalArgumentException("Arrays sizes do not match: " + mu.length + " != " + w.length); + } - public final static String DEFAULT_SET = "G8M"; - private String name; + setName(name); + this.mu = mu; + this.w = w; + totalNodes = mu.length; - public OrdinateSet(String name, double[] mu, double[] w) { - if (mu.length != w.length) - throw new IllegalArgumentException("Arrays sizes do not match: " + mu.length + " != " + w.length); + checkWeights(); - setName(name); - this.mu = mu; - this.w = w; - totalNodes = mu.length; + firstPositiveNode = hasZeroNode() ? 1 : 0; //zero node should always be the first one + firstNegativeNode = totalNodes / 2 + (hasZeroNode() ? 1 : 0); - checkWeights(); + } - firstPositiveNode = hasZeroNode() ? 1 : 0; //zero node should always be the first one - firstNegativeNode = totalNodes / 2 + (hasZeroNode() ? 1 : 0); - - } + @Override + public String toString() { + return this.getName(); + } - @Override - public String toString() { - return this.getName(); - } + public String printOrdinateSet() { + var sb = new StringBuilder(); - public String printOrdinateSet() { - var sb = new StringBuilder(); + sb.append("Quadrature set: " + this.getName()); + sb.append(System.lineSeparator()); - sb.append("Quadrature set: " + this.getName()); - sb.append(System.lineSeparator()); + for (int i = 0; i < mu.length; i++) { + sb.append(String.format("%nmu[%1d] = %3.8f; w[%1d] = %3.8f", i, mu[i], i, w[i])); + } - for (int i = 0; i < mu.length; i++) { - sb.append(String.format("%nmu[%1d] = %3.8f; w[%1d] = %3.8f", i, mu[i], i, w[i])); - } + return sb.toString(); - return sb.toString(); + } - } + public boolean hasZeroNode() { + return Arrays.stream(mu).anyMatch(Double.valueOf(0.0)::equals); + } - public boolean hasZeroNode() { - return Arrays.stream(mu).anyMatch(Double.valueOf(0.0)::equals); - } + public int getFirstPositiveNode() { + return firstPositiveNode; + } - public int getFirstPositiveNode() { - return firstPositiveNode; - } + public int getFirstNegativeNode() { + return firstNegativeNode; + } - public int getFirstNegativeNode() { - return firstNegativeNode; - } + public String getName() { + return name; + } - public String getName() { - return name; - } + public void setName(String name) { + this.name = name; + } - public void setName(String name) { - this.name = name; - } + @Override + public String describe() { + return "Ordinate set"; + } - @Override - public String describe() { - return "Ordinate set"; - } + public int getNumberOfNodes() { + return totalNodes; + } - public int getNumberOfNodes() { - return totalNodes; - } + public int getTotalNodes() { + return totalNodes; + } - public int getTotalNodes() { - return totalNodes; - } + public double getNode(int i) { + return mu[i]; + } - public double getNode(int i) { - return mu[i]; - } + public double getWeight(int i) { + return w[i]; + } - public double getWeight(int i) { - return w[i]; - } + public int getHalfLength() { + return firstNegativeNode - firstPositiveNode; + } - public int getHalfLength() { - return firstNegativeNode - firstPositiveNode; - } + private void checkWeights() { + final double sum = Arrays.stream(w).sum(); + if (!approximatelyEquals(sum, 2.0)) { + throw new IllegalStateException("Summed quadrature weights != 2.0"); + } + } - private void checkWeights() { - final double sum = Arrays.stream(w).sum(); - if (!approximatelyEquals(sum, 2.0)) - throw new IllegalStateException("Summed quadrature weights != 2.0"); - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index b5a6a034..6ed02e47 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -6,68 +6,68 @@ public abstract class PhaseFunction implements Reflexive { - private Discretisation intensities; - private double anisotropy; - private double halfAlbedo; - - public PhaseFunction(ParticipatingMedium medium, Discretisation intensities) { - this.intensities = intensities; - init(medium); - } - - public double fullSum(int i, int j) { - return partialSum(i, j, 0, intensities.getOrdinates().getTotalNodes()); - } - - public double sumExcludingIndex(int i, int j, int index) { - return partialSum(i, j, 0, index) + partialSum(i, j, index + 1, intensities.getOrdinates().getTotalNodes()); - } - - public double partialSum(int i, int j, int startInclusive, int endExclusive) { - double result = 0; - final var ordinates = intensities.getOrdinates(); - final var quantities = intensities.getQuantities(); - - for (int k = startInclusive; k < endExclusive; k++) { - result += ordinates.getWeight(k) * quantities.getIntensity(j, k) * function(i, k); - } - return result; - } - - public double inwardPartialSum(int i, double[] inward, int kStart, int kEndExclusive) { - double result = 0; - final var ordinates = intensities.getOrdinates(); - - for (int k = kStart; k < kEndExclusive; k++) { - result += ordinates.getWeight(k) * inward[k - kStart] * function(i, k); - } - - return result; - } - - public abstract double function(int i, int k); - - public double getAnisotropyFactor() { - return anisotropy; - } - - protected Discretisation getDiscreteIntensities() { - return intensities; - } - - public void init(ParticipatingMedium problem) { - var properties = (ThermoOpticalProperties)problem.getProperties(); - this.anisotropy = (double) properties.getScatteringAnisostropy().getValue(); - this.halfAlbedo = 0.5 * (double) properties.getScatteringAlbedo().getValue(); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - public double getHalfAlbedo() { - return halfAlbedo; - } - -} \ No newline at end of file + private Discretisation intensities; + private double anisotropy; + private double halfAlbedo; + + public PhaseFunction(ParticipatingMedium medium, Discretisation intensities) { + this.intensities = intensities; + init(medium); + } + + public double fullSum(int i, int j) { + return partialSum(i, j, 0, intensities.getOrdinates().getTotalNodes()); + } + + public double sumExcludingIndex(int i, int j, int index) { + return partialSum(i, j, 0, index) + partialSum(i, j, index + 1, intensities.getOrdinates().getTotalNodes()); + } + + public double partialSum(int i, int j, int startInclusive, int endExclusive) { + double result = 0; + final var ordinates = intensities.getOrdinates(); + final var quantities = intensities.getQuantities(); + + for (int k = startInclusive; k < endExclusive; k++) { + result += ordinates.getWeight(k) * quantities.getIntensity(j, k) * function(i, k); + } + return result; + } + + public double inwardPartialSum(int i, double[] inward, int kStart, int kEndExclusive) { + double result = 0; + final var ordinates = intensities.getOrdinates(); + + for (int k = kStart; k < kEndExclusive; k++) { + result += ordinates.getWeight(k) * inward[k - kStart] * function(i, k); + } + + return result; + } + + public abstract double function(int i, int k); + + public double getAnisotropyFactor() { + return anisotropy; + } + + protected Discretisation getDiscreteIntensities() { + return intensities; + } + + public void init(ParticipatingMedium problem) { + var properties = (ThermoOpticalProperties) problem.getProperties(); + this.anisotropy = (double) properties.getScatteringAnisostropy().getValue(); + this.halfAlbedo = 0.5 * (double) properties.getScatteringAlbedo().getValue(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public double getHalfAlbedo() { + return halfAlbedo; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java index 7a26d5f5..335d1ca1 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java @@ -7,145 +7,150 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.util.PropertyHolder; public class StretchedGrid extends PropertyHolder { - private double[] nodes; - - private double stretchingFactor; - private double dimension; - - /** - * Constructs a uniform grid where the dimension is set to the argument. The stretching factor - * and grid density are set to a default value. - * @param dimension the dimension of the grid - */ - - public StretchedGrid(double dimension) { - this(def(DOM_GRID_DENSITY), dimension, def(GRID_STRETCHING_FACTOR), true); - } - - /** - * Constructs a non-uniform grid where the dimension and the grid density are specified by the arguments. The stretching factor - * and grid density are set to a default value. - * @param gridDensity the grid density, which is a property of the {@code DOM_GRID_DENSITY} type - * @param dimension the dimension of the grid - */ - - public StretchedGrid(NumericProperty gridDensity, double dimension) { - this(gridDensity, dimension, def(GRID_STRETCHING_FACTOR), false); - } - - protected StretchedGrid(NumericProperty gridDensity, double dimension, NumericProperty stretchingFactor, boolean uniform) { - this.stretchingFactor = (double) stretchingFactor.getValue(); - this.dimension = dimension; - int n = (int) gridDensity.getValue(); - if (uniform) - generateUniformBase(n, true); - else - generate(n); - } - - public void generate(int n) { - generateUniformBase(n, false); - - // apply stretching function - - for (int i = 0; i < nodes.length; i++) { - nodes[i] = 0.5 * dimension * tanh(nodes[i], stretchingFactor); - } - - } - - public void generateUniform(boolean scaled) { - int n1 = (int) def(DOM_GRID_DENSITY).getValue(); - generateUniformBase(n1, scaled); - } - - public void generateUniformBase(int n, boolean scaled) { - nodes = new double[n + 1]; - double h = (scaled ? dimension : 1.0) / n; - - for (int i = 0; i < nodes.length; i++) { - nodes[i] = i * h; - } - - } - - public int getDensity() { - return nodes.length - 1; - } - - public NumericProperty getStretchingFactor() { - return derive(GRID_STRETCHING_FACTOR, stretchingFactor); - } - - public void setStretchingFactor(NumericProperty p) { - if (p.getType() != GRID_STRETCHING_FACTOR) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.stretchingFactor = (double) p.getValue(); - } - - public double getDimension() { - return dimension; - } - - public double getNode(int i) { - return nodes[i]; - } - - public double[] getNodes() { - return nodes; - } - - public double step(int i, double sign) { - return nodes[i + (int) ((1. + sign) * 0.5)] - nodes[i - (int) ((1. - sign) * 0.5)]; - } - - public double stepLeft(int i) { - return nodes[i] - nodes[i - 1]; - } - - public double stepRight(int i) { - return nodes[i + 1] - nodes[i]; - } - - public double tanh(final double x, final double stretchingFactor) { - return 1.0 - Math.tanh(stretchingFactor * (1.0 - 2.0 * x)) / Math.tanh(stretchingFactor); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case GRID_STRETCHING_FACTOR: - setStretchingFactor(property); - break; - default: - throw new IllegalArgumentException("Unknown type: " + type); - } - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(GRID_STRETCHING_FACTOR)); - list.add(def(DOM_GRID_DENSITY)); - return list; - } - - @Override - public String toString() { - return "{ " + derive(DOM_GRID_DENSITY, getDensity()) + " ; " + getStretchingFactor() + " }"; - } - - @Override - public String getDescriptor() { - return "Adaptive grid"; - } - -} \ No newline at end of file + private double[] nodes; + + private double stretchingFactor; + private double dimension; + + /** + * Constructs a uniform grid where the dimension is set to the argument. The + * stretching factor and grid density are set to a default value. + * + * @param dimension the dimension of the grid + */ + public StretchedGrid(double dimension) { + this(def(DOM_GRID_DENSITY), dimension, def(GRID_STRETCHING_FACTOR), true); + } + + /** + * Constructs a non-uniform grid where the dimension and the grid density + * are specified by the arguments. The stretching factor and grid density + * are set to a default value. + * + * @param gridDensity the grid density, which is a property of the + * {@code DOM_GRID_DENSITY} type + * @param dimension the dimension of the grid + */ + public StretchedGrid(NumericProperty gridDensity, double dimension) { + this(gridDensity, dimension, def(GRID_STRETCHING_FACTOR), false); + } + + protected StretchedGrid(NumericProperty gridDensity, double dimension, NumericProperty stretchingFactor, boolean uniform) { + this.stretchingFactor = (double) stretchingFactor.getValue(); + this.dimension = dimension; + int n = (int) gridDensity.getValue(); + if (uniform) { + generateUniformBase(n, true); + } else { + generate(n); + } + } + + public void generate(int n) { + generateUniformBase(n, false); + + // apply stretching function + for (int i = 0; i < nodes.length; i++) { + nodes[i] = 0.5 * dimension * tanh(nodes[i], stretchingFactor); + } + + } + + public void generateUniform(boolean scaled) { + int n1 = (int) def(DOM_GRID_DENSITY).getValue(); + generateUniformBase(n1, scaled); + } + + public void generateUniformBase(int n, boolean scaled) { + nodes = new double[n + 1]; + double h = (scaled ? dimension : 1.0) / n; + + for (int i = 0; i < nodes.length; i++) { + nodes[i] = i * h; + } + + } + + public int getDensity() { + return nodes.length - 1; + } + + public NumericProperty getStretchingFactor() { + return derive(GRID_STRETCHING_FACTOR, stretchingFactor); + } + + public void setStretchingFactor(NumericProperty p) { + if (p.getType() != GRID_STRETCHING_FACTOR) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.stretchingFactor = (double) p.getValue(); + } + + public double getDimension() { + return dimension; + } + + public double getNode(int i) { + return nodes[i]; + } + + public double[] getNodes() { + return nodes; + } + + public double step(int i, double sign) { + return nodes[i + (int) ((1. + sign) * 0.5)] - nodes[i - (int) ((1. - sign) * 0.5)]; + } + + public double stepLeft(int i) { + return nodes[i] - nodes[i - 1]; + } + + public double stepRight(int i) { + return nodes[i + 1] - nodes[i]; + } + + public double tanh(final double x, final double stretchingFactor) { + return 1.0 - Math.tanh(stretchingFactor * (1.0 - 2.0 * x)) / Math.tanh(stretchingFactor); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case GRID_STRETCHING_FACTOR: + setStretchingFactor(property); + break; + default: + throw new IllegalArgumentException("Unknown type: " + type); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRID_STRETCHING_FACTOR); + set.add(DOM_GRID_DENSITY); + return set; + } + + @Override + public String toString() { + return "{ " + derive(DOM_GRID_DENSITY, getDensity()) + " ; " + getStretchingFactor() + " }"; + } + + @Override + public String getDescriptor() { + return "Adaptive grid"; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java index 691ea493..a051ce9b 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java @@ -6,117 +6,121 @@ import static pulse.properties.NumericPropertyKeyword.RELAXATION_PARAMETER; import java.util.List; +import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; public class SuccessiveOverrelaxation extends IterativeSolver { - private double W; + private double W; - public SuccessiveOverrelaxation() { - super(); - this.W = (double) def(RELAXATION_PARAMETER).getValue(); - } + public SuccessiveOverrelaxation() { + super(); + this.W = (double) def(RELAXATION_PARAMETER).getValue(); + } - private void successiveOverrelaxation(AdaptiveIntegrator integrator) { + private void successiveOverrelaxation(AdaptiveIntegrator integrator) { - final var intensities = integrator.getDiscretisation(); - final var quantities = intensities.getQuantities(); - final int density = intensities.getGrid().getDensity(); - final int total = intensities.getOrdinates().getTotalNodes(); + final var intensities = integrator.getDiscretisation(); + final var quantities = intensities.getQuantities(); + final int density = intensities.getGrid().getDensity(); + final int total = intensities.getOrdinates().getTotalNodes(); - final double ONE_MINUS_W = 1.0 - W; + final double ONE_MINUS_W = 1.0 - W; - for (int i = 0; i < density + 1; i++) { - for (int j = 0; j < total; j++) { - quantities.setIntensity(i, j, - ONE_MINUS_W * quantities.getStoredIntensity(i, j) + W * quantities.getIntensity(i, j)); - quantities.setStoredDerivative(i, j, - ONE_MINUS_W * quantities.getStoredDerivative(i, j) + W * quantities.getDerivative(i, j)); - } - } + for (int i = 0; i < density + 1; i++) { + for (int j = 0; j < total; j++) { + quantities.setIntensity(i, j, + ONE_MINUS_W * quantities.getStoredIntensity(i, j) + W * quantities.getIntensity(i, j)); + quantities.setStoredDerivative(i, j, + ONE_MINUS_W * quantities.getStoredDerivative(i, j) + W * quantities.getDerivative(i, j)); + } + } - } + } - @Override - public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { + @Override + public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { - var discrete = integrator.getDiscretisation(); - var quantities = discrete.getQuantities(); - double relativeError = 100; + var discrete = integrator.getDiscretisation(); + var quantities = discrete.getQuantities(); + double relativeError = 100; - double qld = 0; - double qrd = 0; - double qsum; + double qld = 0; + double qrd = 0; + double qsum; - int iterations = 0; - final var ef = integrator.getEmissionFunction(); - RTECalculationStatus status = RTECalculationStatus.NORMAL; + int iterations = 0; + final var ef = integrator.getEmissionFunction(); + RTECalculationStatus status = RTECalculationStatus.NORMAL; - for (double ql = 1e8, qr = ql; relativeError > getIterationError(); status = sanityCheck(status, - ++iterations)) { - quantities.store(); - ql = qld; - qr = qrd; - - status = integrator.integrate(); - - // if the integrator attempted rescaling, last iteration is not valid anymore - if (integrator.wasRescaled()) { - relativeError = Double.POSITIVE_INFINITY; - } else { // calculate the (k+1) iteration as: I_k+1 = I_k * (1 - W) + I* + for (double ql = 1e8, qr = ql; relativeError > getIterationError(); status = sanityCheck(status, + ++iterations)) { + quantities.store(); + ql = qld; + qr = qrd; - // get the difference in boundary heat fluxes - qld = discrete.fluxLeft(ef); - qrd = discrete.fluxRight(ef); - qsum = abs(qld - ql) + abs(qrd - qr); + status = integrator.integrate(); - successiveOverrelaxation(integrator); - relativeError = qsum / ( abs(qld) + abs(qrd) ); - - } + // if the integrator attempted rescaling, last iteration is not valid anymore + if (integrator.wasRescaled()) { + relativeError = Double.POSITIVE_INFINITY; + } else { // calculate the (k+1) iteration as: I_k+1 = I_k * (1 - W) + I* - } + // get the difference in boundary heat fluxes + qld = discrete.fluxLeft(ef); + qrd = discrete.fluxRight(ef); + qsum = abs(qld - ql) + abs(qrd - qr); - return status; + successiveOverrelaxation(integrator); + relativeError = qsum / (abs(qld) + abs(qrd)); - } + } - public NumericProperty getRelaxationParameter() { - return derive(RELAXATION_PARAMETER, W); - } + } - public void setRelaxationParameter(NumericProperty p) { - if (p.getType() != RELAXATION_PARAMETER) - throw new IllegalArgumentException("Unknown type: " + p.getType()); - W = (double) p.getValue(); - } + return status; - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { + } - super.set(type, property); + public NumericProperty getRelaxationParameter() { + return derive(RELAXATION_PARAMETER, W); + } - if (type == RELAXATION_PARAMETER) - setRelaxationParameter(property); - else - throw new IllegalArgumentException("Unknown type: " + type); + public void setRelaxationParameter(NumericProperty p) { + if (p.getType() != RELAXATION_PARAMETER) { + throw new IllegalArgumentException("Unknown type: " + p.getType()); + } + W = (double) p.getValue(); + } - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(RELAXATION_PARAMETER)); - return list; - } + super.set(type, property); - @Override - public String toString() { - return super.toString() + " ; " + getRelaxationParameter(); - } - -} \ No newline at end of file + if (type == RELAXATION_PARAMETER) { + setRelaxationParameter(property); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(RELAXATION_PARAMETER); + return set; + } + + @Override + public String toString() { + return super.toString() + " ; " + getRelaxationParameter(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java index a97aebca..fa1bc4f2 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java @@ -6,24 +6,23 @@ import pulse.problem.schemes.rte.RTECalculationStatus; /** - * TRBDF2 (Trapezoidal Backward Differencing Second Order) Scheme for the solution of one-dimensional radiative transfer problems. - * + * TRBDF2 (Trapezoidal Backward Differencing Second Order) Scheme for the + * solution of one-dimensional radiative transfer problems. + * * @author Artem Lunev, Vadim Zborovskii * */ - public class TRBDF2 extends AdaptiveIntegrator { - /* + /* * Coefficients of the Butcher tableau as originally defined in M.E. Hosea, L.E * Shampine/Applied Numerical Mathematics 20 (1996) 21-37 - */ + */ + private final static double gamma = 2.0 - Math.sqrt(2.0); + private final static double w = Math.sqrt(2.0) / 4.0; + private final static double d = gamma / 2.0; - private final static double gamma = 2.0 - Math.sqrt(2.0); - private final static double w = Math.sqrt(2.0) / 4.0; - private final static double d = gamma / 2.0; - - /* + /* * Uncomment this for coefficients for the error estimator from: Christopher A. * Kennedy, Mark H. Carpenter. Diagonally Implicit Runge-Kutta Methods for * Ordinary Differential Equations. A Review. NASA/TM–2016–219173, p. 72 @@ -33,209 +32,198 @@ public class TRBDF2 extends AdaptiveIntegrator { * g) / (2.0 * g - 1.0); bHat[0] = 1.0 - bHat[1] - bHat[2]; * * - */ - - private static double[] bHat = new double[3]; + */ + private static double[] bHat = new double[3]; - /* + /* * These are the original error estimator coefficients. - */ + */ + static { + bHat[0] = (1.0 - w) / 3.0; + bHat[1] = (3.0 * w + 1.0) / 3.0; + bHat[2] = d / 3.0; + } - static { - bHat[0] = (1.0 - w) / 3.0; - bHat[1] = (3.0 * w + 1.0) / 3.0; - bHat[2] = d / 3.0; - } + private final static double[] bbHat = new double[]{w - bHat[0], w - bHat[1], d - bHat[2]}; - private final static double[] bbHat = new double[] { w - bHat[0], w - bHat[1], d - bHat[2] }; + private double k[][]; - private double k[][]; + private double[] inward; + private double[] bVector; // right-hand side of linear set A * x = B + private double[] est; // error estimator + private double[][] aMatrix; // matrix of linear set A * x = B - private double[] inward; - private double[] bVector; // right-hand side of linear set A * x = B - private double[] est; // error estimator - private double[][] aMatrix; // matrix of linear set A * x = B + private SquareMatrix invA; // inverse matrix + private Vector i2; // second stage (trapezoidal) + private Vector i3; // third stage (backward-difference second order) - private SquareMatrix invA; // inverse matrix - private Vector i2; // second stage (trapezoidal) - private Vector i3; // third stage (backward-difference second order) - - /* + /* * Constants for third-stage calculation - */ - - private double w_d = w / d; - private double _1w_d = (1.0 - w_d); - - public TRBDF2(Discretisation intensities) { - super(intensities); - } - - @Override - public RTECalculationStatus integrate() { - final int nH = getDiscretisation().getOrdinates().getHalfLength(); - - bVector = new double[nH]; - est = new double[nH]; - aMatrix = new double[nH][nH]; - inward = new double[nH]; - - k = new double[3][nH]; - return super.integrate(); - } - - /** - * Generates a non-uniform (stretched at boundaries) grid using the - * argument as the density. - * @param nNew new grid density - */ - - @Override - public void generateGrid(int nNew) { - getDiscretisation().getGrid().generate(nNew); - } - - /** - * Performs a TRBDF2 step. - */ - - @Override - public Vector[] step(final int j, final double sign) { - var intensities = getDiscretisation(); - var quantities = intensities.getQuantities(); - var hermite = getHermiteInterpolator(); - - final double h = sign * intensities.getGrid().step(j, sign); - hermite.bMinusA = h; // <---- for Hermite interpolation - - final int total = getDiscretisation().getOrdinates().getTotalNodes(); - final int increment = (int) (1 * sign); - final double t = intensities.getGrid().getNode(j); - hermite.a = t; // <---- for Hermite interpolation - - /* + */ + private double w_d = w / d; + private double _1w_d = (1.0 - w_d); + + public TRBDF2(Discretisation intensities) { + super(intensities); + } + + @Override + public RTECalculationStatus integrate() { + final int nH = getDiscretisation().getOrdinates().getHalfLength(); + + bVector = new double[nH]; + est = new double[nH]; + aMatrix = new double[nH][nH]; + inward = new double[nH]; + + k = new double[3][nH]; + return super.integrate(); + } + + /** + * Generates a non-uniform (stretched at boundaries) grid using the argument + * as the density. + * + * @param nNew new grid density + */ + @Override + public void generateGrid(int nNew) { + getDiscretisation().getGrid().generate(nNew); + } + + /** + * Performs a TRBDF2 step. + */ + @Override + public Vector[] step(final int j, final double sign) { + var intensities = getDiscretisation(); + var quantities = intensities.getQuantities(); + var hermite = getHermiteInterpolator(); + + final double h = sign * intensities.getGrid().step(j, sign); + hermite.bMinusA = h; // <---- for Hermite interpolation + + final int total = getDiscretisation().getOrdinates().getTotalNodes(); + final int increment = (int) (1 * sign); + final double t = intensities.getGrid().getNode(j); + hermite.a = t; // <---- for Hermite interpolation + + /* * Indices of OUTWARD intensities (n1 <= i < n2) - */ - - final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); - final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); - final int halfLength = nNegativeStart - nPositiveStart; - final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index or first negative - final int n2 = sign > 0 ? nNegativeStart : total; // either first negative index or n - - /* + */ + final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); + final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); + final int halfLength = nNegativeStart - nPositiveStart; + final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index or first negative + final int n2 = sign > 0 ? nNegativeStart : total; // either first negative index or n + + /* * Indices of INWARD intensities (n3 <= i < n4) - */ + */ + final int n3 = total - n2; // either first negative index or 0 (for INWARD intensities) + final int n4 = total - n1; // either n or first negative index (for INWARD intensities) + final int n5 = nNegativeStart - n3; // either 0 or first negative index - final int n3 = total - n2; // either first negative index or 0 (for INWARD intensities) - final int n4 = total - n1; // either n or first negative index (for INWARD intensities) - final int n5 = nNegativeStart - n3; // either 0 or first negative index - - /* + /* * Try to use FSAL - */ - - if (!isFirstRun()) { // if this is not the first step + */ + if (!isFirstRun()) { // if this is not the first step - for (int l = n1; l < n2; l++) { - k[0][l - n1] = quantities.getQLast(l - n1); - } + for (int l = n1; l < n2; l++) { + k[0][l - n1] = quantities.getQLast(l - n1); + } - } else { + } else { - for (int l = n1; l < n2; l++) { - k[0][l - n1] = super.derivative(l, j, t, quantities.getIntensity(j, l)); // first-stage right-hand - // side: f( t, In) - // ) - } // ) + for (int l = n1; l < n2; l++) { + k[0][l - n1] = super.derivative(l, j, t, quantities.getIntensity(j, l)); // first-stage right-hand + // side: f( t, In) + // ) + } // ) - setFirstRun(false); + setFirstRun(false); - } + } - /* + /* * ============================= 1st and 2nd stages begin here * ============================= - */ - - final double hd = h * d; - final double tPlusGamma = t + gamma * h; + */ + final double hd = h * d; + final double tPlusGamma = t + gamma * h; - /* + /* * Interpolate INWARD intensities at t + gamma*h (second stage) - */ - - for (int i = 0; i < inward.length; i++) { - hermite.y0 = quantities.getIntensity(j, i + n3); - hermite.y1 = quantities.getIntensity(j + increment, i + n3); - hermite.d0 = quantities.getDerivative(j, i + n3); - hermite.d1 = quantities.getDerivative(j + increment, i + n3); - inward[i] = hermite.interpolate(tPlusGamma); - } - - /* + */ + for (int i = 0; i < inward.length; i++) { + hermite.y0 = quantities.getIntensity(j, i + n3); + hermite.y1 = quantities.getIntensity(j + increment, i + n3); + hermite.d0 = quantities.getDerivative(j, i + n3); + hermite.d1 = quantities.getDerivative(j + increment, i + n3); + inward[i] = hermite.interpolate(tPlusGamma); + } + + /* * Trapezoidal step - */ + */ + final double prefactorNumerator = -hd * getPhaseFunction().getHalfAlbedo(); - final double prefactorNumerator = -hd * getPhaseFunction().getHalfAlbedo(); - - double matrixPrefactor; - final var ordinates = intensities.getOrdinates(); + double matrixPrefactor; + final var ordinates = intensities.getOrdinates(); - for (int i = 0; i < halfLength; i++) { + for (int i = 0; i < halfLength; i++) { - quantities.setDerivative(j, i + n1, k[0][i]); // store derivatives for Hermite interpolation + quantities.setDerivative(j, i + n1, k[0][i]); // store derivatives for Hermite interpolation - bVector[i] = quantities.getIntensity(j, i + n1) - + hd * (k[0][i] + partial(i + n1, tPlusGamma, inward, n3, n4)); // only - // INWARD - // intensities + bVector[i] = quantities.getIntensity(j, i + n1) + + hd * (k[0][i] + partial(i + n1, tPlusGamma, inward, n3, n4)); // only + // INWARD + // intensities - matrixPrefactor = prefactorNumerator / ordinates.getNode(i + n1); + matrixPrefactor = prefactorNumerator / ordinates.getNode(i + n1); - // all elements - for (int k = 0; k < aMatrix[0].length; k++) { - aMatrix[i][k] = matrixPrefactor * ordinates.getWeight(k + n5) - * getPhaseFunction().function(i + n1, k + n5); // only - // OUTWARD - // (and zero) - // intensities - } + // all elements + for (int k = 0; k < aMatrix[0].length; k++) { + aMatrix[i][k] = matrixPrefactor * ordinates.getWeight(k + n5) + * getPhaseFunction().function(i + n1, k + n5); // only + // OUTWARD + // (and zero) + // intensities + } - // additionally for the diagonal elements - aMatrix[i][i] += 1.0 + hd / ordinates.getNode(i + n1); + // additionally for the diagonal elements + aMatrix[i][i] += 1.0 + hd / ordinates.getNode(i + n1); - } + } - invA = (Matrices.createSquareMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages - i2 = invA.multiply(new Vector(bVector)); // intensity vector at 2nd stage + invA = (Matrices.createSquareMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages + i2 = invA.multiply(new Vector(bVector)); // intensity vector at 2nd stage - /* + /* * ================== Third stage (BDF2) ================== - */ - - final double th = t + h; + */ + final double th = t + h; - for (int i = 0; i < aMatrix.length; i++) { + for (int i = 0; i < aMatrix.length; i++) { - bVector[i] = quantities.getIntensity(j, i + n1) * _1w_d + w_d * i2.get(i) - + hd * partial(i + n1, j + increment, th, n3, n4); // only INWARD intensities at node j + 1 (i.e. no - // interpolation) - k[1][i] = (i2.get(i) - quantities.getIntensity(j, i + n1)) / hd - k[0][i]; + bVector[i] = quantities.getIntensity(j, i + n1) * _1w_d + w_d * i2.get(i) + + hd * partial(i + n1, j + increment, th, n3, n4); // only INWARD intensities at node j + 1 (i.e. no + // interpolation) + k[1][i] = (i2.get(i) - quantities.getIntensity(j, i + n1)) / hd - k[0][i]; - } + } - i3 = invA.multiply(new Vector(bVector)); + i3 = invA.multiply(new Vector(bVector)); - for (int i = 0; i < aMatrix.length; i++) { - k[2][i] = (i3.get(i) - quantities.getIntensity(j, i + n1) - - w_d * (i2.get(i) - quantities.getIntensity(j, i + n1))) / hd; - quantities.setQLast(i, k[2][i]); - est[i] = (bbHat[0] * k[0][i] + bbHat[1] * k[1][i] + bbHat[2] * k[2][i]) * h; - } + for (int i = 0; i < aMatrix.length; i++) { + k[2][i] = (i3.get(i) - quantities.getIntensity(j, i + n1) + - w_d * (i2.get(i) - quantities.getIntensity(j, i + n1))) / hd; + quantities.setQLast(i, k[2][i]); + est[i] = (bbHat[0] * k[0][i] + bbHat[1] * k[1][i] + bbHat[2] * k[2][i]) * h; + } - return new Vector[] { i3, invA.multiply(new Vector(est)) }; + return new Vector[]{i3, invA.multiply(new Vector(est))}; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/package-info.java b/src/main/java/pulse/problem/schemes/rte/dom/package-info.java index 42f5716b..4c5177ea 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/package-info.java @@ -4,5 +4,4 @@ * includes ODE solvers, ordinate set handlers, non-uniform grid, iterative * solvers, and scattering phase functions. */ - -package pulse.problem.schemes.rte.dom; \ No newline at end of file +package pulse.problem.schemes.rte.dom; diff --git a/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java b/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java index f008189b..26b5d0bb 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java @@ -6,79 +6,80 @@ import pulse.problem.schemes.rte.BlackbodySpectrum; /** - * A class for evaluating the definite integral ab f(x) En (α + - * β x) dx}. This integral appears as a result of analytically integrating the radiative transfer equation for an - * absorbing-emitting medium. The number n is the order of this integral, and α and β are the coefficients. + * A class for evaluating the definite integral + * ab f(x) En + * (α + β x) dx}. This integral appears as a + * result of analytically integrating the radiative transfer equation for an + * absorbing-emitting medium. The number n is the order of this integral, + * and α and β are the coefficients. * */ - public abstract class CompositionProduct extends AbstractIntegrator { - private double alpha; - private double beta; - private int order; + private double alpha; + private double beta; + private int order; + + private FunctionWithInterpolation expIntegral; + private BlackbodySpectrum blackbody; + + /** + * Constructs the composition product with the specified integration bounds. + * + * @param bounds integration bounds + */ + public CompositionProduct(Segment bounds) { + super(bounds); + } - private FunctionWithInterpolation expIntegral; - private BlackbodySpectrum blackbody; + /** + * Evaluates the integrand f(x) En + * (α + β x) dx}. + */ + @Override + public double integrand(double... vars) { + return blackbody.powerAt(vars[0]) * expIntegral.valueAt(alpha + beta * vars[0]); + } - /** - * Constructs the composition product with the specified integration bounds. - * @param bounds integration bounds - */ - - public CompositionProduct(Segment bounds) { - super(bounds); - } - - /** - * Evaluates the integrand f(x) En (α + - * β x) dx}. - */ - - @Override - public double integrand(double... vars) { - return blackbody.powerAt(vars[0]) * expIntegral.valueAt(alpha + beta * vars[0]); - } + public BlackbodySpectrum getEmissionFunction() { + return blackbody; + } - public BlackbodySpectrum getEmissionFunction() { - return blackbody; - } + public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.blackbody = emissionFunction; + } - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.blackbody = emissionFunction; - } + public double getBeta() { + return beta; + } - public double getBeta() { - return beta; - } + public double getAlpha() { + return alpha; + } - public double getAlpha() { - return alpha; - } + public void setCoefficients(double alpha, double beta) { + this.alpha = alpha; + this.beta = beta; + } - public void setCoefficients(double alpha, double beta) { - this.alpha = alpha; - this.beta = beta; - } - - public int getOrder() { - return order; - } + public int getOrder() { + return order; + } - /** - * Sets the integration order n. Updates the exponential integral associated with this - * {@code CompositionProduct} upon completion. - * @param order an integer in the range from 1 to 4 inclusively - */ - - public void setOrder(int order) { - this.order = order; - expIntegral = ExponentialIntegrals.get(order); - } + /** + * Sets the integration order n. Updates the exponential integral + * associated with this {@code CompositionProduct} upon completion. + * + * @param order an integer in the range from 1 to 4 inclusively + */ + public void setOrder(int order) { + this.order = order; + expIntegral = ExponentialIntegrals.get(order); + } - @Override - public String getPrefix() { - return "Composition Product Integrator"; - } + @Override + public String getPrefix() { + return "Composition Product Integrator"; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java index 0c06593a..340b5f2f 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java @@ -10,72 +10,70 @@ import pulse.properties.NumericProperty; /** - * A {@code MidpointIntegrator} for calculating the exponential integrals - * of order from 1 through 4. + * A {@code MidpointIntegrator} for calculating the exponential integrals of + * order from 1 through 4. * */ - class ExponentialIntegral extends MidpointIntegrator { - private double t; - private int order; + private double t; + private int order; - private final static double EPS = 1E-10; - private final static int DEFAULT_INTEGRATION_SEGMENTS = 2048; - - /** - * Constructs an {@code ExponentialIntegral} with the specified - * {@code order} and the required number of {@code segments}. - * @param order the order of exponential integral - * @param segments the number of integration segments - */ + private final static double EPS = 1E-10; + private final static int DEFAULT_INTEGRATION_SEGMENTS = 2048; - public ExponentialIntegral(int order, NumericProperty segments) { - super(new Segment(0, 1), segments); // [0, 1] - cosine domain - setOrder(order); - } - - /** - * Constructs an {@code ExponentialIntegral} of the specified order - * with the default number of integration segments. - * @param order the order of exponential integral - */ + /** + * Constructs an {@code ExponentialIntegral} with the specified + * {@code order} and the required number of {@code segments}. + * + * @param order the order of exponential integral + * @param segments the number of integration segments + */ + public ExponentialIntegral(int order, NumericProperty segments) { + super(new Segment(0, 1), segments); // [0, 1] - cosine domain + setOrder(order); + } - public ExponentialIntegral(int order) { - this(order, derive(INTEGRATION_SEGMENTS, DEFAULT_INTEGRATION_SEGMENTS)); - setOrder(order); - } + /** + * Constructs an {@code ExponentialIntegral} of the specified order with the + * default number of integration segments. + * + * @param order the order of exponential integral + */ + public ExponentialIntegral(int order) { + this(order, derive(INTEGRATION_SEGMENTS, DEFAULT_INTEGRATION_SEGMENTS)); + setOrder(order); + } - /** - * Either calls the superclass method or, if the parameter is zero, - * uses the shortcut formula En(0) = 1/(n - 1). - */ - - @Override - public double integrate() { - return t < EPS ? 1.0 / (order - 1.0) : super.integrate(); - } + /** + * Either calls the superclass method or, if the parameter is zero, uses the + * shortcut formula En(0) = 1/(n - 1). + */ + @Override + public double integrate() { + return t < EPS ? 1.0 / (order - 1.0) : super.integrate(); + } - @Override - public double integrand(double... vars) { - final double mu = vars[0]; - return fastPowLoop(mu, order - 2) * exp(-t / mu); - } + @Override + public double integrand(double... vars) { + final double mu = vars[0]; + return fastPowLoop(mu, order - 2) * exp(-t / mu); + } - protected double getParameter() { - return t; - } + protected double getParameter() { + return t; + } - protected void setParameter(double t) { - this.t = t; - } + protected void setParameter(double t) { + this.t = t; + } - public int getOrder() { - return order; - } + public int getOrder() { + return order; + } - public void setOrder(int order) { - this.order = order; - } + public void setOrder(int order) { + this.order = order; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java index 549358e6..461d7738 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java @@ -4,51 +4,50 @@ import pulse.math.Segment; /** - * A factory class for creating and evaluating {@code ExponentialIntegral}s of - * orders from 1 to 4. + * A factory class for creating and evaluating {@code ExponentialIntegral}s of + * orders from 1 to 4. * */ - public class ExponentialIntegrals { - public final static double CUTOFF = 20.0; // corresponds to a precision of 1E-5 - public final static int HIGHEST_ORDER = 4; - - private FunctionWithInterpolation[] exponentialIntegrals = new FunctionWithInterpolation[HIGHEST_ORDER + 1]; - private static ExponentialIntegrals instance = new ExponentialIntegrals(); - - private ExponentialIntegrals() { - final double LOWER_BOUND_E1 = 1E-8; - for (int i = 1; i < HIGHEST_ORDER + 1; i++) { - var ei = new ExponentialIntegral(i); - var min = i == 1 ? LOWER_BOUND_E1 : 0.0; // First-order exponential integral is discontinuous at 0.0, so - // discard this point - - exponentialIntegrals[i] = new FunctionWithInterpolation(new Segment(min, CUTOFF)) { - - @Override - public double evaluate(double t) { - ei.setParameter(t); - return ei.integrate(); - } - - }; - } - } - - /** - * Retrieves the pre-calculated interpolation functions for the exponential integrals. - * @param order the order (1 to 4) of the exponential integral - * @return a pre-calculated interpolation with the default bounds - */ - - public static FunctionWithInterpolation get(int order) { - return instance.exponentialIntegrals[order]; - } + public final static double CUTOFF = 20.0; // corresponds to a precision of 1E-5 + public final static int HIGHEST_ORDER = 4; + + private FunctionWithInterpolation[] exponentialIntegrals = new FunctionWithInterpolation[HIGHEST_ORDER + 1]; + private static ExponentialIntegrals instance = new ExponentialIntegrals(); + + private ExponentialIntegrals() { + final double LOWER_BOUND_E1 = 1E-8; + for (int i = 1; i < HIGHEST_ORDER + 1; i++) { + var ei = new ExponentialIntegral(i); + var min = i == 1 ? LOWER_BOUND_E1 : 0.0; // First-order exponential integral is discontinuous at 0.0, so + // discard this point + + exponentialIntegrals[i] = new FunctionWithInterpolation(new Segment(min, CUTOFF)) { + + @Override + public double evaluate(double t) { + ei.setParameter(t); + return ei.integrate(); + } + + }; + } + } + + /** + * Retrieves the pre-calculated interpolation functions for the exponential + * integrals. + * + * @param order the order (1 to 4) of the exponential integral + * @return a pre-calculated interpolation with the default bounds + */ + public static FunctionWithInterpolation get(int order) { + return instance.exponentialIntegrals[order]; + } // public static void main(String[] args) { // var ei = get(2); // System.out.println(ei.valueAt(0.01)); // } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java index c063a33e..aaa2c3d1 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -18,80 +18,76 @@ * formulae with the selected numerical quadrature. * */ - public class NonscatteringAnalyticalDerivatives extends NonscatteringRadiativeTransfer { - private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); - - public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { - super(problem, grid); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - } - - /** - * Evaluates fluxes and their derivatives using analytical formulae and the - * selected numerical quadrature. Usually works best with the - * {@code ChandrasekharsQuadrature}. - */ - - @Override - public RTECalculationStatus compute(double U[]) { - super.compute(U); - fluxes(); - var fluxContainer = (FluxesAndExplicitDerivatives) getFluxes(); - IntStream.range(0, fluxContainer.getDensity() + 1) - .forEach(i -> fluxContainer.setFluxDerivative(i, evalFluxDerivative(i))); - - return RTECalculationStatus.NORMAL; - } - - /* + private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); + + public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { + super(problem, grid); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + } + + /** + * Evaluates fluxes and their derivatives using analytical formulae and the + * selected numerical quadrature. Usually works best with the + * {@code ChandrasekharsQuadrature}. + */ + @Override + public RTECalculationStatus compute(double U[]) { + super.compute(U); + fluxes(); + var fluxContainer = (FluxesAndExplicitDerivatives) getFluxes(); + IntStream.range(0, fluxContainer.getDensity() + 1) + .forEach(i -> fluxContainer.setFluxDerivative(i, evalFluxDerivative(i))); + + return RTECalculationStatus.NORMAL; + } + + /* * -dF/d\tau * * = 2 R_1 E_2(y \tau_0) + 2 R_2 E_2( (1 - y) \tau_0 ) - \pi J*(y 'tau_0) * - */ - - private double evalFluxDerivative(final int uIndex) { - double t = opticalCoordinateAt(uIndex); - - double value = getRadiosityFront() * ei2.valueAt(t) - + getRadiosityRear() * ei2.valueAt(getFluxes().getOpticalThickness() - t) - - 2.0 * getEmissionFunction().powerAt(t) + integrateFirstOrder(t); - return 2.0 * value; - } - - private double integrateFirstOrder(final double y) { - double integral = 0; - final double tau0 = getFluxes().getOpticalThickness(); - var quadrature = getQuadrature(); - - setForIntegration(0, y); - quadrature.setCoefficients(y, -1); - integral += compare(y, 0) == 0 ? 0 : quadrature.integrate(); - - setForIntegration(y, tau0); - quadrature.setCoefficients(-y, 1); - integral += compare(y, tau0) == 0 ? 0 : quadrature.integrate(); - - return integral; - } - - /** - * This will set integration bounds by creating a segment using {@code x} and - * {@code y} values. Note this ignores the order of arguments, as the lower and - * upper bound will be equal to {@code min(x,y)} and {@code max(x,y)} - * respectively. The order of integration is set to unity. - * - * @param x lower bound - * @param y upper bound - */ - - private void setForIntegration(final double x, final double y) { - final var quadrature = getQuadrature(); - quadrature.setBounds(new Segment(x, y)); - quadrature.setOrder(1); - } - -} \ No newline at end of file + */ + private double evalFluxDerivative(final int uIndex) { + double t = opticalCoordinateAt(uIndex); + + double value = getRadiosityFront() * ei2.valueAt(t) + + getRadiosityRear() * ei2.valueAt(getFluxes().getOpticalThickness() - t) + - 2.0 * getEmissionFunction().powerAt(t) + integrateFirstOrder(t); + return 2.0 * value; + } + + private double integrateFirstOrder(final double y) { + double integral = 0; + final double tau0 = getFluxes().getOpticalThickness(); + var quadrature = getQuadrature(); + + setForIntegration(0, y); + quadrature.setCoefficients(y, -1); + integral += compare(y, 0) == 0 ? 0 : quadrature.integrate(); + + setForIntegration(y, tau0); + quadrature.setCoefficients(-y, 1); + integral += compare(y, tau0) == 0 ? 0 : quadrature.integrate(); + + return integral; + } + + /** + * This will set integration bounds by creating a segment using {@code x} + * and {@code y} values. Note this ignores the order of arguments, as the + * lower and upper bound will be equal to {@code min(x,y)} and + * {@code max(x,y)} respectively. The order of integration is set to unity. + * + * @param x lower bound + * @param y upper bound + */ + private void setForIntegration(final double x, final double y) { + final var quadrature = getQuadrature(); + quadrature.setBounds(new Segment(x, y)); + quadrature.setOrder(1); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java index fed6f271..b620d91b 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java @@ -12,20 +12,19 @@ * derivatives are calculated using the central-difference approximation. * */ - public class NonscatteringDiscreteDerivatives extends NonscatteringRadiativeTransfer { - public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { - super(problem, grid); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndImplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - } + public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { + super(problem, grid); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndImplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + } - @Override - public RTECalculationStatus compute(double U[]) { - super.compute(U); - fluxes(); - return RTECalculationStatus.NORMAL; - } + @Override + public RTECalculationStatus compute(double U[]) { + super.compute(U); + fluxes(); + return RTECalculationStatus.NORMAL; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java index b5c7db2e..7e725687 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -17,229 +17,219 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSolver { - private static FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); - - private double emissivity; - - private BlackbodySpectrum emissionFunction; - private CompositionProduct quadrature; - - private double radiosityFront; - private double radiosityRear; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Quadrature Selector", CompositionProduct.class); - - protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { - super(); - instanceDescriptor.setSelectedDescriptor(ChandrasekharsQuadrature.class.getSimpleName()); - init(problem, grid); - emissionFunction = new BlackbodySpectrum(problem); - initQuadrature(); - instanceDescriptor.addListener(() -> initQuadrature()); - } - - @Override - public void init(ParticipatingMedium p, Grid grid) { - super.init(p, grid); - emissivity = (double)p.getProperties().getEmissivity().getValue(); - } - - /** - * The superclass method will update the interpolation that the blackbody - * spectrum uses to evaluate the temperature profile and calculate the - * radiosities. A {@code NORMAL} status is always returned. - */ - - @Override - public RTECalculationStatus compute(double[] array) { - emissionFunction.setInterpolation(interpolateTemperatureProfile(array)); - radiosities(); - return RTECalculationStatus.NORMAL; - } - - /** - * Calculates the radiative fluxes on the grid specified in the constructor - * arguments. This uses the values of radiosities and involves calculating the - * composition product using the selected quadratures. - * - * @see pulse.problem.schemes.rte.exact.CompositionProduct - */ - - public void fluxes() { - fluxFront(); - IntStream.range(1, getFluxes().getDensity()).forEach(i -> flux(i)); - fluxRear(); - } - - private void fluxFront() { - final double tau0 = getFluxes().getOpticalThickness(); - double flux = radiosityFront - 2.0 * radiosityRear * ei3.valueAt(tau0) - 2.0 * integrateSecondOrder(0.0, 1.0); - getFluxes().setFlux(0, flux); - } - - /* + private static FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); + + private double emissivity; + + private BlackbodySpectrum emissionFunction; + private CompositionProduct quadrature; + + private double radiosityFront; + private double radiosityRear; + + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Quadrature Selector", CompositionProduct.class); + + protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { + super(); + instanceDescriptor.setSelectedDescriptor(ChandrasekharsQuadrature.class.getSimpleName()); + init(problem, grid); + emissionFunction = new BlackbodySpectrum(problem); + initQuadrature(); + instanceDescriptor.addListener(() -> initQuadrature()); + } + + @Override + public void init(ParticipatingMedium p, Grid grid) { + super.init(p, grid); + emissivity = (double) p.getProperties().getEmissivity().getValue(); + } + + /** + * The superclass method will update the interpolation that the blackbody + * spectrum uses to evaluate the temperature profile and calculate the + * radiosities. A {@code NORMAL} status is always returned. + */ + @Override + public RTECalculationStatus compute(double[] array) { + emissionFunction.setInterpolation(interpolateTemperatureProfile(array)); + radiosities(); + return RTECalculationStatus.NORMAL; + } + + /** + * Calculates the radiative fluxes on the grid specified in the constructor + * arguments. This uses the values of radiosities and involves calculating + * the composition product using the selected quadratures. + * + * @see pulse.problem.schemes.rte.exact.CompositionProduct + */ + public void fluxes() { + fluxFront(); + IntStream.range(1, getFluxes().getDensity()).forEach(i -> flux(i)); + fluxRear(); + } + + private void fluxFront() { + final double tau0 = getFluxes().getOpticalThickness(); + double flux = radiosityFront - 2.0 * radiosityRear * ei3.valueAt(tau0) - 2.0 * integrateSecondOrder(0.0, 1.0); + getFluxes().setFlux(0, flux); + } + + /* * Assumes radiosities have already been calculated using radiosities() F*(1) = * -R_2 + 2R_1 E_3(\tau_0) + 2 int - */ - - private void fluxRear() { - var fluxes = getFluxes(); - final int N = fluxes.getDensity(); - final double tau0 = fluxes.getOpticalThickness(); - double flux = -radiosityRear + 2.0 * radiosityFront * ei3.valueAt(tau0) - + 2.0 * integrateSecondOrder(tau0, -1.0); - fluxes.setFlux(N, flux); - } - - protected void flux(int uIndex) { - final double t = opticalCoordinateAt(uIndex); - final double tau0 = getFluxes().getOpticalThickness(); - - quadrature.setOrder(2); - quadrature.setBounds(new Segment(0, t)); - quadrature.setCoefficients(t, -1.0); - final double I_1 = quadrature.integrate(); - - quadrature.setBounds(new Segment(t, tau0)); - quadrature.setCoefficients(-t, 1.0); - final double I_2 = quadrature.integrate(); - - double result = radiosityFront * ei3.valueAt(t) - radiosityRear * ei3.valueAt(tau0 - t) + I_1 - I_2; - - getFluxes().setFlux(uIndex, result * 2.0); - - } - - /** - * Retrieves the quadrature that is used to evaluate the composition product - * invoked when calculating the radiative fluxes. - * - * @return the quadrature - */ - - public CompositionProduct getQuadrature() { - return quadrature; - } - - /** - * Sets the quadrature and updates its spectral function to that specified by - * this object. - * - * @param specialIntegrator the quadrature used to evaluate the composition - * product - */ - - public void setQuadrature(CompositionProduct specialIntegrator) { - this.quadrature = specialIntegrator; - quadrature.setParent(this); - quadrature.setEmissionFunction(emissionFunction); - } - - public BlackbodySpectrum getEmissionFunction() { - return emissionFunction; - } - - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.emissionFunction = emissionFunction; - quadrature.setEmissionFunction(emissionFunction); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(instanceDescriptor); - return list; - } - - @Override - public String toString() { - return "( " + this.getSimpleName() + " )"; - } - - public InstanceDescriptor getInstanceDescriptor() { - return instanceDescriptor; - } - - @Override - public String getDescriptor() { - return "Non-scattering Radiative Transfer"; - } - - public double getRadiosityFront() { - return radiosityFront; - } - - public double getRadiosityRear() { - return radiosityRear; - } - - /* + */ + private void fluxRear() { + var fluxes = getFluxes(); + final int N = fluxes.getDensity(); + final double tau0 = fluxes.getOpticalThickness(); + double flux = -radiosityRear + 2.0 * radiosityFront * ei3.valueAt(tau0) + + 2.0 * integrateSecondOrder(tau0, -1.0); + fluxes.setFlux(N, flux); + } + + protected void flux(int uIndex) { + final double t = opticalCoordinateAt(uIndex); + final double tau0 = getFluxes().getOpticalThickness(); + + quadrature.setOrder(2); + quadrature.setBounds(new Segment(0, t)); + quadrature.setCoefficients(t, -1.0); + final double I_1 = quadrature.integrate(); + + quadrature.setBounds(new Segment(t, tau0)); + quadrature.setCoefficients(-t, 1.0); + final double I_2 = quadrature.integrate(); + + double result = radiosityFront * ei3.valueAt(t) - radiosityRear * ei3.valueAt(tau0 - t) + I_1 - I_2; + + getFluxes().setFlux(uIndex, result * 2.0); + + } + + /** + * Retrieves the quadrature that is used to evaluate the composition product + * invoked when calculating the radiative fluxes. + * + * @return the quadrature + */ + public CompositionProduct getQuadrature() { + return quadrature; + } + + /** + * Sets the quadrature and updates its spectral function to that specified + * by this object. + * + * @param specialIntegrator the quadrature used to evaluate the composition + * product + */ + public void setQuadrature(CompositionProduct specialIntegrator) { + this.quadrature = specialIntegrator; + quadrature.setParent(this); + quadrature.setEmissionFunction(emissionFunction); + } + + public BlackbodySpectrum getEmissionFunction() { + return emissionFunction; + } + + public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.emissionFunction = emissionFunction; + quadrature.setEmissionFunction(emissionFunction); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public String toString() { + return "( " + this.getSimpleName() + " )"; + } + + public InstanceDescriptor getInstanceDescriptor() { + return instanceDescriptor; + } + + @Override + public String getDescriptor() { + return "Non-scattering Radiative Transfer"; + } + + public double getRadiosityFront() { + return radiosityFront; + } + + public double getRadiosityRear() { + return radiosityRear; + } + + /* * Radiosities of front and rear surfaces respectively in the assumption of * diffuse and opaque boundaries - */ - - private void radiosities() { - final double doubleReflectivity = 2.0 * (1.0 - emissivity); - ; - final double b = b(doubleReflectivity); - final double sq = 1.0 - b * b; - final double a1 = a1(doubleReflectivity); - final double a2 = a2(doubleReflectivity); - - radiosityFront = (a1 + b * a2) / sq; - radiosityRear = (a2 + b * a1) / sq; - } - - /* + */ + private void radiosities() { + final double doubleReflectivity = 2.0 * (1.0 - emissivity); + ; + final double b = b(doubleReflectivity); + final double sq = 1.0 - b * b; + final double a1 = a1(doubleReflectivity); + final double a2 = a2(doubleReflectivity); + + radiosityFront = (a1 + b * a2) / sq; + radiosityRear = (a2 + b * a1) / sq; + } + + /* * Coefficient b - */ + */ + private double b(final double doubleReflectivity) { + return doubleReflectivity * ei3.valueAt(getFluxes().getOpticalThickness()); + } - private double b(final double doubleReflectivity) { - return doubleReflectivity * ei3.valueAt(getFluxes().getOpticalThickness()); - } - - /* + /* * Coefficient a1 * * a1 = \varepsilon*J*(0) + integral = int_0^1 { J*(t) E_2(\tau_0 t) dt } - */ - - private double a1(final double doubleReflectivity) { - return emissivity * emissionFunction.powerAt(0.0) + doubleReflectivity * integrateSecondOrder(0.0, 1.0); - } + */ + private double a1(final double doubleReflectivity) { + return emissivity * emissionFunction.powerAt(0.0) + doubleReflectivity * integrateSecondOrder(0.0, 1.0); + } - /* + /* * Coefficient a2 * * a2 = \varepsilon*J*(0) + ... integral = int_0^1 { J*(t) E_2(\tau_0 t) dt } - */ + */ + private double a2(final double doubleReflectivity) { + final double tau0 = getFluxes().getOpticalThickness(); + return emissivity * emissionFunction.powerAt(tau0) + doubleReflectivity * integrateSecondOrder(tau0, -1.0); + } - private double a2(final double doubleReflectivity) { - final double tau0 = getFluxes().getOpticalThickness(); - return emissivity * emissionFunction.powerAt(tau0) + doubleReflectivity * integrateSecondOrder(tau0, -1.0); - } - - /* + /* * Source function J*(t) = (1 + @U[i]*tFactor)^4, where i = t/hx tFactor = * (tMax/t0) - */ - - private double integrateSecondOrder(double a, double b) { - quadrature.setOrder(2); - quadrature.setBounds(new Segment(0, getFluxes().getOpticalThickness())); - quadrature.setCoefficients(a, b); - return quadrature.integrate(); - } - - private void initQuadrature() { - setQuadrature(instanceDescriptor.newInstance(CompositionProduct.class)); - firePropertyChanged(this, instanceDescriptor); - } - -} \ No newline at end of file + */ + private double integrateSecondOrder(double a, double b) { + quadrature.setOrder(2); + quadrature.setBounds(new Segment(0, getFluxes().getOpticalThickness())); + quadrature.setCoefficients(a, b); + return quadrature.integrate(); + } + + private void initQuadrature() { + setQuadrature(instanceDescriptor.newInstance(CompositionProduct.class)); + firePropertyChanged(this, instanceDescriptor); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/package-info.java b/src/main/java/pulse/problem/schemes/rte/exact/package-info.java index 1937a0ae..86abaeb4 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/package-info.java @@ -1,6 +1,5 @@ /** - * Contains classes for solving the radiative transfer equation in an absorbing-emitting medium (no scattering) + * Contains classes for solving the radiative transfer equation in an absorbing-emitting medium (no scattering) * using a semi-analytical approach. */ - -package pulse.problem.schemes.rte.exact; \ No newline at end of file +package pulse.problem.schemes.rte.exact; diff --git a/src/main/java/pulse/problem/schemes/rte/package-info.java b/src/main/java/pulse/problem/schemes/rte/package-info.java index 1a0c0508..e13033d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/package-info.java @@ -2,5 +2,4 @@ * Contains generic classes that act as a bridge between the specific * implementation of radiative heat transfer and the heat problem solvers. */ - -package pulse.problem.schemes.rte; \ No newline at end of file +package pulse.problem.schemes.rte; diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java index fa2f20f7..1f7a5894 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java @@ -16,75 +16,74 @@ public class ADILayeredSolver extends ADIScheme implements Solver { - public ADILayeredSolver() { - super(); - initGrid(getGrid().getGridDensity(), def(SHELL_GRID_DENSITY), getGrid().getTimeFactor()); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - initGrid(nCore, nShell, timeFactor); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor, - NumericProperty timeLimit) { - setTimeLimit(timeLimit); - initGrid(nCore, nShell, timeFactor); - } - - public void initGrid(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - var map = new HashMap(); - map.put(Location.CORE_X, new Partition((int) nCore.getValue(), 1.0, 0.5)); - map.put(Location.CORE_Y, new Partition((int) nCore.getValue(), 1.0, 0.0)); - map.put(Location.FRONT_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.REAR_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_X, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - setGrid(new LayeredGrid2D(map, timeFactor)); - getGrid().setTimeFactor(timeFactor); - } - - private void prepareGrid(CoreShellProblem problem) { - var layeredGrid = (LayeredGrid2D) getGrid(); // TODO - layeredGrid.getPartition(Location.FRONT_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.REAR_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.SIDE_X).setGridMultiplier(problem.radialFactor()); - } - - @Override - public void solve(CoreShellProblem problem) { - prepareGrid(problem); - - // TODO - - } - - @Override - public DifferenceScheme copy() { - // TODO Auto-generated method stub - return null; - } - - @Override - public Class domain() { - return CoreShellProblem.class; - } - - @Override - public double signal() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void timeStep(int m) { - // TODO Auto-generated method stub - - } - - @Override - public void finaliseStep() { - // TODO Auto-generated method stub - - } - -} \ No newline at end of file + public ADILayeredSolver() { + super(); + initGrid(getGrid().getGridDensity(), def(SHELL_GRID_DENSITY), getGrid().getTimeFactor()); + } + + public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { + initGrid(nCore, nShell, timeFactor); + } + + public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor, + NumericProperty timeLimit) { + setTimeLimit(timeLimit); + initGrid(nCore, nShell, timeFactor); + } + + public void initGrid(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { + var map = new HashMap(); + map.put(Location.CORE_X, new Partition((int) nCore.getValue(), 1.0, 0.5)); + map.put(Location.CORE_Y, new Partition((int) nCore.getValue(), 1.0, 0.0)); + map.put(Location.FRONT_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); + map.put(Location.REAR_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); + map.put(Location.SIDE_X, new Partition((int) nShell.getValue(), 1.0, 0.0)); + map.put(Location.SIDE_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); + setGrid(new LayeredGrid2D(map, timeFactor)); + getGrid().setTimeFactor(timeFactor); + } + + private void prepareGrid(CoreShellProblem problem) { + var layeredGrid = (LayeredGrid2D) getGrid(); // TODO + layeredGrid.getPartition(Location.FRONT_Y).setGridMultiplier(problem.axialFactor()); + layeredGrid.getPartition(Location.REAR_Y).setGridMultiplier(problem.axialFactor()); + layeredGrid.getPartition(Location.SIDE_X).setGridMultiplier(problem.radialFactor()); + } + + @Override + public void solve(CoreShellProblem problem) { + prepareGrid(problem); + + // TODO + } + + @Override + public DifferenceScheme copy() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Class domain() { + return CoreShellProblem.class; + } + + @Override + public double signal() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void timeStep(int m) { + // TODO Auto-generated method stub + + } + + @Override + public void finaliseStep() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index db6e1202..e24120f4 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -15,295 +15,289 @@ * two-dimensional linearised problem. * */ - public class ADILinearisedSolver extends ADIScheme implements Solver { - private TridiagonalMatrixAlgorithm tridiagonal; - - private int N; - private double hx; - private double hy; - private double tau; - private int firstIndex; - private int lastIndex; + private TridiagonalMatrixAlgorithm tridiagonal; + + private int N; + private double hx; + private double hy; + private double tau; + private int firstIndex; + private int lastIndex; + + private double d; + private double l; + private double Bi1; + private double Bi3; + + private double[][] U1; + private double[][] U2; + private double[][] U1_E; + private double[][] U2_E; + + private double[] a1; + private double[] b1; + private double[] c1; + + private double a2; + private double b2; + private double c2; + + private double a11; + private double _a11; + private double b11; + private double _b11; + private double _b12; + private double _c11; + private double HX2; + private double HY2; + + private double C1_U2; + private double C2_U2; + private double C3_U2; + + private double C1_U1; + + private double TAU_HY; + private double OMEGA_SQ_HX2; + private double E_C_U2; + private double E_C_U1; + + private final static double EPS = 1e-8; + + public ADILinearisedSolver() { + super(); + } + + public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + private void prepare(ClassicalProblem2D problem) { + super.prepare(problem); + + var grid = getGrid(); + tridiagonal = new TridiagonalMatrixAlgorithm(grid); + + N = (int) grid.getGridDensity().getValue(); + + hx = grid.getXStep(); + hy = ((Grid2D) getGrid()).getYStep(); + HX2 = hx * hx; + HY2 = hy * hy; + + tau = grid.getTimeStep(); + + var properties = (ExtendedThermalProperties) problem.getProperties(); + + Bi1 = (double) properties.getHeatLoss().getValue(); + Bi3 = (double) properties.getSideLosses().getValue(); + + d = (double) properties.getSampleDiameter().getValue(); + final double fovOuter = (double) properties.getFOVOuter().getValue(); + final double fovInner = (double) properties.getFOVInner().getValue(); + l = (double) properties.getSampleThickness().getValue(); + + // end + U1 = new double[N + 1][N + 1]; + U2 = new double[N + 1][N + 1]; + + U1_E = new double[N + 3][N + 3]; + U2_E = new double[N + 3][N + 3]; + + a1 = new double[N + 1]; + b1 = new double[N + 1]; + c1 = new double[N + 1]; + + // a[i]*u[i-1] - b[i]*u[i] + c[i]*u[i+1] = F[i] + lastIndex = (int) (fovOuter / d / hx); + lastIndex = lastIndex > N ? N : lastIndex; - private double d; - private double l; - private double Bi1; - private double Bi3; + firstIndex = (int) (fovInner / d / hx); + firstIndex = firstIndex < 0 ? 0 : firstIndex; - private double[][] U1; - private double[][] U2; - private double[][] U1_E; - private double[][] U2_E; - - private double[] a1; - private double[] b1; - private double[] c1; - - private double a2; - private double b2; - private double c2; - - private double a11; - private double _a11; - private double b11; - private double _b11; - private double _b12; - private double _c11; - private double HX2; - private double HY2; - - private double C1_U2; - private double C2_U2; - private double C3_U2; - - private double C1_U1; - - private double TAU_HY; - private double OMEGA_SQ_HX2; - private double E_C_U2; - private double E_C_U1; - - private final static double EPS = 1e-8; - - public ADILinearisedSolver() { - super(); - } - - public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(ClassicalProblem2D problem) { - super.prepare(problem); - - var grid = getGrid(); - tridiagonal = new TridiagonalMatrixAlgorithm(grid); - - N = (int) grid.getGridDensity().getValue(); - - hx = grid.getXStep(); - hy = ((Grid2D) getGrid()).getYStep(); - HX2 = hx * hx; - HY2 = hy * hy; - - tau = grid.getTimeStep(); - - var properties = (ExtendedThermalProperties)problem.getProperties(); - - Bi1 = (double) properties.getHeatLoss().getValue(); - Bi3 = (double) properties.getSideLosses().getValue(); - - d = (double) properties.getSampleDiameter().getValue(); - final double fovOuter = (double) properties.getFOVOuter().getValue(); - final double fovInner = (double) properties.getFOVInner().getValue(); - l = (double) properties.getSampleThickness().getValue(); - - // end - - U1 = new double[N + 1][N + 1]; - U2 = new double[N + 1][N + 1]; - - U1_E = new double[N + 3][N + 3]; - U2_E = new double[N + 3][N + 3]; - - a1 = new double[N + 1]; - b1 = new double[N + 1]; - c1 = new double[N + 1]; - - // a[i]*u[i-1] - b[i]*u[i] + c[i]*u[i+1] = F[i] - - lastIndex = (int) (fovOuter / d / hx); - lastIndex = lastIndex > N ? N : lastIndex; - - firstIndex = (int) (fovInner / d / hx); - firstIndex = firstIndex < 0 ? 0 : firstIndex; - - initConst(); - } - - // precalculated FD constants - private void initConst() { - final double OMEGA = 2.0 * l / d; - final double OMEGA_SQ = OMEGA * OMEGA; - - for (int i = 1; i < N + 1; i++) { - a1[i] = OMEGA_SQ * (i - 0.5) / (HX2 * i); - b1[i] = 2. / tau + 2. * OMEGA_SQ / HX2; - c1[i] = OMEGA_SQ * (i + 0.5) / (HX2 * i); - } + initConst(); + } - a2 = 1. / HY2; - b2 = 2. / HY2 + 2. / tau; - c2 = 1. / HY2; + // precalculated FD constants + private void initConst() { + final double OMEGA = 2.0 * l / d; + final double OMEGA_SQ = OMEGA * OMEGA; - // precalc coefs + for (int i = 1; i < N + 1; i++) { + a1[i] = OMEGA_SQ * (i - 0.5) / (HX2 * i); + b1[i] = 2. / tau + 2. * OMEGA_SQ / HX2; + c1[i] = OMEGA_SQ * (i + 0.5) / (HX2 * i); + } - a11 = 1.0 / (1.0 + HX2 / (OMEGA_SQ * tau)); - b11 = 0.5 * tau / (1.0 + OMEGA_SQ * tau / HX2); + a2 = 1. / HY2; + b2 = 2. / HY2 + 2. / tau; + c2 = 1. / HY2; - _a11 = 1.0 / (1.0 + Bi1 * hy + HY2 / tau); - _b11 = 1.0 / ((1 + hy * Bi1) * tau + HY2); - _c11 = 0.5 * HY2 * tau * OMEGA_SQ / HX2; - _b12 = _c11 * _b11; + // precalc coefs + a11 = 1.0 / (1.0 + HX2 / (OMEGA_SQ * tau)); + b11 = 0.5 * tau / (1.0 + OMEGA_SQ * tau / HX2); - C1_U2 = 1.0 + hx * OMEGA * Bi3; - C2_U2 = OMEGA_SQ * tau; - C3_U2 = HX2 * tau / (2.0 * HY2); + _a11 = 1.0 / (1.0 + Bi1 * hy + HY2 / tau); + _b11 = 1.0 / ((1 + hy * Bi1) * tau + HY2); + _c11 = 0.5 * HY2 * tau * OMEGA_SQ / HX2; + _b12 = _c11 * _b11; - C1_U1 = 1.0 + hy * Bi1; - - TAU_HY = tau * hy; - OMEGA_SQ_HX2 = OMEGA_SQ / HX2; - - E_C_U2 = 2.0 * hx * OMEGA * Bi3; - E_C_U1 = 2.0 * hy * Bi1; - } + C1_U2 = 1.0 + hx * OMEGA * Bi3; + C2_U2 = OMEGA_SQ * tau; + C3_U2 = HX2 * tau / (2.0 * HY2); - @Override - public void solve(ClassicalProblem2D problem) { - prepare(problem); - runTimeSequence(problem); - } + C1_U1 = 1.0 + hy * Bi1; - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ADILinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } + TAU_HY = tau * hy; + OMEGA_SQ_HX2 = OMEGA_SQ / HX2; - @Override - public Class domain() { - return ClassicalProblem2D.class; - } + E_C_U2 = 2.0 * hx * OMEGA * Bi3; + E_C_U1 = 2.0 * hy * Bi1; + } - @Override - public double signal() { - double sum = 0; + @Override + public void solve(ClassicalProblem2D problem) { + prepare(problem); + runTimeSequence(problem); + } - for (int i = firstIndex; i <= lastIndex; i++) - sum += U1[i][N]; + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ADILinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } - return sum / (lastIndex - firstIndex + 1); - } + @Override + public Class domain() { + return ClassicalProblem2D.class; + } - public double pulse(final int m, final int i) { - return ((DiscretePulse2D) getDiscretePulse()).evaluateAt((m - EPS) * tau, i * hx); - } + @Override + public double signal() { + double sum = 0; - private void extendedU1(final int m) { - for (int i = 0; i <= N; i++) { + for (int i = firstIndex; i <= lastIndex; i++) { + sum += U1[i][N]; + } - System.arraycopy(U1[i], 0, U1_E[i + 1], 1, N + 1); + return sum / (lastIndex - firstIndex + 1); + } - U1_E[i + 1][0] = U1[i][1] + 2.0 * hy * pulse(m, i) - E_C_U1 * U1[i][0]; - U1_E[i + 1][N + 2] = U1[i][N - 1] - E_C_U1 * U1[i][N]; - } + public double pulse(final int m, final int i) { + return ((DiscretePulse2D) getDiscretePulse()).evaluateAt((m - EPS) * tau, i * hx); + } - } + private void extendedU1(final int m) { + for (int i = 0; i <= N; i++) { - private void extendedU2() { - for (int j = 0; j <= N; j++) { + System.arraycopy(U1[i], 0, U1_E[i + 1], 1, N + 1); - for (int i = 0; i <= N; i++) { - U2_E[i + 1][j + 1] = U2[i][j]; - } + U1_E[i + 1][0] = U1[i][1] + 2.0 * hy * pulse(m, i) - E_C_U1 * U1[i][0]; + U1_E[i + 1][N + 2] = U1[i][N - 1] - E_C_U1 * U1[i][N]; + } - U2_E[N + 2][j + 1] = U2[N - 1][j] - E_C_U2 * U2[N][j]; - } - } + } - private double diff2(double[][] U, final int i, final int j) { - return (U[i][j + 1] - 2. * U1_E[i][j] + U[i][j - 1]); - } + private void extendedU2() { + for (int j = 0; j <= N; j++) { - private double diff2r(double[][] U, final int i, final int j) { - final double C = 1.0 / (2.0 * (i - 1.0)); - return U[i + 1][j] * (1.0 + C) - 2. * U[i][j] + (1.0 - C) * U[i - 1][j]; - } + for (int i = 0; i <= N; i++) { + U2_E[i + 1][j + 1] = U2[i][j]; + } - @Override - public void timeStep(int m) { - var alpha = tridiagonal.getAlpha(); - var beta = tridiagonal.getBeta(); + U2_E[N + 2][j + 1] = U2[N - 1][j] - E_C_U2 * U2[N][j]; + } + } - /* create extended U1 array to accommodate edge values */ + private double diff2(double[][] U, final int i, final int j) { + return (U[i][j + 1] - 2. * U1_E[i][j] + U[i][j - 1]); + } - extendedU1(m); + private double diff2r(double[][] U, final int i, final int j) { + final double C = 1.0 / (2.0 * (i - 1.0)); + return U[i + 1][j] * (1.0 + C) - 2. * U[i][j] + (1.0 - C) * U[i - 1][j]; + } - // first equation, i -> x (radius), j -> y (thickness) + @Override + public void timeStep(int m) { + var alpha = tridiagonal.getAlpha(); + var beta = tridiagonal.getBeta(); - tridiagonal.setAlpha(1, a11); + /* create extended U1 array to accommodate edge values */ + extendedU1(m); - for (int j = 0; j <= N; j++) { + // first equation, i -> x (radius), j -> y (thickness) + tridiagonal.setAlpha(1, a11); - tridiagonal.setBeta(1, b11 * (2. * U1_E[1][j + 1] / tau + diff2(U1_E, 1, j + 1) / HY2)); + for (int j = 0; j <= N; j++) { - for (int i = 1; i < N; i++) { - final double F = -2. * U1_E[i + 1][j + 1] / tau - diff2(U1_E, i + 1, j + 1) / HY2; - final double denominator = b1[i] - a1[i] * alpha[i]; - tridiagonal.setAlpha(i + 1, c1[i] / denominator); - tridiagonal.setBeta(i + 1, (a1[i] * beta[i] - F) / denominator); - } + tridiagonal.setBeta(1, b11 * (2. * U1_E[1][j + 1] / tau + diff2(U1_E, 1, j + 1) / HY2)); - U2[N][j] = (C2_U2 * beta[N] + HX2 * U1_E[N + 1][j + 1] + C3_U2 * diff2(U1_E, N + 1, j + 1)) - / ((C1_U2 - alpha[N]) * C2_U2 + HX2); + for (int i = 1; i < N; i++) { + final double F = -2. * U1_E[i + 1][j + 1] / tau - diff2(U1_E, i + 1, j + 1) / HY2; + final double denominator = b1[i] - a1[i] * alpha[i]; + tridiagonal.setAlpha(i + 1, c1[i] / denominator); + tridiagonal.setBeta(i + 1, (a1[i] * beta[i] - F) / denominator); + } - for (int i = N - 1; i >= 0; i--) { - U2[i][j] = alpha[i + 1] * U2[i + 1][j] + beta[i + 1]; - } + U2[N][j] = (C2_U2 * beta[N] + HX2 * U1_E[N + 1][j + 1] + C3_U2 * diff2(U1_E, N + 1, j + 1)) + / ((C1_U2 - alpha[N]) * C2_U2 + HX2); - } + for (int i = N - 1; i >= 0; i--) { + U2[i][j] = alpha[i + 1] * U2[i + 1][j] + beta[i + 1]; + } - // second equation + } - /* create extended U2 array to accommodate edge values */ + // second equation - extendedU2(); - tridiagonal.setAlpha(1, _a11); + /* create extended U2 array to accommodate edge values */ + extendedU2(); + tridiagonal.setAlpha(1, _a11); - for (int i = 1; i <= N; i++) { + for (int i = 1; i <= N; i++) { - tridiagonal.setBeta(1, - (TAU_HY * pulse(m + 1, i) + HY2 * U2_E[i + 1][1]) * _b11 + _b12 * diff2r(U2_E, i + 1, 1)); + tridiagonal.setBeta(1, + (TAU_HY * pulse(m + 1, i) + HY2 * U2_E[i + 1][1]) * _b11 + _b12 * diff2r(U2_E, i + 1, 1)); - for (int j = 1; j < N; j++) { - final double F = -2. / tau * U2_E[i + 1][j + 1] - OMEGA_SQ_HX2 * diff2r(U2_E, i + 1, j + 1); - final double denominator = b2 - a2 * alpha[j]; - tridiagonal.setAlpha(j + 1, c2 / denominator); - tridiagonal.setBeta(j + 1, (a2 * beta[j] - F) / denominator); - } + for (int j = 1; j < N; j++) { + final double F = -2. / tau * U2_E[i + 1][j + 1] - OMEGA_SQ_HX2 * diff2r(U2_E, i + 1, j + 1); + final double denominator = b2 - a2 * alpha[j]; + tridiagonal.setAlpha(j + 1, c2 / denominator); + tridiagonal.setBeta(j + 1, (a2 * beta[j] - F) / denominator); + } - U1[i][N] = (tau * beta[N] + HY2 * U2_E[i + 1][N + 1] + _c11 * diff2r(U2_E, i + 1, N +1) ) - / ((C1_U1 - alpha[N]) * tau + HY2); + U1[i][N] = (tau * beta[N] + HY2 * U2_E[i + 1][N + 1] + _c11 * diff2r(U2_E, i + 1, N + 1)) + / ((C1_U1 - alpha[N]) * tau + HY2); - tridiagonal.sweep(U1[i]); + tridiagonal.sweep(U1[i]); - } + } - // i = 0 boundary - tridiagonal.setBeta(1, - (TAU_HY * pulse(m + 1) + HY2 * U2_E[1][1]) * _b11 + 2.0 * _b12 * (U2_E[2][1] - U2_E[1][1])); + // i = 0 boundary + tridiagonal.setBeta(1, + (TAU_HY * pulse(m + 1) + HY2 * U2_E[1][1]) * _b11 + 2.0 * _b12 * (U2_E[2][1] - U2_E[1][1])); - for (int j = 1; j < N; j++) { - final double F = -2. / tau * U2_E[1][j + 1] - 2.0 * OMEGA_SQ_HX2 * (U2_E[2][j + 1] - U2_E[1][j + 1]); - tridiagonal.setBeta(j + 1, (F - a2 * beta[j]) / (a2 * alpha[j] - b2)); - } + for (int j = 1; j < N; j++) { + final double F = -2. / tau * U2_E[1][j + 1] - 2.0 * OMEGA_SQ_HX2 * (U2_E[2][j + 1] - U2_E[1][j + 1]); + tridiagonal.setBeta(j + 1, (F - a2 * beta[j]) / (a2 * alpha[j] - b2)); + } - U1[0][N] = (tau * beta[N] + HY2 * U2_E[1][N + 1] + 2.0 * _c11 * (U2_E[2][N + 1] - U2_E[1][N + 1])) - / ((C1_U1 - alpha[N]) * tau + HY2); + U1[0][N] = (tau * beta[N] + HY2 * U2_E[1][N + 1] + 2.0 * _c11 * (U2_E[2][N + 1] - U2_E[1][N + 1])) + / ((C1_U1 - alpha[N]) * tau + HY2); - tridiagonal.sweep(U1[0]); - } + tridiagonal.sweep(U1[0]); + } - @Override - public void finaliseStep() { - // do nothing - } + @Override + public void finaliseStep() { + // do nothing + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 594f450a..b41cb031 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -21,139 +21,139 @@ public class ExplicitCoupledSolver extends ExplicitScheme implements Solver, FixedPointIterations { - private RadiativeTransferCoupling coupling; - private RadiativeTransferSolver rte; - private RTECalculationStatus status; - private Fluxes fluxes; - - private double hx; - private double a; - private double nonlinearPrecision; - private double pls; - - private int N; - - private double HX_NP; - private double prefactor; - - public ExplicitCoupledSolver() { - this( derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5) ); - } - - public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { - super( N, timeFactor ); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - setCoupling(new RadiativeTransferCoupling()); - status = RTECalculationStatus.NORMAL; - } - - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); - fluxes = coupling.getRadiativeTransferEquation().getFluxes(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - - var p = (ThermoOpticalProperties) problem.getProperties(); - double Bi = (double) p.getHeatLoss().getValue(); - - a = 1. / (1. + Bi * hx); - - final double opticalThickness = (double) p.getOpticalThickness().getValue(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double tau = getGrid().getTimeStep(); - - HX_NP = hx / Np; - prefactor = tau * opticalThickness / Np; - } - - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - status = coupling.getRadiativeTransferEquation().compute(getPreviousSolution()); - runTimeSequence(problem); - - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); - } - - @Override - public boolean normalOperation() { - return super.normalOperation() && (status == RTECalculationStatus.NORMAL); - } - - @Override - public void timeStep(int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(final int m) { - /* + private RadiativeTransferCoupling coupling; + private RadiativeTransferSolver rte; + private RTECalculationStatus status; + private Fluxes fluxes; + + private double hx; + private double a; + private double nonlinearPrecision; + private double pls; + + private int N; + + private double HX_NP; + private double prefactor; + + public ExplicitCoupledSolver() { + this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); + } + + public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setCoupling(new RadiativeTransferCoupling()); + status = RTECalculationStatus.NORMAL; + } + + private void prepare(ParticipatingMedium problem) { + super.prepare(problem); + + var grid = getGrid(); + + coupling.init(problem, grid); + rte = coupling.getRadiativeTransferEquation(); + fluxes = coupling.getRadiativeTransferEquation().getFluxes(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + + var p = (ThermoOpticalProperties) problem.getProperties(); + double Bi = (double) p.getHeatLoss().getValue(); + + a = 1. / (1. + Bi * hx); + + final double opticalThickness = (double) p.getOpticalThickness().getValue(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double tau = getGrid().getTimeStep(); + + HX_NP = hx / Np; + prefactor = tau * opticalThickness / Np; + } + + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + this.prepare(problem); + status = coupling.getRadiativeTransferEquation().compute(getPreviousSolution()); + runTimeSequence(problem); + + if (status != RTECalculationStatus.NORMAL) { + throw new SolverException(status.toString()); + } + } + + @Override + public boolean normalOperation() { + return super.normalOperation() && (status == RTECalculationStatus.NORMAL); + } + + @Override + public void timeStep(int m) { + pls = pulse(m); + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) { + /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries - */ - explicitSolution(); - - var V = getCurrentSolution(); - - // Front face - V[0] = (V[1] + hx * pls - HX_NP * fluxes.getFlux(0)) * a; - // Rear face - V[N] = (V[N - 1] + HX_NP * fluxes.getFlux(N)) * a; - } - - @Override - public void finaliseIteration(double[] V) { - status = rte.compute(V); - } - - @Override - public double phi(final int i) { - return prefactor * fluxes.fluxDerivative(i); - } - - @Override - public void finaliseStep() { - super.finaliseStep(); - coupling.getRadiativeTransferEquation().getFluxes().store(); - } - - public RadiativeTransferCoupling getCoupling() { - return coupling; - } - - public void setCoupling(RadiativeTransferCoupling coupling) { - this.coupling = coupling; - this.coupling.setParent(this); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ExplicitScheme.4"); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor()); - } - - @Override - public Class domain() { - return ParticipatingMedium.class; - } - -} \ No newline at end of file + */ + explicitSolution(); + + var V = getCurrentSolution(); + + // Front face + V[0] = (V[1] + hx * pls - HX_NP * fluxes.getFlux(0)) * a; + // Rear face + V[N] = (V[N - 1] + HX_NP * fluxes.getFlux(N)) * a; + } + + @Override + public void finaliseIteration(double[] V) { + status = rte.compute(V); + } + + @Override + public double phi(final int i) { + return prefactor * fluxes.fluxDerivative(i); + } + + @Override + public void finaliseStep() { + super.finaliseStep(); + coupling.getRadiativeTransferEquation().getFluxes().store(); + } + + public RadiativeTransferCoupling getCoupling() { + return coupling; + } + + public void setCoupling(RadiativeTransferCoupling coupling) { + this.coupling = coupling; + this.coupling.setParent(this); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ExplicitScheme.4"); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor()); + } + + @Override + public Class domain() { + return ParticipatingMedium.class; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index db3a7c99..3e0fc4c2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -9,10 +9,9 @@ /** * Performs a fully-dimensionless calculation for the {@code LinearisedProblem}. *

- * Relies on using the heat equation to - * calculate the value of the grid-function at the next timestep. Fills the - * {@code grid} completely at each specified spatial point. The heating curve is - * updated with the rear-side temperature + * Relies on using the heat equation to calculate the value of the grid-function + * at the next timestep. Fills the {@code grid} completely at each specified + * spatial point. The heating curve is updated with the rear-side temperature * Θ(xN,ti) (here * N is the grid density) at the end of {@code timeLimit} * intervals, which comprise of {@code timeLimit/tau} time steps. The @@ -42,62 +41,61 @@ * ensures that the error is not too high (typically a {@code 1.5E-2} relative * error). *

- * + * * @see super.solve(Problem) */ - public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { - private int N; - private double hx; - private double a; + private int N; + private double hx; + private double a; + + public ExplicitLinearisedSolver() { + super(); + } - public ExplicitLinearisedSolver() { - super(); - } + public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } - public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } + public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } + @Override + public void prepare(Problem problem) { + super.prepare(problem); - @Override - public void prepare(Problem problem) { - super.prepare(problem); + N = (int) getGrid().getGridDensity().getValue(); + hx = getGrid().getXStep(); - N = (int) getGrid().getGridDensity().getValue(); - hx = getGrid().getXStep(); + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + a = 1. / (1. + Bi1 * hx); + } - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - a = 1. / (1. + Bi1 * hx); - } + @Override + public void solve(ClassicalProblem problem) { + prepare(problem); + runTimeSequence(problem); + } - @Override - public void solve(ClassicalProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public void timeStep(int m) { - explicitSolution(); - var V = getCurrentSolution(); - setSolutionAt(0, (V[1] + hx * pulse(m)) * a); - setSolutionAt(N, V[N - 1] * a); - } + @Override + public void timeStep(int m) { + explicitSolution(); + var V = getCurrentSolution(); + setSolutionAt(0, (V[1] + hx * pulse(m)) * a); + setSolutionAt(N, V[N - 1] * a); + } - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } - @Override - public Class domain() { - return ClassicalProblem.class; - } + @Override + public Class domain() { + return ClassicalProblem.class; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java index 5e8a9a66..f6b8dc5a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java @@ -19,123 +19,122 @@ public class ExplicitNonlinearSolver extends ExplicitScheme implements Solver, FixedPointIterations { - private int N; - private double hx; - private double pls; - - private double dT_T; - - private double a00; - private double a11; - private double f01; - private double fN1; - - private double nonlinearPrecision; - - public ExplicitNonlinearSolver() { - super(); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - private void prepare(NonlinearProblem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - final double tau = grid.getTimeStep(); - - var p = problem.getProperties(); - final double T = (double) p.getTestTemperature().getValue(); - final double dT = p.maximumHeating((Pulse2D)problem.getPulse()); - - a00 = 2 * tau / (hx * hx + 2 * tau); - a11 = hx * hx / (2.0 * tau); - final double Bi1 = (double) p.getHeatLoss().getValue(); - f01 = 0.25 * Bi1 * T / dT; - fN1 = 0.25 * Bi1 * T / dT; - - dT_T = dT/T; - } - - @Override - public void timeStep(int m) { - explicitSolution(); - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(int m) { - var V = getCurrentSolution(); - var U = getPreviousSolution(); - - /** - * y = 0 - */ - - final double f0 = f01 * (fastPowLoop(V[0] * dT_T + 1, 4) - 1); - V[0] = a00 * (V[1] + a11 * U[0] + hx * (pls - f0)); - - /** - * y = 1 - */ - - final double fN = fN1 * (fastPowLoop(V[N] * dT_T + 1, 4) - 1); - V[N] = a00 * (V[N - 1] + a11 * U[N] - hx * fN); - } - - @Override - public void solve(NonlinearProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return NonlinearProblem.class; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - - if(type == NONLINEAR_PRECISION) - setNonlinearPrecision(property); - else - throw new IllegalArgumentException("Property not recognised: " + property); - } - -} \ No newline at end of file + private int N; + private double hx; + private double pls; + + private double dT_T; + + private double a00; + private double a11; + private double f01; + private double fN1; + + private double nonlinearPrecision; + + public ExplicitNonlinearSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + private void prepare(NonlinearProblem problem) { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + final double tau = grid.getTimeStep(); + + var p = problem.getProperties(); + final double T = (double) p.getTestTemperature().getValue(); + final double dT = p.maximumHeating((Pulse2D) problem.getPulse()); + + a00 = 2 * tau / (hx * hx + 2 * tau); + a11 = hx * hx / (2.0 * tau); + final double Bi1 = (double) p.getHeatLoss().getValue(); + f01 = 0.25 * Bi1 * T / dT; + fN1 = 0.25 * Bi1 * T / dT; + + dT_T = dT / T; + } + + @Override + public void timeStep(int m) { + explicitSolution(); + pls = pulse(m); + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(int m) { + var V = getCurrentSolution(); + var U = getPreviousSolution(); + + /** + * y = 0 + */ + final double f0 = f01 * (fastPowLoop(V[0] * dT_T + 1, 4) - 1); + V[0] = a00 * (V[1] + a11 * U[0] + hx * (pls - f0)); + + /** + * y = 1 + */ + final double fN = fN1 * (fastPowLoop(V[N] * dT_T + 1, 4) - 1); + V[N] = a00 * (V[N - 1] + a11 * U[N] - hx * fN); + } + + @Override + public void solve(NonlinearProblem problem) { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class domain() { + return NonlinearProblem.class; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(def(NONLINEAR_PRECISION)); + return list; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + throw new IllegalArgumentException("Property not recognised: " + property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index b1d32b7c..d22a20da 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -13,95 +13,93 @@ public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver { - private int N; - private double hx; - private double tau; - private double a; + private int N; + private double hx; + private double tau; + private double a; - private double pls; + private double pls; - private final static double EPS = 1e-7; // a small value ensuring numeric stability + private final static double EPS = 1e-7; // a small value ensuring numeric stability - private AbsorptionModel model; + private AbsorptionModel model; - public ExplicitTranslucentSolver() { - super(); - } + public ExplicitTranslucentSolver() { + super(); + } - public ExplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } + public ExplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - private void prepare(PenetrationProblem problem) { - super.prepare(problem); + private void prepare(PenetrationProblem problem) { + super.prepare(problem); - var grid = getGrid(); - model = problem.getAbsorptionModel(); + var grid = getGrid(); + model = problem.getAbsorptionModel(); - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - tau = grid.getTimeStep(); + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + tau = grid.getTimeStep(); - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - a = 1. / (1. + Bi1 * hx); - } + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + a = 1. / (1. + Bi1 * hx); + } - @Override - public void solve(PenetrationProblem problem) throws SolverException { - this.prepare(problem); - runTimeSequence(problem); - } + @Override + public void solve(PenetrationProblem problem) throws SolverException { + this.prepare(problem); + runTimeSequence(problem); + } - @Override - public void timeStep(final int m) { - pls = this.pulse(m); + @Override + public void timeStep(final int m) { + pls = this.pulse(m); - /* + /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries - */ - explicitSolution(); + */ + explicitSolution(); - /* + /* * Calculates boundary values - */ - - var V = getCurrentSolution(); - setSolutionAt(0, V[1] * a); - setSolutionAt(N, V[N - 1] * a); - - } - - @Override - public double phi(final int i) { - return tau * pls * model.absorption(LASER, (i - EPS) * hx); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitTranslucentSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ExplicitScheme.4"); - } - - @Override - public Class domain() { - return PenetrationProblem.class; - } - - @Override - public double signal() { - return evaluateSignal(model, getGrid(), getCurrentSolution()); - } - -} \ No newline at end of file + */ + var V = getCurrentSolution(); + setSolutionAt(0, V[1] * a); + setSolutionAt(N, V[N - 1] * a); + + } + + @Override + public double phi(final int i) { + return tau * pls * model.absorption(LASER, (i - EPS) * hx); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitTranslucentSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ExplicitScheme.4"); + } + + @Override + public Class domain() { + return PenetrationProblem.class; + } + + @Override + public double signal() { + return evaluateSignal(model, getGrid(), getCurrentSolution()); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 871ad37f..a4e4d15f 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -17,121 +17,122 @@ public class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { - private RadiativeTransferSolver rte; - private Fluxes fluxes; + private RadiativeTransferSolver rte; + private Fluxes fluxes; - private double alpha1; - private int N; + private double alpha1; + private int N; - private double hx; + private double hx; - private double HX2_2TAU; - private double HX2TAU0_2NP; - private double HX_NP; + private double HX2_2TAU; + private double HX2TAU0_2NP; + private double HX_NP; - private double v1; + private double v1; - public ImplicitCoupledSolver() { - super(derive(GRID_DENSITY, 20), derive(TAU_FACTOR, 0.66667)); - } + public ImplicitCoupledSolver() { + super(derive(GRID_DENSITY, 20), derive(TAU_FACTOR, 0.66667)); + } - public ImplicitCoupledSolver(NumericProperty gridDensity, NumericProperty timeFactor, NumericProperty timeLimit) { - super(gridDensity, timeFactor, timeLimit); - } + public ImplicitCoupledSolver(NumericProperty gridDensity, NumericProperty timeFactor, NumericProperty timeLimit) { + super(gridDensity, timeFactor, timeLimit); + } - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); + private void prepare(ParticipatingMedium problem) { + super.prepare(problem); - final var grid = getGrid(); + final var grid = getGrid(); - var coupling = getCoupling(); - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); + var coupling = getCoupling(); + coupling.init(problem, grid); + rte = coupling.getRadiativeTransferEquation(); - N = (int) getGrid().getGridDensity().getValue(); - hx = grid.getXStep(); - final double HH = hx * hx; - final double tau = grid.getTimeStep(); + N = (int) getGrid().getGridDensity().getValue(); + hx = grid.getXStep(); + final double HH = hx * hx; + final double tau = grid.getTimeStep(); - var p = (ThermoOpticalProperties)problem.getProperties(); - final double Bi1 = (double) p.getHeatLoss().getValue(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double tau0 = (double) p.getOpticalThickness().getValue(); + var p = (ThermoOpticalProperties) problem.getProperties(); + final double Bi1 = (double) p.getHeatLoss().getValue(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double tau0 = (double) p.getOpticalThickness().getValue(); - final double TAU0_NP = tau0 / Np; - HX2_2TAU = HH / (2.0 * tau); - HX_NP = hx / Np; - HX2TAU0_2NP = 0.5 * HH * tau0 / Np; + final double TAU0_NP = tau0 / Np; + HX2_2TAU = HH / (2.0 * tau); + HX_NP = hx / Np; + HX2TAU0_2NP = 0.5 * HH * tau0 / Np; - v1 = 1.0 + HX2_2TAU + hx * Bi1; + v1 = 1.0 + HX2_2TAU + hx * Bi1; - fluxes = rte.getFluxes(); + fluxes = rte.getFluxes(); - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - @Override - public double phi(int i) { - return TAU0_NP * fluxes.fluxDerivative(i); - } + @Override + public double phi(int i) { + return TAU0_NP * fluxes.fluxDerivative(i); + } - }; + }; - alpha1 = 1.0 / (1.0 + Bi1 * hx + HX2_2TAU); - tridiagonal.setAlpha(1, alpha1); + alpha1 = 1.0 / (1.0 + Bi1 * hx + HX2_2TAU); + tridiagonal.setAlpha(1, alpha1); - tridiagonal.setCoefA(1. / HH); - tridiagonal.setCoefB(1. / tau + 2. / HH); - tridiagonal.setCoefC(1. / HH); + tridiagonal.setCoefA(1. / HH); + tridiagonal.setCoefB(1. / tau + 2. / HH); + tridiagonal.setCoefC(1. / HH); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); - } + } - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - setCalculationStatus(rte.compute(getPreviousSolution())); + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + this.prepare(problem); + setCalculationStatus(rte.compute(getPreviousSolution())); - runTimeSequence(problem); + runTimeSequence(problem); - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); + var status = getCalculationStatus(); + if (status != RTECalculationStatus.NORMAL) { + throw new SolverException(status.toString()); + } - } + } - @Override - public String toString() { - return getString("ImplicitScheme.4"); - } + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - /* + @Override + public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_2NP * (fluxes.getFlux(N - 1) + * fluxes.getFlux(N))) / (v1 - alphaN); - */ - return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_NP * fluxes.getFlux(N) - + HX2TAU0_2NP * fluxes.fluxDerivativeRear()) / (v1 - alphaN); - } - - @Override - public double firstBeta(final int m) { - /* + */ + return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_NP * fluxes.getFlux(N) + + HX2TAU0_2NP * fluxes.fluxDerivativeRear()) / (v1 - alphaN); + } + + @Override + public double firstBeta(final int m) { + /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() - HX_2NP * * (fluxes.getFlux(0) + fluxes.getFlux(1))) * alpha1; - */ - return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() - + HX2TAU0_2NP * fluxes.fluxDerivativeFront() - HX_NP * fluxes.getFlux(0)) * alpha1; - } + */ + return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() + + HX2TAU0_2NP * fluxes.fluxDerivativeFront() - HX_NP * fluxes.getFlux(0)) * alpha1; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index f3515685..7bbc10dd 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -10,99 +10,97 @@ public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver { - private double hx; - private int N; - - private double HX2_2TAU; - private double z0; - private double zN_1; - private double fN1; - - public ImplicitDiathermicSolver() { - super(); - } - - public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(DiathermicMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - - final double HX2 = hx * hx; - final double tau = grid.getTimeStep(); - HX2_2TAU = HX2 / (2.0 * grid.getTimeStep()); - - /* Constants */ - - var properties = (DiathermicProperties)problem.getProperties(); - - final double Bi1 = (double) properties.getHeatLoss().getValue(); - final double eta = (double) properties.getDiathermicCoefficient().getValue(); - - z0 = 1.0 + HX2_2TAU + hx * Bi1 * (1.0 + eta); - zN_1 = -hx * eta * Bi1; - final double f01 = HX2_2TAU; - fN1 = f01; - - /* End of constants */ - - var tridiagonal = new BlockMatrixAlgorithm(grid); - - tridiagonal.setCoefA(1.0); - tridiagonal.setCoefB(2.0 + HX2 / tau); - tridiagonal.setCoefC(1.0); - - tridiagonal.setAlpha(1, 1.0 / z0); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); - - } - - @Override - public void leftBoundary(final int m) { - var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); - tridiagonal.setGamma(1, -zN_1 / z0); - super.leftBoundary(m); - } - - @Override - public double firstBeta(final int m) { - return (HX2_2TAU * getPreviousSolution()[0] + hx * pulse(m)) / z0; - } - - @Override - public double evalRightBoundary(int m, double alphaN, double betaN) { - var tri = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); - var p = tri.getP(); - var q = tri.getQ(); - return (fN1 * getPreviousSolution()[N] - zN_1 * p[0] + p[N - 1]) / (z0 + zN_1 * q[0] - q[N - 1]); - } - - @Override - public void solve(DiathermicMedium problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitDiathermicSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return DiathermicMedium.class; - } - -} \ No newline at end of file + private double hx; + private int N; + + private double HX2_2TAU; + private double z0; + private double zN_1; + private double fN1; + + public ImplicitDiathermicSolver() { + super(); + } + + public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + private void prepare(DiathermicMedium problem) { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + + final double HX2 = hx * hx; + final double tau = grid.getTimeStep(); + HX2_2TAU = HX2 / (2.0 * grid.getTimeStep()); + + /* Constants */ + var properties = (DiathermicProperties) problem.getProperties(); + + final double Bi1 = (double) properties.getHeatLoss().getValue(); + final double eta = (double) properties.getDiathermicCoefficient().getValue(); + + z0 = 1.0 + HX2_2TAU + hx * Bi1 * (1.0 + eta); + zN_1 = -hx * eta * Bi1; + final double f01 = HX2_2TAU; + fN1 = f01; + + /* End of constants */ + var tridiagonal = new BlockMatrixAlgorithm(grid); + + tridiagonal.setCoefA(1.0); + tridiagonal.setCoefB(2.0 + HX2 / tau); + tridiagonal.setCoefC(1.0); + + tridiagonal.setAlpha(1, 1.0 / z0); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); + + } + + @Override + public void leftBoundary(final int m) { + var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); + tridiagonal.setGamma(1, -zN_1 / z0); + super.leftBoundary(m); + } + + @Override + public double firstBeta(final int m) { + return (HX2_2TAU * getPreviousSolution()[0] + hx * pulse(m)) / z0; + } + + @Override + public double evalRightBoundary(int m, double alphaN, double betaN) { + var tri = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); + var p = tri.getP(); + var q = tri.getQ(); + return (fN1 * getPreviousSolution()[N] - zN_1 * p[0] + p[N - 1]) / (z0 + zN_1 * q[0] - q[N - 1]); + } + + @Override + public void solve(DiathermicMedium problem) { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitDiathermicSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class domain() { + return DiathermicMedium.class; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index a24f47a5..a333070e 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -11,10 +11,10 @@ /** * Performs a fully-dimensionless calculation for the {@code LinearisedProblem}. *

- * Initiates constants for calculations - * and uses a sweep method to evaluate the solution for each subsequent - * timestep, filling the {@code grid} completely at each specified spatial - * point. The heating curve is updated with the rear-side temperature + * Initiates constants for calculations and uses a sweep method to evaluate the + * solution for each subsequent timestep, filling the {@code grid} completely at + * each specified spatial point. The heating curve is updated with the rear-side + * temperature * Θ(xN,ti) (here * N is the grid density) at the end of {@code timeLimit} * intervals, which comprise of {@code timeLimit/tau} time steps. The @@ -23,7 +23,7 @@ * calculated solution (with respect to time), and {@code maxTemp} is the * {@code maximumTemperature} {@code NumericProperty} of {@code problem}. *

- * + * *

* The fully implicit scheme uses a standard 4-point template on a * one-dimensional grid that utilises the following grid-function values on each @@ -41,91 +41,87 @@ * approximation of at least O(τ + h2) for * both the heat equation and the boundary conditions. *

- * + * * @see super.solve(Problem) */ - public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { - private double Bi1HTAU; - - private int N; - private double tau; - - private double HH; - private double _2HTAU; - - public ImplicitLinearisedSolver() { - super(); - } - - public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - @Override - public void prepare(Problem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - final double hx = grid.getXStep(); - tau = grid.getTimeStep(); - - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - Bi1HTAU = Bi1 * hx * tau; - - // precalculated constants - - HH = hx*hx; - _2HTAU = 2. * hx * tau; - - final double alpha0 = 2. * tau / (2. * Bi1HTAU + 2. * tau + hx*hx); - final var tridiagonal = getTridiagonalMatrixAlgorithm(); - tridiagonal.setAlpha(1, alpha0); - - // coefficients for difference equation - - tridiagonal.setCoefA( 1. / pow(hx, 2) ); - tridiagonal.setCoefB( 1. / tau + 2. / pow(hx, 2) ); - tridiagonal.setCoefC( 1. / pow(hx, 2) ); - - tridiagonal.evaluateAlpha(); - } - - @Override - public void solve(ClassicalProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public double firstBeta(final int m) { - final double pls = super.pulse(m); - return (HH * getPreviousSolution()[0] + _2HTAU * pls) / (2. * Bi1HTAU + 2. * tau + HH); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - return (HH * getPreviousSolution()[N] + 2. * tau * betaN) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); - } - - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return ClassicalProblem.class; - } - -} \ No newline at end of file + private double Bi1HTAU; + + private int N; + private double tau; + + private double HH; + private double _2HTAU; + + public ImplicitLinearisedSolver() { + super(); + } + + public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + final double hx = grid.getXStep(); + tau = grid.getTimeStep(); + + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + + Bi1HTAU = Bi1 * hx * tau; + + // precalculated constants + HH = hx * hx; + _2HTAU = 2. * hx * tau; + + final double alpha0 = 2. * tau / (2. * Bi1HTAU + 2. * tau + hx * hx); + final var tridiagonal = getTridiagonalMatrixAlgorithm(); + tridiagonal.setAlpha(1, alpha0); + + // coefficients for difference equation + tridiagonal.setCoefA(1. / pow(hx, 2)); + tridiagonal.setCoefB(1. / tau + 2. / pow(hx, 2)); + tridiagonal.setCoefC(1. / pow(hx, 2)); + + tridiagonal.evaluateAlpha(); + } + + @Override + public void solve(ClassicalProblem problem) { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public double firstBeta(final int m) { + final double pls = super.pulse(m); + return (HH * getPreviousSolution()[0] + _2HTAU * pls) / (2. * Bi1HTAU + 2. * tau + HH); + } + + @Override + public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + return (HH * getPreviousSolution()[N] + 2. * tau * betaN) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class domain() { + return ClassicalProblem.class; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java index 30f71b21..7dbcc1e4 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import java.util.List; +import java.util.Set; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.FixedPointIterations; @@ -15,140 +16,140 @@ import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; public class ImplicitNonlinearSolver extends ImplicitScheme implements Solver, FixedPointIterations { - private int N; - private double HH; - private double tau; - private double pls; - - private double dT_T; - - private double b1; - private double c1; - private double c2; - private double b2; - private double b3; - - private double nonlinearPrecision; - - public ImplicitNonlinearSolver() { - super(); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - private void prepare(NonlinearProblem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - final double hx = grid.getXStep(); - tau = grid.getTimeStep(); - - HH = hx*hx; - - var p = problem.getProperties(); - - final double Bi1 = (double) p.getHeatLoss().getValue(); - - final double T = (double) p.getTestTemperature().getValue(); - final double dT = p.maximumHeating((Pulse2D)problem.getPulse()); - dT_T = dT/T; - - // constant for bc calc - - final double a1 = 2. * tau / (HH + 2. * tau); - b1 = HH / (2. * tau + HH); - b2 = a1 * hx; - b3 = Bi1 * T / (4.0 * dT); - c1 = -0.5 * hx * tau * Bi1 * T / dT; - - var tridiagonal = getTridiagonalMatrixAlgorithm(); - - tridiagonal.setCoefA( 1.0/HH); - tridiagonal.setCoefB( 1.0/tau + 2.0/HH); - tridiagonal.setCoefC( 1.0/HH); - - tridiagonal.setAlpha(1, a1); - tridiagonal.evaluateAlpha(); - c2 = 1. / (HH + 2. * tau - 2 * tridiagonal.getAlpha()[N] * tau); - } - - @Override - public void solve(NonlinearProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return NonlinearProblem.class; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case NONLINEAR_PRECISION: - setNonlinearPrecision(property); - break; - default: - throw new IllegalArgumentException("Property not recognised: " + property); - } - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(int m) { - super.timeStep(m); - } - - @Override - public double evalRightBoundary(int m, double alphaN, double betaN) { - return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); - } - - @Override - public double firstBeta(int m) { - return b1 * getPreviousSolution()[0] + b2 * (pls - b3 * (fastPowLoop(getCurrentSolution()[0] * dT_T + 1, 4) - 1)); - } - -} \ No newline at end of file + private int N; + private double HH; + private double tau; + private double pls; + + private double dT_T; + + private double b1; + private double c1; + private double c2; + private double b2; + private double b3; + + private double nonlinearPrecision; + + public ImplicitNonlinearSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + private void prepare(NonlinearProblem problem) { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + final double hx = grid.getXStep(); + tau = grid.getTimeStep(); + + HH = hx * hx; + + var p = problem.getProperties(); + + final double Bi1 = (double) p.getHeatLoss().getValue(); + + final double T = (double) p.getTestTemperature().getValue(); + final double dT = p.maximumHeating((Pulse2D) problem.getPulse()); + dT_T = dT / T; + + // constant for bc calc + final double a1 = 2. * tau / (HH + 2. * tau); + b1 = HH / (2. * tau + HH); + b2 = a1 * hx; + b3 = Bi1 * T / (4.0 * dT); + c1 = -0.5 * hx * tau * Bi1 * T / dT; + + var tridiagonal = getTridiagonalMatrixAlgorithm(); + + tridiagonal.setCoefA(1.0 / HH); + tridiagonal.setCoefB(1.0 / tau + 2.0 / HH); + tridiagonal.setCoefC(1.0 / HH); + + tridiagonal.setAlpha(1, a1); + tridiagonal.evaluateAlpha(); + c2 = 1. / (HH + 2. * tau - 2 * tridiagonal.getAlpha()[N] * tau); + } + + @Override + public void solve(NonlinearProblem problem) { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class domain() { + return NonlinearProblem.class; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case NONLINEAR_PRECISION: + setNonlinearPrecision(property); + break; + default: + throw new IllegalArgumentException("Property not recognised: " + property); + } + } + + @Override + public void timeStep(final int m) { + pls = pulse(m); + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(int m) { + super.timeStep(m); + } + + @Override + public double evalRightBoundary(int m, double alphaN, double betaN) { + return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); + } + + @Override + public double firstBeta(int m) { + return b1 * getPreviousSolution()[0] + b2 * (pls - b3 * (fastPowLoop(getCurrentSolution()[0] * dT_T + 1, 4) - 1)); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index f2c6cbe8..2fcab7ed 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -15,114 +15,112 @@ public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { - private AbsorptionModel absorption; - private Grid grid; - private double pls; - private int N; - - private double HH; - private double _2Bi1HTAU; - private double b11; - - private double frontAbsorption; - private double rearAbsorption; - - public ImplicitTranslucentSolver() { - super(); - } - - public ImplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(PenetrationProblem problem) { - super.prepare(problem); - - grid = getGrid(); - final double tau = grid.getTimeStep(); - N = (int) grid.getGridDensity().getValue(); - - final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); - final double hx = grid.getXStep(); - HH = hx * hx; - _2Bi1HTAU = 2.0 * Bi1H * tau; - b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - - absorption = problem.getAbsorptionModel(); - final double EPS = 1E-7; - rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); - frontAbsorption = tau * absorption.absorption(LASER, 0.0); - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(final int i) { - return pls * absorption.absorption(LASER, (i - EPS) * hx); - } - - }; - - // coefficients for difference equation - - tridiagonal.setCoefA(1. / HH); - tridiagonal.setCoefB(1. / tau + 2. / HH); - tridiagonal.setCoefC(1. / HH); - - tridiagonal.setAlpha(1, 1.0 / (1.0 + HH / (2.0 * tau) + Bi1H)); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); - } - - @Override - public void solve(PenetrationProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - super.timeStep(m); - } - - @Override - public double signal() { - return evaluateSignal(absorption, grid, getCurrentSolution()); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double tau = grid.getTimeStep(); - - return (HH * (getPreviousSolution()[N] + pls * rearAbsorption) - + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); - } - - @Override - public double firstBeta(int m) { - return (getPreviousSolution()[0] + pls * frontAbsorption) * b11; - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitTranslucentSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ImplicitScheme.4"); - } - - @Override - public Class domain() { - return PenetrationProblem.class; - } - -} \ No newline at end of file + private AbsorptionModel absorption; + private Grid grid; + private double pls; + private int N; + + private double HH; + private double _2Bi1HTAU; + private double b11; + + private double frontAbsorption; + private double rearAbsorption; + + public ImplicitTranslucentSolver() { + super(); + } + + public ImplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + private void prepare(PenetrationProblem problem) { + super.prepare(problem); + + grid = getGrid(); + final double tau = grid.getTimeStep(); + N = (int) grid.getGridDensity().getValue(); + + final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); + final double hx = grid.getXStep(); + HH = hx * hx; + _2Bi1HTAU = 2.0 * Bi1H * tau; + b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); + + absorption = problem.getAbsorptionModel(); + final double EPS = 1E-7; + rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); + frontAbsorption = tau * absorption.absorption(LASER, 0.0); + + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return pls * absorption.absorption(LASER, (i - EPS) * hx); + } + + }; + + // coefficients for difference equation + tridiagonal.setCoefA(1. / HH); + tridiagonal.setCoefB(1. / tau + 2. / HH); + tridiagonal.setCoefC(1. / HH); + + tridiagonal.setAlpha(1, 1.0 / (1.0 + HH / (2.0 * tau) + Bi1H)); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); + } + + @Override + public void solve(PenetrationProblem problem) { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public void timeStep(final int m) { + pls = pulse(m); + super.timeStep(m); + } + + @Override + public double signal() { + return evaluateSignal(absorption, grid, getCurrentSolution()); + } + + @Override + public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + final double tau = grid.getTimeStep(); + + return (HH * (getPreviousSolution()[N] + pls * rearAbsorption) + + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); + } + + @Override + public double firstBeta(int m) { + return (getPreviousSolution()[0] + pls * frontAbsorption) * b11; + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitTranslucentSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + @Override + public Class domain() { + return PenetrationProblem.class; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 0a0b4ee1..28166846 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -24,7 +24,7 @@ * calculated solution (with respect to time), and {@code maxTemp} is the * {@code maximumTemperature} {@code NumericProperty} of {@code problem}. *

- * + * *

* The semi-implicit scheme uses a 6-point template on a one-dimensional grid * that utilises the following grid-function values on each step: @@ -45,118 +45,114 @@ * pulse term in the boundary condition, a higher error is introduced into the * calculation than for the implicit scheme. *

- * + * * @see super.solve(Problem) */ - public class MixedLinearisedSolver extends MixedScheme implements Solver { - private double b1; - private double b2; - private double b3; - private double c1; - private double c2; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - - public MixedLinearisedSolver() { - super(); - } - - public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - @Override - public void prepare(Problem problem) { - super.prepare(problem); - - var grid = getGrid(); - - final double hx = grid.getXStep(); - final double tau = grid.getTimeStep(); - - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - // precalculated constants - - final double HH = pow(hx, 2); - final double Bi1HTAU = Bi1 * hx * tau; - - // constant for boundary-conditions calculation - - b1 = 1. / (Bi1HTAU + HH + tau); - b2 = -hx * (Bi1 * tau - hx); - b3 = hx * tau; - c1 = b2; - c2 = Bi1HTAU + HH; - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(int i) { - final var U = getPreviousSolution(); - return U[i] / tau + (U[i + 1] - 2. * U[i] + U[i - 1]) / HH; - } - - }; - - setTridiagonalMatrixAlgorithm(tridiagonal); - - final double a1 = tau / (Bi1HTAU + HH + tau); - tridiagonal.setAlpha(1, a1); - - // coefficients for the finite-difference heat equation - - tridiagonal.setCoefA( 1. / pow(hx, 2) ); - tridiagonal.setCoefB( 2. / tau + 2. / pow(hx, 2) ); - tridiagonal.setCoefC( 1. / pow(hx, 2) ); - - tridiagonal.evaluateAlpha(); - - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final var U = getPreviousSolution(); - - final var grid = getGrid(); - final double tau = grid.getTimeStep(); - final int N = (int)grid.getGridDensity().getValue(); - - return (c1 * U[N] + tau * betaN - tau * (U[N] - U[N - 1])) / (c2 - tau * (alphaN - 1)); - } - - @Override - public double firstBeta(final int m) { - //TODO - final double tau = getGrid().getTimeStep(); - final var pulse = getDiscretePulse(); - final double pls = pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); - - final var U = getPreviousSolution(); - return b1 * (b2 * U[0] + b3 * pls - tau * (U[0] - U[1])); - } - - @Override - public void solve(ClassicalProblem problem) { - this.prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new MixedLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return ClassicalProblem.class; - } - -} \ No newline at end of file + private double b1; + private double b2; + private double b3; + private double c1; + private double c2; + + private final static double EPS = 1e-7; // a small value ensuring numeric stability + + public MixedLinearisedSolver() { + super(); + } + + public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) { + super.prepare(problem); + + var grid = getGrid(); + + final double hx = grid.getXStep(); + final double tau = grid.getTimeStep(); + + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + + // precalculated constants + final double HH = pow(hx, 2); + final double Bi1HTAU = Bi1 * hx * tau; + + // constant for boundary-conditions calculation + b1 = 1. / (Bi1HTAU + HH + tau); + b2 = -hx * (Bi1 * tau - hx); + b3 = hx * tau; + c1 = b2; + c2 = Bi1HTAU + HH; + + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(int i) { + final var U = getPreviousSolution(); + return U[i] / tau + (U[i + 1] - 2. * U[i] + U[i - 1]) / HH; + } + + }; + + setTridiagonalMatrixAlgorithm(tridiagonal); + + final double a1 = tau / (Bi1HTAU + HH + tau); + tridiagonal.setAlpha(1, a1); + + // coefficients for the finite-difference heat equation + tridiagonal.setCoefA(1. / pow(hx, 2)); + tridiagonal.setCoefB(2. / tau + 2. / pow(hx, 2)); + tridiagonal.setCoefC(1. / pow(hx, 2)); + + tridiagonal.evaluateAlpha(); + + } + + @Override + public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + final var U = getPreviousSolution(); + + final var grid = getGrid(); + final double tau = grid.getTimeStep(); + final int N = (int) grid.getGridDensity().getValue(); + + return (c1 * U[N] + tau * betaN - tau * (U[N] - U[N - 1])) / (c2 - tau * (alphaN - 1)); + } + + @Override + public double firstBeta(final int m) { + //TODO + final double tau = getGrid().getTimeStep(); + final var pulse = getDiscretePulse(); + final double pls = pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); + + final var U = getPreviousSolution(); + return b1 * (b2 * U[0] + b3 * pls - tau * (U[0] - U[1])); + } + + @Override + public void solve(ClassicalProblem problem) { + this.prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new MixedLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class domain() { + return ClassicalProblem.class; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/Solver.java b/src/main/java/pulse/problem/schemes/solvers/Solver.java index 535710c0..f371b189 100644 --- a/src/main/java/pulse/problem/schemes/solvers/Solver.java +++ b/src/main/java/pulse/problem/schemes/solvers/Solver.java @@ -6,20 +6,18 @@ * A solver interface which provides the capability to use the {@code solve} * method on a {@code Problem}. This interface is implemented by the subclasses * of {@code DifferenceSCheme}. - * + * * @param an instance of Problem */ - public interface Solver { - /** - * Calculates the solution of the {@code t} and stores it in the respective - * {@code HeatingCurve}. - * - * @param problem - an accepted instance of {@code T} - * @throws SolverException - */ - - public void solve(T problem) throws SolverException; + /** + * Calculates the solution of the {@code t} and stores it in the respective + * {@code HeatingCurve}. + * + * @param problem - an accepted instance of {@code T} + * @throws SolverException + */ + public void solve(T problem) throws SolverException; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 745dfd1d..28bddf45 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -3,8 +3,8 @@ @SuppressWarnings("serial") public class SolverException extends Exception { - public SolverException(String status) { - super(status); - } + public SolverException(String status) { + super(status); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/package-info.java b/src/main/java/pulse/problem/schemes/solvers/package-info.java index 5e2ab6bc..965635b9 100644 --- a/src/main/java/pulse/problem/schemes/solvers/package-info.java +++ b/src/main/java/pulse/problem/schemes/solvers/package-info.java @@ -1,7 +1,6 @@ /** * Contains various finite-difference solvers for the different problem statements available. - * Each solver is tailored to a specific problem statement and the solution is highly + * Each solver is tailored to a specific problem statement and the solution is highly * hard-coded. */ - -package pulse.problem.schemes.solvers; \ No newline at end of file +package pulse.problem.schemes.solvers; diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index 847cde5b..a335facd 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -11,100 +11,101 @@ public class AdiabaticSolution { - public final static int DEFAULT_CLASSIC_PRECISION = 200; - public final static int DEFAULT_POINTS = 100; - - private AdiabaticSolution() { - //do nothing - } - - /** - * A static factory method for calculating a heating curve based on the - * analytical solution of Parker et al. - *

- * The math itself is done separately in the {@code Problem} class. This method - * creates a {@code HeatingCurve} with the number of points equal to that of the - * {@code p.getHeatingCurve()}, and with the same baseline. The solution is - * calculated for the time range {@code 0 <= t <= timeLimit}. - *

- * - * @param p The problem statement, providing access to the - * {@code classicSolutionAt} method and to the - * {@code HeatingCurve} object it owns. - * @param timeLimit The upper time limit (in seconds) - * @param precision The second argument passed to the {@code classicSolutionAt} - * @return a {@code HeatingCurve} representing the analytical solution. - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - * @see Problem.classicSolutionAt(double,int) - */ - - public static HeatingCurve classicSolution(Problem p, double timeLimit, int precision) { - final int points = DEFAULT_POINTS; - var classicCurve = new HeatingCurve(derive(NUMPOINTS, points)); - - final double step = timeLimit / (points - 1.0); - var prop = p.getProperties(); - - for (int i = 1; i < points; i++) - classicCurve.addPoint(i * step, solutionAt(prop, i * step, precision)); - - classicCurve.apply(p.getBaseline()); - classicCurve.setName("Adiabatic Solution"); - - return classicCurve; - } - - /** - *

- * Calculates the classic analytical solution - * T(x=l,time) of Parker et al. at the - * specified {@code time} using the first {@code n = precision} terms of the - * solution series. The results is then scaled by a factor of - * {@code signalHeight} and returned. - *

- * - * @param time The calculation time - * @param precision The number of terms in the approximated solution - * @return a double, representing T(x=l,time) - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - */ - - private final static double solutionAt(ThermalProperties p, double time, int precision) { - - final double EPS = 1E-8; - final double Fo = time / p.timeFactor(); - - if (time < EPS) - return 0; - - double sum = 0; - - for (int i = 1; i <= precision; i++) { - sum += pow(-1, i) * exp(-pow(i * PI, 2) * Fo); - } - - return (1. + 2. * sum) * (double) p.getMaximumTemperature().getValue(); - - } - - /** - * Calculates the classic solution, using the default value of the - * {@code precision} and the time limit specified by the {@code HeatingCurve} of - * {@code p}. - * - * @param p the problem statement - * @return a {@code HeatinCurve}, representing the classic solution. - * @see classicSolution - */ - - public static HeatingCurve classicSolution(Problem p) { - return classicSolution(p, p.getHeatingCurve().timeLimit(), DEFAULT_CLASSIC_PRECISION); - } - - public static HeatingCurve classicSolution(Problem p, double timeLimit) { - return classicSolution(p, timeLimit, DEFAULT_CLASSIC_PRECISION); - } - -} \ No newline at end of file + public final static int DEFAULT_CLASSIC_PRECISION = 200; + public final static int DEFAULT_POINTS = 100; + + private AdiabaticSolution() { + //do nothing + } + + /** + * A static factory method for calculating a heating curve based on the + * analytical solution of Parker et al. + *

+ * The math itself is done separately in the {@code Problem} class. This + * method creates a {@code HeatingCurve} with the number of points equal to + * that of the {@code p.getHeatingCurve()}, and with the same baseline. The + * solution is calculated for the time range {@code 0 <= t <= timeLimit}. + *

+ * + * @param p The problem statement, providing access to the + * {@code classicSolutionAt} method and to the {@code HeatingCurve} object + * it owns. + * @param timeLimit The upper time limit (in seconds) + * @param precision The second argument passed to the + * {@code classicSolutionAt} + * @return a {@code HeatingCurve} representing the analytical solution. + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + * @see Problem.classicSolutionAt(double,int) + */ + public static HeatingCurve classicSolution(Problem p, double timeLimit, int precision) { + final int points = DEFAULT_POINTS; + var classicCurve = new HeatingCurve(derive(NUMPOINTS, points)); + + final double step = timeLimit / (points - 1.0); + var prop = p.getProperties(); + + for (int i = 1; i < points; i++) { + classicCurve.addPoint(i * step, solutionAt(prop, i * step, precision)); + } + + classicCurve.apply(p.getBaseline()); + classicCurve.setName("Adiabatic Solution"); + + return classicCurve; + } + + /** + *

+ * Calculates the classic analytical solution + * T(x=l,time) of Parker et al. at the + * specified {@code time} using the first {@code n = precision} terms of the + * solution series. The results is then scaled by a factor of + * {@code signalHeight} and returned. + *

+ * + * @param time The calculation time + * @param precision The number of terms in the approximated solution + * @return a double, representing + * T(x=l,time) + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + */ + private final static double solutionAt(ThermalProperties p, double time, int precision) { + + final double EPS = 1E-8; + final double Fo = time / p.timeFactor(); + + if (time < EPS) { + return 0; + } + + double sum = 0; + + for (int i = 1; i <= precision; i++) { + sum += pow(-1, i) * exp(-pow(i * PI, 2) * Fo); + } + + return (1. + 2. * sum) * (double) p.getMaximumTemperature().getValue(); + + } + + /** + * Calculates the classic solution, using the default value of the + * {@code precision} and the time limit specified by the + * {@code HeatingCurve} of {@code p}. + * + * @param p the problem statement + * @return a {@code HeatinCurve}, representing the classic solution. + * @see classicSolution + */ + public static HeatingCurve classicSolution(Problem p) { + return classicSolution(p, p.getHeatingCurve().timeLimit(), DEFAULT_CLASSIC_PRECISION); + } + + public static HeatingCurve classicSolution(Problem p, double timeLimit) { + return classicSolution(p, timeLimit, DEFAULT_CLASSIC_PRECISION); + } + +} diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index e691c77d..381e33a3 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -10,47 +10,46 @@ * formulated in the dimensionless form and with linearised boundary conditions. * */ - public class ClassicalProblem extends Problem { - public ClassicalProblem() { - super(); - setPulse(new Pulse()); - } - - public ClassicalProblem(Problem p) { - super(p); - setPulse(new Pulse(p.getPulse())); - } - - @Override - public Class defaultScheme() { - return ImplicitLinearisedSolver.class; - } - - @Override - public void initProperties() { - setProperties(new ThermalProperties()); - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ThermalProperties(properties)); - } - - @Override - public String toString() { - return Messages.getString("LinearizedProblem.Descriptor"); - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public Problem copy() { - return new ClassicalProblem(this); - } - -} \ No newline at end of file + public ClassicalProblem() { + super(); + setPulse(new Pulse()); + } + + public ClassicalProblem(Problem p) { + super(p); + setPulse(new Pulse(p.getPulse())); + } + + @Override + public Class defaultScheme() { + return ImplicitLinearisedSolver.class; + } + + @Override + public void initProperties() { + setProperties(new ThermalProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ThermalProperties(properties)); + } + + @Override + public String toString() { + return Messages.getString("LinearizedProblem.Descriptor"); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public Problem copy() { + return new ClassicalProblem(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index a80101aa..d85462b8 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -28,118 +28,117 @@ * pulse-to-diameter ratio. * */ - public class ClassicalProblem2D extends Problem { - public ClassicalProblem2D() { - super(); - setPulse(new Pulse2D()); - setComplexity(ProblemComplexity.MODERATE); - } - - public ClassicalProblem2D(Problem p) { - super(p); - setPulse(new Pulse2D(p.getPulse())); - setComplexity(ProblemComplexity.MODERATE); - } - - @Override - public void initProperties() { - setProperties(new ExtendedThermalProperties()); - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ExtendedThermalProperties(properties)); - } - - @Override - public Class defaultScheme() { - return ADIScheme.class; - } - - @Override - public String toString() { - return Messages.getString("LinearizedProblem2D.Descriptor"); //$NON-NLS-1$ - } - - @Override - public DiscretePulse discretePulseOn(Grid grid) { - return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - var properties = (ExtendedThermalProperties) getProperties(); - double value; - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case FOV_OUTER: - value = (double) properties.getFOVOuter().getValue(); - break; - case FOV_INNER: - value = (double) properties.getFOVInner().getValue(); - break; - case SPOT_DIAMETER: - value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); - break; - case HEAT_LOSS_SIDE: - final double Bi = (double) properties.getSideLosses().getValue(); - setHeatLossParameter(output, i, Bi); - continue; - case HEAT_LOSS_COMBINED: - final double combined = (double) properties.getHeatLoss().getValue(); - setHeatLossParameter(output, i, combined); - continue; - default: - continue; - } - - output.setTransform(i, new InvDiamTransform(properties)); - output.set(i, value); - output.setParameterBounds(i, new Segment(0.5 * value, 1.5 * value)); - - } - - } - - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var properties = (ExtendedThermalProperties) getProperties(); - - // TODO one-to-one mapping for FOV and SPOT_DIAMETER - for (int i = 0, size = params.dimension(); i < size; i++) { - var type = params.getIndex(i); - switch (type) { - case FOV_OUTER: - case FOV_INNER: - case HEAT_LOSS_SIDE: - case HEAT_LOSS_COMBINED: - properties.set(type, derive(type, params.inverseTransform(i) )); - break; - case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter( derive(SPOT_DIAMETER, params.inverseTransform(i) )); - break; - default: - continue; - } - } - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public Problem copy() { - return new ClassicalProblem2D(this); - } - -} \ No newline at end of file + public ClassicalProblem2D() { + super(); + setPulse(new Pulse2D()); + setComplexity(ProblemComplexity.MODERATE); + } + + public ClassicalProblem2D(Problem p) { + super(p); + setPulse(new Pulse2D(p.getPulse())); + setComplexity(ProblemComplexity.MODERATE); + } + + @Override + public void initProperties() { + setProperties(new ExtendedThermalProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ExtendedThermalProperties(properties)); + } + + @Override + public Class defaultScheme() { + return ADIScheme.class; + } + + @Override + public String toString() { + return Messages.getString("LinearizedProblem2D.Descriptor"); //$NON-NLS-1$ + } + + @Override + public DiscretePulse discretePulseOn(Grid grid) { + return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + var properties = (ExtendedThermalProperties) getProperties(); + double value; + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case FOV_OUTER: + value = (double) properties.getFOVOuter().getValue(); + break; + case FOV_INNER: + value = (double) properties.getFOVInner().getValue(); + break; + case SPOT_DIAMETER: + value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); + break; + case HEAT_LOSS_SIDE: + final double Bi = (double) properties.getSideLosses().getValue(); + setHeatLossParameter(output, i, Bi); + continue; + case HEAT_LOSS_COMBINED: + final double combined = (double) properties.getHeatLoss().getValue(); + setHeatLossParameter(output, i, combined); + continue; + default: + continue; + } + + output.setTransform(i, new InvDiamTransform(properties)); + output.set(i, value); + output.setParameterBounds(i, new Segment(0.5 * value, 1.5 * value)); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var properties = (ExtendedThermalProperties) getProperties(); + + // TODO one-to-one mapping for FOV and SPOT_DIAMETER + for (int i = 0, size = params.dimension(); i < size; i++) { + var type = params.getIndex(i); + switch (type) { + case FOV_OUTER: + case FOV_INNER: + case HEAT_LOSS_SIDE: + case HEAT_LOSS_COMBINED: + properties.set(type, derive(type, params.inverseTransform(i))); + break; + case SPOT_DIAMETER: + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, params.inverseTransform(i))); + break; + default: + continue; + } + } + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public Problem copy() { + return new ClassicalProblem2D(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java index b30f53d7..d575a39e 100644 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ b/src/main/java/pulse/problem/statements/CoreShellProblem.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -19,145 +20,145 @@ import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.ui.Messages; public class CoreShellProblem extends ClassicalProblem2D { - private double tA; - private double tR; - private double coatingDiffusivity; - private final static boolean DEBUG = true; - - public CoreShellProblem() { - super(); - tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); - tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); - coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - @Override - public String toString() { - return Messages.getString("UniformlyCoatedSample.Descriptor"); - } - - public NumericProperty getCoatingAxialThickness() { - return derive(AXIAL_COATING_THICKNESS, tA); - } - - public NumericProperty getCoatingRadialThickness() { - return derive(RADIAL_COATING_THICKNESS, tR); - } - - public double axialFactor() { - return tA / (double)getProperties().getSampleThickness().getValue(); - } - - public double radialFactor() { - return tR / (double)getProperties().getSampleThickness().getValue(); - } - - public void setCoatingAxialThickness(NumericProperty t) { - this.tA = (double) t.getValue(); - } - - public void setCoatingRadialThickness(NumericProperty t) { - this.tR = (double) t.getValue(); - } - - public NumericProperty getCoatingDiffusivity() { - return derive(COATING_DIFFUSIVITY, coatingDiffusivity); - } - - public void setCoatingDiffusivity(NumericProperty a) { - this.coatingDiffusivity = (double) a.getValue(); - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.addAll(super.listedTypes()); - list.add(def(AXIAL_COATING_THICKNESS)); - list.add(def(RADIAL_COATING_THICKNESS)); - list.add(def(COATING_DIFFUSIVITY)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case COATING_DIFFUSIVITY: - setCoatingDiffusivity(property); - break; - case AXIAL_COATING_THICKNESS: - setCoatingAxialThickness(property); - break; - case RADIAL_COATING_THICKNESS: - setCoatingRadialThickness(property); - break; - default: - super.set(type, property); - break; - } - } - - @Override - public boolean isEnabled() { - return !DEBUG; - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - var bounds = new Segment(0.1, 1.0); - var properties = (ExtendedThermalProperties) this.getProperties(); - - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); - switch (key) { - case AXIAL_COATING_THICKNESS: - output.setTransform(i, new InvLenTransform(properties) ); - output.set(i, tA); - output.setParameterBounds(i, bounds ); - break; - case RADIAL_COATING_THICKNESS: - output.setTransform(i, new InvDiamTransform(properties) ); - output.set(i, tR); - output.setParameterBounds(i, bounds ); - break; - case COATING_DIFFUSIVITY: - output.setTransform(i, new InvLenSqTransform(properties) ); - output.set(i, coatingDiffusivity); - output.setParameterBounds( i, new Segment(0.5 * coatingDiffusivity, 1.5 * coatingDiffusivity) ); - break; - default: - continue; - } - } - - } - - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case AXIAL_COATING_THICKNESS: - tA = params.inverseTransform(i); - break; - case RADIAL_COATING_THICKNESS: - tR = params.inverseTransform(i); - break; - case COATING_DIFFUSIVITY: - coatingDiffusivity = params.inverseTransform(i); - break; - default: - continue; - } - } - } - -} \ No newline at end of file + private double tA; + private double tR; + private double coatingDiffusivity; + private final static boolean DEBUG = true; + + public CoreShellProblem() { + super(); + tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); + tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); + coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); + setComplexity(ProblemComplexity.HIGH); + } + + @Override + public String toString() { + return Messages.getString("UniformlyCoatedSample.Descriptor"); + } + + public NumericProperty getCoatingAxialThickness() { + return derive(AXIAL_COATING_THICKNESS, tA); + } + + public NumericProperty getCoatingRadialThickness() { + return derive(RADIAL_COATING_THICKNESS, tR); + } + + public double axialFactor() { + return tA / (double) getProperties().getSampleThickness().getValue(); + } + + public double radialFactor() { + return tR / (double) getProperties().getSampleThickness().getValue(); + } + + public void setCoatingAxialThickness(NumericProperty t) { + this.tA = (double) t.getValue(); + } + + public void setCoatingRadialThickness(NumericProperty t) { + this.tR = (double) t.getValue(); + } + + public NumericProperty getCoatingDiffusivity() { + return derive(COATING_DIFFUSIVITY, coatingDiffusivity); + } + + public void setCoatingDiffusivity(NumericProperty a) { + this.coatingDiffusivity = (double) a.getValue(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(AXIAL_COATING_THICKNESS); + set.add(RADIAL_COATING_THICKNESS); + set.add(COATING_DIFFUSIVITY); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case COATING_DIFFUSIVITY: + setCoatingDiffusivity(property); + break; + case AXIAL_COATING_THICKNESS: + setCoatingAxialThickness(property); + break; + case RADIAL_COATING_THICKNESS: + setCoatingRadialThickness(property); + break; + default: + super.set(type, property); + break; + } + } + + @Override + public boolean isEnabled() { + return !DEBUG; + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + var bounds = new Segment(0.1, 1.0); + var properties = (ExtendedThermalProperties) this.getProperties(); + + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + switch (key) { + case AXIAL_COATING_THICKNESS: + output.setTransform(i, new InvLenTransform(properties)); + output.set(i, tA); + output.setParameterBounds(i, bounds); + break; + case RADIAL_COATING_THICKNESS: + output.setTransform(i, new InvDiamTransform(properties)); + output.set(i, tR); + output.setParameterBounds(i, bounds); + break; + case COATING_DIFFUSIVITY: + output.setTransform(i, new InvLenSqTransform(properties)); + output.set(i, coatingDiffusivity); + output.setParameterBounds(i, new Segment(0.5 * coatingDiffusivity, 1.5 * coatingDiffusivity)); + break; + default: + continue; + } + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + + for (int i = 0, size = params.dimension(); i < size; i++) { + switch (params.getIndex(i)) { + case AXIAL_COATING_THICKNESS: + tA = params.inverseTransform(i); + break; + case RADIAL_COATING_THICKNESS: + tR = params.inverseTransform(i); + break; + case COATING_DIFFUSIVITY: + coatingDiffusivity = params.inverseTransform(i); + break; + default: + continue; + } + } + } + +} diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index ee2e31ad..2082d03d 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -8,7 +8,7 @@ import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.AtanhTransform; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitDiathermicSolver; import pulse.problem.schemes.solvers.SolverException; @@ -32,98 +32,97 @@ *

* */ - public class DiathermicMedium extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; + private final static int DEFAULT_CURVE_POINTS = 300; - public DiathermicMedium() { - super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - } + public DiathermicMedium() { + super(); + getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); + } - public DiathermicMedium(Problem p) { - super(p); - } + public DiathermicMedium(Problem p) { + super(p); + } - @Override - public void initProperties() { - setProperties(new DiathermicProperties()); - } + @Override + public void initProperties() { + setProperties(new DiathermicProperties()); + } - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new DiathermicProperties(properties)); - } + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new DiathermicProperties(properties)); + } - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - var properties = (DiathermicProperties) this.getProperties(); + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); + var key = output.getIndex(i); - if (key == DIATHERMIC_COEFFICIENT) { + if (key == DIATHERMIC_COEFFICIENT) { - var bounds = new Segment(1E-3, 1.0); - final double etta = (double) properties.getDiathermicCoefficient().getValue(); + var bounds = new Segment(0.0, 1.0); + final double etta = (double) properties.getDiathermicCoefficient().getValue(); - output.setTransform(i, new AtanhTransform(bounds)); - output.set(i, etta); - output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, etta); + output.setParameterBounds(i, bounds); - } + } - } + } - } + } - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var properties = (DiathermicProperties) this.getProperties(); + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); + var key = params.getIndex(i); - switch (key) { + switch (key) { - case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); - break; - case HEAT_LOSS: - if (properties.areThermalPropertiesLoaded()) { - properties.emissivity(); - final double emissivity = (double) properties.getEmissivity().getValue(); - properties - .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); - } - break; - default: - continue; + case DIATHERMIC_COEFFICIENT: + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); + break; + case HEAT_LOSS: + if (properties.areThermalPropertiesLoaded()) { + properties.emissivity(); + final double emissivity = (double) properties.getEmissivity().getValue(); + properties + .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); + } + break; + default: + continue; - } + } - } + } - } + } - @Override - public String toString() { - return Messages.getString("DiathermicProblem.Descriptor"); - } + @Override + public String toString() { + return Messages.getString("DiathermicProblem.Descriptor"); + } - @Override - public Class defaultScheme() { - return ImplicitDiathermicSolver.class; - } + @Override + public Class defaultScheme() { + return ImplicitDiathermicSolver.class; + } - @Override - public DiathermicMedium copy() { - return new DiathermicMedium(this); - } + @Override + public DiathermicMedium copy() { + return new DiathermicMedium(this); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 155f68e7..c61083ff 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -9,6 +9,7 @@ import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.AtanhTransform; +import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; @@ -20,119 +21,119 @@ public class ParticipatingMedium extends NonlinearProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - - public ParticipatingMedium() { - super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - setComplexity(ProblemComplexity.HIGH); - } - - public ParticipatingMedium(ParticipatingMedium p) { - super(p); - setComplexity(ProblemComplexity.HIGH); - } - - @Override - public String toString() { - return Messages.getString("ParticipatingMedium.Descriptor"); - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - var properties = (ThermoOpticalProperties)getProperties(); - - Segment bounds; - double value = 0; - Transformable transform; - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case PLANCK_NUMBER: - bounds = new Segment(1E-5, properties.maxNp() ); - value = (double)properties.getPlanckNumber().getValue(); - transform = new AtanhTransform(bounds); - break; - case OPTICAL_THICKNESS: - value = (double)properties.getOpticalThickness().getValue(); - bounds = new Segment(1E-8, 1E5); - transform = LOG; - break; - case SCATTERING_ALBEDO: - value = (double)properties.getScatteringAlbedo().getValue(); - bounds = new Segment(1e-5, 0.999); - transform = new AtanhTransform(bounds); - break; - case SCATTERING_ANISOTROPY: - value = (double)properties.getScatteringAnisostropy().getValue(); - bounds = new Segment(-0.999, 0.999); - transform = new AtanhTransform(bounds); - break; - default: - continue; - - } - - output.setTransform(i, transform); - output.set(i, value); - output.setParameterBounds(i, bounds); - - } - - } - - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var properties = (ThermoOpticalProperties)getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - var type = params.getIndex(i); - - switch (type) { - - case PLANCK_NUMBER: - case SCATTERING_ALBEDO : - case SCATTERING_ANISOTROPY : - case OPTICAL_THICKNESS: - properties.set( type, derive( type, params.inverseTransform(i) ) ); - break; - case HEAT_LOSS: - case DIFFUSIVITY: - properties.emissivity(); - break; - default: - break; - - } - - } - - } - - @Override - public Class defaultScheme() { - return MixedCoupledSolver.class; - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ThermoOpticalProperties(properties)); - } - - @Override - public void initProperties() { - setProperties( new ThermoOpticalProperties() ); - } - - @Override - public Problem copy() { - return new ParticipatingMedium(this); - } - -} \ No newline at end of file + private final static int DEFAULT_CURVE_POINTS = 300; + + public ParticipatingMedium() { + super(); + getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); + setComplexity(ProblemComplexity.HIGH); + } + + public ParticipatingMedium(ParticipatingMedium p) { + super(p); + setComplexity(ProblemComplexity.HIGH); + } + + @Override + public String toString() { + return Messages.getString("ParticipatingMedium.Descriptor"); + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + var properties = (ThermoOpticalProperties) getProperties(); + + Segment bounds; + double value = 0; + Transformable transform; + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case PLANCK_NUMBER: + bounds = new Segment(1E-5, properties.maxNp()); + value = (double) properties.getPlanckNumber().getValue(); + transform = new AtanhTransform(bounds); + break; + case OPTICAL_THICKNESS: + value = (double) properties.getOpticalThickness().getValue(); + bounds = new Segment(1E-8, 1E5); + transform = LOG; + break; + case SCATTERING_ALBEDO: + value = (double) properties.getScatteringAlbedo().getValue(); + bounds = new Segment(0.0, 1.0); + transform = new StickTransform(bounds); + break; + case SCATTERING_ANISOTROPY: + value = (double) properties.getScatteringAnisostropy().getValue(); + bounds = new Segment(-1.0, 1.0); + transform = new StickTransform(bounds); + break; + default: + continue; + + } + + output.setTransform(i, transform); + output.set(i, value); + output.setParameterBounds(i, bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var properties = (ThermoOpticalProperties) getProperties(); + + for (int i = 0, size = params.dimension(); i < size; i++) { + + var type = params.getIndex(i); + + switch (type) { + + case PLANCK_NUMBER: + case SCATTERING_ALBEDO: + case SCATTERING_ANISOTROPY: + case OPTICAL_THICKNESS: + properties.set(type, derive(type, params.inverseTransform(i))); + break; + case HEAT_LOSS: + case DIFFUSIVITY: + properties.emissivity(); + break; + default: + break; + + } + + } + + } + + @Override + public Class defaultScheme() { + return MixedCoupledSolver.class; + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ThermoOpticalProperties(properties)); + } + + @Override + public void initProperties() { + setProperties(new ThermoOpticalProperties()); + } + + @Override + public Problem copy() { + return new ParticipatingMedium(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index ac748524..bca2c673 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -23,9 +23,9 @@ public class PenetrationProblem extends ClassicalProblem { private final static int DEFAULT_CURVE_POINTS = 300; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Absorption Model Selector", AbsorptionModel.class); + "Absorption Model Selector", AbsorptionModel.class); private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); @@ -39,7 +39,7 @@ public PenetrationProblem() { public PenetrationProblem(PenetrationProblem p) { super(p); - instanceDescriptor.setSelectedDescriptor((String)p.getAbsorptionSelector().getValue()); + instanceDescriptor.setSelectedDescriptor((String) p.getAbsorptionSelector().getValue()); instanceDescriptor.addListener(() -> initAbsorption()); initAbsorption(); } diff --git a/src/main/java/pulse/problem/statements/ProblemComplexity.java b/src/main/java/pulse/problem/statements/ProblemComplexity.java index d63c4d3f..693b4f74 100644 --- a/src/main/java/pulse/problem/statements/ProblemComplexity.java +++ b/src/main/java/pulse/problem/statements/ProblemComplexity.java @@ -4,16 +4,16 @@ public enum ProblemComplexity { - LOW(Color.green), MODERATE(Color.yellow), HIGH(Color.red); + LOW(Color.green), MODERATE(Color.yellow), HIGH(Color.red); - private Color clr; + private Color clr; - private ProblemComplexity(Color clr) { - this.clr = clr; - } + private ProblemComplexity(Color clr) { + this.clr = clr; + } - public Color getColor() { - return clr; - } + public Color getColor() { + return clr; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/SpectralRange.java b/src/main/java/pulse/problem/statements/model/SpectralRange.java index 7a54f6a0..bbb074ed 100644 --- a/src/main/java/pulse/problem/statements/model/SpectralRange.java +++ b/src/main/java/pulse/problem/statements/model/SpectralRange.java @@ -3,22 +3,22 @@ import pulse.properties.NumericPropertyKeyword; public enum SpectralRange { - LASER("Laser Absorption"), THERMAL("Thermal Radiation Absorption"); + LASER("Laser Absorption"), THERMAL("Thermal Radiation Absorption"); - String name; + String name; - SpectralRange(String name) { - this.name = name; - } + SpectralRange(String name) { + this.name = name; + } - @Override - public String toString() { - return name; - } + @Override + public String toString() { + return name; + } - public NumericPropertyKeyword typeOfAbsorption() { - return this == SpectralRange.LASER ? NumericPropertyKeyword.LASER_ABSORPTIVITY - : NumericPropertyKeyword.THERMAL_ABSORPTIVITY; - } + public NumericPropertyKeyword typeOfAbsorption() { + return this == SpectralRange.LASER ? NumericPropertyKeyword.LASER_ABSORPTIVITY + : NumericPropertyKeyword.THERMAL_ABSORPTIVITY; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/package-info.java b/src/main/java/pulse/problem/statements/package-info.java index 295cf5ba..1144423a 100644 --- a/src/main/java/pulse/problem/statements/package-info.java +++ b/src/main/java/pulse/problem/statements/package-info.java @@ -2,5 +2,4 @@ * Introduces various problem statements for the heat conduction problem in the * laser flash experiment. */ - -package pulse.problem.statements; \ No newline at end of file +package pulse.problem.statements; diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index fbb71212..3a45116b 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -4,516 +4,355 @@ import java.util.Optional; /** - * Contains a list of NumericProperty types recognised by the constituent modules of PULsE. + * Contains a list of NumericProperty types recognised by the constituent + * modules of PULsE. * */ - public enum NumericPropertyKeyword { - /** - * The thermal diffusivity of the sample. - */ - - DIFFUSIVITY, - - /** - * Not implemented yet. - */ - - COATING_DIFFUSIVITY, - - /** - * Sample thickness. - */ - - THICKNESS, - - /** - * Sample diameter. - */ - - DIAMETER, - - /** - * The maximum temperature, or the signal height -- if a relative temperature - * scale is used. - */ - - MAXTEMP, - - /** - * The number of points in the {@code HeatingCurve}. - */ - - NUMPOINTS, - - /** - * The precision parameter used to solve nonlinear problems. - */ - - NONLINEAR_PRECISION, - - /** - * Pulse width (time). - */ - - PULSE_WIDTH, - - /** - * Laser spot diameter. - */ - - SPOT_DIAMETER, - - /** - * Calculation time limit. - */ - - TIME_LIMIT, - - /** - * Grid (space partitioning) density. - */ - - GRID_DENSITY, - - /** - * Not implemented yet. - */ - - - SHELL_GRID_DENSITY, - - /** - * Not implemented yet. - */ - - - AXIAL_COATING_THICKNESS, - - /** - * Not implemented yet. - */ - - RADIAL_COATING_THICKNESS, - - /** - * Specific heat (Cp). - */ - - SPECIFIC_HEAT, - - /** - * Thermal conductivity. - */ - - CONDUCTIVITY, - - /** - * Emissivity of the sample (nonlinear problems). - */ - - EMISSIVITY, - - /** - * Density of the sample. - */ - - DENSITY, - - /** - * Absorbed energy (nonlinear problems). - */ - - LASER_ENERGY, - - /** - * Test temperature, at which the laser was fired at the sample. - */ - - TEST_TEMPERATURE, - - /** - * The resolution of linear search. - */ - - LINEAR_RESOLUTION, - - /** - * The accuracy of gradient calculation. - */ - - GRADIENT_RESOLUTION, - - /** - * The buffer size that is used to establish whether results are converging. - */ - - BUFFER_SIZE, - - /** - * The total error tolerance. - */ - - ERROR_TOLERANCE, - - /** - * The outer field of view diameter for the detector sighting. - */ - - FOV_OUTER, - - /** - * The inner field of view diameter for the detector sighting. - */ - - FOV_INNER, - - /** - * The baseline slope (for linear baselines). - */ - - BASELINE_SLOPE, - - /** - * Frequency of the sinusoidal baseline. - */ - - BASELINE_FREQUENCY, - - /** - * Phase shift of the sinusoidal baseline. - */ - - BASELINE_PHASE_SHIFT, - - /** - * Amplitude of the sinusoidal baseline. - */ - - BASELINE_AMPLITUDE, - - /** - * The baseline intercept value. - */ - - BASELINE_INTERCEPT, - - /** - * The factor used to convert squared grid spacing to time step. - */ - - TAU_FACTOR, - - /** - * The detector gain (amplification). - */ - - DETECTOR_GAIN, - - /** - * The detector iris (aperture). - */ - - DETECTOR_IRIS, - - /** - * The coefficient of heat losses from the side surface of the sample - * (two-dimensional problems). - */ - - HEAT_LOSS_SIDE, - - /** - * The coefficient of heat losses from the front and rear surfaces of the sample - * (1D and 2D problems). - */ - - HEAT_LOSS, - - /** - * A directive for the optimiser to maintain equal heat losses on all - * surfaces of the sample. Note that the dimensionless heat losses, - * i.e. Biot numbers, will differ due to different areas of the side and - * front/rear surfaces. - */ - - HEAT_LOSS_COMBINED, - - /** - * Search iteration. - */ - - ITERATION, - - /** - * Task identifier. - */ - - IDENTIFIER, - - /** - * Iteration limit for reverse problem solution. - */ - - ITERATION_LIMIT, - - /** - * Dimensionless coefficient of laser energy absorption (γ0). - */ - - LASER_ABSORPTIVITY, - - /** - * Dimensionless coefficient of thermal radiation absorption. - */ - - THERMAL_ABSORPTIVITY, - - /** - * A directive to the optimiser informing both front and rear side absorptivities must be equal. - */ - - COMBINED_ABSORPTIVITY, - - /** - * Reflectance of the sample (0 < R ≤ 1). - */ - - REFLECTANCE, - - /** - * A dimensionless coefficient in the radiation flux expression for the - * radiative heat transfer between the front and the rear (coated) surfaces. - * Used by the DiathermicMaterialProblem. - */ - - DIATHERMIC_COEFFICIENT, - - /** - * The Planck number. - */ - - PLANCK_NUMBER, - - /** - * The optical thickness of a material, equal to a product of its geometric thickness and the absorptivity. - */ - - OPTICAL_THICKNESS, - - /** - * Time shift (pulse sync) - */ - - TIME_SHIFT, - - /** - * Statistical significance. - */ - - SIGNIFICANCE, - - /** - * Statistical probability. - */ - - PROBABILITY, - - /** - * Optimiser statistic (usually, RSS). - */ - - OPTIMISER_STATISTIC, - - /** - * Model selection criterion (AIC, BIC, etc.) - */ - - MODEL_CRITERION, - - /** - * Test statistic (e.g. normality test criterion). - */ - - TEST_STATISTIC, - - /** - * Lower calculation bound for optimiser. - */ - - LOWER_BOUND, - - /** - * Upper calculation bound for optimiser. - */ - - UPPER_BOUND, - - /** - * Averaging window. - */ - - WINDOW, - - /** - * Intensity of incident radiation. - */ - - INCIDENT_INTENSITY, - - /** - * Threshold above which properties are thought to be strongly correlated. - */ - - CORRELATION_THRESHOLD, - - /** - * Number of subdivisions for numeric integration. - */ - - INTEGRATION_SEGMENTS, - - /** - * Cutoff for numeric integration. - */ - - INTEGRATION_CUTOFF, - - /** - * Weight of the semi-implicit finite-difference scheme. - */ - - SCHEME_WEIGHT, - - /** - * Number of quadrature points (RTE). - */ - - QUADRATURE_POINTS, - - /** - * Albedo of single scattering. - */ - - SCATTERING_ALBEDO, - - /** - * Anisotropy coefficient for the phase function of scattering. - */ - - SCATTERING_ANISOTROPY, - - /** - * Iteration error tolerance in DOM calculations. - */ - - DOM_ITERATION_ERROR, - - /** - * Number of independent directions (DOM). - */ - - DOM_DIRECTIONS, - - /** - * Error tolerance for the Laguerre solver (RTE). - */ - - LAGUERRE_SOLVER_ERROR, - - /** - * Absolute tolerance (atol) for RK calculations (RTE). - */ - - ATOL, - - /** - * Relative tolerance (atol) for RK calculations (RTE). - */ - - RTOL, - - /** - * Grid scaling factor. - */ - - GRID_SCALING_FACTOR, - - /** - * Internal DOM grid density (RTE). - */ - - DOM_GRID_DENSITY, - - /** - * Grid stretching factor (RTE). - */ - - GRID_STRETCHING_FACTOR, - - /** - * Relaxation parameter of iterative solver (RTE). - */ - - RELAXATION_PARAMETER, - - /** - * Iteration threshold for RTE calculations. - */ - - RTE_MAX_ITERATIONS, - - /** - * Maximum allowed time spent on integration (RTE). - */ - - RTE_INTEGRATION_TIMEOUT, - - /** - * Percentage of initial (rise) segment of the pulse trapezoid. - */ - - TRAPEZOIDAL_RISE_PERCENTAGE, - - /** - * Percentage of final (fall) segment of the pulse trapezoid. - */ - - TRAPEZOIDAL_FALL_PERCENTAGE, - - /** - * μ parameter for skewed normal distribution. - */ - - SKEW_MU, - - /** - * σ parameter for skewed normal distribution. - */ - - SKEW_SIGMA, - - /** - * λ parameter for skewed normal distribution. - */ - - SKEW_LAMBDA, - - /** - * A weight indicating how good a calculation model is. - */ - - MODEL_WEIGHT, - - /** - * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg damping. A value of 1 gives pure Marquardt damping. - */ - - DAMPING_RATIO; - - public static Optional findAny(String key) { - return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); - } - -} \ No newline at end of file + /** + * The thermal diffusivity of the sample. + */ + DIFFUSIVITY, + /** + * Not implemented yet. + */ + COATING_DIFFUSIVITY, + /** + * Sample thickness. + */ + THICKNESS, + /** + * Sample diameter. + */ + DIAMETER, + /** + * The maximum temperature, or the signal height -- if a relative + * temperature scale is used. + */ + MAXTEMP, + /** + * The number of points in the {@code HeatingCurve}. + */ + NUMPOINTS, + /** + * The precision parameter used to solve nonlinear problems. + */ + NONLINEAR_PRECISION, + /** + * Pulse width (time). + */ + PULSE_WIDTH, + /** + * Laser spot diameter. + */ + SPOT_DIAMETER, + /** + * Calculation time limit. + */ + TIME_LIMIT, + /** + * Grid (space partitioning) density. + */ + GRID_DENSITY, + /** + * Not implemented yet. + */ + SHELL_GRID_DENSITY, + /** + * Not implemented yet. + */ + AXIAL_COATING_THICKNESS, + /** + * Not implemented yet. + */ + RADIAL_COATING_THICKNESS, + /** + * Specific heat (Cp). + */ + SPECIFIC_HEAT, + /** + * Thermal conductivity. + */ + CONDUCTIVITY, + /** + * Emissivity of the sample (nonlinear problems). + */ + EMISSIVITY, + /** + * Density of the sample. + */ + DENSITY, + /** + * Absorbed energy (nonlinear problems). + */ + LASER_ENERGY, + /** + * Test temperature, at which the laser was fired at the sample. + */ + TEST_TEMPERATURE, + /** + * The resolution of linear search. + */ + LINEAR_RESOLUTION, + /** + * The accuracy of gradient calculation. + */ + GRADIENT_RESOLUTION, + /** + * The buffer size that is used to establish whether results are converging. + */ + BUFFER_SIZE, + /** + * The total error tolerance. + */ + ERROR_TOLERANCE, + /** + * The outer field of view diameter for the detector sighting. + */ + FOV_OUTER, + /** + * The inner field of view diameter for the detector sighting. + */ + FOV_INNER, + /** + * The baseline slope (for linear baselines). + */ + BASELINE_SLOPE, + /** + * Frequency of the sinusoidal baseline. + */ + BASELINE_FREQUENCY, + /** + * Phase shift of the sinusoidal baseline. + */ + BASELINE_PHASE_SHIFT, + /** + * Amplitude of the sinusoidal baseline. + */ + BASELINE_AMPLITUDE, + /** + * The baseline intercept value. + */ + BASELINE_INTERCEPT, + /** + * The factor used to convert squared grid spacing to time step. + */ + TAU_FACTOR, + /** + * The detector gain (amplification). + */ + DETECTOR_GAIN, + /** + * The detector iris (aperture). + */ + DETECTOR_IRIS, + /** + * The coefficient of heat losses from the side surface of the sample + * (two-dimensional problems). + */ + HEAT_LOSS_SIDE, + /** + * The coefficient of heat losses from the front and rear surfaces of the + * sample (1D and 2D problems). + */ + HEAT_LOSS, + /** + * A directive for the optimiser to maintain equal heat losses on all + * surfaces of the sample. Note that the dimensionless heat losses, i.e. + * Biot numbers, will differ due to different areas of the side and + * front/rear surfaces. + */ + HEAT_LOSS_COMBINED, + /** + * Search iteration. + */ + ITERATION, + /** + * Task identifier. + */ + IDENTIFIER, + /** + * Iteration limit for reverse problem solution. + */ + ITERATION_LIMIT, + /** + * Dimensionless coefficient of laser energy absorption + * (γ0). + */ + LASER_ABSORPTIVITY, + /** + * Dimensionless coefficient of thermal radiation absorption. + */ + THERMAL_ABSORPTIVITY, + /** + * A directive to the optimiser informing both front and rear side + * absorptivities must be equal. + */ + COMBINED_ABSORPTIVITY, + /** + * Reflectance of the sample (0 < R ≤ 1). + */ + REFLECTANCE, + /** + * A dimensionless coefficient in the radiation flux expression for the + * radiative heat transfer between the front and the rear (coated) surfaces. + * Used by the DiathermicMaterialProblem. + */ + DIATHERMIC_COEFFICIENT, + /** + * The Planck number. + */ + PLANCK_NUMBER, + /** + * The optical thickness of a material, equal to a product of its geometric + * thickness and the absorptivity. + */ + OPTICAL_THICKNESS, + /** + * Time shift (pulse sync) + */ + TIME_SHIFT, + /** + * Statistical significance. + */ + SIGNIFICANCE, + /** + * Statistical probability. + */ + PROBABILITY, + /** + * Optimiser statistic (usually, RSS). + */ + OPTIMISER_STATISTIC, + /** + * Model selection criterion (AIC, BIC, etc.) + */ + MODEL_CRITERION, + /** + * Test statistic (e.g. normality test criterion). + */ + TEST_STATISTIC, + /** + * Lower calculation bound for optimiser. + */ + LOWER_BOUND, + /** + * Upper calculation bound for optimiser. + */ + UPPER_BOUND, + /** + * Averaging window. + */ + WINDOW, + /** + * Intensity of incident radiation. + */ + INCIDENT_INTENSITY, + /** + * Threshold above which properties are thought to be strongly correlated. + */ + CORRELATION_THRESHOLD, + /** + * Number of subdivisions for numeric integration. + */ + INTEGRATION_SEGMENTS, + /** + * Cutoff for numeric integration. + */ + INTEGRATION_CUTOFF, + /** + * Weight of the semi-implicit finite-difference scheme. + */ + SCHEME_WEIGHT, + /** + * Number of quadrature points (RTE). + */ + QUADRATURE_POINTS, + /** + * Albedo of single scattering. + */ + SCATTERING_ALBEDO, + /** + * Anisotropy coefficient for the phase function of scattering. + */ + SCATTERING_ANISOTROPY, + /** + * Iteration error tolerance in DOM calculations. + */ + DOM_ITERATION_ERROR, + /** + * Number of independent directions (DOM). + */ + DOM_DIRECTIONS, + /** + * Error tolerance for the Laguerre solver (RTE). + */ + LAGUERRE_SOLVER_ERROR, + /** + * Absolute tolerance (atol) for RK calculations (RTE). + */ + ATOL, + /** + * Relative tolerance (atol) for RK calculations (RTE). + */ + RTOL, + /** + * Grid scaling factor. + */ + GRID_SCALING_FACTOR, + /** + * Internal DOM grid density (RTE). + */ + DOM_GRID_DENSITY, + /** + * Grid stretching factor (RTE). + */ + GRID_STRETCHING_FACTOR, + /** + * Relaxation parameter of iterative solver (RTE). + */ + RELAXATION_PARAMETER, + /** + * Iteration threshold for RTE calculations. + */ + RTE_MAX_ITERATIONS, + /** + * Maximum allowed time spent on integration (RTE). + */ + RTE_INTEGRATION_TIMEOUT, + /** + * Percentage of initial (rise) segment of the pulse trapezoid. + */ + TRAPEZOIDAL_RISE_PERCENTAGE, + /** + * Percentage of final (fall) segment of the pulse trapezoid. + */ + TRAPEZOIDAL_FALL_PERCENTAGE, + /** + * μ parameter for skewed normal distribution. + */ + SKEW_MU, + /** + * σ parameter for skewed normal distribution. + */ + SKEW_SIGMA, + /** + * λ parameter for skewed normal distribution. + */ + SKEW_LAMBDA, + /** + * A weight indicating how good a calculation model is. + */ + MODEL_WEIGHT, + /** + * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg + * damping. A value of 1 gives pure Marquardt damping. + */ + DAMPING_RATIO; + + public static Optional findAny(String key) { + return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); + } + +} diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 55d2c625..04cb0bc0 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -4,55 +4,59 @@ public class SampleName implements Property { - private String name; + private String name; - public SampleName() { - name = "Nameless"; - } + public SampleName() { + name = "Nameless"; + } - @Override - public Object getValue() { - return name; - } + @Override + public Object getValue() { + return name; + } - @Override - public String getDescriptor(boolean addHtmlTags) { - return "Sample name"; - } + @Override + public String getDescriptor(boolean addHtmlTags) { + return "Sample name"; + } - @Override - public boolean attemptUpdate(Object value) { - Objects.requireNonNull(value); + @Override + public boolean attemptUpdate(Object value) { + Objects.requireNonNull(value); - if (!(value instanceof String)) - throw new IllegalArgumentException( - "Illegal type: " + value.getClass().getSimpleName() + ". String expected."); + if (!(value instanceof String)) { + throw new IllegalArgumentException( + "Illegal type: " + value.getClass().getSimpleName() + ". String expected."); + } - final boolean result = !name.equals(value); - this.name = (String) value; - return result; + final boolean result = !name.equals(value); + this.name = (String) value; + return result; - } + } - @Override - public String toString() { - return name; - } + @Override + public String toString() { + return name; + } - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (o == null) - return false; + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } - boolean result = false; + if (o == null) { + return false; + } - if (o instanceof SampleName) - result = name.equals(((SampleName) o).getValue()); + boolean result = false; - return result; - } + if (o instanceof SampleName) { + result = name.equals(((SampleName) o).getValue()); + } -} \ No newline at end of file + return result; + } + +} diff --git a/src/main/java/pulse/properties/package-info.java b/src/main/java/pulse/properties/package-info.java index ee1f8ba4..1aed5413 100644 --- a/src/main/java/pulse/properties/package-info.java +++ b/src/main/java/pulse/properties/package-info.java @@ -4,5 +4,4 @@ * {@code Problem}s, etc. This package contains all property types used by all * other classes. */ - -package pulse.properties; \ No newline at end of file +package pulse.properties; diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 5512f3a0..2bbf6b90 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -11,31 +11,31 @@ * collected in {@code IndexedVector}s according to the pattern set up by a list * of {@code Flag}s. */ - public interface Optimisable { - /** - * Assigns parameter values of this {@code Optimisable} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @throws SolverException if {@code params} contains invalid parameter values - * @see pulse.util.PropertyHolder.listedTypes() - */ - - public void assign(ParameterVector params) throws SolverException; - - /** - * Calculates the vector argument defined on Rn - * to the scalar objective function for this {@code Optimisable}. - * - * @param output the output vector where the result will be stored - * @param flags a list of {@code Flag} objects, which determine the basis of - * the search - */ - - public void optimisationVector(ParameterVector output, List flags); - -} \ No newline at end of file + /** + * Assigns parameter values of this {@code Optimisable} using the + * optimisation vector {@code params}. Only those parameters will be + * updated, the types of which are listed as indices in the {@code params} + * vector. + * + * @param params the optimisation vector, containing a similar set of + * parameters to this {@code Problem} + * @throws SolverException if {@code params} contains invalid parameter + * values + * @see pulse.util.PropertyHolder.listedTypes() + */ + public void assign(ParameterVector params) throws SolverException; + + /** + * Calculates the vector argument defined on + * Rn + * to the scalar objective function for this {@code Optimisable}. + * + * @param output the output vector where the result will be stored + * @param flags a list of {@code Flag} objects, which determine the basis of + * the search + */ + public void optimisationVector(ParameterVector output, List flags); + +} diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index e525a468..239760e9 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -22,88 +22,85 @@ * for the 'Hessian' matrix is an identity matrix. It is recommended to use this * {@code PathSolver} in combination with the {@code WolfeSolver}. *

- * + * * @see Wikipedia - * page + * page * @see pulse.search.linear.WolfeOptimiser */ - public class BFGSOptimiser extends CompositePathOptimiser { - private static BFGSOptimiser instance = new BFGSOptimiser(); - - private BFGSOptimiser() { - super(); - this.setSolver(new HessianDirectionSolver() { - //empty statement - }); - } - - /** - *

- * Calculated the gradient at the end of this step. Invokes {@code hessian(...)} - * to calculate the Hessian matrix at the {@code k+1} step using the - * gk and - * gk+1 gradient values, the previously - * calculated Hessian matrix on step k, and the result of the linear - * search αk+1. - *

- * - * @throws SolverException - */ - - @Override - public void prepare(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getIterativeState(); - Vector dir = p.getDirection(); //p[k] - - final double minimumPoint = p.getMinimumPoint(); // alpha[k] - final SquareMatrix prevHessian = p.getHessian(); // B[k] - - final Vector g0 = p.getGradient(); // g[k] - final Vector g1 = gradient(task); // g[k+1] - - var hessian = hessian(g0, g1, dir, prevHessian, minimumPoint); //B[k+1] - - p.setHessian(hessian); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 - p.setGradient(g1); // set g1 as the new gradient for next step - } - - /** - * Uses the BFGS formula to calculate the Hessian. - * - * @param g1 gradient at step k - * @param g2 gradient at step k+1 - * @param dir direction pointing to the minimum at step k+1 - * @param prevHessian the Hessian matrix at step k - * @param alpha the results of the linear search at step k+1 - * @return a Hessian {@code Matrix} - */ - - private SquareMatrix hessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevHessian, double alpha) { - Vector y = g2.subtract(g1); // g[k+1] - g[k] - - var m = prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) - .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS formula - - return asSquareMatrix(m); - } - - @Override - public String toString() { - return Messages.getString("ApproximatedHessianSolver.Descriptor"); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static BFGSOptimiser getInstance() { - return instance; - } - -} \ No newline at end of file + private static BFGSOptimiser instance = new BFGSOptimiser(); + + private BFGSOptimiser() { + super(); + this.setSolver(new HessianDirectionSolver() { + //empty statement + }); + } + + /** + *

+ * Calculated the gradient at the end of this step. Invokes + * {@code hessian(...)} to calculate the Hessian matrix at the + * {@code k+1} step using the + * gk and + * gk+1 gradient values, the + * previously calculated Hessian matrix on step k, and the result of + * the linear search αk+1. + *

+ * + * @throws SolverException + */ + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (ComplexPath) task.getIterativeState(); + Vector dir = p.getDirection(); //p[k] + + final double minimumPoint = p.getMinimumPoint(); // alpha[k] + final SquareMatrix prevHessian = p.getHessian(); // B[k] + + final Vector g0 = p.getGradient(); // g[k] + final Vector g1 = gradient(task); // g[k+1] + + var hessian = hessian(g0, g1, dir, prevHessian, minimumPoint); //B[k+1] + + p.setHessian(hessian); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 + p.setGradient(g1); // set g1 as the new gradient for next step + } + + /** + * Uses the BFGS formula to calculate the Hessian. + * + * @param g1 gradient at step k + * @param g2 gradient at step k+1 + * @param dir direction pointing to the minimum at step k+1 + * @param prevHessian the Hessian matrix at step k + * @param alpha the results of the linear search at step k+1 + * @return a Hessian {@code Matrix} + */ + private SquareMatrix hessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevHessian, double alpha) { + Vector y = g2.subtract(g1); // g[k+1] - g[k] + + var m = prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) + .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS formula + + return asSquareMatrix(m); + } + + @Override + public String toString() { + return Messages.getString("ApproximatedHessianSolver.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static BFGSOptimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index f4ba4b0e..9cf4d470 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import pulse.math.ParameterVector; import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.math.linear.SquareMatrix; @@ -14,44 +15,42 @@ *

* */ - public class ComplexPath extends GradientGuidedPath { - private SquareMatrix hessian; - private SquareMatrix inverseHessian; - - protected ComplexPath(SearchTask task) { - super(task); - } - - /** - * In addition to the superclass method, resets the Hessian to an Identity - * matrix. - * - * @throws SolverException - */ - - @Override - public void configure(SearchTask task) { - super.configure(task); - hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); - inverseHessian = createIdentityMatrix(hessian.getData().length); - } - - public SquareMatrix getHessian() { - return hessian; - } - - public void setHessian(SquareMatrix hes) { - this.hessian = hes; - } - - public SquareMatrix getInverseHessian() { - return inverseHessian; - } - - public void setInverseHessian(SquareMatrix inverseHessian) { - this.inverseHessian = inverseHessian; - } - -} \ No newline at end of file + private SquareMatrix hessian; + private SquareMatrix inverseHessian; + + protected ComplexPath(SearchTask task) { + super(task); + } + + /** + * In addition to the superclass method, resets the Hessian to an Identity + * matrix. + * + * @throws SolverException + */ + @Override + public void configure(SearchTask task) { + super.configure(task); + hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); + inverseHessian = createIdentityMatrix(hessian.getData().length); + } + + public SquareMatrix getHessian() { + return hessian; + } + + public void setHessian(SquareMatrix hes) { + this.hessian = hes; + } + + public SquareMatrix getInverseHessian() { + return inverseHessian; + } + + public void setInverseHessian(SquareMatrix inverseHessian) { + this.inverseHessian = inverseHessian; + } + +} diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java index c209fc8b..c8e55a1e 100644 --- a/src/main/java/pulse/search/direction/DirectionSolver.java +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -5,17 +5,16 @@ public interface DirectionSolver { - /** - * Finds the direction of the minimum using the previously calculated values - * stored in {@code p}. - * - * @param p a {@code Path} object - * @return a {@code Vector} pointing to the minimum direction for this - * {@code Path} - * @throws SolverException - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ + /** + * Finds the direction of the minimum using the previously calculated values + * stored in {@code p}. + * + * @param p a {@code Path} object + * @return a {@code Vector} pointing to the minimum direction for this + * {@code Path} + * @throws SolverException + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + public Vector direction(GradientGuidedPath p) throws SolverException; - public Vector direction(GradientGuidedPath p) throws SolverException; - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index b62b5fb2..22a0e05b 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -11,7 +11,7 @@ * information is used by the {@code PathSolver} to perform the next search * step. *

- * + * *

* This is the most basic implementation, which stores only the gradient, the * direction (equal to the inverse gradient), minimum point achieved by the @@ -20,61 +20,59 @@ * {@code Path} are protected, as they should not be invoked directly. Instead, * they are invoked from within the {@code PathSolver}. *

- * + * * */ - public class GradientGuidedPath extends IterativeState { - private Vector direction; - private Vector gradient; - private double minimumPoint; - - protected GradientGuidedPath(SearchTask t) { - configure(t); - } + private Vector direction; + private Vector gradient; + private double minimumPoint; - /** - * Resets the {@code Path}: calculates the current gradient and the direction of - * search. Sets the minimum point to 0.0. - * - * @param t the {@code SearchTask}, for which this {@code Path} is created. - * @see pulse.search.direction.PathSolver.direction(Path) - */ + protected GradientGuidedPath(SearchTask t) { + configure(t); + } - public void configure(SearchTask t) { - super.reset(); - try { - this.gradient = ( (GradientBasedOptimiser) PathOptimiser.getInstance() ).gradient(t); - } catch (SolverException e) { - System.err.println("Failed on gradient calculation while resetting optimiser..."); - e.printStackTrace(); - } - minimumPoint = 0.0; - } + /** + * Resets the {@code Path}: calculates the current gradient and the + * direction of search. Sets the minimum point to 0.0. + * + * @param t the {@code SearchTask}, for which this {@code Path} is created. + * @see pulse.search.direction.PathSolver.direction(Path) + */ + public void configure(SearchTask t) { + super.reset(); + try { + this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); + } catch (SolverException e) { + System.err.println("Failed on gradient calculation while resetting optimiser..."); + e.printStackTrace(); + } + minimumPoint = 0.0; + } - public Vector getDirection() { - return direction; - } + public Vector getDirection() { + return direction; + } - public void setDirection(Vector currentDirection) { - this.direction = currentDirection; - } + public void setDirection(Vector currentDirection) { + this.direction = currentDirection; + } - public Vector getGradient() { - return gradient; - } + public Vector getGradient() { + return gradient; + } - public void setGradient(Vector currentGradient) { - this.gradient = currentGradient; - } + public void setGradient(Vector currentGradient) { + this.gradient = currentGradient; + } - public double getMinimumPoint() { - return minimumPoint; - } + public double getMinimumPoint() { + return minimumPoint; + } - public void setLinearStep(double min) { - minimumPoint = min; - } + public void setLinearStep(double min) { + minimumPoint = min; + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index ee92b4c3..15b9f9a9 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -8,47 +8,48 @@ public interface HessianDirectionSolver extends DirectionSolver { - /** - * Uses an approximation of the Hessian matrix, containing the information on - * second derivatives, calculated with the BFGS formula in combination with the - * local value of the gradient to evaluate the direction of the minimum on - * {@code p}. Invokes {@code p.setDirection()}. - * - * @throws SolverException - */ - - @Override - public default Vector direction(GradientGuidedPath p) throws SolverException { - var cp = (ComplexPath) p; - - Vector invGrad = p.getGradient().inverted(); - var result = solve(cp, invGrad); - - p.setDirection(result); - return result; - } - - public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { - final int dimg = cp.getGradient().dimension(); - Vector result; - // use linear solver for big matrices - if (dimg > 4) { - - var hess = new DMatrixRMaj(cp.getHessian().getData()); - var antigrad = new DMatrixRMaj(rhs.getData()); - var dirv = new DMatrixRMaj(dimg, 1); - - if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { - throw new SolverException("Singular matrix!"); - } - - result = new Vector(dirv.getData()); - - } else // use fast inverse - result = cp.getHessian().inverse().multiply(rhs); - - return result; - - } - -} \ No newline at end of file + /** + * Uses an approximation of the Hessian matrix, containing the information + * on second derivatives, calculated with the BFGS formula in combination + * with the local value of the gradient to evaluate the direction of the + * minimum on {@code p}. Invokes {@code p.setDirection()}. + * + * @throws SolverException + */ + @Override + public default Vector direction(GradientGuidedPath p) throws SolverException { + var cp = (ComplexPath) p; + + Vector invGrad = p.getGradient().inverted(); + var result = solve(cp, invGrad); + + p.setDirection(result); + return result; + } + + public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { + final int dimg = cp.getGradient().dimension(); + Vector result; + // use linear solver for big matrices + if (dimg > 4) { + + var hess = new DMatrixRMaj(cp.getHessian().getData()); + var antigrad = new DMatrixRMaj(rhs.getData()); + var dirv = new DMatrixRMaj(dimg, 1); + + if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { + throw new SolverException("Singular matrix!"); + } + + result = new Vector(dirv.getData()); + + } else // use fast inverse + { + result = cp.getHessian().inverse().multiply(rhs); + } + + return result; + + } + +} diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index dff3617f..3f006e19 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -1,6 +1,5 @@ package pulse.search.direction; -import pulse.math.ParameterVector; import pulse.math.linear.RectangularMatrix; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; @@ -8,70 +7,61 @@ class LMPath extends ComplexPath { - private ParameterVector parameters; - private Vector residualVector; - private RectangularMatrix jacobian; - private SquareMatrix nonregularisedHessian; - private double lambda; - private boolean computeJacobian; - - public LMPath(SearchTask t) { - super(t); - } - - @Override - public void configure(SearchTask t) { - super.configure(t); - this.lambda = 1.0; - computeJacobian = true; - } - - public RectangularMatrix getJacobian() { - return jacobian; - } - - public void setJacobian(RectangularMatrix jacobian) { - this.jacobian = jacobian; - } - - public double getLambda() { - return lambda; - } - - public void setLambda(double lambda) { - this.lambda = lambda; - } - - public SquareMatrix getNonregularisedHessian() { - return nonregularisedHessian; - } - - public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { - this.nonregularisedHessian = nonregularisedHessian; - } - - public Vector getResidualVector() { - return residualVector; - } - - public void setResidualVector(Vector residualVector) { - this.residualVector = residualVector; - } - - public ParameterVector getParameters() { - return parameters; - } - - public void setParameters(ParameterVector parameters) { - this.parameters = parameters; - } - - public boolean isComputeJacobian() { - return computeJacobian; - } - - public void setComputeJacobian(boolean computeJacobian) { - this.computeJacobian = computeJacobian; - } - -} \ No newline at end of file + private Vector residualVector; + private RectangularMatrix jacobian; + private SquareMatrix nonregularisedHessian; + private double lambda; + private boolean computeJacobian; + + public LMPath(SearchTask t) { + super(t); + } + + @Override + public void configure(SearchTask t) { + super.configure(t); + this.lambda = 1.0; + computeJacobian = true; + } + + public RectangularMatrix getJacobian() { + return jacobian; + } + + public void setJacobian(RectangularMatrix jacobian) { + this.jacobian = jacobian; + } + + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + public SquareMatrix getNonregularisedHessian() { + return nonregularisedHessian; + } + + public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { + this.nonregularisedHessian = nonregularisedHessian; + } + + public Vector getResidualVector() { + return residualVector; + } + + public void setResidualVector(Vector residualVector) { + this.residualVector = residualVector; + } + + public boolean isComputeJacobian() { + return computeJacobian; + } + + public void setComputeJacobian(boolean computeJacobian) { + this.computeJacobian = computeJacobian; + } + +} diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index 5b376242..bf89b075 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -12,82 +12,80 @@ public class SR1Optimiser extends CompositePathOptimiser { - private static SR1Optimiser instance = new SR1Optimiser(); - - private final static double r = 1E-8; - - private SR1Optimiser() { - super(); - this.setSolver(path -> ((ComplexPath)path).getInverseHessian().multiply(path.getGradient().inverted())); - } - - /** - *

- * Calculated the gradient at the end of this step. Invokes {@code hessian(...)} - * to calculate the Hessian matrix at the {@code k+1} step using the - * gk and - * gk+1 gradient values, the previously - * calculated Hessian matrix on step k, and the result of the linear - * search αk+1. - *

- * - * @throws SolverException - */ - - @Override - public void prepare(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getIterativeState(); - Vector dir = p.getDirection(); - - final double minimumPoint = p.getMinimumPoint(); - final Vector g0 = p.getGradient(); // g0 - final Vector g1 = gradient(task); // g1 - - /* + private static SR1Optimiser instance = new SR1Optimiser(); + + private final static double r = 1E-8; + + private SR1Optimiser() { + super(); + this.setSolver(path -> ((ComplexPath) path).getInverseHessian().multiply(path.getGradient().inverted())); + } + + /** + *

+ * Calculated the gradient at the end of this step. Invokes + * {@code hessian(...)} to calculate the Hessian matrix at the + * {@code k+1} step using the + * gk and + * gk+1 gradient values, the + * previously calculated Hessian matrix on step k, and the result of + * the linear search αk+1. + *

+ * + * @throws SolverException + */ + @Override + public void prepare(SearchTask task) throws SolverException { + var p = (ComplexPath) task.getIterativeState(); + Vector dir = p.getDirection(); + + final double minimumPoint = p.getMinimumPoint(); + final Vector g0 = p.getGradient(); // g0 + final Vector g1 = gradient(task); // g1 + + /* * Evaluate condition and update if needed - */ - - Vector y = g1.subtract(g0); // g[k+1] - g[k] - - final var dx = dir.multiply(minimumPoint); - final var m1 = y.subtract(p.getHessian().multiply(dx)); - - if(abs(dx.dot(m1)) > r*dx.length()*m1.length() ) { - - var m = p.getHessian().sum((outerProduct(m1, m1)).multiply(1. / m1.dot(dx))); - p.setHessian( asSquareMatrix(m) ); - p.setInverseHessian( inverseHessian(g0, g1, dir, p.getInverseHessian(), minimumPoint) ); - - } - - p.setGradient(g1); // set g1 as the new gradient for next step - - } - - private SquareMatrix inverseHessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevInvHessian, double alpha) { - Vector y = g2.subtract(g1); // g[k+1] - g[k] - - final var dx = dir.multiply(alpha); - final var m1 = dx.subtract(prevInvHessian.multiply(y)); - - var m = prevInvHessian.sum((outerProduct(m1, m1)).multiply(1. / m1.dot(y))); //SR1 formula - return asSquareMatrix(m); - } - - @Override - public String toString() { - return Messages.getString("SR1.Descriptor"); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static SR1Optimiser getInstance() { - return instance; - } - -} \ No newline at end of file + */ + Vector y = g1.subtract(g0); // g[k+1] - g[k] + + final var dx = dir.multiply(minimumPoint); + final var m1 = y.subtract(p.getHessian().multiply(dx)); + + if (abs(dx.dot(m1)) > r * dx.length() * m1.length()) { + + var m = p.getHessian().sum((outerProduct(m1, m1)).multiply(1. / m1.dot(dx))); + p.setHessian(asSquareMatrix(m)); + p.setInverseHessian(inverseHessian(g0, g1, dir, p.getInverseHessian(), minimumPoint)); + + } + + p.setGradient(g1); // set g1 as the new gradient for next step + + } + + private SquareMatrix inverseHessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevInvHessian, double alpha) { + Vector y = g2.subtract(g1); // g[k+1] - g[k] + + final var dx = dir.multiply(alpha); + final var m1 = dx.subtract(prevInvHessian.multiply(y)); + + var m = prevInvHessian.sum((outerProduct(m1, m1)).multiply(1. / m1.dot(y))); //SR1 formula + return asSquareMatrix(m); + } + + @Override + public String toString() { + return Messages.getString("SR1.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static SR1Optimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 4d9550e9..390437f7 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -9,67 +9,63 @@ * The simplest possible {@code PathSolver}, which assumes that the minimum * direction coincides with the inverted gradient. Used in combination with the * {@code GoldenSectionSolver} for increased accuracy. - * + * * @see pulse.search.linear.GoldenSectionOptimiser * @see Wikipedia - * page + * page */ - public class SteepestDescentOptimiser extends CompositePathOptimiser { - private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); - - private SteepestDescentOptimiser() { - super(); - //init gradient solver - this.setSolver( p -> { - - Vector dir = p.getGradient().inverted(); // p_k = -g - p.setDirection(dir); - return dir; - - }); - } + private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); - /** - * Calculates the gradient value at the end of each step. - * - * @throws SolverException - */ + private SteepestDescentOptimiser() { + super(); + //init gradient solver + this.setSolver(p -> { - @Override - public void prepare(SearchTask task) throws SolverException { - ( (GradientGuidedPath) task.getIterativeState() ).setGradient(gradient(task)); - } + Vector dir = p.getGradient().inverted(); // p_k = -g + p.setDirection(dir); + return dir; - @Override - public String toString() { - return Messages.getString("SteepestDescentSolver.Descriptor"); - } + }); + } - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ + /** + * Calculates the gradient value at the end of each step. + * + * @throws SolverException + */ + @Override + public void prepare(SearchTask task) throws SolverException { + ((GradientGuidedPath) task.getIterativeState()).setGradient(gradient(task)); + } - public static SteepestDescentOptimiser getInstance() { - return instance; - } + @Override + public String toString() { + return Messages.getString("SteepestDescentSolver.Descriptor"); + } - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static SteepestDescentOptimiser getInstance() { + return instance; + } - @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); - return new GradientGuidedPath(t); - } + /** + * Creates a new {@code Path} instance for storing the gradient, direction, + * and minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + @Override + public GradientGuidedPath initState(SearchTask t) { + this.configure(t); + return new GradientGuidedPath(t); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/package-info.java b/src/main/java/pulse/search/direction/package-info.java index bd4394f5..a2a9b048 100644 --- a/src/main/java/pulse/search/direction/package-info.java +++ b/src/main/java/pulse/search/direction/package-info.java @@ -3,5 +3,4 @@ * to determine the direction of the minimum of a specific {@code SearchTask} * using an iterative approach. */ - -package pulse.search.direction; \ No newline at end of file +package pulse.search.direction; diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index 771fca56..ab4ca1f8 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -5,40 +5,40 @@ public class FIPSMover implements Mover { - private double chi; - private double phi; - public final static double DEFAULT_CHI = 0.7298; - public final static double DEFAULT_PHI = 4.1; - - public FIPSMover() { - chi = DEFAULT_CHI; - phi = DEFAULT_PHI; - } - - @Override - public ParticleState attemptMove(Particle p, Particle[] neighbours) { - var current = p.getCurrentState(); - - var pos = current.getPosition(); - - final int n = pos.dimension(); - var nsum = new Vector(n); - - for(var neighbour : neighbours) { - var nPos = neighbour.getCurrentState().getPosition(); - nsum = nsum.sum( Vector.random(n, 0.0, phi).multComponents( nPos.subtract(pos) ) ); - } - - nsum = nsum.multiply(1.0/((double)neighbours.length)); - - var newVelocity = ( current.getVelocity().sum(nsum) ).multiply(chi); - var newPosition = pos.sum(newVelocity); - System.out.println(newPosition); - - return new ParticleState( - new ParameterVector(pos, newPosition ), - new ParameterVector(pos, newVelocity ) ); - - } + private double chi; + private double phi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_PHI = 4.1; + + public FIPSMover() { + chi = DEFAULT_CHI; + phi = DEFAULT_PHI; + } + + @Override + public ParticleState attemptMove(Particle p, Particle[] neighbours) { + var current = p.getCurrentState(); + + var pos = current.getPosition(); + + final int n = pos.dimension(); + var nsum = new Vector(n); + + for (var neighbour : neighbours) { + var nPos = neighbour.getCurrentState().getPosition(); + nsum = nsum.sum(Vector.random(n, 0.0, phi).multComponents(nPos.subtract(pos))); + } + + nsum = nsum.multiply(1.0 / ((double) neighbours.length)); + + var newVelocity = (current.getVelocity().sum(nsum)).multiply(chi); + var newPosition = pos.sum(newVelocity); + System.out.println(newPosition); + + return new ParticleState( + new ParameterVector(pos, newPosition), + new ParameterVector(pos, newVelocity)); + + } } diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index c086f608..6dd7387d 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -2,6 +2,6 @@ public interface Mover { - public ParticleState attemptMove(Particle p, Particle[] neighbours); - -} \ No newline at end of file + public ParticleState attemptMove(Particle p, Particle[] neighbours); + +} diff --git a/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java index 3a6f4b4b..64fa9519 100644 --- a/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java +++ b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java @@ -2,6 +2,6 @@ public interface NeighbourhoodTopology { - public Particle[] neighbours(Particle p, SwarmState ss); - -} \ No newline at end of file + public Particle[] neighbours(Particle p, SwarmState ss); + +} diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java index 60a5cd71..e5384505 100644 --- a/src/main/java/pulse/search/direction/pso/Particle.java +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -24,61 +24,60 @@ /** * Class defining a particle - the basic unit of a swarm. */ - public class Particle { - - private int id; - private ParticleState current; - private ParticleState pbest; - - private Particle[] neighbours; - - public Particle(ParticleState cur, int id) { - this.id = id; - current = cur; - pbest = new ParticleState(current); - } - - public void adopt(ParticleState state) { - this.current = state; - } - - public void evaluate(SearchTask t) throws SolverException { - var params = t.searchVector(); - t.assign( current.getPosition() ); - current.setFitness( t.solveProblemAndCalculateCost() ); - t.assign( params ); - - if(current.isBetterThan(pbest)) - pbest = new ParticleState(current); - } + private int id; + + private ParticleState current; + private ParticleState pbest; + + private Particle[] neighbours; + + public Particle(ParticleState cur, int id) { + this.id = id; + current = cur; + pbest = new ParticleState(current); + } + + public void adopt(ParticleState state) { + this.current = state; + } + + public void evaluate(SearchTask t) throws SolverException { + var params = t.searchVector(); + t.assign(current.getPosition()); + current.setFitness(t.solveProblemAndCalculateCost()); + t.assign(params); + + if (current.isBetterThan(pbest)) { + pbest = new ParticleState(current); + } + } - /** - * Returns the current state (position, velocity, fitness) of the particle. - * - * @return current state. - */ - - public ParticleState getCurrentState() { - return current; - } + /** + * Returns the current state (position, velocity, fitness) of the particle. + * + * @return current state. + */ + public ParticleState getCurrentState() { + return current; + } - /** - * Returns the personal best state ever achieved by the particle. - * - * @return personal best state. - */ - public ParticleState getBestState() { - return pbest; - } + /** + * Returns the personal best state ever achieved by the particle. + * + * @return personal best state. + */ + public ParticleState getBestState() { + return pbest; + } - public int getId() { - return id; - } + public int getId() { + return id; + } - public Particle[] getNeighbours() { - return neighbours; - } + public Particle[] getNeighbours() { + return neighbours; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index c9fb52f6..e5fcdc09 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -3,69 +3,70 @@ import pulse.math.ParameterVector; public class ParticleState { - - private ParameterVector position; - private ParameterVector velocity; - private double fitness; - - public ParticleState(ParameterVector cur) { - randomise(cur); - this.velocity = new ParameterVector(cur); - - //set initial velocity to zero - for(int i = 0, n = velocity.dimension(); i < n; i++) - velocity.set(i, 0.0); - - this.fitness = Double.MAX_VALUE; - } - - public ParticleState(ParticleState another) { - this.position = new ParameterVector(another.position); - this.velocity = new ParameterVector(another.velocity); - this.fitness = another.fitness; - } - - public ParticleState(ParameterVector p, ParameterVector v) { - this.position = p; - this.velocity = v; - } - - public boolean isBetterThan(ParticleState s) { - return this.fitness < s.fitness; - } - - public void randomise(ParameterVector pos) { - - this.position = new ParameterVector(pos); - - for (int i = 0, n = position.dimension(); i < n; i++) { - - var bounds = position.getBounds(); - - double max = bounds[i].getMaximum(); - double min = bounds[i].getMinimum(); - - double value = min + Math.random() * ( max - min ); - position.set(i, value ); - - } - - } - - public ParameterVector getPosition() { - return this.position; - } - - public ParameterVector getVelocity() { - return this.velocity; - } - - public double getFitness() { - return this.fitness; - } - - protected void setFitness(double fitness) { - this.fitness = fitness; - } - -} \ No newline at end of file + + private ParameterVector position; + private ParameterVector velocity; + private double fitness; + + public ParticleState(ParameterVector cur) { + randomise(cur); + this.velocity = new ParameterVector(cur); + + //set initial velocity to zero + for (int i = 0, n = velocity.dimension(); i < n; i++) { + velocity.set(i, 0.0); + } + + this.fitness = Double.MAX_VALUE; + } + + public ParticleState(ParticleState another) { + this.position = new ParameterVector(another.position); + this.velocity = new ParameterVector(another.velocity); + this.fitness = another.fitness; + } + + public ParticleState(ParameterVector p, ParameterVector v) { + this.position = p; + this.velocity = v; + } + + public boolean isBetterThan(ParticleState s) { + return this.fitness < s.fitness; + } + + public void randomise(ParameterVector pos) { + + this.position = new ParameterVector(pos); + + for (int i = 0, n = position.dimension(); i < n; i++) { + + var bounds = position.getBounds(); + + double max = bounds[i].getMaximum(); + double min = bounds[i].getMinimum(); + + double value = min + Math.random() * (max - min); + position.set(i, value); + + } + + } + + public ParameterVector getPosition() { + return this.position; + } + + public ParameterVector getVelocity() { + return this.velocity; + } + + public double getFitness() { + return this.fitness; + } + + protected void setFitness(double fitness) { + this.fitness = fitness; + } + +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index edbde3fe..badabe99 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -1,30 +1,31 @@ package pulse.search.direction.pso; -public class ParticleSwarmOptimiser - //extends PathOptimiser - { - - private SwarmState swarmState; - private Mover mover; - - public ParticleSwarmOptimiser() { - swarmState = new SwarmState(); - mover = new FIPSMover(); - } - - protected void moveParticles() { - var topology = swarmState.getNeighborhoodTopology(); - for (var p : swarmState.getParticles()) - p.adopt( mover.attemptMove( p, topology.neighbours(p, swarmState) ) ); - } - - /** - * Iterates the swarm. - * - * @param max_iterations max number of iterations to be computed by the swarm. - */ - - /* +public class ParticleSwarmOptimiser //extends PathOptimiser +{ + + private SwarmState swarmState; + private Mover mover; + + public ParticleSwarmOptimiser() { + swarmState = new SwarmState(); + mover = new FIPSMover(); + } + + protected void moveParticles() { + var topology = swarmState.getNeighborhoodTopology(); + for (var p : swarmState.getParticles()) { + p.adopt(mover.attemptMove(p, topology.neighbours(p, swarmState))); + } + } + + /** + * Iterates the swarm. + * + * @param max_iterations max number of iterations to be computed by the + * swarm. + */ + + /* @Override public boolean iteration(SearchTask task) throws SolverException { this.prepare(task); @@ -39,9 +40,9 @@ public boolean iteration(SearchTask task) throws SolverException { return true; } - */ + */ - /* + /* @Override public void prepare(SearchTask task) throws SolverException { swarmState.prepare(task); @@ -53,6 +54,5 @@ public IterativeState initState(SearchTask t) { swarmState.create(); return swarmState; } - */ - -} \ No newline at end of file + */ +} diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 3ff0d0c8..99f31bdd 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -4,56 +4,51 @@ public class StaticTopologies { - - /** Global best - * - */ - - public final static NeighbourhoodTopology GLOBAL = (p, state) -> state.getParticles(); - - /** - * Ring topology (1D - lattice) - */ - - public final static NeighbourhoodTopology RING = (p, state) -> { - var ps = state.getParticles(); - final int i = Arrays.asList(ps).indexOf(p); - return new Particle[] { ps[i > 0 ? i - 1 : ps.length - 1], - ps[i + 1 < ps.length ? i + 1 : 0] - }; - }; - - /** - * Von Neumann topology (square lattice) - * Condition: - if( ( ps.length & (ps.length - 1) ) != 0) - throw new IllegalArgumentException("Number of particles: " + ps.length + " is not power of 2"); - */ - - public final static NeighbourhoodTopology SQUARE = (p, state) -> { - var ps = state.getParticles(); - final int i = Arrays.asList(ps).indexOf(p); - - final int latticeParameter = (int) Math.sqrt(ps.length); - - final int row = i % latticeParameter; - final int column = i - row*latticeParameter; - - final int above = column + (row > 0 ? - (row - 1)*latticeParameter : (latticeParameter - 1)*latticeParameter ); - - final int below = column + ( row + 1 < ps.length ? - latticeParameter*(row + 1) : 0 ); - - final int left = row*latticeParameter + ( column > 0 ? column - 1 : ps.length - 1 ); - final int right = row*latticeParameter + ( column + 1 < ps.length ? column + 1 : 0 ); - - return new Particle[] { ps[left], ps[right], ps[above], ps[below] }; - }; - - private StaticTopologies() { - //empty - } - - -} \ No newline at end of file + /** + * Global best + * + */ + public final static NeighbourhoodTopology GLOBAL = (p, state) -> state.getParticles(); + + /** + * Ring topology (1D - lattice) + */ + public final static NeighbourhoodTopology RING = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + return new Particle[]{ps[i > 0 ? i - 1 : ps.length - 1], + ps[i + 1 < ps.length ? i + 1 : 0] + }; + }; + + /** + * Von Neumann topology (square lattice) Condition: if( ( ps.length & + * (ps.length - 1) ) != 0) throw new IllegalArgumentException("Number of + * particles: " + ps.length + " is not power of 2"); + */ + public final static NeighbourhoodTopology SQUARE = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + + final int latticeParameter = (int) Math.sqrt(ps.length); + + final int row = i % latticeParameter; + final int column = i - row * latticeParameter; + + final int above = column + (row > 0 + ? (row - 1) * latticeParameter : (latticeParameter - 1) * latticeParameter); + + final int below = column + (row + 1 < ps.length + ? latticeParameter * (row + 1) : 0); + + final int left = row * latticeParameter + (column > 0 ? column - 1 : ps.length - 1); + final int right = row * latticeParameter + (column + 1 < ps.length ? column + 1 : 0); + + return new Particle[]{ps[left], ps[right], ps[above], ps[below]}; + }; + + private StaticTopologies() { + //empty + } + +} diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 6937f0fd..4a2f4244 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -7,106 +7,106 @@ public class SwarmState extends IterativeState { - private ParameterVector seed; - - private Particle[] particles; - private NeighbourhoodTopology neighborhoodTopology; - - private Particle bestSoFar; - private int bestSoFarIndex; - - private final static int DEFAULT_PARTICLES = 16; - - public SwarmState() { - this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); - } - - public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { - this.neighborhoodTopology = neighborhoodTopology; - this.particles = new Particle[numberOfParticles]; - this.bestSoFar = null; - this.bestSoFarIndex = -1; - } - - public void evaluate(SearchTask t) throws SolverException { - for (var p : particles) - p.evaluate(t); - } - - public void prepare(SearchTask t) { - seed = t.searchVector(); - } - - public void create() { - for (int i = 0; i < particles.length; i++) - particles[i] = new Particle(new ParticleState(seed), i); - } - - /** - * Returns the best state achieved by any particle so far. - * - * @return State object. - */ - - public ParticleState bestSoFar() { - int bestIndex = 0; - - double fitness = 0; - double bestFitness = Double.MAX_VALUE; - - for (int i = 0; i < particles.length; i++) { - - fitness = particles[i].getBestState().getFitness(); - - if (fitness < bestFitness) { - bestIndex = i; - bestFitness = fitness; - } - - } - - this.bestSoFar = particles[bestIndex]; - this.bestSoFarIndex = bestIndex; - - return bestSoFar.getBestState(); - } - - public NeighbourhoodTopology getNeighborhoodTopology() { - return neighborhoodTopology; - } - - public void setNeighborhoodTopology(NeighbourhoodTopology neighborhoodTopology) { - this.neighborhoodTopology = neighborhoodTopology; - } - - /** - * Returns the particles of the swarm. - * - * @return array of Particles. - */ - - public Particle[] getParticles() { - return particles; - } - - public void setParticles(Particle[] particles) { - this.particles = particles; - } - - public Particle getBestSoFar() { - return bestSoFar; - } - - public void setBestSoFar(Particle bestSoFar) { - this.bestSoFar = bestSoFar; - } - - public int getBestSoFarIndex() { - return bestSoFarIndex; - } - - public void setBestSoFarIndex(int bestSoFarIndex) { - this.bestSoFarIndex = bestSoFarIndex; - } - -} \ No newline at end of file + private ParameterVector seed; + + private Particle[] particles; + private NeighbourhoodTopology neighborhoodTopology; + + private Particle bestSoFar; + private int bestSoFarIndex; + + private final static int DEFAULT_PARTICLES = 16; + + public SwarmState() { + this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); + } + + public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + this.particles = new Particle[numberOfParticles]; + this.bestSoFar = null; + this.bestSoFarIndex = -1; + } + + public void evaluate(SearchTask t) throws SolverException { + for (var p : particles) { + p.evaluate(t); + } + } + + public void prepare(SearchTask t) { + seed = t.searchVector(); + } + + public void create() { + for (int i = 0; i < particles.length; i++) { + particles[i] = new Particle(new ParticleState(seed), i); + } + } + + /** + * Returns the best state achieved by any particle so far. + * + * @return State object. + */ + public ParticleState bestSoFar() { + int bestIndex = 0; + + double fitness = 0; + double bestFitness = Double.MAX_VALUE; + + for (int i = 0; i < particles.length; i++) { + + fitness = particles[i].getBestState().getFitness(); + + if (fitness < bestFitness) { + bestIndex = i; + bestFitness = fitness; + } + + } + + this.bestSoFar = particles[bestIndex]; + this.bestSoFarIndex = bestIndex; + + return bestSoFar.getBestState(); + } + + public NeighbourhoodTopology getNeighborhoodTopology() { + return neighborhoodTopology; + } + + public void setNeighborhoodTopology(NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + } + + /** + * Returns the particles of the swarm. + * + * @return array of Particles. + */ + public Particle[] getParticles() { + return particles; + } + + public void setParticles(Particle[] particles) { + this.particles = particles; + } + + public Particle getBestSoFar() { + return bestSoFar; + } + + public void setBestSoFar(Particle bestSoFar) { + this.bestSoFar = bestSoFar; + } + + public int getBestSoFarIndex() { + return bestSoFarIndex; + } + + public void setBestSoFarIndex(int bestSoFarIndex) { + this.bestSoFarIndex = bestSoFarIndex; + } + +} diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index a0736e29..a4229d27 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -11,92 +11,91 @@ * The golden-section search is a simple dichotomy search for finding the * minimum of strictly unimodal functions by successively narrowing the domain * of the search using the golden ratio partitioning. - * + * * @see Wikipedia - * page + * page */ - public class GoldenSectionOptimiser extends LinearOptimiser { - /** - * The golden section φ, which is approximately equal to 0.618033989. - */ - - public final static double PHI = 1.0 - (3.0 - Math.sqrt(5.0)) / 2.0; - - private static GoldenSectionOptimiser instance = new GoldenSectionOptimiser(); - - private GoldenSectionOptimiser() { - super(); - } - - /** - *

- * Let {@code a} and {@code b} be the start and end point of a {@code Segment}, - * initially defined by the {@code super.domain(IndexedVector,Vector)} method. - * This method will start a loop, which at each step i will compare the - * values of the target function at the end points of a {@code Segment} - * constructed from one of the end points ai or - * bi and substituting the second end point with either - * ai + φ*(b-a) or bi - - * φ*(b-a). This theoretically ensures the least number of steps - * to reach the minimum (as compared to the standard dichotomy methods). - *

- * - * @throws SolverException - */ - - @Override - public double linearStep(SearchTask task) throws SolverException { - - final double EPS = 1e-14; - - final var params = task.searchVector(); - final Vector direction = ( (GradientGuidedPath) task.getIterativeState() ).getDirection(); - - var segment = domain(params, direction); - - final double absError = searchResolution * PHI * segment.length(); - - for (double t = PHI * segment.length(); Math.abs(t) > absError; t = PHI * segment.length()) { - final double alpha = segment.getMinimum() + t; - final double one_minus_alpha = segment.getMaximum() - t; - - final var newParams1 = params.sum(direction.multiply(alpha)); // alpha - task.assign(new ParameterVector(params, newParams1 )); - final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) - - final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha - task.assign(new ParameterVector(params, newParams2)); - final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) - - task.assign(new ParameterVector(params, newParams2)); // return to old position - - if (ss2 - ss1 > EPS) - segment.setMaximum(alpha); - else - segment.setMinimum(one_minus_alpha); - - } - - return segment.mean(); - - } - - @Override - public String toString() { - return Messages.getString("GoldenSectionSolver.Descriptor"); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static GoldenSectionOptimiser getInstance() { - return instance; - } - -} \ No newline at end of file + /** + * The golden section φ, which is approximately equal to 0.618033989. + */ + public final static double PHI = 1.0 - (3.0 - Math.sqrt(5.0)) / 2.0; + + private static GoldenSectionOptimiser instance = new GoldenSectionOptimiser(); + + private GoldenSectionOptimiser() { + super(); + } + + /** + *

+ * Let {@code a} and {@code b} be the start and end point of a + * {@code Segment}, initially defined by the + * {@code super.domain(IndexedVector,Vector)} method. This method will start + * a loop, which at each step i will compare the values of the target + * function at the end points of a {@code Segment} constructed from one of + * the end points ai or + * bi and substituting the second end point with either + * ai + φ*(b-a) or bi + * - φ*(b-a). This theoretically ensures the least number of + * steps to reach the minimum (as compared to the standard dichotomy + * methods). + *

+ * + * @throws SolverException + */ + @Override + public double linearStep(SearchTask task) throws SolverException { + + final double EPS = 1e-14; + + final var params = task.searchVector(); + final Vector direction = ((GradientGuidedPath) task.getIterativeState()).getDirection(); + + var segment = domain(params, direction); + + final double absError = searchResolution * PHI * segment.length(); + + for (double t = PHI * segment.length(); Math.abs(t) > absError; t = PHI * segment.length()) { + final double alpha = segment.getMinimum() + t; + final double one_minus_alpha = segment.getMaximum() - t; + + final var newParams1 = params.sum(direction.multiply(alpha)); // alpha + task.assign(new ParameterVector(params, newParams1)); + final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) + + final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + task.assign(new ParameterVector(params, newParams2)); + final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) + + task.assign(new ParameterVector(params, newParams2)); // return to old position + + if (ss2 - ss1 > EPS) { + segment.setMaximum(alpha); + } else { + segment.setMinimum(one_minus_alpha); + } + + } + + return segment.mean(); + + } + + @Override + public String toString() { + return Messages.getString("GoldenSectionSolver.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static GoldenSectionOptimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index 00593f6c..5588f782 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -15,6 +16,7 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import pulse.properties.Property; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; @@ -27,119 +29,121 @@ * algorithm to initialise the calculation domain. * */ - public abstract class LinearOptimiser extends PropertyHolder implements Reflexive { - protected static double searchResolution = (double) def(LINEAR_RESOLUTION).getValue(); - private final static double EPS = 1E-15; - - protected LinearOptimiser() { - super(); - } - - /** - * Finds the minimum of the target function on the {@code domain} - * {@code Segment}. - * - * @param task the target function is the sum of squared residuals (SSR) for - * this {@code task} - * @return a double, representing the step magnitude that needs to be multiplied - * by the direction of the search determined previously using the - * {@code PathSolver} to arrive at the next set of parameters - * corresponding to a lower SSR value of this {@code task} - * @throws SolverException - */ - - public abstract double linearStep(SearchTask task) throws SolverException; - - /** - * Sets the domain for this linear search on {@code p}. - *

- * The domain is defined as a {@code Segment} {@code [0; max]}, where - * {@code max} determines the maximum magnitude of the {@code linearStep}. This - * value is calculated initially as - * max = 0.5*xi/pi, where i is the - * index of the {@code DIFFUSIVITY NumericProperty}. Later it is corrected to - * ensure that the change in the {@code HEAT_LOSS} {@code NumericProperty} is - * less than unity. - *

- * - * @param x the current set of parameter - * @param p the result of the direction search with the {@code PathSolver} - * @return a {@code Segment} defining the domain of this search - * @see pulse.search.direction.PathSolver.direction(SearchTask) - */ - - public static Segment domain(ParameterVector x, Vector p) { - double alphaMax = Double.POSITIVE_INFINITY; - double alpha = 0.0; - - for (int i = 0; i < x.dimension(); i++) { - - final double component = p.get(i); - - //check if zero - if (component < EPS && component > -EPS) - continue; - - var bound = x.getTransformedBounds(i); - - alpha = abs( - ( ( component > 0 ? bound.getMaximum() : bound.getMinimum() ) - x.get(i) ) - / component); - - if(Double.isFinite(alpha) && alpha < alphaMax) - alphaMax = alpha; - - } - - return new Segment(0.0, alphaMax); - - } - - /** - *

- * The linear resolution determines the minimum distance between any two points - * belonging to the {@code domain} of this search while they still are - * considered separate. In case of a partitioning method, e.g. the - * golden-section search, this determines the partitioning limit. Note different - * {@code PathSolver}s can have different sensitivities to the linear search and - * may require different linear resolutions to work effectively. - *

- * - * @return a {@code NumericProperty} with the current value of the linear - * resolution - * @see domain(IndexedVector,IndexedVector,Vector) - */ - - public static NumericProperty getLinearResolution() { - return derive(LINEAR_RESOLUTION, searchResolution); - } - - public static void setLinearResolution(NumericProperty searchError) { - LinearOptimiser.searchResolution = (double) searchError.getValue(); - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * The {@code LINEAR_RESOLUTION} is the single listed parameter for this class. - * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - return new ArrayList<>(Arrays.asList(def(LINEAR_RESOLUTION))); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == LINEAR_RESOLUTION) - setLinearResolution(property); - } - -} \ No newline at end of file + protected static double searchResolution = (double) def(LINEAR_RESOLUTION).getValue(); + private final static double EPS = 1E-15; + + protected LinearOptimiser() { + super(); + } + + /** + * Finds the minimum of the target function on the {@code domain} + * {@code Segment}. + * + * @param task the target function is the sum of squared residuals (SSR) for + * this {@code task} + * @return a double, representing the step magnitude that needs to be + * multiplied by the direction of the search determined previously using the + * {@code PathSolver} to arrive at the next set of parameters corresponding + * to a lower SSR value of this {@code task} + * @throws SolverException + */ + public abstract double linearStep(SearchTask task) throws SolverException; + + /** + * Sets the domain for this linear search on {@code p}. + *

+ * The domain is defined as a {@code Segment} {@code [0; max]}, where + * {@code max} determines the maximum magnitude of the {@code linearStep}. + * This value is calculated initially as + * max = 0.5*xi/pi, where i is the + * index of the {@code DIFFUSIVITY NumericProperty}. Later it is corrected + * to ensure that the change in the {@code HEAT_LOSS} + * {@code NumericProperty} is less than unity. + *

+ * + * @param x the current set of parameter + * @param p the result of the direction search with the {@code PathSolver} + * @return a {@code Segment} defining the domain of this search + * @see pulse.search.direction.PathSolver.direction(SearchTask) + */ + public static Segment domain(ParameterVector x, Vector p) { + double alphaMax = Double.POSITIVE_INFINITY; + double alpha = 0.0; + + for (int i = 0; i < x.dimension(); i++) { + + final double component = p.get(i); + + //check if zero + if (component < EPS && component > -EPS) { + continue; + } + + var bound = x.getTransformedBounds(i); + + alpha = abs( + ((component > 0 ? bound.getMaximum() : bound.getMinimum()) - x.get(i)) + / component); + + if (Double.isFinite(alpha) && alpha < alphaMax) { + alphaMax = alpha; + } + + } + + return new Segment(0.0, alphaMax); + + } + + /** + *

+ * The linear resolution determines the minimum distance between any two + * points belonging to the {@code domain} of this search while they still + * are considered separate. In case of a partitioning method, e.g. the + * golden-section search, this determines the partitioning limit. Note + * different {@code PathSolver}s can have different sensitivities to the + * linear search and may require different linear resolutions to work + * effectively. + *

+ * + * @return a {@code NumericProperty} with the current value of the linear + * resolution + * @see domain(IndexedVector,IndexedVector,Vector) + */ + public static NumericProperty getLinearResolution() { + return derive(LINEAR_RESOLUTION, searchResolution); + } + + public static void setLinearResolution(NumericProperty searchError) { + LinearOptimiser.searchResolution = (double) searchError.getValue(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * The {@code LINEAR_RESOLUTION} is the single listed parameter for this + * class. + * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(LINEAR_RESOLUTION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == LINEAR_RESOLUTION) { + setLinearResolution(property); + } + } + +} diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 715e594b..b4db6d7a 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -18,133 +18,129 @@ * inexact linear search. This type of linear search works best with the * {@code ApproximatedHessianSolver}. *

- * + * * @see pulse.search.direction.BFGSOptimiser * @see Wikipedia - * page + * page */ - public class WolfeOptimiser extends LinearOptimiser { - private static WolfeOptimiser instance = new WolfeOptimiser(); - - /** - * The constant used in the Armijo inequality, equal to {@value C1}. - */ - - public final static double C1 = 0.05; - - /** - * The constant used in the strong Wolfe inequality for the modulus of the - * gradient projection, equal to {@value C2}. - */ - - public final static double C2 = 0.8; - - private WolfeOptimiser() { - super(); - } - - /** - *

- * This uses a combination of the Wolfe conditions for conducting an inexact - * line search with the domain partitioning using a random number generator. The - * partitioning is done in such a way that: (a) whenever the Armijo inequality - * is not satisfied, the original domain {@code [a; b]} is reduced to - * [ai; α], where α is the random number - * confined inside [ai; bi]; (b) when the - * Armijo inequality is satisfied and the second (strong) Wolfe condition for - * the modulus of the gradient projection is not satisfied, the α value is - * used to substitute the lower end point for the search domain: [α; - * bi]. As this is done iteratively, the length of the - * associated {@code Segment} will decrease. The method will return a value if - * either the strong Wolfe conditions are strictly satisfied, or if the linear - * precision has been reached. - *

- * - * @throws SolverException - */ - - @Override - public double linearStep(SearchTask task) throws SolverException { - - GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); - - final Vector direction = p.getDirection(); - final Vector g1 = p.getGradient(); - - final double G1P = g1.dot(direction); - final double G1P_ABS = abs(G1P); - - var params = task.searchVector(); - Segment segment = domain(params, direction); - - double cost1 = task.solveProblemAndCalculateCost(); - - double randomConfinedValue = 0; - double g2p; - - var instance = (GradientBasedOptimiser) PathOptimiser.getInstance(); - - for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { - - randomConfinedValue = segment.randomValue(); - - final var newParams = params.sum(direction.multiply(randomConfinedValue)); - task.assign(new ParameterVector(params, newParams)); - - final double cost2 = task.solveProblemAndCalculateCost(); - - /** - * Checks if the first Armijo inequality is not satisfied. In this case, it will - * set the maximum of the search domain to the {@code randomConfinedValue}. - */ - - if (cost2 - cost1 > C1 * randomConfinedValue * G1P) { - segment.setMaximum(randomConfinedValue); - continue; - } - - final var g2 = instance.gradient(task); - g2p = g2.dot(direction); - - /** - * This is the strong Wolfe condition that ensures that the absolute value of - * the projection of the gradient decreases. - */ - - if (abs(g2p) <= C2 * G1P_ABS) - break; - - /* + private static WolfeOptimiser instance = new WolfeOptimiser(); + + /** + * The constant used in the Armijo inequality, equal to {@value C1}. + */ + public final static double C1 = 0.05; + + /** + * The constant used in the strong Wolfe inequality for the modulus of the + * gradient projection, equal to {@value C2}. + */ + public final static double C2 = 0.8; + + private WolfeOptimiser() { + super(); + } + + /** + *

+ * This uses a combination of the Wolfe conditions for conducting an inexact + * line search with the domain partitioning using a random number generator. + * The partitioning is done in such a way that: (a) whenever the Armijo + * inequality is not satisfied, the original domain {@code [a; b]} is + * reduced to + * [ai; α], where α is the random + * number confined inside [ai; bi]; (b) + * when the Armijo inequality is satisfied and the second (strong) Wolfe + * condition for the modulus of the gradient projection is not satisfied, + * the α value is used to substitute the lower end point for the + * search domain: [α; + * bi]. As this is done iteratively, the length of the + * associated {@code Segment} will decrease. The method will return a value + * if either the strong Wolfe conditions are strictly satisfied, or if the + * linear precision has been reached. + *

+ * + * @throws SolverException + */ + @Override + public double linearStep(SearchTask task) throws SolverException { + + GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); + + final Vector direction = p.getDirection(); + final Vector g1 = p.getGradient(); + + final double G1P = g1.dot(direction); + final double G1P_ABS = abs(G1P); + + var params = task.searchVector(); + Segment segment = domain(params, direction); + + double cost1 = task.solveProblemAndCalculateCost(); + + double randomConfinedValue = 0; + double g2p; + + var instance = (GradientBasedOptimiser) PathOptimiser.getInstance(); + + for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { + + randomConfinedValue = segment.randomValue(); + + final var newParams = params.sum(direction.multiply(randomConfinedValue)); + task.assign(new ParameterVector(params, newParams)); + + final double cost2 = task.solveProblemAndCalculateCost(); + + /** + * Checks if the first Armijo inequality is not satisfied. In this + * case, it will set the maximum of the search domain to the + * {@code randomConfinedValue}. + */ + if (cost2 - cost1 > C1 * randomConfinedValue * G1P) { + segment.setMaximum(randomConfinedValue); + continue; + } + + final var g2 = instance.gradient(task); + g2p = g2.dot(direction); + + /** + * This is the strong Wolfe condition that ensures that the absolute + * value of the projection of the gradient decreases. + */ + if (abs(g2p) <= C2 * G1P_ABS) { + break; + } + + /* * if( g2p >= C2*G1P ) break; - */ - - segment.setMinimum(randomConfinedValue); - - } + */ + segment.setMinimum(randomConfinedValue); - task.assign(params); - p.setGradient(g1); + } - return randomConfinedValue; + task.assign(params); + p.setGradient(g1); - } + return randomConfinedValue; - @Override - public String toString() { - return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ - } + } - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ + @Override + public String toString() { + return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ + } - public static WolfeOptimiser getInstance() { - return instance; - } + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static WolfeOptimiser getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/linear/package-info.java b/src/main/java/pulse/search/linear/package-info.java index 2355e067..7acb4733 100644 --- a/src/main/java/pulse/search/linear/package-info.java +++ b/src/main/java/pulse/search/linear/package-info.java @@ -3,5 +3,4 @@ * of a vector variable that is unimodal on a specific {@code Segment}. These * should be subclasses of {@code LinearSolver}. */ - -package pulse.search.linear; \ No newline at end of file +package pulse.search.linear; diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 6605fd67..529b2e69 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -3,40 +3,38 @@ /** * AIC algorithm: Banks, H. T., & Joyner, M. L. (2017). Applied Mathematics * Letters, 74, 33–45. doi:10.1016/j.aml.2017.05.005 - * + * */ - public class AICStatistic extends ModelSelectionCriterion { - public AICStatistic(OptimiserStatistic os) { - super(os); - } - - public AICStatistic(AICStatistic another) { - super(another); - } - - public AICStatistic() { - super(new SumOfSquares()); - } - - @Override - public ModelSelectionCriterion copy() { - return new AICStatistic(this); - } - - /** - * @return the AIC penalising term. - */ - - @Override - public double penalisingTerm(final int kq, final int n) { - return 2.0 * (kq + 1); - } - - @Override - public String getDescriptor() { - return "Akaike Information Criterion (AIC)"; - } - -} \ No newline at end of file + public AICStatistic(OptimiserStatistic os) { + super(os); + } + + public AICStatistic(AICStatistic another) { + super(another); + } + + public AICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new AICStatistic(this); + } + + /** + * @return the AIC penalising term. + */ + @Override + public double penalisingTerm(final int kq, final int n) { + return 2.0 * (kq + 1); + } + + @Override + public String getDescriptor() { + return "Akaike Information Criterion (AIC)"; + } + +} diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 20aaa740..948adfe5 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -7,47 +7,47 @@ import pulse.tasks.SearchTask; /** - * A statistical optimality criterion relying on absolute deviations or the L1 norm condition. Similar to the least squares technique, - * it attempts to find a function which closely approximates a set of data. However, unlike the L2 norm, it is much more robust to - * data outliers. + * A statistical optimality criterion relying on absolute deviations or the L1 + * norm condition. Similar to the least squares technique, it attempts to find a + * function which closely approximates a set of data. However, unlike the L2 + * norm, it is much more robust to data outliers. * */ - public class AbsoluteDeviations extends OptimiserStatistic { - - public AbsoluteDeviations() { - super(); - } - - public AbsoluteDeviations(AbsoluteDeviations another) { - super(another); - } - - /** - * Calculates the L1 norm statistic, which simply sums up the absolute values of residuals. - */ - - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> abs(r[1]) ).reduce(Double::sum).get() / getResiduals().size(); - setStatistic(derive(OPTIMISER_STATISTIC, statistic)); - } - - @Override - public double variance() { - final double stat = (double)this.getStatistic().getValue(); - return stat*stat; - } - - @Override - public String getDescriptor() { - return "Absolute Deviations"; - } - - @Override - public OptimiserStatistic copy() { - return new AbsoluteDeviations(this); - } + + public AbsoluteDeviations() { + super(); + } + + public AbsoluteDeviations(AbsoluteDeviations another) { + super(another); + } + + /** + * Calculates the L1 norm statistic, which simply sums up the absolute + * values of residuals. + */ + @Override + public void evaluate(SearchTask t) { + calculateResiduals(t); + final double statistic = getResiduals().stream().map(r -> abs(r[1])).reduce(Double::sum).get() / getResiduals().size(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public double variance() { + final double stat = (double) this.getStatistic().getValue(); + return stat * stat; + } + + @Override + public String getDescriptor() { + return "Absolute Deviations"; + } + + @Override + public OptimiserStatistic copy() { + return new AbsoluteDeviations(this); + } } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index de6fc0b3..60b970ad 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -11,41 +11,40 @@ import umontreal.ssj.probdist.NormalDist; /** - * The Anderson-Darling normality test. In this variant of the test, the mean and the variance - * are assumed to be known. + * The Anderson-Darling normality test. In this variant of the test, the mean + * and the variance are assumed to be known. * */ - public class AndersonDarlingTest extends NormalityTest { - /** - * This uses the SSJ statistical library to calculate the Anderson-Darling test - * with the input parameters formed by the {@code task} residuals and a normal distribution - * with zero mean and variance equal to the residuals variance. - */ - - @Override - public boolean test(SearchTask task) { - calculateResiduals(task); - - double[] residuals = super.transformResiduals(); - var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); - var testResult = GofStat.andersonDarling(residuals, nd); - - this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - setProbability(derive(PROBABILITY, testResult[1])); - - return significanceTest(); - } - - @Override - public String getDescriptor() { - return "Anderson-Darling test"; - } - - @Override - public void evaluate(SearchTask t) { - test(t); - } - -} \ No newline at end of file + /** + * This uses the SSJ statistical library to calculate the Anderson-Darling + * test with the input parameters formed by the {@code task} residuals and a + * normal distribution with zero mean and variance equal to the residuals + * variance. + */ + @Override + public boolean test(SearchTask task) { + calculateResiduals(task); + + double[] residuals = super.transformResiduals(); + var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); + var testResult = GofStat.andersonDarling(residuals, nd); + + this.setStatistic(derive(TEST_STATISTIC, testResult[0])); + setProbability(derive(PROBABILITY, testResult[1])); + + return significanceTest(); + } + + @Override + public String getDescriptor() { + return "Anderson-Darling test"; + } + + @Override + public void evaluate(SearchTask t) { + test(t); + } + +} diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java index 9077eea5..2c6edfe0 100644 --- a/src/main/java/pulse/search/statistics/BICStatistic.java +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -3,43 +3,42 @@ import static java.lang.Math.log; /** - * Bayesian Information Criterion (BIC) algorithm formulated for the Gaussian distribution of residuals. - * This is used in model selection. BIC values are always negative. The absolute BIC value is meaningless, - * it is only used as a comparative statistic. + * Bayesian Information Criterion (BIC) algorithm formulated for the Gaussian + * distribution of residuals. This is used in model selection. BIC values are + * always negative. The absolute BIC value is meaningless, it is only used as a + * comparative statistic. * */ - public class BICStatistic extends ModelSelectionCriterion { - public BICStatistic(BICStatistic another) { - super(another); - } - - public BICStatistic(OptimiserStatistic os) { - super(os); - } - - public BICStatistic() { - super(new SumOfSquares()); - } - - @Override - public ModelSelectionCriterion copy() { - return new BICStatistic(this); - } - - /** - * @return the BIC penalising term - */ - - @Override - public double penalisingTerm(final int kq, final int n) { - return (kq + 1)*log(n); - } - - @Override - public String getDescriptor() { - return "Bayesian Information Criterion (BIC)"; - } - -} \ No newline at end of file + public BICStatistic(BICStatistic another) { + super(another); + } + + public BICStatistic(OptimiserStatistic os) { + super(os); + } + + public BICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new BICStatistic(this); + } + + /** + * @return the BIC penalising term + */ + @Override + public double penalisingTerm(final int kq, final int n) { + return (kq + 1) * log(n); + } + + @Override + public String getDescriptor() { + return "Bayesian Information Criterion (BIC)"; + } + +} diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index b89cc3c2..f724a758 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -12,40 +12,41 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexive { - private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - private static String selectedTestDescriptor; + private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); + private static String selectedTestDescriptor; - public CorrelationTest() { - //intentionally blank - } + public CorrelationTest() { + //intentionally blank + } - public abstract double evaluate(double[] x, double[] y); + public abstract double evaluate(double[] x, double[] y); - public boolean compareToThreshold(double value) { - return Math.abs(value) > threshold; - } + public boolean compareToThreshold(double value) { + return Math.abs(value) > threshold; + } - public static NumericProperty getThreshold() { - return derive(CORRELATION_THRESHOLD, threshold); - } + public static NumericProperty getThreshold() { + return derive(CORRELATION_THRESHOLD, threshold); + } - public static void setThreshold(NumericProperty p) { - requireType(p, CORRELATION_THRESHOLD); - threshold = (double) p.getValue(); - } + public static void setThreshold(NumericProperty p) { + requireType(p, CORRELATION_THRESHOLD); + threshold = (double) p.getValue(); + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NumericPropertyKeyword.CORRELATION_THRESHOLD) - threshold = (double) property.getValue(); - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NumericPropertyKeyword.CORRELATION_THRESHOLD) { + threshold = (double) property.getValue(); + } + } - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } + public static String getSelectedTestDescriptor() { + return selectedTestDescriptor; + } - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - CorrelationTest.selectedTestDescriptor = selectedTestDescriptor; - } + public static void setSelectedTestDescriptor(String selectedTestDescriptor) { + CorrelationTest.selectedTestDescriptor = selectedTestDescriptor; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java index a4b2d9de..ba6195fb 100644 --- a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java @@ -2,14 +2,14 @@ public class EmptyCorrelationTest extends CorrelationTest { - @Override - public double evaluate(double[] x, double[] y) { - return 0; - } - - @Override - public String getDescriptor() { - return "Don't test please"; - } + @Override + public double evaluate(double[] x, double[] y) { + return 0; + } + + @Override + public String getDescriptor() { + return "Don't test please"; + } } diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index 652de5a2..f4925014 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -4,23 +4,22 @@ public class EmptyTest extends NormalityTest { - /** - * Always returns true - */ + /** + * Always returns true + */ + @Override + public boolean test(SearchTask task) { + return true; + } - @Override - public boolean test(SearchTask task) { - return true; - } + @Override + public String getDescriptor() { + return "Don't test please"; + } - @Override - public String getDescriptor() { - return "Don't test please"; - } + @Override + public void evaluate(SearchTask t) { + // deliberately empty + } - @Override - public void evaluate(SearchTask t) { - // deliberately empty - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index dd416bef..d350d7e3 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -11,37 +11,37 @@ import pulse.tasks.SearchTask; /** - * The Kolmogorov-Smirnov normality test as implemented in {@code ApacheCommonsMath}. + * The Kolmogorov-Smirnov normality test as implemented in + * {@code ApacheCommonsMath}. * */ - public class KSTest extends NormalityTest { - private double[] residuals; - private NormalDistribution nd; - - @Override - public boolean test(SearchTask task) { - evaluate(task); - setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); - return significanceTest(); - } - - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - residuals = transformResiduals(); - - final double sd = (new StandardDeviation()).evaluate(residuals); - nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical - // standard dev - final double statistic = TestUtils.kolmogorovSmirnovStatistic(nd, residuals); - this.setStatistic(derive(TEST_STATISTIC, statistic)); - } - - @Override - public String getDescriptor() { - return "Kolmogorov-Smirnov test"; - } - -} \ No newline at end of file + private double[] residuals; + private NormalDistribution nd; + + @Override + public boolean test(SearchTask task) { + evaluate(task); + setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); + return significanceTest(); + } + + @Override + public void evaluate(SearchTask t) { + calculateResiduals(t); + residuals = transformResiduals(); + + final double sd = (new StandardDeviation()).evaluate(residuals); + nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical + // standard dev + final double statistic = TestUtils.kolmogorovSmirnovStatistic(nd, residuals); + this.setStatistic(derive(TEST_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Kolmogorov-Smirnov test"; + } + +} diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index aa35220a..63e7bbba 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -19,111 +19,114 @@ * An abstract superclass for the BIC and AIC statistics. * */ - public abstract class ModelSelectionCriterion extends Statistic { - private OptimiserStatistic os; - private int kq; //the number of parameters (dimensionality of the search vector) - private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); - private double criterion; - - public ModelSelectionCriterion(OptimiserStatistic os) { - super(); - setOptimiser(os); - } - - public ModelSelectionCriterion(ModelSelectionCriterion another) { - this.os = another.os.copy(); - this.kq = another.kq; - this.criterion = another.criterion; - } - - @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of parameters - calcCriterion(); - } - - /** - * This calculates either the AIC or BIC statistic, which only differ - * by the penalising term. - * @see penalisingTerm() - */ - - public void calcCriterion() { - final int n = os.getResiduals().size(); //sample size - criterion = n * log(os.variance()) + penalisingTerm(kq,n) + n * PENALISATION_FACTOR; - this.tellParent(new PropertyEvent(null, this, getStatistic())); - } - - /** - * The penalising term, which is different depending on implementation. - * @param k the number of model variables - * @param n the sample size - * @return the penalising term - */ - - public abstract double penalisingTerm(int k, int n); - - public abstract ModelSelectionCriterion copy(); - - /** - * Calculates the weight (in the Akaike sense) when comparing the model associated - * with this statistic with other models represented by statistics of the same type. - * @param the selection statistics of the same type as this one - * @return a {@code NumericProperty} of the {@code MODEL_WEIGHT} type, which is the probability - * this model is the best one. - */ - - public NumericProperty weight(List all) { - if(all.stream().anyMatch(s -> s.getClass() != this.getClass())) - throw new IllegalArgumentException("Cannot mix different model selection criteria!"); - final double sum = all.stream().map(criterion -> criterion.probability(all)).reduce( (a, b) -> a + b).get(); - return derive(MODEL_WEIGHT, probability(all)/sum); - } - - /** - * Calculates the probability that this model is the best among {@code all} others. - * @param all statistics from models that will be compared with this one - * @return the probability, which is a decimal value within the [0,1] range. - */ - - public double probability(List all) { - final double min = all.stream().map(criterion -> (double)criterion.getStatistic().getValue()).reduce( (a, b) -> a < b ? a : b).get(); - final double di = (double)this.getStatistic().getValue() - min; - return exp(-0.5*di); - } - - @Override - public String getDescriptor() { - return "Akaike Information Criterion (AIC)"; - } - - public int getNumVariables() { - return kq; - } - - public OptimiserStatistic getOptimiser() { - return os; - } - - public void setOptimiser(OptimiserStatistic os) { - this.os = os; - } - - public void setStatistic(NumericProperty p) { - requireType(p, MODEL_CRITERION); - this.criterion = (double)p.getValue(); - } - - public NumericProperty getStatistic() { - return derive(MODEL_CRITERION, criterion); - } - - @Override - public void set(NumericPropertyKeyword key, NumericProperty p) { - if(key == MODEL_CRITERION) - setStatistic(p); - } - -} \ No newline at end of file + private OptimiserStatistic os; + private int kq; //the number of parameters (dimensionality of the search vector) + private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); + private double criterion; + + public ModelSelectionCriterion(OptimiserStatistic os) { + super(); + setOptimiser(os); + } + + public ModelSelectionCriterion(ModelSelectionCriterion another) { + this.os = another.os.copy(); + this.kq = another.kq; + this.criterion = another.criterion; + } + + @Override + public void evaluate(SearchTask t) { + kq = t.alteredParameters().size(); //number of parameters + calcCriterion(); + } + + /** + * This calculates either the AIC or BIC statistic, which only differ by the + * penalising term. + * + * @see penalisingTerm() + */ + public void calcCriterion() { + final int n = os.getResiduals().size(); //sample size + criterion = n * log(os.variance()) + penalisingTerm(kq, n) + n * PENALISATION_FACTOR; + this.tellParent(new PropertyEvent(null, this, getStatistic())); + } + + /** + * The penalising term, which is different depending on implementation. + * + * @param k the number of model variables + * @param n the sample size + * @return the penalising term + */ + public abstract double penalisingTerm(int k, int n); + + public abstract ModelSelectionCriterion copy(); + + /** + * Calculates the weight (in the Akaike sense) when comparing the model + * associated with this statistic with other models represented by + * statistics of the same type. + * + * @param the selection statistics of the same type as this one + * @return a {@code NumericProperty} of the {@code MODEL_WEIGHT} type, which + * is the probability this model is the best one. + */ + public NumericProperty weight(List all) { + if (all.stream().anyMatch(s -> s.getClass() != this.getClass())) { + throw new IllegalArgumentException("Cannot mix different model selection criteria!"); + } + final double sum = all.stream().map(criterion -> criterion.probability(all)).reduce((a, b) -> a + b).get(); + return derive(MODEL_WEIGHT, probability(all) / sum); + } + + /** + * Calculates the probability that this model is the best among {@code all} + * others. + * + * @param all statistics from models that will be compared with this one + * @return the probability, which is a decimal value within the [0,1] range. + */ + public double probability(List all) { + final double min = all.stream().map(criterion -> (double) criterion.getStatistic().getValue()).reduce((a, b) -> a < b ? a : b).get(); + final double di = (double) this.getStatistic().getValue() - min; + return exp(-0.5 * di); + } + + @Override + public String getDescriptor() { + return "Akaike Information Criterion (AIC)"; + } + + public int getNumVariables() { + return kq; + } + + public OptimiserStatistic getOptimiser() { + return os; + } + + public void setOptimiser(OptimiserStatistic os) { + this.os = os; + } + + public void setStatistic(NumericProperty p) { + requireType(p, MODEL_CRITERION); + this.criterion = (double) p.getValue(); + } + + public NumericProperty getStatistic() { + return derive(MODEL_CRITERION, criterion); + } + + @Override + public void set(NumericPropertyKeyword key, NumericProperty p) { + if (key == MODEL_CRITERION) { + setStatistic(p); + } + } + +} diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index 59b9aff0..f7bb3ed6 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -12,76 +12,77 @@ import pulse.tasks.SearchTask; /** - * A normality test is invoked after a task finishes, to validate its result. - * It may be used as an acceptance criterion for tasks. - * - * For the test to pass, the model residuals need be distributed according - * to a (0, σ) normal distribution, where σ is the variance of the model residuals. As - * this is the pre-requisite for optimizers based on the ordinary least-square statistic, the normality - * test can also be used to estimate if a fit 'failed' or 'succeeded' in describing the data. + * A normality test is invoked after a task finishes, to validate its result. It + * may be used as an acceptance criterion for tasks. + * + * For the test to pass, the model residuals need be distributed according to a + * (0, σ) normal distribution, where σ is the variance of the model + * residuals. As this is the pre-requisite for optimizers based on the ordinary + * least-square statistic, the normality test can also be used to estimate if a + * fit 'failed' or 'succeeded' in describing the data. * */ - public abstract class NormalityTest extends ResidualStatistic { - private double statistic; - private double probability; - private static double significance = (double) def(SIGNIFICANCE).getValue(); - - private static String selectedTestDescriptor; - - protected NormalityTest() { - probability = (double) def(PROBABILITY).getValue(); - statistic = (double) def(TEST_STATISTIC).getValue(); - } - - public boolean significanceTest() { - return probability > significance; - } - - public static NumericProperty getStatisticalSignifiance() { - return derive(SIGNIFICANCE, significance); - } - - public static void setStatisticalSignificance(NumericProperty alpha) { - requireType(alpha, SIGNIFICANCE); - NormalityTest.significance = (double) alpha.getValue(); - } - - public NumericProperty getProbability() { - return derive(PROBABILITY, probability); - } - - public abstract boolean test(SearchTask task); - - @Override - public NumericProperty getStatistic() { - return derive(TEST_STATISTIC, statistic); - } - - @Override - public void setStatistic(NumericProperty statistic) { - requireType(statistic, TEST_STATISTIC); - this.statistic = (double) statistic.getValue(); - } - - public void setProbability(NumericProperty probability) { - requireType(probability, PROBABILITY); - this.probability = (double) probability.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == TEST_STATISTIC) - statistic = (double) property.getValue(); - } - - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } - - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - NormalityTest.selectedTestDescriptor = selectedTestDescriptor; - } - -} \ No newline at end of file + private double statistic; + private double probability; + private static double significance = (double) def(SIGNIFICANCE).getValue(); + + private static String selectedTestDescriptor; + + protected NormalityTest() { + probability = (double) def(PROBABILITY).getValue(); + statistic = (double) def(TEST_STATISTIC).getValue(); + } + + public boolean significanceTest() { + return probability > significance; + } + + public static NumericProperty getStatisticalSignifiance() { + return derive(SIGNIFICANCE, significance); + } + + public static void setStatisticalSignificance(NumericProperty alpha) { + requireType(alpha, SIGNIFICANCE); + NormalityTest.significance = (double) alpha.getValue(); + } + + public NumericProperty getProbability() { + return derive(PROBABILITY, probability); + } + + public abstract boolean test(SearchTask task); + + @Override + public NumericProperty getStatistic() { + return derive(TEST_STATISTIC, statistic); + } + + @Override + public void setStatistic(NumericProperty statistic) { + requireType(statistic, TEST_STATISTIC); + this.statistic = (double) statistic.getValue(); + } + + public void setProbability(NumericProperty probability) { + requireType(probability, PROBABILITY); + this.probability = (double) probability.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == TEST_STATISTIC) { + statistic = (double) property.getValue(); + } + } + + public static String getSelectedTestDescriptor() { + return selectedTestDescriptor; + } + + public static void setSelectedTestDescriptor(String selectedTestDescriptor) { + NormalityTest.selectedTestDescriptor = selectedTestDescriptor; + } + +} diff --git a/src/main/java/pulse/search/statistics/OptimiserStatistic.java b/src/main/java/pulse/search/statistics/OptimiserStatistic.java index 14506925..7cb5c992 100644 --- a/src/main/java/pulse/search/statistics/OptimiserStatistic.java +++ b/src/main/java/pulse/search/statistics/OptimiserStatistic.java @@ -1,32 +1,32 @@ package pulse.search.statistics; /** - * An Optimiser statistic is simply the objective function that is calculated - * by the Optimiser. + * An Optimiser statistic is simply the objective function that is calculated by + * the Optimiser. * */ - public abstract class OptimiserStatistic extends ResidualStatistic { - private static String selectedOptimiserDescriptor; - - public OptimiserStatistic(OptimiserStatistic stat) { - super(stat); - } - - protected OptimiserStatistic() { - super(); - } - - public static String getSelectedOptimiserDescriptor() { - return selectedOptimiserDescriptor; - } - - public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { - OptimiserStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; - } - - public abstract OptimiserStatistic copy(); - public abstract double variance(); - -} \ No newline at end of file + private static String selectedOptimiserDescriptor; + + public OptimiserStatistic(OptimiserStatistic stat) { + super(stat); + } + + protected OptimiserStatistic() { + super(); + } + + public static String getSelectedOptimiserDescriptor() { + return selectedOptimiserDescriptor; + } + + public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { + OptimiserStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; + } + + public abstract OptimiserStatistic copy(); + + public abstract double variance(); + +} diff --git a/src/main/java/pulse/search/statistics/PearsonCorrelation.java b/src/main/java/pulse/search/statistics/PearsonCorrelation.java index 64819e9b..401413cf 100644 --- a/src/main/java/pulse/search/statistics/PearsonCorrelation.java +++ b/src/main/java/pulse/search/statistics/PearsonCorrelation.java @@ -3,21 +3,20 @@ import org.apache.commons.math3.stat.correlation.PearsonsCorrelation; /** - * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Pearson Correlation. + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Pearson + * Correlation. * */ - - public class PearsonCorrelation extends CorrelationTest { - @Override - public double evaluate(double[] x, double[] y) { - return (new PearsonsCorrelation()).correlation(x, y); - } + @Override + public double evaluate(double[] x, double[] y) { + return (new PearsonsCorrelation()).correlation(x, y); + } - @Override - public String getDescriptor() { - return "Pearson's Product-Moment Correlation"; - } + @Override + public String getDescriptor() { + return "Pearson's Product-Moment Correlation"; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index 7bb32f41..b29f6b16 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -10,88 +10,86 @@ import pulse.tasks.SearchTask; /** - *The coefficient of determination represents the goodness of fit that a {@code HeatingCurve} - *provides for the {@code ExperimentalData} + * The coefficient of determination represents the goodness of fit that a + * {@code HeatingCurve} provides for the {@code ExperimentalData} * */ - public class RSquaredTest extends NormalityTest { - private SumOfSquares sos; - private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); - - public RSquaredTest() { - super(); - sos = new SumOfSquares(); - } - - @Override - public boolean test(SearchTask task) { - evaluate(task); - sos = new SumOfSquares(); - return getStatistic().compareTo(signifiance) > 0; - } - - /** - * Calculates the coefficient of determination, or simply the - * R2 value. - *

- * First, the mean temperature of the {@code data} is calculated. Then, the - * {@code TSS} (total sum of squares) is calculated as proportional to the - * variance of data. The residual sum of squares ({@code RSS}) is calculated by - * calling {@code this.deviationSquares(curve)}. Finally, these values are - * combined together as: {@code 1 - RSS/TSS}. - *

- * - * @param t the task containing the reference data - * @see Wikipedia - * page - */ - - @Override - public void evaluate(SearchTask t) { - var reference = t.getExperimentalCurve(); - - sos.evaluate(t); - - final int start = reference.getIndexRange().getLowerBound(); - final int end = reference.getIndexRange().getUpperBound(); - - final double mean = mean(reference, start, end); - double TSS = 0; - - for (int i = start; i < end; i++) { - TSS += pow(reference.signalAt(i) - mean, 2); - } - - TSS /= (end - start); - - setStatistic(derive(TEST_STATISTIC, (1. - (double)sos.getStatistic().getValue() / TSS))); - } - - private double mean(ExperimentalData data, final int start, final int end) { - double mean = 0; - - for (int i = start; i < end; i++) { - mean += data.signalAt(i); - } - - mean /= (end - start); - return mean; - } - - public SumOfSquares getSumOfSquares() { - return sos; - } - - public void setSumOfSquares(SumOfSquares sos) { - this.sos = sos; - } - - @Override - public String getDescriptor() { - return "R2 test"; - } - -} \ No newline at end of file + private SumOfSquares sos; + private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); + + public RSquaredTest() { + super(); + sos = new SumOfSquares(); + } + + @Override + public boolean test(SearchTask task) { + evaluate(task); + sos = new SumOfSquares(); + return getStatistic().compareTo(signifiance) > 0; + } + + /** + * Calculates the coefficient of determination, or simply the + * R2 value. + *

+ * First, the mean temperature of the {@code data} is calculated. Then, the + * {@code TSS} (total sum of squares) is calculated as proportional to the + * variance of data. The residual sum of squares ({@code RSS}) is calculated + * by calling {@code this.deviationSquares(curve)}. Finally, these values + * are combined together as: {@code 1 - RSS/TSS}. + *

+ * + * @param t the task containing the reference data + * @see Wikipedia + * page + */ + @Override + public void evaluate(SearchTask t) { + var reference = t.getExperimentalCurve(); + + sos.evaluate(t); + + final int start = reference.getIndexRange().getLowerBound(); + final int end = reference.getIndexRange().getUpperBound(); + + final double mean = mean(reference, start, end); + double TSS = 0; + + for (int i = start; i < end; i++) { + TSS += pow(reference.signalAt(i) - mean, 2); + } + + TSS /= (end - start); + + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); + } + + private double mean(ExperimentalData data, final int start, final int end) { + double mean = 0; + + for (int i = start; i < end; i++) { + mean += data.signalAt(i); + } + + mean /= (end - start); + return mean; + } + + public SumOfSquares getSumOfSquares() { + return sos; + } + + public void setSumOfSquares(SumOfSquares sos) { + this.sos = sos; + } + + @Override + public String getDescriptor() { + return "R2 test"; + } + +} diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index 1b070b60..a2205258 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -6,68 +6,67 @@ import pulse.tasks.SearchTask; /** - * This is an experimental feature. The objective function here is equal to the ordinary least-square (OLS) - * plus a penalising term proportional to the squared length of a search vector. This way, search vectors - * of lower dimensionality are favoured. + * This is an experimental feature. The objective function here is equal to the + * ordinary least-square (OLS) plus a penalising term proportional to the + * squared length of a search vector. This way, search vectors of lower + * dimensionality are favoured. * */ - public class RegularisedLeastSquares extends OptimiserStatistic { - private double lambda = 1e-4; - private SumOfSquares sos; - - public RegularisedLeastSquares() { - super(); - sos = new SumOfSquares(); - } - - public RegularisedLeastSquares(RegularisedLeastSquares rls) { - super(rls); - sos = new SumOfSquares(rls.sos); - this.lambda = rls.lambda; - } - - /** - * The lambda is the regularisation strength. - * @return the lambda factor. - */ + private double lambda = 1e-4; + private SumOfSquares sos; + + public RegularisedLeastSquares() { + super(); + sos = new SumOfSquares(); + } + + public RegularisedLeastSquares(RegularisedLeastSquares rls) { + super(rls); + sos = new SumOfSquares(rls.sos); + this.lambda = rls.lambda; + } + + /** + * The lambda is the regularisation strength. + * + * @return the lambda factor. + */ + public double getLambda() { + return lambda; + } - public double getLambda() { - return lambda; - } + public void setLambda(double lambda) { + this.lambda = lambda; + } - public void setLambda(double lambda) { - this.lambda = lambda; - } - - /* + /* * OLS with L2 regularisation. The penalisation term is equal to {@code lambda} times the * L2 norm of the search vector. * @see pulse.search.statistics.SumOfSquares - */ - - @Override - public void evaluate(SearchTask t) { - sos.evaluate(t); - final double ssr = (double)sos.getStatistic().getValue(); - final double statistic = ssr + lambda*t.searchVector().lengthSq(); - setStatistic(derive(OPTIMISER_STATISTIC, statistic)); - } + */ + @Override + public void evaluate(SearchTask t) { + sos.evaluate(t); + final double ssr = (double) sos.getStatistic().getValue(); + final double statistic = ssr + lambda * t.searchVector().lengthSq(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "L2 Regularised Least Squares"; + } + + @Override + public double variance() { + return (double) sos.getStatistic().getValue(); + } + + @Override + public OptimiserStatistic copy() { + return new RegularisedLeastSquares(this); + } - @Override - public String getDescriptor() { - return "L2 Regularised Least Squares"; - } - - @Override - public double variance() { - return (double)sos.getStatistic().getValue(); - } - - @Override - public OptimiserStatistic copy() { - return new RegularisedLeastSquares(this); - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 7129ff81..01ed160b 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -16,109 +16,116 @@ import pulse.tasks.SearchTask; /** - * An abstract statistic (= a numeric value resulting from a statistical procedure) that operates with model residuals. - * The list of residuals is stored in a field value for objects of this class. Each {@code SearchTask} will have at - * least two {@code ResidualStatistic}s associated with its {@code Calculation}s. + * An abstract statistic (= a numeric value resulting from a statistical + * procedure) that operates with model residuals. The list of residuals is + * stored in a field value for objects of this class. Each {@code SearchTask} + * will have at least two {@code ResidualStatistic}s associated with its + * {@code Calculation}s. + * * @see pulse.tasks.SearchTask * @see pulse.tasks.Calculation * */ - public abstract class ResidualStatistic extends Statistic { - private double statistic; - private List residuals; - - public ResidualStatistic() { - super(); - residuals = new ArrayList<>(); - setPrefix("Residuals"); - } - - public ResidualStatistic(ResidualStatistic another) { - this.statistic = another.statistic; - this.residuals = new ArrayList<>(another.residuals); - } - - public double[] transformResiduals() { - return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); - } - - /** - * This will calculate the residuals for the {@code task} using the time sequence defined - * by the {@code ExperimentalData} object. The residuals are calculated between the model, - * which was previously used to populate the {@code HeatingCurve} and the experimental data. - * The temperature value of the model at the reference time is ti. - * and unknown a priori. Therefore, it needs to be interpolated based on the discrete dataset - * generated by the solver. The interpolation is currently done using natural cubic splines, - * which are re-constructed each time a new solution is generated. Therefore, calling this method - * does not involve expensive calculation of the spline coefficents. The residuals are calculated - * only for the range that is specified by the {@code ExperimentalData} reference. The output of this method - * is stored in the field of the {@code residuals} object. - * @param task the optimisation task - * @see pulse.input.ExperimentalData - * @see pulse.HeatingCurve - */ - - public void calculateResiduals(SearchTask task) { - var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); - var reference = task.getExperimentalCurve(); - - residuals.clear(); - var indexRange = reference.getIndexRange(); - var time = reference.getTimeSequence(); - - var s = estimate.getSplineInterpolation(); - - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); - - double interpolated; - - for (int i = startIndex; i <= endIndex; i++) { - /* + private double statistic; + private List residuals; + + public ResidualStatistic() { + super(); + residuals = new ArrayList<>(); + setPrefix("Residuals"); + } + + public ResidualStatistic(ResidualStatistic another) { + this.statistic = another.statistic; + this.residuals = new ArrayList<>(another.residuals); + } + + public double[] transformResiduals() { + return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); + } + + /** + * This will calculate the residuals for the {@code task} using the time + * sequence defined by the {@code ExperimentalData} object. The residuals + * are calculated between the model, which was previously used to populate + * the {@code HeatingCurve} and the experimental data. The temperature value + * of the model at the reference time is + * ti. and unknown a + * priori. Therefore, it needs to be interpolated based on the discrete + * dataset generated by the solver. The interpolation is currently done + * using natural cubic splines, which are re-constructed each time a new + * solution is generated. Therefore, calling this method does not involve + * expensive calculation of the spline coefficents. The residuals are + * calculated only for the range that is specified by the + * {@code ExperimentalData} reference. The output of this method is stored + * in the field of the {@code residuals} object. + * + * @param task the optimisation task + * @see pulse.input.ExperimentalData + * @see pulse.HeatingCurve + */ + public void calculateResiduals(SearchTask task) { + var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); + var reference = task.getExperimentalCurve(); + + residuals.clear(); + var indexRange = reference.getIndexRange(); + var time = reference.getTimeSequence(); + + var s = estimate.getSplineInterpolation(); + + int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); + int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); + + double interpolated; + + for (int i = startIndex; i <= endIndex; i++) { + /* * find the point on the calculated heating curve which has the closest time * value smaller than the experimental points' time value - */ + */ - interpolated = s.value(reference.timeAt(i)); + interpolated = s.value(reference.timeAt(i)); - residuals.add(new double[] { reference.timeAt(i), - reference.signalAt(i) - interpolated }); // y_exp - y* + residuals.add(new double[]{reference.timeAt(i), + reference.signalAt(i) - interpolated}); // y_exp - y* - } + } - } + } - public List getResiduals() { - return residuals; - } + public List getResiduals() { + return residuals; + } - public double residualUpperBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); - } + public double residualUpperBound() { + return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); + } - public double residualLowerBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); - } + public double residualLowerBound() { + return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); + } - public NumericProperty getStatistic() { - return derive(OPTIMISER_STATISTIC, statistic); - } + public NumericProperty getStatistic() { + return derive(OPTIMISER_STATISTIC, statistic); + } - public void setStatistic(NumericProperty statistic) { - requireType(statistic, OPTIMISER_STATISTIC); - this.statistic = (double) statistic.getValue(); - } + public void setStatistic(NumericProperty statistic) { + requireType(statistic, OPTIMISER_STATISTIC); + this.statistic = (double) statistic.getValue(); + } - public void incrementStatistic(final double increment) { - this.statistic += increment; - } + public void incrementStatistic(final double increment) { + this.statistic += increment; + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == OPTIMISER_STATISTIC) - statistic = (double) property.getValue(); - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == OPTIMISER_STATISTIC) { + statistic = (double) property.getValue(); + } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java index 0e6ae516..8bb45072 100644 --- a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java @@ -3,20 +3,20 @@ import org.apache.commons.math3.stat.correlation.SpearmansCorrelation; /** - * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Spearmans Correlation. + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Spearmans + * Correlation. * */ - public class SpearmansCorrelationTest extends CorrelationTest { - - @Override - public double evaluate(double[] x, double[] y) { - return (new SpearmansCorrelation()).correlation(x, y); - } - @Override - public String getDescriptor() { - return "Spearman's Rank Correlation"; - } + @Override + public double evaluate(double[] x, double[] y) { + return (new SpearmansCorrelation()).correlation(x, y); + } + + @Override + public String getDescriptor() { + return "Spearman's Rank Correlation"; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 6f84cca7..6ac94f15 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -6,15 +6,16 @@ import pulse.util.Reflexive; /** - * A statistic is an abstract class that hosts the {@code evaluate} method - * to validate the results of a {@code SearchTask}. + * A statistic is an abstract class that hosts the {@code evaluate} method to + * validate the results of a {@code SearchTask}. * */ - public abstract class Statistic extends PropertyHolder implements Reflexive { - public abstract void evaluate(SearchTask t); - public abstract NumericProperty getStatistic(); - public abstract void setStatistic(NumericProperty statistic); + public abstract void evaluate(SearchTask t); + + public abstract NumericProperty getStatistic(); + + public abstract void setStatistic(NumericProperty statistic); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index d88de831..c4905ef8 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -6,59 +6,60 @@ import pulse.tasks.SearchTask; /** - * The standard optimality criterion of the L2 norm condition, or simply ordinary least squares. + * The standard optimality criterion of the L2 norm condition, or simply + * ordinary least squares. * */ - public class SumOfSquares extends OptimiserStatistic { - - public SumOfSquares() { - super(); - } - - public SumOfSquares(SumOfSquares sos) { - super(sos); - } - - /** - * Calculates the sum of squared deviations using {@code curve} as reference. - *

- * This calculates - * i=i1i2(T(ti)-T(ti)ref)2, - * where - * Tiref - * is the temperature value corresponding to the {@code time} at index {@code i} - * for the reference {@code curve}. Note that the time - * ti corresponds to the - * reference's time list, which generally does not match to that of this - * heating curve. The - * T(ti) - * is the interpolated value. - * - * @param t The task containing the reference and calculated curves - * @see calculateResiduals() - */ - - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> r[1] * r[1]).reduce(Double::sum).get() / getResiduals().size(); - setStatistic(derive(OPTIMISER_STATISTIC, statistic)); - } - @Override - public String getDescriptor() { - return "Ordinary Least Squares"; - } + public SumOfSquares() { + super(); + } + + public SumOfSquares(SumOfSquares sos) { + super(sos); + } + + /** + * Calculates the sum of squared deviations using {@code curve} as + * reference. + *

+ * This calculates + * i=i1i2(T(ti)-T(ti)ref)2, + * where + * Tiref + * is the temperature value corresponding to the {@code time} at index + * {@code i} for the reference {@code curve}. Note that the time + * ti corresponds to the + * reference's time list, which generally does not match to that of + * this heating curve. The + * T(ti) + * is the interpolated value. + * + * @param t The task containing the reference and calculated curves + * @see calculateResiduals() + */ + @Override + public void evaluate(SearchTask t) { + calculateResiduals(t); + final double statistic = getResiduals().stream().map(r -> r[1] * r[1]) + .reduce(Double::sum).get() / getResiduals().size(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Ordinary Least Squares"; + } - @Override - public double variance() { - return (double)getStatistic().getValue(); - } + @Override + public double variance() { + return (double) getStatistic().getValue(); + } - @Override - public OptimiserStatistic copy() { - return new SumOfSquares(this); - } + @Override + public OptimiserStatistic copy() { + return new SumOfSquares(this); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/package-info.java b/src/main/java/pulse/search/statistics/package-info.java index d0209dc0..4a9e7b35 100644 --- a/src/main/java/pulse/search/statistics/package-info.java +++ b/src/main/java/pulse/search/statistics/package-info.java @@ -1,5 +1,4 @@ /** * PULsE Statistical Kit. */ - -package pulse.search.statistics; \ No newline at end of file +package pulse.search.statistics; diff --git a/src/main/java/pulse/tasks/Identifier.java b/src/main/java/pulse/tasks/Identifier.java index 4b19c0f6..f640fbd3 100644 --- a/src/main/java/pulse/tasks/Identifier.java +++ b/src/main/java/pulse/tasks/Identifier.java @@ -13,50 +13,49 @@ *

* */ - public class Identifier extends NumericProperty { - private static int lastId = -1; - - private Identifier(int value, boolean addToList) { - super(def(IDENTIFIER)); - setValue(value); - if (addToList) - setLastId(value); - } - - private static void setLastId(int value) { - Identifier.lastId = value; - } - - /** - * Creates an {@code Identifier} by incrementing the previously recorded ID. - */ - - public Identifier() { - this(Identifier.lastId + 1, true); - } - - /** - * Seeks an {@code Identifier} from the list of available tasks in - * {@code TaskManager} that matches this {@code string}. - * - * @param string the string describing the identifier. - * @return a matching {@code Identifier}. - */ - - public static Identifier parse(String string) { - var i = TaskManager.getManagerInstance().getTaskList().stream().map(t -> t.getIdentifier()) - .filter(id -> id.toString().equals(string)).findFirst(); - return i.isPresent() ? i.get() : null; - } - - public static Identifier externalIdentifier(int id) { - return id > -1 ? new Identifier(id, false) : null; - } - - @Override - public String toString() { - return Messages.getString("Identifier.Tag") + " " + getValue(); - } - -} \ No newline at end of file + + private static int lastId = -1; + + private Identifier(int value, boolean addToList) { + super(def(IDENTIFIER)); + setValue(value); + if (addToList) { + setLastId(value); + } + } + + private static void setLastId(int value) { + Identifier.lastId = value; + } + + /** + * Creates an {@code Identifier} by incrementing the previously recorded ID. + */ + public Identifier() { + this(Identifier.lastId + 1, true); + } + + /** + * Seeks an {@code Identifier} from the list of available tasks in + * {@code TaskManager} that matches this {@code string}. + * + * @param string the string describing the identifier. + * @return a matching {@code Identifier}. + */ + public static Identifier parse(String string) { + var i = TaskManager.getManagerInstance().getTaskList().stream().map(t -> t.getIdentifier()) + .filter(id -> id.toString().equals(string)).findFirst(); + return i.isPresent() ? i.get() : null; + } + + public static Identifier externalIdentifier(int id) { + return id > -1 ? new Identifier(id, false) : null; + } + + @Override + public String toString() { + return Messages.getString("Identifier.Tag") + " " + getValue(); + } + +} diff --git a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java index 0f8cdc4c..5bbdafa3 100644 --- a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java +++ b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java @@ -3,5 +3,6 @@ import pulse.tasks.logs.LogEntry; public interface DataCollectionListener { - public void onDataCollected(LogEntry e); -} \ No newline at end of file + + public void onDataCollected(LogEntry e); +} diff --git a/src/main/java/pulse/tasks/listeners/LogEntryListener.java b/src/main/java/pulse/tasks/listeners/LogEntryListener.java index f528b4ee..660ce187 100644 --- a/src/main/java/pulse/tasks/listeners/LogEntryListener.java +++ b/src/main/java/pulse/tasks/listeners/LogEntryListener.java @@ -5,8 +5,8 @@ public interface LogEntryListener { - public void onNewEntry(LogEntry e); + public void onNewEntry(LogEntry e); - public void onLogFinished(Log log); + public void onLogFinished(Log log); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java index f71d255e..54077573 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java @@ -4,14 +4,14 @@ public class ResultFormatEvent { - private ResultFormat rf; + private ResultFormat rf; - public ResultFormatEvent(ResultFormat rf) { - this.rf = rf; - } + public ResultFormatEvent(ResultFormat rf) { + this.rf = rf; + } - public ResultFormat getResultFormat() { - return rf; - } + public ResultFormat getResultFormat() { + return rf; + } } diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java index 9a8e1896..bfa0c1c4 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java @@ -2,6 +2,6 @@ public interface ResultFormatListener { - public void resultFormatChanged(ResultFormatEvent rfe); + public void resultFormatChanged(ResultFormatEvent rfe); } diff --git a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java index 877eb73d..61b767ca 100644 --- a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java +++ b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java @@ -3,5 +3,6 @@ import pulse.tasks.logs.StateEntry; public interface StatusChangeListener { - public void onStatusChange(StateEntry e); + + public void onStatusChange(StateEntry e); } diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 1e8550ce..0c232abd 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -4,84 +4,66 @@ public class TaskRepositoryEvent { - private State state; - private Identifier id; - - public TaskRepositoryEvent(State state, Identifier id) { - this.state = state; - this.id = id; - } - - public State getState() { - return state; - } - - public Identifier getId() { - return id; - } - - public enum State { - - /** - * Indicates a task has been added to the repository. - */ - - TASK_ADDED, - - /** - * A task has been removed from the repository. - */ - - TASK_REMOVED, - - /** - * A task has been submitted for execution. - */ - - TASK_SUBMITTED, - - /** - * A task has finished executing. - */ - - TASK_FINISHED, - - /** - * A task has been reset. - */ - - TASK_RESET, - - /** - * An external request has been received to browse previous calculations. - */ - - TASK_BROWSING_REQUEST, - - /** - * The task has switched to a new model. - */ - - TASK_MODEL_SWITCH, - - /** - * The task changed its selection criterion. - */ - - TASK_CRITERION_SWITCH, - - /** - * Indicates the task has discarded superfluous calculations. - */ - - BEST_MODEL_SELECTED, - - /** - * The repository has been shut down/ - */ - - SHUTDOWN; - - } - -} \ No newline at end of file + private State state; + private Identifier id; + + public TaskRepositoryEvent(State state, Identifier id) { + this.state = state; + this.id = id; + } + + public State getState() { + return state; + } + + public Identifier getId() { + return id; + } + + public enum State { + + /** + * Indicates a task has been added to the repository. + */ + TASK_ADDED, + /** + * A task has been removed from the repository. + */ + TASK_REMOVED, + /** + * A task has been submitted for execution. + */ + TASK_SUBMITTED, + /** + * A task has finished executing. + */ + TASK_FINISHED, + /** + * A task has been reset. + */ + TASK_RESET, + /** + * An external request has been received to browse previous + * calculations. + */ + TASK_BROWSING_REQUEST, + /** + * The task has switched to a new model. + */ + TASK_MODEL_SWITCH, + /** + * The task changed its selection criterion. + */ + TASK_CRITERION_SWITCH, + /** + * Indicates the task has discarded superfluous calculations. + */ + BEST_MODEL_SELECTED, + /** + * The repository has been shut down/ + */ + SHUTDOWN; + + } + +} diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java index a5462427..d30cf038 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java @@ -1,5 +1,6 @@ package pulse.tasks.listeners; public interface TaskRepositoryListener { - public void onTaskListChanged(TaskRepositoryEvent e); -} \ No newline at end of file + + public void onTaskListChanged(TaskRepositoryEvent e); +} diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java index b4149942..89ed0659 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java @@ -4,18 +4,18 @@ public class TaskSelectionEvent extends EventObject { - /** - * - */ - private static final long serialVersionUID = 4278832926994139917L; + /** + * + */ + private static final long serialVersionUID = 4278832926994139917L; - public TaskSelectionEvent(Object source) { - super(source); - // TODO Auto-generated constructor stub - } + public TaskSelectionEvent(Object source) { + super(source); + // TODO Auto-generated constructor stub + } - public void setSource(Object source) { - this.source = source; - } + public void setSource(Object source) { + this.source = source; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java index fbcda125..fb422b47 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java @@ -2,6 +2,6 @@ public interface TaskSelectionListener { - public void onSelectionChanged(TaskSelectionEvent e); + public void onSelectionChanged(TaskSelectionEvent e); } diff --git a/src/main/java/pulse/tasks/listeners/package-info.java b/src/main/java/pulse/tasks/listeners/package-info.java index 44d859eb..034b020d 100644 --- a/src/main/java/pulse/tasks/listeners/package-info.java +++ b/src/main/java/pulse/tasks/listeners/package-info.java @@ -4,5 +4,4 @@ * generation, as well as with the task repository events generated by a * {@code TaskManager}. */ - -package pulse.tasks.listeners; \ No newline at end of file +package pulse.tasks.listeners; diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index 0f804d8c..cacac2f2 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -9,45 +9,49 @@ public class CorrelationLogEntry extends LogEntry { - public CorrelationLogEntry(SearchTask t) { - super(t); - } - - @Override - public String toString() { - var t = TaskManager.getManagerInstance().getTask(getIdentifier()); - var buffer = t.getCorrelationBuffer(); - var test = t.getCorrelationTest(); - var map = buffer.evaluate(test); - - if (map == null) - return ""; - - if (map.isEmpty()) - return ""; - - StringBuilder sb = new StringBuilder(); - sb.append("

"); - sb.append(""); - - for (ImmutablePair key : map.keySet()) { - sb.append(""); - } - - sb.append("
Correlation table
x y Correlation
"); - sb.append(def(key.getFirst()).getAbbreviation(false)); - sb.append(""); - sb.append(def(key.getSecond()).getAbbreviation(false)); - sb.append(""); - if (test.compareToThreshold(map.get(key))) - sb.append(""); - sb.append("" + String.format("%3.2f", map.get(key)) + ""); - if (test.compareToThreshold(map.get(key))) - sb.append(""); - sb.append("

"); - - return sb.toString(); - - } + public CorrelationLogEntry(SearchTask t) { + super(t); + } + + @Override + public String toString() { + var t = TaskManager.getManagerInstance().getTask(getIdentifier()); + var buffer = t.getCorrelationBuffer(); + var test = t.getCorrelationTest(); + var map = buffer.evaluate(test); + + if (map == null) { + return ""; + } + + if (map.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("

"); + sb.append(""); + + for (ImmutablePair key : map.keySet()) { + sb.append(""); + } + + sb.append("
Correlation table
x y Correlation
"); + sb.append(def(key.getFirst()).getAbbreviation(false)); + sb.append(""); + sb.append(def(key.getSecond()).getAbbreviation(false)); + sb.append(""); + if (test.compareToThreshold(map.get(key))) { + sb.append(""); + } + sb.append("" + String.format("%3.2f", map.get(key)) + ""); + if (test.compareToThreshold(map.get(key))) { + sb.append(""); + } + sb.append("

"); + + return sb.toString(); + + } } diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 196ee139..8448b5fb 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -15,67 +15,64 @@ * from a {@code SearchTask}. The output is accessible via the * {@code toString()} method. *

- * + * */ - public class DataLogEntry extends LogEntry { - private List entry; - - /** - * Creates a new {@code DataLogEntry} based on the current values of the - * properties from {@code task} which match the currently selected - * {@code LogFormat}. - * - * @param task a task, which will be used to build the {@code DataLogEntry} - */ - - public DataLogEntry(SearchTask task) { - super(task); - try { - fill(); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Failed to fill this log entry with data. Details below."); - e.printStackTrace(); - } - } - - /** - * Fills this {@code DataLogEtnry} with properties from the {@code SearchTask}, - * which have types matching to those listed in the {@code LogFormat}. - * - * @throws IllegalAccessException if the call to - * {@code task.numericProperties() fails} - * @throws IllegalArgumentException if the call to - * {@code task.numericProperties() fails} - * @throws InvocationTargetException if the call to - * {@code task.numericProperties() fails} - */ - - private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - var task = TaskManager.getManagerInstance().getTask(getIdentifier()); - - entry = task.alteredParameters(); - Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); - entry.add(0, task.getIterativeState().getIteration()); - } - - public List getData() { - return entry; - } - - /** - * This {@code String} will be displayed by the {@code LogPane} if the verbose - * log option is enabled. - * - * @see pulse.ui.components.LogPane - */ - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - /* + private List entry; + + /** + * Creates a new {@code DataLogEntry} based on the current values of the + * properties from {@code task} which match the currently selected + * {@code LogFormat}. + * + * @param task a task, which will be used to build the {@code DataLogEntry} + */ + public DataLogEntry(SearchTask task) { + super(task); + try { + fill(); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Failed to fill this log entry with data. Details below."); + e.printStackTrace(); + } + } + + /** + * Fills this {@code DataLogEtnry} with properties from the + * {@code SearchTask}, which have types matching to those listed in the + * {@code LogFormat}. + * + * @throws IllegalAccessException if the call to + * {@code task.numericProperties() fails} + * @throws IllegalArgumentException if the call to + * {@code task.numericProperties() fails} + * @throws InvocationTargetException if the call to + * {@code task.numericProperties() fails} + */ + private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + var task = TaskManager.getManagerInstance().getTask(getIdentifier()); + + entry = task.alteredParameters(); + Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); + entry.add(0, task.getIterativeState().getIteration()); + } + + public List getData() { + return entry; + } + + /** + * This {@code String} will be displayed by the {@code LogPane} if the + * verbose log option is enabled. + * + * @see pulse.ui.components.LogPane + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + /* * // UNCOMMENT THIS TO PRODUCE EASY-TO-READ DATA ENTRIES sb.append("\n"); * * for(NumericProperty p : entry) { sb.append((p.getValue() instanceof Double ? @@ -83,26 +80,25 @@ public String toString() { * sb.append("\n"); } * * return sb.toString(); - */ - - sb.append(""); + */ + sb.append("
"); - for (NumericProperty p : entry) { - sb.append("<
"); - } + for (NumericProperty p : entry) { + sb.append("<
"); + } - sb.append("
"); - sb.append(p.getAbbreviation(false)); - sb.append(""); - sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ - sb.append(""); - sb.append(p.formattedOutput()); - sb.append(""); - sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ - sb.append("
"); + sb.append(p.getAbbreviation(false)); + sb.append(""); + sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ + sb.append(""); + sb.append(p.formattedOutput()); + sb.append(""); + sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ + sb.append("


"); + sb.append("

"); - return sb.toString(); + return sb.toString(); - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 251d605f..15529097 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -4,67 +4,48 @@ * An enum which lists different possible problems wit the {@code SearchTask}. * */ - public enum Details { - NONE, - - /** - * The {@code Problem} has not been specified by the user. - */ - - MISSING_PROBLEM_STATEMENT, - - /** - * The {@code DifferenceScheme} for solving the {@code Problem} has not been - * specified by the user. - */ - - MISSING_DIFFERENCE_SCHEME, - - /** - * A heating curve has not been set up for the {@code DifferenceScheme}. - */ - - MISSING_HEATING_CURVE, - - /** - * There is no information about the selected optimiser. - */ - - MISSING_OPTIMISER, - - /** - * The buffer has not been created. - */ - - MISSING_BUFFER, - - /** - * The optimisation statistic is not suported by the selected optimiser. - */ - - INCOMPATIBLE_OPTIMISER, - - /** - * Some data is missing in the problem statement. Probably, the interpolation - * datasets have been set up incorrectly or the specific heat and density data - * have not been loaded. - */ - - INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT, - - SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, - - PARAMETER_VALUES_NOT_SENSIBLE, - - MAX_ITERATIONS_REACHED, - - ABNORMAL_DISTRIBUTION_OF_RESIDUALS; - - @Override - public String toString() { - return Status.parse(super.toString()); - } - -} \ No newline at end of file + NONE, + /** + * The {@code Problem} has not been specified by the user. + */ + MISSING_PROBLEM_STATEMENT, + /** + * The {@code DifferenceScheme} for solving the {@code Problem} has not been + * specified by the user. + */ + MISSING_DIFFERENCE_SCHEME, + /** + * A heating curve has not been set up for the {@code DifferenceScheme}. + */ + MISSING_HEATING_CURVE, + /** + * There is no information about the selected optimiser. + */ + MISSING_OPTIMISER, + /** + * The buffer has not been created. + */ + MISSING_BUFFER, + /** + * The optimisation statistic is not suported by the selected optimiser. + */ + INCOMPATIBLE_OPTIMISER, + /** + * Some data is missing in the problem statement. Probably, the + * interpolation datasets have been set up incorrectly or the specific heat + * and density data have not been loaded. + */ + INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT, + SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, + PARAMETER_VALUES_NOT_SENSIBLE, + MAX_ITERATIONS_REACHED, + ABNORMAL_DISTRIBUTION_OF_RESIDUALS; + + @Override + public String toString() { + return Status.parse(super.toString()); + } + +} diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index bd59d5ef..c8780bac 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -18,190 +18,178 @@ * such as changes of status and/or data collection events. * */ - public class Log extends Group { - private List logEntries; - private LocalTime start; - private LocalTime end; - private Identifier id; - private List listeners; - private static boolean verbose = false; - - /** - * Creates a {@code Log} for this {@code task} that will automatically store - * {@code TaskStatEvent}s and a list of {@code DataLogEntr}ies in thread-safe - * collections. This is done by adding a {@code TaskListener} and a - * {@code StatusChangeListener} to the {@code task} object. - * - * @param task the task to be logged. - */ - - public Log(SearchTask task) { - Objects.requireNonNull(task, Messages.getString("Log.NullTaskError")); - - setParent(task); - id = task.getIdentifier(); - - this.logEntries = new CopyOnWriteArrayList<>(); - listeners = new CopyOnWriteArrayList<>(); - - task.addTaskListener(le -> { - - /** - * Do these actions each time data has been collected for this task. - */ - - if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { - logEntries.add(le); - notifyListeners(le); - } - - }); - - task.addStatusChangeListener(new StatusChangeListener() { - - /** - * Do these actions every time the task status has changed. - */ - - @Override - public void onStatusChange(StateEntry e) { - logEntries.add(e); - - if (e.getStatus() == Status.IN_PROGRESS) { - start = e.getTime(); - end = null; - } - - else { - end = e.getTime(); - } - - notifyListeners(e); - - if (e.getState() == Status.DONE) - logFinished(); - - } - - }); - - } - - private void logFinished() { - listeners.stream().forEach(l -> l.onLogFinished(this)); - } - - private void notifyListeners(LogEntry logEntry) { - listeners.stream().forEach(l -> l.onNewEntry(logEntry)); - } - - public List getListeners() { - return listeners; - } - - public void addListener(LogEntryListener l) { - listeners.add(l); - } - - public Identifier getIdentifier() { - return id; - } - - /** - * Checks whether this {@code Log} has observed a {@code TaskStateEvent} - * triggered by a change of status of its respective {@code SearchTask} to - * {@code IN_PROGRESS}. - * - * @return {@code true} if the start time is not {@code null} - */ - - public boolean isStarted() { - return start != null; - } - - /** - * Outputs all log entries consecutively. - */ - - @Override - public String toString() { - - StringBuilder sb = new StringBuilder(); - String newLine = System.lineSeparator(); - - sb.append(TaskManager.getManagerInstance().getTask(id)); - sb.append(newLine); - sb.append(newLine); - - for (LogEntry le : logEntries) { - sb.append(le); - sb.append(newLine); - } - - return sb.toString(); - - } - - public List getLogEntries() { - return logEntries; - } - - /** - * This is the time after the creation of the {@code Log} when a change of - * status to {@code IN_PROGRESS} happened. - * - * @return the start time - */ - - public LocalTime getStart() { - return start; - } - - /** - * This is the time after the creation of the {@code Log} when a change of - * status to {@code DONE} happened. - * - * @return the start time - */ - - public LocalTime getEnd() { - return end; - } - - /** - * Finds the last recorded entry in this {@code Log}. - * - * @return last recorded entry. - */ - - public LogEntry lastEntry() { - return logEntries.stream().reduce((first, second) -> second).get(); - } - - /** - * Checks whether this {@code Log} is verbose. Verbose logs stores all data - * entries and outputs them on request. This is useful to get an idea of how the - * search method works, how many iterations are taken to reach a converged - * value, etc. - * - * @return {@code true} if the verbose flag is on - */ - - public static boolean isVerbose() { - return verbose; - } - - /** - * Sets the verbose flag to {@code verbose} - * - * @param verbose the new value of the flag - * @see isVerbose() - */ - - public static void setVerbose(boolean verbose) { - Log.verbose = verbose; - } - -} \ No newline at end of file + private List logEntries; + private LocalTime start; + private LocalTime end; + private Identifier id; + private List listeners; + private static boolean verbose = false; + + /** + * Creates a {@code Log} for this {@code task} that will automatically store + * {@code TaskStatEvent}s and a list of {@code DataLogEntr}ies in + * thread-safe collections. This is done by adding a {@code TaskListener} + * and a {@code StatusChangeListener} to the {@code task} object. + * + * @param task the task to be logged. + */ + public Log(SearchTask task) { + Objects.requireNonNull(task, Messages.getString("Log.NullTaskError")); + + setParent(task); + id = task.getIdentifier(); + + this.logEntries = new CopyOnWriteArrayList<>(); + listeners = new CopyOnWriteArrayList<>(); + + task.addTaskListener(le -> { + + /** + * Do these actions each time data has been collected for this task. + */ + if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { + logEntries.add(le); + notifyListeners(le); + } + + }); + + task.addStatusChangeListener(new StatusChangeListener() { + + /** + * Do these actions every time the task status has changed. + */ + @Override + public void onStatusChange(StateEntry e) { + logEntries.add(e); + + if (e.getStatus() == Status.IN_PROGRESS) { + start = e.getTime(); + end = null; + } else { + end = e.getTime(); + } + + notifyListeners(e); + + if (e.getState() == Status.DONE) { + logFinished(); + } + + } + + }); + + } + + private void logFinished() { + listeners.stream().forEach(l -> l.onLogFinished(this)); + } + + private void notifyListeners(LogEntry logEntry) { + listeners.stream().forEach(l -> l.onNewEntry(logEntry)); + } + + public List getListeners() { + return listeners; + } + + public void addListener(LogEntryListener l) { + listeners.add(l); + } + + public Identifier getIdentifier() { + return id; + } + + /** + * Checks whether this {@code Log} has observed a {@code TaskStateEvent} + * triggered by a change of status of its respective {@code SearchTask} to + * {@code IN_PROGRESS}. + * + * @return {@code true} if the start time is not {@code null} + */ + public boolean isStarted() { + return start != null; + } + + /** + * Outputs all log entries consecutively. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + String newLine = System.lineSeparator(); + + sb.append(TaskManager.getManagerInstance().getTask(id)); + sb.append(newLine); + sb.append(newLine); + + for (LogEntry le : logEntries) { + sb.append(le); + sb.append(newLine); + } + + return sb.toString(); + + } + + public List getLogEntries() { + return logEntries; + } + + /** + * This is the time after the creation of the {@code Log} when a change of + * status to {@code IN_PROGRESS} happened. + * + * @return the start time + */ + public LocalTime getStart() { + return start; + } + + /** + * This is the time after the creation of the {@code Log} when a change of + * status to {@code DONE} happened. + * + * @return the start time + */ + public LocalTime getEnd() { + return end; + } + + /** + * Finds the last recorded entry in this {@code Log}. + * + * @return last recorded entry. + */ + public LogEntry lastEntry() { + return logEntries.stream().reduce((first, second) -> second).get(); + } + + /** + * Checks whether this {@code Log} is verbose. Verbose logs stores all data + * entries and outputs them on request. This is useful to get an idea of how + * the search method works, how many iterations are taken to reach a + * converged value, etc. + * + * @return {@code true} if the verbose flag is on + */ + public static boolean isVerbose() { + return verbose; + } + + /** + * Sets the verbose flag to {@code verbose} + * + * @param verbose the new value of the flag + * @see isVerbose() + */ + public static void setVerbose(boolean verbose) { + Log.verbose = verbose; + } + +} diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index d0d93e48..7841a910 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -17,33 +17,31 @@ *

* */ - public class LogEntry { - private Identifier identifier; - private LocalTime time; - - /** - *

- * Creates a {@code LogEntry} from this {@code SearchTask}. The data of the - * creation of this {@code LogEntry} will be stored. - *

- * - * @param t a {@code SearchTask} - */ - - public LogEntry(SearchTask t) { - Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); - time = LocalDateTime.now().toLocalTime(); - identifier = t.getIdentifier(); - } - - public Identifier getIdentifier() { - return identifier; - } - - public LocalTime getTime() { - return time; - } - -} \ No newline at end of file + private Identifier identifier; + private LocalTime time; + + /** + *

+ * Creates a {@code LogEntry} from this {@code SearchTask}. The data of the + * creation of this {@code LogEntry} will be stored. + *

+ * + * @param t a {@code SearchTask} + */ + public LogEntry(SearchTask t) { + Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); + time = LocalDateTime.now().toLocalTime(); + identifier = t.getIdentifier(); + } + + public Identifier getIdentifier() { + return identifier; + } + + public LocalTime getTime() { + return time; + } + +} diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index c70128c4..f73282d7 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -8,41 +8,42 @@ public class StateEntry extends LogEntry { - private Status status; - - public StateEntry(SearchTask task, Status status) { - super(task); - this.setStatus(status); - } - - public SearchTask getTask() { - return TaskManager.getManagerInstance().getTask(getIdentifier()); - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public Status getState() { - return status; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("
"); - sb.append(getIdentifier().toString() + " changed status to "); - var hex = "#" + toHexString(status.getColor().getRGB()).substring(2); - sb.append("" + status.toString() + ""); - if (status.getDetails() != NONE) - sb.append(" due to " + status.getDetails() + ""); - sb.append(" at "); - sb.append(getTime()); - return sb.toString(); - } + private Status status; + + public StateEntry(SearchTask task, Status status) { + super(task); + this.setStatus(status); + } + + public SearchTask getTask() { + return TaskManager.getManagerInstance().getTask(getIdentifier()); + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Status getState() { + return status; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("
"); + sb.append(getIdentifier().toString() + " changed status to "); + var hex = "#" + toHexString(status.getColor().getRGB()).substring(2); + sb.append("" + status.toString() + ""); + if (status.getDetails() != NONE) { + sb.append(" due to " + status.getDetails() + ""); + } + sb.append(" at "); + sb.append(getTime()); + return sb.toString(); + } } diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 6092b3a2..5520dbeb 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -7,130 +7,112 @@ * can be. * */ - public enum Status { - /** - * Not all necessary details have been uploaded to a {@code SearchTask} and that - * it cannot be executed yet. - */ - - INCOMPLETE(Color.RED), - - /** - * Everything seems to be in order and the task can now be executed. - */ - - READY(Color.MAGENTA), - - /** - * The task is being executed. - */ - - IN_PROGRESS(Color.DARK_GRAY), - - /** - * Task successfully finished. - */ - - DONE(Color.BLUE), - - /** - * An error has occurred during execution. - */ - - EXECUTION_ERROR(Color.red), - - /** - * The task has been terminated by the user. - */ - - TERMINATED(Color.DARK_GRAY), - - /** - * Task has been queued and is waiting to be executed. - */ - - QUEUED(Color.GREEN), - - /** - * Task has finished, but the results cannot be considered reliable (perhaps, - * due to large scatter of data points). - */ - - AMBIGUOUS(Color.GRAY), - - /** - * The iteration limit has been reached and the task aborted. - */ - - TIMEOUT(Color.RED), - - /** - * Task has finished without errors, however failing to meet a statistical - * criterion. - */ - - FAILED(Color.RED); - - private final Color clr; - private Details details = Details.NONE; - - Status(Color clr) { - this.clr = clr; - } - - public final Color getColor() { - return clr; - } - - public Details getDetails() { - return details; - } - - public void setDetails(Details details) { - this.details = details; - } - - static String parse(String str) { - var tokens = str.split("_"); - var sb = new StringBuilder(); - final var BLANK_SPACE = ' '; - for (var t : tokens) { - sb.append(t.toLowerCase()); - sb.append(BLANK_SPACE); - } - - return sb.toString(); - } - - public boolean checkProblemStatementSet() { - if(details == null) - return true; - - switch(details) { - case MISSING_DIFFERENCE_SCHEME : - case MISSING_HEATING_CURVE : - case MISSING_PROBLEM_STATEMENT : - case INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT : - return false; - default : - return true; - } - - } - - @Override - public String toString() { - return parse(super.toString()); - } - - public String getMessage() { - var sb = new StringBuilder(); - sb.append(toString()); - if (details != null) - sb.append(" : ").append(details.toString()); - return sb.toString(); - } - -} \ No newline at end of file + /** + * Not all necessary details have been uploaded to a {@code SearchTask} and + * that it cannot be executed yet. + */ + INCOMPLETE(Color.RED), + /** + * Everything seems to be in order and the task can now be executed. + */ + READY(Color.MAGENTA), + /** + * The task is being executed. + */ + IN_PROGRESS(Color.DARK_GRAY), + /** + * Task successfully finished. + */ + DONE(Color.BLUE), + /** + * An error has occurred during execution. + */ + EXECUTION_ERROR(Color.red), + /** + * The task has been terminated by the user. + */ + TERMINATED(Color.DARK_GRAY), + /** + * Task has been queued and is waiting to be executed. + */ + QUEUED(Color.GREEN), + /** + * Task has finished, but the results cannot be considered reliable + * (perhaps, due to large scatter of data points). + */ + AMBIGUOUS(Color.GRAY), + /** + * The iteration limit has been reached and the task aborted. + */ + TIMEOUT(Color.RED), + /** + * Task has finished without errors, however failing to meet a statistical + * criterion. + */ + FAILED(Color.RED); + + private final Color clr; + private Details details = Details.NONE; + + Status(Color clr) { + this.clr = clr; + } + + public final Color getColor() { + return clr; + } + + public Details getDetails() { + return details; + } + + public void setDetails(Details details) { + this.details = details; + } + + static String parse(String str) { + var tokens = str.split("_"); + var sb = new StringBuilder(); + final var BLANK_SPACE = ' '; + for (var t : tokens) { + sb.append(t.toLowerCase()); + sb.append(BLANK_SPACE); + } + + return sb.toString(); + } + + public boolean checkProblemStatementSet() { + if (details == null) { + return true; + } + + switch (details) { + case MISSING_DIFFERENCE_SCHEME: + case MISSING_HEATING_CURVE: + case MISSING_PROBLEM_STATEMENT: + case INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT: + return false; + default: + return true; + } + + } + + @Override + public String toString() { + return parse(super.toString()); + } + + public String getMessage() { + var sb = new StringBuilder(); + sb.append(toString()); + if (details != null) { + sb.append(" : ").append(details.toString()); + } + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/tasks/logs/package-info.java b/src/main/java/pulse/tasks/logs/package-info.java index f41cde04..7a00e289 100644 --- a/src/main/java/pulse/tasks/logs/package-info.java +++ b/src/main/java/pulse/tasks/logs/package-info.java @@ -1,5 +1,4 @@ /** * Lists classes for logging, storing runtime information including statuses. */ - -package pulse.tasks.logs; \ No newline at end of file +package pulse.tasks.logs; diff --git a/src/main/java/pulse/tasks/package-info.java b/src/main/java/pulse/tasks/package-info.java index 8abb2174..45aa320c 100644 --- a/src/main/java/pulse/tasks/package-info.java +++ b/src/main/java/pulse/tasks/package-info.java @@ -6,5 +6,4 @@ * ordering of final execution results, storing intermediate results of * execution to check convergence. */ - -package pulse.tasks; \ No newline at end of file +package pulse.tasks; diff --git a/src/main/java/pulse/tasks/processing/AbstractResult.java b/src/main/java/pulse/tasks/processing/AbstractResult.java index 3cd8cca3..cc727edb 100644 --- a/src/main/java/pulse/tasks/processing/AbstractResult.java +++ b/src/main/java/pulse/tasks/processing/AbstractResult.java @@ -17,119 +17,118 @@ * {@code NumericPropert}ies. * */ - public abstract class AbstractResult extends UpwardsNavigable { - private List properties; - private ResultFormat format; - - /** - * Constructs an {@code AbstractResult} with the list of properties specified by - * {@code format}. - * - * @param format a {@code ResultFormat} - */ - - public AbstractResult(ResultFormat format) { - this.format = format; - properties = new ArrayList<>(format.size()); - } - - public AbstractResult(AbstractResult r) { - this.properties = new ArrayList<>(r.getProperties()); - this.format = r.format; - } - - public ResultFormat getFormat() { - return format; - } - - /** - * This will print out all the properties according to the {@code ResultFormat}. - */ - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - for (NumericProperty p : properties) { - if (p != null) { - sb.append(p.toString()); - sb.append(System.lineSeparator()); - } - } - return sb.toString(); - } - - /** - * Returns a list of {@code NumericPropert}ies, which conform to the chosen - * {@code ResultFormat} - * - * @return a list of relevant {@code NumericProperty} objects - */ - - public List getProperties() { - return properties; - } - - protected void addProperty(NumericProperty p) { - properties.add(p); - } - - protected NumericProperty getProperty(int i) { - return filterProperties(this, format).get(i); - } - - public void setFormat(ResultFormat format) { - this.format = format; - } - - /** - * A static method for filtering the properties contained in the {@code result} - * to choose only those that conform to the {@code format}. - * - * @param result an {@code AbstractResult} with a list of properties - * @param format the format used for filtering - * @return the filtered list of properties - */ - - public static List filterProperties(AbstractResult result, ResultFormat format) { - return format.getKeywords().stream().map(keyword -> { - var p = result.properties.stream().filter(property -> property.getType().equals(keyword)).findFirst(); - return p.isPresent() ? p.get() : def(keyword); - }).collect(Collectors.toList()); - } - - /** - * A static method for filtering the properties contained in the {@code result} - * to choose only those that conform to its {@code format}. - * - * @param result an {@code AbstractResult} with a list of properties and a - * specified format - * @return the filtered list of properties - */ - - public static List filterProperties(AbstractResult result) { - return filterProperties(result, result.format); - } - - @Override - public boolean equals(Object o) { - if(o == this) - return true; - - if(o == null) - return false; - - if(! (o.getClass().equals(o.getClass()))) - return false; - - var another = (AbstractResult)o; - - if(!another.properties.containsAll(this.properties) || !this.properties.containsAll(another.properties)) - return false; - - return another.format.equals(this.format); - - } - -} \ No newline at end of file + private List properties; + private ResultFormat format; + + /** + * Constructs an {@code AbstractResult} with the list of properties + * specified by {@code format}. + * + * @param format a {@code ResultFormat} + */ + public AbstractResult(ResultFormat format) { + this.format = format; + properties = new ArrayList<>(format.size()); + } + + public AbstractResult(AbstractResult r) { + this.properties = new ArrayList<>(r.getProperties()); + this.format = r.format; + } + + public ResultFormat getFormat() { + return format; + } + + /** + * This will print out all the properties according to the + * {@code ResultFormat}. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (NumericProperty p : properties) { + if (p != null) { + sb.append(p.toString()); + sb.append(System.lineSeparator()); + } + } + return sb.toString(); + } + + /** + * Returns a list of {@code NumericPropert}ies, which conform to the chosen + * {@code ResultFormat} + * + * @return a list of relevant {@code NumericProperty} objects + */ + public List getProperties() { + return properties; + } + + protected void addProperty(NumericProperty p) { + properties.add(p); + } + + protected NumericProperty getProperty(int i) { + return filterProperties(this, format).get(i); + } + + public void setFormat(ResultFormat format) { + this.format = format; + } + + /** + * A static method for filtering the properties contained in the + * {@code result} to choose only those that conform to the {@code format}. + * + * @param result an {@code AbstractResult} with a list of properties + * @param format the format used for filtering + * @return the filtered list of properties + */ + public static List filterProperties(AbstractResult result, ResultFormat format) { + return format.getKeywords().stream().map(keyword -> { + var p = result.properties.stream().filter(property -> property.getType().equals(keyword)).findFirst(); + return p.isPresent() ? p.get() : def(keyword); + }).collect(Collectors.toList()); + } + + /** + * A static method for filtering the properties contained in the + * {@code result} to choose only those that conform to its {@code format}. + * + * @param result an {@code AbstractResult} with a list of properties and a + * specified format + * @return the filtered list of properties + */ + public static List filterProperties(AbstractResult result) { + return filterProperties(result, result.format); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o.getClass().equals(o.getClass()))) { + return false; + } + + var another = (AbstractResult) o; + + if (!another.properties.containsAll(this.properties) || !this.properties.containsAll(another.properties)) { + return false; + } + + return another.format.equals(this.format); + + } + +} diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index 61f65c3e..bcfbac0e 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -19,187 +19,176 @@ /** * A {@code Buffer} is used to estimate the convergence of the reverse problem * solution, by comparing the variance of the properties to a pre-specified - * error tolerance. - * + * error tolerance. + * * @see pulse.tasks.SearchTask.run() */ - public class Buffer extends PropertyHolder { - private ParameterVector[] data; - private double[] statistic; - private static int size = (int) def(BUFFER_SIZE).getValue(); - - /** - * Creates a {@code Buffer} with a default size. - */ - - public Buffer() { - init(); - } - - /** - * Retrieves the contents of this {@code Buffer}. - * - * @return the data - */ - - public ParameterVector[] getData() { - return data; - } - - /* + private ParameterVector[] data; + private double[] statistic; + private static int size = (int) def(BUFFER_SIZE).getValue(); + + /** + * Creates a {@code Buffer} with a default size. + */ + public Buffer() { + init(); + } + + /** + * Retrieves the contents of this {@code Buffer}. + * + * @return the data + */ + public ParameterVector[] getData() { + return data; + } + + /* * Re-inits the storage. - */ - - public void init() { - this.data = new ParameterVector[size]; - statistic = new double[size]; - } - - /** - * (Over)writes a buffer cell corresponding to the {@code bufferElement} with - * the current set of parameters of {@code SearchTask} and the search statistic. - * - * @param t the {@code SearchTask} - * @param bufferElement the {@code bufferElement} which will be written over - */ - - public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); - data[bufferElement] = t.searchVector(); - } - - /** - * Determines whether the relative error (variance divided by mean) for any of - * the properties in this buffer is higher than the expect - * {@code errorTolerance}. - * - * @param errorTolerance the maximum tolerated relative error. - * @return {@code true} if convergence has not been reached. - */ - - public boolean isErrorTooHigh(double errorTolerance) { - double[] e = new double[data[0].dimension()]; - - boolean result = false; - - for (int i = 0; i < e.length && (!result); i++) { - var index = data[0].getIndex(i); - final double av = average(index); - e[i] = variance(index) / (av*av); - - result = e[i] > errorTolerance; - } - - return result; - - } - - /** - * Calculates the average for the {@code index} -- if the respective - * {@code NumericProperty} is contained in the {@code IndexedVector} data of - * this {@code Buffer}. - * - * @param index a symbolic index (keyword) - * @return the mean of the data sample for the specific type of - * {@code NumericPropert}ies - */ - - public double average(NumericPropertyKeyword index) { - - double av = 0; - - for (ParameterVector v : data) { - av += v.getParameterValue(index); - } - - return av / data.length; - - } - - /** - * Calculates the average statistic value - * - * @return the mean statistic value. - */ - - public double averageStatistic() { - double av = 0; - - for (double ss : statistic) { - av += ss; - } - - return av / data.length; - - } - - /** - * Calculates the variance for the {@code index} -- if the respective - * {@code NumericProperty} is contained in the {@code IndexedVector} data of - * this {@code Buffer}. - * - * @param index a symbolic index (keyword). - * @return the variance of the data sample for the specific type of - * {@code NumericPropert}ies. - */ - - public double variance(NumericPropertyKeyword index) { - double sd = 0; - double av = average(index); - - for (ParameterVector v : data) { - final double s = v.getParameterValue(index) - av; - sd += s*s; - } - - return sd / data.length; - - } - - /** - * Gets the buffer size (a NumericProperty derived from {@code BUFFER_SIZE}. - * - * @return the buffer size property - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty getSize() { - return derive(BUFFER_SIZE, size); - } - - /** - * Sets a new size for this {@code Buffer}. - * - * @param newSize a {@code NumericProperty} of the type {@code BUFFER_SIZE}. - */ - - public static void setSize(NumericProperty newSize) { - requireType(newSize, BUFFER_SIZE); - Buffer.size = ((Number) newSize.getValue()).intValue(); - } - - /* + */ + public void init() { + this.data = new ParameterVector[size]; + statistic = new double[size]; + } + + /** + * (Over)writes a buffer cell corresponding to the {@code bufferElement} + * with the current set of parameters of {@code SearchTask} and the search + * statistic. + * + * @param t the {@code SearchTask} + * @param bufferElement the {@code bufferElement} which will be written over + */ + public void fill(SearchTask t, int bufferElement) { + statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); + data[bufferElement] = t.searchVector(); + } + + /** + * Determines whether the relative error (variance divided by mean) for any + * of the properties in this buffer is higher than the expect + * {@code errorTolerance}. + * + * @param errorTolerance the maximum tolerated relative error. + * @return {@code true} if convergence has not been reached. + */ + public boolean isErrorTooHigh(double errorTolerance) { + double[] e = new double[data[0].dimension()]; + + boolean result = false; + + for (int i = 0; i < e.length && (!result); i++) { + var index = data[0].getIndex(i); + final double av = average(index); + e[i] = variance(index) / (av * av); + + result = e[i] > errorTolerance; + } + + return result; + + } + + /** + * Calculates the average for the {@code index} -- if the respective + * {@code NumericProperty} is contained in the {@code IndexedVector} data of + * this {@code Buffer}. + * + * @param index a symbolic index (keyword) + * @return the mean of the data sample for the specific type of + * {@code NumericPropert}ies + */ + public double average(NumericPropertyKeyword index) { + + double av = 0; + + for (ParameterVector v : data) { + av += v.getParameterValue(index); + } + + return av / data.length; + + } + + /** + * Calculates the average statistic value + * + * @return the mean statistic value. + */ + public double averageStatistic() { + double av = 0; + + for (double ss : statistic) { + av += ss; + } + + return av / data.length; + + } + + /** + * Calculates the variance for the {@code index} -- if the respective + * {@code NumericProperty} is contained in the {@code IndexedVector} data of + * this {@code Buffer}. + * + * @param index a symbolic index (keyword). + * @return the variance of the data sample for the specific type of + * {@code NumericPropert}ies. + */ + public double variance(NumericPropertyKeyword index) { + double sd = 0; + double av = average(index); + + for (ParameterVector v : data) { + final double s = v.getParameterValue(index) - av; + sd += s * s; + } + + return sd / data.length; + + } + + /** + * Gets the buffer size (a NumericProperty derived from {@code BUFFER_SIZE}. + * + * @return the buffer size property + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty getSize() { + return derive(BUFFER_SIZE, size); + } + + /** + * Sets a new size for this {@code Buffer}. + * + * @param newSize a {@code NumericProperty} of the type {@code BUFFER_SIZE}. + */ + public static void setSize(NumericProperty newSize) { + requireType(newSize, BUFFER_SIZE); + Buffer.size = ((Number) newSize.getValue()).intValue(); + } + + /* * Sets the buffer size * @param type @code{BUFFER_SIZE} - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == BUFFER_SIZE) - setSize(property); - } - - /** - * The {@code BUFFER_SIZE} is the single listed parameter for this class. - * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); - } - -} \ No newline at end of file + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == BUFFER_SIZE) { + setSize(property); + } + } + + /** + * The {@code BUFFER_SIZE} is the single listed parameter for this class. + * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public List listedTypes() { + return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); + } + +} diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index dc01df99..5464396a 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -18,83 +18,88 @@ public class CorrelationBuffer { - private List params; - private static Set> excludePairList; - private static Set excludeSingleList; - - static { - excludePairList = new HashSet<>(); - excludeSingleList = new HashSet<>(); - excludeSingle(NumericPropertyKeyword.DIFFUSIVITY); - excludePair(NumericPropertyKeyword.HEAT_LOSS_SIDE, NumericPropertyKeyword.MAXTEMP); - excludePair(NumericPropertyKeyword.HEAT_LOSS, NumericPropertyKeyword.MAXTEMP); - excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_INTERCEPT); - excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_SLOPE); - excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.HEAT_LOSS_COMBINED); - } - - public CorrelationBuffer() { - params = new ArrayList<>(); - } - - public void inflate(SearchTask t) { - params.add(t.searchVector()); - } - - public void clear() { - params.clear(); - } - - public Map, Double> evaluate(CorrelationTest t) { - if(params.isEmpty()) - throw new IllegalStateException("Zero number of entries in parameter list"); - - if (t instanceof EmptyCorrelationTest) - return null; - - var indices = params.get(0).getIndices(); - var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) - .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); - - int indicesSize = indices.size(); - var correlationMap = new HashMap, Double>(); - ImmutablePair pair = null; - - for (int i = 0; i < indicesSize; i++) { - - if (!excludeSingleList.contains(indices.get(i))) - for (int j = i + 1; j < indicesSize; j++) { - pair = new ImmutablePair<>(indices.get(i), indices.get(j)); - if (!excludeSingleList.contains(indices.get(j)) && !excludePairList.contains(pair)) - correlationMap.put(pair, t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); - } - - } - - return correlationMap; - - } - - public boolean test(CorrelationTest t) { - var map = evaluate(t); - - if (map == null) - return false; - - return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); - } - - public static void excludePair(ImmutablePair pair) { - excludePairList.add(pair); - } - - public static void excludePair(NumericPropertyKeyword first, NumericPropertyKeyword second) { - excludePair(new ImmutablePair<>(first, second)); - } - - public static void excludeSingle(NumericPropertyKeyword key) { - excludeSingleList.add(key); - } - -} \ No newline at end of file + private List params; + private static Set> excludePairList; + private static Set excludeSingleList; + + static { + excludePairList = new HashSet<>(); + excludeSingleList = new HashSet<>(); + excludeSingle(NumericPropertyKeyword.DIFFUSIVITY); + excludePair(NumericPropertyKeyword.HEAT_LOSS_SIDE, NumericPropertyKeyword.MAXTEMP); + excludePair(NumericPropertyKeyword.HEAT_LOSS, NumericPropertyKeyword.MAXTEMP); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_INTERCEPT); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_SLOPE); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.HEAT_LOSS_COMBINED); + } + + public CorrelationBuffer() { + params = new ArrayList<>(); + } + + public void inflate(SearchTask t) { + params.add(t.searchVector()); + } + + public void clear() { + params.clear(); + } + + public Map, Double> evaluate(CorrelationTest t) { + if (params.isEmpty()) { + throw new IllegalStateException("Zero number of entries in parameter list"); + } + + if (t instanceof EmptyCorrelationTest) { + return null; + } + + var indices = params.get(0).getIndices(); + var map = indices.stream() + .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) + .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); + + int indicesSize = indices.size(); + var correlationMap = new HashMap, Double>(); + ImmutablePair pair = null; + + for (int i = 0; i < indicesSize; i++) { + + if (!excludeSingleList.contains(indices.get(i))) { + for (int j = i + 1; j < indicesSize; j++) { + pair = new ImmutablePair<>(indices.get(i), indices.get(j)); + if (!excludeSingleList.contains(indices.get(j)) && !excludePairList.contains(pair)) { + correlationMap.put(pair, t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } + } + } + + } + + return correlationMap; + + } + + public boolean test(CorrelationTest t) { + var map = evaluate(t); + + if (map == null) { + return false; + } + + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); + } + + public static void excludePair(ImmutablePair pair) { + excludePairList.add(pair); + } + + public static void excludePair(NumericPropertyKeyword first, NumericPropertyKeyword second) { + excludePair(new ImmutablePair<>(first, second)); + } + + public static void excludeSingle(NumericPropertyKeyword key) { + excludeSingleList.add(key); + } + +} diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 7cf8bcbe..8350aedd 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -6,37 +6,36 @@ /** * The individual {@code Result} that is associated with a {@code SearchTask}. * The {@code Identifier} of the task is stored as a field value. - * + * * @see pulse.tasks.SearchTask * @see pulse.tasks.Identifier */ - public class Result extends AbstractResult { - /** - * Creates an individual {@code Result} related to the current state of - * {@code task} using the specified {@code format}. - * - * @param task a {@code SearchTask}, the properties of which that conform to - * {@code ResultFormat} will form this {@code Result} - * @param format a {@code ResultFormat} - * @throws IllegalArgumentException if {@code task} is null - */ + /** + * Creates an individual {@code Result} related to the current state of + * {@code task} using the specified {@code format}. + * + * @param task a {@code SearchTask}, the properties of which that conform to + * {@code ResultFormat} will form this {@code Result} + * @param format a {@code ResultFormat} + * @throws IllegalArgumentException if {@code task} is null + */ + public Result(SearchTask task, ResultFormat format) throws IllegalArgumentException { + super(format); - public Result(SearchTask task, ResultFormat format) throws IllegalArgumentException { - super(format); + if (task == null) { + throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); + } - if (task == null) - throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); + setParent(task.getCurrentCalculation()); - setParent(task.getCurrentCalculation()); + format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); - format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); + } - } - - public Result(Result r) { - super(r); - } + public Result(Result r) { + super(r); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/processing/ResultFormat.java b/src/main/java/pulse/tasks/processing/ResultFormat.java index 55225dc0..63c7679a 100644 --- a/src/main/java/pulse/tasks/processing/ResultFormat.java +++ b/src/main/java/pulse/tasks/processing/ResultFormat.java @@ -22,152 +22,148 @@ * characters. *

*/ - public class ResultFormat { - private List nameMap; - - private final static NumericPropertyKeyword[] minimalArray = new NumericPropertyKeyword[] { IDENTIFIER, - TEST_TEMPERATURE, DIFFUSIVITY }; - - /** - *

- * The default format specified by the - * {@code Messages.getString("ResultFormat.DefaultFormat")}. See file - * messages.properties in {@code pulse.ui}. - *

- */ - - private static ResultFormat format = new ResultFormat(); - private static List listeners = new ArrayList(); - - private ResultFormat() { - this(asList(minimalArray)); - } - - private ResultFormat(List keys) { - nameMap = new ArrayList<>(); - for (var key : keys) { - nameMap.add(key); - } - } - - private ResultFormat(ResultFormat fmt) { - nameMap = new ArrayList<>(fmt.nameMap.size()); - nameMap.addAll(fmt.nameMap); - } - - public static void addResultFormatListener(ResultFormatListener rfl) { - listeners.add(rfl); - } - - public static ResultFormat generateFormat(List keys) { - format = new ResultFormat(keys); - - var rfe = new ResultFormatEvent(format); - for (var rfl : listeners) { - rfl.resultFormatChanged(rfe); - } - - return format; - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static ResultFormat getInstance() { - return format; - } - - /** - * Retrieves the list of keyword associated with this {@code ResultFormat} - * - * @return a list of keywords that can be used to access {@code NumericProperty} - * objects - */ - - public List getKeywords() { - return nameMap; - } - - /** - * Creates a {@code List} of default abbreviations corresponding to the - * list of keywords specific to {@code NumericProperty} objects. - * - * @return a list of abbreviations (typically, for filling the result table - * headers) - */ - - public List abbreviations() { - return nameMap.stream().map(keyword -> def(keyword).getAbbreviation(true)).collect(toList()); - } - - /** - * Creates a {@code List} of default descriptions corresponding to the - * list of keywords specific to {@code NumericProperty} objects. - * - * @return a list of abbreviations (typically, for filling the result table - * tooltips) - */ - - public List descriptors() { - return nameMap.stream().map(keyword -> def(keyword).getDescriptor(false)).collect(toList()); - } - - /** - * Finds a {@code NumericPropertyKeyword} contained in the {@code nameMap}, the - * description of which matches {@code descriptor}. - * - * @param descriptor a {@code String} describing the - * {@code NumericPropertyKeyword} - * @return the {@code NumericPropertyKeyword} object - */ - - public NumericPropertyKeyword fromAbbreviation(String descriptor) { - return nameMap.stream().filter(keyword -> def(keyword).getAbbreviation(true).equals(descriptor)) - .findFirst().get(); - } - - /** - * Calculates the length of the format string, which is the same as the size of - * the keyword list. - * - * @return an integer, representing the size of the format string. - */ - - public int size() { - return nameMap.size(); - } - - public int indexOf(NumericPropertyKeyword key) { - if (nameMap.contains(key)) - return nameMap.indexOf(key); - return -1; - } - - public static NumericPropertyKeyword[] getMinimalArray() { - return minimalArray; - } - - @Override - public boolean equals(Object o) { - if(o == this) - return true; - - if(o == null) - return false; - - if(! (o.getClass().equals(o.getClass()))) - return false; - - var another = (ResultFormat)o; - - return (another.nameMap.containsAll(this.nameMap)) && (this.nameMap.containsAll(another.nameMap)); - - } - -} \ No newline at end of file + private List nameMap; + + private final static NumericPropertyKeyword[] minimalArray = new NumericPropertyKeyword[]{IDENTIFIER, + TEST_TEMPERATURE, DIFFUSIVITY}; + + /** + *

+ * The default format specified by the + * {@code Messages.getString("ResultFormat.DefaultFormat")}. See file + * messages.properties in {@code pulse.ui}. + *

+ */ + private static ResultFormat format = new ResultFormat(); + private static List listeners = new ArrayList(); + + private ResultFormat() { + this(asList(minimalArray)); + } + + private ResultFormat(List keys) { + nameMap = new ArrayList<>(); + for (var key : keys) { + nameMap.add(key); + } + } + + private ResultFormat(ResultFormat fmt) { + nameMap = new ArrayList<>(fmt.nameMap.size()); + nameMap.addAll(fmt.nameMap); + } + + public static void addResultFormatListener(ResultFormatListener rfl) { + listeners.add(rfl); + } + + public static ResultFormat generateFormat(List keys) { + format = new ResultFormat(keys); + + var rfe = new ResultFormatEvent(format); + for (var rfl : listeners) { + rfl.resultFormatChanged(rfe); + } + + return format; + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static ResultFormat getInstance() { + return format; + } + + /** + * Retrieves the list of keyword associated with this {@code ResultFormat} + * + * @return a list of keywords that can be used to access + * {@code NumericProperty} objects + */ + public List getKeywords() { + return nameMap; + } + + /** + * Creates a {@code List} of default abbreviations corresponding to + * the list of keywords specific to {@code NumericProperty} objects. + * + * @return a list of abbreviations (typically, for filling the result table + * headers) + */ + public List abbreviations() { + return nameMap.stream().map(keyword -> def(keyword).getAbbreviation(true)).collect(toList()); + } + + /** + * Creates a {@code List} of default descriptions corresponding to + * the list of keywords specific to {@code NumericProperty} objects. + * + * @return a list of abbreviations (typically, for filling the result table + * tooltips) + */ + public List descriptors() { + return nameMap.stream().map(keyword -> def(keyword).getDescriptor(true)).collect(toList()); + } + + /** + * Finds a {@code NumericPropertyKeyword} contained in the {@code nameMap}, + * the description of which matches {@code descriptor}. + * + * @param descriptor a {@code String} describing the + * {@code NumericPropertyKeyword} + * @return the {@code NumericPropertyKeyword} object + */ + public NumericPropertyKeyword fromAbbreviation(String descriptor) { + return nameMap.stream().filter(keyword -> def(keyword).getAbbreviation(true).equals(descriptor)) + .findFirst().get(); + } + + /** + * Calculates the length of the format string, which is the same as the size + * of the keyword list. + * + * @return an integer, representing the size of the format string. + */ + public int size() { + return nameMap.size(); + } + + public int indexOf(NumericPropertyKeyword key) { + if (nameMap.contains(key)) { + return nameMap.indexOf(key); + } + return -1; + } + + public static NumericPropertyKeyword[] getMinimalArray() { + return minimalArray; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o.getClass().equals(o.getClass()))) { + return false; + } + + var another = (ResultFormat) o; + + return (another.nameMap.containsAll(this.nameMap)) && (this.nameMap.containsAll(another.nameMap)); + + } + +} diff --git a/src/main/java/pulse/tasks/processing/package-info.java b/src/main/java/pulse/tasks/processing/package-info.java index 7542dcc6..5db104f1 100644 --- a/src/main/java/pulse/tasks/processing/package-info.java +++ b/src/main/java/pulse/tasks/processing/package-info.java @@ -3,5 +3,4 @@ * ordering of final execution results, storing intermediate results of * execution to check convergence. */ - -package pulse.tasks.processing; \ No newline at end of file +package pulse.tasks.processing; diff --git a/src/main/java/pulse/ui/Version.java b/src/main/java/pulse/ui/Version.java index 1a1074e4..c1a17903 100644 --- a/src/main/java/pulse/ui/Version.java +++ b/src/main/java/pulse/ui/Version.java @@ -12,55 +12,56 @@ import pulse.io.readers.ReaderManager; public class Version { - - private long versionDate; - private String versionLabel; - private static Version currentVersion = ReaderManager.readVersion(); - - public Version(String label, long versionDate) { - this.versionLabel = label; - this.versionDate = versionDate; - } - - public Version checkNewVersion() { - - try { - var website = new URL("https://kotik-coder.github.io/Version.txt"); - var conn = website.openConnection(); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - - long date = conn.getLastModified(); - - if (date == 0) - System.out.println("No remote version info found"); - - var label = IOUtils.toString(website, "UTF-8"); - - return Long.compare(date, versionDate) > 0 ? new Version(label, date) : null; - - } catch (IOException e) { - System.err.println( "Could not check for new version"); - e.printStackTrace(); - return null; - } - } - - public long getVersionDate() { - return versionDate; - } - - public String getVersionLabel() { - return versionLabel; - } - - public String toString() { - var fmt = DateFormat.getDateInstance(DateFormat.SHORT); - return getString("TaskControlFrame.SoftwareTitle") + " - " + versionLabel + " (" + fmt.format(new Date(versionDate)) + ")"; - } - - public static Version getCurrentVersion() { - return currentVersion; - } - -} \ No newline at end of file + + private long versionDate; + private String versionLabel; + private static Version currentVersion = ReaderManager.readVersion(); + + public Version(String label, long versionDate) { + this.versionLabel = label; + this.versionDate = versionDate; + } + + public Version checkNewVersion() { + + try { + var website = new URL("https://kotik-coder.github.io/Version.txt"); + var conn = website.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + long date = conn.getLastModified(); + + if (date == 0) { + System.out.println("No remote version info found"); + } + + var label = IOUtils.toString(website, "UTF-8"); + + return Long.compare(date, versionDate) > 0 ? new Version(label, date) : null; + + } catch (IOException e) { + System.err.println("Could not check for new version"); + e.printStackTrace(); + return null; + } + } + + public long getVersionDate() { + return versionDate; + } + + public String getVersionLabel() { + return versionLabel; + } + + public String toString() { + var fmt = DateFormat.getDateInstance(DateFormat.SHORT); + return getString("TaskControlFrame.SoftwareTitle") + " - " + versionLabel + " (" + fmt.format(new Date(versionDate)) + ")"; + } + + public static Version getCurrentVersion() { + return currentVersion; + } + +} diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 7e379d03..8150c4c4 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -10,48 +10,49 @@ public abstract class AuxPlotter { - private ChartPanel chartPanel; - private JFreeChart chart; - private XYPlot plot; - - public AuxPlotter(String xLabel, String yLabel) { - createChart(xLabel, yLabel); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); - - plot = chart.getXYPlot(); - setFonts(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); - } - - public void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 16); - var plot = getPlot(); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); - } - - public abstract void createChart(String xLabel, String yLabel); - public abstract void plot(T t); - - public ChartPanel getChartPanel() { - return chartPanel; - } - - public JFreeChart getChart() { - return chart; - } - - public XYPlot getPlot() { - return plot; - } - - public void setChart(JFreeChart chart) { - this.chart = chart; - } - -} \ No newline at end of file + private ChartPanel chartPanel; + private JFreeChart chart; + private XYPlot plot; + + public AuxPlotter(String xLabel, String yLabel) { + createChart(xLabel, yLabel); + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + + plot = chart.getXYPlot(); + setFonts(); + + chart.removeLegend(); + chartPanel = new ChartPanel(chart); + } + + public void setFonts() { + var fontLabel = new Font("Arial", Font.PLAIN, 20); + var fontTicks = new Font("Arial", Font.PLAIN, 16); + var plot = getPlot(); + plot.getDomainAxis().setLabelFont(fontLabel); + plot.getDomainAxis().setTickLabelFont(fontTicks); + plot.getRangeAxis().setLabelFont(fontLabel); + plot.getRangeAxis().setTickLabelFont(fontTicks); + } + + public abstract void createChart(String xLabel, String yLabel); + + public abstract void plot(T t); + + public ChartPanel getChartPanel() { + return chartPanel; + } + + public JFreeChart getChart() { + return chart; + } + + public XYPlot getPlot() { + return plot; + } + + public void setChart(JFreeChart chart) { + this.chart = chart; + } + +} diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index d6d65433..42c9c9ba 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -19,79 +19,79 @@ @SuppressWarnings("serial") public class CalculationTable extends JTable { - private final static int ROW_HEIGHT = 70; - private final static int HEADER_HEIGHT = 30; + private final static int ROW_HEIGHT = 70; + private final static int HEADER_HEIGHT = 30; - private TaskTableRenderer taskTableRenderer; + private TaskTableRenderer taskTableRenderer; - public CalculationTable() { - super(); - setDefaultEditor(Object.class, null); - taskTableRenderer = new TaskTableRenderer(); - this.setRowSelectionAllowed(true); - setRowHeight(ROW_HEIGHT); + public CalculationTable() { + super(); + setDefaultEditor(Object.class, null); + taskTableRenderer = new TaskTableRenderer(); + this.setRowSelectionAllowed(true); + setRowHeight(ROW_HEIGHT); - setFillsViewportHeight(true); - setSelectionMode(SINGLE_SELECTION); - setShowHorizontalLines(false); + setFillsViewportHeight(true); + setSelectionMode(SINGLE_SELECTION); + setShowHorizontalLines(false); - var model = new StoredCalculationTableModel(); - setModel(model); + var model = new StoredCalculationTableModel(); + setModel(model); - getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); - setAutoCreateRowSorter(false); - initListeners(); + setAutoCreateRowSorter(false); + initListeners(); - var instance = TaskManager.getManagerInstance(); - instance.addTaskRepositoryListener(e -> { + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener(e -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_MODEL_SWITCH) { - var t = instance.getTask(e.getId()); - identifySelection(t); - } + if (e.getState() == TaskRepositoryEvent.State.TASK_MODEL_SWITCH) { + var t = instance.getTask(e.getId()); + identifySelection(t); + } else if (e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { + update(TaskManager.getManagerInstance().getSelectedTask()); + } - else if (e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { - update(TaskManager.getManagerInstance().getSelectedTask()); - } + }); - }); + } - } + public void update(SearchTask t) { + if (t != null) { + SwingUtilities.invokeLater(() -> { + ((StoredCalculationTableModel) getModel()).update(t); + identifySelection(t); + }); + } + } - public void update(SearchTask t) { - if (t != null) - SwingUtilities.invokeLater(() -> { - ((StoredCalculationTableModel) getModel()).update(t); - identifySelection(t); - }); - } + public void identifySelection(SearchTask t) { + int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); + if (modelIndex > -1) { + this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + } + } - public void identifySelection(SearchTask t) { - int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); - if (modelIndex > -1) - this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); - } + public void initListeners() { - public void initListeners() { + var lsm = getSelectionModel(); - var lsm = getSelectionModel(); + lsm.addListSelectionListener((ListSelectionEvent e) -> { + var task = TaskManager.getManagerInstance().getSelectedTask(); + var id = convertRowIndexToModel(this.getSelectedRow()); + if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + } - lsm.addListSelectionListener((ListSelectionEvent e) -> { - var task = TaskManager.getManagerInstance().getSelectedTask(); - var id = convertRowIndexToModel(this.getSelectedRow()); - if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); - } + }); - }); + } - } + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + return taskTableRenderer; + } - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - return taskTableRenderer; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index 72802e5b..0fb17161 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -25,21 +25,21 @@ @SuppressWarnings("serial") public class LogPane extends JEditorPane implements Descriptive { - private ExecutorService updateExecutor = newSingleThreadExecutor(); + private ExecutorService updateExecutor = newSingleThreadExecutor(); - public LogPane() { - super(); - setContentType("text/html"); - setEditable(false); - var c = (DefaultCaret) getCaret(); - c.setUpdatePolicy(ALWAYS_UPDATE); - } + public LogPane() { + super(); + setContentType("text/html"); + setEditable(false); + var c = (DefaultCaret) getCaret(); + c.setUpdatePolicy(ALWAYS_UPDATE); + } - private void post(LogEntry logEntry) { - post(logEntry.toString()); - } + private void post(LogEntry logEntry) { + post(logEntry.toString()); + } - /* + /* private void postError(String text) { var sb = new StringBuilder(); sb.append(getString("DataLogEntry.FontTagError")); @@ -47,88 +47,90 @@ private void postError(String text) { sb.append(getString("DataLogEntry.FontTagClose")); post(sb.toString()); }*/ + private void post(String text) { - private void post(String text) { - - final var doc = (HTMLDocument) getDocument(); - final var kit = (HTMLEditorKit) this.getEditorKit(); - try { - kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); - } catch (BadLocationException e) { - err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ - e.printStackTrace(); - } catch (IOException e) { - err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ - e.printStackTrace(); - } - - } - - public void printTimeTaken(Log log) { - var seconds = SECONDS.between(log.getStart(), log.getEnd()); - var ms = MILLIS.between(log.getStart(), log.getEnd()) - 1000L * seconds; - var sb = new StringBuilder(); - sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ - sb.append(seconds + getString("LogPane.Seconds")); //$NON-NLS-1$ - sb.append(ms + getString("LogPane.Milliseconds")); //$NON-NLS-1$ - post(sb.toString()); - } + final var doc = (HTMLDocument) getDocument(); + final var kit = (HTMLEditorKit) this.getEditorKit(); + try { + kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); + } catch (BadLocationException e) { + err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ + e.printStackTrace(); + } catch (IOException e) { + err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ + e.printStackTrace(); + } + + } + + public void printTimeTaken(Log log) { + var seconds = SECONDS.between(log.getStart(), log.getEnd()); + var ms = MILLIS.between(log.getStart(), log.getEnd()) - 1000L * seconds; + var sb = new StringBuilder(); + sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ + sb.append(seconds + getString("LogPane.Seconds")); //$NON-NLS-1$ + sb.append(ms + getString("LogPane.Milliseconds")); //$NON-NLS-1$ + post(sb.toString()); + } - public synchronized void callUpdate() { - updateExecutor.submit(() -> update()); - } + public synchronized void callUpdate() { + updateExecutor.submit(() -> update()); + } - public void printAll() { - clear(); + public void printAll() { + clear(); - var task = TaskManager.getManagerInstance().getSelectedTask(); + var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) { + if (task != null) { - var log = task.getLog(); + var log = task.getLog(); - if (log.isStarted()) { + if (log.isStarted()) { - log.getLogEntries().stream().forEach(entry -> post(entry)); + log.getLogEntries().stream().forEach(entry -> post(entry)); - if (task.getCurrentCalculation().getStatus() == DONE) - printTimeTaken(log); + if (task.getCurrentCalculation().getStatus() == DONE) { + printTimeTaken(log); + } - } + } - } + } - } + } - private synchronized void update() { - var task = TaskManager.getManagerInstance().getSelectedTask(); + private synchronized void update() { + var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task == null) - return; + if (task == null) { + return; + } - var log = task.getLog(); + var log = task.getLog(); - if (!log.isStarted()) - return; + if (!log.isStarted()) { + return; + } - post(log.lastEntry()); - } + post(log.lastEntry()); + } - public void clear() { - try { - getDocument().remove(0, getDocument().getLength()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } + public void clear() { + try { + getDocument().remove(0, getDocument().getLength()); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } - public ExecutorService getUpdateExecutor() { - return updateExecutor; - } + public ExecutorService getUpdateExecutor() { + return updateExecutor; + } - @Override - public String describe() { - return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); - } + @Override + public String describe() { + return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index 657b08a0..54513d97 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -21,97 +21,101 @@ @SuppressWarnings("serial") public class ProblemTree extends JTree { - private List selectionListeners; - - public ProblemTree(List allProblems) { - super(); - this.setCellRenderer(new ProblemCellRenderer()); - var root = new DefaultMutableTreeNode("Problem Statements"); - - for (var c : ProblemComplexity.values()) { - var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); - - allProblems.stream().filter(p -> p.getComplexity() == c).forEach(pFiltered -> { - var node = new DefaultMutableTreeNode(pFiltered); - currentComplexity.add(node); - }); + private List selectionListeners; - root.add(currentComplexity); + public ProblemTree(List allProblems) { + super(); + this.setCellRenderer(new ProblemCellRenderer()); + var root = new DefaultMutableTreeNode("Problem Statements"); - } + for (var c : ProblemComplexity.values()) { + var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); - var model = (DefaultTreeModel) this.getModel(); - model.setRoot(root); + allProblems.stream().filter(p -> p.getComplexity() == c).forEach(pFiltered -> { + var node = new DefaultMutableTreeNode(pFiltered); + currentComplexity.add(node); + }); - for (int i = 0; i < getRowCount(); i++) { - expandRow(i); - } + root.add(currentComplexity); - this.setRootVisible(false); + } - selectionListeners = new ArrayList<>(); - this.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + var model = (DefaultTreeModel) this.getModel(); + model.setRoot(root); - addListeners(); - } + for (int i = 0; i < getRowCount(); i++) { + expandRow(i); + } - private void addListeners() { - var instance = getManagerInstance(); + this.setRootVisible(false); - addTreeSelectionListener(e -> { - var object = ((DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); - if (object instanceof Problem) - fireProblemSelection(new ProblemSelectionEvent((Problem) object, this)); - }); + selectionListeners = new ArrayList<>(); + this.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); - instance.addSelectionListener(e -> { - var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); - // select appropriate problem type from list + addListeners(); + } - setSelectedProblem(current); - fireProblemSelection(new ProblemSelectionEvent(current, instance)); + private void addListeners() { + var instance = getManagerInstance(); - }); + addTreeSelectionListener(e -> { + var object = ((DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); + if (object instanceof Problem) { + fireProblemSelection(new ProblemSelectionEvent((Problem) object, this)); + } + }); - } + instance.addSelectionListener(e -> { + var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); + // select appropriate problem type from list - public void setSelectedProblem(Problem p) { - if (p == null) - return; + setSelectedProblem(current); + fireProblemSelection(new ProblemSelectionEvent(current, instance)); - var model = this.getModel(); - var root = model.getRoot(); + }); - SwingUtilities.invokeLater(() -> { + } - TreePath path = null; + public void setSelectedProblem(Problem p) { + if (p == null) { + return; + } - outer: for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { - var child = model.getChild(model.getRoot(), i); + var model = this.getModel(); + var root = model.getRoot(); - for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { - var node = (DefaultMutableTreeNode) model.getChild(child, j); - var problem = (Problem) node.getUserObject(); - if (p.getClass().equals(problem.getClass())) { - path = new TreePath(new Object[] { root, child, node }); - break outer; - } - } + SwingUtilities.invokeLater(() -> { - } + TreePath path = null; - this.setSelectionPath(path); + outer: + for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { + var child = model.getChild(model.getRoot(), i); - }); - } + for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { + var node = (DefaultMutableTreeNode) model.getChild(child, j); + var problem = (Problem) node.getUserObject(); + if (p.getClass().equals(problem.getClass())) { + path = new TreePath(new Object[]{root, child, node}); + break outer; + } + } - public void addProblemSelectionListener(ProblemSelectionListener l) { - selectionListeners.add(l); - } + } - private void fireProblemSelection(ProblemSelectionEvent e) { - for (var l : selectionListeners) - l.onProblemSelected(e); - } + this.setSelectionPath(path); -} \ No newline at end of file + }); + } + + public void addProblemSelectionListener(ProblemSelectionListener l) { + selectionListeners.add(l); + } + + private void fireProblemSelection(ProblemSelectionEvent e) { + for (var l : selectionListeners) { + l.onProblemSelected(e); + } + } + +} diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index 6dba661b..05413b65 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -35,161 +35,170 @@ @SuppressWarnings("serial") public class PropertyHolderTable extends JTable { - private PropertyHolder propertyHolder; + private PropertyHolder propertyHolder; - private final static int ROW_HEIGHT = 40; + private final static int ROW_HEIGHT = 40; - public PropertyHolderTable(PropertyHolder p) { - super(); - putClientProperty("terminateEditOnFocusLost", TRUE); + public PropertyHolderTable(PropertyHolder p) { + super(); + putClientProperty("terminateEditOnFocusLost", TRUE); - var model = new DefaultTableModel(dataArray(p), new String[] { getString("PropertyHolderTable.ParameterColumn"), //$NON-NLS-1$ - getString("PropertyHolderTable.ValueColumn") } //$NON-NLS-1$ - ); + var model = new DefaultTableModel(dataArray(p), new String[]{getString("PropertyHolderTable.ParameterColumn"), //$NON-NLS-1$ + getString("PropertyHolderTable.ValueColumn")} //$NON-NLS-1$ + ); - setModel(model); + setModel(model); - setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); + setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); - setShowGrid(false); - setRowHeight(ROW_HEIGHT); + setShowGrid(false); + setRowHeight(ROW_HEIGHT); - var list = new ArrayList(); - list.add(new SortKey(0, ASCENDING)); + var list = new ArrayList(); + list.add(new SortKey(0, ASCENDING)); - setPropertyHolder(p); + setPropertyHolder(p); - addListeners(); + addListeners(); - } + } - private void addListeners() { - /* + private void addListeners() { + /* * Update properties of the PropertyHolder when table is changed by the user - */ + */ - getModel().addTableModelListener((TableModelEvent e) -> { - - final int row = e.getFirstRow(); - final int column = e.getColumn(); - - if ((row < 0) || (column < 0)) - return; - - var changedObject = ((TableModel) e.getSource()).getValueAt(row, column); - - if (changedObject instanceof Property) { - var changedProperty = (Property) changedObject; - propertyHolder.updateProperty(this, changedProperty); - } - - }); - - } - - private Object[][] dataArray(PropertyHolder p) { - if (p == null) - return null; - - List dataList = new ArrayList<>(); - //ignore flags - var data = p.data().stream().filter(property -> !(property instanceof Flag)) - .map(property -> new Object[] { property.getDescriptor(true), property }) - .collect(Collectors.toList()); - dataList.addAll(data); - - if (p.ignoreSiblings()) - return dataList.toArray(new Object[data.size()][2]); - - p.subgroups().stream().filter(group -> group instanceof PropertyHolder).forEach(holder -> dataList.add( - new Object[] { ((PropertyHolder) holder).getPrefix() != null ? ((PropertyHolder) holder).getPrefix() - : holder.getDescriptor(), holder }) - - ); - - return dataList.toArray(new Object[dataList.size()][2]); - - } - - public void setPropertyHolder(PropertyHolder propertyHolder) { - this.propertyHolder = propertyHolder; - if (propertyHolder != null) { - updateTable(); - propertyHolder.addListener(event -> { - if (!(event.getSource() instanceof PropertyHolderTable)) - updateTable(); - }); - } - } - - public void updateTable() { - this.editCellAt(-1, -1); - this.clearSelection(); - - var model = ((DefaultTableModel) getModel()); - model.setDataVector(dataArray(propertyHolder), new String[] { model.getColumnName(0), model.getColumnName(1) }); - } - - @Override - public TableCellEditor getCellEditor(int row, int column) { - - var value = super.getValueAt(row, column); - - if (value == null) - super.getCellEditor(row, column); - - // do not edit labels - - if (value instanceof String) - return null; - - if (value instanceof NumericProperty) - return new NumberEditor((NumericProperty) value); - - if (value instanceof JComboBox) - return new DefaultCellEditor((JComboBox) value); - - if (value instanceof Enum) - return new DefaultCellEditor( - new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); - - if (value instanceof InstanceDescriptor) { - return new InstanceCellEditor((InstanceDescriptor) value); - } - - if (value instanceof DiscreteSelector) { - var selector = (DiscreteSelector) value; - var combo = new JComboBox<>(selector.getAllOptions().toArray()); - combo.setSelectedItem(selector.getValue()); - combo.addItemListener(e -> { - if (e.getStateChange() == ItemEvent.SELECTED) { - selector.attemptUpdate(e.getItem()); - updateTable(); - } - }); - return new DefaultCellEditor(combo); - } - - if ((value instanceof PropertyHolder)) - return new ButtonEditor((AbstractButton) getCellRenderer(row, column).getTableCellRendererComponent(this, - value, false, false, row, column), (PropertyHolder) value); - - if (value instanceof Flag) - return new ButtonEditor((IconCheckBox) getCellRenderer(row, column).getTableCellRendererComponent(this, - value, false, false, row, column), ((Flag) value).getType()); - - return getDefaultEditor(value.getClass()); - - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - var value = super.getValueAt(row, column); - return value != null ? new AccessibleTableRenderer() : super.getCellRenderer(row, column); - } - - public PropertyHolder getPropertyHolder() { - return propertyHolder; - } - -} \ No newline at end of file + getModel().addTableModelListener((TableModelEvent e) -> { + + final int row = e.getFirstRow(); + final int column = e.getColumn(); + + if ((row < 0) || (column < 0)) { + return; + } + + var changedObject = ((TableModel) e.getSource()).getValueAt(row, column); + + if (changedObject instanceof Property) { + var changedProperty = (Property) changedObject; + propertyHolder.updateProperty(this, changedProperty); + } + + }); + + } + + private Object[][] dataArray(PropertyHolder p) { + if (p == null) { + return null; + } + + List dataList = new ArrayList<>(); + //ignore flags + var data = p.data().stream().filter(property -> !(property instanceof Flag)) + .map(property -> new Object[]{property.getDescriptor(true), property}) + .collect(Collectors.toList()); + dataList.addAll(data); + + if (p.ignoreSiblings()) { + return dataList.toArray(new Object[data.size()][2]); + } + + p.subgroups().stream().filter(group -> group instanceof PropertyHolder).forEach(holder -> dataList.add( + new Object[]{((PropertyHolder) holder).getPrefix() != null ? ((PropertyHolder) holder).getPrefix() + : holder.getDescriptor(), holder}) + ); + + return dataList.toArray(new Object[dataList.size()][2]); + + } + + public void setPropertyHolder(PropertyHolder propertyHolder) { + this.propertyHolder = propertyHolder; + if (propertyHolder != null) { + updateTable(); + propertyHolder.addListener(event -> { + if (!(event.getSource() instanceof PropertyHolderTable)) { + updateTable(); + } + }); + } + } + + public void updateTable() { + this.editCellAt(-1, -1); + this.clearSelection(); + + var model = ((DefaultTableModel) getModel()); + model.setDataVector(dataArray(propertyHolder), new String[]{model.getColumnName(0), model.getColumnName(1)}); + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + + var value = super.getValueAt(row, column); + + if (value == null) { + super.getCellEditor(row, column); + } + + // do not edit labels + if (value instanceof String) { + return null; + } + + if (value instanceof NumericProperty) { + return new NumberEditor((NumericProperty) value); + } + + if (value instanceof JComboBox) { + return new DefaultCellEditor((JComboBox) value); + } + + if (value instanceof Enum) { + return new DefaultCellEditor( + new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); + } + + if (value instanceof InstanceDescriptor) { + return new InstanceCellEditor((InstanceDescriptor) value); + } + + if (value instanceof DiscreteSelector) { + var selector = (DiscreteSelector) value; + var combo = new JComboBox<>(selector.getAllOptions().toArray()); + combo.setSelectedItem(selector.getValue()); + combo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + selector.attemptUpdate(e.getItem()); + updateTable(); + } + }); + return new DefaultCellEditor(combo); + } + + if ((value instanceof PropertyHolder)) { + return new ButtonEditor((AbstractButton) getCellRenderer(row, column).getTableCellRendererComponent(this, + value, false, false, row, column), (PropertyHolder) value); + } + + if (value instanceof Flag) { + return new ButtonEditor((IconCheckBox) getCellRenderer(row, column).getTableCellRendererComponent(this, + value, false, false, row, column), ((Flag) value).getType()); + } + + return getDefaultEditor(value.getClass()); + + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + var value = super.getValueAt(row, column); + return value != null ? new AccessibleTableRenderer() : super.getCellRenderer(row, column); + } + + public PropertyHolder getPropertyHolder() { + return propertyHolder; + } + +} diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index 70ecef2c..418ec706 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -25,74 +25,74 @@ public class PulseChart extends AuxPlotter { - private final static int NUM_PULSE_POINTS = 600; - private final static double TO_MILLIS = 1E3; - - public PulseChart(String xLabel, String yLabel) { - super(xLabel,yLabel); - setRenderer(); - setLegendTitle(); - } - - private void setRenderer() { - var rendererPulse = new XYDifferenceRenderer(new Color(0.0f, 0.2f, 0.8f, 0.1f), Color.red, false); - rendererPulse.setSeriesPaint(0, RED); - rendererPulse.setSeriesStroke(0, new BasicStroke(3.0f)); - getPlot().setRenderer(rendererPulse); - } - - @Override - public void createChart(String xLabel, String yLabel) { - setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); - } - - private void setLegendTitle() { - var plot = getPlot(); - var lt = new LegendTitle(plot); - lt.setItemFont(new Font("Dialog", PLAIN, 16)); - //lt.setBackgroundPaint(new Color(200, 200, 255, 100)); - lt.setFrame(new BlockBorder(black)); - lt.setPosition(RectangleEdge.RIGHT); - var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); - ta.setMaxWidth(0.58); - plot.addAnnotation(ta); - } - - @Override - public void plot(Pulse pulse) { - requireNonNull(pulse); - - var problem = (Problem) pulse.getParent(); - double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); - - var pulseDataset = new XYSeriesCollection(); - pulseDataset.addSeries(series(problem, startTime)); - - getPlot().setDataset(0, pulseDataset); - } - - private static XYSeries series(Problem problem, double startTime) { - var pulse = problem.getPulse(); - - var series = new XYSeries(pulse.getPulseShape().toString()); - - double timeLimit = (double)pulse.getPulseWidth().getValue(); - final double timeFactor = problem.getProperties().timeFactor(); - - double dx = timeLimit / (NUM_PULSE_POINTS - 1); - double x = startTime; - - series.add(TO_MILLIS*(startTime - dx / 10.), 0.0); - series.add(TO_MILLIS*(startTime + timeLimit + dx / 10.), 0.0); - - var pulseShape = pulse.getPulseShape(); - - for (var i = 0; i < NUM_PULSE_POINTS; i++) { - series.add(x*TO_MILLIS, pulseShape.evaluateAt((x-startTime)/timeFactor)); - x += dx; - } - - return series; - } - -} \ No newline at end of file + private final static int NUM_PULSE_POINTS = 600; + private final static double TO_MILLIS = 1E3; + + public PulseChart(String xLabel, String yLabel) { + super(xLabel, yLabel); + setRenderer(); + setLegendTitle(); + } + + private void setRenderer() { + var rendererPulse = new XYDifferenceRenderer(new Color(0.0f, 0.2f, 0.8f, 0.1f), Color.red, false); + rendererPulse.setSeriesPaint(0, RED); + rendererPulse.setSeriesStroke(0, new BasicStroke(3.0f)); + getPlot().setRenderer(rendererPulse); + } + + @Override + public void createChart(String xLabel, String yLabel) { + setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); + } + + private void setLegendTitle() { + var plot = getPlot(); + var lt = new LegendTitle(plot); + lt.setItemFont(new Font("Dialog", PLAIN, 16)); + //lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); + ta.setMaxWidth(0.58); + plot.addAnnotation(ta); + } + + @Override + public void plot(Pulse pulse) { + requireNonNull(pulse); + + var problem = (Problem) pulse.getParent(); + double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); + + var pulseDataset = new XYSeriesCollection(); + pulseDataset.addSeries(series(problem, startTime)); + + getPlot().setDataset(0, pulseDataset); + } + + private static XYSeries series(Problem problem, double startTime) { + var pulse = problem.getPulse(); + + var series = new XYSeries(pulse.getPulseShape().toString()); + + double timeLimit = (double) pulse.getPulseWidth().getValue(); + final double timeFactor = problem.getProperties().timeFactor(); + + double dx = timeLimit / (NUM_PULSE_POINTS - 1); + double x = startTime; + + series.add(TO_MILLIS * (startTime - dx / 10.), 0.0); + series.add(TO_MILLIS * (startTime + timeLimit + dx / 10.), 0.0); + + var pulseShape = pulse.getPulseShape(); + + for (var i = 0; i < NUM_PULSE_POINTS; i++) { + series.add(x * TO_MILLIS, pulseShape.evaluateAt((x - startTime) / timeFactor)); + x += dx; + } + + return series; + } + +} diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index c21907dd..992157d5 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -55,322 +55,323 @@ @SuppressWarnings("serial") public class PulseMainMenu extends JMenuBar { - private final static int ICON_SIZE = 24; - - private static JMenuItem aboutItem; - private static JMenu fileMenu; - private static JMenuItem exitItem; - private static JMenuItem exportAllItem; - private static JMenuItem exportCurrentItem; - private static JMenuItem loadDataItem; - private static JMenuItem resultFormatItem; - private static JMenuItem searchSettingsItem; - private static JMenuItem loadMetadataItem; - private static JMenuItem loadPulseItem; - private static JMenuItem modelSettingsItem; - - private static ExportDialog exportDialog = new ExportDialog(); - private static FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); - - private static File dir; - - private List listeners; - private List exitListeners; - - public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); - - initComponents(); - initListeners(); - assignMenuFunctions(); - addListeners(); - - listeners = new ArrayList<>(); - exitListeners = new ArrayList<>(); - } - - private void addListeners() { - getManagerInstance().addTaskRepositoryListener(event -> { - if (event.getState() == TASK_ADDED) { - exportCurrentItem.setEnabled(true); - exportAllItem.setEnabled(true); - } - }); - } - - private void initListeners() { - exportCurrentItem.addActionListener(e -> { - var selectedTask = getManagerInstance().getSelectedTask(); - - if (selectedTask == null) { - showMessageDialog(getWindowAncestor(this), "No data to export!", "No Data to Export", WARNING_MESSAGE); - return; - } - - var fileChooser = new JFileChooser(); - fileChooser.setMultiSelectionEnabled(false); - fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - - var returnVal = fileChooser.showSaveDialog(this); - - if (returnVal == APPROVE_OPTION) { - dir = new File(fileChooser.getSelectedFile() + separator + getManagerInstance().describe()); - dir.mkdirs(); - exportCurrentTask(dir); - } - - }); - - exitItem.addActionListener(e -> notifyExit()); - - } - - private void initComponents() { - fileMenu = new JMenu("File"); - loadDataItem = new JMenuItem("Load Heating Curve(s)...", loadIcon("load.png", ICON_SIZE)); - loadMetadataItem = new JMenuItem("Load Metadata...", loadIcon("metadata.png", ICON_SIZE)); - loadPulseItem = new JMenuItem("Load Pulse Measurement(s)...", loadIcon("pulse.png", ICON_SIZE)); - exportCurrentItem = new JMenuItem("Export Current", loadIcon("save.png", ICON_SIZE)); - exportAllItem = new JMenuItem("Export...", loadIcon("save.png", ICON_SIZE)); - exitItem = new JMenuItem("Exit"); - var settingsMenu = new JMenu("Calculation Settings"); - modelSettingsItem = new JMenuItem("Heat Problem: Statement & Solution", - loadIcon("heat_problem.png", ICON_SIZE)); - searchSettingsItem = new JMenuItem("Parameter Estimation: Method & Settings", - loadIcon("inverse_problem.png", ICON_SIZE)); - resultFormatItem = new JMenuItem("Change Result Format...", loadIcon("result_format.png", ICON_SIZE)); - var infoMenu = new JMenu("Info"); - aboutItem = new JMenuItem("PULsE Web-site"); - var selectBuffer = new JMenuItem("Buffer size...", loadIcon("buffer.png", ICON_SIZE)); - selectBuffer.addActionListener(e -> bufferDialog.setVisible(true)); - - fileMenu.setMnemonic('f'); - loadDataItem.setMnemonic('h'); - loadMetadataItem.setMnemonic('M'); - loadPulseItem.setMnemonic('P'); - exportCurrentItem.setMnemonic('c'); - exportAllItem.setMnemonic('e'); - exitItem.setMnemonic('x'); - aboutItem.setMnemonic('a'); - settingsMenu.setMnemonic('s'); - - loadMetadataItem.setEnabled(false); - loadPulseItem.setEnabled(false); - exportCurrentItem.setEnabled(false); - exportAllItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - - fileMenu.add(loadDataItem); - fileMenu.add(loadMetadataItem); - fileMenu.add(loadPulseItem); - fileMenu.add(new JSeparator()); - fileMenu.add(exportCurrentItem); - fileMenu.add(exportAllItem); - fileMenu.add(new JSeparator()); - fileMenu.add(exitItem); - add(fileMenu); - - settingsMenu.add(modelSettingsItem); - settingsMenu.add(searchSettingsItem); - settingsMenu.add(initAnalysisSubmenu()); - settingsMenu.add(new JSeparator()); - settingsMenu.add(resultFormatItem); - settingsMenu.add(selectBuffer); - - add(settingsMenu); - - infoMenu.add(aboutItem); - add(infoMenu); - } - - private JMenu initAnalysisSubmenu() { - var analysisSubMenu = new JMenu("Statistical Analysis"); - var statisticsSubMenu = new JMenu("Normality tests"); - statisticsSubMenu.setIcon(loadIcon("normality_test.png", ICON_SIZE)); - - var statisticItems = new ButtonGroup(); - - for (var statisticName : allDescriptors(NormalityTest.class)) { - var item = new JRadioButtonMenuItem(statisticName); - statisticItems.add(item); - statisticsSubMenu.add(item); - item.addItemListener(e -> { - - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - NormalityTest.setSelectedTestDescriptor(text); - - getManagerInstance().getTaskList().stream().forEach(t -> t.initNormalityTest()); - - } - - }); - } - - var significanceDialog = new FormattedInputDialog(def(SIGNIFICANCE)); - - significanceDialog - .setConfirmAction(() -> setStatisticalSignificance(derive(SIGNIFICANCE, significanceDialog.value()))); - - var sigItem = new JMenuItem("Change significance..."); - statisticsSubMenu.add(new JSeparator()); - statisticsSubMenu.add(sigItem); - sigItem.addActionListener(e -> significanceDialog.setVisible(true)); - - statisticsSubMenu.getItem(0).setSelected(true); - analysisSubMenu.add(statisticsSubMenu); - - var optimisersSubMenu = new JMenu("Optimiser statistics"); - optimisersSubMenu.setIcon(loadIcon("optimiser.png", ICON_SIZE)); - - var optimisersItems = new ButtonGroup(); - - var set = allDescriptors(OptimiserStatistic.class); - var defaultOptimiser = new SumOfSquares(); - - for (var statisticName : set) { - var item = new JRadioButtonMenuItem(statisticName); - optimisersItems.add(item); - optimisersSubMenu.add(item); - - if(statisticName.equalsIgnoreCase(defaultOptimiser.getDescriptor())) - item.setSelected(true); - - item.addItemListener(e -> { - - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); - } - - }); - - } - - //for some reason it does not work without this line! - optimisersSubMenu.getItem(0).setSelected(true); - - for(int i = 0, size = set.size(); i < size; i++) { - var item = optimisersSubMenu.getItem(i); - if(item.getText().equalsIgnoreCase(defaultOptimiser.getDescriptor())) - item.setSelected(true); - } - - analysisSubMenu.add(optimisersSubMenu); - - // - - var correlationsSubMenu = new JMenu("Correlation tests"); - correlationsSubMenu.setIcon(loadIcon("correlation.png", ICON_SIZE)); - - var corrItems = new ButtonGroup(); - - JRadioButtonMenuItem corrItem = null; - - for (var corrName : allDescriptors(CorrelationTest.class)) { - corrItem = new JRadioButtonMenuItem(corrName); - corrItems.add(corrItem); - correlationsSubMenu.add(corrItem); - corrItem.addItemListener(e -> { - - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - CorrelationTest.setSelectedTestDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); - } - - }); - } - - var thresholdDialog = new FormattedInputDialog(def(CORRELATION_THRESHOLD)); - - thresholdDialog.setConfirmAction(() -> setThreshold(derive(CORRELATION_THRESHOLD, thresholdDialog.value()))); - - var thrItem = new JMenuItem("Change threshold..."); - correlationsSubMenu.add(new JSeparator()); - correlationsSubMenu.add(thrItem); - thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); - - correlationsSubMenu.getItem(0).setSelected(true); - - analysisSubMenu.add(correlationsSubMenu); - return analysisSubMenu; - } - - private void assignMenuFunctions() { - loadDataItem.addActionListener(e -> loadDataDialog()); - loadMetadataItem.setEnabled(false); - loadPulseItem.setEnabled(false); - loadMetadataItem.addActionListener(e -> loadMetadataDialog()); - loadPulseItem.addActionListener(e -> loadPulseDialog()); - - modelSettingsItem.setEnabled(false); - - modelSettingsItem.addActionListener(e -> notifyProblem()); - searchSettingsItem.addActionListener(e -> notifySearch()); - - searchSettingsItem.setEnabled(false); - - resultFormatItem.addActionListener(e -> { - var changeDialog = new ResultChangeDialog(); - changeDialog.setLocationRelativeTo(getWindowAncestor(this)); - changeDialog.setAlwaysOnTop(true); - changeDialog.setVisible(true); - }); - - getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (getManagerInstance().getTaskList().size() > 0) { - loadMetadataItem.setEnabled(true); - loadPulseItem.setEnabled(true);; - modelSettingsItem.setEnabled(true); - searchSettingsItem.setEnabled(true); - } else { - loadMetadataItem.setEnabled(false); - loadPulseItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - } - }); - - exportAllItem.setEnabled(true); - exportAllItem.addActionListener(e -> { - exportDialog.setLocationRelativeTo(null); - exportDialog.setAlwaysOnTop(true); - exportDialog.setVisible(true); - }); - - aboutItem.addActionListener(e -> { - try { - Desktop.getDesktop().browse(new URL("https://kotik-coder.github.io/").toURI()); - } catch (IOException | URISyntaxException e1) { - System.err.println("Unable to open URL. Details: "); - e1.printStackTrace(); - } - - }); - - } - - public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { - listeners.add(l); - } - - public void addExitRequestListener(ExitRequestListener el) { - exitListeners.add(el); - } - - public void notifyProblem() { - listeners.stream().forEach(l -> l.onProblemStatementShowRequest()); - } - - public void notifySearch() { - listeners.stream().forEach(l -> l.onSearchSettingsShowRequest()); - } - - public void notifyExit() { - exitListeners.stream().forEach(el -> el.onExitRequested()); - } - -} \ No newline at end of file + private final static int ICON_SIZE = 24; + + private static JMenuItem aboutItem; + private static JMenu fileMenu; + private static JMenuItem exitItem; + private static JMenuItem exportAllItem; + private static JMenuItem exportCurrentItem; + private static JMenuItem loadDataItem; + private static JMenuItem resultFormatItem; + private static JMenuItem searchSettingsItem; + private static JMenuItem loadMetadataItem; + private static JMenuItem loadPulseItem; + private static JMenuItem modelSettingsItem; + + private static ExportDialog exportDialog = new ExportDialog(); + private static FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); + + private static File dir; + + private List listeners; + private List exitListeners; + + public PulseMainMenu() { + bufferDialog.setConfirmAction(() -> Buffer.setSize(def(BUFFER_SIZE))); + + initComponents(); + initListeners(); + assignMenuFunctions(); + addListeners(); + + listeners = new ArrayList<>(); + exitListeners = new ArrayList<>(); + } + + private void addListeners() { + getManagerInstance().addTaskRepositoryListener(event -> { + if (event.getState() == TASK_ADDED) { + exportCurrentItem.setEnabled(true); + exportAllItem.setEnabled(true); + } + }); + } + + private void initListeners() { + exportCurrentItem.addActionListener(e -> { + var selectedTask = getManagerInstance().getSelectedTask(); + + if (selectedTask == null) { + showMessageDialog(getWindowAncestor(this), "No data to export!", "No Data to Export", WARNING_MESSAGE); + return; + } + + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); + + var returnVal = fileChooser.showSaveDialog(this); + + if (returnVal == APPROVE_OPTION) { + dir = new File(fileChooser.getSelectedFile() + separator + getManagerInstance().describe()); + dir.mkdirs(); + exportCurrentTask(dir); + } + + }); + + exitItem.addActionListener(e -> notifyExit()); + + } + + private void initComponents() { + fileMenu = new JMenu("File"); + loadDataItem = new JMenuItem("Load Heating Curve(s)...", loadIcon("load.png", ICON_SIZE)); + loadMetadataItem = new JMenuItem("Load Metadata...", loadIcon("metadata.png", ICON_SIZE)); + loadPulseItem = new JMenuItem("Load Pulse Measurement(s)...", loadIcon("pulse.png", ICON_SIZE)); + exportCurrentItem = new JMenuItem("Export Current", loadIcon("save.png", ICON_SIZE)); + exportAllItem = new JMenuItem("Export...", loadIcon("save.png", ICON_SIZE)); + exitItem = new JMenuItem("Exit"); + var settingsMenu = new JMenu("Calculation Settings"); + modelSettingsItem = new JMenuItem("Heat Problem: Statement & Solution", + loadIcon("heat_problem.png", ICON_SIZE)); + searchSettingsItem = new JMenuItem("Parameter Estimation: Method & Settings", + loadIcon("inverse_problem.png", ICON_SIZE)); + resultFormatItem = new JMenuItem("Change Result Format...", loadIcon("result_format.png", ICON_SIZE)); + var infoMenu = new JMenu("Info"); + aboutItem = new JMenuItem("PULsE Web-site"); + var selectBuffer = new JMenuItem("Buffer size...", loadIcon("buffer.png", ICON_SIZE)); + selectBuffer.addActionListener(e -> bufferDialog.setVisible(true)); + + fileMenu.setMnemonic('f'); + loadDataItem.setMnemonic('h'); + loadMetadataItem.setMnemonic('M'); + loadPulseItem.setMnemonic('P'); + exportCurrentItem.setMnemonic('c'); + exportAllItem.setMnemonic('e'); + exitItem.setMnemonic('x'); + aboutItem.setMnemonic('a'); + settingsMenu.setMnemonic('s'); + + loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); + exportCurrentItem.setEnabled(false); + exportAllItem.setEnabled(false); + modelSettingsItem.setEnabled(false); + searchSettingsItem.setEnabled(false); + + fileMenu.add(loadDataItem); + fileMenu.add(loadMetadataItem); + fileMenu.add(loadPulseItem); + fileMenu.add(new JSeparator()); + fileMenu.add(exportCurrentItem); + fileMenu.add(exportAllItem); + fileMenu.add(new JSeparator()); + fileMenu.add(exitItem); + add(fileMenu); + + settingsMenu.add(modelSettingsItem); + settingsMenu.add(searchSettingsItem); + settingsMenu.add(initAnalysisSubmenu()); + settingsMenu.add(new JSeparator()); + settingsMenu.add(resultFormatItem); + settingsMenu.add(selectBuffer); + + add(settingsMenu); + + infoMenu.add(aboutItem); + add(infoMenu); + } + + private JMenu initAnalysisSubmenu() { + var analysisSubMenu = new JMenu("Statistical Analysis"); + var statisticsSubMenu = new JMenu("Normality tests"); + statisticsSubMenu.setIcon(loadIcon("normality_test.png", ICON_SIZE)); + + var statisticItems = new ButtonGroup(); + + for (var statisticName : allDescriptors(NormalityTest.class)) { + var item = new JRadioButtonMenuItem(statisticName); + statisticItems.add(item); + statisticsSubMenu.add(item); + item.addItemListener(e -> { + + if (((AbstractButton) e.getItem()).isSelected()) { + var text = ((AbstractButton) e.getItem()).getText(); + NormalityTest.setSelectedTestDescriptor(text); + + getManagerInstance().getTaskList().stream().forEach(t -> t.initNormalityTest()); + + } + + }); + } + + var significanceDialog = new FormattedInputDialog(def(SIGNIFICANCE)); + + significanceDialog + .setConfirmAction(() -> setStatisticalSignificance(derive(SIGNIFICANCE, significanceDialog.value()))); + + var sigItem = new JMenuItem("Change significance..."); + statisticsSubMenu.add(new JSeparator()); + statisticsSubMenu.add(sigItem); + sigItem.addActionListener(e -> significanceDialog.setVisible(true)); + + statisticsSubMenu.getItem(0).setSelected(true); + analysisSubMenu.add(statisticsSubMenu); + + var optimisersSubMenu = new JMenu("Optimiser statistics"); + optimisersSubMenu.setIcon(loadIcon("optimiser.png", ICON_SIZE)); + + var optimisersItems = new ButtonGroup(); + + var set = allDescriptors(OptimiserStatistic.class); + var defaultOptimiser = new SumOfSquares(); + + for (var statisticName : set) { + var item = new JRadioButtonMenuItem(statisticName); + optimisersItems.add(item); + optimisersSubMenu.add(item); + + if (statisticName.equalsIgnoreCase(defaultOptimiser.getDescriptor())) { + item.setSelected(true); + } + + item.addItemListener(e -> { + + if (((AbstractButton) e.getItem()).isSelected()) { + var text = ((AbstractButton) e.getItem()).getText(); + setSelectedOptimiserDescriptor(text); + getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); + } + + }); + + } + + //for some reason it does not work without this line! + optimisersSubMenu.getItem(0).setSelected(true); + + for (int i = 0, size = set.size(); i < size; i++) { + var item = optimisersSubMenu.getItem(i); + if (item.getText().equalsIgnoreCase(defaultOptimiser.getDescriptor())) { + item.setSelected(true); + } + } + + analysisSubMenu.add(optimisersSubMenu); + + // + var correlationsSubMenu = new JMenu("Correlation tests"); + correlationsSubMenu.setIcon(loadIcon("correlation.png", ICON_SIZE)); + + var corrItems = new ButtonGroup(); + + JRadioButtonMenuItem corrItem = null; + + for (var corrName : allDescriptors(CorrelationTest.class)) { + corrItem = new JRadioButtonMenuItem(corrName); + corrItems.add(corrItem); + correlationsSubMenu.add(corrItem); + corrItem.addItemListener(e -> { + + if (((AbstractButton) e.getItem()).isSelected()) { + var text = ((AbstractButton) e.getItem()).getText(); + CorrelationTest.setSelectedTestDescriptor(text); + getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + } + + }); + } + + var thresholdDialog = new FormattedInputDialog(def(CORRELATION_THRESHOLD)); + + thresholdDialog.setConfirmAction(() -> setThreshold(derive(CORRELATION_THRESHOLD, thresholdDialog.value()))); + + var thrItem = new JMenuItem("Change threshold..."); + correlationsSubMenu.add(new JSeparator()); + correlationsSubMenu.add(thrItem); + thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); + + correlationsSubMenu.getItem(0).setSelected(true); + + analysisSubMenu.add(correlationsSubMenu); + return analysisSubMenu; + } + + private void assignMenuFunctions() { + loadDataItem.addActionListener(e -> loadDataDialog()); + loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); + loadMetadataItem.addActionListener(e -> loadMetadataDialog()); + loadPulseItem.addActionListener(e -> loadPulseDialog()); + + modelSettingsItem.setEnabled(false); + + modelSettingsItem.addActionListener(e -> notifyProblem()); + searchSettingsItem.addActionListener(e -> notifySearch()); + + searchSettingsItem.setEnabled(false); + + resultFormatItem.addActionListener(e -> { + var changeDialog = new ResultChangeDialog(); + changeDialog.setLocationRelativeTo(getWindowAncestor(this)); + changeDialog.setAlwaysOnTop(true); + changeDialog.setVisible(true); + }); + + getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (getManagerInstance().getTaskList().size() > 0) { + loadMetadataItem.setEnabled(true); + loadPulseItem.setEnabled(true);; + modelSettingsItem.setEnabled(true); + searchSettingsItem.setEnabled(true); + } else { + loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); + modelSettingsItem.setEnabled(false); + searchSettingsItem.setEnabled(false); + } + }); + + exportAllItem.setEnabled(true); + exportAllItem.addActionListener(e -> { + exportDialog.setLocationRelativeTo(null); + exportDialog.setAlwaysOnTop(true); + exportDialog.setVisible(true); + }); + + aboutItem.addActionListener(e -> { + try { + Desktop.getDesktop().browse(new URL("https://kotik-coder.github.io/").toURI()); + } catch (IOException | URISyntaxException e1) { + System.err.println("Unable to open URL. Details: "); + e1.printStackTrace(); + } + + }); + + } + + public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { + listeners.add(l); + } + + public void addExitRequestListener(ExitRequestListener el) { + exitListeners.add(el); + } + + public void notifyProblem() { + listeners.stream().forEach(l -> l.onProblemStatementShowRequest()); + } + + public void notifySearch() { + listeners.stream().forEach(l -> l.onSearchSettingsShowRequest()); + } + + public void notifyExit() { + exitListeners.stream().forEach(el -> el.onExitRequested()); + } + +} diff --git a/src/main/java/pulse/ui/components/TaskBox.java b/src/main/java/pulse/ui/components/TaskBox.java index 416d9b12..9be85ed4 100644 --- a/src/main/java/pulse/ui/components/TaskBox.java +++ b/src/main/java/pulse/ui/components/TaskBox.java @@ -17,39 +17,40 @@ @SuppressWarnings("serial") public class TaskBox extends JComboBox { - public TaskBox() { - super(); - - init(); - this.setModel(new TaskBoxModel()); - - var instance = TaskManager.getManagerInstance(); - - addItemListener((ItemEvent event) -> { - if (event.getStateChange() == SELECTED) { - var id = ((SearchTask) this.getModel().getSelectedItem()).getIdentifier(); - /* + public TaskBox() { + super(); + + init(); + this.setModel(new TaskBoxModel()); + + var instance = TaskManager.getManagerInstance(); + + addItemListener((ItemEvent event) -> { + if (event.getStateChange() == SELECTED) { + var id = ((SearchTask) this.getModel().getSelectedItem()).getIdentifier(); + /* * if task already selected, just ignore this event and return - */ - if (instance.getSelectedTask() != instance.getTask(id)) { - instance.selectTask(id, this); - } - - } - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // simply ignore if source of event is taskBox - if (e.getSource() != this) - getModel().setSelectedItem(instance.getSelectedTask()); - }); - } - - public void init() { - setMaximumSize(new Dimension(32767, 24)); - setMinimumSize(new Dimension(250, 20)); - setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ - setBackground(WHITE); - } - -} \ No newline at end of file + */ + if (instance.getSelectedTask() != instance.getTask(id)) { + instance.selectTask(id, this); + } + + } + }); + + instance.addSelectionListener((TaskSelectionEvent e) -> { + // simply ignore if source of event is taskBox + if (e.getSource() != this) { + getModel().setSelectedItem(instance.getSelectedTask()); + } + }); + } + + public void init() { + setMaximumSize(new Dimension(32767, 24)); + setMinimumSize(new Dimension(250, 20)); + setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ + setBackground(WHITE); + } + +} diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index a0c4db2c..d89e43d8 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -37,172 +37,175 @@ @SuppressWarnings("serial") public class TaskPopupMenu extends JPopupMenu { - private JMenuItem itemViewStored; - - private final static int ICON_SIZE = 24; - - private static ImageIcon ICON_GRAPH = loadIcon("graph.png", ICON_SIZE); - private static ImageIcon ICON_METADATA = loadIcon("metadata.png", ICON_SIZE); - private static ImageIcon ICON_MISSING = loadIcon("missing.png", ICON_SIZE); - private static ImageIcon ICON_RUN = loadIcon("execute_single.png", ICON_SIZE); - private static ImageIcon ICON_RESET = loadIcon("reset.png", ICON_SIZE); - private static ImageIcon ICON_RESULT = loadIcon("result.png", ICON_SIZE); - private static ImageIcon ICON_STORED = loadIcon("stored.png", ICON_SIZE); - - public TaskPopupMenu() { - var referenceWindow = getWindowAncestor(this); - - var itemChart = new JMenuItem(getString("TaskTablePopupMenu.ShowHeatingCurve"), ICON_GRAPH); //$NON-NLS-1$ - itemChart.addActionListener(e -> plot(false)); - - var itemExtendedChart = new JMenuItem(getString("TaskTablePopupMenu.ShowExtendedHeatingCurve"), //$NON-NLS-1$ - ICON_GRAPH); - itemExtendedChart.addActionListener(e -> plot(true)); - - var instance = TaskManager.getManagerInstance(); - - var itemShowMeta = new JMenuItem("Show metadata", ICON_METADATA); - itemShowMeta.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t == null) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ - } else - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); - }); - - var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); - - instance.addSelectionListener(event -> { - instance.getSelectedTask().checkProblems(false); - var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); - itemShowStatus.setEnabled((details != null) & (details != NONE)); - }); - - itemShowStatus.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t != null) { - var d = t.getCurrentCalculation().getStatus().getDetails(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), - "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); - } - }); - - var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ - itemExecute.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t == null) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ - } else { - t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); - - if (status == DONE) { - var dialogButton = YES_NO_OPTION; - var dialogResult = showConfirmDialog(referenceWindow, - getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() - + getString("TaskTablePopupMenu.AskToDelete"), - getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); - if (dialogResult == 0) { - // instance.removeResult(t); - instance.getSelectedTask().setStatus(READY); - instance.execute(instance.getSelectedTask()); - } - } else if (status != READY) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ - getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ - ERROR_MESSAGE); - } else - instance.execute(instance.getSelectedTask()); - } - - }); - - var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); - - itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); - - var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); - - itemGenerateResult.addActionListener((ActionEvent arg0) -> { - var t = instance.getSelectedTask(); - if (t == null) - return; - var current = t.getCurrentCalculation(); - if (current != null) { - var r = new Result(t, getInstance()); - current.setResult(r); - var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - instance.notifyListeners(e); - } - }); - - itemViewStored = new JMenuItem(getString("TaskTablePopupMenu.ViewStored"), ICON_STORED); - - itemViewStored.setEnabled(false); - - itemViewStored.addActionListener(arg0 -> instance.notifyListeners( - new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier()))); - - add(itemShowMeta); - add(itemShowStatus); - add(new JSeparator()); - add(itemChart); - add(itemExtendedChart); - add(new JSeparator()); - add(itemReset); - add(itemGenerateResult); - add(itemViewStored); - add(new JSeparator()); - add(itemExecute); - - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void plot(boolean extended) { - var t = TaskManager.getManagerInstance().getSelectedTask(); - - if (t == null) { - showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ - } else { - - var calc = t.getCurrentCalculation(); - var statusDetails = calc.getStatus().getDetails(); - - if (statusDetails == MISSING_HEATING_CURVE) { - - showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.12"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.13"), //$NON-NLS-1$ - ERROR_MESSAGE); - - } else { - - var scheme = (Solver) calc.getScheme(); - if (scheme != null) { - try { - scheme.solve(calc.getProblem()); - } catch (SolverException e) { - err.println("Solver error for " + t + "Details: "); - e.printStackTrace(); - } - } - - getChart().plot(t, extended); - - } - - } - - } - - public JMenuItem getItemViewStored() { - return itemViewStored; - } - -} \ No newline at end of file + private JMenuItem itemViewStored; + + private final static int ICON_SIZE = 24; + + private static ImageIcon ICON_GRAPH = loadIcon("graph.png", ICON_SIZE); + private static ImageIcon ICON_METADATA = loadIcon("metadata.png", ICON_SIZE); + private static ImageIcon ICON_MISSING = loadIcon("missing.png", ICON_SIZE); + private static ImageIcon ICON_RUN = loadIcon("execute_single.png", ICON_SIZE); + private static ImageIcon ICON_RESET = loadIcon("reset.png", ICON_SIZE); + private static ImageIcon ICON_RESULT = loadIcon("result.png", ICON_SIZE); + private static ImageIcon ICON_STORED = loadIcon("stored.png", ICON_SIZE); + + public TaskPopupMenu() { + var referenceWindow = getWindowAncestor(this); + + var itemChart = new JMenuItem(getString("TaskTablePopupMenu.ShowHeatingCurve"), ICON_GRAPH); //$NON-NLS-1$ + itemChart.addActionListener(e -> plot(false)); + + var itemExtendedChart = new JMenuItem(getString("TaskTablePopupMenu.ShowExtendedHeatingCurve"), //$NON-NLS-1$ + ICON_GRAPH); + itemExtendedChart.addActionListener(e -> plot(true)); + + var instance = TaskManager.getManagerInstance(); + + var itemShowMeta = new JMenuItem("Show metadata", ICON_METADATA); + itemShowMeta.addActionListener((ActionEvent e) -> { + var t = instance.getSelectedTask(); + if (t == null) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ + } else { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); + } + }); + + var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); + + instance.addSelectionListener(event -> { + instance.getSelectedTask().checkProblems(false); + var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); + itemShowStatus.setEnabled((details != null) & (details != NONE)); + }); + + itemShowStatus.addActionListener((ActionEvent e) -> { + var t = instance.getSelectedTask(); + if (t != null) { + var d = t.getCurrentCalculation().getStatus().getDetails(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); + } + }); + + var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ + itemExecute.addActionListener((ActionEvent e) -> { + var t = instance.getSelectedTask(); + if (t == null) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ + } else { + t.checkProblems(true); + var status = t.getCurrentCalculation().getStatus(); + + if (status == DONE) { + var dialogButton = YES_NO_OPTION; + var dialogResult = showConfirmDialog(referenceWindow, + getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() + + getString("TaskTablePopupMenu.AskToDelete"), + getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); + if (dialogResult == 0) { + // instance.removeResult(t); + instance.getSelectedTask().setStatus(READY); + instance.execute(instance.getSelectedTask()); + } + } else if (status != READY) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ + getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ + ERROR_MESSAGE); + } else { + instance.execute(instance.getSelectedTask()); + } + } + + }); + + var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); + + itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); + + var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); + + itemGenerateResult.addActionListener((ActionEvent arg0) -> { + var t = instance.getSelectedTask(); + if (t == null) { + return; + } + var current = t.getCurrentCalculation(); + if (current != null) { + var r = new Result(t, getInstance()); + current.setResult(r); + var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); + instance.notifyListeners(e); + } + }); + + itemViewStored = new JMenuItem(getString("TaskTablePopupMenu.ViewStored"), ICON_STORED); + + itemViewStored.setEnabled(false); + + itemViewStored.addActionListener(arg0 -> instance.notifyListeners( + new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier()))); + + add(itemShowMeta); + add(itemShowStatus); + add(new JSeparator()); + add(itemChart); + add(itemExtendedChart); + add(new JSeparator()); + add(itemReset); + add(itemGenerateResult); + add(itemViewStored); + add(new JSeparator()); + add(itemExecute); + + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void plot(boolean extended) { + var t = TaskManager.getManagerInstance().getSelectedTask(); + + if (t == null) { + showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ + } else { + + var calc = t.getCurrentCalculation(); + var statusDetails = calc.getStatus().getDetails(); + + if (statusDetails == MISSING_HEATING_CURVE) { + + showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.12"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.13"), //$NON-NLS-1$ + ERROR_MESSAGE); + + } else { + + var scheme = (Solver) calc.getScheme(); + if (scheme != null) { + try { + scheme.solve(calc.getProblem()); + } catch (SolverException e) { + err.println("Solver error for " + t + "Details: "); + e.printStackTrace(); + } + } + + getChart().plot(t, extended); + + } + + } + + } + + public JMenuItem getItemViewStored() { + return itemViewStored; + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index f759e07a..943ce3e8 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -21,99 +21,99 @@ @SuppressWarnings("serial") public class ExecutionButton extends JButton { - private ExecutionState state = EXECUTE; + private ExecutionState state = EXECUTE; - public ExecutionButton() { - super(); - setIcon(state.getIcon()); - setToolTipText(state.getMessage()); + public ExecutionButton() { + super(); + setIcon(state.getIcon()); + setToolTipText(state.getMessage()); - var instance = TaskManager.getManagerInstance(); + var instance = TaskManager.getManagerInstance(); - this.addActionListener((ActionEvent e) -> { - /* + this.addActionListener((ActionEvent e) -> { + /* * STOP PRESSED? - */ - if (state == STOP) { - instance.cancelAllTasks(); - return; - } - /* + */ + if (state == STOP) { + instance.cancelAllTasks(); + return; + } + /* * EXECUTE PRESSED? - */ - if (instance.getTaskList().isEmpty()) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskControlFrame.PleaseLoadData"), //$NON-NLS-1$ - "No Tasks", //$NON-NLS-1$ - ERROR_MESSAGE); - return; - } - var problematicTask = instance.getTaskList().stream().filter(t -> { - t.checkProblems(true); - return t.getCurrentCalculation().getStatus() == INCOMPLETE; - }).findFirst(); - if (problematicTask.isPresent()) { - var t = problematicTask.get(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", - ERROR_MESSAGE); - } else { - instance.executeAll(); - } - }); - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - switch (e.getState()) { - case TASK_SUBMITTED: - setExecutionState(STOP); - break; - case TASK_FINISHED: - if (instance.isTaskQueueEmpty()) { - setExecutionState(EXECUTE); - } else { - setExecutionState(STOP); - } - break; - case SHUTDOWN: - setExecutionState(EXECUTE); - break; - default: - return; - } - }); - - } - - public void setExecutionState(ExecutionState state) { - this.state = state; - this.setToolTipText(state.getMessage()); - this.setIcon(state.getIcon()); - } - - public ExecutionState getExecutionState() { - return state; - } - - public enum ExecutionState { - EXECUTE("Execute All Tasks", loadIcon("execute.png", 24)), - STOP("Terminate All Running Tasks", loadIcon("stop.png", 24)); - - private String message; - private ImageIcon icon; - - private ExecutionState(String message, ImageIcon icon) { - this.icon = icon; - this.message = message; - } - - public ImageIcon getIcon() { - return icon; - } - - public String getMessage() { - return message; - } - - } - -} \ No newline at end of file + */ + if (instance.getTaskList().isEmpty()) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + getString("TaskControlFrame.PleaseLoadData"), //$NON-NLS-1$ + "No Tasks", //$NON-NLS-1$ + ERROR_MESSAGE); + return; + } + var problematicTask = instance.getTaskList().stream().filter(t -> { + t.checkProblems(true); + return t.getCurrentCalculation().getStatus() == INCOMPLETE; + }).findFirst(); + if (problematicTask.isPresent()) { + var t = problematicTask.get(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", + ERROR_MESSAGE); + } else { + instance.executeAll(); + } + }); + + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + switch (e.getState()) { + case TASK_SUBMITTED: + setExecutionState(STOP); + break; + case TASK_FINISHED: + if (instance.isTaskQueueEmpty()) { + setExecutionState(EXECUTE); + } else { + setExecutionState(STOP); + } + break; + case SHUTDOWN: + setExecutionState(EXECUTE); + break; + default: + return; + } + }); + + } + + public void setExecutionState(ExecutionState state) { + this.state = state; + this.setToolTipText(state.getMessage()); + this.setIcon(state.getIcon()); + } + + public ExecutionState getExecutionState() { + return state; + } + + public enum ExecutionState { + EXECUTE("Execute All Tasks", loadIcon("execute.png", 24)), + STOP("Terminate All Running Tasks", loadIcon("stop.png", 24)); + + private String message; + private ImageIcon icon; + + private ExecutionState(String message, ImageIcon icon) { + this.icon = icon; + this.message = message; + } + + public ImageIcon getIcon() { + return icon; + } + + public String getMessage() { + return message; + } + + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java index 6c11d32d..845754d9 100644 --- a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java +++ b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java @@ -8,31 +8,31 @@ @SuppressWarnings("serial") public class IconCheckBox extends JCheckBox { - /* + /* * Checkbox icons for the inner button editor class - */ - - private final static int ICON_SIZE = 20; - private final static ImageIcon ICON_ENABLED = loadIcon("checked.png", ICON_SIZE); - private final static ImageIcon ICON_DISABLED = loadIcon("unchecked.png", ICON_SIZE); - - public IconCheckBox() { - super(); - setHorizontalAlignment(CENTER); - } - - public IconCheckBox(boolean b) { - super("", b); - setSelected(b); - } - - @Override - public void setSelected(boolean selected) { - super.setSelected(selected); - if (selected) - this.setIcon(ICON_ENABLED); - else - this.setIcon(ICON_DISABLED); - } - -} \ No newline at end of file + */ + private final static int ICON_SIZE = 20; + private final static ImageIcon ICON_ENABLED = loadIcon("checked.png", ICON_SIZE); + private final static ImageIcon ICON_DISABLED = loadIcon("unchecked.png", ICON_SIZE); + + public IconCheckBox() { + super(); + setHorizontalAlignment(CENTER); + } + + public IconCheckBox(boolean b) { + super("", b); + setSelected(b); + } + + @Override + public void setSelected(boolean selected) { + super.setSelected(selected); + if (selected) { + this.setIcon(ICON_ENABLED); + } else { + this.setIcon(ICON_DISABLED); + } + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index c138c8e6..5b93f01e 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -31,89 +31,91 @@ @SuppressWarnings("serial") public class LoaderButton extends JButton { - private InterpolationDataset.StandartType dataType; - private static File dir; - - private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); - private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.75f); - - public LoaderButton() { - super(); - init(); - } - - public LoaderButton(String str) { - super(str); - init(); - } - - public void init() { - - InterpolationDataset.addListener(e -> { - if (dataType == e) - highlight(false); - }); - - addActionListener((ActionEvent arg0) -> { - var fileChooser = new JFileChooser(); - fileChooser.setCurrentDirectory(dir); - var extensions = getDatasetExtensions(); - var extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter( - new FileNameExtensionFilter(getString("LoaderButton.SupportedExtensionsDescriptor"), extArray)); //$NON-NLS-1$ - // $NON-NLS-1$ - var approve = fileChooser.showOpenDialog(getWindowAncestor((Component) arg0.getSource())) == APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - if (!approve) - return; - try { - switch (dataType) { - case HEAT_CAPACITY: - load(HEAT_CAPACITY, fileChooser.getSelectedFile()); - break; - case DENSITY: - load(DENSITY, fileChooser.getSelectedFile()); - break; - default: - throw new IllegalStateException("Unrecognised type: " + dataType); - } - } catch (IOException e) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ - getString("LoaderButton.IOError"), //$NON-NLS-1$ - ERROR_MESSAGE); - e.printStackTrace(); - } - var size = getDataset(dataType).getData().size(); - var label = ""; - switch (dataType) { - case HEAT_CAPACITY: - label = getString("LoaderButton.5"); //$NON-NLS-1$ - // $NON-NLS-1$ - break; - case DENSITY: - label = getString("LoaderButton.6"); //$NON-NLS-1$ - // $NON-NLS-1$ - break; - default: - throw new IllegalStateException("Unknown data type: " + dataType); - } - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - "" + label + " data loaded! A total of " + size + " data points loaded.", - "Data loaded", INFORMATION_MESSAGE); - }); - } - - public void setDataType(InterpolationDataset.StandartType dataType) { - this.dataType = dataType; - } - - public void highlight(boolean highlighted) { - setBorder(highlighted ? BorderFactory.createLineBorder(HIGHLIGHTED) : null ); - } - - public void highlightIfNeeded() { - highlight(getDataset(dataType) == null); - } - -} \ No newline at end of file + private InterpolationDataset.StandartType dataType; + private static File dir; + + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.75f); + + public LoaderButton() { + super(); + init(); + } + + public LoaderButton(String str) { + super(str); + init(); + } + + public void init() { + + InterpolationDataset.addListener(e -> { + if (dataType == e) { + highlight(false); + } + }); + + addActionListener((ActionEvent arg0) -> { + var fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(dir); + var extensions = getDatasetExtensions(); + var extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter( + new FileNameExtensionFilter(getString("LoaderButton.SupportedExtensionsDescriptor"), extArray)); //$NON-NLS-1$ + // $NON-NLS-1$ + var approve = fileChooser.showOpenDialog(getWindowAncestor((Component) arg0.getSource())) == APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + if (!approve) { + return; + } + try { + switch (dataType) { + case HEAT_CAPACITY: + load(HEAT_CAPACITY, fileChooser.getSelectedFile()); + break; + case DENSITY: + load(DENSITY, fileChooser.getSelectedFile()); + break; + default: + throw new IllegalStateException("Unrecognised type: " + dataType); + } + } catch (IOException e) { + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ + getString("LoaderButton.IOError"), //$NON-NLS-1$ + ERROR_MESSAGE); + e.printStackTrace(); + } + var size = getDataset(dataType).getData().size(); + var label = ""; + switch (dataType) { + case HEAT_CAPACITY: + label = getString("LoaderButton.5"); //$NON-NLS-1$ + // $NON-NLS-1$ + break; + case DENSITY: + label = getString("LoaderButton.6"); //$NON-NLS-1$ + // $NON-NLS-1$ + break; + default: + throw new IllegalStateException("Unknown data type: " + dataType); + } + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), + "" + label + " data loaded! A total of " + size + " data points loaded.", + "Data loaded", INFORMATION_MESSAGE); + }); + } + + public void setDataType(InterpolationDataset.StandartType dataType) { + this.dataType = dataType; + } + + public void highlight(boolean highlighted) { + setBorder(highlighted ? BorderFactory.createLineBorder(HIGHLIGHTED) : null); + } + + public void highlightIfNeeded() { + highlight(getDataset(dataType) == null); + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/package-info.java b/src/main/java/pulse/ui/components/buttons/package-info.java index da58c342..a6cf9c9a 100644 --- a/src/main/java/pulse/ui/components/buttons/package-info.java +++ b/src/main/java/pulse/ui/components/buttons/package-info.java @@ -1 +1 @@ -package pulse.ui.components.buttons; \ No newline at end of file +package pulse.ui.components.buttons; diff --git a/src/main/java/pulse/ui/components/controllers/ButtonEditor.java b/src/main/java/pulse/ui/components/controllers/ButtonEditor.java index 71b88e0f..89b0dd0f 100644 --- a/src/main/java/pulse/ui/components/controllers/ButtonEditor.java +++ b/src/main/java/pulse/ui/components/controllers/ButtonEditor.java @@ -22,53 +22,54 @@ @SuppressWarnings("serial") public class ButtonEditor extends AbstractCellEditor implements TableCellEditor { - private AbstractButton btn; - private PropertyHolder dat; - private NumericPropertyKeyword type; + private AbstractButton btn; + private PropertyHolder dat; + private NumericPropertyKeyword type; - public ButtonEditor(AbstractButton btn, PropertyHolder dat) { - this.btn = btn; - this.dat = dat; + public ButtonEditor(AbstractButton btn, PropertyHolder dat) { + this.btn = btn; + this.dat = dat; - btn.addActionListener((ActionEvent e) -> { - JFrame dataFrame = new DataFrame(dat, btn); - dataFrame.setVisible(true); - btn.setEnabled(false); - dataFrame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent we) { - btn.setText(((DataFrame) dataFrame).getDataObject().toString()); - btn.setEnabled(true); - } - }); - }); + btn.addActionListener((ActionEvent e) -> { + JFrame dataFrame = new DataFrame(dat, btn); + dataFrame.setVisible(true); + btn.setEnabled(false); + dataFrame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent we) { + btn.setText(((DataFrame) dataFrame).getDataObject().toString()); + btn.setEnabled(true); + } + }); + }); - } + } - public ButtonEditor(IconCheckBox btn, NumericPropertyKeyword index) { - this.btn = btn; - this.type = index; + public ButtonEditor(IconCheckBox btn, NumericPropertyKeyword index) { + this.btn = btn; + this.type = index; - btn.addActionListener((ActionEvent e) -> { - var source = (IconCheckBox) e.getSource(); - source.setHorizontalAlignment(CENTER); - stopCellEditing(); - }); + btn.addActionListener((ActionEvent e) -> { + var source = (IconCheckBox) e.getSource(); + source.setHorizontalAlignment(CENTER); + stopCellEditing(); + }); - } + } - @Override - public Object getCellEditorValue() { - if (dat != null) - return dat; - var f = new Flag(type); - f.setValue(btn.isSelected()); - return f; - } + @Override + public Object getCellEditorValue() { + if (dat != null) { + return dat; + } + var f = new Flag(type); + f.setValue(btn.isSelected()); + return f; + } - @Override - public Component getTableCellEditorComponent(JTable arg0, Object value, boolean arg2, int arg3, int arg4) { - return btn; - } + @Override + public Component getTableCellEditorComponent(JTable arg0, Object value, boolean arg2, int arg3, int arg4) { + return btn; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ConfirmAction.java b/src/main/java/pulse/ui/components/controllers/ConfirmAction.java index f63e0053..be3a35ec 100644 --- a/src/main/java/pulse/ui/components/controllers/ConfirmAction.java +++ b/src/main/java/pulse/ui/components/controllers/ConfirmAction.java @@ -2,6 +2,6 @@ public interface ConfirmAction { - public void onConfirm(); + public void onConfirm(); } diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 988a7094..2c5d4fdd 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -8,7 +8,6 @@ import javax.swing.JComboBox; import javax.swing.JTable; import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; import pulse.util.InstanceDescriptor; diff --git a/src/main/java/pulse/ui/components/controllers/NumberEditor.java b/src/main/java/pulse/ui/components/controllers/NumberEditor.java index 1e1a2321..c51120f2 100644 --- a/src/main/java/pulse/ui/components/controllers/NumberEditor.java +++ b/src/main/java/pulse/ui/components/controllers/NumberEditor.java @@ -59,174 +59,176 @@ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/ - public class NumberEditor extends DefaultCellEditor { - /** - * - */ - private static final long serialVersionUID = 1L; - JFormattedTextField ftf; - NumberFormat numberFormat; - private boolean DEBUG = false; - private NumericProperty property; - - public NumberEditor(NumericProperty property) { - super(new JFormattedTextField()); - - this.property = property; - ftf = (JFormattedTextField) getComponent(); - - // Set up the editor for the integer cells. - - var numFormatter = new NumberFormatter(numberFormat); - numFormatter.setFormat(numberFormat); - - Number value; - - if (property.getValue() instanceof Integer) { - numberFormat = getIntegerInstance(); - value = (int) property.getValue() * (int) property.getDimensionFactor(); - numFormatter.setMinimum(property.getMinimum().intValue() * property.getDimensionFactor().intValue()); - numFormatter.setMaximum(property.getMaximum().intValue() * property.getDimensionFactor().intValue()); - } else { - numberFormat = new DecimalFormat(getString("NumberEditor.NumberFormat")); //$NON-NLS-1$ - value = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue(); - numFormatter.setMinimum(property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue()); - numFormatter.setMaximum(property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue()); - } - - ftf.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); - ftf.setValue(value); - ftf.setHorizontalAlignment(CENTER); - ftf.setFocusLostBehavior(PERSIST); - - // React when the user presses Enter while the editor is - // active. (Tab is handled as specified by - // JFormattedTextField's focusLostBehavior property.) - ftf.getInputMap().put(getKeyStroke(VK_ENTER, 0), "check"); - ftf.getActionMap().put("check", new AbstractAction() { - /** - * - */ - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed(ActionEvent e) { - if (!ftf.isEditValid()) { // The text is invalid. - if (userSaysRevert()) { // reverted - ftf.postActionEvent(); // inform the editor - } - } else + + /** + * + */ + private static final long serialVersionUID = 1L; + JFormattedTextField ftf; + NumberFormat numberFormat; + private boolean DEBUG = false; + private NumericProperty property; + + public NumberEditor(NumericProperty property) { + super(new JFormattedTextField()); + + this.property = property; + ftf = (JFormattedTextField) getComponent(); + + // Set up the editor for the integer cells. + var numFormatter = new NumberFormatter(numberFormat); + numFormatter.setFormat(numberFormat); + + Number value; + + if (property.getValue() instanceof Integer) { + numberFormat = getIntegerInstance(); + value = (int) property.getValue() * (int) property.getDimensionFactor(); + numFormatter.setMinimum(property.getMinimum().intValue() * property.getDimensionFactor().intValue()); + numFormatter.setMaximum(property.getMaximum().intValue() * property.getDimensionFactor().intValue()); + } else { + numberFormat = new DecimalFormat(getString("NumberEditor.NumberFormat")); //$NON-NLS-1$ + value = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue(); + numFormatter.setMinimum(property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue()); + numFormatter.setMaximum(property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue()); + } + + ftf.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); + ftf.setValue(value); + ftf.setHorizontalAlignment(CENTER); + ftf.setFocusLostBehavior(PERSIST); + + // React when the user presses Enter while the editor is + // active. (Tab is handled as specified by + // JFormattedTextField's focusLostBehavior property.) + ftf.getInputMap().put(getKeyStroke(VK_ENTER, 0), "check"); + ftf.getActionMap().put("check", new AbstractAction() { + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent e) { + if (!ftf.isEditValid()) { // The text is invalid. + if (userSaysRevert()) { // reverted + ftf.postActionEvent(); // inform the editor + } + } else try { // The text is valid, - ftf.commitEdit(); // so use it. - ftf.postActionEvent(); // stop editing - } catch (java.text.ParseException exc) { - } - } - }); - } - - // Override to invoke setValue on the formatted text field. - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - var ftf = (JFormattedTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); - - Number num; - var prop = (NumericProperty) value; - - if ((prop.getValue() instanceof Integer)) - num = (int) prop.getValue() * (int) prop.getDimensionFactor(); - else - num = ((Number) prop.getValue()).doubleValue() * prop.getDimensionFactor().doubleValue(); - - ftf.setValue(num); - return ftf; - } - - @Override - public Object getCellEditorValue() { - var ftf = (JFormattedTextField) getComponent(); - var o = ftf.getValue(); - if (o instanceof Number) { - - try { - if (o instanceof Integer) { - if (property.getValue() instanceof Integer) - property.setValue((int) o / (int) property.getDimensionFactor()); - else - property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); - } else { - if (property.getValue() instanceof Integer) - property.setValue(((Number) o).intValue() / (int) property.getDimensionFactor()); - else - property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); - } - } catch (IllegalArgumentException e) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor(ftf), e.getMessage(), getString("NumberEditor.IllegalTableEntry"), //$NON-NLS-1$ - ERROR_MESSAGE); - property.setValue(property.getMinimum()); - } - return property; - } else { - if (DEBUG) { - out.println(getString("NumberEditor.NotANumberError")); //$NON-NLS-1$ - } - try { - return numberFormat.parseObject(o.toString()); - } catch (ParseException exc) { - err.println(getString("NumberEditor.ParseError") + o); //$NON-NLS-1$ - return null; - } - } - } - - // Override to check whether the edit is valid, - // setting the value if it is and complaining if - // it isn't. If it's OK for the editor to go - // away, we need to invoke the superclass's version - // of this method so that everything gets cleaned up. - @Override - public boolean stopCellEditing() { - var ftf = (JFormattedTextField) getComponent(); - if (ftf.isEditValid()) { - try { - ftf.commitEdit(); - } catch (java.text.ParseException exc) { - } - - } else { // text is invalid - if (!userSaysRevert()) { // user wants to edit - return false; // don't let the editor go away - } - } - - return super.stopCellEditing(); - } - - /** - * Lets the user know that the text they entered is bad. Returns true if the - * user elects to revert to the last good value. Otherwise, returns false, - * indicating that the user wants to continue editing. - */ - protected boolean userSaysRevert() { - getDefaultToolkit().beep(); - ftf.selectAll(); - Object[] options = { getString("NumberEditor.EditText"), getString("NumberEditor.RevertText") }; - var answer = showOptionDialog(getWindowAncestor(ftf), - "The value must be a " + property.getMinimum().getClass().getSimpleName() + " between " - + property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue() + " and " - + property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue() + ".\n" - + getString("NumberEditor.MessageLine1") //$NON-NLS-1$ - + getString("NumberEditor.MessageLine2"), //$NON-NLS-1$ - getString("NumberEditor.InvalidText"), //$NON-NLS-1$ - YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - ftf.setValue(ftf.getValue()); - return true; - } - return false; - } + ftf.commitEdit(); // so use it. + ftf.postActionEvent(); // stop editing + } catch (java.text.ParseException exc) { + } + } + }); + } + + // Override to invoke setValue on the formatted text field. + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + var ftf = (JFormattedTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); + + Number num; + var prop = (NumericProperty) value; + + if ((prop.getValue() instanceof Integer)) { + num = (int) prop.getValue() * (int) prop.getDimensionFactor(); + } else { + num = ((Number) prop.getValue()).doubleValue() * prop.getDimensionFactor().doubleValue(); + } + + ftf.setValue(num); + return ftf; + } + + @Override + public Object getCellEditorValue() { + var ftf = (JFormattedTextField) getComponent(); + var o = ftf.getValue(); + if (o instanceof Number) { + + try { + if (o instanceof Integer) { + if (property.getValue() instanceof Integer) { + property.setValue((int) o / (int) property.getDimensionFactor()); + } else { + property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); + } + } else { + if (property.getValue() instanceof Integer) { + property.setValue(((Number) o).intValue() / (int) property.getDimensionFactor()); + } else { + property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); + } + } + } catch (IllegalArgumentException e) { + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor(ftf), e.getMessage(), getString("NumberEditor.IllegalTableEntry"), //$NON-NLS-1$ + ERROR_MESSAGE); + property.setValue(property.getMinimum()); + } + return property; + } else { + if (DEBUG) { + out.println(getString("NumberEditor.NotANumberError")); //$NON-NLS-1$ + } + try { + return numberFormat.parseObject(o.toString()); + } catch (ParseException exc) { + err.println(getString("NumberEditor.ParseError") + o); //$NON-NLS-1$ + return null; + } + } + } + + // Override to check whether the edit is valid, + // setting the value if it is and complaining if + // it isn't. If it's OK for the editor to go + // away, we need to invoke the superclass's version + // of this method so that everything gets cleaned up. + @Override + public boolean stopCellEditing() { + var ftf = (JFormattedTextField) getComponent(); + if (ftf.isEditValid()) { + try { + ftf.commitEdit(); + } catch (java.text.ParseException exc) { + } + + } else { // text is invalid + if (!userSaysRevert()) { // user wants to edit + return false; // don't let the editor go away + } + } + + return super.stopCellEditing(); + } + + /** + * Lets the user know that the text they entered is bad. Returns true if the + * user elects to revert to the last good value. Otherwise, returns false, + * indicating that the user wants to continue editing. + */ + protected boolean userSaysRevert() { + getDefaultToolkit().beep(); + ftf.selectAll(); + Object[] options = {getString("NumberEditor.EditText"), getString("NumberEditor.RevertText")}; + var answer = showOptionDialog(getWindowAncestor(ftf), + "The value must be a " + property.getMinimum().getClass().getSimpleName() + " between " + + property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue() + " and " + + property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue() + ".\n" + + getString("NumberEditor.MessageLine1") //$NON-NLS-1$ + + getString("NumberEditor.MessageLine2"), //$NON-NLS-1$ + getString("NumberEditor.InvalidText"), //$NON-NLS-1$ + YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); + + if (answer == 1) { // Revert! + ftf.setValue(ftf.getValue()); + return true; + } + return false; + } } diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java index 9c77734f..7bd86cf9 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java @@ -6,16 +6,16 @@ public class NumericPropertyComparator implements Comparator { - protected NumericPropertyComparator() { + protected NumericPropertyComparator() { - } + } - @Override - public int compare(NumericProperty o1, NumericProperty o2) { - var v1 = (Double) o1.getValue(); - var v2 = (Double) o2.getValue(); + @Override + public int compare(NumericProperty o1, NumericProperty o2) { + var v1 = (Double) o1.getValue(); + var v2 = (Double) o2.getValue(); - return v1.compareTo(v2); - } + return v1.compareTo(v2); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java index daecbf52..7c360774 100644 --- a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -15,18 +15,19 @@ @SuppressWarnings("serial") public class ProblemCellRenderer extends DefaultTreeCellRenderer { - private static ImageIcon defaultIcon = (ImageIcon) ((LazyIcon) UIManager.getIcon("Tree.leafIcon")).getIcon(); - - public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, - int row, boolean hasFocus) { - - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - var object = ((DefaultMutableTreeNode)value).getUserObject(); - if (leaf && object instanceof Problem) { - var icon = ImageUtils.dye(defaultIcon, ((Problem)object).getComplexity().getColor()); - setIcon(icon); - } - return this; - } - -} \ No newline at end of file + + private static ImageIcon defaultIcon = (ImageIcon) ((LazyIcon) UIManager.getIcon("Tree.leafIcon")).getIcon(); + + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, + int row, boolean hasFocus) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + var object = ((DefaultMutableTreeNode) value).getUserObject(); + if (leaf && object instanceof Problem) { + var icon = ImageUtils.dye(defaultIcon, ((Problem) object).getComplexity().getColor()); + setIcon(icon); + } + return this; + } + +} diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index 893e28a8..e74951a2 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -12,16 +12,16 @@ @SuppressWarnings("serial") public class SearchListRenderer extends DefaultListCellRenderer { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { - var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - renderer.setForeground(isSelected ? Color.DARK_GRAY : Color.white); + var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); + renderer.setForeground(isSelected ? Color.DARK_GRAY : Color.white); - return renderer; + return renderer; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/package-info.java b/src/main/java/pulse/ui/components/controllers/package-info.java index fc6bf5f0..e46aff83 100644 --- a/src/main/java/pulse/ui/components/controllers/package-info.java +++ b/src/main/java/pulse/ui/components/controllers/package-info.java @@ -1 +1 @@ -package pulse.ui.components.controllers; \ No newline at end of file +package pulse.ui.components.controllers; diff --git a/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java b/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java index 0764d6fa..74dd233f 100644 --- a/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java @@ -2,6 +2,6 @@ public interface ExitRequestListener { - public void onExitRequested(); + public void onExitRequested(); } diff --git a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java index 8415432f..fe791695 100644 --- a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java @@ -2,7 +2,8 @@ public interface FrameVisibilityRequestListener { - public void onProblemStatementShowRequest(); - public void onSearchSettingsShowRequest(); + public void onProblemStatementShowRequest(); -} \ No newline at end of file + public void onSearchSettingsShowRequest(); + +} diff --git a/src/main/java/pulse/ui/components/listeners/LogExportListener.java b/src/main/java/pulse/ui/components/listeners/LogExportListener.java index c64fe350..99a32ae9 100644 --- a/src/main/java/pulse/ui/components/listeners/LogExportListener.java +++ b/src/main/java/pulse/ui/components/listeners/LogExportListener.java @@ -2,6 +2,6 @@ public interface LogExportListener { - public void onLogExportRequest(); + public void onLogExportRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java b/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java index f030ec61..b6144fc0 100644 --- a/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java @@ -2,6 +2,6 @@ public interface PlotRequestListener { - public void onPlotRequest(); + public void onPlotRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java b/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java index ba0037c6..401a236f 100644 --- a/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java +++ b/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java @@ -2,6 +2,6 @@ public interface PreviewFrameCreationListener { - public void onPreviewFrameRequest(); + public void onPreviewFrameRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java index 93c7a045..1eb7c6dc 100644 --- a/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java @@ -4,28 +4,28 @@ public class ProblemSelectionEvent { - private Problem problem; - private Object source; - - public ProblemSelectionEvent(Problem problem, Object source) { - this.problem = problem; - this.source = source; - } - - public Problem getProblem() { - return problem; - } - - public void setProblem(Problem problem) { - this.problem = problem; - } - - public Object getSource() { - return source; - } - - public void setSource(Object source) { - this.source = source; - } - -} \ No newline at end of file + private Problem problem; + private Object source; + + public ProblemSelectionEvent(Problem problem, Object source) { + this.problem = problem; + this.source = source; + } + + public Problem getProblem() { + return problem; + } + + public void setProblem(Problem problem) { + this.problem = problem; + } + + public Object getSource() { + return source; + } + + public void setSource(Object source) { + this.source = source; + } + +} diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java index 8e975ed9..e217383b 100644 --- a/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java @@ -2,6 +2,6 @@ public interface ProblemSelectionListener { - public void onProblemSelected(ProblemSelectionEvent e); - -} \ No newline at end of file + public void onProblemSelected(ProblemSelectionEvent e); + +} diff --git a/src/main/java/pulse/ui/components/listeners/ResultListener.java b/src/main/java/pulse/ui/components/listeners/ResultListener.java index f90fe5be..17ba3bf3 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultListener.java @@ -4,6 +4,6 @@ public interface ResultListener { - public void onFormatChanged(ResultFormatEvent fme); + public void onFormatChanged(ResultFormatEvent fme); } diff --git a/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java b/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java index 88156bc9..ad7961cf 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java @@ -2,14 +2,14 @@ public interface ResultRequestListener { - public void onMergeRequest(); + public void onMergeRequest(); - public void onDeleteRequest(); + public void onDeleteRequest(); - public void onPreviewRequest(); + public void onPreviewRequest(); - public void onUndoRequest(); + public void onUndoRequest(); - public void onExportRequest(); + public void onExportRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/TaskActionListener.java b/src/main/java/pulse/ui/components/listeners/TaskActionListener.java index 747c7598..5262cd2c 100644 --- a/src/main/java/pulse/ui/components/listeners/TaskActionListener.java +++ b/src/main/java/pulse/ui/components/listeners/TaskActionListener.java @@ -2,12 +2,12 @@ public interface TaskActionListener { - public void onRemoveRequest(); + public void onRemoveRequest(); - public void onClearRequest(); + public void onClearRequest(); - public void onResetRequest(); + public void onResetRequest(); - public void onGraphRequest(); + public void onGraphRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/package-info.java b/src/main/java/pulse/ui/components/listeners/package-info.java index 030cf495..fc163630 100644 --- a/src/main/java/pulse/ui/components/listeners/package-info.java +++ b/src/main/java/pulse/ui/components/listeners/package-info.java @@ -1 +1 @@ -package pulse.ui.components.listeners; \ No newline at end of file +package pulse.ui.components.listeners; diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java index 64bd897d..e82d734e 100644 --- a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -12,39 +12,39 @@ @SuppressWarnings("serial") public class StoredCalculationTableModel extends DefaultTableModel { - public static final int WEIGHT_COLUMN = 5; - public static final int STATUS_COLUMN = 4; - public static final int MODEL_STATISTIC_COLUMN = 3; - public static final int OPTIMISER_STATISTIC_COLUMN = 2; - public static final int BASELINE_COLUMN = 1; - public static final int PROBLEM_COLUMN = 0; - - public StoredCalculationTableModel() { - - super(new Object[][] {}, - new String[] { "Problem Statement", "Baseline", "Parameter count", - "Optimiser Statistic", "Model Selection Statistic", - def(MODEL_WEIGHT).getAbbreviation(true) }); - } - - public void update(SearchTask t) { - super.setRowCount(0); - var list = t.getStoredCalculations(); - - for(Calculation c : list) { - var problem = c.getProblem(); - var baseline = c.getProblem().getBaseline(); - var optimiser = c.getOptimiserStatistic(); - var criterion = c.getModelSelectionCriterion(); - var parameters = c.getModelSelectionCriterion().getNumVariables(); - - var weight = c.weight(list); - - var data = new Object[] { problem, baseline, parameters, optimiser.getStatistic(), criterion.getStatistic(), weight }; - - invokeLater(() -> super.addRow(data)); - } - - } - -} \ No newline at end of file + public static final int WEIGHT_COLUMN = 5; + public static final int STATUS_COLUMN = 4; + public static final int MODEL_STATISTIC_COLUMN = 3; + public static final int OPTIMISER_STATISTIC_COLUMN = 2; + public static final int BASELINE_COLUMN = 1; + public static final int PROBLEM_COLUMN = 0; + + public StoredCalculationTableModel() { + + super(new Object[][]{}, + new String[]{"Problem Statement", "Baseline", "Parameter count", + "Optimiser Statistic", "Model Selection Statistic", + def(MODEL_WEIGHT).getAbbreviation(true)}); + } + + public void update(SearchTask t) { + super.setRowCount(0); + var list = t.getStoredCalculations(); + + for (Calculation c : list) { + var problem = c.getProblem(); + var baseline = c.getProblem().getBaseline(); + var optimiser = c.getOptimiserStatistic(); + var criterion = c.getModelSelectionCriterion(); + var parameters = c.getModelSelectionCriterion().getNumVariables(); + + var weight = c.weight(list); + + var data = new Object[]{problem, baseline, parameters, optimiser.getStatistic(), criterion.getStatistic(), weight}; + + invokeLater(() -> super.addRow(data)); + } + + } + +} diff --git a/src/main/java/pulse/ui/components/models/TaskBoxModel.java b/src/main/java/pulse/ui/components/models/TaskBoxModel.java index 5042ae4c..5230f284 100644 --- a/src/main/java/pulse/ui/components/models/TaskBoxModel.java +++ b/src/main/java/pulse/ui/components/models/TaskBoxModel.java @@ -15,83 +15,85 @@ /* * BASED ON DefaultComboBoxModel */ - public class TaskBoxModel extends AbstractListModel implements ComboBoxModel { - /** - * - */ - private static final long serialVersionUID = 5394433933807306979L; - protected SearchTask selectedTask; - - public TaskBoxModel() { - var instance = TaskManager.getManagerInstance(); - selectedTask = instance.getSelectedTask(); - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TASK_ADDED) { - notifyTaskAdded(e.getId()); - } - if (e.getState() == TASK_REMOVED) { - notifyTaskRemoved(e.getId()); - } - }); - - } - - @Override - public int getSize() { - return TaskManager.getManagerInstance().numberOfTasks(); - } - - @Override - public SearchTask getElementAt(int index) { - return TaskManager.getManagerInstance().getTaskList().get(index); - } - - @Override - public void setSelectedItem(Object anItem) { - // No item is selected and object is null, so no change required. - if (selectedTask == null && anItem == null) - return; - - if (!(anItem instanceof SearchTask)) - throw new IllegalArgumentException(getString("TaskBoxModel.WrongClassError")); //$NON-NLS-1$ - - // object is already selected so no change required. - if (selectedTask != null && selectedTask.equals(anItem)) - return; - - // Simply return if object is not in the list. - if (selectedTask != null && !TaskManager.getManagerInstance().getTaskList().contains(anItem)) - return; - - // Here we know that object is either an item in the list or null. - // Handle the three change cases: selectedItem is null, object is - // non-null; selectedItem is non-null, object is null; - // selectedItem is non-null, object is non-null and they're not - // equal. - selectedTask = (SearchTask) anItem; - fireContentsChanged(this, -1, -1); - } - - public int getSelectedIndex() { - return TaskManager.getManagerInstance().getTaskList().indexOf(selectedTask); - } - - @Override - public Object getSelectedItem() { - return selectedTask; - } - - private void notifyTaskAdded(Identifier id) { - var index = (int) id.getValue(); - fireIntervalAdded(this, index, index); - } - - private void notifyTaskRemoved(Identifier id) { - var index = (int) id.getValue(); - fireIntervalRemoved(this, index, index); - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = 5394433933807306979L; + protected SearchTask selectedTask; + + public TaskBoxModel() { + var instance = TaskManager.getManagerInstance(); + selectedTask = instance.getSelectedTask(); + + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TASK_ADDED) { + notifyTaskAdded(e.getId()); + } + if (e.getState() == TASK_REMOVED) { + notifyTaskRemoved(e.getId()); + } + }); + + } + + @Override + public int getSize() { + return TaskManager.getManagerInstance().numberOfTasks(); + } + + @Override + public SearchTask getElementAt(int index) { + return TaskManager.getManagerInstance().getTaskList().get(index); + } + + @Override + public void setSelectedItem(Object anItem) { + // No item is selected and object is null, so no change required. + if (selectedTask == null && anItem == null) { + return; + } + + if (!(anItem instanceof SearchTask)) { + throw new IllegalArgumentException(getString("TaskBoxModel.WrongClassError")); //$NON-NLS-1$ + } + // object is already selected so no change required. + if (selectedTask != null && selectedTask.equals(anItem)) { + return; + } + + // Simply return if object is not in the list. + if (selectedTask != null && !TaskManager.getManagerInstance().getTaskList().contains(anItem)) { + return; + } + + // Here we know that object is either an item in the list or null. + // Handle the three change cases: selectedItem is null, object is + // non-null; selectedItem is non-null, object is null; + // selectedItem is non-null, object is non-null and they're not + // equal. + selectedTask = (SearchTask) anItem; + fireContentsChanged(this, -1, -1); + } + + public int getSelectedIndex() { + return TaskManager.getManagerInstance().getTaskList().indexOf(selectedTask); + } + + @Override + public Object getSelectedItem() { + return selectedTask; + } + + private void notifyTaskAdded(Identifier id) { + var index = (int) id.getValue(); + fireIntervalAdded(this, index, index); + } + + private void notifyTaskRemoved(Identifier id) { + var index = (int) id.getValue(); + fireIntervalRemoved(this, index, index); + } + +} diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 766e6550..77d51b81 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -22,63 +22,71 @@ @SuppressWarnings("serial") public class TaskTableModel extends DefaultTableModel { - public static final int SEARCH_STATISTIC_COLUMN = 2; - public static final int TEST_STATISTIC_COLUMN = 3; - public static final int STATUS_COLUMN = 4; + public static final int SEARCH_STATISTIC_COLUMN = 2; + public static final int TEST_STATISTIC_COLUMN = 3; + public static final int STATUS_COLUMN = 4; - public TaskTableModel() { + public TaskTableModel() { - super(new Object[][] {}, - new String[] { def(IDENTIFIER).getAbbreviation(true), def(TEST_TEMPERATURE).getAbbreviation(true), - def(OPTIMISER_STATISTIC).getAbbreviation(true), def(TEST_STATISTIC).getAbbreviation(true), - getString("TaskTable.Status") }); + super(new Object[][]{}, + new String[]{def(IDENTIFIER).getAbbreviation(true), + def(TEST_TEMPERATURE).getAbbreviation(true), + def(OPTIMISER_STATISTIC).getAbbreviation(true), + def(TEST_STATISTIC).getAbbreviation(true), + getString("TaskTable.Status")}); - var instance = TaskManager.getManagerInstance(); + var instance = TaskManager.getManagerInstance(); - /* + /* * task removed/added listener - */ - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TASK_REMOVED) - removeTask(e.getId()); - else if (e.getState() == TASK_ADDED) - addTask(instance.getTask(e.getId())); - }); - - } - - public void addTask(SearchTask t) { - var temperature = t.getExperimentalCurve().getMetadata().numericProperty(TEST_TEMPERATURE); - var data = new Object[] { t.getIdentifier(), temperature, t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus() }; - - invokeLater(() -> super.addRow(data)); - - t.addStatusChangeListener((StateEntry e) -> { - setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); - if (t.getNormalityTest() != null) - setValueAt(t.getNormalityTest().getStatistic(), searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); - }); - - t.addTaskListener((LogEntry e) -> { - setValueAt(t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); - }); - - } - - public void removeTask(Identifier id) { - var index = searchRow(id); - - if (index > -1) - invokeLater(() -> super.removeRow(index)); - - } - - public int searchRow(Identifier id) { - var data = this.getDataVector(); - var v = dataVector.stream().filter(row -> ((Identifier) row.get(0)).equals(id)).findFirst(); - return v.isPresent() ? data.indexOf(v.get()) : -1; - } - -} \ No newline at end of file + */ + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TASK_REMOVED) { + removeTask(e.getId()); + } else if (e.getState() == TASK_ADDED) { + addTask(instance.getTask(e.getId())); + } + }); + + } + + public void addTask(SearchTask t) { + var temperature = t.getExperimentalCurve() + .getMetadata().numericProperty(TEST_TEMPERATURE); + var data = new Object[]{t.getIdentifier(), temperature, + t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), + t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus()}; + + invokeLater(() -> super.addRow(data)); + + t.addStatusChangeListener((StateEntry e) -> { + setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); + if (t.getNormalityTest() != null) { + setValueAt(t.getNormalityTest().getStatistic(), + searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); + } + }); + + t.addTaskListener((LogEntry e) -> { + setValueAt(t.getCurrentCalculation().getOptimiserStatistic() + .getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); + }); + + } + + public void removeTask(Identifier id) { + var index = searchRow(id); + + if (index > -1) { + invokeLater(() -> super.removeRow(index)); + } + + } + + public int searchRow(Identifier id) { + var data = this.getDataVector(); + var v = dataVector.stream().filter(row -> ((Identifier) row.get(0)).equals(id)).findFirst(); + return v.isPresent() ? data.indexOf(v.get()) : -1; + } + +} diff --git a/src/main/java/pulse/ui/components/models/package-info.java b/src/main/java/pulse/ui/components/models/package-info.java index f15d7bc4..ac134d93 100644 --- a/src/main/java/pulse/ui/components/models/package-info.java +++ b/src/main/java/pulse/ui/components/models/package-info.java @@ -1 +1 @@ -package pulse.ui.components.models; \ No newline at end of file +package pulse.ui.components.models; diff --git a/src/main/java/pulse/ui/components/package-info.java b/src/main/java/pulse/ui/components/package-info.java index 3fbd7a3f..c71c6d7c 100644 --- a/src/main/java/pulse/ui/components/package-info.java +++ b/src/main/java/pulse/ui/components/package-info.java @@ -3,5 +3,4 @@ * interface of {@code PULsE} that are used to interact with all other entities, * such as {@code PropertyHolder}s, etc. {@code Propert}ies. */ - -package pulse.ui.components; \ No newline at end of file +package pulse.ui.components; diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index 4646c453..a9fd6a16 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -19,40 +19,40 @@ @SuppressWarnings("serial") public class LogToolbar extends JToolBar { - private final static int ICON_SIZE = 16; - private List listeners; + private final static int ICON_SIZE = 16; + private List listeners; - public LogToolbar() { - super(); - setFloatable(false); - initComponents(); - listeners = new ArrayList<>(); - } + public LogToolbar() { + super(); + setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - setLayout(new GridLayout()); + public void initComponents() { + setLayout(new GridLayout()); - var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); - saveLogBtn.setToolTipText("Save"); + var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); + saveLogBtn.setToolTipText("Save"); - var verboseCheckBox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ - verboseCheckBox.setSelected(isVerbose()); - verboseCheckBox.setHorizontalAlignment(CENTER); + var verboseCheckBox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ + verboseCheckBox.setSelected(isVerbose()); + verboseCheckBox.setHorizontalAlignment(CENTER); - verboseCheckBox.addActionListener(event -> setVerbose(verboseCheckBox.isSelected())); + verboseCheckBox.addActionListener(event -> setVerbose(verboseCheckBox.isSelected())); - saveLogBtn.addActionListener(e -> notifyLog()); + saveLogBtn.addActionListener(e -> notifyLog()); - add(saveLogBtn); - add(verboseCheckBox); - } + add(saveLogBtn); + add(verboseCheckBox); + } - public void notifyLog() { - listeners.stream().forEach(l -> l.onLogExportRequest()); - } + public void notifyLog() { + listeners.stream().forEach(l -> l.onLogExportRequest()); + } - public void addLogExportListener(LogExportListener l) { - listeners.add(l); - } + public void addLogExportListener(LogExportListener l) { + listeners.add(l); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index d3c19e5d..7c0b5508 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -18,46 +18,46 @@ @SuppressWarnings("serial") public class ModelToolbar extends JToolBar { - private final static int ICON_SIZE = 20; - - public ModelToolbar() { - super(); - setOpaque(false); - setFloatable(false); - setRollover(true); - var set = Calculation.getModelSelectionDescriptor().getAllDescriptors(); - var criterionSelection = new JComboBox<>(set.toArray(String[]::new)); - criterionSelection.addActionListener(e -> - Calculation.getModelSelectionDescriptor().setSelectedDescriptor((String)criterionSelection.getSelectedItem()) - ); - criterionSelection.setSelectedItem(Calculation.getModelSelectionDescriptor().getValue()); - - add(new JLabel("Model Selection Criterion: ")); - add(Box.createRigidArea(new Dimension(5,0))); - add(criterionSelection); - - var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE, Color.WHITE)); - doCalc.setToolTipText("Re-calculate model weights"); - add(Box.createRigidArea(new Dimension(15,0))); - add(doCalc); - - doCalc.addActionListener(e -> { - var instance = TaskManager.getManagerInstance(); - var t = instance.getSelectedTask(); - t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().calcCriterion() ); - instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); - }); - - var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE, Color.RED)); - bestSelection.setToolTipText("Select Best Model"); - add(Box.createRigidArea(new Dimension(15,0))); - add(bestSelection); - - bestSelection.addActionListener(e -> { - var t = TaskManager.getManagerInstance().getSelectedTask(); - t.switchToBestModel(); - }); - - } + private final static int ICON_SIZE = 20; + + public ModelToolbar() { + super(); + setOpaque(false); + setFloatable(false); + setRollover(true); + var set = Calculation.getModelSelectionDescriptor().getAllDescriptors(); + var criterionSelection = new JComboBox<>(set.toArray(String[]::new)); + criterionSelection.addActionListener(e + -> Calculation.getModelSelectionDescriptor().setSelectedDescriptor((String) criterionSelection.getSelectedItem()) + ); + criterionSelection.setSelectedItem(Calculation.getModelSelectionDescriptor().getValue()); + + add(new JLabel("Model Selection Criterion: ")); + add(Box.createRigidArea(new Dimension(5, 0))); + add(criterionSelection); + + var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE, Color.WHITE)); + doCalc.setToolTipText("Re-calculate model weights"); + add(Box.createRigidArea(new Dimension(15, 0))); + add(doCalc); + + doCalc.addActionListener(e -> { + var instance = TaskManager.getManagerInstance(); + var t = instance.getSelectedTask(); + t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().calcCriterion()); + instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); + }); + + var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE, Color.RED)); + bestSelection.setToolTipText("Select Best Model"); + add(Box.createRigidArea(new Dimension(15, 0))); + add(bestSelection); + + bestSelection.addActionListener(e -> { + var t = TaskManager.getManagerInstance().getSelectedTask(); + t.switchToBestModel(); + }); + + } } diff --git a/src/main/java/pulse/ui/components/panels/OpacitySlider.java b/src/main/java/pulse/ui/components/panels/OpacitySlider.java index 257fdebe..c8ef55ee 100644 --- a/src/main/java/pulse/ui/components/panels/OpacitySlider.java +++ b/src/main/java/pulse/ui/components/panels/OpacitySlider.java @@ -14,33 +14,33 @@ @SuppressWarnings("serial") public class OpacitySlider extends JSlider { - private List listeners; + private List listeners; - private final static float SLIDER_A_COEF = 0.01f; - private final static float SLIDER_B_COEF = 0.04605f; + private final static float SLIDER_A_COEF = 0.01f; + private final static float SLIDER_B_COEF = 0.04605f; - public OpacitySlider() { - initComponents(); - listeners = new ArrayList<>(); - } + public OpacitySlider() { + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - setBorder(createEmptyBorder(5, 0, 5, 0)); - setOrientation(VERTICAL); - setToolTipText("Slide to change the dataset opacity"); + public void initComponents() { + setBorder(createEmptyBorder(5, 0, 5, 0)); + setOrientation(VERTICAL); + setToolTipText("Slide to change the dataset opacity"); - addChangeListener(e -> { - getChart().setOpacity((float) (SLIDER_A_COEF * exp(SLIDER_B_COEF * getValue()))); - notifyPlot(); - }); - } + addChangeListener(e -> { + getChart().setOpacity((float) (SLIDER_A_COEF * exp(SLIDER_B_COEF * getValue()))); + notifyPlot(); + }); + } - public void addPlotRequestListener(PlotRequestListener plotRequestListener) { - listeners.add(plotRequestListener); - } + public void addPlotRequestListener(PlotRequestListener plotRequestListener) { + listeners.add(plotRequestListener); + } - private void notifyPlot() { - listeners.stream().forEach(l -> l.onPlotRequest()); - } + private void notifyPlot() { + listeners.stream().forEach(l -> l.onPlotRequest()); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/ResultToolbar.java b/src/main/java/pulse/ui/components/panels/ResultToolbar.java index fc3c3590..64a83365 100644 --- a/src/main/java/pulse/ui/components/panels/ResultToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ResultToolbar.java @@ -15,105 +15,105 @@ @SuppressWarnings("serial") public class ResultToolbar extends JToolBar { - private final static int ICON_SIZE = 16; + private final static int ICON_SIZE = 16; - private JButton deleteEntryBtn; - private JButton mergeBtn; - private JButton undoBtn; - private JButton previewBtn; - private JButton saveResultsBtn; + private JButton deleteEntryBtn; + private JButton mergeBtn; + private JButton undoBtn; + private JButton previewBtn; + private JButton saveResultsBtn; - private List listeners; + private List listeners; - public ResultToolbar() { - super(); - this.setFloatable(false); - initComponents(); - listeners = new ArrayList<>(); - } + public ResultToolbar() { + super(); + this.setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - deleteEntryBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); - mergeBtn = new JButton(loadIcon("merge.png", ICON_SIZE)); - undoBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); - previewBtn = new JButton(loadIcon("preview.png", ICON_SIZE)); - saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); - setLayout(new GridLayout(5, 0)); + public void initComponents() { + deleteEntryBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); + mergeBtn = new JButton(loadIcon("merge.png", ICON_SIZE)); + undoBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); + previewBtn = new JButton(loadIcon("preview.png", ICON_SIZE)); + saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); + setLayout(new GridLayout(5, 0)); - deleteEntryBtn.setToolTipText("Delete Entry"); - add(deleteEntryBtn); + deleteEntryBtn.setToolTipText("Delete Entry"); + add(deleteEntryBtn); - mergeBtn.setToolTipText("Merge (Auto)"); - add(mergeBtn); + mergeBtn.setToolTipText("Merge (Auto)"); + add(mergeBtn); - undoBtn.setToolTipText("Undo"); - add(undoBtn); + undoBtn.setToolTipText("Undo"); + add(undoBtn); - previewBtn.setToolTipText("Preview"); - add(previewBtn); + previewBtn.setToolTipText("Preview"); + add(previewBtn); - saveResultsBtn.setToolTipText("Save"); - add(saveResultsBtn); + saveResultsBtn.setToolTipText("Save"); + add(saveResultsBtn); - deleteEntryBtn.setEnabled(false); - deleteEntryBtn.addActionListener(e -> notifyDelete()); + deleteEntryBtn.setEnabled(false); + deleteEntryBtn.addActionListener(e -> notifyDelete()); - mergeBtn.setEnabled(false); - mergeBtn.addActionListener(e -> notifyMerge()); + mergeBtn.setEnabled(false); + mergeBtn.addActionListener(e -> notifyMerge()); - undoBtn.setEnabled(false); - undoBtn.addActionListener(e -> notifyUndo()); + undoBtn.setEnabled(false); + undoBtn.addActionListener(e -> notifyUndo()); - previewBtn.setEnabled(false); - previewBtn.addActionListener(e -> notifyPreview()); + previewBtn.setEnabled(false); + previewBtn.addActionListener(e -> notifyPreview()); - saveResultsBtn.setEnabled(false); - saveResultsBtn.addActionListener(e -> notifyExport()); + saveResultsBtn.setEnabled(false); + saveResultsBtn.addActionListener(e -> notifyExport()); - } + } - public void setDeleteEnabled(boolean deleteEnabled) { - deleteEntryBtn.setEnabled(deleteEnabled); - } + public void setDeleteEnabled(boolean deleteEnabled) { + deleteEntryBtn.setEnabled(deleteEnabled); + } - public void setMergeEnabled(boolean mergeEnabled) { - mergeBtn.setEnabled(mergeEnabled); - } + public void setMergeEnabled(boolean mergeEnabled) { + mergeBtn.setEnabled(mergeEnabled); + } - public void setUndoEnabled(boolean undoEnabled) { - undoBtn.setEnabled(undoEnabled); - } + public void setUndoEnabled(boolean undoEnabled) { + undoBtn.setEnabled(undoEnabled); + } - public void setPreviewEnabled(boolean previewEnabled) { - previewBtn.setEnabled(previewEnabled); - } + public void setPreviewEnabled(boolean previewEnabled) { + previewBtn.setEnabled(previewEnabled); + } - public void setExportEnabled(boolean exportEnabled) { - saveResultsBtn.setEnabled(exportEnabled); - } + public void setExportEnabled(boolean exportEnabled) { + saveResultsBtn.setEnabled(exportEnabled); + } - private void notifyDelete() { - listeners.stream().forEach(l -> l.onDeleteRequest()); - } + private void notifyDelete() { + listeners.stream().forEach(l -> l.onDeleteRequest()); + } - private void notifyMerge() { - listeners.stream().forEach(l -> l.onMergeRequest()); - } + private void notifyMerge() { + listeners.stream().forEach(l -> l.onMergeRequest()); + } - private void notifyUndo() { - listeners.stream().forEach(l -> l.onUndoRequest()); - } + private void notifyUndo() { + listeners.stream().forEach(l -> l.onUndoRequest()); + } - private void notifyPreview() { - listeners.stream().forEach(l -> l.onPreviewRequest()); - } + private void notifyPreview() { + listeners.stream().forEach(l -> l.onPreviewRequest()); + } - private void notifyExport() { - listeners.stream().forEach(l -> l.onExportRequest()); - } + private void notifyExport() { + listeners.stream().forEach(l -> l.onExportRequest()); + } - public void addResultRequestListener(ResultRequestListener l) { - listeners.add(l); - } + public void addResultRequestListener(ResultRequestListener l) { + listeners.add(l); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java index 9ef76689..405e5453 100644 --- a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java +++ b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java @@ -19,64 +19,64 @@ public class SettingsToolBar extends JToolBar { - private static final long serialVersionUID = -1171612225785102673L; + private static final long serialVersionUID = -1171612225785102673L; - private JCheckBox cbSingleStatement, cbHideDetails; + private JCheckBox cbSingleStatement, cbHideDetails; - public SettingsToolBar(PropertyHolderTable... tables) { - super(); - setFloatable(false); + public SettingsToolBar(PropertyHolderTable... tables) { + super(); + setFloatable(false); - var taskBox = new TaskBox(); + var taskBox = new TaskBox(); - cbSingleStatement = new JCheckBox(getString("TaskSelectionToolBar.ApplyToAll")); //$NON-NLS-1$ - cbSingleStatement.setSelected(TaskManager.getManagerInstance().isSingleStatement()); + cbSingleStatement = new JCheckBox(getString("TaskSelectionToolBar.ApplyToAll")); //$NON-NLS-1$ + cbSingleStatement.setSelected(TaskManager.getManagerInstance().isSingleStatement()); - cbHideDetails = new JCheckBox(getString("TaskSelectionToolBar.Hide")); //$NON-NLS-1$ - cbHideDetails.setSelected(true); + cbHideDetails = new JCheckBox(getString("TaskSelectionToolBar.Hide")); //$NON-NLS-1$ + cbHideDetails.setSelected(true); - setLayout(new GridBagLayout()); + setLayout(new GridBagLayout()); - var gbc = new GridBagConstraints(); - gbc.fill = BOTH; - gbc.weightx = 3.0; - gbc.gridx = 0; - gbc.gridy = 0; + var gbc = new GridBagConstraints(); + gbc.fill = BOTH; + gbc.weightx = 3.0; + gbc.gridx = 0; + gbc.gridy = 0; - add(taskBox, gbc); + add(taskBox, gbc); - gbc.gridx = 1; - gbc.weightx = 1.0; - add(createHorizontalStrut(5), gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + add(createHorizontalStrut(5), gbc); - gbc.gridx = 2; - gbc.weightx = 1.0; + gbc.gridx = 2; + gbc.weightx = 1.0; - add(cbSingleStatement, gbc); + add(cbSingleStatement, gbc); - cbSingleStatement.addChangeListener(e -> TaskManager.getManagerInstance().setSingleStatement(cbSingleStatement.isSelected())); + cbSingleStatement.addChangeListener(e -> TaskManager.getManagerInstance().setSingleStatement(cbSingleStatement.isSelected())); - gbc.gridx = 3; + gbc.gridx = 3; - add(cbHideDetails, gbc); + add(cbHideDetails, gbc); - cbHideDetails.addChangeListener((ChangeEvent e) -> { - var selected = cbHideDetails.isSelected(); - Problem.setDetailsHidden(selected); - DifferenceScheme.setDetailsHidden(selected); - for (var table : tables) { - table.updateTable(); - } - }); + cbHideDetails.addChangeListener((ChangeEvent e) -> { + var selected = cbHideDetails.isSelected(); + Problem.setDetailsHidden(selected); + DifferenceScheme.setDetailsHidden(selected); + for (var table : tables) { + table.updateTable(); + } + }); - } + } - public JCheckBox getHideDetailsCheckBox() { - return cbHideDetails; - } + public JCheckBox getHideDetailsCheckBox() { + return cbHideDetails; + } - public JCheckBox getSingleStatementCheckBox() { - return cbHideDetails; - } + public JCheckBox getSingleStatementCheckBox() { + return cbHideDetails; + } } diff --git a/src/main/java/pulse/ui/components/panels/SystemPanel.java b/src/main/java/pulse/ui/components/panels/SystemPanel.java index d57df067..0fe6a200 100644 --- a/src/main/java/pulse/ui/components/panels/SystemPanel.java +++ b/src/main/java/pulse/ui/components/panels/SystemPanel.java @@ -21,64 +21,64 @@ @SuppressWarnings("serial") public class SystemPanel extends JPanel { - private JLabel coresLabel; - private JLabel cpuLabel; - private JLabel memoryLabel; - - public SystemPanel() { - initComponents(); - startSystemMonitors(); - } - - private void initComponents() { - coresLabel = new JLabel(); - cpuLabel = new JLabel(); - memoryLabel = new JLabel(); - - setLayout(new GridBagLayout()); - var gridBagConstraints = new GridBagConstraints(); - - cpuLabel.setHorizontalAlignment(LEFT); - cpuLabel.setText("CPU:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(cpuLabel, gridBagConstraints); - - memoryLabel.setHorizontalAlignment(CENTER); - memoryLabel.setText("Memory:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(memoryLabel, gridBagConstraints); - - coresLabel.setHorizontalAlignment(RIGHT); - coresLabel.setText("{n cores} "); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(coresLabel, gridBagConstraints); - } - - private void startSystemMonitors() { - var monitor = ResourceMonitor.getInstance(); - - var coresAvailable = format("{" + (monitor.getThreadsAvailable() + 1) + " cores}"); - coresLabel.setText(coresAvailable); - - var executor = newSingleThreadScheduledExecutor(); - var defColor = UIManager.getColor("Label.foreground"); - - Runnable periodicTask = () -> { - monitor.update(); - var cpuString = format("CPU usage: %3.1f%%", monitor.getCpuUsage()); - cpuLabel.setText(cpuString); - var memoryString = format("Memory usage: %3.1f%%", monitor.getMemoryUsage()); - memoryLabel.setText(memoryString); - - cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float)monitor.getCpuUsage()/100)); - memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float)monitor.getMemoryUsage()/100)); - - }; - - executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); - } - -} \ No newline at end of file + private JLabel coresLabel; + private JLabel cpuLabel; + private JLabel memoryLabel; + + public SystemPanel() { + initComponents(); + startSystemMonitors(); + } + + private void initComponents() { + coresLabel = new JLabel(); + cpuLabel = new JLabel(); + memoryLabel = new JLabel(); + + setLayout(new GridBagLayout()); + var gridBagConstraints = new GridBagConstraints(); + + cpuLabel.setHorizontalAlignment(LEFT); + cpuLabel.setText("CPU:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(cpuLabel, gridBagConstraints); + + memoryLabel.setHorizontalAlignment(CENTER); + memoryLabel.setText("Memory:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(memoryLabel, gridBagConstraints); + + coresLabel.setHorizontalAlignment(RIGHT); + coresLabel.setText("{n cores} "); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(coresLabel, gridBagConstraints); + } + + private void startSystemMonitors() { + var monitor = ResourceMonitor.getInstance(); + + var coresAvailable = format("{" + (monitor.getThreadsAvailable() + 1) + " cores}"); + coresLabel.setText(coresAvailable); + + var executor = newSingleThreadScheduledExecutor(); + var defColor = UIManager.getColor("Label.foreground"); + + Runnable periodicTask = () -> { + monitor.update(); + var cpuString = format("CPU usage: %3.1f%%", monitor.getCpuUsage()); + cpuLabel.setText(cpuString); + var memoryString = format("Memory usage: %3.1f%%", monitor.getMemoryUsage()); + memoryLabel.setText(memoryString); + + cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float) monitor.getCpuUsage() / 100)); + memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float) monitor.getMemoryUsage() / 100)); + + }; + + executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); + } + +} diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index 858f1038..786178ef 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -19,104 +19,104 @@ @SuppressWarnings("serial") public class TaskToolbar extends JToolBar { - private final static int ICON_SIZE = 16; - - private JButton removeBtn; - private JButton clearBtn; - private JButton graphBtn; - private JButton execBtn; - private JButton resetBtn; - - private List listeners; - - public TaskToolbar() { - super(); - setFloatable(false); - initComponents(); - listeners = new ArrayList<>(); - addButtonListeners(); - } - - private void initComponents() { - - removeBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); - clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE, blend(red, black, 0.5f))); - resetBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); - graphBtn = new JButton(loadIcon("graph.png", ICON_SIZE)); - execBtn = new ExecutionButton(); - - setLayout(new GridLayout(1, 0)); - - removeBtn.setEnabled(false); - clearBtn.setEnabled(false); - resetBtn.setEnabled(false); - graphBtn.setEnabled(false); - execBtn.setEnabled(false); - - removeBtn.setToolTipText("Remove Task"); - add(removeBtn); - - clearBtn.setToolTipText("Clear All Tasks"); - add(clearBtn); - - resetBtn.setToolTipText("Reset All Tasks"); - add(resetBtn); - - graphBtn.setToolTipText("Show Graph"); - add(graphBtn); - - execBtn.setToolTipText("Execute All Tasks"); - add(execBtn); - } - - public void setRemoveEnabled(boolean b) { - removeBtn.setEnabled(b); - } - - public void setClearEnabled(boolean b) { - clearBtn.setEnabled(b); - } - - public void setGraphEnabled(boolean b) { - graphBtn.setEnabled(b); - } - - public void setExecEnabled(boolean b) { - execBtn.setEnabled(b); - } - - public void setResetEnabled(boolean b) { - resetBtn.setEnabled(b); - } - - private void addButtonListeners() { - removeBtn.addActionListener(e -> notifyRemove()); - clearBtn.addActionListener(e -> notifyClear()); - resetBtn.addActionListener(e -> { - TaskManager.getManagerInstance().reset(); - notifyReset(); - }); - graphBtn.addActionListener(e -> notifyGraph()); - } - - public void notifyRemove() { - listeners.stream().forEach(l -> l.onRemoveRequest()); - } - - public void notifyClear() { - listeners.stream().forEach(l -> l.onClearRequest()); - } - - public void notifyReset() { - listeners.stream().forEach(l -> l.onResetRequest()); - } - - public void notifyGraph() { - listeners.stream().forEach(l -> l.onGraphRequest()); - } - - public void addTaskActionListener(TaskActionListener l) { - listeners.add(l); - } - -} \ No newline at end of file + private final static int ICON_SIZE = 16; + + private JButton removeBtn; + private JButton clearBtn; + private JButton graphBtn; + private JButton execBtn; + private JButton resetBtn; + + private List listeners; + + public TaskToolbar() { + super(); + setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + addButtonListeners(); + } + + private void initComponents() { + + removeBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); + clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE, blend(red, black, 0.5f))); + resetBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); + graphBtn = new JButton(loadIcon("graph.png", ICON_SIZE)); + execBtn = new ExecutionButton(); + + setLayout(new GridLayout(1, 0)); + + removeBtn.setEnabled(false); + clearBtn.setEnabled(false); + resetBtn.setEnabled(false); + graphBtn.setEnabled(false); + execBtn.setEnabled(false); + + removeBtn.setToolTipText("Remove Task"); + add(removeBtn); + + clearBtn.setToolTipText("Clear All Tasks"); + add(clearBtn); + + resetBtn.setToolTipText("Reset All Tasks"); + add(resetBtn); + + graphBtn.setToolTipText("Show Graph"); + add(graphBtn); + + execBtn.setToolTipText("Execute All Tasks"); + add(execBtn); + } + + public void setRemoveEnabled(boolean b) { + removeBtn.setEnabled(b); + } + + public void setClearEnabled(boolean b) { + clearBtn.setEnabled(b); + } + + public void setGraphEnabled(boolean b) { + graphBtn.setEnabled(b); + } + + public void setExecEnabled(boolean b) { + execBtn.setEnabled(b); + } + + public void setResetEnabled(boolean b) { + resetBtn.setEnabled(b); + } + + private void addButtonListeners() { + removeBtn.addActionListener(e -> notifyRemove()); + clearBtn.addActionListener(e -> notifyClear()); + resetBtn.addActionListener(e -> { + TaskManager.getManagerInstance().reset(); + notifyReset(); + }); + graphBtn.addActionListener(e -> notifyGraph()); + } + + public void notifyRemove() { + listeners.stream().forEach(l -> l.onRemoveRequest()); + } + + public void notifyClear() { + listeners.stream().forEach(l -> l.onClearRequest()); + } + + public void notifyReset() { + listeners.stream().forEach(l -> l.onResetRequest()); + } + + public void notifyGraph() { + listeners.stream().forEach(l -> l.onGraphRequest()); + } + + public void addTaskActionListener(TaskActionListener l) { + listeners.add(l); + } + +} diff --git a/src/main/java/pulse/ui/components/panels/package-info.java b/src/main/java/pulse/ui/components/panels/package-info.java index 458922b9..6a17ceb3 100644 --- a/src/main/java/pulse/ui/components/panels/package-info.java +++ b/src/main/java/pulse/ui/components/panels/package-info.java @@ -1 +1 @@ -package pulse.ui.components.panels; \ No newline at end of file +package pulse.ui.components.panels; diff --git a/src/main/java/pulse/ui/frames/DataFrame.java b/src/main/java/pulse/ui/frames/DataFrame.java index dcc26c5f..492df291 100644 --- a/src/main/java/pulse/ui/frames/DataFrame.java +++ b/src/main/java/pulse/ui/frames/DataFrame.java @@ -16,59 +16,61 @@ public class DataFrame extends JFrame { - private static final long serialVersionUID = 1L; - private JPanel contentPane; - private PropertyHolderTable dataTable; - private Component ancestorFrame; - private PropertyHolder dataObject; - private final static int ROW_HEIGHT = 70; + private static final long serialVersionUID = 1L; + private JPanel contentPane; + private PropertyHolderTable dataTable; + private Component ancestorFrame; + private PropertyHolder dataObject; + private final static int ROW_HEIGHT = 70; - @Override - public void dispose() { - if (ancestorFrame != null) { - ancestorFrame.setEnabled(true); - if (ancestorFrame.getParent() != null) - ancestorFrame.getParent().setEnabled(true); - } - super.dispose(); - } + @Override + public void dispose() { + if (ancestorFrame != null) { + ancestorFrame.setEnabled(true); + if (ancestorFrame.getParent() != null) { + ancestorFrame.getParent().setEnabled(true); + } + } + super.dispose(); + } - /** - * Create the frame. - */ - public DataFrame(PropertyHolder dataObject, Component ancestor) { - setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setLocationRelativeTo(ancestor); - this.ancestorFrame = ancestor.getParent(); - this.dataObject = dataObject; - if (ancestor != null) { - ancestor.setEnabled(false); - if (ancestorFrame != null) - ancestorFrame.setEnabled(false); - } - setType(UTILITY); - setResizable(false); - setAlwaysOnTop(true); - setTitle(dataObject.getClass().getSimpleName() + " properties"); + /** + * Create the frame. + */ + public DataFrame(PropertyHolder dataObject, Component ancestor) { + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setLocationRelativeTo(ancestor); + this.ancestorFrame = ancestor.getParent(); + this.dataObject = dataObject; + if (ancestor != null) { + ancestor.setEnabled(false); + if (ancestorFrame != null) { + ancestorFrame.setEnabled(false); + } + } + setType(UTILITY); + setResizable(false); + setAlwaysOnTop(true); + setTitle(dataObject.getClass().getSimpleName() + " properties"); - contentPane = new JPanel(); - contentPane.setForeground(BLACK); - contentPane.setBorder(null); - contentPane.setLayout(new BorderLayout(0, 0)); - setContentPane(contentPane); - var scrollPane = new JScrollPane(); - contentPane.add(scrollPane, CENTER); + contentPane = new JPanel(); + contentPane.setForeground(BLACK); + contentPane.setBorder(null); + contentPane.setLayout(new BorderLayout(0, 0)); + setContentPane(contentPane); + var scrollPane = new JScrollPane(); + contentPane.add(scrollPane, CENTER); - dataTable = new PropertyHolderTable(dataObject); - dataTable.setRowHeight(ROW_HEIGHT); - - setBounds(100, 100, 600, 450); + dataTable = new PropertyHolderTable(dataObject); + dataTable.setRowHeight(ROW_HEIGHT); - scrollPane.setViewportView(dataTable); - } + setBounds(100, 100, 600, 450); - public PropertyHolder getDataObject() { - return dataObject; - } + scrollPane.setViewportView(dataTable); + } + + public PropertyHolder getDataObject() { + return dataObject; + } } diff --git a/src/main/java/pulse/ui/frames/ExternalGraphFrame.java b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java index cc0ddc7c..358c373f 100644 --- a/src/main/java/pulse/ui/frames/ExternalGraphFrame.java +++ b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java @@ -12,27 +12,27 @@ @SuppressWarnings("serial") public class ExternalGraphFrame extends JFrame { - private AuxPlotter chart; - - public ExternalGraphFrame(String name, AuxPlotter chart, final int width, final int height) { - super(name); - this.chart = chart; - initComponents(chart); - this.setSize(new Dimension(width, height)); - setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); - } - - private void initComponents(AuxPlotter chart) { - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - } - - public void plot(T t) { - chart.plot(t); - } - - public AuxPlotter getChart() { - return chart; - } - -} \ No newline at end of file + private AuxPlotter chart; + + public ExternalGraphFrame(String name, AuxPlotter chart, final int width, final int height) { + super(name); + this.chart = chart; + initComponents(chart); + this.setSize(new Dimension(width, height)); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java index 9e347d27..f91f8a7b 100644 --- a/src/main/java/pulse/ui/frames/HistogramFrame.java +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -16,22 +16,22 @@ @SuppressWarnings("serial") public class HistogramFrame extends ExternalGraphFrame { - public HistogramFrame(AuxPlotter chart, int width, int height) { - super("Residuals PDF", chart, width, height); - this.getChart().getChartPanel().setBorder(BorderFactory.createRaisedSoftBevelBorder()); - var slider = new JSlider(8, 100, 20); - var panel = new JPanel(); - var info = new JLabel("Number of bins: " + ((ResidualsChart)chart).getBinCount()); - panel.add(info); - panel.add(new JSeparator()); - panel.add(slider); - panel.setBorder(BorderFactory.createRaisedSoftBevelBorder()); - getContentPane().add(panel, SOUTH); - slider.addChangeListener(e -> { - ((ResidualsChart)chart).setBinCount(slider.getValue()); - plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic() ); - info.setText("Number of bins: " + slider.getValue()); - }); - } + public HistogramFrame(AuxPlotter chart, int width, int height) { + super("Residuals PDF", chart, width, height); + this.getChart().getChartPanel().setBorder(BorderFactory.createRaisedSoftBevelBorder()); + var slider = new JSlider(8, 100, 20); + var panel = new JPanel(); + var info = new JLabel("Number of bins: " + ((ResidualsChart) chart).getBinCount()); + panel.add(info); + panel.add(new JSeparator()); + panel.add(slider); + panel.setBorder(BorderFactory.createRaisedSoftBevelBorder()); + getContentPane().add(panel, SOUTH); + slider.addChangeListener(e -> { + ((ResidualsChart) chart).setBinCount(slider.getValue()); + plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic()); + info.setText("Number of bins: " + slider.getValue()); + }); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/InternalGraphFrame.java b/src/main/java/pulse/ui/frames/InternalGraphFrame.java index b5751b4c..44239f81 100644 --- a/src/main/java/pulse/ui/frames/InternalGraphFrame.java +++ b/src/main/java/pulse/ui/frames/InternalGraphFrame.java @@ -9,31 +9,31 @@ @SuppressWarnings("serial") public class InternalGraphFrame extends JInternalFrame { - private AuxPlotter chart; - - public InternalGraphFrame(String name, AuxPlotter chart) { - super(name, true, false, true, true); - this.chart = chart; - initComponents(chart); - setVisible(true); - } - - private void initComponents(AuxPlotter chart) { - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - - chartPanel.setMaximumDrawHeight(2000); - chartPanel.setMaximumDrawWidth(2000); - chartPanel.setMinimumDrawWidth(10); - chartPanel.setMinimumDrawHeight(10); - } - - public void plot(T t) { - chart.plot(t); - } - - public AuxPlotter getChart() { - return chart; - } - -} \ No newline at end of file + private AuxPlotter chart; + + public InternalGraphFrame(String name, AuxPlotter chart) { + super(name, true, false, true, true); + this.chart = chart; + initComponents(chart); + setVisible(true); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + + chartPanel.setMaximumDrawHeight(2000); + chartPanel.setMaximumDrawWidth(2000); + chartPanel.setMinimumDrawWidth(10); + chartPanel.setMinimumDrawHeight(10); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index d68ffaa2..6410fb7d 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -29,82 +29,84 @@ @SuppressWarnings("serial") public class LogFrame extends JInternalFrame { - private LogPane logTextPane; + private LogPane logTextPane; - public LogFrame() { - super("Log", true, false, true, true); - initComponents(); - scheduleLogEvents(); - setVisible(true); - } + public LogFrame() { + super("Log", true, false, true, true); + initComponents(); + scheduleLogEvents(); + setVisible(true); + } - private void initComponents() { - logTextPane = new LogPane(); - var logScroller = new JScrollPane(); - logScroller.setViewportView(logTextPane); + private void initComponents() { + logTextPane = new LogPane(); + var logScroller = new JScrollPane(); + logScroller.setViewportView(logTextPane); - getContentPane().setLayout(new BorderLayout()); - getContentPane().add(logScroller, CENTER); + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(logScroller, CENTER); - var gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.anchor = WEST; - gridBagConstraints.weightx = 0.5; + var gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.anchor = WEST; + gridBagConstraints.weightx = 0.5; - getContentPane().add(new SystemPanel(), PAGE_END); + getContentPane().add(new SystemPanel(), PAGE_END); - var logToolbar = new LogToolbar(); - logToolbar.addLogExportListener(() -> { - if (logTextPane.getDocument().getLength() > 0) - askToExport(logTextPane, (JFrame) getWindowAncestor(this), - getString("LogToolBar.FileFormatDescriptor")); - }); - getContentPane().add(logToolbar, NORTH); + var logToolbar = new LogToolbar(); + logToolbar.addLogExportListener(() -> { + if (logTextPane.getDocument().getLength() > 0) { + askToExport(logTextPane, (JFrame) getWindowAncestor(this), + getString("LogToolBar.FileFormatDescriptor")); + } + }); + getContentPane().add(logToolbar, NORTH); - } + } - private void scheduleLogEvents() { - var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logTextPane.printAll()); + private void scheduleLogEvents() { + var instance = TaskManager.getManagerInstance(); + instance.addSelectionListener(e -> logTextPane.printAll()); - instance.addTaskRepositoryListener(event -> { - if (event.getState() != TASK_ADDED) - return; + instance.addTaskRepositoryListener(event -> { + if (event.getState() != TASK_ADDED) { + return; + } - var task = instance.getTask(event.getId()); + var task = instance.getTask(event.getId()); - task.getLog().addListener(new LogEntryListener() { + task.getLog().addListener(new LogEntryListener() { - @Override - public void onLogFinished(Log log) { - if (instance.getSelectedTask() == task) { + @Override + public void onLogFinished(Log log) { + if (instance.getSelectedTask() == task) { - try { - logTextPane.getUpdateExecutor().awaitTermination(10, MILLISECONDS); - } catch (InterruptedException e) { - err.println("Log not finished in time"); - e.printStackTrace(); - } + try { + logTextPane.getUpdateExecutor().awaitTermination(10, MILLISECONDS); + } catch (InterruptedException e) { + err.println("Log not finished in time"); + e.printStackTrace(); + } - logTextPane.printTimeTaken(log); + logTextPane.printTimeTaken(log); - } - } + } + } - @Override - public void onNewEntry(LogEntry e) { - if (instance.getSelectedTask() == task) - logTextPane.callUpdate(); - } + @Override + public void onNewEntry(LogEntry e) { + if (instance.getSelectedTask() == task) { + logTextPane.callUpdate(); + } + } - } + } + ); - ); + }); + } - }); - } + public LogPane getLogTextPane() { + return logTextPane; + } - public LogPane getLogTextPane() { - return logTextPane; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index 9e8ebac6..3d81254e 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -3,6 +3,7 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; +import java.util.concurrent.Executors; import javax.swing.JInternalFrame; @@ -14,45 +15,46 @@ @SuppressWarnings("serial") public class MainGraphFrame extends JInternalFrame { - private static Chart chart; - private static MainGraphFrame instance = new MainGraphFrame(); - - private MainGraphFrame() { - super("Time-temperature profile(s)", true, false, true, true); - initComponents(); - setVisible(true); - } - - private void initComponents() { - chart = new Chart(); - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - - chartPanel.setMaximumDrawHeight(2000); - chartPanel.setMaximumDrawWidth(2000); - chartPanel.setMinimumDrawWidth(10); - chartPanel.setMinimumDrawHeight(10); - - var opacitySlider = new OpacitySlider(); - opacitySlider.addPlotRequestListener(() -> plot()); - getContentPane().add(opacitySlider, LINE_END); - var chartToolbar = new ChartToolbar(); - chartToolbar.addPlotRequestListener(() -> plot()); - getContentPane().add(chartToolbar, PAGE_END); - } - - public void plot() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) - chart.plot(task, false); - } - - public static Chart getChart() { - return chart; - } - - public static MainGraphFrame getInstance() { - return instance; - } - -} \ No newline at end of file + private static Chart chart; + private static MainGraphFrame instance = new MainGraphFrame(); + + private MainGraphFrame() { + super("Time-temperature profile(s)", true, false, true, true); + initComponents(); + setVisible(true); + } + + private void initComponents() { + chart = new Chart(); + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + + chartPanel.setMaximumDrawHeight(2000); + chartPanel.setMaximumDrawWidth(2000); + chartPanel.setMinimumDrawWidth(10); + chartPanel.setMinimumDrawHeight(10); + + var opacitySlider = new OpacitySlider(); + opacitySlider.addPlotRequestListener(() -> plot()); + getContentPane().add(opacitySlider, LINE_END); + var chartToolbar = new ChartToolbar(); + chartToolbar.addPlotRequestListener(() -> plot()); + getContentPane().add(chartToolbar, PAGE_END); + } + + public void plot() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + if (task != null) { + Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); + } + } + + public static Chart getChart() { + return chart; + } + + public static MainGraphFrame getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java index ba88e853..68856aba 100644 --- a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -14,21 +14,23 @@ @SuppressWarnings("serial") public class ModelSelectionFrame extends JInternalFrame { - private CalculationTable table; - - public ModelSelectionFrame() { - super("Model Comparison", true, true, true, true); - table = new CalculationTable(); - getContentPane().add(new JScrollPane(table)); - setSize(new Dimension(400, 400)); - setTitle("Stored Calculations"); - getContentPane().add(new ModelToolbar(), BorderLayout.SOUTH); - var instance = TaskManager.getManagerInstance(); - instance.addTaskRepositoryListener(e-> { - if(e.getState() == TASK_BROWSING_REQUEST) - table.update(instance.getTask( e.getId() ) ); - }); - this.setDefaultCloseOperation(HIDE_ON_CLOSE); - } - -} \ No newline at end of file + + private CalculationTable table; + + public ModelSelectionFrame() { + super("Model Comparison", true, true, true, true); + table = new CalculationTable(); + getContentPane().add(new JScrollPane(table)); + setSize(new Dimension(400, 400)); + setTitle("Stored Calculations"); + getContentPane().add(new ModelToolbar(), BorderLayout.SOUTH); + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener(e -> { + if (e.getState() == TASK_BROWSING_REQUEST) { + table.update(instance.getTask(e.getId())); + } + }); + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + } + +} diff --git a/src/main/java/pulse/ui/frames/ResultFrame.java b/src/main/java/pulse/ui/frames/ResultFrame.java index 3200c7cb..07031274 100644 --- a/src/main/java/pulse/ui/frames/ResultFrame.java +++ b/src/main/java/pulse/ui/frames/ResultFrame.java @@ -23,107 +23,112 @@ import pulse.ui.components.ResultTable; import pulse.ui.components.listeners.PreviewFrameCreationListener; import pulse.ui.components.listeners.ResultRequestListener; +import pulse.ui.components.models.ResultTableModel; import pulse.ui.components.panels.ResultToolbar; import pulse.ui.frames.dialogs.FormattedInputDialog; @SuppressWarnings("serial") public class ResultFrame extends JInternalFrame { - private ResultToolbar resultToolbar; - private ResultTable resultTable; - private List listeners; - private FormattedInputDialog averageWindowDialog; - - public ResultFrame() { - super("Results", true, false, true, true); - initComponents(); - listeners = new ArrayList<>(); - addListeners(); - setVisible(true); - } - - private void initComponents() { - var resultsScroller = new JScrollPane(); - - resultTable = new ResultTable(getInstance()); - resultsScroller.setViewportView(resultTable); - getContentPane().add(resultsScroller, CENTER); - - resultToolbar = new ResultToolbar(); - getContentPane().add(resultToolbar, EAST); - - averageWindowDialog = new FormattedInputDialog(def(WINDOW)); - } - - private void addListeners() { - resultToolbar.addResultRequestListener(new ResultRequestListener() { - - @Override - public void onDeleteRequest() { - resultTable.deleteSelected(); - } - - @Override - public void onPreviewRequest() { - if (!resultTable.hasEnoughElements(1)) { - showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.NoDataError"), - getString("ResultsToolBar.NoResultsError"), ERROR_MESSAGE); - } else - notifyPreview(); - } - - @Override - public void onMergeRequest() { - if (resultTable.hasEnoughElements(1)) - showInputDialog(); - } - - @Override - public void onUndoRequest() { - resultTable.undo(); - } - - @Override - public void onExportRequest() { - if (!resultTable.hasEnoughElements(1)) { - showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.7"), - getString("ResultsToolBar.8"), ERROR_MESSAGE); - return; - } - - askToExport(resultTable, (JFrame) getWindowAncestor(resultTable), "Calculation results"); - } - - }); - - resultTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { - resultToolbar.setDeleteEnabled(!resultTable.isSelectionEmpty()); - }); - - resultTable.getModel().addTableModelListener((TableModelEvent arg0) -> { - resultToolbar.setPreviewEnabled(resultTable.hasEnoughElements(3)); - resultToolbar.setMergeEnabled(resultTable.hasEnoughElements(2)); - resultToolbar.setExportEnabled(resultTable.hasEnoughElements(1)); - resultToolbar.setUndoEnabled(resultTable.hasEnoughElements(1)); - }); - } - - public void notifyPreview() { - listeners.stream().forEach(l -> l.onPreviewFrameRequest()); - } - - public void addFrameCreationListener(PreviewFrameCreationListener l) { - listeners.add(l); - } - - private void showInputDialog() { - averageWindowDialog.setLocationRelativeTo(null); - averageWindowDialog.setVisible(true); - averageWindowDialog.setConfirmAction(() -> resultTable.merge(averageWindowDialog.value().doubleValue())); - } - - public ResultTable getResultTable() { - return resultTable; - } - -} \ No newline at end of file + private ResultToolbar resultToolbar; + private ResultTable resultTable; + private List listeners; + private FormattedInputDialog averageWindowDialog; + + public ResultFrame() { + super("Results", true, false, true, true); + initComponents(); + listeners = new ArrayList<>(); + addListeners(); + setVisible(true); + } + + private void initComponents() { + var resultsScroller = new JScrollPane(); + + resultTable = new ResultTable(getInstance()); + resultsScroller.setViewportView(resultTable); + getContentPane().add(resultsScroller, CENTER); + + resultToolbar = new ResultToolbar(); + getContentPane().add(resultToolbar, EAST); + + averageWindowDialog = new FormattedInputDialog(def(WINDOW)); + } + + private void addListeners() { + resultToolbar.addResultRequestListener(new ResultRequestListener() { + + @Override + public void onDeleteRequest() { + resultTable.deleteSelected(); + } + + @Override + public void onPreviewRequest() { + if (!resultTable.hasEnoughElements(1)) { + showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.NoDataError"), + getString("ResultsToolBar.NoResultsError"), ERROR_MESSAGE); + } else { + notifyPreview(); + } + } + + @Override + public void onMergeRequest() { + if (resultTable.hasEnoughElements(1)) { + showInputDialog(); + } + } + + @Override + public void onUndoRequest() { + resultTable.undo(); + } + + @Override + public void onExportRequest() { + if (!resultTable.hasEnoughElements(1)) { + showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.7"), + getString("ResultsToolBar.8"), ERROR_MESSAGE); + return; + } + + askToExport(resultTable, (JFrame) getWindowAncestor(resultTable), "Calculation results"); + } + + }); + + resultTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { + resultToolbar.setDeleteEnabled(!resultTable.isSelectionEmpty()); + }); + + resultTable.getModel().addTableModelListener((TableModelEvent arg0) -> { + resultToolbar.setPreviewEnabled(resultTable.hasEnoughElements(3)); + resultToolbar.setMergeEnabled(resultTable.hasEnoughElements(2)); + resultToolbar.setExportEnabled(resultTable.hasEnoughElements(1)); + resultToolbar.setUndoEnabled(resultTable.hasEnoughElements(1)); + }); + } + + public void notifyPreview() { + listeners.stream().forEach(l -> l.onPreviewFrameRequest()); + } + + public void addFrameCreationListener(PreviewFrameCreationListener l) { + listeners.add(l); + } + + private void showInputDialog() { + averageWindowDialog.setLocationRelativeTo(null); + averageWindowDialog.setVisible(true); + averageWindowDialog.setConfirmAction(() -> + ((ResultTableModel)resultTable.getModel()) + .merge(averageWindowDialog.value().doubleValue())); + } + + public ResultTable getResultTable() { + return resultTable; + } + +} diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 39f08889..8a32dc34 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -37,423 +37,424 @@ @SuppressWarnings("serial") public class TaskControlFrame extends JFrame { - private final static int HEIGHT = 730; - private final static int WIDTH = 1035; - - private static TaskControlFrame instance = new TaskControlFrame(); - - private Mode mode = Mode.TASK; - private ProblemStatementFrame problemStatementFrame; - private SearchOptionsFrame searchOptionsFrame; - private TaskManagerFrame taskManagerFrame; - private ModelSelectionFrame modelFrame; - private PreviewFrame previewFrame; - private ResultFrame resultsFrame; - private MainGraphFrame graphFrame; - private LogFrame logFrame; - private InternalGraphFrame pulseFrame; - - private PulseMainMenu mainMenu; - - public static TaskControlFrame getInstance() { - return instance; - } - - /** - * Create the frame. - */ - - private TaskControlFrame() { - setTitle(Version.getCurrentVersion().toString()); - setPreferredSize(new Dimension(WIDTH, HEIGHT)); - setExtendedState(getExtendedState() | MAXIMIZED_BOTH); - initComponents(); - initListeners(); - TaskManager.getManagerInstance().addSelectionListener(e -> graphFrame.plot()); - setIconImage(loadIcon("logo.png", 32).getImage()); - addListeners(); - setDefaultCloseOperation(EXIT_ON_CLOSE); - } - - private void addListeners() { - addWindowListener(new WindowAdapter() { - - @Override - public void windowClosing(WindowEvent evt) { - var closingWindow = (JFrame) evt.getSource(); - if (!exitConfirmed(closingWindow)) { - closingWindow.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - } else - closingWindow.setDefaultCloseOperation(EXIT_ON_CLOSE); - } - - }); - - addComponentListener(new ComponentAdapter() { - - @Override - public void componentResized(ComponentEvent e) { - doResize(); - } - - }); - - } - - private boolean exitConfirmed(Component closingComponent) { - Object[] options = { "Yes", "No" }; - return showOptionDialog(closingComponent, getString("TaskControlFrame.ExitMessage"), - getString("TaskControlFrame.ExitTitle"), YES_NO_OPTION, WARNING_MESSAGE, null, options, - options[1]) == YES_OPTION; - } - - private void initListeners() { - mainMenu.addFrameVisibilityRequestListener(new FrameVisibilityRequestListener() { - - @Override - public void onProblemStatementShowRequest() { - problemStatementFrame.update(); - setProblemStatementFrameVisible(true); - } - - @Override - public void onSearchSettingsShowRequest() { - setSearchOptionsFrameVisible(true); - } - - }); - - mainMenu.addExitRequestListener(() -> { - if (exitConfirmed(this)) - exit(0); - }); - - var manager = TaskManager.getManagerInstance(); - manager.addTaskRepositoryListener(e -> - { - if(e.getState() == TASK_BROWSING_REQUEST) - setModelSelectionFrameVisible(true); - } - ); - - addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) - .changeFormat(rfe.getResultFormat())); - - resultsFrame.addFrameCreationListener(() -> setPreviewFrameVisible(true)); - - taskManagerFrame.getTaskToolbar().addTaskActionListener(new TaskActionListener() { - - @Override - public void onRemoveRequest() { - // no new actions - } - - @Override - public void onClearRequest() { - logFrame.getLogTextPane().clear(); - resultsFrame.getResultTable().clear(); - } - - @Override - public void onResetRequest() { - logFrame.getLogTextPane().clear(); - resultsFrame.getResultTable().removeAll(); - } - - @Override - public void onGraphRequest() { - graphFrame.plot(); - } - - }); - - } - - /** - * This method is called from within the constructor to initialize the form. - */ - - private void initComponents() { - - var desktopPane = new JDesktopPane(); - setContentPane(desktopPane); - - mainMenu = new PulseMainMenu(); - setJMenuBar(mainMenu); - - logFrame = new LogFrame(); - logFrame.setFrameIcon(loadIcon("log.png", 20, Color.white)); - resultsFrame = new ResultFrame(); - resultsFrame.setFrameIcon(loadIcon("result.png", 20, Color.white)); - previewFrame = new PreviewFrame(); - previewFrame.setFrameIcon(loadIcon("preview.png", 20, Color.white)); - taskManagerFrame = new TaskManagerFrame(); - taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20, Color.white)); - graphFrame = MainGraphFrame.getInstance(); - graphFrame.setFrameIcon(loadIcon("curves.png", 20, Color.white)); - - problemStatementFrame = new ProblemStatementFrame(); - problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20, Color.white)); - modelFrame = new ModelSelectionFrame(); - modelFrame.setFrameIcon(loadIcon("stored.png", 20, Color.white)); - - searchOptionsFrame = new SearchOptionsFrame(); - searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); - - pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); - pulseFrame.setVisible(false); - - /* + private final static int HEIGHT = 730; + private final static int WIDTH = 1035; + + private static TaskControlFrame instance = new TaskControlFrame(); + + private Mode mode = Mode.TASK; + private ProblemStatementFrame problemStatementFrame; + private SearchOptionsFrame searchOptionsFrame; + private TaskManagerFrame taskManagerFrame; + private ModelSelectionFrame modelFrame; + private PreviewFrame previewFrame; + private ResultFrame resultsFrame; + private MainGraphFrame graphFrame; + private LogFrame logFrame; + private InternalGraphFrame pulseFrame; + + private PulseMainMenu mainMenu; + + public static TaskControlFrame getInstance() { + return instance; + } + + /** + * Create the frame. + */ + private TaskControlFrame() { + setTitle(Version.getCurrentVersion().toString()); + setPreferredSize(new Dimension(WIDTH, HEIGHT)); + setExtendedState(getExtendedState() | MAXIMIZED_BOTH); + initComponents(); + initListeners(); + TaskManager.getManagerInstance().addSelectionListener(e -> graphFrame.plot()); + setIconImage(loadIcon("logo.png", 32).getImage()); + addListeners(); + setDefaultCloseOperation(EXIT_ON_CLOSE); + } + + private void addListeners() { + addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent evt) { + var closingWindow = (JFrame) evt.getSource(); + if (!exitConfirmed(closingWindow)) { + closingWindow.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + } else { + closingWindow.setDefaultCloseOperation(EXIT_ON_CLOSE); + } + } + + }); + + addComponentListener(new ComponentAdapter() { + + @Override + public void componentResized(ComponentEvent e) { + doResize(); + } + + }); + + } + + private boolean exitConfirmed(Component closingComponent) { + Object[] options = {"Yes", "No"}; + return showOptionDialog(closingComponent, getString("TaskControlFrame.ExitMessage"), + getString("TaskControlFrame.ExitTitle"), YES_NO_OPTION, WARNING_MESSAGE, null, options, + options[1]) == YES_OPTION; + } + + private void initListeners() { + mainMenu.addFrameVisibilityRequestListener(new FrameVisibilityRequestListener() { + + @Override + public void onProblemStatementShowRequest() { + problemStatementFrame.update(); + setProblemStatementFrameVisible(true); + } + + @Override + public void onSearchSettingsShowRequest() { + setSearchOptionsFrameVisible(true); + } + + }); + + mainMenu.addExitRequestListener(() -> { + if (exitConfirmed(this)) { + exit(0); + } + }); + + var manager = TaskManager.getManagerInstance(); + manager.addTaskRepositoryListener(e + -> { + if (e.getState() == TASK_BROWSING_REQUEST) { + setModelSelectionFrameVisible(true); + } + } + ); + + addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) + .changeFormat(rfe.getResultFormat())); + + resultsFrame.addFrameCreationListener(() -> setPreviewFrameVisible(true)); + + taskManagerFrame.getTaskToolbar().addTaskActionListener(new TaskActionListener() { + + @Override + public void onRemoveRequest() { + // no new actions + } + + @Override + public void onClearRequest() { + logFrame.getLogTextPane().clear(); + resultsFrame.getResultTable().clear(); + } + + @Override + public void onResetRequest() { + logFrame.getLogTextPane().clear(); + resultsFrame.getResultTable().removeAll(); + } + + @Override + public void onGraphRequest() { + graphFrame.plot(); + } + + }); + + } + + /** + * This method is called from within the constructor to initialize the form. + */ + private void initComponents() { + + var desktopPane = new JDesktopPane(); + setContentPane(desktopPane); + + mainMenu = new PulseMainMenu(); + setJMenuBar(mainMenu); + + logFrame = new LogFrame(); + logFrame.setFrameIcon(loadIcon("log.png", 20, Color.white)); + resultsFrame = new ResultFrame(); + resultsFrame.setFrameIcon(loadIcon("result.png", 20, Color.white)); + previewFrame = new PreviewFrame(); + previewFrame.setFrameIcon(loadIcon("preview.png", 20, Color.white)); + taskManagerFrame = new TaskManagerFrame(); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20, Color.white)); + graphFrame = MainGraphFrame.getInstance(); + graphFrame.setFrameIcon(loadIcon("curves.png", 20, Color.white)); + + problemStatementFrame = new ProblemStatementFrame(); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20, Color.white)); + modelFrame = new ModelSelectionFrame(); + modelFrame.setFrameIcon(loadIcon("stored.png", 20, Color.white)); + + searchOptionsFrame = new SearchOptionsFrame(); + searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); + + pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); + pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); + pulseFrame.setVisible(false); + + /* * CONSTRAINT ADJUSTMENT - */ - - resizeQuadrants(); - desktopPane.add(taskManagerFrame); - desktopPane.add(pulseFrame); - desktopPane.add(graphFrame); - desktopPane.add(previewFrame); - desktopPane.add(logFrame); - desktopPane.add(resultsFrame); - desktopPane.add(problemStatementFrame); - desktopPane.add(searchOptionsFrame); - desktopPane.add(modelFrame); - - setDefaultResizeBehaviour(); - - pack(); - - } - - private void setDefaultResizeBehaviour() { - var ifa = new InternalFrameAdapter() { - - @Override - public void internalFrameDeiconified(InternalFrameEvent e) { - resizeQuadrants(); - } - - }; - - taskManagerFrame.addInternalFrameListener(ifa); - graphFrame.addInternalFrameListener(ifa); - logFrame.addInternalFrameListener(ifa); - resultsFrame.addInternalFrameListener(ifa); + */ + resizeQuadrants(); + desktopPane.add(taskManagerFrame); + desktopPane.add(pulseFrame); + desktopPane.add(graphFrame); + desktopPane.add(previewFrame); + desktopPane.add(logFrame); + desktopPane.add(resultsFrame); + desktopPane.add(problemStatementFrame); + desktopPane.add(searchOptionsFrame); + desktopPane.add(modelFrame); + + setDefaultResizeBehaviour(); + + pack(); + + } + + private void setDefaultResizeBehaviour() { + var ifa = new InternalFrameAdapter() { + + @Override + public void internalFrameDeiconified(InternalFrameEvent e) { + resizeQuadrants(); + } + + }; + + taskManagerFrame.addInternalFrameListener(ifa); + graphFrame.addInternalFrameListener(ifa); + logFrame.addInternalFrameListener(ifa); + resultsFrame.addInternalFrameListener(ifa); + + previewFrame.addInternalFrameListener(new InternalFrameAdapter() { - previewFrame.addInternalFrameListener(new InternalFrameAdapter() { + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setPreviewFrameVisible(false); + } + + }); - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setPreviewFrameVisible(false); - } + problemStatementFrame.addInternalFrameListener(new InternalFrameAdapter() { - }); + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setProblemStatementFrameVisible(false); + } - problemStatementFrame.addInternalFrameListener(new InternalFrameAdapter() { + }); - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setProblemStatementFrameVisible(false); - } + searchOptionsFrame.addInternalFrameListener(new InternalFrameAdapter() { - }); + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setSearchOptionsFrameVisible(false); + } - searchOptionsFrame.addInternalFrameListener(new InternalFrameAdapter() { + }); - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setSearchOptionsFrameVisible(false); - } + modelFrame.addInternalFrameListener(new InternalFrameAdapter() { - }); + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setModelSelectionFrameVisible(false); + } - modelFrame.addInternalFrameListener(new InternalFrameAdapter() { + }); - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setModelSelectionFrameVisible(false); - } + } - }); + private void doResize() { + switch (mode) { + case TASK: + resizeQuadrants(); + break; + case PROBLEM: + resizeTriplet(problemStatementFrame, pulseFrame, graphFrame); + break; + case SEARCH: + resizeFull(searchOptionsFrame); + break; + case PREVIEW: + resizeHalves(previewFrame, resultsFrame); + break; + case MODEL_COMPARISON: + resizeTriplet(graphFrame, resultsFrame, modelFrame); + break; + default: + break; + } + } - } + private void resizeFull(JInternalFrame f1) { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 2 * gap; + final var w = this.getContentPane().getWidth() - 2 * gap; - private void doResize() { - switch (mode) { - case TASK: - resizeQuadrants(); - break; - case PROBLEM: - resizeTriplet(problemStatementFrame, pulseFrame, graphFrame); - break; - case SEARCH: - resizeFull(searchOptionsFrame); - break; - case PREVIEW: - resizeHalves(previewFrame, resultsFrame); - break; - case MODEL_COMPARISON: - resizeTriplet(graphFrame, resultsFrame, modelFrame); - break; - default: - break; - } - } + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, h); + f1.setLocation(p1); + f1.setSize(s1); + } - private void resizeFull(JInternalFrame f1) { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 2 * gap; - final var w = this.getContentPane().getWidth() - 2 * gap; + private void resizeHalves(JInternalFrame f1, JInternalFrame f2) { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 3 * gap; + final var w = this.getContentPane().getWidth() - 2 * gap; - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, h); - f1.setLocation(p1); - f1.setSize(s1); - } + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, 6 * h / 10); - private void resizeHalves(JInternalFrame f1, JInternalFrame f2) { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 3 * gap; - final var w = this.getContentPane().getWidth() - 2 * gap; + var p2 = new Point(gap, 2 * gap + 6 * h / 10); + var s2 = new Dimension(w, 4 * h / 10); - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, 6 * h / 10); + f1.setLocation(p1); + f1.setSize(s1); + f2.setLocation(p2); + f2.setSize(s2); + } + + private void resizeTriplet(JInternalFrame f1, JInternalFrame f2, JInternalFrame f3) { + final var gap = 10; + + final var h = this.getContentPane().getHeight() - 3 * gap; + var w = this.getContentPane().getWidth() - 2 * gap; + + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, 6 * h / 10); + + f1.setLocation(p1); + f1.setSize(s1); + + w = this.getContentPane().getWidth() - 3 * gap; + + var p2 = new Point(gap, 2 * gap + 6 * h / 10); + var s2 = new Dimension(w / 4, 4 * h / 10); + + f2.setLocation(p2); + f2.setSize(s2); + + var p3 = new Point(2 * gap + w / 4, 2 * gap + 6 * h / 10); + var s3 = new Dimension(3 * w / 4, 4 * h / 10); + + f3.setLocation(p3); + f3.setSize(s3); + } + + private void resizeQuadrants() { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 3 * gap; + final var w = this.getContentPane().getWidth() - 3 * gap; + + var p1 = new Point(gap, gap); + var s1 = new Dimension(45 * w / 100, 55 * h / 100); + + var p2 = new Point(2 * gap + 45 * w / 100, gap); + var s2 = new Dimension(55 * w / 100, 55 * h / 100); + + var p3 = new Point(gap, 2 * gap + 55 * h / 100); + var s3 = new Dimension(45 * w / 100, 45 * h / 100); + + var p4 = new Point(2 * gap + 45 * w / 100, 2 * gap + 55 * h / 100); + var s4 = new Dimension(55 * w / 100, 45 * h / 100); + + taskManagerFrame.setLocation(p1); + taskManagerFrame.setSize(s1); + graphFrame.setLocation(p2); + graphFrame.setSize(s2); + logFrame.setLocation(p3); + logFrame.setSize(s3); + resultsFrame.setLocation(p4); + resultsFrame.setSize(s4); + } + + private void setPreviewFrameVisible(boolean show) { + previewFrame.update(((ResultTableModel) resultsFrame.getResultTable().getModel()).getFormat(), + resultsFrame.getResultTable().data()); + + previewFrame.setVisible(show); + + resultsFrame.setVisible(true); + taskManagerFrame.setVisible(!show); + graphFrame.setVisible(!show); + logFrame.setVisible(!show); - var p2 = new Point(gap, 2 * gap + 6 * h / 10); - var s2 = new Dimension(w, 4 * h / 10); + mode = show ? Mode.PREVIEW : Mode.TASK; + doResize(); - f1.setLocation(p1); - f1.setSize(s1); - f2.setLocation(p2); - f2.setSize(s2); - } + } - private void resizeTriplet(JInternalFrame f1, JInternalFrame f2, JInternalFrame f3) { - final var gap = 10; - - final var h = this.getContentPane().getHeight() - 3 * gap; - var w = this.getContentPane().getWidth() - 2 * gap; - - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, 6 * h / 10); - - f1.setLocation(p1); - f1.setSize(s1); - - w = this.getContentPane().getWidth() - 3 * gap; - - var p2 = new Point(gap, 2 * gap + 6 * h / 10); - var s2 = new Dimension(w / 4, 4 * h / 10); - - f2.setLocation(p2); - f2.setSize(s2); + private void setProblemStatementFrameVisible(boolean show) { + problemStatementFrame.setVisible(show); + pulseFrame.setVisible(show); + graphFrame.setVisible(true); - var p3 = new Point(2 * gap + w / 4, 2 * gap + 6 * h / 10); - var s3 = new Dimension(3 * w / 4, 4 * h / 10); - - f3.setLocation(p3); - f3.setSize(s3); - } - - private void resizeQuadrants() { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 3 * gap; - final var w = this.getContentPane().getWidth() - 3 * gap; - - var p1 = new Point(gap, gap); - var s1 = new Dimension(45 * w / 100, 55 * h / 100); - - var p2 = new Point(2 * gap + 45 * w / 100, gap); - var s2 = new Dimension(55 * w / 100, 55 * h / 100); - - var p3 = new Point(gap, 2 * gap + 55 * h / 100); - var s3 = new Dimension(45 * w / 100, 45 * h / 100); - - var p4 = new Point(2 * gap + 45 * w / 100, 2 * gap + 55 * h / 100); - var s4 = new Dimension(55 * w / 100, 45 * h / 100); - - taskManagerFrame.setLocation(p1); - taskManagerFrame.setSize(s1); - graphFrame.setLocation(p2); - graphFrame.setSize(s2); - logFrame.setLocation(p3); - logFrame.setSize(s3); - resultsFrame.setLocation(p4); - resultsFrame.setSize(s4); - } - - private void setPreviewFrameVisible(boolean show) { - previewFrame.update(((ResultTableModel) resultsFrame.getResultTable().getModel()).getFormat(), - resultsFrame.getResultTable().data()); - - previewFrame.setVisible(show); - - resultsFrame.setVisible(true); - taskManagerFrame.setVisible(!show); - graphFrame.setVisible(!show); - logFrame.setVisible(!show); + previewFrame.setVisible(false); + resultsFrame.setVisible(!show); + taskManagerFrame.setVisible(!show); + logFrame.setVisible(!show); - mode = show ? Mode.PREVIEW : Mode.TASK; - doResize(); + mode = show ? Mode.PROBLEM : Mode.TASK; + doResize(); + } - } + private void setSearchOptionsFrameVisible(boolean show) { + if (show) { + searchOptionsFrame.update(); + } + searchOptionsFrame.setVisible(show); - private void setProblemStatementFrameVisible(boolean show) { - problemStatementFrame.setVisible(show); - pulseFrame.setVisible(show); - graphFrame.setVisible(true); + problemStatementFrame.setVisible(false); + previewFrame.setVisible(false); + resultsFrame.setVisible(!show); + taskManagerFrame.setVisible(!show); + graphFrame.setVisible(!show); + logFrame.setVisible(!show); - previewFrame.setVisible(false); - resultsFrame.setVisible(!show); - taskManagerFrame.setVisible(!show); - logFrame.setVisible(!show); + mode = show ? Mode.SEARCH : Mode.TASK; + doResize(); + } - mode = show ? Mode.PROBLEM : Mode.TASK; - doResize(); - } + private void setModelSelectionFrameVisible(boolean show) { + modelFrame.setVisible(show); + resultsFrame.setVisible(true); + graphFrame.setVisible(true); - private void setSearchOptionsFrameVisible(boolean show) { - if (show) - searchOptionsFrame.update(); - searchOptionsFrame.setVisible(show); + problemStatementFrame.setVisible(false); + previewFrame.setVisible(false); + taskManagerFrame.setVisible(!show); + logFrame.setVisible(!show); - problemStatementFrame.setVisible(false); - previewFrame.setVisible(false); - resultsFrame.setVisible(!show); - taskManagerFrame.setVisible(!show); - graphFrame.setVisible(!show); - logFrame.setVisible(!show); + mode = show ? Mode.MODEL_COMPARISON : Mode.TASK; + doResize(); + } - mode = show ? Mode.SEARCH : Mode.TASK; - doResize(); - } + public enum Mode { - private void setModelSelectionFrameVisible(boolean show) { - modelFrame.setVisible(show); - resultsFrame.setVisible(true); - graphFrame.setVisible(true); + TASK, PROBLEM, PREVIEW, SEARCH, MODEL_COMPARISON; - problemStatementFrame.setVisible(false); - previewFrame.setVisible(false); - taskManagerFrame.setVisible(!show); - logFrame.setVisible(!show); + } - mode = show ? Mode.MODEL_COMPARISON : Mode.TASK; - doResize(); - } + public Mode getMode() { + return mode; + } - public enum Mode { + public InternalGraphFrame getPulseFrame() { + return pulseFrame; + } - TASK, PROBLEM, PREVIEW, SEARCH, MODEL_COMPARISON; - - } - - public Mode getMode() { - return mode; - } - - public InternalGraphFrame getPulseFrame() { - return pulseFrame; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/TaskManagerFrame.java b/src/main/java/pulse/ui/frames/TaskManagerFrame.java index ddeec09f..033edf96 100644 --- a/src/main/java/pulse/ui/frames/TaskManagerFrame.java +++ b/src/main/java/pulse/ui/frames/TaskManagerFrame.java @@ -17,86 +17,86 @@ @SuppressWarnings("serial") public class TaskManagerFrame extends JInternalFrame { - private TaskTable taskTable; - private TaskToolbar taskToolbar; - - public TaskManagerFrame() { - super("Task Manager", true, false, true, true); - initComponents(); - adjustEnabledControls(); - manageListeners(); - setVisible(true); - } - - private void manageListeners() { - taskToolbar.addTaskActionListener(new TaskActionListener() { - - @Override - public void onRemoveRequest() { - taskTable.removeSelectedRows(); - } - - @Override - public void onClearRequest() { - TaskManager.getManagerInstance().clear(); - } - - @Override - public void onResetRequest() { - // no new actions - } - - @Override - public void onGraphRequest() { - // no new actions - } - - }); - } - - private void initComponents() { - var taskScrollPane = new JScrollPane(); - taskTable = new TaskTable(); - taskScrollPane.setViewportView(taskTable); - getContentPane().add(taskScrollPane, CENTER); - taskToolbar = new TaskToolbar(); - getContentPane().add(taskToolbar, PAGE_START); - } - - private void adjustEnabledControls() { - var ttm = (TaskTableModel) taskTable.getModel(); - - ttm.addTableModelListener((TableModelEvent arg0) -> { - if (ttm.getRowCount() < 1) { - taskToolbar.setClearEnabled(false); - taskToolbar.setResetEnabled(false); - taskToolbar.setExecEnabled(false); - } else { - taskToolbar.setClearEnabled(true); - taskToolbar.setResetEnabled(true); - taskToolbar.setExecEnabled(true); - } - }); - - taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { - var selection = taskTable.getSelectedRows(); - if (taskTable.getSelectedRow() < 0) { - taskToolbar.setRemoveEnabled(false); - taskToolbar.setGraphEnabled(false); - } else { - if (selection.length > 1) { - taskToolbar.setRemoveEnabled(false); - taskToolbar.setGraphEnabled(false); - } else if (selection.length > 0) { - taskToolbar.setRemoveEnabled(true); - taskToolbar.setGraphEnabled(true); - } - } - }); - } - - public TaskToolbar getTaskToolbar() { - return taskToolbar; - } - -} \ No newline at end of file + private TaskTable taskTable; + private TaskToolbar taskToolbar; + + public TaskManagerFrame() { + super("Task Manager", true, false, true, true); + initComponents(); + adjustEnabledControls(); + manageListeners(); + setVisible(true); + } + + private void manageListeners() { + taskToolbar.addTaskActionListener(new TaskActionListener() { + + @Override + public void onRemoveRequest() { + taskTable.removeSelectedRows(); + } + + @Override + public void onClearRequest() { + TaskManager.getManagerInstance().clear(); + } + + @Override + public void onResetRequest() { + // no new actions + } + + @Override + public void onGraphRequest() { + // no new actions + } + + }); + } + + private void initComponents() { + var taskScrollPane = new JScrollPane(); + taskTable = new TaskTable(); + taskScrollPane.setViewportView(taskTable); + getContentPane().add(taskScrollPane, CENTER); + taskToolbar = new TaskToolbar(); + getContentPane().add(taskToolbar, PAGE_START); + } + + private void adjustEnabledControls() { + var ttm = (TaskTableModel) taskTable.getModel(); + + ttm.addTableModelListener((TableModelEvent arg0) -> { + if (ttm.getRowCount() < 1) { + taskToolbar.setClearEnabled(false); + taskToolbar.setResetEnabled(false); + taskToolbar.setExecEnabled(false); + } else { + taskToolbar.setClearEnabled(true); + taskToolbar.setResetEnabled(true); + taskToolbar.setExecEnabled(true); + } + }); + + taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { + var selection = taskTable.getSelectedRows(); + if (taskTable.getSelectedRow() < 0) { + taskToolbar.setRemoveEnabled(false); + taskToolbar.setGraphEnabled(false); + } else { + if (selection.length > 1) { + taskToolbar.setRemoveEnabled(false); + taskToolbar.setGraphEnabled(false); + } else if (selection.length > 0) { + taskToolbar.setRemoveEnabled(true); + taskToolbar.setGraphEnabled(true); + } + } + }); + } + + public TaskToolbar getTaskToolbar() { + return taskToolbar; + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java index 83deee7d..179ab1f0 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java @@ -14,74 +14,74 @@ @SuppressWarnings("serial") public class ProgressDialog extends JDialog implements PropertyChangeListener { - private JProgressBar progressBar; - private int progress; - - public ProgressDialog() { - super(); - initComponents(); - setDefaultCloseOperation(HIDE_ON_CLOSE); - setTitle("Please wait..."); - setPreferredSize(new Dimension(400, 75)); - pack(); - } - - /** - * Invoked when task's progress property changes. - */ - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("progress")) { - int progress = (Integer) evt.getNewValue(); - progressBar.setValue(progress); - } - } - - private void initComponents() { - progressBar = new JProgressBar(HORIZONTAL); - progressBar.setMinimum(0); - progressBar.setStringPainted(true); - getContentPane().add(progressBar); - } - - public void incrementProgress() { - progress++; - } - - private boolean reachedCapacity() { - return progress >= progressBar.getMaximum(); - } - - public void trackProgress(int maximum) { - progressBar.setMaximum(maximum); - setVisible(true); - progress = 0; - - var progressWorker = new SwingWorker() { - - @Override - protected Void doInBackground() { - setProgress(0); - while (!reachedCapacity()) { - try { - sleep(50); - } catch (InterruptedException ignore) { - } - setProgress(progress); - } - return null; - - } - - @Override - protected void done() { - setVisible(false); - } - - }; - - progressWorker.addPropertyChangeListener(this); - progressWorker.execute(); - } - -} \ No newline at end of file + private JProgressBar progressBar; + private int progress; + + public ProgressDialog() { + super(); + initComponents(); + setDefaultCloseOperation(HIDE_ON_CLOSE); + setTitle("Please wait..."); + setPreferredSize(new Dimension(400, 75)); + pack(); + } + + /** + * Invoked when task's progress property changes. + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("progress")) { + int progress = (Integer) evt.getNewValue(); + progressBar.setValue(progress); + } + } + + private void initComponents() { + progressBar = new JProgressBar(HORIZONTAL); + progressBar.setMinimum(0); + progressBar.setStringPainted(true); + getContentPane().add(progressBar); + } + + public void incrementProgress() { + progress++; + } + + private boolean reachedCapacity() { + return progress >= progressBar.getMaximum(); + } + + public void trackProgress(int maximum) { + progressBar.setMaximum(maximum); + setVisible(true); + progress = 0; + + var progressWorker = new SwingWorker() { + + @Override + protected Void doInBackground() { + setProgress(0); + while (!reachedCapacity()) { + try { + sleep(50); + } catch (InterruptedException ignore) { + } + setProgress(progress); + } + return null; + + } + + @Override + protected void done() { + setVisible(false); + } + + }; + + progressWorker.addPropertyChangeListener(this); + progressWorker.execute(); + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/package-info.java b/src/main/java/pulse/ui/frames/dialogs/package-info.java index 78fd6320..ab04ede4 100644 --- a/src/main/java/pulse/ui/frames/dialogs/package-info.java +++ b/src/main/java/pulse/ui/frames/dialogs/package-info.java @@ -1 +1 @@ -package pulse.ui.frames.dialogs; \ No newline at end of file +package pulse.ui.frames.dialogs; diff --git a/src/main/java/pulse/ui/frames/package-info.java b/src/main/java/pulse/ui/frames/package-info.java index d159c12d..7ae2bff5 100644 --- a/src/main/java/pulse/ui/frames/package-info.java +++ b/src/main/java/pulse/ui/frames/package-info.java @@ -2,5 +2,4 @@ * Contains all JFrame classes that are used to create the graphical user * interface of {@code PULsE}. */ - -package pulse.ui.frames; \ No newline at end of file +package pulse.ui.frames; diff --git a/src/main/java/pulse/ui/package-info.java b/src/main/java/pulse/ui/package-info.java index 6ae5a6c4..f215bab1 100644 --- a/src/main/java/pulse/ui/package-info.java +++ b/src/main/java/pulse/ui/package-info.java @@ -4,5 +4,4 @@ * 'message.properties' text file, which is used for storing verbose text * constants used e.g. by the GUI. */ - -package pulse.ui; \ No newline at end of file +package pulse.ui; diff --git a/src/main/java/pulse/util/Descriptive.java b/src/main/java/pulse/util/Descriptive.java index 91ddc32f..23909ad3 100644 --- a/src/main/java/pulse/util/Descriptive.java +++ b/src/main/java/pulse/util/Descriptive.java @@ -2,24 +2,22 @@ /** * Provides the {@code describe()} functionality. - * + * * @see pulse.io.export.Exporter */ - public interface Descriptive { - /** - * Creates a {@code String} 'describing' this object, usually for exporting - * purposes. - * - * @return by default, this will return the name of the implementing class and - * the date of the calculation. - */ - - public default String describe() { + /** + * Creates a {@code String} 'describing' this object, usually for exporting + * purposes. + * + * @return by default, this will return the name of the implementing class + * and the date of the calculation. + */ + public default String describe() { - return getClass().getSimpleName(); + return getClass().getSimpleName(); - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/DescriptorChangeListener.java b/src/main/java/pulse/util/DescriptorChangeListener.java index 513c0130..4c522b7d 100644 --- a/src/main/java/pulse/util/DescriptorChangeListener.java +++ b/src/main/java/pulse/util/DescriptorChangeListener.java @@ -2,6 +2,6 @@ public interface DescriptorChangeListener { - public void onDescriptorChanged(); + public void onDescriptorChanged(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/DiscreteSelector.java b/src/main/java/pulse/util/DiscreteSelector.java index fc2552eb..beb52254 100644 --- a/src/main/java/pulse/util/DiscreteSelector.java +++ b/src/main/java/pulse/util/DiscreteSelector.java @@ -10,72 +10,74 @@ public class DiscreteSelector implements Property { - private Set allOptions; - private T defaultSelection; - private T selection; - - private List listeners; - - public DiscreteSelector(AbstractReader reader, String directory, String listLocation) { - allOptions = ReaderManager.load(reader, directory, listLocation); - listeners = new ArrayList<>(); - } - - public void addListener(DescriptorChangeListener l) { - listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - public void fireDescriptorChange() { - for(var l : listeners) - l.onDescriptorChanged(); - } - - @Override - public String toString() { - return selection.toString(); - } - - @Override - public Object getValue() { - return selection; - } - - @Override - public String getDescriptor(boolean addHtmlTags) { - return selection.describe(); - } - - @Override - public boolean attemptUpdate(Object value) { - selection = find(value.toString()); - - if(selection == null) - return false; - - fireDescriptorChange(); - return true; - } - - public T find(String name) { - var optional = allOptions.stream().filter(t -> t.toString().equalsIgnoreCase(name)).findAny(); - return optional.get(); - } - - public T getDefaultSelection() { - return defaultSelection; - } - - public void setDefaultSelection(String name) { - defaultSelection = allOptions.stream().filter(d -> d.toString().equals(name)).findAny().get(); - selection = defaultSelection; - } - - public Set getAllOptions() { - return allOptions; - } - -} \ No newline at end of file + private Set allOptions; + private T defaultSelection; + private T selection; + + private List listeners; + + public DiscreteSelector(AbstractReader reader, String directory, String listLocation) { + allOptions = ReaderManager.load(reader, directory, listLocation); + listeners = new ArrayList<>(); + } + + public void addListener(DescriptorChangeListener l) { + listeners.add(l); + } + + public List getListeners() { + return listeners; + } + + public void fireDescriptorChange() { + for (var l : listeners) { + l.onDescriptorChanged(); + } + } + + @Override + public String toString() { + return selection.toString(); + } + + @Override + public Object getValue() { + return selection; + } + + @Override + public String getDescriptor(boolean addHtmlTags) { + return selection.describe(); + } + + @Override + public boolean attemptUpdate(Object value) { + selection = find(value.toString()); + + if (selection == null) { + return false; + } + + fireDescriptorChange(); + return true; + } + + public T find(String name) { + var optional = allOptions.stream().filter(t -> t.toString().equalsIgnoreCase(name)).findAny(); + return optional.get(); + } + + public T getDefaultSelection() { + return defaultSelection; + } + + public void setDefaultSelection(String name) { + defaultSelection = allOptions.stream().filter(d -> d.toString().equals(name)).findAny().get(); + selection = defaultSelection; + } + + public Set getAllOptions() { + return allOptions; + } + +} diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 8ed2540d..07936b10 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -10,115 +10,114 @@ public class Group extends UpwardsNavigable { - /** - *

- * Tries to access getter methods to retrieve all {@code Accessible} instances - * belonging to this object. Ignores any methods that return instances of the - * same class as {@code this} one. - *

- * - * @return a {@code List} containing {@code Accessible} objects which could be - * accessed by the declared getter methods. - */ - - public List subgroups() { - var fields = new ArrayList(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { - if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) - || m.getReturnType().isAssignableFrom(getClass())) - continue; - - Group a = null; - - try { - a = (Group) m.invoke(this); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Failed to invoke " + m + " Details: "); - e.printStackTrace(); - } - - /* Ignore null, factor/instance methods returning same accessibles */ - if (a == null || a.getDescriptor().equals(getDescriptor())) - continue; - - fields.add(a); - fields.addAll(a.subgroups()); - - } - - return fields; - - } - - /** - *

- * Recursively analyses all {@code Group} objects that are identified as - * subgroups to {@code root} (explicitly checks that subgroups exclude parents - * of {@code root}) and chooses those for which an {@code Exporter} exists. - *

- * - * @param root the root group. - * @return a set of unique {@code Group}s objects. - * @see pulse.util.Group.subgroups() - */ - - public static Set contents(Group root) { - var contents = root.subgroups().stream().filter(ph -> root.getParent() != ph).collect(Collectors.toSet()); - - for (var it = contents.iterator(); it.hasNext();) - contents(it.next()).stream().forEach(a -> contents.add(a)); - - return contents; - } - - /** - * Searches for a specific {@code Accessible} with a {@code simpleName}. - * - * @see subgroups - * @param simpleName the name of the {@code Accessible}, - * @return the {@code Accessible} object. - */ - - public Group access(String simpleName) { - return subgroups().stream().filter(a -> a.getSimpleName().equals(simpleName)).findFirst().get(); - } - - /** - *

- * Selects only those {@code Accessible}s, the parent of which is {@code this}. - * Note that all {@code Accessible}s are required to explicitly adopt children - * by calling the {@code setParent()} method. - *

- * - * @return a {@code List} of children that this {@code Accessible} has adopted. - * @see subgroups - */ - - public List children() { - return subgroups().stream().filter(a -> a.getParent() == this).collect(toList()); - } - - /** - * The same as {@code getSimpleName} in this implementation. - * - * @return the simple name of the declaring class. - * @see getSimpleName() - */ - - public String getDescriptor() { - return getClass().getSimpleName(); - } - - /** - * This will generate a simple name for identifying this {@code Accessible}. - * - * @return the simple name of the declaring class. - */ - - public String getSimpleName() { - return getClass().getSimpleName(); - } - -} \ No newline at end of file + /** + *

+ * Tries to access getter methods to retrieve all {@code Accessible} + * instances belonging to this object. Ignores any methods that return + * instances of the same class as {@code this} one. + *

+ * + * @return a {@code List} containing {@code Accessible} objects which could + * be accessed by the declared getter methods. + */ + public List subgroups() { + var fields = new ArrayList(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) + || m.getReturnType().isAssignableFrom(getClass())) { + continue; + } + + Group a = null; + + try { + a = (Group) m.invoke(this); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Failed to invoke " + m + " Details: "); + e.printStackTrace(); + } + + /* Ignore null, factor/instance methods returning same accessibles */ + if (a == null || a.getDescriptor().equals(getDescriptor())) { + continue; + } + + fields.add(a); + fields.addAll(a.subgroups()); + + } + + return fields; + + } + + /** + *

+ * Recursively analyses all {@code Group} objects that are identified as + * subgroups to {@code root} (explicitly checks that subgroups exclude + * parents of {@code root}) and chooses those for which an {@code Exporter} + * exists. + *

+ * + * @param root the root group. + * @return a set of unique {@code Group}s objects. + * @see pulse.util.Group.subgroups() + */ + public static Set contents(Group root) { + var contents = root.subgroups().stream().filter(ph -> root.getParent() != ph).collect(Collectors.toSet()); + + for (var it = contents.iterator(); it.hasNext();) { + contents(it.next()).stream().forEach(a -> contents.add(a)); + } + + return contents; + } + + /** + * Searches for a specific {@code Accessible} with a {@code simpleName}. + * + * @see subgroups + * @param simpleName the name of the {@code Accessible}, + * @return the {@code Accessible} object. + */ + public Group access(String simpleName) { + return subgroups().stream().filter(a -> a.getSimpleName().equals(simpleName)).findFirst().get(); + } + + /** + *

+ * Selects only those {@code Accessible}s, the parent of which is + * {@code this}. Note that all {@code Accessible}s are required to + * explicitly adopt children by calling the {@code setParent()} method. + *

+ * + * @return a {@code List} of children that this {@code Accessible} has + * adopted. + * @see subgroups + */ + public List children() { + return subgroups().stream().filter(a -> a.getParent() == this).collect(toList()); + } + + /** + * The same as {@code getSimpleName} in this implementation. + * + * @return the simple name of the declaring class. + * @see getSimpleName() + */ + public String getDescriptor() { + return getClass().getSimpleName(); + } + + /** + * This will generate a simple name for identifying this {@code Accessible}. + * + * @return the simple name of the declaring class. + */ + public String getSimpleName() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/pulse/util/HierarchyListener.java b/src/main/java/pulse/util/HierarchyListener.java index affd18ec..c6c3ef8c 100644 --- a/src/main/java/pulse/util/HierarchyListener.java +++ b/src/main/java/pulse/util/HierarchyListener.java @@ -3,20 +3,18 @@ /** * An hierarchy listener, which listens to any changes happening with the * children of an {@code UpwardsNavigable}. - * + * * @see pulse.util.UpwardsNavigable * */ - public interface HierarchyListener { - /** - * This is invoked by the {@code UpwardsNavigable} when an event resulting in a - * change of the child's property has occurred. - * - * @param property the event data. - */ - - public void onChildPropertyChanged(PropertyEvent property); + /** + * This is invoked by the {@code UpwardsNavigable} when an event resulting + * in a change of the child's property has occurred. + * + * @param property the event data. + */ + public void onChildPropertyChanged(PropertyEvent property); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/ImageUtils.java b/src/main/java/pulse/util/ImageUtils.java index a6c5270f..b059792a 100644 --- a/src/main/java/pulse/util/ImageUtils.java +++ b/src/main/java/pulse/util/ImageUtils.java @@ -14,86 +14,85 @@ public class ImageUtils { - private ImageUtils() { - // intentionally blank - } - - public static ImageIcon loadIcon(String path, int iconSize) { - var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a - // imageIcon - var image = imageIcon.getImage(); // transform it - var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way - return new ImageIcon(newimg); // transform it back - } - - public static ImageIcon loadIcon(String path, int iconSize, Color clr) { - var icon = loadIcon(path, iconSize); - return dye(icon, clr); - } - - /** - * Credit to Marco13 - * (https://stackoverflow.com/questions/21382966/colorize-a-picture-in-java) - */ - - public static BufferedImage dye(BufferedImage image, Color color) { - int w = image.getWidth(); - int h = image.getHeight(); - BufferedImage dyed = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = dyed.createGraphics(); - g.drawImage(image, 0, 0, null); - g.setComposite(AlphaComposite.SrcAtop); - g.setColor(color); - g.fillRect(0, 0, w, h); - g.dispose(); - return dyed; - } - - /** - * Credit to Werner Kvalem Vesterås (https://stackoverflow.com/questions/15053214/converting-an-imageicon-to-a-bufferedimage) - */ - - public static ImageIcon dye(ImageIcon icon, Color color) { - var bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics g = bi.createGraphics(); - // paint the Icon to the BufferedImage. - icon.paintIcon(null, g, 0, 0); - g.dispose(); - var dyedImage = dye(bi, color); - return new ImageIcon(dyedImage); - } - - /** - * Credit to bmauter - * (https://stackoverflow.com/questions/19398238/how-to-mix-two-int-colors-correctly) - */ - - public static Color blend(final Color c1, final Color c2, float ratio) { - if (ratio > 1f) - ratio = 1f; - else if (ratio < 0f) - ratio = 0f; - float iRatio = 1.0f - ratio; - - int i1 = c1.getRGB(); - int i2 = c2.getRGB(); - - int a1 = (i1 >> 24 & 0xff); - int r1 = ((i1 & 0xff0000) >> 16); - int g1 = ((i1 & 0xff00) >> 8); - int b1 = (i1 & 0xff); - - int a2 = (i2 >> 24 & 0xff); - int r2 = ((i2 & 0xff0000) >> 16); - int g2 = ((i2 & 0xff00) >> 8); - int b2 = (i2 & 0xff); - - int a = (int) ((a1 * iRatio) + (a2 * ratio)); - int r = (int) ((r1 * iRatio) + (r2 * ratio)); - int g = (int) ((g1 * iRatio) + (g2 * ratio)); - int b = (int) ((b1 * iRatio) + (b2 * ratio)); - - return new Color(a << 24 | r << 16 | g << 8 | b); - } - -} \ No newline at end of file + private ImageUtils() { + // intentionally blank + } + + public static ImageIcon loadIcon(String path, int iconSize) { + var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a + // imageIcon + var image = imageIcon.getImage(); // transform it + var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way + return new ImageIcon(newimg); // transform it back + } + + public static ImageIcon loadIcon(String path, int iconSize, Color clr) { + var icon = loadIcon(path, iconSize); + return dye(icon, clr); + } + + /** + * Credit to Marco13 + * (https://stackoverflow.com/questions/21382966/colorize-a-picture-in-java) + */ + public static BufferedImage dye(BufferedImage image, Color color) { + int w = image.getWidth(); + int h = image.getHeight(); + BufferedImage dyed = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = dyed.createGraphics(); + g.drawImage(image, 0, 0, null); + g.setComposite(AlphaComposite.SrcAtop); + g.setColor(color); + g.fillRect(0, 0, w, h); + g.dispose(); + return dyed; + } + + /** + * Credit to Werner Kvalem Vesterås + * (https://stackoverflow.com/questions/15053214/converting-an-imageicon-to-a-bufferedimage) + */ + public static ImageIcon dye(ImageIcon icon, Color color) { + var bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics g = bi.createGraphics(); + // paint the Icon to the BufferedImage. + icon.paintIcon(null, g, 0, 0); + g.dispose(); + var dyedImage = dye(bi, color); + return new ImageIcon(dyedImage); + } + + /** + * Credit to bmauter + * (https://stackoverflow.com/questions/19398238/how-to-mix-two-int-colors-correctly) + */ + public static Color blend(final Color c1, final Color c2, float ratio) { + if (ratio > 1f) { + ratio = 1f; + } else if (ratio < 0f) { + ratio = 0f; + } + float iRatio = 1.0f - ratio; + + int i1 = c1.getRGB(); + int i2 = c2.getRGB(); + + int a1 = (i1 >> 24 & 0xff); + int r1 = ((i1 & 0xff0000) >> 16); + int g1 = ((i1 & 0xff00) >> 8); + int b1 = (i1 & 0xff); + + int a2 = (i2 >> 24 & 0xff); + int r2 = ((i2 & 0xff0000) >> 16); + int g2 = ((i2 & 0xff00) >> 8); + int b2 = (i2 & 0xff); + + int a = (int) ((a1 * iRatio) + (a2 * ratio)); + int r = (int) ((r1 * iRatio) + (r2 * ratio)); + int g = (int) ((g1 * iRatio) + (g2 * ratio)); + int b = (int) ((b1 * iRatio) + (b2 * ratio)); + + return new Color(a << 24 | r << 16 | g << 8 | b); + } + +} diff --git a/src/main/java/pulse/util/ImmutableDataEntry.java b/src/main/java/pulse/util/ImmutableDataEntry.java index df8db6f0..7a55587b 100644 --- a/src/main/java/pulse/util/ImmutableDataEntry.java +++ b/src/main/java/pulse/util/ImmutableDataEntry.java @@ -4,50 +4,47 @@ * A {@code DataEntry} is an immutable ordered pair of an instance of {@code T}, * which is considered to be the 'key', and an instance of {@code R}, which is * considered to be the 'value'. - * + * * @param the key * @param the value */ - public class ImmutableDataEntry { - private T key; - private R value; - - /** - * Constructs a new {@code DataEntry} from {@code key} and {@code value}. - * - * @param key the key. - * @param value the value associated with this {@code key}. - */ - - public ImmutableDataEntry(T key, R value) { - this.key = key; - this.value = value; - } - - /** - * Gets the key object - * - * @return the key - */ - - public T getKey() { - return key; - } - - /** - * Gets the value object - * - * @return the value - */ - - public R getValue() { - return value; - } - - @Override - public String toString() { - return "<" + key + " : " + value + ">"; - } -} \ No newline at end of file + private T key; + private R value; + + /** + * Constructs a new {@code DataEntry} from {@code key} and {@code value}. + * + * @param key the key. + * @param value the value associated with this {@code key}. + */ + public ImmutableDataEntry(T key, R value) { + this.key = key; + this.value = value; + } + + /** + * Gets the key object + * + * @return the key + */ + public T getKey() { + return key; + } + + /** + * Gets the value object + * + * @return the value + */ + public R getValue() { + return value; + } + + @Override + public String toString() { + return "<" + key + " : " + value + ">"; + } + +} diff --git a/src/main/java/pulse/util/ImmutablePair.java b/src/main/java/pulse/util/ImmutablePair.java index cd02f293..f264614f 100644 --- a/src/main/java/pulse/util/ImmutablePair.java +++ b/src/main/java/pulse/util/ImmutablePair.java @@ -2,44 +2,47 @@ public class ImmutablePair { - private T anElement; - private T anotherElement; + private T anElement; + private T anotherElement; - public ImmutablePair(T anElement, T anotherElement) { - this.anElement = anElement; - this.anotherElement = anotherElement; - } + public ImmutablePair(T anElement, T anotherElement) { + this.anElement = anElement; + this.anotherElement = anotherElement; + } - public T getFirst() { - return anElement; - } + public T getFirst() { + return anElement; + } - public T getSecond() { - return anotherElement; - } + public T getSecond() { + return anotherElement; + } - @Override - public boolean equals(Object o) { - if (!(o instanceof ImmutablePair)) - return false; + @Override + public boolean equals(Object o) { + if (!(o instanceof ImmutablePair)) { + return false; + } - if (this == o) - return true; + if (this == o) { + return true; + } - var ip = (ImmutablePair) o; + var ip = (ImmutablePair) o; - // direct order - if (this.getFirst().equals(ip.getFirst()) && this.getSecond().equals(ip.getSecond())) - return true; + // direct order + if (this.getFirst().equals(ip.getFirst()) && this.getSecond().equals(ip.getSecond())) { + return true; + } - // reverse order - return this.getFirst().equals(ip.getSecond()) && this.getSecond().equals(ip.getFirst()); + // reverse order + return this.getFirst().equals(ip.getSecond()) && this.getSecond().equals(ip.getFirst()); - } + } - @Override - public int hashCode() { - return anElement.hashCode() + anotherElement.hashCode(); - } + @Override + public int hashCode() { + return anElement.hashCode() + anotherElement.hashCode(); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/PropertyEvent.java b/src/main/java/pulse/util/PropertyEvent.java index cd94c241..70c486a5 100644 --- a/src/main/java/pulse/util/PropertyEvent.java +++ b/src/main/java/pulse/util/PropertyEvent.java @@ -7,55 +7,52 @@ * {@code PropertyHolder}. * */ - public class PropertyEvent { - private Object source; - private PropertyHolder propertyHolder; - private Property property; - - /** - * Constructs an event that has happened because of {@code source}, resulting in - * an action taken on the {@code property}. - * - * @param source the originator of the event - * @param property the object of the event - */ - - public PropertyEvent(Object source, PropertyHolder propertyHolder, Property property) { - this.source = source; - this.property = property; - this.propertyHolder = propertyHolder; - } - - /** - * Gets the 'source', which is an Object that is the originator of this event. - * - * @return - */ - - public Object getSource() { - return source; - } - - /** - * Gets the property, which is related to this event. - * - * @return the related property. - */ - - public Property getProperty() { - return property; - } - - public PropertyHolder getPropertyHolder() { - return propertyHolder; - } - - @Override - public String toString() { - return "Source: " + source.getClass().getSimpleName() + " ; Holder: " - + propertyHolder.getClass().getSimpleName() + " ; Property: " + property; - } - -} \ No newline at end of file + private Object source; + private PropertyHolder propertyHolder; + private Property property; + + /** + * Constructs an event that has happened because of {@code source}, + * resulting in an action taken on the {@code property}. + * + * @param source the originator of the event + * @param property the object of the event + */ + public PropertyEvent(Object source, PropertyHolder propertyHolder, Property property) { + this.source = source; + this.property = property; + this.propertyHolder = propertyHolder; + } + + /** + * Gets the 'source', which is an Object that is the originator of this + * event. + * + * @return + */ + public Object getSource() { + return source; + } + + /** + * Gets the property, which is related to this event. + * + * @return the related property. + */ + public Property getProperty() { + return property; + } + + public PropertyHolder getPropertyHolder() { + return propertyHolder; + } + + @Override + public String toString() { + return "Source: " + source.getClass().getSimpleName() + " ; Holder: " + + propertyHolder.getClass().getSimpleName() + " ; Property: " + property; + } + +} diff --git a/src/main/java/pulse/util/PropertyHolderListener.java b/src/main/java/pulse/util/PropertyHolderListener.java index fbcff235..de5017b8 100644 --- a/src/main/java/pulse/util/PropertyHolderListener.java +++ b/src/main/java/pulse/util/PropertyHolderListener.java @@ -4,16 +4,15 @@ * A listener used by {@code PropertyHolder}s to track changes with the * associated {@code Propert}ies. */ - public interface PropertyHolderListener { - /** - * This event is triggered by any {@code PropertyHolder}, the properties of - * which have been changed. - * - * @param event the event associated with actions taken on a {@code Property}. - */ - - public void onPropertyChanged(PropertyEvent event); + /** + * This event is triggered by any {@code PropertyHolder}, the properties of + * which have been changed. + * + * @param event the event associated with actions taken on a + * {@code Property}. + */ + public void onPropertyChanged(PropertyEvent event); } diff --git a/src/main/java/pulse/util/Reflexive.java b/src/main/java/pulse/util/Reflexive.java index 7dcc41b0..07b1d4b4 100644 --- a/src/main/java/pulse/util/Reflexive.java +++ b/src/main/java/pulse/util/Reflexive.java @@ -10,63 +10,60 @@ * its available subclasses. * */ - public interface Reflexive { - public static List instancesOf(Class reflexiveType, Object... params) { - return instancesOf(reflexiveType, reflexiveType.getPackage().getName(), params); - } - - public static List instancesOf(Class reflexiveType, String pckgname) { - return instancesOf(reflexiveType, pckgname, new Object[0]); - } - - /** - * Uses the {@code ReflexiveFinder} to create a list of simple instance of - * {@code reflexiveType} generated by any classes listed in the package - * {@code pckgname}. - * - * @see ReflexiveFinder.simpleInstances(String) - * @param a class implementing {@code Reflexive} - * @param reflexiveType a class that extends {@code T} - * @param pckgname the String with the package name - * @return a list of {@code Reflexive} conforming with the conditions above. - */ + public static List instancesOf(Class reflexiveType, Object... params) { + return instancesOf(reflexiveType, reflexiveType.getPackage().getName(), params); + } - @SuppressWarnings("unchecked") - public static List instancesOf(Class reflexiveType, String pckgname, Object... params) { - return (List) ReflexiveFinder.simpleInstances(pckgname, params).stream() - .filter(r -> reflexiveType.isAssignableFrom(r.getClass())).collect(Collectors.toList()); - } + public static List instancesOf(Class reflexiveType, String pckgname) { + return instancesOf(reflexiveType, pckgname, new Object[0]); + } - /** - * Uses the {@code ReflexiveFinder} to create a list of simple instance of - * {@code reflexiveType} generated by any classes listed in the same package - * where the {@code reflexiveType} is found. - * - * @see ReflexiveFinder.simpleInstances(String) - * @param a class implementing {@code Reflexive} - * @param reflexiveType a class that extends {@code T} - * @return a list of {@code Reflexive} conforming with the conditions above. - */ + /** + * Uses the {@code ReflexiveFinder} to create a list of simple instance of + * {@code reflexiveType} generated by any classes listed in the package + * {@code pckgname}. + * + * @see ReflexiveFinder.simpleInstances(String) + * @param a class implementing {@code Reflexive} + * @param reflexiveType a class that extends {@code T} + * @param pckgname the String with the package name + * @return a list of {@code Reflexive} conforming with the conditions above. + */ + @SuppressWarnings("unchecked") + public static List instancesOf(Class reflexiveType, String pckgname, Object... params) { + return (List) ReflexiveFinder.simpleInstances(pckgname, params).stream() + .filter(r -> reflexiveType.isAssignableFrom(r.getClass())).collect(Collectors.toList()); + } - public static List instancesOf(Class reflexiveType) { - return Reflexive.instancesOf(reflexiveType, reflexiveType.getPackage().getName()); - } + /** + * Uses the {@code ReflexiveFinder} to create a list of simple instance of + * {@code reflexiveType} generated by any classes listed in the same package + * where the {@code reflexiveType} is found. + * + * @see ReflexiveFinder.simpleInstances(String) + * @param a class implementing {@code Reflexive} + * @param reflexiveType a class that extends {@code T} + * @return a list of {@code Reflexive} conforming with the conditions above. + */ + public static List instancesOf(Class reflexiveType) { + return Reflexive.instancesOf(reflexiveType, reflexiveType.getPackage().getName()); + } - public static T instantiate(Class c, String descriptor) { - var opt = Reflexive.instancesOf(c).stream().filter(test -> test.getDescriptor().equals(descriptor)).findFirst(); - return opt.get(); - } + public static T instantiate(Class c, String descriptor) { + var opt = Reflexive.instancesOf(c).stream().filter(test -> test.getDescriptor().equals(descriptor)).findFirst(); + return opt.get(); + } - public static Set allDescriptors(Class c) { - return Reflexive.instancesOf(c).stream().map(t -> t.getDescriptor()).collect(Collectors.toSet()); - } + public static Set allDescriptors(Class c) { + return Reflexive.instancesOf(c).stream().map(t -> t.getDescriptor()).collect(Collectors.toSet()); + } - public static Set allSubclassesNames(Class c) { - var classes = ReflexiveFinder.classesIn(c.getPackageName()); - return classes.stream().filter(cl -> c.isAssignableFrom(cl) && !Modifier.isAbstract(cl.getModifiers())) - .map(aClass -> aClass.getSimpleName()).collect(Collectors.toSet()); - } + public static Set allSubclassesNames(Class c) { + var classes = ReflexiveFinder.classesIn(c.getPackageName()); + return classes.stream().filter(cl -> c.isAssignableFrom(cl) && !Modifier.isAbstract(cl.getModifiers())) + .map(aClass -> aClass.getSimpleName()).collect(Collectors.toSet()); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/ReflexiveFinder.java b/src/main/java/pulse/util/ReflexiveFinder.java index d052d393..056a7e62 100644 --- a/src/main/java/pulse/util/ReflexiveFinder.java +++ b/src/main/java/pulse/util/ReflexiveFinder.java @@ -23,228 +23,236 @@ * {@code Reflexive} in a {@code PULsE} package. * */ - public class ReflexiveFinder { - - private static Map>> classMap = new HashMap<>(); - private ReflexiveFinder() { - // intentionall blank - } + private static Map>> classMap = new HashMap<>(); - private static List listf(File directory) { + private ReflexiveFinder() { + // intentionall blank + } - var files = new ArrayList(); + private static List listf(File directory) { - // Get all files from a directory. - var fList = directory.listFiles(); + var files = new ArrayList(); - if (fList != null) { + // Get all files from a directory. + var fList = directory.listFiles(); - for (var file : fList) { + if (fList != null) { - if (file.isFile()) - files.add(file); - else if (file.isDirectory()) - files.addAll(listf(file)); + for (var file : fList) { - } + if (file.isFile()) { + files.add(file); + } else if (file.isDirectory()) { + files.addAll(listf(file)); + } - } + } - return files; + } - } + return files; - private static String adjustClassName(String name) { - var result = ""; - if (!name.startsWith(separator)) - result = separatorChar + name; - return result.replace('.', separatorChar); - } + } - private static String initialiseLocationPath() { - String result = null; - try { - result = ReflexiveFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); - } catch (URISyntaxException e) { - System.err.println("Failed to initialise the general path to ReflxeiveFinder"); - e.printStackTrace(); - } - return result; - } + private static String adjustClassName(String name) { + var result = ""; + if (!name.startsWith(separator)) { + result = separatorChar + name; + } + return result.replace('.', separatorChar); + } - private static List> listClassesInDirectory(File root, String pckgname) { - List> classes = new ArrayList<>(); - var files = listf(root); + private static String initialiseLocationPath() { + String result = null; + try { + result = ReflexiveFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); + } catch (URISyntaxException e) { + System.err.println("Failed to initialise the general path to ReflxeiveFinder"); + e.printStackTrace(); + } + return result; + } - files.stream().map(f -> { + private static List> listClassesInDirectory(File root, String pckgname) { + List> classes = new ArrayList<>(); + var files = listf(root); - var pathName = f.getName(); + files.stream().map(f -> { - for (var parent = f.getParentFile(); !parent.equals(root); parent = parent.getParentFile()) { - pathName = parent.getName() + "." + pathName; - } + var pathName = f.getName(); - return pathName; + for (var parent = f.getParentFile(); !parent.equals(root); parent = parent.getParentFile()) { + pathName = parent.getName() + "." + pathName; + } - }).forEach(path -> { - if (path.endsWith(".class")) - try { - classes.add(forName(pckgname + "." + path.substring(0, path.length() - 6))); - } catch (ClassNotFoundException e) { - System.err.println("Failed to find the .class file"); - e.printStackTrace(); - } - }); - - return classes; - } - - private static List> listClassesInJar(String locationPath, String pckgname) { - ZipInputStream zip = null; - List> classes = new ArrayList<>(); - try { - zip = new ZipInputStream(new FileInputStream(locationPath)); - } catch (FileNotFoundException e1) { - System.err.println("Cannt find the main jar file at " + locationPath); - e1.printStackTrace(); - } - - try { - for (var entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - // This ZipEntry represents a class. Now, what class does it represent? - var className = entry.getName().replace('/', '.'); // including ".class" - if (!className.contains(pckgname)) - continue; - classes.add(forName(className.substring(0, className.length() - ".class".length()))); - } - } - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - } - return classes; - } - - /** - * Uses Java Reflection API to find all classes within the package named - * {@code pckgname}. Works well with .jar files. - * - * @param pckgname the name of the package. - * @return a list of {@code Class} objects. - */ - - public static List> classesIn(String pckgname) { - var name = adjustClassName(pckgname); - String locationPath = initialiseLocationPath(); - - var root = new File(locationPath + name); - - return root.isDirectory() ? listClassesInDirectory(root, pckgname) : listClassesInJar(locationPath, pckgname); - } - - @SuppressWarnings("unchecked") - private static V instanceMethod(Class aClass) { - // if the class has a getInstance() method - var methods = aClass.getMethods(); - - for (var method : methods) { - if (method.getName().equals("getInstance")) { - Object o = null; + return pathName; + + }).forEach(path -> { + if (path.endsWith(".class")) try { - o = method.invoke(null, new Object[0]); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - e.printStackTrace(); - } - if (o instanceof Reflexive) - return (V) o; - } - } - return null; - } - - @SuppressWarnings("unchecked") - private static V instanceConstructor(Class aClass, Object... params) { - var ctrs = aClass.getDeclaredConstructors(); - - outer: for (var ctr : ctrs) { - - if (isPublic(ctr.getModifiers())) { - - var types = ctr.getParameterTypes(); - - if (Integer.compare(types.length, params.length) == 0) { - - for (int i = 0; i < types.length; i++) { - if (!types[i].equals(params[i].getClass())) - if (!types[i].isAssignableFrom(params[i].getClass())) - continue outer; - } - - try { - var o = ctr.newInstance(params); - if (o instanceof Reflexive) - return (V) o; - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - e.printStackTrace(); - } - - } - - } - - } - - return null; - - } - - /** - *

- * Finds simple instances of {@code Reflexive} subclasses within - * {@code pckgname}. A simple instance is either one that results from invoking - * a no-argument constructor or a {@code getInstance()} method. - *

- * - * @param a class implementing {@code Reflexive} - * @param pckgname the name of the package for the search - * @return a list of classes implementing {@code Reflexive} that are found in - * {@code pckgname}. - */ - - public static List simpleInstances(String pckgname, Object... params) { - List instances = new ArrayList<>(); - - //generate a class list only once - if(classMap.get(pckgname) == null) - classMap.put( pckgname, classesIn(pckgname) ); - - for (var aClass : classMap.get(pckgname)) { - - if (isAbstract(aClass.getModifiers())) - continue; - - // Try to create an instance of the object - V instance = instanceConstructor(aClass, params); - if (instance != null) - instances.add(instance); - else { - // if the class has a getInstance() method - instance = instanceMethod(aClass); - if (instance != null) - instances.add(instance); - } - - } - - return instances; - - } - - public static List simpleInstances(String pckgname) { - return simpleInstances(pckgname, new Object[0]); - } - -} \ No newline at end of file + classes.add(forName(pckgname + "." + path.substring(0, path.length() - 6))); + } catch (ClassNotFoundException e) { + System.err.println("Failed to find the .class file"); + e.printStackTrace(); + } + }); + + return classes; + } + + private static List> listClassesInJar(String locationPath, String pckgname) { + ZipInputStream zip = null; + List> classes = new ArrayList<>(); + try { + zip = new ZipInputStream(new FileInputStream(locationPath)); + } catch (FileNotFoundException e1) { + System.err.println("Cannt find the main jar file at " + locationPath); + e1.printStackTrace(); + } + + try { + for (var entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (!entry.isDirectory() && entry.getName().endsWith(".class")) { + // This ZipEntry represents a class. Now, what class does it represent? + var className = entry.getName().replace('/', '.'); // including ".class" + if (!className.contains(pckgname)) { + continue; + } + classes.add(forName(className.substring(0, className.length() - ".class".length()))); + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + return classes; + } + + /** + * Uses Java Reflection API to find all classes within the package named + * {@code pckgname}. Works well with .jar files. + * + * @param pckgname the name of the package. + * @return a list of {@code Class} objects. + */ + public static List> classesIn(String pckgname) { + var name = adjustClassName(pckgname); + String locationPath = initialiseLocationPath(); + + var root = new File(locationPath + name); + + return root.isDirectory() ? listClassesInDirectory(root, pckgname) : listClassesInJar(locationPath, pckgname); + } + + @SuppressWarnings("unchecked") + private static V instanceMethod(Class aClass) { + // if the class has a getInstance() method + var methods = aClass.getMethods(); + + for (var method : methods) { + if (method.getName().equals("getInstance")) { + Object o = null; + try { + o = method.invoke(null, new Object[0]); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + } + if (o instanceof Reflexive) { + return (V) o; + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static V instanceConstructor(Class aClass, Object... params) { + var ctrs = aClass.getDeclaredConstructors(); + + outer: + for (var ctr : ctrs) { + + if (isPublic(ctr.getModifiers())) { + + var types = ctr.getParameterTypes(); + + if (Integer.compare(types.length, params.length) == 0) { + + for (int i = 0; i < types.length; i++) { + if (!types[i].equals(params[i].getClass())) { + if (!types[i].isAssignableFrom(params[i].getClass())) { + continue outer; + } + } + } + + try { + var o = ctr.newInstance(params); + if (o instanceof Reflexive) { + return (V) o; + } + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + e.printStackTrace(); + } + + } + + } + + } + + return null; + + } + + /** + *

+ * Finds simple instances of {@code Reflexive} subclasses within + * {@code pckgname}. A simple instance is either one that results from + * invoking a no-argument constructor or a {@code getInstance()} method. + *

+ * + * @param a class implementing {@code Reflexive} + * @param pckgname the name of the package for the search + * @return a list of classes implementing {@code Reflexive} that are found + * in {@code pckgname}. + */ + public static List simpleInstances(String pckgname, Object... params) { + List instances = new ArrayList<>(); + + //generate a class list only once + if (classMap.get(pckgname) == null) { + classMap.put(pckgname, classesIn(pckgname)); + } + + for (var aClass : classMap.get(pckgname)) { + + if (isAbstract(aClass.getModifiers())) { + continue; + } + + // Try to create an instance of the object + V instance = instanceConstructor(aClass, params); + if (instance != null) { + instances.add(instance); + } else { + // if the class has a getInstance() method + instance = instanceMethod(aClass); + if (instance != null) { + instances.add(instance); + } + } + + } + + return instances; + + } + + public static List simpleInstances(String pckgname) { + return simpleInstances(pckgname, new Object[0]); + } + +} diff --git a/src/main/java/pulse/util/ResourceMonitor.java b/src/main/java/pulse/util/ResourceMonitor.java index a78e3b0f..b7d1ab12 100644 --- a/src/main/java/pulse/util/ResourceMonitor.java +++ b/src/main/java/pulse/util/ResourceMonitor.java @@ -12,109 +12,106 @@ import javax.management.ReflectionException; /** - * Provides unified means of storage and methods of access to runtime system information, - * such as CPU usage, memory usage, an number of available threads. + * Provides unified means of storage and methods of access to runtime system + * information, such as CPU usage, memory usage, an number of available threads. * */ - public class ResourceMonitor { - private double memoryUsage; - private double cpuUsage; - private int threadsAvailable; - - private static ResourceMonitor instance = new ResourceMonitor(); - - private ResourceMonitor() { - threadsAvailable(); - } - - public void update() { - cpuUsage(); - memoryUsage(); - } - - /** - *

- * This will calculate the ratio {@code totalMemory/maxMemory} using the - * standard {@code Runtime}. Note this memory usage depends on heap allocation - * for the JVM. - *

- * - */ - - public void memoryUsage() { - final double totalMemory = getRuntime().totalMemory(); - final double maxMemory = getRuntime().maxMemory(); - memoryUsage = (totalMemory / maxMemory * 100); - } - - /** - *

- * This will calculate the CPU load for the machine running {@code PULsE}. Note - * this is rather code-intensive, so it is recommended for use only at certain - * time intervals. - *

- * - */ - - public void cpuUsage() { - - var mbs = getPlatformMBeanServer(); - ObjectName name = null; - try { - name = ObjectName.getInstance("java.lang:type=OperatingSystem"); - } catch (MalformedObjectNameException | NullPointerException e1) { - err.println("Error while calculating CPU usage:"); - e1.printStackTrace(); - } - - AttributeList list = null; - try { - list = mbs.getAttributes(name, new String[] { "ProcessCpuLoad" }); - } catch (InstanceNotFoundException | ReflectionException e) { - err.println("Error while calculating CPU usage:"); - e.printStackTrace(); - } - - if (!list.isEmpty()) { - - var att = (Attribute) list.get(0); - var value = (double) att.getValue(); - - cpuUsage = value < 0 ? 0 : (value * 100); - - } - - } - - /** - * Finds the number of threads available for calculation. This will be used by - * the {@code TaskManager} when allocating the {@code ForkJoinPool} for running - * several tasks in parallel. The number of threads is greater or equal to the number of - * cores - * @see pulse.tasks.TaskManager - */ - - public void threadsAvailable() { - final int number = getRuntime().availableProcessors(); - threadsAvailable = number > 1 ? (number - 1) : 1; - } - - public double getCpuUsage() { - return cpuUsage; - } - - public int getThreadsAvailable() { - return threadsAvailable; - } - - public double getMemoryUsage() { - return memoryUsage; - } - - public static ResourceMonitor getInstance() { - return instance; - } - -} \ No newline at end of file + private double memoryUsage; + private double cpuUsage; + private int threadsAvailable; + + private static ResourceMonitor instance = new ResourceMonitor(); + + private ResourceMonitor() { + threadsAvailable(); + } + + public void update() { + cpuUsage(); + memoryUsage(); + } + + /** + *

+ * This will calculate the ratio {@code totalMemory/maxMemory} using the + * standard {@code Runtime}. Note this memory usage depends on heap + * allocation for the JVM. + *

+ * + */ + public void memoryUsage() { + final double totalMemory = getRuntime().totalMemory(); + final double maxMemory = getRuntime().maxMemory(); + memoryUsage = (totalMemory / maxMemory * 100); + } + + /** + *

+ * This will calculate the CPU load for the machine running {@code PULsE}. + * Note this is rather code-intensive, so it is recommended for use only at + * certain time intervals. + *

+ * + */ + public void cpuUsage() { + + var mbs = getPlatformMBeanServer(); + ObjectName name = null; + try { + name = ObjectName.getInstance("java.lang:type=OperatingSystem"); + } catch (MalformedObjectNameException | NullPointerException e1) { + err.println("Error while calculating CPU usage:"); + e1.printStackTrace(); + } + + AttributeList list = null; + try { + list = mbs.getAttributes(name, new String[]{"ProcessCpuLoad"}); + } catch (InstanceNotFoundException | ReflectionException e) { + err.println("Error while calculating CPU usage:"); + e.printStackTrace(); + } + + if (!list.isEmpty()) { + + var att = (Attribute) list.get(0); + var value = (double) att.getValue(); + + cpuUsage = value < 0 ? 0 : (value * 100); + + } + + } + + /** + * Finds the number of threads available for calculation. This will be used + * by the {@code TaskManager} when allocating the {@code ForkJoinPool} for + * running several tasks in parallel. The number of threads is greater or + * equal to the number of cores + * + * @see pulse.tasks.TaskManager + */ + public void threadsAvailable() { + final int number = getRuntime().availableProcessors(); + threadsAvailable = number > 1 ? (number - 1) : 1; + } + + public double getCpuUsage() { + return cpuUsage; + } + + public int getThreadsAvailable() { + return threadsAvailable; + } + + public double getMemoryUsage() { + return memoryUsage; + } + + public static ResourceMonitor getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index f8590805..7a3079c5 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -16,103 +16,103 @@ *

* */ - public abstract class UpwardsNavigable implements Descriptive { - private UpwardsNavigable parent; - private List listeners = new ArrayList(); - - public void removeHierarchyListeners() { - this.listeners.clear(); - } - - public void removeHierarchyListener(HierarchyListener l) { - this.listeners.remove(l); - } - - public void addHierarchyListener(HierarchyListener l) { - this.listeners.add(l); - } - - public List getHierarchyListeners() { - return listeners; - } - - /** - * Recursively informs the parent, the parent of its parent, etc. of this - * {@code UpwardsNavigable} that an action has been taken on its child's - * properties specified by {@e}. - * - * @param e the property event - */ - - public void tellParent(PropertyEvent e) { - if (parent != null) { - parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); - parent.tellParent(e); - } - } - - /** - * Return the parent of this {@code UpwardsNavigable} -- if is has been - * previously explicitly set. - * - * @return the parent (which is also an {@code UpwardsNavigable}). - */ - - public UpwardsNavigable getParent() { - return parent; - } - - /** - * Finds an ancestor that looks similar to {@code aClass} by recursively calling - * {@code getParent()}. - * - * @param aClass a class which should be similar to an ancestor of this - * {@code UpwardsNavigable} - * @return the ancestor, which is a parent, or grand-parent, or - * grand-grand-parent, etc. of this {@code UpwardsNavigable}. - */ - - public UpwardsNavigable specificAncestor(Class aClass) { - if(aClass.equals(this.getClass())) - return this; - var parent = this.getParent(); - UpwardsNavigable result = null; - if(parent != null) - result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); - return result; - } - - /** - * Explicitly sets the parent of this {@code UpwardsNavigable}. - * - * @param parent the new parent that will adopt this {@code UpwardsNavigable}. - */ - - public void setParent(UpwardsNavigable parent) { - this.parent = parent; - } - - /** - * Retrieves the Identifier of the SearchTaks this UpwardsNavigable belongs to. - * @return the identifier of the SearchTask - */ - - public Identifier identify() { - var un = specificAncestor(SearchTask.class); - return un == null ? null : ((SearchTask) un).getIdentifier(); - } - - /** - * Uses the SearchTask id (if present) to describe this UpwardsNavigable. - */ - - @Override - public String describe() { - var id = identify(); - String name = getClass().getSimpleName(); - return id == null ? name : name + "_" + id.getValue(); - } - -} \ No newline at end of file + private UpwardsNavigable parent; + private List listeners = new ArrayList(); + + public void removeHierarchyListeners() { + this.listeners.clear(); + } + + public void removeHierarchyListener(HierarchyListener l) { + this.listeners.remove(l); + } + + public void addHierarchyListener(HierarchyListener l) { + this.listeners.add(l); + } + + public List getHierarchyListeners() { + return listeners; + } + + /** + * Recursively informs the parent, the parent of its parent, etc. of this + * {@code UpwardsNavigable} that an action has been taken on its child's + * properties specified by { + * + * @e}. + * + * @param e the property event + */ + public void tellParent(PropertyEvent e) { + if (parent != null) { + parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + parent.tellParent(e); + } + } + + /** + * Return the parent of this {@code UpwardsNavigable} -- if is has been + * previously explicitly set. + * + * @return the parent (which is also an {@code UpwardsNavigable}). + */ + public UpwardsNavigable getParent() { + return parent; + } + + /** + * Finds an ancestor that looks similar to {@code aClass} by recursively + * calling {@code getParent()}. + * + * @param aClass a class which should be similar to an ancestor of this + * {@code UpwardsNavigable} + * @return the ancestor, which is a parent, or grand-parent, or + * grand-grand-parent, etc. of this {@code UpwardsNavigable}. + */ + public UpwardsNavigable specificAncestor(Class aClass) { + if (aClass.equals(this.getClass())) { + return this; + } + var parent = this.getParent(); + UpwardsNavigable result = null; + if (parent != null) { + result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); + } + return result; + } + + /** + * Explicitly sets the parent of this {@code UpwardsNavigable}. + * + * @param parent the new parent that will adopt this + * {@code UpwardsNavigable}. + */ + public void setParent(UpwardsNavigable parent) { + this.parent = parent; + } + + /** + * Retrieves the Identifier of the SearchTaks this UpwardsNavigable belongs + * to. + * + * @return the identifier of the SearchTask + */ + public Identifier identify() { + var un = specificAncestor(SearchTask.class); + return un == null ? null : ((SearchTask) un).getIdentifier(); + } + + /** + * Uses the SearchTask id (if present) to describe this UpwardsNavigable. + */ + @Override + public String describe() { + var id = identify(); + String name = getClass().getSimpleName(); + return id == null ? name : name + "_" + id.getValue(); + } + +} diff --git a/src/main/java/pulse/util/package-info.java b/src/main/java/pulse/util/package-info.java index f5616328..c1429066 100644 --- a/src/main/java/pulse/util/package-info.java +++ b/src/main/java/pulse/util/package-info.java @@ -2,5 +2,4 @@ * Contains abstract data and hierarchical structures that implement much of the * Java Reflection API. */ - -package pulse.util; \ No newline at end of file +package pulse.util; From f20790874216f8408b45e5d17b01cf139676c63d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 09:49:41 +0100 Subject: [PATCH 070/116] Improving logic of baseline classes and of the parameter-setting Create a separate abstract class for horizontally-adjustable baselines. The FlatBaseline and LinearBaseline now both inherit from this class. The reason to do this was a logical error when the LinearBaseline extends a FlatBaseline and therefore it is a FlatBaseline (although it isn't). Included a fitToNegative method in the Baseline class accepting AbstractData arguments. This is specifically needed to calculate the baseline of e.g. pulse diode data, which can't be treated neither as a HeatingCurve nor as an ExperimentalData. Made an override of the listedKeywords() method instead of listedTypes() Removed the constant values from the optimisationVector() method and replaced explicitly setting bounds in this method by a call to the overriden set(int, double, NumericPropertyKeyword) method from ParameterVector, which takes the default bounds for the keyword from the default loaded set of parameters. Intoduced a utility boundsFrom method to the Segment.class to extract the bounds from a default instance of the NumericProperty corresponding to a given NumericPropertyKeyword --- .../pulse/baseline/AdjustableBaseline.java | 119 +++++++ src/main/java/pulse/baseline/Baseline.java | 188 +++++----- .../java/pulse/baseline/FlatBaseline.java | 198 +++-------- .../java/pulse/baseline/LinearBaseline.java | 325 +++++++++-------- .../pulse/baseline/SinusoidalBaseline.java | 327 +++++++++--------- src/main/java/pulse/math/ParameterVector.java | 20 +- src/main/java/pulse/math/Segment.java | 14 + 7 files changed, 631 insertions(+), 560 deletions(-) create mode 100644 src/main/java/pulse/baseline/AdjustableBaseline.java diff --git a/src/main/java/pulse/baseline/AdjustableBaseline.java b/src/main/java/pulse/baseline/AdjustableBaseline.java new file mode 100644 index 00000000..bb69ca91 --- /dev/null +++ b/src/main/java/pulse/baseline/AdjustableBaseline.java @@ -0,0 +1,119 @@ +package pulse.baseline; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; +import java.util.List; +import java.util.Set; + +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.properties.Flag; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.util.PropertyHolder; + +/** + * A baseline that can shift in the vertical direction. + * + * @author Artem Lunev + */ +public abstract class AdjustableBaseline extends Baseline { + + private double intercept; + + /** + * Creates a flat baseline equal to the argument. + * + * @param intercept the constant baseline value. + */ + public AdjustableBaseline(double intercept) { + this.intercept = intercept; + } + + /** + * @return the constant value of this {@code FlatBaseline} + */ + @Override + public double valueAt(double x) { + return intercept; + } + + protected double mean(List x) { + double sum = x.stream().reduce((a, b) -> a + b).get(); + return sum / x.size(); + } + + /** + * Provides getter accessibility to the intercept as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_INTERCEPT where the value is set to that + * of {@code slope} + */ + public NumericProperty getIntercept() { + return derive(BASELINE_INTERCEPT, intercept); + } + + /** + * Checks whether {@code intercept} is a baseline intercept property and + * updates the respective value of this baseline. + * + * @param intercept a {@code NumericProperty} of the + * {@code BASELINE_INTERCEPT} type + * @see set + */ + public void setIntercept(NumericProperty intercept) { + requireType(intercept, BASELINE_INTERCEPT); + this.intercept = (double) intercept.getValue(); + firePropertyChanged(this, intercept); + } + + /** + * Lists the {@code intercept} as accessible property for this + * {@code FlatBaseline}. + * + * @see PropertyHolder + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_INTERCEPT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == BASELINE_INTERCEPT) { + setIntercept(property); + this.firePropertyChanged(this, property); + } + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == BASELINE_INTERCEPT) { + output.set(i, intercept, key); + } + + } + + } + + @Override + public void assign(ParameterVector params) { + for (int i = 0, size = params.dimension(); i < size; i++) { + + if (params.getIndex(i) == BASELINE_INTERCEPT) { + setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); + } + + } + + } + +} diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index fc225cb8..20c2ab94 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import pulse.AbstractData; import pulse.input.ExperimentalData; import pulse.input.IndexRange; @@ -19,91 +20,114 @@ * in time (either before or after the laser pulse). The baseline parameters can * be modified within an optimisation loop, hence there are two abstract methods * to implement that functionality. - * + * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask * @see pulse.math.ParameterVector */ - public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { - public abstract Baseline copy(); - - /** - * Calculates the baseline at the given position. - * - * @param x the position on the profile (e.g., the time value) - * @return the baseline value - */ - - public abstract double valueAt(double x); - - /** - * Calculates the baseline parameters based on input arguments. - *

- * This usually runs a simple least-squares estimation of the parameters of this - * baseline using the specified {@code data} within the time range - * {@code rangeMin < t < rangeMax}. If no data is available, the method will NOT - * change the baseline parameters. Upon completion, the - * method will use the respective {@code set} methods of this class to update - * the parameter values, triggering whatever events are associated with them. - *

- * - * @param x a list of independent variable values - * @param y a list of dependent variable values - * @param size the size of the region - */ - - protected abstract void doFit(List x, List y, int size); - - /** - * Selects part of the {@code data} that can be used for baseline estimation - * (typically, this means selecting 'negative' time values and the corresponding - * signal) data and runs the fitting algorithms, - * - * @param data the experimental data - * @param rangeMin the minimum of the time range - * @param rangeMax the maximum of the time range - */ - - public void fitTo(ExperimentalData data, double rangeMin, double rangeMax) { - var indexRange = data.getIndexRange(); - - Objects.requireNonNull(indexRange); - - if (!indexRange.isValid()) - throw new IllegalArgumentException("Index range not valid: " + indexRange); - - List x = new ArrayList<>(); - List y = new ArrayList<>(); - - int size = 0; - - for (int i = IndexRange.closestLeft(rangeMin, data.getTimeSequence()) + 1, max = min(indexRange.getLowerBound(), - IndexRange.closestRight(rangeMax, data.getTimeSequence())); i < max; i++, size++) { - - x.add(data.timeAt(i)); - y.add(data.signalAt(i)); - - } - - if (size > 0) // do fitting only if data is present - doFit(x, y, size); - - } - - /** - * Calls {@code fitTo} using the default time range for the data: - * {@code -Infinity < t < ZERO_LEFT}, where the upper bound is - * a small negative constant. - * - * @param data the experimental data stretching to negative time values - * @see fitTo(ExperimentalData,double,double) - */ - - public void fitTo(ExperimentalData data) { - final double ZERO_LEFT = -1E-5; - fitTo(data, NEGATIVE_INFINITY, ZERO_LEFT); - } - -} \ No newline at end of file + public abstract Baseline copy(); + + /** + * Calculates the baseline at the given position. + * + * @param x the position on the profile (e.g., the time value) + * @return the baseline value + */ + public abstract double valueAt(double x); + + /** + * Calculates the baseline parameters based on input arguments. + *

+ * This usually runs a simple least-squares estimation of the parameters of + * this baseline using the specified {@code data} within the time range + * {@code rangeMin < t < rangeMax}. If no data is available, the method will + * NOT change the baseline parameters. Upon completion, the method will use + * the respective {@code set} methods of this class to update the parameter + * values, triggering whatever events are associated with them. + *

+ * + * @param x a list of independent variable values + * @param y a list of dependent variable values + * @param size the size of the region + */ + protected abstract void doFit(List x, List y, int size); + + /** + * Selects part of the {@code data} that can be used for baseline estimation + * (typically, this means selecting 'negative' time values and the + * corresponding signal) data and runs the fitting algorithms, + * + * @param data the experimental data + * @param rangeMin the minimum of the time range + * @param rangeMax the maximum of the time range + */ + public void fitTo(ExperimentalData data, double rangeMin, double rangeMax) { + var indexRange = data.getIndexRange(); + + Objects.requireNonNull(indexRange); + + if (!indexRange.isValid()) { + throw new IllegalArgumentException("Index range not valid: " + indexRange); + } + + List x = new ArrayList<>(); + List y = new ArrayList<>(); + + int size = 0; + + for (int i = IndexRange.closestLeft(rangeMin, data.getTimeSequence()) + 1, max = min(indexRange.getLowerBound(), + IndexRange.closestRight(rangeMax, data.getTimeSequence())); i < max; i++, size++) { + + x.add(data.timeAt(i)); + y.add(data.signalAt(i)); + + } + + if (size > 0) // do fitting only if data is present + { + doFit(x, y, size); + } + + } + + /** + * Fit to an abstract set of data, using only the subset corresponding to the negative time range. + * @param data a dataset + */ + + public void fitNegative(AbstractData data) { + final int MIN_POINTS = 15; + + var time = data.getTimeSequence(); + var signal = data.getSignalData(); + + var subsetTime = new ArrayList(); + var subsetSignal = new ArrayList(); + + int i; + + for(i = 0; time.get(i) < 0; i++) { + subsetTime.add(time.get(i)); + subsetSignal.add(signal.get(i)); + } + + if(i > MIN_POINTS) + doFit(subsetTime, subsetSignal, i); + } + + /** + * Calls {@code fitTo} using the default time range for the data: + * {@code -Infinity < t < ZERO_LEFT}, where the upper bound is a small + * negative constant. + * + * @param data the experimental data stretching to negative time values + * @see fitTo(ExperimentalData,double,double) + */ + public void fitTo(ExperimentalData data) { + final double ZERO_LEFT = -1E-5; + fitTo(data, NEGATIVE_INFINITY, ZERO_LEFT); + } + +} diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 8eea0bad..56e1d7cc 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -1,152 +1,64 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package pulse.baseline; import static java.lang.String.format; +import java.util.List; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.util.PropertyHolder; - /** - * A simple constant baseline with no slope. The intercept value can be used as - * an optimisation variable. - * + * A flat baseline. + * @author Artem Lunev */ -public class FlatBaseline extends Baseline { - - private double intercept; - - /** - * A primitive constructor, which initialises a {@code CONSTANT} baseline with - * zero intercept and slope. - */ - - public FlatBaseline() { - // intentionally blank - } - - /** - * Creates a flat baseline equal to the argument. - * - * @param intercept the constant baseline value. - */ - - public FlatBaseline(double intercept) { - this.intercept = intercept; - } - - /** - * @return the constant value of this {@code FlatBaseline} - */ - - @Override - public double valueAt(double x) { - return intercept; - } - - @Override - protected void doFit(List x, List y, int size) { - intercept = mean(y); - set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); - } - - protected double mean(List x) { - double sum = x.stream().reduce( (a, b) -> a + b).get(); - return sum / x.size(); - } - - /** - * Provides getter accessibility to the intercept as a NumericProperty - * - * @return a NumericProperty derived from - * NumericPropertyKeyword.BASELINE_INTERCEPT where the value is set to - * that of {@code slope} - */ - - public NumericProperty getIntercept() { - return derive(BASELINE_INTERCEPT, intercept); - } - - /** - * Checks whether {@code intercept} is a baseline intercept property and updates - * the respective value of this baseline. - * - * @param intercept a {@code NumericProperty} of the {@code BASELINE_INTERCEPT} - * type - * @see set - */ - - public void setIntercept(NumericProperty intercept) { - requireType(intercept, BASELINE_INTERCEPT); - this.intercept = (double) intercept.getValue(); - firePropertyChanged(this, intercept); - } - - /** - * Lists the {@code intercept} as accessible property for this - * {@code FlatBaseline}. - * - * @see PropertyHolder - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(getIntercept())); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f", intercept); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == BASELINE_INTERCEPT) { - setIntercept(property); - this.firePropertyChanged(this, property); - } - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - if (key == BASELINE_INTERCEPT) { - output.set(i, intercept); - output.setParameterBounds(i, new Segment(-10, 10)); - } - - } - - } - - @Override - public void assign(ParameterVector params) { - for (int i = 0, size = params.dimension(); i < size; i++) { - - if (params.getIndex(i) == BASELINE_INTERCEPT) - setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); - - } - - } - - @Override - public Baseline copy() { - return new FlatBaseline(this.intercept); - } - -} \ No newline at end of file +public class FlatBaseline extends AdjustableBaseline { + + /** + * A primitive constructor, which initialises a {@code CONSTANT} baseline + * with zero intercept and slope. + */ + public FlatBaseline() { + this(0.0); + } + + /** + * Creates a flat baseline equal to the argument. + * + * @param intercept the constant baseline value. + */ + public FlatBaseline(double intercept) { + super(intercept); + } + + + @Override + protected void doFit(List x, List y, int size) { + double intercept = mean(y); + set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); + } + + @Override + public Baseline copy() { + return new FlatBaseline((double)getIntercept().getValue()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); + } + +} diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index 35b90890..c6381ebc 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -3,17 +3,17 @@ import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; /** * A linear {@code Baseline} which specifies the {@code intercept} and @@ -24,172 +24,163 @@ * associated with the {@code intercept} and {@code slope} parameters can be * used as fitting variables. *

- * + * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask * @see pulse.math.ParameterVector */ - -public class LinearBaseline extends FlatBaseline { - - private double slope; - - /** - * A primitive constructor, which initialises a {@code CONSTANT} baseline with - * zero intercept and slope. - */ - - public LinearBaseline() { - super(); - } - - /** - * A constructor, which allows to specify all three parameters in one go. - * - * @param intercept the intercept is the value of the Baseline's linear function - * at {@code x = 0} - * @param slope the slope determines the inclination angle of the Baseline's - * graph. - */ - - public LinearBaseline(double intercept, double slope) { - super(intercept); - this.slope = slope; - } - - /** - * Calculates the linear function {@code g(x) = intercept + slope*time} - * - * @param x the argument of the linear function - * @return the result of this simple calculation - */ - - @Override - public double valueAt(double x) { - final double intercept = (double) getIntercept().getValue(); - return intercept + x * slope; - } - - @Override - protected void doFit(List x, List y, int size) { - double meanx = mean(x); - double meany = mean(y); - - double x1; - double y1; - double xxbar = 0.0; - double xybar = 0.0; - - for (int i = 0; i < size; i++) { - x1 = x.get(i); - y1 = y.get(i); - xxbar += (x1 - meanx) * (x1 - meanx); - xybar += (x1 - meanx) * (y1 - meany); - } - - slope = xybar / xxbar; - double intercept = meany - slope * meanx; - - set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); - set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); - } - - /** - * Provides getter accessibility to the slope as a NumericProperty - * - * @return a NumericProperty derived from NumericPropertyKeyword.BASELINE_SLOPE - * with a value equal to slop - */ - - public NumericProperty getSlope() { - return derive(BASELINE_SLOPE, slope); - } - - /** - * Checks whether {@code slope} is a baseline slope property and updates the - * respective value of this baseline. - * - * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type - * @see set - */ - - public void setSlope(NumericProperty slope) { - requireType(slope, BASELINE_SLOPE); - this.slope = (double) slope.getValue(); - firePropertyChanged(this, slope); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == BASELINE_SLOPE) { - setSlope(property); - this.firePropertyChanged(this, property); - } - - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - if (key == BASELINE_SLOPE) { - output.set(i, slope); - output.setParameterBounds(i, new Segment(1E-4, 1E4)); - } - - } - - } - - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @see listedTypes() - */ - - @Override - public void assign(ParameterVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - if (params.getIndex(i) == BASELINE_SLOPE) - setSlope(derive(BASELINE_SLOPE, params.get(i))); - - } - - } - - /** - * @return a list containing {@code BASELINE_INTERCEPT} and - * {@code BASELINE_SLOPE} properties - */ - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(getSlope()); - return list; - } - - @Override - public Baseline copy() { - return new LinearBaseline((double)this.getIntercept().getValue(), this.slope); - } - - -} \ No newline at end of file +public class LinearBaseline extends AdjustableBaseline { + + private double slope; + + /** + * A primitive constructor, which initialises a {@code CONSTANT} baseline + * with zero intercept and slope. + */ + public LinearBaseline() { + super(0.0); + } + + /** + * A constructor, which allows to specify all three parameters in one go. + * + * @param intercept the intercept is the value of the Baseline's linear + * function at {@code x = 0} + * @param slope the slope determines the inclination angle of the Baseline's + * graph. + */ + public LinearBaseline(double intercept, double slope) { + super(intercept); + this.slope = slope; + } + + /** + * Calculates the linear function {@code g(x) = intercept + slope*time} + * + * @param x the argument of the linear function + * @return the result of this simple calculation + */ + @Override + public double valueAt(double x) { + final double intercept = (double) getIntercept().getValue(); + return intercept + x * slope; + } + + @Override + protected void doFit(List x, List y, int size) { + double meanx = mean(x); + double meany = mean(y); + + double x1; + double y1; + double xxbar = 0.0; + double xybar = 0.0; + + for (int i = 0; i < size; i++) { + x1 = x.get(i); + y1 = y.get(i); + xxbar += (x1 - meanx) * (x1 - meanx); + xybar += (x1 - meanx) * (y1 - meany); + } + + slope = xybar / xxbar; + double intercept = meany - slope * meanx; + + set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); + set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); + } + + /** + * Provides getter accessibility to the slope as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop + */ + public NumericProperty getSlope() { + return derive(BASELINE_SLOPE, slope); + } + + /** + * Checks whether {@code slope} is a baseline slope property and updates the + * respective value of this baseline. + * + * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type + * @see set + */ + public void setSlope(NumericProperty slope) { + requireType(slope, BASELINE_SLOPE); + this.slope = (double) slope.getValue(); + firePropertyChanged(this, slope); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " = " + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == BASELINE_SLOPE) { + setSlope(property); + this.firePropertyChanged(this, property); + } + + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == BASELINE_SLOPE) { + output.set(i, slope, BASELINE_SLOPE); + } + + } + + } + + /** + * Assigns parameter values of this {@code Problem} using the optimisation + * vector {@code params}. Only those parameters will be updated, the types + * of which are listed as indices in the {@code params} vector. + * + * @param params the optimisation vector, containing a similar set of + * parameters to this {@code Problem} + * @see listedTypes() + */ + @Override + public void assign(ParameterVector params) { + super.assign(params); + + for (int i = 0, size = params.dimension(); i < size; i++) { + + if (params.getIndex(i) == BASELINE_SLOPE) { + setSlope(derive(BASELINE_SLOPE, params.get(i))); + } + + } + + } + + /** + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_SLOPE); + return set; + } + + @Override + public Baseline copy() { + return new LinearBaseline((double) this.getIntercept().getValue(), this.slope); + } + +} diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index 1b62a077..c55ec0ed 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,7 +1,6 @@ package pulse.baseline; import static java.lang.Math.sin; -import static pulse.math.transforms.StandardTransformations.SQRT; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -10,13 +9,14 @@ import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * A simple sinusoidal baseline. @@ -31,162 +31,167 @@ *

* */ - -public class SinusoidalBaseline extends FlatBaseline { - - private double frequency; - private double phaseShift; - private double amplitude; - private final static double _2PI = 2.0 * Math.PI; - - /** - * Creates a sinusoidal baseline with default properties. - */ - - public SinusoidalBaseline() { - super(); - setFrequency(def(BASELINE_FREQUENCY)); - setAmplitude(def(BASELINE_AMPLITUDE)); - setPhaseShift(def(BASELINE_PHASE_SHIFT)); - } - - @Override - public double valueAt(double x) { - var intercept = (double) getIntercept().getValue(); - return intercept + amplitude * sin(_2PI * (x * frequency + phaseShift)); - } - - /** - * Listed properties include the frequency, amplitude, phase shift, and - * intercept. - */ - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(BASELINE_FREQUENCY)); - list.add(def(BASELINE_AMPLITUDE)); - list.add(def(BASELINE_PHASE_SHIFT)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - - switch (type) { - case BASELINE_FREQUENCY: - setFrequency(property); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(property); - break; - case BASELINE_AMPLITUDE: - setAmplitude(property); - break; - default: - super.set(type, property); - } - - } - - public NumericProperty getFrequency() { - return derive(BASELINE_FREQUENCY, frequency); - } - - public NumericProperty getAmplitude() { - return derive(BASELINE_AMPLITUDE, amplitude); - } - - public NumericProperty getPhaseShift() { - return derive(BASELINE_PHASE_SHIFT, phaseShift); - } - - public void setFrequency(NumericProperty frequency) { - requireType(frequency, BASELINE_FREQUENCY); - this.frequency = (double) frequency.getValue(); - firePropertyChanged(this, frequency); - } - - public void setAmplitude(NumericProperty amplitude) { - requireType(amplitude, BASELINE_AMPLITUDE); - this.amplitude = (double) amplitude.getValue(); - firePropertyChanged(this, amplitude); - } - - public void setPhaseShift(NumericProperty phaseShift) { - requireType(phaseShift, BASELINE_PHASE_SHIFT); - this.phaseShift = (double) phaseShift.getValue(); - firePropertyChanged(this, phaseShift); - } - - /** - * The optimisation vector can include the amplitude, frequency and phase shift of a sinusoid, and - * a baseline intercept value of the superclass. - */ - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case BASELINE_FREQUENCY: - output.set(i, frequency); - output.setParameterBounds(i, new Segment(0, 200)); - break; - case BASELINE_PHASE_SHIFT: - output.set(i, phaseShift); - output.setParameterBounds(i, new Segment(-3.14, 3.14) ); - break; - case BASELINE_AMPLITUDE: - output.setTransform(i, SQRT); - output.set(i, amplitude); - output.setParameterBounds(i, new Segment( 0.0, 10.0 ) ); - break; - default: - break; - } - - } - - } - - @Override - public void assign(ParameterVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - switch (params.getIndex(i)) { - case BASELINE_FREQUENCY: - setFrequency(derive(BASELINE_FREQUENCY, params.get(i))); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.get(i))); - break; - case BASELINE_AMPLITUDE: - setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i) )); - break; - default: - break; - } - - } - - } - - @Override - public Baseline copy() { - var baseline = new SinusoidalBaseline(); - baseline.setIntercept(this.getIntercept()); - baseline.amplitude = this.amplitude; - baseline.frequency = this.frequency; - baseline.phaseShift = this.phaseShift; - return baseline; - } - - -} \ No newline at end of file +public class SinusoidalBaseline extends AdjustableBaseline { + + private double frequency; + private double phaseShift; + private double amplitude; + private final static double _2PI = 2.0 * Math.PI; + + /** + * Creates a sinusoidal baseline with default properties. + */ + public SinusoidalBaseline() { + super(0.0); + setFrequency(def(BASELINE_FREQUENCY)); + setAmplitude(def(BASELINE_AMPLITUDE)); + setPhaseShift(def(BASELINE_PHASE_SHIFT)); + } + + @Override + public double valueAt(double x) { + var intercept = (double) getIntercept().getValue(); + return intercept + amplitude * sin(_2PI * x * frequency + phaseShift); + } + + /** + * Listed properties include the frequency, amplitude, phase shift, and + * intercept. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_FREQUENCY); + set.add(BASELINE_AMPLITUDE); + set.add(BASELINE_PHASE_SHIFT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case BASELINE_FREQUENCY: + setFrequency(property); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(property); + break; + case BASELINE_AMPLITUDE: + setAmplitude(property); + break; + default: + super.set(type, property); + } + + } + + public NumericProperty getFrequency() { + return derive(BASELINE_FREQUENCY, frequency); + } + + public NumericProperty getAmplitude() { + return derive(BASELINE_AMPLITUDE, amplitude); + } + + public NumericProperty getPhaseShift() { + return derive(BASELINE_PHASE_SHIFT, phaseShift); + } + + public void setFrequency(NumericProperty frequency) { + requireType(frequency, BASELINE_FREQUENCY); + this.frequency = (double) frequency.getValue(); + firePropertyChanged(this, frequency); + } + + public void setAmplitude(NumericProperty amplitude) { + requireType(amplitude, BASELINE_AMPLITUDE); + this.amplitude = (double) amplitude.getValue(); + firePropertyChanged(this, amplitude); + } + + public void setPhaseShift(NumericProperty phaseShift) { + requireType(phaseShift, BASELINE_PHASE_SHIFT); + this.phaseShift = (double) phaseShift.getValue(); + firePropertyChanged(this, phaseShift); + } + + /** + * The optimisation vector can include the amplitude, frequency and phase + * shift of a sinusoid, and a baseline intercept value of the superclass. + */ + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case BASELINE_FREQUENCY: + output.set(i, frequency, BASELINE_FREQUENCY); + break; + case BASELINE_PHASE_SHIFT: + output.set(i, phaseShift, BASELINE_PHASE_SHIFT); + break; + case BASELINE_AMPLITUDE: + output.set(i, amplitude, BASELINE_AMPLITUDE); + break; + default: + continue; + } + + output.setTransform(i, new StickTransform(output.getParameterBounds(i))); + + } + + } + + @Override + public void assign(ParameterVector params) { + super.assign(params); + + for (int i = 0, size = params.dimension(); i < size; i++) { + + switch (params.getIndex(i)) { + case BASELINE_FREQUENCY: + setFrequency(derive(BASELINE_FREQUENCY, params.inverseTransform(i))); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.inverseTransform(i))); + break; + case BASELINE_AMPLITUDE: + setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i))); + break; + default: + break; + } + + } + + } + + @Override + public Baseline copy() { + var baseline = new SinusoidalBaseline(); + baseline.setIntercept(this.getIntercept()); + baseline.amplitude = this.amplitude; + baseline.frequency = this.frequency; + baseline.phaseShift = this.phaseShift; + return baseline; + } + + @Override + protected void doFit(List x, List y, int size) { + var flatBaseline = new FlatBaseline(); + flatBaseline.doFit(x, y, size); + //TODO Fourier transform + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 22242cc7..ca0cb49b 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -6,7 +6,6 @@ import pulse.math.linear.Vector; import pulse.math.transforms.Transformable; -import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperties; import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; @@ -78,16 +77,23 @@ private ParameterVector(final int n) { bounds = new Segment[n]; } - /** - * Applies the corresponding transformation (defined by the respective - * {@code Transformable}) -- if present, and sets the result of this - * transformation to the ith component of this - * {@code ParameterVector}. - */ @Override public void set(final int i, final double x) { set(i, x, false); } + + /** + * Sets the i-th parameter value to {@code x} without applying the + * transform. Sets the bound for this value as the default bound for {@code key}. + * @param i the index of the parameter + * @param x value to be set + * @param key type of property + */ + + public void set(final int i, final double x, NumericPropertyKeyword key) { + set(i, x); + setParameterBounds(i, Segment.boundsFrom(key)); + } /** * Sets the i-component of this vector to {@code x} or diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index 5d0e1b49..9691ab34 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -1,6 +1,8 @@ package pulse.math; import java.util.Random; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericProperties.def; /** * A {@code Segment} is simply a pair of values {@code a} and {@code b} such @@ -32,6 +34,18 @@ public Segment(Segment segment) { this.a = segment.a; this.b = segment.b; } + + /** + * Creates a segment representing the bounds of {@code p}, i.e. the range + * in which the property value is allowed to change + * @param p a property keyword to extract default bounds + * @return a {@code Segment} with the bounds + */ + + public static Segment boundsFrom(NumericPropertyKeyword p) { + return new Segment(def(p).getMinimum().doubleValue(), + def(p).getMaximum().doubleValue()); + } /** * Gets the {@code a} value for this {@code Segment} From edb92fa25e580683167191faf3a6be420a54dd5b Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 10:07:30 +0100 Subject: [PATCH 071/116] Improvements to handling of experimental datasets ExperimentalData now stores a value of the half-time, which is used for accurate calculation of the Range bounds. The half-time is updated using a DataLsistener addead to each new ExperimentalData object. The Range objects will now perform checks when asked to set a new minimum or maximum, by verifying that the new value is contained within a dynamically calculated range interval, which is based on the half-time value of the main experimental dataset. Removed pop-up windows appearing (uncontrollably) when the half-time calculation failed, the output stream is now re-directed to System.err. Replaced cubic normal splines in InterpolatorDataset with Akima splines. This is not ideal, but at least will yield better results to datasets with rapidly varying second derivatives. In future, this needs to be replaced by a procedure producing uniformly spaced data to which LOESS interpolation is applied. Changes listedTypes in Metadata.java to listedKeyword(), which reduces the complexity of the code. --- .../java/pulse/input/ExperimentalData.java | 791 +++++++++--------- .../pulse/input/InterpolationDataset.java | 230 +++-- src/main/java/pulse/input/Metadata.java | 457 +++++----- src/main/java/pulse/input/Range.java | 449 +++++----- 4 files changed, 977 insertions(+), 950 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 3c1a9be6..3b2e5bd4 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import pulse.AbstractData; @@ -31,399 +32,401 @@ * {@code CurveReader}s. Any manipulation (e.g. truncation) of the data triggers * an event associated with this {@code ExperimentalData}. */ - public class ExperimentalData extends AbstractData { - private Metadata metadata; - private IndexRange indexRange; - private Range range; - private List dataListeners; - - /** - * This is the cutoff factor which is used as a criterion for data truncation. - * Described in Lunev, A., & Heymer, R. (2020). Review of Scientific - * Instruments, 91(6), 064902. - */ - - public final static double CUTOFF_FACTOR = 7.2; - - /** - * The binning factor used to build a crude approximation of the heating curve. - * Described in Lunev, A., & Heymer, R. (2020). Review of Scientific - * Instruments, 91(6), 064902. - */ - - public final static int REDUCTION_FACTOR = 32; - - /** - * A fail-safe factor. - */ - - public final static double FAIL_SAFE_FACTOR = 3.0; - - private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); - - /** - * Constructs an {@code ExperimentalData} object using the superclass - * constructor and creating a new list of data listeners. The number of points is set to zero by default, - * and a new {@code IndexRange} is initialized. - * - */ - - public ExperimentalData() { - super(); - dataListeners = new ArrayList<>(); - setPrefix("RawData"); - setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(); - } - - public void addDataListener(DataListener listener) { - dataListeners.add(listener); - } - - public void clearDataListener() { - dataListeners.clear(); - } - - public void fireDataChanged(DataEvent dataEvent) { - dataListeners.stream().forEach(l -> l.onDataChanged(dataEvent)); - } - - /** - * Calls reset for both the {@code IndexRange} and {@code Range} objects using - * the current time sequence. - * - * @see pulse.input.Range.reset() - * @see pulse.input.IndexRange.reset() - */ - - public void resetRanges() { - indexRange.reset(getTimeSequence()); - range.reset(indexRange, getTimeSequence()); - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("Experimental data "); - if (metadata.getSampleName() != null) - sb.append("for " + metadata.getSampleName() + " "); - sb.append("(" + metadata.numericProperty(TEST_TEMPERATURE).formattedOutput() + ")"); - return sb.toString(); - } - - /** - * Adds {@code time} and {@code temperature} to the respective {@code List}s. Increments the counter of points. - * Note that no baseline correction is performed. - * - * @param time the next time value - * @param signal the next signal value - */ - - @Override - public void addPoint(double time, double signal) { - super.addPoint(time, signal); - incrementCount(); - } - - /** - * Constructs a deliberately crude representation of this heating curve by - * calculating a running average. - *

- * This is done using a binning algorithm, which will group the time-temperature - * data associated with this {@code ExperimentalData} in - * {@code count/reductionFactor - 1} bins, calculate the average value for time - * and temperature within each bin, and collect those values in a - * {@code List}. This is useful to cancel out the effect of signal - * outliers, e.g. when calculating the half-rise time. - *

- * - * The algorithm is described in more detail in Lunev, A., & Heymer, R. - * (2020). Review of Scientific Instruments, 91(6), 064902. - * - * @param reductionFactor the factor, by which the number of points - * {@code count} will be reduced for this - * {@code ExperimentalData}. - * @return a {@code List}, representing the degraded - * {@code ExperimentalData}. - * @see halfRiseTime() - * @see pulse.AbstractData.maxTemperature() - */ - - public List runningAverage(int reductionFactor) { - - int count = (int) getNumPoints().getValue(); - - List crudeAverage = new ArrayList<>(count / reductionFactor); - - int start = indexRange.getLowerBound(); - int end = indexRange.getUpperBound(); - - int step = (end - start) / (count / reductionFactor); - double av = 0; - - int i1, i2; - - for (int i = 0, max = (count / reductionFactor) - 1; i < max; i++) { - i1 = start + step * i; - i2 = i1 + step; - - av = 0; - - for (int j = i1; j < i2; j++) - av += signalAt(j); - - av /= step; - - crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); - - } - - return crudeAverage; - - } - - /** - * Instead of returning the simple maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the maximum of the - * {@code runningAverage} using the default reduction factor - * {@value REDUCTION_FACTOR}. - * - * @see pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) - */ - - public Point2D maxAdjustedSignal() { - var degraded = runningAverage(REDUCTION_FACTOR); - return max(degraded, pointComparator); - } - - /** - * Calculates the approximate half-rise time used for crude estimation of - * thermal diffusivity. - *

- * This uses the {@code runningAverage} method by applying the default reduction - * factor of {@value REDUCTION_FACTOR}. The calculation is based on finding the - * approximate value corresponding to the half-maximum of the temperature. The - * latter is calculated using the running average curve. The index corresponding - * to the closest temperature value available for that curve is used to retrieve - * the half-rise time (which also has the same index). If this fails, i.e. the - * associated index is less than 1, this will print out a warning message and - * still return a value equal to the acquisition time divided by a fail-safe - * factor {@value FAIL_SAFE_FACTOR}. - *

- * - * @return A double, representing the half-rise time (in seconds). - */ - - public double halfRiseTime() { - var degraded = runningAverage(REDUCTION_FACTOR); - var max = (max(degraded, pointComparator)); - var baseline = new FlatBaseline(); - baseline.fitTo(this); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - - int cutoffIndex = degraded.indexOf(max); - degraded = degraded.subList(0, cutoffIndex); - - int index = IndexRange.closestLeft(halfMax, - degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - - if (index < 1) { - System.out.println(Messages.getString("ExperimentalData.HalfRiseError")); - return max(getTimeSequence()) / FAIL_SAFE_FACTOR; - } - - return degraded.get(index).getX(); - - } - - /** - * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. - * - * @return the linked {@code Metadata} - */ - - public Metadata getMetadata() { - return metadata; - } - - @Override - public boolean equals(Object o) { - if(!super.equals(o)) - return false; - - if (!(o instanceof ExperimentalData)) - return false; - - var other = (ExperimentalData) o; - return this.metadata.equals(other.getMetadata()); - } - - /** - * Checks if the acquisition time used to collect this {@code ExperimentalData} - * is sensible. - *

- * The acquisition time is essentially the last element in the - * {@code time List}. By default, it is deemed sensible if that last element is - * less than {@value CUTOFF_FACTOR}*{@code halfRiseTime}. - *

- * - * @return {@code true} if the acquisition time is below the truncation - * threshold, {@code false} otherwise. - */ - - public boolean isAcquisitionTimeSensible() { - final double halfMaximum = halfRiseTime(); - final double cutoff = CUTOFF_FACTOR * halfMaximum; - final int count = (int) getNumPoints().getValue(); - return getTimeSequence().get(count - 1) < cutoff; - } - - /** - * Truncates the {@code range} and {@code indexRange} of this - * {@code ExperimentalData} above a certain threshold, NOT removing any data - * elements. - *

- * The threshold is calculated based on the {@code halfRiseTime} value and is - * set by default to {@value CUTOFF_FACTOR}*{@code halfRiseTime}. A - * {@code DataEvent} will be created and passed to the {@code dataListeners} (if - * any) with the {@code DataEventType.TRUNCATED} as argument. - *

- * - * @see halfRiseTime - * @see DataEvent - * @see fireDataChanged - */ - - public void truncate() { - final double halfMaximum = halfRiseTime(); - final double cutoff = CUTOFF_FACTOR * halfMaximum; - - this.range.setUpperBound(derive(UPPER_BOUND, cutoff));; - } - - /** - * Sets a new {@code Metadata} object for this {@code ExperimentalData}. - *

- * The {@code pulseWidth} property recorded in {@code Metadata} will be used to - * set the time range for the reverse problem solution. Whenever this property - * is changed in the {@code metadata}, a listener will ensure an updated range - * is used. - *

- * - * @param metadata the new Metadata object - * @see PropertyHolderListener - */ - - public void setMetadata(Metadata metadata) { - this.metadata = metadata; - metadata.setParent(this); - doSetMetadata(); - } - - private void doSetMetadata() { - - if (range != null) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - - metadata.addListener(event -> { - - if (event.getProperty() instanceof NumericProperty) { - var p = (NumericProperty) event.getProperty(); - - if (p.getType() == PULSE_WIDTH) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - - } - - }); - - } - - /** - * Gets the time sequence element corresponding to the lower bound of the index - * range - * - * @return the time (in seconds) associated with - * {@code indexRange.getLowerBound()} - */ - - public double getEffectiveStartTime() { - return getTimeSequence().get(indexRange.getLowerBound()); - } - - /** - * Gets the time sequence element corresponding to the upper bound of the index - * range - * - * @return the time (in seconds) associated with - * {@code indexRange.getUpperBound()} - */ - - public double getEffectiveEndTime() { - return getTimeSequence().get(indexRange.getUpperBound()); - } - - /** - * Gets the dimensional time {@code Range} of this data. - * - * @return the range - */ - - public Range getRange() { - return range; - } - - /** - * Gets the index range of this data. - * - * @return the index range - */ - - public IndexRange getIndexRange() { - return indexRange; - } - - /** - * Sets the range, assigning {@code this} to its parent, and forcing changes to - * the {@code indexRange}. - * - * @param range the range - */ - - public void setRange(Range range) { - this.range = range; - range.setParent(this); - doSetRange(); - } - - private void doSetRange() { - var time = getTimeSequence(); - indexRange.set(time, range); - - addHierarchyListener(l -> { - if (l.getSource() == range) { - indexRange.set(time, range); - this.fireDataChanged(new DataEvent(DataEventType.RANGE_CHANGED, this)); - } - }); - - if (metadata != null) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - } - - /** - * Retrieves the - * - * @see pulse.problem.schemes.DifferenceScheme - * @return a double, equal to the last element of the {@code time List}. - */ - - @Override - public double timeLimit() { - return timeAt(indexRange.getUpperBound()); - } - -} \ No newline at end of file + private Metadata metadata; + private IndexRange indexRange; + private Range range; + private List dataListeners; + private double halfTime; + + /** + * This is the cutoff factor which is used as a criterion for data + * truncation. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + public final static double CUTOFF_FACTOR = 7.2; + + /** + * The binning factor used to build a crude approximation of the heating + * curve. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + public final static int REDUCTION_FACTOR = 32; + + /** + * A fail-safe factor. + */ + public final static double FAIL_SAFE_FACTOR = 3.0; + + private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + + /** + * Constructs an {@code ExperimentalData} object using the superclass + * constructor and creating a new list of data listeners. The number of + * points is set to zero by default, and a new {@code IndexRange} is + * initialized. + * + */ + public ExperimentalData() { + super(); + dataListeners = new ArrayList<>(); + setPrefix("RawData"); + setNumPoints(derive(NUMPOINTS, 0)); + indexRange = new IndexRange(); + this.addDataListener((DataEvent e) -> calculateHalfTime() ); + + } + + public void addDataListener(DataListener listener) { + dataListeners.add(listener); + } + + public void clearDataListener() { + dataListeners.clear(); + } + + public void fireDataChanged(DataEvent dataEvent) { + dataListeners.stream().forEach(l -> l.onDataChanged(dataEvent)); + } + + /** + * Calls reset for both the {@code IndexRange} and {@code Range} objects + * using the current time sequence. + * + * @see pulse.input.Range.reset() + * @see pulse.input.IndexRange.reset() + */ + public void resetRanges() { + indexRange.reset(getTimeSequence()); + range.reset(indexRange, getTimeSequence()); + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Experimental data "); + if (metadata.getSampleName() != null) { + sb.append("for " + metadata.getSampleName() + " "); + } + sb.append("(").append(metadata.numericProperty(TEST_TEMPERATURE).formattedOutput()).append(")"); + return sb.toString(); + } + + /** + * Adds {@code time} and {@code temperature} to the respective + * {@code List}s. Increments the counter of points. Note that no baseline + * correction is performed. + * + * @param time the next time value + * @param signal the next signal value + */ + @Override + public void addPoint(double time, double signal) { + super.addPoint(time, signal); + incrementCount(); + } + + /** + * Constructs a deliberately crude representation of this heating curve by + * calculating a running average. + *

+ * This is done using a binning algorithm, which will group the + * time-temperature data associated with this {@code ExperimentalData} in + * {@code count/reductionFactor - 1} bins, calculate the average value for + * time and temperature within each bin, and collect those values in a + * {@code List}. This is useful to cancel out the effect of signal + * outliers, e.g. when calculating the half-rise time. + *

+ * + * The algorithm is described in more detail in Lunev, A., & Heymer, R. + * (2020). Review of Scientific Instruments, 91(6), 064902. + * + * @param reductionFactor the factor, by which the number of points + * {@code count} will be reduced for this {@code ExperimentalData}. + * @return a {@code List}, representing the degraded + * {@code ExperimentalData}. + * @see halfRiseTime() + * @see pulse.AbstractData.maxTemperature() + */ + public List runningAverage(int reductionFactor) { + + int count = (int) getNumPoints().getValue(); + + List crudeAverage = new ArrayList<>(count / reductionFactor); + + int start = indexRange.getLowerBound(); + int end = indexRange.getUpperBound(); + + int step = (end - start) / (count / reductionFactor); + double av = 0; + + int i1, i2; + + for (int i = 0, max = (count / reductionFactor) - 1; i < max; i++) { + i1 = start + step * i; + i2 = i1 + step; + + av = 0; + + for (int j = i1; j < i2; j++) { + av += signalAt(j); + } + + av /= step; + + crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); + + } + + return crudeAverage; + + } + + /** + * Instead of returning the simple maximum (which can be an outlier!) of the + * temperature, this overriden method calculates the maximum of the + * {@code runningAverage} using the default reduction factor + * {@value REDUCTION_FACTOR}. + * + * @return a {@code Point2D} object containing the coordinates of the + * adjusted maximum. + * @see + * pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) + */ + public Point2D maxAdjustedSignal() { + var degraded = runningAverage(REDUCTION_FACTOR); + return max(degraded, pointComparator); + } + + /** + * Calculates the approximate half-rise time used for crude estimation of + * thermal diffusivity. + *

+ * This uses the {@code runningAverage} method by applying the default + * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based + * on finding the approximate value corresponding to the half-maximum of the + * temperature. The latter is calculated using the running average curve. + * The index corresponding to the closest temperature value available for + * that curve is used to retrieve the half-rise time (which also has the + * same index). If this fails, i.e. the associated index is less than 1, + * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe factor + * {@value FAIL_SAFE_FACTOR}. + *

+ * @see getHalfTime() + */ + public void calculateHalfTime() { + var degraded = runningAverage(REDUCTION_FACTOR); + var max = (max(degraded, pointComparator)); + var baseline = new FlatBaseline(); + baseline.fitTo(this); + + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + + int cutoffIndex = degraded.indexOf(max); + degraded = degraded.subList(0, cutoffIndex); + + int index = IndexRange.closestLeft(halfMax, + degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); + + if (index < 1) { + System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); + halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; + } + else + halfTime = degraded.get(index).getX(); + + } + + /** + * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. + * + * @return the linked {@code Metadata} + */ + public Metadata getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + + if (!(o instanceof ExperimentalData)) { + return false; + } + + var other = (ExperimentalData) o; + return this.metadata.equals(other.getMetadata()); + } + + /** + * Checks if the acquisition time used to collect this + * {@code ExperimentalData} is sensible. + *

+ * The acquisition time is essentially the last element in the + * {@code time List}. By default, it is deemed sensible if that last element + * is less than {@value CUTOFF_FACTOR}*{@code halfRiseTime}. + *

+ * + * @return {@code true} if the acquisition time is below the truncation + * threshold, {@code false} otherwise. + */ + public boolean isAcquisitionTimeSensible() { + final double cutoff = CUTOFF_FACTOR * halfTime; + final int count = (int) getNumPoints().getValue(); + double d = getTimeSequence().get(count - 1); + return getTimeSequence().get(count - 1) < cutoff; + } + + /** + * Truncates the {@code range} and {@code indexRange} of this + * {@code ExperimentalData} above a certain threshold, NOT removing any data + * elements. + *

+ * The threshold is calculated based on the {@code halfRiseTime} value and + * is set by default to {@value CUTOFF_FACTOR}*{@code halfRiseTime}. A + * {@code DataEvent} will be created and passed to the {@code dataListeners} + * (if any) with the {@code DataEventType.TRUNCATED} as argument. + *

+ * + * @see halfRiseTime + * @see DataEvent + * @see fireDataChanged + */ + public void truncate() { + final double cutoff = CUTOFF_FACTOR * halfTime; + this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); + } + + /** + * Sets a new {@code Metadata} object for this {@code ExperimentalData}. + *

+ * The {@code pulseWidth} property recorded in {@code Metadata} will be used + * to set the time range for the reverse problem solution. Whenever this + * property is changed in the {@code metadata}, a listener will ensure an + * updated range is used. + *

+ * + * @param metadata the new Metadata object + * @see PropertyHolderListener + */ + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + metadata.setParent(this); + doSetMetadata(); + } + + private void doSetMetadata() { + + if (range != null) { + range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); + } + + metadata.addListener(event -> { + + if (event.getProperty() instanceof NumericProperty) { + var p = (NumericProperty) event.getProperty(); + + if (p.getType() == PULSE_WIDTH) { + range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); + } + + } + + }); + + } + + /** + * Retrieves the half-time value of this dataset, which is equal to the + * time needed to reach half of the signal maximum. + * @return the half-time value. + */ + + public double getHalfTime() { + return halfTime; + } + + /** + * Gets the time sequence element corresponding to the lower bound of the + * index range + * + * @return the time (in seconds) associated with + * {@code indexRange.getLowerBound()} + */ + public double getEffectiveStartTime() { + return getTimeSequence().get(indexRange.getLowerBound()); + } + + /** + * Gets the time sequence element corresponding to the upper bound of the + * index range + * + * @return the time (in seconds) associated with + * {@code indexRange.getUpperBound()} + */ + public double getEffectiveEndTime() { + return getTimeSequence().get(indexRange.getUpperBound()); + } + + /** + * Gets the dimensional time {@code Range} of this data. + * + * @return the range + */ + public Range getRange() { + return range; + } + + /** + * Gets the index range of this data. + * + * @return the index range + */ + public IndexRange getIndexRange() { + return indexRange; + } + + /** + * Sets the range, assigning {@code this} to its parent, and forcing changes + * to the {@code indexRange}. + * + * @param range the range + */ + public void setRange(Range range) { + this.range = range; + range.setParent(this); + doSetRange(); + } + + private void doSetRange() { + var time = getTimeSequence(); + indexRange.set(time, range); + + addHierarchyListener(l -> { + if (l.getSource() == range) { + indexRange.set(time, range); + this.fireDataChanged(new DataEvent(DataEventType.RANGE_CHANGED, this)); + } + }); + + if (metadata != null) { + range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); + } + } + + /** + * Retrieves the + * + * @see pulse.problem.schemes.DifferenceScheme + * @return a double, equal to the last element of the {@code time List}. + */ + @Override + public double timeLimit() { + return timeAt(indexRange.getUpperBound()); + } + +} diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 2146532c..63c98270 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -10,7 +10,7 @@ import java.util.Map; import org.apache.commons.math3.analysis.UnivariateFunction; -import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; import pulse.input.listeners.ExternalDatasetListener; import pulse.properties.NumericPropertyKeyword; @@ -23,130 +23,124 @@ * 'keys'. This is used mainly to interpolate between available data for thermal * properties loaded in tabular representation, e.g. the density and specific * heat tables. Features a static list of {@code ExternalDatasetListener}s. + * * @see pulse.input.listeners.ExternalDatasetListener */ - public class InterpolationDataset { - private UnivariateFunction interpolation; - private List> dataset; - private static Map standartDatasets = new HashMap(); - private static List listeners = new ArrayList<>();; + private UnivariateFunction interpolation; + private List> dataset; + private static Map standartDatasets = new HashMap(); + private static List listeners = new ArrayList<>(); /** * Creates an empty {@code InterpolationDataset}. */ public InterpolationDataset() { - dataset = new ArrayList<>(); - } - - /** - * Provides an interpolated value at {@code key} based on the available data in - * the {@code DataEntry List}. The interpolation is done using natural cubic - * splines, hence it is important that the input noise is minimal. - * - * @param key the argument, at which interpolation needs to be done (e.g. - * temperature) - * @return a double, representing the interpolated value - */ - - public double interpolateAt(double key) { - return interpolation.value(key); - - } - - /** - * Adds {@code entry} to this {@code InterpolationDataset}. - * - * @param entry the entry to be added - */ - - public void add(ImmutableDataEntry entry) { - dataset.add(entry); - } - - /** - * Constructs a new spline interpolator and uses the available dataset to - * produce a {@code SplineInterpolation}. - */ - - public void doInterpolation() { - var interpolator = new SplineInterpolator(); - interpolation = interpolator.interpolate(dataset.stream().map(a -> a.getKey()).mapToDouble(d -> d).toArray(), - dataset.stream().map(a -> a.getValue()).mapToDouble(d -> d).toArray()); - } - - /** - * Extracts all data available in this {@code InterpolationDataset}. - * - * @return the {@code List} of data. - */ - - public List> getData() { - return dataset; - } - - /** - * Retrieves a standard dataset previously loaded by the respective reader. - * - * @param type the standard dataset type - * @return an {@code InterpolationDataset} corresponding to {@code type} - */ - - public static InterpolationDataset getDataset(StandartType type) { - return standartDatasets.get(type); - } - - /** - * Puts a datset specified by {@code type} into the static hash map of this - * class, using {@code type} as key. Triggers {@code onDensityDataLoaded} - * - * @param dataset a dataset to be appended to the static hash map - * @param type the dataset type - */ - - public static void setDataset(InterpolationDataset dataset, StandartType type) { - standartDatasets.put(type, dataset); - listeners.stream().forEach(l -> l.onDataLoaded(type)); - } - - /** - * Creates a list of property keywords that can be derived with help of the loaded data. - * For example, if heat capacity and density data is available, the returned list will contain - * {@code CONDUCTIVITY}. - * @return - */ - - public static List derivableProperties() { - var list = new ArrayList(); - if(standartDatasets.containsKey(StandartType.HEAT_CAPACITY)) - list.add(SPECIFIC_HEAT); - if(standartDatasets.containsKey(StandartType.DENSITY)) - list.add(DENSITY); - if(list.contains(SPECIFIC_HEAT) && list.contains(DENSITY)) - list.add(CONDUCTIVITY); - return list; - } - - public static void addListener(ExternalDatasetListener l) { - listeners.add(l); - } - - public enum StandartType { - - /** - * A keyword for the heat capacity dataset (in J/kg/K). - */ - - HEAT_CAPACITY, - - /** - * A keyword for the density dataset (in kg/m3). - */ - - DENSITY; - - } - -} \ No newline at end of file + dataset = new ArrayList<>(); + } + + /** + * Provides an interpolated value at {@code key} based on the available data + * in the {@code DataEntry List}. The interpolation is done using natural + * cubic splines, hence it is important that the input noise is minimal. + * + * @param key the argument, at which interpolation needs to be done (e.g. + * temperature) + * @return a double, representing the interpolated value + */ + public double interpolateAt(double key) { + return interpolation.value(key); + + } + + /** + * Adds {@code entry} to this {@code InterpolationDataset}. + * + * @param entry the entry to be added + */ + public void add(ImmutableDataEntry entry) { + dataset.add(entry); + } + + /** + * Constructs a new spline interpolator and uses the available dataset to + * produce a {@code SplineInterpolation}. + */ + public void doInterpolation() { + var interpolator = new AkimaSplineInterpolator(); + interpolation = interpolator.interpolate(dataset.stream().map(a -> a.getKey()).mapToDouble(d -> d).toArray(), + dataset.stream().map(a -> a.getValue()).mapToDouble(d -> d).toArray()); + } + + /** + * Extracts all data available in this {@code InterpolationDataset}. + * + * @return the {@code List} of data. + */ + public List> getData() { + return dataset; + } + + /** + * Retrieves a standard dataset previously loaded by the respective reader. + * + * @param type the standard dataset type + * @return an {@code InterpolationDataset} corresponding to {@code type} + */ + public static InterpolationDataset getDataset(StandartType type) { + return standartDatasets.get(type); + } + + /** + * Puts a datset specified by {@code type} into the static hash map of this + * class, using {@code type} as key. Triggers {@code onDensityDataLoaded} + * + * @param dataset a dataset to be appended to the static hash map + * @param type the dataset type + */ + public static void setDataset(InterpolationDataset dataset, StandartType type) { + standartDatasets.put(type, dataset); + listeners.stream().forEach(l -> l.onDataLoaded(type)); + } + + /** + * Creates a list of property keywords that can be derived with help of the + * loaded data. For example, if heat capacity and density data is available, + * the returned list will contain {@code CONDUCTIVITY}. + * + * @return + */ + public static List derivableProperties() { + var list = new ArrayList(); + if (standartDatasets.containsKey(StandartType.HEAT_CAPACITY)) { + list.add(SPECIFIC_HEAT); + } + if (standartDatasets.containsKey(StandartType.DENSITY)) { + list.add(DENSITY); + } + if (list.contains(SPECIFIC_HEAT) && list.contains(DENSITY)) { + list.add(CONDUCTIVITY); + } + return list; + } + + public static void addListener(ExternalDatasetListener l) { + listeners.add(l); + } + + public enum StandartType { + + /** + * A keyword for the heat capacity dataset (in J/kg/K). + */ + HEAT_CAPACITY, + /** + * A keyword for the density dataset (in kg/m3). + */ + DENSITY; + + } + +} diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index de0c8d5f..89e80301 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -39,234 +39,233 @@ *

* */ - public class Metadata extends PropertyHolder implements Reflexive { - private Set data; - private SampleName sampleName; - private int externalID; - - private InstanceDescriptor pulseDescriptor = new InstanceDescriptor( - "Pulse Shape Selector", PulseTemporalShape.class); - - private NumericPulseData pulseData; - - /** - * Creates a {@code Metadata} with the specified parameters and a default - * rectangular pulse shape. Properties are stored in a {@code TreeSet}. - * - * @param temperature the NumericProperty of the type - * {@code NumericPropertyKeyword.TEST_TEMPERATURE} - * @param externalId an integer, specifying the external ID recorded by the - * experimental setup. - */ - - public Metadata(NumericProperty temperature, int externalId) { - sampleName = new SampleName(); - setExternalID(externalId); - pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); - data = new TreeSet(); - set(TEST_TEMPERATURE, temperature); - } - - /** - * Gets the external ID usually specified in the experimental files. Note this - * is not a {@code NumericProperty} - * - * @return an integer, representing the external ID - */ - - public int getExternalID() { - return externalID; - } - - /** - * Sets the external ID in this {@code Metadata} to {@code externalId} - * - * @param externalId the value of the external ID - */ - - private void setExternalID(int externalId) { - this.externalID = externalId; - } - - /** - * Retrieves the pulse shape recorded in this {@code Metadata} - * - * @return a {@code PulseShape} object - */ - - public InstanceDescriptor getPulseDescriptor() { - return pulseDescriptor; - } - - /** - * Retrieves the sample name. This name is used to create directories when - * exporting the data and also to fill the legend when plotting. - * - * @return the sample name - */ - - public SampleName getSampleName() { - return sampleName; - } - - /** - * Sets the sample name property. - * - * @param sampleName the sample name - */ - - public void setSampleName(SampleName sampleName) { - this.sampleName = sampleName; - } - - public void setPulseData(NumericPulseData pulseData) { - this.pulseData = pulseData; - } - - /** - * If a Numerical Pulse has been loaded (for example, when importing from Proteus), this will return - * an object describing this data. - */ - - public NumericPulseData getPulseData() { - return pulseData; - } - - /** - * Searches the internal list of this class for a property with the {@code key} - * type. - * - * @return if present, returns a property belonging to this {@code Metadata} - * with the specified type, otherwise return null. - */ - - @Override - public NumericProperty numericProperty(NumericPropertyKeyword key) { - var optional = data.stream().filter(p -> p.getType() == key).findFirst(); - return optional.isPresent() ? optional.get() : null; - } - - /** - * If {@code type} is listed by this {@code Metadata}, will attempt to either - * set a value to the property belonging to this {@code Metadata} and identified - * by {@code type} or add {@code property} to the internal repository of this - * {@code Metadata}. Triggers {@code firePropertyChanged} upon successful - * completion. - * - * @param type the type to be searched for - * @param property a property with the type specified by its first argument. The - * value of this property will be used to update its counterpart - * in this {@code Metadata}. The signature of this method is - * dictated by the use of Reflection API. - * @throws IllegalArgumentException if the types of the arguments do not match - * or if {@code} property is not a listed - * parameter - * @see PropertyHolder.isListedParameter() - * @see PropertyHolder.firePropertyChanged() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - - if (type != property.getType() || !isListedParameter(property)) - return; //ingore unrecognised properties - - var optional = numericProperty(type); - - if (optional != null) - optional.setValue((Number) property.getValue()); - else - data.add(property); - - firePropertyChanged(this, property); - - } - - /** - * The listed types include {@code TEST_TEMPERATURE}, {@code THICKNESS}, {@code DIAMETER}, {@code PULSE_WIDTH}, {@code SPOT_DIAMETER}, - * {@code LASER_ENERGY}, {@code DETECTOR_GAIN}, {@code DETECTOR_IRIS}, sample name and the types listed by the pulse descriptor. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(9); - list.add(def(TEST_TEMPERATURE)); - list.add(def(THICKNESS)); - list.add(def(DIAMETER)); - list.add(def(PULSE_WIDTH)); - list.add(def(SPOT_DIAMETER)); - list.add(def(LASER_ENERGY)); - list.add(def(DETECTOR_GAIN)); - list.add(def(DETECTOR_IRIS)); - list.add(new SampleName()); - list.add(pulseDescriptor); - return list; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append(sampleName + " [" + externalID + "]"); - sb.append(lineSeparator()); - sb.append(lineSeparator()); - - data.forEach(entry -> { - sb.append(entry.toString()); - sb.append(lineSeparator()); - }); - - sb.append(pulseDescriptor.toString()); - - return sb.toString(); - - } - - /** - * Creates a list of data that contain all {@code NumericProperty} objects - * belonging to this {@code Metadata} and an {@code InstanceDescriptor} relating - * to the pulse shape. - */ - - @Override - public List data() { - var list = new ArrayList(); - list.addAll(data); - list.add(pulseDescriptor); - return list; - } - - /** - * @return If this {@code Metadata} is NOT assigned to a {@code SearchTask}, - * returns a new {@code Identifier} based on the {@code externalID}. - * Otherwise, calls {@code super.identify()}. - * @see Identifier.externalIdentifier() - */ - - @Override - public Identifier identify() { - return getParent() == null ? externalIdentifier(externalID) : super.identify(); - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof Metadata)) - return false; - - var other = (Metadata) o; - - if (other.getExternalID() != this.getExternalID()) - return false; - - if (!sampleName.equals(other.getSampleName())) - return false; - - return this.data().containsAll(other.data()); - - } - -} \ No newline at end of file + private Set data; + private SampleName sampleName; + private int externalID; + + private InstanceDescriptor pulseDescriptor = new InstanceDescriptor( + "Pulse Shape Selector", PulseTemporalShape.class); + + private NumericPulseData pulseData; + + /** + * Creates a {@code Metadata} with the specified parameters and a default + * rectangular pulse shape. Properties are stored in a {@code TreeSet}. + * + * @param temperature the NumericProperty of the type + * {@code NumericPropertyKeyword.TEST_TEMPERATURE} + * @param externalId an integer, specifying the external ID recorded by the + * experimental setup. + */ + public Metadata(NumericProperty temperature, int externalId) { + sampleName = new SampleName(); + setExternalID(externalId); + pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); + data = new TreeSet(); + set(TEST_TEMPERATURE, temperature); + } + + /** + * Gets the external ID usually specified in the experimental files. Note + * this is not a {@code NumericProperty} + * + * @return an integer, representing the external ID + */ + public int getExternalID() { + return externalID; + } + + /** + * Sets the external ID in this {@code Metadata} to {@code externalId} + * + * @param externalId the value of the external ID + */ + private void setExternalID(int externalId) { + this.externalID = externalId; + } + + /** + * Retrieves the pulse shape recorded in this {@code Metadata} + * + * @return a {@code PulseShape} object + */ + public InstanceDescriptor getPulseDescriptor() { + return pulseDescriptor; + } + + /** + * Retrieves the sample name. This name is used to create directories when + * exporting the data and also to fill the legend when plotting. + * + * @return the sample name + */ + public SampleName getSampleName() { + return sampleName; + } + + /** + * Sets the sample name property. + * + * @param sampleName the sample name + */ + public void setSampleName(SampleName sampleName) { + this.sampleName = sampleName; + } + + public void setPulseData(NumericPulseData pulseData) { + this.pulseData = pulseData; + } + + /** + * If a Numerical Pulse has been loaded (for example, when importing from + * Proteus), this will return an object describing this data. + */ + public NumericPulseData getPulseData() { + return pulseData; + } + + /** + * Searches the internal list of this class for a property with the + * {@code key} type. + * + * @return if present, returns a property belonging to this {@code Metadata} + * with the specified type, otherwise return null. + */ + @Override + public NumericProperty numericProperty(NumericPropertyKeyword key) { + var optional = data.stream().filter(p -> p.getType() == key).findFirst(); + return optional.isPresent() ? optional.get() : null; + } + + /** + * If {@code type} is listed by this {@code Metadata}, will attempt to + * either set a value to the property belonging to this {@code Metadata} and + * identified by {@code type} or add {@code property} to the internal + * repository of this {@code Metadata}. Triggers {@code firePropertyChanged} + * upon successful completion. + * + * @param type the type to be searched for + * @param property a property with the type specified by its first argument. + * The value of this property will be used to update its counterpart in this + * {@code Metadata}. The signature of this method is dictated by the use of + * Reflection API. + * @throws IllegalArgumentException if the types of the arguments do not + * match or if {@code} property is not a listed parameter + * @see PropertyHolder.isListedParameter() + * @see PropertyHolder.firePropertyChanged() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + if (type != property.getType() || !isListedParameter(property)) { + return; //ingore unrecognised properties + } + var optional = numericProperty(type); + + if (optional != null) { + optional.setValue((Number) property.getValue()); + } else { + data.add(property); + } + + firePropertyChanged(this, property); + + } + + /** + * The listed types include {@code TEST_TEMPERATURE}, {@code THICKNESS}, + * {@code DIAMETER}, {@code PULSE_WIDTH}, {@code SPOT_DIAMETER}, + * {@code LASER_ENERGY}, {@code DETECTOR_GAIN}, {@code DETECTOR_IRIS}, + * sample name and the types listed by the pulse descriptor. + */ + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(new SampleName()); + list.add(pulseDescriptor); + return list; + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TEST_TEMPERATURE); + set.add(THICKNESS); + set.add(DIAMETER); + set.add(PULSE_WIDTH); + set.add(SPOT_DIAMETER); + set.add(LASER_ENERGY); + set.add(DETECTOR_GAIN); + set.add(DETECTOR_IRIS); + return set; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append(sampleName + " [" + externalID + "]"); + sb.append(lineSeparator()); + sb.append(lineSeparator()); + + data.forEach(entry -> { + sb.append(entry.toString()); + sb.append(lineSeparator()); + }); + + sb.append(pulseDescriptor.toString()); + + return sb.toString(); + + } + + /** + * Creates a list of data that contain all {@code NumericProperty} objects + * belonging to this {@code Metadata} and an {@code InstanceDescriptor} + * relating to the pulse shape. + */ + @Override + public List data() { + var list = new ArrayList(); + list.addAll(data); + list.add(pulseDescriptor); + return list; + } + + /** + * @return If this {@code Metadata} is NOT assigned to a {@code SearchTask}, + * returns a new {@code Identifier} based on the {@code externalID}. + * Otherwise, calls {@code super.identify()}. + * @see Identifier.externalIdentifier() + */ + @Override + public Identifier identify() { + return getParent() == null ? externalIdentifier(externalID) : super.identify(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Metadata)) { + return false; + } + + var other = (Metadata) o; + + if (other.getExternalID() != this.getExternalID()) { + return false; + } + + if (!sampleName.equals(other.getSampleName())) { + return false; + } + + return this.data().containsAll(other.data()); + + } + +} diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 166f9867..ab95fa08 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -1,6 +1,7 @@ package pulse.input; import static java.lang.Math.max; + import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; @@ -8,9 +9,11 @@ import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; @@ -24,216 +27,244 @@ * by the {@code ExperimentalData}. * */ - public class Range extends PropertyHolder implements Optimisable { - private Segment segment; - - /** - * Constructs a {@code Range} from the minimum and maximum values of - * {@code data}. - * - * @param data a list of double values - */ - - public Range(List data) { - double min = data.stream().reduce((a, b) -> a < b ? a : b).get(); - double max = data.stream().reduce((a, b) -> b > a ? b : a).get(); - segment = new Segment(min, max); - } - - /** - * Constructs a new {@code Range} based on the segment specified by {@code a} - * and {@code b} - * - * @param a a double value - * @param b another double value - */ - - public Range(double a, double b) { - this.segment = new Segment(a, b); - } - - /** - * Resets the minimum and maximum values of this range to those specified by the - * elements of {@code data}, the indices of which correspond to the lower and - * upper bound of the {@code IndexRange}. - * - * @param range an object specifying the start/end indices in regard to the - * {@code data} list - * @param data a list of double values (usually, a time sequence) - */ - - public void reset(IndexRange range, List data) { - segment.setMaximum(data.get(range.getUpperBound())); - segment.setMinimum(data.get(range.getLowerBound())); - } - - /** - * Gets the numeric property defining the lower bound of this range. - * - * @return the lower bound (usually referring to a time sequence). - */ - - public NumericProperty getLowerBound() { - return derive(LOWER_BOUND, segment.getMinimum()); - } - - /** - * Gets the numeric property defining the upper bound of this range. - * - * @return the upper bound (usually referring to a time sequence). - */ - - public NumericProperty getUpperBound() { - return derive(UPPER_BOUND, segment.getMaximum()); - } - - /** - * Sets the lower bound and triggers {@code firePropertyChanged}. - * - * @param p a numeric property with the required {@code LOWER_BOUND} type. - */ - - public void setLowerBound(NumericProperty p) { - requireType(p, LOWER_BOUND); - segment.setMinimum((double) p.getValue()); - firePropertyChanged(this, p); - } - - /** - * Sets the upper bound and triggers {@code firePropertyChanged}. - * - * @param p a numeric property with the required {@code UPPER_BOUND} type. - */ - - public void setUpperBound(NumericProperty p) { - requireType(p, UPPER_BOUND); - segment.setMaximum((double) p.getValue()); - firePropertyChanged(this, p); - } - - /** - * Gets the segment representing this range - * - * @return a segment - */ - - public Segment getSegment() { - return segment; - } - - /** - * Updates the lower bound of this range using the information contained in - * {@code p}. Since this is not fail-safe, the method has been made protected. - * - * @param p a {@code NumericProperty} representing the laser pulse width. - */ - - protected void updateMinimum(NumericProperty p) { - if (p == null) - return; - - requireType(p, PULSE_WIDTH); - double pulseWidth = (double) p.getValue(); - segment.setMinimum(max(segment.getMinimum(), pulseWidth)); - - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case LOWER_BOUND: - setLowerBound(property); - break; - case UPPER_BOUND: - setUpperBound(property); - break; - default: - // do nothing - break; - } - } - - /* + private Segment segment; + + /** + * Constructs a {@code Range} from the minimum and maximum values of + * {@code data}. + * + * @param data a list of double values + */ + public Range(List data) { + double min = data.stream().reduce((a, b) -> a < b ? a : b).get(); + double max = data.stream().reduce((a, b) -> b > a ? b : a).get(); + segment = new Segment(min, max); + } + + /** + * Constructs a new {@code Range} based on the segment specified by + * {@code a} and {@code b} + * + * @param a a double value + * @param b another double value + */ + public Range(double a, double b) { + this.segment = new Segment(a, b); + } + + /** + * Resets the minimum and maximum values of this range to those specified by + * the elements of {@code data}, the indices of which correspond to the + * lower and upper bound of the {@code IndexRange}. + * + * @param range an object specifying the start/end indices in regard to the + * {@code data} list + * @param data a list of double values (usually, a time sequence) + */ + public void reset(IndexRange range, List data) { + segment.setMaximum(data.get(range.getUpperBound())); + segment.setMinimum(data.get(range.getLowerBound())); + } + + /** + * Gets the numeric property defining the lower bound of this range. + * + * @return the lower bound (usually referring to a time sequence). + */ + public NumericProperty getLowerBound() { + return derive(LOWER_BOUND, segment.getMinimum()); + } + + /** + * Gets the numeric property defining the upper bound of this range. + * + * @return the upper bound (usually referring to a time sequence). + */ + public NumericProperty getUpperBound() { + return derive(UPPER_BOUND, segment.getMaximum()); + } + + /** + * Sets the lower bound and triggers {@code firePropertyChanged}. + * + * @param p a numeric property with the required {@code LOWER_BOUND} type. + */ + public void setLowerBound(NumericProperty p) { + requireType(p, LOWER_BOUND); + + if( boundLimits(false).contains( ((Number)p.getValue()).doubleValue())) { + segment.setMinimum((double) p.getValue()); + firePropertyChanged(this, p); + } + + } + + /** + * Sets the upper bound and triggers {@code firePropertyChanged}. + * + * @param p a numeric property with the required {@code UPPER_BOUND} type. + */ + public void setUpperBound(NumericProperty p) { + requireType(p, UPPER_BOUND); + + if( boundLimits(true).contains( ((Number)p.getValue()).doubleValue()) ) { + segment.setMaximum((double) p.getValue()); + firePropertyChanged(this, p); + } + + } + + /** + * Gets the segment representing this range + * + * @return a segment + */ + public Segment getSegment() { + return segment; + } + + /** + * Updates the lower bound of this range using the information contained in + * {@code p}. Since this is not fail-safe, the method has been made + * protected. + * + * @param p a {@code NumericProperty} representing the laser pulse width. + */ + protected void updateMinimum(NumericProperty p) { + if (p == null) { + return; + } + + requireType(p, PULSE_WIDTH); + double pulseWidth = (double) p.getValue(); + segment.setMinimum(max(segment.getMinimum(), pulseWidth)); + + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case LOWER_BOUND: + setLowerBound(property); + break; + case UPPER_BOUND: + setUpperBound(property); + break; + default: + // do nothing + break; + } + } + + /** + * Lists lower and upper bounds as properties. + * + * @see PropertyHolder + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(LOWER_BOUND); + set.add(UPPER_BOUND); + return set; + } + + /** + * Calculates the allowed range for either the upper or lower bound. + * @param isUpperBound if {@code true}, will calculate the range for the upper bound, otherwise -- for the lower one., + * @return a Segment range of limits for the specific bound + */ + + public Segment boundLimits(boolean isUpperBound) { + + var curve = (ExperimentalData) this.getParent(); + var seq = curve.getTimeSequence(); + double tHalf = curve.getHalfTime(); + + Segment result = null; + if(isUpperBound) + result = new Segment(2.5 * tHalf, seq.get(seq.size() - 1)); + else + result = new Segment( Math.max(-0.15 * tHalf, seq.get(0)), 0.75 * tHalf); + + return result; + } + + /* * TODO put relative bounds in a constant field Consider creating a Bounds * class, or putting them in the XML file - */ - - /** - * The optimisation vector contain both the lower and upper bounds with the - * absolute constraints equal to a fourth of their values. - * - * @param output the vector to be updated - * @param flags a list of active flags - */ - - @Override - public void optimisationVector(ParameterVector output, List flags) { - - var curve = (ExperimentalData)this.getParent(); - Segment bounds; - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case UPPER_BOUND: - output.set(i, segment.getMaximum()); - var seq = curve.getTimeSequence(); - bounds = new Segment( 1.1 * curve.maxAdjustedSignal().getX(), seq.get(seq.size() - 1) ); - break; - case LOWER_BOUND: - output.set(i, segment.getMinimum()); - bounds = new Segment( 0.0, 0.35 * curve.halfRiseTime() ); - break; - default: - continue; - } - - output.setParameterBounds(i, bounds); - - } - - } - - /** - * Tries to assign the upper and lower bound based on {@code params}. - * - * @param params an {@code IndexedVector} which may contain the bounds. - * @throws SolverException - */ - - @Override - public void assign(ParameterVector params) throws SolverException { - if(!params.validate()) - throw new SolverException("Parameter values not sensible: " + params); - - NumericProperty p = null; - - for (int i = 0, size = params.dimension(); i < size; i++) { - - p = derive(params.getIndex(i), params.get(i)); - - switch (params.getIndex(i)) { - case UPPER_BOUND: - setUpperBound(p); - break; - case LOWER_BOUND: - setLowerBound(p); - break; - default: - continue; - } - - } - - } - - @Override - public String toString() { - return "Range given by: " + segment.toString(); - } - -} \ No newline at end of file + */ + /** + * The optimisation vector contain both the lower and upper bounds with the + * absolute constraints equal to a fourth of their values. + * + * @param output the vector to be updated + * @param flags a list of active flags + */ + @Override + public void optimisationVector(ParameterVector output, List flags) { + + Segment bounds; + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case UPPER_BOUND: + output.set(i, segment.getMaximum()); + bounds = boundLimits(true); + break; + case LOWER_BOUND: + output.set(i, segment.getMinimum()); + bounds = boundLimits(false); + break; + default: + continue; + } + + var transform = new StickTransform(bounds); + + output.setParameterBounds(i, bounds); + output.setTransform(i, transform); + + } + + } + + /** + * Tries to assign the upper and lower bound based on {@code params}. + * + * @param params an {@code IndexedVector} which may contain the bounds. + * @throws SolverException + */ + @Override + public void assign(ParameterVector params) throws SolverException { + NumericProperty p = null; + + for (int i = 0, size = params.dimension(); i < size; i++) { + + p = derive( params.getIndex(i), params.inverseTransform(i) ); + + switch (params.getIndex(i)) { + case UPPER_BOUND: + setUpperBound(p); + break; + case LOWER_BOUND: + setLowerBound(p); + break; + default: + } + + } + + } + + @Override + public String toString() { + return "Range given by: " + segment.toString(); + } + +} From 2b46ad5342acf1effbb1234dd212a0f93ecd7fff Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 11:40:02 +0100 Subject: [PATCH 072/116] Improvements in exporting Fixed a bug in the Exporter.java when the files were saved in a new format but with the default extension (e.g. html as csv). This was fixed by manually replacing the extension substring with the selected export extension. Modified the ResultTableExporter to correctly print the properties in a .csv table (both value and error, which are delimited by +/-) using the String.format() functionality. Unfortunately, it was not possible to align the values in the columns directly below the header string, but, perhaps, in a future release that could be fixed. Fixed the directoryQuery() method in ExportDialog, which previously incorrectly selected a file in the current directory, and not that directory itself. This had led to duplication of a directory (e.g. if 'Documents' was selected, this would created a 'Documents' folder inside, and the export path would be 'Document/Documents/.../'. --- src/main/java/pulse/io/export/Exporter.java | 289 +++++----- .../pulse/io/export/ResultTableExporter.java | 346 ++++++------ .../pulse/ui/frames/dialogs/ExportDialog.java | 497 +++++++++--------- 3 files changed, 573 insertions(+), 559 deletions(-) diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index 80c2d91d..71cb1bad 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -17,150 +17,149 @@ * type of PULsE objects (typically, instances of the PropertyHolder class). * */ - public interface Exporter extends Reflexive { - /** - * Gets the default export extension. - * - * @return {@code Extension.CSV} by default - */ - - public static Extension getDefaultExportExtension() { - return Extension.CSV; - } - - /** - * Returns an array of supported extensions, which by default contains only the - * default extension. - * - * @return an array with {@code Extension} type objects. - */ - - public default Extension[] getSupportedExtensions() { - return new Extension[] { getDefaultExportExtension() }; - } - - /** - * Exports the available contents to {@code directory} without asking a - * confirmation from the user. - *

- * A file is created with the name specified by the {@code describe()} method of - * {@code target} with the extension equal to the third argument of this method. - * A {@code FileOutputStream} writes the contents to the file by invoking - * {@code printToStream} and is closed upon completion. - *

- * - * @param directory the directory where the contents will be exported to. - * @param target a {@code Descriptive} target - * @param extension the file extension. If it is not supported, the exporter - * will revert to its default extension - * @throws IllegalArgumentException if {@code directory} is not really a - * directory - * @see printToStream - */ - - public default void export(T target, File directory, Extension extension) { - - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var supportedExtension = extension; - - if (!Arrays.stream(getSupportedExtensions()).anyMatch(extension::equals)) - supportedExtension = getDefaultExportExtension(); // revert to default extension - - try { - var newFile = new File(directory, target.describe() + "." + supportedExtension); - newFile.createNewFile(); - var fos = new FileOutputStream(newFile); - printToStream(target, fos, supportedExtension); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + directory); - e.printStackTrace(); - } - - } - - /** - * Provides a {@code JFileChooser} for the user to select the export destination - * for {@code target}. The name of the file and its extension come from the - * selection the user makes by interacting with the dialog. - * - * @param target the exported target - * @param parentWindow the parent frame. - * @param fileTypeLabel the label describing the specific type of files that - * will be saved. - */ - - public default void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var fileChooser = new JFileChooser(); - var workingDirectory = new File(System.getProperty("user.home")); - fileChooser.setCurrentDirectory(workingDirectory); - fileChooser.setMultiSelectionEnabled(true); - - FileNameExtensionFilter choosable = null; - - for (var s : getSupportedExtensions()) { - choosable = new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase()); - fileChooser.addChoosableFileFilter( choosable ); - } - - fileChooser.setAcceptAllFileFilterUsed(false); - fileChooser.setFileFilter( choosable ); - fileChooser.setSelectedFile(new File(target.describe() + "." + choosable.getExtensions()[0])); - - int returnVal = fileChooser.showSaveDialog(parentWindow); - if (returnVal == JFileChooser.APPROVE_OPTION) { - var file = fileChooser.getSelectedFile(); - var path = file.getPath(); - - if(! (fileChooser.getFileFilter() instanceof FileNameExtensionFilter) ) - return; - - var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); - var ext = currentFilter.getExtensions()[0]; - - if (!path.contains(".")) - file = new File(path + "." + ext); - - try { - var fos = new FileOutputStream(file); - printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + file); - e.printStackTrace(); - } - } - } - - /** - * Defines the class, instances of which can be fed into the exporter to produce - * a result. - * - * @return a class implementing the {@code Descriptive} interface. - */ - - public Class target(); - - /** - * The interface method is implemented by the subclasses to define the - * exportable content in detail. Depending on the supported extensions, this - * will typically involve a switch statement that will invoke private methods - * defined in the subclass handling the different choices. The stream - * {@code fos} is usually closed implicitly in a try-with-resource clause. - * - * @param target the exported target - * @param fos a FileOutputStream created by the {@code export} method - * @param extension an extension of the file saved on disk - * @see export - * @see askToExport - */ - - public void printToStream(T target, FileOutputStream fos, Extension extension); - -} \ No newline at end of file + /** + * Gets the default export extension. + * + * @return {@code Extension.CSV} by default + */ + public static Extension getDefaultExportExtension() { + return Extension.CSV; + } + + /** + * Returns an array of supported extensions, which by default contains only + * the default extension. + * + * @return an array with {@code Extension} type objects. + */ + public default Extension[] getSupportedExtensions() { + return new Extension[]{getDefaultExportExtension()}; + } + + /** + * Exports the available contents to {@code directory} without asking a + * confirmation from the user. + *

+ * A file is created with the name specified by the {@code describe()} + * method of {@code target} with the extension equal to the third argument + * of this method. A {@code FileOutputStream} writes the contents to the + * file by invoking {@code printToStream} and is closed upon completion. + *

+ * + * @param directory the directory where the contents will be exported to. + * @param target a {@code Descriptive} target + * @param extension the file extension. If it is not supported, the exporter + * will revert to its default extension + * @throws IllegalArgumentException if {@code directory} is not really a + * directory + * @see printToStream + */ + public default void export(T target, File directory, Extension extension) { + + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + var supportedExtension = extension; + + if (!Arrays.stream(getSupportedExtensions()).anyMatch(extension::equals)) { + supportedExtension = getDefaultExportExtension(); // revert to default extension + } + try { + var newFile = new File(directory, target.describe() + "." + supportedExtension); + newFile.createNewFile(); + var fos = new FileOutputStream(newFile); + printToStream(target, fos, supportedExtension); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + directory); + e.printStackTrace(); + } + + } + + /** + * Provides a {@code JFileChooser} for the user to select the export + * destination for {@code target}. The name of the file and its extension + * come from the selection the user makes by interacting with the dialog. + * + * @param target the exported target + * @param parentWindow the parent frame. + * @param fileTypeLabel the label describing the specific type of files that + * will be saved. + */ + public default void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { + var fileChooser = new JFileChooser(); + var workingDirectory = new File(System.getProperty("user.home")); + fileChooser.setCurrentDirectory(workingDirectory); + fileChooser.setMultiSelectionEnabled(true); + + FileNameExtensionFilter choosable = null; + + for (var s : getSupportedExtensions()) { + choosable = new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase()); + fileChooser.addChoosableFileFilter(choosable); + } + + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.setFileFilter(choosable); + fileChooser.setSelectedFile(new File(target.describe() + "." + choosable.getExtensions()[0])); + + int returnVal = fileChooser.showSaveDialog(parentWindow); + if (returnVal == JFileChooser.APPROVE_OPTION) { + var file = fileChooser.getSelectedFile(); + var path = file.getPath(); + + if (!(fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { + return; + } + + var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); + var ext = currentFilter.getExtensions()[0]; + + if (!path.contains(".")) { + file = new File(path + "." + ext); + } + else + file = new File(path.substring(0, path.indexOf(".") + 1) + ext); + + try { + var fos = new FileOutputStream(file); + printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + file); + e.printStackTrace(); + } + } + } + + /** + * Defines the class, instances of which can be fed into the exporter to + * produce a result. + * + * @return a class implementing the {@code Descriptive} interface. + */ + public Class target(); + + /** + * The interface method is implemented by the subclasses to define the + * exportable content in detail. Depending on the supported extensions, this + * will typically involve a switch statement that will invoke private + * methods defined in the subclass handling the different choices. The + * stream {@code fos} is usually closed implicitly in a try-with-resource + * clause. + * + * @param target the exported target + * @param fos a FileOutputStream created by the {@code export} method + * @param extension an extension of the file saved on disk + * @see export + * @see askToExport + */ + public void printToStream(T target, FileOutputStream fos, Extension extension); + +} diff --git a/src/main/java/pulse/io/export/ResultTableExporter.java b/src/main/java/pulse/io/export/ResultTableExporter.java index 6cc85848..f61631ed 100644 --- a/src/main/java/pulse/io/export/ResultTableExporter.java +++ b/src/main/java/pulse/io/export/ResultTableExporter.java @@ -18,179 +18,189 @@ * summary file in either {@code csv} or {@code html} format. * */ - public class ResultTableExporter implements Exporter { - private static ResultTableExporter instance = new ResultTableExporter(); - - private ResultTableExporter() { - // intentionally blank - } - - /** - * Both {@code html} and {@code csv} are suported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { Extension.HTML, Extension.CSV }; - } - - /** - * This will create a single file with the output. Depending on whether the - * results table contain average results (with the respective error margins) or - * only individual results, the file might consist of one or two tables, first - * listing the average results and then finding what individual results have - * been used to calculate the latter. In the {@code html} format, the errors are - * given in the same table cells as the values and are delimited by a plus-minus - * sign. The {@code html} table gives a pretty representation of the results - * whereas the {@code csv}, while difficult to read by a human, can be - * interpreted by most external tools such as LaTeX pgfplots, gnuplot, or excel. - */ - - @Override - public void printToStream(ResultTable table, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(table, fos); - break; - case CSV: - printCSV(table, fos); - break; - default: - System.err.println("Extension not supported: " + extension); - } - } - - private void printHeaderCSV(ResultTable table, PrintStream stream) { - NumericPropertyKeyword p = null; - for (int col = 0; col < table.getColumnCount(); col++) { - p = ((ResultTableModel) table.getModel()).getFormat().fromAbbreviation(table.getColumnName(col)); - stream.print(p + "\t"); - stream.print("STDEV" + "\t"); - } - stream.println(""); - } - - private void printIndividualCSV(NumericProperty p, PrintStream stream) { - stream.print(p.valueOutput() + "\t" + p.errorOutput() + "\t"); - } - - private void printCSV(ResultTable table, FileOutputStream fos) { - try (PrintStream stream = new PrintStream(fos)) { - - printHeaderCSV(table, stream); - - for (int i = 0; i < table.getRowCount(); i++) { - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualCSV((NumericProperty) table.getValueAt(i, j), stream); - stream.println(); - } - - List results = ((ResultTableModel) table.getModel()).getResults(); - - if (results.stream().anyMatch(r -> r instanceof AverageResult)) { - - stream.println(""); - stream.print(Messages.getString("ResultTable.SeparatorCSV")); - stream.println(""); - - printHeaderCSV(table, stream); - - results.stream().filter(r -> r instanceof AverageResult) - .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { - var props = AbstractResult.filterProperties(ir); - - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualCSV(props.get(j), stream); - - stream.println(); - })); - - } - - } - - } - - private void printHeaderHTML(ResultTable table, PrintStream stream, String caption) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("" + caption + ""); - stream.print(""); - - for (int col = 0; col < table.getColumnCount(); col++) { - stream.print(""); - stream.print(table.getColumnName(col) + "\t"); - stream.print(""); - } - - stream.print(""); - } - - private void printIndividualHTML(NumericProperty p, PrintStream stream) { - stream.print(""); - stream.print(p.formattedOutput()); - stream.print(""); - } - - private void printHTML(ResultTable table, FileOutputStream fos) { - try (PrintStream stream = new PrintStream(fos)) { - printHeaderHTML(table, stream, "Exported table (contains either averaged or individual results)"); - - stream.println(""); - - for (int i = 0; i < table.getRowCount(); i++) { - stream.print(""); - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualHTML((NumericProperty) table.getValueAt(i, j), stream); - stream.println(""); - } - - stream.print(""); - - List results = ((ResultTableModel) table.getModel()).getResults(); - - if (results.stream().anyMatch(r -> r instanceof AverageResult)) { - - stream.print(Messages.getString("ResultTable.IndividualResults")); - printHeaderHTML(table, stream, "Exported individual results for each processed task"); - - stream.println(""); - - results.stream().filter(r -> r instanceof AverageResult) - .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { - var props = AbstractResult.filterProperties(ir); - - stream.print(""); - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualHTML(props.get(j), stream); - stream.print(""); - - stream.println(); - })); - - stream.print(""); - } + private static final ResultTableExporter instance = new ResultTableExporter(); + + private ResultTableExporter() { + // intentionally blank + } + + /** + * Both {@code html} and {@code csv} are suported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{Extension.HTML, Extension.CSV}; + } + + /** + * This will create a single file with the output. Depending on whether the + * results table contain average results (with the respective error margins) + * or only individual results, the file might consist of one or two tables, + * first listing the average results and then finding what individual + * results have been used to calculate the latter. In the {@code html} + * format, the errors are given in the same table cells as the values and + * are delimited by a plus-minus sign. The {@code html} table gives a pretty + * representation of the results whereas the {@code csv}, while difficult to + * read by a human, can be interpreted by most external tools such as LaTeX + * pgfplots, gnuplot, or excel. + */ + @Override + public void printToStream(ResultTable table, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(table, fos); + break; + case CSV: + printCSV(table, fos); + break; + default: + System.err.println("Extension not supported: " + extension); + } + } + + private void printHeaderCSV(ResultTable table, PrintStream stream) { + NumericPropertyKeyword p = null; + for (int col = 0; col < table.getColumnCount(); col++) { + p = ((ResultTableModel) table.getModel()).getFormat().fromAbbreviation(table.getColumnName(col)); + stream.printf("%20s ", p); + } + stream.println(""); + } + + private void printIndividualCSV(NumericProperty p, PrintStream stream) { + if(p.getError() == null || p.getError().doubleValue() < 1E-20 ) { + if(p.getValue() instanceof Double) + stream.printf("%12.5e", p.valueInCurrentUnits()); + else + stream.printf("%12d", p.valueInCurrentUnits().intValue()); + } + else { + if(p.getValue() instanceof Double) + stream.printf("%12.5e +/- %12.5e", p.valueInCurrentUnits(), p.errorInCurrentUnits()); + else + stream.printf("%12d +/- %12d", p.valueInCurrentUnits().intValue(), p.errorInCurrentUnits().intValue()); + } + } + + private void printCSV(ResultTable table, FileOutputStream fos) { + try (PrintStream stream = new PrintStream(fos)) { + + printHeaderCSV(table, stream); + + for (int i = 0; i < table.getRowCount(); i++) { + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualCSV((NumericProperty) table.getValueAt(i, j), stream); + } + stream.println(); + } + + List results = ((ResultTableModel) table.getModel()).getResults(); + + if (results.stream().anyMatch(r -> r instanceof AverageResult)) { + + stream.println(""); + stream.print(Messages.getString("ResultTable.SeparatorCSV")); + stream.println(""); + + printHeaderCSV(table, stream); + + results.stream().filter(r -> r instanceof AverageResult) + .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { + var props = AbstractResult.filterProperties(ir); + + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualCSV(props.get(j), stream); + } + + stream.println(); + })); + + } + + } + + } + + private void printHeaderHTML(ResultTable table, PrintStream stream, String caption) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("" + caption + ""); + stream.print(""); + + for (int col = 0; col < table.getColumnCount(); col++) { + stream.print(""); + stream.print(table.getColumnName(col) + "\t"); + stream.print(""); + } + + stream.print(""); + } + + private void printIndividualHTML(NumericProperty p, PrintStream stream) { + stream.print(""); + stream.print(p.formattedOutput()); + stream.print(""); + } + + private void printHTML(ResultTable table, FileOutputStream fos) { + try (PrintStream stream = new PrintStream(fos)) { + printHeaderHTML(table, stream, "Exported table (contains either averaged or individual results)"); + + stream.println(""); + + for (int i = 0; i < table.getRowCount(); i++) { + stream.print(""); + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualHTML((NumericProperty) table.getValueAt(i, j), stream); + } + stream.println(""); + } + + stream.print(""); + + List results = ((ResultTableModel) table.getModel()).getResults(); + + if (results.stream().anyMatch(r -> r instanceof AverageResult)) { + + stream.print(Messages.getString("ResultTable.IndividualResults")); + printHeaderHTML(table, stream, "Exported individual results for each processed task"); + + stream.println(""); + + results.stream().filter(r -> r instanceof AverageResult) + .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { + var props = AbstractResult.filterProperties(ir); + + stream.print(""); + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualHTML(props.get(j), stream); + } + stream.print(""); - } - } + stream.println(); + })); - /** - * Gets the single instance of this class. - * - * @return the single instance of {@code ResultTableExporter}. - */ + stream.print(""); + } - public static ResultTableExporter getInstance() { - return instance; - } + } + } - /** - * @return {@code ResultTable.class}. - */ + /** + * Gets the single instance of this class. + * + * @return the single instance of {@code ResultTableExporter}. + */ + public static ResultTableExporter getInstance() { + return instance; + } - @Override - public Class target() { - return ResultTable.class; - } + /** + * @return {@code ResultTable.class}. + */ + @Override + public Class target() { + return ResultTable.class; + } } diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index 37d0fe93..f8a93008 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -47,256 +47,261 @@ @SuppressWarnings("serial") public class ExportDialog extends JDialog { - private static Map, Boolean> exportSettings = new HashMap, Boolean>(); - private final static int HEIGHT = 180; - private final static int WIDTH = 750; + private static Map, Boolean> exportSettings = new HashMap, Boolean>(); + private final static int HEIGHT = 180; + private final static int WIDTH = 750; + + private static ProgressDialog progressFrame = new ProgressDialog(); + + static { + progressFrame.setLocationRelativeTo(null); + progressFrame.setAlwaysOnTop(true); + } - private static ProgressDialog progressFrame = new ProgressDialog(); + static { + exportSettings.put(MetadataExporter.getInstance().target(), false); + exportSettings.put(CurveExporter.getInstance().target(), true); + exportSettings.put(ResidualStatisticExporter.getInstance().target(), true); + exportSettings.put(RawDataExporter.getInstance().target(), true); + exportSettings.put(ResultExporter.getInstance().target(), true); + exportSettings.put(LogExporter.getInstance().target(), false); + } + private boolean createSubdirectories = false; - static { - progressFrame.setLocationRelativeTo(null); - progressFrame.setAlwaysOnTop(true); - } + private File dir; - static { - exportSettings.put(MetadataExporter.getInstance().target(), false); - exportSettings.put(CurveExporter.getInstance().target(), true); - exportSettings.put(ResidualStatisticExporter.getInstance().target(), true); - exportSettings.put(RawDataExporter.getInstance().target(), true); - exportSettings.put(ResultExporter.getInstance().target(), true); - exportSettings.put(LogExporter.getInstance().target(), false); - } - private boolean createSubdirectories = false; + private JFileChooser fileChooser; + + private String projectName; - private File dir; + public ExportDialog() { + initComponents(); + setTitle("Export Dialog"); + setSize(new Dimension(WIDTH, HEIGHT)); + } - private JFileChooser fileChooser; + private File directoryQuery() { + var returnVal = fileChooser.showSaveDialog(this); - private String projectName; - - public ExportDialog() { - initComponents(); - setTitle("Export Dialog"); - setSize(new Dimension(WIDTH, HEIGHT)); - } - - private File directoryQuery() { - var returnVal = fileChooser.showSaveDialog(this); - - if (returnVal == APPROVE_OPTION) { - dir = fileChooser.getSelectedFile(); - return dir; - } - - return null; - - } - - private void export(Extension extension) { - var instance = TaskManager.getManagerInstance(); - - if (instance.numberOfTasks() < 1) - return; // nothing to export - - var destination = new File(dir + separator + projectName); - var subdirs = instance.getTaskList(); - - if (subdirs.size() > 0 && !destination.exists()) - destination.mkdirs(); - - var monitor = ResourceMonitor.getInstance(); - - if (createSubdirectories) { - progressFrame.trackProgress(subdirs.size()); - var pool = newFixedThreadPool( monitor.getThreadsAvailable() ); - subdirs.stream().forEach(s -> { - pool.submit(() -> { - exportGroup(s, destination, extension); - progressFrame.incrementProgress(); - }); - }); - } else { - var groupped = instance.allGrouppedContents(); - var pool = newFixedThreadPool( monitor.getThreadsAvailable() ); - progressFrame.trackProgress(groupped.size()); - - groupped.stream().forEach(individual -> pool.submit(() -> { - Class individualClass = individual.getClass(); - - if (!exportSettings.containsKey(individualClass)) { - - var key = exportSettings.keySet().stream() - .filter(aClass -> aClass.isAssignableFrom(individual.getClass())).findFirst(); - - if (key.isPresent()) - individualClass = key.get(); - - } - - if (individualClass != null) { - if (exportSettings.containsKey(individualClass)) - if (exportSettings.get(individualClass)) - ExportManager.export(individual, destination, extension); - } - - progressFrame.incrementProgress(); - - }) - - ); - } - - if (exportSettings.get(Result.class)) - exportAllResults(destination, extension); - - } - - private void initComponents() { - - var layout = new GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setAutoCreateGaps(true); - layout.setAutoCreateContainerGaps(true); - - final var defaultProjectName = TaskManager.getManagerInstance().describe(); - projectName = defaultProjectName; - - var directoryLabel = new JLabel("Export to:"); - - fileChooser = new JFileChooser(); - fileChooser.setMultiSelectionEnabled(false); - fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - // Checkboxex - dir = fileChooser.getCurrentDirectory(); - - var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); - directoryField.setEditable(false); - - var formatLabel = new JLabel("Export format:"); - var formats = new JComboBox(Extension.values()); - - var projectLabel = new JLabel("Project name:"); - var projectText = new JTextField(projectName); - - projectText.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - // - } - - @Override - public void insertUpdate(DocumentEvent e) { - if (projectText.getText().trim().isEmpty()) - return; - projectName = projectText.getText(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - - @Override - public void removeUpdate(DocumentEvent e) { - if (projectText.getText().trim().isEmpty()) { - projectName = TaskManager.getManagerInstance().describe(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } else { - projectName = projectText.getText(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - } - }); - - var solutionCheckbox = new JCheckBox("Export Solution(s)"); - solutionCheckbox.setSelected(exportSettings.get(AbstractData.class)); - solutionCheckbox.addActionListener(e -> { - exportSettings.put(AbstractData.class, solutionCheckbox.isSelected()); - exportSettings.put(ResidualStatisticExporter.class, solutionCheckbox.isSelected()); - }); - - var rawDataCheckbox = new JCheckBox("Export Raw Data"); - rawDataCheckbox.setSelected(exportSettings.get(ExperimentalData.class)); - rawDataCheckbox - .addActionListener(e -> exportSettings.put(ExperimentalData.class, rawDataCheckbox.isSelected())); - - var metadataCheckbox = new JCheckBox("Export Metadata"); - metadataCheckbox.setSelected(exportSettings.get(Metadata.class)); - metadataCheckbox.addActionListener(e -> exportSettings.put(Metadata.class, metadataCheckbox.isSelected())); - - var createDirCheckbox = new JCheckBox("Create Sub-Directories"); - createDirCheckbox.setSelected(createSubdirectories); - - var logCheckbox = new JCheckBox("Export log(s)"); - logCheckbox.setSelected(exportSettings.get(Log.class)); - logCheckbox.addActionListener(e -> exportSettings.put(Log.class, logCheckbox.isSelected())); - - createDirCheckbox.addActionListener(e -> { - metadataCheckbox.setEnabled(!createDirCheckbox.isSelected()); - rawDataCheckbox.setEnabled(!createDirCheckbox.isSelected()); - solutionCheckbox.setEnabled(!createDirCheckbox.isSelected()); - logCheckbox.setEnabled(!createDirCheckbox.isSelected()); - createSubdirectories = createDirCheckbox.isSelected(); - }); - - var resultsCheckbox = new JCheckBox("Export Results"); - resultsCheckbox.setSelected(exportSettings.get(Result.class)); - resultsCheckbox.addActionListener(e -> exportSettings.put(Result.class, resultsCheckbox.isSelected())); - - var browseBtn = new JButton("Browse..."); - - browseBtn.addActionListener(e -> { - if (directoryQuery() != null) - directoryField.setText(dir.getPath() + separator + projectName + separator); - }); - - var exportBtn = new JButton("Export"); - - exportBtn.addActionListener( - e -> invokeLater(() -> export(valueOf(formats.getSelectedItem().toString().toUpperCase())))); - - /* + File f = null; + + if (returnVal == APPROVE_OPTION) { + f = fileChooser.getCurrentDirectory(); + } + + return f; + + } + + private void export(Extension extension) { + var instance = TaskManager.getManagerInstance(); + + if (instance.numberOfTasks() < 1) { + return; // nothing to export + } + var destination = new File(dir + separator + projectName); + var subdirs = instance.getTaskList(); + + if (subdirs.size() > 0 && !destination.exists()) { + destination.mkdirs(); + } + + var monitor = ResourceMonitor.getInstance(); + + if (createSubdirectories) { + progressFrame.trackProgress(subdirs.size()); + var pool = newFixedThreadPool(monitor.getThreadsAvailable()); + subdirs.stream().forEach(s -> { + pool.submit(() -> { + exportGroup(s, destination, extension); + progressFrame.incrementProgress(); + }); + }); + } else { + var groupped = instance.allGrouppedContents(); + var pool = newFixedThreadPool(monitor.getThreadsAvailable()); + progressFrame.trackProgress(groupped.size()); + + groupped.stream().forEach(individual -> pool.submit(() -> { + Class individualClass = individual.getClass(); + + if (!exportSettings.containsKey(individualClass)) { + + var key = exportSettings.keySet().stream() + .filter(aClass -> aClass.isAssignableFrom(individual.getClass())).findFirst(); + + if (key.isPresent()) { + individualClass = key.get(); + } + + } + + if (individualClass != null) { + if (exportSettings.containsKey(individualClass)) { + if (exportSettings.get(individualClass)) { + ExportManager.export(individual, destination, extension); + } + } + } + + progressFrame.incrementProgress(); + + }) + ); + } + + if (exportSettings.get(Result.class)) { + exportAllResults(destination, extension); + } + + } + + private void initComponents() { + + var layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setAutoCreateGaps(true); + layout.setAutoCreateContainerGaps(true); + + final var defaultProjectName = TaskManager.getManagerInstance().describe(); + projectName = defaultProjectName; + + var directoryLabel = new JLabel("Export to:"); + + fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); + // Checkboxex + dir = fileChooser.getCurrentDirectory(); + + var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); + directoryField.setEditable(false); + + var formatLabel = new JLabel("Export format:"); + var formats = new JComboBox(Extension.values()); + + var projectLabel = new JLabel("Project name:"); + var projectText = new JTextField(projectName); + + projectText.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + // + } + + @Override + public void insertUpdate(DocumentEvent e) { + if (projectText.getText().trim().isEmpty()) { + return; + } + projectName = projectText.getText(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } + + @Override + public void removeUpdate(DocumentEvent e) { + if (projectText.getText().trim().isEmpty()) { + projectName = TaskManager.getManagerInstance().describe(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } else { + projectName = projectText.getText(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } + } + }); + + var solutionCheckbox = new JCheckBox("Export Solution(s)"); + solutionCheckbox.setSelected(exportSettings.get(AbstractData.class)); + solutionCheckbox.addActionListener(e -> { + exportSettings.put(AbstractData.class, solutionCheckbox.isSelected()); + exportSettings.put(ResidualStatisticExporter.class, solutionCheckbox.isSelected()); + }); + + var rawDataCheckbox = new JCheckBox("Export Raw Data"); + rawDataCheckbox.setSelected(exportSettings.get(ExperimentalData.class)); + rawDataCheckbox + .addActionListener(e -> exportSettings.put(ExperimentalData.class, rawDataCheckbox.isSelected())); + + var metadataCheckbox = new JCheckBox("Export Metadata"); + metadataCheckbox.setSelected(exportSettings.get(Metadata.class)); + metadataCheckbox.addActionListener(e -> exportSettings.put(Metadata.class, metadataCheckbox.isSelected())); + + var createDirCheckbox = new JCheckBox("Create Sub-Directories"); + createDirCheckbox.setSelected(createSubdirectories); + + var logCheckbox = new JCheckBox("Export log(s)"); + logCheckbox.setSelected(exportSettings.get(Log.class)); + logCheckbox.addActionListener(e -> exportSettings.put(Log.class, logCheckbox.isSelected())); + + createDirCheckbox.addActionListener(e -> { + metadataCheckbox.setEnabled(!createDirCheckbox.isSelected()); + rawDataCheckbox.setEnabled(!createDirCheckbox.isSelected()); + solutionCheckbox.setEnabled(!createDirCheckbox.isSelected()); + logCheckbox.setEnabled(!createDirCheckbox.isSelected()); + createSubdirectories = createDirCheckbox.isSelected(); + }); + + var resultsCheckbox = new JCheckBox("Export Results"); + resultsCheckbox.setSelected(exportSettings.get(Result.class)); + resultsCheckbox.addActionListener(e -> exportSettings.put(Result.class, resultsCheckbox.isSelected())); + + var browseBtn = new JButton("Browse..."); + + browseBtn.addActionListener(e -> { + if (directoryQuery() != null) { + directoryField.setText(dir.getPath() + separator + projectName + separator); + } + }); + + var exportBtn = new JButton("Export"); + + exportBtn.addActionListener( + e -> invokeLater(() -> export(valueOf(formats.getSelectedItem().toString().toUpperCase())))); + + /* * layout - */ - - layout.setHorizontalGroup(layout.createSequentialGroup() - // #1 - .addComponent(directoryLabel) - // #2 - .addGroup(layout.createParallelGroup(LEADING).addComponent(directoryField) - // #2a - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(LEADING).addComponent(solutionCheckbox) - .addComponent(rawDataCheckbox)) - .addGroup(layout.createParallelGroup(LEADING).addComponent(metadataCheckbox) - .addComponent(createDirCheckbox)) - .addGroup(layout.createParallelGroup(LEADING).addComponent(logCheckbox) - .addComponent(resultsCheckbox))) - // #2b - // .addGroup(layout.createSequentialGroup() - .addGroup(layout.createSequentialGroup().addComponent(formatLabel).addComponent(formats) - .addComponent(projectLabel).addComponent(projectText)) - // ) - ) - // #3 - .addGroup(layout.createParallelGroup(LEADING).addComponent(browseBtn).addComponent(exportBtn))); - layout.linkSize(HORIZONTAL, browseBtn, exportBtn); - - layout.setVerticalGroup(layout.createSequentialGroup() - // #1 - .addGroup(layout.createParallelGroup(BASELINE).addComponent(directoryLabel) - - .addComponent(directoryField).addComponent(browseBtn)) - // #2 - .addGroup(layout.createParallelGroup(LEADING) - // #2a - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(BASELINE).addComponent(solutionCheckbox) - .addComponent(metadataCheckbox).addComponent(logCheckbox)) - .addGroup(layout.createParallelGroup(BASELINE).addComponent(rawDataCheckbox) - .addComponent(createDirCheckbox).addComponent(resultsCheckbox))) - // #2b - .addComponent(exportBtn)) - // 2b - .addGroup(layout.createParallelGroup(BASELINE).addComponent(formats).addComponent(formatLabel) - .addComponent(projectLabel).addComponent(projectText))); - - } - -} \ No newline at end of file + */ + layout.setHorizontalGroup(layout.createSequentialGroup() + // #1 + .addComponent(directoryLabel) + // #2 + .addGroup(layout.createParallelGroup(LEADING).addComponent(directoryField) + // #2a + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(LEADING).addComponent(solutionCheckbox) + .addComponent(rawDataCheckbox)) + .addGroup(layout.createParallelGroup(LEADING).addComponent(metadataCheckbox) + .addComponent(createDirCheckbox)) + .addGroup(layout.createParallelGroup(LEADING).addComponent(logCheckbox) + .addComponent(resultsCheckbox))) + // #2b + // .addGroup(layout.createSequentialGroup() + .addGroup(layout.createSequentialGroup().addComponent(formatLabel).addComponent(formats) + .addComponent(projectLabel).addComponent(projectText)) + // ) + ) + // #3 + .addGroup(layout.createParallelGroup(LEADING).addComponent(browseBtn).addComponent(exportBtn))); + layout.linkSize(HORIZONTAL, browseBtn, exportBtn); + + layout.setVerticalGroup(layout.createSequentialGroup() + // #1 + .addGroup(layout.createParallelGroup(BASELINE).addComponent(directoryLabel) + .addComponent(directoryField).addComponent(browseBtn)) + // #2 + .addGroup(layout.createParallelGroup(LEADING) + // #2a + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(BASELINE).addComponent(solutionCheckbox) + .addComponent(metadataCheckbox).addComponent(logCheckbox)) + .addGroup(layout.createParallelGroup(BASELINE).addComponent(rawDataCheckbox) + .addComponent(createDirCheckbox).addComponent(resultsCheckbox))) + // #2b + .addComponent(exportBtn)) + // 2b + .addGroup(layout.createParallelGroup(BASELINE).addComponent(formats).addComponent(formatLabel) + .addComponent(projectLabel).addComponent(projectText))); + + } + +} From 34c73054038477296b22d960bd1326b3129bb615 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 11:45:54 +0100 Subject: [PATCH 073/116] Improvements in XML handling The xml format was changed in such a way that xml elements declaring a 'default-search-variable' attribute will be marked as 'optimisable', even if the value of that variable is 'false'. Therefore, there are now two distinct states: isDefaultSearchVariable() and isOptimisable(). Any element that should be allowed to be optimisable should have a 'default-search-variable' attribute. Those elements that declare this property as 'true' will have their search flag enabled by default. --- .../java/pulse/io/export/XMLConverter.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/pulse/io/export/XMLConverter.java b/src/main/java/pulse/io/export/XMLConverter.java index 73b91e43..62ecd25d 100644 --- a/src/main/java/pulse/io/export/XMLConverter.java +++ b/src/main/java/pulse/io/export/XMLConverter.java @@ -80,9 +80,11 @@ private static void toXML(NumericProperty np, Document doc, Element rootElement) primitiveType.setValue(np.getValue() instanceof Double ? "double" : "int"); property.setAttributeNode(primitiveType); - Attr defSearch = doc.createAttribute("default-search-variable"); - primitiveType.setValue(np.isDefaultSearchVariable() + ""); - property.setAttributeNode(defSearch); + if (np.isOptimisable()) { + Attr defSearch = doc.createAttribute("default-search-variable"); + primitiveType.setValue(np.isDefaultSearchVariable() + ""); + property.setAttributeNode(defSearch); + } } @@ -183,7 +185,8 @@ public static List readXML(InputStream inputStream) boolean discrete = Boolean.valueOf(eElement.getAttribute("discreet")); String descriptor = eElement.getAttribute("descriptor"); String abbreviation = eElement.getAttribute("abbreviation"); - boolean defSearch = Boolean.valueOf(eElement.getAttribute("default-search-variable")); + + String search = eElement.getAttribute("default-search-variable"); Number value, minimum, maximum, dimensionFactor; @@ -202,25 +205,28 @@ public static List readXML(InputStream inputStream) NodeList excludeList = eElement.getElementsByTagName("excludes"); var np = new NumericProperty(keyword, value, minimum, maximum, dimensionFactor); - + if (excludeList.getLength() > 0) { var excludeKeywords = ((Element) excludeList.item(0)).getElementsByTagName("keyword"); NumericPropertyKeyword[] array = new NumericPropertyKeyword[excludeKeywords.getLength()]; - + for (int i = 0; i < excludeKeywords.getLength(); i++) { String textValue = excludeKeywords.item(i).getChildNodes().item(0).getNodeValue(); array[i] = NumericPropertyKeyword.valueOf(textValue); } - + np.setExcludeKeywords(array); - + } np.setDescriptor(descriptor); np.setAbbreviation(abbreviation); np.setVisibleByDefault(visible); np.setDiscrete(discrete); - np.setDefaultSearchVariable(defSearch); + if (!search.isEmpty()) { + np.setDefaultSearchVariable(Boolean.valueOf(search)); + np.setOptimisable(true); + } properties.add(np); } } From cff52844e157e8cdb2f02ba81ee688a0ed639902 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 11:59:17 +0100 Subject: [PATCH 074/116] Fixed property transforms and problem statements Introduced a new StickTransform class, which prevents properties from taking values outside a given region by returning their values back to the boundary (so that the property 'sticks' to the boundary). This has proven to yield better (in terms of possible optimiser bias) results compared to the previously used ATANH transform. Changed the ATANH transform to ABS in NonlinearProblem. Converted listedTypes() to listed Keywords() Changed default baseline for all problem statements to LinearBaseline (previously - FlatBaseline). Fixed wrong baseline setting in the copy constructor when the listeners were not notified of this, which caused a loss of search parameters (BASELINE_INTERCEPT and BASELINE_SLOPE could have gone missing). Removed calls to calculated half-time in ThermalProperties, replacing them with a getter instead. --- .../pulse/math/transforms/StickTransform.java | 62 ++ .../problem/statements/NonlinearProblem.java | 169 ++-- .../pulse/problem/statements/Problem.java | 811 +++++++++--------- .../model/ExtendedThermalProperties.java | 19 +- .../statements/model/ThermalProperties.java | 651 +++++++------- .../model/ThermoOpticalProperties.java | 271 +++--- 6 files changed, 1022 insertions(+), 961 deletions(-) create mode 100644 src/main/java/pulse/math/transforms/StickTransform.java diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java new file mode 100644 index 00000000..d68c9352 --- /dev/null +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.math.transforms; + +import static java.lang.Math.tanh; +import static pulse.math.MathUtils.atanh; +import pulse.math.Segment; + +/** + * A simple bounded transform which makes the parameter stick to the + * boundaries upon reaching them. For insatnce, when a parameter x + * attempts to escape its bounds due to a larger increment then allowed, this + * transform will return it directly to the respective boundary, where it will + * "stick". + * @author Artem Lunev + */ + +public class StickTransform extends BoundedParameterTransform { + + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public StickTransform(Segment bounds) { + super(bounds); + } + + /** + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + double max = getBounds().getMaximum(); + double min = getBounds().getMinimum(); + return a > max ? max : (a < min ? min : a); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return transform(t); + } + +} diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 1b0f7b1d..3bfc634d 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,6 +1,5 @@ package pulse.problem.statements; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; @@ -10,125 +9,125 @@ import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.AtanhTransform; +import pulse.math.transforms.StandardTransformations; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; -import pulse.properties.Property; +import pulse.properties.NumericPropertyKeyword; import pulse.ui.Messages; public class NonlinearProblem extends ClassicalProblem { - public NonlinearProblem() { - super(); - setPulse(new Pulse2D()); - setComplexity(ProblemComplexity.MODERATE); - } + public NonlinearProblem() { + super(); + setPulse(new Pulse2D()); + setComplexity(ProblemComplexity.MODERATE); + } - public NonlinearProblem(NonlinearProblem p) { - super(p); - setPulse(new Pulse2D((Pulse2D) p.getPulse())); - } + public NonlinearProblem(NonlinearProblem p) { + super(p); + setPulse(new Pulse2D((Pulse2D) p.getPulse())); + } - @Override - public boolean isReady() { - return getProperties().areThermalPropertiesLoaded(); - } + @Override + public boolean isReady() { + return getProperties().areThermalPropertiesLoaded(); + } - @Override - public void retrieveData(ExperimentalData c) { - super.retrieveData(c); - getProperties().setTestTemperature(c.getMetadata().numericProperty(TEST_TEMPERATURE)); - } + @Override + public void retrieveData(ExperimentalData c) { + super.retrieveData(c); + getProperties().setTestTemperature(c.getMetadata().numericProperty(TEST_TEMPERATURE)); + } - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(TEST_TEMPERATURE)); - list.add(def(SPECIFIC_HEAT)); - list.add(def(DENSITY)); - list.remove(def(SPOT_DIAMETER)); - return list; - } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TEST_TEMPERATURE); + set.add(SPECIFIC_HEAT); + set.add(DENSITY); + set.remove(SPOT_DIAMETER); + return set; + } - @Override - public String toString() { - return Messages.getString("NonlinearProblem.Descriptor"); - } + @Override + public String toString() { + return Messages.getString("NonlinearProblem.Descriptor"); + } - public NumericProperty getThermalConductivity() { - return derive(CONDUCTIVITY, getProperties().thermalConductivity()); - } + public NumericProperty getThermalConductivity() { + return derive(CONDUCTIVITY, getProperties().thermalConductivity()); + } - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - int size = output.dimension(); - var properties = getProperties(); + @Override + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + int size = output.dimension(); + var properties = getProperties(); - for (int i = 0; i < size; i++) { + for (int i = 0; i < size; i++) { - var key = output.getIndex(i); + var key = output.getIndex(i); - if (key == HEAT_LOSS) { + if (key == HEAT_LOSS) { - var bounds = new Segment(1e-5, properties.maxBiot()); - final double Bi1 = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, new AtanhTransform(bounds)); - output.set(i, Bi1); - output.setParameterBounds(i, bounds); + var bounds = new Segment(0.0, properties.maxBiot()); + final double Bi1 = (double) properties.getHeatLoss().getValue(); + output.setTransform(i, StandardTransformations.ABS); + output.set(i, Bi1); + output.setParameterBounds(i, bounds); - } + } - } + } - } + } - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @throws SolverException - * @see listedTypes() - */ + /** + * Assigns parameter values of this {@code Problem} using the optimisation + * vector {@code params}. Only those parameters will be updated, the types + * of which are listed as indices in the {@code params} vector. + * + * @param params the optimisation vector, containing a similar set of + * parameters to this {@code Problem} + * @throws SolverException + * @see listedTypes() + */ + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var p = getProperties(); - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var p = getProperties(); + for (int i = 0, size = params.dimension(); i < size; i++) { - for (int i = 0, size = params.dimension(); i < size; i++) { + var key = params.getIndex(i); - var key = params.getIndex(i); + if (key == HEAT_LOSS) { - if (key == HEAT_LOSS) { + p.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); + p.emissivity(); - p.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); - p.emissivity(); + } - } + } - } + } - } + @Override + public Class defaultScheme() { + return ImplicitScheme.class; + } - @Override - public Class defaultScheme() { - return ImplicitScheme.class; - } + @Override + public Problem copy() { + return new NonlinearProblem(this); + } - @Override - public Problem copy() { - return new NonlinearProblem(this); - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 0f6f63ed..acde58f8 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,20 +1,16 @@ package pulse.problem.statements; import static pulse.input.listeners.CurveEventType.RESCALED; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import pulse.HeatingCurve; import pulse.baseline.Baseline; -import pulse.baseline.FlatBaseline; +import pulse.baseline.LinearBaseline; import pulse.input.ExperimentalData; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -27,8 +23,13 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import static pulse.properties.NumericPropertyKeyword.THICKNESS; import pulse.properties.Property; import pulse.search.Optimisable; import pulse.tasks.SearchTask; @@ -43,411 +44,405 @@ * Most importantly, this class sets out the procedures for reading and writing * the vector argument of the objective function for solving the optimisation * problem. - * + * * @see pulse.problem.schemes.DifferenceScheme */ - public abstract class Problem extends PropertyHolder implements Reflexive, Optimisable { - private ThermalProperties properties; - private HeatingCurve curve; - private Baseline baseline; - private Pulse pulse; - - private static boolean hideDetailedAdjustment = true; - private ProblemComplexity complexity = ProblemComplexity.LOW; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Baseline Selector", Baseline.class); - - /** - * Creates a {@code Problem} with default parameters (as found in the .XML - * file). - *

- * First, invokes the {@code super()} constructor of {@code PropertyHolder} to - * initialise {@code PropertyHolderListener}s, then initialises the variables - * and creates default {@code Pulse} and {@code HeatingCurve}, setting this - * object as their parent. - *

- */ - - protected Problem() { - initProperties(); - - setHeatingCurve(new HeatingCurve()); - - instanceDescriptor.attemptUpdate(FlatBaseline.class.getSimpleName()); - addListeners(); - initBaseline(); - } - - /** - * Copies all essential parameters from {@code p}, excluding the heating curve, - * which is created anew. - * - * @param p the {@code Problem} to replicate - */ - - public Problem(Problem p) { - initProperties(p.getProperties().copy()); - - setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); - curve.setNumPoints(p.getHeatingCurve().getNumPoints()); - - instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); - addListeners(); - this.baseline = p.getBaseline().copy(); - } - - public abstract Problem copy(); - - public void setHeatingCurve(HeatingCurve curve) { - this.curve = curve; - curve.setParent(this); - } - - private void addListeners() { - instanceDescriptor.addListener(() -> { - initBaseline(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - curve.addHeatingCurveListener(e -> { - if (e.getType() == RESCALED) { - var c = e.getData(); - if (!c.isIncomplete()) - curve.apply(getBaseline()); - } - }); - } - - /** - * Lists the available {@code DifferenceScheme}s for this {@code Problem}. - *

- * This is done utilising the {@code Reflexive} interface implemented by the - * class {@code DifferenceSheme}. This method dynamically locates any subclasses - * of the {@code DifferenceScheme} in the associated package (note this can be - * extended to include plugins) and checks whether any of the instances of those - * schemes return a non-{@code null} result when calling the - * {@code solver(Problem)} method. - *

- * - * @return a {@code List} of available {@code DifferenceScheme}s for solving - * this {@code Problem}. - */ - - public List availableSolutions() { - var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); - return allSchemes.stream().filter(scheme -> scheme instanceof Solver).filter(s -> s.domain() == this.getClass()) - .collect(Collectors.toList()); - } - - /** - * Used to change the parameter values of this {@code Problem}. It is only - * allowed to use those types of {@code NumericPropery} that are listed by the - * {@code listedParameters()}. - * - * @see listedTypes() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - properties.set(type, value); - } - - public HeatingCurve getHeatingCurve() { - return curve; - } - - public Pulse getPulse() { - return pulse; - } - - /** - * Sets the {@code pulse} of this {@code Problem} and assigns this - * {@code Problem} as its parent. - * - * @param pulse a {@code Pulse} object - */ - - public void setPulse(Pulse pulse) { - this.pulse = pulse; - pulse.setParent(this); - } - - /** - * This will use the data contained in {@code c} to estimate the detector signal - * span and the thermal diffusivity for this {@code Problem}. Note these - * estimates may be very rough. - * - * @param c the {@code ExperimentalData} object - */ - - public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); // used to estimate the floor of the signal range - estimateSignalRange(c); - updateProperties(this, c.getMetadata()); - properties.useTheoreticalEstimates(c); - } - - /** - * The signal range is defined as max{ T(t) } - min{ T(t) - * }, where max{...} and min{...} are robust to - * outliers. This calls the {@code maxTemperature} method of {@code c} and uses - * the baseline value at {@code 0} as the min{...} value. - * - * @param c the {@code ExperimentalData} object - * @see pulse.input.ExperimentalData.maxTemperature() - */ - - public void estimateSignalRange(ExperimentalData c) { - var maxPoint = c.maxAdjustedSignal(); - final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); - properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); - } - - /** - * Calculates the vector argument defined on Rn - * to the scalar objective function for this {@code Problem}. To fill the vector - * with data, only those parameters from this {@code Problem} will be used which - * are defined by the {@code flags}, e.g. if the flag associated with the - * {@code HEAT_LOSS} keyword is set to false, its value will be skipped when - * creating the vector. - *

- * - * @see listedTypes() - */ - - /* + private ThermalProperties properties; + private HeatingCurve curve; + private Baseline baseline; + private Pulse pulse; + + private static boolean hideDetailedAdjustment = true; + private ProblemComplexity complexity = ProblemComplexity.LOW; + + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Baseline Selector", Baseline.class); + + /** + * Creates a {@code Problem} with default parameters (as found in the .XML + * file). + *

+ * First, invokes the {@code super()} constructor of {@code PropertyHolder} + * to initialise {@code PropertyHolderListener}s, then initialises the + * variables and creates default {@code Pulse} and {@code HeatingCurve}, + * setting this object as their parent. + *

+ */ + protected Problem() { + initProperties(); + + setHeatingCurve(new HeatingCurve()); + + instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); + addListeners(); + initBaseline(); + } + + /** + * Copies all essential parameters from {@code p}, excluding the heating + * curve, which is created anew. + * + * @param p the {@code Problem} to replicate + */ + public Problem(Problem p) { + initProperties(p.getProperties().copy()); + + setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); + curve.setNumPoints(p.getHeatingCurve().getNumPoints()); + + instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); + addListeners(); + setBaseline( p.getBaseline().copy() ); + } + + public abstract Problem copy(); + + public void setHeatingCurve(HeatingCurve curve) { + this.curve = curve; + curve.setParent(this); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> { + initBaseline(); + this.firePropertyChanged(instanceDescriptor, instanceDescriptor); + }); + curve.addHeatingCurveListener(e -> { + if (e.getType() == RESCALED) { + var c = e.getData(); + if (!c.isIncomplete()) { + curve.apply(getBaseline()); + } + } + }); + } + + /** + * Lists the available {@code DifferenceScheme}s for this {@code Problem}. + *

+ * This is done utilising the {@code Reflexive} interface implemented by the + * class {@code DifferenceSheme}. This method dynamically locates any + * subclasses of the {@code DifferenceScheme} in the associated package + * (note this can be extended to include plugins) and checks whether any of + * the instances of those schemes return a non-{@code null} result when + * calling the {@code solver(Problem)} method. + *

+ * + * @return a {@code List} of available {@code DifferenceScheme}s for solving + * this {@code Problem}. + */ + public List availableSolutions() { + var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); + return allSchemes.stream().filter(scheme -> scheme instanceof Solver).filter(s -> s.domain() == this.getClass()) + .collect(Collectors.toList()); + } + + /** + * Used to change the parameter values of this {@code Problem}. It is only + * allowed to use those types of {@code NumericPropery} that are listed by + * the {@code listedParameters()}. + * + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + properties.set(type, value); + } + + public HeatingCurve getHeatingCurve() { + return curve; + } + + public Pulse getPulse() { + return pulse; + } + + /** + * Sets the {@code pulse} of this {@code Problem} and assigns this + * {@code Problem} as its parent. + * + * @param pulse a {@code Pulse} object + */ + public void setPulse(Pulse pulse) { + this.pulse = pulse; + pulse.setParent(this); + } + + /** + * This will use the data contained in {@code c} to estimate the detector + * signal span and the thermal diffusivity for this {@code Problem}. Note + * these estimates may be very rough. + * + * @param c the {@code ExperimentalData} object + */ + public void retrieveData(ExperimentalData c) { + baseline.fitTo(c); // used to estimate the floor of the signal range + estimateSignalRange(c); + updateProperties(this, c.getMetadata()); + properties.useTheoreticalEstimates(c); + } + + /** + * The signal range is defined as max{ T(t) } - min{ + * T(t) + * }, where max{...} and min{...} are + * robust to outliers. This calls the {@code maxTemperature} method of + * {@code c} and uses the baseline value at {@code 0} as the + * min{...} value. + * + * @param c the {@code ExperimentalData} object + * @see pulse.input.ExperimentalData.maxTemperature() + */ + public void estimateSignalRange(ExperimentalData c) { + var maxPoint = c.maxAdjustedSignal(); + final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); + properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); + } + + /** + * Calculates the vector argument defined on + * Rn + * to the scalar objective function for this {@code Problem}. To fill the + * vector with data, only those parameters from this {@code Problem} will be + * used which are defined by the {@code flags}, e.g. if the flag associated + * with the {@code HEAT_LOSS} keyword is set to false, its value will be + * skipped when creating the vector. + *

+ * + * @see listedTypes() + */ + + /* * TODO put relative bounds in a constant field Consider creating a Bounds * class, or putting them in the XML file - */ - - @Override - public void optimisationVector(ParameterVector output, List flags) { - - baseline.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case DIFFUSIVITY: - final double a = (double) properties.getDiffusivity().getValue(); - output.setTransform(i, new InvLenSqTransform(properties)); - output.setParameterBounds(i, new Segment(0.33 * a, 3.0 * a)); - output.set(i, a); - break; - case MAXTEMP: - final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output.set(i, signalHeight); - output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); - break; - case HEAT_LOSS: - final double Bi = (double) properties.getHeatLoss().getValue(); - setHeatLossParameter(output, i, Bi); - break; - case TIME_SHIFT: - output.set(i, (double) curve.getTimeShift().getValue()); - double magnitude = 0.25 * properties.timeFactor(); - output.setParameterBounds(i, new Segment(-magnitude, magnitude)); - break; - default: - continue; - } - - } - - } - - //TODO remove atanh transform and replace with abs - protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { - if(output.getTransform(i) == null) { - final double min = (double) def(HEAT_LOSS).getMinimum(); - final double max = (double) def(HEAT_LOSS).getMaximum(); - var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); - output.setTransform(i, StandardTransformations.ABS ); - output.setParameterBounds(i, bounds); - } - output.set(i, Bi); - } - - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @see listedTypes() - */ - - @Override - public void assign(ParameterVector params) throws SolverException { - baseline.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { - - double value = params.get(i); - var key = params.getIndex(i); - - switch (key) { - case DIFFUSIVITY: - properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i) ) ); - break; - case MAXTEMP: - properties.setMaximumTemperature( derive(MAXTEMP, value) ); - break; - case HEAT_LOSS: - properties.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i) ) ); - break; - case TIME_SHIFT: - curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); - break; - default: - continue; - } - } - - } - - /** - * Checks whether some 'advanced' details should stay hidden by the GUI when - * customising the {@code Problem} statement. - * - * @return {@code true} if the user does not want to see the details (by - * default), {@code false} otherwise. - */ - - @Override - public boolean areDetailsHidden() { - return Problem.hideDetailedAdjustment; - } - - /** - * Allows to either hide or display all 'advanced' settings for this - * {@code Problem}. - * - * @param b {@code true} if the user does not want to see the details, - * {@code false} otherwise. - */ - - public static void setDetailsHidden(boolean b) { - Problem.hideDetailedAdjustment = b; - } - - public String shortName() { - return getClass().getSimpleName(); - } - - /** - * Used for debugging. Initially, the nonlinear and two-dimensional problem - * statements are disabled, since they have not yet been thoroughly tested - * - * @return {@code true} if this problem statement has been enabled, - * {@code false} otherwise - */ - - public boolean isEnabled() { - return true; - } - - /** - * Constructs a {@code DiscretePulse} on the specified {@code grid} using the - * {@code Pulse} corresponding to this {@code Problem}. - * - * @param grid the grid - * @return a {@code DiscretePulse} objects constructed for this {@code Problem} - * and the {@code grid} - */ - - public DiscretePulse discretePulseOn(Grid grid) { - return new DiscretePulse(this, grid); - } - - /** - * Listed parameters include: - * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. - */ - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(MAXTEMP)); - list.add(def(DIFFUSIVITY)); - list.add(def(THICKNESS)); - list.add(def(HEAT_LOSS)); - list.add(instanceDescriptor); - return list; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - public ProblemComplexity getComplexity() { - return complexity; - } - - public void setComplexity(ProblemComplexity complexity) { - this.complexity = complexity; - } - - /** - * Return the {@code Baseline} of this {@code Problem}. - * - * @return the baseline - */ - - public Baseline getBaseline() { - return baseline; - } - - /** - * Sets a new baseline. Calls {@code apply(baseline)} on the - * {@code HeatingCurve} when done and sets the {@code parent} of the baseline to - * this object. - * - * @param baseline the new baseline. - * @see pulse.baseline.Baseline.apply(Baseline) - */ - - public void setBaseline(Baseline baseline) { - this.baseline = baseline; - if (!curve.isIncomplete()) - curve.apply(baseline); - baseline.setParent(this); - - var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); - if (searchTask != null) { - var experimentalData = searchTask.getExperimentalCurve(); - baseline.fitTo(experimentalData); - } - } - - public InstanceDescriptor getBaselineDescriptor() { - return instanceDescriptor; - } - - private void initBaseline() { - var baseline = instanceDescriptor.newInstance(Baseline.class); - setBaseline(baseline); - parameterListChanged(); - } - - public ThermalProperties getProperties() { - return properties; - } - - public final void setProperties(ThermalProperties properties) { - this.properties = properties; - this.properties.setParent(this); - } - - public abstract void initProperties(); - - public abstract void initProperties(ThermalProperties properties); - - public abstract Class defaultScheme(); - - public abstract boolean isReady(); - -} \ No newline at end of file + */ + @Override + public void optimisationVector(ParameterVector output, List flags) { + + baseline.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case DIFFUSIVITY: + final double a = (double) properties.getDiffusivity().getValue(); + output.setTransform(i, new InvLenSqTransform(properties)); + output.setParameterBounds(i, new Segment(0.33 * a, 3.0 * a)); + output.set(i, a); + break; + case MAXTEMP: + final double signalHeight = (double) properties.getMaximumTemperature().getValue(); + output.set(i, signalHeight); + output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); + break; + case HEAT_LOSS: + final double Bi = (double) properties.getHeatLoss().getValue(); + setHeatLossParameter(output, i, Bi); + break; + case TIME_SHIFT: + output.set(i, (double) curve.getTimeShift().getValue()); + double magnitude = 0.25 * properties.timeFactor(); + output.setParameterBounds(i, new Segment(-magnitude, magnitude)); + break; + default: + continue; + } + + } + + } + + //TODO remove atanh transform and replace with abs + protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { + if (output.getTransform(i) == null) { + final double min = (double) def(HEAT_LOSS).getMinimum(); + final double max = (double) def(HEAT_LOSS).getMaximum(); + var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); + output.setTransform(i, StandardTransformations.ABS); + output.setParameterBounds(i, bounds); + } + output.set(i, Bi); + } + + /** + * Assigns parameter values of this {@code Problem} using the optimisation + * vector {@code params}. Only those parameters will be updated, the types + * of which are listed as indices in the {@code params} vector. + * + * @see listedTypes() + */ + @Override + public void assign(ParameterVector params) throws SolverException { + baseline.assign(params); + for (int i = 0, size = params.dimension(); i < size; i++) { + + double value = params.get(i); + var key = params.getIndex(i); + + switch (key) { + case DIFFUSIVITY: + properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i))); + break; + case MAXTEMP: + properties.setMaximumTemperature(derive(MAXTEMP, value)); + break; + case HEAT_LOSS: + properties.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); + break; + case TIME_SHIFT: + curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); + break; + default: + continue; + } + } + + } + + /** + * Checks whether some 'advanced' details should stay hidden by the GUI when + * customising the {@code Problem} statement. + * + * @return {@code true} if the user does not want to see the details (by + * default), {@code false} otherwise. + */ + @Override + public boolean areDetailsHidden() { + return Problem.hideDetailedAdjustment; + } + + /** + * Allows to either hide or display all 'advanced' settings for this + * {@code Problem}. + * + * @param b {@code true} if the user does not want to see the details, + * {@code false} otherwise. + */ + public static void setDetailsHidden(boolean b) { + Problem.hideDetailedAdjustment = b; + } + + public String shortName() { + return getClass().getSimpleName(); + } + + /** + * Used for debugging. Initially, the nonlinear and two-dimensional problem + * statements are disabled, since they have not yet been thoroughly tested + * + * @return {@code true} if this problem statement has been enabled, + * {@code false} otherwise + */ + public boolean isEnabled() { + return true; + } + + /** + * Constructs a {@code DiscretePulse} on the specified {@code grid} using + * the {@code Pulse} corresponding to this {@code Problem}. + * + * @param grid the grid + * @return a {@code DiscretePulse} objects constructed for this + * {@code Problem} and the {@code grid} + */ + public DiscretePulse discretePulseOn(Grid grid) { + return new DiscretePulse(this, grid); + } + + /** + * Listed parameters include: + * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAXTEMP); + set.add(DIFFUSIVITY); + set.add(THICKNESS); + set.add(HEAT_LOSS); + return set; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + public ProblemComplexity getComplexity() { + return complexity; + } + + public void setComplexity(ProblemComplexity complexity) { + this.complexity = complexity; + } + + /** + * Return the {@code Baseline} of this {@code Problem}. + * + * @return the baseline + */ + public Baseline getBaseline() { + return baseline; + } + + /** + * Sets a new baseline. Calls {@code apply(baseline)} on the + * {@code HeatingCurve} when done and sets the {@code parent} of the + * baseline to this object. + * + * @param baseline the new baseline. + * @see pulse.baseline.Baseline.apply(Baseline) + */ + public void setBaseline(Baseline baseline) { + this.baseline = baseline; + if (!curve.isIncomplete()) { + curve.apply(baseline); + } + baseline.setParent(this); + + var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); + if (searchTask != null) { + var experimentalData = searchTask.getExperimentalCurve(); + baseline.fitTo(experimentalData); + } + } + + public InstanceDescriptor getBaselineDescriptor() { + return instanceDescriptor; + } + + private void initBaseline() { + var baseline = instanceDescriptor.newInstance(Baseline.class); + setBaseline(baseline); + parameterListChanged(); + } + + public ThermalProperties getProperties() { + return properties; + } + + public final void setProperties(ThermalProperties properties) { + this.properties = properties; + this.properties.setParent(this); + } + + public abstract void initProperties(); + + public abstract void initProperties(ThermalProperties properties); + + public abstract Class defaultScheme(); + + public abstract boolean isReady(); + +} diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index 40c38962..a0790c3e 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -9,12 +9,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperties; import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_COMBINED; import pulse.properties.Property; @@ -120,15 +122,14 @@ public void setFOVInner(NumericProperty fovInner) { } @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.addAll(super.listedTypes()); - list.add(def(HEAT_LOSS_SIDE)); - list.add(def(HEAT_LOSS_COMBINED)); - list.add(def(DIAMETER)); - list.add(def(FOV_OUTER)); - list.add(def(FOV_INNER)); - return list; + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(HEAT_LOSS_SIDE); + set.add(HEAT_LOSS_COMBINED); + set.add(DIAMETER); + set.add(FOV_OUTER); + set.add(FOV_INNER); + return set; } @Override diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 0e53c5c9..8cce847e 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -8,8 +8,7 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.*; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; @@ -17,332 +16,330 @@ import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; public class ThermalProperties extends PropertyHolder { - private double a; - private double l; - private double Bi; - private double signalHeight; - private double cP; - private double rho; - private double T; - private double emissivity; - - public final static double STEFAN_BOTLZMAN = 5.6703E-08; // Stephan-Boltzmann constant - - /** - * The corrected proportionality factor setting out the relation between - * the thermal diffusivity and the half-rise time of an {@code ExperimentalData} - * curve. - * - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - * @see Parker et al. - * Chem. Eng. Sci. 199 (2019) 546-551 - */ - - public final double PARKERS_COEFFICIENT = 0.1388; // in mm - - public ThermalProperties() { - super(); - a = (double) def(DIFFUSIVITY).getValue(); - l = (double) def(THICKNESS).getValue(); - Bi = (double) def(HEAT_LOSS).getValue(); - signalHeight = (double) def(MAXTEMP).getValue(); - T = (double) def(TEST_TEMPERATURE).getValue(); - emissivity = (double) def(EMISSIVITY).getValue(); - initListeners(); - fill(); - } - - public ThermalProperties(ThermalProperties p) { - super(); - this.l = p.l; - this.a = p.a; - this.Bi = p.Bi; - this.T = p.T; - this.emissivity = p.emissivity; - initListeners(); - fill(); - } - - private void fill() { - var rhoCurve = getDataset(StandartType.DENSITY); - var cpCurve = getDataset(StandartType.HEAT_CAPACITY); - if(rhoCurve != null) - rhoCurve.interpolateAt(T); - if(cpCurve != null) - cpCurve.interpolateAt(T); - } - - /** - * Calculates some or all of the following properties: - * Cp, ρ, &labmda;, - * ε. - *

- * These properties will be calculated only if the necessary - * {@code InterpolationDataset}s were previously loaded by the - * {@code TaskManager}. - *

- */ - - private void initListeners() { - - InterpolationDataset.addListener(e -> { - if(getParent() == null) - return; - - if (e == StandartType.DENSITY) { - rho = getDataset(StandartType.DENSITY).interpolateAt(T); - } - else if (e == StandartType.HEAT_CAPACITY) { - cP = getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); - } - - }); - - } - - public ThermalProperties copy() { - return new ThermalProperties(this); - } - - /** - * Hides optimiser directives - * @return true - */ - - @Override - public boolean areDetailsHidden() { - return true; - } - - /** - * Used to change the parameter values of this {@code Problem}. It is only - * allowed to use those types of {@code NumericPropery} that are listed by the - * {@code listedParameters()}. - * - * @see listedTypes() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - switch (type) { - case DIFFUSIVITY: - setDiffusivity(value); - break; - case MAXTEMP: - setMaximumTemperature(value); - break; - case THICKNESS: - setSampleThickness(value); - break; - case HEAT_LOSS: - setHeatLoss(value); - break; - case SPECIFIC_HEAT: - setSpecificHeat(value); - break; - case DENSITY: - setDensity(value); - break; - case TEST_TEMPERATURE: - setTestTemperature(value); - break; - default: - break; - } - } - - public void setHeatLoss(NumericProperty Bi) { - requireType(Bi, HEAT_LOSS); - this.Bi = (double) Bi.getValue(); - firePropertyChanged(this, Bi); - } - - public NumericProperty getDiffusivity() { - return derive(DIFFUSIVITY, a); - } - - public void setDiffusivity(NumericProperty a) { - requireType(a, DIFFUSIVITY); - this.a = (double) a.getValue(); - firePropertyChanged(this, a); - } - - public NumericProperty getMaximumTemperature() { - return derive(MAXTEMP, signalHeight); - } - - public void setMaximumTemperature(NumericProperty maxTemp) { - requireType(maxTemp, MAXTEMP); - this.signalHeight = (double) maxTemp.getValue(); - firePropertyChanged(this, maxTemp); - } - - public NumericProperty getSampleThickness() { - return derive(THICKNESS, l); - } - - public void setSampleThickness(NumericProperty l) { - requireType(l, THICKNESS); - this.l = (double) l.getValue(); - firePropertyChanged(this, l); - } - - /** - *

- * Assuming that Bi1 = Bi2, returns the value - * of Bi1. If - * Bi1 = Bi2, this will print a warning - * message (but will not throw an exception) - *

- * - * @return Bi1 as a {@code NumericProperty} - */ - - public NumericProperty getHeatLoss() { - return derive(HEAT_LOSS, Bi); - } - - public NumericProperty getSpecificHeat() { - return derive(SPECIFIC_HEAT, cP); - } - - public void setSpecificHeat(NumericProperty cP) { - requireType(cP, SPECIFIC_HEAT); - this.cP = (double) cP.getValue(); - } - - public NumericProperty getDensity() { - return derive(DENSITY, rho); - } - - public void setDensity(NumericProperty p) { - requireType(p, DENSITY); - this.rho = (double) (p.getValue()); - } - - public NumericProperty getTestTemperature() { - return derive(TEST_TEMPERATURE, T); - } - - public void setTestTemperature(NumericProperty T) { - requireType(T, TEST_TEMPERATURE); - this.T = (double) T.getValue(); - - var heatCapacity = getDataset(HEAT_CAPACITY); - - if (heatCapacity != null) - cP = heatCapacity.interpolateAt(this.T); - - var density = getDataset(StandartType.DENSITY); - - if (density != null) - rho = density.interpolateAt(this.T); - - firePropertyChanged(this, T); - } - - /** - * Listed parameters include: - * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. - */ - - @Override - public List listedTypes() { - List list = new ArrayList(); - list.add(def(MAXTEMP)); - list.add(def(DIFFUSIVITY)); - list.add(def(THICKNESS)); - list.add(def(HEAT_LOSS)); - list.add(def(DENSITY)); - list.add(def(SPECIFIC_HEAT)); - return list; - } - - public final double thermalConductivity() { - return a * cP * rho; - } - - public NumericProperty getThermalConductivity() { - return derive(CONDUCTIVITY, thermalConductivity()); - } - - public void emissivity() { - setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); - } - - public double maxBiot() { - double lambda = thermalConductivity(); - return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; - } - - public double biot() { - double lambda = thermalConductivity(); - return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; - } - - /** - * Performs simple calculation of the l2/a factor - * that is commonly used to evaluate the dimensionless time - * {@code t/timeFactor}. - * - * @return the time factor - */ - - public double timeFactor() { - return l * l / a; - } - - /** - * Calculates the half-rise time t1/2 of {@code c} and uses it - * to estimate the thermal diffusivity of this problem: - * a={@value PARKERS_COEFFICIENT}*l2/t1/2. - * - * @param c the {@code ExperimentalData} used to estimate the thermal - * diffusivity value - * @see pulse.input.ExperimentalData.halfRiseTime() - */ - - public void useTheoreticalEstimates(ExperimentalData c) { - final double t0 = c.halfRiseTime(); - this.a = PARKERS_COEFFICIENT * l * l / t0; - if (areThermalPropertiesLoaded()) - Bi = biot(); - } - - public final boolean areThermalPropertiesLoaded() { - return (Double.compare(cP, 0.0) > 0 && Double.compare(rho, 0.0) > 0); - } - - public double maximumHeating(Pulse2D pulse) { - final double Q = (double) pulse.getLaserEnergy().getValue(); - final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * cP * rho); - } - - public NumericProperty getEmissivity() { - return derive(EMISSIVITY, emissivity); - } - - public void setEmissivity(NumericProperty e) { - requireType(e, EMISSIVITY); - this.emissivity = (double) e.getValue(); - setHeatLoss(derive(HEAT_LOSS, biot())); - } - - @Override - public String getDescriptor() { - return "Sample Thermo-Physical Properties"; - } - - @Override - public String toString() { - return "Show Details..."; - } - -} \ No newline at end of file + private double a; + private double l; + private double Bi; + private double signalHeight; + private double cP; + private double rho; + private double T; + private double emissivity; + + public final static double STEFAN_BOTLZMAN = 5.6703E-08; // Stephan-Boltzmann constant + + /** + * The corrected proportionality factor setting out the relation + * between the thermal diffusivity and the half-rise time of an + * {@code ExperimentalData} curve. + * + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + * @see Parker et + * al. + * Chem. Eng. Sci. 199 (2019) 546-551 + */ + public final double PARKERS_COEFFICIENT = 0.1388; // in mm + + public ThermalProperties() { + super(); + a = (double) def(DIFFUSIVITY).getValue(); + l = (double) def(THICKNESS).getValue(); + Bi = (double) def(HEAT_LOSS).getValue(); + signalHeight = (double) def(MAXTEMP).getValue(); + T = (double) def(TEST_TEMPERATURE).getValue(); + emissivity = (double) def(EMISSIVITY).getValue(); + initListeners(); + fill(); + } + + public ThermalProperties(ThermalProperties p) { + super(); + this.l = p.l; + this.a = p.a; + this.Bi = p.Bi; + this.T = p.T; + this.emissivity = p.emissivity; + initListeners(); + fill(); + } + + private void fill() { + var rhoCurve = getDataset(StandartType.DENSITY); + var cpCurve = getDataset(StandartType.HEAT_CAPACITY); + if (rhoCurve != null) { + rhoCurve.interpolateAt(T); + } + if (cpCurve != null) { + cpCurve.interpolateAt(T); + } + } + + /** + * Calculates some or all of the following properties: + * Cp, ρ, &labmda;, + * ε. + *

+ * These properties will be calculated only if the necessary + * {@code InterpolationDataset}s were previously loaded by the + * {@code TaskManager}. + *

+ */ + private void initListeners() { + + InterpolationDataset.addListener(e -> { + if (getParent() == null) { + return; + } + + if (e == StandartType.DENSITY) { + rho = getDataset(StandartType.DENSITY).interpolateAt(T); + } else if (e == StandartType.HEAT_CAPACITY) { + cP = getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); + } + + }); + + } + + public ThermalProperties copy() { + return new ThermalProperties(this); + } + + /** + * Hides optimiser directives + * + * @return true + */ + @Override + public boolean areDetailsHidden() { + return true; + } + + /** + * Used to change the parameter values of this {@code Problem}. It is only + * allowed to use those types of {@code NumericPropery} that are listed by + * the {@code listedParameters()}. + * + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + switch (type) { + case DIFFUSIVITY: + setDiffusivity(value); + break; + case MAXTEMP: + setMaximumTemperature(value); + break; + case THICKNESS: + setSampleThickness(value); + break; + case HEAT_LOSS: + setHeatLoss(value); + break; + case SPECIFIC_HEAT: + setSpecificHeat(value); + break; + case DENSITY: + setDensity(value); + break; + case TEST_TEMPERATURE: + setTestTemperature(value); + break; + default: + break; + } + } + + public void setHeatLoss(NumericProperty Bi) { + requireType(Bi, HEAT_LOSS); + this.Bi = (double) Bi.getValue(); + firePropertyChanged(this, Bi); + } + + public NumericProperty getDiffusivity() { + return derive(DIFFUSIVITY, a); + } + + public void setDiffusivity(NumericProperty a) { + requireType(a, DIFFUSIVITY); + this.a = (double) a.getValue(); + firePropertyChanged(this, a); + } + + public NumericProperty getMaximumTemperature() { + return derive(MAXTEMP, signalHeight); + } + + public void setMaximumTemperature(NumericProperty maxTemp) { + requireType(maxTemp, MAXTEMP); + this.signalHeight = (double) maxTemp.getValue(); + firePropertyChanged(this, maxTemp); + } + + public NumericProperty getSampleThickness() { + return derive(THICKNESS, l); + } + + public void setSampleThickness(NumericProperty l) { + requireType(l, THICKNESS); + this.l = (double) l.getValue(); + firePropertyChanged(this, l); + } + + /** + *

+ * Assuming that Bi1 = Bi2, returns the + * value of Bi1. If Bi1 = + * Bi2, this will print a warning message (but will not + * throw an exception) + *

+ * + * @return Bi1 as a {@code NumericProperty} + */ + public NumericProperty getHeatLoss() { + return derive(HEAT_LOSS, Bi); + } + + public NumericProperty getSpecificHeat() { + return derive(SPECIFIC_HEAT, cP); + } + + public void setSpecificHeat(NumericProperty cP) { + requireType(cP, SPECIFIC_HEAT); + this.cP = (double) cP.getValue(); + } + + public NumericProperty getDensity() { + return derive(DENSITY, rho); + } + + public void setDensity(NumericProperty p) { + requireType(p, DENSITY); + this.rho = (double) (p.getValue()); + } + + public NumericProperty getTestTemperature() { + return derive(TEST_TEMPERATURE, T); + } + + public void setTestTemperature(NumericProperty T) { + requireType(T, TEST_TEMPERATURE); + this.T = (double) T.getValue(); + + var heatCapacity = getDataset(HEAT_CAPACITY); + + if (heatCapacity != null) { + cP = heatCapacity.interpolateAt(this.T); + } + + var density = getDataset(StandartType.DENSITY); + + if (density != null) { + rho = density.interpolateAt(this.T); + } + + firePropertyChanged(this, T); + } + + /** + * Listed parameters include: + * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAXTEMP); + set.add(DIFFUSIVITY); + set.add(THICKNESS); + set.add(HEAT_LOSS); + set.add(DENSITY); + set.add(SPECIFIC_HEAT); + return set; + } + + public final double thermalConductivity() { + return a * cP * rho; + } + + public NumericProperty getThermalConductivity() { + return derive(CONDUCTIVITY, thermalConductivity()); + } + + public void emissivity() { + setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); + } + + public double maxBiot() { + double lambda = thermalConductivity(); + return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; + } + + public double biot() { + double lambda = thermalConductivity(); + return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; + } + + /** + * Performs simple calculation of the l2/a + * factor that is commonly used to evaluate the dimensionless time + * {@code t/timeFactor}. + * + * @return the time factor + */ + public double timeFactor() { + return l * l / a; + } + + /** + * Calculates the half-rise time t1/2 of {@code c} and + * uses it to estimate the thermal diffusivity of this problem: + * a={@value PARKERS_COEFFICIENT}*l2/t1/2. + * + * @param c the {@code ExperimentalData} used to estimate the thermal + * diffusivity value + * @see pulse.input.ExperimentalData.halfRiseTime() + */ + public void useTheoreticalEstimates(ExperimentalData c) { + final double t0 = c.getHalfTime(); + this.a = PARKERS_COEFFICIENT * l * l / t0; + if (areThermalPropertiesLoaded()) { + Bi = biot(); + } + } + + public final boolean areThermalPropertiesLoaded() { + return (Double.compare(cP, 0.0) > 0 && Double.compare(rho, 0.0) > 0); + } + + public double maximumHeating(Pulse2D pulse) { + final double Q = (double) pulse.getLaserEnergy().getValue(); + final double dLas = (double) pulse.getSpotDiameter().getValue(); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * cP * rho); + } + + public NumericProperty getEmissivity() { + return derive(EMISSIVITY, emissivity); + } + + public void setEmissivity(NumericProperty e) { + requireType(e, EMISSIVITY); + this.emissivity = (double) e.getValue(); + setHeatLoss(derive(HEAT_LOSS, biot())); + } + + @Override + public String getDescriptor() { + return "Sample Thermo-Physical Properties"; + } + + @Override + public String toString() { + return "Show Details..."; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 7077b288..f6304e3f 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -10,143 +10,150 @@ import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DENSITY; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; +import static pulse.properties.NumericPropertyKeyword.THICKNESS; import pulse.properties.Property; public class ThermoOpticalProperties extends ThermalProperties { - private double opticalThickness; - private double planckNumber; - private double scatteringAlbedo; - private double scatteringAnisotropy; - - public ThermoOpticalProperties() { - super(); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - } - - public ThermoOpticalProperties(ThermalProperties p) { - super(p); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - } - - public ThermoOpticalProperties(ThermoOpticalProperties p) { - super(p); - this.opticalThickness = p.opticalThickness; - this.planckNumber = p.planckNumber; - this.scatteringAlbedo = p.scatteringAlbedo; - this.scatteringAnisotropy = p.scatteringAnisotropy; - } - - @Override - public ThermalProperties copy() { - return new ThermoOpticalProperties(this); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - super.set(type, value); - - switch (type) { - case PLANCK_NUMBER: - setPlanckNumber(value); - break; - case OPTICAL_THICKNESS: - setOpticalThickness(value); - break; - case SCATTERING_ALBEDO: - setScatteringAlbedo(value); - break; - case SCATTERING_ANISOTROPY: - setScatteringAnisotropy(value); - break; - default: - break; - } - - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(PLANCK_NUMBER)); - list.add(def(OPTICAL_THICKNESS)); - list.add(def(SCATTERING_ALBEDO)); - list.add(def(SCATTERING_ANISOTROPY)); - return list; - } - - public double maxNp() { - final double l = (double) getSampleThickness().getValue(); - final double T = (double) getTestTemperature().getValue(); - return thermalConductivity() / (4.0 * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); - } - - public NumericProperty getOpticalThickness() { - return derive(OPTICAL_THICKNESS, opticalThickness); - } - - public void setOpticalThickness(NumericProperty tau0) { - requireType(tau0, OPTICAL_THICKNESS); - this.opticalThickness = (double) tau0.getValue(); - firePropertyChanged(this, tau0); - } - - public NumericProperty getPlanckNumber() { - return derive(PLANCK_NUMBER, planckNumber); - } - - public void setPlanckNumber(NumericProperty planckNumber) { - requireType(planckNumber, PLANCK_NUMBER); - this.planckNumber = (double) planckNumber.getValue(); - firePropertyChanged(this, planckNumber); - } - - public NumericProperty getScatteringAnisostropy() { - return derive(SCATTERING_ANISOTROPY, scatteringAnisotropy); - } - - public void setScatteringAnisotropy(NumericProperty A1) { - requireType(A1, SCATTERING_ANISOTROPY); - this.scatteringAnisotropy = (double) A1.getValue(); - firePropertyChanged(this, A1); - } - - public NumericProperty getScatteringAlbedo() { - return derive(SCATTERING_ALBEDO, scatteringAlbedo); - } - - public void setScatteringAlbedo(NumericProperty omega0) { - requireType(omega0, SCATTERING_ALBEDO); - this.scatteringAlbedo = (double) omega0.getValue(); - firePropertyChanged(this, omega0); - } - - @Override - public void useTheoreticalEstimates(ExperimentalData c) { - super.useTheoreticalEstimates(c); - if ( areThermalPropertiesLoaded() ) { - final double nSq = 4; - final double lambda = thermalConductivity(); - final double l = (double) getSampleThickness().getValue(); - final double T = (double) getTestTemperature().getValue(); - final double nP = lambda / (4.0 * nSq * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); - setPlanckNumber(derive(PLANCK_NUMBER, nP)); - } - } - - @Override - public String getDescriptor() { - return "Thermo-Physical & Optical Properties"; - } - -} \ No newline at end of file + private double opticalThickness; + private double planckNumber; + private double scatteringAlbedo; + private double scatteringAnisotropy; + + public ThermoOpticalProperties() { + super(); + this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + } + + public ThermoOpticalProperties(ThermalProperties p) { + super(p); + this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + } + + public ThermoOpticalProperties(ThermoOpticalProperties p) { + super(p); + this.opticalThickness = p.opticalThickness; + this.planckNumber = p.planckNumber; + this.scatteringAlbedo = p.scatteringAlbedo; + this.scatteringAnisotropy = p.scatteringAnisotropy; + } + + @Override + public ThermalProperties copy() { + return new ThermoOpticalProperties(this); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + super.set(type, value); + + switch (type) { + case PLANCK_NUMBER: + setPlanckNumber(value); + break; + case OPTICAL_THICKNESS: + setOpticalThickness(value); + break; + case SCATTERING_ALBEDO: + setScatteringAlbedo(value); + break; + case SCATTERING_ANISOTROPY: + setScatteringAnisotropy(value); + break; + default: + break; + } + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(PLANCK_NUMBER); + set.add(OPTICAL_THICKNESS); + set.add(SCATTERING_ALBEDO); + set.add(SCATTERING_ANISOTROPY); + return set; + } + + public double maxNp() { + final double l = (double) getSampleThickness().getValue(); + final double T = (double) getTestTemperature().getValue(); + return thermalConductivity() / (4.0 * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); + } + + public NumericProperty getOpticalThickness() { + return derive(OPTICAL_THICKNESS, opticalThickness); + } + + public void setOpticalThickness(NumericProperty tau0) { + requireType(tau0, OPTICAL_THICKNESS); + this.opticalThickness = (double) tau0.getValue(); + firePropertyChanged(this, tau0); + } + + public NumericProperty getPlanckNumber() { + return derive(PLANCK_NUMBER, planckNumber); + } + + public void setPlanckNumber(NumericProperty planckNumber) { + requireType(planckNumber, PLANCK_NUMBER); + this.planckNumber = (double) planckNumber.getValue(); + firePropertyChanged(this, planckNumber); + } + + public NumericProperty getScatteringAnisostropy() { + return derive(SCATTERING_ANISOTROPY, scatteringAnisotropy); + } + + public void setScatteringAnisotropy(NumericProperty A1) { + requireType(A1, SCATTERING_ANISOTROPY); + this.scatteringAnisotropy = (double) A1.getValue(); + firePropertyChanged(this, A1); + } + + public NumericProperty getScatteringAlbedo() { + return derive(SCATTERING_ALBEDO, scatteringAlbedo); + } + + public void setScatteringAlbedo(NumericProperty omega0) { + requireType(omega0, SCATTERING_ALBEDO); + this.scatteringAlbedo = (double) omega0.getValue(); + firePropertyChanged(this, omega0); + } + + @Override + public void useTheoreticalEstimates(ExperimentalData c) { + super.useTheoreticalEstimates(c); + if (areThermalPropertiesLoaded()) { + final double nSq = 4; + final double lambda = thermalConductivity(); + final double l = (double) getSampleThickness().getValue(); + final double T = (double) getTestTemperature().getValue(); + final double nP = lambda / (4.0 * nSq * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); + setPlanckNumber(derive(PLANCK_NUMBER, nP)); + } + } + + @Override + public String getDescriptor() { + return "Thermo-Physical & Optical Properties"; + } + +} From c13355cba697770f728c8f4d2971a9b34033dac4 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 12:17:53 +0100 Subject: [PATCH 075/116] Fixed laser/light pulse handling NumericPulse now calculates a baseline using the pulse diode data prior to the pulse. The baseline is then subtracted from the pulse diode data. Previously no baseline was calcualted, which led to erroneous calculation. Interpolation for numeric pulses now changed to Akima since second derivative is often rapidly varying. Added a new PropertyHolderListener for the freshly created Pulse objects which updates the ExperimentaData's Range lower bound when the pulse width is updated. Added a check to setPulseWidth(), which determines whether the new pulse width is actually different from the previously set. In the latter case, this invokes the listeners when finished. In addition, another check prevents from setting a pulse width greater than two half-times. Converted listedTypes to listedKeywords() --- .../pulse/problem/laser/NumericPulse.java | 253 +++++++------- .../java/pulse/problem/statements/Pulse.java | 323 ++++++++++-------- .../pulse/problem/statements/Pulse2D.java | 123 ++++--- 3 files changed, 379 insertions(+), 320 deletions(-) diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 88539768..60989bf0 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -4,7 +4,7 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import org.apache.commons.math3.analysis.UnivariateFunction; -import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; import pulse.input.ExperimentalData; import pulse.problem.statements.Problem; @@ -12,131 +12,142 @@ import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; +import pulse.baseline.FlatBaseline; + /** - * A numeric pulse is given by a set of discrete {@code NumericPulseData} measured - * independelty using a pulse diode. + * A numeric pulse is given by a set of discrete {@code NumericPulseData} + * measured independently using a pulse diode. + * * @see pulse.problem.laser.NumericPulseData * */ - public class NumericPulse extends PulseTemporalShape { - private NumericPulseData pulseData; - private UnivariateFunction interpolation; - private double adjustedPulseWidth; - - public NumericPulse() { - //intentionally blank - } - - /** - * Copy constructor - * @param pulse another numeric pulse, the data of which will be copied - */ - - public NumericPulse(NumericPulse pulse) { - super(pulse); - this.pulseData = new NumericPulseData(pulseData); - } - - /** - * Defines the pulse width as the last element of the time sequence contained in {@code NumericPulseData}. - * Calls {@code super.init}, then interpolates the input pulse using spline functions and normalises the - * output. - * @see normalise() - * - */ - - @Override - public void init(ExperimentalData data, DiscretePulse pulse) { - pulseData = data.getMetadata().getPulseData(); - - var problem = ((SearchTask)data.getParent()).getCurrentCalculation().getProblem(); - setPulseWidth(problem); - - double timeFactor = problem.getProperties().timeFactor(); - - super.init(data, pulse); - - doInterpolation( timeFactor ); - - normalise(problem); - } - - /** - * Checks that the area of the pulse curve is unity (within a small error margin). - * If this is {@code false}, re-scales the numeric data using {@code 1/area} as the scaling factor. - * @param problem defines the {@code timeFactor} needed for re-building the interpolation - * @see pulse.problem.laser.NumericPulseData.scale() - */ - - public void normalise(Problem problem) { - - final double EPS = 1E-2; - double timeFactor = problem.getProperties().timeFactor(); - - for( double area = area() ; Math.abs(area - 1.0) > EPS; area = area() ) { - pulseData.scale( 1.0 / area ); - doInterpolation( timeFactor ); - } - - } - - private void setPulseWidth(Problem problem) { - var timeSequence = pulseData.getTimeSequence(); - double pulseWidth = timeSequence.get( timeSequence.size() - 1 ); - - var pulseObject = problem.getPulse(); - pulseObject.setPulseWidth( derive(PULSE_WIDTH, pulseWidth) ); - - } - - private void doInterpolation(double timeFactor) { - var interpolator = new SplineInterpolator(); - - var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); - adjustedPulseWidth = timeList[timeList.length - 1]; - var powerList = pulseData.getSignalData(); - - interpolation = interpolator.interpolate(timeList, - powerList.stream().mapToDouble(d -> d).toArray()); - - } - - /** - * If the argument is less than the pulse width, uses the spline function to interpolated the pulse - * function at {@code time}. Otherwise returns zero. - */ - - @Override - public double evaluateAt(double time) { - return time > adjustedPulseWidth ? 0.0 : interpolation.value(time); - } - - @Override - public PulseTemporalShape copy() { - return new NumericPulse(); - } - - /** - * Does not define any property. - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // TODO Auto-generated method stub - } - - public NumericPulseData getData() { - return pulseData; - } - - public void setData(NumericPulseData pulseData) { - this.pulseData = pulseData; - } - - public UnivariateFunction getInterpolation() { - return interpolation; - } + private NumericPulseData pulseData; + private UnivariateFunction interpolation; + private double adjustedPulseWidth; + + public NumericPulse() { + //intentionally blank + } + + /** + * Copy constructor + * + * @param pulse another numeric pulse, the data of which will be copied + */ + public NumericPulse(NumericPulse pulse) { + super(pulse); + this.pulseData = new NumericPulseData(pulseData); + } + + /** + * Defines the pulse width as the last element of the time sequence + * contained in {@code NumericPulseData}. Calls {@code super.init}, then + * interpolates the input pulse using spline functions and normalises the + * output. + * + * @see normalise() + * + */ + @Override + public void init(ExperimentalData data, DiscretePulse pulse) { + pulseData = data.getMetadata().getPulseData(); + + //subtracts a horizontal baseline from the pulse data + var baseline = new FlatBaseline(); + baseline.fitNegative(pulseData); + + for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) + pulseData.setSignalAt(i, + pulseData.signalAt(i) - baseline.valueAt(pulseData.timeAt(i))); + + var problem = ((SearchTask) data.getParent()).getCurrentCalculation().getProblem(); + setPulseWidth(problem); + + double timeFactor = problem.getProperties().timeFactor(); + + super.init(data, pulse); + + doInterpolation(timeFactor); + + normalise(problem); + } + + /** + * Checks that the area of the pulse curve is unity (within a small error + * margin). If this is {@code false}, re-scales the numeric data using + * {@code 1/area} as the scaling factor. + * + * @param problem defines the {@code timeFactor} needed for re-building the + * interpolation + * @see pulse.problem.laser.NumericPulseData.scale() + */ + public void normalise(Problem problem) { + + final double EPS = 1E-2; + double timeFactor = problem.getProperties().timeFactor(); + + for (double area = area(); Math.abs(area - 1.0) > EPS; area = area()) { + pulseData.scale(1.0 / area); + doInterpolation(timeFactor); + } + + } + + private void setPulseWidth(Problem problem) { + var timeSequence = pulseData.getTimeSequence(); + double pulseWidth = timeSequence.get(timeSequence.size() - 1); + + var pulseObject = problem.getPulse(); + pulseObject.setPulseWidth(derive(PULSE_WIDTH, pulseWidth)); + + } + + private void doInterpolation(double timeFactor) { + var interpolator = new AkimaSplineInterpolator(); + + var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); + adjustedPulseWidth = timeList[timeList.length - 1]; + var powerList = pulseData.getSignalData(); + + interpolation = interpolator.interpolate(timeList, + powerList.stream().mapToDouble(d -> d).toArray()); + + } + + /** + * If the argument is less than the pulse width, uses a spline to + * interpolate the pulse data at {@code time}. Otherwise returns zero. + */ + @Override + public double evaluateAt(double time) { + return time > adjustedPulseWidth ? 0.0 : interpolation.value(time); + } + + @Override + public PulseTemporalShape copy() { + return new NumericPulse(); + } + + /** + * Does not define any property. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // TODO Auto-generated method stub + } + + public NumericPulseData getData() { + return pulseData; + } + + public void setData(NumericPulseData pulseData) { + this.pulseData = pulseData; + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } } diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 730a5de2..cec8937c 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -6,157 +6,206 @@ import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; -import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; +import pulse.tasks.SearchTask; import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; /** * A {@code Pulse} stores the parameters of the laser pulse, but does not * provide the calculation facilities. - * + * * @see pulse.problem.laser.DiscretePulse - * + * */ - public class Pulse extends PropertyHolder { - private double pulseWidth; - private double laserEnergy; - - private PulseTemporalShape pulseShape; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Pulse Shape Selector", PulseTemporalShape.class); - - /** - * Creates a {@code Pulse} with default values of pulse width and laser spot - * diameter (as per XML specification) and with a default pulse temporal shape (rectangular). - * - */ - - public Pulse() { - super(); - pulseWidth = (double) def(PULSE_WIDTH).getValue(); - laserEnergy = (double) def(LASER_ENERGY).getValue(); - instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); - initShape(); - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - } - - /** - * Copy constructor - * - * @param p the pulse, parameters of which will be copied. - */ - - public Pulse(Pulse p) { - super(); - this.pulseShape = p.getPulseShape(); - this.pulseWidth = p.pulseWidth; - this.laserEnergy = p.laserEnergy; - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - } - - public Pulse copy() { - return new Pulse(this); - } - - public void initFrom(Pulse pulse) { - this.pulseWidth = pulse.pulseWidth; - this.laserEnergy = pulse.laserEnergy; - this.pulseShape = pulse.pulseShape; - } - - private void initShape() { - setPulseShape(instanceDescriptor.newInstance(PulseTemporalShape.class)); - parameterListChanged(); - } - - public NumericProperty getPulseWidth() { - return derive(PULSE_WIDTH, pulseWidth); - } - - public void setPulseWidth(NumericProperty pulseWidth) { - requireType(pulseWidth, PULSE_WIDTH); - this.pulseWidth = (double) pulseWidth.getValue(); - - if(pulseWidth.compareTo(this.getPulseWidth()) != 0) - firePropertyChanged(this, pulseWidth); - } - - public NumericProperty getLaserEnergy() { - return derive(LASER_ENERGY, laserEnergy); - } - - public void setLaserEnergy(NumericProperty laserEnergy) { - requireType(laserEnergy, LASER_ENERGY); - this.laserEnergy = (double) laserEnergy.getValue(); - firePropertyChanged(this, laserEnergy); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(getPulseShape()); - sb.append(" ; "); - sb.append(getPulseWidth()); - sb.append(" ; "); - sb.append(getLaserEnergy()); - return sb.toString(); - } - - /** - * The listed parameters for {@code Pulse} are: - * PulseShape, PULSE_WIDTH, SPOT_DIAMETER. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(PULSE_WIDTH)); - list.add(def(LASER_ENERGY)); - list.add(instanceDescriptor); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if(type == PULSE_WIDTH) - setPulseWidth(property); - else if(type == LASER_ENERGY) - setLaserEnergy(property); - } - - public InstanceDescriptor getPulseDescriptor() { - return instanceDescriptor; - } - - public void setPulseDescriptor(InstanceDescriptor shapeDescriptor) { - this.instanceDescriptor = shapeDescriptor; - //TODO - initShape(); - } - - public PulseTemporalShape getPulseShape() { - return pulseShape; - } - - public void setPulseShape(PulseTemporalShape pulseShape) { - this.pulseShape = pulseShape; - pulseShape.setParent(this); - } - -} \ No newline at end of file + private double pulseWidth; + private double laserEnergy; + + private PulseTemporalShape pulseShape; + + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Pulse Shape Selector", PulseTemporalShape.class); + + /** + * Creates a {@code Pulse} with default values of pulse width and laser spot + * diameter (as per XML specification) and with a default pulse temporal + * shape (rectangular). + * + */ + public Pulse() { + super(); + pulseWidth = (double) def(PULSE_WIDTH).getValue(); + laserEnergy = (double) def(LASER_ENERGY).getValue(); + instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); + initShape(); + instanceDescriptor.addListener(() -> { + initShape(); + this.firePropertyChanged(instanceDescriptor, instanceDescriptor); + }); + addListeners(); + } + + /** + * Copy constructor + * + * @param p the pulse, parameters of which will be copied. + */ + public Pulse(Pulse p) { + super(); + this.pulseShape = p.getPulseShape(); + this.pulseWidth = p.pulseWidth; + this.laserEnergy = p.laserEnergy; + addListeners(); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> { + initShape(); + this.firePropertyChanged(instanceDescriptor, instanceDescriptor); + }); + addListener((PropertyEvent event) -> { + + //when a property of the pulse is changed + if (event.getProperty() instanceof NumericProperty) { + + var np = (NumericProperty) event.getProperty(); + var type = np.getType(); + + //when this property is a pulse width + if (type == NumericPropertyKeyword.PULSE_WIDTH) { + //find the specific SearchTask ancestor + var corrTask = (SearchTask) specificAncestor(SearchTask.class); + //new lower bound + NumericProperty pw = NumericProperties + .derive(NumericPropertyKeyword.LOWER_BOUND, + (Number) np.getValue()); + //update lower bound of the range for that SearchTask + corrTask.getExperimentalCurve().getRange() + .setLowerBound(pw); + } + + } + + }); + } + + public Pulse copy() { + return new Pulse(this); + } + + public void initFrom(Pulse pulse) { + this.pulseWidth = pulse.pulseWidth; + this.laserEnergy = pulse.laserEnergy; + this.pulseShape = pulse.pulseShape; + } + + private void initShape() { + setPulseShape(instanceDescriptor.newInstance(PulseTemporalShape.class)); + parameterListChanged(); + } + + public NumericProperty getPulseWidth() { + return derive(PULSE_WIDTH, pulseWidth); + } + + public void setPulseWidth(NumericProperty pulseWidth) { + requireType(pulseWidth, PULSE_WIDTH); + + double newValue = (double) pulseWidth.getValue(); + final double EPS = 1E-3; + + //do not update -- if new value is the same as the previous one + if (Math.abs((newValue - this.pulseWidth) + / (this.pulseWidth + newValue)) < EPS) { + return; + } + + //validate -- do not update if the new pulse width is greater than 2 half-times + var task = (SearchTask) this.specificAncestor(SearchTask.class); + var data = task.getExperimentalCurve(); + if (newValue > 0 && newValue < 2.0 * data.getHalfTime()) { + this.pulseWidth = (double) pulseWidth.getValue(); + firePropertyChanged(this, pulseWidth); + } + } + + public NumericProperty getLaserEnergy() { + return derive(LASER_ENERGY, laserEnergy); + } + + public void setLaserEnergy(NumericProperty laserEnergy) { + requireType(laserEnergy, LASER_ENERGY); + this.laserEnergy = (double) laserEnergy.getValue(); + firePropertyChanged(this, laserEnergy); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getPulseShape()); + sb.append(" ; "); + sb.append(getPulseWidth()); + sb.append(" ; "); + sb.append(getLaserEnergy()); + return sb.toString(); + } + + /** + * The listed parameters for {@code Pulse} are: + * PulseShape, PULSE_WIDTH, SPOT_DIAMETER. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(PULSE_WIDTH); + set.add(LASER_ENERGY); + return set; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == PULSE_WIDTH) { + setPulseWidth(property); + } else if (type == LASER_ENERGY) { + setLaserEnergy(property); + } + } + + public InstanceDescriptor getPulseDescriptor() { + return instanceDescriptor; + } + + public void setPulseDescriptor(InstanceDescriptor shapeDescriptor) { + this.instanceDescriptor = shapeDescriptor; + //TODO + initShape(); + } + + public PulseTemporalShape getPulseShape() { + return pulseShape; + } + + public void setPulseShape(PulseTemporalShape pulseShape) { + this.pulseShape = pulseShape; + pulseShape.setParent(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index 0145240e..16895b60 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -5,81 +5,80 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; public class Pulse2D extends Pulse { - private double spotDiameter; + private double spotDiameter; - /** - * Creates a {@code Pulse} with default values of pulse width and laser spot - * diameter (as per XML specification). - * - */ + /** + * Creates a {@code Pulse} with default values of pulse width and laser spot + * diameter (as per XML specification). + * + */ + public Pulse2D() { + super(); + spotDiameter = (double) def(SPOT_DIAMETER).getValue(); + } - public Pulse2D() { - super(); - spotDiameter = (double) def(SPOT_DIAMETER).getValue(); - } + /** + * Copy constructor + * + * @param p the pulse, parameters of which will be copied. + */ + public Pulse2D(Pulse p) { + super(p); + this.spotDiameter = p instanceof Pulse2D ? ((Pulse2D) p).spotDiameter : (double) def(SPOT_DIAMETER).getValue(); + } - /** - * Copy constructor - * - * @param p the pulse, parameters of which will be copied. - */ + @Override + public void initFrom(Pulse pulse) { + super.initFrom(pulse); + if (pulse instanceof Pulse2D) { + this.spotDiameter = ((Pulse2D) pulse).spotDiameter; + } + } - public Pulse2D(Pulse p) { - super(p); - this.spotDiameter = p instanceof Pulse2D ? ((Pulse2D) p).spotDiameter : (double) def(SPOT_DIAMETER).getValue(); - } - - @Override - public void initFrom(Pulse pulse) { - super.initFrom(pulse); - if(pulse instanceof Pulse2D) - this.spotDiameter = ((Pulse2D) pulse).spotDiameter; - } - - @Override - public Pulse copy() { - return new Pulse2D(this); - } + @Override + public Pulse copy() { + return new Pulse2D(this); + } - public NumericProperty getSpotDiameter() { - return derive(SPOT_DIAMETER, spotDiameter); - } + public NumericProperty getSpotDiameter() { + return derive(SPOT_DIAMETER, spotDiameter); + } - public void setSpotDiameter(NumericProperty spotDiameter) { - requireType(spotDiameter, SPOT_DIAMETER); - this.spotDiameter = (double) spotDiameter.getValue(); - firePropertyChanged(this, spotDiameter); - } + public void setSpotDiameter(NumericProperty spotDiameter) { + requireType(spotDiameter, SPOT_DIAMETER); + this.spotDiameter = (double) spotDiameter.getValue(); + firePropertyChanged(this, spotDiameter); + } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(super.toString()); - sb.append(" ; "); - sb.append(getSpotDiameter()); - return sb.toString(); - } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(" ; "); + sb.append(getSpotDiameter()); + return sb.toString(); + } - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(SPOT_DIAMETER)); - return list; - } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SPOT_DIAMETER); + return set; + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == SPOT_DIAMETER) - setSpotDiameter(property); - else - super.set(type, property); - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == SPOT_DIAMETER) { + setSpotDiameter(property); + } else { + super.set(type, property); + } + } -} \ No newline at end of file +} From 0ff74e1ff3f80766a66496530c6e9f83a3f9edbd Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 12:27:30 +0100 Subject: [PATCH 076/116] listedTypes() to listedKeywords() + formatting --- .../schemes/CoupledImplicitScheme.java | 195 +++--- .../problem/schemes/DifferenceScheme.java | 553 +++++++++--------- src/main/java/pulse/problem/schemes/Grid.java | 417 +++++++------ .../pulse/problem/schemes/LayeredGrid2D.java | 100 ++-- .../schemes/rte/dom/AdaptiveIntegrator.java | 456 +++++++-------- .../rte/exact/ChandrasekharsQuadrature.java | 458 +++++++-------- .../rte/exact/NewtonCotesQuadrature.java | 255 ++++---- .../schemes/solvers/MixedCoupledSolver.java | 390 ++++++------ .../statements/model/AbsorptionModel.java | 161 ++--- .../model/BeerLambertAbsorption.java | 12 +- .../model/DiathermicProperties.java | 99 ++-- .../problem/statements/model/Insulator.java | 9 +- src/main/java/pulse/properties/Property.java | 73 ++- 13 files changed, 1585 insertions(+), 1593 deletions(-) diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 681ff79e..6981853b 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -5,108 +5,111 @@ import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import java.util.List; +import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import pulse.properties.Property; public abstract class CoupledImplicitScheme extends ImplicitScheme implements FixedPointIterations { - - private RadiativeTransferCoupling coupling; - private RTECalculationStatus calculationStatus; - private double nonlinearPrecision; - - private double pls; - - public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - setCoupling(new RadiativeTransferCoupling()); - calculationStatus = RTECalculationStatus.NORMAL; - } - - public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - this(N, timeFactor); - setTimeLimit(timeLimit); - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(final int m) { - super.timeStep(m); - } - - public void finaliseIteration(double[] V) { - setCalculationStatus( coupling.getRadiativeTransferEquation().compute(V) ); - } - - public RadiativeTransferCoupling getCoupling() { - return coupling; - } - - public void setCoupling(RadiativeTransferCoupling coupling) { - this.coupling = coupling; - this.coupling.setParent(this); - } - - @Override - public void finaliseStep() { - super.finaliseStep(); - coupling.getRadiativeTransferEquation().getFluxes().store(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public Class domain() { - return ParticipatingMedium.class; - } - - @Override - public boolean normalOperation() { - return super.normalOperation() && (getCalculationStatus() == RTECalculationStatus.NORMAL); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NONLINEAR_PRECISION) { - setNonlinearPrecision(property); - } else - super.set(type, property); - } - - public RTECalculationStatus getCalculationStatus() { - return calculationStatus; - } - - public void setCalculationStatus(RTECalculationStatus calculationStatus) { - this.calculationStatus = calculationStatus; - } - - public double getCurrentPulseValue() { - return pls; - } - -} \ No newline at end of file + + private RadiativeTransferCoupling coupling; + private RTECalculationStatus calculationStatus; + private double nonlinearPrecision; + + private double pls; + + public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setCoupling(new RadiativeTransferCoupling()); + calculationStatus = RTECalculationStatus.NORMAL; + } + + public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + this(N, timeFactor); + setTimeLimit(timeLimit); + } + + @Override + public void timeStep(final int m) { + pls = pulse(m); + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) { + super.timeStep(m); + } + + public void finaliseIteration(double[] V) { + setCalculationStatus(coupling.getRadiativeTransferEquation().compute(V)); + } + + public RadiativeTransferCoupling getCoupling() { + return coupling; + } + + public void setCoupling(RadiativeTransferCoupling coupling) { + this.coupling = coupling; + this.coupling.setParent(this); + } + + @Override + public void finaliseStep() { + super.finaliseStep(); + coupling.getRadiativeTransferEquation().getFluxes().store(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public Class domain() { + return ParticipatingMedium.class; + } + + @Override + public boolean normalOperation() { + return super.normalOperation() && (getCalculationStatus() == RTECalculationStatus.NORMAL); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + super.set(type, property); + } + } + + public RTECalculationStatus getCalculationStatus() { + return calculationStatus; + } + + public void setCalculationStatus(RTECalculationStatus calculationStatus) { + this.calculationStatus = calculationStatus; + } + + public double getCurrentPulseValue() { + return pls; + } + +} diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 0e3544da..309e19b1 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -7,11 +7,13 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.problem.laser.DiscretePulse; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -23,297 +25,282 @@ * partitioning, adjusted to ensure a stable or conditionally-stable behaviour * of the solution. The {@code Grid} is also used to define a * {@code DiscretePulse} function. - * + * * @see pulse.problem.schemes.Grid * @see pulse.problem.laser.DiscretePulse */ - public abstract class DifferenceScheme extends PropertyHolder implements Reflexive { - private DiscretePulse discretePulse; - private Grid grid; - - private double timeLimit; - private int timeInterval; - - private static boolean hideDetailedAdjustment = true; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - - /** - * A constructor which merely sets the time limit to its default value. - */ - - protected DifferenceScheme() { - setTimeLimit(def(TIME_LIMIT)); - } - - /** - * A constructor for setting the time limit to a pre-set value. - * - * @param timeLimit the calculation time limit - */ - - protected DifferenceScheme(NumericProperty timeLimit) { - setTimeLimit(timeLimit); - } - - public void initFrom(DifferenceScheme another) { - this.grid = grid.copy(); - this.timeLimit = another.timeLimit; - this.timeInterval = another.timeInterval; - } - - /** - * Used to get a class of problems on which this difference scheme is - * applicable. - * - * @return a subclass of the {@code Problem} class which can be used as input - * for this difference scheme. - */ - - public abstract Class domain(); - - /** - * Creates a {@code DifferenceScheme}, which is an exact copy of this object. - * - * @return an exact copy of this {@code DifferenceScheme}. - */ - - public abstract DifferenceScheme copy(); - - /** - * Copies the {@code Grid} and {@code timeLimit} from {@code df}. - * - * @param df the DifferenceScheme to copy from - */ - - public void copyFrom(DifferenceScheme df) { - this.grid = df.getGrid().copy(); - discretePulse = null; - timeLimit = df.timeLimit; - } - - /** - *

- * Contains preparatory steps to ensure smooth running of the solver. This - * includes creating a {@code DiscretePulse} object and adjusting the grid of this - * scheme to match the {@code DiscretePulse} created for this {@code problem}. - * Finally, a heating curve is cleared from the previously calculated values. - *

- *

- * All subclasses of - * {@code DifferenceScheme} should override and explicitly call this superclass - * method where appropriate. - *

- * - * @param problem the heat problem to be solved - * @see pulse.problem.schemes.Grid.adjustTo() - */ - - protected void prepare(Problem problem) { - discretePulse = problem.discretePulseOn(grid); - grid.adjustTo(discretePulse); - - var hc = problem.getHeatingCurve(); - hc.clear(); - } - - public void runTimeSequence(Problem problem) { - runTimeSequence(problem, 0, timeLimit); - var curve = problem.getHeatingCurve(); - final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); - curve.scale(maxTemp / curve.apparentMaximum() ); - } - - public void runTimeSequence(Problem problem, final double offset, final double endTime) { - final var grid = getGrid(); - - var curve = problem.getHeatingCurve(); - - int adjustedNumPoints = (int)curve.getNumPoints().getValue(); - - final double startTime = (double)curve.getTimeShift().getValue(); - final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); - final double tau = grid.getTimeStep(); - - for (double dt = 0, factor = 1.0; dt < tau; adjustedNumPoints *= factor) { - dt = timeSegment / (adjustedNumPoints - 1); - factor = dt / tau; - timeInterval = (int) factor; - } - - final double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); - - // First point (index = 0) is always (0.0, 0.0) - - /* + private DiscretePulse discretePulse; + private Grid grid; + + private double timeLimit; + private int timeInterval; + + private static boolean hideDetailedAdjustment = true; + + private final static double EPS = 1e-7; // a small value ensuring numeric stability + + /** + * A constructor which merely sets the time limit to its default value. + */ + protected DifferenceScheme() { + setTimeLimit(def(TIME_LIMIT)); + } + + /** + * A constructor for setting the time limit to a pre-set value. + * + * @param timeLimit the calculation time limit + */ + protected DifferenceScheme(NumericProperty timeLimit) { + setTimeLimit(timeLimit); + } + + public void initFrom(DifferenceScheme another) { + this.grid = grid.copy(); + this.timeLimit = another.timeLimit; + this.timeInterval = another.timeInterval; + } + + /** + * Used to get a class of problems on which this difference scheme is + * applicable. + * + * @return a subclass of the {@code Problem} class which can be used as + * input for this difference scheme. + */ + public abstract Class domain(); + + /** + * Creates a {@code DifferenceScheme}, which is an exact copy of this + * object. + * + * @return an exact copy of this {@code DifferenceScheme}. + */ + public abstract DifferenceScheme copy(); + + /** + * Copies the {@code Grid} and {@code timeLimit} from {@code df}. + * + * @param df the DifferenceScheme to copy from + */ + public void copyFrom(DifferenceScheme df) { + this.grid = df.getGrid().copy(); + discretePulse = null; + timeLimit = df.timeLimit; + } + + /** + *

+ * Contains preparatory steps to ensure smooth running of the solver. This + * includes creating a {@code DiscretePulse} object and adjusting the grid + * of this scheme to match the {@code DiscretePulse} created for this + * {@code problem}. Finally, a heating curve is cleared from the previously + * calculated values. + *

+ *

+ * All subclasses of {@code DifferenceScheme} should override and explicitly + * call this superclass method where appropriate. + *

+ * + * @param problem the heat problem to be solved + * @see pulse.problem.schemes.Grid.adjustTo() + */ + protected void prepare(Problem problem) { + discretePulse = problem.discretePulseOn(grid); + grid.adjustTo(discretePulse); + + var hc = problem.getHeatingCurve(); + hc.clear(); + } + + public void runTimeSequence(Problem problem) { + runTimeSequence(problem, 0, timeLimit); + var curve = problem.getHeatingCurve(); + final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); + curve.scale(maxTemp / curve.apparentMaximum()); + } + + public void runTimeSequence(Problem problem, final double offset, final double endTime) { + final var grid = getGrid(); + + var curve = problem.getHeatingCurve(); + + int adjustedNumPoints = (int) curve.getNumPoints().getValue(); + + final double startTime = (double) curve.getTimeShift().getValue(); + final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); + final double tau = grid.getTimeStep(); + + for (double dt = 0, factor = 1.0; dt < tau; adjustedNumPoints *= factor) { + dt = timeSegment / (adjustedNumPoints - 1); + factor = dt / tau; + timeInterval = (int) factor; + } + + final double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); + + // First point (index = 0) is always (0.0, 0.0) + + /* * The outer cycle iterates over the number of points of the HeatingCurve - */ + */ + double nextTime = offset + wFactor; + curve.addPoint(0.0, 0.0); - double nextTime = offset + wFactor; - curve.addPoint(0.0, 0.0); - - for (int w = 1; nextTime < 1.01*endTime; nextTime = offset + (++w)*wFactor) { + for (int w = 1; nextTime < 1.01 * endTime; nextTime = offset + (++w) * wFactor) { - /* + /* * Two adjacent points of the heating curves are separated by timeInterval on * the time grid. Thus, to calculate the next point on the heating curve, * timeInterval/tau time steps have to be made first. - */ - - timeSegment((w - 1) * timeInterval + 1, w * timeInterval + 1); - curve.addPoint(nextTime, signal()); - - } - - } - - private void timeSegment(final int m1, final int m2) { - for (int m = m1; m < m2 && normalOperation(); m++) { - timeStep(m); - finaliseStep(); - } - } - - public double pulse(final int m) { - return getDiscretePulse().laserPowerAt((m - EPS) * getGrid().getTimeStep()); - } - - public abstract double signal(); - - public abstract void timeStep(final int m); - - public abstract void finaliseStep(); - - public boolean normalOperation() { - return true; - } - - /** - * The superclass only lists the {@code TIME_LIMIT} property. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(TIME_LIMIT)); - return list; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * Gets the discrete representation of {@code Pulse} on the {@code Grid}. - * - * @return the discrete pulse - * @see pulse.problem.statements.Pulse - */ - - public DiscretePulse getDiscretePulse() { - return discretePulse; - } - - /** - * Gets the {@code Grid} object defining partioning used in this - * {@code DifferenceScheme} - * - * @return the grid - */ - - public Grid getGrid() { - return grid; - } - - /** - * Sets the grid and adopts it as its child. - * - * @param grid the grid - */ - - public void setGrid(Grid grid) { - this.grid = grid; - this.grid.setParent(this); - } - - /** - * The time interval is the number of discrete timesteps that will be discarded - * when storing the resulting solution into a {@code HeatingCurve} object, thus - * ensuring that only a limited set of points is stored. - * - * @return the time interval - */ - - public int getTimeInterval() { - return timeInterval; - } - - /** - * Sets the time interval to the argument of this method. - * - * @param timeInterval a positive integer. - */ - - public void setTimeInterval(int timeInterval) { - this.timeInterval = timeInterval; - } - - /** - * If true, Lets the UI know that the user only wants to have the most important - * properties displayed. Otherwise this will signal all properties need to be - * displayed. - */ - - @Override - public boolean areDetailsHidden() { - return hideDetailedAdjustment; - } - - /** - * Changes the policy of displaying a detailed information about this scheme. - * - * @param b a boolean. - */ - - public static void setDetailsHidden(boolean b) { - hideDetailedAdjustment = b; - } - - /** - * The time limit (in whatever units this {@code DifferenceScheme} uses to - * process the solution), which serves as the ultimate breakpoint for the - * calculations. - * - * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} - * @see pulse.properties.NumericPropertyKeyword - */ - - public NumericProperty getTimeLimit() { - return derive(TIME_LIMIT, timeLimit); - } - - /** - * Sets the time limit (in units defined by the corresponding - * {@code NumericProperty}), which serves as the breakpoint for the - * calculations. - * - * @param timeLimit the {@code NumericProperty} with the type {@code TIME_LIMIT} - * @see pulse.properties.NumericPropertyKeyword - */ - - public void setTimeLimit(NumericProperty timeLimit) { - requireType(timeLimit, TIME_LIMIT); - this.timeLimit = (double) timeLimit.getValue(); - firePropertyChanged(this, timeLimit); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == TIME_LIMIT) - setTimeLimit(property); - } - -} \ No newline at end of file + */ + timeSegment((w - 1) * timeInterval + 1, w * timeInterval + 1); + curve.addPoint(nextTime, signal()); + + } + + } + + private void timeSegment(final int m1, final int m2) { + for (int m = m1; m < m2 && normalOperation(); m++) { + timeStep(m); + finaliseStep(); + } + } + + public double pulse(final int m) { + return getDiscretePulse().laserPowerAt((m - EPS) * getGrid().getTimeStep()); + } + + public abstract double signal(); + + public abstract void timeStep(final int m); + + public abstract void finaliseStep(); + + public boolean normalOperation() { + return true; + } + + /** + * The superclass only lists the {@code TIME_LIMIT} property. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TIME_LIMIT); + return set; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * Gets the discrete representation of {@code Pulse} on the {@code Grid}. + * + * @return the discrete pulse + * @see pulse.problem.statements.Pulse + */ + public DiscretePulse getDiscretePulse() { + return discretePulse; + } + + /** + * Gets the {@code Grid} object defining partioning used in this + * {@code DifferenceScheme} + * + * @return the grid + */ + public Grid getGrid() { + return grid; + } + + /** + * Sets the grid and adopts it as its child. + * + * @param grid the grid + */ + public void setGrid(Grid grid) { + this.grid = grid; + this.grid.setParent(this); + } + + /** + * The time interval is the number of discrete timesteps that will be + * discarded when storing the resulting solution into a {@code HeatingCurve} + * object, thus ensuring that only a limited set of points is stored. + * + * @return the time interval + */ + public int getTimeInterval() { + return timeInterval; + } + + /** + * Sets the time interval to the argument of this method. + * + * @param timeInterval a positive integer. + */ + public void setTimeInterval(int timeInterval) { + this.timeInterval = timeInterval; + } + + /** + * If true, Lets the UI know that the user only wants to have the most + * important properties displayed. Otherwise this will signal all properties + * need to be displayed. + */ + @Override + public boolean areDetailsHidden() { + return hideDetailedAdjustment; + } + + /** + * Changes the policy of displaying a detailed information about this + * scheme. + * + * @param b a boolean. + */ + public static void setDetailsHidden(boolean b) { + hideDetailedAdjustment = b; + } + + /** + * The time limit (in whatever units this {@code DifferenceScheme} uses to + * process the solution), which serves as the ultimate breakpoint for the + * calculations. + * + * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} + * @see pulse.properties.NumericPropertyKeyword + */ + public NumericProperty getTimeLimit() { + return derive(TIME_LIMIT, timeLimit); + } + + /** + * Sets the time limit (in units defined by the corresponding + * {@code NumericProperty}), which serves as the breakpoint for the + * calculations. + * + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.properties.NumericPropertyKeyword + */ + public void setTimeLimit(NumericProperty timeLimit) { + requireType(timeLimit, TIME_LIMIT); + this.timeLimit = (double) timeLimit.getValue(); + firePropertyChanged(this, timeLimit); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == TIME_LIMIT) { + setTimeLimit(property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 20537fb8..d62e7dcd 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -11,10 +11,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import pulse.problem.laser.DiscretePulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.util.PropertyHolder; @@ -28,218 +30,207 @@ *

* */ - public class Grid extends PropertyHolder { - private double hx; - private double tau; - private double tauFactor; - private int N; - - /** - * Creates a {@code Grid} object with the specified {@code gridDensity} and - * {@code timeFactor}. - * - * @param gridDensity a {@code NumericProperty} of the type {@code GRID_DENSITY} - * @param timeFactor a {@code NumericProperty} of the type {@code TIME_FACTOR} - * @see pulse.properties.NumericPropertyKeyword - */ - - public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { - setGridDensity(gridDensity); - setTimeFactor(timeFactor); - } - - protected Grid() { - // intentionally blank - } - - /** - * Creates a new {@code Grid} object with exactly the same parameters as this - * one. - * - * @return a new {@code Grid} object replicating this {@code Grid} - */ - - public Grid copy() { - return new Grid(getGridDensity(), getTimeFactor()); - } - - /** - * Optimises the {@code Grid} parameters. - *

- * This can change the {@code tauFactor} and {@code tau} variables in the - * {@code Grid} object if {@code discretePulseWidth < grid.tau}. - *

- * - * @param pulse the discrete pulse representation - */ - - public void adjustTo(DiscretePulse pulse) { - final double ADJUSTMENT_FACTOR = 0.75; - for (final double factor = 0.95; factor * tau > pulse.getDiscreteWidth(); pulse.recalculate()) { - tauFactor *= ADJUSTMENT_FACTOR; - tau = tauFactor * pow(hx, 2); - } - } - - /** - * The listed properties include {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(2); - list.add(def(GRID_DENSITY)); - list.add(def(TAU_FACTOR)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case TAU_FACTOR: - setTimeFactor(property); - break; - case GRID_DENSITY: - setGridDensity(property); - break; - default: - break; - } - } - - /** - * Retrieves the value of the hx coordinate step - * used in finite-difference calculation. - * - * @return a double, representing the {@code hx} value. - */ - - public double getXStep() { - return hx; - } - - /** - * Sets the value of the hx coordinate step. - * - * @param hx a double, representing the new {@code hx} value. - */ - - public void setXStep(double hx) { - this.hx = hx; - } - - /** - * Retrieves the value of the τ time step used in finite-difference - * calculation. - * - * @return a double, representing the {@code tau} value. - */ - - public double getTimeStep() { - return tau; - } - - protected void setTimeStep(double tau) { - this.tau = tau; - } - - /** - * Retrieves the value of the τ-factor, or the time factor, used in - * finite-difference calculation. This factor determines the proportionally - * coefficient between τ and hx. - * - * @return a NumericProperty of the {@code TAU_FACTOR} type, representing the - * {@code tauFactor} value. - */ - - public NumericProperty getTimeFactor() { - return derive(TAU_FACTOR, tauFactor); - } - - /** - * Retrieves the value of the {@code gridDensity} used to calculate the - * {@code hx} and {@code tau}. - * - * @return a NumericProperty of the {@code GRID_DENSITY} type, representing the - * {@code gridDensity} value. - */ - - public NumericProperty getGridDensity() { - return derive(GRID_DENSITY, N); - } - - protected int getGridDensityValue() { - return N; - } - - protected void setGridDensityValue(int N) { - this.N = N; - } - - /** - * Sets the value of the {@code gridDensity}. Automatically recalculates the - * {@code hx} value. - * - * @param gridDensity a NumericProperty of the {@code GRID_DENSITY} type - */ - - public void setGridDensity(NumericProperty gridDensity) { - requireType(gridDensity, GRID_DENSITY); - this.N = (int) gridDensity.getValue(); - hx = 1. / N; - setTimeFactor(derive(TAU_FACTOR, 1.0)); - } - - /** - * Sets the value of the {@code tauFactor}. Automatically recalculates the - * {@code tau} value. - * - * @param timeFactor a NumericProperty of the {@code TAU_FACTOR} type - */ - - public void setTimeFactor(NumericProperty timeFactor) { - requireType(timeFactor, TAU_FACTOR); - this.tauFactor = (double) timeFactor.getValue(); - setTimeStep(tauFactor * pow(hx, 2)); - } - - /** - * The dimensionless time on this {@code Grid}, which is the - * {@code time/dimensionFactor} rounded up to a factor of the time step - * {@code tau}. - * - * @param time the time - * @param dimensionFactor a conversion factor with the dimension of time - * @return a double representing the time on the finite grid - */ - - public double gridTime(double time, double dimensionFactor) { - return rint((time / dimensionFactor) / tau) * tau; - } - - /** - * The dimensionless axial distance on this {@code Grid}, which is the - * {@code distance/lengthFactor} rounded up to a factor of the coordinate step - * {@code hx}. - * - * @param distance the distance along the axial direction - * @param lengthFactor a conversion factor with the dimension of length - * @return a double representing the axial distance on the finite grid - */ - - public double gridAxialDistance(double distance, double lengthFactor) { - return rint((distance / lengthFactor) / hx) * hx; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append(""); - sb.append(getClass().getSimpleName() + ": hx=" + format("%3.2e", hx) + "; "); - sb.append("τ=" + format("%3.2e", tau) + "; "); - return sb.toString(); - } - -} \ No newline at end of file + private double hx; + private double tau; + private double tauFactor; + private int N; + + /** + * Creates a {@code Grid} object with the specified {@code gridDensity} and + * {@code timeFactor}. + * + * @param gridDensity a {@code NumericProperty} of the type + * {@code GRID_DENSITY} + * @param timeFactor a {@code NumericProperty} of the type + * {@code TIME_FACTOR} + * @see pulse.properties.NumericPropertyKeyword + */ + public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { + setGridDensity(gridDensity); + setTimeFactor(timeFactor); + } + + protected Grid() { + // intentionally blank + } + + /** + * Creates a new {@code Grid} object with exactly the same parameters as + * this one. + * + * @return a new {@code Grid} object replicating this {@code Grid} + */ + public Grid copy() { + return new Grid(getGridDensity(), getTimeFactor()); + } + + /** + * Optimises the {@code Grid} parameters. + *

+ * This can change the {@code tauFactor} and {@code tau} variables in the + * {@code Grid} object if {@code discretePulseWidth < grid.tau}. + *

+ * + * @param pulse the discrete pulse representation + */ + public void adjustTo(DiscretePulse pulse) { + final double ADJUSTMENT_FACTOR = 0.75; + for (final double factor = 0.95; factor * tau > pulse.getDiscreteWidth(); pulse.recalculate()) { + tauFactor *= ADJUSTMENT_FACTOR; + tau = tauFactor * pow(hx, 2); + } + } + + /** + * The listed properties include {@code GRID_DENSITY} and + * {@code TAU_FACTOR}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRID_DENSITY); + set.add(TAU_FACTOR); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case TAU_FACTOR: + setTimeFactor(property); + break; + case GRID_DENSITY: + setGridDensity(property); + break; + default: + break; + } + } + + /** + * Retrieves the value of the hx coordinate + * step used in finite-difference calculation. + * + * @return a double, representing the {@code hx} value. + */ + public double getXStep() { + return hx; + } + + /** + * Sets the value of the hx coordinate step. + * + * @param hx a double, representing the new {@code hx} value. + */ + public void setXStep(double hx) { + this.hx = hx; + } + + /** + * Retrieves the value of the τ time step used in finite-difference + * calculation. + * + * @return a double, representing the {@code tau} value. + */ + public double getTimeStep() { + return tau; + } + + protected void setTimeStep(double tau) { + this.tau = tau; + } + + /** + * Retrieves the value of the τ-factor, or the time factor, used in + * finite-difference calculation. This factor determines the proportionally + * coefficient between τ and hx. + * + * @return a NumericProperty of the {@code TAU_FACTOR} type, representing + * the {@code tauFactor} value. + */ + public NumericProperty getTimeFactor() { + return derive(TAU_FACTOR, tauFactor); + } + + /** + * Retrieves the value of the {@code gridDensity} used to calculate the + * {@code hx} and {@code tau}. + * + * @return a NumericProperty of the {@code GRID_DENSITY} type, representing + * the {@code gridDensity} value. + */ + public NumericProperty getGridDensity() { + return derive(GRID_DENSITY, N); + } + + protected int getGridDensityValue() { + return N; + } + + protected void setGridDensityValue(int N) { + this.N = N; + } + + /** + * Sets the value of the {@code gridDensity}. Automatically recalculates the + * {@code hx} value. + * + * @param gridDensity a NumericProperty of the {@code GRID_DENSITY} type + */ + public void setGridDensity(NumericProperty gridDensity) { + requireType(gridDensity, GRID_DENSITY); + this.N = (int) gridDensity.getValue(); + hx = 1. / N; + setTimeFactor(derive(TAU_FACTOR, 1.0)); + } + + /** + * Sets the value of the {@code tauFactor}. Automatically recalculates the + * {@code tau} value. + * + * @param timeFactor a NumericProperty of the {@code TAU_FACTOR} type + */ + public void setTimeFactor(NumericProperty timeFactor) { + requireType(timeFactor, TAU_FACTOR); + this.tauFactor = (double) timeFactor.getValue(); + setTimeStep(tauFactor * pow(hx, 2)); + } + + /** + * The dimensionless time on this {@code Grid}, which is the + * {@code time/dimensionFactor} rounded up to a factor of the time step + * {@code tau}. + * + * @param time the time + * @param dimensionFactor a conversion factor with the dimension of time + * @return a double representing the time on the finite grid + */ + public double gridTime(double time, double dimensionFactor) { + return rint((time / dimensionFactor) / tau) * tau; + } + + /** + * The dimensionless axial distance on this {@code Grid}, which is the + * {@code distance/lengthFactor} rounded up to a factor of the coordinate + * step {@code hx}. + * + * @param distance the distance along the axial direction + * @param lengthFactor a conversion factor with the dimension of length + * @return a double representing the axial distance on the finite grid + */ + public double gridAxialDistance(double distance, double lengthFactor) { + return rint((distance / lengthFactor) / hx) * hx; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append(""); + sb.append(getClass().getSimpleName() + ": hx=" + format("%3.2e", hx) + "; "); + sb.append("τ=" + format("%3.2e", tau) + "; "); + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java b/src/main/java/pulse/problem/schemes/LayeredGrid2D.java index 68f2c8e3..0cea4c9c 100644 --- a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java +++ b/src/main/java/pulse/problem/schemes/LayeredGrid2D.java @@ -14,70 +14,72 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import pulse.problem.schemes.Partition.Location; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; public class LayeredGrid2D extends Grid2D { - private Map h; + private Map h; - public LayeredGrid2D(Map partitions, NumericProperty timeFactor) { - h = new HashMap<>(partitions); - setGridDensity(derive(CORE_Y.densityKeyword(), partitions.get(CORE_Y).getDensity())); - setTimeFactor(timeFactor); - } + public LayeredGrid2D(Map partitions, NumericProperty timeFactor) { + h = new HashMap<>(partitions); + setGridDensity(derive(CORE_Y.densityKeyword(), partitions.get(CORE_Y).getDensity())); + setTimeFactor(timeFactor); + } - public Partition getPartition(Location location) { - return h.get(location); - } + public Partition getPartition(Location location) { + return h.get(location); + } - @Override - public Grid2D copy() { - return new LayeredGrid2D(h, getTimeFactor()); - } + @Override + public Grid2D copy() { + return new LayeredGrid2D(h, getTimeFactor()); + } - private void setDensity(Location location, NumericProperty density) { - h.get(location).setDensity((int) density.getValue()); - } + private void setDensity(Location location, NumericProperty density) { + h.get(location).setDensity((int) density.getValue()); + } - @Override - public void setGridDensity(NumericProperty gridDensity) { - super.setGridDensity(gridDensity); - setDensity(CORE_X, gridDensity); - setDensity(CORE_Y, gridDensity); - } + @Override + public void setGridDensity(NumericProperty gridDensity) { + super.setGridDensity(gridDensity); + setDensity(CORE_X, gridDensity); + setDensity(CORE_Y, gridDensity); + } - public NumericProperty getGridDensity(Location location) { - return derive(location.densityKeyword(), h.get(location).getDensity()); - } + public NumericProperty getGridDensity(Location location) { + return derive(location.densityKeyword(), h.get(location).getDensity()); + } - @Override - public NumericProperty getGridDensity() { - return getGridDensity(CORE_X); - } + @Override + public NumericProperty getGridDensity() { + return getGridDensity(CORE_X); + } - @Override - public List listedTypes() { - List list = new ArrayList<>(2); - list.add(def(SHELL_GRID_DENSITY)); - return list; - } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SHELL_GRID_DENSITY); + return set; + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case SHELL_GRID_DENSITY: - setDensity(FRONT_Y, property); - setDensity(REAR_Y, property); - setDensity(SIDE_X, property); - setDensity(SIDE_Y, property); - break; - default: - super.set(type, property); - } - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case SHELL_GRID_DENSITY: + setDensity(FRONT_Y, property); + setDensity(REAR_Y, property); + setDensity(SIDE_X, property); + setDensity(SIDE_Y, property); + break; + default: + super.set(type, property); + } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java index 614f1bd1..efbba180 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java @@ -11,253 +11,253 @@ import java.time.Duration; import java.time.Instant; -import java.util.List; +import java.util.Set; import pulse.math.linear.Vector; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * An ODE integrator with an adaptive step size. * */ - public abstract class AdaptiveIntegrator extends ODEIntegrator { - private HermiteInterpolator hermite; - - private double atol; - private double rtol; - private double scalingFactor; - - private boolean firstRun; - private boolean rescaled; - - private Instant start; - private double timeThreshold; - - public AdaptiveIntegrator(Discretisation intensities) { - super(intensities); - atol = (double) def(ATOL).getValue(); - rtol = (double) def(RTOL).getValue(); - scalingFactor = (double) def(GRID_SCALING_FACTOR).getValue(); - timeThreshold = (double) def(RTE_INTEGRATION_TIMEOUT).getValue(); - hermite = new HermiteInterpolator(); - } - - @Override - public RTECalculationStatus integrate() { - Vector[] v; - final var intensities = getDiscretisation(); - final var quantities = intensities.getQuantities(); - - int N = intensities.getGrid().getDensity(); - final int total = intensities.getOrdinates().getTotalNodes(); - rescaled = false; - - final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); - final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); - final int halfLength = nNegativeStart - nPositiveStart; - - RTECalculationStatus status = RTECalculationStatus.NORMAL; - - for (double error = 1.0, relFactor = 0.0, i0Max = 0, i1Max = 0; (error > atol + relFactor * rtol) - && status == RTECalculationStatus.NORMAL; N = intensities.getGrid() - .getDensity(), status = sanityCheck()) { - - start = Instant.now(); - error = 0; - - treatZeroIndex(); - - /* + private HermiteInterpolator hermite; + + private double atol; + private double rtol; + private double scalingFactor; + + private boolean firstRun; + private boolean rescaled; + + private Instant start; + private double timeThreshold; + + public AdaptiveIntegrator(Discretisation intensities) { + super(intensities); + atol = (double) def(ATOL).getValue(); + rtol = (double) def(RTOL).getValue(); + scalingFactor = (double) def(GRID_SCALING_FACTOR).getValue(); + timeThreshold = (double) def(RTE_INTEGRATION_TIMEOUT).getValue(); + hermite = new HermiteInterpolator(); + } + + @Override + public RTECalculationStatus integrate() { + Vector[] v; + final var intensities = getDiscretisation(); + final var quantities = intensities.getQuantities(); + + int N = intensities.getGrid().getDensity(); + final int total = intensities.getOrdinates().getTotalNodes(); + rescaled = false; + + final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); + final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); + final int halfLength = nNegativeStart - nPositiveStart; + + RTECalculationStatus status = RTECalculationStatus.NORMAL; + + for (double error = 1.0, relFactor = 0.0, i0Max = 0, i1Max = 0; (error > atol + relFactor * rtol) + && status == RTECalculationStatus.NORMAL; N = intensities.getGrid() + .getDensity(), status = sanityCheck()) { + + start = Instant.now(); + error = 0; + + treatZeroIndex(); + + /* * First set of ODE's. Initial condition corresponds to I(0) /t ----> tau0 The * streams propagate in the positive hemisphere - */ - - intensities.intensitiesLeftBoundary(getEmissionFunction()); // initial value for tau = 0 - i0Max = (new Vector(quantities.getIntensities()[0])).maxAbsComponent(); + */ + intensities.intensitiesLeftBoundary(getEmissionFunction()); // initial value for tau = 0 + i0Max = (new Vector(quantities.getIntensities()[0])).maxAbsComponent(); - firstRun = true; + firstRun = true; - for (int j = 0; j < N && error < atol + relFactor * rtol; j++) { + for (int j = 0; j < N && error < atol + relFactor * rtol; j++) { - v = step(j, 1.0); - System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j + 1], nPositiveStart, halfLength); + v = step(j, 1.0); + System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j + 1], nPositiveStart, halfLength); - i1Max = (new Vector(quantities.getIntensities()[j + 1])).maxAbsComponent(); - relFactor = Math.max(i0Max, i1Max); - i0Max = i1Max; + i1Max = (new Vector(quantities.getIntensities()[j + 1])).maxAbsComponent(); + relFactor = Math.max(i0Max, i1Max); + i0Max = i1Max; - error = v[1].maxAbsComponent(); - } + error = v[1].maxAbsComponent(); + } - /* + /* * Second set of ODE. Initial condition corresponds to I(tau0) /0 <---- t The * streams propagate in the negative hemisphere - */ - - intensities.intensitiesRightBoundary(getEmissionFunction()); // initial value for tau = tau_0 - i0Max = (new Vector(quantities.getIntensities()[N])).maxAbsComponent(); - - firstRun = true; - - for (int j = N; j > 0 && error < atol + relFactor * rtol; j--) { - - v = step(j, -1.0); - System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j - 1], nNegativeStart, halfLength); - - i1Max = (new Vector(quantities.getIntensities()[j - 1])).maxAbsComponent(); - relFactor = Math.max(i0Max, i1Max); - i0Max = i1Max; - - error = v[1].maxAbsComponent(); - } - - // store derivatives for Hermite interpolation - for (int i = 0; i < total; i++) { - quantities.setDerivative(N, i, quantities.getDerivative(N - 1, i)); - quantities.setDerivative(0, i, quantities.getDerivative(1, i)); - } - - if (error > atol + relFactor * rtol) { - reduceStepSize(); - hermite.clear(); - } - - } - - return status; - - } - - private RTECalculationStatus sanityCheck() { - if (!isValueSensible(def(DOM_GRID_DENSITY), - getDiscretisation().getGrid().getDensity())) - return RTECalculationStatus.GRID_TOO_LARGE; - else if (Duration.between(Instant.now(), start).toSeconds() > timeThreshold) - return RTECalculationStatus.INTEGRATOR_TIMEOUT; - return RTECalculationStatus.NORMAL; - } - - public abstract Vector[] step(final int j, final double sign); - - public void reduceStepSize() { - var intensities = getDiscretisation(); - final int nNew = (roundEven(scalingFactor * intensities.getGrid().getDensity())); - generateGrid(nNew); - intensities.getQuantities().init(intensities.getGrid().getDensity(), intensities.getOrdinates().getTotalNodes()); - rescaled = true; - } - - public boolean wasRescaled() { - return rescaled; - } - - /** - * Generates a uniform grid using the argument as the density. - * - * @param nNew new grid density - */ - - public void generateGrid(int nNew) { - getDiscretisation().getGrid().generateUniformBase(nNew, true); - } - - private int roundEven(double a) { - return (int) (a / 2 * 2); - } - - public NumericProperty getRelativeTolerance() { - return derive(RTOL, rtol); - } - - public NumericProperty getAbsoluteTolerance() { - return derive(ATOL, atol); - } - - public NumericProperty getGridScalingFactor() { - return derive(GRID_SCALING_FACTOR, scalingFactor); - } - - public void setRelativeTolerance(NumericProperty p) { - if (p.getType() != RTOL) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.rtol = (double) p.getValue(); - } - - public void setAbsoluteTolerance(NumericProperty p) { - if (p.getType() != ATOL) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.atol = (double) p.getValue(); - } - - public void setGridScalingFactor(NumericProperty p) { - if (p.getType() != GRID_SCALING_FACTOR) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.scalingFactor = (double) p.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case RTOL: - setRelativeTolerance(property); - break; - case ATOL: - setAbsoluteTolerance(property); - break; - case GRID_SCALING_FACTOR: - setGridScalingFactor(property); - break; - case RTE_INTEGRATION_TIMEOUT: - setTimeThreshold(property); - break; - default: - return; - } - - firePropertyChanged(this, property); - - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(RTOL)); - list.add(def(ATOL)); - list.add(def(GRID_SCALING_FACTOR)); - list.add(def(RTE_INTEGRATION_TIMEOUT)); - return list; - } - - @Override - public String toString() { - return super.toString() + " : " + this.getRelativeTolerance() + " ; " + this.getAbsoluteTolerance() + " ; " - + this.getGridScalingFactor(); - } - - public NumericProperty getTimeThreshold() { - return derive(RTE_INTEGRATION_TIMEOUT, (double) timeThreshold); - } - - public void setTimeThreshold(NumericProperty timeThreshold) { - if (timeThreshold.getType() == RTE_INTEGRATION_TIMEOUT) - this.timeThreshold = ((Number) timeThreshold.getValue()).longValue(); - } - - public boolean isFirstRun() { - return firstRun; - } - - public void setFirstRun(boolean firstRun) { - this.firstRun = firstRun; - } - - public HermiteInterpolator getHermiteInterpolator() { - return hermite; - } - -} \ No newline at end of file + */ + intensities.intensitiesRightBoundary(getEmissionFunction()); // initial value for tau = tau_0 + i0Max = (new Vector(quantities.getIntensities()[N])).maxAbsComponent(); + + firstRun = true; + + for (int j = N; j > 0 && error < atol + relFactor * rtol; j--) { + + v = step(j, -1.0); + System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j - 1], nNegativeStart, halfLength); + + i1Max = (new Vector(quantities.getIntensities()[j - 1])).maxAbsComponent(); + relFactor = Math.max(i0Max, i1Max); + i0Max = i1Max; + + error = v[1].maxAbsComponent(); + } + + // store derivatives for Hermite interpolation + for (int i = 0; i < total; i++) { + quantities.setDerivative(N, i, quantities.getDerivative(N - 1, i)); + quantities.setDerivative(0, i, quantities.getDerivative(1, i)); + } + + if (error > atol + relFactor * rtol) { + reduceStepSize(); + hermite.clear(); + } + + } + + return status; + + } + + private RTECalculationStatus sanityCheck() { + if (!isValueSensible(def(DOM_GRID_DENSITY), + getDiscretisation().getGrid().getDensity())) { + return RTECalculationStatus.GRID_TOO_LARGE; + } else if (Duration.between(Instant.now(), start).toSeconds() > timeThreshold) { + return RTECalculationStatus.INTEGRATOR_TIMEOUT; + } + return RTECalculationStatus.NORMAL; + } + + public abstract Vector[] step(final int j, final double sign); + + public void reduceStepSize() { + var intensities = getDiscretisation(); + final int nNew = (roundEven(scalingFactor * intensities.getGrid().getDensity())); + generateGrid(nNew); + intensities.getQuantities().init(intensities.getGrid().getDensity(), intensities.getOrdinates().getTotalNodes()); + rescaled = true; + } + + public boolean wasRescaled() { + return rescaled; + } + + /** + * Generates a uniform grid using the argument as the density. + * + * @param nNew new grid density + */ + public void generateGrid(int nNew) { + getDiscretisation().getGrid().generateUniformBase(nNew, true); + } + + private int roundEven(double a) { + return (int) (a / 2 * 2); + } + + public NumericProperty getRelativeTolerance() { + return derive(RTOL, rtol); + } + + public NumericProperty getAbsoluteTolerance() { + return derive(ATOL, atol); + } + + public NumericProperty getGridScalingFactor() { + return derive(GRID_SCALING_FACTOR, scalingFactor); + } + + public void setRelativeTolerance(NumericProperty p) { + if (p.getType() != RTOL) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.rtol = (double) p.getValue(); + } + + public void setAbsoluteTolerance(NumericProperty p) { + if (p.getType() != ATOL) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.atol = (double) p.getValue(); + } + + public void setGridScalingFactor(NumericProperty p) { + if (p.getType() != GRID_SCALING_FACTOR) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.scalingFactor = (double) p.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case RTOL: + setRelativeTolerance(property); + break; + case ATOL: + setAbsoluteTolerance(property); + break; + case GRID_SCALING_FACTOR: + setGridScalingFactor(property); + break; + case RTE_INTEGRATION_TIMEOUT: + setTimeThreshold(property); + break; + default: + return; + } + + firePropertyChanged(this, property); + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(RTOL); + set.add(ATOL); + set.add(GRID_SCALING_FACTOR); + set.add(RTE_INTEGRATION_TIMEOUT); + return set; + } + + @Override + public String toString() { + return super.toString() + " : " + this.getRelativeTolerance() + " ; " + this.getAbsoluteTolerance() + " ; " + + this.getGridScalingFactor(); + } + + public NumericProperty getTimeThreshold() { + return derive(RTE_INTEGRATION_TIMEOUT, (double) timeThreshold); + } + + public void setTimeThreshold(NumericProperty timeThreshold) { + if (timeThreshold.getType() == RTE_INTEGRATION_TIMEOUT) { + this.timeThreshold = ((Number) timeThreshold.getValue()).longValue(); + } + } + + public boolean isFirstRun() { + return firstRun; + } + + public void setFirstRun(boolean firstRun) { + this.firstRun = firstRun; + } + + public HermiteInterpolator getHermiteInterpolator() { + return hermite; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java index a243cf64..208ab29b 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.IntStream; import org.apache.commons.math3.analysis.solvers.LaguerreSolver; @@ -24,252 +25,251 @@ import pulse.math.linear.Vector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; /** * This quadrature methods of evaluating the composition product of the * exponential integral and blackbody spectral power spectrum has been given by * Chandrasekhar and is based on constructing a moment matrix. - * + * * @see Chandrasekhar, - * S. Radiative transfer - * + * S. Radiative transfer + * */ - public class ChandrasekharsQuadrature extends CompositionProduct { - private int m; - private double expLower; - private double expUpper; - private LaguerreSolver solver; - private double[] moments; - - /** - * Constructs a {@code ChandrasekharsQuadrature} object with a default number of - * nodes, a {@code LaguerreSolver} with default precision and integration bounds - * set to [0,1]. - */ - - public ChandrasekharsQuadrature() { - super(new Segment(0, 1)); - m = (int) def(QUADRATURE_POINTS).getValue(); - solver = new LaguerreSolver(); - } - - @Override - public double integrate() { - var bounds = this.transformedBounds(); - expLower = -exp(-bounds[0]); - expUpper = -exp(-bounds[1]); - - double[] roots = roots(); - - Vector weights = weights(roots); - - return f(roots).dot(weights) / getBeta(); - } - - public NumericProperty getQuadraturePoints() { - return derive(QUADRATURE_POINTS, m); - } - - public void setQuadraturePoints(NumericProperty m) { - requireType(m, QUADRATURE_POINTS); - this.m = (int) m.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == QUADRATURE_POINTS) { - setQuadraturePoints(property); - firePropertyChanged(this, property); - } - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(QUADRATURE_POINTS))); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + getQuadraturePoints(); - } - - /* + private int m; + private double expLower; + private double expUpper; + private LaguerreSolver solver; + private double[] moments; + + /** + * Constructs a {@code ChandrasekharsQuadrature} object with a default + * number of nodes, a {@code LaguerreSolver} with default precision and + * integration bounds set to [0,1]. + */ + public ChandrasekharsQuadrature() { + super(new Segment(0, 1)); + m = (int) def(QUADRATURE_POINTS).getValue(); + solver = new LaguerreSolver(); + } + + @Override + public double integrate() { + var bounds = this.transformedBounds(); + expLower = -exp(-bounds[0]); + expUpper = -exp(-bounds[1]); + + double[] roots = roots(); + + Vector weights = weights(roots); + + return f(roots).dot(weights) / getBeta(); + } + + public NumericProperty getQuadraturePoints() { + return derive(QUADRATURE_POINTS, m); + } + + public void setQuadraturePoints(NumericProperty m) { + requireType(m, QUADRATURE_POINTS); + this.m = (int) m.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == QUADRATURE_POINTS) { + setQuadraturePoints(property); + firePropertyChanged(this, property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(QUADRATURE_POINTS); + return set; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + getQuadraturePoints(); + } + + /* * Private methods - */ - - private Vector f(final double[] roots) { - final var ef = getEmissionFunction(); - return new Vector(Arrays.stream(roots).map(root -> ef.powerAt(root)).toArray()); - } - - private double[] transformedBounds() { - final double min = getBounds().getMinimum(); - final double max = getBounds().getMaximum(); - return new double[] { getAlpha() + getBeta() * min, getAlpha() + getBeta() * max }; - } - - private SquareMatrix xMatrix(final double[] roots) { - double[][] x = new double[m][m]; - - for (int l = 0; l < m; l++) { - for (int j = 0; j < m; j++) { - x[l][j] = fastPowLoop(roots[j] * getBeta() + getAlpha(), l); - } - } - - return Matrices.createSquareMatrix(x); - } - - /** - * Calculates \int_{r_{min}}^{r_{max}}{x^{l+1}exp(-x)dx}. - * - * @param l an integer such that 0 <= l <= 2*m - 1. - * @return the value of this definite integral. - */ - - private static double auxilliaryIntegral(final double x, final int lPlusN, final double exp) { - - double f = 0; - long m = 0; - - final int k = lPlusN - 1; - - for (int i = 0; i < lPlusN; i++) { - m = 1; - for (int j = 0; j < i; j++) { - m *= (k - j); - } - f += m * fastPowLoop(x, k - i); - } - - return f * exp; - - } + */ + private Vector f(final double[] roots) { + final var ef = getEmissionFunction(); + return new Vector(Arrays.stream(roots).map(root -> ef.powerAt(root)).toArray()); + } - private static double[] solveCubic(final double a, final double b, final double c) { - final double p = b / 3.0 - a * a / 9.0; - final double q = a * a * a / 27.0 - a * b / 6.0 + c / 2.0; + private double[] transformedBounds() { + final double min = getBounds().getMinimum(); + final double max = getBounds().getMaximum(); + return new double[]{getAlpha() + getBeta() * min, getAlpha() + getBeta() * max}; + } + + private SquareMatrix xMatrix(final double[] roots) { + double[][] x = new double[m][m]; + + for (int l = 0; l < m; l++) { + for (int j = 0; j < m; j++) { + x[l][j] = fastPowLoop(roots[j] * getBeta() + getAlpha(), l); + } + } + + return Matrices.createSquareMatrix(x); + } + + /** + * Calculates \int_{r_{min}}^{r_{max}}{x^{l+1}exp(-x)dx}. + * + * @param l an integer such that 0 <= l <= 2*m - 1. + * @return the value of this definite integral. + */ + private static double auxilliaryIntegral(final double x, final int lPlusN, final double exp) { + + double f = 0; + long m = 0; - final double ang = acos(-q / sqrt(-p * p * p)); - final double r = 2.0 * sqrt(-p); - var result = new double[3]; - double theta; - for (int k = -1; k < 2; k++) { - theta = (ang - 2.0 * PI * k) / 3.0; - result[k + 1] = r * cos(theta); - } - - for (int i = 0; i < result.length; i++) { - result[i] -= a / 3.0; - } - - return result; - } - - private double moment(int l) { - var bounds = this.transformedBounds(); - return momentIntegral(bounds[1], l, expUpper) - momentIntegral(bounds[0], l, expLower); - } - - private double momentIntegral(final double x, final int l, final double exp) { - - double e = 0; - int m = 0; - - final int n = getOrder(); - final int lPlusOne = l + 1; - - for (int i = 0; i < n; i++) { - m = lPlusOne; - for (int j = 1; j < i + 1; j++) { - m *= (lPlusOne + j); - } - e += ExponentialIntegrals.get(n - i).valueAt(x) * fastPowLoop(x, lPlusOne + i) / ((double) m); - } - - return e + auxilliaryIntegral(x, l + n, exp) / ((double) m); - - } - - private Vector coefficients() { - return momentMatrix().inverse().multiply(momentVector(m, 2 * m)); - } - - private SquareMatrix momentMatrix() { - - double[][] data = new double[m][m]; - moments = new double[2 * m]; - - // diagonal elements - IntStream.range(0, m).forEach(i -> data[i][i] = moment(i * 2)); - - // find (symmetric) non-diagonal elements - for (int i = 1, j = 0; i < m; i++) { - for (j = 0; j < i; j++) { - data[i][j] = moment(i + j); - data[j][i] = data[i][j]; - } - } - - for (int i = 0; i < m; i++) { - moments[i] = data[0][i]; - moments[i + m - 1] = data[i][m - 1]; - } + final int k = lPlusN - 1; - moments[2 * m - 1] = moment(2 * m - 1); - - return Matrices.createSquareMatrix(data); + for (int i = 0; i < lPlusN; i++) { + m = 1; + for (int j = 0; j < i; j++) { + m *= (k - j); + } + f += m * fastPowLoop(x, k - i); + } - } - - private Vector momentVector(final int lowerInclusive, final int upperExclusive) { - var array = IntStream.range(lowerInclusive, upperExclusive).mapToDouble(i -> -moments[i]).toArray(); - return new Vector(array); - } + return f * exp; - private Vector weights(final double[] roots) { - final var x = xMatrix(roots); - final var a = momentVector(0, m).inverted(); - - return x.inverse().multiply(a); - } - - private double[] roots() { - double[] roots; - double[] c = new double[m + 1]; - - // coefficients of the monic polynomial x_j^m + sum_{l=0}^{m-1}{c_lx_j^l} - System.arraycopy(coefficients().getData(), 0, c, 0, m); - c[m] = 1.0; + } + + private static double[] solveCubic(final double a, final double b, final double c) { + final double p = b / 3.0 - a * a / 9.0; + final double q = a * a * a / 27.0 - a * b / 6.0 + c / 2.0; + + final double ang = acos(-q / sqrt(-p * p * p)); + final double r = 2.0 * sqrt(-p); + var result = new double[3]; + double theta; + for (int k = -1; k < 2; k++) { + theta = (ang - 2.0 * PI * k) / 3.0; + result[k + 1] = r * cos(theta); + } + + for (int i = 0; i < result.length; i++) { + result[i] -= a / 3.0; + } + + return result; + } + + private double moment(int l) { + var bounds = this.transformedBounds(); + return momentIntegral(bounds[1], l, expUpper) - momentIntegral(bounds[0], l, expLower); + } + + private double momentIntegral(final double x, final int l, final double exp) { + + double e = 0; + int m = 0; + + final int n = getOrder(); + final int lPlusOne = l + 1; + + for (int i = 0; i < n; i++) { + m = lPlusOne; + for (int j = 1; j < i + 1; j++) { + m *= (lPlusOne + j); + } + e += ExponentialIntegrals.get(n - i).valueAt(x) * fastPowLoop(x, lPlusOne + i) / ((double) m); + } + + return e + auxilliaryIntegral(x, l + n, exp) / ((double) m); + + } + + private Vector coefficients() { + return momentMatrix().inverse().multiply(momentVector(m, 2 * m)); + } + + private SquareMatrix momentMatrix() { + + double[][] data = new double[m][m]; + moments = new double[2 * m]; + + // diagonal elements + IntStream.range(0, m).forEach(i -> data[i][i] = moment(i * 2)); + + // find (symmetric) non-diagonal elements + for (int i = 1, j = 0; i < m; i++) { + for (j = 0; j < i; j++) { + data[i][j] = moment(i + j); + data[j][i] = data[i][j]; + } + } + + for (int i = 0; i < m; i++) { + moments[i] = data[0][i]; + moments[i + m - 1] = data[i][m - 1]; + } + + moments[2 * m - 1] = moment(2 * m - 1); + + return Matrices.createSquareMatrix(data); + + } + + private Vector momentVector(final int lowerInclusive, final int upperExclusive) { + var array = IntStream.range(lowerInclusive, upperExclusive).mapToDouble(i -> -moments[i]).toArray(); + return new Vector(array); + } + + private Vector weights(final double[] roots) { + final var x = xMatrix(roots); + final var a = momentVector(0, m).inverted(); + + return x.inverse().multiply(a); + } + + private double[] roots() { + double[] roots; + double[] c = new double[m + 1]; + + // coefficients of the monic polynomial x_j^m + sum_{l=0}^{m-1}{c_lx_j^l} + System.arraycopy(coefficients().getData(), 0, c, 0, m); + c[m] = 1.0; - switch (m) { - // m = 1 never used - case 2: - roots = new double[2]; - // solve quadratic equation, all roots of which are real - final double det = sqrt(c[1] * c[1] - 4.0 * c[0]); - roots[0] = (-c[1] + det) * 0.5; - roots[1] = (-c[1] - det) * 0.5; - break; - case 3: - roots = new double[3]; - // solve cubic equation, all roots of which are real - roots = solveCubic(c[2], c[1], c[0]); - break; - default: - // use LaguerreSolver - roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); - } + switch (m) { + // m = 1 never used + case 2: + roots = new double[2]; + // solve quadratic equation, all roots of which are real + final double det = sqrt(c[1] * c[1] - 4.0 * c[0]); + roots[0] = (-c[1] + det) * 0.5; + roots[1] = (-c[1] - det) * 0.5; + break; + case 3: + roots = new double[3]; + // solve cubic equation, all roots of which are real + roots = solveCubic(c[2], c[1], c[0]); + break; + default: + // use LaguerreSolver + roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); + } - for (int i = 0; i < roots.length; i++) { - roots[i] = (roots[i] - getAlpha()) / getBeta(); - } + for (int i = 0; i < roots.length; i++) { + roots[i] = (roots[i] - getAlpha()) / getBeta(); + } - return roots; - - } + return roots; + + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java index c602745b..f2f23282 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java @@ -9,141 +9,146 @@ import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import java.util.List; +import java.util.Set; import pulse.math.FixedIntervalIntegrator; import pulse.math.MidpointIntegrator; import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; /** - * A class for evaluating the composition product using a simple Newton-Cotes quadrature - * with a cutoff. + * A class for evaluating the composition product using a simple Newton-Cotes + * quadrature with a cutoff. * */ - public class NewtonCotesQuadrature extends CompositionProduct { - private final static int DEFAULT_SEGMENTS = 64; - private final static double DEFAULT_CUTOFF = 16.0; - private FixedIntervalIntegrator integrator; - private double cutoff; - - /** - * Constructs a default {@code NewtonCotesQuadrature} with integration bounds spanning from 0 to 1. - */ - - public NewtonCotesQuadrature() { - this(new Segment(0, 1)); - } - - /** - * Constructs a default {@code NewtonCotesQuadrature} whose integration bounds are specified by the argument. - * @param bounds the integration bounds - */ - - public NewtonCotesQuadrature(Segment bounds) { - this(bounds, derive(INTEGRATION_SEGMENTS, DEFAULT_SEGMENTS)); - } - - /** - * Constructs a custom {@code NewtonCotesQuadrature} with specified integration bounds and number of integration segments. - * The underlying integration scheme by default is a {@code SimpsonIntegrator}. - * @param bounds the integration bounds - * @param segments the number of integration segments. The higher this number, the higher is the accuracy. - * @see pulse.math.SimpsonIntegrator - */ - - public NewtonCotesQuadrature(Segment bounds, NumericProperty segments) { - super(bounds); - setCutoff(derive(INTEGRATION_CUTOFF, DEFAULT_CUTOFF)); - CompositionProduct reference = this; - integrator = new MidpointIntegrator(new Segment(0.0, 1.0), segments) { - - @Override - public double integrand(double... vars) { - return reference.integrand(vars); - } - - @Override - public String toString() { - return getDescriptor() + " ; " + getIntegrationSegments(); - } - - @Override - public String getDescriptor() { - return "Midpoint Integrator"; - } - - }; - integrator.setParent(this); - } - - /** - * Uses the Newton-Cotes integrator (by default, the Simpson's rule) to evaluate the composition product. - */ - - @Override - public double integrate() { - integrator.setBounds(truncatedBounds()); - return integrator.integrate(); - } - - /** - * This will retrieve the Newton-Cotes integrator, which by default is the Simpson integrator. - * @return the integrator - */ - - public FixedIntervalIntegrator getIntegrator() { - return integrator; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + cutoff + " ; " + integrator.getIntegrationSegments(); - } - - public NumericProperty getCutoff() { - return derive(INTEGRATION_CUTOFF, cutoff); - } - - public void setCutoff(NumericProperty cutoff) { - requireType(cutoff, INTEGRATION_CUTOFF); - this.cutoff = (double) cutoff.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == INTEGRATION_CUTOFF) { - setCutoff(property); - firePropertyChanged(this, property); - } - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(INTEGRATION_CUTOFF)); - list.add(def(INTEGRATION_SEGMENTS)); - return list; - } - - @Override - public boolean ignoreSiblings() { - return true; - } - - private Segment truncatedBounds() { - final double min = getBounds().getMinimum(); - final double max = getBounds().getMaximum(); - - double bound = (cutoff - getAlpha()) / getBeta(); - - double a = 0.5 - getBeta() / 2; // beta usually takes values of 1 or -1, so a is either 0 or 1 - double b = 1. - a; // either 1 or 0 - - return new Segment(max(bound, min) * a + min * b, max * a + min(bound, max) * b); - } - -} \ No newline at end of file + private final static int DEFAULT_SEGMENTS = 64; + private final static double DEFAULT_CUTOFF = 16.0; + private FixedIntervalIntegrator integrator; + private double cutoff; + + /** + * Constructs a default {@code NewtonCotesQuadrature} with integration + * bounds spanning from 0 to 1. + */ + public NewtonCotesQuadrature() { + this(new Segment(0, 1)); + } + + /** + * Constructs a default {@code NewtonCotesQuadrature} whose integration + * bounds are specified by the argument. + * + * @param bounds the integration bounds + */ + public NewtonCotesQuadrature(Segment bounds) { + this(bounds, derive(INTEGRATION_SEGMENTS, DEFAULT_SEGMENTS)); + } + + /** + * Constructs a custom {@code NewtonCotesQuadrature} with specified + * integration bounds and number of integration segments. The underlying + * integration scheme by default is a {@code SimpsonIntegrator}. + * + * @param bounds the integration bounds + * @param segments the number of integration segments. The higher this + * number, the higher is the accuracy. + * @see pulse.math.SimpsonIntegrator + */ + public NewtonCotesQuadrature(Segment bounds, NumericProperty segments) { + super(bounds); + setCutoff(derive(INTEGRATION_CUTOFF, DEFAULT_CUTOFF)); + CompositionProduct reference = this; + integrator = new MidpointIntegrator(new Segment(0.0, 1.0), segments) { + + @Override + public double integrand(double... vars) { + return reference.integrand(vars); + } + + @Override + public String toString() { + return getDescriptor() + " ; " + getIntegrationSegments(); + } + + @Override + public String getDescriptor() { + return "Midpoint Integrator"; + } + + }; + integrator.setParent(this); + } + + /** + * Uses the Newton-Cotes integrator (by default, the Simpson's rule) to + * evaluate the composition product. + */ + @Override + public double integrate() { + integrator.setBounds(truncatedBounds()); + return integrator.integrate(); + } + + /** + * This will retrieve the Newton-Cotes integrator, which by default is the + * Simpson integrator. + * + * @return the integrator + */ + public FixedIntervalIntegrator getIntegrator() { + return integrator; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + cutoff + " ; " + integrator.getIntegrationSegments(); + } + + public NumericProperty getCutoff() { + return derive(INTEGRATION_CUTOFF, cutoff); + } + + public void setCutoff(NumericProperty cutoff) { + requireType(cutoff, INTEGRATION_CUTOFF); + this.cutoff = (double) cutoff.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == INTEGRATION_CUTOFF) { + setCutoff(property); + firePropertyChanged(this, property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(INTEGRATION_CUTOFF); + set.add(INTEGRATION_SEGMENTS); + return set; + } + + @Override + public boolean ignoreSiblings() { + return true; + } + + private Segment truncatedBounds() { + final double min = getBounds().getMinimum(); + final double max = getBounds().getMaximum(); + + double bound = (cutoff - getAlpha()) / getBeta(); + + double a = 0.5 - getBeta() / 2; // beta usually takes values of 1 or -1, so a is either 0 or 1 + double b = 1. - a; // either 1 or 0 + + return new Segment(max(bound, min) * a + min * b, max * a + min(bound, max) * b); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index 87a90859..2fcaea61 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -8,6 +8,7 @@ import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import java.util.List; +import java.util.Set; import pulse.problem.schemes.CoupledImplicitScheme; import pulse.problem.schemes.DifferenceScheme; @@ -19,210 +20,213 @@ import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import pulse.properties.Property; import pulse.ui.Messages; public class MixedCoupledSolver extends CoupledImplicitScheme implements Solver { - private RadiativeTransferSolver rte; - private Fluxes fluxes; + private RadiativeTransferSolver rte; + private Fluxes fluxes; - private int N; - private double hx; - private double tau; - private double sigma; + private int N; + private double hx; + private double tau; + private double sigma; - private final static double A = 5.0 / 6.0; - private final static double B = 1.0 / 12.0; + private final static double A = 5.0 / 6.0; + private final static double B = 1.0 / 12.0; - private final static double EPS = 1e-7; // a small value ensuring numeric stability + private final static double EPS = 1e-7; // a small value ensuring numeric stability - private double Bi1; + private double Bi1; - private double HX2; - private double HX_NP; - private double TAU0_NP; - private double ONE_PLUS_Bi1_HX; - private double SIGMA_NP; + private double HX2; + private double HX_NP; + private double TAU0_NP; + private double ONE_PLUS_Bi1_HX; + private double SIGMA_NP; - private double _2TAUHX; - private double HX2_2TAU; - private double ONE_MINUS_SIGMA_NP; - private double _2TAU_ONE_MINUS_SIGMA; - private double BETA1_FACTOR; - private double ONE_MINUS_SIGMA; + private double _2TAUHX; + private double HX2_2TAU; + private double ONE_MINUS_SIGMA_NP; + private double _2TAU_ONE_MINUS_SIGMA; + private double BETA1_FACTOR; + private double ONE_MINUS_SIGMA; - public MixedCoupledSolver() { - super(derive(GRID_DENSITY, 16), derive(TAU_FACTOR, 0.25)); - sigma = (double) def(SCHEME_WEIGHT).getValue(); - } + public MixedCoupledSolver() { + super(derive(GRID_DENSITY, 16), derive(TAU_FACTOR, 0.25)); + sigma = (double) def(SCHEME_WEIGHT).getValue(); + } - public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - sigma = (double) def(SCHEME_WEIGHT).getValue(); - } + public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + sigma = (double) def(SCHEME_WEIGHT).getValue(); + } - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - var coupling = getCoupling(); - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); - - var U = getPreviousSolution(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - tau = grid.getTimeStep(); - - Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - fluxes = rte.getFluxes(); - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(int i) { - return A * fluxes.meanFluxDerivative(i) - + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); - } - - @Override - public double beta(final double f, final double phi, final int i) { - return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); - } - - @Override - public void evaluateBeta(final double[] U) { - final double phiSecond = A * fluxes.meanFluxDerivative(1) - + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); - setBeta(2, beta(U[1] / tau, phiSecond, 2)); - - super.evaluateBeta(U, 3, N); - - final double phiLast = A * fluxes.meanFluxDerivative(N - 1) - + B * (fluxes.meanFluxDerivative(N - 2) + fluxes.meanFluxDerivativeRear()); - setBeta(N, beta(U[N - 1] / tau, phiLast, N)); - - } - - }; - setTridiagonalMatrixAlgorithm(tridiagonal); - } - - private void initConst(ParticipatingMedium problem) { - var p = (ThermoOpticalProperties)problem.getProperties(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double opticalThickness = (double) p.getOpticalThickness().getValue(); - - HX2 = hx * hx; - adjustSchemeWeight(); - - ONE_MINUS_SIGMA = 1.0 - sigma; - TAU0_NP = opticalThickness / Np; - - final double Bi2HX = Bi1 * hx; - ONE_PLUS_Bi1_HX = 1. + Bi2HX; - - _2TAUHX = 2.0 * tau * hx; - HX2_2TAU = HX2 / (2.0 * tau); - ONE_MINUS_SIGMA_NP = ONE_MINUS_SIGMA / Np; - _2TAU_ONE_MINUS_SIGMA = 2.0 * tau * ONE_MINUS_SIGMA; - BETA1_FACTOR = 1.0 / (HX2 + 2.0 * tau * sigma * ONE_PLUS_Bi1_HX); - SIGMA_NP = sigma / Np; - HX_NP = hx / Np; - - final double sigma_HX2 = sigma / HX2; - var tridiagonal = getTridiagonalMatrixAlgorithm(); - tridiagonal.setCoefA(sigma_HX2); - tridiagonal.setCoefB(1. / tau + 2. * sigma_HX2); - tridiagonal.setCoefC(sigma_HX2); - final double alpha0 = 1.0 / (HX2_2TAU / sigma + ONE_PLUS_Bi1_HX); - tridiagonal.setAlpha(1, alpha0); - tridiagonal.evaluateAlpha(); - } - - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - initConst(problem); - - setCalculationStatus(rte.compute(getPreviousSolution())); - this.runTimeSequence(problem); - - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); - - } - - @Override - public double pulse(final int m) { - //todo - var pulse = getDiscretePulse(); - return (pulse.laserPowerAt((m - 1 + EPS) * tau) * ONE_MINUS_SIGMA - + pulse.laserPowerAt((m - EPS) * tau) * sigma); - } - - @Override - public double firstBeta(final int m) { - var U = getPreviousSolution(); - final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); - return (_2TAUHX - * (getCurrentPulseValue() - SIGMA_NP * fluxes.getFlux(0) - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) - + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); - final var U = getPreviousSolution(); - return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi - + ONE_MINUS_SIGMA * (U[N - 1] - U[N] * ONE_PLUS_Bi1_HX) - + HX_NP * (sigma * fluxes.getFlux(N) + ONE_MINUS_SIGMA * fluxes.getStoredFlux(N))) - / (HX2_2TAU + sigma * (ONE_PLUS_Bi1_HX - alphaN)); - } - - private void adjustSchemeWeight() { - final double newSigma = 0.5 - HX2 / (12.0 * tau); - setWeight(derive(SCHEME_WEIGHT, newSigma > 0 ? newSigma : 0.5)); - } - - public void setWeight(NumericProperty weight) { - requireType(weight, SCHEME_WEIGHT); - this.sigma = (double) weight.getValue(); - } - - public NumericProperty getWeight() { - return derive(SCHEME_WEIGHT, sigma); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(SCHEME_WEIGHT)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == SCHEME_WEIGHT) - setWeight(property); - else - super.set(type, property); - } - - @Override - public String toString() { - return Messages.getString("MixedScheme2.4"); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - -} \ No newline at end of file + private void prepare(ParticipatingMedium problem) { + super.prepare(problem); + + var grid = getGrid(); + + var coupling = getCoupling(); + coupling.init(problem, grid); + rte = coupling.getRadiativeTransferEquation(); + + var U = getPreviousSolution(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + tau = grid.getTimeStep(); + + Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + + fluxes = rte.getFluxes(); + + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(int i) { + return A * fluxes.meanFluxDerivative(i) + + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); + } + + @Override + public double beta(final double f, final double phi, final int i) { + return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); + } + + @Override + public void evaluateBeta(final double[] U) { + final double phiSecond = A * fluxes.meanFluxDerivative(1) + + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); + setBeta(2, beta(U[1] / tau, phiSecond, 2)); + + super.evaluateBeta(U, 3, N); + + final double phiLast = A * fluxes.meanFluxDerivative(N - 1) + + B * (fluxes.meanFluxDerivative(N - 2) + fluxes.meanFluxDerivativeRear()); + setBeta(N, beta(U[N - 1] / tau, phiLast, N)); + + } + + }; + setTridiagonalMatrixAlgorithm(tridiagonal); + } + + private void initConst(ParticipatingMedium problem) { + var p = (ThermoOpticalProperties) problem.getProperties(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double opticalThickness = (double) p.getOpticalThickness().getValue(); + + HX2 = hx * hx; + adjustSchemeWeight(); + + ONE_MINUS_SIGMA = 1.0 - sigma; + TAU0_NP = opticalThickness / Np; + + final double Bi2HX = Bi1 * hx; + ONE_PLUS_Bi1_HX = 1. + Bi2HX; + + _2TAUHX = 2.0 * tau * hx; + HX2_2TAU = HX2 / (2.0 * tau); + ONE_MINUS_SIGMA_NP = ONE_MINUS_SIGMA / Np; + _2TAU_ONE_MINUS_SIGMA = 2.0 * tau * ONE_MINUS_SIGMA; + BETA1_FACTOR = 1.0 / (HX2 + 2.0 * tau * sigma * ONE_PLUS_Bi1_HX); + SIGMA_NP = sigma / Np; + HX_NP = hx / Np; + + final double sigma_HX2 = sigma / HX2; + var tridiagonal = getTridiagonalMatrixAlgorithm(); + tridiagonal.setCoefA(sigma_HX2); + tridiagonal.setCoefB(1. / tau + 2. * sigma_HX2); + tridiagonal.setCoefC(sigma_HX2); + final double alpha0 = 1.0 / (HX2_2TAU / sigma + ONE_PLUS_Bi1_HX); + tridiagonal.setAlpha(1, alpha0); + tridiagonal.evaluateAlpha(); + } + + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + this.prepare(problem); + initConst(problem); + + setCalculationStatus(rte.compute(getPreviousSolution())); + this.runTimeSequence(problem); + + var status = getCalculationStatus(); + if (status != RTECalculationStatus.NORMAL) { + throw new SolverException(status.toString()); + } + + } + + @Override + public double pulse(final int m) { + //todo + var pulse = getDiscretePulse(); + return (pulse.laserPowerAt((m - 1 + EPS) * tau) * ONE_MINUS_SIGMA + + pulse.laserPowerAt((m - EPS) * tau) * sigma); + } + + @Override + public double firstBeta(final int m) { + var U = getPreviousSolution(); + final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); + return (_2TAUHX + * (getCurrentPulseValue() - SIGMA_NP * fluxes.getFlux(0) - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) + + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; + } + + @Override + public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); + final var U = getPreviousSolution(); + return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi + + ONE_MINUS_SIGMA * (U[N - 1] - U[N] * ONE_PLUS_Bi1_HX) + + HX_NP * (sigma * fluxes.getFlux(N) + ONE_MINUS_SIGMA * fluxes.getStoredFlux(N))) + / (HX2_2TAU + sigma * (ONE_PLUS_Bi1_HX - alphaN)); + } + + private void adjustSchemeWeight() { + final double newSigma = 0.5 - HX2 / (12.0 * tau); + setWeight(derive(SCHEME_WEIGHT, newSigma > 0 ? newSigma : 0.5)); + } + + public void setWeight(NumericProperty weight) { + requireType(weight, SCHEME_WEIGHT); + this.sigma = (double) weight.getValue(); + } + + public NumericProperty getWeight() { + return derive(SCHEME_WEIGHT, sigma); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SCHEME_WEIGHT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == SCHEME_WEIGHT) { + setWeight(property); + } else { + super.set(type, property); + } + } + + @Override + public String toString() { + return Messages.getString("MixedScheme2.4"); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 23421c60..4a27a0ff 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -9,91 +9,96 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.COMBINED_ABSORPTIVITY; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import static pulse.properties.NumericPropertyKeyword.THICKNESS; import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; public abstract class AbsorptionModel extends PropertyHolder implements Reflexive { - private Map absorptionMap; - - protected AbsorptionModel() { - setPrefix("Absorption model"); - absorptionMap = new HashMap<>(); - absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); - absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); - } - - public abstract double absorption(SpectralRange range, double x); - - public NumericProperty getLaserAbsorptivity() { - return absorptionMap.get(LASER); - } - - public NumericProperty getThermalAbsorptivity() { - return absorptionMap.get(THERMAL); - } - - public NumericProperty getCombinedAbsorptivity() { - return getThermalAbsorptivity(); - } - - public NumericProperty getAbsorptivity(SpectralRange spectrum) { - return absorptionMap.get(spectrum); - } - - public void setAbsorptivity(SpectralRange range, NumericProperty a) { - absorptionMap.put(range, a); - } - - public void setLaserAbsorptivity(NumericProperty a) { - absorptionMap.put(LASER, a); - } - - public void setThermalAbsorptivity(NumericProperty a) { - absorptionMap.put(THERMAL, a); - } - - public void setCombinedAbsorptivity(NumericProperty a) { - setThermalAbsorptivity(a); - setLaserAbsorptivity(a); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - - switch (type) { - case LASER_ABSORPTIVITY: - absorptionMap.put(LASER, property); - break; - case THERMAL_ABSORPTIVITY: - absorptionMap.put(THERMAL, property); - break; - case COMBINED_ABSORPTIVITY: - setCombinedAbsorptivity(property); - break; - default: - break; - } - - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + absorptionMap.get(LASER) + " ; " + absorptionMap.get(THERMAL); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(LASER_ABSORPTIVITY)); - list.add(def(THERMAL_ABSORPTIVITY)); - list.add(def(COMBINED_ABSORPTIVITY)); - return list; - } - -} \ No newline at end of file + private Map absorptionMap; + + protected AbsorptionModel() { + setPrefix("Absorption model"); + absorptionMap = new HashMap<>(); + absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); + absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); + } + + public abstract double absorption(SpectralRange range, double x); + + public NumericProperty getLaserAbsorptivity() { + return absorptionMap.get(LASER); + } + + public NumericProperty getThermalAbsorptivity() { + return absorptionMap.get(THERMAL); + } + + public NumericProperty getCombinedAbsorptivity() { + return getThermalAbsorptivity(); + } + + public NumericProperty getAbsorptivity(SpectralRange spectrum) { + return absorptionMap.get(spectrum); + } + + public void setAbsorptivity(SpectralRange range, NumericProperty a) { + absorptionMap.put(range, a); + } + + public void setLaserAbsorptivity(NumericProperty a) { + absorptionMap.put(LASER, a); + } + + public void setThermalAbsorptivity(NumericProperty a) { + absorptionMap.put(THERMAL, a); + } + + public void setCombinedAbsorptivity(NumericProperty a) { + setThermalAbsorptivity(a); + setLaserAbsorptivity(a); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case LASER_ABSORPTIVITY: + absorptionMap.put(LASER, property); + break; + case THERMAL_ABSORPTIVITY: + absorptionMap.put(THERMAL, property); + break; + case COMBINED_ABSORPTIVITY: + setCombinedAbsorptivity(property); + break; + default: + break; + } + + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + absorptionMap.get(LASER) + " ; " + absorptionMap.get(THERMAL); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(LASER_ABSORPTIVITY); + set.add(THERMAL_ABSORPTIVITY); + set.add(COMBINED_ABSORPTIVITY); + return set; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 74c0ec4a..c7719879 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,10 +2,10 @@ public class BeerLambertAbsorption extends AbsorptionModel { - @Override - public double absorption(SpectralRange range, double y) { - double a = (double) (this.getAbsorptivity(range).getValue()); - return a * Math.exp(-a * y); - } + @Override + public double absorption(SpectralRange range, double y) { + double a = (double) (this.getAbsorptivity(range).getValue()); + return a * Math.exp(-a * y); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java index 097eb1ac..23153904 100644 --- a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -1,60 +1,57 @@ package pulse.problem.statements.model; +import java.util.Set; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; - -import java.util.List; - import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; public class DiathermicProperties extends ThermalProperties { - private double diathermicCoefficient; - - public DiathermicProperties() { - super(); - this.diathermicCoefficient = (double) def(DIATHERMIC_COEFFICIENT).getValue(); - } - - public DiathermicProperties(ThermalProperties p) { - super(p); - var property = p instanceof DiathermicProperties - ? ((DiathermicProperties) p).getDiathermicCoefficient() - : def(DIATHERMIC_COEFFICIENT); - this.diathermicCoefficient = (double)property.getValue(); - } - - public ThermalProperties copy() { - return new ThermalProperties(this); - } - - public NumericProperty getDiathermicCoefficient() { - return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); - } - - public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { - requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); - this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == DIATHERMIC_COEFFICIENT) { - diathermicCoefficient = ((Number) property.getValue()).doubleValue(); - } else { - super.set(type, property); - } - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(DIATHERMIC_COEFFICIENT)); - return list; - } - -} \ No newline at end of file + private double diathermicCoefficient; + + public DiathermicProperties() { + super(); + this.diathermicCoefficient = (double) def(DIATHERMIC_COEFFICIENT).getValue(); + } + + public DiathermicProperties(ThermalProperties p) { + super(p); + var property = p instanceof DiathermicProperties + ? ((DiathermicProperties) p).getDiathermicCoefficient() + : def(DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double) property.getValue(); + } + + public ThermalProperties copy() { + return new ThermalProperties(this); + } + + public NumericProperty getDiathermicCoefficient() { + return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); + } + + public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { + requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == DIATHERMIC_COEFFICIENT) { + diathermicCoefficient = ((Number) property.getValue()).doubleValue(); + } else { + super.set(type, property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DIATHERMIC_COEFFICIENT); + return set; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 9c554b29..b4c37b95 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -5,6 +5,7 @@ import static pulse.properties.NumericPropertyKeyword.REFLECTANCE; import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -43,10 +44,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(REFLECTANCE)); - return list; + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(REFLECTANCE); + return set; } } diff --git a/src/main/java/pulse/properties/Property.java b/src/main/java/pulse/properties/Property.java index c371e635..a1215d74 100644 --- a/src/main/java/pulse/properties/Property.java +++ b/src/main/java/pulse/properties/Property.java @@ -4,43 +4,40 @@ * The basic interface for properties. The only declared functionality consists * in the ability to report the associated value and deliver text description. */ - public interface Property { - /** - * Retrieves the value of this {@code Property}. - * - * @return an object representing the value of this {@code Property} - */ - - public Object getValue(); - - /** - * Formats the value so that it is suitable for output using the GUI or console. - * - * @return a formatted {@code String} representing the {@code value} - */ - - public default String formattedOutput() { - return getValue().toString(); - }; - - /** - * Creates a {@code String} to describe this property (often used in GUI - * applications). - * - * @param addHtmlTags if {@code true}, adds the 'html' tags at both ends of the - * descriptor {@code String}. - * @return a {@code String}, with or without 'html' tags, describing this - * {@code Property} - */ - - public String getDescriptor(boolean addHtmlTags); - - public boolean attemptUpdate(Object value); - - public default Object identifier() { - return getClass(); - } - -} \ No newline at end of file + /** + * Retrieves the value of this {@code Property}. + * + * @return an object representing the value of this {@code Property} + */ + public Object getValue(); + + /** + * Formats the value so that it is suitable for output using the GUI or + * console. + * + * @return a formatted {@code String} representing the {@code value} + */ + public default String formattedOutput() { + return getValue().toString(); + } + + /** + * Creates a {@code String} to describe this property (often used in GUI + * applications). + * + * @param addHtmlTags if {@code true}, adds the 'html' tags at both ends of + * the descriptor {@code String}. + * @return a {@code String}, with or without 'html' tags, describing this + * {@code Property} + */ + public String getDescriptor(boolean addHtmlTags); + + public boolean attemptUpdate(Object value); + + public default Object identifier() { + return getClass(); + } + +} From 83dd1e0f2725491ff3b965e7e5216d02873b3fac Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 12:40:53 +0100 Subject: [PATCH 077/116] Re-arranged handling of Flags and NumericProperties Introduced the optimisable boolean in NumericProperty. This boolean indicates whether this property can be optimised. Flag.allFlags() now returns a list of only those flags which correspond to numeric properties with isOptimisable() = true Removed Flag.allProblemIndependentFlags() due to being obsolete Intoduced dimensionDelta in NumericProperty to account for a possible change of format of the test temperature (for example, conversion between K to deg. C). This dimensionDelta is directly added to the value returned by valueInCurrentUnts. Fixed errorInCurrentUnits throwing typecast exceptions due to incorrect casting to double. Now this works with any type of Numbers including ints. Removed valueOutput() and errorOutput() (formatting is now a prerogative of a separate NumericPropertyFormatter) Removed formatting methods from NumericProperties. Formatting is now handled by NumericPropertyFormatter, which is a type of AbstractFormatter that accepts NumericProperty objects. There are three formats available to format any property: a standard decimal format with fixed number of digits after the decimal point, an integer format for properties with integer values, and a newly introduced ScientificFormat, which renders values using an html output, e.g.: 10 × 52. This is then converted to a scientific notation. Works with JTables. --- src/main/java/pulse/properties/Flag.java | 40 +-- .../pulse/properties/NumericProperties.java | 291 +++++++----------- .../pulse/properties/NumericProperty.java | 74 +++-- .../properties/NumericPropertyFormatter.java | 214 +++++++++++++ .../pulse/properties/ScientificFormat.java | 112 +++++++ .../pulse/search/direction/ActiveFlags.java | 99 +++--- 6 files changed, 541 insertions(+), 289 deletions(-) create mode 100644 src/main/java/pulse/properties/NumericPropertyFormatter.java create mode 100644 src/main/java/pulse/properties/ScientificFormat.java diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 2433f4eb..830f8ab1 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -1,12 +1,6 @@ package pulse.properties; import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.defaultList; -import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; -import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; -import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; - -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -63,26 +57,23 @@ public static List convert(List flags) { } /** - * The default list of {@code Flag}s used in finding the reverse solution of - * the heat conduction problem contains: - * DIFFUSIVITY (true), HEAT_LOSS (true), MAXTEMP (true), BASELINE_INTERCEPT (false), BASELINE_SLOPE (false) . + * List of all possible {@code Flag}s that can be used in finding the + * reverse solution of the heat conduction problems. Includes all flags that + * correspond to {@code NumericPropert}ies satisfying + * {@code p.isOptimisable() = true}. The default value of the flag is set to + * {@code p.isDefaultSearchVariable()} -- based on the information contained + * in the {@code NumericProperty.xml} file. * - * @return a {@code List} of default {@code Flag}s + * @return a {@code List} of all possible {@code Flag}s + * @see */ - public static List allProblemDependentFlags() { - return defaultList().stream() + public static List allFlags() { + return NumericProperties.defaultList().stream() + .filter(p -> p.isOptimisable()) .map(p -> new Flag(p, p.isDefaultSearchVariable())) .collect(Collectors.toList()); } - public static List allProblemIndependentFlags() { - List flags = new ArrayList<>(); - flags.add(new Flag(def(TIME_SHIFT), false)); - flags.add(new Flag(def(LOWER_BOUND), false)); - flags.add(new Flag(def(UPPER_BOUND), false)); - return flags; - } - /** * Returns the type of this {@code Flag}. * @@ -159,13 +150,8 @@ public boolean equals(Object o) { Flag f = (Flag) o; - if (f.getType() == this.getType()) { - if (f.getValue().equals(this.getValue())) { - return true; - } - } - - return false; + return (f.getType() == this.getType()) + && (f.getValue().equals(this.getValue())); } diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index 9be3c48f..cdda6226 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -11,188 +11,113 @@ * Default operations with NumericProperties * */ - public class NumericProperties { - /** - * The list of default properties read that is created by reading the default - * {@code .xml} file. - */ - - private final static List DEFAULT = XMLConverter.readDefaultXML(); - - private NumericProperties() { - //empty constructor - } - - /** - * Checks whether the {@code val} that is going to be passed to the - * {@code property} (a) has the same type as the {@code property.getValue()} - * object; (b) is confined within the definition domain: - * {@code minimum <= value <= maximum}. - * - * @param property the {@code property} containing the definition domain - * @param val a numeric value, the conformity of which to the definition - * domain needs to be checked - * @return {@code true} if {@code minimum <= val <= maximum} and if both - * {@code val} and {@code value} are instances of the same - * {@code class}; {@code false} otherwise - */ - - public static boolean isValueSensible(NumericProperty property, Number val) { - if (!property.getValue().getClass().equals(val.getClass())) - return false; - - double v = val.doubleValue(); - - final double EPS = 1E-12; - - if (v > property.getMaximum().doubleValue() + EPS) - return false; - - return v >= property.getMinimum().doubleValue() - EPS; - - } - - public static String printRangeAndNumber(NumericProperty p, Number value) { - StringBuilder msg = new StringBuilder(); - msg.append("Acceptable region for "); - msg.append("parameter : "); - msg.append(p.getValue().getClass().getSimpleName()); - msg.append(" [ " + p.getMinimum()); - msg.append(" : " + p.getMaximum() + " ]. "); - msg.append("Value received: " + value); - return msg.toString(); - } - - public static NumberFormat numberFormat(NumericProperty p, boolean convertDimension) { - var value = p.getValue(); - - if (value instanceof Integer) - return NumberFormat.getIntegerInstance(); - - double adjustedValue = convertDimension ? (double) value * p.getDimensionFactor().doubleValue() - : (double) value; - double absAdjustedValue = Math.abs(adjustedValue); - - final double UPPER_LIMIT = 1e4; // the upper limit, used for formatting - final double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting - final double ZERO = 1e-30; - - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) - return new DecimalFormat(Messages.getString("NumericProperty.BigNumberFormat")); - else - return new DecimalFormat(Messages.getString("NumericProperty.NumberFormat")); - } - - public static List defaultList() { - return DEFAULT; - } - - /** - * Searches for the default {@code NumericProperty} corresponding to - * {@code keyword} in the list of pre-defined properties loaded from the - * respective {@code .xml} file. - * - * @param keyword one of the constant {@code NumericPropertyKeyword}s - * @return a {@code NumericProperty} in the default list of properties - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty def(NumericPropertyKeyword keyword) { - return new NumericProperty(DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); - } - - /** - * Compares the numeric values of this {@code NumericProperty} and {@code arg0} - * - * @param a a {@code NumericProperty} - * @param b another {@code NumericProperty} - * @return {@code true} if the values are equals - */ - - public static int compare(NumericProperty a, NumericProperty b) { - Double d1 = ((Number) a.getValue()).doubleValue(); - Double d2 = ((Number) b.getValue()).doubleValue(); - - final double eps = 1E-8 * (d1 + d2) / 2.0; - - return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); - } - - /** - * Searches for the default {@code NumericProperty} corresponding to - * {@code keyword} in the list of pre-defined properties loaded from the - * respective {@code .xml} file, and if found creates a new - * {@NumericProperty} which will replicate all field of the latter, but will set - * its value to {@code value}. - * - * @param keyword one of the constant {@code NumericPropertyKeyword}s - * @param value the new value for the created {@code NumericProperty} - * @return a new {@code NumericProperty} that is built according to the default - * pattern specified by the {@code keyword}, but with a different - * {@code value} - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty derive(NumericPropertyKeyword keyword, Number value) { - return new NumericProperty(value, DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); - } - - /** - * Used to print out a nice {@code value} for GUI applications and for - * exporting. - *

- * Will use a {@code DecimalFormat} to reduce the number of digits, if - * neccessary. Automatically detects whether it is dealing with {@code int} or - * {@code double} values, and adjust formatting accordingly. If - * {@code error != null}, will use the latter as the error value, which is - * separated from the main value by a plus-minus sign. - *

- * - * @param convertDimension if {@code true}, the output will be the - * {@code value * dimensionFactor} - * @return a nice {@code String} representing the {@code value} of this - * {@code NumericProperty} and its {@code error} - */ - - public static String formattedValueAndError(NumericProperty p, boolean convertDimension) { - - if (p.getValue() instanceof Integer) { - Number val = convertDimension ? ((Number) p.getValue()).intValue() * p.getDimensionFactor().intValue() - : ((Number) p.getValue()).intValue(); - return (NumberFormat.getIntegerInstance()).format(val); - } - - final String PLUS_MINUS = Messages.getString("NumericProperty.PlusMinus"); - - final double UPPER_LIMIT = 1e4; // the upper limit, used for formatting - final double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting - final double ZERO = 1e-30; - - double adjustedValue = convertDimension ? p.valueInCurrentUnits().doubleValue() : (double) p.getValue(); - - double absAdjustedValue = Math.abs(adjustedValue); - - DecimalFormat selectedFormat = null; - - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) - selectedFormat = new DecimalFormat(Messages.getString("NumericProperty.BigNumberFormat")); - else - selectedFormat = new DecimalFormat(Messages.getString("NumericProperty.NumberFormat")); - - if (p.getError() != null) - return selectedFormat.format(adjustedValue) + PLUS_MINUS - + selectedFormat - .format(convertDimension ? (double) p.getError() * p.getDimensionFactor().doubleValue() - : (double) p.getError()); - else - return selectedFormat.format(adjustedValue); - - } - - public static boolean isDiscrete(NumericPropertyKeyword key) { - return def(key).isDiscrete(); - } - -} \ No newline at end of file + /** + * The list of default properties read that is created by reading the + * default {@code .xml} file. + */ + private final static List DEFAULT = XMLConverter.readDefaultXML(); + + private NumericProperties() { + //empty constructor + } + + /** + * Checks whether the {@code val} that is going to be passed to the + * {@code property} (a) has the same type as the {@code property.getValue()} + * object; (b) is confined within the definition domain: + * {@code minimum <= value <= maximum}. + * + * @param property the {@code property} containing the definition domain + * @param val a numeric value, the conformity of which to the definition + * domain needs to be checked + * @return {@code true} if {@code minimum <= val <= maximum} and if both + * {@code val} and {@code value} are instances of the same {@code class}; + * {@code false} otherwise + */ + public static boolean isValueSensible(NumericProperty property, Number val) { + if (!property.getValue().getClass().equals(val.getClass())) { + return false; + } + + double v = val.doubleValue(); + + final double EPS = 1E-12; + + if (v > property.getMaximum().doubleValue() + EPS) { + return false; + } + + return v >= property.getMinimum().doubleValue() - EPS; + + } + + public static String printRangeAndNumber(NumericProperty p, Number value) { + StringBuilder msg = new StringBuilder(); + msg.append("Acceptable region for "); + msg.append("parameter : "); + msg.append(p.getValue().getClass().getSimpleName()); + msg.append(" [ " + p.getMinimum()); + msg.append(" : " + p.getMaximum() + " ]. "); + msg.append("Value received: " + value); + return msg.toString(); + } + + public static List defaultList() { + return DEFAULT; + } + + /** + * Searches for the default {@code NumericProperty} corresponding to + * {@code keyword} in the list of pre-defined properties loaded from the + * respective {@code .xml} file. + * + * @param keyword one of the constant {@code NumericPropertyKeyword}s + * @return a {@code NumericProperty} in the default list of properties + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty def(NumericPropertyKeyword keyword) { + return new NumericProperty(DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); + } + + /** + * Compares the numeric values of this {@code NumericProperty} and + * {@code arg0} + * + * @param a a {@code NumericProperty} + * @param b another {@code NumericProperty} + * @return {@code true} if the values are equals + */ + public static int compare(NumericProperty a, NumericProperty b) { + Double d1 = ((Number) a.getValue()).doubleValue(); + Double d2 = ((Number) b.getValue()).doubleValue(); + + final double eps = 1E-8 * (d1 + d2) / 2.0; + + return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); + } + + /** + * Searches for the default {@code NumericProperty} corresponding to + * {@code keyword} in the list of pre-defined properties loaded from the + * respective {@code .xml} file, and if found creates a new + * {@NumericProperty} which will replicate all field of the latter, but will + * set its value to {@code value}. + * + * @param keyword one of the constant {@code NumericPropertyKeyword}s + * @param value the new value for the created {@code NumericProperty} + * @return a new {@code NumericProperty} that is built according to the + * default pattern specified by the {@code keyword}, but with a different + * {@code value} + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty derive(NumericPropertyKeyword keyword, Number value) { + return new NumericProperty(value, DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); + } + + public static boolean isDiscrete(NumericPropertyKeyword key) { + return def(key).isDiscrete(); + } + +} diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 30f4c04a..10aee7e8 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -1,10 +1,12 @@ package pulse.properties; +import java.text.ParseException; +import java.util.logging.Level; +import java.util.logging.Logger; +import pulse.math.Segment; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.formattedValueAndError; import static pulse.properties.NumericProperties.isValueSensible; -import static pulse.properties.NumericProperties.numberFormat; import static pulse.properties.NumericProperties.printRangeAndNumber; /** @@ -40,6 +42,7 @@ public class NumericProperty implements Property, Comparable { private boolean autoAdjustable; private boolean discrete; private boolean defaultSearchVariable; + private boolean optimisable; /** * Creates a {@code NumericProperty} based on { @@ -94,6 +97,7 @@ public NumericProperty(NumericProperty num) { this.autoAdjustable = num.autoAdjustable; this.error = num.error; this.defaultSearchVariable = num.defaultSearchVariable; + this.optimisable = num.optimisable; this.excludes = num.excludes; } @@ -176,33 +180,16 @@ public Number getMaximum() { */ @Override public String toString() { - return (type + " = " + formattedValueAndError(this, false)); - } - - /** - * Calls {@code formattedValue(true)}. - * - * @see NumericProperties.formattedValueAndError(boolean) - */ - @Override - public String formattedOutput() { - return formattedValueAndError(this, true); - } - - public String valueOutput() { - return numberFormat(this, true).format(valueInCurrentUnits()); - } - - public String errorOutput() { - return numberFormat(this, true).format(errorInCurrentUnits()); + return type + " = " + formattedOutput(); } public Number valueInCurrentUnits() { - return value instanceof Double ? (double) value * dimensionFactor.doubleValue() : (int) value; + return value instanceof Double ? (double) value * dimensionFactor.doubleValue() + + getDimensionDelta().doubleValue() : (int) value; } - public double errorInCurrentUnits() { - return error == null ? 0.0 : (double) error * dimensionFactor.doubleValue(); + public Number errorInCurrentUnits() { + return error == null ? 0.0 : error.doubleValue() * dimensionFactor.doubleValue(); } public Number getDimensionFactor() { @@ -315,8 +302,45 @@ public boolean isDefaultSearchVariable() { return defaultSearchVariable; } + public boolean isOptimisable() { + return optimisable; + } + public void setDefaultSearchVariable(boolean defaultSearchVariable) { this.defaultSearchVariable = defaultSearchVariable; } -} + public void setOptimisable(boolean optimisable) { + this.optimisable = optimisable; + } + + public Number getDimensionDelta() { + if(type == NumericPropertyKeyword.TEST_TEMPERATURE) + return -273.15; + else + return 0.0; + } + + /** + * Represents the bounds specified for this numeric property + * as a {@code Segment} object. The bound numbers are taken by + * their double values and assigned to the segment. + * @return the bounds in which this property is allowed to change + */ + + public Segment getBounds() { + return new Segment(minimum.doubleValue(), maximum.doubleValue()); + } + + /** + * Uses a {@code NumericPropertyFormatter} to generate a formatted output + * @return a formatted string output with the value (and error -- if available) + * of this numeric property + */ + + public String formattedOutput() { + var num = new NumericPropertyFormatter(this, true, true); + return num.numberFormat(this).format(value); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java new file mode 100644 index 00000000..1f98dc4c --- /dev/null +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -0,0 +1,214 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.properties; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import javax.swing.JFormattedTextField.AbstractFormatter; +import javax.swing.text.NumberFormatter; +import pulse.math.Segment; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class NumericPropertyFormatter extends AbstractFormatter { + + private NumericPropertyKeyword key; + private Segment bounds; + private boolean convertDimension = true; + private boolean addHtmlTags = true; + + private final static String PLUS_MINUS = Messages.getString("NumericProperty.PlusMinus"); + + /** + * Start using scientific notations for number whose absolute values are + * larger than {@value UPPER_LIMIT}. + */ + public final static double UPPER_LIMIT = 1e4; + + /** + * Start using scientific notations for number whose absolute values are + * lower than {@value LOWER_LIMIT}. + */ + public final static double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting + + private final static double ZERO = 1e-30; + + /** + * @param convertDimension if {@code true}, the output will be the + * {@code value * dimensionFactor} + */ + public NumericPropertyFormatter(NumericProperty p, boolean convertDimension, boolean addHtmlTags) { + this.key = p.getType(); + this.convertDimension = convertDimension; + this.bounds = p.getBounds(); + this.addHtmlTags = addHtmlTags; + } + + public NumberFormat numberFormat(NumericProperty p) { + Number value = (Number) p.getValue(); + NumberFormat f; + + if (value instanceof Integer) { + f = NumberFormat.getIntegerInstance(); + } else { + + double adjustedValue = convertDimension ? value.doubleValue() * p.getDimensionFactor().doubleValue() + : (double) value; + double absAdjustedValue = Math.abs(adjustedValue); + + if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) { + //format with scientific notations + f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); + } else { + //format "standard" numbers + f = new DecimalFormatImpl(p); + } + + } + + return f; + + } + + @Override + public Object stringToValue(String arg0) throws ParseException { + var nf = new NumberFormatter(); + Number n = (Number) nf.stringToValue(arg0); + this.setEditValid( + bounds.contains(n.doubleValue())); + return NumericProperties.derive(key, n); + } + + /** + * Used to print out a nice {@code value} for GUI applications and for + * exporting. + *

+ * Will use a {@code DecimalFormat} to reduce the number of digits, if + * necessary. Automatically detects whether it is dealing with {@code int} + * or {@code double} values, and adjust formatting accordingly. If + * {@code error != null}, will use the latter as the error value, which is + * separated from the main value by a plus-minus sign. + *

+ * + * @return a nice {@code String} representing the {@code value} of this + * {@code NumericProperty} and its {@code error} + */ + @Override + public String valueToString(Object o) throws ParseException { + if (o == null) { + return ""; + } + + if (!(o instanceof NumericProperty)) { + throw new IllegalArgumentException("Cannot format. Not a property: " + + o.getClass()); + } + + var p = (NumericProperty) o; + String result; + + if (Double.isInfinite( + ((Number) p.getValue()).doubleValue())) { + result = "∞"; + } else if (Double.isNaN( + ((Number) p.getValue()).doubleValue())) { + result = "unknown"; + } else { + + if (p.getError() != null) { + result = formatValueAndError(p); + } else { + result = formatValueOnly(p); + } + + } + + return addHtmlTags ? encloseInHtmlTags(result) : result; + + } + + private String encloseInHtmlTags(String s) { + return new StringBuffer("").append(s).append("").toString(); + } + + private String formatValueOnly(NumericProperty p) { + return numberFormat(p).format(p.getValue()); + } + + private String formatValueAndError(NumericProperty p) { + Number adjustedValue = ((Number) p.getValue()); + var selectedFormat = numberFormat(p); + String value = selectedFormat.format(adjustedValue); + String errorString = selectedFormat.format( + adjustedValue instanceof Double + ? (p.getError().doubleValue() - p.getDimensionDelta().doubleValue()) + : p.getError().intValue() - p.getDimensionDelta().intValue()); + return selectedFormat.format(adjustedValue) + PLUS_MINUS + errorString; + } + + public boolean isDimensionConverted() { + return convertDimension; + } + + public boolean areHtmlTagsAdded() { + return addHtmlTags; + } + + public Segment getBounds() { + return bounds; + } + + private class DecimalFormatImpl extends DecimalFormat { + + private final long dimensionDelta; + + public DecimalFormatImpl(NumericProperty p) { + super(); + dimensionDelta = p.getDimensionDelta().longValue(); + final int digits = p.getType() == NumericPropertyKeyword.TEST_TEMPERATURE ? 1 : 4; + setMinimumFractionDigits(digits); + setMaximumFractionDigits(digits); + + if (convertDimension) { + setMultiplier(p.getDimensionFactor().intValue()); + } + } + + @Override + public StringBuffer format(long arg0, StringBuffer arg1, FieldPosition arg2) { + return super.format( + //add delta (e.g. -273.15) + arg0 + dimensionDelta, + arg1, arg2); + } + + @Override + public StringBuffer format(double arg0, StringBuffer arg1, FieldPosition arg2) { + return super.format( + //add delta (e.g. -237.15) + arg0 + dimensionDelta, + arg1, arg2); + } + + //parse not needed for temperature since this is not changed + } + +} diff --git a/src/main/java/pulse/properties/ScientificFormat.java b/src/main/java/pulse/properties/ScientificFormat.java new file mode 100644 index 00000000..cd9bacaf --- /dev/null +++ b/src/main/java/pulse/properties/ScientificFormat.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.properties; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Artem Lunev + */ +public class ScientificFormat extends NumberFormat { + + private final int dimensionFactor; + private final double dimensionDelta; + + public ScientificFormat(Number dimensionFactor, Number dimensionDelta) { + super(); + this.dimensionFactor = dimensionFactor.intValue(); + this.dimensionDelta = dimensionDelta.doubleValue(); + } + + private static int getExponentForNumber(double number) { + var nf = new DecimalFormat("0.000E000"); + String numberAsString = nf.format(number); + try { + var substring = numberAsString.substring(numberAsString.indexOf('E') + 1, numberAsString.length()); + return NumberFormat.getIntegerInstance().parse(substring).intValue(); + } catch (ParseException ex) { + //no "E" found + return 0; + } + } + + @Override + public StringBuffer format(double arg0, StringBuffer arg1, FieldPosition arg2) { + double adjusted = arg0 * dimensionFactor + dimensionDelta; + + int exponent = getExponentForNumber(adjusted); + double mantissa = adjusted / Math.pow(10, exponent); + + return format(mantissa, exponent, true); + } + + private static StringBuffer format(Number a, Number b, boolean decimal) { + StringBuffer sb = new StringBuffer(); + var nf = new DecimalFormat(); + nf.setMaximumFractionDigits(2); + nf.setMinimumFractionDigits(decimal ? 2 : 0); + + sb.append(nf.format(a)) + .append(" × 10") + .append(b) + .append(""); + + return sb; + } + + @Override + public StringBuffer format(long arg0, StringBuffer arg1, FieldPosition arg2) { + long adjusted = arg0 * dimensionFactor + Double.doubleToLongBits(dimensionDelta); + + int exponent = Math.getExponent(adjusted); + long mantissa = Double.doubleToLongBits(adjusted / Math.pow(2, exponent)); + + return format(mantissa, exponent, false); + } + + @Override + public Number parse(String arg0, ParsePosition arg1) { + var tokenizer = new StringTokenizer(arg0); + Number a = null; + Number b = null; + try { + a = NumberFormat.getInstance().parse(tokenizer.nextToken(" ")); + tokenizer.nextToken(); //ignore × + b = NumberFormat.getInstance().parse(tokenizer.nextToken(" ")); + } catch (ParseException ex) { + Logger.getLogger(ScientificFormat.class.getName()).log(Level.SEVERE, null, ex); + } + + Number result; + + if (a instanceof Double) { + result = a.doubleValue() * Math.pow(10, b.doubleValue()); + } else { + result = a.longValue() * Math.pow(10, b.intValue()); + } + + return result; + } + +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 61004b9f..5edc8275 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -1,106 +1,97 @@ package pulse.search.direction; -import static pulse.properties.Flag.allProblemDependentFlags; -import static pulse.properties.Flag.allProblemIndependentFlags; -import static pulse.properties.Flag.selectActive; - import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; - import pulse.problem.statements.Problem; import pulse.properties.Flag; -import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; +import pulse.util.PropertyHolder; public class ActiveFlags { - private static List problemIndependentFlags = allProblemIndependentFlags(); - private static List problemDependentFlags = allProblemDependentFlags(); + private static List flags; + + static { + reset(); + } private ActiveFlags() { //empty constructor } public static void reset() { - problemDependentFlags = allProblemDependentFlags(); - problemIndependentFlags = allProblemIndependentFlags(); + flags = Flag.allFlags(); } public static List getAllFlags() { - var newList = new ArrayList(); - newList.addAll(problemDependentFlags); - newList.addAll(problemIndependentFlags); - return newList; + return flags; } - public static void listAvailableProperties(List list) { - list.addAll(problemIndependentFlags); + public static Set availableProperties() { + var set = new HashSet(); var t = TaskManager.getManagerInstance().getSelectedTask(); - if (t != null) { - var p = t.getCurrentCalculation().getProblem(); - - if (p != null) { + if (t == null) { + return set; + } - var params = p.listedTypes().stream().filter(pp - -> pp instanceof NumericProperty) - .map(pMap -> ((NumericProperty) pMap) - .getType()).collect( - Collectors.toList()); + var p = t.getCurrentCalculation().getProblem(); - NumericPropertyKeyword key; + if (p != null) { - for (Flag property : problemDependentFlags) { - key = property.getType(); - if (params.contains(key)) { - list.add(property); - } + var fullList = p.listedKeywords(); + fullList.addAll(t.getExperimentalCurve().listedKeywords()); + NumericPropertyKeyword key; + for (Flag property : flags) { + key = property.getType(); + if (fullList.contains(key)) { + set.add(property); } } - } else { - for (Flag property : problemDependentFlags) { - list.add(property); - } + } + + return set; } /** * Finds what properties are being altered in the search * + * @param t task for which the active parameters should be listed * @return a {@code List} of property types represented by * {@code NumericPropertyKeyword}s */ public static List activeParameters(SearchTask t) { - Problem p = t.getCurrentCalculation().getProblem(); - - var list = new ArrayList(); - list.addAll(selectActiveAndListed(problemDependentFlags, p)); - list.addAll(selectActiveTypes(problemIndependentFlags)); - return list; + var c = t.getCurrentCalculation(); + //problem dependent + var allActiveParams = selectActiveAndListed(flags, c.getProblem()); + //problem independent (lower/upper bound) + var listed = selectActiveAndListed(flags, t.getExperimentalCurve() ); + allActiveParams.addAll( selectActiveAndListed(flags, t.getExperimentalCurve() ) ); + return allActiveParams; } - public static List selectActiveAndListed(List flags, Problem listed) { - return selectActiveTypes(flags).stream().filter(type -> listed.isListedNumericType(type)) + public static List selectActiveAndListed(List flags, PropertyHolder listed) { + //return empty list + if(listed == null) { + return new ArrayList(); + } + + return selectActiveTypes(flags).stream() + .filter(type -> listed.isListedNumericType(type)) .collect(Collectors.toList()); } - + public static List selectActiveTypes(List flags) { - return selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); - } - - public static List getProblemIndependentFlags() { - return problemIndependentFlags; - } - - public static List getProblemDependentFlags() { - return problemDependentFlags; + return Flag.selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); } } From c157f58dff38d37f80b4523b6e298ab8105e758c Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 13:13:54 +0100 Subject: [PATCH 078/116] Optimiser update Adds a MAX_FAILED_ATTEMPTS field, which represents acceptable number of retries when the optimiser refuses to accept the uphill step and tries to re-configure itself to arrive at a downhill step. Previously this was only possible for the LMOptimiser, however, in the new logic, even the Hessian-based optimisers can now do this -- the re-configuration is achieved by reseting the Hessian matrix to identity. In future releases, this can probably be merged with LMOptimiser to reduce duplications. The GradientBasedOptimiser now hosts a dx() method for calculating the gradient step, callable by all subclasses. Previously, the same code was inserted in different optimisers. In addition, it failed miserably for properties which could reach 0.0, since, in that case, the gradient step was calculated as a factor of the property and the GRADIENT_RESOLUTION (either low- or high, depending on whether the property is discrete). This caveat has been fixed by replacing the 0.0 value with the maximal property value. The IterativeState, a superclass for all paths, now stores the current value of the cost function and the set of parameters corresponding to that cost function value. This is needed in order to be able to revert back to a 'safe' set of parameters. The maximum number of failed attempts for the LMOptimiser was reduced to 4. At each successful attempt, the current state is stored in the path. listedTypes -> listedKeywords All flags were removed from being listed in the PathOptimiser. The override for the data() method was removed. --- .../direction/CompositePathOptimiser.java | 212 ++++++------ .../direction/GradientBasedOptimiser.java | 306 +++++++++--------- .../search/direction/IterativeState.java | 84 +++-- .../pulse/search/direction/LMOptimiser.java | 32 +- .../pulse/search/direction/PathOptimiser.java | 29 +- 5 files changed, 362 insertions(+), 301 deletions(-) diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 81d0b866..99a243f9 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -5,6 +5,7 @@ import java.util.List; import pulse.math.ParameterVector; +import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Property; import pulse.search.linear.LinearOptimiser; @@ -15,95 +16,124 @@ public abstract class CompositePathOptimiser extends GradientBasedOptimiser { - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Linear Optimiser Selector", LinearOptimiser.class); - - private LinearOptimiser linearSolver; - - public CompositePathOptimiser() { - instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); - linearSolver = instanceDescriptor.newInstance(LinearOptimiser.class); - linearSolver.setParent(this); - instanceDescriptor.addListener(() -> initLinearOptimiser()); - } - - private void initLinearOptimiser() { - setLinearSolver(instanceDescriptor.newInstance(LinearOptimiser.class)); - } - - public boolean iteration(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getIterativeState(); // the previous path of the task - - /* + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Linear Optimiser Selector", LinearOptimiser.class); + + private LinearOptimiser linearSolver; + + /** + * Maximum number of consequent failed iterations that can be rejected. + * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. + */ + + public final static int MAX_FAILED_ATTEMPTS = 2; + + /** + * For numerical comparison. + */ + public final static double EPS = 1e-10; + + public CompositePathOptimiser() { + instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); + linearSolver = instanceDescriptor.newInstance(LinearOptimiser.class); + linearSolver.setParent(this); + instanceDescriptor.addListener(() -> initLinearOptimiser()); + } + + private void initLinearOptimiser() { + setLinearSolver(instanceDescriptor.newInstance(LinearOptimiser.class)); + } + + public boolean iteration(SearchTask task) throws SolverException { + var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task + + boolean accept = true; + + /* * Checks whether an iteration limit has been already reached - */ - - if (compare(p.getIteration(), getMaxIterations()) > 0) { - - task.setStatus(Status.TIMEOUT); - - } else { - - var parameters = task.searchVector(); // current parameters - var dir = getSolver().direction(p); // find p[k] - - double step = linearSolver.linearStep(task); // find magnitude of step - p.setLinearStep(step); - - var candidateParams = parameters.sum(dir.multiply(step)); // new set of parameters determined through search - task.assign(new ParameterVector(parameters, candidateParams)); // assign to this task - - prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] - p.incrementStep(); // increment the counter of successful steps - - task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals - - } - - return true; - - } - - public LinearOptimiser getLinearSolver() { - return linearSolver; - } - - /** - * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this - * object as its parent. - * - * @param linearSearch a {@code LinearSolver} - */ - - public void setLinearSolver(LinearOptimiser linearSearch) { - this.linearSolver = linearSearch; - linearSolver.setParent(this); - super.parameterListChanged(); - } - - public InstanceDescriptor getLinearOptimiserDescriptor() { - return instanceDescriptor; - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(instanceDescriptor); - return list; - } - - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ - - @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); - return new ComplexPath(t); - } - -} \ No newline at end of file + */ + if (compare(p.getIteration(), getMaxIterations()) > 0) { + + task.setStatus(Status.TIMEOUT); + + } else { + + double initialCost = task.solveProblemAndCalculateCost(); + var parameters = task.searchVector(); + + p.setParameters(parameters); // store current parameters + + var dir = getSolver().direction(p); // find p[k] + double step = linearSolver.linearStep(task); // find magnitude of step + p.setLinearStep(step); + + // new set of parameters determined through search + var candidateParams = parameters.sum(dir.multiply(step)); + + task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + + if (newCost > initialCost - EPS + && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS + && p instanceof ComplexPath) { + var complexPath = (ComplexPath)p; + task.assign(parameters); // roll back if cost increased + // attempt to reset -> in case of Hessian-based methods, + // this will change the Hessian) { + complexPath.setHessian( createIdentityMatrix(parameters.dimension()) ); + p.incrementFailedAttempts(); + accept = false; + } else { + task.storeState(); + p.resetFailedAttempts(); + this.prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] + p.incrementStep(); // increment the counter of successful steps + } + + } + + return accept; + + } + + public LinearOptimiser getLinearSolver() { + return linearSolver; + } + + /** + * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this + * object as its parent. + * + * @param linearSearch a {@code LinearSolver} + */ + public void setLinearSolver(LinearOptimiser linearSearch) { + this.linearSolver = linearSearch; + linearSolver.setParent(this); + super.parameterListChanged(); + } + + public InstanceDescriptor getLinearOptimiserDescriptor() { + return instanceDescriptor; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + /** + * Creates a new {@code Path} instance for storing the gradient, direction, + * and minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + @Override + public GradientGuidedPath initState(SearchTask t) { + this.configure(t); + return new ComplexPath(t); + } + +} diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index 16e8aa94..a3db7fc6 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -6,7 +6,7 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; -import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.linear.Vector; @@ -14,154 +14,164 @@ import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.tasks.SearchTask; public abstract class GradientBasedOptimiser extends PathOptimiser { - private double gradientResolution; - private double gradientStep; - - /** - * Abstract constructor that sets up the default - * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} for - * this {@code PathSolver}. In addition, sets up a list of search flags defined - * by the {@code Flag.defaultList} method. - * - * @see pulse.properties.Flag.defaultList() - */ - - protected GradientBasedOptimiser() { - super(); - } - - /** - * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and - * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In addition, - * sets up a list of search flags defined by the {@code Flag.defaultList} - * method. - * - * @see pulse.properties.Flag.defaultList() - */ - - public void reset() { - super.reset(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); - } - - /** - * Calculates the {@code Vector} gradient of the target function (the sum of - * squared residuals, SSR, for this {@code task}. - *

- * If Δf(Δxi) is the change in the - * target function associated with the change of the parameter - * xi, the i-th component of the gradient - * is equal to gi = - * (Δf(Δxi)/Δxi). The - * accuracy of this calculation depends on the - * Δxi value, which is roughly the - * {@code GRADIENT_RESOLUTION}. Note however that instead of using a - * forward-difference scheme to calculate the gradient, this method utilises the - * central-difference calculation of the gradient, which significantly increases - * the overall accuracy of calculation. This means that to evaluate each - * component of this vector, the {@code Problem} associated with this - * {@code task} is solved twice (for xi ± - * Δxi). - *

- * - * @param task a {@code SearchTask} that is being driven to the minimum of SSR - * @return the gradient of the target function - * @throws SolverException - */ - - public Vector gradient(SearchTask task) throws SolverException { - - final var params = task.searchVector(); - var grad = new Vector(params.dimension()); - - - final double resolutionHigh = (double) getGradientResolution().getValue(); - final double resolutionLow = 5E-2; //TODO - - for (int i = 0; i < params.dimension(); i++) { - boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); - double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); - - final var shift = new Vector(params.dimension()); - shift.set(i, 0.5 * dx); - - task.assign(new ParameterVector( params, params.sum(shift) )); - final double ss2 = task.solveProblemAndCalculateCost(); - - task.assign(new ParameterVector( params, params.subtract(shift) )); - final double ss1 = task.solveProblemAndCalculateCost(); - - grad.set(i, (ss2 - ss1) / dx); - - } - - task.assign(params); - - return grad; - - } - - /** - * Checks whether a discrete property is being optimised and selects the gradient step - * best suited to the optimisation strategy. Should be called before creating the optimisation path. - * @param task the search task defining the search vector - */ - - public void configure(SearchTask task) { - var params = task.searchVector(); - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); - } - - public void setGradientResolution(NumericProperty resolution) { - requireType(resolution, GRADIENT_RESOLUTION); - this.gradientResolution = (double) resolution.getValue(); - firePropertyChanged(this, resolution); - } - - public NumericProperty getGradientResolution() { - return derive(GRADIENT_RESOLUTION, gradientResolution); - } - - /** - *

- * The types of the listed parameters for this class include: - * GRADIENT_RESOLUTION, - * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class - * are treated as separate listed parameters. - *

- * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(GRADIENT_RESOLUTION)); - return list; - } - - /** - * The accepted types are: - * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if(type == GRADIENT_RESOLUTION) { - setGradientResolution(property); - } - } - - public double getGradientStep() { - return gradientStep; - } - -} \ No newline at end of file + private double gradientResolution; + private double gradientStep; + + private final static double resolutionHigh = (double)def(GRADIENT_RESOLUTION).getValue(); + private final static double resolutionLow = 5E-2; //TODO + + /** + * Abstract constructor that sets up the default + * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} + * for this {@code PathSolver}. In addition, sets up a list of search flags + * defined by the {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + protected GradientBasedOptimiser() { + super(); + } + + /** + * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and + * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In + * addition, sets up a list of search flags defined by the + * {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + public void reset() { + super.reset(); + gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); + } + + /** + * Calculates the {@code Vector} gradient of the target function (the sum of + * squared residuals, SSR, for this {@code task}. + *

+ * If Δf(Δxi) is the change in + * the target function associated with the change of the parameter + * xi, the i-th component of the + * gradient is equal to gi = + * (Δf(Δxi)/Δxi). The + * accuracy of this calculation depends on the + * Δxi value, which is roughly the + * {@code GRADIENT_RESOLUTION}. Note however that instead of using a + * forward-difference scheme to calculate the gradient, this method utilises + * the central-difference calculation of the gradient, which significantly + * increases the overall accuracy of calculation. This means that to + * evaluate each component of this vector, the {@code Problem} associated + * with this {@code task} is solved twice (for xi ± + * Δxi). + *

+ * + * @param task a {@code SearchTask} that is being driven to the minimum of + * SSR + * @return the gradient of the target function + * @throws SolverException + */ + public Vector gradient(SearchTask task) throws SolverException { + + final var params = task.searchVector(); + var grad = new Vector(params.dimension()); + + for (int i = 0; i < params.dimension(); i++) { + NumericProperty defProp = NumericProperties.def(params.getIndex(i)); + double dx = dx(defProp, params.get(i)); + + final var shift = new Vector(params.dimension()); + shift.set(i, 0.5 * dx); + + task.assign(new ParameterVector(params, params.sum(shift))); + final double ss2 = task.solveProblemAndCalculateCost(); + + task.assign(new ParameterVector(params, params.subtract(shift))); + final double ss1 = task.solveProblemAndCalculateCost(); + + grad.set(i, (ss2 - ss1) / dx); + + } + + task.assign(params); + + return grad; + + } + + /** + * Calculates the gradient step. Ensures dx is not zero even if the parameter values is. + * Applicable to discrete properties. + * @param defProp the default property + * @param value the value of the parameter under the optimisation vector + * @return the gradient step + */ + + protected double dx(NumericProperty defProp, double value) { + boolean discrete = defProp.isDiscrete(); + return (discrete ? resolutionLow : resolutionHigh) + * (Math.abs(value) < 1E-20 + ? defProp.getMaximum().doubleValue() + : value); + } + + /** + * Checks whether a discrete property is being optimised and selects the + * gradient step best suited to the optimisation strategy. Should be called + * before creating the optimisation path. + * + * @param task the search task defining the search vector + */ + public void configure(SearchTask task) { + var params = task.searchVector(); + boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); + final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); + gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + } + + public void setGradientResolution(NumericProperty resolution) { + requireType(resolution, GRADIENT_RESOLUTION); + this.gradientResolution = (double) resolution.getValue(); + firePropertyChanged(this, resolution); + } + + public NumericProperty getGradientResolution() { + return derive(GRADIENT_RESOLUTION, gradientResolution); + } + + /** + *

+ * The types of the listed parameters for this class include: GRADIENT_RESOLUTION, + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this + * class are treated as separate listed parameters. + *

+ * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRADIENT_RESOLUTION); + return set; + } + + /** + * The accepted types are: + * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == GRADIENT_RESOLUTION) { + setGradientResolution(property); + } + } + + public double getGradientStep() { + return gradientStep; + } + +} diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 34e1cc14..1db8ba2e 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import pulse.math.ParameterVector; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.ITERATION; @@ -7,31 +8,62 @@ public class IterativeState { - private int iteration; - private int failedAttempts; - - public void reset() { - iteration = 0; - } - - public NumericProperty getIteration() { - return derive(ITERATION, iteration); - } - - public void incrementStep() { - iteration++; - } - - public int getFailedAttempts() { - return failedAttempts; - } - - public void resetFailedAttempts() { - failedAttempts = 0; - } - - public void incrementFailedAttempts() { - failedAttempts++; - } + private ParameterVector parameters; + private double cost = Double.POSITIVE_INFINITY; + private int iteration; + private int failedAttempts; + + /** + * Stores the parameter vector and cost function value associated with the specified state. + * @param other another state of the optimiser + */ + + public IterativeState(IterativeState other) { + this.parameters = new ParameterVector(other.parameters); + this.cost = other.cost; + } + + //default constructor + public IterativeState() {} + + public double getCost() { + return cost; + } + + public void setCost(double cost) { + this.cost = cost; + } + + public void reset() { + iteration = 0; + } + + public NumericProperty getIteration() { + return derive(ITERATION, iteration); + } + + public void incrementStep() { + iteration++; + } + + public int getFailedAttempts() { + return failedAttempts; + } + + public void resetFailedAttempts() { + failedAttempts = 0; + } + + public void incrementFailedAttempts() { + failedAttempts++; + } + + public ParameterVector getParameters() { + return parameters; + } + + public void setParameters(ParameterVector parameters) { + this.parameters = parameters; + } } \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index d488b876..72c8b83c 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -7,7 +7,7 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.DAMPING_RATIO; -import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.math.linear.Matrices; @@ -18,7 +18,7 @@ import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import static pulse.search.direction.CompositePathOptimiser.EPS; import pulse.search.statistics.OptimiserStatistic; import pulse.search.statistics.ResidualStatistic; import pulse.search.statistics.SumOfSquares; @@ -35,14 +35,13 @@ public class LMOptimiser extends GradientBasedOptimiser { private static LMOptimiser instance = new LMOptimiser(); - - private final static double EPS = 1e-10; // for numerical comparison private double dampingRatio; - + /** - * Maximum number of consequent failed iterations that can be rejected. + * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 10; + + public final static int MAX_FAILED_ATTEMPTS = 4; private LMOptimiser() { super(); @@ -91,6 +90,7 @@ public boolean iteration(SearchTask task) throws SolverException { p.incrementFailedAttempts(); accept = false; } else { + task.storeState(); p.resetFailedAttempts(); p.setLambda(p.getLambda() / 3.0); p.setComputeJacobian(false); @@ -168,13 +168,9 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { var jacobian = new double[numPoints][numParams]; - final double resolutionHigh = super.getGradientStep(); - final double resolutionLow = 1E-2; //TODO - for (int i = 0; i < numParams; i++) { - boolean discrete = NumericProperties.def(params.getIndex(i)).isDiscrete(); - double dx = (discrete ? resolutionLow : resolutionHigh) * params.get(i); + double dx = dx( NumericProperties.def(params.getIndex(i)), params.get(i)); final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); @@ -240,12 +236,12 @@ private SquareMatrix levenbergDamping(SquareMatrix hessian) { private SquareMatrix marquardtDamping(SquareMatrix hessian) { return hessian.blockDiagonal(); } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(DAMPING_RATIO)); - return list; + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DAMPING_RATIO); + return set; } /** diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index c5039b3d..d033dc81 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -8,12 +8,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import pulse.properties.Property; import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.SearchTask; @@ -136,8 +138,7 @@ public String toString() { @Override public List genericProperties() { var original = super.genericProperties(); - original.addAll(ActiveFlags.getProblemDependentFlags()); - original.addAll(ActiveFlags.getProblemIndependentFlags()); + original.addAll(ActiveFlags.getAllFlags()); return original; } @@ -151,20 +152,11 @@ public List genericProperties() { * @see pulse.properties.NumericPropertyKeyword */ @Override - public List listedTypes() { - List list = new ArrayList(); - list.add(def(ERROR_TOLERANCE)); - list.add(def(ITERATION_LIMIT)); - - ActiveFlags.listAvailableProperties(list); - - return list; - } - - @Override - public List data() { - var list = listedTypes(); - return super.data().stream().filter(p -> list.contains(p)).collect(Collectors.toList()); + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(ERROR_TOLERANCE); + set.add(ITERATION_LIMIT); + return set; } /** @@ -226,9 +218,10 @@ protected void setSolver(DirectionSolver solver) { /** * Checks if this optimiser is compatible with the statistic passed to the - * method as its argument. By default, this will accept any - * {@code OptimiserStatistic}. + * method as its argument.By default, this will accept any + {@code OptimiserStatistic} * + * @param os a selected optimiser metric * @return {@code true}, if not specified otherwise by its subclass * implementation. */ From 43bf52c5a0c0804146cf469e485ba856b73e1091 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 13:36:25 +0100 Subject: [PATCH 079/116] Updated Calculation and SearchTask Modified Calculation.setStatus() to return false in cases when a change of status is not appropriate (for example, when the Calculation is being forced to run, although it is not ready). Contrary to the previous implementation, the method does NOT return false when the status being set is the same as the one already assigned. This functionality has been transferred to SearchTask.setStatus() instead; only there this is needed to avoid triggering the listeners without actually updating anything. In SearchTask, a field has been added to store the global-best iterative state. At the end of execution, it is checked whether the converged state is actually worse than the global-best -- if so, the global-best set of parameters are taken instead, and the task is re-calculated. --- src/main/java/pulse/tasks/Calculation.java | 32 +- src/main/java/pulse/tasks/SearchTask.java | 1105 ++++++++++---------- 2 files changed, 601 insertions(+), 536 deletions(-) diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index b893e377..797b9f8c 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -88,14 +88,14 @@ public void clear() { * After setting and adopting the {@code problem} by this * {@code SearchTask}, this will attempt to change the parameters of that * {@code problem} in accordance with the loaded {@code ExperimentalData} - * for this {@code SearchTask} (if not null). Later, if any changes to the + * for this {@code SearchTask} (if not null).Later, if any changes to the * properties of that {@code Problem} occur and if the source of that event * is either the {@code Metadata} or the {@code PropertyHolderTable}, they * will be accounted for by altering the parameters of the {@code problem} * accordingly -- immediately after the former take place. - *

* * @param problem a {@code Problem} + * @param curve */ public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; @@ -112,7 +112,7 @@ private void addProblemListeners(Problem problem, ExperimentalData curve) { if (source instanceof Metadata || source instanceof PropertyHolderTable) { var property = event.getProperty(); - if (property instanceof NumericProperty && ((NumericProperty) property).isVisibleByDefault()) { + if (property instanceof NumericProperty && ((NumericProperty) property).isOptimisable()) { return; } @@ -172,10 +172,32 @@ public Status getStatus() { return status; } + /** + * Attempts to set the status of this calculation to {@code status}. + * @param status a status + * @return {@code true} if this attempt is successful, including the case + * when the status being set is equal to the current status. {@code false} + * if the current status is one of the following: {@code DONE}, {@code EXECUTION_ERROR}, + * {@code INCOMPLETE}, {@code IN_PROGRES}, AND the {@code status} being set + * is {@code QUEUED}. + */ + public boolean setStatus(Status status) { - boolean done = this.status != status; + + switch(this.status) { + case DONE: + case EXECUTION_ERROR: + case INCOMPLETE: + case IN_PROGRESS: + //if the TaskManager attempts to run this calculation + if(status == Status.QUEUED) + return false; + default: + } + this.status = status; - return done; + return true; + } public NumericProperty weight(List all) { diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index f9e25d83..33dbfb55 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -3,7 +3,6 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; import static pulse.search.direction.ActiveFlags.activeParameters; -import static pulse.search.direction.ActiveFlags.getAllFlags; import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; import static pulse.tasks.logs.Details.INCOMPATIBLE_OPTIMISER; @@ -32,6 +31,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import pulse.input.ExperimentalData; @@ -40,6 +41,7 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.ActiveFlags; import pulse.search.direction.IterativeState; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; @@ -64,537 +66,578 @@ * heat conduction, which is done using the {@code PathSolver}. A * {@code SearchTask} has an associated {@code ExperimentalData} object linked * to it. - * + * * @see pulse.tasks.TaskManager */ - public class SearchTask extends Accessible implements Runnable { - private Calculation current; - private List stored; - private ExperimentalData curve; - - private IterativeState path; - private Buffer buffer; - private Log log; - - private CorrelationBuffer correlationBuffer; - private CorrelationTest correlationTest; - private NormalityTest normalityTest; - - private Identifier identifier; - - /** - * If {@code SearchTask} finishes, and its R2 value is lower - * than this constant, the result will be considered {@code AMBIGUOUS}. - */ - - private List listeners = new CopyOnWriteArrayList<>(); - private List statusChangeListeners = new CopyOnWriteArrayList<>(); - - /** - *

- * Creates a new {@code SearchTask} from {@code curve}. Generates a new - * {@code Identifier}, sets the parent of {@code curve} to {@code this}, and - * invokes clear(). If any changes to the {@code ExperimentalData} occur, a - * listener will ensure the {@code DifferenceScheme} is modified accordingly. - *

- * - * @param curve the {@code ExperimentalData} - */ - - public SearchTask(ExperimentalData curve) { - current = new Calculation(); - current.setParent(this); - this.identifier = new Identifier(); - this.curve = curve; - curve.setParent(this); - correlationBuffer = new CorrelationBuffer(); - clear(); - addListeners(); - } - - private void addListeners() { - InterpolationDataset.addListener(e -> { - var p = current.getProblem().getProperties(); - if (p.areThermalPropertiesLoaded()) - p.useTheoreticalEstimates(curve); - }); - - /** - * Sets the difference scheme's time limit to the upper bound of the range of {@code ExperimentalData} - * multiplied by a safety margin {@value Calculation.RELATIVE_TIME_MARGIN}. - */ - - curve.addDataListener(dataEvent -> { - var scheme = current.getScheme(); - if (scheme != null) { - var hcurve = current.getProblem().getHeatingCurve(); - var startTime = (double) hcurve.getTimeShift().getValue(); - scheme.setTimeLimit( - derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); - } - }); - } - - /** - *

- * Resets everything to default values (for a list of default values please see - * the {@code .xml} document. Sets the status of this task to - * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { var scheme = - * current.getScheme(); if (scheme != null) { var curve = - * current.getProblem().getHeatingCurve(); var startTime = (double) - * curve.getTimeShift().getValue(); scheme.setTimeLimit(derive(TIME_LIMIT, - * RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); - *

- */ - - public void clear() { - stored = new ArrayList(); - curve.resetRanges(); - buffer = new Buffer(); - correlationBuffer.clear(); - buffer.setParent(this); - log = new Log(this); - - initCorrelationTest(); - initNormalityTest(); - - this.path = null; - current.clear(); - - this.checkProblems(true); - } - - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code SearchTask} and calculate the SSR value - * showing how well (or bad) the calculated solution describes the - * {@code ExperimentalData}. - * - * @return the value of SSR (sum of squared residuals). - * @throws SolverException - */ - - public double solveProblemAndCalculateCost() throws SolverException { - current.process(); - var rs = current.getOptimiserStatistic(); - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - - public List alteredParameters() { - return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); - } - - /** - * Generates a search vector (= optimisation vector) using the search flags set - * by the {@code PathSolver}. - * - * @return an {@code IndexedVector} with search parameters of this - * {@code SearchTaks} - * @see pulse.search.direction.PathSolver.getSearchFlags() - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - - public ParameterVector searchVector() { - var flags = getAllFlags(); - var keywords = activeParameters(this); - var optimisationVector = new ParameterVector(keywords); - - current.getProblem().optimisationVector(optimisationVector, flags); - curve.getRange().optimisationVector(optimisationVector, flags); - - return optimisationVector; - } - - /** - * Assigns the values of the parameters of this {@code SearchTask} to - * {@code searchParameters}. - * - * @param searchParameters an {@code IndexedVector} with relevant search - * parameters - * @see pulse.problem.statements.Problem.assign(IndexedVector) - */ - - public void assign(ParameterVector searchParameters) { - try { - current.getProblem().assign(searchParameters); - curve.getRange().assign(searchParameters); - } catch (SolverException e) { - var status = FAILED; - status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - e.printStackTrace(); - } - } - - /** - *

- * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, will - * do nothing. After making some preparatory steps, will initiate a loop with - * successive calls to {@code PathSolver.iteration(this)}, filling the buffer - * and notifying any data change listeners in parallel. This loop will go on - * until either converging results are obtained, or a timeout is reached, or if - * an execution error happens. Whether the run has been successful will be - * determined by comparing the associated R2 value with the - * {@code SUCCESS_CUTOFF}. - *

- */ - - @Override - public void run() { - - current.setResult(null); - - /* check of status */ - - switch (current.getStatus()) { - case READY: - case QUEUED: - setStatus(IN_PROGRESS); - break; - default: - return; - } - - /* preparatory steps */ - - current.getProblem().parameterListChanged(); // get updated list of parameters - - var optimiser = getInstance(); - - path = optimiser.initState(this); - - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); - int bufferSize = (Integer) getSize().getValue(); - buffer.init(); - correlationBuffer.clear(); - - /* search cycle */ - - /* sets an independent thread for manipulating the buffer */ - - List> bufferFutures = new ArrayList<>(bufferSize); - var singleThreadExecutor = Executors.newSingleThreadExecutor(); - - try { - solveProblemAndCalculateCost(); - } catch (SolverException e1) { - System.err.println("Failed on first calculation. Details:"); - e1.printStackTrace(); - } - - final int maxIterations = (int)getInstance().getMaxIterations().getValue(); - - outer: do { - - bufferFutures.clear(); - - for (var i = 0; i < bufferSize; i++) { - - if (current.getStatus() != IN_PROGRESS) - break outer; - - int iter = 0; - - try { - for (boolean finished = false; !finished && iter < maxIterations; iter++) { - finished = optimiser.iteration(this); - } - } catch (SolverException e) { - setStatus(FAILED); - System.err.println(this + " failed during execution. Details: "); - e.printStackTrace(); - break outer; - } - - if(iter >= maxIterations) { - var fail = FAILED; - fail.setDetails(MAX_ITERATIONS_REACHED); - setStatus(fail); - } - - final var j = i; - - bufferFutures.add(CompletableFuture.runAsync(() -> { - buffer.fill(this, j); - correlationBuffer.inflate(this); - notifyDataListeners(new DataLogEntry(this)); - }, singleThreadExecutor)); - - } - - bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance)); - - singleThreadExecutor.shutdown(); - - if (current.getStatus() == IN_PROGRESS) - runChecks(); - - } - - private void runChecks() { - - if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed - var status = FAILED; - status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); - setStatus(status); - } - - else { - - var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected - // correlations - notifyDataListeners(new CorrelationLogEntry(this)); - - if (test) { - var status = AMBIGUOUS; - status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); - setStatus(status); - } else { - // lastly, check if the parameter values estimated in this procedure are - // reasonable - - var properties = alteredParameters(); - - if (properties.stream().anyMatch(np -> !np.validate())) { - var status = FAILED; - status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - } else { - current.getModelSelectionCriterion().evaluate(this); - setStatus(DONE); - } - - } - - } - - } - - public void addTaskListener(DataCollectionListener toAdd) { - listeners.add(toAdd); - } - - public void addStatusChangeListener(StatusChangeListener toAdd) { - statusChangeListeners.add(toAdd); - } - - public void removeTaskListeners() { - listeners.clear(); - } - - public void removeStatusChangeListeners() { - statusChangeListeners.clear(); - } - - @Override - public String toString() { - return getIdentifier().toString(); - } - - public ExperimentalData getExperimentalCurve() { - return curve; - } - - public IterativeState getIterativeState() { - return path; - } - - /** - * Adopts the {@code curve} by this {@code SearchTask}. - * - * @param curve the {@code ExperimentalData}. - */ - - public void setExperimentalCurve(ExperimentalData curve) { - this.curve = curve; - - if (curve != null) - curve.setParent(this); - - } - - public void setStatus(Status status) { - Objects.requireNonNull(status); - boolean changed = current.setStatus(status); - if (changed) - notifyStatusListeners(new StateEntry(this, status)); - } - - /** - *

- * Checks if this {@code SearchTask} is ready to be run. Performs basic check to - * see whether the user has uploaded all necessary data. If not, will create a - * status update with information about the missing data. - *

- * - * @return {@code READY} if the task is ready to be run, {@code DONE} if has - * already been done previously, {@code INCOMPLETE} if some problems - * exist. For the latter, additional details will be available using the - * {@code status.getDetails()} method. - *

- */ - - public void checkProblems(boolean updateStatus) { - var status = current.getStatus(); - - if (status == DONE) - return; - - var pathSolver = getInstance(); - var s = INCOMPLETE; - - if (current.getProblem() == null) - s.setDetails(MISSING_PROBLEM_STATEMENT); - else if (!current.getProblem().isReady()) - s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); - else if (current.getScheme() == null) - s.setDetails(MISSING_DIFFERENCE_SCHEME); - else if (curve == null) - s.setDetails(MISSING_HEATING_CURVE); - else if (pathSolver == null) - s.setDetails(MISSING_OPTIMISER); - else if (buffer == null) - s.setDetails(MISSING_BUFFER); - else if (!getInstance().compatibleWith(current.getOptimiserStatistic()) ) - s.setDetails(INCOMPATIBLE_OPTIMISER); - else - s = READY; - - if (updateStatus) - setStatus(s); - } - - public Identifier getIdentifier() { - return identifier; - } - - public Log getLog() { - return log; - } - - private void notifyDataListeners(LogEntry e) { - for (var l : listeners) { - l.onDataCollected(e); - } - } - - private void notifyStatusListeners(StateEntry e) { - for (var l : statusChangeListeners) { - l.onStatusChange(e); - } - } - - @Override - public String describe() { - - var sb = new StringBuilder(); - sb.append(TaskManager.getManagerInstance().getSampleName()); - sb.append("_Task_"); - var extId = curve.getMetadata().getExternalID(); - if (extId < 0) - sb.append("IntID_" + identifier.getValue()); - else - sb.append("ExtID_" + extId); - - return sb.toString(); - - } - - /** - * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or - * {@code READY}, terminates it by setting its status to {@code TERMINATED}. - * This change of status will then force the {@code run()} loop to stop (if - * running). - */ - - public void terminate() { - switch (current.getStatus()) { - case IN_PROGRESS: - case QUEUED: - case READY: - setStatus(TERMINATED); - break; - default: - return; - } - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - /** - * A {@code SearchTask} is deemed equal to another one if it has the same - * {@code ExperimentalData}. - */ - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof SearchTask)) - return false; - - return curve.equals(((SearchTask) o).getExperimentalCurve()); - - } - - public NormalityTest getNormalityTest() { - return normalityTest; - } - - public void initNormalityTest() { - normalityTest = instantiate(NormalityTest.class, NormalityTest.getSelectedTestDescriptor()); - normalityTest.setParent(this); - } - - public void initCorrelationTest() { - correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); - correlationTest.setParent(this); - } - - public CorrelationBuffer getCorrelationBuffer() { - return correlationBuffer; - } - - public CorrelationTest getCorrelationTest() { - return correlationTest; - } - - public Calculation getCurrentCalculation() { - return current; - } - - public List getStoredCalculations() { - return this.stored; - } - - public void switchTo(Calculation calc) { - current.setParent(null); - current = calc; - current.setParent(this); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); - fireRepositoryEvent(e); - } - - public void switchToBestModel() { - var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); - this.switchTo(best.get()); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); - fireRepositoryEvent(e); - } - - private void fireRepositoryEvent(TaskRepositoryEvent e) { - var instance = TaskManager.getManagerInstance(); - for (var l : instance.getTaskRepositoryListeners()) - l.onTaskListChanged(e); - } - -} \ No newline at end of file + private Calculation current; + private List stored; + private ExperimentalData curve; + + private IterativeState path; //current sate + private IterativeState best; //best state + private Buffer buffer; + private Log log; + + private CorrelationBuffer correlationBuffer; + private CorrelationTest correlationTest; + private NormalityTest normalityTest; + + private Identifier identifier; + + /** + * If {@code SearchTask} finishes, and its R2 value is + * lower than this constant, the result will be considered + * {@code AMBIGUOUS}. + */ + private List listeners = new CopyOnWriteArrayList<>(); + private List statusChangeListeners = new CopyOnWriteArrayList<>(); + + /** + *

+ * Creates a new {@code SearchTask} from {@code curve}. Generates a new + * {@code Identifier}, sets the parent of {@code curve} to {@code this}, and + * invokes clear(). If any changes to the {@code ExperimentalData} occur, a + * listener will ensure the {@code DifferenceScheme} is modified + * accordingly. + *

+ * + * @param curve the {@code ExperimentalData} + */ + public SearchTask(ExperimentalData curve) { + current = new Calculation(); + current.setParent(this); + this.identifier = new Identifier(); + this.curve = curve; + curve.setParent(this); + correlationBuffer = new CorrelationBuffer(); + clear(); + addListeners(); + } + + /** + * Update the best state. The instance of this class stores two objects + * of the type IterativeState: the current state of the optimiser and + * the global best state. Calling this method will check if a new global + * best is found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search task, + * comparing the converged result to the global best, and selecting whichever + * has the lowest cost. Such routine is required due to the possibility of + * some optimisers going uphill. + */ + + public void storeState() { + if(best == null || best.getCost() > path.getCost()) + best = new IterativeState(path); + } + + private void addListeners() { + InterpolationDataset.addListener(e -> { + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) { + p.useTheoreticalEstimates(curve); + } + }); + + /** + * Sets the difference scheme's time limit to the upper bound of the + * range of {@code ExperimentalData} multiplied by a safety margin + * {@value Calculation.RELATIVE_TIME_MARGIN}. + */ + curve.addDataListener(dataEvent -> { + var scheme = current.getScheme(); + if (scheme != null) { + var hcurve = current.getProblem().getHeatingCurve(); + var startTime = (double) hcurve.getTimeShift().getValue(); + scheme.setTimeLimit( + derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + } + }); + } + + /** + *

+ * Resets everything to default values (for a list of default values please + * see the {@code .xml} document. Sets the status of this task to + * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { var scheme = + * current.getScheme(); if (scheme != null) { var curve = + * current.getProblem().getHeatingCurve(); var startTime = (double) + * curve.getTimeShift().getValue(); scheme.setTimeLimit(derive(TIME_LIMIT, + * RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); + *

+ */ + public void clear() { + stored = new ArrayList(); + curve.resetRanges(); + buffer = new Buffer(); + correlationBuffer.clear(); + buffer.setParent(this); + log = new Log(this); + + initCorrelationTest(); + initNormalityTest(); + + this.path = null; + current.clear(); + + this.checkProblems(true); + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code SearchTask} and calculate the SSR value + * showing how well (or bad) the calculated solution describes the + * {@code ExperimentalData}. + * + * @return the value of SSR (sum of squared residuals). + * @throws SolverException + */ + public double solveProblemAndCalculateCost() throws SolverException { + current.process(); + var rs = current.getOptimiserStatistic(); + rs.evaluate(this); + return (double) rs.getStatistic().getValue(); + } + + public List alteredParameters() { + return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + public ParameterVector searchVector() { + var flags = ActiveFlags.getAllFlags(); + var keywords = activeParameters(this); + var optimisationVector = new ParameterVector(keywords); + + current.getProblem().optimisationVector(optimisationVector, flags); + curve.getRange().optimisationVector(optimisationVector, flags); + + return optimisationVector; + } + + /** + * Assigns the values of the parameters of this {@code SearchTask} to + * {@code searchParameters}. + * + * @param searchParameters an {@code IndexedVector} with relevant search + * parameters + * @see pulse.problem.statements.Problem.assign(IndexedVector) + */ + public void assign(ParameterVector searchParameters) { + try { + current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); + } catch (SolverException e) { + var status = FAILED; + status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + e.printStackTrace(); + } + } + + /** + *

+ * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, + * will do nothing. After making some preparatory steps, will initiate a + * loop with successive calls to {@code PathSolver.iteration(this)}, filling + * the buffer and notifying any data change listeners in parallel. This loop + * will go on until either converging results are obtained, or a timeout is + * reached, or if an execution error happens. Whether the run has been + * successful will be determined by comparing the associated + * R2 value with the {@code SUCCESS_CUTOFF}. + *

+ */ + @Override + public void run() { + + current.setResult(null); + + /* check of status */ + switch (current.getStatus()) { + case READY: + case QUEUED: + setStatus(IN_PROGRESS); + break; + default: + return; + } + + /* preparatory steps */ + current.getProblem().parameterListChanged(); // get updated list of parameters + + var optimiser = getInstance(); + + path = optimiser.initState(this); + + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + int bufferSize = (Integer) getSize().getValue(); + buffer.init(); + correlationBuffer.clear(); + + /* search cycle */ + + /* sets an independent thread for manipulating the buffer */ + List> bufferFutures = new ArrayList<>(bufferSize); + var singleThreadExecutor = Executors.newSingleThreadExecutor(); + + try { + solveProblemAndCalculateCost(); + } catch (SolverException e1) { + System.err.println("Failed on first calculation. Details:"); + e1.printStackTrace(); + } + + final int maxIterations = (int) getInstance().getMaxIterations().getValue(); + + outer: + do { + + bufferFutures.clear(); + + for (var i = 0; i < bufferSize; i++) { + + if (current.getStatus() != IN_PROGRESS) { + break outer; + } + + int iter = 0; + + try { + for (boolean finished = false; !finished && iter < maxIterations; iter++) { + finished = optimiser.iteration(this); + } + } catch (SolverException e) { + setStatus(FAILED); + System.err.println(this + " failed during execution. Details: "); + e.printStackTrace(); + break outer; + } + + if (iter >= maxIterations) { + var fail = FAILED; + fail.setDetails(MAX_ITERATIONS_REACHED); + setStatus(fail); + } + + //if global best is better than the converged value + if(best != null && best.getCost() < path.getCost()) { + //assign the global best parameters + assign(path.getParameters()); + //and try to re-calculate + try { + solveProblemAndCalculateCost(); + } catch (SolverException ex) { + Logger.getLogger(SearchTask.class.getName()).log(Level.SEVERE, null, ex); + } + } + + final var j = i; + + bufferFutures.add(CompletableFuture.runAsync(() -> { + buffer.fill(this, j); + correlationBuffer.inflate(this); + notifyDataListeners(new DataLogEntry(this)); + }, singleThreadExecutor)); + + } + + bufferFutures.forEach(future -> future.join()); + + } while (buffer.isErrorTooHigh(errorTolerance)); + + singleThreadExecutor.shutdown(); + + if (current.getStatus() == IN_PROGRESS) { + runChecks(); + } + + } + + private void runChecks() { + + if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed + var status = FAILED; + status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + setStatus(status); + } else { + + var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected + // correlations + notifyDataListeners(new CorrelationLogEntry(this)); + + if (test) { + var status = AMBIGUOUS; + status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + setStatus(status); + } else { + // lastly, check if the parameter values estimated in this procedure are + // reasonable + + var properties = alteredParameters(); + + if (properties.stream().anyMatch(np -> !np.validate())) { + var status = FAILED; + status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + } else { + current.getModelSelectionCriterion().evaluate(this); + setStatus(DONE); + } + + } + + } + + } + + public void addTaskListener(DataCollectionListener toAdd) { + listeners.add(toAdd); + } + + public void addStatusChangeListener(StatusChangeListener toAdd) { + statusChangeListeners.add(toAdd); + } + + public void removeTaskListeners() { + listeners.clear(); + } + + public void removeStatusChangeListeners() { + statusChangeListeners.clear(); + } + + @Override + public String toString() { + return getIdentifier().toString(); + } + + public ExperimentalData getExperimentalCurve() { + return curve; + } + + public IterativeState getIterativeState() { + return path; + } + + /** + * Adopts the {@code curve} by this {@code SearchTask}. + * + * @param curve the {@code ExperimentalData}. + */ + public void setExperimentalCurve(ExperimentalData curve) { + this.curve = curve; + + if (curve != null) { + curve.setParent(this); + } + + } + + /** + * Will return {@code true} if status could be updated. + * @param status the status of the task + * @return {@code} true if status has been updated. {@code false} if + * the status was already set to {@code status} previously, or if it could + * not be updated at this time. + * @see Calculation.setStatus() + */ + + public boolean setStatus(Status status) { + Objects.requireNonNull(status); + + Status oldStatus = current.getStatus(); + boolean changed = current.setStatus(status) + && (oldStatus != current.getStatus()); + if (changed) { + notifyStatusListeners(new StateEntry(this, status)); + } + + return changed; + } + + /** + *

+ * Checks if this {@code SearchTask} is ready to be run. Performs basic + * check to see whether the user has uploaded all necessary data. If not, + * will create a status update with information about the missing data. + *

+ * + * @return {@code READY} if the task is ready to be run, {@code DONE} if has + * already been done previously, {@code INCOMPLETE} if some problems exist. + * For the latter, additional details will be available using the + * {@code status.getDetails()} method. + *

+ */ + public void checkProblems(boolean updateStatus) { + var status = current.getStatus(); + + if (status == DONE) { + return; + } + + var pathSolver = getInstance(); + var s = INCOMPLETE; + + if (current.getProblem() == null) { + s.setDetails(MISSING_PROBLEM_STATEMENT); + } else if (!current.getProblem().isReady()) { + s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); + } else if (current.getScheme() == null) { + s.setDetails(MISSING_DIFFERENCE_SCHEME); + } else if (curve == null) { + s.setDetails(MISSING_HEATING_CURVE); + } else if (pathSolver == null) { + s.setDetails(MISSING_OPTIMISER); + } else if (buffer == null) { + s.setDetails(MISSING_BUFFER); + } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { + s.setDetails(INCOMPATIBLE_OPTIMISER); + } else { + s = READY; + } + + if (updateStatus) { + setStatus(s); + } + } + + public Identifier getIdentifier() { + return identifier; + } + + public Log getLog() { + return log; + } + + private void notifyDataListeners(LogEntry e) { + for (var l : listeners) { + l.onDataCollected(e); + } + } + + private void notifyStatusListeners(StateEntry e) { + for (var l : statusChangeListeners) { + l.onStatusChange(e); + } + } + + @Override + public String describe() { + + var sb = new StringBuilder(); + sb.append(TaskManager.getManagerInstance().getSampleName()); + sb.append("_Task_"); + var extId = curve.getMetadata().getExternalID(); + if (extId < 0) { + sb.append("IntID_" + identifier.getValue()); + } else { + sb.append("ExtID_" + extId); + } + + return sb.toString(); + + } + + /** + * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or + * {@code READY}, terminates it by setting its status to {@code TERMINATED}. + * This change of status will then force the {@code run()} loop to stop (if + * running). + */ + public void terminate() { + switch (current.getStatus()) { + case IN_PROGRESS: + case QUEUED: + case READY: + setStatus(TERMINATED); + break; + default: + return; + } + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + /** + * A {@code SearchTask} is deemed equal to another one if it has the same + * {@code ExperimentalData}. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof SearchTask)) { + return false; + } + + return curve.equals(((SearchTask) o).getExperimentalCurve()); + + } + + public NormalityTest getNormalityTest() { + return normalityTest; + } + + public void initNormalityTest() { + normalityTest = instantiate(NormalityTest.class, NormalityTest.getSelectedTestDescriptor()); + normalityTest.setParent(this); + } + + public void initCorrelationTest() { + correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); + correlationTest.setParent(this); + } + + public CorrelationBuffer getCorrelationBuffer() { + return correlationBuffer; + } + + public CorrelationTest getCorrelationTest() { + return correlationTest; + } + + public Calculation getCurrentCalculation() { + return current; + } + + public List getStoredCalculations() { + return this.stored; + } + + public void switchTo(Calculation calc) { + current.setParent(null); + current = calc; + current.setParent(this); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); + fireRepositoryEvent(e); + } + + public void switchToBestModel() { + var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); + this.switchTo(best.get()); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); + fireRepositoryEvent(e); + } + + private void fireRepositoryEvent(TaskRepositoryEvent e) { + var instance = TaskManager.getManagerInstance(); + for (var l : instance.getTaskRepositoryListeners()) { + l.onTaskListChanged(e); + } + } + +} From 0661d5fa144074964777f63e842fb4c2105ca0a2 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 13:44:47 +0100 Subject: [PATCH 080/116] TaskManager -- concurrency of tasks and listeners TaskManager now has a TaskRepositoryListener, which triggers half-time calculation each time an ExperimentalData is generated. The execute sequence now starts from checking the status of all tasks before queuing them. If the tasks reject the status update to QUEUED, the calculation will not continue. dataNeedsTruncation() and truncateData() have been removed as these are now obsolete. Instead, the ExperimentalData for each task, upon being loaded, is processed by data.isAcquisitionTimeSensible() and data.truncate(). When multiple tasks area loaded, the forEach statement is now handled from within a Runnable being passed to a Single Thread Executor. This ensures there are no deadlocks when loading tasks, and that the progress bar for loading tasks now works smoothly. --- src/main/java/pulse/tasks/TaskManager.java | 991 ++++++++++----------- 1 file changed, 490 insertions(+), 501 deletions(-) diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index d909b537..44fa9c25 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -26,6 +26,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.SwingUtilities; +import pulse.input.ExperimentalData; import pulse.properties.SampleName; import pulse.search.direction.PathOptimiser; @@ -50,506 +55,490 @@ *

* */ - public class TaskManager extends UpwardsNavigable { - private static TaskManager instance = new TaskManager(); - - private List tasks; - private SearchTask selectedTask; - - private boolean singleStatement = true; - - private ForkJoinPool taskPool; - - private List selectionListeners; - private List taskRepositoryListeners; - - private final static String DEFAULT_NAME = "Project 1 - " + now().format(ISO_WEEK_DATE); - - private HierarchyListener statementListener = e -> { - - if (!(e.getSource() instanceof PropertyHolder)) { - - var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); - for (SearchTask t : tasks) { - if (t == task) - continue; - t.update(e.getProperty()); - } - - } - - }; - - private TaskManager() { - tasks = new ArrayList(); - taskPool = new ForkJoinPool(ResourceMonitor.getInstance().getThreadsAvailable()); - selectionListeners = new CopyOnWriteArrayList(); - taskRepositoryListeners = new CopyOnWriteArrayList(); - this.addHierarchyListener(statementListener); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static TaskManager getManagerInstance() { - return instance; - } - - /** - * Executes {@code t} asynchronously using a {@code CompletableFuture}. When - * done, creates a {@code Result} and puts it into the - * {@code Map(SearchTask,Result)} in this {@code TaskManager}. - * - * @param t a {@code SearchTask} that will be executed - */ - - public void execute(SearchTask t) { - t.setStatus(QUEUED); // notify listeners computation is about to start - - // notify listeners - notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - - // run task t -- after task completed, write result and trigger listeners - - CompletableFuture.runAsync(t).thenRun(() -> { - var current = t.getCurrentCalculation(); - var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - if (current.getStatus() == DONE) { - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned - notifyListeners(e); - current.setParent(null); - t.getStoredCalculations().add(current.copy()); - current.setParent(t); - } else - notifyListeners(e); - }); - - } - - /** - * Notifies the {@code TaskRepositoryListener}s of the {@code e} - * - * @param e an event - */ - - public void notifyListeners(TaskRepositoryEvent e) { - taskRepositoryListeners.stream().forEach(l -> l.onTaskListChanged(e)); - } - - /** - *

- * Creates a queue of {@code SearchTask}s based on their readiness and feeds - * that queue to a {@code ForkJoinPool} using a parallel stream. The size of the - * pool is usually limited by hardware, e.g. for a 4 core system with 2 - * independent threads on each core, the limitation will be 4*2 - 1 = - * 7, etc. - */ - - public void executeAll() { - - var queue = tasks.stream().filter(t -> { - switch (t.getCurrentCalculation().getStatus()) { - case DONE: - case IN_PROGRESS: - case EXECUTION_ERROR: - return false; - default: - return true; - } - }).collect(toList()); - - for(SearchTask t : queue) - taskPool.submit(() -> execute(t)); - - } - - /** - * Checks if any of the tasks that this {@code TaskManager} manages is either - * {@code QUEUED} or {@code IN_PROGRESS}. - * - * @return {@code false} if the status of the {@code SearchTask} is any of the - * above; {@code false} otherwise. - */ - - public boolean isTaskQueueEmpty() { - return !tasks.stream().anyMatch(t -> { - var status = t.getCurrentCalculation().getStatus(); - return status == QUEUED || status == IN_PROGRESS; - }); - } - - /** - * This will terminate all tasks in this {@code TaskManager} and trigger a - * {@code SHUTDOWN} {@code TaskRepositoryEvent}. - * - * @see pulse.tasks.Task.terminate() - */ - - public void cancelAllTasks() { - - tasks.stream().forEach(t -> t.terminate()); - - var e = new TaskRepositoryEvent(SHUTDOWN, null); - - notifyListeners(e); - - } - - /** - * Checks whether the acquisition time recorded by the experimental setup has - * been chosen appropriately. - * - * @return {@code false} if the acquisition time seems sensible for the - * {@code ExperimentalData} in each of the tasks; {@code true} - * otherwise. - * @see pulse.input.ExperimentalData.isAcquisitionTimeSensible() - */ - - public boolean dataNeedsTruncation() { - - return tasks.stream().anyMatch(t -> - - !t.getExperimentalCurve().isAcquisitionTimeSensible() - - ); - - } - - /** - * Calls {@code truncate()} on {@code ExperimentalData} for each - * {@code SearchTask}. - * - * @see pulse.input.ExperimentalData.truncate() - */ - - public void truncateData() { - tasks.stream().forEach(t -> - t.getExperimentalCurve().truncate() - ); - } - - private void fireTaskSelected(Object source) { - var e = new TaskSelectionEvent(source); - for (var l : selectionListeners) { - l.onSelectionChanged(e); - } - } - - /** - *

- * Purges all tasks from this {@code TaskManager}. Generates a - * {@code TASK_REMOVED} {@code TaskRepositoryEvent} for each of the removed - * tasks. Clears task selection. - *

- */ - - public void clear() { - tasks.stream().sorted((t1, t2) -> -t1.getIdentifier().compareTo(t2.getIdentifier())).forEach(task -> { - var e = new TaskRepositoryEvent(TASK_REMOVED, task.getIdentifier()); - notifyListeners(e); - }); - - tasks.clear(); - selectTask(null, null); - } - - /** - * Uses the first non-{@code null} {@code SearchTask} to retrieve the sample - * name from the {@code Metadata} associated with its {@code ExperimentalData}. - * - * @return a {@code String} with the sample name, or {@code null} if no suitable - * task can be found. - */ - - public SampleName getSampleName() { - if (tasks.size() < 1) - return null; - - var optional = tasks.stream().filter(t -> t != null).findFirst(); - - if (!optional.isPresent()) - return null; - - return optional.get().getExperimentalCurve().getMetadata().getSampleName(); - } - - /** - *

- * Clears any progress for all the tasks and resets everything. Triggers a - * {@code TASK_RESET} event. - *

- */ - - public void reset() { - if (tasks.isEmpty()) - return; - - for (var task : tasks) { - var e = new TaskRepositoryEvent(TASK_RESET, task.getIdentifier()); - - task.clear(); - - notifyListeners(e); - } - - PathOptimiser.getInstance().reset(); - - } - - /** - * Finds a {@code SearchTask} whose {@code Identifier} matches {@code id}. - * - * @param id the {@code Identifier} of the task. - * @return the {@code SearchTask} associated with this {@code Identifier}. - */ - - public SearchTask getTask(Identifier id) { - var o = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); - return o.isPresent() ? o.get() : null; - } - - /** - * Finds a {@code SearchTask} using the external identifier specified in its metadata. - * - * @param externalId the external ID of the data. - * @return the {@code SearchTask} associated with this {@code Identifier}. - */ - - public SearchTask getTask(int externalId) { - var o = tasks.stream().filter(t -> - Integer.compare( t.getExperimentalCurve().getMetadata().getExternalID(), - externalId ) == 0 ).findFirst(); - return o.isPresent() ? o.get() : null; - } - - /** - *

- * Generates a {@code SearchTask} assuming that the {@code ExperimentalData} is - * stored in the {@code file}. This will make the {@code ReaderManager} attempt - * to read that {@code file}. If successful, invokes {@code addTask(...)} on the - * created {@code SearchTask}. - *

- * - * @param file - * @see addTask(SearchTask) - * @see pulse.io.readers.ReaderManager.extract(File) - */ - - public void generateTask(File file) { - read(curveReaders(), file).stream().forEach(curve -> addTask(new SearchTask(curve))); - } - - /** - * Generates multiple tasks from multiple {@code files}. - * - * @param files a list of {@code File}s that can be parsed down to - * {@code ExperimentalData}. - */ - - public void generateTasks(List files) { - requireNonNull(files, "Null list of files passed to generatesTasks(...)"); - - - var pool = Executors.newSingleThreadExecutor(); - files.stream().forEach(f -> pool.submit(() -> generateTask(f))); - pool.shutdown(); - try { - pool.awaitTermination(2, TimeUnit.MINUTES); - } catch (InterruptedException e) { - System.err.println("Failed to load all tasks within 2 minutes. Details:"); - e.printStackTrace(); - } - - selectFirstTask(); - - // check if the data loaded needs truncation - if (instance.dataNeedsTruncation()) - this.truncateData(); - - } - - /** - *

- * If a task {@code equal} to {@code t} has already been previously loaded, does - * nothing. Otherwise, adds this {@code t} to the task repository and triggers a - * {@code TASK_ADDED} event. - *

- * - * @param t the {@code SearchTask} that needs to be added to the internal - * repository - * @return {@code null} if a task like {@code t} has already been added - * previously, {@code t} otherwise. - * @see pulse.tasks.SearchTask.equals(SearchTask) - */ - - public SearchTask addTask(SearchTask t) { - - if (tasks.stream().filter(task -> task.equals(t)).count() > 0) - return null; - - tasks.add(t); - - var e = new TaskRepositoryEvent(TASK_ADDED, t.getIdentifier()); - t.setParent(getManagerInstance()); - notifyListeners(e); - - return t; - } - - /** - * If {@code t} is found in the local repository, removes it and triggers a - * {@code TASK_REMOVED} event. - * - * @param t a {@code SearchTask} that has been previously loaded to this - * repository. - * @return {@code true} if the operation is successful, {@code false} otherwise. - */ - - public boolean removeTask(SearchTask t) { - if (tasks.stream().filter(task -> task.equals(t)).count() < 1) - return false; - - tasks.remove(t); - - var e = new TaskRepositoryEvent(TASK_REMOVED, t.getIdentifier()); - - notifyListeners(e); - selectedTask = null; - - return true; - } - - /** - * Gets the current number of tasks in the repository. - * - * @return the number of available tasks. - */ - - public int numberOfTasks() { - return tasks.size(); - } - - /** - *

- * Selects a {@code SearchTask} within this repository with the specified - * {@code id} (if present). Informs the listeners this selection has been - * triggered by {@code src}. - *

- * - * @param id the {@code Identifier} of a task within this repository. - * @param src the source of the selection. - */ - - public void selectTask(Identifier id, Object src) { - tasks.stream().filter(t -> t.getIdentifier().equals(id)).filter(t -> t != selectedTask).findAny() - .ifPresent(t -> { - selectedTask = t; - fireTaskSelected(src); - }); - } - - public void selectFirstTask() { - if (!tasks.isEmpty()) - selectTask(tasks.get(0).getIdentifier(), this); - } - - public void addSelectionListener(TaskSelectionListener listener) { - selectionListeners.add(listener); - } - - public void addTaskRepositoryListener(TaskRepositoryListener listener) { - taskRepositoryListeners.add(listener); - } - - public TaskSelectionListener[] getSelectionListeners() { - return (TaskSelectionListener[]) selectionListeners.toArray(); - } - - public void removeSelectionListeners() { - selectionListeners.clear(); - } - - public void removeTaskRepositoryListener(TaskRepositoryListener trl) { - taskRepositoryListeners.remove(trl); - } - - public int indexOfTask(SearchTask t) { - return tasks.indexOf(t); - } - - public List getTaskList() { - return tasks; - } - - public SearchTask getSelectedTask() { - return selectedTask; - } - - public List getTaskRepositoryListeners() { - return taskRepositoryListeners; - } - - /** - * This {@code TaskManager} will be described by the sample name for the - * experiment. - */ - - @Override - public String describe() { - return tasks.size() > 0 ? getSampleName().toString() : DEFAULT_NAME; - } - - public void evaluate() { - tasks.stream().forEach(t -> { - var properties = t.getCurrentCalculation().getProblem().getProperties(); - var c = t.getExperimentalCurve(); - properties.useTheoreticalEstimates(c); - }); - } - - public Set allGrouppedContents() { - - return getTaskList().stream().map(t -> contents(t)).reduce((a, b) -> { - a.addAll(b); - return a; - }).get(); - - } - - /** - * Checks whether changes in this {@code PropertyHolder} should automatically be - * accounted for by other instances of this class. - * - * @return {@code true} if the user has specified so (set by default), - * {@code false} otherwise - */ - - public boolean isSingleStatement() { - return singleStatement; - } - - /** - * Sets the flag to isolate or inter-connects changes in all instances of - * {@code PropertyHolder} - * - * @param singleStatement {@code false} if other {@code PropertyHoder}s should - * disregard changes, which happened to this instances. - * {@code true} otherwise. - */ - - public void setSingleStatement(boolean singleStatement) { - this.singleStatement = singleStatement; - if (!singleStatement) - this.removeHierarchyListener(statementListener); - else - this.addHierarchyListener(statementListener); - } - -} \ No newline at end of file + private static TaskManager instance = new TaskManager(); + + private List tasks; + private SearchTask selectedTask; + + private boolean singleStatement = true; + + private ForkJoinPool taskPool; + + private List selectionListeners; + private List taskRepositoryListeners; + + private final static String DEFAULT_NAME = "Project 1 - " + now().format(ISO_WEEK_DATE); + + private final HierarchyListener statementListener = e -> { + + if (!(e.getSource() instanceof PropertyHolder)) { + + var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); + for (SearchTask t : tasks) { + if (t == task) { + continue; + } + t.update(e.getProperty()); + } + + } + + }; + + private TaskManager() { + tasks = new ArrayList<>(); + taskPool = new ForkJoinPool(ResourceMonitor.getInstance().getThreadsAvailable()); + selectionListeners = new CopyOnWriteArrayList<>(); + taskRepositoryListeners = new CopyOnWriteArrayList<>(); + addHierarchyListener(statementListener); + /* + Calculate the half-time once data is loaded. + */ + addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if(e.getState() == TaskRepositoryEvent.State.TASK_ADDED) + getTask(e.getId()).getExperimentalCurve().calculateHalfTime(); + }); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static TaskManager getManagerInstance() { + return instance; + } + + /** + * Executes {@code t} asynchronously using a {@code CompletableFuture}. + * When done, creates a {@code Result} and puts it into the + * {@code Map(SearchTask,Result)} in this {@code TaskManager}. + * + * @param t a {@code SearchTask} that will be executed + */ + public void execute(SearchTask t) { + t.checkProblems(true); + + //try to start cmputation + // notify listeners computation is about to start + + if( ! t.setStatus(QUEUED) ) + return; + + // notify listeners calculation started + notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); + + // run task t -- after task completed, write result and trigger listeners + CompletableFuture.runAsync(t).thenRun(() -> { + var current = t.getCurrentCalculation(); + var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); + if (current.getStatus() == DONE) { + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + current.setParent(null); + t.getStoredCalculations().add(current.copy()); + current.setParent(t); + } else { + notifyListeners(e); + } + }); + + } + + /** + * Notifies the {@code TaskRepositoryListener}s of the {@code e} + * + * @param e an event + */ + public void notifyListeners(TaskRepositoryEvent e) { + taskRepositoryListeners.stream().forEach(l -> l.onTaskListChanged(e)); + } + + /** + *

+ * Creates a queue of {@code SearchTask}s based on their readiness and feeds + * that queue to a {@code ForkJoinPool} using a parallel stream. The size of + * the pool is usually limited by hardware, e.g. for a 4 core system with 2 + * independent threads on each core, the limitation will be 4*2 - 1 = + * 7, etc. + */ + public void executeAll() { + + var queue = tasks.stream().filter(t -> { + switch (t.getCurrentCalculation().getStatus()) { + case DONE: + case IN_PROGRESS: + case EXECUTION_ERROR: + return false; + default: + return true; + } + }).collect(toList()); + + for (SearchTask t : queue) { + taskPool.submit(() -> execute(t)); + } + + } + + /** + * Checks if any of the tasks that this {@code TaskManager} manages is + * either {@code QUEUED} or {@code IN_PROGRESS}. + * + * @return {@code false} if the status of the {@code SearchTask} is any of + * the above; {@code false} otherwise. + */ + public boolean isTaskQueueEmpty() { + return !tasks.stream().anyMatch(t -> { + var status = t.getCurrentCalculation().getStatus(); + return status == QUEUED || status == IN_PROGRESS; + }); + } + + /** + * This will terminate all tasks in this {@code TaskManager} and trigger a + * {@code SHUTDOWN} {@code TaskRepositoryEvent}. + * + * @see pulse.tasks.Task.terminate() + */ + public void cancelAllTasks() { + + tasks.stream().forEach(t -> t.terminate()); + + var e = new TaskRepositoryEvent(SHUTDOWN, null); + + notifyListeners(e); + + } + + private void fireTaskSelected(Object source) { + var e = new TaskSelectionEvent(source); + for (var l : selectionListeners) { + l.onSelectionChanged(e); + } + } + + /** + *

+ * Purges all tasks from this {@code TaskManager}. Generates a + * {@code TASK_REMOVED} {@code TaskRepositoryEvent} for each of the removed + * tasks. Clears task selection. + *

+ */ + public void clear() { + tasks.stream().sorted((t1, t2) -> -t1.getIdentifier().compareTo(t2.getIdentifier())).forEach(task -> { + var e = new TaskRepositoryEvent(TASK_REMOVED, task.getIdentifier()); + notifyListeners(e); + }); + + tasks.clear(); + selectTask(null, null); + } + + /** + * Uses the first non-{@code null} {@code SearchTask} to retrieve the sample + * name from the {@code Metadata} associated with its + * {@code ExperimentalData}. + * + * @return a {@code String} with the sample name, or {@code null} if no + * suitable task can be found. + */ + public SampleName getSampleName() { + if (tasks.size() < 1) { + return null; + } + + var optional = tasks.stream().filter(t -> t != null).findFirst(); + + if (!optional.isPresent()) { + return null; + } + + return optional.get().getExperimentalCurve().getMetadata().getSampleName(); + } + + /** + *

+ * Clears any progress for all the tasks and resets everything. Triggers a + * {@code TASK_RESET} event. + *

+ */ + public void reset() { + if (tasks.isEmpty()) { + return; + } + + for (var task : tasks) { + var e = new TaskRepositoryEvent(TASK_RESET, task.getIdentifier()); + + task.clear(); + + notifyListeners(e); + } + + PathOptimiser.getInstance().reset(); + + } + + /** + * Finds a {@code SearchTask} whose {@code Identifier} matches {@code id}. + * + * @param id the {@code Identifier} of the task. + * @return the {@code SearchTask} associated with this {@code Identifier}. + */ + public SearchTask getTask(Identifier id) { + var o = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); + return o.isPresent() ? o.get() : null; + } + + /** + * Finds a {@code SearchTask} using the external identifier specified in its + * metadata. + * + * @param externalId the external ID of the data. + * @return the {@code SearchTask} associated with this {@code Identifier}. + */ + public SearchTask getTask(int externalId) { + var o = tasks.stream().filter(t + -> Integer.compare(t.getExperimentalCurve().getMetadata().getExternalID(), + externalId) == 0).findFirst(); + return o.isPresent() ? o.get() : null; + } + + /** + *

+ * Generates a {@code SearchTask} assuming that the {@code ExperimentalData} + * is stored in the {@code file}. This will make the {@code ReaderManager} + * attempt to read that {@code file}. If successful, invokes + * {@code addTask(...)} on the created {@code SearchTask}. After the + * task is generated, checks whether the acquisition time recorded by the experimental setup + * has been chosen appropriately. + * + * @see pulse.input.ExperimentalData.isAcquisitionTimeSensible() + + *

+ * + * @param file the file to load the experimental data from + * @see addTask(SearchTask) + * @see pulse.io.readers.ReaderManager.extract(File) + */ + public void generateTask(File file) { + read(curveReaders(), file).stream().forEach((ExperimentalData curve) -> { + var task = new SearchTask(curve); + addTask(task); + var data = task.getExperimentalCurve(); + if(!data.isAcquisitionTimeSensible()) + data.truncate(); + }); + } + + /** + * Generates multiple tasks from multiple {@code files}. + * + * @param files a list of {@code File}s that can be parsed down to + * {@code ExperimentalData}. + */ + public void generateTasks(List files) { + requireNonNull(files, "Null list of files passed to generatesTasks(...)"); + + //this is the loader runnable submitted to the executor service + Runnable loader = () -> { + var pool = Executors.newSingleThreadExecutor(); + files.stream().forEach(f -> pool.submit(() -> generateTask(f))); + pool.shutdown(); + + try { + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (InterruptedException ex) { + Logger.getLogger(TaskManager.class.getName()).log(Level.SEVERE, null, ex); + } + + //when pool has been shutdown + selectFirstTask(); + + }; + + Executors.newSingleThreadExecutor().submit(loader); + + } + + /** + *

+ * If a task {@code equal} to {@code t} has already been previously loaded, + * does nothing. Otherwise, adds this {@code t} to the task repository and + * triggers a {@code TASK_ADDED} event. + *

+ * + * @param t the {@code SearchTask} that needs to be added to the internal + * repository + * @return {@code null} if a task like {@code t} has already been added + * previously, {@code t} otherwise. + * @see pulse.tasks.SearchTask.equals(SearchTask) + */ + public SearchTask addTask(SearchTask t) { + + if (tasks.stream().filter(task -> task.equals(t)).count() > 0) { + return null; + } + + tasks.add(t); + + var e = new TaskRepositoryEvent(TASK_ADDED, t.getIdentifier()); + t.setParent(getManagerInstance()); + notifyListeners(e); + + return t; + } + + /** + * If {@code t} is found in the local repository, removes it and triggers a + * {@code TASK_REMOVED} event. + * + * @param t a {@code SearchTask} that has been previously loaded to this + * repository. + * @return {@code true} if the operation is successful, {@code false} + * otherwise. + */ + public boolean removeTask(SearchTask t) { + if (tasks.stream().filter(task -> task.equals(t)).count() < 1) { + return false; + } + + tasks.remove(t); + + var e = new TaskRepositoryEvent(TASK_REMOVED, t.getIdentifier()); + + notifyListeners(e); + selectedTask = null; + + return true; + } + + /** + * Gets the current number of tasks in the repository. + * + * @return the number of available tasks. + */ + public int numberOfTasks() { + return tasks.size(); + } + + /** + *

+ * Selects a {@code SearchTask} within this repository with the specified + * {@code id} (if present). Informs the listeners this selection has been + * triggered by {@code src}. + *

+ * + * @param id the {@code Identifier} of a task within this repository. + * @param src the source of the selection. + */ + public void selectTask(Identifier id, Object src) { + tasks.stream().filter(t -> t.getIdentifier().equals(id)).filter(t -> t != selectedTask).findAny() + .ifPresent(t -> { + selectedTask = t; + fireTaskSelected(src); + }); + } + + public void selectFirstTask() { + if (!tasks.isEmpty() && selectedTask != tasks.get(0)) { + selectTask(tasks.get(0).getIdentifier(), this); + } + } + + public final void addSelectionListener(TaskSelectionListener listener) { + selectionListeners.add(listener); + } + + public final void addTaskRepositoryListener(TaskRepositoryListener listener) { + taskRepositoryListeners.add(listener); + } + + public TaskSelectionListener[] getSelectionListeners() { + return (TaskSelectionListener[]) selectionListeners.toArray(); + } + + public void removeSelectionListeners() { + selectionListeners.clear(); + } + + public void removeTaskRepositoryListener(TaskRepositoryListener trl) { + taskRepositoryListeners.remove(trl); + } + + public int indexOfTask(SearchTask t) { + return tasks.indexOf(t); + } + + public List getTaskList() { + return tasks; + } + + public SearchTask getSelectedTask() { + return selectedTask; + } + + public List getTaskRepositoryListeners() { + return taskRepositoryListeners; + } + + /** + * This {@code TaskManager} will be described by the sample name for the + * experiment. + */ + @Override + public String describe() { + return tasks.size() > 0 ? getSampleName().toString() : DEFAULT_NAME; + } + + public void evaluate() { + tasks.stream().forEach(t -> { + var properties = t.getCurrentCalculation().getProblem().getProperties(); + var c = t.getExperimentalCurve(); + properties.useTheoreticalEstimates(c); + }); + } + + public Set allGrouppedContents() { + + return getTaskList().stream().map(t -> contents(t)).reduce((a, b) -> { + a.addAll(b); + return a; + }).get(); + + } + + /** + * Checks whether changes in this {@code PropertyHolder} should + * automatically be accounted for by other instances of this class. + * + * @return {@code true} if the user has specified so (set by default), + * {@code false} otherwise + */ + public boolean isSingleStatement() { + return singleStatement; + } + + /** + * Sets the flag to isolate or inter-connects changes in all instances of + * {@code PropertyHolder} + * + * @param singleStatement {@code false} if other {@code PropertyHoder}s + * should disregard changes, which happened to this instances. {@code true} + * otherwise. + */ + public void setSingleStatement(boolean singleStatement) { + this.singleStatement = singleStatement; + if (!singleStatement) { + this.removeHierarchyListener(statementListener); + } else { + this.addHierarchyListener(statementListener); + } + } + +} From 07718778f641fb602e0de8a6bb5be90fc164d483 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 13:56:02 +0100 Subject: [PATCH 081/116] Fixed result processing Remove ResultOperation, as this was more confusing than it actually solved any problems. The ResultProcessing has now changed to ResultStatistic. The ResultStatistic has a method process(), which shuffles the input array of results and maps it to a Map>, i.e. it collects all values of properties and group them by the property keyword. This map is then used to calculate the averages and the errors of the average properties. The output is assigned strictly according to the specified ResultFormat, i.e. the order in which properties appear are taken from the ResultFormat instance. An error associated with the calculation of the standard deviation of the mean was corrected. This is associated with the fact that, such standard deviation was calculated as bias-uncorrected stdev of the population, i.e. it was larger than the stdev of the mean by sqrt(n-1). This has now been corrected. The error value now represents half of the span of the confidence interval calculated at a confidence level of 0.95. This is achieved by calculating the inverse cumulative distribution function of the t-distribution, for the number of degrees of freedom equal to the input array size (number of results). The qunatile of the t-distribution is then multiplied by the stdev of the mean. The AverageResult.calculate() method was modified to allow calculating the mean values and errors associated with averaging of integer-type results (the error is then rounded up to an integer value). Additionally, the sqrt(...) operation was removed for the calculation of stdev from the variance, as the ResultStatistics now has the full calculation of errors. --- .../pulse/tasks/processing/AverageResult.java | 211 +++++++++--------- .../tasks/processing/ResultOperations.java | 34 --- .../tasks/processing/ResultProcessing.java | 24 -- .../tasks/processing/ResultStatistics.java | 118 ++++++++++ 4 files changed, 227 insertions(+), 160 deletions(-) delete mode 100644 src/main/java/pulse/tasks/processing/ResultOperations.java delete mode 100644 src/main/java/pulse/tasks/processing/ResultProcessing.java create mode 100644 src/main/java/pulse/tasks/processing/ResultStatistics.java diff --git a/src/main/java/pulse/tasks/processing/AverageResult.java b/src/main/java/pulse/tasks/processing/AverageResult.java index a94995d7..f5cdf85b 100644 --- a/src/main/java/pulse/tasks/processing/AverageResult.java +++ b/src/main/java/pulse/tasks/processing/AverageResult.java @@ -3,10 +3,10 @@ import static pulse.properties.NumericProperties.derive; import java.math.BigDecimal; -import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -16,107 +16,114 @@ * {@code AbstractResult}s and calculating the associated errors of averaging. * */ - public class AverageResult extends AbstractResult { - private List results; - - public final static int SIGNIFICANT_FIGURES = 2; - - /** - * This will create an {@code AverageResult} based on the {@code AbstractResult} - * in {@code res}. - *

- * It will also use the {@code resultFormat}. A method will be invoked to: (a) - * calculate the average values of the list of {@code NumericProperty} according - * to the {@code resultFormat}; (b) calculate the standard error associated with - * averaging; (c) create a {@code BigDecimal} representation of the values and - * the errors, so that only {@value SIGNIFICANT_FIGURES} significant figures are - * left for consistency between the {@code value} and the {@code error}. - *

- * - * @param res a list of {@code AbstractResult}s that are going to be - * averaged (not necessarily instances of {@code Result}). - * @param resultFormat the {@code ResultFormat}, which will be used for this - * {@code AveragedResult}. - */ - - public AverageResult(List res, ResultFormat resultFormat) { - super(resultFormat); - - results = new ArrayList<>(); - results.addAll(res); - - calculate(); - } - - private void calculate() { - - ResultOperations.process(results, ResultFormat.getInstance().size()); - - /* - * Calculate average and standard error for each column - */ - - double[] av = ResultOperations.getAverages(); - double[] std = ResultOperations.getDeviations(); - - /* + private final List results; + + public final static int SIGNIFICANT_FIGURES = 2; + + /** + * This will create an {@code AverageResult} based on the + * {@code AbstractResult} in {@code res}. + *

+ * It will also use the {@code resultFormat}. A method will be invoked to: + * (a) calculate the mean values of the list of {@code NumericProperty} + * according to the {@code resultFormat}; (b) calculate the statistical error; + * (c) create a {@code BigDecimal} representation + * of the values and the errors, so that only {@value SIGNIFICANT_FIGURES} + * significant figures are left for consistency between the {@code value} + * and the {@code error}. + *

+ * + * @param res a list of {@code AbstractResult}s that are going to be + * averaged (not necessarily instances of {@code Result}). + * @param resultFormat the {@code ResultFormat}, which will be used for this + * {@code AveragedResult}. + */ + public AverageResult(List res, ResultFormat resultFormat) { + super(resultFormat); + + results = new ArrayList<>(); + results.addAll(res); + + calculate(); + } + + private void calculate() { + + var rs = new ResultStatistics(); + + rs.process(results); + + /* + * Calculate average and statistical error for each column + */ + double[] av = rs.getAverages(); + double[] err = rs.getErrors(); + + /* * Round up - */ - - NumericProperty p; - NumericPropertyKeyword key; - - for (int j = 0; j < av.length; j++) { - key = getFormat().getKeywords().get(j); - - if (!Double.isFinite(std[j])) - p = derive(key, av[j]); // ignore error as the value is not finite - else { - var stdBig = (new BigDecimal(std[j])).sqrt(MathContext.DECIMAL64); - var avBig = new BigDecimal(av[j]); - - var resultStd = stdBig.setScale(SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), - RoundingMode.HALF_UP); - - var resultAv = stdBig.precision() > 1 ? avBig.setScale(resultStd.scale(), RoundingMode.CEILING) : avBig; - - p = derive(key, resultAv.doubleValue()); - p.setError(resultStd.doubleValue()); - - } - - addProperty(p); - - } - - } - - /** - * This will analyse the list of {@code AbstractResult}s used for calculation of - * the average and find all associated individual results. - *

- * If it is established that some instances of {@code AverageResult} were used - * in the calculation, this will invoke this method recursively to get a full - * list of {@code AbstractResult}s that are not {@code AverageResult}s - * - * @return a list of {@code AbstractResult}s that are guaranteed not to be - * {@code AveragedResult}s. - */ - - public List getIndividualResults() { - List indResults = new ArrayList<>(); - - for (AbstractResult r : results) { - if (r instanceof AverageResult) { - var ar = (AverageResult) r; - indResults.addAll(ar.getIndividualResults()); - } else - indResults.add(r); - } - - return indResults; - } - -} \ No newline at end of file + */ + NumericProperty p; + NumericPropertyKeyword key; + + for (int j = 0; j < av.length; j++) { + key = getFormat().getKeywords().get(j); + + if (!Double.isFinite(err[j])) { + p = derive(key, av[j]); // ignore error as the value is not finite + } else if(NumericProperties.def(key).getValue() instanceof Double) { + var stdBig = new BigDecimal(err[j]); + var avBig = new BigDecimal(av[j]); + + var error = stdBig.setScale( + SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), + RoundingMode.HALF_UP); + + var mean = stdBig.precision() > 1 ? + avBig.setScale(error.scale(), RoundingMode.CEILING) + : avBig; + + p = derive(key, mean.doubleValue()); + p.setError(error.doubleValue()); + + } else { + //if integer + p = derive(key, (int)Math.round( av[j] ) ); + p.setError((int)Math.round( err[j] ) ); + } + + addProperty(p); + + } + + } + + /** + * This will analyse the list of {@code AbstractResult}s used for + * calculation of the mean and find all associated individual results. + *

+ * If it is established that some instances of {@code AverageResult} were + * used in the calculation, this will invoke this method recursively to get + * a full list of {@code AbstractResult}s that are not + * {@code AverageResult}s + * + * @return a list of {@code AbstractResult}s that are guaranteed not to be + * {@code AveragedResult}s. + */ + public List getIndividualResults() { + List indResults = new ArrayList<>(); + + for (AbstractResult r : results) { + if (r instanceof AverageResult) { + var ar = (AverageResult) r; + indResults.addAll(ar.getIndividualResults()); + } else { + indResults.add(r); + } + } + + return indResults; + } + +} diff --git a/src/main/java/pulse/tasks/processing/ResultOperations.java b/src/main/java/pulse/tasks/processing/ResultOperations.java deleted file mode 100644 index 0391133c..00000000 --- a/src/main/java/pulse/tasks/processing/ResultOperations.java +++ /dev/null @@ -1,34 +0,0 @@ -package pulse.tasks.processing; - -import java.util.List; - -class ResultOperations { - - private static double[] av; - private static double[] dev; - - public static ResultProcessing average = ( (p, i) -> ((Number) p.getValue()).doubleValue()); - - public static ResultProcessing stdev = (p, i) -> { - double x = ( (Number) p.getValue() ).doubleValue() - av[i]; - return x*x; - }; - - private ResultOperations() { - //intentionaly blank - } - - public static void process(List results, final int properties) { - av = average.process(results, properties); - dev = stdev.process(results, properties); - } - - public static double[] getAverages() { - return av; - } - - public static double[] getDeviations() { - return dev; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultProcessing.java b/src/main/java/pulse/tasks/processing/ResultProcessing.java deleted file mode 100644 index cd2cd14f..00000000 --- a/src/main/java/pulse/tasks/processing/ResultProcessing.java +++ /dev/null @@ -1,24 +0,0 @@ -package pulse.tasks.processing; - -import java.util.List; - -import pulse.properties.NumericProperty; - -interface ResultProcessing { - - public default double[] process(List results, final int properties) { - final int size = results.size(); - double[] av = new double[properties]; - - for (int i = 0; i < av.length; i++) { - for (AbstractResult r : results) { - av[i] += value(r.getProperty(i), i); - } - av[i] /= size; - } - return av; - } - - public abstract double value(NumericProperty p, int i); - -} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultStatistics.java b/src/main/java/pulse/tasks/processing/ResultStatistics.java new file mode 100644 index 00000000..807b2f57 --- /dev/null +++ b/src/main/java/pulse/tasks/processing/ResultStatistics.java @@ -0,0 +1,118 @@ +package pulse.tasks.processing; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import org.apache.commons.math3.distribution.TDistribution; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import pulse.properties.NumericPropertyKeyword; + +import pulse.util.ImmutablePair; + +/** + * A simple collection of mean values and errors associated with an + * {@code AverageResult}, and a method for calculating those statistics. + * + * @author Artem Lunev + */ +class ResultStatistics { + + private double[] av; + private double[] err; + + /** + * Confidence level of {@value CONFIDENCE_LEVEL} for error calculation using + * t-distribution quantiles. + */ + public final static double CONFIDENCE_LEVEL = 0.95; + + public ResultStatistics() { + //intentionaly blank + } + + /** + * This will calculate the statistics for each type of + * {@code NumericProperty} stored in the {@code results}. It is assumed that + * the order in which these properties appear in each element of + * {@code results} is consistent, and it will be maintained as the order in + * which these statistic are stored. The calculated statistics include the + * arithmetic mean and the statistical error. The latter is calculated + * assuming a {@value CONFIDENCE_LEVEL} confidence level, by calculating a + * standard deviation for each {@code NumericPropertyKeyword} and + * multiplying the result by the quantile value of the + * t-distribution. The inverse cumulative distribution function of + * Student distribution is calculated using {@code ApacheCommonsMath} library. + * + * @param results a list of individual (or average) results to be processed + */ + public void process(List results) { + + //group all properties contained in the list of results by their keyword + Map> map = results.stream() + .flatMap(r -> r.getProperties().stream()) + .collect(Collectors.groupingBy(t -> t.getType(), + Collectors.mapping(p + -> ((Number) p.getValue()).doubleValue(), + Collectors.toList()))); + + /* + The number of elements in the parameter list. This ASSUMES that the input + list contains results with the same number of output parameters! + */ + + StandardDeviation sd = new StandardDeviation(true); //bias-corrected sd + double sqrtn = Math.sqrt(results.size()); + + //calculate average values + + var stats = ResultFormat.getInstance().getKeywords().stream() + .map(key -> map.get(key)) //preserve the original order of keywods + .map(c -> { + double mean = openStream(c).average().orElse(0.0); //fail-safe, in case if avg is undefined + return new ImmutablePair( + mean, //the mean value + sd.evaluate(openStream(c).toArray(), mean) //that would be the sample standard deviation + / sqrtn //however, since we are calculating the std of the MEAN, + //we need to divide the result by sqrtn + ); + }).collect(Collectors.toList()); + + av = stats.stream().mapToDouble(pair -> pair.getFirst()).toArray(); //store mean values + + //Student t-distribution with degrees of freedom equal to number of individual results + TDistribution student = new TDistribution(av.length); + //CDF value + double t = student.inverseCumulativeProbability(CONFIDENCE_LEVEL); //right tail + + err = stats.stream().mapToDouble(pair + -> t * pair.getSecond() //the error is equal to half of the span of the confidence interval + ).toArray(); //store errors + + } + + private DoubleStream openStream(List input) { + return input.stream().mapToDouble(d -> d); + } + + /** + * Retrieves the mean values of properties. + * + * @return the mean values + * @see process() + */ + public double[] getAverages() { + return av; + } + + /** + * Retrieves the statistical errors associated with the property values. + * + * @return the values of the statistical error + * @see process() + */ + public double[] getErrors() { + return err; + } + +} From 2ef000ab8cdc20eb21613105b87e2250fed49f41 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 14:21:10 +0100 Subject: [PATCH 082/116] Charts, markers, and mouse events - update Introduced interactive ValueMarkers in Charts, which mark the boundaries of the search range. The markers are now highlighted whenever the cursor appears in the direct vicinity. They also respond to mouseDragged events now, which not only changes the rendering of those items in the chart, but simultaneously changes the Range object of the currently selected task's ExperimentalData. The markers can only be dragged within pre-defined 'sanity' regions. For example, attempting to drag them at t > tmax will set the marker position to the top boundary (= tmax). The markes are automatically updated whenever the Range's lower or upper bounds change -- including during the optimisation process (in real time). The changes to the ExperimentalData's range is reflected in the range JFormattedTextFields appearing in the ChartToolbar. Now these elements are specified in their own class, RangeTextFields. The values in the textfields are synchronised with both the ExperimentalData's Range and the ValueMarkers on the the Charts, so that each edit to the ValueMarker and to the Range will be reflected in the text fields. However, if the textfields are edited themselves, the Range will only be updated when the user clicks to Set Range Button (formerly Limit Range To). In addition, typing an illegal value in the textfields now results in that value being discarded. Only numbers within the intervals specified in the Range class are allowed. Finally, the text fields display the range in milliseconds with one digit after the decimal point (this is additionally highlighted by the 'ms' suffix). When the Set Range button is pressed, the user is given three different options now: to update range to All tasks, just to the current one, and to discard changes. The pop-up window now has text wrapping set by the html tags. The property values displayed are formatted according to the same convention used for the range textfields. --- src/main/java/pulse/ui/components/Chart.java | 517 ++++++++++-------- .../ui/components/MovableValueMarker.java | 58 ++ .../pulse/ui/components/RangeTextFields.java | 216 ++++++++ .../pulse/ui/components/ResidualsChart.java | 75 +-- .../listeners/MouseOnMarkerListener.java | 71 +++ .../ui/components/panels/ChartToolbar.java | 366 +++++-------- 6 files changed, 828 insertions(+), 475 deletions(-) create mode 100644 src/main/java/pulse/ui/components/MovableValueMarker.java create mode 100644 src/main/java/pulse/ui/components/RangeTextFields.java create mode 100644 src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 6287fcc8..a4858060 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -18,7 +18,8 @@ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; -import java.awt.Stroke; +import java.awt.event.MouseEvent; +import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -26,7 +27,6 @@ import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.block.BlockBorder; -import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.LegendTitle; @@ -39,272 +39,353 @@ import pulse.HeatingCurve; import pulse.input.ExperimentalData; import pulse.input.IndexRange; +import pulse.input.Range; +import pulse.input.listeners.DataEvent; +import pulse.properties.NumericProperties; +import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; +import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; import pulse.tasks.Calculation; import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.ui.components.listeners.MouseOnMarkerListener; public class Chart { - private ChartPanel chartPanel; - private JFreeChart chart; - private XYPlot plot; - - private float opacity = 0.15f; - private boolean residualsShown = true; - private boolean zeroApproximationShown = false; - - private final static double TO_MILLIS = 1E3; - private final static double RANGE_THRESHOLD = 1E-1; - private double factor; - - public Chart() { - chart = createScatterPlot("", getString("Charting.TimeAxisLabel"), (getString("Charting.TemperatureAxisLabel")), - null, VERTICAL, true, true, false); - - plot = chart.getXYPlot(); - setRenderers(); - setBackgroundAndGrid(); - setLegendTitle(); - setFonts(); - - chart.removeLegend(); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); - chartPanel = new ChartPanel(chart); - } - - private void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 14); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); - } - - private void setBackgroundAndGrid() { - // plot.setBackgroundPaint(UIManager.getColor("Panel.background")); - plot.setRangeGridlinesVisible(true); - plot.setRangeGridlinePaint(GRAY); + private ChartPanel chartPanel; + private JFreeChart chart; + private XYPlot plot; + + private float opacity = 0.15f; + private boolean residualsShown = true; + private boolean zeroApproximationShown = false; + + private final static double TO_MILLIS = 1E3; + private final static double RANGE_THRESHOLD = 1E-1; + private double factor; + + private MovableValueMarker lowerMarker; + private MovableValueMarker upperMarker; + + public Chart() { + chart = createScatterPlot("", getString("Charting.TimeAxisLabel"), (getString("Charting.TemperatureAxisLabel")), + null, VERTICAL, true, true, false); + + plot = chart.getXYPlot(); + setRenderers(); + setBackgroundAndGrid(); + setLegendTitle(); + setFonts(); + + final TaskManager instance = TaskManager.getManagerInstance(); + + chart.removeLegend(); + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + chartPanel = new ChartPanel(chart) { + + @Override + public void mouseDragged(MouseEvent e) { + if (lowerMarker == null || upperMarker == null) { + super.mouseDragged(e); + } + + SwingUtilities.invokeLater(() -> { + + //process dragged events + Range range = instance.getSelectedTask() + .getExperimentalCurve().getRange(); + double value = xCoord(e); + + if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { + if (range.boundLimits(false).contains(value)) { + range.setLowerBound(NumericProperties.derive(LOWER_BOUND, value)); + } + } else if (upperMarker.getState() != MovableValueMarker.State.IDLE) { + if (range.boundLimits(true).contains(value)) { + range.setUpperBound(NumericProperties.derive(UPPER_BOUND, value)); + } + } else { + super.mouseDragged(e); + } + + }); + + } + + }; + + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + //for each new task + var eventTask = instance.getTask(e.getId()); + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + //add passive data listener + eventTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + //that will be triggered only when this task is selected + if (instance.getSelectedTask() == eventTask) { + //update marker values + var segment = eventTask.getExperimentalCurve().getRange().getSegment(); + lowerMarker.setValue(segment.getMinimum()); + upperMarker.setValue(segment.getMaximum()); + } + }); + } //tasks that have been finihed + else if (e.getState() == TaskRepositoryEvent.State.TASK_FINISHED + && instance.getSelectedTask() == eventTask) { + //add passive data listener + plot(eventTask, false); + } + }); + + } + + public double xCoord(MouseEvent e) { + double xVirtual = e.getX(); + return plot.getDomainAxis().java2DToValue(xVirtual, + chartPanel.getScreenDataArea(), plot.getDomainAxisEdge()); + } + + private void setFonts() { + var fontLabel = new Font("Arial", Font.PLAIN, 20); + var fontTicks = new Font("Arial", Font.PLAIN, 14); + plot.getDomainAxis().setLabelFont(fontLabel); + plot.getDomainAxis().setTickLabelFont(fontTicks); + plot.getRangeAxis().setLabelFont(fontLabel); + plot.getRangeAxis().setTickLabelFont(fontTicks); + } + + private void setBackgroundAndGrid() { + // plot.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setRangeGridlinesVisible(true); + plot.setRangeGridlinePaint(GRAY); + + plot.setDomainGridlinesVisible(true); + plot.setDomainGridlinePaint(GRAY); + } + + private void setLegendTitle() { + var lt = new LegendTitle(plot); + lt.setItemFont(new Font("Dialog", PLAIN, 14)); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.98, 0.2, lt, RectangleAnchor.RIGHT); + ta.setMaxWidth(0.58); + plot.addAnnotation(ta); + } + + private void setRenderers() { + var renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer(); + renderer.setDefaultShapesFilled(false); + renderer.setUseFillPaint(false); + renderer.setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); + + var rendererLines = new XYLineAndShapeRenderer(); + rendererLines.setSeriesPaint(0, BLUE); + rendererLines.setSeriesStroke(0, new BasicStroke(2.0f)); + rendererLines.setSeriesShapesVisible(0, false); + + var rendererResiduals = new XYLineAndShapeRenderer(); + rendererResiduals.setSeriesPaint(0, GREEN); + rendererResiduals.setSeriesShapesVisible(0, false); + + var rendererClassic = new XYLineAndShapeRenderer(); + rendererClassic.setSeriesPaint(0, BLACK); + rendererClassic.setSeriesShapesVisible(0, false); + + var rendererOld = new XYLineAndShapeRenderer(); + rendererOld.setSeriesPaint(0, BLUE); + rendererOld.setSeriesStroke(0, new BasicStroke(2.0f, CAP_BUTT, JOIN_MITER, 2.0f, new float[]{10f}, 0)); + rendererOld.setSeriesShapesVisible(0, false); + + plot.setRenderer(0, rendererLines); + plot.setRenderer(1, rendererResiduals); + plot.setRenderer(2, rendererClassic); + plot.setRenderer(3, renderer); + + } + + private void adjustAxisLabel(double maximum) { + if (maximum < RANGE_THRESHOLD) { + factor = TO_MILLIS; + plot.getDomainAxis().setLabel("Time (ms)"); + } else { + factor = 1.0; + plot.getDomainAxis().setLabel("Time (s)"); + } + } + + public void plot(SearchTask task, boolean extendedCurve) { + requireNonNull(task); + + var plot = chart.getXYPlot(); + + for (int i = 0; i < 6; i++) { + plot.setDataset(i, null); + } + + var rawData = task.getExperimentalCurve(); + var segment = rawData.getRange().getSegment(); + + adjustAxisLabel(segment.getMaximum()); + + factor = segment.getMaximum() < RANGE_THRESHOLD ? TO_MILLIS : 1.0; - plot.setDomainGridlinesVisible(true); - plot.setDomainGridlinePaint(GRAY); - } + var rawDataset = new XYSeriesCollection(); - private void setLegendTitle() { - var lt = new LegendTitle(plot); - lt.setItemFont(new Font("Dialog", PLAIN, 14)); - lt.setBackgroundPaint(new Color(200, 200, 255, 100)); - lt.setFrame(new BlockBorder(black)); - lt.setPosition(RectangleEdge.RIGHT); - var ta = new XYTitleAnnotation(0.98, 0.2, lt, RectangleAnchor.RIGHT); - ta.setMaxWidth(0.58); - plot.addAnnotation(ta); - } + rawDataset.addSeries(series(rawData, "Raw data (" + task.getIdentifier() + ")", extendedCurve)); + plot.setDataset(3, rawDataset); + plot.getRenderer(3).setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); - private void setRenderers() { - var renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer(); - renderer.setDefaultShapesFilled(false); - renderer.setUseFillPaint(false); - renderer.setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); + plot.clearDomainMarkers(); - var rendererLines = new XYLineAndShapeRenderer(); - rendererLines.setSeriesPaint(0, BLUE); - rendererLines.setSeriesStroke(0, new BasicStroke(2.0f)); - rendererLines.setSeriesShapesVisible(0, false); + lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); + upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - var rendererResiduals = new XYLineAndShapeRenderer(); - rendererResiduals.setSeriesPaint(0, GREEN); - rendererResiduals.setSeriesShapesVisible(0, false); + final double margin = segment.getMaximum() / 20.0; - var rendererClassic = new XYLineAndShapeRenderer(); - rendererClassic.setSeriesPaint(0, BLACK); - rendererClassic.setSeriesShapesVisible(0, false); + //add listener to handle range adjustment + var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, margin); + var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, margin); - var rendererOld = new XYLineAndShapeRenderer(); - rendererOld.setSeriesPaint(0, BLUE); - rendererOld.setSeriesStroke(0, new BasicStroke(2.0f, CAP_BUTT, JOIN_MITER, 2.0f, new float[] { 10f }, 0)); - rendererOld.setSeriesShapesVisible(0, false); + chartPanel.addChartMouseListener(lowerMarkerListener); + chartPanel.addChartMouseListener(upperMarkerListener); - plot.setRenderer(0, rendererLines); - plot.setRenderer(1, rendererResiduals); - plot.setRenderer(2, rendererClassic); - plot.setRenderer(3, renderer); + plot.addDomainMarker(upperMarker); + plot.addDomainMarker(lowerMarker); - } + var calc = task.getCurrentCalculation(); + var problem = calc.getProblem(); - private void adjustAxisLabel(double maximum) { - if (maximum < RANGE_THRESHOLD) { - factor = TO_MILLIS; - plot.getDomainAxis().setLabel("Time (ms)"); - } else { - factor = 1.0; - plot.getDomainAxis().setLabel("Time (s)"); - } - } + if (problem != null) { - public void plot(SearchTask task, boolean extendedCurve) { - requireNonNull(task); + var solution = problem.getHeatingCurve(); + var scheme = calc.getScheme(); - var plot = chart.getXYPlot(); + if (solution != null && scheme != null && !solution.isIncomplete()) { - for (int i = 0; i < 6; i++) - plot.setDataset(i, null); + var solutionDataset = new XYSeriesCollection(); + var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; - var rawData = task.getExperimentalCurve(); - var segment = rawData.getRange().getSegment(); + solutionDataset + .addSeries(series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); + plot.setDataset(0, solutionDataset); - adjustAxisLabel(segment.getMaximum()); - - factor = segment.getMaximum() < RANGE_THRESHOLD ? TO_MILLIS : 1.0; - - var rawDataset = new XYSeriesCollection(); - - rawDataset.addSeries(series(rawData, "Raw data (" + task.getIdentifier() + ")", extendedCurve)); - plot.setDataset(3, rawDataset); - plot.getRenderer(3).setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); - - plot.clearDomainMarkers(); - - var lowerMarker = new ValueMarker(segment.getMinimum() * factor); - - Stroke dashed = new BasicStroke(1.5f, CAP_BUTT, JOIN_MITER, 5.0f, new float[] { 10f }, 0.0f); - - lowerMarker.setPaint(black); - lowerMarker.setStroke(dashed); - - var upperMarker = new ValueMarker(segment.getMaximum() * factor); - upperMarker.setPaint(black); - upperMarker.setStroke(dashed); - - plot.addDomainMarker(upperMarker); - plot.addDomainMarker(lowerMarker); - - var calc = task.getCurrentCalculation(); - var problem = calc.getProblem(); - - if (problem != null) { - - var solution = problem.getHeatingCurve(); - var scheme = calc.getScheme(); - - if (solution != null && scheme != null && !solution.isIncomplete()) { - - var solutionDataset = new XYSeriesCollection(); - var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; - - solutionDataset - .addSeries(series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); - plot.setDataset(0, solutionDataset); - - /* + /* * plot residuals - */ + */ + if (residualsShown) { + var residuals = calc.getOptimiserStatistic().getResiduals(); + if (residuals != null && residuals.size() > 0) { + var residualsDataset = new XYSeriesCollection(); + residualsDataset.addSeries(residuals(calc)); + plot.setDataset(1, residualsDataset); + } + } - if (residualsShown) { - var residuals = calc.getOptimiserStatistic().getResiduals(); - if (residuals != null && residuals.size() > 0) { - var residualsDataset = new XYSeriesCollection(); - residualsDataset.addSeries(residuals(calc)); - plot.setDataset(1, residualsDataset); - } - } + } - } + } - } + if (zeroApproximationShown) { + var p = calc.getProblem(); + var s = calc.getScheme(); - if (zeroApproximationShown) { - var p = calc.getProblem(); - var s = calc.getScheme(); + if (p != null && s != null) { + plotSingle(classicSolution(p, (double) (s.getTimeLimit().getValue()))); + } + } - if (p != null && s != null) - plotSingle(classicSolution(p, (double) (s.getTimeLimit().getValue()))); - } + } - } + public void plotSingle(HeatingCurve curve) { + requireNonNull(curve); - public void plotSingle(HeatingCurve curve) { - requireNonNull(curve); + var plot = chart.getXYPlot(); - var plot = chart.getXYPlot(); + var classicDataset = new XYSeriesCollection(); - var classicDataset = new XYSeriesCollection(); + classicDataset.addSeries(series(curve, curve.getName(), false)); - classicDataset.addSeries(series(curve, curve.getName(), false)); + plot.setDataset(2, classicDataset); + plot.getRenderer(2).setSeriesPaint(0, black); + } - plot.setDataset(2, classicDataset); - plot.getRenderer(2).setSeriesPaint(0, black); - } + public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { + final int realCount = curve.getBaselineCorrectedData().size(); + final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); + return series(curve, title, startTime, realCount, extendedCurve); + } - public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { - final int realCount = curve.getBaselineCorrectedData().size(); - final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); - return series(curve, title, startTime, realCount, extendedCurve); - } + public XYSeries series(ExperimentalData curve, String title, boolean extendedCurve) { + return series(curve, title, 0, curve.actualNumPoints(), extendedCurve); + } - public XYSeries series(ExperimentalData curve, String title, boolean extendedCurve) { - return series(curve, title, 0, curve.actualNumPoints(), extendedCurve); - } + private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, + boolean extendedCurve) { + var series = new XYSeries(title); - private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, - boolean extendedCurve) { - var series = new XYSeries(title); + int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); - int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); + for (var i = 0; i < iStart && extendedCurve; i++) { + series.add(factor * curve.timeAt(i), curve.signalAt(i)); + } - for (var i = 0; i < iStart && extendedCurve; i++) - series.add(factor * curve.timeAt(i), curve.signalAt(i)); + for (var i = iStart; i < realCount; i++) { + series.add(factor * curve.timeAt(i), curve.signalAt(i)); + } - for (var i = iStart; i < realCount; i++) - series.add(factor * curve.timeAt(i), curve.signalAt(i)); + return series; + } - return series; - } + public XYSeries residuals(Calculation calc) { + var problem = calc.getProblem(); + var baseline = problem.getBaseline(); - public XYSeries residuals(Calculation calc) { - var problem = calc.getProblem(); - var baseline = problem.getBaseline(); + var residuals = calc.getOptimiserStatistic().getResiduals(); + var size = residuals.size(); - var residuals = calc.getOptimiserStatistic().getResiduals(); - var size = residuals.size(); + final var span = problem.getHeatingCurve().maxAdjustedSignal() - baseline.valueAt(0); + final var offset = baseline.valueAt(0) - span / 2.0; - final var span = problem.getHeatingCurve().maxAdjustedSignal() - baseline.valueAt(0); - final var offset = baseline.valueAt(0) - span / 2.0; + var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); - var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); + for (var i = 0; i < size; i++) { + series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); + } - for (var i = 0; i < size; i++) { - series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); - } + return series; + } - return series; - } + public void setOpacity(float opacity) { + this.opacity = opacity; + } - public void setOpacity(float opacity) { - this.opacity = opacity; - } + public double getOpacity() { + return opacity; + } - public double getOpacity() { - return opacity; - } + public boolean isResidualsShown() { + return residualsShown; + } - public boolean isResidualsShown() { - return residualsShown; - } + public void setResidualsShown(boolean residualsShown) { + this.residualsShown = residualsShown; + } - public void setResidualsShown(boolean residualsShown) { - this.residualsShown = residualsShown; - } + public boolean isZeroApproximationShown() { + return zeroApproximationShown; + } - public boolean isZeroApproximationShown() { - return zeroApproximationShown; - } + public void setZeroApproximationShown(boolean zeroApproximationShown) { + this.zeroApproximationShown = zeroApproximationShown; + } - public void setZeroApproximationShown(boolean zeroApproximationShown) { - this.zeroApproximationShown = zeroApproximationShown; - } + public ChartPanel getChartPanel() { + return chartPanel; + } - public ChartPanel getChartPanel() { - return chartPanel; - } + public XYPlot getChartPlot() { + return plot; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/MovableValueMarker.java b/src/main/java/pulse/ui/components/MovableValueMarker.java new file mode 100644 index 00000000..5dfb7408 --- /dev/null +++ b/src/main/java/pulse/ui/components/MovableValueMarker.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.ui.components; + +import java.awt.BasicStroke; +import static java.awt.BasicStroke.CAP_BUTT; +import static java.awt.BasicStroke.JOIN_MITER; +import static java.awt.Color.black; +import java.awt.Stroke; +import org.jfree.chart.plot.ValueMarker; + +/** + * + * @author Artem Lunev + */ +public class MovableValueMarker extends ValueMarker { + + private State state = State.IDLE; + + public final static Stroke IDLE_STROKE = new BasicStroke(1.5f, CAP_BUTT, JOIN_MITER, 5.0f, new float[]{10f}, 0.0f); + public final static Stroke SELECTED_STROKE = new BasicStroke(3.0f, CAP_BUTT, JOIN_MITER, 5.0f, new float[]{10f}, 0.0f); + + public MovableValueMarker(double value) { + super(value); + setPaint(black); + setStroke(IDLE_STROKE); + } + + public State getState() { + return state; + } + + public void setState(State state) { + if (this.state != state) { + //do only if state has changed + this.state = state; + super.setStroke(state == State.IDLE ? IDLE_STROKE : SELECTED_STROKE); + } + } + + public enum State { + IDLE, SELECTED, MOVING; + } + +} diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java new file mode 100644 index 00000000..47cacf11 --- /dev/null +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.ui.components; + +import java.awt.Color; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JFormattedTextField; +import javax.swing.text.NumberFormatter; +import pulse.input.Range; +import pulse.input.listeners.DataEvent; +import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.listeners.TaskSelectionEvent; +import pulse.ui.components.panels.ChartToolbar; + +/** + * Two JFormattedTextFields used to display the range of the currently + * selected task. + * @author Artem Lunev + */ +public final class RangeTextFields { + + private JFormattedTextField lowerLimitField; + private JFormattedTextField upperLimitField; + + /** + * Creates textfield objects, which may be accessed with getters from this instance. + * Additionally, binds listeners to all current and future tasks in order to observe + * and reflect upon the changes with the textfield. + */ + + public RangeTextFields() { + initTextFields(); + + var instance = TaskManager.getManagerInstance(); + + //for each new task created in the repo + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + + var newTask = instance.getTask(e.getId()); + //when the range of the selected data is changed and the task is the selected one, + //update the textfields values + updateTextfieldsFromTask(newTask); + + } + }); + + //when a new task is selected + instance.addSelectionListener((TaskSelectionEvent e) -> { + var task = instance.getSelectedTask(); + var segment = task.getExperimentalCurve().getRange().getSegment(); + //update the textfield values + lowerLimitField.setValue(segment.getMinimum()); + upperLimitField.setValue(segment.getMaximum()); + }); + + } + + /* + Creates a formatter for the textfields + */ + + private NumberFormatter initFormatter() { + var format = new DecimalFormat(); + format.setMinimumFractionDigits(1); + format.setMaximumFractionDigits(1); + format.setMinimumIntegerDigits(1); + format.setMaximumIntegerDigits(6); + format.setMultiplier(1000); //ms to seconds + format.setPositiveSuffix(" ms"); + format.setGroupingUsed(false); + + /* + * A custom formatter for the time range + */ + var formatter = new NumberFormatter(format); + formatter.setAllowsInvalid(false); + formatter.setOverwriteMode(true); + return formatter; + } + + /** + * Checks if the candidate value produced by the formatter is sensible, i.e. + * if it lies within the bounds defined in the Range class. + * @param jtf the textfield containing the candidate value as text + * @param upperBound whether the upper bound is checked ({@code false} if the lower bound is checked) + * @return {@code true} if the edit may proceed + */ + + private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { + Range range = TaskManager.getManagerInstance().getSelectedTask() + .getExperimentalCurve().getRange(); + + double candidateValue = 0.0; + try { + candidateValue = ((Number) jtf.getFormatter().stringToValue(jtf.getText())).doubleValue(); + } catch (ParseException ex) { + Logger.getLogger(ChartToolbar.class.getName()).log(Level.SEVERE, null, ex); + } + + boolean b = range.boundLimits(upperBound).contains(candidateValue); + + return range.boundLimits(upperBound).contains(candidateValue); + } + + /** + * Creates a formatter and initialised the textfields, setting up rules + * for edit validation. + */ + + private void initTextFields() { + var instance = TaskManager.getManagerInstance(); + + var formatter = initFormatter(); + + lowerLimitField = new JFormattedTextField(formatter) { + + @Override + public boolean isEditValid() { + return super.isEditValid() + ? RangeTextFields.isEditValid(this, false) + : false; + } + + @Override + public void commitEdit() throws ParseException { + if (isEditValid()) { + super.commitEdit(); + } + } + + }; + + upperLimitField = new JFormattedTextField(formatter) { + + @Override + public boolean isEditValid() { + return super.isEditValid() + ? RangeTextFields.isEditValid(this, true) + : false; + } + + @Override + public void commitEdit() throws ParseException { + if (isEditValid()) { + super.commitEdit(); + } + } + + }; + + var fl = new FocusListener() { + @Override + public void focusGained(FocusEvent arg0) { + arg0.getComponent().setForeground(Color.WHITE); + } + + @Override + public void focusLost(FocusEvent arg0) { + arg0.getComponent().setForeground(Color.lightGray); + } + + }; + + lowerLimitField.addFocusListener(fl); + upperLimitField.addFocusListener(fl); + + lowerLimitField.setColumns(10); + upperLimitField.setColumns(10); + + lowerLimitField.setForeground(Color.lightGray); + upperLimitField.setForeground(Color.lightGray); + + } + + private void updateTextfieldsFromTask(SearchTask newTask) { + //add data listeners in case when the range of the selected task is changed + newTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + if (TaskManager.getManagerInstance().getSelectedTask() == newTask) { + var segment = newTask.getExperimentalCurve().getRange().getSegment(); + lowerLimitField.setValue(segment.getMinimum()); + upperLimitField.setValue(segment.getMaximum()); + } + }); + } + + public JFormattedTextField getLowerLimitField() { + return lowerLimitField; + } + + public JFormattedTextField getUpperLimitField() { + return upperLimitField; + } + +} diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 26a0aa53..614d786b 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -10,40 +10,41 @@ import pulse.search.statistics.ResidualStatistic; public class ResidualsChart extends AuxPlotter { - - private int binCount; - - public ResidualsChart(String xLabel, String yLabel) { - super(xLabel, yLabel); - binCount = 32; - } - - @Override - public void createChart(String xLabel, String yLabel) { - setChart( ChartFactory.createHistogram("", xLabel, yLabel, null, VERTICAL, true, true, false) ); - } - - @Override - public void plot(ResidualStatistic stat) { - requireNonNull(stat); - - var pulseDataset = new HistogramDataset(); - pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); - - var residuals = stat.transformResiduals(); - - if(residuals.length > 0) - pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); - - getPlot().setDataset(0, pulseDataset); - } - - public int getBinCount() { - return binCount; - } - - public void setBinCount(int binCount) { - this.binCount = binCount; - } - -} \ No newline at end of file + + private int binCount; + + public ResidualsChart(String xLabel, String yLabel) { + super(xLabel, yLabel); + binCount = 32; + } + + @Override + public void createChart(String xLabel, String yLabel) { + setChart(ChartFactory.createHistogram("", xLabel, yLabel, null, VERTICAL, true, true, false)); + } + + @Override + public void plot(ResidualStatistic stat) { + requireNonNull(stat); + + var pulseDataset = new HistogramDataset(); + pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); + + var residuals = stat.transformResiduals(); + + if (residuals.length > 0) { + pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); + } + + getPlot().setDataset(0, pulseDataset); + } + + public int getBinCount() { + return binCount; + } + + public void setBinCount(int binCount) { + this.binCount = binCount; + } + +} diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java new file mode 100644 index 00000000..4a4d1ef6 --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.ui.components.listeners; + +import java.awt.Cursor; +import org.jfree.chart.ChartMouseEvent; +import org.jfree.chart.ChartMouseListener; +import pulse.ui.components.Chart; +import pulse.ui.components.MovableValueMarker; + +/** + * + * @author Artem Lunev + */ +public class MouseOnMarkerListener implements ChartMouseListener { + + private final MovableValueMarker marker; + private final Chart chart; + private final double margin; + + private final static Cursor CROSSHAIR = new Cursor(Cursor.CROSSHAIR_CURSOR); + private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); + + public MouseOnMarkerListener(Chart chart, MovableValueMarker marker, double margin) { + this.chart = chart; + this.marker = marker; + this.margin = margin; + } + + @Override + public void chartMouseClicked(ChartMouseEvent arg0) { + //blank + } + + @Override + public void chartMouseMoved(ChartMouseEvent arg0) { + double xCoord = chart.xCoord(arg0.getTrigger()); + highlightMarker(xCoord, marker); + } + + private void highlightMarker(double xCoord, MovableValueMarker marker) { + + if (xCoord > (marker.getValue() - margin) + & xCoord < (marker.getValue() + margin)) { + + marker.setState(MovableValueMarker.State.SELECTED); + chart.getChartPanel().setCursor(RESIZE); + + } else { + + marker.setState(MovableValueMarker.State.IDLE); + chart.getChartPanel().setCursor(CROSSHAIR); + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 00232b70..011d2f71 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -1,21 +1,11 @@ package pulse.ui.components.panels; -import static java.awt.Color.GRAY; -import static java.awt.Color.gray; -import static java.awt.Color.white; import static java.awt.GridBagConstraints.BOTH; -import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.String.format; -import static javax.swing.JOptionPane.ERROR_MESSAGE; -import static javax.swing.JOptionPane.YES_NO_OPTION; -import static javax.swing.JOptionPane.YES_OPTION; -import static javax.swing.JOptionPane.showConfirmDialog; import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; -import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; import static pulse.util.ImageUtils.loadIcon; @@ -23,239 +13,175 @@ import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; +import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JButton; -import javax.swing.JFormattedTextField; -import javax.swing.JTextField; +import javax.swing.JOptionPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; -import javax.swing.text.NumberFormatter; +import pulse.input.ExperimentalData; import pulse.input.Range; import pulse.tasks.TaskManager; +import pulse.ui.Messages; +import pulse.ui.components.RangeTextFields; import pulse.ui.components.ResidualsChart; import pulse.ui.components.listeners.PlotRequestListener; import pulse.ui.frames.HistogramFrame; @SuppressWarnings("serial") -public class ChartToolbar extends JToolBar { +public final class ChartToolbar extends JToolBar { - private final static int ICON_SIZE = 16; - private List listeners; - - public ChartToolbar() { - super(); - setFloatable(false); - listeners = new ArrayList<>(); - initComponents(); - } + private final static int ICON_SIZE = 16; + private List listeners; - public void initComponents() { - setLayout(new GridBagLayout()); + private RangeTextFields rtf; - var lowerLimitField = new JFormattedTextField(new NumberFormatter()); - var upperLimitField = new JFormattedTextField(new NumberFormatter()); + public ChartToolbar() { + super(); + setFloatable(false); + listeners = new ArrayList<>(); + rtf = new RangeTextFields(); + initComponents(); + } - var limitRangeBtn = new JButton(); - var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE, Color.white)); - var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE, Color.white)); - var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE, Color.white)); - pdfBtn.setToolTipText("Residuals Histogram"); - - var instance = TaskManager.getManagerInstance(); - - var residualsChart = new ResidualsChart("Residual value", "Frequency"); - var chFrame = new HistogramFrame(residualsChart, 450, 450); - - pdfBtn.addActionListener(e -> { - - var task = instance.getSelectedTask(); - - if(task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { - - chFrame.setLocationRelativeTo(null); - chFrame.setVisible(true); - chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); - - } - - } ); - - var gbc = new GridBagConstraints(); - gbc.fill = BOTH; - gbc.weightx = 0.25; + public final void initComponents() { + setLayout(new GridBagLayout()); - lowerLimitField.setValue(0.0); - - var ghostText1 = "Lower bound"; - lowerLimitField.setText(ghostText1); - - var ghostText2 = "Upper bound"; + var limitRangeBtn = new JButton(); + var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE, Color.white)); + var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE, Color.white)); + var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE, Color.white)); + pdfBtn.setToolTipText("Residuals Histogram"); - add(lowerLimitField, gbc); - - upperLimitField.setValue(1.0); - upperLimitField.setText(ghostText2); + var residualsChart = new ResidualsChart("Residual value", "Frequency"); + var chFrame = new HistogramFrame(residualsChart, 450, 450); - add(upperLimitField, gbc); - - limitRangeBtn.setText("Limit Range To"); + pdfBtn.addActionListener(e -> { - lowerLimitField.setForeground(GRAY); - upperLimitField.setForeground(GRAY); - - var ftfFocusListener = new FocusListener() { - - @Override - public void focusGained(FocusEvent e) { - var src = (JTextField) e.getSource(); - if (src.getText().length() > 0) - src.setForeground(white); - } - - @Override - public void focusLost(FocusEvent e) { - var src = (JFormattedTextField) e.getSource(); - if (src.getValue() == null) { - src.setText(ghostText1); - src.setForeground(gray); - } - } - - }; - - instance.addSelectionListener(event -> { - var t = instance.getSelectedTask(); - var expCurve = t.getExperimentalCurve(); - - lowerLimitField.setValue(expCurve.getRange().getSegment().getMinimum()); - upperLimitField.setValue(expCurve.getRange().getSegment().getMaximum()); - - }); - - instance.addTaskRepositoryListener(e -> { - - if (e.getState() == TASK_FINISHED) { - - var t = instance.getSelectedTask(); - - if (e.getId().equals(t.getIdentifier())) { - lowerLimitField.setValue(t.getExperimentalCurve().getRange().getSegment().getMinimum()); - upperLimitField.setValue(t.getExperimentalCurve().getRange().getSegment().getMaximum()); - notifyPlot(); - } - - } - - }); - - lowerLimitField.addFocusListener(ftfFocusListener); - upperLimitField.addFocusListener(ftfFocusListener); - - limitRangeBtn.addActionListener(e -> { - if ((!lowerLimitField.isEditValid()) || (!upperLimitField.isEditValid())) { // The text is invalid. - if (userSaysRevert(lowerLimitField)) { // reverted - lowerLimitField.postActionEvent(); // inform the editor - } - } - - else { - var lower = ((Number) lowerLimitField.getValue()).doubleValue(); - var upper = ((Number) upperLimitField.getValue()).doubleValue(); - validateRange(lower, upper); - notifyPlot(); - } - }); - - gbc.weightx = 0.25; - add(limitRangeBtn, gbc); - - adiabaticSolutionBtn.setToolTipText("Sanity check (original adiabatic solution)"); - - adiabaticSolutionBtn.addActionListener(e -> { - getChart().setZeroApproximationShown(adiabaticSolutionBtn.isSelected()); - notifyPlot(); - }); + var task = TaskManager.getManagerInstance().getSelectedTask(); - gbc.weightx = 0.08; - add(adiabaticSolutionBtn, gbc); + if (task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { - residualsBtn.setToolTipText("Plot residuals"); - residualsBtn.setSelected(true); + chFrame.setLocationRelativeTo(null); + chFrame.setVisible(true); + chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); + + } - residualsBtn.addActionListener(e -> { - getChart().setResidualsShown(residualsBtn.isSelected()); - notifyPlot(); - }); + }); - add(residualsBtn, gbc); - add(pdfBtn, gbc); - } - - public void addPlotRequestListener(PlotRequestListener plotRequestListener) { - listeners.add(plotRequestListener); - } - - private void notifyPlot() { - listeners.stream().forEach(l -> l.onPlotRequest()); - } - - private static boolean userSaysRevert(JFormattedTextField ftf) { - getDefaultToolkit().beep(); - ftf.selectAll(); - Object[] options = { getString("NumberEditor.EditText"), getString("NumberEditor.RevertText") }; - var answer = showOptionDialog(getWindowAncestor(ftf), - "Time domain should be consistent with the experimental data range.
" - + getString("NumberEditor.MessageLine1") + getString("NumberEditor.MessageLine2") + "", - getString("NumberEditor.InvalidText"), YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - ftf.setValue(ftf.getValue()); - return true; - } - return false; - } - - private void validateRange(double a, double b) { - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task == null) - return; - - var expCurve = task.getExperimentalCurve(); - - if (expCurve == null) - return; - - var sb = new StringBuilder(); - - sb.append("

"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage1")); - sb.append("


"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage2")); - sb.append(format("%3.6f", expCurve.getEffectiveStartTime())); - sb.append(" to "); - sb.append(format("%3.6f", expCurve.getEffectiveEndTime())); - sb.append("

"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage3")); - sb.append(format("%3.6f", a) + " to " + format("%3.6f", b)); - sb.append(""); - - var dialogResult = showConfirmDialog(getWindowAncestor(this), sb.toString(), "Confirm chocie", YES_NO_OPTION); - - if (dialogResult == YES_OPTION) { - if(expCurve.getRange() == null) - expCurve.setRange(new Range(a, b)); - else { - expCurve.getRange().setLowerBound(derive(LOWER_BOUND, a)); - expCurve.getRange().setUpperBound(derive(UPPER_BOUND, b)); - } - } - - } - -} \ No newline at end of file + var gbc = new GridBagConstraints(); + gbc.fill = BOTH; + gbc.weightx = 0.25; + + add(rtf.getLowerLimitField(), gbc); + add(rtf.getUpperLimitField(), gbc); + + limitRangeBtn.setText("Set Range"); + limitRangeBtn.addActionListener(e -> { + var lower = ((Number) rtf.getLowerLimitField().getValue()).doubleValue(); + var upper = ((Number) rtf.getUpperLimitField().getValue()).doubleValue(); + validateRange(lower, upper); + notifyPlot(); + }); + + gbc.weightx = 0.25; + add(limitRangeBtn, gbc); + + adiabaticSolutionBtn.setToolTipText("Sanity check (original adiabatic solution)"); + + adiabaticSolutionBtn.addActionListener(e -> { + getChart().setZeroApproximationShown(adiabaticSolutionBtn.isSelected()); + notifyPlot(); + }); + + gbc.weightx = 0.08; + add(adiabaticSolutionBtn, gbc); + + residualsBtn.setToolTipText("Plot residuals"); + residualsBtn.setSelected(true); + + residualsBtn.addActionListener(e -> { + getChart().setResidualsShown(residualsBtn.isSelected()); + notifyPlot(); + }); + + add(residualsBtn, gbc); + add(pdfBtn, gbc); + } + + public void addPlotRequestListener(PlotRequestListener plotRequestListener) { + listeners.add(plotRequestListener); + } + + private void notifyPlot() { + listeners.stream().forEach(l -> l.onPlotRequest()); + } + + private void validateRange(double a, double b) { + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task == null) { + return; + } + + var expCurve = task.getExperimentalCurve(); + + if (expCurve == null) { + return; + } + + var sb = new StringBuilder(); + + sb.append(Messages.getString("TextWrap.0")) + .append(getString("RangeSelectionFrame.ConfirmationMessage1")) + .append("
") + .append(getString("RangeSelectionFrame.ConfirmationMessage2")); + try { + sb.append(rtf.getLowerLimitField().getFormatter().valueToString(expCurve.getEffectiveStartTime())) + .append(" to ") + .append(rtf.getUpperLimitField().getFormatter().valueToString(expCurve.getEffectiveEndTime()) + ); + } catch (ParseException ex) { + Logger.getLogger(ChartToolbar.class.getName()).log(Level.SEVERE, null, ex); + } + sb.append("
").append(getString("RangeSelectionFrame.ConfirmationMessage3")) + .append(rtf.getLowerLimitField().getText()) + .append(" to ") + .append(rtf.getUpperLimitField().getText()) + .append(Messages.getString("TextWrap.1")); + + String[] options = new String[]{"Apply to all", "Change current", "Cancel"}; + + var dialogResult = showOptionDialog(getWindowAncestor(this), + sb.toString(), "Confirm chocie", JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, null, options, options[2]); + + if (dialogResult == JOptionPane.NO_OPTION) { + //just set the range for this particular dataset + setRange(expCurve, a, b); + } else if (dialogResult == JOptionPane.YES_OPTION) { + // set range for all available experimental datasets + TaskManager.getManagerInstance().getTaskList() + .stream().forEach((aTask) + -> setRange(aTask.getExperimentalCurve(), a, b) + ); + } + + } + + private void setRange(ExperimentalData expCurve, double a, double b) { + if (expCurve.getRange() == null) { + expCurve.setRange(new Range(a, b)); + } else { + expCurve.getRange().setLowerBound(derive(LOWER_BOUND, a)); + expCurve.getRange().setUpperBound(derive(UPPER_BOUND, b)); + } + } + +} From db8db1243549fe62f97c7d7d1d412cdfc8e047a2 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 14:40:03 +0100 Subject: [PATCH 083/116] Data loading and Problem/Scheme Changes - Concurrency Loading numerical pulse information is now handled through a combination of a Cached Thread Pool, where requests update properties based on the loaded information are submitted, and a Single Thread Executor, which manages the read requests. This avoids deadlocks. ProblemStatementFrame: Some re-shuffling of code to reduce complexity -> new methods changeProblems(), changeSchemes(). Three Cached Thread Pools now manage changing problems, schemes, and updating properties -- this greatly improves the performance of processing many tasks (hundreds) and ensures that processing is done in the correct order (that also avoids ConcurrentModificationException!). To ensure correct operation, each time when a heap of tasks is required to be processed, the pools use the invokeAll() method, preserving the operation lock until processing is complete. The only exception here is the property-update pool, which simply accepts submitted callables and doesn't use invokeAll(). --- .../java/pulse/ui/components/DataLoader.java | 379 ++++++------ .../ui/frames/ProblemStatementFrame.java | 547 ++++++++++-------- 2 files changed, 494 insertions(+), 432 deletions(-) diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index 0261cf47..ec13f1bb 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -10,6 +10,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executors; import javax.swing.JFileChooser; import javax.swing.JOptionPane; @@ -33,194 +34,198 @@ * {@code ProgressDialog}. * */ - public class DataLoader { - private static File dir; - private static ProgressDialog progressFrame = new ProgressDialog(); - - static { - TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) - progressFrame.incrementProgress(); - }); - progressFrame.setLocationRelativeTo(null); - progressFrame.setAlwaysOnTop(true); - } - - private DataLoader() { - // intentionally blank - } - - /** - * Initiates a user dialog to load experimental time-temperature profiles. - * Multiple selection is possible. When the user finalises selection, the - * {@code TaskManager} will start generating tasks using the files selected by - * the user as input. The tracker progress bar is reset and made visible. - */ - - public static void loadDataDialog() { - var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), - ReaderManager.getCurveExtensions()); - - var instance = TaskManager.getManagerInstance(); - - if (files != null) { - progressFrame.trackProgress(files.size()); - instance.generateTasks(files); - } - - } - - /** - * Asks the user to select a single file containing the metadata, with the - * extension given by the {@code MetaFilePopulator} class. If a valid selection - * is made and the task list is not empty, proceeds to populating each task's - * metadata object using the information contained in the selected file. If the - * task has a problem assigned to it, sets the parameters of that problem to - * match the loaded {@code Metadata}. Throughout the process, progress is - * monitored in a separate dialog with a {@code JProgressBar}. Upon finishing, - * the data range will be checked to determine if truncation is needed. - * - * @see truncateDataDialog - */ - - public static void loadMetadataDialog() { - var handler = MetaFilePopulator.getInstance(); - var file = userInputSingle(Messages.getString("TaskControlFrame.ExtensionDescriptor"), - handler.getSupportedExtension()); - - var instance = TaskManager.getManagerInstance(); - - if (instance.numberOfTasks() < 1 || file == null) - return; // invalid input received, do nothing - - progressFrame.trackProgress(instance.numberOfTasks() + 1); - - // attempt to fill metadata and problem - - for (SearchTask task : instance.getTaskList()) { - var data = task.getExperimentalCurve(); + private static File dir; + private static ProgressDialog progressFrame = new ProgressDialog(); + + static { + TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + progressFrame.incrementProgress(); + } + }); + progressFrame.setLocationRelativeTo(null); + progressFrame.setAlwaysOnTop(true); + } + + private DataLoader() { + // intentionally blank + } + + /** + * Initiates a user dialog to load experimental time-temperature profiles. + * Multiple selection is possible. When the user finalises selection, the + * {@code TaskManager} will start generating tasks using the files selected + * by the user as input. The tracker progress bar is reset and made visible. + */ + public static void loadDataDialog() { + var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + ReaderManager.getCurveExtensions()); + + var instance = TaskManager.getManagerInstance(); + + if (files != null) { + progressFrame.trackProgress(files.size()); + instance.generateTasks(files); + } + + } + + /** + * Asks the user to select a single file containing the metadata, with the + * extension given by the {@code MetaFilePopulator} class. If a valid + * selection is made and the task list is not empty, proceeds to populating + * each task's metadata object using the information contained in the + * selected file. If the task has a problem assigned to it, sets the + * parameters of that problem to match the loaded {@code Metadata}. + * Throughout the process, progress is monitored in a separate dialog with a + * {@code JProgressBar}. Upon finishing, the data range will be checked to + * determine if truncation is needed. + * + * @see truncateDataDialog + */ + public static void loadMetadataDialog() { + var handler = MetaFilePopulator.getInstance(); + var file = userInputSingle(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + handler.getSupportedExtension()); + + var instance = TaskManager.getManagerInstance(); + + if (instance.numberOfTasks() < 1 || file == null) { + return; // invalid input received, do nothing + } + progressFrame.trackProgress(instance.numberOfTasks() + 1); + + // attempt to fill metadata and problem + for (SearchTask task : instance.getTaskList()) { + var data = task.getExperimentalCurve(); + + try { + handler.populate(file, data.getMetadata()); + } catch (IOException e) { + JOptionPane.showMessageDialog(progressFrame, Messages.getString("TaskControlFrame.LoadError"), + Messages.getString("TaskControlFrame.IOError"), JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + } + + var p = task.getCurrentCalculation().getProblem(); + if (p != null) { + p.retrieveData(data); + } + progressFrame.incrementProgress(); + + } + + progressFrame.incrementProgress(); + + // select first of the generated task + instance.selectFirstTask(); + + } + + public static void loadPulseDialog() { + var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + ReaderManager.getPulseExtensions()); + + if (files != null) { + + var manager = TaskManager.getManagerInstance(); + + progressFrame.trackProgress(files.size()); + + Runnable loader = () -> { + + final var pool = Executors.newCachedThreadPool(); + + files.stream().map(f -> read(pulseReaders(), f)) + .filter(pulseData -> (pulseData != null)) + .forEach(pulseData -> { + + var task = manager.getTask(pulseData.getExternalID()); + + if (task != null) { + pool.submit(() -> { + var metadata = task.getExperimentalCurve().getMetadata(); + metadata.setPulseData(pulseData); + metadata.getPulseDescriptor() + .setSelectedDescriptor( + NumericPulse.class.getSimpleName()); + }); + } + + }); + + }; + + Executors.newSingleThreadExecutor().submit(loader); + + } + } + + /** + * Uses the {@code ReaderManager} to create an {@code InterpolationDataset} + * from data stored in {@code f} and updates the associated properties of + * each task. + * + * @param f a {@code File} containing a property specified by the + * {@code type} + * @param type the type of the loaded data + * @throws IOException if file cannot be read + * @see pulse.tasks.TaskManager.evaluate() + */ + public static void load(StandartType type, File f) throws IOException { + Objects.requireNonNull(f); + InterpolationDataset.setDataset(read(datasetReaders(), f), type); + TaskManager.getManagerInstance().evaluate(); + } + + private static List userInput(String descriptor, List extensions) { + JFileChooser fileChooser = new JFileChooser(); + + fileChooser.setCurrentDirectory(directory()); + fileChooser.setMultiSelectionEnabled(true); + + String[] extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); + + boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + + return approve ? Arrays.asList(fileChooser.getSelectedFiles()) : null; + } + + private static File userInputSingle(String descriptor, List extensions) { + JFileChooser fileChooser = new JFileChooser(); + + fileChooser.setCurrentDirectory(directory()); + fileChooser.setMultiSelectionEnabled(false); + + String[] extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); + + boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + + return approve ? fileChooser.getSelectedFile() : null; + } + + private static File userInputSingle(String descriptor, String... extensions) { + return userInputSingle(descriptor, Arrays.asList(extensions)); + } + + private static File directory() { + if (dir != null) { + return dir; + } else try { - handler.populate(file, data.getMetadata()); - } catch (IOException e) { - JOptionPane.showMessageDialog(progressFrame, Messages.getString("TaskControlFrame.LoadError"), - Messages.getString("TaskControlFrame.IOError"), JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - } - - var p = task.getCurrentCalculation().getProblem(); - if (p != null) - p.retrieveData(data); - progressFrame.incrementProgress(); - - } - - progressFrame.incrementProgress(); - - // select first of the generated task - instance.selectFirstTask(); - - } - - public static void loadPulseDialog() { - var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), - ReaderManager.getPulseExtensions()); - - if (files != null) { - - var manager = TaskManager.getManagerInstance(); - - progressFrame.trackProgress(files.size()); - - //TODO replace with pool loading - for(var f : files) { - - NumericPulseData pulseData = read(pulseReaders(), f); - - if(pulseData != null) { - - var task = manager.getTask(pulseData.getExternalID()); - - if(task != null) { - - var metadata = task.getExperimentalCurve().getMetadata(); - metadata.setPulseData(pulseData); - metadata.getPulseDescriptor().setSelectedDescriptor(NumericPulse.class.getSimpleName()); - - } - - } - - } - - } - - } - - /** - * Uses the {@code ReaderManager} to create an {@code InterpolationDataset} from - * data stored in {@code f} and updates the associated properties of each task. - * - * @param f a {@code File} containing a property specified by the - * {@code type} - * @param type the type of the loaded data - * @throws IOException if file cannot be read - * @see pulse.tasks.TaskManager.evaluate() - */ - - public static void load(StandartType type, File f) throws IOException { - Objects.requireNonNull(f); - InterpolationDataset.setDataset(read(datasetReaders(), f), type); - TaskManager.getManagerInstance().evaluate(); - } - - private static List userInput(String descriptor, List extensions) { - JFileChooser fileChooser = new JFileChooser(); - - fileChooser.setCurrentDirectory(directory()); - fileChooser.setMultiSelectionEnabled(true); - - String[] extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); - - boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - - return approve ? Arrays.asList(fileChooser.getSelectedFiles()) : null; - } - - private static File userInputSingle(String descriptor, List extensions) { - JFileChooser fileChooser = new JFileChooser(); - - fileChooser.setCurrentDirectory(directory()); - fileChooser.setMultiSelectionEnabled(false); - - String[] extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); - - boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - - return approve ? fileChooser.getSelectedFile() : null; - } - - private static File userInputSingle(String descriptor, String... extensions) { - return userInputSingle(descriptor, Arrays.asList(extensions)); - } - - private static File directory() { - if (dir != null) - return dir; - else - try { - return new File(DataLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - } catch (URISyntaxException e) { - System.err.println("Cannot determine current working directory."); - e.printStackTrace(); - } - return null; - } - -} \ No newline at end of file + return new File(DataLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + System.err.println("Cannot determine current working directory."); + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 97bc86a5..41d5bbf5 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -15,6 +15,12 @@ import java.awt.BorderLayout; import java.awt.GridLayout; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.swing.DefaultListModel; import javax.swing.JInternalFrame; @@ -30,9 +36,11 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.statements.Problem; import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; +import pulse.ui.components.listeners.ProblemSelectionEvent; import pulse.ui.components.panels.ProblemToolbar; import pulse.ui.components.panels.SettingsToolBar; import pulse.ui.frames.TaskControlFrame.Mode; @@ -40,300 +48,349 @@ @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { - private PropertyHolderTable problemTable; - private PropertyHolderTable schemeTable; - private JList schemeSelectionList; - private ProblemTree problemTree; - private ProblemToolbar toolbar; + private PropertyHolderTable problemTable; + private PropertyHolderTable schemeTable; + private JList schemeSelectionList; + private ProblemTree problemTree; + private ProblemToolbar toolbar; - private final static List knownProblems = instancesOf(Problem.class); + private final static List knownProblems = instancesOf(Problem.class); - /** - * Create the frame. - */ - public ProblemStatementFrame() { - setResizable(true); - setClosable(true); - setMaximizable(true); - setDefaultCloseOperation(HIDE_ON_CLOSE); + private ExecutorService problemListExecutor; + private ExecutorService schemeListExecutor; + private ExecutorService propertyExecutor; - setTitle(getString("ProblemStatementFrame.Title")); //$NON-NLS-1$ + /** + * Create the frame. + */ + public ProblemStatementFrame() { + setResizable(true); + setClosable(true); + setMaximizable(true); + setDefaultCloseOperation(HIDE_ON_CLOSE); - setBounds(100, 100, WIDTH, HEIGHT); + setTitle(getString("ProblemStatementFrame.Title")); //$NON-NLS-1$ - getContentPane().setLayout(new BorderLayout()); + setBounds(100, 100, WIDTH, HEIGHT); - /* - * Create a 2x2 grid for lists and tables - */ - - var contentPane = new JPanel(); - var layout = new GridLayout(2, 2); - layout.setHgap(5); - layout.setVgap(5); - contentPane.setLayout(layout); + getContentPane().setLayout(new BorderLayout()); - /* + /* + * Create a 2x2 grid for lists and tables + */ + var contentPane = new JPanel(); + var layout = new GridLayout(2, 2); + layout.setHgap(5); + layout.setVgap(5); + contentPane.setLayout(layout); + + /* * Problem selection list and scroller - */ - - problemTree = new ProblemTree(knownProblems); - contentPane.add(new JScrollPane(problemTree)); - - var instance = getManagerInstance(); - - problemTree.addProblemSelectionListener(e -> { - - var newlySelectedProblem = e.getProblem(); - - if (newlySelectedProblem == null) { - - ((DefaultTableModel) problemTable.getModel()).setRowCount(0); - - } + */ + problemTree = new ProblemTree(knownProblems); + contentPane.add(new JScrollPane(problemTree)); + + var instance = getManagerInstance(); + + problemListExecutor = Executors.newCachedThreadPool(); + schemeListExecutor = Executors.newCachedThreadPool(); + propertyExecutor = Executors.newCachedThreadPool(); + + problemTree.addProblemSelectionListener((ProblemSelectionEvent e) + -> { + if (e.getProblem() == null) { + ((DefaultTableModel) problemTable.getModel()).setRowCount(0); + } else { + changeProblems(e.getProblem(), e.getSource()); + } + }); + + /* + * Scheme list and scroller + */ + schemeSelectionList = new JList(); + schemeSelectionList.setSelectionMode(SINGLE_SELECTION); + schemeSelectionList.setModel(new DefaultListModel()); - else { + schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { + if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) { + return; + } - var selectedTask = instance.getSelectedTask(); + var selectedValue = schemeSelectionList.getSelectedValue(); - if (e.getSource() != instance) { - if (instance.isSingleStatement()) - instance.getTaskList().stream().forEach(t -> changeProblem(t, newlySelectedProblem)); - else - changeProblem(selectedTask, newlySelectedProblem); - } + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + } + else { + changeSchemes(selectedValue); + } - problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); - // after problem is selected for this task, show available difference schemes - var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); - defaultModel.clear(); - var schemes = newlySelectedProblem.availableSolutions(); - schemes.forEach(s -> defaultModel.addElement(s)); - selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); - schemeSelectionList.setToolTipText(null); + }); - } + schemeSelectionList.setToolTipText(getString("ProblemStatementFrame.PleaseSelect")); //$NON-NLS-1$ - }); + var schemeScroller = new JScrollPane(schemeSelectionList); + contentPane.add(schemeScroller); - /* - * Scheme list and scroller - */ - - schemeSelectionList = new JList(); - schemeSelectionList.setSelectionMode(SINGLE_SELECTION); - schemeSelectionList.setModel(new DefaultListModel()); - - schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { - if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) - return; - - var selectedValue = schemeSelectionList.getSelectedValue(); - - if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - return; - } - - var selectedTask = instance.getSelectedTask(); - var newScheme = selectedValue; - if (instance.isSingleStatement()) { - instance.getTaskList().stream().forEach(t -> changeScheme(t, newScheme)); - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); - if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); - } - }); - - schemeSelectionList.setToolTipText(getString("ProblemStatementFrame.PleaseSelect")); //$NON-NLS-1$ - - var schemeScroller = new JScrollPane(schemeSelectionList); - contentPane.add(schemeScroller); - - /* + /* * Problem details scroller - */ - - problemTable = new PropertyHolderTable(null); - var problemDetailsScroller = new JScrollPane(problemTable); - contentPane.add(problemDetailsScroller); + */ + problemTable = new PropertyHolderTable(null); + var problemDetailsScroller = new JScrollPane(problemTable); + contentPane.add(problemDetailsScroller); - /* + /* * Scheme details table and scroller - */ + */ + schemeTable = new PropertyHolderTable(null); + var schemeDetailsScroller = new JScrollPane(schemeTable); + contentPane.add(schemeDetailsScroller); - schemeTable = new PropertyHolderTable(null); - var schemeDetailsScroller = new JScrollPane(schemeTable); - contentPane.add(schemeDetailsScroller); + toolbar = new ProblemToolbar(); - toolbar = new ProblemToolbar(); + problemTree.setSelectionModel(new DefaultTreeSelectionModel() { - problemTree.setSelectionModel(new DefaultTreeSelectionModel() { + @Override + public void setSelectionPath(TreePath path) { + var object = (DefaultMutableTreeNode) path.getLastPathComponent(); - @Override - public void setSelectionPath(TreePath path) { - var object = (DefaultMutableTreeNode) path.getLastPathComponent(); + if (!(object.getUserObject() instanceof Problem)) { + super.setSelectionPath(path); + } else { - if (!(object.getUserObject() instanceof Problem)) - super.setSelectionPath(path); + var problem = (Problem) object.getUserObject(); + var enabledFlag = problem.isEnabled(); - else { + if (enabledFlag) { + super.setSelectionPath(path); + toolbar.highlightButtons(!problem.isReady()); + } else { + showMessageDialog(null, getString("problem.notsupportedmessage"), + getString("problem.notsupportedtitle"), WARNING_MESSAGE); + path = null; + } - var problem = (Problem) object.getUserObject(); - var enabledFlag = problem.isEnabled(); + } - if (enabledFlag) { - super.setSelectionPath(path); - toolbar.highlightButtons(!problem.isReady()); - } else { - showMessageDialog(null, getString("problem.notsupportedmessage"), - getString("problem.notsupportedtitle"), WARNING_MESSAGE); - path = null; - } + } - } + }); - } - - }); - - /* + /* * - */ + */ + getContentPane().add(new SettingsToolBar(problemTable, schemeTable), NORTH); + getContentPane().add(contentPane, CENTER); + getContentPane().add(toolbar, SOUTH); - getContentPane().add(new SettingsToolBar(problemTable, schemeTable), NORTH); - getContentPane().add(contentPane, CENTER); - getContentPane().add(toolbar, SOUTH); - - /* + /* * listeners - */ - - instance.addSelectionListener((TaskSelectionEvent e) -> update(instance.getSelectedTask())); - // TODO - - getManagerInstance().addHierarchyListener(event -> { - if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) - instance.getTaskList().stream().map(t -> t.getCurrentCalculation().getProblem()).filter(p -> p != null) - .forEach(pp -> pp.updateProperty(event, event.getProperty())); - - }); - - } - - public void update() { - update(getManagerInstance().getSelectedTask()); - } - - private void update(SearchTask selectedTask) { - - var calc = selectedTask.getCurrentCalculation(); - var selectedProblem = selectedTask == null ? null : calc.getProblem(); - var selectedScheme = selectedTask == null ? null : calc.getScheme(); - - // problem - - if (selectedProblem == null) - problemTree.clearSelection(); - else - problemTree.setSelectedProblem(selectedProblem); - - // scheme - - if (selectedScheme == null) - schemeSelectionList.clearSelection(); - else { - setSelectedElement(schemeSelectionList, selectedScheme); - schemeTable.setPropertyHolder(selectedScheme); - } - - } - - private void changeProblem(SearchTask task, Problem newProblem) { - var data = task.getExperimentalCurve(); - var calc = task.getCurrentCalculation(); - var oldProblem = calc.getProblem(); // stores previous information - var np = newProblem.copy(); - - if (oldProblem != null) { - np.initProperties(oldProblem.getProperties().copy()); - np.getPulse().initFrom(oldProblem.getPulse()); - } - - calc.setProblem(np, data); // copies information from old problem to new problem type - - task.checkProblems(true); - toolbar.highlightButtons(!np.isReady()); - - } - - private static void selectDefaultScheme(JList list, Problem p) { - var defaultSchemeClass = p.defaultScheme(); + */ + instance.addSelectionListener((TaskSelectionEvent e) -> update(instance.getSelectedTask())); + + getManagerInstance().addHierarchyListener(event -> { + if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) { + + //for all tasks + instance.getTaskList().stream(). + //select the problem statement of the current calculation + map(t -> t.getCurrentCalculation().getProblem()) + //that is non-null + .filter(problem -> problem != null) + //for each problem, update its properties in a separete thread + .forEach(p -> propertyExecutor.submit(() + -> p.updateProperty(event, event.getProperty()) + ) + ); + + } + + }); + + } + + public void update() { + update(getManagerInstance().getSelectedTask()); + } + + private void update(SearchTask selectedTask) { + + if(selectedTask == null) + return; + + var calc = selectedTask.getCurrentCalculation(); + var selectedProblem = selectedTask == null ? null : calc.getProblem(); + var selectedScheme = selectedTask == null ? null : calc.getScheme(); + + // problem + if (selectedProblem == null) { + problemTree.clearSelection(); + } else { + problemTree.setSelectedProblem(selectedProblem); + } + + // scheme + if (selectedScheme == null) { + schemeSelectionList.clearSelection(); + } else { + setSelectedElement(schemeSelectionList, selectedScheme); + schemeTable.setPropertyHolder(selectedScheme); + } + + } + + private void changeSchemes(DifferenceScheme newScheme) { + var instance = TaskManager.getManagerInstance(); + var selectedTask = instance.getSelectedTask(); + if (instance.isSingleStatement()) { + + var callableList = instance.getTaskList().stream().map(t -> new Callable() { + @Override + public DifferenceScheme call() throws Exception { + changeScheme(t, newScheme); + return t.getCurrentCalculation().getScheme(); + } + + }).collect(Collectors.toList()); + + try { + schemeListExecutor.invokeAll(callableList); + } catch (InterruptedException ex) { + Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + } + + } else { + changeScheme(selectedTask, newScheme); + } + schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); + if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); + } + } - var model = list.getModel(); - DifferenceScheme element = null; + private void changeProblems(Problem newlySelectedProblem, Object source) { + + var instance = TaskManager.getManagerInstance(); + var selectedTask = instance.getSelectedTask(); + + if (source != instance) { + if (instance.isSingleStatement()) { + + var callableList = instance.getTaskList().stream().map(t -> new Callable() { + @Override + public Problem call() throws Exception { + changeProblem(t, newlySelectedProblem); + return t.getCurrentCalculation().getProblem(); + } + + }).collect(Collectors.toList()); + try { + problemListExecutor.invokeAll(callableList); + } catch (InterruptedException ex) { + Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + } + + } else { + changeProblem(selectedTask, newlySelectedProblem); + } + + } + + problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); + // after problem is selected for this task, show available difference schemes + var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + schemes.forEach(s -> defaultModel.addElement(s)); + selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); + schemeSelectionList.setToolTipText(null); - for (int i = 0, size = model.getSize(); i < size; i++) { - element = model.getElementAt(i); + Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - if (defaultSchemeClass.isAssignableFrom(element.getClass())) { - list.setSelectedValue(element, true); - break; - } - } + } + + private void changeProblem(SearchTask task, Problem newProblem) { + var data = task.getExperimentalCurve(); + var calc = task.getCurrentCalculation(); + var oldProblem = calc.getProblem(); // stores previous information + var np = newProblem.copy(); + + if (oldProblem != null) { + np.initProperties(oldProblem.getProperties().copy()); + np.getPulse().initFrom(oldProblem.getPulse()); + } + + calc.setProblem(np, data); // copies information from old problem to new problem type + + task.checkProblems(true); + toolbar.highlightButtons(!np.isReady()); + + } - } + private static void selectDefaultScheme(JList list, Problem p) { + var defaultSchemeClass = p.defaultScheme(); + + var model = list.getModel(); + DifferenceScheme element = null; - private void changeScheme(SearchTask task, DifferenceScheme newScheme) { + for (int i = 0, size = model.getSize(); i < size; i++) { + element = model.getElementAt(i); - // TODO + if (defaultSchemeClass.isAssignableFrom(element.getClass())) { + list.setSelectedValue(element, true); + break; + } + } - var calc = task.getCurrentCalculation(); - var data = task.getExperimentalCurve(); + } - if (calc.getScheme() == null) - calc.setScheme(newScheme.copy(), data); + private void changeScheme(SearchTask task, DifferenceScheme newScheme) { - else { + // TODO + var calc = task.getCurrentCalculation(); + var data = task.getExperimentalCurve(); - var oldScheme = calc.getScheme().copy(); // stores previous information - calc.setScheme(newScheme.copy(), data); // assigns new problem type + if (calc.getScheme() == null) { + calc.setScheme(newScheme.copy(), data); + } else { - if (newScheme.getClass().getSimpleName().equals(oldScheme.getClass().getSimpleName())) - calc.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type + var oldScheme = calc.getScheme().copy(); // stores previous information + calc.setScheme(newScheme.copy(), data); // assigns new problem type - oldScheme = null; // deletes reference to old problem + if (newScheme.getClass().getSimpleName().equals(oldScheme.getClass().getSimpleName())) { + calc.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type + } + oldScheme = null; // deletes reference to old problem - } + } - task.checkProblems(true); + task.checkProblems(true); - } + } - private void setSelectedElement(JList list, Object o) { - if (o == null) { - list.clearSelection(); - return; - } + private void setSelectedElement(JList list, Object o) { + if (o == null) { + list.clearSelection(); + return; + } - var size = list.getModel().getSize(); - Object fromList = null; - var found = false; + var size = list.getModel().getSize(); + Object fromList = null; + var found = false; - for (var i = 0; i < size; i++) { - fromList = list.getModel().getElementAt(i); - if (fromList.toString().equals(o.toString())) { - list.setSelectedIndex(i); - found = true; - } - } + for (var i = 0; i < size; i++) { + fromList = list.getModel().getElementAt(i); + if (fromList.toString().equals(o.toString())) { + list.setSelectedIndex(i); + found = true; + } + } - if (!found) - list.clearSelection(); + if (!found) { + list.clearSelection(); + } - } + } -} \ No newline at end of file +} From 93b03708d028d8e2f5f8617c0d9f3d7bb3ad0793 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 14:50:26 +0100 Subject: [PATCH 084/116] GUI - optimisation parameters selection and result format change dialog Both the result format dialog and the optimisation parameter selection in the SearchOptionsFrame now use the same DoubleTablePanel GUI, where a left table holds all available properties (this is encapsulated in ParameterTableModel, which calls ActiveFlags.availableProperties()) and the right table holds currently selected output (in case of the result format) or the list of optimised parameters (in case of SearchOptionsFrame) -- managed via a SelectedKeysModel. --- .../controllers/KeywordListRenderer.java | 34 -- .../models/ActiveFlagsListModel.java | 92 ----- .../components/models/ParameterListModel.java | 56 --- .../models/ParameterTableModel.java | 79 ++++ .../components/models/SelectedKeysModel.java | 98 +++++ ...leListPanel.java => DoubleTablePanel.java} | 38 +- .../pulse/ui/frames/SearchOptionsFrame.java | 355 +++++++++--------- .../ui/frames/dialogs/ResultChangeDialog.java | 217 ++++++----- 8 files changed, 492 insertions(+), 477 deletions(-) delete mode 100644 src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java delete mode 100644 src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java delete mode 100644 src/main/java/pulse/ui/components/models/ParameterListModel.java create mode 100644 src/main/java/pulse/ui/components/models/ParameterTableModel.java create mode 100644 src/main/java/pulse/ui/components/models/SelectedKeysModel.java rename src/main/java/pulse/ui/components/panels/{DoubleListPanel.java => DoubleTablePanel.java} (76%) diff --git a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java b/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java deleted file mode 100644 index 8e8232e1..00000000 --- a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java +++ /dev/null @@ -1,34 +0,0 @@ -package pulse.ui.components.controllers; - -import static pulse.properties.NumericProperties.def; - -import java.awt.Component; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.JList; - -import pulse.properties.NumericPropertyKeyword; - -public class KeywordListRenderer extends DefaultListCellRenderer { - - /** - * - */ - private static final long serialVersionUID = 1L; - - public KeywordListRenderer() { - super(); - } - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - - var renderer = super.getListCellRendererComponent(list, - (def((NumericPropertyKeyword) value).getDescriptor(true)), index, cellHasFocus, cellHasFocus); - - return renderer; - - } - -} diff --git a/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java b/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java deleted file mode 100644 index 215d75b2..00000000 --- a/src/main/java/pulse/ui/components/models/ActiveFlagsListModel.java +++ /dev/null @@ -1,92 +0,0 @@ -package pulse.ui.components.models; - -import static pulse.tasks.processing.ResultFormat.getMinimalArray; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.DefaultListModel; - -import pulse.properties.NumericPropertyKeyword; - -public class ActiveFlagsListModel extends DefaultListModel { - - /** - * - */ - private static final long serialVersionUID = 1L; - private List elements = new ArrayList(); - private final List referenceList; - private NumericPropertyKeyword[] mandatorySelection; - - public ActiveFlagsListModel(List keys, NumericPropertyKeyword[] mandatorySelection) { - super(); - this.mandatorySelection = mandatorySelection; - referenceList = keys; - update(); - } - - public void update() { - update(referenceList); - } - - public void update(List keys) { - elements.clear(); - elements.addAll(keys); - } - - @Override - public int getSize() { - return elements.size(); - } - - @Override - public NumericPropertyKeyword getElementAt(int i) { - return elements.get(i); - } - - @Override - public void addElement(NumericPropertyKeyword key) { - elements.add(key); - var size = this.getSize(); - this.fireIntervalAdded(key, size, size); - } - - @Override - public boolean removeElement(Object obj) { - if (!(obj instanceof NumericPropertyKeyword)) { - return false; - } - - var key = (NumericPropertyKeyword) obj; - - if (!elements.contains(key)) { - return false; - } - - for (var keyMin : mandatorySelection) { - if (key == keyMin) { - return false; - } - } - var index = elements.indexOf(key); - elements.remove(key); - this.fireIntervalRemoved(key, index, index); - return true; - } - - @Override - public boolean contains(Object obj) { - if (!(obj instanceof NumericPropertyKeyword)) { - return false; - } - - var key = (NumericPropertyKeyword) obj; - return elements.contains(key); - } - - public List getData() { - return elements; - } - -} diff --git a/src/main/java/pulse/ui/components/models/ParameterListModel.java b/src/main/java/pulse/ui/components/models/ParameterListModel.java deleted file mode 100644 index 0a353b1f..00000000 --- a/src/main/java/pulse/ui/components/models/ParameterListModel.java +++ /dev/null @@ -1,56 +0,0 @@ -package pulse.ui.components.models; - -import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; -import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; -import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; -import static pulse.search.direction.ActiveFlags.listAvailableProperties; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.AbstractListModel; - -import pulse.input.InterpolationDataset; -import pulse.properties.Flag; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class ParameterListModel extends AbstractListModel { - - /** - * - */ - private static final long serialVersionUID = 1L; - private List elements = new ArrayList(); - private boolean extendedList; - - public ParameterListModel(boolean extendedList) { - super(); - this.extendedList = extendedList; - update(); - } - - public void update() { - elements.clear(); - var list = new ArrayList(); - listAvailableProperties(list); - list.stream().forEach(property -> elements.add(((Flag) property).getType())); - if(extendedList) { - elements.add(OPTIMISER_STATISTIC); - elements.add(TEST_STATISTIC); - elements.add(IDENTIFIER); - elements.addAll(InterpolationDataset.derivableProperties()); - } - } - - @Override - public int getSize() { - return elements.size(); - } - - @Override - public NumericPropertyKeyword getElementAt(int i) { - return elements.get(i); - } - -} diff --git a/src/main/java/pulse/ui/components/models/ParameterTableModel.java b/src/main/java/pulse/ui/components/models/ParameterTableModel.java new file mode 100644 index 00000000..66aa0cb4 --- /dev/null +++ b/src/main/java/pulse/ui/components/models/ParameterTableModel.java @@ -0,0 +1,79 @@ +package pulse.ui.components.models; + +import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +import pulse.input.InterpolationDataset; +import pulse.properties.Flag; +import pulse.properties.NumericProperties; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.ActiveFlags; +import pulse.ui.Messages; + +public class ParameterTableModel extends AbstractTableModel { + + /** + * + */ + private static final long serialVersionUID = 1L; + protected List elements; + private final boolean extendedList; + + public ParameterTableModel(boolean extendedList) { + super(); + this.elements = new ArrayList<>(); + this.extendedList = extendedList; + } + + public final void populateWithAllProperties() { + elements.clear(); + var set = ActiveFlags.availableProperties(); + set.stream().forEach(property -> elements.add(((Flag) property).getType())); + if (extendedList) { + elements.add(OPTIMISER_STATISTIC); + elements.add(TEST_STATISTIC); + elements.add(IDENTIFIER); + elements.addAll(InterpolationDataset.derivableProperties()); + } + } + + @Override + public int getRowCount() { + return elements != null ? elements.size() : 0; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int i, int i1) { + if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + var p = NumericProperties.def(elements.get(i)); + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } + else + return null; + } + + public boolean contains(NumericPropertyKeyword key) { + return elements.contains(key); + } + + public NumericPropertyKeyword getElementAt(int index) { + return elements.get(index); + } + + public List getData() { + return elements; + } + +} diff --git a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java new file mode 100644 index 00000000..3234193c --- /dev/null +++ b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java @@ -0,0 +1,98 @@ +package pulse.ui.components.models; + + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.table.DefaultTableModel; +import pulse.properties.NumericProperties; + +import pulse.properties.NumericPropertyKeyword; +import pulse.ui.Messages; + +public class SelectedKeysModel extends DefaultTableModel { + + /** + * + */ + private static final long serialVersionUID = 1L; + private List elements; + private final List referenceList; + private final NumericPropertyKeyword[] mandatorySelection; + + public SelectedKeysModel(List keys, NumericPropertyKeyword[] mandatorySelection) { + super(); + this.elements = new ArrayList<>(); + this.mandatorySelection = mandatorySelection; + referenceList = keys; + update(); + } + + public void update() { + update(referenceList); + } + + public void update(List keys) { + elements.clear(); + elements.addAll(keys); + } + + @Override + public int getRowCount() { + return elements != null ? elements.size() : 0; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int i, int i1) { + if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + var p = NumericProperties.def(elements.get(i)); + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } + else + return null; + } + + public void addElement(NumericPropertyKeyword key) { + elements.add(key); + var e = NumericProperties.def(key); + int index = elements.size() - 1; + super.fireTableRowsInserted(index, index); + } + + public boolean contains(NumericPropertyKeyword key) { + return elements.contains(key); + } + + public List getData() { + return elements; + } + + public NumericPropertyKeyword getElementAt(int index) { + return elements.get(index); + } + + public boolean removeElement(NumericPropertyKeyword key) { + + if (!elements.contains(key)) { + return false; + } + + for (var keyMin : mandatorySelection) { + if (key == keyMin) { + return false; + } + } + + var index = elements.indexOf(key); + super.fireTableRowsDeleted(index, index); + elements.remove(key); + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/DoubleListPanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java similarity index 76% rename from src/main/java/pulse/ui/components/panels/DoubleListPanel.java rename to src/main/java/pulse/ui/components/panels/DoubleTablePanel.java index c8fcc653..11a5c192 100644 --- a/src/main/java/pulse/ui/components/panels/DoubleListPanel.java +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -18,27 +18,30 @@ import java.awt.GridBagConstraints; import static java.awt.GridBagConstraints.BOTH; import static javax.swing.BorderFactory.createTitledBorder; -import javax.swing.DefaultListModel; -import javax.swing.JList; import javax.swing.SwingConstants; import javax.swing.JPanel; +import javax.swing.JTable; import pulse.properties.NumericProperties; import pulse.properties.NumericPropertyKeyword; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; -public class DoubleListPanel extends JPanel { +public class DoubleTablePanel extends JPanel { private javax.swing.JButton moveLeftBtn; private javax.swing.JButton moveRightBtn; - public DoubleListPanel(JList leftList, String titleLeft, JList rightList, String titleRight) { + public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { super(); - initComponents(leftList, titleLeft, rightList, titleRight); - + initComponents(leftTable, titleLeft, rightTable, titleRight); + moveRightBtn.addActionListener(e -> { - var key = leftList.getSelectedValue(); - var model = (DefaultListModel) rightList.getModel(); + var model = (SelectedKeysModel) rightTable.getModel(); + NumericPropertyKeyword key = ( (ParameterTableModel) leftTable.getModel() ) + .getElementAt(leftTable + .convertRowIndexToModel(leftTable.getSelectedRow())); if (key != null) { if (!model.contains(key)) { @@ -60,18 +63,19 @@ public DoubleListPanel(JList leftList, String titleLeft, JList rightList, String moveLeftBtn.addActionListener(e -> { - var key = rightList.getSelectedValue(); - var model = (DefaultListModel) rightList.getModel(); + var model = (SelectedKeysModel) rightTable.getModel(); + NumericPropertyKeyword key = model.getElementAt(rightTable + .convertRowIndexToModel(rightTable.getSelectedRow())); if (key != null) { model.removeElement(key); } }); - + } - public void initComponents(JList leftList, String titleLeft, JList rightList, String titleRight) { + public void initComponents(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { var leftScroller = new javax.swing.JScrollPane(); var rightScroller = new javax.swing.JScrollPane(); var moveToolbar = new javax.swing.JToolBar(); @@ -84,10 +88,10 @@ public void initComponents(JList leftList, String titleLeft, JList rightList, St var borderLeft = createTitledBorder(titleLeft); leftScroller.setBorder(borderLeft); borderLeft.setTitleColor(java.awt.Color.WHITE); + + leftTable.setRowHeight(80); - leftList.setFixedCellHeight(50); - - leftScroller.setViewportView(leftList); + leftScroller.setViewportView(leftTable); var gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.fill = BOTH; @@ -100,8 +104,8 @@ public void initComponents(JList leftList, String titleLeft, JList rightList, St rightScroller.setBorder(borderRight); borderRight.setTitleColor(java.awt.Color.WHITE); - rightList.setFixedCellHeight(50); - rightScroller.setViewportView(rightList); + rightTable.setRowHeight(80); + rightScroller.setViewportView(rightTable); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 2; diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 30fbf26e..d8b0b6cd 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -4,13 +4,10 @@ import static java.awt.GridBagConstraints.BOTH; import static javax.swing.BorderFactory.createTitledBorder; import static javax.swing.ListSelectionModel.SINGLE_SELECTION; -import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.search.direction.PathOptimiser.setInstance; import static pulse.ui.Messages.getString; import static pulse.util.Reflexive.instancesOf; -import java.util.stream.Collectors; - import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -21,10 +18,12 @@ import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; import javax.swing.border.EmptyBorder; -import javax.swing.event.ListDataEvent; -import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionEvent; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -35,186 +34,188 @@ import pulse.search.direction.PathOptimiser; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; -import pulse.ui.components.controllers.KeywordListRenderer; import pulse.ui.components.controllers.SearchListRenderer; -import pulse.ui.components.models.ParameterListModel; -import pulse.ui.components.models.ActiveFlagsListModel; -import pulse.ui.components.panels.DoubleListPanel; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; +import pulse.ui.components.panels.DoubleTablePanel; @SuppressWarnings("serial") public class SearchOptionsFrame extends JInternalFrame { - private PropertyHolderTable pathTable; - private JList leftList; - private JList rightList; - private PathSolversList pathList; + private final PropertyHolderTable pathTable; + private final JTable leftTable; + private final JTable rightTable; + private final PathSolversList pathList; - private final static Font font = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); - private final static List pathSolvers = instancesOf(PathOptimiser.class); - - private NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{DIFFUSIVITY, MAXTEMP}; - - /** - * Create the frame. - */ - public SearchOptionsFrame() { - setClosable(true); - setTitle(getString("SearchOptionsFrame.SelectSearch")); //$NON-NLS-1$ - setDefaultCloseOperation(HIDE_ON_CLOSE); - setBounds(100, 100, WIDTH, HEIGHT); - - /* + private final static Font FONT = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); + private final static List pathSolvers = instancesOf(PathOptimiser.class); + + private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{DIFFUSIVITY, MAXTEMP}; + + /** + * Create the frame. + */ + public SearchOptionsFrame() { + setClosable(true); + setTitle(getString("SearchOptionsFrame.SelectSearch")); //$NON-NLS-1$ + setDefaultCloseOperation(HIDE_ON_CLOSE); + setBounds(100, 100, WIDTH, HEIGHT); + + /* * Path solver list and scroller - */ - - var panel = new JPanel(); - panel.setBorder(new EmptyBorder(5, 5, 5, 5)); - setContentPane(panel); - - pathList = new PathSolversList(); - var pathListScroller = new JScrollPane(pathList); - pathListScroller.setBorder(createTitledBorder("Select an Optimiser")); - - pathTable = new PropertyHolderTable(null); - - getContentPane().setLayout(new GridBagLayout()); - - var gbc = new GridBagConstraints(); - - gbc.fill = BOTH; - gbc.gridy = 0; - gbc.gridx = 0; - gbc.weightx = 1.0; - gbc.weighty = 0.3; - - leftList = new javax.swing.JList(); - leftList.setModel(new ParameterListModel(false)); - leftList.setCellRenderer(new KeywordListRenderer()); - - rightList = new javax.swing.JList(); - rightList.setCellRenderer(new KeywordListRenderer()); - - var mainContainer = new DoubleListPanel(leftList, "All Parameters", rightList, "Optimised Parameters"); - - getContentPane().add(pathListScroller, gbc); - - gbc.gridy = 1; - gbc.weighty = 0.45; - - getContentPane().add(mainContainer, gbc); - - gbc.gridy = 2; - gbc.weighty = 0.25; - - var tableScroller = new JScrollPane(pathTable); - tableScroller.setBorder( - createTitledBorder("Select search variables and settings")); - getContentPane().add(tableScroller, gbc); - - } - - public void update() { - var selected = PathOptimiser.getInstance(); - /* + */ + var panel = new JPanel(); + panel.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(panel); + + pathList = new PathSolversList(); + var pathListScroller = new JScrollPane(pathList); + pathListScroller.setBorder(createTitledBorder("Select an Optimiser")); + + pathTable = new PropertyHolderTable(null); + + getContentPane().setLayout(new GridBagLayout()); + + var gbc = new GridBagConstraints(); + + gbc.fill = BOTH; + gbc.gridy = 0; + gbc.gridx = 0; + gbc.weightx = 1.0; + gbc.weighty = 0.3; + + leftTable = new javax.swing.JTable(); + leftTable.setModel(new ParameterTableModel(false)); + leftTable.setTableHeader(null); + leftTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + rightTable = new javax.swing.JTable(); + rightTable.setTableHeader(null); + rightTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + var mainContainer = new DoubleTablePanel(leftTable, "All Parameters", rightTable, "Optimised Parameters"); + + getContentPane().add(pathListScroller, gbc); + + gbc.gridy = 1; + gbc.weighty = 0.45; + + getContentPane().add(mainContainer, gbc); + + gbc.gridy = 2; + gbc.weighty = 0.25; + + var tableScroller = new JScrollPane(pathTable); + tableScroller.setBorder( + createTitledBorder("Select search variables and settings")); + getContentPane().add(tableScroller, gbc); + + } + + public void update() { + var selected = PathOptimiser.getInstance(); + /* Select Levenberg-Marquardt as default optimiser - */ - if (selected == null) - pathList.setSelectedValue(LMOptimiser.getInstance(), closable); - - pathList.setSelectedIndex(pathSolvers.indexOf(selected)); - ((ParameterListModel)leftList.getModel()).update(); - - var rightListModel = rightList.getModel(); - var activeTask = TaskManager.getManagerInstance().getSelectedTask(); - - //model for the flags list already created - if(rightListModel instanceof ActiveFlagsListModel) { - var searchKeys = ActiveFlags.activeParameters(activeTask); - ((ActiveFlagsListModel)rightListModel).update(searchKeys); - } - //Create a new model for the flags list - else { - if(activeTask != null - && activeTask.getCurrentCalculation() != null - && activeTask.getCurrentCalculation().getProblem() != null) { - var searchKeys = ActiveFlags.activeParameters(activeTask); - rightList.setModel(new ActiveFlagsListModel(searchKeys, mandatorySelection)); - - /* + */ + if (selected == null) { + pathList.setSelectedValue(LMOptimiser.getInstance(), closable); + } + + pathList.setSelectedIndex(pathSolvers.indexOf(selected)); + ((ParameterTableModel) leftTable.getModel()).populateWithAllProperties(); + + leftTable.setAutoCreateRowSorter(true); + leftTable.getRowSorter().toggleSortOrder(0); + + var rightTblModel = rightTable.getModel(); + var activeTask = TaskManager.getManagerInstance().getSelectedTask(); + + //model for the flags list already created + if (rightTblModel instanceof SelectedKeysModel) { + var searchKeys = ActiveFlags.activeParameters(activeTask); + ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); + ((SelectedKeysModel) rightTblModel).update(searchKeys); + } //Create a new model for the flags list + else { + if (activeTask != null + && activeTask.getCurrentCalculation() != null + && activeTask.getCurrentCalculation().getProblem() != null) { + var searchKeys = ActiveFlags.activeParameters(activeTask); + rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); + + /* Add listener to this - */ - rightList.getModel().addListDataListener(new ListDataListener() { - @Override - public void intervalAdded(ListDataEvent arg0) { - updateFlag(arg0, true); - } - - @Override - public void intervalRemoved(ListDataEvent arg0) { - updateFlag(arg0, false); - } - - private void updateFlag(ListDataEvent arg0, boolean value) { - var source = (NumericPropertyKeyword)arg0.getSource(); - var flag = new Flag(source); - flag.setValue(value); - PathOptimiser.getInstance().update(flag); - - } - - @Override - public void contentsChanged(ListDataEvent arg0) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - }); - + */ + rightTable.getModel().addTableModelListener(new TableModelListener() { + + private void updateFlag(TableModelEvent arg0, boolean value) { + var source = (NumericPropertyKeyword) + ( (SelectedKeysModel)rightTable.getModel() ) + .getElementAt(arg0.getFirstRow()); + var flag = new Flag(source); + flag.setValue(value); + PathOptimiser.getInstance().update(flag); + } + + @Override + public void tableChanged(TableModelEvent tme) { + if(tme.getType() == TableModelEvent.INSERT) + updateFlag(tme, true); + else if(tme.getType() == TableModelEvent.DELETE) + updateFlag(tme, false); } + + }); + + } + } + pathTable.updateTable(); + + } + + class PathSolversList extends JList { + + public PathSolversList() { + + super(); + + setModel(new AbstractListModel() { + /** + * + */ + private static final long serialVersionUID = -7683200230096704268L; + + @Override + public int getSize() { + return pathSolvers.size(); } - pathTable.updateTable(); - } - - class PathSolversList extends JList { - - public PathSolversList() { - - super(); - - setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -7683200230096704268L; - - @Override - public int getSize() { - return pathSolvers.size(); - } - - @Override - public PathOptimiser getElementAt(int index) { - return pathSolvers.get(index); - } - }); - - setFont(font); - setSelectionMode(SINGLE_SELECTION); - setCellRenderer(new SearchListRenderer()); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - - var optimiser = getSelectedValue(); - - setInstance(optimiser); - pathTable.setPropertyHolder(optimiser); - - for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(true); - } - }); - - } - } - -} \ No newline at end of file + + @Override + public PathOptimiser getElementAt(int index) { + return pathSolvers.get(index); + } + }); + + setFont(FONT); + setSelectionMode(SINGLE_SELECTION); + setCellRenderer(new SearchListRenderer()); + + addListSelectionListener((ListSelectionEvent arg0) -> { + if (arg0.getValueIsAdjusting()) { + return; + } + + var optimiser = getSelectedValue(); + + setInstance(optimiser); + pathTable.setPropertyHolder(optimiser); + + for (var t : TaskManager.getManagerInstance().getTaskList()) { + t.checkProblems(true); + } + }); + + } + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index a321983c..ee1c7d9a 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -1,113 +1,128 @@ package pulse.ui.frames.dialogs; -import static java.awt.GridBagConstraints.BOTH; -import static javax.swing.BorderFactory.createTitledBorder; import static javax.swing.SwingConstants.BOTTOM; -import static javax.swing.SwingConstants.VERTICAL; -import static pulse.tasks.processing.ResultFormat.generateFormat; import java.awt.BorderLayout; -import java.awt.GridBagConstraints; +import java.awt.Component; import javax.swing.JDialog; +import javax.swing.JTable; +import javax.swing.JTextArea; import javax.swing.SwingConstants; +import static javax.swing.SwingConstants.SOUTH; +import static javax.swing.SwingConstants.TOP; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; -import pulse.properties.NumericPropertyKeyword; import pulse.tasks.processing.ResultFormat; -import pulse.ui.components.controllers.KeywordListRenderer; -import pulse.ui.components.models.ParameterListModel; -import pulse.ui.components.models.ActiveFlagsListModel; -import pulse.ui.components.panels.DoubleListPanel; +import pulse.ui.Messages; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; +import pulse.ui.components.panels.DoubleTablePanel; public class ResultChangeDialog extends JDialog { - /** - * - */ - private static final long serialVersionUID = 1L; - private final static int WIDTH = 500; - private final static int HEIGHT = 500; - - public ResultChangeDialog() { - - setTitle("Result output formatting"); - - setSize(WIDTH, HEIGHT); - - initComponents(); - var model = (ActiveFlagsListModel)rightList.getModel(); - commitBtn.addActionListener(e -> generateFormat(model.getData())); - cancelBtn.addActionListener(e -> this.setVisible(false)); - } - - @Override - public void setVisible(boolean value) { - super.setVisible(value); - ((ActiveFlagsListModel) rightList.getModel()).update(); - ((ParameterListModel) leftList.getModel()).update(); - } - - private void initComponents() { - java.awt.GridBagConstraints gridBagConstraints; - - MainToolbar = new javax.swing.JToolBar(); - filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), - new java.awt.Dimension(32767, 0)); - cancelBtn = new javax.swing.JButton(); - filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(25, 0), new java.awt.Dimension(25, 0), - new java.awt.Dimension(25, 32767)); - commitBtn = new javax.swing.JButton(); - filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), - new java.awt.Dimension(32767, 0)); - - setDefaultCloseOperation(HIDE_ON_CLOSE); - - leftList = new javax.swing.JList<>(); - leftList.setModel(new ParameterListModel(true)); - leftList.setCellRenderer(new KeywordListRenderer()); - - rightList = new javax.swing.JList<>(); - rightList.setModel(new ActiveFlagsListModel( - ResultFormat.getInstance().getKeywords(), - ResultFormat.getMinimalArray())); - rightList.setCellRenderer(new KeywordListRenderer()); - - MainContainer = new DoubleListPanel - (leftList, "All Parameters", - rightList, "Output"); - - getContentPane().add(MainContainer, BorderLayout.CENTER); - - MainToolbar.setFloatable(false); - MainToolbar.setRollover(true); - MainToolbar.add(filler1); - - cancelBtn.setText("Cancel"); - cancelBtn.setFocusable(false); - cancelBtn.setHorizontalTextPosition(SwingConstants.CENTER); - cancelBtn.setVerticalTextPosition(BOTTOM); - MainToolbar.add(cancelBtn); - MainToolbar.add(filler3); - - commitBtn.setText("Commit"); - commitBtn.setFocusable(false); - commitBtn.setHorizontalTextPosition(SwingConstants.CENTER); - commitBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - MainToolbar.add(commitBtn); - MainToolbar.add(filler2); - - getContentPane().add(MainToolbar, BorderLayout.SOUTH); - - pack(); - } - - private javax.swing.JPanel MainContainer; - private javax.swing.JToolBar MainToolbar; - private javax.swing.JButton cancelBtn; - private javax.swing.JButton commitBtn; - private javax.swing.Box.Filler filler1; - private javax.swing.Box.Filler filler2; - private javax.swing.Box.Filler filler3; - private javax.swing.JList leftList; - private javax.swing.JList rightList; - -} \ No newline at end of file + + /** + * + */ + private static final long serialVersionUID = 1L; + private final static int WIDTH = 1000; + private final static int HEIGHT = 600; + + public ResultChangeDialog() { + + setTitle("Result output formatting"); + initComponents(); + setSize(WIDTH, HEIGHT); + var model = (SelectedKeysModel) rightTbl.getModel(); + commitBtn.addActionListener(e -> ResultFormat.generateFormat(model.getData())); + cancelBtn.addActionListener(e -> this.setVisible(false)); + } + + @Override + public void setVisible(boolean value) { + super.setVisible(value); + ((SelectedKeysModel) rightTbl.getModel()).update(); + ((ParameterTableModel) leftTbl.getModel()).populateWithAllProperties(); + } + + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + MainToolbar = new javax.swing.JToolBar(); + filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), + new java.awt.Dimension(32767, 0)); + cancelBtn = new javax.swing.JButton(); + filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(25, 0), new java.awt.Dimension(25, 0), + new java.awt.Dimension(25, 32767)); + commitBtn = new javax.swing.JButton(); + filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), + new java.awt.Dimension(32767, 0)); + + setDefaultCloseOperation(HIDE_ON_CLOSE); + + leftTbl = new javax.swing.JTable() { + + @Override + public boolean isCellEditable(int row, int column) { + return false; + }; + + }; + + leftTbl.setModel(new ParameterTableModel(true)); + leftTbl.setTableHeader(null); + + rightTbl = new javax.swing.JTable() { + + @Override + public boolean isCellEditable(int row, int column) { + return false; + }; + + }; + + rightTbl.setModel(new SelectedKeysModel( + ResultFormat.getInstance().getKeywords(), + ResultFormat.getMinimalArray())); + rightTbl.setTableHeader(null); + + MainContainer = new DoubleTablePanel(leftTbl, "All Parameters", + rightTbl, "Output"); + + getContentPane().add(MainContainer, BorderLayout.CENTER); + + MainToolbar.setFloatable(false); + MainToolbar.setRollover(true); + MainToolbar.add(filler1); + + cancelBtn.setText("Cancel"); + cancelBtn.setFocusable(false); + cancelBtn.setHorizontalTextPosition(SwingConstants.CENTER); + cancelBtn.setVerticalTextPosition(BOTTOM); + MainToolbar.add(cancelBtn); + MainToolbar.add(filler3); + + commitBtn.setText("Commit"); + commitBtn.setFocusable(false); + commitBtn.setHorizontalTextPosition(SwingConstants.CENTER); + commitBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + MainToolbar.add(commitBtn); + MainToolbar.add(filler2); + + getContentPane().add(MainToolbar, BorderLayout.SOUTH); + + pack(); + } + + private javax.swing.JPanel MainContainer; + private javax.swing.JToolBar MainToolbar; + private javax.swing.JButton cancelBtn; + private javax.swing.JButton commitBtn; + private javax.swing.Box.Filler filler1; + private javax.swing.Box.Filler filler2; + private javax.swing.Box.Filler filler3; + private javax.swing.JTable leftTbl; + private javax.swing.JTable rightTbl; + +} From 818dc0cf3a3c471bd66b654a7c053d6bdfff9798 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 15:21:09 +0100 Subject: [PATCH 085/116] Tables and renderers - changes merge() and group() have been removed from the ResultTable, as they really should be included in the table model instead. The new merge() and group() methods introduced in the ResultTableModel now handle the results directly, instead of invoking getValueAt(). The merge logic proceeds as follows: first, the result list is sorted in order of ascending ids. Then, on each element of that list, minus the skip list, the method group is invoked, which creates a group of results matching the following criteria: (a) these results test temperature should be within a specified tolerance (specified by the user) and (b) the minimum difference between their id value must be less-or-equal-to unity. This ensure that only a sequence of shots is averaged; that avoids averaging over the heating and cooling phase, for example. After generating a result from that group, which can either be an individual result, if the group size is one, or an average results, these results forming the group are then added to the skip list, and thus excluded from being processed later on. TableRenderer -> changed getTableCellRendererComponent() to always have task ids and status centred in the respective cells. AccessibleTableRenderer -- removed colouring of Properties and Property Holders. Changed the default AbstractFormatter of the JFormattedTextFields generated by the NumericPropertyRenderer to NumericPropertyFormatter. If the property is not editable, a JLabel is created with its text set to the getText() of the JFormattedTextField (thus, the formatting is preserved). --- .../java/pulse/ui/components/ResultTable.java | 441 ++++++++---------- .../java/pulse/ui/components/TaskTable.java | 258 +++++----- .../controllers/AccessibleTableRenderer.java | 86 ++-- .../controllers/NumericPropertyRenderer.java | 70 +-- .../controllers/TaskTableRenderer.java | 47 +- .../components/models/ResultTableModel.java | 317 +++++++++---- 6 files changed, 644 insertions(+), 575 deletions(-) diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index b4d85b1b..b47a0eeb 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -1,18 +1,13 @@ package pulse.ui.components; -import static java.lang.Math.abs; -import static java.util.stream.Collectors.toList; import static javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION; import static javax.swing.SortOrder.ASCENDING; import static javax.swing.SwingConstants.TOP; import static javax.swing.SwingUtilities.invokeLater; -import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; import javax.swing.JTable; import javax.swing.RowSorter; @@ -21,13 +16,10 @@ import javax.swing.table.TableRowSorter; import pulse.properties.NumericProperty; -import pulse.properties.Property; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.listeners.TaskSelectionEvent; -import pulse.tasks.processing.AbstractResult; -import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.ui.components.controllers.NumericPropertyRenderer; @@ -37,261 +29,208 @@ @SuppressWarnings("serial") public class ResultTable extends JTable implements Descriptive { - private final static int ROW_HEIGHT = 25; - private final static int RESULTS_HEADER_HEIGHT = 30; + private final static int ROW_HEIGHT = 25; + private final static int RESULTS_HEADER_HEIGHT = 50; - private NumericPropertyRenderer renderer; + private NumericPropertyRenderer renderer; - public ResultTable(ResultFormat fmt) { - super(); + public ResultTable(ResultFormat fmt) { + super(); - renderer = new NumericPropertyRenderer(); - renderer.setVerticalAlignment(TOP); + renderer = new NumericPropertyRenderer(); + renderer.setVerticalAlignment(TOP); - var model = new ResultTableModel(fmt); - setModel(model); - setRowSorter(sorter()); + var model = new ResultTableModel(fmt); + setModel(model); + setRowSorter(sorter()); - model.addListener(event -> setRowSorter(sorter())); + model.addListener(event -> setRowSorter(sorter())); - this.setRowHeight(ROW_HEIGHT); - setShowHorizontalLines(false); - setFillsViewportHeight(true); + this.setRowHeight(ROW_HEIGHT); + setShowHorizontalLines(false); + setFillsViewportHeight(true); - setSelectionMode(MULTIPLE_INTERVAL_SELECTION); - setRowSelectionAllowed(true); - setColumnSelectionAllowed(false); + setSelectionMode(MULTIPLE_INTERVAL_SELECTION); + setRowSelectionAllowed(true); + setColumnSelectionAllowed(false); - var headersSize = getPreferredSize(); - headersSize.height = RESULTS_HEADER_HEIGHT; - getTableHeader().setPreferredSize(headersSize); + var headersSize = getPreferredSize(); + headersSize.height = RESULTS_HEADER_HEIGHT; + getTableHeader().setPreferredSize(headersSize); - /* + /* * Listen to TaskTable and select appropriate results when task selection * changes - */ + */ + var instance = TaskManager.getManagerInstance(); - var instance = TaskManager.getManagerInstance(); + instance.addSelectionListener((TaskSelectionEvent e) -> { + var t = instance.getSelectedTask(); + getSelectionModel().clearSelection(); + select(t); + }); - instance.addSelectionListener((TaskSelectionEvent e) -> { - var t = instance.getSelectedTask(); - getSelectionModel().clearSelection(); - select(t); - }); - - /* + /* * Automatically add finished tasks to this result table Automatically remove * results if corresponding task is removed - */ - - TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - var t = instance.getTask(e.getId()); - switch (e.getState()) { - case TASK_FINISHED: - var r = t.getCurrentCalculation().getResult(); - invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); - break; - case TASK_REMOVED: - case TASK_RESET: - ((ResultTableModel) getModel()).removeAll(e.getId()); - getSelectionModel().clearSelection(); - break; - case BEST_MODEL_SELECTED: - for (var c : t.getStoredCalculations()) - if (c.getResult() != null && c != t.getCurrentCalculation()) - ((ResultTableModel) getModel()).remove(c.getResult()); - this.select(t.getCurrentCalculation().getResult()); - break; - case TASK_MODEL_SWITCH: - var c = t.getCurrentCalculation(); - this.getSelectionModel().clearSelection(); - if (c != null && c.getResult() != null) - select(c.getResult()); - break; - default: - break; - } - }); - - } - - public void clear() { - var model = (ResultTableModel) getModel(); - model.clear(); - } - - private TableRowSorter sorter() { - var sorter = new TableRowSorter((ResultTableModel) getModel()); - var list = new ArrayList(); - Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); - - for (var i = 0; i < getColumnCount(); i++) { - list.add(new RowSorter.SortKey(i, ASCENDING)); - sorter.setComparator(i, numericComparator); - } - - sorter.setSortKeys(list); - sorter.sort(); - return sorter; - } - - public double[][][] data() { - var data = new double[getColumnCount()][2][getRowCount()]; - NumericProperty property = null; - - for (var i = 0; i < data.length; i++) { - for (var j = 0; j < data[0][0].length; j++) { - property = (NumericProperty) getValueAt(j, i); - data[i][0][j] = ((Number) property.getValue()).doubleValue() - * property.getDimensionFactor().doubleValue(); - data[i][1][j] = property.getError() == null ? 0 - : property.getError().doubleValue() * property.getDimensionFactor().doubleValue(); - } - } - return data; - } - - private void scrollToSelection(int rowIndex) { - scrollRectToVisible(getCellRect(rowIndex, rowIndex, true)); - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - var value = getValueAt(row, column); - - if (value instanceof NumericProperty) - return renderer; - - return super.getCellRenderer(row, column); - - } - - /* - * Merges data withing a temperature interval - */ - - public void merge(double temperatureDelta) { - var model = (ResultTableModel) this.getModel(); - var temperatureIndex = model.getFormat().indexOf(TEST_TEMPERATURE); - - if (temperatureIndex < 0) - return; - - List newRows = new LinkedList<>(); - List skipList = new ArrayList<>(); - - for (var i = 0; i < this.getRowCount(); i++) { - if (skipList.contains(convertRowIndexToModel(i))) - continue; // check if value is independent (does not belong to a group) - - var val = ((Number) ((Property) this.getValueAt(i, temperatureIndex)).getValue()); - - var indices = group(val.doubleValue(), temperatureIndex, temperatureDelta); // get indices of results in - // table - skipList.addAll(indices); // skip those indices if they refer to the same group - - if (indices.size() < 2) - newRows.add(model.getResults().get(indices.get(0))); - else - newRows.add(new AverageResult(indices.stream().map(model.getResults()::get).collect(toList()), - model.getFormat())); - - } - - invokeLater(() -> { - model.setRowCount(0); - model.getResults().clear(); - newRows.stream().forEach(r -> model.addRow(r)); - }); - - } - - public List group(double val, int index, double precision) { - - List selection = new ArrayList<>(); - - for (var i = 0; i < getRowCount(); i++) { - - var valNumber = (Number) ((Property) getValueAt(i, index)).getValue(); - - if (abs(valNumber.doubleValue() - val) < precision) - selection.add(convertRowIndexToModel(i)); - - } - - return selection; - - } - - // Implement table header tool tips. - @Override - protected JTableHeader createDefaultTableHeader() { - return new JTableHeader(columnModel) { - @Override - public String getToolTipText(MouseEvent e) { - var index = columnModel.getColumnIndexAtX(e.getPoint().x); - var realIndex = columnModel.getColumn(index).getModelIndex(); - return ((ResultTableModel) getModel()).getTooltips().get(realIndex); - } - }; - } - - @Override - public String describe() { - return "SummaryTable"; - } - - public boolean isSelectionEmpty() { - return getSelectedRows().length < 1; - } - - public boolean hasEnoughElements(int elements) { - return getRowCount() >= elements; - } - - public void deleteSelected() { - - invokeLater(() -> { - var rtm = (ResultTableModel) getModel(); - var selection = getSelectedRows(); - - if (selection.length < 0) - return; - - for (var i = selection.length - 1; i >= 0; i--) { - rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); - } - }); - - } - - public void select(Result r) { - var results = ((ResultTableModel) getModel()).getResults(); - int modelIndex = results.indexOf(r); - if (modelIndex > -1) { - int jj = convertRowIndexToView(modelIndex); - getSelectionModel().addSelectionInterval(jj, jj); - scrollToSelection(jj); - } - } - - public void select(SearchTask t) { - t.getStoredCalculations().stream().forEach(c -> { - if (c.getResult() != null) - select(c.getResult()); - }); - } - - public void undo() { - var dtm = (ResultTableModel) getModel(); - - for (var i = dtm.getRowCount() - 1; i >= 0; i--) { - dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); - } - - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) - .forEach(c -> dtm.addRow(c.getResult())); - } - -} \ No newline at end of file + */ + TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { + var t = instance.getTask(e.getId()); + switch (e.getState()) { + case TASK_FINISHED: + var r = t.getCurrentCalculation().getResult(); + invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); + break; + case TASK_REMOVED: + case TASK_RESET: + ((ResultTableModel) getModel()).removeAll(e.getId()); + getSelectionModel().clearSelection(); + break; + case BEST_MODEL_SELECTED: + for (var c : t.getStoredCalculations()) { + if (c.getResult() != null && c != t.getCurrentCalculation()) { + ((ResultTableModel) getModel()).remove(c.getResult()); + } + } + this.select(t.getCurrentCalculation().getResult()); + break; + case TASK_MODEL_SWITCH: + var c = t.getCurrentCalculation(); + this.getSelectionModel().clearSelection(); + if (c != null && c.getResult() != null) { + select(c.getResult()); + } + break; + default: + break; + } + }); + + } + + public void clear() { + var model = (ResultTableModel) getModel(); + model.clear(); + } + + private TableRowSorter sorter() { + var sorter = new TableRowSorter((ResultTableModel) getModel()); + var list = new ArrayList(); + Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); + + for (var i = 0; i < getColumnCount(); i++) { + list.add(new RowSorter.SortKey(i, ASCENDING)); + sorter.setComparator(i, numericComparator); + } + + sorter.setSortKeys(list); + sorter.sort(); + return sorter; + } + + public double[][][] data() { + var data = new double[getColumnCount()][2][getRowCount()]; + NumericProperty property = null; + + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[0][0].length; j++) { + property = (NumericProperty) getValueAt(j, i) ; + data[i][0][j] = ((Number) property.getValue()).doubleValue() + * property.getDimensionFactor().doubleValue() + property.getDimensionDelta().doubleValue(); + data[i][1][j] = property.getError() == null ? 0 + : property.getError().doubleValue() * property.getDimensionFactor().doubleValue(); + } + } + return data; + } + + private void scrollToSelection(int rowIndex) { + scrollRectToVisible(getCellRect(rowIndex, rowIndex, true)); + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + var value = getValueAt(row, column); + + if (value instanceof NumericProperty) { + return renderer; + } + + return super.getCellRenderer(row, column); + + } + + // Implement table header tool tips. + @Override + protected JTableHeader createDefaultTableHeader() { + return new JTableHeader(columnModel) { + @Override + public String getToolTipText(MouseEvent e) { + var index = columnModel.getColumnIndexAtX(e.getPoint().x); + var realIndex = columnModel.getColumn(index).getModelIndex(); + return ((ResultTableModel) getModel()).getTooltips().get(realIndex); + } + }; + } + + @Override + public String describe() { + return "SummaryTable"; + } + + public boolean isSelectionEmpty() { + return getSelectedRows().length < 1; + } + + public boolean hasEnoughElements(int elements) { + return getRowCount() >= elements; + } + + public void deleteSelected() { + + invokeLater(() -> { + var rtm = (ResultTableModel) getModel(); + var selection = getSelectedRows(); + + if (selection.length < 0) { + return; + } + + for (var i = selection.length - 1; i >= 0; i--) { + rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); + } + }); + + } + + public void select(Result r) { + var results = ((ResultTableModel) getModel()).getResults(); + int modelIndex = results.indexOf(r); + if (modelIndex > -1) { + int jj = convertRowIndexToView(modelIndex); + getSelectionModel().addSelectionInterval(jj, jj); + scrollToSelection(jj); + } + } + + public void select(SearchTask t) { + t.getStoredCalculations().stream().forEach(c -> { + if (c.getResult() != null) { + select(c.getResult()); + } + }); + } + + public void undo() { + var dtm = (ResultTableModel) getModel(); + + for (var i = dtm.getRowCount() - 1; i >= 0; i--) { + dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); + } + + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) + .forEach(c -> dtm.addRow(c.getResult())); + } + +} diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index 6d48cd33..c7cff7b6 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -36,152 +36,154 @@ @SuppressWarnings("serial") public class TaskTable extends JTable { - private final static int ROW_HEIGHT = 35; - private final static int HEADER_HEIGHT = 30; + private final static int ROW_HEIGHT = 35; + private final static int HEADER_HEIGHT = 30; - private TaskTableRenderer taskTableRenderer; - private TaskPopupMenu menu; + private TaskTableRenderer taskTableRenderer; + private TaskPopupMenu menu; - private Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); - private Comparator statusComparator = (s1, s2) -> s1.compareTo(s2); + private Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); + private Comparator statusComparator = (s1, s2) -> s1.compareTo(s2); - public TaskTable() { - super(); - setDefaultEditor(Object.class, null); - taskTableRenderer = new TaskTableRenderer(); - this.setRowSelectionAllowed(true); - setRowHeight(ROW_HEIGHT); + public TaskTable() { + super(); + setDefaultEditor(Object.class, null); + taskTableRenderer = new TaskTableRenderer(); + this.setRowSelectionAllowed(true); + setRowHeight(ROW_HEIGHT); - setFillsViewportHeight(true); - setSelectionMode(SINGLE_INTERVAL_SELECTION); - setShowHorizontalLines(false); + setFillsViewportHeight(true); + setSelectionMode(SINGLE_INTERVAL_SELECTION); + setShowHorizontalLines(false); - var model = new TaskTableModel(); - setModel(model); + var model = new TaskTableModel(); + setModel(model); - var th = new TableHeader(getColumnModel()); + var th = new TableHeader(getColumnModel()); - setTableHeader(th); + setTableHeader(th); - getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); - setAutoCreateRowSorter(true); - var sorter = new TableRowSorter(); - sorter.setModel(model); - var list = new ArrayList(); + setAutoCreateRowSorter(true); + var sorter = new TableRowSorter(); + sorter.setModel(model); + var list = new ArrayList(); - for (var i = 0; i < this.getModel().getColumnCount(); i++) { - list.add(new RowSorter.SortKey(i, ASCENDING)); - if (i == TaskTableModel.STATUS_COLUMN) - sorter.setComparator(i, statusComparator); - else - sorter.setComparator(i, numericComparator); - } + for (var i = 0; i < this.getModel().getColumnCount(); i++) { + list.add(new RowSorter.SortKey(i, ASCENDING)); + if (i == TaskTableModel.STATUS_COLUMN) { + sorter.setComparator(i, statusComparator); + } else { + sorter.setComparator(i, numericComparator); + } + } - sorter.setSortKeys(list); - setRowSorter(sorter); + sorter.setSortKeys(list); + setRowSorter(sorter); - initListeners(); - menu = new TaskPopupMenu(); + initListeners(); + menu = new TaskPopupMenu(); - } + } - public void initListeners() { - - var instance = TaskManager.getManagerInstance(); - - /* - * mouse listener - */ + public void initListeners() { + + var instance = TaskManager.getManagerInstance(); - addMouseListener(new MouseAdapter() { + /* + * mouse listener + */ + addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { + @Override + public void mouseClicked(MouseEvent e) { - if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { - var task = instance.getSelectedTask(); - menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); - menu.show(e.getComponent(), e.getX(), e.getY()); - } + if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { + var task = instance.getSelectedTask(); + menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); + menu.show(e.getComponent(), e.getX(), e.getY()); + } - } + } - }); + }); - /* + /* * selection listener - */ - - var lsm = getSelectionModel(); - var reference = this; - - lsm.addListSelectionListener((ListSelectionEvent e) -> { - if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { - var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); - instance.selectTask(id, reference); - } - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // simply ignore call if event is triggered by taskTable - if (e.getSource() instanceof TaskTable) - return; - - var id = instance.getSelectedTask().getIdentifier(); - Identifier idFromTable = null; - int i = 0; - - for (i = 0; i < getRowCount() && !id.equals(idFromTable); i++) - idFromTable = (Identifier) getValueAt(i, 0); - - if(i < getRowCount()) - setRowSelectionInterval(i, i); - clearSelection(); - }); - - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - return taskTableRenderer; - } - - public void removeSelectedRows() { - SwingUtilities.invokeLater(() -> { - var rows = getSelectedRows(); - Identifier id; - - var instance = TaskManager.getManagerInstance(); - - for (var i = rows.length - 1; i >= 0; i--) { - id = (Identifier) getValueAt(rows[i], 0); - instance.removeTask(instance.getTask(id)); - } - - clearSelection(); - }); - } - - private class TableHeader extends JTableHeader { - - private String[] tooltips; - - public TableHeader(TableColumnModel columnModel) { - super(columnModel);// do everything a normal JTableHeader does - tooltips = new String[] { def(IDENTIFIER).getDescriptor(true), - def(TEST_TEMPERATURE).getDescriptor(true), def(OPTIMISER_STATISTIC).getDescriptor(true), - def(TEST_STATISTIC).getDescriptor(true), ("Task status")}; - } - - @Override - public String getToolTipText(MouseEvent e) { - var p = e.getPoint(); - var index = columnModel.getColumnIndexAtX(p.x); - var realIndex = columnModel.getColumn(index).getModelIndex(); - return this.tooltips[realIndex]; - } - - } - -} \ No newline at end of file + */ + var lsm = getSelectionModel(); + var reference = this; + + lsm.addListSelectionListener((ListSelectionEvent e) -> { + if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { + var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); + instance.selectTask(id, reference); + } + }); + + instance.addSelectionListener((TaskSelectionEvent e) -> { + // simply ignore call if event is triggered by taskTable + if (e.getSource() instanceof TaskTable) { + return; + } + + var id = instance.getSelectedTask().getIdentifier(); + Identifier idFromTable = null; + int i = 0; + + for (i = 0; i < getRowCount() && !id.equals(idFromTable); i++) { + idFromTable = (Identifier) getValueAt(i, 0); + } + + if (i < getRowCount()) { + setRowSelectionInterval(i, i); + } + clearSelection(); + }); + + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + return taskTableRenderer; + } + + public void removeSelectedRows() { + SwingUtilities.invokeLater(() -> { + var rows = getSelectedRows(); + Identifier id; + + var instance = TaskManager.getManagerInstance(); + + for (var i = rows.length - 1; i >= 0; i--) { + id = (Identifier) getValueAt(rows[i], 0); + instance.removeTask(instance.getTask(id)); + } + + clearSelection(); + }); + } + + private class TableHeader extends JTableHeader { + + private String[] tooltips; + + public TableHeader(TableColumnModel columnModel) { + super(columnModel);// do everything a normal JTableHeader does + tooltips = new String[]{def(IDENTIFIER).getDescriptor(true), + def(TEST_TEMPERATURE).getDescriptor(true), def(OPTIMISER_STATISTIC).getDescriptor(true), + def(TEST_STATISTIC).getDescriptor(true), ("Task status")}; + } + + @Override + public String getToolTipText(MouseEvent e) { + var p = e.getPoint(); + var index = columnModel.getColumnIndexAtX(p.x); + var realIndex = columnModel.getColumn(index).getModelIndex(); + return this.tooltips[realIndex]; + } + + } + +} diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 123202eb..b297d1a5 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -3,8 +3,10 @@ import static java.awt.Color.RED; import java.awt.Component; +import java.awt.Font; import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.UIManager; @@ -17,48 +19,42 @@ @SuppressWarnings("serial") public class AccessibleTableRenderer extends NumericPropertyRenderer { - public AccessibleTableRenderer() { - super(); - } - - @Override - - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { - - var selectedBackground = UIManager.getColor("Table.selectionBackground"); - var deselectedBackground = UIManager.getColor("Table.bakground"); - Component renderer = null; - - if (value instanceof NumericProperty) - renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - - else if (value instanceof Flag) { - renderer = new IconCheckBox((boolean) ((Property) value).getValue()); - ((IconCheckBox)renderer).setHorizontalAlignment(CENTER); - } - - else if (value instanceof PropertyHolder) - renderer = initButton(value.toString()); - - else if (value instanceof Property) { - renderer = initTextField(value.toString(), table.isRowSelected(row)); - renderer.setForeground(RED); - } - - if(renderer != null) { - renderer.setBackground(isSelected ? selectedBackground : deselectedBackground); - return renderer; - } - else - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - - } - - private JButton initButton(String str) { - var button = new JButton(str); - button.setToolTipText(str); - return button; - } - -} \ No newline at end of file + public AccessibleTableRenderer() { + super(); + } + + @Override + + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + + Component renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + if (value instanceof Flag) { + renderer = new IconCheckBox((boolean) ((Property) value).getValue()); + ((IconCheckBox) renderer).setHorizontalAlignment(CENTER); + } else if (value instanceof PropertyHolder) { + renderer = initButton(value.toString()); + } + else if (value instanceof NumericProperty) { + //default + } + else if (value instanceof Property) { + var label = (JLabel) super.getTableCellRendererComponent(table, + ((Property) value).getDescriptor(true), isSelected, + hasFocus, row, column); + label.setHorizontalAlignment(JLabel.CENTER); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + return label; + } + + return renderer; + } + + private JButton initButton(String str) { + var button = new JButton(str); + button.setToolTipText(str); + return button; + } + +} diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index 8c8f6073..e655f9b1 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -7,52 +7,56 @@ import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.table.DefaultTableCellRenderer; +import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.Property; +import pulse.properties.NumericPropertyFormatter; @SuppressWarnings("serial") public class NumericPropertyRenderer extends DefaultTableCellRenderer { - public NumericPropertyRenderer() { - super(); - } + public NumericPropertyRenderer() { + super(); + } - @Override + @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - Component result = null; + Component result = null; - if (value instanceof NumericProperty) { - var output = ((Property) value).formattedOutput(); - result = table.getEditorComponent() != null ? - initTextField(output, table.isRowSelected(row)) - : initLabel(output, table.isRowSelected(row)); - } else if(value instanceof Number) { - result = initLabel(value.toString(), table.isRowSelected(row)); - } else - result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof NumericProperty) { + var jtf = initTextField((NumericProperty) value, table.isRowSelected(row)); - return result; + if (table.getEditorComponent() != null) { + result = jtf; + } else { + result = new JLabel(jtf.getText(), JLabel.RIGHT); + jtf = null; + } - } + } else { + var superRenderer = (JLabel) super.getTableCellRendererComponent(table, + value, isSelected, hasFocus, row, column); + superRenderer.setHorizontalAlignment(JLabel.LEFT); + superRenderer.setBackground( + isSelected + ? UIManager.getColor("JFormattedTextField.selectionBackground") + : UIManager.getColor("JFormattedTextField.background")); + result = superRenderer; + } - protected static JFormattedTextField initTextField(String text, boolean rowSelected) { - var jtf = new JFormattedTextField(text); - jtf.setHorizontalAlignment(CENTER); - jtf.setBackground( - rowSelected ? UIManager.getColor("Table.selectionBackground") : UIManager.getColor("Table.background")); - return jtf; - } + return result; - protected static JLabel initLabel(String text, boolean rowSelected) { - var lab = new JLabel(text); - lab.setHorizontalAlignment(CENTER); - lab.setBackground( - rowSelected ? UIManager.getColor("Table.selectionBackground") : UIManager.getColor("Table.background")); - return lab; - } + } -} + private static JFormattedTextField initTextField(NumericProperty np, boolean rowSelected) { + var jtf = new JFormattedTextField(new NumericPropertyFormatter(np, true, true)); + jtf.setValue(np); + jtf.setHorizontalAlignment(RIGHT); + return jtf; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index bc99304e..85d3ba62 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -3,6 +3,8 @@ import static java.awt.Font.BOLD; import java.awt.Component; +import java.awt.Font; +import javax.swing.JLabel; import javax.swing.JTable; @@ -15,37 +17,34 @@ @SuppressWarnings("serial") public class TaskTableRenderer extends NumericPropertyRenderer { - public TaskTableRenderer() { - super(); - } + public TaskTableRenderer() { + super(); + } - @Override + @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - if (value instanceof NumericProperty) - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + var superRenderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - else if (value instanceof Identifier) - return initLabel("" + ((Property) value).getValue(), table.isRowSelected(row)); + if (value instanceof Identifier) { + var lab = (JLabel) superRenderer; + lab.setHorizontalAlignment(JLabel.CENTER); + } else if (value instanceof NumericProperty) { + return superRenderer; + } else if (value instanceof Status) { - else if (value instanceof Status) { + superRenderer.setForeground(((Status) value).getColor()); + superRenderer.setFont(superRenderer.getFont().deriveFont(BOLD)); + ((JLabel)superRenderer).setHorizontalAlignment(JLabel.CENTER); - var lab = initLabel(value.toString(), table.isRowSelected(row)); - lab.setForeground(((Status) value).getColor()); - lab.setFont(lab.getFont().deriveFont(BOLD)); + return superRenderer; - return lab; + } - } - - else if(value instanceof PropertyHolder) { - return initLabel("" + ((PropertyHolder)value).getDescriptor(), table.isRowSelected(row)); - } + return superRenderer; - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index bb8417bb..4bb5ef18 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -1,16 +1,25 @@ package pulse.ui.components.models; +import static java.lang.Math.abs; import static java.util.stream.Collectors.toList; import static pulse.tasks.processing.AbstractResult.filterProperties; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Optional; +import static javax.swing.SwingUtilities.invokeLater; import javax.swing.table.DefaultTableModel; +import pulse.properties.NumericProperties; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import pulse.tasks.Identifier; import pulse.tasks.listeners.ResultFormatEvent; import pulse.tasks.processing.AbstractResult; +import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.ui.components.listeners.ResultListener; @@ -18,127 +27,247 @@ @SuppressWarnings("serial") public class ResultTableModel extends DefaultTableModel { - private ResultFormat fmt; - private List results; - private List tooltips; - private List listeners; + private ResultFormat fmt; + private List results; + private List tooltips; + private List listeners; + + public ResultTableModel(ResultFormat fmt, int rowCount) { + super(fmt.abbreviations().toArray(), rowCount); + this.fmt = fmt; + results = new ArrayList<>(); + tooltips = tooltips(); + listeners = new ArrayList<>(); + } + + public ResultTableModel(ResultFormat fmt) { + this(fmt, 0); + } + + public void addListener(ResultListener listener) { + listeners.add(listener); + } + + public void removeListeners() { + listeners.clear(); + } + + public void clear() { + results.clear(); + listeners.clear(); + setRowCount(0); + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; // all cells false + } + + public void changeFormat(ResultFormat fmt) { + this.fmt = fmt; + + for (var r : results) { + r.setFormat(fmt); + } + + if (this.getRowCount() > 0) { + this.setRowCount(0); + + List oldResults = new ArrayList<>(results); + + results.clear(); + this.setColumnIdentifiers(fmt.abbreviations().toArray()); + + for (var r : oldResults) { + addRow(r); + } + + } else { + this.setColumnIdentifiers(fmt.abbreviations().toArray()); + } + + tooltips = tooltips(); + + listeners.stream().forEach(l -> l.onFormatChanged(new ResultFormatEvent(fmt))); + + } + + /** + * Transforms the result model by merging individual results which: + * (a) correspond to test temperatures within a specified {@code temperatureDelta} + * (b) form a single sequence of measurements + * @param temperatureDelta the maximum difference between the test temperature of two results being merged + */ + + public void merge(double temperatureDelta) { + List skipList = new ArrayList<>(); + List avgResults = new ArrayList<>(); + List sortedResults = new ArrayList<>(results); + + /*sort results in the order of their ids + * This is essential for the algorithm below which assumes the results + * are listed in the order of ascending ids. + */ + sortedResults.sort((AbstractResult arg0, AbstractResult arg1) -> { + var id1 = arg0.getProperties().get(fmt.indexOf(IDENTIFIER)); + var id2 = arg1.getProperties().get(fmt.indexOf(IDENTIFIER)); + return NumericProperties.compare(id1, id2); + }); + + //iterated over the merged list + for (var r : sortedResults) { + + //ignore results added to the skip list + if (skipList.contains(r)) { + continue; + } + + //form a group of individual results corresponding to the specified criteria + List group = group(sortedResults, r, temperatureDelta); + //remove any previous occurences in that group + group.removeAll(skipList); + + if (group.isEmpty()) { + continue; + } else if (group.size() == 1) { + //just one result is being added - no need to average + avgResults.addAll(group); + } else { + //add and average result + avgResults.add(new AverageResult(group, fmt)); + } + + //ignore processed results later on + skipList.addAll(group); + + } + + //populate model + invokeLater(() -> { + setRowCount(0); + results.clear(); + avgResults.stream().forEach(r -> addRow(r)); + }); + + } + + /** + * Takes a list of results, which should be mandatory sorted in the order of ascending id values, + * and searches for those results that can be merged with {@code r}, satisfying these criteria: + * (a) these results correspond to test temperatures within a specified {@code temperatureDelta} + * (b) they form a single sequence of measurements + * @param listOfResults an orderer list of results, as explained above + * @param r the result of interest + * @param propertyInterval an interval for the temperature merging + * @return a group of results + */ - public ResultTableModel(ResultFormat fmt, int rowCount) { - super(fmt.abbreviations().toArray(), rowCount); - this.fmt = fmt; - results = new ArrayList<>(); - tooltips = tooltips(); - listeners = new ArrayList<>(); - } + public List group(List listOfResults, AbstractResult r, double propertyInterval) { + List selection = new ArrayList<>(); - public ResultTableModel(ResultFormat fmt) { - this(fmt, 0); - } + final int idIndex = fmt.indexOf(IDENTIFIER); + final int temperatureIndex = fmt.indexOf(TEST_TEMPERATURE); - public void addListener(ResultListener listener) { - listeners.add(listener); - } + final double curTemp = ((Number) r.getProperties().get(temperatureIndex) + .getValue()).doubleValue(); - public void removeListeners() { - listeners.clear(); - } + final int curId = ((Number) r.getProperties().get(idIndex).getValue()) + .intValue(); - public void clear() { - results.clear(); - listeners.clear(); - setRowCount(0); - } + List ids = new ArrayList<>(); + ids.add(curId); - @Override - public boolean isCellEditable(int row, int column) { - return false; // all cells false - } + for (var rr : listOfResults) { + + var props = rr.getProperties(); + //temperature of a different result + double temp = ((Number) props.get(temperatureIndex).getValue()).doubleValue(); + + //if the property at modelIndex and the property value lie withing a specified interval + if (abs(temp - curTemp) < propertyInterval) { - public void changeFormat(ResultFormat fmt) { - this.fmt = fmt; + //what is ID of that property? + int newId = ((Number) props.get(idIndex).getValue()).intValue(); + + //calculate the minimum "ID" distance between that property and + //the group elements, that should be either "one" or "zero" + Optional minDistance = ids.stream().map(id + -> (int) Math.abs(id - newId)) + .reduce((a, b) -> a < b ? a : b); + + //accept only measurements within a single interval + if (minDistance.get() < 2) { + selection.add(rr); + ids.add(newId); + } - for (var r : results) { - r.setFormat(fmt); - } + } - if (this.getRowCount() > 0) { - this.setRowCount(0); + } - List oldResults = new ArrayList<>(results); + return selection; - results.clear(); - this.setColumnIdentifiers(fmt.abbreviations().toArray()); + } - for (var r : oldResults) { - addRow(r); - } + private List tooltips() { + return fmt.descriptors(); + } - } else - this.setColumnIdentifiers(fmt.abbreviations().toArray()); + public void addRow(AbstractResult result) { + if (result == null) { + return; + } - tooltips = tooltips(); + var propertyList = filterProperties(result, fmt); + super.addRow(propertyList.toArray()); + results.add(result); - listeners.stream().forEach(l -> l.onFormatChanged(new ResultFormatEvent(fmt))); + } - } + public void removeAll(Identifier id) { + AbstractResult result = null; - private List tooltips() { - return fmt.descriptors().stream().map(d -> "" + d + "").collect(toList()); - } + for (var i = results.size() - 1; i >= 0; i--) { + result = results.get(i); - public void addRow(AbstractResult result) { - if (result == null) - return; + if (!(result instanceof Result)) { + continue; + } - var propertyList = filterProperties(result, fmt); - super.addRow(propertyList.toArray()); - results.add(result); + if (id.equals(result.identify())) { + results.remove(result); + super.removeRow(i); + } - } + } - public void removeAll(Identifier id) { - AbstractResult result = null; + } - for (var i = results.size() - 1; i >= 0; i--) { - result = results.get(i); + public void remove(AbstractResult r) { + AbstractResult result = null; - if (!(result instanceof Result)) - continue; + for (var i = results.size() - 1; i >= 0; i--) { + result = results.get(i); - if (id.equals(result.identify())) { - results.remove(result); - super.removeRow(i); - } + if (result.equals(r)) { + results.remove(result); + super.removeRow(i); + } - } + } - } + } - public void remove(AbstractResult r) { - AbstractResult result = null; + public List getResults() { + return results; + } - for (var i = results.size() - 1; i >= 0; i--) { - result = results.get(i); + public ResultFormat getFormat() { + return fmt; + } - if (result.equals(r)) { - results.remove(result); - super.removeRow(i); - } + public List getTooltips() { + return tooltips; + } - } - - } - - public List getResults() { - return results; - } - - public ResultFormat getFormat() { - return fmt; - } - - public List getTooltips() { - return tooltips; - } - -} \ No newline at end of file +} From fdd4bd83e13c23f43a6bd77312d90a614783a72a Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 15:45:19 +0100 Subject: [PATCH 086/116] Toolbars and dialogs Plotting requests in ProblemToolbar are now handled via an ExecutorService to avoid concurrent modification. FormattedInputDialog now formats the properties according to the NumericPropertyFormatter, thus maintaining consistency. This means that checks for isEditValid() are no longer necessary, since invalid edits will be rejected by the formatter anyway. Finally, the method value() had to be adjusted, as with the new formatter, the return type of the ftf.getValue() changed to NumericProperty. --- .../ui/components/panels/ProblemToolbar.java | 144 +++++----- .../frames/dialogs/FormattedInputDialog.java | 249 ++++++++---------- 2 files changed, 186 insertions(+), 207 deletions(-) diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index fb1191af..aa85aed7 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -13,6 +13,7 @@ import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; +import java.util.concurrent.Executors; import javax.swing.JButton; import javax.swing.JToolBar; @@ -27,76 +28,73 @@ @SuppressWarnings("serial") public class ProblemToolbar extends JToolBar { - private JButton btnSimulate; - private LoaderButton btnLoadCv; - private LoaderButton btnLoadDensity; - - public ProblemToolbar() { - super(); - setFloatable(false); - setLayout(new GridLayout()); - - btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ - add(btnSimulate); - - btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ - btnLoadCv.setDataType(HEAT_CAPACITY); - add(btnLoadCv); - - btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ - btnLoadDensity.setDataType(DENSITY); - add(btnLoadDensity); - - addListeners(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void addListeners() { - var instance = TaskManager.getManagerInstance(); - - // simulate btn listener - - btnSimulate.addActionListener((ActionEvent e) -> { - if (instance.getSelectedTask() == null) - instance.selectFirstTask(); - - var t = instance.getSelectedTask(); - - var calc = t.getCurrentCalculation(); - - t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); - - if (status == INCOMPLETE && !status.checkProblemStatementSet()) { - - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), - getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ - ERROR_MESSAGE); - - } else { - try { - ((Solver) calc.getScheme()).solve(calc.getProblem()); - } catch (SolverException se) { - err.println("Solver of " + t + " has encountered an error. Details: "); - se.printStackTrace(); - } - MainGraphFrame.getInstance().plot(); - TaskControlFrame.getInstance().getPulseFrame().plot(calc.getProblem().getPulse()); - } - }); - - } - - public void highlightButtons(boolean highlight) { - if(highlight) { - btnLoadDensity.highlightIfNeeded(); - btnLoadCv.highlightIfNeeded(); - } - else { - btnLoadDensity.highlight(false); - btnLoadCv.highlight(false); - } - } - -} \ No newline at end of file + private JButton btnSimulate; + private LoaderButton btnLoadCv; + private LoaderButton btnLoadDensity; + + public ProblemToolbar() { + super(); + setFloatable(false); + setLayout(new GridLayout()); + + btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ + add(btnSimulate); + + btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ + btnLoadCv.setDataType(HEAT_CAPACITY); + add(btnLoadCv); + + btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ + btnLoadDensity.setDataType(DENSITY); + add(btnLoadDensity); + + btnSimulate.addActionListener((ActionEvent e) + -> Executors.newSingleThreadExecutor().submit(() + -> ProblemToolbar.plot(e))); + } + + public static void plot(ActionEvent e) { + var instance = TaskManager.getManagerInstance(); + + if (instance.getSelectedTask() == null) { + instance.selectFirstTask(); + } + + var t = instance.getSelectedTask(); + + var calc = t.getCurrentCalculation(); + + t.checkProblems(true); + var status = t.getCurrentCalculation().getStatus(); + + if (status == INCOMPLETE && !status.checkProblemStatementSet()) { + + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), + getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ + ERROR_MESSAGE); + + } else { + try { + ((Solver) calc.getScheme()).solve(calc.getProblem()); + } catch (SolverException se) { + err.println("Solver of " + t + " has encountered an error. Details: "); + se.printStackTrace(); + } + MainGraphFrame.getInstance().plot(); + TaskControlFrame.getInstance().getPulseFrame().plot(calc.getProblem().getPulse()); + } + + } + + public void highlightButtons(boolean highlight) { + if (highlight) { + btnLoadDensity.highlightIfNeeded(); + btnLoadCv.highlightIfNeeded(); + } else { + btnLoadDensity.highlight(false); + btnLoadCv.highlight(false); + } + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java index 148e35d8..84628d7f 100644 --- a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java @@ -2,13 +2,11 @@ import static java.awt.BorderLayout.SOUTH; import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.System.out; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.YES_NO_OPTION; import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.properties.NumericProperties.numberFormat; import static pulse.ui.Messages.getString; import java.awt.BorderLayout; @@ -28,142 +26,125 @@ import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.text.DefaultFormatterFactory; -import javax.swing.text.NumberFormatter; +import pulse.math.Segment; import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyFormatter; import pulse.ui.components.controllers.ConfirmAction; @SuppressWarnings("serial") public class FormattedInputDialog extends JDialog { - private final static int WIDTH = 550; - private final static int HEIGHT = 130; - private JFormattedTextField ftf; - private ConfirmAction confirmAction; - private NumberFormatter numFormatter; - - public FormattedInputDialog(NumericProperty p) { - this.setDefaultCloseOperation(HIDE_ON_CLOSE); - this.setMinimumSize(new Dimension(WIDTH, HEIGHT)); - setLocationRelativeTo(null); - - setTitle("Choose " + p.getAbbreviation(false)); - - var northPanel = new JPanel(); - - northPanel.setBorder(createEmptyBorder(5, 5, 5, 5)); - - northPanel.setLayout(new GridLayout()); - - northPanel.add(new JLabel(p.getDescriptor(true))); - northPanel.add(new JSeparator()); - northPanel.add(ftf = initFormattedTextField(p)); - add(northPanel, BorderLayout.CENTER); - // - var btnPanel = new JPanel(); - var okBtn = new JButton("OK"); - var cancelBtn = new JButton("Cancel"); - btnPanel.add(okBtn); - btnPanel.add(cancelBtn); - add(btnPanel, SOUTH); - // - cancelBtn.addActionListener(event -> { - close(); - }); - okBtn.addActionListener(event -> { - if (!ftf.isEditValid()) { // The text is invalid. - if (userSaysRevert(ftf, numFormatter, p)) // reverted - ftf.postActionEvent(); // inform the editor - } else { - try { - ftf.commitEdit(); - } catch (ParseException e) { - out.println("Could not parse merge value"); - e.printStackTrace(); - } - confirmAction.onConfirm(); - close(); - } - }); - } - - private void close() { - this.setVisible(false); - } - - public Number value() { - return (Number) ftf.getValue(); - } - - private JFormattedTextField initFormattedTextField(NumericProperty p) { - var inputTextField = new JFormattedTextField(p.getValue()); - // Set up the editor for the integer cells. - - var numberFormat = numberFormat(p, true); - numFormatter = new NumberFormatter(numberFormat); - - numFormatter.setMinimum(p.getMinimum().doubleValue()); - numFormatter.setMaximum(p.getMaximum().doubleValue()); - - numFormatter.setFormat(numberFormat); - - inputTextField.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); - inputTextField.setHorizontalAlignment(SwingConstants.CENTER); - inputTextField.setFocusLostBehavior(JFormattedTextField.PERSIST); - - // React when the user presses Enter while the editor is - // active. (Tab is handled as specified by - // JFormattedTextField's focusLostBehavior property.) - inputTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check"); //$NON-NLS-1$ - inputTextField.getActionMap().put("check", new AbstractAction() { //$NON-NLS-1$ - - @Override - public void actionPerformed(ActionEvent e) { - if (!inputTextField.isEditValid()) { // The text is invalid. - if (userSaysRevert(inputTextField, numFormatter, p)) { // reverted - inputTextField.postActionEvent(); // inform the editor - } - } else - try { // The text is valid, - inputTextField.commitEdit(); // so use it. - inputTextField.postActionEvent(); // stop editing - } catch (ParseException exc) { - } - } - }); - - inputTextField.setColumns(10); - return inputTextField; - } - - public void setConfirmAction(ConfirmAction confirmAction) { - this.confirmAction = confirmAction; - } - - public ConfirmAction getConfirmAction() { - return confirmAction; - } - - private static boolean userSaysRevert(JFormattedTextField inputTextField, NumberFormatter numFormatter, - NumericProperty p) { - getDefaultToolkit().beep(); - inputTextField.selectAll(); - Object[] options = { getString("SimpleInputFrame.Edit"), //$NON-NLS-1$ - getString("SimpleInputFrame.Revert") }; //$NON-NLS-1$ - var answer = showOptionDialog(getWindowAncestor(inputTextField), - "The value must be a " + p.getValue().getClass().getSimpleName() + " between " //$NON-NLS-1$ - + numFormatter.getMinimum() + " and " //$NON-NLS-1$ - + numFormatter.getMaximum() + ".\n" //$NON-NLS-1$ - + getString("SimpleInputFrame.MessageLine1") //$NON-NLS-1$ - + getString("SimpleInputFrame.MessageLine2"), //$NON-NLS-1$ - "Invalid Text Entered", //$NON-NLS-1$ - YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - inputTextField.setValue(inputTextField.getValue()); - return true; - } - return false; - } - -} \ No newline at end of file + private final static int WIDTH = 550; + private final static int HEIGHT = 130; + private JFormattedTextField ftf; + private ConfirmAction confirmAction; + + public FormattedInputDialog(NumericProperty p) { + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + this.setMinimumSize(new Dimension(WIDTH, HEIGHT)); + setLocationRelativeTo(null); + + setTitle("Choose " + p.getAbbreviation(false)); + + var northPanel = new JPanel(); + + northPanel.setBorder(createEmptyBorder(5, 5, 5, 5)); + + northPanel.setLayout(new GridLayout()); + + northPanel.add(new JLabel(p.getDescriptor(true))); + northPanel.add(new JSeparator()); + northPanel.add(ftf = initFormattedTextField(p)); + + add(northPanel, BorderLayout.CENTER); + // + var btnPanel = new JPanel(); + var okBtn = new JButton("OK"); + var cancelBtn = new JButton("Cancel"); + btnPanel.add(okBtn); + btnPanel.add(cancelBtn); + add(btnPanel, SOUTH); + // + cancelBtn.addActionListener(event -> { + close(); + }); + okBtn.addActionListener(event -> { + confirmAction.onConfirm(); + close(); + }); + + } + + private void close() { + this.setVisible(false); + } + + private JFormattedTextField initFormattedTextField(NumericProperty p) { + var numFormatter = new NumericPropertyFormatter(p, true, false); + + var inputTextField = new JFormattedTextField(numFormatter); + + inputTextField.setValue(p); + inputTextField.setHorizontalAlignment(SwingConstants.CENTER); + + // React when the user presses Enter while the editor is + // active. (Tab is handled as specified by + // JFormattedTextField's focusLostBehavior property.) + inputTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check"); //$NON-NLS-1$ + inputTextField.getActionMap().put("check", new AbstractAction() { //$NON-NLS-1$ + + @Override + public void actionPerformed(ActionEvent e) { + if (!inputTextField.isEditValid()) { // The text is invalid. + if (userSaysRevert(inputTextField, numFormatter, p)) { // reverted + inputTextField.postActionEvent(); // inform the editor + } + } else + try { // The text is valid, + inputTextField.commitEdit(); // so use it. + inputTextField.postActionEvent(); // stop editing + } catch (ParseException exc) { + } + } + }); + + inputTextField.setColumns(10); + return inputTextField; + } + + public void setConfirmAction(ConfirmAction confirmAction) { + this.confirmAction = confirmAction; + } + + public ConfirmAction getConfirmAction() { + return confirmAction; + } + + public Number value() { + return (Number) ((NumericProperty) ftf.getValue()).getValue(); + } + + private static boolean userSaysRevert(JFormattedTextField inputTextField, NumericPropertyFormatter numFormatter, + NumericProperty p) { + getDefaultToolkit().beep(); + inputTextField.selectAll(); + Object[] options = {getString("SimpleInputFrame.Edit"), //$NON-NLS-1$ + getString("SimpleInputFrame.Revert")}; //$NON-NLS-1$ + var answer = showOptionDialog(getWindowAncestor(inputTextField), + "The value must be a " + p.getValue().getClass().getSimpleName() + " between " //$NON-NLS-1$ + + numFormatter.getBounds().getMinimum() + " and " //$NON-NLS-1$ + + numFormatter.getBounds().getMaximum() + ".\n" //$NON-NLS-1$ + + getString("SimpleInputFrame.MessageLine1") //$NON-NLS-1$ + + getString("SimpleInputFrame.MessageLine2"), //$NON-NLS-1$ + "Invalid Text Entered", //$NON-NLS-1$ + YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); + + if (answer == 1) { // Revert! + inputTextField.setValue(inputTextField.getValue()); + return true; + } + return false; + } + +} From a403c23253d9a8ecc7d7b66fbe9f3dae3a4a3a2b Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 16:03:35 +0100 Subject: [PATCH 087/116] Preview plotting Previously, a XYSplineRenderer was used to interpolated between points for the result plot. However, it did not yield good results, and in most tricky cases it generated a bizarre curve which did not do the interpolation job well. One of the reasons for this could be a poor implementation of natural cubic splines in JFreeChart -- the other could be the general problem associated with badly fitting data with rapidly varying second derivatives. Regardless of that, the new version now constructs a spline interpolation using ApacheCommonsMath, and, for a fixed sample size of 100 points, calculates the discrete points of the curve, which it then renders using a XYLineAndShapeRenderer. In the vast majority of cases, this yields better results. It is worth mentioning that, the routine tries to use the LoessInterpolator first, which will give generally reasonable results for a dataset with noise (or random error). If this fails (if an exception is thrown), then it switches to AkimaSplineInterpolator. The routine will exit if the points are not strictly increasing. In future, this needs to be changed to generate a set of points with uniform spacing, which would then be interpolated. --- .../java/pulse/ui/frames/PreviewFrame.java | 361 +++++++++--------- 1 file changed, 191 insertions(+), 170 deletions(-) diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index 92187481..76a249da 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -5,7 +5,6 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.SOUTH; import static java.awt.Color.BLUE; -import static java.awt.Color.GRAY; import static java.awt.Color.RED; import static org.jfree.chart.ChartFactory.createScatterPlot; import static org.jfree.chart.plot.PlotOrientation.VERTICAL; @@ -17,8 +16,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; -import java.awt.Shape; -import java.awt.geom.Rectangle2D; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; @@ -29,236 +27,259 @@ import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.UIManager; +import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.LoessInterpolator; +import org.apache.commons.math3.exception.NonMonotonicSequenceException; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.renderer.xy.XYErrorRenderer; -import org.jfree.chart.renderer.xy.XYSplineRenderer; import org.jfree.data.xy.XYIntervalSeries; import org.jfree.data.xy.XYIntervalSeriesCollection; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.exception.NumberIsTooSmallException; +import org.apache.commons.math3.exception.OutOfRangeException; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import pulse.tasks.processing.ResultFormat; @SuppressWarnings("serial") public class PreviewFrame extends JInternalFrame { - private final static int FRAME_WIDTH = 640; - private final static int FRAME_HEIGHT = 480; + private final static int FRAME_WIDTH = 640; + private final static int FRAME_HEIGHT = 480; - private List propertyNames; - private JComboBox selectXBox, selectYBox; + private List propertyNames; + private JComboBox selectXBox, selectYBox; - private static String xLabel, yLabel; + private static String xLabel, yLabel; - private double[][][] data; - private static JFreeChart chart; + private double[][][] data; + private static JFreeChart chart; - private final static Color RESULT_COLOR = BLUE; - private final static Color SMOOTH_COLOR = RED; + private final static Color RESULT_COLOR = BLUE; + private final static Color SMOOTH_COLOR = RED; - private static boolean drawSmooth = true; + private static boolean drawSmooth = true; - private final static int ICON_SIZE = 24; + private final static int ICON_SIZE = 24; + private final static int MARKER_SIZE = 6; + private final static int SPLINE_SAMPLES = 100; - public PreviewFrame() { - super("Preview Plotting", true, true, true, true); - init(); - } + public PreviewFrame() { + super("Preview Plotting", true, true, true, true); + init(); + } - private void init() { - setSize(FRAME_WIDTH, FRAME_HEIGHT); - setDefaultCloseOperation(HIDE_ON_CLOSE); + private void init() { + setSize(FRAME_WIDTH, FRAME_HEIGHT); + setDefaultCloseOperation(HIDE_ON_CLOSE); - getContentPane().setLayout(new BorderLayout()); + getContentPane().setLayout(new BorderLayout()); - getContentPane().add(createEmptyPanel(), CENTER); + getContentPane().add(createEmptyPanel(), CENTER); - var toolbar = new JToolBar(); - toolbar.setFloatable(false); - toolbar.setLayout(new GridLayout()); + var toolbar = new JToolBar(); + toolbar.setFloatable(false); + toolbar.setLayout(new GridLayout()); - getContentPane().add(toolbar, SOUTH); + getContentPane().add(toolbar, SOUTH); - var selectX = new JLabel("Bottom axis: "); - toolbar.add(selectX); + var selectX = new JLabel("Bottom axis: "); + toolbar.add(selectX); - selectXBox = new JComboBox<>(); + selectXBox = new JComboBox<>(); + selectXBox.setFont(selectXBox.getFont().deriveFont(11)); - toolbar.add(selectXBox); - toolbar.add(new JSeparator()); + toolbar.add(selectXBox); + toolbar.add(new JSeparator()); - var selectY = new JLabel("Vertical axis:"); - toolbar.add(selectY); + var selectY = new JLabel("Vertical axis:"); + toolbar.add(selectY); - selectYBox = new JComboBox<>(); - toolbar.add(selectYBox); + selectYBox = new JComboBox<>(); + selectYBox.setFont(selectYBox.getFont().deriveFont(11)); + toolbar.add(selectYBox); - var drawSmoothBtn = new JToggleButton(); - drawSmoothBtn.setToolTipText("Smooth with cubic normal splines"); - drawSmoothBtn.setIcon(loadIcon("spline.png", ICON_SIZE)); - drawSmoothBtn.setSelected(true); - toolbar.add(drawSmoothBtn); + var drawSmoothBtn = new JToggleButton(); + drawSmoothBtn.setToolTipText("Smooth with cubic normal splines"); + drawSmoothBtn.setIcon(loadIcon("spline.png", ICON_SIZE)); + drawSmoothBtn.setSelected(true); + toolbar.add(drawSmoothBtn); - drawSmoothBtn.addActionListener(e -> { - drawSmooth = drawSmoothBtn.isSelected(); - replot(chart); - }); + drawSmoothBtn.addActionListener(e -> { + drawSmooth = drawSmoothBtn.isSelected(); + replot(chart); + }); - selectXBox.addItemListener(e -> replot(chart)); - selectYBox.addItemListener(e -> replot(chart)); - this.setDefaultCloseOperation(EXIT_ON_CLOSE); - } + selectXBox.addItemListener(e -> replot(chart)); + selectYBox.addItemListener(e -> replot(chart)); + this.setDefaultCloseOperation(EXIT_ON_CLOSE); + } - private void replot(JFreeChart chart) { - var selectedX = selectXBox.getSelectedIndex(); - var selectedY = selectYBox.getSelectedIndex(); + private void replot(JFreeChart chart) { + int selectedX = selectXBox.getSelectedIndex(); + int selectedY = selectYBox.getSelectedIndex(); - if (selectedX < 0 || selectedY < 0) - return; + if (selectedX < 0 || selectedY < 0) { + return; + } - xLabel = propertyNames.get(selectedX); - yLabel = propertyNames.get(selectedY); + var plot = chart.getXYPlot(); - var plot = chart.getXYPlot(); + plot.setDataset(0, null); + plot.setDataset(1, null); - plot.setDataset(0, null); - plot.setDataset(1, null); + var dataset = new XYIntervalSeriesCollection(); - plot.getDomainAxis().setLabel(xLabel); - plot.getRangeAxis().setLabel(yLabel); + if (data == null) { + return; + } - var dataset = new XYIntervalSeriesCollection(); - var datasetSmooth = new XYSeriesCollection(); + dataset.addSeries(series(data[selectedX][0], data[selectedX][1], data[selectedY][0], data[selectedY][1])); + plot.setDataset(0, dataset); - if (data == null) - return; + if (drawSmooth) { + drawSmooth(plot, selectedX, selectedY); + } + + } - dataset.addSeries(series(data[selectedX][0], data[selectedX][1], data[selectedY][0], data[selectedY][1])); - plot.setDataset(0, dataset); + private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { + PolynomialSplineFunction interpolation = null; - if (drawSmooth) { - datasetSmooth.addSeries(series(data[selectedX][0], data[selectedY][0])); - plot.setDataset(1, datasetSmooth); - } - - } - - public void update(ResultFormat fmt, double[][][] data) { - this.data = data; - var descriptors = fmt.descriptors(); - List htmlDescriptors = new ArrayList<>(); - var size = descriptors.size(); - - propertyNames = new ArrayList<>(size); - String tmp; - - for (var i = 0; i < size; i++) { - tmp = descriptors.get(i).replaceAll("<.*?>", " ").replaceAll("&.*?;", ""); - htmlDescriptors.add("" + descriptors.get(i) + ""); - propertyNames.add(tmp); - } - - selectXBox.removeAllItems(); - - for (var s : htmlDescriptors) { - selectXBox.addItem(s); - } - - selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); - - selectYBox.removeAllItems(); - - for (var s : htmlDescriptors) { - selectYBox.addItem(s); - } - - selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); - } - - /* + try { + //LOESS interpolator for monotonic x sequence (average results) + //usually works when number of points is large + var interpolator = new LoessInterpolator(); + interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); + } catch (DimensionMismatchException | NumberIsTooSmallException e) { + //Akima spline for small number of points + var interpolator = new AkimaSplineInterpolator(); + interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); + } catch( NonMonotonicSequenceException e) { + //do not draw if points not strictly increasing + return; + } + + double[] x = new double[SPLINE_SAMPLES]; + double[] y = new double[SPLINE_SAMPLES]; + + double dx = (data[selectedX][0][data[selectedX][0].length - 1] - data[selectedX][0][0]) / (SPLINE_SAMPLES - 1); + + for (int i = 0; i < SPLINE_SAMPLES; i++) { + x[i] = data[selectedX][0][0] + dx * i; + try { + y[i] = interpolation.value(x[i]); + } catch(OutOfRangeException e) { + y[i] = Double.NaN; + } + } + + var datasetSmooth = new XYSeriesCollection(); + datasetSmooth.addSeries(series(x, y)); + plot.setDataset(1, datasetSmooth); + } + + public void update(ResultFormat fmt, double[][][] data) { + this.data = data; + var descriptors = fmt.descriptors(); + var size = descriptors.size(); + + propertyNames = new ArrayList<>(size); + String tmp; + + selectXBox.removeAllItems(); + selectYBox.removeAllItems(); + + for (var s : descriptors) { + selectXBox.addItem(s); + selectYBox.addItem(s); + } + + selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); + selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); + } + + /* * - */ - - private static ChartPanel createEmptyPanel() { - chart = createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false); + */ + private static ChartPanel createEmptyPanel() { + chart = createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false); - var renderer = new XYErrorRenderer(); - renderer.setSeriesPaint(0, RESULT_COLOR); + var renderer = new XYErrorRenderer(); + renderer.setSeriesPaint(0, RESULT_COLOR); + renderer.setDefaultShapesFilled(false); - var rendererSpline = new XYSplineRenderer(); - rendererSpline.setSeriesPaint(0, SMOOTH_COLOR); - rendererSpline.setSeriesStroke(0, - new BasicStroke(2.0f, CAP_ROUND, JOIN_ROUND, 1.0f, new float[] { 6.0f, 6.0f }, 0.0f)); + var rendererLine = new XYLineAndShapeRenderer(); + rendererLine.setDefaultShapesVisible(false); + rendererLine.setSeriesPaint(0, SMOOTH_COLOR); + rendererLine.setSeriesStroke(0, + new BasicStroke(2.0f, CAP_ROUND, JOIN_ROUND, 1.0f, new float[]{6.0f, 6.0f}, 0.0f)); - var size = 6.0; - var delta = size / 2.0; - Shape shape1 = new Rectangle2D.Double(-delta, -delta, size, size); - renderer.setSeriesShape(0, shape1); + var plot = chart.getXYPlot(); - var plot = chart.getXYPlot(); + plot.setRenderer(0, renderer); + plot.setRenderer(1, rendererLine); - plot.setRenderer(0, renderer); - plot.setRenderer(1, rendererSpline); + //plot.setRangeGridlinesVisible(false); + //plot.setDomainGridlinesVisible(false); - plot.setRangeGridlinesVisible(true); - plot.setRangeGridlinePaint(GRAY); + plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); + plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); + plot.getRenderer(0).setSeriesShape(0, + new Rectangle(-MARKER_SIZE/2, -MARKER_SIZE/2, MARKER_SIZE, MARKER_SIZE)); - plot.setDomainGridlinesVisible(true); - plot.setDomainGridlinePaint(GRAY); + chart.removeLegend(); - plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); - plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); + var cp = new ChartPanel(chart); - chart.removeLegend(); + cp.setMaximumDrawHeight(2000); + cp.setMaximumDrawWidth(2000); + cp.setMinimumDrawWidth(10); + cp.setMinimumDrawHeight(10); - var cp = new ChartPanel(chart); + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + + return cp; + } - cp.setMaximumDrawHeight(2000); - cp.setMaximumDrawWidth(2000); - cp.setMinimumDrawWidth(10); - cp.setMinimumDrawHeight(10); - - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); - - return cp; - } - - /* + /* * - */ + */ + private static XYIntervalSeries series(double[] x, double[] xerr, double[] y, double[] yerr) { + var series = new XYIntervalSeries("Preview"); - private static XYIntervalSeries series(double[] x, double[] xerr, double[] y, double[] yerr) { - var series = new XYIntervalSeries("Preview"); + for (var i = 0; i < x.length; i++) { + series.add(x[i], x[i] - xerr[i], x[i] + xerr[i], y[i], y[i] - yerr[i], y[i] + yerr[i]); + } - for (var i = 0; i < x.length; i++) { - series.add(x[i], x[i] - xerr[i], x[i] + xerr[i], y[i], y[i] - yerr[i], y[i] + yerr[i]); - } + return series; + } - return series; - } - - /* + /* * - */ - - private static XYSeries series(double[] x, double[] y) { - var series = new XYSeries("Preview"); + */ + private static XYSeries series(double[] x, double[] y) { + var series = new XYSeries("Preview"); - for (var i = 0; i < x.length; i++) { - series.add(x[i], y[i]); - } + for (var i = 0; i < x.length; i++) { + series.add(x[i], y[i]); + } - return series; - } + return series; + } - public boolean isDrawSmooth() { - return drawSmooth; - } + public boolean isDrawSmooth() { + return drawSmooth; + } - public void setDrawSmooth(boolean drawSmooth) { - PreviewFrame.drawSmooth = drawSmooth; - } + public void setDrawSmooth(boolean drawSmooth) { + PreviewFrame.drawSmooth = drawSmooth; + } -} \ No newline at end of file +} From 7d70aa51f942a52f7529fa8694975992000951b3 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 16:07:13 +0100 Subject: [PATCH 088/116] Introduced the listedKeywords() method In PropertyHolder, this acts similarly to listedTypes(), except that it produces a Set, containing only unique elements. Similarly to listedTypes(), this method tries to access children property holders and extract their listed keywords. --- src/main/java/pulse/util/PropertyHolder.java | 45 ++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index 34410ad3..8238a339 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -1,9 +1,13 @@ package pulse.util; import static java.util.stream.Collectors.toList; +import static pulse.properties.NumericProperties.def; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -24,24 +28,39 @@ public abstract class PropertyHolder extends Accessible { /** *

* By default, this will search the children of this {@code PropertyHolder} - * to collect the types of their listed parameters recursively. Note this - * method is used only to retrieve the type and not the data! + * to collect the types of their listed numeric parameters recursively. *

* - * @return a list of {@code Property} instances, which have been explicitly - * marked as a listed parameter for this {@code PropertyHolder}. + * @return a set of {@code NumericPropertyKeyword} instances, which have + * been explicitly marked as a listed parameter for this + * {@code PropertyHolder}. */ - public List listedTypes() { + public Set listedKeywords() { - List properties = new ArrayList<>(); + Set keys = new HashSet<>(); - for (var accessible : accessibleChildren()) { - if (accessible instanceof PropertyHolder) { - properties.addAll(((PropertyHolder) accessible).listedTypes()); - } - } + accessibleChildren().stream() + .filter(accessible -> (accessible instanceof PropertyHolder)) + .forEachOrdered(accessible -> { + keys.addAll(((PropertyHolder) accessible).listedKeywords()); + }); + + return keys; + + } - return properties; + /** + *

+ * By default, collects a list of default properties corresponding to types + * defined by listedKeywords(). However, this method is overridable to + * include non-numeric properties. + *

+ * + * @return a list of {@code Property} instances, which have been explicitly + * marked as a listed parameter for this {@code PropertyHolder}. + */ + public List listedTypes() { + return listedKeywords().stream().map(key -> def(key)).collect(Collectors.toList()); } public PropertyHolder() { @@ -66,7 +85,7 @@ public boolean isListedNumericType(NumericPropertyKeyword p) { return false; } - return parameters.stream().filter(pr -> pr instanceof NumericProperty) + return listedTypes().stream().filter(pr -> pr instanceof NumericProperty) .anyMatch(param -> ((NumericProperty) param).getType() == p); } From 517b9134336f8e47a4b241aa7268b89d18a642ce Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 16:09:12 +0100 Subject: [PATCH 089/116] Changes to NumericProperty.xml and string constants in messages.properties Changed min/max values of some properties. Fixed the 'default-search-variable' attribute not appearing where it should have appeared, and being present where it is not required. Added some string constants. --- src/main/java/pulse/ui/Messages.java | 25 ++-- src/main/resources/NumericProperty.xml | 156 +++++++++++-------------- src/main/resources/messages.properties | 13 ++- 3 files changed, 85 insertions(+), 109 deletions(-) diff --git a/src/main/java/pulse/ui/Messages.java b/src/main/java/pulse/ui/Messages.java index 9de6ff73..169c9f34 100644 --- a/src/main/java/pulse/ui/Messages.java +++ b/src/main/java/pulse/ui/Messages.java @@ -6,18 +6,19 @@ import java.util.ResourceBundle; public class Messages { - private static final String BUNDLE_NAME = "messages"; - private static final ResourceBundle RESOURCE_BUNDLE = getBundle(BUNDLE_NAME); + private static final String BUNDLE_NAME = "messages"; - private Messages() { - } + private static final ResourceBundle RESOURCE_BUNDLE = getBundle(BUNDLE_NAME); - public static String getString(String key) { - try { - return RESOURCE_BUNDLE.getString(key); - } catch (MissingResourceException e) { - return '!' + key + '!'; - } - } -} \ No newline at end of file + private Messages() { + } + + public static String getString(String key) { + try { + return RESOURCE_BUNDLE.getString(key); + } catch (MissingResourceException e) { + return '!' + key + '!'; + } + } +} diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 6daaac70..89dca9d3 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -2,51 +2,51 @@ + maximum="10" minimum="0" primitive-type="double" value="0.1" default-search-variable="true"> + discreet="false" keyword="BASELINE_PHASE_SHIFT" maximum="6.28" + minimum="0" value="0.1" primitive-type="double" + default-search-variable="true"> + default-search-variable="true"> + primitive-type="double" value="0.14"> + primitive-type="double" value="15.0"> + primitive-type="double" value="0.425"> + primitive-type="int" value="10"> + primitive-type="int" value="10"> + maximum="10000" minimum="1" primitive-type="int" value="250"> + maximum="10" minimum="1E-3" primitive-type="double" value="0.5"> + maximum="2" minimum="1" value="1.7" primitive-type="double"> + maximum="4096" minimum="8" primitive-type="int" value="8"> + maximum="5" minimum="1.0" primitive-type="double" value="3.0"> + maximum="3.0" minimum="1.1" value="1.5" primitive-type="double"> + value="1E-3" primitive-type="double"> + value="1E-2" primitive-type="double"> + primitive-type="double" value="1e-12"> + maximum="64" minimum="2" primitive-type="int" value="8"> + maximum="1E-3" minimum="1E-10" primitive-type="double" value="1E-4"> + maximum="32" minimum="2" value="2" primitive-type="int"> + primitive-type="double"> + maximum="100" minimum="2" primitive-type="double" value="3.5"> + maximum="1024" minimum="8" primitive-type="int" value="64"> + maximum="1" minimum="0.5" primitive-type="double" value="0.8"> + primitive-type="double" value="1"> + primitive-type="double" value="0"> + maximum="0.5" minimum="0.00001" primitive-type="double" value="0.001"> + minimum="1.0E-6" value="0.001" primitive-type="double" discreet="true"> + minimum="1.0E-6" value="0.01" primitive-type="double" discreet="true"/> + discreet="false"> + /> + /> + /> + minimum="1.0E-4" value="0.01" primitive-type="double" discreet="true"> + minimum="1.0E-6" value="1.0" primitive-type="double" discreet="true"/> + minimum="1.0" value="0.0" primitive-type="double" discreet="false"> + minimum="1.0" value="0.0" primitive-type="double" discreet="false"/> + value="0.8" primitive-type="double" discreet="false"/> + minimum="10.0" value="0.0" primitive-type="double" discreet="false"/> + minimum="0.01" value="5.0" primitive-type="double" discreet="false"/> + minimum="0.0" value="298.0" primitive-type="double" discreet="false"/> + discreet="false"> + discreet="false"/> + discreet="false"/> + discreet="false"/> + discreet="false"/> + discreet="false" /> + discreet="false"> + maximum="200" minimum="1" primitive-type="int" value="10"> + maximum="1000" discreet="false"> + minimum="1E-4" value="0.25" discreet="false"> + minimum="-1e10" primitive-type="double" value="Infinity"> + value="Infinity"> + primitive-type="double" value="1.0"> diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b71d01ab..f88018b7 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,14 +1,14 @@ TaskControlFrame.SoftwareTitle=PULsE TaskControlFrame.AboutDialog=About PULsE NumericProperty.XMLFile=/NumericProperty.xml -NumericProperty.PlusMinus=\ ± +NumericProperty.PlusMinus=\ \u00b1 NumericProperty.BigNumberFormat=0.0\#\#\#E00 NumericProperty.NumberFormat=\#\#\#0.0\#\#\#\# Identifier.Tag=Task HeatingCurve.3=Heating curve: Number of data points -HeatingCurve.4=Baseline (mV or °) +HeatingCurve.4=Baseline (mV or \u00b0) HeatingCurve.6=Time (s) -HeatingCurve.7=Temperature (mV or °) +HeatingCurve.7=Temperature (mV or \u00b0) ExperimentalData.HalfRiseError=Failed to calculate half-rise time from data. Check data / metadata for possible problems. Switching to rough approximation now. DataLogEntry.AccessError=Problems when accessing value of the parameter DataLogEntry.BigNumberFormat=0.0\#\#E00 @@ -139,7 +139,7 @@ TaskTable.R2=R2 TaskTable.SS2=&\#8721i(Tiexp - Ti)2 TaskTable.Status=Status TaskTable.TaskID=Task ID -TaskTable.Temperature=Temperature,
T0 (K) +TaskTable.Temperature=Temperature,
T0 (°C) TaskTablePopupMenu.11=Error TaskTablePopupMenu.12=Missing heat curve\! TaskTablePopupMenu.13=Error @@ -195,7 +195,7 @@ RangeSelectionFrame.RangeSelectorTitle=Range Selector RangeSelectionFrame.TextFieldFont=Arial RangeSelectionFrame.TimeLimitMax=Time (MAX) RangeSelectionFrame.TimeLimitMin=Time (MIN) -RangeSelectionFrame.ConfirmationMessage1=This will change the time domain for the reverse solution of the heat problem. +RangeSelectionFrame.ConfirmationMessage1=This will change the time domain for the solution of the inverse heat problem. RangeSelectionFrame.ConfirmationMessage2=Current range: RangeSelectionFrame.ConfirmationMessage3=New range: SearchOptionsFrame.ListFont=Tahoma @@ -281,4 +281,5 @@ MixedScheme.3=Problem not supported or unknown: MixedScheme.4=Symmetric Semi-Implicit Scheme
  • Order of approximation O(h2 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Weight is set to 0.5
MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
TextWrap.0=

-TextWrap.1=

\ No newline at end of file +TextWrap.1=

+TextWrap.2=

\ No newline at end of file From 6746ce578fde3b0edfdeeb2c5f22a8326a781301 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 14 Dec 2021 16:10:08 +0100 Subject: [PATCH 090/116] v1.93 Release version --- pom.xml | 2 +- src/main/java/pulse/ui/Launcher.java | 230 +++++++++++++-------------- src/main/resources/Version.txt | 2 +- src/main/resources/images/splash.png | Bin 67732 -> 68380 bytes 4 files changed, 117 insertions(+), 117 deletions(-) diff --git a/pom.xml b/pom.xml index 68c0076b..a71c065e 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.92 + 1.93 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index f3528731..65e31e44 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -29,121 +29,121 @@ *

* */ - public class Launcher { - private PrintStream errStream; - private File errorLog; - private final static boolean DEBUG = false; - - private Launcher() { - if(!DEBUG) - arrangeErrorOutput(); - arrangeMessages(); - } - - /** - * Launches the application and creates a GUI. - */ - - public static void main(String[] args) { - new Launcher(); - splashScreen(); - - WebLookAndFeel.install(WebDarkSkin.class); - try { - UIManager.setLookAndFeel(new WebLookAndFeel()); - } catch (Exception ex) { - System.err.println("Failed to initialize LaF"); - } - - var newVersion = Version.getCurrentVersion().checkNewVersion(); - - /* Create and display the form */ - invokeLater(() -> { - getInstance().setLocationRelativeTo(null); - getInstance().setVisible(true); - - if (newVersion != null) { - JOptionPane.showMessageDialog(null, "A new version of this software is available: " - + newVersion.toString() + "
Please visit the PULsE website for more details."); - } - - }); - } - - private static void splashScreen() { - var splash = getSplashScreen(); - if (splash == null) - err.println("SplashScreen.getSplashScreen() returned null"); - else { - var g = splash.createGraphics(); - requireNonNull(g, "splash.createGraphics() returned null"); - } - } - - private void arrangeErrorOutput() { - String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); - String decodedPath = ""; - // - try { - decodedPath = URLDecoder.decode(path, "UTF-8"); - } catch (UnsupportedEncodingException e1) { - System.err.println("Unsupported UTF-8 encoding. Details below."); - e1.printStackTrace(); - } - // - try { - var dir = new File(decodedPath).getParent(); - errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); - setErr(new PrintStream(errorLog) { - - @Override - public void println(String str) { - super.println(str); - JOptionPane.showMessageDialog(null, "An exception has occurred. " - + "Please check the stored log!", "Exception", JOptionPane.ERROR_MESSAGE); - } - - } - ); - } catch (FileNotFoundException e) { - System.err.println("Unable to set up error stream"); - e.printStackTrace(); - } - - createShutdownHook(); - - } - - private void arrangeMessages() { - System.setOut( new PrintStream(System.out) { - - @Override - public void println(String str) { - JOptionPane.showMessageDialog(null, Messages.getString("TextWrap.0") + str + Messages.getString("TextWrap.0")); - } - - }); - } - - private void createShutdownHook() { - - /* + private PrintStream errStream; + private File errorLog; + private final static boolean DEBUG = false; + + private Launcher() { + if (!DEBUG) { + arrangeErrorOutput(); + } + arrangeMessages(); + } + + /** + * Launches the application and creates a GUI. + */ + public static void main(String[] args) { + new Launcher(); + splashScreen(); + + WebLookAndFeel.install(WebDarkSkin.class); + try { + UIManager.setLookAndFeel(new WebLookAndFeel()); + } catch (Exception ex) { + System.err.println("Failed to initialize LaF"); + } + + var newVersion = Version.getCurrentVersion().checkNewVersion(); + + /* Create and display the form */ + invokeLater(() -> { + getInstance().setLocationRelativeTo(null); + getInstance().setVisible(true); + + if (newVersion != null) { + JOptionPane.showMessageDialog(null, "A new version of this software is available: " + + newVersion.toString() + "
Please visit the PULsE website for more details."); + } + + }); + } + + private static void splashScreen() { + var splash = getSplashScreen(); + if (splash == null) { + err.println("SplashScreen.getSplashScreen() returned null"); + } else { + var g = splash.createGraphics(); + requireNonNull(g, "splash.createGraphics() returned null"); + } + } + + private void arrangeErrorOutput() { + String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + String decodedPath = ""; + // + try { + decodedPath = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e1) { + System.err.println("Unsupported UTF-8 encoding. Details below."); + e1.printStackTrace(); + } + // + try { + var dir = new File(decodedPath).getParent(); + errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); + setErr(new PrintStream(errorLog) { + + @Override + public void println(String str) { + super.println(str); + JOptionPane.showMessageDialog(null, "An exception has occurred. " + + "Please check the stored log!", "Exception", JOptionPane.ERROR_MESSAGE); + } + + } + ); + } catch (FileNotFoundException e) { + System.err.println("Unable to set up error stream"); + e.printStackTrace(); + } + + createShutdownHook(); + + } + + private void arrangeMessages() { + System.setOut(new PrintStream(System.out) { + + @Override + public void println(String str) { + JOptionPane.showMessageDialog(null, Messages.getString("TextWrap.0") + + str + Messages.getString("TextWrap.1")); + } + + }); + } + + private void createShutdownHook() { + + /* * Delete log file on program exit if empty - */ - - Runnable r = () -> { - if (errorLog != null && errorLog.exists() && errorLog.length() < 1) - errorLog.delete(); - }; - Runtime.getRuntime().addShutdownHook(new Thread(r)); - - } - - @Override - public void finalize() { - errStream.close(); - } - -} \ No newline at end of file + */ + Runnable r = () -> { + if (errorLog != null && errorLog.exists() && errorLog.length() < 1) { + errorLog.delete(); + } + }; + Runtime.getRuntime().addShutdownHook(new Thread(r)); + + } + + @Override + public void finalize() { + errStream.close(); + } + +} diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 7484b4f1..183cc88a 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.92 \ No newline at end of file +1.93 \ No newline at end of file diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index 5ff58dda6d92d62620fd1509a033c87bcc3ade32..1f835bc22aac37039b552466abb8cc07a62746b8 100644 GIT binary patch literal 68380 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

12Ay3&-%qlZ z$Nr=vi@Txf;+|4%`#~z+QKSyzo24WAKO1^Fk#hm=PXy^h2)Jt&=mL!@{Ev^?ic?Y< z=&sc4PaVV8@3DSzYLY0)fzTe_`IZ86RpRxyjyD6x)W#=F6HE7MV-r1L4pv@1$QQrlTj3S(5sS*2_yaL*?}u{Ic?{;2DDHRP7JR{M*~S98WqD|hS; zpabMucN=XloeSGMW+VCy^}V)d?r)qj-HCjh4kro!tFRq13iAURoph-0OmM1?`v$4Z zWPxg=8R@dpCDJJT*#8H@$E|)o4hLFhIAM_2G196}X_$%w>DM|aq_Cs_d>}ZfO+oXu z5icc(tTO-xPW-ZHD*v5lb#qTSev*5d<30-2jw4{9r@r~A&i3l!%%E=~>mO^Ad7|k8 zY$Opv$=&?e)6jd3&ETu3lIzcVT^J7Oq$msmqd`@Rk%MZxCxK%GyfDdJ13bG<=uBJ; z!1ZHLpDXlqP4O}bdMo`pik^?TP=FLyy064di`1~+Hg!aQ$n`Tpj;V|1e>o3$BU#9P zOyoMfH2j0g)`+HWuoH0B@xBA|_|aSGClj`w{a=V9eqJKWiXV7t!B!aHEd3d<(|_-2 zXtq2s5x)JA*^OaTKOg~{_3%9P#NVgKX)F*LqvDFf@Bc{;rIG*-FwKl)RZSh_i~WCy z7J0Bha>9Vi26UI`Q8e^@aWAzwd#?n)LK3pzb!n-E)GEx;MY>$DL-KwqRjot)b<=s) zAA@3YrA>{db-L1unFWs?-e4=c(wZE-`Y$(^|LehVj0WYx0}&D%6}A5GQ!Y1f%y_VG z?<{`j^X>`B+4+YKeTADiQ<-Gb9j)DmQZ)3Jbrg^>i)a(5^shW+z<4p-gK3jy2VUB*=HF20A)miD=gf{i-?-VmWNX!iqTO8GI zrXdXiO%V82Qqwx)Z~4Uk^lAPcNe$_E9ZqtBYIK7n;j8zIsx1j7o>3rV=rpfjNg1fR znq{;j&o&Oom~Xz{LUr&_XtIG(1a}2BZhf+7VkngJ-@X-#68SPrL`}7)B7KQZ<1$oj zcM1+)hbk>KAMjMBHzHiL%F)PQ6?7mFk&vzC41@wW-0qBF>zuiesHzx7dN}`rhWK!S z3sb(dhJy z9MC5Y3EHPn|1}a`V08>2%@|+>nS~te1R#PT{A7)~>Of?$R_}gWn<6qR{*$>>x!U~w zPlNERK;rRC3OZz``^8 zz_CDnhp!!4F2YsF*7777v6#T!BMfANaqv5fk_&>!psXv=mwwBHb$&Qc?ma_f7QcU- zb^cGk>_CPXAag(rL>_V?I%$o_OGI7RGx2vjm4G#SnS#-gFj1u`&s``i((YmltEe3% zJ~xtEyDmr;&qfk_!9BY7?Ef9aE9&4tI z>jc!FQ3&7O9Bfwdybs?Z{5$M0pd29jrRU*KIf|ERaCr&1UGnQeGF2$IUtZoED9pf2 zOQxL}f2bF(d^TW{;UgfFMqS3f6rThnJL~qf7OLT+z5knR#|6m!GbY7%LqygQ z$#(;jt_SG*ejlzp0q~`VitTz|)cF-H6IPPDXA*ITrYDs_#Zt*x;@0mn$dt6nHi84P zm!uJ)Kq2y)zwQBl;{W|s0gyu91*tG_DN%5V6}#bBaUGO-p;0<7ZReX1X%lDp+*u%X z2?JO=I|%3jKk7)Z`g&9=wvQu5FZmUPl&k6;la*GA(t3*2Y{hhDm7>AE{@W?)-(R;7 zF3z+2Wr+lo^9HlR_8yPBY?OYiLoE$+qf*qK(i-6&=V2#+<~>D)fRa_cRr`7(~~Tl8X!ZSAmVVUH)(aD-94WuA zmjpaCy7r&Wl~z^;RwtEYA?DyilESAq7@Ik%g=z&ZZoGGu_tu5H6)AD2-Fh4|&r5W~ z*$H%Tn)t=6Jte_ghkB}fDbO;H*Nd^*n^$?mkL&$dOm8n;DNL*f!6vP|YaPB7Stpx%>^ShjnuGqNZ zm2iGR_iFXy3<5{378eF@TP*kG4;zXX`y8Yd4oFTgW7xY=%T;NSYtheD>Fy2+tJ-+X zZ4M;?XLDHF@5HRQq~|!dO7PX@lLa=DncDx@G%Qh5GSpZ#>xN2?<3y{5?1bji(P~E* zd^>d{<%Nf9*Gj*awSNPz>M~gswXWuU%@$k1Vkmbo#o}FxAshSj923*cKi;F%PPgQM z8GFlLiHN&Q&DIfJht_iQ8Ols`cAsVI&BL|xKP#)pl?ih@J?`z8am8h7?No_rrvsio zbC^4uxBZim!}9hu*CqcstzY_Fh2u@6^QynuVSBDlKf-n4ztCj26&YQI!0R%OCzwfA zW$nLqoxiHfWW927?aB6Yk@X&>UeXdgH*X(TII3XScJ!9c{jD>m-Ur4&vtoYxl*GsDLmw2(G&TEg|!#_{9`0G+DIA24%a4tS~UJO3oBMUi%Eyr9gu`Sk@#t*hOvQe)FIcIhVuk zV7k%Vz{$KwjCGUOGpzC&?G1+W6Av}`NL{MUj4Jc&|6b?&N4+xtY1Z#kiRZjgYFAJp zUt-B}V&lp^6>)$58`>@2_F0XGWB1SYm$h+;?zwFLrekx0M(ct9%v>3!)lBmPWf*|K M)78&qol`;+0N~BM(EtDd literal 67732 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⪚;tzjW9a?17Kh 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

BXiAHr-U#3O90kFIz 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 Date: Wed, 15 Dec 2021 21:04:27 +0100 Subject: [PATCH 091/116] v1.93F Fixes Abstract data Made the fields protected for easier access by subclasses ExportManager Introduced a current working directory field, so that the file chooser keeps track of the browsing history. Exporter Fixed directory not updating after selection in the file chooser NetzschCSVReader / PulseCSVReader Added support to different locales (delimiter chars and decimal separator). ReaderManager Importing all files in a directory (e.g. with Linseis format) now invokes an Execution Service to take advantage of the concurrency. Problem It is now possible to select sample thickness as an optimisation variable. NumericPropertyFormatter Added missing condition to skip scientific formatting if html had been disabled. Normality tests Minor fixes to logic Status Prevent status from updating to QUEUED when task is in progress or has failed Launcher Removed pop-up exception windows completely, as this caused uncontrollable breeding of pop-up windows Chart Fixed value markers not taking into account the numerical conversion factor (ms -> s and vice versa). MouseOnMarkerListener Fixed wrong concurrent updates of both value markers when switching statuses. MainGraphFrame Prevented tasks from plotting while being in progress. Avoids ConcurrentModificationException. ExportDialog Changed from Save dialog to Open dialog to avoid ambiguity Other minors changes --- pom.xml | 2 +- src/main/java/pulse/AbstractData.java | 4 +- .../java/pulse/input/ExperimentalData.java | 4 +- .../java/pulse/io/export/ExportManager.java | 5 +- src/main/java/pulse/io/export/Exporter.java | 53 ++++++------ .../java/pulse/io/readers/AbstractReader.java | 1 + .../pulse/io/readers/NetzschCSVReader.java | 86 +++++++++++++------ .../io/readers/NetzschPulseCSVReader.java | 27 +++--- .../java/pulse/io/readers/ReaderManager.java | 33 ++++++- .../pulse/problem/statements/Problem.java | 11 +++ .../properties/NumericPropertyFormatter.java | 4 +- .../properties/NumericPropertyKeyword.java | 11 +-- .../pulse/search/direction/ActiveFlags.java | 4 +- .../statistics/AndersonDarlingTest.java | 7 +- .../java/pulse/search/statistics/KSTest.java | 7 +- .../search/statistics/NormalityTest.java | 23 ++--- src/main/java/pulse/tasks/Calculation.java | 3 +- src/main/java/pulse/ui/Launcher.java | 12 +-- src/main/java/pulse/ui/components/Chart.java | 12 +-- .../listeners/MouseOnMarkerListener.java | 32 ++++--- .../java/pulse/ui/frames/MainGraphFrame.java | 4 +- .../pulse/ui/frames/SearchOptionsFrame.java | 2 +- .../pulse/ui/frames/dialogs/ExportDialog.java | 17 ++-- src/main/resources/NumericProperty.xml | 20 ++--- src/main/resources/Version.txt | 2 +- 25 files changed, 223 insertions(+), 163 deletions(-) diff --git a/pom.xml b/pom.xml index a71c065e..88351e32 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.93 + 1.93F PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index f1f5ab84..2f58d47a 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -32,8 +32,8 @@ public abstract class AbstractData extends PropertyHolder { private int count; - private List time; - private List signal; + protected List time; + protected List signal; private String name; diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 3b2e5bd4..bbbb2e6a 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -419,7 +419,7 @@ private void doSetRange() { } /** - * Retrieves the + * Retrieves the time limit. * * @see pulse.problem.schemes.DifferenceScheme * @return a double, equal to the last element of the {@code time List}. @@ -427,6 +427,6 @@ private void doSetRange() { @Override public double timeLimit() { return timeAt(indexRange.getUpperBound()); - } + } } diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index ec8c81a1..2064c501 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -21,6 +21,9 @@ * */ public class ExportManager { + + //current working dir + private static File cwd = null; private ExportManager() { // intentionally blank @@ -85,7 +88,7 @@ public static Exporter findExporter(Class target) public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { var exporter = findExporter(target); if (exporter != null) { - exporter.askToExport(target, parentWindow, fileTypeLabel); + cwd = exporter.askToExport(target, parentWindow, fileTypeLabel, cwd); } else { throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); } diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index 71cb1bad..d2d35c0a 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -89,12 +89,11 @@ public default void export(T target, File directory, Extension extension) { * @param target the exported target * @param parentWindow the parent frame. * @param fileTypeLabel the label describing the specific type of files that - * will be saved. + * @param directory the default directory of the file will be saved. + * @return the directory where files were exported */ - public default void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var fileChooser = new JFileChooser(); - var workingDirectory = new File(System.getProperty("user.home")); - fileChooser.setCurrentDirectory(workingDirectory); + public default File askToExport(T target, JFrame parentWindow, String fileTypeLabel, File directory) { + var fileChooser = new JFileChooser(directory); fileChooser.setMultiSelectionEnabled(true); FileNameExtensionFilter choosable = null; @@ -113,29 +112,35 @@ public default void askToExport(T target, JFrame parentWindow, String fileTypeLa var file = fileChooser.getSelectedFile(); var path = file.getPath(); - if (!(fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { - return; - } + directory = file.isDirectory() ? file : file.getParentFile(); - var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); - var ext = currentFilter.getExtensions()[0]; + if ((fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { + + var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); + var ext = currentFilter.getExtensions()[0]; + + if (!path.contains(".")) { + file = new File(path + "." + ext); + } else { + file = new File(path.substring(0, path.indexOf(".") + 1) + ext); + } + + try { + var fos = new FileOutputStream(file); + printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + file); + e.printStackTrace(); + } - if (!path.contains(".")) { - file = new File(path + "." + ext); - } - else - file = new File(path.substring(0, path.indexOf(".") + 1) + ext); - - try { - var fos = new FileOutputStream(file); - printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + file); - e.printStackTrace(); } + } + + return directory; + } /** diff --git a/src/main/java/pulse/io/readers/AbstractReader.java b/src/main/java/pulse/io/readers/AbstractReader.java index a557fcbe..d98036ef 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -13,6 +13,7 @@ * lists, arrays and containers may (and usually will) change as a result of * using the reader. *

+ * @param */ public interface AbstractReader extends AbstractHandler { diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index d3320e3f..a50d4e78 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -7,10 +7,16 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import pulse.AbstractData; import pulse.input.ExperimentalData; @@ -44,10 +50,16 @@ public class NetzschCSVReader implements CurveReader { /** * Note comma is included as a delimiter character here. */ - public final static String delims = "[#();,/°Cx%^]+"; - + private final static String ENGLISH_DELIMS = "[#(),/°Cx%^]+"; + private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; + + private static String delims = ENGLISH_DELIMS; + + //default number format (British format) + private static Locale locale = Locale.ENGLISH; + private NetzschCSVReader() { - // intentionally blank + //intentionally blank } /** @@ -87,19 +99,25 @@ public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); ExperimentalData curve = new ExperimentalData(); + + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); + + var format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); - final double thickness = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + + final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); - final double diameter = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + final double diameter = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); - final double sampleTemperature = Double.parseDouble(tempTokens[tempTokens.length - 1]) + TO_KELVIN; + final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; /* * Finds the detector keyword. @@ -122,32 +140,52 @@ public List read(File file) throws IOException { curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); + return new ArrayList<>(Arrays.asList(curve)); + } catch (ParseException ex) { + Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); } - return new ArrayList<>(Arrays.asList(curve)); + return null; } - protected static void populate(AbstractData data, BufferedReader reader) throws IOException { + protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { double time; double power; String[] tokens; + var format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - time = Double.parseDouble(tokens[0]) * NetzschCSVReader.TO_SECONDS; - power = Double.parseDouble(tokens[1]); + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; + power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); } } protected static int determineShotID(BufferedReader reader, File file) throws IOException { - String[] shotID = reader.readLine().split(delims); + String shotIDLine = reader.readLine(); + String[] shotID = shotIDLine.split(delims); int shotId = -1; + + if(shotID.length < 3) { + + if(locale == Locale.ENGLISH) { + delims = GERMAN_DELIMS; + locale = Locale.GERMAN; + } + else { + delims = ENGLISH_DELIMS; + locale = Locale.ENGLISH; + } + + shotID = shotIDLine.split(delims); + } //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { @@ -160,19 +198,7 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO return shotId; } - - /* - private double parseDoubleWithComma(String s) { - var format = NumberFormat.getInstance(Locale.GERMANY); - try { - return format.parse(s).doubleValue(); - } catch (ParseException e) { - System.out.println("Couldn't parse double from: " + s); - e.printStackTrace(); - } - return Double.NaN; - } - */ + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { String line = ""; @@ -195,7 +221,6 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } - /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. @@ -205,5 +230,14 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str public static CurveReader getInstance() { return instance; } - + + /** + * Get the standard delimiter chars. + * @return delims + */ + + public static String getDelims() { + return delims; + } + } diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 2b67fe4c..4ba3303a 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -4,7 +4,10 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.text.ParseException; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import pulse.problem.laser.NumericPulseData; import pulse.ui.Messages; @@ -37,10 +40,12 @@ public String getSupportedExtension() { /** * This performs a basic check, finding the shot ID, which is then passed to - * a new {@code NumericPulseData} object. The latter is populated using the - * time-power sequence stored in this file. If the {@value PULSE} keyword is + * a new {@code NumericPulseData} object.The latter is populated using the + * time-power sequence stored in this file.If the {@value PULSE} keyword is * not found, the method will display an error. * + * @param file + * @throws java.io.IOException * @see pulse.io.readers.NetzschCSVReader.read() * @return a new {@code NumericPulseData} object encapsulating the contents * of {@code file} @@ -49,14 +54,14 @@ public String getSupportedExtension() { public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); - NumericPulseData data; + NumericPulseData data = null; try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = NetzschCSVReader.determineShotID(reader, file); data = new NumericPulseData(shotId); - var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.delims); + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.getDelims()); if (pulseLabel == null) { System.err.println("Skipping " + file.getName()); @@ -66,24 +71,14 @@ public NumericPulseData read(File file) throws IOException { reader.readLine(); NetzschCSVReader.populate(data, reader); + } catch (ParseException ex) { + Logger.getLogger(NetzschPulseCSVReader.class.getName()).log(Level.SEVERE, null, ex); } return data; } - /* - private double parseDoubleWithComma(String s) { - var format = NumberFormat.getInstance(Locale.GERMANY); - try { - return format.parse(s).doubleValue(); - } catch (ParseException e) { - System.out.println("Couldn't parse double from: " + s); - e.printStackTrace(); - } - return Double.NaN; - } - */ /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 83c72eef..97bc4a28 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -8,6 +8,12 @@ import java.util.Objects; import java.util.Scanner; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; @@ -207,13 +213,32 @@ public static Set readDirectory(List> readers, File dir throw new IllegalArgumentException("Not a directory: " + directory); } - var list = new HashSet(); - + var es = Executors.newSingleThreadExecutor(); + + var callableList = new ArrayList>(); + for (File f : directory.listFiles()) { - list.add(read(readers, f)); + Callable callable = () -> read(readers, f); + callableList.add(callable); + } + + Set result = new HashSet<>(); + + try { + List> futures = es.invokeAll(callableList); + + for(Future f : futures) + result.add(f.get()); + + } catch (InterruptedException ex) { + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + "Reading interrupted when loading files from " + directory.toString(), ex); + } catch (ExecutionException ex) { + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + "Error executing read operation using concurrency", ex); } - return list; + return result; } /** diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index acde58f8..4b47ebdb 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -16,6 +16,7 @@ import pulse.math.Segment; import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; @@ -228,6 +229,13 @@ public void optimisationVector(ParameterVector output, List flags) { var key = output.getIndex(i); switch (key) { + case THICKNESS: + final double l = (double) properties.getSampleThickness().getValue(); + var bounds = Segment.boundsFrom(THICKNESS); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, l); + break; case DIFFUSIVITY: final double a = (double) properties.getDiffusivity().getValue(); output.setTransform(i, new InvLenSqTransform(properties)); @@ -284,6 +292,9 @@ public void assign(ParameterVector params) throws SolverException { var key = params.getIndex(i); switch (key) { + case THICKNESS: + properties.setSampleThickness(derive(THICKNESS, params.inverseTransform(i) )); + break; case DIFFUSIVITY: properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i))); break; diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java index 1f98dc4c..f3c83cf0 100644 --- a/src/main/java/pulse/properties/NumericPropertyFormatter.java +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -74,7 +74,9 @@ public NumberFormat numberFormat(NumericProperty p) { : (double) value; double absAdjustedValue = Math.abs(adjustedValue); - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) { + if (addHtmlTags && + ( (absAdjustedValue > UPPER_LIMIT) + || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) ) { //format with scientific notations f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); } else { diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 3a45116b..65cdf46b 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -210,23 +210,20 @@ public enum NumericPropertyKeyword { */ OPTICAL_THICKNESS, /** - * Time shift (pulse sync) + * Time shift (pulse sync). */ TIME_SHIFT, /** - * Statistical significance. + * Statistical significance for calculating the critical value. */ SIGNIFICANCE, - /** - * Statistical probability. - */ - PROBABILITY, + /** * Optimiser statistic (usually, RSS). */ OPTIMISER_STATISTIC, /** - * Model selection criterion (AIC, BIC, etc.) + * Model selection criterion (AIC, BIC, etc.). */ MODEL_CRITERION, /** diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 5edc8275..e13af6e1 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -74,8 +74,8 @@ public static List activeParameters(SearchTask t) { //problem dependent var allActiveParams = selectActiveAndListed(flags, c.getProblem()); //problem independent (lower/upper bound) - var listed = selectActiveAndListed(flags, t.getExperimentalCurve() ); - allActiveParams.addAll( selectActiveAndListed(flags, t.getExperimentalCurve() ) ); + var listed = selectActiveAndListed(flags, t.getExperimentalCurve().getRange() ); + allActiveParams.addAll(listed); return allActiveParams; } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 60b970ad..99ac0048 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -1,7 +1,6 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; @@ -32,9 +31,9 @@ public boolean test(SearchTask task) { var testResult = GofStat.andersonDarling(residuals, nd); this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - setProbability(derive(PROBABILITY, testResult[1])); - - return significanceTest(); + + //compare the p-value and the significance + return testResult[1] > significance; } @Override diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index d350d7e3..ceb4ceb2 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -1,7 +1,6 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.distribution.NormalDistribution; @@ -23,8 +22,10 @@ public class KSTest extends NormalityTest { @Override public boolean test(SearchTask task) { evaluate(task); - setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); - return significanceTest(); + + this.setStatistic(derive(TEST_STATISTIC, + TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); + return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); } @Override diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index f7bb3ed6..a8de54c4 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -3,7 +3,6 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; @@ -17,28 +16,25 @@ * * For the test to pass, the model residuals need be distributed according to a * (0, σ) normal distribution, where σ is the variance of the model - * residuals. As this is the pre-requisite for optimizers based on the ordinary + * residuals. As this is the pre-requisite for optimisers based on the ordinary * least-square statistic, the normality test can also be used to estimate if a * fit 'failed' or 'succeeded' in describing the data. + * + * The test consists in testing the relation statistic < critValue, + * where the critical value is determined based on a given level of significance. * */ public abstract class NormalityTest extends ResidualStatistic { private double statistic; - private double probability; - private static double significance = (double) def(SIGNIFICANCE).getValue(); + protected static double significance = (double) def(SIGNIFICANCE).getValue(); private static String selectedTestDescriptor; protected NormalityTest() { - probability = (double) def(PROBABILITY).getValue(); statistic = (double) def(TEST_STATISTIC).getValue(); } - public boolean significanceTest() { - return probability > significance; - } - public static NumericProperty getStatisticalSignifiance() { return derive(SIGNIFICANCE, significance); } @@ -48,10 +44,6 @@ public static void setStatisticalSignificance(NumericProperty alpha) { NormalityTest.significance = (double) alpha.getValue(); } - public NumericProperty getProbability() { - return derive(PROBABILITY, probability); - } - public abstract boolean test(SearchTask task); @Override @@ -65,11 +57,6 @@ public void setStatistic(NumericProperty statistic) { this.statistic = (double) statistic.getValue(); } - public void setProbability(NumericProperty probability) { - requireType(probability, PROBABILITY); - this.probability = (double) probability.getValue(); - } - @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == TEST_STATISTIC) { diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 797b9f8c..60cdd4ce 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -186,9 +186,10 @@ public boolean setStatus(Status status) { switch(this.status) { case DONE: + case IN_PROGRESS: + case FAILED: case EXECUTION_ERROR: case INCOMPLETE: - case IN_PROGRESS: //if the TaskManager attempts to run this calculation if(status == Status.QUEUED) return false; diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 65e31e44..1ee2635c 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -95,17 +95,7 @@ private void arrangeErrorOutput() { try { var dir = new File(decodedPath).getParent(); errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); - setErr(new PrintStream(errorLog) { - - @Override - public void println(String str) { - super.println(str); - JOptionPane.showMessageDialog(null, "An exception has occurred. " - + "Please check the stored log!", "Exception", JOptionPane.ERROR_MESSAGE); - } - - } - ); + setErr(new PrintStream(errorLog)); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index a4858060..21587580 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -94,7 +94,7 @@ public void mouseDragged(MouseEvent e) { //process dragged events Range range = instance.getSelectedTask() .getExperimentalCurve().getRange(); - double value = xCoord(e); + double value = xCoord(e) / factor; //convert to seconds back from ms -- if needed if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { if (range.boundLimits(false).contains(value)) { @@ -124,8 +124,8 @@ public void mouseDragged(MouseEvent e) { if (instance.getSelectedTask() == eventTask) { //update marker values var segment = eventTask.getExperimentalCurve().getRange().getSegment(); - lowerMarker.setValue(segment.getMinimum()); - upperMarker.setValue(segment.getMaximum()); + lowerMarker.setValue(segment.getMinimum() * factor); //convert to ms -- if needed + upperMarker.setValue(segment.getMaximum() * factor); //convert to ms -- if needed } }); } //tasks that have been finihed @@ -241,11 +241,11 @@ public void plot(SearchTask task, boolean extendedCurve) { lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - final double margin = segment.getMaximum() / 20.0; + final double margin = (lowerMarker.getValue() + upperMarker.getValue())/20.0; //add listener to handle range adjustment - var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, margin); - var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, margin); + var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); + var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, upperMarker, margin); chartPanel.addChartMouseListener(lowerMarkerListener); chartPanel.addChartMouseListener(upperMarkerListener); diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java index 4a4d1ef6..834dc1cc 100644 --- a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -27,16 +27,19 @@ */ public class MouseOnMarkerListener implements ChartMouseListener { - private final MovableValueMarker marker; + private final MovableValueMarker lower; + private final MovableValueMarker upper; + private final Chart chart; private final double margin; private final static Cursor CROSSHAIR = new Cursor(Cursor.CROSSHAIR_CURSOR); private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); - public MouseOnMarkerListener(Chart chart, MovableValueMarker marker, double margin) { + public MouseOnMarkerListener(Chart chart, MovableValueMarker lower, MovableValueMarker upper, double margin) { this.chart = chart; - this.marker = marker; + this.lower = lower; + this.upper = upper; this.margin = margin; } @@ -48,20 +51,29 @@ public void chartMouseClicked(ChartMouseEvent arg0) { @Override public void chartMouseMoved(ChartMouseEvent arg0) { double xCoord = chart.xCoord(arg0.getTrigger()); - highlightMarker(xCoord, marker); + highlightMarker(xCoord); } - private void highlightMarker(double xCoord, MovableValueMarker marker) { + private void highlightMarker(double xCoord) { - if (xCoord > (marker.getValue() - margin) - & xCoord < (marker.getValue() + margin)) { + if (xCoord > (lower.getValue() - margin) + & xCoord < (lower.getValue() + margin)) { - marker.setState(MovableValueMarker.State.SELECTED); + lower.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - } else { + } + else if (xCoord > (upper.getValue() - margin) + & xCoord < (upper.getValue() + margin)) { + + upper.setState(MovableValueMarker.State.SELECTED); + chart.getChartPanel().setCursor(RESIZE); + + } + else { - marker.setState(MovableValueMarker.State.IDLE); + lower.setState(MovableValueMarker.State.IDLE); + upper.setState(MovableValueMarker.State.IDLE); chart.getChartPanel().setCursor(CROSSHAIR); } diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index 3d81254e..fff33ecc 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -8,6 +8,7 @@ import javax.swing.JInternalFrame; import pulse.tasks.TaskManager; +import pulse.tasks.logs.Status; import pulse.ui.components.Chart; import pulse.ui.components.panels.ChartToolbar; import pulse.ui.components.panels.OpacitySlider; @@ -44,7 +45,8 @@ private void initComponents() { public void plot() { var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) { + //do not plot tasks that are not finished + if (task != null && task.getCurrentCalculation().getStatus() != Status.IN_PROGRESS) { Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); } } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index d8b0b6cd..2d7eddab 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -50,7 +50,7 @@ public class SearchOptionsFrame extends JInternalFrame { private final static Font FONT = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); private final static List pathSolvers = instancesOf(PathOptimiser.class); - private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{DIFFUSIVITY, MAXTEMP}; + private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{MAXTEMP}; /** * Create the frame. diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index f8a93008..ed0f7eed 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -14,6 +14,7 @@ import java.awt.Dimension; import java.io.File; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -81,12 +82,12 @@ public ExportDialog() { } private File directoryQuery() { - var returnVal = fileChooser.showSaveDialog(this); + var returnVal = fileChooser.showOpenDialog(this); File f = null; if (returnVal == APPROVE_OPTION) { - f = fileChooser.getCurrentDirectory(); + dir = f = fileChooser.getSelectedFile(); } return f; @@ -171,8 +172,9 @@ private void initComponents() { fileChooser = new JFileChooser(); fileChooser.setMultiSelectionEnabled(false); fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - // Checkboxex - dir = fileChooser.getCurrentDirectory(); + + //get cwd + dir = new File("").getAbsoluteFile(); var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); directoryField.setEditable(false); @@ -247,11 +249,8 @@ public void removeUpdate(DocumentEvent e) { var browseBtn = new JButton("Browse..."); - browseBtn.addActionListener(e -> { - if (directoryQuery() != null) { - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - }); + browseBtn.addActionListener(e -> directoryField.setText(directoryQuery() + .getPath() + separator + projectName + separator) ); var exportBtn = new JButton("Export"); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 89dca9d3..9c78b74a 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,10 @@ + + - - - + - - + minimum="1.0E-6" value="0.001" primitive-type="double" discreet="true" + default-search-variable="false"> Date: Thu, 17 Mar 2022 14:08:05 +0300 Subject: [PATCH 092/116] =?UTF-8?q?AbsorptionModel.java:=20Moved=20optimis?= =?UTF-8?q?ationVector(=E2=80=A6)=20and=20assign(=E2=80=A6)=20from=20Probl?= =?UTF-8?q?em=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADIScheme: Increased default time step to ensure adequate results Calculation.java: - Fixed problem with missing parent when creating a Calculation from a SearchTask. In the copy constructor, the parent is now removed, but the result is preserved. - Fixed incorrect status update in setStatus(…) method. Rules have been created to block status update in certain cases. - Introduced the isBetterThan(…) method, which performs a statistical check (e.g. BIC) plus an F-test, if the latter is possible, and uses this combination to assess which calculations gives a better result. CorrelationBuffer.java: truncate(…) excludes buffer elements which show very close parameter values and therefore negatively affect the correlation tests. CorrelationTest: Changed selector for correlation tests Details: Status may now indicate if the the new results are worse than the previous result. DiathermicMedium: Removed custom default number of data points DifferenceScheme: Added a check to see if the DiscretePulse had already been created. If so, the latter is only updated. This prevents adding superfluous listeners and creating objects DiscretePulse: Added Objects.requireNonNull when accessing the SearchTask ancestor of the Problem instance FTest.java: Added the Fischer test capability to compare two Calculation instances where one model is nested within the other. InstanceDescriptor: Instead of returning false in case if the descriptor is not recognised, the attemptUpdate(…) now throws an IllegalArgumentException. NetzschCSVReader: - Added detector spot size in the list of imported properties. -findLineByLabel now uses reader.mark(…) to keep track of previous valid position. If the search fails, the position of the reader will be reset to this mark. ParticipatingMedium & PenetrationProblem: Removed custom number of data points PenetrationProblem: Moved functionality specific to absoprtion model into the AbsorptionModel class PulseMainMenu: Changed initialisation of the correlation tests and communication of the selector with GUI. ResultTable & ResultTableModel: Added null checks for the result extracted from the Calculation instance ResultTableModel: Heavily modified the addRow(result) method where an entry will not be added to the results table if (a) a previous calculation had been previously completed and (b) the new result is rated worse than the previous result. SearchTask: Added storeCalculation(…) and findBestCalculation(…). TaskManager: Added call to storeCalculation(…) in the execute method, upon successful completion of the CompletableFuture. --- pom.xml | 2 +- src/main/java/pulse/input/Metadata.java | 2 + .../pulse/io/readers/NetzschCSVReader.java | 27 +++- .../pulse/problem/laser/DiscretePulse.java | 9 +- .../java/pulse/problem/schemes/ADIScheme.java | 2 +- .../problem/schemes/DifferenceScheme.java | 6 +- .../solvers/ImplicitTranslucentSolver.java | 11 +- .../problem/statements/DiathermicMedium.java | 2 - .../statements/ParticipatingMedium.java | 3 - .../statements/PenetrationProblem.java | 58 +------ .../statements/model/AbsorptionModel.java | 70 +++++++- .../model/BeerLambertAbsorption.java | 4 + .../problem/statements/model/Insulator.java | 2 - .../search/statistics/CorrelationTest.java | 26 +-- .../java/pulse/search/statistics/FTest.java | 149 ++++++++++++++++++ .../statistics/ModelSelectionCriterion.java | 11 +- .../pulse/search/statistics/Statistic.java | 5 - src/main/java/pulse/tasks/Calculation.java | 125 ++++++++++----- src/main/java/pulse/tasks/SearchTask.java | 37 +++-- src/main/java/pulse/tasks/TaskManager.java | 12 +- src/main/java/pulse/tasks/logs/Details.java | 17 +- .../tasks/processing/CorrelationBuffer.java | 25 +++ .../pulse/ui/components/PulseMainMenu.java | 21 ++- .../java/pulse/ui/components/ResultTable.java | 8 +- .../components/buttons/ExecutionButton.java | 4 +- .../components/models/ResultTableModel.java | 96 ++++++++--- .../models/StoredCalculationTableModel.java | 6 +- .../java/pulse/util/InstanceDescriptor.java | 7 +- .../java/pulse/util/UpwardsNavigable.java | 2 +- src/main/resources/NumericProperty.xml | 11 +- 30 files changed, 558 insertions(+), 202 deletions(-) create mode 100644 src/main/java/pulse/search/statistics/FTest.java diff --git a/pom.xml b/pom.xml index 88351e32..e27af653 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.93F + 1.94 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index 89e80301..7b650e86 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -22,6 +22,7 @@ import pulse.problem.laser.RectangularPulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; import pulse.properties.Property; import pulse.properties.SampleName; import pulse.tasks.Identifier; @@ -199,6 +200,7 @@ public Set listedKeywords() { set.add(LASER_ENERGY); set.add(DETECTOR_GAIN); set.add(DETECTOR_IRIS); + set.add(FOV_OUTER); return set; } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index a50d4e78..c2e0b2ad 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -45,6 +45,7 @@ public class NetzschCSVReader implements CurveReader { private final static String SHOT_DATA = "Shot_data"; private final static String DETECTOR = "DETECTOR"; private final static String THICKNESS = "Thickness_RT"; + private final static String DETECTOR_SPOT_SIZE = "Spotsize"; private final static String DIAMETER = "Diameter"; /** @@ -109,6 +110,13 @@ public List read(File file) throws IOException { var format = DecimalFormat.getInstance(locale); format.setGroupingUsed(false); + var spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, delims); + double spotSize = 0; + if(spot != null) { + var spotTokens = spot.split(delims); + spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; + } + var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; @@ -135,7 +143,7 @@ public List read(File file) throws IOException { var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); - met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, 0.85 * diameter)); + met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94 * diameter)); curve.setMetadata(met); @@ -198,12 +206,18 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO return shotId; } - + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { + return findLineByLabel(reader, label, "!!!", delims); + } + + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, String delims) throws IOException { String line = ""; String[] tokens; + reader.mark(1000); + //find keyword outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { @@ -211,9 +225,17 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str tokens = line.split(delims); for (String token : tokens) { + if (token.equalsIgnoreCase(label)) { break outer; } + + if(token.equalsIgnoreCase(stopLabel)) { + line = null; + reader.reset(); + break outer; + } + } } @@ -221,6 +243,7 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } + /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index dd397e24..d29c073b 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -1,5 +1,7 @@ package pulse.problem.laser; +import java.util.Objects; +import pulse.input.ExperimentalData; import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; @@ -37,7 +39,11 @@ public DiscretePulse(Problem problem, Grid grid) { recalculate(); - var data = ((SearchTask) problem.specificAncestor(SearchTask.class)).getExperimentalCurve(); + Object ancestor = + Objects.requireNonNull( problem.specificAncestor(SearchTask.class), + "Problem has not been assigned to a SearchTask"); + + ExperimentalData data = ((SearchTask)ancestor).getExperimentalCurve(); pulse.getPulseShape().init(data, this); pulse.addListener(e -> { @@ -45,6 +51,7 @@ public DiscretePulse(Problem problem, Grid grid) { recalculate(); pulse.getPulseShape().init(data, this); }); + } /** diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index bad5501c..2ff641fa 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -19,7 +19,7 @@ public abstract class ADIScheme extends DifferenceScheme { * time factor. */ public ADIScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.5)); } /** diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 309e19b1..9f54d46b 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -108,7 +108,11 @@ public void copyFrom(DifferenceScheme df) { * @see pulse.problem.schemes.Grid.adjustTo() */ protected void prepare(Problem problem) { - discretePulse = problem.discretePulseOn(grid); + if(discretePulse == null) + discretePulse = problem.discretePulseOn(grid); + else + discretePulse.recalculate(); + grid.adjustTo(discretePulse); var hc = problem.getHeatingCurve(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index 2fcab7ed..c28d401b 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -11,6 +11,7 @@ import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.NumericProperty; public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { @@ -44,14 +45,16 @@ private void prepare(PenetrationProblem problem) { final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); final double hx = grid.getXStep(); + + absorption = problem.getAbsorptionModel(); + HH = hx * hx; _2Bi1HTAU = 2.0 * Bi1H * tau; b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - absorption = problem.getAbsorptionModel(); final double EPS = 1E-7; - rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); - frontAbsorption = tau * absorption.absorption(LASER, 0.0); + rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); + frontAbsorption = tau * absorption.absorption(LASER, 0.0) + 2.0*tau/hx; var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @@ -61,7 +64,7 @@ public double phi(final int i) { } }; - + // coefficients for difference equation tridiagonal.setCoefA(1. / HH); tridiagonal.setCoefB(1. / tau + 2. / HH); diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 2082d03d..a34c7441 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -34,11 +34,9 @@ */ public class DiathermicMedium extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; public DiathermicMedium() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); } public DiathermicMedium(Problem p) { diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index c61083ff..2d60b1dd 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -21,11 +21,8 @@ public class ParticipatingMedium extends NonlinearProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - public ParticipatingMedium() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); setComplexity(ProblemComplexity.HIGH); } diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index bca2c673..46a2b844 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -1,14 +1,8 @@ package pulse.problem.statements; -import static pulse.math.transforms.StandardTransformations.LOG; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; - import java.util.List; import pulse.math.ParameterVector; -import pulse.math.Segment; -import static pulse.math.transforms.StandardTransformations.ABS; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; import pulse.problem.schemes.solvers.SolverException; @@ -21,8 +15,6 @@ public class PenetrationProblem extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( "Absorption Model Selector", AbsorptionModel.class); @@ -31,7 +23,6 @@ public class PenetrationProblem extends ClassicalProblem { public PenetrationProblem() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); absorption.setParent(this); @@ -72,56 +63,13 @@ public InstanceDescriptor getAbsorptionSelector() { @Override public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); - double value = 0; - - switch (key) { - case LASER_ABSORPTIVITY: - value = (double) (absorption.getLaserAbsorptivity()).getValue(); - break; - case THERMAL_ABSORPTIVITY: - value = (double) (absorption.getThermalAbsorptivity()).getValue(); - break; - case COMBINED_ABSORPTIVITY: - value = (double) (absorption.getCombinedAbsorptivity()).getValue(); - break; - default: - continue; - } - - //do this for the listed key values - output.setTransform(i, ABS); - output.set(i, value); - output.setParameterBounds(i, new Segment(1E-2, 1000.0)); - - } - + absorption.optimisationVector(output, flags); } @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - - double value; - - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); - - switch (key) { - case LASER_ABSORPTIVITY: - case THERMAL_ABSORPTIVITY: - case COMBINED_ABSORPTIVITY: - value = params.inverseTransform(i); - break; - default: - continue; - } - - absorption.set(key, derive(key, value)); - - } + absorption.assign(params); } @Override @@ -139,4 +87,4 @@ public Problem copy() { return new PenetrationProblem(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 4a27a0ff..434db3cd 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -10,22 +10,25 @@ import java.util.List; import java.util.Map; import java.util.Set; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import static pulse.math.transforms.StandardTransformations.ABS; +import pulse.math.transforms.Transformable; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.COMBINED_ABSORPTIVITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; +import pulse.search.Optimisable; -public abstract class AbsorptionModel extends PropertyHolder implements Reflexive { +public abstract class AbsorptionModel extends PropertyHolder implements Reflexive, Optimisable { private Map absorptionMap; - + protected AbsorptionModel() { setPrefix("Absorption model"); absorptionMap = new HashMap<>(); @@ -100,5 +103,58 @@ public Set listedKeywords() { set.add(COMBINED_ABSORPTIVITY); return set; } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + double value = 0; + + Transformable transform = ABS; + output.setParameterBounds(i, new Segment(1E-2, 1000.0)); + + switch (key) { + case LASER_ABSORPTIVITY: + value = (double) (getLaserAbsorptivity()).getValue(); + break; + case THERMAL_ABSORPTIVITY: + value = (double) (getThermalAbsorptivity()).getValue(); + break; + case COMBINED_ABSORPTIVITY: + value = (double) (getCombinedAbsorptivity()).getValue(); + break; + default: + continue; + } + + //do this for the listed key values + output.setTransform(i, transform); + output.set(i, value); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + double value; + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = params.getIndex(i); + + switch (key) { + case LASER_ABSORPTIVITY: + case THERMAL_ABSORPTIVITY: + case COMBINED_ABSORPTIVITY: + value = params.inverseTransform(i); + break; + default: + continue; + } + + set(key, derive(key, value)); + + } + } } diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index c7719879..31713356 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,6 +2,10 @@ public class BeerLambertAbsorption extends AbsorptionModel { + public BeerLambertAbsorption() { + super(); + } + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index b4c37b95..0cbba0cd 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -4,12 +4,10 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.REFLECTANCE; -import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; public class Insulator extends AbsorptionModel { diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index f724a758..67191bcf 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -7,18 +7,34 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.util.InstanceDescriptor; import pulse.util.PropertyHolder; import pulse.util.Reflexive; public abstract class CorrelationTest extends PropertyHolder implements Reflexive { private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - private static String selectedTestDescriptor; + private static InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "Correlation Test Selector", CorrelationTest.class); + + static { + instanceDescriptor.setSelectedDescriptor(PearsonCorrelation.class.getSimpleName()); + } + public CorrelationTest() { //intentionally blank } + public static CorrelationTest init() { + return instanceDescriptor.newInstance(CorrelationTest.class); + } + + public final static InstanceDescriptor getTestDescriptor() { + return instanceDescriptor; + } + public abstract double evaluate(double[] x, double[] y); public boolean compareToThreshold(double value) { @@ -41,12 +57,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } - - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - CorrelationTest.selectedTestDescriptor = selectedTestDescriptor; - } - } diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java new file mode 100644 index 00000000..672bdc16 --- /dev/null +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.search.statistics; + +import org.apache.commons.math3.distribution.FDistribution; +import pulse.tasks.Calculation; + +/** + * A static class for testing two calculations based on the Fischer test (F-Test) + * implemented in Apache Commons Math. + * @author Artem Lunev + */ +public class FTest { + + /** + * False-rejection probability for the F-test, equal to {@value FALSE_REJECTION_PROBABILITY} + */ + + public final static double FALSE_REJECTION_PROBABILITY = 0.05; + + private FTest() { + //intentionall blank + } + + /** + * Tests two models to see which one is better according to the F-test + * @param a a calculation + * @param b another calculation + * @return {@code null} if the result is inconclusive, otherwise the + * best of two calculations. + * @see FTest.evaluate() + */ + + public static Calculation test(Calculation a, Calculation b) { + + double[] data = evaluate(a, b); + + Calculation best = null; + + if(data != null) { + + //Under the null hypothesis the general model does not provide + //a significantly better fit than the nested model + + Calculation nested = findNested(a, b); + + //if the F-statistic is greater than the F-critical, reject the null hypothesis. + + if(nested == a) + best = data[0] > data[1] ? b : a; + else + best = data[0] > data[1] ? a : b; + + } + + return best; + + } + + /** + * Evaluates the F-statistic for two calculations. + * @param a a calculation + * @param b another calculation + * @return {@code null} if the test is inconclusive, i.e., if models are not + * nested, or if the model selection criteria are based on a statistic different + * from least-squares, or if the calculations refer to different data ranges. + * Otherwise returns an double array, consisting of two elements {@code [fStatistic, fCritical] } + */ + + public static double[] evaluate(Calculation a, Calculation b) { + + Calculation nested = findNested(a, b); + + double[] result = null; + + //if one of the models is nested into the other + if(nested != null) { + Calculation general = nested == a ? b : a; + + ResidualStatistic nestedResiduals = nested.getModelSelectionCriterion().getOptimiserStatistic(); + ResidualStatistic generalResiduals = general.getModelSelectionCriterion().getOptimiserStatistic(); + + final int nNested = nestedResiduals.getResiduals().size(); //sample size + final int nGeneral = generalResiduals.getResiduals().size(); //sample size + + //if both models use a sum-of-square statistic for the model selection criteria + //and if both calculations refer to the same calculation range + if(nestedResiduals.getClass() == generalResiduals.getClass() + && nestedResiduals.getClass() == SumOfSquares.class + && nNested == nGeneral) { + + double rssNested = ( (Number) ((SumOfSquares)nestedResiduals).getStatistic().getValue() ).doubleValue(); + double rssGeneral = ( (Number) ((SumOfSquares)generalResiduals).getStatistic().getValue() ).doubleValue(); + + int kGeneral = general.getModelSelectionCriterion().getNumVariables(); + int kNested = nested.getModelSelectionCriterion().getNumVariables(); + + double fStatistic = (rssNested - rssGeneral) + /(kGeneral - kNested) + /(rssGeneral/(nGeneral - kGeneral)); + + var fDistribution = new FDistribution(kGeneral - kNested, nGeneral - kGeneral); + + double fCritical = fDistribution.inverseCumulativeProbability(1.0 - FALSE_REJECTION_PROBABILITY); + + result = new double[]{fStatistic, fCritical}; + + } + + } + + return result; + + } + + /** + * Tests two models to see which one is nested in the other. A model is + * considered nested if it refers to the same class of problems but has + * fewer parameters. + * @param a a calculation + * @param b another calculation + * @return {@code null} if the models refer to different problem classes. + * Otherwise returns the model that is nested in the second model. + */ + + public static Calculation findNested(Calculation a, Calculation b) { + if(a.getProblem().getClass() != b.getProblem().getClass()) + return null; + + int aParams = a.getModelSelectionCriterion().getNumVariables(); + int bParams = b.getModelSelectionCriterion().getNumVariables(); + + return aParams > bParams ? b : a; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 63e7bbba..324acc5e 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -28,7 +28,7 @@ public abstract class ModelSelectionCriterion extends Statistic { public ModelSelectionCriterion(OptimiserStatistic os) { super(); - setOptimiser(os); + setOptimiserStatistic(os); } public ModelSelectionCriterion(ModelSelectionCriterion another) { @@ -96,20 +96,15 @@ public double probability(List all) { return exp(-0.5 * di); } - @Override - public String getDescriptor() { - return "Akaike Information Criterion (AIC)"; - } - public int getNumVariables() { return kq; } - public OptimiserStatistic getOptimiser() { + public OptimiserStatistic getOptimiserStatistic() { return os; } - public void setOptimiser(OptimiserStatistic os) { + public void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; } diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 6ac94f15..7a3c5e1d 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,6 +1,5 @@ package pulse.search.statistics; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -14,8 +13,4 @@ public abstract class Statistic extends PropertyHolder implements Reflexive { public abstract void evaluate(SearchTask t); - public abstract NumericProperty getStatistic(); - - public abstract void setStatistic(NumericProperty statistic); - } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 60cdd4ce..3863df18 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -19,7 +19,8 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.search.statistics.AICStatistic; +import pulse.search.statistics.BICStatistic; +import pulse.search.statistics.FTest; import pulse.search.statistics.ModelSelectionCriterion; import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.logs.Status; @@ -43,38 +44,32 @@ public class Calculation extends PropertyHolder implements Comparable instanceDescriptor = new InstanceDescriptor<>( "Model Selection Criterion", ModelSelectionCriterion.class); + //BIC as default static { - instanceDescriptor.setSelectedDescriptor(AICStatistic.class.getSimpleName()); + instanceDescriptor.setSelectedDescriptor(BICStatistic.class.getSimpleName()); } - public Calculation() { + public Calculation(SearchTask t) { status = INCOMPLETE; this.initOptimiser(); + setParent(t); instanceDescriptor.addListener(() -> initModelCriterion()); } - public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { - this(); - this.problem = problem; - this.scheme = scheme; - this.os = rs.getOptimiser(); - this.rs = rs; - problem.setParent(this); - scheme.setParent(this); - os.setParent(this); - rs.setParent(this); - } - - public Calculation copy() { - var status = this.status; - var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); - var p = nCalc.getProblem(); - p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); - nCalc.status = status; - if (this.getResult() != null) { - nCalc.setResult(new Result(this.getResult())); + /** + * Creates an orphan Calculation, retaining some properties of the argument + * + * @param c another calculation to be archived. + */ + public Calculation(Calculation c) { + this.problem = c.problem.copy(); + this.scheme = c.scheme.copy(); + this.rs = c.rs.copy(); + this.os = c.os.copy(); + this.status = c.status; + if (c.getResult() != null) { + this.result = new Result(c.getResult()); } - return nCalc; } public void clear() { @@ -136,11 +131,10 @@ private void addProblemListeners(Problem problem, ExperimentalData curve) { /** * Adopts the {@code scheme} by this {@code SearchTask} and updates the time - * limit of { - * - * @scheme} to match {@code ExperimentalData}. + * limit of {@code scheme} to match {@code ExperimentalData}. * * @param scheme the {@code DiffenceScheme}. + * @param curve */ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { this.scheme = scheme; @@ -174,31 +168,44 @@ public Status getStatus() { /** * Attempts to set the status of this calculation to {@code status}. + * * @param status a status - * @return {@code true} if this attempt is successful, including the case + * @return {@code true} if this attempt is successful, including the case * when the status being set is equal to the current status. {@code false} - * if the current status is one of the following: {@code DONE}, {@code EXECUTION_ERROR}, - * {@code INCOMPLETE}, {@code IN_PROGRES}, AND the {@code status} being set - * is {@code QUEUED}. + * if the current status is one of the following: {@code DONE}, + * {@code EXECUTION_ERROR}, {@code INCOMPLETE}, {@code IN_PROGRES}, AND the + * {@code status} being set is {@code QUEUED}. */ - public boolean setStatus(Status status) { - switch(this.status) { - case DONE: + boolean changeStatus = true; + + switch (this.status) { + case QUEUED: case IN_PROGRESS: + switch (status) { + case QUEUED: + case READY: + case INCOMPLETE: + changeStatus = false; + break; + default: + } + break; case FAILED: case EXECUTION_ERROR: case INCOMPLETE: - //if the TaskManager attempts to run this calculation - if(status == Status.QUEUED) - return false; + //if the TaskManager attempts to run this calculation + changeStatus = status != Status.QUEUED; + break; default: } + + if(changeStatus) + this.status = status; - this.status = status; - return true; - + return changeStatus; + } public NumericProperty weight(List all) { @@ -259,10 +266,44 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { // intentionally left blank } + /** + * Checks if this {@code Calculation} is better than {@code a}. + * + * @param a another completed calculation + * @return {@code true} if another calculation hasn't been completed or if + * this calculation's statistic is lower than statistic of {@code a}. + */ + public boolean isBetterThan(Calculation a) { + boolean result = true; + + if (a.getStatus() == Status.DONE) { + result = compareTo(a) < 0; //compare statistic + + //do F-test + Calculation fBest = FTest.test(this, a); + //if the models are nested and calculations can be compared + if (fBest != null) { + //use the F-test result instead + result = fBest == this; + } + + } + + return result; + } + + /** + * Compares two calculations based on their model selection criteria. + * + * @param arg0 another calculation + * @return the result of comparing the model selection statistics of + * {@code this} and {@code arg0}. + */ @Override public int compareTo(Calculation arg0) { - var s1 = arg0.getModelSelectionCriterion().getStatistic(); - return getModelSelectionCriterion().getStatistic().compareTo(s1); + var sAnother = arg0.getModelSelectionCriterion().getStatistic(); + var sThis = getModelSelectionCriterion().getStatistic();; + return sThis.compareTo(sAnother); } @Override diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 33dbfb55..d657875b 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -84,15 +84,14 @@ public class SearchTask extends Accessible implements Runnable { private CorrelationTest correlationTest; private NormalityTest normalityTest; - private Identifier identifier; - + private final Identifier identifier; /** * If {@code SearchTask} finishes, and its R2 value is * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private List listeners = new CopyOnWriteArrayList<>(); - private List statusChangeListeners = new CopyOnWriteArrayList<>(); + private List listeners; + private List statusChangeListeners; /** *

- * Checks if this {@code SearchTask} is ready to be run. Performs basic - * check to see whether the user has uploaded all necessary data. If not, - * will create a status update with information about the missing data. + * Checks if this {@code SearchTask} is ready to be run.Performs basic check + * to see whether the user has uploaded all necessary data. If not, will + * create a status update with information about the missing data. *

* - * @return {@code READY} if the task is ready to be run, {@code DONE} if has - * already been done previously, {@code INCOMPLETE} if some problems exist. - * For the latter, additional details will be available using the - * {@code status.getDetails()} method. + * Status will be set to {@code READY} if the task is ready to be run, + * {@code DONE} if has already been done previously, {@code INCOMPLETE} if + * some problems exist. For the latter, additional details will be available + * using the {@code status.getDetails()} method. *

+ * + * @param updateStatus */ public void checkProblems(boolean updateStatus) { var status = current.getStatus(); @@ -617,7 +623,7 @@ public Calculation getCurrentCalculation() { public List getStoredCalculations() { return this.stored; } - + public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); @@ -630,13 +636,14 @@ public void switchTo(Calculation calc) { var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); } - + /** * Finds the best calculation by comparing those already stored by their * model selection statistics. - * @return the calculation showing the optimal value of the model selection statistic. + * + * @return the calculation showing the optimal value of the model selection + * statistic. */ - public Calculation findBestCalculation() { var c = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); return c.isPresent() ? c.get() : null; @@ -655,4 +662,4 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 9c8f46a6..acaf6dfd 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -56,7 +56,9 @@ public enum Details { * model selection criterion showed better result than already present. */ - BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED; + BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED, + + SOLVER_ERROR; @Override public String toString() { diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index f73282d7..17333a67 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -41,6 +41,10 @@ public String toString() { if (status.getDetails() != NONE) { sb.append(" due to " + status.getDetails() + ""); } + if(status.getDetailedMessage().length() > 0) { + sb.append(" Details: "); + sb.append(status.getDetailedMessage()); + } sb.append(" at "); sb.append(getTime()); return sb.toString(); diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 5520dbeb..080f6ed5 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -55,6 +55,7 @@ public enum Status { private final Color clr; private Details details = Details.NONE; + private String message = ""; Status(Color clr) { this.clr = clr; @@ -71,6 +72,14 @@ public Details getDetails() { public void setDetails(Details details) { this.details = details; } + + public String getDetailedMessage() { + return message; + } + + public void setDetailedMessage(String str) { + this.message = str; + } static String parse(String str) { var tokens = str.split("_"); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 1ee2635c..6843a20d 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -19,6 +19,9 @@ import com.alee.laf.WebLookAndFeel; import com.alee.skin.dark.WebDarkSkin; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; /** *

@@ -35,6 +38,8 @@ public class Launcher { private File errorLog; private final static boolean DEBUG = false; + private static final File LOCK = new File("pulse.lock"); + private Launcher() { if (!DEBUG) { arrangeErrorOutput(); @@ -47,28 +52,44 @@ private Launcher() { */ public static void main(String[] args) { new Launcher(); - splashScreen(); - - WebLookAndFeel.install(WebDarkSkin.class); - try { - UIManager.setLookAndFeel(new WebLookAndFeel()); - } catch (Exception ex) { - System.err.println("Failed to initialize LaF"); - } + + if (!LOCK.exists()) { - var newVersion = Version.getCurrentVersion().checkNewVersion(); + try { + LOCK.createNewFile(); + } catch (IOException ex) { + Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, "Unable to create lock file", ex); + } + + LOCK.deleteOnExit(); - /* Create and display the form */ - invokeLater(() -> { - getInstance().setLocationRelativeTo(null); - getInstance().setVisible(true); + splashScreen(); - if (newVersion != null) { - JOptionPane.showMessageDialog(null, "A new version of this software is available: " - + newVersion.toString() + "
Please visit the PULsE website for more details."); + WebLookAndFeel.install(WebDarkSkin.class); + try { + UIManager.setLookAndFeel(new WebLookAndFeel()); + } catch (Exception ex) { + System.err.println("Failed to initialize LaF"); } - }); + var newVersion = Version.getCurrentVersion().checkNewVersion(); + + /* Create and display the form */ + invokeLater(() -> { + getInstance().setLocationRelativeTo(null); + getInstance().setVisible(true); + + if (newVersion != null) { + JOptionPane.showMessageDialog(null, "A new version of this software is available: " + + newVersion.toString() + "
Please visit the PULsE website for more details."); + } + + }); + + } else { + System.out.println("An instance of PULsE is already running!"); + } + } private static void splashScreen() { diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index e5ca9830..3e3180a2 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -72,7 +72,7 @@ + maximum="10000" minimum="1" primitive-type="int" value="200"> + + + dimensionfactor="1.0" keyword="LASER_ENERGY" maximum="32.0" + minimum="0.01" value="5.0" primitive-type="double" discreet="false" + default-search-variable="false"/> n;IoSFIN&YY<;qn0EI+!P)J)GR5@MlR%~Qb@`~E)bTM1yhD9 zPEtt9QB+>v72YbBK%6eLUyjV;cPr+}34vh~7Q$X2N&t!fvl}!?M`Ti8Rf_pl*x1KH z7f}W$yKPs(p3@KliVLL1PnEp_CQ_9U=w~v!lL+YvU|~=6IYUIEI{|lbLcnicFJSu)EY)UIn-6zKEN3oO#&^xJ5?5XMsds{==;> zLPwvI1DZCz3RU}QUSZUXI3gEN)~xKBRqXIbkW?YKRr4vY;qG(~p9NBO+Xy>8>S9%( z9&5!fNt0?PJqwy^>DPEHkcd;i+v5)#ch+uqwP+vx6}ov3$j+HlsZe~-GNMo_G)Bja_R=2t>FMw+|{~znNVeytT_+s)0M=3!D#ItOZp55+Hfa-Z!a5?Y<0^Ug}b}?=3y+}dPV-)9f4i;$s#PX zlB=t?z??^`+XG+Uwhr}M-)DbcFlB&Ts~I9AKHE>c8laS8chXMDEF#XCfGVu8L@oZ& z0j)y3*CwZ+))%(|sr5y57_B84I@L3{>Eo&(!s;hEG;Itrn0uG0njKc++5|xOKs{Nxn?%5H2__CmilvfbMsw*ry$ z-bfwY1u(j_=c_t8+U=Qd+=a0B_CZy=hD;OrF%_JHQ5J_o07t8)9Kx($9&>K}Kz8%O z-d$^y4m zqc4$*e0JEhFwMRKVb4qhnyt zbe}~6+SOF<)B_7Txw<-o&3wJ_bPwt5;uy}+Zlhv_!Hg$A*@OQ0>tQiEw*rBp8bUSO zVCnMOBJ=vR@=F_D4UzM$g`zMHuRL#K54QqQ+MJlF2jR;Y|M?l}Ru9Te8>5r^NM`DS zh$_3AukeBT*_shwUuYmavSucsnfw2=8Vg@yLFU;G4 znIoDzaAQ?g>+o)|4x6wTk5vc0IVE5Y6$mrUYg!6p*u)iBFuAvdFo=KM3PfX2!C;~< z*@sDfh*Igth><0c+PelEyEbwyVXInlPmU@J)s|oyRy$aMu)-KWV;SBV+Yh5AQDIDc zP!_f=;#3$magp5C9?58qI^nk1wHQs^)malQvWeA>cg7FJ_;(hAEiEuJYSLoNpJWmE z%%2VP^1`?k$f;RJQE?anNhbOJ>WKM`WiZ<$k(&}DRj3V9*3WLFDcx(@R~paGJLKk{ zQwLj9B}7=##|nd)*)(0jjLn}Wg*+NtVK7e{?Sd(m0%+?Hq8@!iHh%}_pjq7tL|$pi zu<)0^Xosj$+EYQfIy(3}S~EYJ=J;y2(>#FP4wZZK*`B^_$EXt1@WZcBDqED&`He7_ z1RfDdd~_mWXHS+{CpNE+xE+>SV#;s;GnWykcYidO9I}JY*qwz2k(uaRGcMcFhwab( zSJ0_pS$7IC20MRTfoA_=1V5vAT$ z2T}Wu0O`R?RN|-!cK$iBd_ZkJGZAsINKZ<`L6wNk6Z&K9jMbPiwx^w?WApOTyNgl& zp8Ifc{a;id9%O(y8KFIf;>D%skbGzx=F=c?_~fM|g9h07Ia*xfVv_f{DT^>`!VpKY zfb1R%gmJj|=LwW=WsxNY>aC>ItSe8+2cwQ$`sYMIiS$J52(+WWV_`VDGyDJEw8#Y9 z>s}a+{5~2`nW)(hUJHZyxyHp>(A*RT@* zZu}$Qw?J$x7@!e9Ekf-36Kzh4o@tMcK=QM%DWc^>eq@L%KS1+Bv zy>nUwa!2I=HP;U*TqULEx*>(EKv;4dN2yAZSM&E4kkS&ok_zD^as2*WUj4a;X9U;; zr(&VbnzowD<7H0%rcb1d>#_<0BR<~y3~3?Qwin33`5zw$C@PRTtE$q}37b0eY}ACe zZ7-KAMa>meXdz0#vEFB0z4*Ha1pF2VQ?@a>1l0Cf2C-~2Cs#*C(zX52{g^%DYBoz2 zrJcU#oWk%IcLW@9bn|u9n$q@O3b9-Z7`|@<@!_p*^fm{fR6{T|1nJfZ=oWXjIrBoX zBjMfRwLoetvrIqRf`*RC{<<|(7+IfHM(9(tMxqY4k+?mDVm0J~VYD{G_@3yO&CBM_ zpB4l>7D&Y9^-i0pJ?VdFs30;wt&5QEpFynI!zZ4sFqs~S&BPu042ku|*k1Y50RfK% zQg;0aJBc^F4;8B&r0=0yIR@&xcR|x>l0A$kOW4>m$edP1_A370biW01?$2LjeGeNi zK@6i^AAKxaIvsX=u&yHOw%eXIdT&qL+PTf&waYK1(Ke#U>NanW&!=cRk)FDmu3=$J z#;_^B$YEUll%~(EK;rh!vMcL|OtsdRv94Je|NR=Ox_wE|H}F9%(6j)lB3OHCWjBt& zmG+dU7wkUS?48Qc7Ea&3&5^4zTOY4T4k@nzUXXz zZUy3~?_ncyeHwmy7n=LugsKFoK3}G$%W53;Mr>}bPbK^v!`QyAcg6({MkIY#w(yR? zJK7!NV)i)>%^`aqUQ5Fs_T7*2itZuX(`cZy`1&VSe_glfQmu4`^Tb1;+hrASIlH=Dz2h{6m{Pf~>9I%Wj#82?&cT z0TJB2j^!|y%E_uh=;zZ=|vSQXE^d3#?Jr1VL&;fYzi-V54 z$sBy$t+w>C_>zz9N9FWX(hoTy6EZy!NA(2v&@N3@sqLTHH$sPwS>}meb_?nHDza8j z1GW+%)+WnFtUq&D-X*JU*d1x}s{;GiLGv;-PgbMqH5t+dOM%4yIRRJu&9QkYdb)mv zHUL;)$FGyQzd+kaQ_L2&GpsupUFjaBW~hCXg=Y0t3B4^=r@y8v`|Pq|3F$hnOSEL# zR>-!eBX3)}M%J>h`gi|XzlFglooYzj)~{f;ZG;xQZwpj+ehSi0A6jDa@YnF?0_t@Q zlQ&goouV4_%S>o`ykMnd%03nuCDPa9&3M!IVDz@kI)?S+Hq)UHY_y)}{P{VC88mhX z2H65wpN{^Hc1JRA?v6Cf?o=SMZiwEM#LK%6x~xT0QErW&lc9; zh4&ro>{K8(+P{FfU*5OCVKUO`08ZZ7Fwq_@MS3KX)=ro5NHs)%cZzny)9zktff(te zpkNu+Y?Q2XH$E6#TM^5{2hX8y{mO2%j?Isor`$eUYPY*za9o@azt#G2#4i6p=A8E8 z{fPYd509~=4JCDGKZ>C&py5ga{dLZOZsxQ0ChOt@;}PO9hMYNGKe`7d)W&fV`kzc$ z2&Qjd!`kw){ka`TH$BFh;}t(`!StouWc`oI4-du^k~dzP74uEfohU9RYWtEO&|=U) zd}QUHvy^%dB-F>37>JLSihL zv^(U&Vgr+dZIOL_|+$WPjLq7#_Ho7 zYj^i?>(jPbniU2!uKRD>%C43_9s2b^hkgzYvej10Ezj8w-Dy|K+`hXN$n_T%pya#5 zWWLGmNnvN>$?Q0+dL!8mf!PdaF4_YmJvz>A57aW1cD|3ZYCHJ~yKUhT%p*mT`D5>f z5a%pgn7W!xA&1EgW}Iq;3u8&2LMqVFF0R?HV^=3&M$aXXAo*eg ze$%&$(e6>}j(98ea8h}zE=qWV3f!if$mGQfW13+2%r$g)h8eABxA;Gi^{sK8mE{oP zI(X1sbW4I?2e$%YAui4P8|4SLlLIFk}Yg{DnDZ1pHf68o}brmQN;Qr<-fvZ`5g=>Nw z3Z%3n_q-J1#zgBy-b5)x%R;88Kx`as;Ku!LD#Dk1ZF6>eX7(O{TW?NvbLS5J5eTS2 zZ0r(W8VB~{Cin=bcg`6qtpD$P|KPLc90Nsw%Hl^^KjiD*4!tpJ#ol)VWx(k<0=F{) zqkA`cQx+k%%HcRD&C86llW4pr zN*m>q-pNxH3RV3_0Qxy4lr9S}ecc!ahx^7H!&Ug}LHRYk@`JeC^ixMKZs!ODgn-j` zr!=HWmx!vrH1bMrxlr#o`LneU2SZ~Lc3+#CF5FXlxNy&b(zjJj6^HJBQ(37DbL+yS zPj?Fwaqef-px^n<>!6FVZa{aV;tm`E?+653L29pbzA4g?wW>r-Xoh;;2rSdlz8R*t zp7k-ELO?%eLLT`OLb2+yJ*R&(Pad`$@xFEpjK@~Hv8lB7KB(0Kj`Th2=Eog40=^Ik zsDf1gKHhq~hO=Z)sZ`L8J>@GNC%@3A4v=npL)k5BL1+1?X+>()ZC-Yw3-m?Z?K7~S z{u8Ridh51JD-(l(?z*D%pBPGq;P}%j4m9FBfZn%yM&1IDk(7wP_FY1omh}*4yJ9bI zqqK8ZTtTXT9d8fhXwi0k530t_+hQI4J=#N}p^mNup1J6R_KXl-Zp#se@)7GCPI4Ke5G z2KM_enX9LDCfojQwz*Qi)6C(V1W)vVr8HaJvf(|AQ9Hu9VYBh-*2Z}wrp?{|-fU;D+sg^ z*X7qK3hwT-h4b}PR6(lGt4iZ*cW^_WSr4&9HDCO_`e~lr1jE~wUAT@!x|Od1^b3FU z7|n2L9lZjwi&gGH*h`0z@m@W*aMkBhC3*6Un|n?idbX=?57x6oL+B4~##2kr!+2#s zxi_%;<%Pvoxmntl~t~ZB6mq=cI$}K$0V?-wIM{<7=eq zWNj85qR@1CO>yNgEgJ)|>qH1qC9^qDH%Cn)2X80)XH2ODRcIta-aL>UCQzFu93cbe zLAP*(Q%IJZ%0G0&xq)k)+;j5c?IWE;`g3_|j92XJo3(C&r@Q%<0Y8X7n-(D`R454tLjJHr|+Jk)|F0jC{YR8 zS2oJNs+wJ4*t5og(%EXH)pX-)bgkg*9n(Yd6I|^NO zna#50-Z&PXCvN3R&`Q68Ydx!%v(3qQ;^M(gUDp2LoIiCI!%6wnx>H9cc1@SBJAXZJ zw>Xg!keO_@6Og>ovAEbrYDOJd=V$E7%R#v)jeYd4hN&Jh6+~7!5;m{Jn}6JtNtgt2 zG!v}KyRJAm(kQX}tRNv5|AX1!&PMar-|minTr`h-3DYYzp}%q-`WLH06B+?k)h2XU zou7c@oksMLA7xjPaH=rr&>7)vI~0h3#rizn&*G;$WMqa?J*zW2Y<;?1@Uzh zj;928Y6(5jF*l{Wy9^(QE-p->%T%;W6`NOXTF^kkhSX|Bal1D{wshE@lN>ZQZEm)w zYkaQn5X0T_XAA52bPMO`8KB*#1kv8OkbOQ=fQ&toYs2Wo6FQH{eMsYx+|@B-bRC4x zIwCs@k;S1l)4j0PGqke~?LyW_x`KN8b_mfWT!UJURdtP*&Lrf9Bj5`GpA{sFcFbxn z?DG{qU_VLccIcd99!9;ME*nm!+n-}BVTx6Qbn%2!_|)fGKwG0RLdPw0a%b_n8YSI3 z*D(D3&O*Rv1-a4ZJ(QjHfEyyFWB)-_>uxu9e-8RpU&7Oe>Mxw5zwzry(2SlTJ4xrL z%f-6kE=GTF52UyHAnfhsvX7ZnA`cZL_3nW#Aq)PM^`C8B1ZJQAQd2DbW;a@l84PyD zPRW=Mlx)!fW7`zRNE(JLodIG*RLy1&t4&Y#V^U(U{A~M(=+>@+`mVOn zbnfNgnr+Q`g^my3`%kt*kZc)~o>k1rbS)A3c^|pavqtyEnAvXbce;aIIS7OWlZ)0z zoR{V1w}P<9&wYIq75h|o>uRMKUjq0>C{mc)b#NWq%PDF zh#HlB>|x7}<57B2l2)$3X%fb)%PptDQkefr^Xw?hG~lL+V)zgxZ>#xs@Mv_r(L1UbAVuarEBOjv3cUDxPq|Q&(p_M?y(0T&{3q0 zhP5cSYSw^CB_ig?F>0vP_CtOh=?c=W2@A*BwfhP(32vu6v%}(TW^9vCn-vk|Ya@c* zoGJ&s+xq=BLPzPr+-#>pv&zBD+fMg#kQB03E*l@)*h40!Kb|VzBQFfWBQ^qu=cCm` zJnB+e*`fN(FzLaJEgn`mm>KrEWjy39U$*o(pFC|FH_0sW{ZBv@gr)BCH^0hPOO$U> z%Pp}cn&e~0#!^E<_uSH1&$I=+Fqre3_CgiTJ~?i3gMj0?#kt{Al)?sFLFCNQ$IniQ z6)v7fx!&%xK}@2sv#$!;Twkyh`UZ`2v@fWj)6U~B90=sBf>_foRj4Fa6cb%^6yK_@ zyz{f5v&E#>`OW(whK3b%Qh5Bg0Rp+IAh!Ig)Ed}M(pO(Ni?V&X zk({Ign((l}!JoGg0zs)Dj(8X=#@j0ka}V9I6{W{KL8_>%;}smEq3NirQ)eQx%LBn- z$hmL?ydV(t3gQ}1R`WJEx8u00GtI#paP#0^RNM70!Vm3-kd|tvk!V1jS)&e4w7v%= z8q%CP9TUV2N1z}OaH=4R_TU6Pm9(`Xx;_0|7+5Dr zN1&)7AWM*DBNqm5fKb#B;OXTEctL<&X5`ex@~xX`4Y_p^}wy17)tR zK;)G_5S~t3Q%GS)vYrkhh#J~6twCm3Riu@>50^@l;!|vyyh;k^%MtL1fNeZ(uOJ?& zg5^khA*J?DLF}1tEq8o7@Re8H9p7muE9nLyBy$f!ZtX$wgCEGgf~Z1j`5w5^YzA#Q zVjI@E;RxhC0(n|Nw5g|1{jW}Ru!3cXlbeB#WHur4!Yb6DA3KQpOuIG6imjLT2=N&1 zXaw@4g0Pl9ep4^ntSAeEpaX2A3Rm&Z2O!dd;e}QUkWsc{(Bt3?IRXrU;8qYCN7Pzf z4`!n-U$&Z=rbalC+ileDzt*wVHzD~ zU4fjfHN!^W@RZW7!2I`{FrC||NVg>V*C~t9_o#Xr=?NYpsaKvEFAW8Qv?O zYa01GN5B&T%D+|*p=+(ELM^-R{*b2eNT^zmgXxba1F9Tu(a|)ox(nSPML*M?FJajB zo*RkHfu*VDa#l&s^4p|rpzvpo5Xf-_slB2yOg@wb)nQRk4?Ad2l&bS=7`~_s%~RB} z(+xqnF{;K-(XCcb!SqcN7}9QfWW{#Z*C9!Aw9h$@{@jrxpag*&Rgju%8u+Xn+D;!r ztUbUveyS8g@fyhbyb4{jHWwmwxpHe%kL`wVeiQVoAN9z}#45?p)o|oN+7-+VM<53X z1YAK%|MvH;Zkp}O% zH8h1BI_z&`&Ar!+6AedDe&aA)YQMnEk30BAU_KpPx-~Nj3numSO_)c%+jbp;P8Eg@ zjVt9*4Bpu)t{^do=X>O$uGkb}nT8(yyg91wrNzHL-?n?3Xj(Sw!+3(20gI)`>bB?U!`ApsQwm*8WX+X4yrVjZNLa zMxUcgH(e6sc%QqAm;Ke>Ew_wtE1l~*kgkx(jWMTR!-x<5kXd|4$CwuP+!1k#;z_#Z zflaj7%y%@5jH%faGajyGb8>#t*~Y^rt&-hrcsdgV{-&scl>B!h?f-CR&K3YQgO|B` z_;FD+83z23T|pQ*!$JQ6Ns`yj_zCUbTits;%pX=EDaE)tn~H1u<3hLGPYy7y^;$JV z&2UReUr)G(Er+hik&UG@af4Mr&Z*D&+kmc3JO&nHOL%m5b?h3a;EQ$*WPN|0@JOZa zXAU(IBbD4eLgB8^4YsWK*m0@0n5DFf7(Q(ieTuc@+jL!{W0j**6%l`)@$j*_5-WUH zR6$}7f8%-Z`}hil(YTON%~DggjV>RsiK>bIgKAVsPPqgK{8ck*8%&?Jatj!8X{Vc8 zP95qrtB(gt*ou$No+U;$3GgeOKJDn2(>Q#GGvVn;OXM4&7+vFLcj4Kd^*sjZ*2XgK z^XTcr(RkWBUPsrg#I)_{9vcIOgPC+frsq}J%|<{nIk;};B6L0mgk!HePWn$?)T zJ;jOPo}K$JjDEqCUeA4V9AZh&m9BR8UcmN+*qjEmjdVhU8=P*P(*wuHh`z3swm?eY@C+KDRG^D@c_eyLns`)QniAEN-@>&H7?4 zM(jR~Bb<~bri_A|txRICL^h(}T5Qkeqhc(K(%E2;Z zs~nD%VcDmP@auI6ul#xqA3jjZDc?59=Hy5(j^SPX*n1Wg--l!F^5iAEv6B7j!zE~I zy$2E)N`9!xqbg7na1pAuMb`ML2z}>tHWh6?^u#-xfLf5Z>m%%)GmcI$?Og#?qux$# zzFyRoTflI7yBmr2=5gqY*K~8wt%INr$NUM((wCdlSQkw0Pd7OIC@1&TceA?{@r~}r zgg&+1vf)osxCJ1G-OGNWzrJg_Wd`#Aej&-#qa@LqE#1C%?v);l+N;-uks>2JWJfxg&fRsKfNXZS8=;|zYfHVU>4JaDT18+gU z=_{naRu$n>=$O$EYlqbXZ%4+g{n9NSi;%%H?9P%TLib*4guJoM?&iy)edKEx-g2+d zCD*-%OM_SV3hLJ{kf~U@Kng8GompU3{+{dUL}b%?G%ywDGV)rl^*gCmTH;&iNoWto<3Cyp0G}YW(8lM#e zx-7TpU0F5O!=T$=I@F_of^J-6nJnY|E{J$@gWoQeji+vEJA{neWP63W?UT@Ln2WR* zt0D6JL$pIO%UFBl6h&GSWl|89&XBg@s@l0Q{GE zQ+{s|ZgCFe@6wMKVeL)HE)}7>jA2tZ;Qfh1QOYU=uF56~xTeDOfY&V8pPTjfRA<7|R9gR$l3EcW~h9+iJLW0U@MONb|Nbp{C~> z(5;^<+tXjIhluG%X&;!@D-$uIIFMxNg4l>$vSo7)`csIkhN|BLWe{-yUWBVyKjYw3#hc(({$E9Wb; zLT)o&^B+~vJ~J4Wj*ojNf9Yf)20T9m16+c>^y&~77yqyA9vzH!{>cix@6^4s2jUivfFA?`uOJ3bwZ=~@R&Oz;bgn+stj|Xybj+7BHzBT!8!uye zCiK6|g0{zKrv#aATy~>%BH%9^0WS#ntRS8i@(kyWxoz9_!rRT+xaTj$Ggoeb$-%sP zeYP{rqDv06OE|0X?%(#ss;28m*6~t2)OSpB-GbaVE=AG+u)snkyU-u%+ z^e=aScKQZ;io`RAa}}?hQ65{&w~k=M%-!}_lnid|!~s}Nrbx6#;6A?e#Nrv6ahgW@ zEi|D7re87-t?_oJVos!zJ_Uj{8ZS*;g#S&>9$2%j?QDyCQ8qp+h{`O`Mn$>zZ_lII zlxepIYGGZV+!)7{NSW!h%I}$5)}`}KQcg-qaa%0R!BURr$b#%D#?+Fxa;NUzfKiLi zz;Jvor9A@+#@~IT{_Qqj6_w{i!>~=4PH7*8U zD@#)938%1?(k?+L7KKFGJF}d0G5U~B1_PY6U+?{N3QaTjW8EzcZYAn5e_|hee>?#j ze@~FtbAk+C(AoK{ASP`Lq>N;zNaaSG9osH45R@Ckj&-yuudLNl!Zma5c%S?X%RYgo z-*Yl6I@pAabKIiJXV#S@Ax~(`w3U0Fr z?C-3lc#Rg+;;BfUrHb+xY3xN;s)3m!n#=T452j)ATcqXam}Oe=Z8fkL+7QLauTQ{q zdXI&l;UaV>CWrI-$0h8SH1}e`SFlzN_HNPTc%yGMTHU{i`7bwz?$8$If&HgdMWS-B zv^qk@-knTRi-CywV~&#>doi6l09B2<+}wRRWPb3BFYkc;jDP>)wutG-M8Ax-%TooB zQj|smS{6q8tpFiw5LlM19vLpp`Fy#@Lez_qd^Xs`BE>B z735a`3GR#wvCI6ox?{xD9$AZO!JG)V+e-#R{W~ zhF+iwF?+@m9F@Y5oa#M;GX__~=nsEJ>j&GQU#)Dq2_%`=O8EEWB#0rHPn-WF>fL%W zYC*!wYp!B?cbctqFhk2<#+$R&;A$eN0M&Q!C<0cW6@=Xt$U2g)pQ7RVRYh6Tul^bt zv)@rftAt2y@u(F_e{HZ^nq0%nX(n3z<%_Of^1G=J-g_t(t3%@t=j1M!GT7pviN(Be zeX>2ZahN~Z>PN0$TBU@Iz;S?IXY3%Gv;2Izu8N-3?QU}(uZg)cmSWZ`1CY39BQ40b zxBD@RBQN$YL^y|KZp_SaNy9vAm$2iIUT~DxFEd{jW>t z2vKL?9f>lhj)88=T1Tf`+cnhpP3>O8Z3Cq@#)jRSJjBpo!6yWOXU;7Hi z{Wt=41pHPIF){`sb?04Xd*x46w`3kd2GK|8Vob*lvah+ojpRWQgpEOUwVD=4+E`!S zwY!p=ggEY!9YfQ(pG{dp{Jl$c7MGNM5}+9JE5-9EJtDUB9Pw-!Xmx#_$0*5ndBCK#wYJ1WZ-b>lsdzmIsa1{M7xm}3Wv0= zLkmSZSR%gt><8@Ahaf2{6!YKghLJQy8ygh@F}^9@?-wr*j%l1Nk1qt8{$2XbZnPLP zSf0*h;oWDvn5*UJAyy>wXiUIL##WKuW4aaezGcZuEIevi|A?nyrp>tWV^hX#R_tZc z`<_mVg$oW`!TvSWZ7p{fW-%9*p!kPkXta?6zy^QoFPnHi_0eUa5tC z&keTRwq?zMg7z?c1G*}BereSu|zF<;!xobpA z5>Cdo#b=(|m;8VhgDe$~y{oG=$NX`?;l2AYeZ?`%f3>kg_sV%p=ZRmja^-PO2h=L7`yVLOrnxaiTg_qtvgWlCO z_~`GOIJAB(ipPW@qGCNX>DNf^iiE0wsk90n>%zy|YxACM@a&8=h$vqh*ADHX;}#zz zeevZo`D3!gI_oW{lLRT&qBD}VuLql|oi%L{>XPzOacvSN&f0{qx~+(Z3a)D}UKu-h zH0d|MXTF-%i@~VLi_wJkiuqf+abTNgR>c*BrQpoEW2n%srpLsj3><)%-`CM)omHTT zD*@fjWRG+O<#=x0zJiL{9@ex=z5}?_q!LwX~Yd%8V1e!`g$zm&O<_$8XvO z=%0?QU?r(0Kb`ovtbf>eu{D?lQ|j1WEzj~Ow}Ef8S*P)9?xxyKu{_OLq{r63Kw^tK-r-W zK|Fpqe*vpl8JCu?&)+zCY2QaNY!IJtIjCvZ6fcQ0zx)kBP4}V)l{06?5pYHz zPb!El3XyiEU0S_KepQ`6i!#p*hd@X2WN%5cQTl!g%)4Z1Bz*QYj8&`4#Il?3VBD7@ zkZT0;w1POM&QvKLmsb8MJF(L(G7>JKaC|wSjt=i&h5FJ@@J4Ybj5XiR*;+P}0r;B~~8SK~P0Y@(Knbr`_dq{5(3207rl$z!50o2-t@p z%|-`XPqaXBft^h2yg^)ionh}Lj}74ab)78G3@BU?HmD)07rl$aK|Fxy4K-Q zfZP`7(CvjPLkhyLZAH|DpAnO^4U#T{Hiyw!^<=E~ROBkF?C3&fM6w8>kw}efh}7~u zkY276jN0Plar?q+$vOYa5#R{i$q3vQEu1Rr24c@HLEPEzq0UNx+eM#L!p1x(Wt$K( zF-t}&!ZNlZj06_;WJji=)h3fR7RluwL}HEMFho=$PurfeY&ve^2yg^A0!0mhA}B!U zD!q$aQ0?esXi`toh{Ad4#xlMrJjEbi5S*2Wm{Z@#KbA5!VlhUP#nt+6A+_B7bS^#@ zD21oQcuF_|908>W6bS($>XT4m`;!Q}zQgGXA9){{(wr6|Ix~$}#I(yO`|r~zOX;FP z3Jg*4INxS5jN#=9QA&AII0762j(~Rr3atRqq@G5le;$V_?VNqRqNoZC@9W42ZPOBBfq}iSFGKQcgq4 zN}+|7Oj)?O%%Ww7&4EqMFU47LV_*5oM^j97oNc?5)=?;J+;9Xq0vv%tgg~JXAmP`y zqVm5_z^K>B&;(nP@n=N_Vgl@ z-us3;+Q`i=DYgVHYC<5?9t2gh=V)X>BM8B3Q=d0jiz*1wm7r?*vixI_u-j=RlMd!R zbAbc1X~2?VpN@0udlFXBMH% z-Z$kPR5=}OuoD(ll{-S!VLA}zJiTK&vKaX19SB{%gjlg{Pyq{+nV<@Rs_7Vjer$vz z>C+%>84uI(pJ{{=_?|H~T^4if3&bA(0*Q5=!}U5Z5?3Zdx#0+K1ULfui$J~#kkFfZ z5Wl5ASZ_Kff`d&R6)kHruoN_!2HUmk)s z5<0$imT}TJMhP>T3fJCtAt=Pk#pxDvRaG})#WR&R;Kxj_N5#R`L1agl+ zz6cPLL5JGwS^%UE5qO`Qrh>x4A@=)&ridyi&aqgoB{YLK(lzewVYqP$__{fmQlHRp zIMi+4Qp_h$2~}FC@0kS9PoqnL>4(lRq+O+9it|A=JTRx^ z2yg^A0=YmS&jm=-rL`#c?^A9He@fDovF%M#VA(;ZA#eelfbR-KCB)>cf)9Lk!2PACZQf%Lch&i_cr@O2p z!Rv2dnLA$Gg(JWb;0P#1AWsB{WHO-UipG`&;{Xk8?v zCt?0?o?Bzg7P3vtp8&gHRCUkmP&XZ^h-aP?F+tLEKGd{}%k)iq=#xm`DKz5EQ>gh< zJ)C@qgtsSm*q1__u>-EKUj zvwy>Q?YPIVW|JA_4e!FZ@m;7JKL&ND$sXzQ-I49$8ukxDPOXPw#R#WSl&`dyOw>i8nf7r%3V{({Myk^_!QAjQr>gYQF^8lE>z%zJkswq-f9{c_4s_}2Kb z&}Z%6glCprbPC*h=pc-!l|6FvlgI)GL?Tw}jF7R%Vf?ZcEwtbC zntn$Ql8K1_r8|yyTMd+~=jc^r?2<7Hi@*CBi&={4`kGmj1|lq|jX;rsqEILF;HnkY zscHc;3s87$TCoP->`Ag>CCJ}OW&oQ~lz}Yag^7!hV&yBSBk;~^y-+qJJ9NU){TR3C z0M;#ALqFi|etj^yVPG3vK74l(j${Qr@!5$Cf-PtQL^5O`etAdQqN_Z^ni5r^esX=j zDg6l%B_XzW4)}Y9U+Adqg?atEG%EQ3>H%LvtkTwRm^>$7yS7H}0+v1j!^w?`qGKbg zn!i4X)BQHn3BctPg~=}>e?9q|hqvREB`33!BN;K8D(R5zGy+;I3$Yg6yJ1v|;&SLolNaK=_x@r)G1!R5Ps$b^L&gq4 z=Q#HsO_|yaF@H)!Y+bk%^N$(u-?HVHA2I;LYbn?eWK6{HDZk3o?UA)xp|5c}zB$QS zq{3%{Qr2c#N2u{*JD7EBDCZ8Qr);Y$H--9N$4?!wI+kA`HZGo8& zrQ?NFR~)h>VcvcNo^7b0B07XBY}TE>ti=~Ql#L|IHfWBi{c9AqEZEcY>)hp*?@&-- zLc18dyI+er5yK|^Y9SLem_K=d-_~Q|J)yE`}`8@q~s~T6Q|u9*$SnC^4aETy1g?u zVEBZkPGwfUL1RqnUspba{eT17jvjw}oTdwr$&XCbm7XZQHgdwv#u>L=#&R z+xE>l=evKPy1Q!k?&`gsz3SPmocu}y;k=*ewdZw7X@SAkY2F2XfTq zl^TACoz)6c#B9I|Z1v?-!P6_Iqw!%OJs)j!uFX?Fe4T4-w zy1`d6XgQ(xL^XIlWBwFI`DA8bQe0v*XNg5;vsA?^96o)~*g6F)>0@l>)98yo(y3Svhjhy8kJXZxG%%xbIoo?Zyza7ik(e1_TmoWwtru&OHyE zBD~MABoEgE>sZHgfqdDIBz%M~2!mY@g`B^A@pNhk@@tmWtk>V9Y8MI3BQ#c}yA?x~r+%23cp3a+%eWs}IhjVyx+(Pe0?R&mQ%TRD3Hpmp@kMROXn^ zn=b>-I1dN#C>?%B2RB46wZh#8GUPRYjjHn~z z^D%4##SRbO2W#&y3STu60qj(?j$zIcOy+h> zLbi)*OWKUUC;R3N@-9YVm8~yU{O7mg91(2vb(NsNA@ci~pG=hRsy2B+8*8fFX;NgB&zi}iB@$T!pQa3_T|BrvJ@du7Cs1`;{@@F&MnVo&vAUoHF z6!1#j!#%bnjPx_TXE^nm4tmOk#+$gA?XYi^TQCX#p6h%SI)gR$P~-HfQf+ITCPxAn z##$xNJLG#!eB#8q=X+o*Enbg;D$A_7|79zwlG~&!Ye$!vf6;JrIg|3O@xSH zqD@o6X)VO-Mh}={-=G3Zg;?Ws7v^jspBtU$Ym(BWMTJ9}qP5!Jl~4mHoLQ*lye^_I7@T z??mV?1ul1q6ds9-Vx+L5$p7fI;9TCwq@TR036}NTb#PL=A&H@j;LdIX^mu1l0w|WM z-z}*XKDyD0MB(^ifJJ)={TlL;_pczoX;vnLDMiTSm zrw@51d$P#7vI|?}2&R_OdkB}d$%z2MYpUw`hswv44O`D*7J^v}nQ&vw5`%TpbwQch z&O&xy$gH}`jPDA1)~JQ3USBjX_W|#ItsW8uCpkU>kCGDs`xaPKmA!=}VX?9_QXC%g zl{a|Rl|(W^ocH}NhO&D$v5k%R$vJtwo~RA)4!0*#zdey0SE`Bq9$aSI{D7U}*CP22 zq;$T_>qX3ohL@9D(aJdt06mt zmp}iO7Bq4z9hE+Gh?ko^8sBtUqLowAfU2IAo(As|FKwZ_T8v= zvF_o3D?V7ys$)f4(}<|8R9$!?1VWY{^O`qmv?YW7FfLt=^-yUw<3AH?o;XM>o@Quc zD9l>(k#r~szH{PGShYHm%XqOi*L}|`m#jUrlTrycR1Zlk;5-6q^L|;kE*jkk_V}eqz1cp4JSx${fxwq@K$x5j&X%%i_T8 zlQh7NdQ@)Z+@6-r_G^J1aL>0n&*l=^wiun220~3!`uY z8i|m!3i?2cirLu#v*s~Lc%^HQ?$-@v|)^KBRt5i*_r7juoJ#pFcs z9+YOGDh@JRo#cZI^z*}lur*JnAw2Sz__wcuIV@t=sGXtm5gQ`12 z#}UXUj0EAI;3#A_@P+tI85Fxi8-uUTNSYZdQT?f?pHgQ24~ySl*C-fe-CPV!rqa+H zT@x=7jM}ZpzIffI_Fkn%AMX2;H8}cR(mO%#k}Ia{xCKLsJ9Cg;?H1UtTzR)w0^7(c zwVv;y>n4bLku54AM}Ck`7SqkN7&kC+i*}*fjHE5iq;CUtn13^@mKFqe9>Wksq2|oq z7=D;<9(ITTfaG9K>)m0eul?sanLv5|R7%|sct*hu6z0&%W7|mBUDi<$vin`XED#mV zs^Mw~=CN_j%JJpE$XaKzZ6ypMDK)t=^N;wrbq{|^yCb)&xk{b;P|soSnR4>3;R-=l zpsPQI$zlyxF+2i% z7eWGfkgskzcg-6X4|C0UymA)C`9pr?Dn5-BRAU=ui-NQ#GLLI)G1b6ezV;)4RsWqR zlaNvW@$F5@5aw#SCRxhm>#`IlGySPyKnal$ZXm+~C z`DVu~d?fhp9SddU*hDmr7BUJ5LD=F zc_^cs{|*mC{I^g279?4qn{6TUsQa0RH`_ta-R*Zd=Uq#5Gdxp1iF0JwnA}IrEYzDV zp}cs9+(Uz&hO$|}8K$mCN8f2(PWVgaE1B2|$Bu_P2qfm+1MN7t6fm@F_`VbE_piMY z0Qq+V(Cn-hTb0c?7!*S;4OT|$XNgA^JtK;xoNV~Z9CQS_<$8Uz(UUG!u06|4O2$dc zA2#ww)ZF2NUM>d<-+jViK52vKY+k6qN|!$>4WGf+E_$ckXeuVXu3+E(6_T9{<3CtY z6bS$Ta!4XI%b0k=Iz1_8S8qz%@9^sR1dUkf7~kXTr8ONfT{A11DA*Gn)a$uD*X#5{ zK7_SEyF#0Y4yk{D`$$WL!oRO#^B%1N*>fDPOsoZW#dqNeV1Z7RCT`l1aYP@=~vmtMSC40@yafTuxx^!xVN z4$sc@5A7YCUA)=y4eqIK3$klt4!z9&MA=661J zQU{;-P%VXE)*vffy2}RK^#9-yq9^^^f=z+z;xMm=m2V%YRMU)Vuy>g1Qc(HTXc(A* zV&c^VnLBugc?AX4tglYOAwofgtOFmvup#fxckFnp3schA#^kex(2T!^ski`_XjP`< zAXx2*+3oF54Xe6|&QrGc-2xAhrf2%Rl@iL-dKTBVHaz~hj`0=of=O%iNlVXkksB)I zFpHktm6GsLQbUXNc?ofbPi1bch8awqoQg?bgIKE*kMHQd7irEc@V@((3nXz)ac;+V z{pPP~@Jw}1>BC2ky&Io$AE`N?CI(}Y5Y^@darY99O5A>YU7GO?HfV3b-W)x5GXVeC z!2gCPR_J);UdX!cWlcCHqlM5wj=)%PuZ@s?kYn(0g3@}&Kjej(7^Z34i%IH85G?C$ zJy~EB1b~MEtv8m1#E7pR64RWB(>_W$*TLw?2UFO^$YRir=(U8)2$mnUuFUANShwSE za=c3qkv*{%0YbLga9#jG_MZH(x?n4IH&3VSp)sNu6BGDLEEW%thkuZ3%1TLCTuc$C z)hKtSwP?KzNLS4oUp|yMX{#V5`s-MI4aDvvN9(7}S$%C-C^B9I$Ns#1wYU@7g(nJ5D(ue|F-4P6)lmCf-pI_eaNVPs@mw6 zNQ)euwAeK@Ht<{Bu;oBT)Cz@kwo+0CZ0S^{g#9F&rR3-+758B3V~BSj-HoKpTr6(z zb*%~Bvz%--LC#qM>H$PI#tDPA*1ennEx2su6bE>F80y*P^ko06BKJRI?{eMQ_UZrq zijZIeJh-xT%!zC!fOD>_HNmdUuSZJj!;W64IFGT!$1e3x`R-Pk?Ok-rz z>V}R@&wh`+7v7(cR7O^!fpa(9#FWbor=Hr$5^KN6cU-r9Epv4|K;u0qx+gshAl8cm z+qeu1z*2?1)3-i7yqpz|5oWJapZD*Hdg@9~lP-yYz4er;<19LqhXT%i$a=gplk={~ zIrleQl$qp{fIRJ4gK=#xft9fk_^2}=r}ZYy`#4s-uTfZ^oBv}n8S|0x@B{Jw)Z|9s zYZ)izeVbttUwynff47-6E#(vHRjQXiL0qFgY&_BS`BFpHRR1%se0HwgmGgSAWEg9u z4-_YD;cwkG$KLe(Ns0k7Dvvf;PWoT{96RH&=(cF*C*aun-M!73Km_LI*C`zrETa}3 zr^1~ebtZ!o)ju6$RaT62NILHJ*HfNBB=!G z9c((B4wwZ?QzOYiQeQD0tY4J-S^TbsM%vc|Y7YL8cT==X{;cvh=D_NvbSznJ)2=v| zU=X)C?`Z2e-A8z^$cxe^wVlaF?a+VD;6*&di^kl%=ZE_SM0?I9e|576Wd9jdi&Zo@ zw{DR5L9%0nN{3M{gWqMMFB!d=p1+3^QO?%pYs$2yz4BMJx~%&F9iN~Vaa;a%*bk}T zyJecrTTz3qee}`X6T!C|Q`dQyQNNrWsR7G~1CUucpZ&LFh6qR z-FVo*DMsiQ7*zDb8Y*G&JK!w^e^>GzoAE(%AJJQ?ju?~H;FZo)9vgg9Ut1Q+w1{n# z>41(1m1xb|du|}kfY@ehp#X+!=m2(R&P8)68nnQjFLzo}MLbJJ%EJ~%&qj7r*Z!TP zU_V4na^P>e5i)Ut3WFiwzK_^a6iM2Gw{#}ceJG#s+>u!aT7qu9jZ{IrgzJtivjc-D z>{i{edh{+r@3WyuoUxb~^u!m3)CW6PK~^#w(pt_Uj(9IK?=~9j)K4 z3&JLUn?-3Yb-9Z-NWQCBBr_tbsqPwu@2YmhWcsQp6r#o~Dpw9cztPu;LL{kRiVZ$M z;Z8dFtdr2m3#Cvhk4v`hnp(G%VUKLrPpQ}7A8<~f7siKet@P2KTSH~*i3&S!hhMs7 zA|QL+KGKh^GXxq7amB^d+KX@rH$e^u2dsR_Fnyd*T$blbHL=_n&F|@wlmE84W)wgn zVH5M@?Sa_Q$NE3pXZYsa>_?O-=f^F?xf7o!*A-e_Ho_c9SvGw&V&~ZN*L8-m{$uHv1Ygg#n_uI?cg9-FqPp1@yMF z^gy>hym0k>yYbarFrTlnyQGf{>wQP3+gu2TFJ7o(80zDZy+a&Bp#$_e$85}Bao3?c#NFR8NDNcOB%*DE{ z6Ot6?8_sx{!r4X@>-1u+!lROfZ0Vc4Z7CG)3n(hb{C#isDe1%xVZ4_Ry0p7E==Ttv^aABKOn` z26Hc!o*927?>#aIfeFj=Q0*Y_<^_~M|p-{;N znJfz6k0dd~2>N0!!8|XmBRMjn5==rF-7cILZ#M4L5zzb{tcAOc8E}vpkG{UCz}zd# z-_^mCRG7xUr>p=8*(obphn!~I3S}ww7WLgNFs5?l{R%ErrWc!h$2&#H-5iuIxnU?Z zpS>Z}2aC~(;9VD{QpU09U5Ba-QuT4LC{a;mNjPQ+MMois!-v%v)I4PPW<{A|IO9wf z8lsu7=+11m{e&fF?vEqgw=1keC5ytGkgH}=Rz5(lMp!zpElDn@Vp{#;%9lQ(wijYS zf|9J-(`hgcm}B-12V=Ki70$-BqHpfhSt>umnyFCbJhNdjGT@9m$szu8g5_TYf{mV z9h<|eB!I;-F6)W7&$%_5d&7Gtgg9)3PQ1Liy^5zD6f~K81I>)z+Vsr+N#rgrn@et8 z!{C;;O1#;rUTJK(2lY)8<-M?>PK)b5bwM4c<3(3BP4FIHk-RrV5XL8v^98065*-Ll z;lKquxgMpikYw|qk-`kdhZIRGbqv&qFVu?j${^5iiiuIja=S$2gUE6)>K9xqNoEr^ z)T#+e?#A0{X<{y0iB%I5dRVR~v8~2$%W2k23+jCp6>|oK@oZ zvQb?(EK9mf7fraqBv5Dk+LX5Qd58Arn z{biN0k8yA7FP-j72EWLS8_8X22j}e@`1>S5koW0b?!O&M{pHCZWSiN;?Me~isH*Ju z7c>EdVLb<(f2uhk@8#y`R#7>CH$o~VZeTc{QzYuAzDG|m(O<2DZ&?=xD$oa&h|VA7 z#9LRl2b!i6w)h+fg$&ze1&5PcM5shhSmPwa&bOz_2`vwD_dgw2oX7w?g1(__?%=26 zynY2ko@j^grqR-gt^WH7BrtkzQFeU*EP^1Dqr*2KLPUr}h4dXl>6Id8x+_jdzsJQg zjX*8euVw?5q-=dIL&}wUNf>sN=b4YikFLeSYxaLv6}7}RZ+$r0opV)4*Ru?vWw!q(;D^EoickvK z0!5b0=H1>$-^O=@etuBOeD|||SMhMi?CusqLjI08*Adr#4L#bmhqfBp<1Tng{KHIa zkSLk1Rn~a(?!1riJNe1GXqw!Q6Drw$9wpf8Gpd|X+**>5z*vD2j7%AoxH{mQ?Y9zn zdi+b!By2;WVFT|%#Y7=M%aw2OysgH#9FQ3MYX=?YrwdDUigS`-bT@uCFY3$`xy!Uc zCN8-%-QO(klWN&oxp5bb9Zu%|%iX=U0J8dH>4#&td_!{wryGrofGU%N@YF9?yUnZ>N6-`39GW`%S@Bxdi*bqkJKjGK)OBcpym>oxT;j)C!FQL8fzkgS|C>%Sf_m@zqC2EpKTWaW z1m4~E#_hp!J((%p63eXlfRg5cvRlKpJjS%{M3|Yqaq;Ws<49haAAkbM)f7+mE0_-? zwk}Xb4<_y=fm2pb#9jwl9>NuYV+6MIh!Lf-|L29|0%;QjX{!Z7mdk{a<`0dSI79Zn zp|pHhK2EBlF)MUr(YrTb8$p})`4EJ-@-qdJ8aUJA+{MB(zqP6D$8ksv_PDSH)YY}z}W8?tB~xLJYku!N2J9p5E<VqQ`vemxV=fa> z7~v<;JPxm())BE~!J>pBIk9}duLTK2O3&a>E$zyf8pVG;mksC4X4G3k!8&v)hFE&c zgwW)N*vs^cHEjA~bl4@dTH%h8I1&H+8^5=pfgjEQ-XuwjqW#Ee>CtJmV~o+eBEHpy zv;;5c)HVbWr(uEixnq76Cg`!~3NFQevkDR#7)&k@a-)6GYg#W-()51hWf3!0z+?0|j3 zD)SGT77Qpx@CIYt0&7Gla$KRAB)4`fkH-^JI-Ku4EHpIwUG1Jyt~UG~IFBLS2Z@~5 z8a@V~FlyCQB(ujMv1ngG*YZ|SR=`PPi;wN!+y#XL64+KC9KALkvBXPb=L=@E+y-tJ zj-DN=VxyK2#Thd56H5(U>SqWB>=ptghe+XUlx8b7e%K-$N%sC+sZh5Y&|EL$NluNw z;R1GW<6;Kn4fv-c2GO0-$JXsoeAXF zeX+_F(i!}3=pgZeey|9`NwL`2CtxcLr%hFhAW?K&#KKKTB-sVpYyD0n6ma#PLZKdF zBzuD3=0q3GUsvI)DP2Q90Qa&L9iId_$IZYa^^M?f2iGg#uR!^P*`|R|1RZFDIpgjWMH9^2Ohko;UBR?Wgfyj*_7`3W zuB21TACm>MIjIs@g^ezV!-{e6Ol|_s?>YY&PMUB90uY<$ZQ*d8PQ5lX7KV!rTk+yk zYk8Xp8u}4eC80QYucw-!@CDN_4+oVcQg1Fq`y!$XU-9+@aW`Cx^oawjy0(a?xSrXh zqU?Jo%C+c;Dz}EEv-FbuZwlF`1$m(rvJ6*wp!bzBy7Y--rLJ>UJB4yxX0$Bb2rk0E zs6pxS1s_uE38bwpJZ-{>pyuOuO~kk#miz>6?FBvgyUcX(Q(JaiPHf7RzP|6yenuH`k1n**Gme$%Tl)e(YwA8cKCtvI=hTt-AcErRR678q$72p>=TC$o=mk z;2HBJ9_S zP|61|idL0FL<03dRXxsu{@cWXm>zQ^zutv8S^R=@T=Nk5m=OQV7fumEd9X;X<{-Hh zZzNL>ka;4hQqc!ba$!{iCV`M;NLAijVK1Wuw|1d1fy}g^nj63(;U`Lq$C;#gp6C(u zpG~1|^A4oP7^0#=v-N?pm87f*>W%O$GHmcvbd-nxejB;JQ9ba8)o&gio-I~P(g)0X zfd>6y@<=%53d>T7w@0Z;01z?9RIcCBh3%@PRO_7+=s24w95mur`%xrw{i1hjk zEbXb(M7a?}2Awyg;azaZv3p&x`HqgSpgt$#zcm317(n2I_lBoXex9nfW#!oB(#kucmsnP-f2 zBv5ERd2uk;{nW3qcc}l_#n^j;-O2?-a3YXgK5qnE-73xUgr9yg1{3konNgi2Z-se21Hb6C+c_F z5e;zB|Ci{n0{M`MK;$B}xx;V--CV+NLmgV9rOw&ljaz;JTL3hG+zN{BpVT-}I4EZG zUof>#FRCFvb^l5ha8W4H&4bN-xO}}$4}tQ?PKe3Tyz_&E6K;q;Dc@AmNy>Hn6SGhZ zAf6;b#&N!g>T|0BY_*vg%AAGC&S%Nyo(Zv?J4%KV-b^0KYx4$z@KbHMg!c0{0 zQMU(w=-G2hf$j^mRMx7Ji@g~W>`B5&LLu^x z;$dqjPutb5EhnBOPX11vw|Kxq>aF=Yc0oCm`8v%V8lRK0g)mE+bUf3E@$pkqkP$M)s@wgC3O- z*EJ(TB7o$Vt&kY5!gda=N)Kc}bVW-2^G8>I(ZU7>6^04oP`hvZhp%l}9;;bBtQ(7=>T6Jr1e#UtR_4 znm|u8Sf2NX74rJ@#H|vpHvr;NO^3qjWyFU9^FqqM-$`JqfD$e5ktO*Jth>-~6EE6v z>Ql<%iqOKxpbNi?7Zq%UF0;v37Y&VHoSOv%!XjjB9F)r7f^c^1Kj7bi=gxmH;lxJ0 zgl}*K%qk1omAu6aW#j}{P|y$V--s$H?X&Kp8doo!kWrMNmOE z42khP?$ZyeYMp;3HsVU`Op5v^cFOL|x{pEYhjYG1j+MPJH8_vufE^j#q5-2KXoouu zXS0vbXZSFuH`*IbdiN2}uLZ)#!ifH`E~_KrzU}I;NKU@CocYv`xzJ?JPB#U_U<>XF zq1cfWQO8GKy@$Lczd_XJLNU$ri)`mDSlrQ-v=%@kC0;_s^C*s{LNdK7Ndl4xAn=uw z6eTtf!KEmd=6+FpA>$-{{}5|D3G?4N;Cx}YA6j{|l{EgTQiyuZEQo_KVRnb6SH?FZUHMFW`dXG-1rH9c|& z-~ysw){wo+%M`;^fJ5L2tOn!yt`8YC&f&cIHeh>+eHw{wv-4orH-U}rcj zK}w!4zl7&hImHTrjPQ+jKEGxq37g5DLzU^2lgk=NjQ+^o$xcAQNiPRny$XFiGp4MF z%mkOK`ml?p4RCdV);e-cZFx^2s%FuBY_sWJ@@><~2YNsm9k2fC9j$nCvy-?q?|yhR z)^!>?45wxIo9u_s!yk`#sU$hrtdq_2lK0~O#bp|+Di&Su6R#h~^k5$Kj@CT;Y^tj$ zHgg}AS>;e4`!eqL))`emeZatnN8+>lvbDnE*s(#Dr`1PbxxPHG-1U&oEl473FA3f) z*eIZ%<&q7H)I^LrtnewsdEp5;yeO$2rBGD6HM_{|uq|z!8MO$;aQOo-{^r)ma29DT zwdSe$9Y=De5)ItuUnnODZ-Z7JCn3rT8Aw{_<6T-5Ynly!-iBRN z?cedH&TD>?C1bFTTc*Tb_Xot5SM{$3J+3<4tF=jl2px9<%^xAT!i#bA7jE$wD92Aq zWgJcFKXF3wN?n#8=R=MeFGg>pG5B!#o{4tR40o9?eVpKAo&*oiwKEzUrRHcR=ykX> z9xV8c&F%*PA~kaehp;Xpbs}Xk3jXH2CEFvS^B?@7g|}&Pn-_Y2kFWF}v~^85X9vB< zJ`DbP-7V|&z&v%8v8_N_S1K%(WF^74XQh_>$%wMp{=_~J5EzUB;AT86(o7bgBuG0T zh?OFd$G`@RZ`3cGhDi$`(W>jnpX}4+s~@ygb_`C=QcdQZN@L4Se_M6 z&qlm2-{{Q_(+Tg3$M@#?iyMyON7gJumxFP@j6pOIx63okOnoDL`}$qDqJOg=CP-P| z+MlUDCZ7FX_QKjOV$L|)txiBfIp1}I%=$TQ#x+Yp+IWR=f=2*lmIFr4rjPNi0M>=<80=7X9 zdlyDFnpA$k%38j9%fmeZ@1aJG4zkM$L*&TWiyRa#;Vh{j#g>eI2bB#uzk&cJVp%ve1Vw# z@hFw=Xie$YXMYmPNjTFrqo}val4t|0a{k-66D$(Qwf49bOE*vCTa=&R_~|$`sv(cH zU^H8Dj+FsqM9qc`$24^X|FGhJVm{RGr8DrS5J?N=@OGgfXS+0leH|aO$${b5Hm^Fe zuMrVX>!W+yw2V%Vn~{`u-2C;KaIWf#H{pro;7|uGE{YCJzQS>sQS$Pd)szc*GqU^v zHGfxe^{@--kQ6ryQZg^O2sEx{wW5SxhY`{2nw2CQ+lTlRb-c)@0(mZjS3M%$A6%0K8}sI0xF%=OFLcFD&-Y_S*{Dkq(n;}AF8 zacKqXMqgW*nijZk+lB5YuJ)g?yr)@~=vUAdN>&5wi*v2t+uf#mT2<<=rWLBFXlH&Q zmWwWP_6e~0LKaAbSoHZs8p@9_1ACC>ZN9mJ2^hm+?xZ z23uV{P7R^?ma4I})fv+?W)GpaRdJuzUIaA9+;f{yJGB=|l>&qGOIC5mJm78RbC;Gj zCzmB{RfRzas!JX3ra2+kv=;l4aAc=-LIzUW(v8=2K$Qkd$=Q{1eY_4Oq}arzv70ES z@r(CGb?WI?Nle%HHxy?Tntdj!gT8HKYKcNXrB_1ziLmd%5`v2ntEqeW)z+X0(YWtW zc4g|ihH6*WtVq_tygGa?UXk}yvRP^zDZkQatt;^Ik0tXO>es8#k z+IqtYP%c)>NWPbqFjoc*sL>#vTCMzxD2%abgU*RTN0$71i`&1(@XQ^gi9@?ADeb?B z^5Yu2m`-Dp?RgIc;=9Ar^@vYRUPopX%`7rd{oF>RA`ZA%>(syk+0iZ3<#U_Cc)oS? zZQo7_g@}SV9!jb4{%FCRp||uJAY{8}R(Z6x5#nnK#h1@Md4UC& z+~8%ReM0V!f>_%Q2p7NeiP9=gnFWZW==-5v#seOz@1+jbXQd=tq3&Kq-*zSH2BU5` zeLtYoKm`I5B!k2@O9&_Z<${$5*f1vS0HIUj3IPjN^AL3|u}^wL;s6b@kMd<+-H|t3 zI{313)9gy*SKm#+TScYNzo`wrO1x`5Pp23h28dp4yX`F|`0FK!Iw~NJRI`orA=`dQ zZ-o}0YceqIPrTp;T`p}OA2-CV=Z`^65gFW`NECfc=2(Q&K?w+z%)9oC88gaW6xG?DqZoS;wDMV z(c8?QX2bh6xCOUV3JMWR5HYCQ_((?6xUU&tI{QPEm^JwiYynPPzZAKY$No(NdDgGG zBzu69N_}ytYfq%t7iq!81BV(t1aEo5MFGn^W|hnHKHl1iy!IFKWUSY`+7B&C_x0$k zCFy%I(|ZGifR#sSOTq#+2v5=~DOIa7dV|%X`~c zmmL`Lp0Ka|Y}J!3Im~ZUXvg0}8g^*)}`n(BRyDXcMZq_0`=eL%++7#H0@w)U5e#0vXn)8Uj+5)u0J;F z7`+x3-MxoBUPTn@qqrJxYjr6$L6FzDLGS%&x>_(!^J&GBwPh5@D+~E(p0Mu=9lSUN zCIr5l`6eDquo&hZ_11S2uFzPJi>-h5ITR5+!zhyzt6PadCJ>_(8gm96 z63jxBD+PT7r!$Ey?wGY}`}c{lNK91vq2dBp)Jj9J$jNu?C9MyX1JYhSP` zAsrS&Nr$xZ&GR!}a#&zIX`|mZR^LZaUM<|*sO~$}V2;Hqo;8=SGsFenGp3yQf_{+x0=lh^mJ*~}(e7Pg-UHx%|0kh+R(|)ao zi;VIJrN~R3^)CylZ`9*5K2X>dAuiC`7In4hpt(@=2 z(7X>cdo;Z9$Mz@$7UI!ym#Jn;yGdY*0;ZL&b%s^Ak0y)s-+JELFe`9C?fxQzKa^xnkmQ{M-~6G5fVYO zAiGWB5YOM}hxkK0uZabPso5Hd^fr}m9-ardf9V=FgVQX+nKC3~gr6;CW%BVliOJLT z1lxzJAPRT1$Hs5COl;{ip=f z$A{7L4H>y1{({AO%maY)`CD~4O^q_5A zf=wEfD?8cX6ixlfvvy@ z+`Rj)+_oNML3zo~8dXq~45AvbWK(O!>NOf7{&c(Zd4?Vc=b- zS<|n$ClYKh9y$+rF+?TVJw_RjI%b{QFitLSQg}Z*B!GjwtDRuV-4K4tX}K+`I%hc+dX&VmG zERAh(?)-GYvkc=UW4*DAu+kd#JO>w;`j#AQ4aT3R%e;`%Z$XMXKYQlA&(>JGRK5He zy!cDbD%be*Iglz>F_?1s_->E?d%I%np1&@$A}OO~pi8H|2q{kBF`E)sN#^Tf@w5-@ zd_}jaJbjqI9eg^RN)QQXY}pUSgFpEbn=>G^nOZj@g!9O~0N?2WcQ&m|r=K_;JY$YB zmtW`-^$O=L`ZvD2;@SOV8C35T*xQ|;f36RKlz7t&A%caZqQ!Ev3OLN|mptVFhTI<< zH)8$-29#F&wNNwve4)nm#33~cON%An5=Ev0_0k~el61}Z&z9-Sd0GL)Ffn5~Yn$Uv z-1ToaXuxLSBNc2cd5wtPWWEY+U(H$wdKV+FI&a{w{>#NKelHFxeapFQ4d}53AZA*0 zA~k8V(9Z7hK+IliZ8^@|KL)^qNydEc5q}`Nq@QvpCDG*kInw#qGT?k~tru#5R~Vlt80Y0*Q7^K_ z`f^HH|7k!GclEj-2_o3~2;RA)iKPH#sY)8)^-xhK0n#V&;#&yx11{xYGFEA%n+&%; z)C_=HSpP1?qgT$Nkr;!3(nBp6Uw9MSKd;&0lR?$^HZsls&>o6BN& z@RCc94E?Y~x*m6`s0c@mKwem|Gtox2McbX0`h;p1{8vf1ju(mphgvr%7YnG5y%bS{ z{-TCv3Dv9=xS*Wg^wi*%MzP5*-RuJq-73zts8M$3sET?ZhG82Xxm3C_g!97b@|QcZ z=;=wkWQxBP+8K^4yRbBtnH_;(5bQSm{cSjp7Z`x_y8|duM+V$+%msYL)Jmj6Q~|y{ zi5ee#6)FT!F)!?E&Zx$o)d2|`ABKhl|GeU4Q_$31;!Y^CBDSWrD_sZ3rJX{JYJQ5A zAe1Sf*qQ#He*`^92P%;a4hc9bO~+J7`L%6>n!wyiD4|M*D^l_J3>77Ew}uz0!Z6&>|7e-n#0k#8gE&=#8|!A{ zuD50k;Ht(b_zxt6OHz_C?Kp+Hc55_ZAb@bA`QS46?Ew!B%sFJMz2LdiVT>JkHu|z= zxs;ig_*B(@2oHBA5Kl%Stakmww>v!dB`KPd#3Nym5Wrc&ihWH+lKn>L+#-=u=X=YJ zZ7SgU0IR~{T#X}j__m6y51ft01vH5FpHakd0`XvxUOq|u>4}7gF5wgBP>2C(;|2xC zC5EM)G@gk}pcfS=hz_A;>WrL^B&akn^(MfvR@A>=ceO&|UIt_1lR?-SScdMR{xzRP0|#VLSUw7etGk%Toy2P|m@aOk*V7z7d}SUa$-x z<>u(vo0luZj6Zk*RWhJ;W2P_c^DjSSZmUhp>y{(hA^#1edXn$f0Ft@aAg$Xzp()cC z?w2qh2T{Kwh6X)_g#wz3lOyppFoUQVxxxB;kfAt&*(ooKoq0~AmZKL+>2#}18nwZB zR#T_!%vwEdDk~OS5d^QoRGtOz1Btsl}A1&lT*$c;kM+l3SPnerh1gOKzR18MTdR zb-O{`PTnru^(dvj0ws+FO!a-m8pM_DUOfF!itlyL8^)F!`L-yfKb3?`Hp{!N(&=l= zz1qsPIMyDEdUV&2mC?osxn|MzXa`B2zWQ!H8)=7`A=6h+(cKy6o(K^&a2@g zGe9h(;#Wyk>tE~?93bR1!M_DW2>@%*uqR}va4)ADidHEq_CG0YpFb6$$j^+nnqsd< zpJ?l!Js_geBZN<(Ys1Ezd@iRL}=U&Gr0|!+_M0SR`5*W_Rr;2y-?x?FwVVv+1Jg zC534Fd5IY!X_=T*CTTg`b^U)_y=6dLOS3hMGq}6EySux)2M-$D-2#KVySux)Tae%m zL4!kZ_{cfWd+&X|zcYLG-d)|*RXtT}t*1m6FYt7$OQr#p3={?ow4k$cT7DHWxNR8* zW^YT3RiRS<*I}E?0WV@lI7|XIVgWO(Eg3-nBULB2gy-`m!lSi4Z=5H}<&@087>a?) zga@3G;v2%(4)-#4gcj5U7t0^bcq1(|h%`^wp|jjkHNy>8F9>Y54~^U`znFMLiASq} z_2T^OrDgG&MpLad^P6<(E>x0jV5b|2X^I!YJOUcmxMCNkcLRZ#0pzk_!c>*Kxcm9% zJhGXqW|B)%fBNa-Is3f*xp~5)*0(i=$qBCRU+5lru!)1s0@)`O@{Pu-~EDb zE}2`(JicSRVaAts6oU80g6-KMVX(K9NKF6+HP}$4F9@#L8}9Eu!)GhV8g>ha-`to6 zQXWgNFO6Xd6!h*A2lO0ZQedlrqaHr@AXR*?8imvodVpM}SKKb3Qk}3b};mhN85|{73p8ALm z44>(zwIr+0woU<;{4Eo|qH@7yi9jBvmtQov()lYD#0+Ios~ zf?(fOIbNrjtvf~M8rU-x5}gV)e++Kkm39m>BWp>E$=!x&H#;l5N)M3zWQ}EH1&*1SmNr?(zHb`5w?Gxy1wQ5fEh!oUFFsB@pn?p*!c4j=c8kt%?rc~QIl=j<1H7Kbq;%5leVaHj))&En!}{B zMP|#93kM2WS(QdTMAxh<(e&~!AyP?~638)H^P zJm{%ZB{Bcgy1iyt`&H(*2QKCy>oEOqWdlU33jSRxe4Uwt1!ZA6^Q-Z_kO+Y#2Dp2Z zu@Aywhkwe;?EO0f>M|I9ac8(#gM`1b6&(UfC39os`hekb*S{b0EjQXxielYJzP}cv z6Q!%2`3>8jN@6e66cp4FyDCT4Ad6U~{2Gb)jD^*K#~e)Y#E>!zBxpB z-61d-Vlm9vPsTNrZqSc6d|+7Z2WYN9cawlJF|35MY}%0CK4VP1UJIp{h_1)DtqIf9 zzf4H|#8od&`Mut(b?xWw*#=6p59XpS;iXA={fiz6Tn4K2KKO4Y+j&IRefXlH)!M9lCeQWqxz#6QhjnRVYY!*ju{E@Bvyhw7BlO?^Gy##68+soI^*W@+?6 z6>1S!ak&9uu#WqoKBlSFt#d(ol$L_UX;OBRDmD! zgjDW{Zc520E?oxIK@UE$@e|73YFtkbjSP=q5bgj=Y{94lop)rUs{eyBC2{am6b+fo zg0cUS{f3D!zu+WYK4if%Av&hw0E_guKg0z~(^Sze9};GyxSM-Q zL3F~$8;EbuxkhOMfi-V=?K7fZ59U%_TN91H#0Sb~Yd&OwxCh3}=!*no^k}jQ=afGX&Hx5C48nB>iaWK9>0(3+UD1oEz(FrmT`H*+lD*JT4B?yQQiBpub`P7K1VxUS^J!Woh} z6J7%&2kO}G`Y_*?{Q|+|)HKrV6Rv(7>Bo{@Njnn`)OoQe==~DWl>uPK5(V+l=s50) z>qfS(br6*m{Rf{RB0{_j2C;@*wA=}u-?>C1i#*-a_joUFd0Ch&su}`2l(vdHk^b9& zFffDxpv%^|LxU9M)Af?T1IjRlH=!wI=^{^96#7r!@r62s6O%BYJ;`nmrF|c14WY=E zW!-}#kWuCnAH2{4x@)KZg2wmfB<9mUXm%q^@WpZKHwsu$!O&MC$aX?Rq;>>&1t~f% zg`;Y6Uud6E2y!fYdXc`^X}XN&wgh7)Z(^X{?d`yK6UV*OCSqIsw1lVWKo<_9q-C*1 zFUVwus=!!3$rJe%@jaBmpJvOkh@8M3HX0o#x@k$E$~vsozvkD1g~bKs_i^2{2h|19 zG-H8Kg~vJ54v263hq3nib4E&QED-w4 z&xNnUfYST|la&9+jIo9liCO~=$)j}$%@O%lTAKU3zd2$Y;!BIM6Zn*wW)sMKw?c!? zhtO+bE8ab!DZ-}R79g41MDuFGTZH?oZZN2L>mUXE9X}}aKA++CRVe4(F_lxjxI|cR%*Bmm&G{po{cB3h6PowLyOq>OzjGwh0?M_o z73Cfv)ZYiNDVte$kpf#FA|*8pmv*y+tX|i`-E$#u?czT=(ArqrnS6f4aUW6h|;7aCGzL@9XocKlF)ej=KJ4Am#-qX)_Q3{}z>K4#v z@MX}x-R2jp@Yu=U0r#Urg8^s}0x)U8iyO%Ws0W@L<26i>>As!Jz$Xwf2kY8Rh zLc*}aArlD`RzJzmczQNk5yFTnf;yIPdoEK6Rv-M2XBG@_^bTHxd^kYLUCK-fB0*rH z0aDoQ$<|va(Vxs~>zSz6nw`{920pl^UkGTG5@l^G+lmTjc8RCxY>eyf5+%~uZIBKH zlomE+dM*V5b6@zh<8Afb0&dvEXD$s58Acw6J_2TCsKOM0nT!|xiO#KH-Ov;7?-O!| zN5A}%KGj%FR*JT-N5*OzOn%LAY+agNe=p98Tfb6LzRh~>BcuK^NUzE*tqPxfJp^Os z%L1N1+jYPe#X}QbQj0E2nVWTiucU$&p;t(`$%BJPTxvR^gG;0rcTCF;%Sko%hURv~ z#W!^|2+7C%bbZJ!ny+SXc^2ixT8}b%*Qb{)yAm`nVP%d&BK;^JB3jEDv;pMZLNTFo zsrTrxATR3gek|~pppxCTDz0P#@+Kk*ylz*#9WS(SW;|Sq2~lAJFL&x{3X1P#Cull9 z$Wm@jrhNoWzh`R2mWJjj@yO+|)cz?6CQjWyBeO=gUqVr!2WPgy1{J9O#Kzv8s^ZwB zW|8Wdz8)QIp{0WsPcOd_XsmN2kFVsOCKrmuLINt-p(L@tX;JjK0*Ldxgy$ad2Xt@4w1pxzTs_aR&Leg3(3 zxpS`_$!|3O1941fgsw`SW6G zX7-heRH93MVwg_W`-MP#(Za(U!LIS@6UquuD)a54h`zF}D``s&^|+gEHEsJ7V-1T8 zrD8oArfj=w;X-pwF$`pB&+~R8SEut2L&iH|$vQ#3W}6G!yR3Y`ex7kgCnIyTS*dig zf1H`@?#0KuqJfFU8bOc{Tk@qc?P)+F>*2QbjQ@x@{#nZQwh|fgDJDF!02g8Urw*63 zL}?z(*&y|VAgnK<%F$89<)&Il^}`BdKz2th!_HJvsqllx@GWK&rj+?on=MM&D?Yr} z%;PGRwRUG;K*!_o0uc4&fpsXjy%;Fl5CcN5IFjU&Q9>!AP%C>O;AoV@SBZ_%bkjgF zwBAkkTQP-i8p0p|*tAY%Ja9zK*TF^5nC~;J=TC>?VFDoa(`O(OKCUNDYP`jMRdh7l zzMd}KDtJEK%IsESHhM}T*Tr{l#}C(KQSM}T5R2e*(Y5e#28wFyg3z4vk4&w^>e%1c z4N&B1C1U^HPL#Z)T`Y0^<4HHOU+9-y^)AW(GMDRNVXHL+(TS4JUniK@+BA>_+)ecR z-l9t8!xD6H2#gpu&89PYNN8Nz&`wMX4*R5o7@>^8Tu%4A5AOYOQV(jWs}Ve$h`Cp3 zif|>)P$^D%UC*K-I?4@4Z1c{mq$Y=O;P5%a@!>&#s9Jyh($DL;FmvphqO}}nI*9sg z7}xeg_<2g&=*ksh`^=H|8!skTe5A}6kb8^qQ|+iGy$WJ9ly>7$qEES*a-R2L#ua>z zt3XtdxHzle%vg!1R25KKIPG2dEeqI6$t3wp6@}ZM0bFhSG}=)uW&hXglF3?{oh5dA zvQC3vMjx#`q0$yD9~|_q`04M;{Vur)6S6wAS6m<*;Jev&=pM zCwi!XU>TPP)yM%Lr@T}$DM-B5RJ#a#Z7^ZhR+4MIC)$}_MtScKQ`{Yry=I>k?hX|7 za_LkvfxD7`hv~mZ>MgV~I?7Q4rKH&|Ou6h$QysPFQi>voqSB{6#10X&4d7$PYS< zTm7?PDY%gUjDd;D2cp$mvTJiZfnYb=bdn8}R8o9z?GcCi{)XA}oDW&rl}=N1VxG+8 zQ2z8v8&_0AB7-sSOWyfDe8_gT=wzHM{yVFD{qRK+{jJYmEF|P7Oa%rxrL^kn199OS zINlKigs$DNieE$QKGVWpk~?s(W7NJvZ7!5rv!NNp&QNZsWkNYxi1Om;ts zgh1 zl#tn6aB(RyiHu%JUrAw~3v?3}di>?>r$s@NPPLnJnp}6)eazooP{Fy#TA3wr*50(Z z7jBIU_6SaV z{fu6CV0h;n+obb9IBHyo8~uQ~tc-|M;en`jYJq3RG^aX%oxHrL_2eJn;Ts>6pAYpc2G5jY8mscF1MTp!pS=QQYYo<>cZdAd~ITVK*KQ-c$LrEJ5rJIOqg1 zhP7+`;*V!yO*`-ou=TdTXz=n5`MRLlvGlMvBGqUg3xBQuPgw*r&tDn)G0G=6eSnodR-Cd2N$k5UF!R>JW0jQgk zyiqfaXZ>x}bGVC$$dCV7lZiV5pwoWB^1q#4&5iwDL@3T70=>Q&=Cfuel^p(&^b`LWkJGV3Z7mex zKltH+AmM!U&xe@$aOmsy5VCB~icP)F8qaifEA497uuGo^Ah+gtXV(-U(9mx~EB#yd zxrz|Z#~`L32Fqs6yjh_xLiB*3*z!O$UVra7Z+WS(3`@>e^wobf6P$9%U|hbqdaiat z`ke5*I1=SDNFV}T_@=8!>C@L7!N7@yZOLQ*^~f$_;|%1K}mahaL@B?K$DQFew|+C2?}IB`ir7NX9tte4Or=0_<<|) zb+qP2NmfbhaoeBN`kV^N_?wMv2dYoxr!YN6?>VMjN za-av~o8#FR`EBmMsycxEQ>t|ECUcO2iCgca_PqE9(=Yr^xeM)Zq2mfy*)wOl3#m&<@?YGC)oSHXywm~0_eb+X-Ce>wP{buom-WRS)eluZ9v}twa(w$XhAHZ zT3P)Ezx_Y={TH)-0}lEEpJHl;Ra2}f@WUzEdSX0 z->d6XJO-<*x@9dTh3p~-!G6bqj@yQ=%40Feezk-Y5I2Q-n!| zo73MXyA0%xy@{2TVDZ9uA8DHLzZush(jXbgT_F(kEtLzRL%*n@K9^bl7+wk^X&_}C zP7v011Y|H%ZN2`hAMtl7{bSZVOR_vm$RbON`s30Z1k5NMt1vFp820wpF4Xz}#oV77dQR$ao1iFEhIr&v{0yLpvQ1M{b0mW7 zV;}k0@X+diEwM3<&*vxD0GuZRT7=W#k>}Bnoo|V7Ep4T=_XI6nmO*?gTqOnvGOMxb zwzKVQgVF!6FT9KX$dfM=f>M}g8OkzW+y2MRz*#pYqn}G;z@{X4q)lvgq+-iRQbRr-(+fxES`iLjtZAn)f=EM*ULBI8JOT9HP?4g(hvdx< zAgh3$P^=Nx`!JaeF9nY&ZPV@~?9KfjLpgMkeh#3WUPnxmw?OTI?9Ybknr1Uf!9|Ks z7XzNdieGzF5cI4V#6h*5q=5Q(DT-L{%EC72sWNUjJoNbXh(gLk4#{_ zq+1Eio;yI)hypISSz8R?*jLo42t`hcxReif(9I;4C7gbNO4Dvas#5FW?0S*8Z-n); zP5nb~@z0%wYmj%qk%_L-Eo~GnpRLe^nn6@x&hq9dg0kOfXOmt4T4~(}7Y$;$>E9So zB|4^JbHFyM`*TnY5G*lIN}SxPs-<&*lpA7FnJgrmnBbdAqN6a4!9fy@fui$qQ5S6S z_g_{J&Kow3(1Q`N97F}W($}tQzvFHt>+$WW%l!IQ&$2%uc-*q&3Gs1W?R!Wp6*stRT zi{UE`*}GDMdi(jo48RzMU}&TB*qk(2P5b#2O=;Yj}kW2>)9eADuwMroE5W) z1tZ3Z3d(zVdgDg%>dHt5Cwbwh&ReC`5<#dkiB5EZno-VEL|UPTLb2IOj0a|a*V%oc ztBv}>aIu^EA#OHc$Q(9AQZCB0QT9wuK9Gha4r`cxjsAc30%ecCMz+zBX7?sa@B*Vy zoXJ=G1tpwHPM3(aBm#}6UpLu9Ap%a4)dD7w0={Q0La%`Yj0rL{gw~3C>W%G;xw0SfD1`G5#u; zqA_A&D?-%D1RO%%r@U95Pk6Kbqxn_{k1^7emK#WFE2wQ;foUb>p(q=5e5+-AkF@jv zW>RklWbvTv-q-umIMo^=d6araoW0ur3e^|-5Em<;KtVQw;@T?VYqVUkWXE*H`UKc% z0Rmfdv8B;G#VX};f+1M~%O8%d=6J~wEpnSylvUhAwAsIMvs`tn6YiRd&kj+fvED@;yhGC3s2^_pS1t@yqEPXh3Hc8=*I z-~x%iqoH;)hK$J0KyrQd^xRSTA$8R769HqrM$8LGbTtLJLI5P+g8@*aM@*3$_;iB^ zkT(qC-P=G0BmKq)N70>fQw{oDuG^z1b@a6_%LQtlLYJgn*b8SFNo2lwOs{n%uZTD* zT|%Ch(Q&j_xc*k|IAJiSP2(H4ftPl;W|O5J{=bK@zg$g!6M+k1h>J8(pfK#*+>ut^ zL49og1o^sQ>s{l_I8i!{t-FWGM_!eo5 zAtUiG$@ziOzsv1af}lvbXc79WGiLj+H9L^O#uJDBcZc~r!P=v=okJ6Usu7WtrWVeZ z<0zYN<^@8ZARI815VkjpK=WN_0xO~FCGB;8wdJ}HS|ax+HNRoUSBJX1!_{DGI5Q&W=w&!j)dUqR4;5eHn`%W~Q89sybiD zf4)SMF1`${xIor16bFRUi05jN$iUK^!0i?TK5~Tb2^-#l4u6=+6zRiU%&Ar=5}!N= zF!|=-aJW$1_vFM4b)z#mawVOT16>NzKl&|6M3Sh2|0iGnd|BZblr{!v;*7h`gT|G? z(3dv?D6w}$e#`ve7OgpJDG!)58>H_Q?}dv`4DtJ5PO#D)NEL@ngJrP6Uq5U;<>Ke^ zpc&uAI*>ko)Ys<6(Ygn9oZPa%7-mE9O5C~7m&!xGNCf0X2UQ%3fxOJoSRU@v5ufKm z(_BNyOLp+Htf+YHj*gI+JX0iXauE*C_{P%RQxbQJ%S{f)eHn1D_%C0PAP8Ky1|f|n zb*gXNo*UP6CjPn^5_o@gGd@P8H3uSG=E0<16il3C4X+|mNz~6-be3oLOfT<3UTZyj zO;cQ);4JMp#qlnuq*4W^*xJ>2GggsgtbS6%n-8?vu9__lnQ-!2oPc+0HfNRT@8Z)RM+ zOySTVBYr^qRGD`NmVXkfVwYLRF@}KsdhnT2*o~0IgQ=ep*2VZ-p%ZpSNAN(c_wRFN z?H1%rXp@`)FjmXTe|l4qztm&ZaCxAWAot)Vq^3D<{U0M9Fo8+9fedI03?v{H90Wd~ zIQmwOB6`&2H|3gXRDce3N{Ll%#PHrgdaw|?CjD$eHJOqPm5w<1>YFlZ`gOH!XiV~J zcqz%-6Dm2cf?U~Fwx$GOVYRM?a}hd`tt}kIXVXcgk8_9XyLVdcd9d|1|IUVF$9I9t zb+4Td6Y3L+kHr{>Rgn}<2#ORBrXc0qYbBu8r`JjEPagoMg}M)D;YNIpeOo_XQT+*^_2x^1s|^>(u{@Tcj&% zxzyO0zz)@G&J9XOB;NVo!}nijR*}pbLlB1O81GsP{K$VyzdS*w=Iy7Box@sfkaF?t zP}QD25sc^nk<*(;Mp##S_m`)mE(q) zb0*`z_LwbFaD(ZhsmK~#oP(6<=b3MY{%^)^L8?p+MVw$J zxl$cW-F(L2by0}(-bF{xS)^hmQ%m-V9)9Q&;YGdn#L{8rS&)tXD+h=sEbU)b2Hddpcyg`4QNlk>M8@c7YV=_31>_n%+cOSz z8y=PW0nw(^4co5=zru+`;q{JuJmq>Ih4|V;!cOobn z9WJA7Ept9=zgiePa_adI0tbXZ@^#EhfO<|x;+ASN5bTXamc|p6gu}FAmbbXyT03GX z)8K^oYdqD5TLTx(xQh&O2eWbxPOFczv-ctw&yZW>!#tPq0rw|@@rO!cfJim;cSSN( zQPX@o1*wOnqJ>j@8g&{W<@nxxuTX4MSXv#>br)l%9xhG z^BUWX;$tJy7ppZw&ApMiGk<*$k;8HDN;qQfU?gMXWm z7rPG5jUNudS_XwOpzrJX}Drf@igD6a#8cuqr^fZY4WHa$a76D znBEQk!loZaSW|v@xrI#Kq_JkpN_=`FnY4a`4`KRs3sJB@R>by}b+dA=RJz8i4YLMW z1+^knEWg(WgtlWda}y;2kE|61M~2z{19jg2mZZri1&TEG3IhcxuR()UVRDXH%M$cw zXe)_+3Aq1I`OZ&gfPKiqltlOuUTb1{QxP^PRc7q?@|@xwPLkLKf*Ul z)I8h$C6DWVS?U>!-is!tz#}d!fEnRdCa3zEE4dsGRwVGd z%iS;F=3ys(b$=MW_MB7yeEib&r2OQgJ)G#^Itta*cnCY+eGqJ(iuVCPp8*9-pN{Bj zN6vqALq9(Ubz~2qpkkc2qn0_UZE_`Fcv(sd@Y?Gk;EQ1(bZWPtEV>JU-fk1kLq?Yi zrg9`70)KLlO@l@1eFb~*2eIT3{|U4UKnho#*h9=O{22#3P0TOdx@i9lGEmRat{Z*> zm9g>1ytr^EQ0FsLmWYf|16;V^xgetg%m$J-Gp|p#s9k*S{NT%Fr97}45Yz>L`SPB zC!%(rVi9RvHf--QuG0vvouYCDFFW3Ioe`HJUmt2xb_1C*m zm~yHjSPJ#>Rla27n8}WG*!ztlSrF`%_!VlJ2j#d}w!^>&ezdw{1UTYry{vaHgr_se zVXh?5Po_-ar>C1juA$A4+nk@zAo{TJy=?JY`@;jy$$7u5L+1{n`(~IBH#yw)h_I_S>CsCneYYN< zYf8MFGwMO~4t_4Z=mhDw)g#tlK}^?Srb{Aku#dYL`*pd*MMCtJQzWG%iosF5=?$w^ z_JWB;*5)4OwYHs4^^^X-2g#V4O!CKGDZ}L0V!benQCDJ5(8134pt5z6$ZGn?ma6h| zDy_zEsO0DtUyY7xOtrJqB28|i*LdF&i%D&tPu-K4vcg{q9@7>p*A!m zjjfv2_0VDagd!x#1!fsq4vAJ{w#P$`K6K^tHljWsLikq;J%eyq1-nu4ne7(G0I4K5W#Z92MqSYj5F$4gM}i*!f_5v ze>}FapVa`cdWq%JClO(AygB#po0paC6xq^K<*lJRweb6|N{@qAW;FF~L*32|6I$TY zS0%=#m?T7GAZ6+94od*^>#DvMff9!OqZHB~ zc7owkJL#V7H=8f8zr$hXXj+X8gCTF7DB|kdICBDrE#KEocdZzMIJVN6||Vg_A{5=JHY z>%hm(HZhN9!Jj%Ft1 z=$2y|j%}v9WUd!Blb;{%vh_D7*gySkALV$=`Y5A8tc>%2)@whGmh4io6y_b=iarjxM*{4oSpceS4F?IT=IaxkuvSw znyG1UG>r1)0PhPsj7tpR=zf^*rz&xo{M1;u8Q+yYQ0{WkgNTZGn=1WgI{oXxn&gKa zHnOh-%&X!n(#&UAwzxxB9SB#eEUaRm8^)t0jnQ}uf66m*G9Y2OXRTj`kUnY^S^P!Q z^YSH{ge>45Ll;Qp2hMmA!h8T)wvY8g_Dm(a*ye*Gbw*x6!Rao@YuE%EHq9%)fey<# zw~A3VZ28?ikLa3~wyS^yIesXy8W|boQx@y~UG6KNsjGEi8&o@_Zh|mp&A$E-kDmw+ zgv-;)>%vzbe@&4v%DeXz{mv-E-qPSS-_Z--t49JyU_sHZ>inpeL#+?G%nTJo--zG=3&y=|s24~6VMW!%_Ao!E)ee~o z`_*YV#7QGG-t10Ur@~MKBp*Yg1AgNn=&#BB0fPxAe>??)9tou%yPt{bxRx|VcF+YiY#O8ht^+vc zYejCykJ>}@5`FeS0E6=%UF5#$rpvT;D)Bdxc5i^g!F#WSQg^IF_rr+P^e9lGC%(q zY&%v?bq`^)Y|I+0rGhDc;t0|s)B^*tHH*aX4e5NR6Lb32ly5z^no55Cf&lQrC+tP= z=yRqMwyX1prpQ&|Tav!QZR##3Q{Z>LEie6z794x~8BvDl{E42prnAS0=En_B%!^pf z71*Uemd#!WsJ@9Ql$vS2i4Y20Qv_#7nP^l&Xd@f^@TJ?E;lbvV22{&6RGOC~EnoJT zOLXFK?xkOSv0Bn_DeLyE2x2l=F`YYI^X>C?qQ(drk=1Nu+KN!|dDEkxNc}v-83B0xyYkwiO6RsRw)(Lux1jE53(cQ{lxPk4t_< z)_Vs{CtafBA+MsxV7bi+&lNc$47~{myl)ONwBZEvDmvf(?ygx%Qd`au=?3zmND%OD zU_RQonznW*A~vEyp84cjtr2?+3u2xmj=XDT zck=d4i-3kM#03{$`o~#=|*X^CsM@LIIA2D zAxD34+dTu~v{5xYMR|>%2s}Otzk&Lj3&`lCjncx6_v{^Xdz^>e9*QtaP(`(w(_Ta# zo;Lm@D?KoT3zHnDtxcs+R+lT4)7zQ&EeKC(|8Y;xz-#I#h|G$<_k%B-)MlZEa|@{% zssgCOjIRSbc+^PJ877MFhNQkrcJfPQBsO^G5GfzpI6hteiyqzvqHS09bk`(5$mHr> z^Ot9A_;@s9t~U$-W6v~wbs~RBLm@Xqt8l7&I_b1OB)FP@Ifr(DaagRHr&tS8$HHeNIF^|XN2}^jn4W@y( zaP!#y697>BWAsI?mgBBSJ!0CBNouJLrOhTph@W8(55k3r73|<7u@v(Y-JJ_GoK44l zNQmp>fxngXV%M|p<(Zzl!;g}Z`nqS&H|>7niiNbFC?Mit7ZN$_;f8SBQQyU@?2^Kh+=UPjmwSa28 z81EIOr6C^#H6IPUCX5=>pyEfIV^d!p_@;upjuD%S1TMjnnj#2FW6tHtMk*tH;87N) zyy*B*mcpXqrilFJJt)lAkSA;vVb`t~0By>2Prj>?tq(3X0|`_cGq+z+#wDK%D&@1n zbPCBM^q{+2o)PA5jlNQefw7>#kzV|j(}6K;jHYtP9-|-8a&ori2Ngf`A=OGSq=MHX z?Qjvgeks?doy*AI`wduIED^aCpN5ed;*ONc(RMH%s*1l}hoWTP_N7Ajl(l9n2Y_%R z>ETW!9`Z=Kv5gbs!(AJ^n7~FuWW!aIhE^2fdFkkQy#9)C{j~)M-%CYqwDny>TXHWg zLn95a;*95JY_Lc!Uq7TN!Z_Sg3@wanekgO-H?=+v_oeb-vJ2I(+XR+vwT5vpa0epAkasQ{nj(=vjaJI4CR~}8F zk&TOvoEHK_tyodgfy%?;){deMq#v&&?w#bP_+CWi><9%!1ZJ4=Ns<^Gh;8T2*qX06 zp;fnWNUsp-wd~zl&*Sf?rlO z$^h8#!G#viMu)T7fdL~_=y3Lg5qPibwpPsr%?2cuxV}eIBR21P*N1DBu5KlGqQWH# z83Vs<@lB;noHlKn-g(kwNV)bTE~J?^T#+cvg2AT-1Z!z3YA0FMqORm5f_FK(C@yqxf9<^-i@Od+JVHsttmdd^{*Fj6nppW`Pl%Q>ea>BWi zKg>-87KuupP<9GArd9&6*(=UXXs=pTw4-vgCq#jJn#Lb=uqQ07cIZA~l_`u?pBlSf zNdHv_x#90|tzFQA$%%EDpZ~HDokk|hz@OUS8@|md#Z7~U&(bV~dI5$>9g$AC_Z3lz z>z5HgWJK#810?$(vIA7H}{RV@|R?1*Z$XNwPLJ*NKNJIcg%Cg|ty|7@?mP zJK2ozUJlR*!0K2oRi(i&U4O8-zQ=Jsg3jpEe!cGD4sq}Doq__f`AGeCH*DLr?-jv& z=IF?m6~UOeCbREZ-933&bW8~NX+(OQ(cWnHEj!}KLSA}>c`R|v()$Lz-|a z#S@?d7UCP!$?+v}$9*98t71ZgeCt?eYMvvdyfiC0ocKXT&3nW{Ujf+tkal<}r|1|3 zA8^&5Ozg>Thwrr3y9JgiQ9C(>;3bT%Aahgdpr|OOP2xXPsZsPqFeNO)Zqak4=A1 zwdCR<6f0J^987B&}j^ze5H6Jq}OJtDxFX)ghEqJeX!FGA%bNyqE5R zFO(X1aA%Ul2_rjbBwehdeLKyheYS|;&WIZfF*_4tF!@zK0P3+5kj$O0`hV(T+aX&#HK+E}E$q>~J}vyDE_ z#B1fOMpis%hBL)fuMP$5xSI{SBEM$DP^UfNaBNN1@}GCflnFY^pKY zu;4Plq2yXI#+mX}9E&RO_5Pt>`^u$nDDx3oBr%k(lnIZ38bO5AQ{fR8$$k3)ed7(h znx<<$`8UPf9QfTxJ`yEg#M6E*y>Dlv9uNa|Gyh**?-*TK)2<80wr$(Sifx;nq+`32 ztk|~Aj%}-xj?=MiJDr?7`;5KM^X_ka>-QXURMouisx_{#O7pAn7*~$1zI_1csfmzgQbDqv zys=Z|F{iPZP(Vo#ro0PfRv{#&>F?-Bc>zTN-QnR~eOp-?=A@Tc$gErYHiIV7_hqWW zSC!deV%8iTw_q-QfuIgE1pX1tr`7w~;D&cF{jS6qm$aIl{S}1lY09{e7(}eWRt`d= zPY?VBrI>!{)w`V&J}~DMN`{SGv#IyUDI9iEw4Mu79o3@gdwiCMUA~`FT)0*ZIyx_R ztV6v+-Sa75r{D3=o78A zm$2wbwJ(*$HzDn<12zKPNBm2pQU;XJs?qB?`q5J?*|{E6_@kW8n4y zaS*&A4)cObw`EICQ(k=5$oA8h=(}A&IA(_emOWxqj42^R>r{M}TW^7A}v14u<-fe?&2rnpbvs87#oG#WVcE&?C+QNQX zU_m5$S4043Q_}`6*O6{3sR|0^;dl0_j+GojR=K0Ud?gST1KRh$4_D>fuNUJEVT68H5^z@%_VwgvieMHI!7fYc<2}2v*VW0^ykbzCsR9LQezr*CT~j$HN80!F(Ki~ydSpqd~k6& z3uG!_p}(h^seBN6`$Od9c?lFwdPSPt+))Y(HTrDL>FW81Rq%GATxa!;i>CFJaU|+h6wU+-Ul7d{l7}O+Q z;H)LBaYhEE9j9N4zuT8^nXxe&v|>n%3if<%^)8zNa-RC$%40KBJa5y}bwop;muzrI zQ$liNDrkAepNz_9w*0hqr_5-YZ1j8>a~q2r4Y@gmwfhCbmb&+TY+_9E*CLp?`Fw(g zl|YS5zvumIzg5klL4Q5r^Tg|_J9mKDzOn$vPoWVbK;JR#vuo2pT|EAl-PU_#?No*W zM1#mlSQ_zp)MfJ{5VNLR414JmA>2#{i^Geycv*g^;Kj2UM!IOt;P#e$WIAw0#ZDLx z5Ky5}A-NF~*`%iYt&HQeCglY^Np^zdQV)tiS5akboW*C(4CROSOrVUO5+RuI*BO@mA;T|mYVuGAmmVSvt`n}Qu?j;|BhJ1|;4m=c&& z<3?V*&MxqdyqxHxN=75~(QG&yJ9>dkGP}M|(ft#R9kfyyi4aGVJM$=Z$G>G9Z%3{@ zg&P9`=TeG1Z|1*F@ne25L&pzoX)e;%2gy3lLuiZ(f$x7w&*@C)HNj;LB7m$locsrf z`_}Ky^xGDHKq7TFER|sb?M#EK2K!@1oi@b_`?T4gzCcm?v_vpaB5=u_p1X;gortq6 zkp@!u-s|}`*2YpSu}pa%)wsr z`yg$`@dtAE@`k7z%u`M@5UpS{n z))=Y0%imuFmGihji6cag%6XjJoNA4iSC^=)fZz_(hTYtrg~bR<_)tTpfWvL zTP@tr%_zpS=tmXqqUldiSb|WR*Um5S%UrrhZ3?s;C?e^CayJ5%p|_`cT1>uji%09v zF}%7sDwONs`?7D$do7)-4RYpuc&#q2D2nYzG|`#mFvq2?=b3K1)a4#zmEVj%E^fk= zpC7)v$Rl`Z-#vK${LM-z_=gU(->huspq@y9ZMDXUdwW^}|C-JVZax#kEdOd&&tERo z(vx^ys1fgbG`M`*LU*3rKAl17|7P9D(EkLZuu5r`) zmPw?j#1k40y$>u3XHzhC*T0Dr^gH`Pxv@p^rU%=;+1c2PH#q_yd^v{qlCm>Hwnv%qlKU8m;jp?N?M&d3Gw;b2zI~BiCe6rfC0PLHg@~LHF5nH0Qb@Fq`E(k|yRvlr@=UrevK>d8-Oy$?@3}Cx56o4Z zh;|2E&af@E(vqnw+(v-yaL?!#dxwe?e8ol7B-b}aR%x!3;skT+rB}5>JJL9Jr$#iE zc1kW?3WJxMZba?8rG_ts+zmcqf6KPQ|r zS%p_=;@z%0>g$hAYCNlNW@+P#PM!i-%+Vq=M*?`@l?I!QFHKb&Hoq6uFwO$PEgOf+ z-I>n(s}_*Vhwv>a7OMR_%mhf6zvts|V9yPU!+4!2Tn%NoMs;7Z$%yl*&S@#{-;>*n z)dwIi%;yz;g;l3==dDa3WJ40F>5-?<{~-U!#pUv@CVGV*&Hp%MmHyaKW?lTb$=%e$ z5rd<^njHwmR-xpSV2XwUMG!?*z$6`3$$^cAENk_mZ@rierY~l8Guc=_f=b`zjC$sg zJrcVwfU#HvX5uy?-TwAKs*~V~F~OWMYJi$8>a=%!HnRR8xMWxpA>!oe zJHXl1M%oFgF(T_$EpdaizF8I_qV^`SmvK~RLNnQysR3QjS5ZgHy#J6|esdlwlLT$* z9gV*wTZLT^>s0@>$#r&3#N)7*lYkGL3nSoZm9M|^L7o1#r@TuRS0&;*za4+jSNkBz zGEjQ+oEgKB=1_TLCO3OUDR*d_rwau*=JT?iTfkIMlNb~RMF?a{GjK%+T`(-<34v&| zT7@SOQ!X+jN~4I)>qe}wjMr?C?CX<%PYGm(I|D(F;6xO&ansoGhPcNO2G5JpYFp-_ zk1a+QF-iA_C$z952}2(qmY!ykG*rQblWn3|!i~<|f^hCB(5%|twnFB{ruOu1Bbix) zMdD%&KDY8jcUckyYu%Iyqs>hu3VAV4SMY7qcr1EkaeA^LTQX z2q**RU_`@m<5-vPZ@AU})qjOQ3I2xLjS&+e=6l9ip~=5ry48CjxgdZLFu7rxxUUM#vZ)gOn4m9n4&r zQgwY$Q4+fEkM)_2EyPQAM(>+S8@0#U?SNo@2Z_Lj68DHX-5gNV@j;amxOivuh#z{8 zk=KlEFYd1*=-JX+4NHzVinc`?sii+&JfHx@4QR(p{nCOLY(ZQsCP($}F z@jA&)av%lr3{#OGY%)qco(PrKK;>!w2rvT4Iq}BsKxQ>^jIo#o&x~}c+t3ni(+|%z znRo2r*9A&=w0tKgi7$b^`6uQj$1t?>-7Ne%9=r|f3;|XwfDz)j^K|zh z#L9i4W7yx_5aMgp_0w|fvceb}%NnTg1G|2%iOa+>irmeF>Zn1WrY*%k7x2!Nl{7 z%fqICU57tz>m(q$Q#@I7CA17d6IfZxTEsXnoXVltJ*K&`9ZI7UQ!eZrVKTVVRKIa* z!}k8C8pP?JMPXg_qfvliEv$q`)s&Zz!kk0CT>;0ai(RPeklt$M!*wrn2M`OvxWl%h zE`2Y+2)^IdP$5@wa^zIk8bh>W2W5xyne2gEe8Mo>pTS$+c=k=vO%1uQdN=6uOIxK+ zOvo7u{7&~BY;Jfn>Fmpw|7*7mA0##(?E$_7I9L0t(s0oLmVGxC0b zm1oi+l5q_AsbhBa6R&EHJ0s@Rcd~!2Isq^g)#?XSu{XBnxPcG3O35{o>tK3w690nH zABPgy1^2nm(9C}I`rri$;|cB=c^z}s9S4r{$1B*NV*rAljM%WA?*@)h2@CSTug`ok zPF(l9T;MUW_03(3Me(+!Gi&>=Z}tETeoL#_6G=HYBsr`t1swDDwBurS(?$U-Xxn1;b~xs?1d}T5~LV3#03^@K<{Kubpt&- z!uK<^sA_d(XCL@-Hp^ovlgx+hcdCj{Ifv+&4ITnVMp7U%n}(n17H_=qr%n$+3<8j)4{Ed6b^Cc(1b=(w_CeAvdYFp-_@RkG5iwwNfO+2*XK_Z%yC4!98*98 zZeBLOPH{$OJk|pik&e&RBil{Q)=e$@*xCMfZ~9+?mYHT$D>o?dGh8r*$BH;QW6;&-U>7k4CHn^8)I`dHHRo_^-5jFs$!g-nE1m~>FEfM-Z7^1fmgbv| z@Zi9)^)6zv59C6IseRre6?fUx<-M8TWuV|0@$P|{3)XW*gR0Q2-3V_Ho8M?$1R^dc8092QmcN2GJ*pxMXfI zX|FmCX^#Q^M*J##Lo)rO#51eH+U4snFAjRI7$ldJ|vtFHp%(}*D>=p%MH zw$D+G&l!Jjd9^V9iA-kFN*-`DCfpZ*o3s=Y)lC>#Z9sYD!HtY0HIfaaOFU;

}zI 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 literal 68380 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

12Ay3&-%qlZ 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 M)78&qol`;+0N~BM(EtDd From ac550160d44799c095d436ec166749d170cee14f Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 6 Jun 2022 20:26:00 +0300 Subject: [PATCH 094/116] Major Changes and Fixes #LinearAnisotropicPF - Changed reference from ParticipatingMedium to ThermoOpticalProperties - function(...) : replaced calculation with cosineTheta(i,k) call #CurveEventType - Added CALCULATION_FINISHED event type #DataEvent - Replaced reference to ExperimentalData with a more general reference to AbstractData #NetzschPulseCSVReader - Added locale support #SampleName - A default sample name of null is used, in order to avoid having too many 'Nameless' calculations #TridiagonalMatrixAlgorithm - Instead of holding a reference to the Grid object, the fields tau, N and h are now added - As the instance of this class is created just before the calculation starts, it does not need to reference Grid #ResultTableModel - Better handling of average results vs individual results - Average result now ignored when checking for previously calculated results of a task. This was done because an average result is not assigned to a task, therefore, it returns null when searching for a SearchTask ancestor. This led to a NullPointerException previously, but now has been fixed. #ProblemToolbar - Replaced unnecessary SingleThreadExecturo with a direct call to the plot (...) method #DataLoader - Added missing .incrementProgress() call when loading pulse data #Problem - Removed unnecessary check c.isIncomplete() for ExperimentalData when applying baseline- - Changed availableSolutions() to accomodate for possible multiple application of a single solver - Improved confusing assignments in the optimisationVector(...) method - Fixed error: assign(...) -> changed .get(...) to inverseTransform(...) - Consequently, made sure that this latter value is referenced by the setter methods - Removed shortName(...) method - Removed conditional statement prior to applying baseline in setBaseline(....) #Discretisation - Switched to ThermoOpticalProperties reference #HeatingCurve - Added lastCalculation(...) array to store last successful calculation - Added copyToLastCalculation() - apply(Baseline) now only works for a positive size of the time sequence #ADILinearisedSolver - Added an overriden clearArrays() method for array init purposes - Removed array initialisation from prepare(...) - prepare() now publicly overrides superclass method #ImplicitScheme - Removed unnecessary final int m argument from several methods #MixedLinearisedSolver - Changed scheme to accomodate the additional zeta parameter for rear-side parasitic heating - Added an overriding pulse(...) method - Changed firstBeta(...) #ImplicitLinearisedSolver - Removed unnecessary call to super.pulse(...) #General changes: - Added zeta to 1D Classical, Diathermic and ParticipatingMedium problems. Zeta serves to simulate a possible rear-surface heat source, either due to beam deflection or circumferential conduction #DiathermicMedium - Bounds in optimisationVector(...) for the DIATHERMIC_COEFFICIENT have been changed from arbitrary valueto those initially set in the xml document - Removed constraint on the diathermic coefficient for curves having pre-loaded property tables #DiscreteOrdinatesMethod - Replaced references to Problem with ThermoOpticalProperties where possible #RadiativeTransferCoupling - Added a safety check to init(...) to avoid potential handling of unsupported problems - Changed signature of newRTE method #ExplicitCoupledSolver - Added an overriding timeStep(...) method, which redirects to explicitSolution(...) - Changed finaliseStep() overriding method. If the fluxes are auto-updated, computes the solution to the RTE. After that, stores the fluxes. - Added autoUpdatesFluxes capability and the zeta factor - Removed unnecessary RTE calculation before main sequence started - Removed pls accessor, which is defined in the superclass. Instead, replaced with a getCurrentPulse() method #Chart - Removed unnecessary call in the conditional statement of a for loop to .isIncomplete() #GradientGuidedOptimisation - Replaced the body of the catch block in configure(...) by a call to notifyFailedStatus() in the SearchTask #ThermoOpticalProperties - Made this class implement the Optimisable interface and moved all property assignments from ParticipatingMedium to this class #Pulse - Fixed incorrect range updating after pulse changes when the lower range would be shifted to a lower value automatically although a higher value was set #Metadata - Added call to setPulseWidth after pulse data loaded #OneDimensionalScheme - Moved array initialisation to clearArrays() removing them from prepare() #LoaderButton - Added OutOfRangeException checks if thermal property table does not cover a wide enough temperature range #SearchTask - Removed unnecessary catch clause in assign - Removed wrong logic in relation to maxIterations assignment and the inner iteration cycle - Removed inefficient checks for IN_PROGRESS calculations within the inner cycle - Added distinction between AWAITING_TERMINATION and TERMINATED statuses #DifferenceScheme - Added pls field as a generic place holder for the current pulse value - Added call to clearArrays() from inside the prepare(...) method - Removed maxTemp/apparentMaximum scaling to enable Cp calculation in future - Removed adjustment cycle from runTimeSequence. Prevented number of points from being adjusted - Added missing prepareStep() in the timeSegment() method - Added the clearArrays() method - Somewhat more logical structure of the runTimeSequence method #ResultTable - Added a more informative content to the describe() method so that it actually describes the sample #MixedCoupledSolver - Replaced private prepare() with overriden public prepare() - Added rear-surface heating to BCs - removed unnecessary arguments from evalRightBoundary #AbsorptionModel - Error fixed: Remvoed premature setParameterBounds() call, which overrides all bounds in the vector #BlockMatrixAlgorithm - Fixed problem with grid density not being assigned #RectangularPulse - Added a required number of points threshold of 4 #ImplicitTranslucentSolver - Removed superfluous fields - Replaced pls with reference to getCurrentPulse() - Removed unnecessary timeStep() overriding - Removed redundant parameter arguments #ResultTableExporter - Improved output format of the CSV summary tables, adding version, date, and column identifiers - Fixed an error with number of columns changing - Changed the plus-minus delimiter to ; #ExponentiallyModifiedGaussian #TrapezoidalPulse - Added required number of points (10) - Remove redundant init() override #DiscretePulse2D - A new private init(...) method has been introduced. - A new evalPulseSpot() method is introduced, which caclculates a non-zero pulse spot in multiples of the grid radial step and automatically adjusts the grid to allow a large enough step size - A listener has now been added to the properties of the problem under study, which triggers recalculation of the radial spot size #DiscretePulse - Added WIDTH_TOLERANCE_FACTOR, which determines the minimal allowed pulse width for pulse corrections - Added listener to Grid changes, requesting to re-init the DiscretePulse - Added the init() method - Pulse normalisation (area calculation) now done from within DiscretePulse - recalculate(...) now checks whether the nominal width is feasible. Otherwise adjusts the pulse shape and width to prevent calculations from slowing down dramatically - Upon setting the discreteWidth, the grid is re-adjusted to accomodate these changes #CoupledImplicitScheme - Separated nonlinear BC treatment from the main logic #ImplicitNonlinearSolver #implicitCoupledSolver #ExplicitCoupledSolver - Replaced private prepare(...) method with a public overriden prepare(...) with a Problem argument #ClassicalProblem2D - Fixed optimisationVector #TaskManager - Added support for awaiting termination / terminated statuses - generateTask() now triggers DATA_LOADED events after the experimental profiles have been loaded and before they are transferred to tasks - describe() became slightly more informative #ParticipatingMedium - Bulk of optimisationVector() and assign() moved to ThermoOpticalProperties #ImplicitDiathermicSolver - Fixed possible error in BC equations - Added the rear-surface heat source - private prepare() to public prepare() #InstanceCellEditor - Added check for NullPointerException in case when a change is not possible (e.g. setting a NumericPulse when no such data is available) #NetzschCSVReader - Better handling of locale-specific delimiters - Added guessLocaleAndFormat() method - Delimiters can now be changed multiple times during reading the same document #AbstractData - Removed redundant isIncomplete() method - Added isFull() helper method (note the usage is different from isIncomplete) #NumericPulse - Added required number of calculation points, equal to 20. - init(...) has been simplified by splitting it up in smaller chunks of code - pulse width is now unambigously defined via the doInterpolation(...) method where it is set to the last element of the scaled time sequence (dimensionless) - As a consequence, adjustedPulseWidth field is no longer needed and has been removed #NumericPulseData - Removed redundant scale() #CompositePathOptimiser - Removed redundant checks for malformed candidate parameters #Grid2D - adjustTo() -> adjustStepSize() - replaced for(...) loop with a recursive call to adjustStepSize - after step size changed, calls adjustTimeStep #Grid - adjustTo() -> adjustTimeStep() - Exploits required number of points, which differs depending on pulse shape. Checks if the nominal pulse width is greater than the resolved width. Adjusts time factor #Status - Added AWAITING_TERMINATION and TERMINATED #Added classes: - CornetteSchanksPF represents a new type of a phase function selectable for PaticipatingMedium calculations - ImplicitCoupledSolverNL, ExplicitCoupledSolverNL and MixedCoupledSolverNL, which extends over MixedCoupledSolver and adds functionality specifically to deal with the nonlinear BC terms #Classes removed: - LayeredGrid2D - Partition - CoreShellProblem (no longer considered necessary after introducing zeta) - ADILayeredSolver Minor changes --- pom.xml | 2 +- src/main/java/pulse/AbstractData.java | 28 ++- src/main/java/pulse/HeatingCurve.java | 56 +++--- src/main/java/pulse/HeatingCurveListener.java | 1 + .../java/pulse/input/ExperimentalData.java | 25 +-- src/main/java/pulse/input/Metadata.java | 16 +- .../pulse/input/listeners/CurveEventType.java | 9 +- .../java/pulse/input/listeners/DataEvent.java | 8 +- .../pulse/input/listeners/DataEventType.java | 8 +- .../pulse/io/export/ResultTableExporter.java | 44 +++-- .../pulse/io/readers/NetzschCSVReader.java | 98 +++++++---- .../io/readers/NetzschPulseCSVReader.java | 5 +- .../java/pulse/io/readers/ReaderManager.java | 4 +- .../pulse/math/FixedIntervalIntegrator.java | 10 +- src/main/java/pulse/math/ParameterVector.java | 1 - .../math/transforms/InvLenSqTransform.java | 4 +- .../pulse/math/transforms/StickTransform.java | 1 + .../pulse/problem/laser/DiscretePulse.java | 137 ++++++++++++--- .../pulse/problem/laser/DiscretePulse2D.java | 36 ++-- .../laser/ExponentiallyModifiedGaussian.java | 29 +--- .../pulse/problem/laser/NumericPulse.java | 81 ++++----- .../pulse/problem/laser/NumericPulseData.java | 21 +-- .../problem/laser/PulseTemporalShape.java | 50 +----- .../pulse/problem/laser/RectangularPulse.java | 8 + .../pulse/problem/laser/TrapezoidalPulse.java | 26 ++- .../java/pulse/problem/schemes/ADIScheme.java | 11 ++ .../problem/schemes/BlockMatrixAlgorithm.java | 21 ++- .../schemes/CoupledImplicitScheme.java | 95 ++++------ .../problem/schemes/DifferenceScheme.java | 151 ++++++++-------- .../problem/schemes/DistributedDetection.java | 2 +- src/main/java/pulse/problem/schemes/Grid.java | 76 +++++--- .../java/pulse/problem/schemes/Grid2D.java | 37 ++-- .../pulse/problem/schemes/ImplicitScheme.java | 16 +- .../pulse/problem/schemes/LayeredGrid2D.java | 85 --------- .../problem/schemes/OneDimensionalScheme.java | 7 +- .../java/pulse/problem/schemes/Partition.java | 66 ------- .../schemes/RadiativeTransferCoupling.java | 12 +- .../schemes/TridiagonalMatrixAlgorithm.java | 43 +++-- .../schemes/rte/BlackbodySpectrum.java | 42 +++-- .../schemes/rte/RadiativeTransferSolver.java | 14 +- .../schemes/rte/dom/CornetteSchanksPF.java | 42 +++++ .../rte/dom/DiscreteOrdinatesMethod.java | 39 +++-- .../schemes/rte/dom/DiscreteQuantities.java | 26 +-- .../schemes/rte/dom/Discretisation.java | 14 +- .../schemes/rte/dom/ExplicitRungeKutta.java | 2 +- .../schemes/rte/dom/HenyeyGreensteinPF.java | 13 +- .../schemes/rte/dom/LinearAnisotropicPF.java | 12 +- .../schemes/rte/dom/ODEIntegrator.java | 25 +-- .../problem/schemes/rte/dom/OrdinateSet.java | 4 +- .../schemes/rte/dom/PhaseFunction.java | 20 ++- .../NonscatteringAnalyticalDerivatives.java | 5 +- .../exact/NonscatteringRadiativeTransfer.java | 10 +- .../schemes/solvers/ADILayeredSolver.java | 89 ---------- .../schemes/solvers/ADILinearisedSolver.java | 33 ++-- .../solvers/ExplicitCoupledSolver.java | 92 +++++----- .../solvers/ExplicitCoupledSolverNL.java | 95 ++++++++++ .../solvers/ExplicitLinearisedSolver.java | 8 +- .../solvers/ExplicitNonlinearSolver.java | 11 +- .../solvers/ExplicitTranslucentSolver.java | 17 +- .../solvers/ImplicitCoupledSolver.java | 30 ++-- .../solvers/ImplicitCoupledSolverNL.java | 94 ++++++++++ .../solvers/ImplicitDiathermicSolver.java | 29 ++-- .../solvers/ImplicitLinearisedSolver.java | 19 +- .../solvers/ImplicitNonlinearSolver.java | 20 +-- .../solvers/ImplicitTranslucentSolver.java | 49 ++---- .../schemes/solvers/MixedCoupledSolver.java | 42 ++--- .../schemes/solvers/MixedCoupledSolverNL.java | 92 ++++++++++ .../solvers/MixedLinearisedSolver.java | 30 ++-- .../statements/ClassicalProblem2D.java | 26 +-- .../problem/statements/CoreShellProblem.java | 164 ------------------ .../problem/statements/DiathermicMedium.java | 13 +- .../problem/statements/NonlinearProblem.java | 2 + .../statements/ParticipatingMedium.java | 80 ++------- .../statements/PenetrationProblem.java | 13 +- .../pulse/problem/statements/Problem.java | 89 +++++----- .../java/pulse/problem/statements/Pulse.java | 38 ++-- .../statements/model/AbsorptionModel.java | 2 +- .../model/ExtendedThermalProperties.java | 4 - .../statements/model/ThermalProperties.java | 6 +- .../model/ThermoOpticalProperties.java | 82 ++++++++- .../java/pulse/properties/SampleName.java | 2 +- .../pulse/search/direction/ComplexPath.java | 6 +- .../direction/CompositePathOptimiser.java | 10 +- .../search/direction/GradientGuidedPath.java | 11 +- .../pulse/search/direction/PathOptimiser.java | 3 - .../pulse/search/linear/LinearOptimiser.java | 11 +- .../pulse/search/linear/WolfeOptimiser.java | 45 ++--- .../search/statistics/CorrelationTest.java | 4 +- src/main/java/pulse/tasks/Calculation.java | 12 +- src/main/java/pulse/tasks/SearchTask.java | 66 +++---- src/main/java/pulse/tasks/TaskManager.java | 74 ++++---- src/main/java/pulse/tasks/logs/Status.java | 9 +- src/main/java/pulse/ui/Launcher.java | 6 +- .../pulse/ui/components/CalculationTable.java | 2 + src/main/java/pulse/ui/components/Chart.java | 4 +- .../java/pulse/ui/components/DataLoader.java | 8 +- .../java/pulse/ui/components/ResultTable.java | 2 +- .../ui/components/buttons/LoaderButton.java | 15 +- .../controllers/InstanceCellEditor.java | 9 +- .../components/models/ResultTableModel.java | 81 +++++---- .../ui/components/panels/ProblemToolbar.java | 6 +- src/main/java/pulse/util/PropertyHolder.java | 1 + src/main/resources/NumericProperty.xml | 8 +- src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 9 +- src/test/java/test/NonscatteringSetup.java | 3 +- 106 files changed, 1689 insertions(+), 1587 deletions(-) delete mode 100644 src/main/java/pulse/problem/schemes/LayeredGrid2D.java delete mode 100644 src/main/java/pulse/problem/schemes/Partition.java create mode 100644 src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java delete mode 100644 src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java delete mode 100644 src/main/java/pulse/problem/statements/CoreShellProblem.java diff --git a/pom.xml b/pom.xml index e27af653..3dc51b98 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.94 + 1.94F PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index 2f58d47a..1fd62645 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -8,13 +8,11 @@ import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; /** @@ -105,7 +103,7 @@ public void clear() { * @return a {@code NumericProperty} derived from * {@code NumericPropertyKeyword.NUMPOINTS} with the value of {@code count} */ - public NumericProperty getNumPoints() { + public final NumericProperty getNumPoints() { return derive(NUMPOINTS, count); } @@ -117,7 +115,7 @@ public NumericProperty getNumPoints() { * * @param c */ - public void setNumPoints(NumericProperty c) { + public final void setNumPoints(NumericProperty c) { requireType(c, NUMPOINTS); this.count = (int) c.getValue(); firePropertyChanged(this, c); @@ -168,7 +166,7 @@ public void addPoint(double time, double sgn) { this.signal.add(sgn); } - protected void incrementCount() { + protected final void incrementCount() { count++; } @@ -179,7 +177,7 @@ protected void incrementCount() { * @param index the index * @param t the new time value at this index */ - public void setTimeAt(int index, double t) { + public final void setTimeAt(int index, double t) { time.set(index, t); } @@ -190,7 +188,7 @@ public void setTimeAt(int index, double t) { * @param index the index * @param t the new signal value at this index */ - public void setSignalAt(int index, double t) { + public final void setSignalAt(int index, double t) { signal.set(index, t); } @@ -200,20 +198,10 @@ public void setSignalAt(int index, double t) { * @return the maximum signal value * @see java.util.Collections.max */ - public double apparentMaximum() { + public final double apparentMaximum() { return max(signal); } - /** - * Checks if the time list is incomplete. - * - * @return {@code false} if the list with time values has less elements than - * initially declared, {@code true} otherwise. - */ - public boolean isIncomplete() { - return time.size() < count; - } - @Override public String toString() { return name != null ? name : getClass().getSimpleName() + " (" + getNumPoints() + ")"; @@ -270,6 +258,10 @@ public void remove(int i) { public boolean ignoreSiblings() { return true; } + + public boolean isFull() { + return actualNumPoints() >= count; + } public List getTimeSequence() { return time; diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 81112127..8fb0f556 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -22,7 +22,6 @@ import pulse.input.listeners.CurveEvent; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * The {@code HeatingCurve} represents a time-temperature profile (a @@ -42,10 +41,12 @@ */ public class HeatingCurve extends AbstractData { - private List adjustedSignal; + private final List adjustedSignal; + private List lastCalculation; private double startTime; - private List listeners = new ArrayList(); + private final List listeners + = new ArrayList<>(); private UnivariateInterpolator splineInterpolator; private UnivariateFunction splineInterpolation; @@ -62,7 +63,7 @@ protected HeatingCurve(List time, List signal, final double star */ public HeatingCurve() { super(); - adjustedSignal = new ArrayList((int) this.getNumPoints().getValue()); + adjustedSignal = new ArrayList<>((int) this.getNumPoints().getValue()); splineInterpolator = new SplineInterpolator(); } @@ -102,10 +103,16 @@ public HeatingCurve(NumericProperty count) { splineInterpolator = new SplineInterpolator(); } + //TODO + public void copyToLastCalculation() { + lastCalculation = new ArrayList<>(0); + lastCalculation = new ArrayList<>(adjustedSignal); + } + @Override public void clear() { super.clear(); - this.adjustedSignal.clear(); + adjustedSignal.clear(); } /** @@ -128,6 +135,7 @@ public double timeAt(int index) { * @return a double, representing the baseline-corrected temperature at * {@code index} */ + @Override public double signalAt(int index) { return adjustedSignal.get(index); } @@ -154,7 +162,6 @@ public double signalAt(int index) { * @see pulse.input.listeners.CurveEvent */ public void scale(double scale) { - var signal = getSignalData(); final int count = this.actualNumPoints(); for (int i = 0; i < count; i++) { signal.set(i, signal.get(i) * scale); @@ -166,9 +173,8 @@ public void scale(double scale) { private void refreshInterpolation() { /* - * Prepare extended time array + * Prepare extended time array */ - var time = this.getTimeSequence(); var timeExtended = new double[time.size() + 1]; for (int i = 1; i < timeExtended.length; i++) { @@ -188,11 +194,12 @@ private void refreshInterpolation() { } final double alpha = -1.0; - adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate + adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] + - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate // linearly /* - * Submit to spline interpolation + * Submit to spline interpolation */ splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); } @@ -220,19 +227,24 @@ public double maxAdjustedSignal() { * heating curve. */ public void apply(Baseline baseline) { - var time = this.getTimeSequence(); - var signal = this.getSignalData(); adjustedSignal.clear(); - for (int i = 0, size = time.size(); i < size; i++) { - adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); - } + int size = time.size(); + + if (size > 0) { + + for (int i = 0; i < size; i++) { + adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); + } + + if (time.get(0) > -startTime) { + time.add(0, -startTime); + adjustedSignal.add(0, baseline.valueAt(-startTime)); + } + + refreshInterpolation(); - if (time.get(0) > -startTime) { - time.add(0, -startTime); - adjustedSignal.add(0, baseline.valueAt(-startTime)); } - refreshInterpolation(); } /** @@ -251,6 +263,7 @@ public void apply(Baseline baseline) { * * @param data the experimental data, with a time range broader than the * time range of this {@code HeatingCurve}. + * @param baseline * @return a new {@code HeatingCurve}, extended to match the time limits of * {@code data} */ @@ -266,10 +279,9 @@ public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { var baselineTime = data.getTimeSequence().stream().filter(t -> t < 0).collect(toList()); var baselineSignal = baselineTime.stream().map(bTime -> baseline.valueAt(bTime)).collect(toList()); - var time = this.getTimeSequence(); - baselineTime.addAll(time); - baselineSignal.addAll(adjustedSignal); + this.copyToLastCalculation(); + baselineSignal.addAll(lastCalculation); return new HeatingCurve(baselineTime, baselineSignal, startTime, getName()); } diff --git a/src/main/java/pulse/HeatingCurveListener.java b/src/main/java/pulse/HeatingCurveListener.java index dd2219fe..7dd972c6 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -10,6 +10,7 @@ public interface HeatingCurveListener { /** * Signals that a {@code CurveEvent} has occurred. + * @param event */ public void onCurveEvent(CurveEvent event); diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index bdbeabf6..15776db3 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -19,7 +19,6 @@ import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; import pulse.input.listeners.DataListener; -import pulse.properties.NumericProperty; import pulse.ui.Messages; import pulse.util.PropertyHolderListener; @@ -79,15 +78,15 @@ public ExperimentalData() { } - public void addDataListener(DataListener listener) { + public final void addDataListener(DataListener listener) { dataListeners.add(listener); } - public void clearDataListener() { + public final void clearDataListener() { dataListeners.clear(); } - public void fireDataChanged(DataEvent dataEvent) { + public final void fireDataChanged(DataEvent dataEvent) { dataListeners.stream().forEach(l -> l.onDataChanged(dataEvent)); } @@ -98,7 +97,7 @@ public void fireDataChanged(DataEvent dataEvent) { * @see pulse.input.Range.reset() * @see pulse.input.IndexRange.reset() */ - public void resetRanges() { + public final void resetRanges() { indexRange.reset(getTimeSequence()); range.reset(indexRange, getTimeSequence()); } @@ -335,19 +334,6 @@ private void doSetMetadata() { range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); } - metadata.addListener(event -> { - - if (event.getProperty() instanceof NumericProperty) { - var p = (NumericProperty) event.getProperty(); - - if (p.getType() == PULSE_WIDTH) { - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - } - - } - - }); - } /** @@ -413,7 +399,6 @@ public void setRange(Range range) { } private void doSetRange() { - var time = getTimeSequence(); indexRange.set(time, range); addHierarchyListener(l -> { @@ -439,4 +424,4 @@ public double timeLimit() { return timeAt(indexRange.getUpperBound()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index 7b650e86..cd9369cb 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -1,7 +1,6 @@ package pulse.input; import static java.lang.System.lineSeparator; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericPropertyKeyword.DETECTOR_GAIN; import static pulse.properties.NumericPropertyKeyword.DETECTOR_IRIS; import static pulse.properties.NumericPropertyKeyword.DIAMETER; @@ -20,6 +19,7 @@ import pulse.problem.laser.NumericPulseData; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; @@ -46,8 +46,8 @@ public class Metadata extends PropertyHolder implements Reflexive { private SampleName sampleName; private int externalID; - private InstanceDescriptor pulseDescriptor = new InstanceDescriptor( - "Pulse Shape Selector", PulseTemporalShape.class); + private InstanceDescriptor pulseDescriptor + = new InstanceDescriptor<>("Pulse Shape Selector", PulseTemporalShape.class); private NumericPulseData pulseData; @@ -64,7 +64,7 @@ public Metadata(NumericProperty temperature, int externalId) { sampleName = new SampleName(); setExternalID(externalId); pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); - data = new TreeSet(); + data = new TreeSet<>(); set(TEST_TEMPERATURE, temperature); } @@ -115,15 +115,17 @@ public void setSampleName(SampleName sampleName) { this.sampleName = sampleName; } - public void setPulseData(NumericPulseData pulseData) { + public final void setPulseData(NumericPulseData pulseData) { this.pulseData = pulseData; + this.set(PULSE_WIDTH, derive(PULSE_WIDTH, pulseData.pulseWidth()) ); } /** * If a Numerical Pulse has been loaded (for example, when importing from * Proteus), this will return an object describing this data. + * @return */ - public NumericPulseData getPulseData() { + public final NumericPulseData getPulseData() { return pulseData; } @@ -270,4 +272,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 65b364e2..3dcb5d0d 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -18,6 +18,13 @@ public enum CurveEventType { * shifting it relative to the experimental data points) or by the search * procedure. */ - TIME_ORIGIN_CHANGED; + TIME_ORIGIN_CHANGED, + + /** + * A calculation associated with this curve has finished and + * the required arrays have been filled. + */ + + CALCULATION_FINISHED; } diff --git a/src/main/java/pulse/input/listeners/DataEvent.java b/src/main/java/pulse/input/listeners/DataEvent.java index f2b8ca4e..d42c1a20 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -1,6 +1,6 @@ package pulse.input.listeners; -import pulse.input.ExperimentalData; +import pulse.AbstractData; /** * A {@code DataEvent} is used to track changes happening with a @@ -10,7 +10,7 @@ public class DataEvent { private DataEventType type; - private ExperimentalData data; + private AbstractData data; /** * Constructs a {@code DataEvent} object, combining the {@code type} and @@ -19,7 +19,7 @@ public class DataEvent { * @param type the type of this event * @param data the source of the event */ - public DataEvent(DataEventType type, ExperimentalData data) { + public DataEvent(DataEventType type, AbstractData data) { this.type = type; this.data = data; } @@ -39,7 +39,7 @@ public DataEventType getType() { * * @return the associated data */ - public ExperimentalData getData() { + public AbstractData getData() { return data; } diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index 0423e378..da2160c1 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -14,6 +14,12 @@ public enum DataEventType { * @see pulse.input.ExperimentalData.truncate() */ - RANGE_CHANGED + RANGE_CHANGED, + + /** + * All data points loaded and are ready for processing. + */ + + DATA_LOADED; } diff --git a/src/main/java/pulse/io/export/ResultTableExporter.java b/src/main/java/pulse/io/export/ResultTableExporter.java index f61631ed..ea6bd54c 100644 --- a/src/main/java/pulse/io/export/ResultTableExporter.java +++ b/src/main/java/pulse/io/export/ResultTableExporter.java @@ -2,13 +2,16 @@ import java.io.FileOutputStream; import java.io.PrintStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.tasks.processing.AbstractResult; import pulse.tasks.processing.AverageResult; import pulse.ui.Messages; +import pulse.ui.Version; import pulse.ui.components.ResultTable; import pulse.ui.components.models.ResultTableModel; @@ -61,26 +64,33 @@ public void printToStream(ResultTable table, FileOutputStream fos, Extension ext } private void printHeaderCSV(ResultTable table, PrintStream stream) { - NumericPropertyKeyword p = null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + stream.println("Summary report on " + + LocalDateTime.now().format(formatter)); + stream.println("PULsE Version: " + Version.getCurrentVersion().toString()); + stream.println("Sample: " + TaskManager.getManagerInstance().getSampleName()); + stream.println("Ouput format sequence below: "); + + var fmt = ((ResultTableModel) table.getModel()).getFormat(); + for (int col = 0; col < table.getColumnCount(); col++) { - p = ((ResultTableModel) table.getModel()).getFormat().fromAbbreviation(table.getColumnName(col)); - stream.printf("%20s ", p); + var colName = fmt.fromAbbreviation(table.getColumnName(col)); + stream.println("Col. no.: " + col + " - " + colName); } - stream.println(""); + + stream.println("Note: average results are formatted as ; in the list below."); + stream.println(); } private void printIndividualCSV(NumericProperty p, PrintStream stream) { - if(p.getError() == null || p.getError().doubleValue() < 1E-20 ) { - if(p.getValue() instanceof Double) - stream.printf("%12.5e", p.valueInCurrentUnits()); - else - stream.printf("%12d", p.valueInCurrentUnits().intValue()); - } - else { - if(p.getValue() instanceof Double) - stream.printf("%12.5e +/- %12.5e", p.valueInCurrentUnits(), p.errorInCurrentUnits()); - else - stream.printf("%12d +/- %12d", p.valueInCurrentUnits().intValue(), p.errorInCurrentUnits().intValue()); + String fmt = p.getValue() instanceof Double ? "%-2.5e" : "%-6d"; + String s1 = String.format(fmt, p.getValue()).trim(); + String s2 = ""; + if (p.getError() != null) { + s2 = String.format(fmt, p.getError()).trim(); + stream.print(s1 + " ; " + s2 + " "); + } else { + stream.print(s1 + " "); } } @@ -104,8 +114,6 @@ private void printCSV(ResultTable table, FileOutputStream fos) { stream.print(Messages.getString("ResultTable.SeparatorCSV")); stream.println(""); - printHeaderCSV(table, stream); - results.stream().filter(r -> r instanceof AverageResult) .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { var props = AbstractResult.filterProperties(ir); diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index dae82be0..4445add9 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -8,6 +8,7 @@ import java.io.FileReader; import java.io.IOException; import java.text.DecimalFormat; +import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; @@ -50,16 +51,22 @@ public class NetzschCSVReader implements CurveReader { /** * Note comma is included as a delimiter character here. */ - private final static String ENGLISH_DELIMS = "[#(),/°Cx%^]+"; + private final static String ENGLISH_DELIMS = "[#(),;/°Cx%^]+"; private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; - private static String delims = ENGLISH_DELIMS; - + private static String delims; //default number format (British format) - private static Locale locale = Locale.ENGLISH; + private static Locale locale; + + private static NumberFormat format; private NetzschCSVReader() { - //intentionally blank + //do nothing + } + + protected void setDefaultLocale() { + delims = ENGLISH_DELIMS; + locale = Locale.ENGLISH; } /** @@ -97,39 +104,39 @@ public String getSupportedExtension() { @Override public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); - ExperimentalData curve = new ExperimentalData(); + setDefaultLocale(); //always start with a default locale + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); - var format = DecimalFormat.getInstance(locale); - format.setGroupingUsed(false); - - var spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, delims); + String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); + double spotSize = 0; if(spot != null) { var spotTokens = spot.split(delims); spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; } - var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); + String tempLine = findLineByLabel(reader, THICKNESS, false); + var tempTokens = tempLine.split(delims); final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; - tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); + tempTokens = findLineByLabel(reader, DIAMETER, false).split(delims); final double diameter = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; - tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); + tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, false).split(delims); final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; /* * Finds the detector keyword. */ - var detectorLabel = findLineByLabel(reader, DETECTOR, delims); + var detectorLabel = findLineByLabel(reader, DETECTOR, true); if (detectorLabel == null) { System.err.println("Skipping " + file.getName()); @@ -156,17 +163,41 @@ public List read(File file) throws IOException { return null; } + + /** + * Note: the {@code line} must contain a decimal-separated number. + * @param line a line containing number with a decimal separator + */ + + private static void guessLocaleAndFormat(String line) { + + if(line.contains(".")) { + delims = ENGLISH_DELIMS; + locale = Locale.ENGLISH; + } + + else { + delims = GERMAN_DELIMS; + locale = Locale.GERMAN; + } + + format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); + } protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { double time; double power; String[] tokens; - var format = DecimalFormat.getInstance(locale); - format.setGroupingUsed(false); for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - + + if(tokens.length < 2) { + guessLocaleAndFormat(line); + tokens = line.split(delims); + } + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); @@ -178,39 +209,25 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO String shotIDLine = reader.readLine(); String[] shotID = shotIDLine.split(delims); - int shotId = -1; + int id; - if(shotID.length < 3) { - - if(locale == Locale.ENGLISH) { - delims = GERMAN_DELIMS; - locale = Locale.GERMAN; - } - else { - delims = ENGLISH_DELIMS; - locale = Locale.ENGLISH; - } - - shotID = shotIDLine.split(delims); - } - //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { throw new IllegalArgumentException(file.getName() + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); } else { - shotId = Integer.parseInt(shotID[shotID.length - 1]); + id = Integer.parseInt(shotID[shotID.length - 1]); } - return shotId; + return id; } - protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { - return findLineByLabel(reader, label, "!!!", delims); + protected static String findLineByLabel(BufferedReader reader, String label, boolean ignoreLocale) throws IOException { + return findLineByLabel(reader, label, "!!!", ignoreLocale); } - protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, String delims) throws IOException { + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, boolean ignoreLocale) throws IOException { String line = ""; String[] tokens; @@ -221,6 +238,11 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { + if(line.isBlank()) + continue; + + if(!ignoreLocale) + guessLocaleAndFormat(line); tokens = line.split(delims); for (String token : tokens) { @@ -262,4 +284,4 @@ public static String getDelims() { return delims; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 4ba3303a..7d759e6f 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -55,13 +55,16 @@ public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); NumericPulseData data = null; + + ( (NetzschCSVReader) NetzschCSVReader.getInstance() ) + .setDefaultLocale(); //always start with a default locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = NetzschCSVReader.determineShotID(reader, file); data = new NumericPulseData(shotId); - var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.getDelims()); + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, false); if (pulseLabel == null) { System.err.println("Skipping " + file.getName()); diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 97bc4a28..0f8201bb 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -165,12 +165,12 @@ public static List datasetReaders() { /** * Attempts to find a {@code DatasetReader} for processing {@code file}. * + * @param + * @param readers * @param file the target file supposedly containing data for an * {@code InterpolationDataset}. * @return an {@code InterpolationDataset} extracted from { * @file} using the first available {@code DatasetReader} from the list - * @throws IOException if the reader has been found, but an error occurred - * when reading the file * @throws IllegalArgumentException if the file has an unsupported extension */ public static T read(List> readers, File file) { diff --git a/src/main/java/pulse/math/FixedIntervalIntegrator.java b/src/main/java/pulse/math/FixedIntervalIntegrator.java index 8a857986..ac77ccb4 100644 --- a/src/main/java/pulse/math/FixedIntervalIntegrator.java +++ b/src/main/java/pulse/math/FixedIntervalIntegrator.java @@ -5,14 +5,10 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * A fixed-interval integrator implements a numerical scheme in which the domain @@ -64,7 +60,7 @@ public NumericProperty getIntegrationSegments() { * @param integrationSegments a property of the {@code INTEGRATION_SEGMENTS} * type */ - public void setIntegrationSegments(NumericProperty integrationSegments) { + public final void setIntegrationSegments(NumericProperty integrationSegments) { requireType(integrationSegments, INTEGRATION_SEGMENTS); this.integrationSegments = (int) integrationSegments.getValue(); } @@ -76,7 +72,7 @@ public void setIntegrationSegments(NumericProperty integrationSegments) { * @param bounds the integration bounds */ @Override - public void setBounds(Segment bounds) { + public final void setBounds(Segment bounds) { super.setBounds(bounds); } @@ -104,7 +100,7 @@ public Set listedKeywords() { * * @return the integration step size. */ - public double stepSize() { + public final double stepSize() { return getBounds().length() / (double) this.integrationSegments; } diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 295f5a34..4b852216 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -7,7 +7,6 @@ import pulse.math.linear.Vector; import pulse.math.transforms.Transformable; import pulse.properties.NumericProperties; -import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java index 2a849135..8b512871 100644 --- a/src/main/java/pulse/math/transforms/InvLenSqTransform.java +++ b/src/main/java/pulse/math/transforms/InvLenSqTransform.java @@ -16,12 +16,12 @@ public InvLenSqTransform(ThermalProperties tp) { @Override public double transform(double value) { - return value / (l * l); + return Math.abs(value) / (l * l); } @Override public double inverse(double t) { - return t * (l * l); + return Math.abs(t) * (l * l); } } diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index d68c9352..00239cbf 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -40,6 +40,7 @@ public StickTransform(Segment bounds) { } /** + * @param a * @see pulse.math.MathUtils.atanh() * @see pulse.math.Segment.getBounds() */ diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index d29c073b..8160344d 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -2,6 +2,8 @@ import java.util.Objects; import pulse.input.ExperimentalData; +import pulse.math.MidpointIntegrator; +import pulse.math.Segment; import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; @@ -16,10 +18,23 @@ */ public class DiscretePulse { - private Grid grid; - private Pulse pulse; - private double discretePulseWidth; - private double timeFactor; + private final Grid grid; + private final Pulse pulse; + + private double widthOnGrid; + private double timeConversionFactor; + private double invTotalEnergy; //normalisation factor + + /** + * This number shows how small the actual pulse may be compared to the + * half-time. If the pulse is shorter than + * tc/{@value WIDTH_TOLERANCE_FACTOR}, it will be replaced + * by a rectangular pulse with the width equal to + * tc/{@value WIDTH_TOLERANCE_FACTOR}. Here + * tc + * is the time factor defined in the {@code Problem} class. + */ + public final static int WIDTH_TOLERANCE_FACTOR = 1000; /** * This creates a one-dimensional discrete pulse on a {@code grid}. @@ -34,25 +49,33 @@ public class DiscretePulse { */ public DiscretePulse(Problem problem, Grid grid) { this.grid = grid; - timeFactor = problem.getProperties().timeFactor(); + timeConversionFactor = problem.getProperties().timeFactor(); this.pulse = problem.getPulse(); - recalculate(); + Object ancestor + = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), + "Problem has not been assigned to a SearchTask"); - Object ancestor = - Objects.requireNonNull( problem.specificAncestor(SearchTask.class), - "Problem has not been assigned to a SearchTask"); + ExperimentalData data = ((SearchTask) ancestor).getExperimentalCurve(); + init(data); - ExperimentalData data = ((SearchTask)ancestor).getExperimentalCurve(); - - pulse.getPulseShape().init(data, this); pulse.addListener(e -> { - timeFactor = problem.getProperties().timeFactor(); - recalculate(); - pulse.getPulseShape().init(data, this); + timeConversionFactor = problem.getProperties().timeFactor(); + init(data); }); + + grid.addListener(e + -> init(data) + ); } + + private void init(ExperimentalData data) { + widthOnGrid = 0; + recalculate(); + pulse.getPulseShape().init(data, this); + normalise(); + } /** * Uses the {@code PulseTemporalShape} of the {@code Pulse} object to @@ -62,7 +85,7 @@ public DiscretePulse(Problem problem, Grid grid) { * @return the laser power at the specified moment of {@code time} */ public double laserPowerAt(double time) { - return pulse.getPulseShape().evaluateAt(time); + return invTotalEnergy * pulse.getPulseShape().evaluateAt(time); } /** @@ -71,18 +94,66 @@ public double laserPowerAt(double time) { * * @see pulse.problem.schemes.Grid.gridTime(double,double) */ - public void recalculate() { - final double width = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - discretePulseWidth = Math.max(grid.gridTime(width, timeFactor), grid.getTimeStep()); + public final void recalculate() { + final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); + final double resolvedWidth = timeConversionFactor / WIDTH_TOLERANCE_FACTOR; + + final double EPS = 1E-10; + + /** + * The pulse is too short, which makes calculations too expensive. Can + * we replace it with a rectangular pulse shape instead? + */ + + if (nominalWidth < resolvedWidth - EPS && widthOnGrid < EPS) { + //change shape to rectangular + var shape = new RectangularPulse(); + pulse.setPulseShape(shape); + //change pulse width + setDiscreteWidth(resolvedWidth); + shape.init(null, this); + } else if(nominalWidth > resolvedWidth + EPS) { + setDiscreteWidth(nominalWidth); + } + + } + + /** + * Calculates the total pulse energy using a numerical integrator. The + * normalisation factor is then equal to the inverse total energy. + */ + + public final void normalise() { + invTotalEnergy = 1.0; + var pulseShape = pulse.getPulseShape(); + + var integrator = new MidpointIntegrator(new Segment(0, widthOnGrid)) { + + @Override + public double integrand(double... vars) { + return pulseShape.evaluateAt(vars[0]); + } + + }; + + invTotalEnergy = 1.0 / integrator.integrate(); + } /** - * Gets the discrete pulse width defined by {@code DiscretePulse}. + * Gets the discrete dimensionless pulse width, which is a multiplier of the current + * grid timestep. The pulse width is converted to the dimensionless pulse width by + * dividing the real value by l2/a. * - * @return a double, representing the discrete pulse width. + * @return the dimensionless pulse width mapped to the grid. */ public double getDiscreteWidth() { - return discretePulseWidth; + return widthOnGrid; + } + + private void setDiscreteWidth(double width) { + widthOnGrid = grid.gridTime(width, timeConversionFactor); + grid.adjustTimeStep(this); } /** @@ -102,5 +173,25 @@ public Pulse getPulse() { public Grid getGrid() { return grid; } + + /** + * Gets the dimensional factor required to convert real time variable into + * a dimensional variable, defined in the {@code Problem} class + * @return the conversion factor + */ + + public double getConversionFactor() { + return timeConversionFactor; + } + + /** + * Gets the minimal resolved pulse width defined by the {@code WIDTH_TOLERANCE_FACTOR} + * and the characteristic time given by the {@code getConversionFactor}. + * @return + */ + + public double resolvedPulseWidth() { + return timeConversionFactor / WIDTH_TOLERANCE_FACTOR; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 96ad965e..a34102f5 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -19,7 +19,7 @@ public class DiscretePulse2D extends DiscretePulse { private double discretePulseSpot; - private double coordFactor; + private double radialFactor; /** * The constructor for {@code DiscretePulse2D}. @@ -35,12 +35,11 @@ public class DiscretePulse2D extends DiscretePulse { public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { super(problem, grid); var properties = (ExtendedThermalProperties) problem.getProperties(); - coordFactor = (double) properties.getSampleDiameter().getValue() / 2.0; - var pulse = (Pulse2D) problem.getPulse(); - discretePulseSpot = grid.gridRadialDistance((double) pulse.getSpotDiameter().getValue() / 2.0, coordFactor); - + init(properties); + + properties.addListener(e -> init(properties) ); } - + /** * This calculates the dimensionless, discretised pulse function at a * dimensionless radial coordinate {@code coord}. @@ -59,22 +58,31 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { public double evaluateAt(double time, double radialCoord) { return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); } + + private void init(ExtendedThermalProperties properties) { + radialFactor = (double) properties.getSampleDiameter().getValue() / 2.0; + evalPulseSpot(); + } /** - * Calls the superclass method, then calculates the - * {@code discretePulseSpot} using the {@code gridRadialDistance} method. + * Calculates the {@code discretePulseSpot} using the {@code gridRadialDistance} method. * * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) */ - @Override - public void recalculate() { - super.recalculate(); - final double radius = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue() / 2.0; - discretePulseSpot = ((Grid2D) getGrid()).gridRadialDistance(radius, coordFactor); + public final void evalPulseSpot() { + var pulse = (Pulse2D) getPulse(); + var grid2d = (Grid2D) getGrid(); + final double radius = (double) pulse.getSpotDiameter().getValue() / 2.0; + discretePulseSpot = grid2d.gridRadialDistance(radius, radialFactor); + grid2d.adjustStepSize(this); } - public double getDiscretePulseSpot() { + public final double getDiscretePulseSpot() { return discretePulseSpot; } + + public final double getRadialConversionFactor() { + return radialFactor; + } } diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index 21c39744..379ab97f 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -10,14 +10,10 @@ import static pulse.properties.NumericPropertyKeyword.SKEW_MU; import static pulse.properties.NumericPropertyKeyword.SKEW_SIGMA; -import java.util.List; import java.util.Set; -import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import pulse.properties.Property; /** * Represents the exponentially modified Gaussian function, which is given by @@ -31,7 +27,8 @@ public class ExponentiallyModifiedGaussian extends PulseTemporalShape { private double mu; private double sigma; private double lambda; - private double norm; + + private final static int MIN_POINTS = 10; /** * Creates an exponentially modified Gaussian with the default parameter @@ -41,7 +38,6 @@ public ExponentiallyModifiedGaussian() { mu = (double) def(SKEW_MU).getValue(); lambda = (double) def(SKEW_LAMBDA).getValue(); sigma = (double) def(SKEW_SIGMA).getValue(); - norm = 1.0; } public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { @@ -49,18 +45,6 @@ public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { this.mu = another.mu; this.sigma = another.sigma; this.lambda = another.lambda; - this.norm = another.norm; - } - - /** - * This calls the superclass {@code init method} and sets the normalisation - * factor to 1/∫Φ(Fo)dFo. - */ - @Override - public void init(ExperimentalData data, DiscretePulse pulse) { - super.init(data, pulse); - norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of - // the area. } /** @@ -77,7 +61,7 @@ public double evaluateAt(double time) { final double lambdaHalf = 0.5 * lambda; final double sigmaSq = sigma * sigma; - return norm * lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) + return lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); } @@ -170,4 +154,9 @@ public PulseTemporalShape copy() { return new ExponentiallyModifiedGaussian(this); } -} + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 60989bf0..265ac2a5 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -25,7 +25,8 @@ public class NumericPulse extends PulseTemporalShape { private NumericPulseData pulseData; private UnivariateFunction interpolation; - private double adjustedPulseWidth; + + private final static int MIN_POINTS = 20; public NumericPulse() { //intentionally blank @@ -47,59 +48,47 @@ public NumericPulse(NumericPulse pulse) { * interpolates the input pulse using spline functions and normalises the * output. * + * @param data * @see normalise() * */ @Override public void init(ExperimentalData data, DiscretePulse pulse) { - pulseData = data.getMetadata().getPulseData(); - - //subtracts a horizontal baseline from the pulse data - var baseline = new FlatBaseline(); - baseline.fitNegative(pulseData); - - for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) - pulseData.setSignalAt(i, - pulseData.signalAt(i) - baseline.valueAt(pulseData.timeAt(i))); + //generate baseline-subtracted numeric data from ExperimentalData + baselineSubtractedFrom(data); + //notify host pulse object of a new pulse width var problem = ((SearchTask) data.getParent()).getCurrentCalculation().getProblem(); - setPulseWidth(problem); + setPulseWidthOf(problem); + //convert to dimensionless time and interpolate double timeFactor = problem.getProperties().timeFactor(); - - super.init(data, pulse); - doInterpolation(timeFactor); - - normalise(problem); } - + /** - * Checks that the area of the pulse curve is unity (within a small error - * margin). If this is {@code false}, re-scales the numeric data using - * {@code 1/area} as the scaling factor. - * - * @param problem defines the {@code timeFactor} needed for re-building the - * interpolation - * @see pulse.problem.laser.NumericPulseData.scale() + * Copies the numeric pulse from metadata and subtracts a horizontal baseline + * from the data points assigned to {@code pulseData}. + * @param data the experimental data containing the metadata with numeric pulse data. */ - public void normalise(Problem problem) { - - final double EPS = 1E-2; - double timeFactor = problem.getProperties().timeFactor(); - - for (double area = area(); Math.abs(area - 1.0) > EPS; area = area()) { - pulseData.scale(1.0 / area); - doInterpolation(timeFactor); - } - + + private void baselineSubtractedFrom(ExperimentalData data) { + pulseData = new NumericPulseData(data.getMetadata().getPulseData()); + + //subtracts a horizontal baseline from the pulse data + var baseline = new FlatBaseline(); + baseline.fitNegative(pulseData); + + for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) + pulseData.setSignalAt(i, + pulseData.signalAt(i) - baseline.valueAt(pulseData.timeAt(i))); } - private void setPulseWidth(Problem problem) { - var timeSequence = pulseData.getTimeSequence(); - double pulseWidth = timeSequence.get(timeSequence.size() - 1); + private void setPulseWidthOf(Problem problem) { + var timeSequence = pulseData.getTimeSequence(); + double pulseWidth = timeSequence.get(timeSequence.size() - 1); - var pulseObject = problem.getPulse(); + var pulseObject = problem.getPulse(); pulseObject.setPulseWidth(derive(PULSE_WIDTH, pulseWidth)); } @@ -107,13 +96,13 @@ private void setPulseWidth(Problem problem) { private void doInterpolation(double timeFactor) { var interpolator = new AkimaSplineInterpolator(); - var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); - adjustedPulseWidth = timeList[timeList.length - 1]; - var powerList = pulseData.getSignalData(); + var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); + var powerList = pulseData.getSignalData(); + this.setPulseWidth(timeList[timeList.length - 1]); + interpolation = interpolator.interpolate(timeList, powerList.stream().mapToDouble(d -> d).toArray()); - } /** @@ -122,7 +111,7 @@ private void doInterpolation(double timeFactor) { */ @Override public double evaluateAt(double time) { - return time > adjustedPulseWidth ? 0.0 : interpolation.value(time); + return time > getPulseWidth() ? 0.0 : interpolation.value(time); } @Override @@ -144,10 +133,16 @@ public NumericPulseData getData() { public void setData(NumericPulseData pulseData) { this.pulseData = pulseData; + } public UnivariateFunction getInterpolation() { return interpolation; } + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } + } diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index 0a0493ef..e23c023d 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -10,7 +10,7 @@ */ public class NumericPulseData extends AbstractData { - private int externalID; + private final int externalID; /** * Stores {@code id} and calls super-constructor @@ -50,20 +50,9 @@ public void addPoint(double time, double power) { public int getExternalID() { return externalID; } - - /** - * Uniformly scales the values of the pulse power by {@code factor}. - * - * @param factor the scaling factor - */ - public void scale(double factor) { - - var power = this.getSignalData(); - - for (int i = 0, size = power.size(); i < size; i++) { - power.set(i, power.get(i) * factor); - } - + + public double pulseWidth() { + return super.timeLimit(); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index 57019513..6c3814e4 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -1,12 +1,7 @@ package pulse.problem.laser; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import pulse.input.ExperimentalData; -import pulse.math.FixedIntervalIntegrator; -import pulse.math.MidpointIntegrator; -import pulse.math.Segment; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -21,49 +16,14 @@ public abstract class PulseTemporalShape extends PropertyHolder implements Refle private double width; - private final static int DEFAULT_POINTS = 256; - private FixedIntervalIntegrator integrator; - public PulseTemporalShape() { //intentionlly blank } public PulseTemporalShape(PulseTemporalShape another) { - this.integrator = another.integrator; - } - - /** - * Creates a new midpoint-integrator using the number of segments equal to - * {@value DEFAULT_POINTS}. The integrand function is specified by the - * {@code evaluateAt} method of this class. - * - * @see pulse.math.MidpointIntegrator - * @see evaluateAt() - */ - public void initAreaIntegrator() { - integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), - derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { - - @Override - public double integrand(double... vars) { - return evaluateAt(vars[0]); - } - - }; + this.width = another.width; } - - /** - * Uses numeric integration (midpoint rule) to calculate the area of the - * pulse shape corresponding to the selected parameters. The integration - * bounds are non-negative. - * - * @return the area - */ - public double area() { - integrator.setBounds(new Segment(0.0, getPulseWidth())); - return integrator.integrate(); - } - + /** * This evaluates the dimensionless, discretised pulse function on a * {@code grid} needed to evaluate the heat source in the difference scheme. @@ -78,11 +38,11 @@ public double area() { * Stores the pulse width from {@code pulse} and initialises area * integration. * + * @param data * @param pulse the discrete pulse containing the pulse width */ public void init(ExperimentalData data, DiscretePulse pulse) { width = pulse.getDiscreteWidth(); - this.initAreaIntegrator(); } public abstract PulseTemporalShape copy(); @@ -104,5 +64,7 @@ public double getPulseWidth() { public void setPulseWidth(double width) { this.width = width; } + + public abstract int getRequiredDiscretisation(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index 39369a21..2ba6b9da 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -14,8 +14,11 @@ */ public class RectangularPulse extends PulseTemporalShape { + private final static int MIN_POINTS = 4; + /** * @param time the time measured from the start of the laser pulse. + * @return */ @Override public double evaluateAt(double time) { @@ -32,5 +35,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new RectangularPulse(); } + + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } } diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index 82bb8542..c305b816 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -6,14 +6,10 @@ import static pulse.properties.NumericPropertyKeyword.TRAPEZOIDAL_FALL_PERCENTAGE; import static pulse.properties.NumericPropertyKeyword.TRAPEZOIDAL_RISE_PERCENTAGE; -import java.util.List; import java.util.Set; -import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import pulse.properties.Property; /** * A trapezoidal pulse shape, which combines a rise segment, a constant-power @@ -26,6 +22,8 @@ public class TrapezoidalPulse extends PulseTemporalShape { private double fall; private double h; + private final static int MIN_POINTS = 6; + /** * Constructs a trapezoidal pulse using a default segmentation principle. * The reader is referred to the {@code .xml} file containing the default @@ -36,7 +34,7 @@ public class TrapezoidalPulse extends PulseTemporalShape { public TrapezoidalPulse() { rise = (int) def(TRAPEZOIDAL_RISE_PERCENTAGE).getValue() / 100.0; fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; - h = height(); + h = 1.0; } public TrapezoidalPulse(TrapezoidalPulse another) { @@ -44,16 +42,7 @@ public TrapezoidalPulse(TrapezoidalPulse another) { this.fall = another.fall; this.h = another.h; } - - /** - * Calculates the height of the trapez after calling the super-class method. - */ - @Override - public void init(ExperimentalData data, DiscretePulse pulse) { - super.init(data, pulse); - h = height(); - } - + /** * Calculates the height of the trapezium which under current segmentation * will yield an area of unity. @@ -136,5 +125,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new TrapezoidalPulse(this); } + + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index 2ff641fa..db0c7e48 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -56,5 +56,16 @@ public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty public String toString() { return getString("ADIScheme.4"); } + + /** + * Contains only an empty statement, as the pulse needs to be calculated not only + * for the time step {@code m} but also accounting for the radial coordinate + * @param m thte time step + */ + + @Override + public void prepareStep(int m) { + //do nothing + } } diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index 0c108abc..acaa5cd1 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -11,15 +11,16 @@ */ public class BlockMatrixAlgorithm extends TridiagonalMatrixAlgorithm { - private double[] gamma; - private double[] p; - private double[] q; + private final double[] gamma; + private final double[] p; + private final double[] q; public BlockMatrixAlgorithm(Grid grid) { super(grid); - gamma = new double[getAlpha().length]; - p = new double[gamma.length - 1]; - q = new double[gamma.length - 1]; + final int N = this.getGridPoints(); + gamma = new double[N + 2]; + p = new double[N]; + q = new double[N]; } @Override @@ -33,9 +34,10 @@ public void sweep(double[] V) { @Override public void evaluateBeta(final double[] U) { super.evaluateBeta(U); - final int N = getGrid().getGridDensityValue(); var alpha = getAlpha(); var beta = getBeta(); + + final int N = getGridPoints(); p[N - 1] = beta[N]; q[N - 1] = alpha[N] + gamma[N]; @@ -49,8 +51,9 @@ public void evaluateBeta(final double[] U) { @Override public void evaluateBeta(final double[] U, final int start, final int endExclusive) { var alpha = getAlpha(); - var grid = getGrid(); - final double HX2_TAU = grid.getXStep() * grid.getXStep() / getGrid().getTimeStep(); + + final double h = this.getGridStep(); + final double HX2_TAU = h * h / this.getTimeStep(); final double a = getCoefA(); final double b = getCoefB(); diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 848482bb..879a51ce 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -1,7 +1,5 @@ package pulse.problem.schemes; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import java.util.Set; @@ -13,18 +11,15 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -public abstract class CoupledImplicitScheme extends ImplicitScheme implements FixedPointIterations { +public abstract class CoupledImplicitScheme extends ImplicitScheme { private RadiativeTransferCoupling coupling; private RTECalculationStatus calculationStatus; - private double nonlinearPrecision; - - private double pls; + private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor) { super(); setGrid(new Grid(N, timeFactor)); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setCoupling(new RadiativeTransferCoupling()); calculationStatus = RTECalculationStatus.NORMAL; } @@ -33,37 +28,14 @@ public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, Nume this(N, timeFactor); setTimeLimit(timeLimit); } - - @Override - public void timeStep(final int m) throws SolverException { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(final int m) throws SolverException { - super.timeStep(m); - } - - @Override - public void finaliseIteration(double[] V) throws SolverException { - FixedPointIterations.super.finaliseIteration(V); - var rte = coupling.getRadiativeTransferEquation(); - setCalculationStatus(coupling.getRadiativeTransferEquation().compute(V)); - } - - public RadiativeTransferCoupling getCoupling() { - return coupling; - } - - public final void setCoupling(RadiativeTransferCoupling coupling) { - this.coupling = coupling; - this.coupling.setParent(this); - } - + @Override public void finaliseStep() throws SolverException { super.finaliseStep(); + if(autoUpdateFluxes) { + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(getCurrentSolution())); + } coupling.getRadiativeTransferEquation().getFluxes().store(); } @@ -74,45 +46,42 @@ public Set listedKeywords() { return set; } - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public Class domain() { - return ParticipatingMedium.class; - } - @Override public boolean normalOperation() { return super.normalOperation() && (getCalculationStatus() == RTECalculationStatus.NORMAL); } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NONLINEAR_PRECISION) { - setNonlinearPrecision(property); - } else { - super.set(type, property); - } - } - - public RTECalculationStatus getCalculationStatus() { + public final RTECalculationStatus getCalculationStatus() { return calculationStatus; } - public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { + public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; - if(calculationStatus != RTECalculationStatus.NORMAL) + if (calculationStatus != RTECalculationStatus.NORMAL) { throw new SolverException(calculationStatus.toString()); + } + } + + public final RadiativeTransferCoupling getCoupling() { + return coupling; } - public double getCurrentPulseValue() { - return pls; + public final void setCoupling(RadiativeTransferCoupling coupling) { + this.coupling = coupling; + this.coupling.setParent(this); + } + + public final boolean isAutoUpdateFluxes() { + return this.autoUpdateFluxes; + } + + public final void setAutoUpdateFluxes(boolean auto) { + this.autoUpdateFluxes = auto; + } + + @Override + public Class[] domain() { + return new Class[]{ParticipatingMedium.class}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 09ea1cf6..64e33e1b 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -12,7 +12,8 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; +import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; +import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -33,6 +34,7 @@ public abstract class DifferenceScheme extends PropertyHolder implements Reflexi private Grid grid; private double timeLimit; + private double pls; private int timeInterval; private static boolean hideDetailedAdjustment = true; @@ -61,23 +63,6 @@ public void initFrom(DifferenceScheme another) { this.timeInterval = another.timeInterval; } - /** - * Used to get a class of problems on which this difference scheme is - * applicable. - * - * @return a subclass of the {@code Problem} class which can be used as - * input for this difference scheme. - */ - public abstract Class domain(); - - /** - * Creates a {@code DifferenceScheme}, which is an exact copy of this - * object. - * - * @return an exact copy of this {@code DifferenceScheme}. - */ - public abstract DifferenceScheme copy(); - /** * Copies the {@code Grid} and {@code timeLimit} from {@code df}. * @@ -91,91 +76,88 @@ public void copyFrom(DifferenceScheme df) { /** *

- * Contains preparatory steps to ensure smooth running of the solver. This - * includes creating a {@code DiscretePulse} object and adjusting the grid - * of this scheme to match the {@code DiscretePulse} created for this - * {@code problem}. Finally, a heating curve is cleared from the previously - * calculated values. - *

+ * Contains preparatory steps to ensure smooth running of the solver.This + * includes creating a {@code DiscretePulse}object and adjusting the grid of + * this scheme to match the {@code DiscretePulse}created for this + * {@code problem} Finally, a heating curve is cleared from the previously + * calculated values.

*

* All subclasses of {@code DifferenceScheme} should override and explicitly * call this superclass method where appropriate. *

* * @param problem the heat problem to be solved + * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.problem.schemes.Grid.adjustTo() */ - protected void prepare(Problem problem) { - if(discretePulse == null) + protected void prepare(Problem problem) throws SolverException { + if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - else + } + else { discretePulse.recalculate(); + } - grid.adjustTo(discretePulse); - - var hc = problem.getHeatingCurve(); - hc.clear(); + clearArrays(); } public void runTimeSequence(Problem problem) throws SolverException { runTimeSequence(problem, 0, timeLimit); + } + + public void scaleSolution(Problem problem) { var curve = problem.getHeatingCurve(); final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); - curve.scale(maxTemp / curve.apparentMaximum()); + //curve.scale(maxTemp / curve.apparentMaximum()); + curve.scale(maxTemp); } public void runTimeSequence(Problem problem, final double offset, final double endTime) throws SolverException { var curve = problem.getHeatingCurve(); + curve.clear(); - int adjustedNumPoints = (int) curve.getNumPoints().getValue(); + int numPoints = (int) curve.getNumPoints().getValue(); - final double startTime = (double) curve.getTimeShift().getValue(); + final double startTime = (double) curve.getTimeShift().getValue(); final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); - final double tau = grid.getTimeStep(); - for (double dt = 0, factor; dt < tau; adjustedNumPoints *= factor) { - dt = timeSegment / (adjustedNumPoints - 1); - factor = dt / tau; - timeInterval = (int) factor; - } + double tau = grid.getTimeStep(); + final double dt = timeSegment / (numPoints - 1); + timeInterval = Math.max( (int) (dt / tau), 1); - final double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); + double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); // First point (index = 0) is always (0.0, 0.0) + curve.addPoint(0.0, 0.0); + + double nextTime; + int previous; /* - * The outer cycle iterates over the number of points of the HeatingCurve + * The outer cycle iterates over the number of points of the HeatingCurve */ - double nextTime = offset + wFactor; - curve.addPoint(0.0, 0.0); - - for (int w = 1; nextTime < 1.01 * endTime; nextTime = offset + (++w) * wFactor) { + for (previous = 1, nextTime = offset; nextTime < endTime || !curve.isFull(); + previous += timeInterval) { /* - * Two adjacent points of the heating curves are separated by timeInterval on - * the time grid. Thus, to calculate the next point on the heating curve, - * timeInterval/tau time steps have to be made first. + * Two adjacent points of the heating curves are separated by timeInterval on + * the time grid. Thus, to calculate the next point on the heating curve, + * timeInterval/tau time steps have to be made first. */ - timeSegment((w - 1) * timeInterval + 1, w * timeInterval + 1); + timeSegment(previous, previous + timeInterval); + nextTime += wFactor; curve.addPoint(nextTime, signal()); - - } - - /** - * If the total number of points added by the procedure - * is actually less than the pre-set number of points -- change that number - */ - - if(curve.actualNumPoints() < (int)curve.getNumPoints().getValue()) { - curve.setNumPoints(derive(NUMPOINTS, curve.actualNumPoints())); } + curve.copyToLastCalculation(); + scaleSolution(problem); } private void timeSegment(final int m1, final int m2) throws SolverException { for (int m = m1; m < m2 && normalOperation(); m++) { - timeStep(m); - finaliseStep(); + prepareStep(m); //prepare + timeStep(m); //calculate + finaliseStep(); //finalise } } @@ -183,11 +165,15 @@ public double pulse(final int m) { return getDiscretePulse().laserPowerAt((m - EPS) * getGrid().getTimeStep()); } - public abstract double signal(); - - public abstract void timeStep(final int m) throws SolverException; - - public abstract void finaliseStep() throws SolverException; + /** + * Do preparatory calculations that depend only on the time variable, e.g., + * calculate the pulse power. + * + * @param m the time step number + */ + public void prepareStep(int m) { + pls = pulse(m); + } public boolean normalOperation() { return true; @@ -290,6 +276,10 @@ public final NumericProperty getTimeLimit() { return derive(TIME_LIMIT, timeLimit); } + public double getCurrentPulseValue() { + return pls; + } + /** * Sets the time limit (in units defined by the corresponding * {@code NumericProperty}), which serves as the breakpoint for the @@ -311,5 +301,30 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { setTimeLimit(property); } } + + public abstract double signal(); + + public abstract void clearArrays(); + + public abstract void timeStep(int m) throws SolverException; -} + public abstract void finaliseStep() throws SolverException; + + /** + * Retrieves all problem statements that can be solved with this + * implementation of the difference scheme. + * + * @return an array containing subclasses of the {@code Problem} class which + * can be used as input for this difference scheme. + */ + public abstract Class[] domain(); + + /** + * Creates a {@code DifferenceScheme}, which is an exact copy of this + * object. + * + * @return an exact copy of this {@code DifferenceScheme}. + */ + public abstract DifferenceScheme copy(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index 2ef708df..88831e4c 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -35,4 +35,4 @@ public static double evaluateSignal(final AbsorptionModel absorption, final Grid return signal * 0.5 * hx; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index d62e7dcd..f68467bf 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -3,21 +3,16 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; import static java.lang.String.format; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.problem.laser.DiscretePulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; import pulse.util.PropertyHolder; /** @@ -48,8 +43,10 @@ public class Grid extends PropertyHolder { * @see pulse.properties.NumericPropertyKeyword */ public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { - setGridDensity(gridDensity); - setTimeFactor(timeFactor); + this.N = (int) gridDensity.getValue(); + this.tauFactor = (double) timeFactor.getValue(); + hx = 1. / N; + setTimeStep(tauFactor * pow(hx, 2)); } protected Grid() { @@ -67,20 +64,34 @@ public Grid copy() { } /** - * Optimises the {@code Grid} parameters. + * Optimises the {@code Grid} parameters so that the timestep is + * sufficiently small to enable accurate pulse correction. *

* This can change the {@code tauFactor} and {@code tau} variables in the - * {@code Grid} object if {@code discretePulseWidth < grid.tau}. + * {@code Grid} object if {@code discretePulseWidth/(M - 1) < grid.tau}, + * where M is the required number of pulse calculations. *

* * @param pulse the discrete pulse representation + * @see PulseTemporalShape.getRequiredDiscretisation() */ - public void adjustTo(DiscretePulse pulse) { - final double ADJUSTMENT_FACTOR = 0.75; - for (final double factor = 0.95; factor * tau > pulse.getDiscreteWidth(); pulse.recalculate()) { - tauFactor *= ADJUSTMENT_FACTOR; - tau = tauFactor * pow(hx, 2); + public final void adjustTimeStep(DiscretePulse pulse) { + double timeFactor = pulse.getConversionFactor(); + + final int reqPoints = pulse.getPulse().getPulseShape().getRequiredDiscretisation(); + + double pNominalWidth = (double) pulse.getPulse().getPulseWidth().getValue(); + double pResolvedWidth = pulse.resolvedPulseWidth(); + double pWidth = pNominalWidth < pResolvedWidth ? pResolvedWidth : pNominalWidth; + + double newTau = pWidth / timeFactor / (reqPoints > 1 ? reqPoints - 1 : 1); + double newTauFactor = newTau / (hx * hx); + + final double EPS = 1E-10; + if (newTauFactor < tauFactor - EPS) { + setTimeFactor(derive(TAU_FACTOR, newTauFactor)); } + } /** @@ -115,7 +126,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { * * @return a double, representing the {@code hx} value. */ - public double getXStep() { + public final double getXStep() { return hx; } @@ -124,7 +135,7 @@ public double getXStep() { * * @param hx a double, representing the new {@code hx} value. */ - public void setXStep(double hx) { + public final void setXStep(double hx) { this.hx = hx; } @@ -134,12 +145,13 @@ public void setXStep(double hx) { * * @return a double, representing the {@code tau} value. */ - public double getTimeStep() { + public final double getTimeStep() { return tau; } - protected void setTimeStep(double tau) { + protected final void setTimeStep(double tau) { this.tau = tau; + } /** @@ -150,7 +162,7 @@ protected void setTimeStep(double tau) { * @return a NumericProperty of the {@code TAU_FACTOR} type, representing * the {@code tauFactor} value. */ - public NumericProperty getTimeFactor() { + public final NumericProperty getTimeFactor() { return derive(TAU_FACTOR, tauFactor); } @@ -161,16 +173,17 @@ public NumericProperty getTimeFactor() { * @return a NumericProperty of the {@code GRID_DENSITY} type, representing * the {@code gridDensity} value. */ - public NumericProperty getGridDensity() { + public final NumericProperty getGridDensity() { return derive(GRID_DENSITY, N); } - protected int getGridDensityValue() { + protected final int getGridDensityValue() { return N; } protected void setGridDensityValue(int N) { this.N = N; + hx = 1. / N; } /** @@ -183,7 +196,8 @@ public void setGridDensity(NumericProperty gridDensity) { requireType(gridDensity, GRID_DENSITY); this.N = (int) gridDensity.getValue(); hx = 1. / N; - setTimeFactor(derive(TAU_FACTOR, 1.0)); + setTimeStep(tauFactor * pow(hx, 2)); + firePropertyChanged(this, gridDensity); } /** @@ -195,7 +209,8 @@ public void setGridDensity(NumericProperty gridDensity) { public void setTimeFactor(NumericProperty timeFactor) { requireType(timeFactor, TAU_FACTOR); this.tauFactor = (double) timeFactor.getValue(); - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); + firePropertyChanged(this, timeFactor); } /** @@ -207,7 +222,7 @@ public void setTimeFactor(NumericProperty timeFactor) { * @param dimensionFactor a conversion factor with the dimension of time * @return a double representing the time on the finite grid */ - public double gridTime(double time, double dimensionFactor) { + public final double gridTime(double time, double dimensionFactor) { return rint((time / dimensionFactor) / tau) * tau; } @@ -220,16 +235,21 @@ public double gridTime(double time, double dimensionFactor) { * @param lengthFactor a conversion factor with the dimension of length * @return a double representing the axial distance on the finite grid */ - public double gridAxialDistance(double distance, double lengthFactor) { + public final double gridAxialDistance(double distance, double lengthFactor) { return rint((distance / lengthFactor) / hx) * hx; } @Override public String toString() { var sb = new StringBuilder(); - sb.append(""); - sb.append(getClass().getSimpleName() + ": hx=" + format("%3.2e", hx) + "; "); - sb.append("τ=" + format("%3.2e", tau) + "; "); + sb.append(""). + append(getClass().getSimpleName()) + .append(": hx=") + .append(format("%3.2e", hx)) + .append("; "). + append("τ=") + .append(format("%3.2e", tau)) + .append("; "); return sb.toString(); } diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index e70710aa..7c34840b 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -56,24 +56,25 @@ public void setTimeFactor(NumericProperty timeFactor) { * * @param pulse the discrete puls representation */ - @Override - public void adjustTo(DiscretePulse pulse) { - super.adjustTo(pulse); - if (pulse instanceof DiscretePulse2D) { - adjustTo((DiscretePulse2D) pulse); + + public void adjustStepSize(DiscretePulse pulse) { + var pulse2d = (DiscretePulse2D)pulse; + double pulseSpotSize = pulse2d.getDiscretePulseSpot(); + + if(hy > pulseSpotSize) { + final int INCREMENT = 5; + final int newN = getGridDensityValue() + INCREMENT; + setGridDensityValue(newN); + adjustStepSize(pulse); } + + adjustTimeStep(pulse); } - private void adjustTo(DiscretePulse2D pulse) { - final int GRID_DENSITY_INCREMENT = 5; - - for (final var factor = 1.05; factor * hy > pulse.getDiscretePulseSpot(); pulse.recalculate()) { - int N = getGridDensityValue(); - setGridDensityValue(N + GRID_DENSITY_INCREMENT); - hy = 1. / N; - setXStep(1. / N); - } - + @Override + protected void setGridDensityValue(int N) { + super.setGridDensityValue(N); + hy = 1. / N; } /** @@ -101,13 +102,11 @@ public double gridRadialDistance(double radial, double lengthFactor) { @Override public String toString() { - var sb = new StringBuilder(super.toString()); - sb.append("hy=" + format("%3.3f", hy)); - return sb.toString(); + return super.toString() + "hy=" + format("%3.3f", hy); } public double getYStep() { return hy; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index ae80435f..8d852846 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -61,7 +61,7 @@ public ImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProp } @Override - protected void prepare(Problem problem) { + protected void prepare(Problem problem) throws SolverException { super.prepare(problem); tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); } @@ -77,21 +77,21 @@ protected void prepare(Problem problem) { @Override public void timeStep(final int m) throws SolverException { - leftBoundary(m); + leftBoundary(); final var V = getCurrentSolution(); final int N = V.length - 1; - setSolutionAt(N, evalRightBoundary(m, tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); + setSolutionAt(N, evalRightBoundary(tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); tridiagonal.sweep(V); } - public void leftBoundary(final int m) { - tridiagonal.setBeta(1, firstBeta(m)); + public void leftBoundary() { + tridiagonal.setBeta(1, firstBeta()); tridiagonal.evaluateBeta(getPreviousSolution()); } - public abstract double evalRightBoundary(final int m, final double alphaN, final double betaN); + public abstract double evalRightBoundary(final double alphaN, final double betaN); - public abstract double firstBeta(final int m); + public abstract double firstBeta(); /** * Prints out the description of this problem type. @@ -111,4 +111,4 @@ public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal this.tridiagonal = tridiagonal; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java b/src/main/java/pulse/problem/schemes/LayeredGrid2D.java deleted file mode 100644 index 0cea4c9c..00000000 --- a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java +++ /dev/null @@ -1,85 +0,0 @@ -package pulse.problem.schemes; - -import static pulse.problem.schemes.Partition.Location.CORE_X; -import static pulse.problem.schemes.Partition.Location.CORE_Y; -import static pulse.problem.schemes.Partition.Location.FRONT_Y; -import static pulse.problem.schemes.Partition.Location.REAR_Y; -import static pulse.problem.schemes.Partition.Location.SIDE_X; -import static pulse.problem.schemes.Partition.Location.SIDE_Y; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.SHELL_GRID_DENSITY; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import pulse.problem.schemes.Partition.Location; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; - -public class LayeredGrid2D extends Grid2D { - - private Map h; - - public LayeredGrid2D(Map partitions, NumericProperty timeFactor) { - h = new HashMap<>(partitions); - setGridDensity(derive(CORE_Y.densityKeyword(), partitions.get(CORE_Y).getDensity())); - setTimeFactor(timeFactor); - } - - public Partition getPartition(Location location) { - return h.get(location); - } - - @Override - public Grid2D copy() { - return new LayeredGrid2D(h, getTimeFactor()); - } - - private void setDensity(Location location, NumericProperty density) { - h.get(location).setDensity((int) density.getValue()); - } - - @Override - public void setGridDensity(NumericProperty gridDensity) { - super.setGridDensity(gridDensity); - setDensity(CORE_X, gridDensity); - setDensity(CORE_Y, gridDensity); - } - - public NumericProperty getGridDensity(Location location) { - return derive(location.densityKeyword(), h.get(location).getDensity()); - } - - @Override - public NumericProperty getGridDensity() { - return getGridDensity(CORE_X); - } - - @Override - public Set listedKeywords() { - var set = super.listedKeywords(); - set.add(SHELL_GRID_DENSITY); - return set; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case SHELL_GRID_DENSITY: - setDensity(FRONT_Y, property); - setDensity(REAR_Y, property); - setDensity(SIDE_X, property); - setDensity(SIDE_Y, property); - break; - default: - super.set(type, property); - } - } - -} diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index 54b33954..5483b5a2 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -1,7 +1,6 @@ package pulse.problem.schemes; import pulse.problem.schemes.solvers.SolverException; -import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; public abstract class OneDimensionalScheme extends DifferenceScheme { @@ -16,10 +15,10 @@ protected OneDimensionalScheme() { protected OneDimensionalScheme(NumericProperty timeLimit) { super(timeLimit); } - + + @Override - protected void prepare(Problem problem) { - super.prepare(problem); + public void clearArrays() { final int N = (int) getGrid().getGridDensity().getValue(); U = new double[N + 1]; V = new double[N + 1]; diff --git a/src/main/java/pulse/problem/schemes/Partition.java b/src/main/java/pulse/problem/schemes/Partition.java deleted file mode 100644 index 563fb9f8..00000000 --- a/src/main/java/pulse/problem/schemes/Partition.java +++ /dev/null @@ -1,66 +0,0 @@ -package pulse.problem.schemes; - -import pulse.properties.NumericPropertyKeyword; - -public class Partition { - - private int density; - private double multiplier; - private double shift; - - public Partition(int value, double multiplier, double shift) { - this.setDensity(value); - this.setShift(shift); - this.setGridMultiplier(multiplier); - } - - public double evaluate() { - return multiplier / (density * (1.0 + shift)); - } - - public int getDensity() { - return density; - } - - public void setDensity(int density) { - this.density = density; - } - - public double getGridMultiplier() { - return multiplier; - } - - public void setGridMultiplier(double multiplier) { - this.multiplier = multiplier; - } - - public double getShift() { - return shift; - } - - public void setShift(double shift) { - this.shift = shift; - } - - public enum Location { - - FRONT_Y, REAR_Y, SIDE_Y, SIDE_X, CORE_X, CORE_Y; - - public NumericPropertyKeyword densityKeyword() { - switch (this) { - case FRONT_Y: - case REAR_Y: - case SIDE_Y: - case SIDE_X: - return NumericPropertyKeyword.SHELL_GRID_DENSITY; - case CORE_X: - case CORE_Y: - return NumericPropertyKeyword.GRID_DENSITY; - default: - throw new IllegalArgumentException("Type not recognized: " + this); - } - } - - } - -} diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index 73801b7f..189548a1 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -4,7 +4,10 @@ import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.rte.dom.DiscreteOrdinatesMethod; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -31,19 +34,24 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public void init(ParticipatingMedium problem, Grid grid) { if (rte == null) { + + if (!(problem.getProperties() instanceof ThermoOpticalProperties)) { + throw new IllegalArgumentException("Illegal problem type: " + problem); + } + newRTE(problem, grid); instanceDescriptor.addListener(() -> { newRTE(problem, grid); rte.init(problem, grid); }); - + } else { rte.init(problem, grid); } } - private void newRTE(ParticipatingMedium problem, Grid grid) { + private void newRTE(Problem problem, Grid grid) { rte = instanceDescriptor.newInstance(RadiativeTransferSolver.class, problem, grid); rte.setParent(this); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 7426317d..88c7c658 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -8,20 +8,23 @@ */ public class TridiagonalMatrixAlgorithm { - private Grid grid; + private final double tau; + private final double h; private double a; private double b; private double c; - private double[] alpha; - private double[] beta; + private final int N; + private final double[] alpha; + private final double[] beta; public TridiagonalMatrixAlgorithm(Grid grid) { - this.grid = grid; - final int N = grid.getGridDensityValue(); - alpha = new double[N + 2]; - beta = new double[N + 2]; + tau = grid.getTimeStep(); + N = grid.getGridDensityValue(); + h = grid.getXStep(); + alpha = new double[N + 2]; + beta = new double[N + 2]; } /** @@ -34,7 +37,7 @@ public TridiagonalMatrixAlgorithm(Grid grid) { * from the respective boundary condition */ public void sweep(double[] V) { - for (int j = grid.getGridDensityValue() - 1; j >= 0; j--) { + for (int j = N - 1; j >= 0; j--) { V[j] = alpha[j + 1] * V[j + 1] + beta[j + 1]; } } @@ -44,21 +47,23 @@ public void sweep(double[] V) { * matrix algorithm. */ public void evaluateAlpha() { - for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { + for (int i = 1; i < N; i++) { alpha[i + 1] = c / (b - a * alpha[i]); } } public void evaluateBeta(final double[] U) { - evaluateBeta(U, 2, grid.getGridDensityValue() + 1); + evaluateBeta(U, 2, N + 1); } /** * Calculates the {@code beta} coefficients as part of the tridiagonal * matrix algorithm. + * @param U + * @param start + * @param endExclusive */ public void evaluateBeta(final double[] U, final int start, final int endExclusive) { - final double tau = grid.getTimeStep(); for (int i = start; i < endExclusive; i++) { beta[i] = beta(U[i - 1] / tau, phi(i - 1), i); } @@ -111,9 +116,17 @@ protected double getCoefB() { protected double getCoefC() { return c; } - - public Grid getGrid() { - return grid; + + public final double getTimeStep() { + return tau; + } + + public final int getGridPoints() { + return N; + } + + public final double getGridStep() { + return h; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java index a0e78806..f395d007 100644 --- a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java +++ b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java @@ -1,12 +1,12 @@ package pulse.problem.schemes.rte; -import static pulse.math.MathUtils.fastPowLoop; import org.apache.commons.math3.analysis.UnivariateFunction; - +import static pulse.math.MathUtils.fastPowLoop; import pulse.problem.statements.NonlinearProblem; import pulse.problem.statements.Pulse2D; + /** * Contains methods for calculating the integral spectral characteristics of a * black body with a specific spatial temperature profile. The latter is managed @@ -16,8 +16,8 @@ */ public class BlackbodySpectrum { - private double reductionFactor; private UnivariateFunction interpolation; + private final double reductionFactor; /** * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor @@ -32,6 +32,24 @@ public BlackbodySpectrum(NonlinearProblem p) { reductionFactor = maxHeating / ((double) p.getProperties().getTestTemperature().getValue()); } + @Override + public String toString() { + return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; + } + + /** + * Calculates the emissive power. This is equal to + * 0.25 T0Tm [1 + * +δTm /T0 θ (x) + * ]4, where θ is the reduced temperature. + * @param reducedTemperature the dimensionless reduced temperature + * @return the amount of emissive power + */ + + public double emissivePower(double reducedTemperature) { + return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); + } + /** * Calculates the spectral radiance, which is equal to the spectral power * divided by π, at the given coordinate. @@ -45,10 +63,7 @@ public double radianceAt(double x) { } /** - * Calculates the emissive power at the given coordinate. This is equal to - * 0.25 T0Tm [1 - * +δTm /T0 θ (x) - * ]4, where θ is the reduced temperature. + * Calculates the emissive power at the given coordinate. * * @param x the geometric coordinate inside the sample * @return the local emissive power value @@ -71,17 +86,8 @@ public UnivariateFunction getInterpolation() { return interpolation; } - @Override - public String toString() { - return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; - } - - private double emissivePower(double reducedTemperature) { - return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); - } - - private double radiance(double reducedTemperature) { + public final double radiance(double reducedTemperature) { return emissivePower(reducedTemperature) / Math.PI; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index bb23d099..8013c33c 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -27,7 +27,7 @@ public abstract class RadiativeTransferSolver extends PropertyHolder implements Reflexive, Descriptive { private Fluxes fluxes; - private List rteListeners; + private final List rteListeners; /** * Dummy constructor. @@ -47,17 +47,17 @@ public RadiativeTransferSolver() { /** * Retrieves the parameters from {@code p} and {@code grid} needed to run - * the calculations. Resets the flux arrays. + * the calculations.Resets the flux arrays. * - * @param p the problem statement + * @param p * @param grid the grid */ public void init(ParticipatingMedium p, Grid grid) { if (fluxes != null) { fluxes.setDensity(grid.getGridDensity()); fluxes.init(); - var properties = (ThermoOpticalProperties) p.getProperties(); - fluxes.setOpticalThickness(properties.getOpticalThickness()); + ThermoOpticalProperties top = (ThermoOpticalProperties) p.getProperties(); + fluxes.setOpticalThickness(top.getOpticalThickness()); } } @@ -128,11 +128,11 @@ public void fireStatusUpdate(RTECalculationStatus status) { } } - public Fluxes getFluxes() { + public final Fluxes getFluxes() { return fluxes; } - public void setFluxes(Fluxes fluxes) { + public final void setFluxes(Fluxes fluxes) { this.fluxes = fluxes; } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java new file mode 100644 index 00000000..f2a695e6 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java @@ -0,0 +1,42 @@ +package pulse.problem.schemes.rte.dom; + +import static java.lang.Math.sqrt; + +import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.model.ThermoOpticalProperties; + +/** + * The single-parameter Cornette-Schanks scattering phase function. + * It converges to the Rayleigh phase function as 〈μ〉 → 0 and approaches + * the Henyey–Greenstein phase function as |〈μ〉| → 1 + * @see https://doi.org/10.1364/ao.31.003152 + * + */ +public class CornetteSchanksPF extends PhaseFunction { + + private double anisoFactor; + private double onePlusGSq; + private double g2; + + public CornetteSchanksPF(ThermoOpticalProperties top, Discretisation intensities) { + super(top, intensities); + } + + @Override + public void init(ThermoOpticalProperties top) { + super.init(top); + final double anisotropy = getAnisotropyFactor(); + g2 = 2.0 * anisotropy; + final double aSq = anisotropy * anisotropy; + onePlusGSq = 1.0 + aSq; + anisoFactor = 1.5*(1.0 - aSq)/(2.0 + aSq); + } + + @Override + public double function(final int i, final int k) { + double cosine = cosineTheta(i,k); + final double f = onePlusGSq - g2 * cosine; + return anisoFactor * (1.0 + cosine*cosine) / (f * sqrt(f)); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index 5ad9d0d4..af59449a 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -6,6 +6,7 @@ import pulse.problem.schemes.rte.FluxesAndExplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; @@ -45,7 +46,7 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { var properties = (ThermoOpticalProperties) problem.getProperties(); setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - var discrete = new Discretisation(problem); + var discrete = new Discretisation(properties); integratorDescriptor.setSelectedDescriptor(TRBDF2.class.getSimpleName()); setIntegrator(integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)); @@ -54,8 +55,8 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); - phaseFunctionSelector.addListener(() -> initPhaseFunction(problem, discrete)); - initPhaseFunction(problem, discrete); + phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); + initPhaseFunction(properties, discrete); init(problem, grid); @@ -83,7 +84,8 @@ public RTECalculationStatus compute(double[] tempArray) { } private void fluxesAndDerivatives(final int nExclusive) { - final var interpolation = integrator.getHermiteInterpolator().interpolateOnExternalGrid(nExclusive, integrator); + final var interpolation = integrator.getHermiteInterpolator() + .interpolateOnExternalGrid(nExclusive, integrator); final double DOUBLE_PI = 2.0 * Math.PI; final var discrete = integrator.getDiscretisation(); @@ -92,7 +94,8 @@ private void fluxesAndDerivatives(final int nExclusive) { for (int i = 0; i < nExclusive; i++) { double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); fluxes.setFlux(i, flux); - fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); + fluxes.setFluxDerivative(i, + -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); } } @@ -105,9 +108,11 @@ public String getDescriptor() { @Override public void init(ParticipatingMedium problem, Grid grid) { super.init(problem, grid); - initPhaseFunction(problem, integrator.getDiscretisation()); + var top = (ThermoOpticalProperties)problem.getProperties(); + initPhaseFunction(top, + integrator.getDiscretisation()); integrator.init(problem); - integrator.getPhaseFunction().init(problem); + integrator.getPhaseFunction().init(top); } @Override @@ -119,35 +124,35 @@ public List listedTypes() { return list; } - public AdaptiveIntegrator getIntegrator() { + public final AdaptiveIntegrator getIntegrator() { return integrator; } - public InstanceDescriptor getIntegratorDescriptor() { + public final InstanceDescriptor getIntegratorDescriptor() { return integratorDescriptor; } - public void setIntegrator(AdaptiveIntegrator integrator) { + public final void setIntegrator(AdaptiveIntegrator integrator) { this.integrator = integrator; integrator.setParent(this); firePropertyChanged(this, integratorDescriptor); } - public IterativeSolver getIterativeSolver() { + public final IterativeSolver getIterativeSolver() { return iterativeSolver; } - public InstanceDescriptor getIterativeSolverSelector() { + public final InstanceDescriptor getIterativeSolverSelector() { return iterativeSolverSelector; } - public void setIterativeSolver(IterativeSolver solver) { + public final void setIterativeSolver(IterativeSolver solver) { this.iterativeSolver = solver; solver.setParent(this); firePropertyChanged(this, iterativeSolverSelector); } - public InstanceDescriptor getPhaseFunctionSelector() { + public final InstanceDescriptor getPhaseFunctionSelector() { return phaseFunctionSelector; } @@ -161,10 +166,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { // intentionally left blank } - private void initPhaseFunction(ParticipatingMedium problem, Discretisation discrete) { - var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, problem, discrete); + private void initPhaseFunction(ThermoOpticalProperties top, Discretisation discrete) { + var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, top, discrete); integrator.setPhaseFunction(pf); - pf.init(problem); + pf.init(top); firePropertyChanged(this, phaseFunctionSelector); } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index 548e4885..846dc1bc 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -24,7 +24,7 @@ public DiscreteQuantities(int gridDensity, int ordinates) { init(gridDensity, ordinates); } - public void init(int gridDensity, int ordinates) { + protected final void init(int gridDensity, int ordinates) { I = new double[gridDensity + 1][ordinates]; f = new double[gridDensity + 1][ordinates]; Ik = new double[gridDensity + 1][ordinates]; @@ -32,7 +32,7 @@ public void init(int gridDensity, int ordinates) { qLast = new double[ordinates]; } - public void store() { + protected void store() { final int n = I.length; final int m = I[0].length; @@ -49,47 +49,47 @@ public void store() { } - public double[][] getIntensities() { + public final double[][] getIntensities() { return I; } - public double[][] getDerivatives() { + public final double[][] getDerivatives() { return f; } - public double getQLast(int i) { + protected final double getQLast(int i) { return qLast[i]; } - public void setQLast(int i, double q) { + protected final void setQLast(int i, double q) { this.qLast[i] = q; } - public double getDerivative(int i, int j) { + public final double getDerivative(int i, int j) { return f[i][j]; } - public void setDerivative(int i, int j, double f) { + public final void setDerivative(int i, int j, double f) { this.f[i][j] = f; } - public double getStoredIntensity(final int i, final int j) { + public final double getStoredIntensity(final int i, final int j) { return Ik[i][j]; } - public double getStoredDerivative(final int i, final int j) { + public final double getStoredDerivative(final int i, final int j) { return fk[i][j]; } - public void setStoredDerivative(final int i, final int j, final double f) { + public final void setStoredDerivative(final int i, final int j, final double f) { this.f[i][j] = f; } - public double getIntensity(int i, int j) { + public final double getIntensity(int i, int j) { return I[i][j]; } - public void setIntensity(int i, int j, double value) { + public final void setIntensity(int i, int j, double value) { I[i][j] = value; } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index 20776d3f..932d7d69 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -2,13 +2,10 @@ import static java.lang.Math.PI; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import pulse.io.readers.QuadratureReader; import pulse.problem.schemes.rte.BlackbodySpectrum; -import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -38,9 +35,9 @@ public class Discretisation extends PropertyHolder { * Constructs a {@code DiscreteIntensities} with the default * {@code OrdinateSet} and a new uniform grid. * - * @param problem the problem statement + * @param properties */ - public Discretisation(ParticipatingMedium problem) { + public Discretisation(ThermoOpticalProperties properties) { quadSelector = new DiscreteSelector<>(QuadratureReader.getInstance(), "/quadratures/", "Quadratures.list"); @@ -52,10 +49,9 @@ public Discretisation(ParticipatingMedium problem) { this.firePropertyChanged(this, quadSelector); }); - var properties = (ThermoOpticalProperties) problem.getProperties(); setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); quantities = new DiscreteQuantities(grid.getDensity(), ordinates.getTotalNodes()); - setEmissivity((double) problem.getProperties().getEmissivity().getValue()); + setEmissivity((double) properties.getEmissivity().getValue()); } /** @@ -224,7 +220,7 @@ public void intensitiesRightBoundary(final BlackbodySpectrum ef) { } - protected void setEmissivity(double emissivity) { + protected final void setEmissivity(double emissivity) { this.emissivity = emissivity; boundaryFluxFactor = (1.0 - emissivity) / (emissivity * PI); } @@ -267,7 +263,7 @@ public StretchedGrid getGrid() { return grid; } - public void setGrid(StretchedGrid grid) { + public final void setGrid(StretchedGrid grid) { this.grid = grid; this.grid.setParent(this); } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java index 246338a5..648096ec 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java @@ -115,7 +115,7 @@ public Vector[] step(final int j, final double sign) { for (int l = n1; l < n2; l++) { // find unknown intensities (sum over the outward intensities) /* - * OUTWARD + * OUTWARD */ sum = tableau.getMatrix().get(m, 0) * q[l - n1][0]; for (int k = 1; k < m; k++) { diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java index 03c4477f..f1e0d617 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java @@ -3,6 +3,7 @@ import static java.lang.Math.sqrt; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * The single-parameter Henyey-Greenstein scattering phase function. @@ -14,13 +15,13 @@ public class HenyeyGreensteinPF extends PhaseFunction { private double a2; private double b1; - public HenyeyGreensteinPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); + public HenyeyGreensteinPF(ThermoOpticalProperties properties, Discretisation intensities) { + super(properties, intensities); } @Override - public void init(ParticipatingMedium problem) { - super.init(problem); + public void init(ThermoOpticalProperties properties) { + super.init(properties); final double anisotropy = getAnisotropyFactor(); b1 = 2.0 * anisotropy; final double aSq = anisotropy * anisotropy; @@ -30,9 +31,7 @@ public void init(ParticipatingMedium problem) { @Override public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - final double theta = ordinates.getNode(k) * ordinates.getNode(i); - final double f = a2 - b1 * theta; + final double f = a2 - b1 * cosineTheta(i, k); return a1 / (f * sqrt(f)); } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java index dc277010..7c382c65 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java @@ -1,6 +1,7 @@ package pulse.problem.schemes.rte.dom; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * The linear-anisotropic scattering phase function. @@ -10,13 +11,13 @@ public class LinearAnisotropicPF extends PhaseFunction { private double g; - public LinearAnisotropicPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); + public LinearAnisotropicPF(ThermoOpticalProperties top, Discretisation intensities) { + super(top, intensities); } @Override - public void init(ParticipatingMedium medium) { - super.init(medium); + public void init(ThermoOpticalProperties top) { + super.init(top); g = 3.0 * getAnisotropyFactor(); } @@ -28,8 +29,7 @@ public double partialSum(final int i, final int j, final int n1, final int n2Exc @Override public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - return 1.0 + g * ordinates.getNode(i) * ordinates.getNode(k); + return 1.0 + g * cosineTheta(i,k); } } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java index 4c6680a8..1785a2a7 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -2,7 +2,7 @@ import pulse.problem.schemes.rte.BlackbodySpectrum; import pulse.problem.schemes.rte.RTECalculationStatus; -import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.NonlinearProblem; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -19,11 +19,14 @@ public ODEIntegrator(Discretisation intensities) { public abstract RTECalculationStatus integrate(); - protected void init(ParticipatingMedium problem) { - discretisation.setEmissivity((double) problem.getProperties().getEmissivity().getValue()); - var properties = (ThermoOpticalProperties) problem.getProperties(); + protected void init(NonlinearProblem problem) { + extract((ThermoOpticalProperties) problem.getProperties()); + setEmissionFunction( new BlackbodySpectrum(problem) ); + } + + protected void extract(ThermoOpticalProperties properties) { + discretisation.setEmissivity((double) properties.getEmissivity().getValue()); discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); - setEmissionFunction(new BlackbodySpectrum(problem)); } protected void treatZeroIndex() { @@ -100,11 +103,11 @@ public double emission(double t) { return (1.0 - 2.0 * pf.getHalfAlbedo()) * spectrum.radianceAt(t); } - public PhaseFunction getPhaseFunction() { + public final PhaseFunction getPhaseFunction() { return pf; } - protected void setPhaseFunction(PhaseFunction pf) { + protected final void setPhaseFunction(PhaseFunction pf) { this.pf = pf; } @@ -118,20 +121,20 @@ public String toString() { return getClass().getSimpleName(); } - public Discretisation getDiscretisation() { + public final Discretisation getDiscretisation() { return discretisation; } - public void setDiscretisation(Discretisation discretisation) { + public final void setDiscretisation(Discretisation discretisation) { this.discretisation = discretisation; discretisation.setParent(this); } - public BlackbodySpectrum getEmissionFunction() { + public final BlackbodySpectrum getEmissionFunction() { return spectrum; } - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + public final void setEmissionFunction(BlackbodySpectrum emissionFunction) { this.spectrum = emissionFunction; } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java index f6c922b5..68662f6f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java @@ -59,7 +59,7 @@ public String printOrdinateSet() { } - public boolean hasZeroNode() { + public final boolean hasZeroNode() { return Arrays.stream(mu).anyMatch(Double.valueOf(0.0)::equals); } @@ -75,7 +75,7 @@ public String getName() { return name; } - public void setName(String name) { + public final void setName(String name) { this.name = name; } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index 0892aabe..e8123f8f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -10,9 +10,22 @@ public abstract class PhaseFunction implements Reflexive { private double anisotropy; private double halfAlbedo; - public PhaseFunction(ParticipatingMedium medium, Discretisation intensities) { + public PhaseFunction(ThermoOpticalProperties top, Discretisation intensities) { this.intensities = intensities; - init(medium); + init(top); + } + + /** + * Calculates the cosine of the scattering angle as the product + * of the two discrete cosine nodes. + * @param i + * @param k + * @return + */ + + public final double cosineTheta(int i, int k) { + final var ordinates = getDiscreteIntensities().getOrdinates(); + return ordinates.getNode(k) * ordinates.getNode(i); } public double fullSum(int i, int j) { @@ -55,8 +68,7 @@ protected Discretisation getDiscreteIntensities() { return intensities; } - public void init(ParticipatingMedium problem) { - var properties = (ThermoOpticalProperties) problem.getProperties(); + public void init(ThermoOpticalProperties properties) { this.anisotropy = (double) properties.getScatteringAnisostropy().getValue(); this.halfAlbedo = 0.5 * (double) properties.getScatteringAlbedo().getValue(); } diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java index aaa2c3d1..bbcf0d3a 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -30,8 +30,9 @@ public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid /** * Evaluates fluxes and their derivatives using analytical formulae and the - * selected numerical quadrature. Usually works best with the - * {@code ChandrasekharsQuadrature}. + * selected numerical quadrature.Usually works best with the + {@code ChandrasekharsQuadrature} + * @return */ @Override public RTECalculationStatus compute(double U[]) { diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java index 7e725687..a4583b5b 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -17,7 +17,7 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSolver { - private static FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); + private static final FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); private double emissivity; @@ -27,7 +27,8 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSo private double radiosityFront; private double radiosityRear; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( "Quadrature Selector", CompositionProduct.class); protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { @@ -48,7 +49,8 @@ public void init(ParticipatingMedium p, Grid grid) { /** * The superclass method will update the interpolation that the blackbody * spectrum uses to evaluate the temperature profile and calculate the - * radiosities. A {@code NORMAL} status is always returned. + * radiosities.A {@code NORMAL}status is always returned. + * @param array */ @Override public RTECalculationStatus compute(double[] array) { @@ -180,7 +182,7 @@ public double getRadiosityRear() { */ private void radiosities() { final double doubleReflectivity = 2.0 * (1.0 - emissivity); - ; + final double b = b(doubleReflectivity); final double sq = 1.0 - b * b; final double a1 = a1(doubleReflectivity); diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java deleted file mode 100644 index 1f7a5894..00000000 --- a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java +++ /dev/null @@ -1,89 +0,0 @@ -package pulse.problem.schemes.solvers; - -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericPropertyKeyword.SHELL_GRID_DENSITY; - -import java.util.HashMap; - -import pulse.problem.schemes.ADIScheme; -import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.LayeredGrid2D; -import pulse.problem.schemes.Partition; -import pulse.problem.schemes.Partition.Location; -import pulse.problem.statements.CoreShellProblem; -import pulse.problem.statements.Problem; -import pulse.properties.NumericProperty; - -public class ADILayeredSolver extends ADIScheme implements Solver { - - public ADILayeredSolver() { - super(); - initGrid(getGrid().getGridDensity(), def(SHELL_GRID_DENSITY), getGrid().getTimeFactor()); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - initGrid(nCore, nShell, timeFactor); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor, - NumericProperty timeLimit) { - setTimeLimit(timeLimit); - initGrid(nCore, nShell, timeFactor); - } - - public void initGrid(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - var map = new HashMap(); - map.put(Location.CORE_X, new Partition((int) nCore.getValue(), 1.0, 0.5)); - map.put(Location.CORE_Y, new Partition((int) nCore.getValue(), 1.0, 0.0)); - map.put(Location.FRONT_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.REAR_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_X, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - setGrid(new LayeredGrid2D(map, timeFactor)); - getGrid().setTimeFactor(timeFactor); - } - - private void prepareGrid(CoreShellProblem problem) { - var layeredGrid = (LayeredGrid2D) getGrid(); // TODO - layeredGrid.getPartition(Location.FRONT_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.REAR_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.SIDE_X).setGridMultiplier(problem.radialFactor()); - } - - @Override - public void solve(CoreShellProblem problem) { - prepareGrid(problem); - - // TODO - } - - @Override - public DifferenceScheme copy() { - // TODO Auto-generated method stub - return null; - } - - @Override - public Class domain() { - return CoreShellProblem.class; - } - - @Override - public double signal() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void timeStep(int m) { - // TODO Auto-generated method stub - - } - - @Override - public void finaliseStep() { - // TODO Auto-generated method stub - - } - -} diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index 7ce79f98..d2472df5 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -77,15 +77,28 @@ public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { super(N, timeFactor, timeLimit); } + + @Override + public void clearArrays() { + N = (int) getGrid().getGridDensity().getValue(); + U1 = new double[N + 1][N + 1]; + U2 = new double[N + 1][N + 1]; + + U1_E = new double[N + 3][N + 3]; + U2_E = new double[N + 3][N + 3]; + + a1 = new double[N + 1]; + b1 = new double[N + 1]; + c1 = new double[N + 1]; + } - private void prepare(ClassicalProblem2D problem) { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); tridiagonal = new TridiagonalMatrixAlgorithm(grid); - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); hy = ((Grid2D) getGrid()).getYStep(); HX2 = hx * hx; @@ -104,15 +117,6 @@ private void prepare(ClassicalProblem2D problem) { l = (double) properties.getSampleThickness().getValue(); // end - U1 = new double[N + 1][N + 1]; - U2 = new double[N + 1][N + 1]; - - U1_E = new double[N + 3][N + 3]; - U2_E = new double[N + 3][N + 3]; - - a1 = new double[N + 1]; - b1 = new double[N + 1]; - c1 = new double[N + 1]; // a[i]*u[i-1] - b[i]*u[i] + c[i]*u[i+1] = F[i] lastIndex = (int) (fovOuter / d / hx); @@ -174,8 +178,8 @@ public DifferenceScheme copy() { } @Override - public Class domain() { - return ClassicalProblem2D.class; + public Class[] domain() { + return new Class[]{ClassicalProblem2D.class}; } @Override @@ -197,7 +201,6 @@ private void extendedU1(final int m) { for (int i = 0; i <= N; i++) { System.arraycopy(U1[i], 0, U1_E[i + 1], 1, N + 1); - U1_E[i + 1][0] = U1[i][1] + 2.0 * hy * pulse(m, i) - E_C_U1 * U1[i][0]; U1_E[i + 1][N + 2] = U1[i][N - 1] - E_C_U1 * U1[i][N]; } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 40927d79..9da5e215 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -1,62 +1,57 @@ package pulse.problem.schemes.solvers; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import static pulse.ui.Messages.getString; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ExplicitScheme; -import pulse.problem.schemes.FixedPointIterations; import pulse.problem.schemes.RadiativeTransferCoupling; import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; -import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public class ExplicitCoupledSolver extends ExplicitScheme implements Solver, FixedPointIterations { +public abstract class ExplicitCoupledSolver extends ExplicitScheme + implements Solver { private RadiativeTransferCoupling coupling; - private RadiativeTransferSolver rte; private RTECalculationStatus status; private Fluxes fluxes; private double hx; private double a; - private double nonlinearPrecision; - private double pls; private int N; private double HX_NP; private double prefactor; + private double zeta; + private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers + public ExplicitCoupledSolver() { this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); } public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { super(N, timeFactor); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setCoupling(new RadiativeTransferCoupling()); status = RTECalculationStatus.NORMAL; } - private void prepare(ParticipatingMedium problem) throws SolverException { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); + coupling.init((ParticipatingMedium)problem, grid); fluxes = coupling.getRadiativeTransferEquation().getFluxes(); setCalculationStatus(fluxes.checkArrays()); - + N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); @@ -71,46 +66,52 @@ private void prepare(ParticipatingMedium problem) throws SolverException { HX_NP = hx / Np; prefactor = tau * opticalThickness / Np; + + zeta = (double) ((ParticipatingMedium)problem).getGeometricFactor().getValue(); } @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - setCalculationStatus(coupling.getRadiativeTransferEquation().compute(getPreviousSolution())); - runTimeSequence(problem); + public void timeStep(int m) throws SolverException { + explicitSolution(); } @Override - public boolean normalOperation() { - return super.normalOperation() && (status == RTECalculationStatus.NORMAL); + public void finaliseStep() throws SolverException { + super.finaliseStep(); + if (autoUpdateFluxes) { + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(getCurrentSolution())); + } + coupling.getRadiativeTransferEquation().getFluxes().store(); } @Override - public void timeStep(int m) throws SolverException { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); + public void solve(ParticipatingMedium problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); } @Override - public void iteration(final int m) { + public void explicitSolution() { /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries */ - explicitSolution(); + super.explicitSolution(); var V = getCurrentSolution(); + double pls = getCurrentPulseValue(); + // Front face - V[0] = (V[1] + hx * pls - HX_NP * fluxes.getFlux(0)) * a; + V[0] = (V[1] + hx * zeta * pls - HX_NP * fluxes.getFlux(0)) * a; // Rear face - V[N] = (V[N - 1] + HX_NP * fluxes.getFlux(N)) * a; + V[N] = (V[N - 1] + hx * (1.0 - zeta) * pls + HX_NP * fluxes.getFlux(N)) * a; } @Override - public void finaliseIteration(double[] V) throws SolverException { - FixedPointIterations.super.finaliseIteration(V); - setCalculationStatus(rte.compute(V)); + public boolean normalOperation() { + return super.normalOperation() && (status == RTECalculationStatus.NORMAL); } @Override @@ -118,12 +119,6 @@ public double phi(final int i) { return prefactor * fluxes.fluxDerivative(i); } - @Override - public void finaliseStep() throws SolverException { - super.finaliseStep(); - coupling.getRadiativeTransferEquation().getFluxes().store(); - } - public RadiativeTransferCoupling getCoupling() { return coupling; } @@ -142,22 +137,25 @@ public final void setCoupling(RadiativeTransferCoupling coupling) { public String toString() { return getString("ExplicitScheme.4"); } - + @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor()); + public Class[] domain() { + return new Class[]{ParticipatingMedium.class}; } - @Override - public Class domain() { - return ParticipatingMedium.class; - } - public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; - if(status != RTECalculationStatus.NORMAL) + if (status != RTECalculationStatus.NORMAL) { throw new SolverException(status.toString()); + } + } + + public final boolean isAutoUpdateFluxes() { + return this.autoUpdateFluxes; + } + + public final void setAutoUpdateFluxes(boolean auto) { + this.autoUpdateFluxes = auto; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java new file mode 100644 index 00000000..07247ef4 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class ExplicitCoupledSolverNL extends ExplicitCoupledSolver + implements FixedPointIterations +{ + + private double nonlinearPrecision; + + public ExplicitCoupledSolverNL() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + public ExplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + @Override + public void timeStep(int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) { + explicitSolution(); + } + + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + setCalculationStatus(this.getCoupling().getRadiativeTransferEquation().compute(V)); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor()); + } + + public final NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + super.set(type, property); + } + } + + @Override + public String toString() { + return Messages.getString("ExplicitScheme.5"); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index 5d3a6f9f..7b7f0aeb 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -64,7 +64,7 @@ public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, N } @Override - public void prepare(Problem problem) { + public void prepare(Problem problem) throws SolverException { super.prepare(problem); zeta = (double) ( (ClassicalProblem) problem).getGeometricFactor().getValue(); @@ -86,7 +86,7 @@ public void solve(ClassicalProblem problem) throws SolverException { public void timeStep(int m) { explicitSolution(); var V = getCurrentSolution(); - double pulse = pulse(m); + double pulse = getCurrentPulseValue(); setSolutionAt(0, (V[1] + hx * zeta * pulse) * a); setSolutionAt(N, (V[N - 1] + hx * (1.0 - zeta) * pulse) * a); } @@ -98,8 +98,8 @@ public DifferenceScheme copy() { } @Override - public Class domain() { - return ClassicalProblem.class; + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java index 18dcb958..ed8e7fa3 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java @@ -21,7 +21,6 @@ public class ExplicitNonlinearSolver extends ExplicitScheme implements Solver domain() { - return NonlinearProblem.class; + public Class[] domain() { + return new Class[]{NonlinearProblem.class}; } public NumericProperty getNonlinearPrecision() { diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index d22a20da..56e747c8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -18,10 +18,6 @@ public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver< private double tau; private double a; - private double pls; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - private AbsorptionModel model; public ExplicitTranslucentSolver() { @@ -32,11 +28,12 @@ public ExplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, super(N, timeFactor, timeLimit); } - private void prepare(PenetrationProblem problem) { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); - model = problem.getAbsorptionModel(); + model = ((PenetrationProblem)problem).getAbsorptionModel(); N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); @@ -54,8 +51,6 @@ public void solve(PenetrationProblem problem) throws SolverException { @Override public void timeStep(final int m) { - pls = this.pulse(m); - /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries @@ -73,7 +68,7 @@ public void timeStep(final int m) { @Override public double phi(final int i) { - return tau * pls * model.absorption(LASER, (i - EPS) * hx); + return tau * getCurrentPulseValue() * model.absorption(LASER, i * hx); } @Override @@ -93,8 +88,8 @@ public String toString() { } @Override - public Class domain() { - return PenetrationProblem.class; + public Class[] domain() { + return new Class[]{PenetrationProblem.class}; } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 5bd30ccc..13016048 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -6,16 +6,18 @@ import static pulse.ui.Messages.getString; import pulse.problem.schemes.CoupledImplicitScheme; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { +public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme + implements Solver { private RadiativeTransferSolver rte; private Fluxes fluxes; @@ -30,6 +32,7 @@ public class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solv private double HX_NP; private double v1; + private double zeta; public ImplicitCoupledSolver() { super(derive(GRID_DENSITY, 20), derive(TAU_FACTOR, 0.66667)); @@ -39,13 +42,14 @@ public ImplicitCoupledSolver(NumericProperty gridDensity, NumericProperty timeFa super(gridDensity, timeFactor, timeLimit); } - private void prepare(ParticipatingMedium problem) throws SolverException { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); final var grid = getGrid(); var coupling = getCoupling(); - coupling.init(problem, grid); + coupling.init( (ParticipatingMedium) problem, grid); rte = coupling.getRadiativeTransferEquation(); N = (int) getGrid().getGridDensity().getValue(); @@ -85,7 +89,8 @@ public double phi(int i) { tridiagonal.evaluateAlpha(); setTridiagonalMatrixAlgorithm(tridiagonal); - + + zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); } @Override @@ -108,30 +113,25 @@ public String toString() { } @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + public double evalRightBoundary(final double alphaN, final double betaN) { /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_2NP * (fluxes.getFlux(N - 1) + * fluxes.getFlux(N))) / (v1 - alphaN); */ - return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_NP * fluxes.getFlux(N) + return (betaN + HX2_2TAU * getPreviousSolution()[N] + hx * (1.0 - zeta) * getCurrentPulseValue() + + HX_NP * fluxes.getFlux(N) + HX2TAU0_2NP * fluxes.fluxDerivativeRear()) / (v1 - alphaN); } @Override - public double firstBeta(final int m) { + public double firstBeta() { /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() - HX_2NP * * (fluxes.getFlux(0) + fluxes.getFlux(1))) * alpha1; */ - return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() + return (HX2_2TAU * getPreviousSolution()[0] + hx * zeta * getCurrentPulseValue() + HX2TAU0_2NP * fluxes.fluxDerivativeFront() - HX_NP * fluxes.getFlux(0)) * alpha1; } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java new file mode 100644 index 00000000..42c9c032 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java @@ -0,0 +1,94 @@ +/* + * Copyright 2022 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class ImplicitCoupledSolverNL extends ImplicitCoupledSolver implements FixedPointIterations { + + private double nonlinearPrecision; + + public ImplicitCoupledSolverNL() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + public ImplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) throws SolverException { + super.timeStep(m); + } + + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(V)); + } + + public final NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + super.set(type, property); + } + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public String toString() { + return Messages.getString("ImplicitScheme.5"); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 3e628488..3d36e222 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -3,6 +3,7 @@ import pulse.problem.schemes.BlockMatrixAlgorithm; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.DiathermicMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.DiathermicProperties; @@ -16,8 +17,8 @@ public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver domain() { - return DiathermicMedium.class; + public Class[] domain() { + return new Class[]{DiathermicMedium.class}; } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 6b3b0f63..5020d2c2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -68,7 +68,7 @@ public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, N } @Override - public void prepare(Problem problem) { + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -76,6 +76,7 @@ public void prepare(Problem problem) { N = (int) grid.getGridDensity().getValue(); final double hx = grid.getXStep(); tau = grid.getTimeStep(); + zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); @@ -105,16 +106,14 @@ public void solve(ClassicalProblem problem) throws SolverException { } @Override - public double firstBeta(final int m) { - final double pls = super.pulse(m); - return (HH * getPreviousSolution()[0] + _2HTAU * pls * zeta) / (2. * Bi1HTAU + 2. * tau + HH); + public double firstBeta() { + return (HH * getPreviousSolution()[0] + _2HTAU * getCurrentPulseValue() * zeta) / (2. * Bi1HTAU + 2. * tau + HH); } @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double pls = super.pulse(m); + public double evalRightBoundary(final double alphaN, final double betaN) { return (HH * getPreviousSolution()[N] + 2. * tau * betaN - + _2HTAU * (1.0 - zeta) * pls //additional term due to stray light + + _2HTAU * (1.0 - zeta) * getCurrentPulseValue() //additional term due to stray light ) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); } @@ -125,8 +124,8 @@ public DifferenceScheme copy() { } @Override - public Class domain() { - return ClassicalProblem.class; + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java index f61fa057..0cc048ac 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java @@ -3,9 +3,6 @@ import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; - -import java.util.List; import java.util.Set; import pulse.problem.schemes.DifferenceScheme; @@ -17,14 +14,12 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; public class ImplicitNonlinearSolver extends ImplicitScheme implements Solver, FixedPointIterations { private int N; private double HH; private double tau; - private double pls; private double dT_T; @@ -51,7 +46,8 @@ public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, Nu nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); } - private void prepare(NonlinearProblem problem) { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -101,8 +97,8 @@ public DifferenceScheme copy() { } @Override - public Class domain() { - return NonlinearProblem.class; + public Class[] domain() { + return new Class[]{NonlinearProblem.class}; } public NumericProperty getNonlinearPrecision() { @@ -133,7 +129,6 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public void timeStep(final int m) throws SolverException { - pls = pulse(m); doIterations(getCurrentSolution(), nonlinearPrecision, m); } @@ -143,14 +138,15 @@ public void iteration(int m) throws SolverException { } @Override - public double evalRightBoundary(int m, double alphaN, double betaN) { + public double evalRightBoundary(double alphaN, double betaN) { return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); } @Override - public double firstBeta(int m) { - return b1 * getPreviousSolution()[0] + b2 * (pls - b3 * (fastPowLoop(getCurrentSolution()[0] * dT_T + 1, 4) - 1)); + public double firstBeta() { + return b1 * getPreviousSolution()[0] + b2 * (getCurrentPulseValue() + - b3 * (fastPowLoop(getCurrentSolution()[0] * dT_T + 1, 4) - 1)); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index 5a769b71..d5005bae 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -5,29 +5,22 @@ import static pulse.ui.Messages.getString; import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.Grid; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; import pulse.problem.statements.model.AbsorptionModel; -import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.NumericProperty; public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { private AbsorptionModel absorption; - private Grid grid; - private double pls; private int N; private double HH; private double _2Bi1HTAU; private double b11; - private double frontAbsorption; - private double rearAbsorption; - public ImplicitTranslucentSolver() { super(); } @@ -36,31 +29,28 @@ public ImplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, super(N, timeFactor, timeLimit); } - private void prepare(PenetrationProblem problem) { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); - grid = getGrid(); + var grid = getGrid(); final double tau = grid.getTimeStep(); N = (int) grid.getGridDensity().getValue(); final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); final double hx = grid.getXStep(); - absorption = problem.getAbsorptionModel(); + absorption = ((PenetrationProblem)problem).getAbsorptionModel(); HH = hx * hx; _2Bi1HTAU = 2.0 * Bi1H * tau; b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - - final double EPS = 1E-7; - rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); - frontAbsorption = tau * absorption.absorption(LASER, 0.0) + 2.0*tau/hx; - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override public double phi(final int i) { - return pls * absorption.absorption(LASER, (i - EPS) * hx); + return getCurrentPulseValue() * absorption.absorption(LASER, i * hx); } }; @@ -81,28 +71,25 @@ public void solve(PenetrationProblem problem) throws SolverException { runTimeSequence(problem); } - @Override - public void timeStep(final int m) throws SolverException { - pls = pulse(m); - super.timeStep(m); - } - @Override public double signal() { - return evaluateSignal(absorption, grid, getCurrentSolution()); + return evaluateSignal(absorption, getGrid(), getCurrentSolution()); } @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double tau = grid.getTimeStep(); + public double evalRightBoundary(final double alphaN, final double betaN) { + final double tau = getGrid().getTimeStep(); + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); - return (HH * (getPreviousSolution()[N] + pls * rearAbsorption) + return (HH * getPreviousSolution()[N] + HH*tau*tridiagonal.phi(N) + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); } @Override - public double firstBeta(int m) { - return (getPreviousSolution()[0] + pls * frontAbsorption) * b11; + public double firstBeta() { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + double tau = getGrid().getTimeStep(); + return (getPreviousSolution()[0] + tau*tridiagonal.phi(0))* b11; } @Override @@ -122,8 +109,8 @@ public String toString() { } @Override - public Class domain() { - return PenetrationProblem.class; + public Class[] domain() { + return new Class[]{PenetrationProblem.class}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index 463ee020..f5393dac 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -10,16 +10,17 @@ import java.util.Set; import pulse.problem.schemes.CoupledImplicitScheme; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.ui.Messages; -public class MixedCoupledSolver extends CoupledImplicitScheme implements Solver { +public abstract class MixedCoupledSolver extends CoupledImplicitScheme + implements Solver { private RadiativeTransferSolver rte; @@ -47,6 +48,7 @@ public class MixedCoupledSolver extends CoupledImplicitScheme implements Solver< private double _2TAU_ONE_MINUS_SIGMA; private double BETA1_FACTOR; private double ONE_MINUS_SIGMA; + private double zeta; public MixedCoupledSolver() { super(derive(GRID_DENSITY, 16), derive(TAU_FACTOR, 0.25)); @@ -58,13 +60,14 @@ public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, Numeric sigma = (double) def(SCHEME_WEIGHT).getValue(); } - private void prepare(ParticipatingMedium problem) throws SolverException { + @Override + public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); var coupling = getCoupling(); - coupling.init(problem, grid); + coupling.init((ParticipatingMedium) problem, grid); rte = coupling.getRadiativeTransferEquation(); N = (int) grid.getGridDensity().getValue(); @@ -72,7 +75,9 @@ private void prepare(ParticipatingMedium problem) throws SolverException { tau = grid.getTimeStep(); Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - + + zeta = (double) ( (ClassicalProblem)problem ).getGeometricFactor().getValue(); + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -107,7 +112,7 @@ public void evaluateBeta(final double[] U) { setTridiagonalMatrixAlgorithm(tridiagonal); } - private void initConst(ParticipatingMedium problem) { + private void initConst(ClassicalProblem problem) { var p = (ThermoOpticalProperties) problem.getProperties(); final double Np = (double) p.getPlanckNumber().getValue(); final double opticalThickness = (double) p.getOpticalThickness().getValue(); @@ -155,21 +160,23 @@ public double pulse(final int m) { } @Override - public double firstBeta(final int m) { + public double firstBeta() { var fluxes = rte.getFluxes(); var U = getPreviousSolution(); final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); return (_2TAUHX - * (getCurrentPulseValue() - SIGMA_NP * fluxes.getFlux(0) - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) - + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; + * (getCurrentPulseValue() * zeta - SIGMA_NP * fluxes.getFlux(0) + - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) + + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * + (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; } @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + public double evalRightBoundary(final double alphaN, final double betaN) { var fluxes = rte.getFluxes(); final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); final var U = getPreviousSolution(); - return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi + return (sigma * betaN + hx * getCurrentPulseValue() * (1.0 - zeta) + HX2_2TAU * U[N] + 0.5 * HX2 * phi + ONE_MINUS_SIGMA * (U[N - 1] - U[N] * ONE_PLUS_Bi1_HX) + HX_NP * (sigma * fluxes.getFlux(N) + ONE_MINUS_SIGMA * fluxes.getStoredFlux(N))) / (HX2_2TAU + sigma * (ONE_PLUS_Bi1_HX - alphaN)); @@ -205,15 +212,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } - @Override - public String toString() { - return Messages.getString("MixedScheme2.4"); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java new file mode 100644 index 00000000..77d7b192 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java @@ -0,0 +1,92 @@ +/* + * Copyright 2022 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class MixedCoupledSolverNL extends MixedCoupledSolver implements FixedPointIterations { + + private double nonlinearPrecision; + + public MixedCoupledSolverNL() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + public MixedCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) throws SolverException { + super.timeStep(m); + } + + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(V)); + } + + public final NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + super.set(type, property); + } + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new MixedCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public String toString() { + return Messages.getString("MixedScheme2.5"); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 979d50f7..56a74f0a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -55,6 +55,8 @@ public class MixedLinearisedSolver extends MixedScheme implements Solver domain() { - return ClassicalProblem.class; + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; } } diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index d85462b8..2a9ed2d8 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -8,6 +8,8 @@ import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.InvDiamTransform; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; import pulse.problem.laser.DiscretePulse; import pulse.problem.laser.DiscretePulse2D; import pulse.problem.schemes.ADIScheme; @@ -18,7 +20,6 @@ import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; @@ -67,6 +68,7 @@ public DiscretePulse discretePulseOn(Grid grid) { return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); } + @Override public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); @@ -76,7 +78,9 @@ public void optimisationVector(ParameterVector output, List flags) { for (int i = 0, size = output.dimension(); i < size; i++) { var key = output.getIndex(i); - + Transformable transform = new InvDiamTransform(properties); + var bounds = Segment.boundsFrom(key); + switch (key) { case FOV_OUTER: value = (double) properties.getFOVOuter().getValue(); @@ -88,20 +92,20 @@ public void optimisationVector(ParameterVector output, List flags) { value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); break; case HEAT_LOSS_SIDE: - final double Bi = (double) properties.getSideLosses().getValue(); - setHeatLossParameter(output, i, Bi); - continue; + value = (double) properties.getSideLosses().getValue(); + transform = new StickTransform(bounds); + break; case HEAT_LOSS_COMBINED: - final double combined = (double) properties.getHeatLoss().getValue(); - setHeatLossParameter(output, i, combined); - continue; + value = (double) properties.getHeatLoss().getValue(); + transform = new StickTransform(bounds); + break; default: continue; } - output.setTransform(i, new InvDiamTransform(properties)); + output.setTransform(i, transform); + output.setParameterBounds(i, bounds); output.set(i, value); - output.setParameterBounds(i, new Segment(0.5 * value, 1.5 * value)); } @@ -141,4 +145,4 @@ public Problem copy() { return new ClassicalProblem2D(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java deleted file mode 100644 index d575a39e..00000000 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ /dev/null @@ -1,164 +0,0 @@ -package pulse.problem.statements; - -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.AXIAL_COATING_THICKNESS; -import static pulse.properties.NumericPropertyKeyword.COATING_DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.RADIAL_COATING_THICKNESS; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.math.transforms.InvDiamTransform; -import pulse.math.transforms.InvLenSqTransform; -import pulse.math.transforms.InvLenTransform; -import pulse.problem.schemes.solvers.SolverException; -import pulse.problem.statements.model.ExtendedThermalProperties; -import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; -import pulse.ui.Messages; - -public class CoreShellProblem extends ClassicalProblem2D { - - private double tA; - private double tR; - private double coatingDiffusivity; - private final static boolean DEBUG = true; - - public CoreShellProblem() { - super(); - tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); - tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); - coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - @Override - public String toString() { - return Messages.getString("UniformlyCoatedSample.Descriptor"); - } - - public NumericProperty getCoatingAxialThickness() { - return derive(AXIAL_COATING_THICKNESS, tA); - } - - public NumericProperty getCoatingRadialThickness() { - return derive(RADIAL_COATING_THICKNESS, tR); - } - - public double axialFactor() { - return tA / (double) getProperties().getSampleThickness().getValue(); - } - - public double radialFactor() { - return tR / (double) getProperties().getSampleThickness().getValue(); - } - - public void setCoatingAxialThickness(NumericProperty t) { - this.tA = (double) t.getValue(); - } - - public void setCoatingRadialThickness(NumericProperty t) { - this.tR = (double) t.getValue(); - } - - public NumericProperty getCoatingDiffusivity() { - return derive(COATING_DIFFUSIVITY, coatingDiffusivity); - } - - public void setCoatingDiffusivity(NumericProperty a) { - this.coatingDiffusivity = (double) a.getValue(); - } - - @Override - public Set listedKeywords() { - var set = super.listedKeywords(); - set.add(AXIAL_COATING_THICKNESS); - set.add(RADIAL_COATING_THICKNESS); - set.add(COATING_DIFFUSIVITY); - return set; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case COATING_DIFFUSIVITY: - setCoatingDiffusivity(property); - break; - case AXIAL_COATING_THICKNESS: - setCoatingAxialThickness(property); - break; - case RADIAL_COATING_THICKNESS: - setCoatingRadialThickness(property); - break; - default: - super.set(type, property); - break; - } - } - - @Override - public boolean isEnabled() { - return !DEBUG; - } - - @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - var bounds = new Segment(0.1, 1.0); - var properties = (ExtendedThermalProperties) this.getProperties(); - - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); - switch (key) { - case AXIAL_COATING_THICKNESS: - output.setTransform(i, new InvLenTransform(properties)); - output.set(i, tA); - output.setParameterBounds(i, bounds); - break; - case RADIAL_COATING_THICKNESS: - output.setTransform(i, new InvDiamTransform(properties)); - output.set(i, tR); - output.setParameterBounds(i, bounds); - break; - case COATING_DIFFUSIVITY: - output.setTransform(i, new InvLenSqTransform(properties)); - output.set(i, coatingDiffusivity); - output.setParameterBounds(i, new Segment(0.5 * coatingDiffusivity, 1.5 * coatingDiffusivity)); - break; - default: - continue; - } - } - - } - - @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case AXIAL_COATING_THICKNESS: - tA = params.inverseTransform(i); - break; - case RADIAL_COATING_THICKNESS: - tR = params.inverseTransform(i); - break; - case COATING_DIFFUSIVITY: - coatingDiffusivity = params.inverseTransform(i); - break; - default: - continue; - } - } - } - -} diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index b2aca281..0e8fcada 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -2,7 +2,6 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.List; @@ -64,7 +63,7 @@ public void optimisationVector(ParameterVector output, List flags) { if (key == DIATHERMIC_COEFFICIENT) { - var bounds = new Segment(0.0, 1.0); + var bounds = Segment.boundsFrom(DIATHERMIC_COEFFICIENT); final double etta = (double) properties.getDiathermicCoefficient().getValue(); output.setTransform(i, new StickTransform(bounds)); @@ -91,14 +90,6 @@ public void assign(ParameterVector params) throws SolverException { case DIATHERMIC_COEFFICIENT: properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); break; - case HEAT_LOSS: - if (properties.areThermalPropertiesLoaded()) { - properties.calculateEmissivity(); - final double emissivity = (double) properties.getEmissivity().getValue(); - properties - .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); - } - break; default: continue; @@ -123,4 +114,4 @@ public DiathermicMedium copy() { return new DiathermicMedium(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 5097ab31..28b2857e 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -21,6 +21,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class NonlinearProblem extends ClassicalProblem { @@ -54,6 +55,7 @@ public Set listedKeywords() { set.add(SPECIFIC_HEAT); set.add(DENSITY); set.remove(SPOT_DIAMETER); + set.remove(SOURCE_GEOMETRIC_FACTOR); return set; } diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 5674dcc1..ad0d177b 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,21 +1,18 @@ package pulse.problem.statements; -import static pulse.properties.NumericProperties.derive; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.math.transforms.StickTransform; -import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; -import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; -import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { @@ -39,72 +36,14 @@ public String toString() { public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (ThermoOpticalProperties) getProperties(); - - Segment bounds = null; - double value; - Transformable transform; - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case PLANCK_NUMBER: - final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); - bounds = new Segment(lowerBound, properties.maxNp()); - value = (double) properties.getPlanckNumber().getValue(); - break; - case OPTICAL_THICKNESS: - value = (double) properties.getOpticalThickness().getValue(); - bounds = Segment.boundsFrom(OPTICAL_THICKNESS); - break; - case SCATTERING_ALBEDO: - value = (double) properties.getScatteringAlbedo().getValue(); - bounds = new Segment(0.0, 1.0); - break; - case SCATTERING_ANISOTROPY: - value = (double) properties.getScatteringAnisostropy().getValue(); - bounds = new Segment(-1.0, 1.0); - break; - default: - continue; - - } - - transform = new StickTransform(bounds); - output.setTransform(i, transform); - output.set(i, value); - output.setParameterBounds(i, bounds); - - } - + properties.optimisationVector(output, flags); } @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - var properties = (ThermoOpticalProperties) getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - var type = params.getIndex(i); - - switch (type) { - - case PLANCK_NUMBER: - case SCATTERING_ALBEDO: - case SCATTERING_ANISOTROPY: - case OPTICAL_THICKNESS: - properties.set(type, derive(type, params.inverseTransform(i))); - break; - default: - break; - - } - - } - + properties.assign(params); } @Override @@ -122,9 +61,16 @@ public void initProperties() { setProperties(new ThermoOpticalProperties()); } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SOURCE_GEOMETRIC_FACTOR); + return set; + } + @Override public Problem copy() { return new ParticipatingMedium(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index 46a2b844..b93e68ff 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -1,6 +1,7 @@ package pulse.problem.statements; import java.util.List; +import java.util.Set; import pulse.math.ParameterVector; import pulse.problem.schemes.DifferenceScheme; @@ -9,6 +10,8 @@ import pulse.problem.statements.model.AbsorptionModel; import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.Flag; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.properties.Property; import pulse.ui.Messages; import pulse.util.InstanceDescriptor; @@ -16,9 +19,8 @@ public class PenetrationProblem extends ClassicalProblem { private InstanceDescriptor instanceDescriptor - = new InstanceDescriptor( + = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); public PenetrationProblem() { @@ -55,6 +57,13 @@ public List listedTypes() { list.add(instanceDescriptor); return list; } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.remove(SOURCE_GEOMETRIC_FACTOR); + return set; + } public InstanceDescriptor getAbsorptionSelector() { return instanceDescriptor; diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index d0e8adc3..d2bde96c 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,5 +1,6 @@ package pulse.problem.statements; +import java.util.Arrays; import static pulse.input.listeners.CurveEventType.RESCALED; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; @@ -15,8 +16,6 @@ import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.InvLenSqTransform; -import pulse.math.transforms.StandardTransformations; -import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; @@ -58,7 +57,8 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim private static boolean hideDetailedAdjustment = true; private ProblemComplexity complexity = ProblemComplexity.LOW; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( "Baseline Selector", Baseline.class); /** @@ -73,7 +73,6 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim */ protected Problem() { initProperties(); - setHeatingCurve(new HeatingCurve()); instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); @@ -100,7 +99,7 @@ public Problem(Problem p) { public abstract Problem copy(); - public void setHeatingCurve(HeatingCurve curve) { + public final void setHeatingCurve(HeatingCurve curve) { this.curve = curve; curve.setParent(this); } @@ -112,10 +111,7 @@ private void addListeners() { }); curve.addHeatingCurveListener(e -> { if (e.getType() == RESCALED) { - var c = e.getData(); - if (!c.isIncomplete()) { - curve.apply(getBaseline()); - } + curve.apply(getBaseline()); } }); } @@ -134,9 +130,10 @@ private void addListeners() { * @return a {@code List} of available {@code DifferenceScheme}s for solving * this {@code Problem}. */ - public List availableSolutions() { + public final List availableSolutions() { var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); - return allSchemes.stream().filter(scheme -> scheme instanceof Solver).filter(s -> s.domain() == this.getClass()) + return allSchemes.stream().filter(scheme -> scheme instanceof Solver) + .filter(s -> Arrays.asList(s.domain()).contains(this.getClass()) ) .collect(Collectors.toList()); } @@ -152,7 +149,7 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { properties.set(type, value); } - public HeatingCurve getHeatingCurve() { + public final HeatingCurve getHeatingCurve() { return curve; } @@ -228,48 +225,47 @@ public void optimisationVector(ParameterVector output, List flags) { var key = output.getIndex(i); + Segment bounds = Segment.boundsFrom(key); + double value = 0; + switch (key) { case THICKNESS: - final double l = (double) properties.getSampleThickness().getValue(); - var bounds = Segment.boundsFrom(THICKNESS); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, l); + value = (double) properties.getSampleThickness().getValue(); break; case DIFFUSIVITY: final double a = (double) properties.getDiffusivity().getValue(); output.setTransform(i, new InvLenSqTransform(properties)); - output.setParameterBounds(i, new Segment(0.33 * a, 3.0 * a)); + bounds = new Segment(0.01 * a, 20.0 * a); + output.setParameterBounds(i, bounds); output.set(i, a); - break; + //custom transform here -- skip assigning StickTransform + continue; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output.setTransform(i, ABS); - output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); - output.set(i, signalHeight); + bounds = new Segment(0.5 * signalHeight, 1.5 * signalHeight); + value = signalHeight; break; case HEAT_LOSS: - final double Bi = (double) properties.getHeatLoss().getValue(); - output.setParameterBounds(i, Segment.boundsFrom(HEAT_LOSS)); - setHeatLossParameter(output, i, Bi); + value = (double) properties.getHeatLoss().getValue(); + output.setTransform(i, new StickTransform(bounds)); break; case TIME_SHIFT: - output.set(i, (double) curve.getTimeShift().getValue()); double magnitude = 0.25 * properties.timeFactor(); - output.setParameterBounds(i, new Segment(-magnitude, magnitude)); + bounds = new Segment(-magnitude, magnitude); + value = (double) curve.getTimeShift().getValue(); break; default: + continue; } - + + output.setTransform(i, new StickTransform(bounds)); + output.setParameterBounds(i, bounds); + output.set(i, value); + } } - protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { - output.setTransform(i, StandardTransformations.ABS); - output.set(i, Bi); - } - /** * Assigns parameter values of this {@code Problem} using the optimisation * vector {@code params}. Only those parameters will be updated, the types @@ -293,21 +289,21 @@ public void assign(ParameterVector params) throws SolverException { for (int i = 0, size = params.dimension(); i < size; i++) { - double value = params.get(i); + double value = params.inverseTransform(i); var key = params.getIndex(i); switch (key) { case THICKNESS: - properties.setSampleThickness(derive(THICKNESS, params.inverseTransform(i) )); + properties.setSampleThickness(derive(THICKNESS, value )); break; case DIFFUSIVITY: - properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i))); + properties.setDiffusivity(derive(DIFFUSIVITY, value)); break; case MAXTEMP: properties.setMaximumTemperature(derive(MAXTEMP, value)); break; case HEAT_LOSS: - properties.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); + properties.setHeatLoss(derive(HEAT_LOSS, value)); break; case TIME_SHIFT: curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); @@ -337,14 +333,10 @@ public boolean areDetailsHidden() { * @param b {@code true} if the user does not want to see the details, * {@code false} otherwise. */ - public static void setDetailsHidden(boolean b) { + public final static void setDetailsHidden(boolean b) { Problem.hideDetailedAdjustment = b; } - public String shortName() { - return getClass().getSimpleName(); - } - /** * Used for debugging. Initially, the nonlinear and two-dimensional problem * statements are disabled, since they have not yet been thoroughly tested @@ -419,11 +411,10 @@ public Baseline getBaseline() { * @param baseline the new baseline. * @see pulse.baseline.Baseline.apply(Baseline) */ - public void setBaseline(Baseline baseline) { + public final void setBaseline(Baseline baseline) { this.baseline = baseline; - if (!curve.isIncomplete()) { - curve.apply(baseline); - } + curve.apply(baseline); + baseline.setParent(this); var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); @@ -433,7 +424,7 @@ public void setBaseline(Baseline baseline) { } } - public InstanceDescriptor getBaselineDescriptor() { + public final InstanceDescriptor getBaselineDescriptor() { return instanceDescriptor; } @@ -443,7 +434,7 @@ private void initBaseline() { parameterListChanged(); } - public ThermalProperties getProperties() { + public final ThermalProperties getProperties() { return properties; } @@ -460,4 +451,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index cec8937c..d2b53249 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Set; +import pulse.input.ExperimentalData; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; @@ -90,9 +91,17 @@ private void addListeners() { NumericProperty pw = NumericProperties .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); + + var range = corrTask.getExperimentalCurve().getRange(); + + if( range.getLowerBound().compareTo(pw) < 0 ) { + //update lower bound of the range for that SearchTask corrTask.getExperimentalCurve().getRange() .setLowerBound(pw); + + } + } } @@ -123,21 +132,24 @@ public void setPulseWidth(NumericProperty pulseWidth) { requireType(pulseWidth, PULSE_WIDTH); double newValue = (double) pulseWidth.getValue(); + + double relChange = Math.abs((newValue - this.pulseWidth) / (this.pulseWidth + newValue)); final double EPS = 1E-3; - + //do not update -- if new value is the same as the previous one - if (Math.abs((newValue - this.pulseWidth) - / (this.pulseWidth + newValue)) < EPS) { - return; - } - - //validate -- do not update if the new pulse width is greater than 2 half-times - var task = (SearchTask) this.specificAncestor(SearchTask.class); - var data = task.getExperimentalCurve(); - if (newValue > 0 && newValue < 2.0 * data.getHalfTime()) { - this.pulseWidth = (double) pulseWidth.getValue(); - firePropertyChanged(this, pulseWidth); + if (relChange > EPS && newValue > 0) { + + //validate -- do not update if the new pulse width is greater than 2 half-times + SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); + ExperimentalData data = task.getExperimentalCurve(); + + if(newValue < 2.0 * data.getHalfTime()) { + this.pulseWidth = (double) pulseWidth.getValue(); + firePropertyChanged(this, pulseWidth); + } + } + } public NumericProperty getLaserEnergy() { @@ -205,7 +217,7 @@ public PulseTemporalShape getPulseShape() { public void setPulseShape(PulseTemporalShape pulseShape) { this.pulseShape = pulseShape; - pulseShape.setParent(this); + pulseShape.setParent(this); } } diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 434db3cd..3560fafd 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -111,7 +111,6 @@ public void optimisationVector(ParameterVector output, List flags) { double value = 0; Transformable transform = ABS; - output.setParameterBounds(i, new Segment(1E-2, 1000.0)); switch (key) { case LASER_ABSORPTIVITY: @@ -128,6 +127,7 @@ public void optimisationVector(ParameterVector output, List flags) { } //do this for the listed key values + output.setParameterBounds(i, Segment.boundsFrom(key)); output.setTransform(i, transform); output.set(i, value); diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index a0790c3e..df620015 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -7,8 +7,6 @@ import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.input.ExperimentalData; @@ -16,10 +14,8 @@ import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_COMBINED; -import pulse.properties.Property; public class ExtendedThermalProperties extends ThermalProperties { diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 1039f258..75a7ee59 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -134,6 +134,7 @@ public boolean areDetailsHidden() { * allowed to use those types of {@code NumericPropery} that are listed by * the {@code listedParameters()}. * + * @param value * @see listedTypes() */ @Override @@ -222,6 +223,7 @@ public NumericProperty getSpecificHeat() { public void setSpecificHeat(NumericProperty cP) { requireType(cP, SPECIFIC_HEAT); this.cP = (double) cP.getValue(); + firePropertyChanged(this, cP); } public NumericProperty getDensity() { @@ -231,6 +233,7 @@ public NumericProperty getDensity() { public void setDensity(NumericProperty p) { requireType(p, DENSITY); this.rho = (double) (p.getValue()); + firePropertyChanged(this, p); } public NumericProperty getTestTemperature() { @@ -338,6 +341,7 @@ public NumericProperty getEmissivity() { public void setEmissivity(NumericProperty e) { requireType(e, EMISSIVITY); this.emissivity = (double) e.getValue(); + firePropertyChanged(this, e); } @Override @@ -355,4 +359,4 @@ public String toString() { return sb.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index feafb90f..62282873 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -1,21 +1,29 @@ package pulse.problem.statements.model; +import java.util.List; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; -import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; import static pulse.properties.NumericPropertyKeyword.SCATTERING_ALBEDO; import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; +import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; +import pulse.search.Optimisable; -public class ThermoOpticalProperties extends ThermalProperties { +public class ThermoOpticalProperties extends ThermalProperties implements Optimisable { private double opticalThickness; private double planckNumber; @@ -159,5 +167,71 @@ public String toString() { sb.append(String.format("%n %-25s", this.getDensity())); return sb.toString(); } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + Segment bounds = null; + double value; + Transformable transform; + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + switch (key) { + case PLANCK_NUMBER: + final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); + bounds = new Segment(lowerBound, maxNp()); + value = (double) getPlanckNumber().getValue(); + break; + case OPTICAL_THICKNESS: + value = (double) getOpticalThickness().getValue(); + bounds = Segment.boundsFrom(OPTICAL_THICKNESS); + break; + case SCATTERING_ALBEDO: + value = (double) getScatteringAlbedo().getValue(); + bounds = new Segment(0.0, 1.0); + break; + case SCATTERING_ANISOTROPY: + value = (double) getScatteringAnisostropy().getValue(); + bounds = new Segment(-1.0, 1.0); + break; + default: + continue; + + } + + transform = new StickTransform(bounds); + output.setTransform(i, transform); + output.set(i, value); + output.setParameterBounds(i, bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + + for (int i = 0, size = params.dimension(); i < size; i++) { + + var type = params.getIndex(i); + + switch (type) { + + case PLANCK_NUMBER: + case SCATTERING_ALBEDO: + case SCATTERING_ANISOTROPY: + case OPTICAL_THICKNESS: + set(type, derive(type, params.inverseTransform(i))); + break; + default: + break; + + } + + } + + } } diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 04cb0bc0..5ef74195 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -7,7 +7,7 @@ public class SampleName implements Property { private String name; public SampleName() { - name = "Nameless"; + //null name } @Override diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index 9cf4d470..da33dddb 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -1,10 +1,8 @@ package pulse.search.direction; -import pulse.math.ParameterVector; import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.math.linear.SquareMatrix; -import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.SearchTask; /** @@ -28,13 +26,13 @@ protected ComplexPath(SearchTask task) { * In addition to the superclass method, resets the Hessian to an Identity * matrix. * - * @throws SolverException + * @param task */ @Override public void configure(SearchTask task) { - super.configure(task); hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); inverseHessian = createIdentityMatrix(hessian.getData().length); + super.configure(task); } public SquareMatrix getHessian() { diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 9b4c0bf0..3b78913d 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -1,6 +1,5 @@ package pulse.search.direction; -import java.util.Arrays; import static pulse.properties.NumericProperties.compare; import java.util.List; @@ -17,7 +16,8 @@ public abstract class CompositePathOptimiser extends GradientBasedOptimiser { - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; @@ -45,6 +45,7 @@ private void initLinearOptimiser() { setLinearSolver(instanceDescriptor.newInstance(LinearOptimiser.class)); } + @Override public boolean iteration(SearchTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task @@ -71,11 +72,8 @@ public boolean iteration(SearchTask task) throws SolverException { // new set of parameters determined through search var candidateParams = parameters.sum(dir.multiply(step)); - if( Arrays.stream( candidateParams.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { - throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); - } - task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals if (newCost > initialCost - EPS diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 22a0e05b..9dcab909 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -1,8 +1,11 @@ package pulse.search.direction; +import java.util.logging.Level; +import java.util.logging.Logger; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.SearchTask; +import pulse.tasks.logs.Status; /** *
*/ public void clear() { - stored = new ArrayList(); + stored = new ArrayList<>(); curve.resetRanges(); buffer = new Buffer(); correlationBuffer.clear(); @@ -233,15 +231,12 @@ public ParameterVector searchVector() { * * @param searchParameters an {@code IndexedVector} with relevant search * parameters + * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.problem.statements.Problem.assign(IndexedVector) */ - public void assign(ParameterVector searchParameters) { - try { - current.getProblem().assign(searchParameters); - curve.getRange().assign(searchParameters); - } catch (SolverException e) { - notifyFailedStatus(e); - } + public void assign(ParameterVector searchParameters) throws SolverException { + current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); } /** @@ -284,8 +279,7 @@ public void run() { correlationBuffer.clear(); /* search cycle */ - - /* sets an independent thread for manipulating the buffer */ + /* sets an independent thread for manipulating the buffer */ List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); @@ -295,8 +289,6 @@ public void run() { notifyFailedStatus(e1); } - final int maxIterations = (int) getInstance().getMaxIterations().getValue(); - outer: do { @@ -304,14 +296,8 @@ public void run() { for (var i = 0; i < bufferSize; i++) { - if (current.getStatus() != IN_PROGRESS) { - break outer; - } - - int iter = 0; - try { - for (boolean finished = false; !finished && iter < maxIterations; iter++) { + for (boolean finished = false; !finished;) { finished = optimiser.iteration(this); } } catch (SolverException e) { @@ -319,18 +305,12 @@ public void run() { break outer; } - if (iter >= maxIterations) { - var fail = FAILED; - fail.setDetails(MAX_ITERATIONS_REACHED); - setStatus(fail); - } - //if global best is better than the converged value if (best != null && best.getCost() < path.getCost()) { - //assign the global best parameters - assign(path.getParameters()); - //and try to re-calculate try { + //assign the global best parameters + assign(path.getParameters()); + //and try to re-calculate solveProblemAndCalculateCost(); } catch (SolverException ex) { notifyFailedStatus(ex); @@ -338,7 +318,7 @@ public void run() { } final var j = i; - + bufferFutures.add(CompletableFuture.runAsync(() -> { buffer.fill(this, j); correlationBuffer.inflate(this); @@ -349,13 +329,14 @@ public void run() { bufferFutures.forEach(future -> future.join()); - } while (buffer.isErrorTooHigh(errorTolerance)); + } while (buffer.isErrorTooHigh(errorTolerance) + && current.getStatus() == IN_PROGRESS); singleThreadExecutor.shutdown(); if (current.getStatus() == IN_PROGRESS) { runChecks(); - } + } } @@ -393,13 +374,13 @@ private void runChecks() { } } - } - - private void notifyFailedStatus(SolverException e1) { + + public void notifyFailedStatus(SolverException e1) { var status = Status.FAILED; status.setDetails(Details.SOLVER_ERROR); status.setDetailedMessage(e1.getMessage()); + e1.printStackTrace(); setStatus(status); } @@ -544,9 +525,9 @@ public String describe() { sb.append("_Task_"); var extId = curve.getMetadata().getExternalID(); if (extId < 0) { - sb.append("IntID_" + identifier.getValue()); + sb.append("IntID_").append(identifier.getValue()); } else { - sb.append("ExtID_" + extId); + sb.append("ExtID_").append(extId); } return sb.toString(); @@ -564,10 +545,9 @@ public void terminate() { case IN_PROGRESS: case QUEUED: case READY: - setStatus(TERMINATED); + setStatus(AWAITING_TERMINATION); break; default: - return; } } diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 03aa3341..f8801c24 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -26,11 +26,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; -import javax.swing.SwingUtilities; import pulse.input.ExperimentalData; +import pulse.input.listeners.DataEvent; +import pulse.input.listeners.DataEventType; import pulse.properties.SampleName; import pulse.search.direction.PathOptimiser; @@ -39,6 +39,7 @@ import pulse.tasks.listeners.TaskSelectionEvent; import pulse.tasks.listeners.TaskSelectionListener; import pulse.tasks.logs.Status; +import static pulse.tasks.logs.Status.AWAITING_TERMINATION; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.util.Group; @@ -58,19 +59,19 @@ */ public class TaskManager extends UpwardsNavigable { - private static TaskManager instance = new TaskManager(); + private static final TaskManager instance = new TaskManager(); private List tasks; private SearchTask selectedTask; private boolean singleStatement = true; - private ForkJoinPool taskPool; + private final ForkJoinPool taskPool; - private List selectionListeners; - private List taskRepositoryListeners; + private final List selectionListeners; + private final List taskRepositoryListeners; - private final static String DEFAULT_NAME = "Project 1 - " + now().format(ISO_WEEK_DATE); + private final static String DEFAULT_NAME = "Measurement_" + now().format(ISO_WEEK_DATE); private final HierarchyListener statementListener = e -> { @@ -96,10 +97,11 @@ private TaskManager() { addHierarchyListener(statementListener); /* Calculate the half-time once data is loaded. - */ + */ addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if(e.getState() == TaskRepositoryEvent.State.TASK_ADDED) + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { getTask(e.getId()).getExperimentalCurve().calculateHalfTime(); + } }); } @@ -125,10 +127,10 @@ public void execute(SearchTask t) { //try to start cmputation // notify listeners computation is about to start - - if( ! t.setStatus(QUEUED) ) - return; - + if (!t.setStatus(QUEUED)) { + return; + } + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); @@ -141,9 +143,13 @@ public void execute(SearchTask t) { //notify listeners before the task is re-assigned notifyListeners(e); t.storeCalculation(); + } + else if(current.getStatus() == AWAITING_TERMINATION) { + t.setStatus(Status.TERMINATED); } - else + else { notifyListeners(e); + } }); } @@ -312,12 +318,12 @@ public SearchTask getTask(int externalId) { * Generates a {@code SearchTask} assuming that the {@code ExperimentalData} * is stored in the {@code file}. This will make the {@code ReaderManager} * attempt to read that {@code file}. If successful, invokes - * {@code addTask(...)} on the created {@code SearchTask}. After the - * task is generated, checks whether the acquisition time recorded by the experimental setup - * has been chosen appropriately. + * {@code addTask(...)} on the created {@code SearchTask}. After the task is + * generated, checks whether the acquisition time recorded by the + * experimental setup has been chosen appropriately. * * @see pulse.input.ExperimentalData.isAcquisitionTimeSensible() - + * *

* * @param file the file to load the experimental data from @@ -325,17 +331,24 @@ public SearchTask getTask(int externalId) { * @see pulse.io.readers.ReaderManager.extract(File) */ public void generateTask(File file) { - read(curveReaders(), file).stream().forEach((ExperimentalData curve) -> { + var curves = read(curveReaders(), file); + //notify curves have been loaded + curves.stream().forEach(c -> c.fireDataChanged(new DataEvent( + DataEventType.DATA_LOADED, c + ))); + //create tasks + curves.stream().forEach((ExperimentalData curve) -> { var task = new SearchTask(curve); addTask(task); var data = task.getExperimentalCurve(); - if(!data.isAcquisitionTimeSensible()) + if (!data.isAcquisitionTimeSensible()) { data.truncate(); + } }); } /** - * Generates multiple tasks from multiple {@code files}. + * Generates multiple tasks from multiple {@code files}. * * @param files a list of {@code File}s that can be parsed down to * {@code ExperimentalData}. @@ -344,22 +357,22 @@ public void generateTasks(List files) { requireNonNull(files, "Null list of files passed to generatesTasks(...)"); //this is the loader runnable submitted to the executor service - Runnable loader = () -> { - var pool = Executors.newSingleThreadExecutor(); + Runnable loader = () -> { + var pool = Executors.newSingleThreadExecutor(); files.stream().forEach(f -> pool.submit(() -> generateTask(f))); pool.shutdown(); - + try { pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException ex) { Logger.getLogger(TaskManager.class.getName()).log(Level.SEVERE, null, ex); - } - + } + //when pool has been shutdown selectFirstTask(); - + }; - + Executors.newSingleThreadExecutor().submit(loader); } @@ -488,10 +501,13 @@ public List getTaskRepositoryListeners() { /** * This {@code TaskManager} will be described by the sample name for the * experiment. + * + * @return the string descriptor */ @Override public String describe() { - return tasks.size() > 0 ? getSampleName().toString() : DEFAULT_NAME; + var name = getSampleName(); + return name == null || name.getValue() == null ? DEFAULT_NAME : name.toString(); } public void evaluate() { diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 080f6ed5..87e226d9 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -31,9 +31,16 @@ public enum Status { */ EXECUTION_ERROR(Color.red), /** - * The task has been terminated by the user. + * Termination requested. */ + AWAITING_TERMINATION(Color.DARK_GRAY), + + /** + * Task terminated + */ + TERMINATED(Color.DARK_GRAY), + /** * Task has been queued and is waiting to be executed. */ diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 6843a20d..acadc0bb 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -36,7 +36,7 @@ public class Launcher { private PrintStream errStream; private File errorLog; - private final static boolean DEBUG = false; + private final static boolean DEBUG = true; private static final File LOCK = new File("pulse.lock"); @@ -147,6 +147,10 @@ private void createShutdownHook() { if (errorLog != null && errorLog.exists() && errorLog.length() < 1) { errorLog.delete(); } + //delete lock explicitly on abnormal termination + if(LOCK.exists()) { + LOCK.delete(); + } }; Runtime.getRuntime().addShutdownHook(new Thread(r)); diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 42c9c9ba..9d966b43 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -81,8 +81,10 @@ public void initListeners() { var task = TaskManager.getManagerInstance().getSelectedTask(); var id = convertRowIndexToModel(this.getSelectedRow()); if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { + task.switchTo(task.getStoredCalculations().get(id)); getChart().plot(task, true); + } }); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 21587580..753b8784 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -217,8 +217,6 @@ private void adjustAxisLabel(double maximum) { public void plot(SearchTask task, boolean extendedCurve) { requireNonNull(task); - var plot = chart.getXYPlot(); - for (int i = 0; i < 6; i++) { plot.setDataset(i, null); } @@ -261,7 +259,7 @@ public void plot(SearchTask task, boolean extendedCurve) { var solution = problem.getHeatingCurve(); var scheme = calc.getScheme(); - if (solution != null && scheme != null && !solution.isIncomplete()) { + if (solution != null && scheme != null) { var solutionDataset = new XYSeriesCollection(); var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index ec13f1bb..d6b6539d 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -21,7 +21,6 @@ import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; -import pulse.problem.laser.NumericPulseData; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -66,6 +65,7 @@ public static void loadDataDialog() { var instance = TaskManager.getManagerInstance(); if (files != null) { + progressFrame.trackProgress(files.size()); instance.generateTasks(files); } @@ -151,13 +151,15 @@ public static void loadPulseDialog() { metadata.getPulseDescriptor() .setSelectedDescriptor( NumericPulse.class.getSimpleName()); - }); + progressFrame.incrementProgress(); + } + ); } }); }; - + Executors.newSingleThreadExecutor().submit(loader); } diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 141d0f4e..520aa0e0 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -178,7 +178,7 @@ public String getToolTipText(MouseEvent e) { @Override public String describe() { - return "SummaryTable"; + return "Summary_" + TaskManager.getManagerInstance().describe(); } public boolean isSelectionEmpty() { diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 5b93f01e..46c9b435 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -24,8 +24,10 @@ import javax.swing.JFileChooser; import javax.swing.UIManager; import javax.swing.filechooser.FileNameExtensionFilter; +import org.apache.commons.math3.exception.OutOfRangeException; import pulse.input.InterpolationDataset; +import pulse.ui.Messages; import pulse.util.ImageUtils; @SuppressWarnings("serial") @@ -84,7 +86,18 @@ public void init() { showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ getString("LoaderButton.IOError"), //$NON-NLS-1$ ERROR_MESSAGE); - e.printStackTrace(); + } + catch(OutOfRangeException ofre) { + getDefaultToolkit().beep(); + StringBuilder sb = new StringBuilder(getString("TextWrap.0")); + sb.append(getString("LoaderButton.OFRErrorDescriptor") ); + sb.append(ofre.getMessage()); + sb.append(getString("LoaderButton.OFRErrorDescriptor2")); + sb.append(getString("TextWrap.1")); + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), + sb.toString(), + getString("LoaderButton.OFRError"), //$NON-NLS-1$ + ERROR_MESSAGE); } var size = getDataset(dataType).getData().size(); var label = ""; diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 2c5d4fdd..ac0c3bd0 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -27,9 +27,14 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean combobox = new JComboBox<>(((InstanceDescriptor) value).getAllDescriptors().toArray()); combobox.setSelectedItem(descriptor.getValue()); - combobox.addItemListener(e -> { + combobox.addItemListener((ItemEvent e) -> { if (e.getStateChange() == ItemEvent.SELECTED) { - descriptor.attemptUpdate(e.getItem()); + try { + descriptor.attemptUpdate(e.getItem()); + } catch(NullPointerException npe) { + System.out.println("Error updating " + descriptor.getDescriptor(false) + + ". Cannot be set to " + e.getItem()); + } } }); diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 5910a002..ffd35db1 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -135,7 +135,8 @@ public void merge(double temperatureDelta) { avgResults.addAll(group); } else { //add and average result - avgResults.add(new AverageResult(group, fmt)); + var result = new AverageResult(group, fmt); + avgResults.add(result); } //ignore processed results later on @@ -218,47 +219,53 @@ private List tooltips() { public void addRow(AbstractResult result) { Objects.requireNonNull(result, "Entry added to the results table must not be null"); - //result must have a valid ancestor! - var ancestor = Objects.requireNonNull( - result.specificAncestor(SearchTask.class), - "Result " + result.toString() + " does not belong a SearchTask!"); - - //the ancestor then has the SearchTask type - SearchTask parentTask = (SearchTask) ancestor; - - //any old result asssociated withis this task - var oldResult = results.stream().filter(r - -> r.specificAncestor( - SearchTask.class) == parentTask).findAny(); - //ignore average results - if (result instanceof Result && oldResult.isPresent()) { - AbstractResult oldResultExisting = oldResult.get(); - Optional oldCalculation = parentTask.getStoredCalculations().stream() - .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); - - //old calculation found - if (oldCalculation.isPresent()) { - - //since the task has already been completed anyway - Status status = Status.DONE; + if (result instanceof Result) { + + //result must have a valid ancestor! + var ancestor = Objects.requireNonNull( + result.specificAncestor(SearchTask.class), + "Result " + result.toString() + " does not belong a SearchTask!"); + + //the ancestor then has the SearchTask type + SearchTask parentTask = (SearchTask) ancestor; + + //any old result asssociated withis this task + var oldResult = results.stream().filter(r + -> r.specificAncestor( + SearchTask.class) == parentTask).findAny(); + + //check the following only if the old result is present + if (oldResult.isPresent()) { + + AbstractResult oldResultExisting = oldResult.get(); + Optional oldCalculation = parentTask.getStoredCalculations().stream() + .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); + + //old calculation found + if (oldCalculation.isPresent()) { + + //since the task has already been completed anyway + Status status = Status.DONE; + + //better result than already present -- update table + if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + remove(oldResultExisting); + status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + } else { + //do not remove result and do not add new result + status.setDetails(Details.CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + return; + } - //better result than already present -- update table - if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { - remove(oldResultExisting); - status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); - parentTask.setStatus(status); } else { - //do not remove result and do not add new result - status.setDetails(Details.CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED); - parentTask.setStatus(status); - return; - } + //calculation has been purged -- delete previous result - } else { - //calculation has been purged -- delete previous result + remove(oldResultExisting); - remove(oldResultExisting); + } } diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index aa85aed7..95156428 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -49,8 +49,7 @@ public ProblemToolbar() { add(btnLoadDensity); btnSimulate.addActionListener((ActionEvent e) - -> Executors.newSingleThreadExecutor().submit(() - -> ProblemToolbar.plot(e))); + -> plot(e)); } public static void plot(ActionEvent e) { @@ -76,7 +75,8 @@ public static void plot(ActionEvent e) { } else { try { - ((Solver) calc.getScheme()).solve(calc.getProblem()); + Solver solver = (Solver) calc.getScheme(); + solver.solve(calc.getProblem()); } catch (SolverException se) { err.println("Solver of " + t + " has encountered an error. Details: "); se.printStackTrace(); diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index 8238a339..05cef73c 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -289,6 +289,7 @@ public String getPrefix() { * * @return the descriptor */ + @Override public String getDescriptor() { return prefix != null ? getPrefix() : super.getDescriptor(); } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 3e3180a2..ea013165 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -212,7 +212,7 @@ default-search-variable="false"> @@ -301,9 +301,9 @@ dimensionfactor="1000.0" keyword="DIAMETER" maximum="0.1" minimum="1.0E-6" value="0.01" primitive-type="double" discreet="true"/> diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index b7e420c3..06373aeb 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.94 \ No newline at end of file +1.94F \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f88018b7..e09393db 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -43,7 +43,8 @@ SearchFlags.Label=SearchFlags SearchFlags.Descriptor=Search flags LinearizedProblem.Descriptor=Classical 1D Problem Statement
  • Linearized heat losses (front and rear)
  • One-dimensional heat flow
  • Dimensionless formulation
  • Cp and ρ assumed constant
DistributedProblem.Descriptor=Penetration (1D) Problem Statement
  • Based on 1D formulation, except:
  • Distributed radiation absorption (finite penetration depth) is considered
  • Detector measures an integral signal from the bulk of the sample
  • Laser and thermal radiation absorption are considered separately
-ParticipatingMedium.Descriptor=Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
+ParticipatingMedium.Descriptor=Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
  • Allows a nonlinear emission function -- hence requires Cp and ρ data
+LinearisedParticipatingMedium.Descriptor=Linearised Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
  • Assumes heating is small to suppress nonlinear emission
DiathermicProblem.Descriptor=Diathermic Sample with Grey Walls (1D) Problem Statement
  • Based on 1D formulation, except:
  • Laser radiation is absorbed at the front surface and then re-radiated to the rear surface in the form of thermal radiation
  • Sample material considered fully transparent to any kind of radiation
LinearizedProblem2D.Descriptor=Classical 2D Problem Statement
  • Based on 1D formulation, except:
  • Allows heat losses from side surface
  • Allows radial heat flow
UniformlyCoatedSample.Descriptor=Core-Shell 2D Problem Statement
  • Based on the classical 2D problem, except:
  • Explicitly accounts for a coating that covers front, rear, and side surfaces
  • Allows for axial, radial, and circumferential heat fluxes
@@ -91,6 +92,9 @@ LoaderButton.5=Specific heat, (CP) LoaderButton.6=Density, (ρ) LoaderButton.IOError=I/O Error LoaderButton.ReadError=Unable to read data from file\! +LoaderButton.OFRError=Out of Range +LoaderButton.OFRErrorDescriptor=Data file does not cover the test temperature of current measurement: +LoaderButton.OFRErrorDescriptor2=. Please try expanding the temperature range in the input file! LoaderButton.SupportedExtensionsDescriptor=Supported thermal properties files LogPane.Init=Initializing... LogPane.InsertError=Could not insert log entry to log panel @@ -273,13 +277,16 @@ complexity.warning=

You have selected a high ExplicitScheme.2=Time interval too small: ExplicitScheme.3=Problem not supported or unknown: ExplicitScheme.4=Forward Time, Centred Space (FTCS) Scheme

  • Order of approximation O(h2 + τ)
  • Conditionally stable
  • Faster than other schemes
+ExplicitScheme.5=Forward Time, Centred Space (FTCS) Scheme (NL)
  • Order of approximation O(h2 + τ)
  • Conditionally stable
  • Faster than other schemes
ImplicitScheme.2=Time interval too small: ImplicitScheme.3=Problem not supported or unknown: ImplicitScheme.4=Fully Implicit Scheme
  • Order of approximation O(h2 + &tau)
  • Unconditionally stable
  • Intermediate computational cost
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
+ImplicitScheme.5=Fully Implicit Scheme (NL)
  • Order of approximation O(h2 + &tau)
  • Unconditionally stable
  • Intermediate computational cost
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
MixedScheme.2=Time interval too small: MixedScheme.3=Problem not supported or unknown: MixedScheme.4=Symmetric Semi-Implicit Scheme
  • Order of approximation O(h2 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Weight is set to 0.5
MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
+MixedScheme2.5=Increased Accuracy Semi-implicit Scheme (NL)
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
TextWrap.0=

TextWrap.1=

TextWrap.2=

\ No newline at end of file diff --git a/src/test/java/test/NonscatteringSetup.java b/src/test/java/test/NonscatteringSetup.java index d56f41cd..5de96229 100644 --- a/src/test/java/test/NonscatteringSetup.java +++ b/src/test/java/test/NonscatteringSetup.java @@ -16,6 +16,7 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.solvers.ImplicitCoupledSolver; +import pulse.problem.schemes.solvers.ImplicitCoupledSolverNL; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Pulse2D; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -38,7 +39,7 @@ public NonscatteringSetup(final int testProfileSize, final double maxHeating) { properties.setTestTemperature(derive(TEST_TEMPERATURE, 800.0)); properties.setScatteringAlbedo(derive(SCATTERING_ALBEDO, 0.0)); - testScheme = new ImplicitCoupledSolver(); + testScheme = new ImplicitCoupledSolverNL(); var grid = testScheme.getGrid(); grid.setGridDensity(derive(GRID_DENSITY, testProfileSize - 1)); From 1ce1f8e6963aad84cf075d067fa6ba998eef165b Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Tue, 7 Jun 2022 14:10:37 +0300 Subject: [PATCH 095/116] Minor Fixes - Fixed pulse visualisation. Now it takes parameters directly from the PulseShape, which is initialised by the DiscretePulse. Moreover, it only plots points at discrete time steps defined by the Grid. Therefore, the user should have an idea as to how detailed the pulse correction is. - Adjusted the pulse resolution allowed for 2D models. This had to be done since 2D calculations are more demanding in terms of the number of pulse points. Previously these resulted in incorrect Tinf values. --- .../pulse/problem/laser/DiscretePulse.java | 31 +++++++---- .../pulse/problem/laser/DiscretePulse2D.java | 54 ++++++++++++++----- .../statements/ClassicalProblem2D.java | 1 - .../java/pulse/ui/components/PulseChart.java | 39 +++++++------- .../ui/components/panels/ProblemToolbar.java | 2 +- .../pulse/ui/frames/TaskControlFrame.java | 7 +-- src/main/resources/NumericProperty.xml | 2 +- 7 files changed, 88 insertions(+), 48 deletions(-) diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 8160344d..b27e4eb9 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,6 +7,8 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import pulse.tasks.SearchTask; /** @@ -34,7 +36,7 @@ public class DiscretePulse { * tc * is the time factor defined in the {@code Problem} class. */ - public final static int WIDTH_TOLERANCE_FACTOR = 1000; + private final static int WIDTH_TOLERANCE_FACTOR = 1000; /** * This creates a one-dimensional discrete pulse on a {@code grid}. @@ -74,7 +76,7 @@ private void init(ExperimentalData data) { widthOnGrid = 0; recalculate(); pulse.getPulseShape().init(data, this); - normalise(); + invTotalEnergy = 1.0/totalEnergy(); } /** @@ -96,7 +98,7 @@ public double laserPowerAt(double time) { */ public final void recalculate() { final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - final double resolvedWidth = timeConversionFactor / WIDTH_TOLERANCE_FACTOR; + final double resolvedWidth = timeConversionFactor / getWidthToleranceFactor(); final double EPS = 1E-10; @@ -112,6 +114,7 @@ public final void recalculate() { //change pulse width setDiscreteWidth(resolvedWidth); shape.init(null, this); + //adjust the pulse object to update the visualised pulse } else if(nominalWidth > resolvedWidth + EPS) { setDiscreteWidth(nominalWidth); } @@ -119,12 +122,12 @@ public final void recalculate() { } /** - * Calculates the total pulse energy using a numerical integrator. The + * Calculates the total pulse energy using a numerical integrator.The * normalisation factor is then equal to the inverse total energy. + * @return the total pulse energy, assuming sample area fully covered by the beam */ - public final void normalise() { - invTotalEnergy = 1.0; + public final double totalEnergy() { var pulseShape = pulse.getPulseShape(); var integrator = new MidpointIntegrator(new Segment(0, widthOnGrid)) { @@ -136,8 +139,7 @@ public double integrand(double... vars) { }; - invTotalEnergy = 1.0 / integrator.integrate(); - + return integrator.integrate(); } /** @@ -191,7 +193,18 @@ public double getConversionFactor() { */ public double resolvedPulseWidth() { - return timeConversionFactor / WIDTH_TOLERANCE_FACTOR; + return timeConversionFactor / getWidthToleranceFactor(); + } + + /** + * Assuming a characteristic time is divided by the return value of this method + * and is set to the minimal resolved pulse width, shows how small a pulse width + * can be to enable finite pulse correction. + * @return the smallest fraction of a characteristic time resolved as a finite pulse. + */ + + public int getWidthToleranceFactor() { + return WIDTH_TOLERANCE_FACTOR; } } \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index a34102f5..ab9fd0e2 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -19,7 +19,14 @@ public class DiscretePulse2D extends DiscretePulse { private double discretePulseSpot; - private double radialFactor; + private double sampleRadius; + private double normFactor; + + /** + * This had to be decreased for the 2d pulses. + */ + + private final static int WIDTH_TOLERANCE_FACTOR = 200; /** * The constructor for {@code DiscretePulse2D}. @@ -35,9 +42,8 @@ public class DiscretePulse2D extends DiscretePulse { public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { super(problem, grid); var properties = (ExtendedThermalProperties) problem.getProperties(); - init(properties); - - properties.addListener(e -> init(properties) ); + calcPulseSpot(properties); + properties.addListener(e -> calcPulseSpot(properties) ); } /** @@ -47,7 +53,7 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { * It uses a Heaviside function to determine whether the {@code radialCoord} * lies within the {@code 0 <= radialCoord <= discretePulseSpot} interval. * It uses the {@code time} parameter to determine the discrete pulse - * function using {@code evaluateAt(time)}. + * function using {@code evaluateAt(time)}.

* * @param time the time for calculation * @param radialCoord - the radial coordinate [length dimension] @@ -55,12 +61,26 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { * {@code coord > spotDiameter}. * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) */ + public double evaluateAt(double time, double radialCoord) { - return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); + return laserPowerAt(time) + * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); } - private void init(ExtendedThermalProperties properties) { - radialFactor = (double) properties.getSampleDiameter().getValue() / 2.0; + /** + * Calculates the laser power at a give moment in time. The total laser + * energy is normalised over a beam partially illuminating the sample surface. + * @param time a moment in time (in dimensionless units) + * @return the laser power in arbitrary units + */ + + @Override + public double laserPowerAt(double time) { + return normFactor * super.laserPowerAt(time); + } + + private void calcPulseSpot(ExtendedThermalProperties properties) { + sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; evalPulseSpot(); } @@ -72,9 +92,10 @@ private void init(ExtendedThermalProperties properties) { public final void evalPulseSpot() { var pulse = (Pulse2D) getPulse(); var grid2d = (Grid2D) getGrid(); - final double radius = (double) pulse.getSpotDiameter().getValue() / 2.0; - discretePulseSpot = grid2d.gridRadialDistance(radius, radialFactor); + final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; + discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); grid2d.adjustStepSize(this); + normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; } public final double getDiscretePulseSpot() { @@ -82,7 +103,16 @@ public final double getDiscretePulseSpot() { } public final double getRadialConversionFactor() { - return radialFactor; + return sampleRadius; + } + + /** + * A smaller tolerance factor is set for 2D calculations + */ + + @Override + public int getWidthToleranceFactor() { + return WIDTH_TOLERANCE_FACTOR; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 2a9ed2d8..278fe8ea 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -67,7 +67,6 @@ public String toString() { public DiscretePulse discretePulseOn(Grid grid) { return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); } - @Override public void optimisationVector(ParameterVector output, List flags) { diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index 418ec706..bb336469 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -22,10 +22,10 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; +import pulse.tasks.Calculation; -public class PulseChart extends AuxPlotter { +public class PulseChart extends AuxPlotter { - private final static int NUM_PULSE_POINTS = 600; private final static double TO_MILLIS = 1E3; public PulseChart(String xLabel, String yLabel) { @@ -59,36 +59,33 @@ private void setLegendTitle() { } @Override - public void plot(Pulse pulse) { - requireNonNull(pulse); + public void plot(Calculation c) { + requireNonNull(c); + + Problem problem = c.getProblem(); - var problem = (Problem) pulse.getParent(); double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); var pulseDataset = new XYSeriesCollection(); - pulseDataset.addSeries(series(problem, startTime)); + + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep(), + problem.getProperties().timeFactor(), startTime)); getPlot().setDataset(0, pulseDataset); } - private static XYSeries series(Problem problem, double startTime) { - var pulse = problem.getPulse(); - + private static XYSeries series(Pulse pulse, double dx, double timeFactor, double startTime) { var series = new XYSeries(pulse.getPulseShape().toString()); - - double timeLimit = (double) pulse.getPulseWidth().getValue(); - final double timeFactor = problem.getProperties().timeFactor(); - - double dx = timeLimit / (NUM_PULSE_POINTS - 1); - double x = startTime; - - series.add(TO_MILLIS * (startTime - dx / 10.), 0.0); - series.add(TO_MILLIS * (startTime + timeLimit + dx / 10.), 0.0); - var pulseShape = pulse.getPulseShape(); + + double timeLimit = pulseShape.getPulseWidth(); + double x = startTime/timeFactor; + + series.add(TO_MILLIS * (startTime - dx * timeFactor / 10.), 0.0); + series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 10.)), 0.0); - for (var i = 0; i < NUM_PULSE_POINTS; i++) { - series.add(x * TO_MILLIS, pulseShape.evaluateAt((x - startTime) / timeFactor)); + for (int i = 0, numPoints = (int) (timeLimit/dx); i < numPoints; i++) { + series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime/timeFactor)); x += dx; } diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 95156428..0aeedc80 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -82,7 +82,7 @@ public static void plot(ActionEvent e) { se.printStackTrace(); } MainGraphFrame.getInstance().plot(); - TaskControlFrame.getInstance().getPulseFrame().plot(calc.getProblem().getPulse()); + TaskControlFrame.getInstance().getPulseFrame().plot(calc); } } diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 8a32dc34..3bcccead 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -26,6 +26,7 @@ import javax.swing.event.InternalFrameEvent; import pulse.problem.statements.Pulse; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Version; import pulse.ui.components.PulseChart; @@ -51,7 +52,7 @@ public class TaskControlFrame extends JFrame { private ResultFrame resultsFrame; private MainGraphFrame graphFrame; private LogFrame logFrame; - private InternalGraphFrame pulseFrame; + private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; @@ -201,7 +202,7 @@ private void initComponents() { searchOptionsFrame = new SearchOptionsFrame(); searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); - pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); + pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); pulseFrame.setVisible(false); @@ -453,7 +454,7 @@ public Mode getMode() { return mode; } - public InternalGraphFrame getPulseFrame() { + public InternalGraphFrame getPulseFrame() { return pulseFrame; } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index ea013165..a4293ab2 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -351,7 +351,7 @@ abbreviation="<i>d</i><sub>las</sub> (mm)" visible="true" descriptor="Laser spot diameter, <i>d</i><sub>las</sub> (mm)" - dimensionfactor="1000.0" keyword="SPOT_DIAMETER" maximum="0.05" + dimensionfactor="1000.0" keyword="SPOT_DIAMETER" maximum="0.2" minimum="1.0E-4" value="0.01" primitive-type="double" discreet="true">
Date: Wed, 8 Jun 2022 10:47:54 +0300 Subject: [PATCH 096/116] Model improvements - Convective heat losses now introduced in diathermic and participating medium models - Emissivity can now be output in the result format dialog --- pom.xml | 2 +- .../pulse/input/InterpolationDataset.java | 11 +-- .../solvers/ExplicitCoupledSolver.java | 3 +- .../solvers/ExplicitCoupledSolverNL.java | 2 - .../solvers/ImplicitCoupledSolver.java | 3 +- .../solvers/ImplicitCoupledSolverNL.java | 2 - .../solvers/ImplicitDiathermicSolver.java | 9 +-- .../schemes/solvers/MixedCoupledSolver.java | 5 +- .../problem/statements/DiathermicMedium.java | 38 +++++++--- .../model/DiathermicProperties.java | 32 +++++++-- .../model/ExtendedThermalProperties.java | 2 +- .../statements/model/ThermalProperties.java | 27 +++++++- .../model/ThermoOpticalProperties.java | 65 +++++++++++++----- .../properties/NumericPropertyKeyword.java | 7 ++ .../pulse/tasks/processing/ResultFormat.java | 13 ++-- .../components/models/SelectedKeysModel.java | 4 +- .../ui/frames/dialogs/ResultChangeDialog.java | 11 --- src/main/resources/NumericProperty.xml | 11 ++- src/main/resources/Version.txt | 2 +- src/main/resources/images/splash.png | Bin 67179 -> 67897 bytes 20 files changed, 169 insertions(+), 80 deletions(-) diff --git a/pom.xml b/pom.xml index 3dc51b98..ecbe713f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.94F + 1.95 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 3db3182d..45f244ed 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -5,7 +5,7 @@ import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import java.util.ArrayList; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -14,6 +14,7 @@ import pulse.input.listeners.ExternalDatasetListener; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; import pulse.util.ImmutableDataEntry; /** @@ -29,9 +30,10 @@ public class InterpolationDataset { private UnivariateFunction interpolation; - private List> dataset; - private static Map standartDatasets = new HashMap(); - private static List listeners = new ArrayList<>(); + private final List> dataset; + private static final Map standartDatasets + = new EnumMap(StandartType.class); + private static final List listeners = new ArrayList<>(); /** * Creates an empty {@code InterpolationDataset}. @@ -121,6 +123,7 @@ public static List derivableProperties() { } if (list.contains(SPECIFIC_HEAT) && list.contains(DENSITY)) { list.add(CONDUCTIVITY); + list.add(EMISSIVITY); } return list; } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 9da5e215..d50b442c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -56,7 +56,8 @@ public void prepare(Problem problem) throws SolverException { hx = grid.getXStep(); var p = (ThermoOpticalProperties) problem.getProperties(); - double Bi = (double) p.getHeatLoss().getValue(); + //combined Biot + double Bi = (double) p.getHeatLoss().getValue() + (double) p.getConvectiveLosses().getValue(); a = 1. / (1. + Bi * hx); diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java index 07247ef4..ab30eeba 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java @@ -17,8 +17,6 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.FixedPointIterations; -import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.Problem; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 13016048..98c3dbb0 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -58,7 +58,8 @@ public void prepare(Problem problem) throws SolverException { final double tau = grid.getTimeStep(); var p = (ThermoOpticalProperties) problem.getProperties(); - final double Bi1 = (double) p.getHeatLoss().getValue(); + //combined Biot + final double Bi1 = (double) p.getHeatLoss().getValue() + (double) p.getConvectiveLosses().getValue(); final double Np = (double) p.getPlanckNumber().getValue(); final double tau0 = (double) p.getOpticalThickness().getValue(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java index 42c9c032..389d148a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java @@ -17,8 +17,6 @@ import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.FixedPointIterations; -import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.Problem; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 3d36e222..920e4fbd 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -47,11 +47,12 @@ public void prepare(Problem problem) throws SolverException { /* Constants */ var properties = (DiathermicProperties) problem.getProperties(); - final double Bi1 = (double) properties.getHeatLoss().getValue(); + final double BiR = (double) properties.getHeatLoss().getValue(); + final double BiC = (double) properties.getConvectiveLosses().getValue(); final double eta = (double) properties.getDiathermicCoefficient().getValue(); - z0 = 1.0 + HX2_2TAU + hx * Bi1 * (1.0 + eta); - zN_1 = -hx * eta * Bi1; + z0 = 1.0 + HX2_2TAU + hx * BiR * (1.0 + eta) + hx * BiC; + zN_1 = -hx * eta * BiR; /* End of constants */ var tridiagonal = new BlockMatrixAlgorithm(grid); @@ -106,4 +107,4 @@ public Class[] domain() { return new Class[]{DiathermicMedium.class}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index f5393dac..fc3e5167 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -74,7 +74,10 @@ public void prepare(Problem problem) throws SolverException { hx = grid.getXStep(); tau = grid.getTimeStep(); - Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + var properties = (ThermoOpticalProperties)problem.getProperties(); + //combined biot + Bi1 = (double) properties.getHeatLoss().getValue() + + (double) properties.getConvectiveLosses().getValue(); zeta = (double) ( (ClassicalProblem)problem ).getGeometricFactor().getValue(); diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 0e8fcada..488eafdb 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -14,6 +14,7 @@ import pulse.problem.statements.model.DiathermicProperties; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import pulse.ui.Messages; /** @@ -60,17 +61,31 @@ public void optimisationVector(ParameterVector output, List flags) { for (int i = 0, size = output.dimension(); i < size; i++) { var key = output.getIndex(i); + Segment bounds = null; + double value = 0; - if (key == DIATHERMIC_COEFFICIENT) { - - var bounds = Segment.boundsFrom(DIATHERMIC_COEFFICIENT); - final double etta = (double) properties.getDiathermicCoefficient().getValue(); - - output.setTransform(i, new StickTransform(bounds)); - output.set(i, etta); - output.setParameterBounds(i, bounds); - + switch (key) { + case DIATHERMIC_COEFFICIENT: + bounds = Segment.boundsFrom(DIATHERMIC_COEFFICIENT); + value = (double) properties.getDiathermicCoefficient().getValue(); + break; + case HEAT_LOSS_CONVECTIVE: + bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); + value = (double) properties.getConvectiveLosses().getValue(); + break; + case HEAT_LOSS: + if(properties.areThermalPropertiesLoaded()) { + value = (double) properties.getHeatLoss().getValue(); + bounds = new Segment(0.0, properties.maxRadiationBiot() ); + break; + } + default: + continue; } + + output.setTransform(i, new StickTransform(bounds)); + output.set(i, value); + output.setParameterBounds(i, bounds); } @@ -90,9 +105,10 @@ public void assign(ParameterVector params) throws SolverException { case DIATHERMIC_COEFFICIENT: properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); break; + case HEAT_LOSS_CONVECTIVE: + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, params.inverseTransform(i))); + break; default: - continue; - } } diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java index 23153904..bc0a9a56 100644 --- a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -7,14 +7,17 @@ import static pulse.properties.NumericProperty.requireType; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; public class DiathermicProperties extends ThermalProperties { private double diathermicCoefficient; + private double convectiveLosses; public DiathermicProperties() { super(); this.diathermicCoefficient = (double) def(DIATHERMIC_COEFFICIENT).getValue(); + this.convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public DiathermicProperties(ThermalProperties p) { @@ -23,8 +26,10 @@ public DiathermicProperties(ThermalProperties p) { ? ((DiathermicProperties) p).getDiathermicCoefficient() : def(DIATHERMIC_COEFFICIENT); this.diathermicCoefficient = (double) property.getValue(); + this.convectiveLosses = (double) property.getValue(); } + @Override public ThermalProperties copy() { return new ThermalProperties(this); } @@ -37,13 +42,29 @@ public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); } + + public NumericProperty getConvectiveLosses() { + return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); + } + + public void setConvectiveLosses(NumericProperty convectiveLosses) { + requireType(convectiveLosses, HEAT_LOSS_CONVECTIVE); + this.convectiveLosses = (double) convectiveLosses.getValue(); + } @Override public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == DIATHERMIC_COEFFICIENT) { - diathermicCoefficient = ((Number) property.getValue()).doubleValue(); - } else { - super.set(type, property); + double value = ((Number) property.getValue()).doubleValue(); + switch (type) { + case DIATHERMIC_COEFFICIENT: + diathermicCoefficient = value; + break; + case HEAT_LOSS_CONVECTIVE: + convectiveLosses = value; + break; + default: + super.set(type, property); + break; } } @@ -51,7 +72,8 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public Set listedKeywords() { var set = super.listedKeywords(); set.add(DIATHERMIC_COEFFICIENT); + set.add(HEAT_LOSS_CONVECTIVE); return set; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index df620015..4ff6ff7a 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -62,7 +62,7 @@ public ThermalProperties copy() { public void useTheoreticalEstimates(ExperimentalData c) { super.useTheoreticalEstimates(c); if (areThermalPropertiesLoaded()) { - Bi3 = biot(); + Bi3 = radiationBiot(); } } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 75a7ee59..c3691eb6 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -169,6 +169,9 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { public void setHeatLoss(NumericProperty Bi) { requireType(Bi, HEAT_LOSS); this.Bi = (double) Bi.getValue(); + if(areThermalPropertiesLoaded()) { + calculateEmissivity(); + } firePropertyChanged(this, Bi); } @@ -272,6 +275,7 @@ public Set listedKeywords() { set.add(HEAT_LOSS); set.add(DENSITY); set.add(SPECIFIC_HEAT); + set.add(EMISSIVITY); return set; } @@ -285,16 +289,33 @@ public NumericProperty getThermalConductivity() { public void calculateEmissivity() { double newEmissivity = Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN); - var transform = new StickTransform(new Segment(0.01, 1.0)); + var transform = new StickTransform(Segment.boundsFrom(EMISSIVITY)); setEmissivity(derive(EMISSIVITY, transform.transform(newEmissivity)) ); } - public double biot() { + /** + * Calculates the radiative Biot number. + * @return the radiative Biot number. + */ + + public double radiationBiot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; } + + /** + * Calculates the maximum Biot number at these conditions, which + * corresponds to an emissivity of unity. If emissivity is non-positive, + * returns the maximum Biot number defined in the XML file. + * @return the maximum Biot number + */ + + public double maxRadiationBiot() { + double absMax = Segment.boundsFrom(HEAT_LOSS).getMaximum(); + return emissivity > 0 ? radiationBiot() / emissivity : absMax; + } /** * Performs simple calculation of the l2/a @@ -320,7 +341,7 @@ public void useTheoreticalEstimates(ExperimentalData c) { final double t0 = c.getHalfTime(); this.a = PARKERS_COEFFICIENT * l * l / t0; if (areThermalPropertiesLoaded()) { - Bi = biot(); + Bi = radiationBiot(); } } diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 62282873..67db88ba 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -19,6 +19,7 @@ import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; import pulse.search.Optimisable; @@ -29,29 +30,33 @@ public class ThermoOpticalProperties extends ThermalProperties implements Optimi private double planckNumber; private double scatteringAlbedo; private double scatteringAnisotropy; + private double convectiveLosses; public ThermoOpticalProperties() { super(); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermalProperties p) { super(p); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermoOpticalProperties p) { super(p); - this.opticalThickness = p.opticalThickness; - this.planckNumber = p.planckNumber; - this.scatteringAlbedo = p.scatteringAlbedo; - this.scatteringAnisotropy = p.scatteringAnisotropy; + this.opticalThickness = p.opticalThickness; + this.planckNumber = p.planckNumber; + this.scatteringAlbedo = p.scatteringAlbedo; + this.scatteringAnisotropy = p.scatteringAnisotropy; + this.convectiveLosses = p.convectiveLosses; } @Override @@ -76,6 +81,9 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { case SCATTERING_ANISOTROPY: setScatteringAnisotropy(value); break; + case HEAT_LOSS_CONVECTIVE: + setConvectiveLosses(value); + break; default: break; } @@ -89,6 +97,7 @@ public Set listedKeywords() { set.add(OPTICAL_THICKNESS); set.add(SCATTERING_ALBEDO); set.add(SCATTERING_ANISOTROPY); + set.add(HEAT_LOSS_CONVECTIVE); return set; } @@ -127,7 +136,17 @@ public void setScatteringAnisotropy(NumericProperty A1) { this.scatteringAnisotropy = (double) A1.getValue(); firePropertyChanged(this, A1); } + + public void setConvectiveLosses(NumericProperty losses) { + requireType(losses, HEAT_LOSS_CONVECTIVE); + this.convectiveLosses = (double) losses.getValue(); + firePropertyChanged(this, losses); + } + public NumericProperty getConvectiveLosses() { + return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); + } + public NumericProperty getScatteringAlbedo() { return derive(SCATTERING_ALBEDO, scatteringAlbedo); } @@ -159,6 +178,7 @@ public String getDescriptor() { @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); + sb.append(String.format("%n %-25s", this.getConvectiveLosses())); sb.append(String.format("%n %-25s", this.getOpticalThickness())); sb.append(String.format("%n %-25s", this.getPlanckNumber())); sb.append(String.format("%n %-25s", this.getScatteringAlbedo())); @@ -182,20 +202,28 @@ public void optimisationVector(ParameterVector output, List flags) { case PLANCK_NUMBER: final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); bounds = new Segment(lowerBound, maxNp()); - value = (double) getPlanckNumber().getValue(); + value = planckNumber; break; case OPTICAL_THICKNESS: - value = (double) getOpticalThickness().getValue(); + value = opticalThickness; bounds = Segment.boundsFrom(OPTICAL_THICKNESS); break; case SCATTERING_ALBEDO: - value = (double) getScatteringAlbedo().getValue(); - bounds = new Segment(0.0, 1.0); + value = scatteringAlbedo; + bounds = Segment.boundsFrom(SCATTERING_ALBEDO); break; case SCATTERING_ANISOTROPY: - value = (double) getScatteringAnisostropy().getValue(); - bounds = new Segment(-1.0, 1.0); + value = scatteringAnisotropy; + bounds = Segment.boundsFrom(SCATTERING_ANISOTROPY); + break; + case HEAT_LOSS_CONVECTIVE: + value = convectiveLosses; + bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); break; + case HEAT_LOSS: + value = (double) getHeatLoss().getValue(); + bounds = new Segment(0.0, maxRadiationBiot() ); + break; default: continue; @@ -223,6 +251,7 @@ public void assign(ParameterVector params) throws SolverException { case SCATTERING_ALBEDO: case SCATTERING_ANISOTROPY: case OPTICAL_THICKNESS: + case HEAT_LOSS_CONVECTIVE: set(type, derive(type, params.inverseTransform(i))); break; default: diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index c9e63cbe..539a832d 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -157,6 +157,13 @@ public enum NumericPropertyKeyword { * sample (1D and 2D problems). */ HEAT_LOSS, + + /** + * The convective heat loss in diathermic and participating medium problems. + */ + + HEAT_LOSS_CONVECTIVE, + /** * A directive for the optimiser to maintain equal heat losses on all * surfaces of the sample. Note that the dimensionless heat losses, i.e. diff --git a/src/main/java/pulse/tasks/processing/ResultFormat.java b/src/main/java/pulse/tasks/processing/ResultFormat.java index 63c7679a..6611a84c 100644 --- a/src/main/java/pulse/tasks/processing/ResultFormat.java +++ b/src/main/java/pulse/tasks/processing/ResultFormat.java @@ -45,16 +45,11 @@ private ResultFormat() { private ResultFormat(List keys) { nameMap = new ArrayList<>(); - for (var key : keys) { - nameMap.add(key); - } - } - - private ResultFormat(ResultFormat fmt) { - nameMap = new ArrayList<>(fmt.nameMap.size()); - nameMap.addAll(fmt.nameMap); + keys.forEach(key -> + nameMap.add(key) + ); } - + public static void addResultFormatListener(ResultFormatListener rfl) { listeners.add(rfl); } diff --git a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java index 3234193c..98491bf7 100644 --- a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java +++ b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java @@ -16,7 +16,7 @@ public class SelectedKeysModel extends DefaultTableModel { * */ private static final long serialVersionUID = 1L; - private List elements; + private final List elements; private final List referenceList; private final NumericPropertyKeyword[] mandatorySelection; @@ -28,7 +28,7 @@ public SelectedKeysModel(List keys, NumericPropertyKeywo update(); } - public void update() { + public final void update() { update(referenceList); } diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index ee1c7d9a..b0554c3a 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -3,19 +3,11 @@ import static javax.swing.SwingConstants.BOTTOM; import java.awt.BorderLayout; -import java.awt.Component; import javax.swing.JDialog; -import javax.swing.JTable; -import javax.swing.JTextArea; import javax.swing.SwingConstants; -import static javax.swing.SwingConstants.SOUTH; -import static javax.swing.SwingConstants.TOP; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; import pulse.tasks.processing.ResultFormat; -import pulse.ui.Messages; import pulse.ui.components.models.ParameterTableModel; import pulse.ui.components.models.SelectedKeysModel; import pulse.ui.components.panels.DoubleTablePanel; @@ -30,7 +22,6 @@ public class ResultChangeDialog extends JDialog { private final static int HEIGHT = 600; public ResultChangeDialog() { - setTitle("Result output formatting"); initComponents(); setSize(WIDTH, HEIGHT); @@ -47,8 +38,6 @@ public void setVisible(boolean value) { } private void initComponents() { - java.awt.GridBagConstraints gridBagConstraints; - MainToolbar = new javax.swing.JToolBar(); filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0)); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index a4293ab2..3d4b61ad 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -180,7 +180,7 @@ visible="true" descriptor="Optical thickness, <i>&tau;</i><sub>0</sub>" dimensionfactor="1" discreet="false" keyword="OPTICAL_THICKNESS" - maximum="100000" minimum="1e-4" primitive-type="double" value="0.1" + maximum="100000" minimum="1e-3" primitive-type="double" value="0.1" default-search-variable="true"> @@ -375,7 +375,7 @@ minimum="1.0" value="0.0" primitive-type="double" discreet="false"/> HEAT_LOSS_COMBINED + + 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 N@O1TaS?83{1OQJbFdhH^ literal 67179 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

}zI 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 From 94e20181f11794866e9567ed787d0088e53368d6 Mon Sep 17 00:00:00 2001 From: Artem Lunev <53570328+kotik-coder@users.noreply.github.com> Date: Sun, 26 Jun 2022 13:45:40 +0300 Subject: [PATCH 097/116] Update README.md Added references --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index f61d167c..29b5b111 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ Click [here](https://www.draw.io/?lightbox=1&highlight=0000ff&edit=_blank ## More info +This software is citable with the following DOI: + +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6622739.svg)](https://doi.org/10.5281/zenodo.6622739) + +For a Journal reference, please use: + +[Lunev, A. et al. Software Impacts 6 (2020) 100044](https://doi.org/10.1016/j.simpa.2020.100044) + Please refer to the software [GitHub Page](https://kotik-coder.github.io/) for a [quickstart guide](https://kotik-coder.github.io/PULsE_Quickstart_Guide.pdf), input file examples and download links. ## Buy me a coffee From 54c5b7ab988b255a4e02debfa5cddd3655cf8a7a Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:24:57 +0300 Subject: [PATCH 098/116] v1.97 --- src/main/java/pulse/AbstractData.java | 2 +- src/main/java/pulse/DiscreteInput.java | 45 ++ src/main/java/pulse/HeatingCurve.java | 31 +- src/main/java/pulse/Response.java | 25 + .../pulse/baseline/AdjustableBaseline.java | 69 ++- src/main/java/pulse/baseline/Baseline.java | 99 +--- .../java/pulse/baseline/FlatBaseline.java | 22 +- .../java/pulse/baseline/LinearBaseline.java | 95 +-- .../pulse/baseline/SinusoidalBaseline.java | 461 ++++++++++----- .../java/pulse/input/ExperimentalData.java | 198 ++----- src/main/java/pulse/input/IndexRange.java | 27 +- src/main/java/pulse/input/Range.java | 84 ++- .../io/export/ResidualStatisticExporter.java | 10 +- .../pulse/io/readers/NetzschCSVReader.java | 101 ++-- src/main/java/pulse/math/FFTTransformer.java | 136 +++++ src/main/java/pulse/math/Harmonic.java | 266 +++++++++ src/main/java/pulse/math/Parameter.java | 91 +++ .../java/pulse/math/ParameterIdentifier.java | 53 ++ src/main/java/pulse/math/ParameterVector.java | 248 +++----- src/main/java/pulse/math/Segment.java | 2 + src/main/java/pulse/math/Window.java | 60 ++ src/main/java/pulse/math/ZScore.java | 134 +++++ .../math/filters/AssignmentListener.java | 7 + src/main/java/pulse/math/filters/Filter.java | 14 + .../math/filters/HalfTimeCalculator.java | 95 +++ .../math/filters/OptimisablePolyline.java | 62 ++ .../math/filters/OptimisedRunningAverage.java | 26 + .../pulse/math/filters/PolylineOptimiser.java | 90 +++ .../java/pulse/math/filters/Randomiser.java | 26 + .../pulse/math/filters/RunningAverage.java | 131 +++++ src/main/java/pulse/math/linear/Vector.java | 5 +- .../math/transforms/InvLenSqTransform.java | 27 - .../math/transforms/InvLenTransform.java | 26 - .../math/transforms/PeriodicTransform.java | 38 ++ .../transforms/StandardTransformations.java | 3 + .../pulse/math/transforms/StickTransform.java | 2 - .../pulse/problem/laser/DiscretePulse.java | 9 +- .../pulse/problem/laser/NumericPulse.java | 6 +- .../pulse/problem/laser/NumericPulseData.java | 21 +- .../schemes/CoupledImplicitScheme.java | 4 +- .../problem/schemes/DifferenceScheme.java | 9 +- .../problem/schemes/FixedPointIterations.java | 27 +- .../pulse/problem/schemes/ImplicitScheme.java | 4 +- .../schemes/TridiagonalMatrixAlgorithm.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 4 +- .../solvers/ImplicitDiathermicSolver.java | 4 +- .../solvers/ImplicitLinearisedSolver.java | 16 +- .../solvers/ImplicitTwoTemperatureSolver.java | 226 ++++++++ .../schemes/solvers/SolverException.java | 23 +- .../problem/statements/ClassicalProblem.java | 22 +- .../statements/ClassicalProblem2D.java | 27 +- .../problem/statements/DiathermicMedium.java | 32 +- .../problem/statements/NonlinearProblem.java | 25 +- .../statements/ParticipatingMedium.java | 6 +- .../statements/PenetrationProblem.java | 15 +- .../pulse/problem/statements/Problem.java | 115 ++-- .../java/pulse/problem/statements/Pulse.java | 10 +- .../statements/TwoTemperatureModel.java | 176 ++++++ .../statements/model/AbsorptionModel.java | 30 +- .../model/BeerLambertAbsorption.java | 9 + .../pulse/problem/statements/model/Gas.java | 75 +++ .../problem/statements/model/Helium.java | 14 + .../problem/statements/model/Insulator.java | 14 + .../problem/statements/model/Nitrogen.java | 14 + .../statements/model/ThermalProperties.java | 11 +- .../model/ThermoOpticalProperties.java | 21 +- .../model/TwoTemperatureProperties.java | 110 ++++ src/main/java/pulse/properties/Flag.java | 12 +- .../pulse/properties/NumericProperty.java | 3 - .../properties/NumericPropertyKeyword.java | 34 +- src/main/java/pulse/search/GeneralTask.java | 217 +++++++ src/main/java/pulse/search/Optimisable.java | 10 +- .../pulse/search/SimpleOptimisationTask.java | 93 +++ .../java/pulse/search/SimpleResponse.java | 35 ++ .../pulse/search/direction/ActiveFlags.java | 56 +- .../pulse/search/direction/BFGSOptimiser.java | 4 +- .../pulse/search/direction/ComplexPath.java | 7 +- .../direction/CompositePathOptimiser.java | 27 +- .../direction/GradientBasedOptimiser.java | 77 ++- .../search/direction/GradientGuidedPath.java | 11 +- .../direction/HessianDirectionSolver.java | 3 +- .../search/direction/IterativeState.java | 5 + .../pulse/search/direction/LMOptimiser.java | 75 +-- .../java/pulse/search/direction/LMPath.java | 6 +- .../pulse/search/direction/PathOptimiser.java | 10 +- .../pulse/search/direction/SR1Optimiser.java | 3 +- .../direction/SteepestDescentOptimiser.java | 9 +- .../direction/pso/ConstrictionMover.java | 49 ++ .../pulse/search/direction/pso/FIPSMover.java | 33 +- .../pulse/search/direction/pso/Mover.java | 4 +- .../pulse/search/direction/pso/Particle.java | 5 +- .../search/direction/pso/ParticleState.java | 31 +- .../direction/pso/ParticleSwarmOptimiser.java | 84 +-- .../direction/pso/StaticTopologies.java | 2 +- .../search/direction/pso/SwarmState.java | 36 +- .../search/linear/GoldenSectionOptimiser.java | 12 +- .../pulse/search/linear/LinearOptimiser.java | 33 +- .../pulse/search/linear/WolfeOptimiser.java | 10 +- .../search/statistics/AbsoluteDeviations.java | 9 +- .../statistics/AndersonDarlingTest.java | 10 +- .../search/statistics/CorrelationTest.java | 2 +- .../pulse/search/statistics/EmptyTest.java | 6 +- .../java/pulse/search/statistics/FTest.java | 16 - .../java/pulse/search/statistics/KSTest.java | 9 +- .../statistics/ModelSelectionCriterion.java | 10 +- .../search/statistics/NormalityTest.java | 3 +- .../pulse/search/statistics/RSquaredTest.java | 38 +- .../RangePenalisedLeastSquares.java | 60 ++ .../statistics/RegularisedLeastSquares.java | 24 +- .../search/statistics/ResidualStatistic.java | 110 ++-- .../pulse/search/statistics/Statistic.java | 7 +- .../pulse/search/statistics/SumOfSquares.java | 10 +- src/main/java/pulse/tasks/Calculation.java | 41 +- src/main/java/pulse/tasks/SearchTask.java | 542 ++++++++---------- src/main/java/pulse/tasks/TaskManager.java | 58 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 14 +- .../java/pulse/tasks/logs/DataLogEntry.java | 37 +- src/main/java/pulse/tasks/logs/Log.java | 61 +- src/main/java/pulse/tasks/logs/Status.java | 20 +- .../java/pulse/tasks/processing/Buffer.java | 15 +- .../tasks/processing/CorrelationBuffer.java | 55 +- .../java/pulse/tasks/processing/Result.java | 3 +- src/main/java/pulse/ui/Launcher.java | 2 +- .../pulse/ui/components/CalculationTable.java | 16 +- src/main/java/pulse/ui/components/Chart.java | 33 +- .../java/pulse/ui/components/DataLoader.java | 8 +- .../java/pulse/ui/components/LogPane.java | 2 +- .../java/pulse/ui/components/ProblemTree.java | 7 +- .../pulse/ui/components/PulseMainMenu.java | 4 +- .../pulse/ui/components/RangeTextFields.java | 29 +- .../pulse/ui/components/ResidualsChart.java | 4 +- .../java/pulse/ui/components/ResultTable.java | 63 +- .../pulse/ui/components/TaskPopupMenu.java | 19 +- .../components/buttons/ExecutionButton.java | 4 +- .../controllers/InstanceCellEditor.java | 6 +- .../components/models/ResultTableModel.java | 9 +- .../ui/components/models/TaskTableModel.java | 11 +- .../ui/components/panels/ChartToolbar.java | 10 +- .../ui/components/panels/ProblemToolbar.java | 8 +- .../java/pulse/ui/frames/HistogramFrame.java | 5 +- .../java/pulse/ui/frames/MainGraphFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 154 +++-- .../pulse/ui/frames/SearchOptionsFrame.java | 10 +- .../pulse/ui/frames/TaskControlFrame.java | 8 +- src/main/java/pulse/util/Group.java | 6 +- .../java/pulse/util/UpwardsNavigable.java | 11 +- 147 files changed, 4497 insertions(+), 1989 deletions(-) create mode 100644 src/main/java/pulse/DiscreteInput.java create mode 100644 src/main/java/pulse/Response.java create mode 100644 src/main/java/pulse/math/FFTTransformer.java create mode 100644 src/main/java/pulse/math/Harmonic.java create mode 100644 src/main/java/pulse/math/Parameter.java create mode 100644 src/main/java/pulse/math/ParameterIdentifier.java create mode 100644 src/main/java/pulse/math/Window.java create mode 100644 src/main/java/pulse/math/ZScore.java create mode 100644 src/main/java/pulse/math/filters/AssignmentListener.java create mode 100644 src/main/java/pulse/math/filters/Filter.java create mode 100644 src/main/java/pulse/math/filters/HalfTimeCalculator.java create mode 100644 src/main/java/pulse/math/filters/OptimisablePolyline.java create mode 100644 src/main/java/pulse/math/filters/OptimisedRunningAverage.java create mode 100644 src/main/java/pulse/math/filters/PolylineOptimiser.java create mode 100644 src/main/java/pulse/math/filters/Randomiser.java create mode 100644 src/main/java/pulse/math/filters/RunningAverage.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenSqTransform.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenTransform.java create mode 100644 src/main/java/pulse/math/transforms/PeriodicTransform.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java create mode 100644 src/main/java/pulse/problem/statements/TwoTemperatureModel.java create mode 100644 src/main/java/pulse/problem/statements/model/Gas.java create mode 100644 src/main/java/pulse/problem/statements/model/Helium.java create mode 100644 src/main/java/pulse/problem/statements/model/Nitrogen.java create mode 100644 src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java create mode 100644 src/main/java/pulse/search/GeneralTask.java create mode 100644 src/main/java/pulse/search/SimpleOptimisationTask.java create mode 100644 src/main/java/pulse/search/SimpleResponse.java create mode 100644 src/main/java/pulse/search/direction/pso/ConstrictionMover.java create mode 100644 src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index 1fd62645..a1d276c7 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -306,4 +306,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java new file mode 100644 index 00000000..c6bb8d85 --- /dev/null +++ b/src/main/java/pulse/DiscreteInput.java @@ -0,0 +1,45 @@ +package pulse; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.input.IndexRange; +import pulse.math.Segment; + +public interface DiscreteInput { + + public List getX(); + public List getY(); + public IndexRange getIndexRange(); + + public static List convert(double[] x, double[] y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.length; i < size; i++) { + ps.add(new Point2D.Double(x[i], y[i])); + } + + return ps; + + } + + public static List convert(List x, List y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.size(); i < size; i++) { + ps.add(new Point2D.Double(x.get(i), y.get(i))); + } + + return ps; + + } + + public default Segment bounds() { + var ir = getIndexRange(); + var x = getX(); + return new Segment(x.get(ir.getLowerBound()), x.get(ir.getUpperBound())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 8fb0f556..86e14621 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -10,6 +10,7 @@ import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.ArrayList; +import static java.util.Collections.max; import java.util.List; import java.util.Set; @@ -48,8 +49,8 @@ public class HeatingCurve extends AbstractData { private final List listeners = new ArrayList<>(); - private UnivariateInterpolator splineInterpolator; - private UnivariateFunction splineInterpolation; + private UnivariateInterpolator interpolator; + private UnivariateFunction interpolation; protected HeatingCurve(List time, List signal, final double startTime, String name) { super(time, name); @@ -64,7 +65,7 @@ protected HeatingCurve(List time, List signal, final double star public HeatingCurve() { super(); adjustedSignal = new ArrayList<>((int) this.getNumPoints().getValue()); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } /** @@ -78,8 +79,8 @@ public HeatingCurve(HeatingCurve c) { super(c); this.adjustedSignal = new ArrayList<>(c.adjustedSignal); this.startTime = c.startTime; - splineInterpolator = new SplineInterpolator(); - if (c.splineInterpolation != null) { + interpolator = new SplineInterpolator(); + if (c.interpolation != null) { this.refreshInterpolation(); } } @@ -100,7 +101,7 @@ public HeatingCurve(NumericProperty count) { adjustedSignal = new ArrayList<>((int) count.getValue()); startTime = (double) def(TIME_SHIFT).getValue(); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } //TODO @@ -163,7 +164,7 @@ public double signalAt(int index) { */ public void scale(double scale) { final int count = this.actualNumPoints(); - for (int i = 0; i < count; i++) { + for (int i = 0, max = Math.min(count, signal.size()); i < max; i++) { signal.set(i, signal.get(i) * scale); } var dataEvent = new CurveEvent(RESCALED, this); @@ -201,7 +202,8 @@ private void refreshInterpolation() { /* * Submit to spline interpolation */ - splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); + + interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); } /** @@ -346,8 +348,8 @@ public void setTimeShift(NumericProperty startTime) { firePropertyChanged(this, startTime); } - public UnivariateFunction getSplineInterpolation() { - return splineInterpolation; + public UnivariateFunction getInterpolation() { + return interpolation; } public List getBaselineCorrectedData() { @@ -377,5 +379,12 @@ public boolean equals(Object o) { return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); } + + public double interpolateSignalAt(double x) { + double min = this.timeAt(0); + double max = timeLimit(); + return min < x && max > x ? interpolation.value(x) + : (x < min ? signalAt(0) : signalAt(actualNumPoints() - 1)); + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/Response.java b/src/main/java/pulse/Response.java new file mode 100644 index 00000000..cc4f23ce --- /dev/null +++ b/src/main/java/pulse/Response.java @@ -0,0 +1,25 @@ +package pulse; + +import pulse.math.Segment; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.statistics.OptimiserStatistic; + +public interface Response { + + public double evaluate(double t); + public Segment accessibleRange(); + + /** + * Calculates the value of the objective function used to identify + * the current state of the optimiser. + * @param task + * @return the value of the objective function in the current state + * @throws pulse.problem.schemes.solvers.SolverException + */ + + public double objectiveFunction(GeneralTask task) throws SolverException; + + public OptimiserStatistic getOptimiserStatistic(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/AdjustableBaseline.java b/src/main/java/pulse/baseline/AdjustableBaseline.java index bb69ca91..12464c76 100644 --- a/src/main/java/pulse/baseline/AdjustableBaseline.java +++ b/src/main/java/pulse/baseline/AdjustableBaseline.java @@ -1,16 +1,16 @@ package pulse.baseline; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.List; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import pulse.util.PropertyHolder; /** @@ -21,27 +21,31 @@ public abstract class AdjustableBaseline extends Baseline { private double intercept; + private double slope; /** * Creates a flat baseline equal to the argument. * * @param intercept the constant baseline value. */ - public AdjustableBaseline(double intercept) { + public AdjustableBaseline(double intercept, double slope) { this.intercept = intercept; + this.slope = slope; } /** - * @return the constant value of this {@code FlatBaseline} + * Calculates the linear function {@code g(x) = intercept + slope*time} + * + * @param x the argument of the linear function + * @return the result of this simple calculation */ @Override public double valueAt(double x) { - return intercept; + return intercept + x * slope; } protected double mean(List x) { - double sum = x.stream().reduce((a, b) -> a + b).get(); - return sum / x.size(); + return x.stream().mapToDouble(d -> d).average().getAsDouble(); } /** @@ -69,6 +73,29 @@ public void setIntercept(NumericProperty intercept) { firePropertyChanged(this, intercept); } + /** + * Provides getter accessibility to the slope as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop + */ + public NumericProperty getSlope() { + return derive(BASELINE_SLOPE, slope); + } + + /** + * Checks whether {@code slope} is a baseline slope property and updates the + * respective value of this baseline. + * + * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type + * @see set + */ + public void setSlope(NumericProperty slope) { + requireType(slope, BASELINE_SLOPE); + this.slope = (double) slope.getValue(); + firePropertyChanged(this, slope); + } + /** * Lists the {@code intercept} as accessible property for this * {@code FlatBaseline}. @@ -91,13 +118,17 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + + if (p != null) { + + var key = p.getIdentifier().getKeyword(); - var key = output.getIndex(i); + if (key == BASELINE_INTERCEPT) { + p.setValue(intercept); + } - if (key == BASELINE_INTERCEPT) { - output.set(i, intercept, key); } } @@ -106,10 +137,12 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_INTERCEPT) { - setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); + if (p.getIdentifier().getKeyword() == BASELINE_INTERCEPT) { + setIntercept( + derive(BASELINE_INTERCEPT, p.inverseTransform()) + ); } } diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 20c2ab94..d42c31d1 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,15 +1,11 @@ package pulse.baseline; -import static java.lang.Double.NEGATIVE_INFINITY; -import static java.lang.Math.min; - -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import pulse.AbstractData; +import pulse.DiscreteInput; import pulse.input.ExperimentalData; import pulse.input.IndexRange; +import pulse.input.Range; import pulse.search.Optimisable; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -27,6 +23,8 @@ */ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { + public final static int MIN_BASELINE_POINTS = 15; + public abstract Baseline copy(); /** @@ -48,74 +46,10 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti * values, triggering whatever events are associated with them. *

* - * @param x a list of independent variable values - * @param y a list of dependent variable values - * @param size the size of the region + * @param x + * @param y */ - protected abstract void doFit(List x, List y, int size); - - /** - * Selects part of the {@code data} that can be used for baseline estimation - * (typically, this means selecting 'negative' time values and the - * corresponding signal) data and runs the fitting algorithms, - * - * @param data the experimental data - * @param rangeMin the minimum of the time range - * @param rangeMax the maximum of the time range - */ - public void fitTo(ExperimentalData data, double rangeMin, double rangeMax) { - var indexRange = data.getIndexRange(); - - Objects.requireNonNull(indexRange); - - if (!indexRange.isValid()) { - throw new IllegalArgumentException("Index range not valid: " + indexRange); - } - - List x = new ArrayList<>(); - List y = new ArrayList<>(); - - int size = 0; - - for (int i = IndexRange.closestLeft(rangeMin, data.getTimeSequence()) + 1, max = min(indexRange.getLowerBound(), - IndexRange.closestRight(rangeMax, data.getTimeSequence())); i < max; i++, size++) { - - x.add(data.timeAt(i)); - y.add(data.signalAt(i)); - - } - - if (size > 0) // do fitting only if data is present - { - doFit(x, y, size); - } - - } - - /** - * Fit to an abstract set of data, using only the subset corresponding to the negative time range. - * @param data a dataset - */ - - public void fitNegative(AbstractData data) { - final int MIN_POINTS = 15; - - var time = data.getTimeSequence(); - var signal = data.getSignalData(); - - var subsetTime = new ArrayList(); - var subsetSignal = new ArrayList(); - - int i; - - for(i = 0; time.get(i) < 0; i++) { - subsetTime.add(time.get(i)); - subsetSignal.add(signal.get(i)); - } - - if(i > MIN_POINTS) - doFit(subsetTime, subsetSignal, i); - } + protected abstract void doFit(List x, List y); /** * Calls {@code fitTo} using the default time range for the data: @@ -125,9 +59,20 @@ public void fitNegative(AbstractData data) { * @param data the experimental data stretching to negative time values * @see fitTo(ExperimentalData,double,double) */ - public void fitTo(ExperimentalData data) { - final double ZERO_LEFT = -1E-5; - fitTo(data, NEGATIVE_INFINITY, ZERO_LEFT); + public void fitTo(DiscreteInput data) { + var filtered = Range.NEGATIVE.filter(data); + if(filtered[0].size() > MIN_BASELINE_POINTS) { + doFit(filtered[0], filtered[1]); + } + } + + public void fitTo(List x, List y) { + int index = IndexRange.closestLeft(0, x); + var xx = x.subList(0, index + 1); + var yy = y.subList(0, index + 1); + if(xx.size() > MIN_BASELINE_POINTS) { + doFit(xx, yy); + } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 56e1d7cc..9500c8f6 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.baseline; import static java.lang.String.format; @@ -22,7 +7,6 @@ /** * A flat baseline. - * @author Artem Lunev */ public class FlatBaseline extends AdjustableBaseline { @@ -41,12 +25,12 @@ public FlatBaseline() { * @param intercept the constant baseline value. */ public FlatBaseline(double intercept) { - super(intercept); + super(intercept, 0.0); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double intercept = mean(y); set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); } @@ -61,4 +45,4 @@ public String toString() { return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index c6381ebc..d3d899e4 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -2,15 +2,13 @@ import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; @@ -31,43 +29,26 @@ */ public class LinearBaseline extends AdjustableBaseline { - private double slope; - /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. */ public LinearBaseline() { - super(0.0); + super(0.0, 0.0); } - - /** - * A constructor, which allows to specify all three parameters in one go. - * - * @param intercept the intercept is the value of the Baseline's linear - * function at {@code x = 0} - * @param slope the slope determines the inclination angle of the Baseline's - * graph. - */ + public LinearBaseline(double intercept, double slope) { - super(intercept); - this.slope = slope; + super(intercept, slope); } - - /** - * Calculates the linear function {@code g(x) = intercept + slope*time} - * - * @param x the argument of the linear function - * @return the result of this simple calculation - */ - @Override - public double valueAt(double x) { - final double intercept = (double) getIntercept().getValue(); - return intercept + x * slope; + + public LinearBaseline(LinearBaseline baseline) { + super( (double) baseline.getIntercept().getValue(), + (double) baseline.getSlope().getValue() + ); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double meanx = mean(x); double meany = mean(y); @@ -76,46 +57,25 @@ protected void doFit(List x, List y, int size) { double xxbar = 0.0; double xybar = 0.0; - for (int i = 0; i < size; i++) { + for (int i = 0, size = x.size(); i < size; i++) { x1 = x.get(i); y1 = y.get(i); xxbar += (x1 - meanx) * (x1 - meanx); xybar += (x1 - meanx) * (y1 - meany); } - - slope = xybar / xxbar; + + double slope = xybar / xxbar; double intercept = meany - slope * meanx; set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); } - /** - * Provides getter accessibility to the slope as a NumericProperty - * - * @return a NumericProperty derived from - * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop - */ - public NumericProperty getSlope() { - return derive(BASELINE_SLOPE, slope); - } - - /** - * Checks whether {@code slope} is a baseline slope property and updates the - * respective value of this baseline. - * - * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type - * @see set - */ - public void setSlope(NumericProperty slope) { - requireType(slope, BASELINE_SLOPE); - this.slope = (double) slope.getValue(); - firePropertyChanged(this, slope); - } - @Override public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + var slope = getSlope().getValue(); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); } @Override @@ -129,15 +89,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == BASELINE_SLOPE) { - output.set(i, slope, BASELINE_SLOPE); + double slope = (double) getSlope().getValue(); + p.setValue(slope); } } @@ -157,10 +118,12 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_SLOPE) { - setSlope(derive(BASELINE_SLOPE, params.get(i))); + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_SLOPE) { + setSlope( derive(BASELINE_SLOPE, p.inverseTransform() )); } } @@ -180,7 +143,7 @@ public Set listedKeywords() { @Override public Baseline copy() { - return new LinearBaseline((double) this.getIntercept().getValue(), this.slope); + return new LinearBaseline(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index c55ec0ed..9074cdc4 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,197 +1,400 @@ package pulse.baseline; -import static java.lang.Math.sin; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; -import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; -import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.math.FFTTransformer; +import pulse.math.Harmonic; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; - +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; +import pulse.input.Range; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.math.transforms.StickTransform; +import pulse.math.ZScore; +import pulse.math.filters.Filter; +import pulse.math.filters.OptimisedRunningAverage; +import pulse.math.filters.Randomiser; +import pulse.math.filters.RunningAverage; import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.ActiveFlags; +import pulse.search.statistics.SumOfSquares; +import pulse.util.Group; +import static pulse.properties.NumericPropertyKeyword.MAX_HIGH_FREQ_WAVES; +import static pulse.properties.NumericPropertyKeyword.MAX_LOW_FREQ_WAVES; /** - * A simple sinusoidal baseline. - *

- * It is given by the expression y = y0 + - * A sin(2πf t + φ) , where f is the - * frequency (in Hz), A is the amplitude, φ is the phase shift. - * Extends the {@code FlatBaseline} class and thus inherits the - * {@code BASELINE_INTERCEPT} property. The sinusoidal baseline is useful to - * mitigate electromagnetic interferences, with the frequencies usually in the - * range of 25 to 60 Hz. - *

+ * A multiple-harmonic baseline. Replaces the Sinusoidal baseline in previous + * version. * */ -public class SinusoidalBaseline extends AdjustableBaseline { +public class SinusoidalBaseline extends LinearBaseline { + + private List hiFreq; + private List loFreq; + private List active; - private double frequency; - private double phaseShift; - private double amplitude; - private final static double _2PI = 2.0 * Math.PI; + private int maxHighFreqHarmonics; + private int maxLowFreqHarmonics; + + private final static double FREQUENCY_THRESHOLD = 400; /** * Creates a sinusoidal baseline with default properties. */ public SinusoidalBaseline() { - super(0.0); - setFrequency(def(BASELINE_FREQUENCY)); - setAmplitude(def(BASELINE_AMPLITUDE)); - setPhaseShift(def(BASELINE_PHASE_SHIFT)); + super(0.0, 0.0); + maxHighFreqHarmonics = (int) def(MAX_HIGH_FREQ_WAVES).getValue(); + maxLowFreqHarmonics = (int) def(MAX_LOW_FREQ_WAVES).getValue(); + hiFreq = new ArrayList<>(); + active = new ArrayList<>(); + loFreq = new ArrayList<>(); } @Override public double valueAt(double x) { - var intercept = (double) getIntercept().getValue(); - return intercept + amplitude * sin(_2PI * x * frequency + phaseShift); + return super.valueAt(x) + + active.stream().mapToDouble(h -> h.valueAt(x)).sum(); } - /** - * Listed properties include the frequency, amplitude, phase shift, and - * intercept. - */ @Override - public Set listedKeywords() { - var set = super.listedKeywords(); - set.add(BASELINE_FREQUENCY); - set.add(BASELINE_AMPLITUDE); - set.add(BASELINE_PHASE_SHIFT); - return set; + public Baseline copy() { + var baseline = new SinusoidalBaseline(); + baseline.setIntercept(this.getIntercept()); + baseline.setSlope(this.getSlope()); + baseline.hiFreq = new ArrayList<>(); + baseline.maxHighFreqHarmonics = this.maxHighFreqHarmonics; + baseline.maxLowFreqHarmonics = this.maxLowFreqHarmonics; + for (Harmonic h : active) { + var newH = new Harmonic(h); + baseline.active.add(newH); + newH.setParent(baseline); + } + for (Harmonic h : hiFreq) { + baseline.hiFreq.add(new Harmonic(h)); + } + for (Harmonic h : loFreq) { + baseline.loFreq.add(new Harmonic(h)); + } + return baseline; } @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + active.forEach(h -> h.optimisationVector(output) ); + } - switch (type) { - case BASELINE_FREQUENCY: - setFrequency(property); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(property); - break; - case BASELINE_AMPLITUDE: - setAmplitude(property); - break; - default: - super.set(type, property); + @Override + public void assign(ParameterVector output) { + super.assign(output); + active.forEach(h + -> h.assign(output) + ); + } + + private void guessHarmonics(double[] x, double[] y) { + var fft = new FFTTransformer(y); + fft.transform(); + double[] sampling = fft.sampling(x); + + var amplitude = fft.getAmpltiudeSpectrum(); + var phase = fft.getPhaseSpectrum(); + + var zscore = new ZScore(); + zscore.process(amplitude); + + var signals = zscore.getSignals(); + double maxAmp = 0; + + hiFreq = new ArrayList<>(); + + double span = x[x.length - 1] - x[0]; + double lowerFrequency = 4.0 / span; + + for (int i = 0; i < sampling.length; i++) { + if (signals[i] > 0) { + if (sampling[i] < FREQUENCY_THRESHOLD && sampling[i] > lowerFrequency) { + var h = new Harmonic(amplitude[i], sampling[i], phase[i]); + hiFreq.add(h); + maxAmp = Math.max(maxAmp, amplitude[i]); + } + } } + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); } - public NumericProperty getFrequency() { - return derive(BASELINE_FREQUENCY, frequency); + private List sort(List hs, int limit) { + var tmp = new ArrayList<>(hs); + tmp.sort(null); + Collections.reverse(tmp); + //leave out a maximum of n harmonics + return tmp.subList(0, Math.min(tmp.size(), limit)); } - public NumericProperty getAmplitude() { - return derive(BASELINE_AMPLITUDE, amplitude); + private void labelActive() { + for (int i = 0, size = active.size(); i < size; i++) { + active.get(i).setRank(i); + active.get(i).setParent(this); + } } - public NumericProperty getPhaseShift() { - return derive(BASELINE_PHASE_SHIFT, phaseShift); - } + private void fitHarmonics(DiscreteInput input) { - public void setFrequency(NumericProperty frequency) { - requireType(frequency, BASELINE_FREQUENCY); - this.frequency = (double) frequency.getValue(); - firePropertyChanged(this, frequency); - } + var sos = new SumOfSquares() { - public void setAmplitude(NumericProperty amplitude) { - requireType(amplitude, BASELINE_AMPLITUDE); - this.amplitude = (double) amplitude.getValue(); - firePropertyChanged(this, amplitude); - } + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + + SimpleResponse response = new SimpleResponse(sos) { + + @Override + public double evaluate(double t) { + return valueAt(t); + } + + }; + + var task = new SimpleOptimisationTask(this, input) { + + @Override + public Response getResponse() { + return response; + } + + }; + + //adjust optimisation flags + var flagList = new ArrayList(); + flagList.add(new Flag(BASELINE_AMPLITUDE, false)); + flagList.add(new Flag(BASELINE_FREQUENCY, true)); + flagList.add(new Flag(BASELINE_PHASE_SHIFT, true)); + flagList.add(new Flag(BASELINE_INTERCEPT, false)); + flagList.add(new Flag(BASELINE_SLOPE, true)); + + var oldState = ActiveFlags.storeState(); + ActiveFlags.loadState(flagList); + + CompletableFuture.runAsync(task).thenRun(() -> { + flagList.stream().filter(f -> f.getType() == BASELINE_AMPLITUDE) + .findFirst().get().setValue(true); + task.run(); + ActiveFlags.loadState(oldState); + } + ); - public void setPhaseShift(NumericProperty phaseShift) { - requireType(phaseShift, BASELINE_PHASE_SHIFT); - this.phaseShift = (double) phaseShift.getValue(); - firePropertyChanged(this, phaseShift); } /** - * The optimisation vector can include the amplitude, frequency and phase - * shift of a sinusoid, and a baseline intercept value of the superclass. + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case BASELINE_FREQUENCY: - output.set(i, frequency, BASELINE_FREQUENCY); - break; - case BASELINE_PHASE_SHIFT: - output.set(i, phaseShift, BASELINE_PHASE_SHIFT); - break; - case BASELINE_AMPLITUDE: - output.set(i, amplitude, BASELINE_AMPLITUDE); - break; - default: - continue; + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAX_HIGH_FREQ_WAVES); + set.add(MAX_LOW_FREQ_WAVES); + return set; + } + + @Override + public List subgroups() { + return getHarmonics() == null ? new ArrayList<>() + : getHarmonics().stream().map(h -> (Group) h).collect(Collectors.toList()); + } + + public List getHarmonics() { + return active; + } + + public NumericProperty getHiFreqMax() { + return derive(MAX_HIGH_FREQ_WAVES, maxHighFreqHarmonics); + } + + public void setHiFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_HIGH_FREQ_WAVES); + int oldValue = this.maxHighFreqHarmonics; + + if ((int) maxHarmonics.getValue() != oldValue) { + + var lowFreq = new ArrayList(); + int size = active.size(); + + if(maxHighFreqHarmonics < size) { + lowFreq = new ArrayList<>(active.subList(maxHighFreqHarmonics, size)); } - output.setTransform(i, new StickTransform(output.getParameterBounds(i))); - + this.maxHighFreqHarmonics = (int) maxHarmonics.getValue(); + active.clear(); + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); + active.addAll(lowFreq); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } + + } + + public NumericProperty getLowFreqMax() { + return derive(MAX_LOW_FREQ_WAVES, maxLowFreqHarmonics); + } + public void setLowFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_LOW_FREQ_WAVES); + int oldValue = this.maxLowFreqHarmonics; + if ((int) maxHarmonics.getValue() != oldValue) { + this.maxLowFreqHarmonics = (int) maxHarmonics.getValue(); + active = active.subList(0, maxHighFreqHarmonics); + active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } } @Override - public void assign(ParameterVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - switch (params.getIndex(i)) { - case BASELINE_FREQUENCY: - setFrequency(derive(BASELINE_FREQUENCY, params.inverseTransform(i))); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.inverseTransform(i))); - break; - case BASELINE_AMPLITUDE: - setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i))); - break; - default: - break; - } + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + switch (type) { + + case MAX_HIGH_FREQ_WAVES: + setHiFreqMax(property); + break; + case MAX_LOW_FREQ_WAVES: + setLowFreqMax(property); + break; + default: } } @Override - public Baseline copy() { - var baseline = new SinusoidalBaseline(); - baseline.setIntercept(this.getIntercept()); - baseline.amplitude = this.amplitude; - baseline.frequency = this.frequency; - baseline.phaseShift = this.phaseShift; - return baseline; + public void fitTo(DiscreteInput input) { + //fit the linear part first + super.fitTo(input); + //then fit the harmonics -- full range is needed here + + DiscreteInputImpl filtered = (DiscreteInputImpl) filter(input); + + var x = filtered.getXasArray(); + var y = filtered.getYasArray(); + + active.clear(); + guessHarmonics(x, y); + labelActive(); + fitHarmonics(new DiscreteInputImpl(x, y)); + addLowFreq(input); + labelActive(); } - @Override - protected void doFit(List x, List y, int size) { - var flatBaseline = new FlatBaseline(); - flatBaseline.doFit(x, y, size); - //TODO Fourier transform + private DiscreteInput filter(DiscreteInput full) { + var x = full.getX().stream().mapToDouble(d -> d).toArray(); + var y = full.getY().stream().mapToDouble(d -> d).toArray(); + + Filter f = new OptimisedRunningAverage(); + Filter fr = new Randomiser(1.0); + var runningAverage = fr.process(f.process(full)); + + var xAv = runningAverage.stream().mapToDouble(p -> p.getX()).toArray(); + var yAv = runningAverage.stream().mapToDouble(p -> p.getY()).toArray(); + + var spline = new SplineInterpolator(); + var interp = spline.interpolate(xAv, yAv); + + for (int i = 0; i < x.length; i++) { + y[i] -= interp.value(x[i]); + //System.err.println(x[i] + " " + interp.value(x[i]) + " " + y[i]); + } + + return new DiscreteInputImpl(x, y); + + } + + private void addLowFreq(DiscreteInput input) { + double amp = !hiFreq.isEmpty() + ? (double) hiFreq.get(0).getAmplitude().getValue() + : Collections.max(input.getY()) / 2.0; + + double span = input.getX().get(input.getX().size() - 1) - input.getX().get(0); + double freq = RunningAverage.DEFAULT_BINS / span; + + loFreq.clear(); + + /* + These harmonics are inaccessible by FFT + */ + + for (double f = freq; f > 1.0 / (2.0 * span); f /= 2.0) { + loFreq.add(new Harmonic(amp, f, 0.0)); + } + + active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); } @Override public String toString() { return getClass().getSimpleName(); - } + } + + private class DiscreteInputImpl implements DiscreteInput { + + private final double[] x; + private final double[] y; + + public DiscreteInputImpl(double[] x, double[] y) { + this.x = x; + this.y = y; + } + + @Override + public List getX() { + return convert(x); + } + + @Override + public List getY() { + return convert(y); + } + + public double[] getXasArray() { + return x; + } + + public double[] getYasArray() { + return y; + } + + private List convert(double[] a) { + return DoubleStream.of(a).boxed().collect(Collectors.toList()); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(getX(), Range.UNLIMITED); + } + } } diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 15776db3..f38a8e0a 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -1,25 +1,20 @@ package pulse.input; -import static java.lang.Double.valueOf; -import static java.util.Collections.max; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; -import java.awt.geom.Point2D; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import pulse.AbstractData; -import pulse.baseline.FlatBaseline; +import pulse.DiscreteInput; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; import pulse.input.listeners.DataListener; -import pulse.ui.Messages; +import pulse.math.filters.HalfTimeCalculator; import pulse.util.PropertyHolderListener; /** @@ -30,13 +25,13 @@ * {@code CurveReader}s. Any manipulation (e.g. truncation) of the data triggers * an event associated with this {@code ExperimentalData}. */ -public class ExperimentalData extends AbstractData { +public class ExperimentalData extends AbstractData implements DiscreteInput { + private HalfTimeCalculator calculator; private Metadata metadata; private IndexRange indexRange; private Range range; private List dataListeners; - private double halfTime; /** * This is the cutoff factor which is used as a criterion for data @@ -45,22 +40,6 @@ public class ExperimentalData extends AbstractData { */ public final static double CUTOFF_FACTOR = 7.2; - /** - * The binning factor used to build a crude approximation of the heating - * curve. Described in Lunev, A., & Heymer, R. (2020). Review of - * Scientific Instruments, 91(6), 064902. - */ - public final static int REDUCTION_FACTOR = 32; - - public final static int MAX_REDUCTION_FACTOR = 256; - - /** - * A fail-safe factor. - */ - public final static double FAIL_SAFE_FACTOR = 10.0; - - private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); - /** * Constructs an {@code ExperimentalData} object using the superclass * constructor and creating a new list of data listeners. The number of @@ -73,9 +52,12 @@ public ExperimentalData() { dataListeners = new ArrayList<>(); setPrefix("RawData"); setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(); - this.addDataListener((DataEvent e) -> calculateHalfTime() ); - + indexRange = new IndexRange(0,0); + this.addDataListener((DataEvent e) -> { + if (e.getType() == DataEventType.DATA_LOADED) { + preprocess(); + } + }); } public final void addDataListener(DataListener listener) { @@ -127,127 +109,6 @@ public void addPoint(double time, double signal) { incrementCount(); } - /** - * Constructs a deliberately crude representation of this heating curve by - * calculating a running average. - *

- * This is done using a binning algorithm, which will group the - * time-temperature data associated with this {@code ExperimentalData} in - * {@code count/reductionFactor - 1} bins, calculate the average value for - * time and temperature within each bin, and collect those values in a - * {@code List}. This is useful to cancel out the effect of signal - * outliers, e.g. when calculating the half-rise time. - *

- * - * The algorithm is described in more detail in Lunev, A., & Heymer, R. - * (2020). Review of Scientific Instruments, 91(6), 064902. - * - * @param reductionFactor the factor, by which the number of points - * {@code count} will be reduced for this {@code ExperimentalData}. - * @return a {@code List}, representing the degraded - * {@code ExperimentalData}. - * @see halfRiseTime() - * @see pulse.AbstractData.maxTemperature() - */ - public List runningAverage(int reductionFactor) { - - int count = (int) getNumPoints().getValue(); - - List crudeAverage = new ArrayList<>(count / reductionFactor); - - int start = indexRange.getLowerBound(); - int end = indexRange.getUpperBound(); - - int step = (end - start) / (count / reductionFactor); - double av = 0; - - int i1, i2; - - for (int i = 0, max = (count / reductionFactor) - 1; i < max; i++) { - i1 = start + step * i; - i2 = i1 + step; - - av = 0; - - for (int j = i1; j < i2; j++) { - av += signalAt(j); - } - - av /= step; - - crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); - - } - - return crudeAverage; - - } - - /** - * Instead of returning the simple maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the maximum of the - * {@code runningAverage} using the default reduction factor - * {@value REDUCTION_FACTOR}. - * - * @return a {@code Point2D} object containing the coordinates of the - * adjusted maximum. - * @see - * pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) - */ - public Point2D maxAdjustedSignal() { - var degraded = runningAverage(REDUCTION_FACTOR); - return max(degraded, pointComparator); - } - - /** - * Calculates the approximate half-rise time used for crude estimation of - * thermal diffusivity. - *

- * This uses the {@code runningAverage} method by applying the default - * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based - * on finding the approximate value corresponding to the half-maximum of the - * temperature. The latter is calculated using the running average curve. - * The index corresponding to the closest temperature value available for - * that curve is used to retrieve the half-rise time (which also has the - * same index). If this fails, i.e. the associated index is less than 1, - * this will print out a warning message and still assign a value to the - * half-time variable equal to the acquisition time divided by a fail-safe factor - * {@value FAIL_SAFE_FACTOR}. - *

- * @see getHalfTime() - */ - public void calculateHalfTime() { - var baseline = new FlatBaseline(); - baseline.fitTo(this); - - int curRedFactor = REDUCTION_FACTOR/2; // reduced twofold since first operation - // in the while loop will increase it likewise - int cutoffIndex = 0; - List degraded = null; //running average - Point2D max = null; - - do { - curRedFactor *= 2; - degraded = runningAverage(curRedFactor); - max = (max(degraded, pointComparator)); - cutoffIndex = degraded.indexOf(max); - } while(cutoffIndex < 1 && curRedFactor < MAX_REDUCTION_FACTOR); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - degraded = degraded.subList(0, cutoffIndex); - - int index = IndexRange.closestLeft(halfMax, - degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - - if (index < 1) { - System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); - halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; - } - else - halfTime = degraded.get(index).getX(); - - } - /** * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. * @@ -284,7 +145,7 @@ public boolean equals(Object o) { * threshold, {@code false} otherwise. */ public boolean isAcquisitionTimeSensible() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); final int count = (int) getNumPoints().getValue(); double d = getTimeSequence().get(count - 1); return getTimeSequence().get(count - 1) < cutoff; @@ -306,7 +167,7 @@ public boolean isAcquisitionTimeSensible() { * @see fireDataChanged */ public void truncate() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); } @@ -335,16 +196,6 @@ private void doSetMetadata() { } } - - /** - * Retrieves the half-time value of this dataset, which is equal to the - * time needed to reach half of the signal maximum. - * @return the half-time value. - */ - - public double getHalfTime() { - return halfTime; - } /** * Gets the time sequence element corresponding to the lower bound of the @@ -382,6 +233,7 @@ public Range getRange() { * * @return the index range */ + @Override public IndexRange getIndexRange() { return indexRange; } @@ -422,6 +274,28 @@ private void doSetRange() { @Override public double timeLimit() { return timeAt(indexRange.getUpperBound()); - } + } + + public HalfTimeCalculator getHalfTimeCalculator() { + return calculator; + } + + public void preprocess() { + if (calculator == null) { + calculator = new HalfTimeCalculator(this); + } + + calculator.calculate(); + } + + @Override + public List getX() { + return this.getTimeSequence(); + } + + @Override + public List getY() { + return this.getSignalData(); + } } \ No newline at end of file diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index bdf4288d..93a92806 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -20,13 +20,14 @@ public class IndexRange { private int iStart; private int iEnd; - /** - * Construct an empty index range where the start index is set to -1 and the - * end index is set to 0. - */ - public IndexRange() { - iStart = -1; - iEnd = 0; + public IndexRange(IndexRange other) { + iStart = other.iStart; + iEnd = other.iEnd; + } + + public IndexRange(int start, int end) { + this.iStart = start; + this.iEnd = end; } /** @@ -73,7 +74,7 @@ protected void reset(List data) { * @see closestLeft * @see closestRight */ - public void setLowerBound(List data, double a) { + public final void setLowerBound(List data, double a) { iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); } @@ -90,7 +91,7 @@ public void setLowerBound(List data, double a) { * @see closestLeft * @see closestRight */ - public void setUpperBound(List data, double b) { + public final void setUpperBound(List data, double b) { iEnd = closestRight(b, data); } @@ -104,9 +105,9 @@ public void setUpperBound(List data, double b) { * @see setLowerBound * @see setUpperBound */ - public void set(List data, Range range) { + public final void set(List data, Range range) { var segment = range.getSegment(); - setLowerBound(data, Math.max(0.0, segment.getMinimum())); + setLowerBound(data, segment.getMinimum()); setUpperBound(data, segment.getMaximum()); } @@ -116,7 +117,7 @@ public void set(List data, Range range) { * * @return the start index */ - public int getLowerBound() { + public final int getLowerBound() { return iStart; } @@ -126,7 +127,7 @@ public int getLowerBound() { * * @return the end index */ - public int getUpperBound() { + public final int getUpperBound() { return iEnd; } diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index ab95fa08..fd6e4e94 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -1,6 +1,7 @@ package pulse.input; import static java.lang.Math.max; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -10,6 +11,8 @@ import java.util.List; import java.util.Set; +import pulse.DiscreteInput; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -30,7 +33,11 @@ public class Range extends PropertyHolder implements Optimisable { private Segment segment; - + + public final static Range UNLIMITED = new Range (Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + public final static Range NEGATIVE = new Range(Double.NEGATIVE_INFINITY, -1E-16); + public final static Range POSITIVE = new Range(1e-16, Double.POSITIVE_INFINITY); + /** * Constructs a {@code Range} from the minimum and maximum values of * {@code data}. @@ -53,6 +60,45 @@ public Range(List data) { public Range(double a, double b) { this.segment = new Segment(a, b); } + + /** + * Contains a data double array ([0] - x, [1] - y), + * where the data points have been filtered so that + * each x fits into this range. + * @param input + * @return a [2][...] array containing filtered x and y values + */ + + public List[] filter(DiscreteInput input) { + var x = input.getX(); + var y = input.getY(); + + if(x.size() != y.size()) { + throw new IllegalArgumentException("x.length != y.length"); + } + + var xf = new ArrayList(); + var yf = new ArrayList(); + + double min = segment.getMinimum(); + double max = segment.getMaximum(); + + final double eps = 1E-10; + + for(int i = 0, size = x.size(); i < size; i++) { + + if(x.get(i) > min && x.get(i) < max + eps) { + + xf.add(x.get(i)); + yf.add(y.get(i)); + + } + + } + + return new List[]{xf, yf}; + + } /** * Resets the minimum and maximum values of this range to those specified by @@ -181,7 +227,7 @@ public Segment boundLimits(boolean isUpperBound) { var curve = (ExperimentalData) this.getParent(); var seq = curve.getTimeSequence(); - double tHalf = curve.getHalfTime(); + double tHalf = curve.getHalfTimeCalculator().getHalfTime(); Segment result = null; if(isUpperBound) @@ -201,25 +247,25 @@ public Segment boundLimits(boolean isUpperBound) { * absolute constraints equal to a fourth of their values. * * @param output the vector to be updated - * @param flags a list of active flags */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds; - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); + double value; + switch (key) { case UPPER_BOUND: - output.set(i, segment.getMaximum()); bounds = boundLimits(true); + value = segment.getMaximum(); break; case LOWER_BOUND: - output.set(i, segment.getMinimum()); bounds = boundLimits(false); + value = segment.getMinimum(); break; default: continue; @@ -227,8 +273,9 @@ public void optimisationVector(ParameterVector output, List flags) { var transform = new StickTransform(bounds); - output.setParameterBounds(i, bounds); - output.setTransform(i, transform); + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); } @@ -242,18 +289,17 @@ public void optimisationVector(ParameterVector output, List flags) { */ @Override public void assign(ParameterVector params) throws SolverException { - NumericProperty p = null; - - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - p = derive( params.getIndex(i), params.inverseTransform(i) ); + var key = p.getIdentifier().getKeyword(); + var np = derive( key, p.inverseTransform() ); - switch (params.getIndex(i)) { + switch (key) { case UPPER_BOUND: - setUpperBound(p); + setUpperBound(np); break; case LOWER_BOUND: - setLowerBound(p); + setLowerBound(np); break; default: } @@ -267,4 +313,4 @@ public String toString() { return "Range given by: " + segment.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index 1de1e4b6..4430ec50 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -59,6 +59,7 @@ public Extension[] getSupportedExtensions() { private void printHTML(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); stream.print(getString("ResultTableExporter.style")); @@ -71,8 +72,8 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { stream.print(""); for (int i = 0; i < residualsLength; i++) { - double tr = residuals.get(i)[0]; - double Tr = residuals.get(i)[1]; + double tr = time.get(i); + double Tr = residuals.get(i); stream.printf("%n%.8f%.8f", tr, Tr); } @@ -83,6 +84,7 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { private void printCSV(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); final String TIME_LABEL = getString("HeatingCurve.6"); @@ -90,9 +92,9 @@ private void printCSV(ResidualStatistic hc, FileOutputStream fos) { stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); double tr, Tr; for (int i = 0; i < residualsLength; i++) { - tr = residuals.get(i)[0]; + tr = time.get(i); stream.printf("%n%3.8f", tr); - Tr = residuals.get(i)[1]; + Tr = residuals.get(i); stream.printf("\t%3.8f", Tr); } } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 4445add9..85c4f1c8 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -47,23 +47,25 @@ public class NetzschCSVReader implements CurveReader { private final static String THICKNESS = "Thickness_RT"; private final static String DETECTOR_SPOT_SIZE = "Spotsize"; private final static String DIAMETER = "Diameter"; + private final static String L_PULSE_WIDTH = "Laser_pulse_width"; + private final static String PULSE_WIDTH = "Pulse_width"; /** * Note comma is included as a delimiter character here. */ private final static String ENGLISH_DELIMS = "[#(),;/°Cx%^]+"; private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; - + private static String delims; //default number format (British format) private static Locale locale; - + private static NumberFormat format; - + private NetzschCSVReader() { //do nothing } - + protected void setDefaultLocale() { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; @@ -105,26 +107,25 @@ public String getSupportedExtension() { public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); ExperimentalData curve = new ExperimentalData(); - + setDefaultLocale(); //always start with a default locale - - //gets the number format for this locale + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); - - String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); - + + String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); + double spotSize = 0; - if(spot != null) { + if (spot != null) { var spotTokens = spot.split(delims); spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; } - + String tempLine = findLineByLabel(reader, THICKNESS, false); var tempTokens = tempLine.split(delims); - + final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, DIAMETER, false).split(delims); @@ -133,6 +134,19 @@ public List read(File file) throws IOException { tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, false).split(delims); final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; + var line = findLineByLabel(reader, L_PULSE_WIDTH, DETECTOR, false); + if (line == null) { + line = findLineByLabel(reader, PULSE_WIDTH, DETECTOR, false); + } + + double pulseWidth = 0; + + if (line != null) { + tempTokens = line.split(delims); + pulseWidth = format.parse(tempTokens[tempTokens.length - 1]) + .doubleValue() * TO_SECONDS; + } + /* * Finds the detector keyword. */ @@ -147,6 +161,9 @@ public List read(File file) throws IOException { populate(curve, reader); var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + if (pulseWidth > 1e-10) { + met.set(NumericPropertyKeyword.PULSE_WIDTH, derive(NumericPropertyKeyword.PULSE_WIDTH, pulseWidth)); + } met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); @@ -154,7 +171,7 @@ public List read(File file) throws IOException { curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); - return new ArrayList<>(Arrays.asList(curve)); + return new ArrayList<>(Arrays.asList(curve)); } catch (ParseException ex) { Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); @@ -163,26 +180,24 @@ public List read(File file) throws IOException { return null; } - + /** * Note: the {@code line} must contain a decimal-separated number. - * @param line a line containing number with a decimal separator + * + * @param line a line containing number with a decimal separator */ - private static void guessLocaleAndFormat(String line) { - - if(line.contains(".")) { + + if (line.contains(".")) { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; - } - - else { + } else { delims = GERMAN_DELIMS; locale = Locale.GERMAN; } - + format = DecimalFormat.getInstance(locale); - format.setGroupingUsed(false); + format.setGroupingUsed(false); } protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { @@ -192,12 +207,12 @@ protected static void populate(AbstractData data, BufferedReader reader) throws for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - - if(tokens.length < 2) { + + if (tokens.length < 2) { guessLocaleAndFormat(line); tokens = line.split(delims); } - + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); @@ -210,7 +225,7 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO String[] shotID = shotIDLine.split(delims); int id; - + //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { throw new IllegalArgumentException(file.getName() @@ -226,37 +241,39 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO protected static String findLineByLabel(BufferedReader reader, String label, boolean ignoreLocale) throws IOException { return findLineByLabel(reader, label, "!!!", ignoreLocale); } - + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, boolean ignoreLocale) throws IOException { String line = ""; String[] tokens; reader.mark(1000); - + //find keyword outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { - if(line.isBlank()) + if (line.isBlank()) { continue; - - if(!ignoreLocale) + } + + if (!ignoreLocale) { guessLocaleAndFormat(line); + } tokens = line.split(delims); for (String token : tokens) { - + if (token.equalsIgnoreCase(label)) { break outer; } - - if(token.equalsIgnoreCase(stopLabel)) { + + if (token.equalsIgnoreCase(stopLabel)) { line = null; reader.reset(); break outer; } - + } } @@ -264,7 +281,7 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } - + /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. @@ -274,14 +291,14 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str public static CurveReader getInstance() { return instance; } - + /** * Get the standard delimiter chars. + * * @return delims */ - public static String getDelims() { return delims; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java new file mode 100644 index 00000000..7e5390de --- /dev/null +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -0,0 +1,136 @@ +package pulse.math; + +import org.apache.commons.math3.complex.Complex; +import org.apache.commons.math3.transform.DftNormalization; +import org.apache.commons.math3.transform.FastFourierTransformer; +import org.apache.commons.math3.transform.TransformType; + +public class FFTTransformer { + + private double[] amplitudeSpec; + private double[] phaseSpec; + + private int n; //number of input points + private Complex[] buffer; + + private Window window; + + public FFTTransformer(double[] realInput) { + this(Window.HANN, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput) { + this(window, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput, double[] imagInput) { + this.window = window; + n = realInput.length; + + if (realInput.length != imagInput.length) { + throw new IllegalArgumentException( + String.format("Invalid data array lengths: %5d and %5d", + realInput.length, imagInput.length)); + } + + //if the input array is a power of two, simply make a shallow copy of the input array + if (IsPowerOfTwo(realInput.length)) { + buffer = new Complex[realInput.length]; + fill(realInput, imagInput, realInput.length); + } else { + int pow2 = numBits(realInput.length); + int nextPowerOfTwo = (int) Math.pow(2, pow2 + 1); + int previousPowerOfTwo = (int) Math.pow(2, pow2); + + final double TOLERANCE_FACTOR = 0.25; + + /* + * if we cut the tails, do we end up removing less elements than the number + * of zeros we had to add to reach next power of two? + */ + if ((nextPowerOfTwo - realInput.length + > realInput.length - previousPowerOfTwo) + && //in this case, do we have to add too many zeros? + (nextPowerOfTwo - realInput.length + > TOLERANCE_FACTOR * realInput.length)) { + cutTails(realInput, imagInput, previousPowerOfTwo); + } else { + zeroPad(realInput, imagInput, nextPowerOfTwo); + } + + } + //create power and phase arrays + amplitudeSpec = new double[buffer.length / 2]; + phaseSpec = new double[buffer.length / 2]; + + } + + public double[] sampling(double[] x) { + final double totalTime = x[n - 2] - x[0]; + double[] sample = new double[buffer.length / 2]; + double fs = n/totalTime; //sampling rate + for (int i = 0; i < sample.length; i++) { + sample[i] = i * fs / buffer.length; + } + return sample; + } + + public void transform() { + FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD); + + Complex[] result = fft.transform(buffer, TransformType.FORWARD); + + final double _2_N = 2.0 / amplitudeSpec.length; + + amplitudeSpec[0] = result[0].abs() / amplitudeSpec.length; + phaseSpec[0] = result[0].getArgument(); + + for (int i = 1; i < amplitudeSpec.length; i++) { + amplitudeSpec[i] = _2_N * result[i].abs(); + phaseSpec[i] = result[i].getArgument(); + } + + } + + private void fill(double[] realInput, double[] imagInput, int size) { + for (int i = 0; i < size; i++) { + buffer[i] = new Complex( + window.evaluate(i, realInput.length) * realInput[i], + imagInput[i]); + } + } + + private void cutTails(double[] realInput, double[] imagInput, int previousPowerOfTwo) { + buffer = new Complex[previousPowerOfTwo]; + fill(realInput, imagInput, previousPowerOfTwo); + } + + private void zeroPad(double[] realInput, double[] imagInput, int nextPowerOfTwo) { + buffer = new Complex[nextPowerOfTwo]; + fill(realInput, imagInput, realInput.length); + for (int i = realInput.length; i < nextPowerOfTwo; i++) { + buffer[i] = new Complex(0.0, 0.0); + } + } + + /** + * Checks if the argument (positive integer) is a power of 2. Returns trues + * if the argument is zero. + */ + private static boolean IsPowerOfTwo(int x) { + return x > 0 && ((x & (x - 1)) == 0); + } + + private int numBits(int value) { + return (int) (Math.log(value) / Math.log(2)); + } + + public double[] getAmpltiudeSpectrum() { + return amplitudeSpec; + } + + public double[] getPhaseSpectrum() { + return phaseSpec; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/Harmonic.java b/src/main/java/pulse/math/Harmonic.java new file mode 100644 index 00000000..06e36043 --- /dev/null +++ b/src/main/java/pulse/math/Harmonic.java @@ -0,0 +1,266 @@ +package pulse.math; + +import static java.lang.Math.cos; +import java.util.Set; +import pulse.math.transforms.PeriodicTransform; +import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +/** + * + * Harmonic class. + *

+ * It is given by the expression y = y0 + + * A cos(2πf t + φ) , where f is the + * frequency (in Hz), A is the amplitude, φ is the phase shift. + *

+ * + * + */ +public class Harmonic extends PropertyHolder implements Optimisable, Comparable { + + private int rank = -1; + + private double amplitude; + private double frequency; + private double phaseShift; + + private final static double _2PI = 2.0 * Math.PI; + + public Harmonic() { + setFrequency(def(BASELINE_FREQUENCY)); + setAmplitude(def(BASELINE_AMPLITUDE)); + setPhaseShift(def(BASELINE_PHASE_SHIFT)); + } + + public Harmonic(double amplitude, double frequency, double phaseShift) { + this.amplitude = amplitude; + this.frequency = frequency; + this.phaseShift = phaseShift; + } + + public Harmonic(Harmonic h) { + this.amplitude = h.amplitude; + this.frequency = h.frequency; + this.phaseShift = h.phaseShift; + this.rank = h.rank; + } + + public NumericProperty getFrequency() { + return derive(BASELINE_FREQUENCY, frequency); + } + + public NumericProperty getAmplitude() { + return derive(BASELINE_AMPLITUDE, amplitude); + } + + public NumericProperty getPhaseShift() { + return derive(BASELINE_PHASE_SHIFT, phaseShift); + } + + public final void setFrequency(NumericProperty frequency) { + requireType(frequency, BASELINE_FREQUENCY); + this.frequency = (double) frequency.getValue(); + firePropertyChanged(this, frequency); + } + + public final void setAmplitude(NumericProperty amplitude) { + requireType(amplitude, BASELINE_AMPLITUDE); + this.amplitude = (double) amplitude.getValue(); + firePropertyChanged(this, amplitude); + } + + public final void setPhaseShift(NumericProperty phaseShift) { + requireType(phaseShift, BASELINE_PHASE_SHIFT); + this.phaseShift = (double) phaseShift.getValue(); + firePropertyChanged(this, phaseShift); + } + + /** + * Amplitude form of the Fourier harmonic + * + * @param x + * @return + */ + public double valueAt(double x) { + return amplitude * cos(_2PI * x * frequency + phaseShift); + } + + /** + * Listed properties include the frequency, amplitude, phase shift, and + * intercept. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_FREQUENCY); + set.add(BASELINE_AMPLITUDE); + set.add(BASELINE_PHASE_SHIFT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case BASELINE_FREQUENCY: + setFrequency(property); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(property); + break; + case BASELINE_AMPLITUDE: + setAmplitude(property); + break; + } + + } + + /** + * The optimisation vector can include the amplitude, frequency and phase + * shift of a sinusoid, and a baseline intercept value of the superclass. + */ + @Override + public void optimisationVector(ParameterVector output) { + + var params = output.getParameters(); + + for (int i = 0, size = params.size(); i < size; i++) { + + var p = params.get(i); + var id = p.getIdentifier(); + var bounds = Segment.boundsFrom(id.getKeyword()); + + double value; + + Transformable transform = null; + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + value = frequency; + transform = StandardTransformations.ABS; + break; + case BASELINE_PHASE_SHIFT: + value = phaseShift; + transform = new PeriodicTransform(bounds); + break; + case BASELINE_AMPLITUDE: + value = amplitude; + transform = new StickTransform(bounds); + break; + default: + continue; + } + + var newId = new ParameterIdentifier(id.getKeyword(), rank); + + if (id.getIndex() == rank) { + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); + } else if (rank > -1) { + + boolean matchFound = output.getParameters().stream().anyMatch(pp -> { + var key = pp.getIdentifier().getKeyword(); + int index = pp.getIdentifier().getIndex(); + return key == id.getKeyword() && rank == index; + }); + + if (!matchFound) { + + var newParam = new Parameter(newId, transform, bounds); + newParam.setValue(value); + params.add(newParam); + + } + + } + + } + + } + + @Override + public void assign(ParameterVector params) { + + for (Parameter p : params.getParameters()) { + + var id = p.getIdentifier(); + + if (id.getIndex() == rank) { + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + setFrequency(derive(BASELINE_FREQUENCY, p.inverseTransform())); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(derive(BASELINE_PHASE_SHIFT, p.inverseTransform())); + break; + case BASELINE_AMPLITUDE: + setAmplitude(derive(BASELINE_AMPLITUDE, p.inverseTransform())); + break; + default: + break; + } + + } + + } + + } + + public void setRank(int rank) { + this.rank = rank; + } + + public int getRank() { + return rank; + } + + public Harmonic increaseAmplitudeBy(int amplitudeFactor) { + var h = new Harmonic(this); + h.amplitude *= amplitudeFactor; + return h; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Harmonic)) { + return false; + } + + Harmonic oH = (Harmonic) o; + + final double tolerance = 1E-3; + + return Math.abs(oH.amplitude - this.amplitude) + / Math.abs(oH.amplitude + this.amplitude) < tolerance + && Math.abs(oH.frequency - this.frequency) + / Math.abs(oH.frequency + this.frequency) < tolerance + && Math.abs(oH.phaseShift - this.phaseShift) + / Math.abs(oH.phaseShift + this.phaseShift) < tolerance; + } + + @Override + public int compareTo(Harmonic o) { + return this.getAmplitude().compareTo(o.getAmplitude()); + } + + @Override + public String toString() { + return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", + rank, frequency, amplitude, phaseShift); + } + +} diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java new file mode 100644 index 00000000..5556cd15 --- /dev/null +++ b/src/main/java/pulse/math/Parameter.java @@ -0,0 +1,91 @@ +package pulse.math; + +import pulse.math.transforms.Transformable; + +/** + * Parameter class + */ +public class Parameter { + + private ParameterIdentifier index; + private Transformable transform; + private Segment bound; + private double value; + + public Parameter(ParameterIdentifier index, Transformable transform, Segment bound) { + this.index = index; + this.transform = transform; + this.bound = bound; + } + + public Parameter(ParameterIdentifier index) { + if(index.getKeyword() != null) { + bound = Segment.boundsFrom(index.getKeyword()); + } + this.index = index; + } + + public Parameter(Parameter p) { + this.index = p.index; + this.transform = p.transform; + this.bound = p.bound; + this.value = p.value; + } + + public ParameterIdentifier getIdentifier() { + return index; + } + + public void setBounds(Segment bounds) { + this.bound = bounds; + } + + public Segment getBounds() { + return bound; + } + + /** + * If transform of {@code i} is not null, applies the transformation to the + * component bounds + * + * @param i the index of the component + * @return the transformed bounds + */ + public Segment getTransformedBounds() { + return transform != null + ? new Segment(transform.transform(bound.getMinimum()), + transform.transform(bound.getMaximum())) + : bound; + } + + public Transformable getTransform() { + return transform; + } + + public void setTransform(Transformable transform) { + this.transform = transform; + } + + public double inverseTransform() { + return transform != null ? transform.inverse(value) : value; + } + + public Parameter copy() { + return new Parameter(index, transform, bound); + } + + public double getApparentValue() { + return value; + } + + public void setValue(double value, boolean ignoreTransform) { + this.value = transform == null || ignoreTransform + ? value + : transform.transform(value); + } + + public void setValue(double value) { + setValue(value, false); + } + +} diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java new file mode 100644 index 00000000..b96847da --- /dev/null +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -0,0 +1,53 @@ +package pulse.math; + +import pulse.properties.NumericPropertyKeyword; + +public class ParameterIdentifier { + + private NumericPropertyKeyword keyword; + private int index; + + public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { + this.keyword = keyword; + this.index = index; + } + + public ParameterIdentifier(NumericPropertyKeyword keyword) { + this(keyword, 0); + } + + public ParameterIdentifier(int index) { + this.index = index; + } + + public NumericPropertyKeyword getKeyword() { + return keyword; + } + + public int getIndex() { + return index; + } + + @Override + public boolean equals(Object id) { + if(!id.getClass().equals(ParameterIdentifier.class)) { + return false; + } + + var pid = (ParameterIdentifier) id; + + boolean result = true; + + if(keyword != pid.keyword || index != pid.index) + result = false; + + return result; + + } + + @Override + public String toString() { + return keyword + " # " + index; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 4b852216..6bd17373 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,51 +1,50 @@ package pulse.math; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import pulse.math.linear.Vector; -import pulse.math.transforms.Transformable; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; /** - * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific + * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific * components of the vector. Used when constructing the optimisation vector. */ -public class ParameterVector extends Vector { +public class ParameterVector { - private NumericPropertyKeyword[] indices; - private Transformable[] transforms; - private Segment[] bounds; + private List params; /** * Constructs an {@code IndexedVector} with the specified list of keywords. * * @param indices a list of keywords */ - public ParameterVector(List indices) { - this(indices.size()); - assign(indices); + public ParameterVector(List indices) { + params = indices.stream().map(ind + -> new Parameter(ind)).collect(Collectors.toList()); } /** * Constructs an {@code IndexedVector} based on {@code v} and a list of * keyword {@code indices} * + * @param proto prototype vector * @param v the vector to be copied - * @param prototype the prototype of the parameter vector */ public ParameterVector(ParameterVector proto, Vector v) { - super(v); - this.indices = new NumericPropertyKeyword[proto.indices.length]; - System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); - this.bounds = new Segment[proto.bounds.length]; - System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); - this.transforms = new Transformable[proto.transforms.length]; - System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); - + params = new ArrayList<>(); + var protoParams = proto.params; + for (Parameter p : protoParams) { + var pp = new Parameter(p); //copy + pp.setValue(v.get( + protoParams.indexOf(p))); //set new value + params.add(pp); //add + } } /** @@ -54,198 +53,79 @@ public ParameterVector(ParameterVector proto, Vector v) { * @param v another vector */ public ParameterVector(ParameterVector v) { - this(v.dimension()); - final int n = dimension(); - for (int i = 0; i < n; i++) { - this.set(i, v.get(i)); + params = new ArrayList<>(v.params); + for (Parameter p : params) { + p.setValue(p.getApparentValue(), true); } - System.arraycopy(v.indices, 0, indices, 0, n); - System.arraycopy(v.transforms, 0, transforms, 0, n); - System.arraycopy(v.bounds, 0, bounds, 0, n); - } - - /** - * Creates an empty ParameterVector with a dimension of {@code n} - * - * @param n dimension - */ - private ParameterVector(final int n) { - super(n); - indices = new NumericPropertyKeyword[n]; - transforms = new Transformable[n]; - bounds = new Segment[n]; - } - - @Override - public void set(final int i, final double x) { - set(i, x, false); - } - - /** - * Sets the i-th parameter value to {@code x} without applying the - * transform. Sets the bound for this value as the default bound for {@code key}. - * @param i the index of the parameter - * @param x value to be set - * @param key type of property - */ - - public void set(final int i, final double x, NumericPropertyKeyword key) { - set(i, x); - setParameterBounds(i, Segment.boundsFrom(key)); - } - - /** - * Sets the i-component of this vector to {@code x} or - * its corresponding transform, if the latter is defined and - * {@code ignoreTransform} is {@code false}. - * - * @param i index of the value and its transform - * @param x the non-transformed value, which needs to be assigned to the - * i-th component - * @param ignoreTransform if {@code} false, will ignore exiting transform. - */ - public void set(final int i, final double x, boolean ignoreTransform) { - final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); - super.set(i, t); - } - - /** - * Retrieves the keyword associated with the {@code dataIndex} - * - * @param dataIndex an index pointing to a component of this vector - * @return a keyword describing this component - */ - public NumericPropertyKeyword getIndex(final int dataIndex) { - return indices[dataIndex]; - } - - /** - * Gets the data index that corresponds to the keyword {@code index} - * - * @param index a keyword-index of the component - * @return a numeric index associated with the original {@code Vector} - */ - private int indexOf(NumericPropertyKeyword index) { - return getIndices().indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - public double getParameterValue(NumericPropertyKeyword index) { - return super.get(indexOf(index)); } - /** - * Performs an inverse transform corresponding to the index {@code i} of - * this vector. - * - * @param i the index of the transform - * @return the inverse transform of {@code get(i) } if the transform is - * defined, {@code get(i)} otherwise. - */ - public double inverseTransform(final int i) { - return transforms[i] != null ? transforms[i].inverse(get(i)) : get(i); + public void add(Parameter p) { + params.add(p); } - /** - * Gets the transformable of the i-th component - * - * @param i index of the component - * @return the corresponding {@code Transforamble} - */ - public Transformable getTransform(final int i) { - return transforms[i]; - } - - public void setTransform(final int i, Transformable transformable) { - transforms[i] = transformable; - } - - public Segment getParameterBounds(final int i) { - return bounds[i]; - } - - /** - * If transform of {@code i} is not null, applies the transformation to the - * component bounds - * - * @param i the index of the component - * @return the transformed bounds - */ - public Segment getTransformedBounds(final int i) { - return transforms[i] != null - ? new Segment(transforms[i].transform(bounds[i].getMinimum()), - transforms[i].transform(bounds[i].getMaximum())) - : getParameterBounds(i); - } - - /** - * Sets the bounds of i-th component of this vector. - * - * @param i the index of the component - * @param segment new parameter bounds - */ - public void setParameterBounds(int i, Segment segment) { - bounds[i] = segment; - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - public List getIndices() { - return Arrays.asList(indices); - } - - /** - * This will assign a new list of indices to this vector - * - * @param indices a list of indices - */ - private void assign(List indices) { - this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); - bounds = new Segment[this.indices.length]; - transforms = new Transformable[this.indices.length]; + public double getParameterValue(NumericPropertyKeyword key, int index) { + return params.stream().filter(p -> { + var pid = p.getIdentifier(); + return pid.getKeyword() == key && pid.getIndex() == index; + } + ).findAny().get().getApparentValue(); } @Override public String toString() { var sb = new StringBuilder(); sb.append("Indices: "); - for (var key : indices) { - sb.append(key).append(" ; "); + for (var key : params) { + sb.append(key.getIdentifier()).append(" ; "); } sb.append(System.lineSeparator()); sb.append(" Values: ").append(super.toString()); return sb.toString(); } - + /** * Finds any elements of this vector which do not pass sanity checks. + * * @return a list of malformed numeric properties * @see pulse.properties.NumericProperties.isValueSensible() */ - public List findMalformedElements() { var list = new ArrayList(); - - for (int i = 0; i < dimension(); i++) { - var property = NumericProperties.derive(getIndex(i), inverseTransform(i)); - if (!property.validate()) { - list.add(property); - } + + params.stream().filter(p -> (p.getIdentifier().getKeyword() != null)) + .map(p -> NumericProperties.derive(p.getIdentifier().getKeyword(), + p.inverseTransform())) + .filter(property -> (!property.validate())) + .forEachOrdered(property -> { + list.add(property); + }); + + return list; + } + + public void setValues(Vector v) { + int dim = v.dimension(); + if (dim != this.dimension()) { + throw new IllegalArgumentException("Illegal vector dimension: " + + dim + " != " + this.dimension()); } - return list; + for(int i = 0; i < dim; i++) { + params.get(i).setValue(v.get(i)); + } + + } + + public int dimension() { + return params.size(); + } + + public List getParameters() { + return params; } - public Segment[] getBounds() { - return bounds; + public Vector toVector() { + return new Vector(params.stream().mapToDouble(p -> p.inverseTransform()).toArray()); } } diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index 9691ab34..c1e0c762 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -13,6 +13,8 @@ public class Segment { private double a; private double b; + + public final static Segment UNBOUNDED = new Segment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); /** * Creates a {@code Segment} bounded by {@code a} and {@code b}. diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java new file mode 100644 index 00000000..48fbd4ff --- /dev/null +++ b/src/main/java/pulse/math/Window.java @@ -0,0 +1,60 @@ +package pulse.math; + +public interface Window { + + public final static Window NONE = (n, N) -> 1.0; + public final static Window HANN = (n, N) -> Math.pow( Math.sin(Math.PI * n / ((double) N)), 2); + public final static Window HAMMING = (n, N) -> 0.54 + 0.46*Math.cos(2.0 * Math.PI * n / ((double) N)); + public final static Window BLACKMANN_HARRIS = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.35875 - 0.48829*Math.cos(x) + 0.14128*Math.cos(2.0*x) - 0.01168*Math.cos(3.0*x); + }; + public final static Window FLAT_TOP = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.21557895 - 0.41663158*Math.cos(x) + 0.277263158*Math.cos(2.0*x) + - 0.083578947*Math.cos(3.0*x) + 0.006947368 * Math.cos(4.0 * x); + }; + public final static Window TUKEY = new Window() { + + private final static double alpha = 0.6; + + @Override + public double evaluate(int n, int N) { + + double result = 0; + + if(n < 0.5*alpha*N) { + result = 0.5 * ( 1 - Math.cos(2.0*Math.PI*n/(alpha*N))); + } + + else if(n <= N/2) { + result = 1.0; + } + + else { + result = TUKEY.evaluate(N - n,N); + } + + return result; + + } + }; + + public final static Window HANN_POISSON = (n, N) -> { + + final double alpha = 2.0; + return HANN.evaluate(n, N) * Math.exp( - alpha * (N - 2 * n) / N); + + }; + + public default double[] apply(double[] input) { + double[] output = new double[input.length]; + for(int i = 0; i < output.length; i++) { + output[i] = input[i] * evaluate(i, input.length); + } + return output; + } + + public abstract double evaluate(int n, int N); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java new file mode 100644 index 00000000..fa13fad1 --- /dev/null +++ b/src/main/java/pulse/math/ZScore.java @@ -0,0 +1,134 @@ +package pulse.math; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; + +/** + * This class finds peaks in data using the Z-score algorithm: + * https://en.wikipedia.org/wiki/Standard_score This splits the data into a + * number of population defined by the 'lag' number. A standard score is + * calculated as the difference of the current value and population mean divided + * by the population standard deviation. + */ + +public class ZScore { + + private double[] avgFilter; + private double[] stdFilter; + private int[] signals; + + private int lag; + private double threshold; + private double influence; + + public ZScore(int lag, double threshold, double influence) { + this.lag = lag; + this.threshold = threshold; + this.influence = influence; + } + + public ZScore() { + this(40, 3.5, 0.3); + } + + public void process(double[] input) { + + signals = new int[input.length]; + List filteredY = DoubleStream.of(input).boxed().collect(Collectors.toList()); + + var initialWindow = filteredY.subList(input.length - lag, input.length - 1); + + avgFilter = new double[input.length]; + stdFilter = new double[input.length]; + + avgFilter[input.length - lag + 1] = mean(initialWindow); + stdFilter[input.length - lag + 1] = stdev(initialWindow); + + for (int i = input.length - lag; i > 0; i--) { + + if (Math.abs(input[i] - avgFilter[i + 1]) > threshold * stdFilter[i + 1]) { + + signals[i] = (input[i] > avgFilter[i + 1]) ? 1 : -1; + filteredY.set(i, influence * input[i] + + (1 - influence) * filteredY.get(i + 1)); + + } else { + + signals[i] = 0; + filteredY.set(i, input[i]); + + } + + // Update rolling average and deviation + var slidingWindow = filteredY.subList(i, i + lag - 1); + + avgFilter[i] = mean(slidingWindow); + stdFilter[i] = stdev(slidingWindow); + } + + } + + private static double mean(List list) { + return list.stream().mapToDouble(d -> d).average().getAsDouble(); + } + + private static double stdev(List values) { + double ret = 0; + int size = values.size(); + if (size > 0) { + double avg = mean(values); + double sum = values.stream().mapToDouble(d -> Math.pow(d - avg, 2)).sum(); + ret = Math.sqrt(sum / (size - 1)); + } + return ret; + } + + public int[] getSignals() { + return signals; + } + + public double[] getFilteredAverage() { + return avgFilter; + } + + public double[] getFilteredStdev() { + return stdFilter; + } + + /* + public static void main(String[] args) { + Scanner sc = null; + try { + sc = new Scanner(new File("fft.txt")); + } catch (FileNotFoundException ex) { + Logger.getLogger(ZScore.class.getName()).log(Level.SEVERE, null, ex); + } + + // we just need to use \\Z as delimiter + sc.useDelimiter("\\n"); + + var list = new ArrayList(); + + while(sc.hasNext()) { + list.add(sc.nextDouble()); + } + + var zscore = new ZScore(); + zscore.process(list.stream().mapToDouble(d -> d).toArray()); + var signals = zscore.getSignals(); + + for(int i = 0; i < signals.length; i++) { + System.out.println(list.get(i) + " " + signals[i]); + } + + } + */ + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/AssignmentListener.java b/src/main/java/pulse/math/filters/AssignmentListener.java new file mode 100644 index 00000000..251884a3 --- /dev/null +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -0,0 +1,7 @@ +package pulse.math.filters; + +public interface AssignmentListener { + + public void onValueAssigned(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/Filter.java b/src/main/java/pulse/math/filters/Filter.java new file mode 100644 index 00000000..067ab3e6 --- /dev/null +++ b/src/main/java/pulse/math/filters/Filter.java @@ -0,0 +1,14 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public interface Filter { + + public List process(List input); + public default List process(DiscreteInput input) { + return process(DiscreteInput.convert(input.getX(), input.getY())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/HalfTimeCalculator.java b/src/main/java/pulse/math/filters/HalfTimeCalculator.java new file mode 100644 index 00000000..e2ca9ce1 --- /dev/null +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -0,0 +1,95 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import static java.lang.Double.valueOf; +import static java.util.Collections.max; +import java.util.Comparator; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.baseline.FlatBaseline; +import pulse.input.IndexRange; + +public class HalfTimeCalculator { + + private final Filter filter; + private final DiscreteInput data; + private Point2D max; + private double halfTime; + + /** + * A fail-safe factor. + */ + public final static double FAIL_SAFE_FACTOR = 10.0; + + private static final Comparator pointComparator = + (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + + public HalfTimeCalculator(DiscreteInput input) { + this.data = input; + this.filter = new RunningAverage(); + } + + /** + * Calculates the approximate half-rise time used for crude estimation of + * thermal diffusivity. + *

+ * This uses the {@code runningAverage} method by applying the default + * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based + * on finding the approximate value corresponding to the half-maximum of the + * temperature. The latter is calculated using the running average curve. + * The index corresponding to the closest temperature value available for + * that curve is used to retrieve the half-rise time (which also has the + * same index). If this fails, i.e. the associated index is less than 1, + * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe factor + * {@value FAIL_SAFE_FACTOR}. + *

+ * @see getHalfTime() + */ + public void calculate() { + var baseline = new FlatBaseline(); + baseline.fitTo(data); + + var filtered = filter.process(data); + + max = max(filtered, pointComparator); + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + + int indexLeft = IndexRange.closestLeft(halfMax, + filtered.stream().map(point -> point.getY()) + .collect(Collectors.toList())); + + if (indexLeft < 1 || indexLeft > filtered.size() - 2) { + halfTime = filtered.get(filtered.size() - 1).getX() / FAIL_SAFE_FACTOR; + } + else { + //extrapolate + Point2D p1 = filtered.get(indexLeft); + Point2D p2 = filtered.get(indexLeft + 1); + + halfTime = (halfMax - p1.getY())/(p2.getY() - p1.getY()) + *(p2.getX() - p1.getX()) + p1.getX(); + } + + } + + + /** + * Retrieves the half-time value of this dataset, which is equal to the + * time needed to reach half of the signal maximum. + * @return the half-time value. + */ + + public final double getHalfTime() { + return halfTime; + } + + public final Point2D getFilteredMaximum() { + return max; + } + + public DiscreteInput getData() { + return data; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisablePolyline.java b/src/main/java/pulse/math/filters/OptimisablePolyline.java new file mode 100644 index 00000000..4d7a8d6a --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -0,0 +1,62 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +public class OptimisablePolyline extends PropertyHolder implements Optimisable { + + private final double[] x; + private final double[] y; + private final List listeners; + + public OptimisablePolyline(List data) { + x = data.stream().mapToDouble(d -> d.getX()).toArray(); + y = data.stream().mapToDouble(d -> d.getY()).toArray(); + listeners = new ArrayList<>(); + } + + @Override + public void assign(ParameterVector input) throws SolverException { + var ps = input.getParameters(); + for(int i = 0, size = ps.size(); i < size; i++) { + y[i] = ps.get(i).getApparentValue(); + } + listeners.stream().forEach(l -> l.onValueAssigned()); + } + + @Override + public void optimisationVector(ParameterVector output) { + output.setValues(new Vector(y)); + } + + public List points() { + return DiscreteInput.convert(x, y); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + public double[] getX() { + return x; + } + + public double[] getY() { + return y; + } + + public void addAssignmentListener(AssignmentListener listener) { + listeners.add(listener); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java new file mode 100644 index 00000000..46c004bb --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public class OptimisedRunningAverage extends RunningAverage { + + public OptimisedRunningAverage() { + super(); + } + + public OptimisedRunningAverage(int reductionFactor) { + super(reductionFactor); + } + + @Override + public List process(DiscreteInput input) { + var p = super.process(input); + var optimisableCurve = new OptimisablePolyline(p); + var task = new PolylineOptimiser(input, optimisableCurve); + task.run(); + return optimisableCurve.points(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java new file mode 100644 index 00000000..06246988 --- /dev/null +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -0,0 +1,90 @@ +package pulse.math.filters; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.linear.Vector; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.BFGSOptimiser; +import pulse.search.statistics.AbsoluteDeviations; +import pulse.search.statistics.OptimiserStatistic; + +public class PolylineOptimiser extends SimpleOptimisationTask { + + private final OptimiserStatistic sos; + private final PolylineResponse response; + private final OptimisablePolyline optimisableCurve; + + public PolylineOptimiser(DiscreteInput di, OptimisablePolyline optimisableCurve) { + super(optimisableCurve, di); + this.sos = new AbsoluteDeviations() { + + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + this.optimisableCurve = optimisableCurve; + response = new PolylineResponse(sos); + optimisableCurve.addAssignmentListener(() -> response.update(optimisableCurve)); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(BFGSOptimiser.getInstance()); + } + + @Override + public Response getResponse() { + return response; + } + + @Override + public ParameterVector searchVector() { + var y = optimisableCurve.getY(); + List ids + = IntStream.range(0, optimisableCurve.getX().length).sequential() + .mapToObj(i -> new ParameterIdentifier(i)) + .collect(Collectors.toList()); + var pv = new ParameterVector(ids); + pv.setValues(new Vector(y)); + var pvParams = pv.getParameters(); + for (int i = 0; i < pv.dimension(); i++) { + pvParams.get(i).setBounds(new Segment(y[i] - 2, y[i] + 2)); + } + return pv; + } + + public class PolylineResponse extends SimpleResponse { + + UnivariateInterpolator interp; + UnivariateFunction func; + + public PolylineResponse(OptimiserStatistic os) { + super(os); + } + + public void update(OptimisablePolyline impl) { + interp = new SplineInterpolator(); + func = interp.interpolate(impl.getX(), impl.getY()); + } + + @Override + public double evaluate(double t) { + return func.value(t); + } + } + +} diff --git a/src/main/java/pulse/math/filters/Randomiser.java b/src/main/java/pulse/math/filters/Randomiser.java new file mode 100644 index 00000000..3d15453e --- /dev/null +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; + +public class Randomiser implements Filter { + + private final double amplitude; + + public Randomiser(double amplitude) { + this.amplitude = amplitude; + } + + @Override + public List process(List input) { + input.forEach(p -> + ((Point2D.Double)p).y += (Math.random() - 0.5) * amplitude + ); + return input; + } + + public double getAmplitude() { + return amplitude; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/RunningAverage.java b/src/main/java/pulse/math/filters/RunningAverage.java new file mode 100644 index 00000000..37a08a20 --- /dev/null +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -0,0 +1,131 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; + +public class RunningAverage implements Filter { + + private int bins; + + /** + * The binning factor used to build a crude approximation of the heating + * curve. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + + public static final int DEFAULT_BINS = 16; + public final static int MIN_BINS = 4; + + /** + * @param reductionFactor the factor, by which the number of points + * {@code count} will be reduced for this {@code ExperimentalData}. + */ + + public RunningAverage(int reductionFactor) { + this.bins = reductionFactor; + } + + public RunningAverage() { + this.bins = DEFAULT_BINS; + } + + /** + * Constructs a deliberately crude representation of this heating curve by + * calculating a running average. + *

+ * This is done using a binning algorithm, which will group the + * time-temperature data associated with this {@code ExperimentalData} in + * {@code count/reductionFactor - 1} bins, calculate the average value for + * time and temperature within each bin, and collect those values in a + * {@code List}. This is useful to cancel out the effect of signal + * outliers, e.g. when calculating the half-rise time. + *

+ * + * The algorithm is described in more detail in Lunev, A., & Heymer, R. + * (2020). Review of Scientific Instruments, 91(6), 064902. + * + * @param points + * @param input + * @return a {@code List}, representing the degraded + * {@code ExperimentalData}. + * @see halfRiseTime() + * @see pulse.AbstractData.maxTemperature() + */ + + @Override + public List process(List points) { + var x = points.stream().mapToDouble(p -> p.getX()).toArray(); + var y = points.stream().mapToDouble(p -> p.getY()).toArray(); + + int size = x.length; + int step = size / bins; + List movingAverage = new ArrayList<>(bins); + + for (int i = 0; i < bins; i++) { + int i1 = step*i; + int i2 = step*(i+1); + + double av = 0; + int j; + + for (j = i1; j < i2 && j < size; j++) { + av += y[j]; + } + + av /= j - i1; + i2 = j - 1; + + movingAverage.add(new Point2D.Double( + (x[i1] + x[i2])/ 2.0, av)); + + } + + addBoundaryPoints(movingAverage, x[0], x[size - 1]); + + /* + for(int i = 0; i < movingAverage.size(); i++) { + System.err.println(movingAverage.get(i)); + } + */ + + return movingAverage; + + } + + private static void addBoundaryPoints(List d, double minTime, double maxTime) { + int max = d.size(); + + d.add( + extrapolate(d.get(max - 1), + d.get(max - 2), + maxTime) + ); + + d.add( 0, + extrapolate(d.get(0), + d.get(1), + minTime) + ); + + } + + private static Point2D extrapolate(Point2D a, Point2D b, double x) { + double y1 = a.getY(); + double y2 = b.getY(); + double x1 = a.getX(); + double x2 = b.getX(); + + return new Point2D.Double(x, y1 + (x - x1)/(x2 - x1)*(y2 - y1)); + } + + public final int getNumberOfBins() { + return bins; + } + + public final void setNumberOfBins(int no) { + this.bins = no > MIN_BINS - 1 ? no : MIN_BINS; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 3b493829..39d06531 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -2,6 +2,7 @@ import static java.lang.Math.abs; import static java.lang.Math.sqrt; +import java.util.List; import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; import static pulse.math.linear.ArithmeticOperations.DIFF_SQUARED; import static pulse.math.linear.ArithmeticOperations.PRODUCT; @@ -63,7 +64,7 @@ public Vector inverted() { * * @return the integer dimension */ - public int dimension() { + public final int dimension() { return x.length; } @@ -121,7 +122,7 @@ public static Vector random(int n, double min, double max) { } return v; } - + /** * Component-wise vector multiplication */ diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java deleted file mode 100644 index 8b512871..00000000 --- a/src/main/java/pulse/math/transforms/InvLenSqTransform.java +++ /dev/null @@ -1,27 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the squared length of the - * sample. - */ -public class InvLenSqTransform implements Transformable { - - private double l; - - public InvLenSqTransform(ThermalProperties tp) { - this.l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return Math.abs(value) / (l * l); - } - - @Override - public double inverse(double t) { - return Math.abs(t) * (l * l); - } - -} diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java deleted file mode 100644 index 571bdd81..00000000 --- a/src/main/java/pulse/math/transforms/InvLenTransform.java +++ /dev/null @@ -1,26 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the length of the sample. - */ -public class InvLenTransform implements Transformable { - - private double l; - - public InvLenTransform(ThermalProperties tp) { - l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return value / l; - } - - @Override - public double inverse(double t) { - return t * l; - } - -} diff --git a/src/main/java/pulse/math/transforms/PeriodicTransform.java b/src/main/java/pulse/math/transforms/PeriodicTransform.java new file mode 100644 index 00000000..31cee068 --- /dev/null +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -0,0 +1,38 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +public class PeriodicTransform extends BoundedParameterTransform { + + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public PeriodicTransform(Segment bounds) { + super(bounds); + } + + /** + * @param a + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + double max = getBounds().getMaximum(); + double min = getBounds().getMinimum(); + double len = max - min; + + return a > max ? transform(a - len) : (a < min ? transform(a + len) : a); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return t; + } +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index a8206fdb..c5b95034 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,6 +19,9 @@ public class StandardTransformations { @Override public double transform(double a) { + if(a < 0) { + System.err.println(a); + } return log(a); } diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index 00239cbf..f5487615 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -15,8 +15,6 @@ */ package pulse.math.transforms; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import pulse.math.Segment; /** diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index b27e4eb9..e1a283f6 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,8 +7,6 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import pulse.tasks.SearchTask; /** @@ -36,7 +34,7 @@ public class DiscretePulse { * tc * is the time factor defined in the {@code Problem} class. */ - private final static int WIDTH_TOLERANCE_FACTOR = 1000; + private final static int WIDTH_TOLERANCE_FACTOR = 10000; /** * This creates a one-dimensional discrete pulse on a {@code grid}. @@ -58,7 +56,8 @@ public DiscretePulse(Problem problem, Grid grid) { = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), "Problem has not been assigned to a SearchTask"); - ExperimentalData data = ((SearchTask) ancestor).getExperimentalCurve(); + ExperimentalData data = + (ExperimentalData) ( ((SearchTask) ancestor).getInput() ); init(data); pulse.addListener(e -> { @@ -119,6 +118,8 @@ public final void recalculate() { setDiscreteWidth(nominalWidth); } + invTotalEnergy = 1.0/totalEnergy(); + } /** diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 265ac2a5..e834f024 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -13,6 +13,7 @@ import pulse.tasks.SearchTask; import pulse.baseline.FlatBaseline; +import pulse.tasks.Calculation; /** * A numeric pulse is given by a set of discrete {@code NumericPulseData} @@ -58,7 +59,8 @@ public void init(ExperimentalData data, DiscretePulse pulse) { baselineSubtractedFrom(data); //notify host pulse object of a new pulse width - var problem = ((SearchTask) data.getParent()).getCurrentCalculation().getProblem(); + var problem = ( (Calculation) ((SearchTask) data.getParent()) + .getResponse() ).getProblem(); setPulseWidthOf(problem); //convert to dimensionless time and interpolate @@ -77,7 +79,7 @@ private void baselineSubtractedFrom(ExperimentalData data) { //subtracts a horizontal baseline from the pulse data var baseline = new FlatBaseline(); - baseline.fitNegative(pulseData); + baseline.fitTo(pulseData); for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) pulseData.setSignalAt(i, diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index e23c023d..5a53fd0a 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -1,6 +1,10 @@ package pulse.problem.laser; +import java.util.List; import pulse.AbstractData; +import pulse.DiscreteInput; +import pulse.input.IndexRange; +import pulse.input.Range; /** * An instance of the {@code AbstractData} class, which also declares an @@ -8,7 +12,7 @@ * measurement imported from an external source. * */ -public class NumericPulseData extends AbstractData { +public class NumericPulseData extends AbstractData implements DiscreteInput { private final int externalID; @@ -55,4 +59,19 @@ public double pulseWidth() { return super.timeLimit(); } + @Override + public List getX() { + return getTimeSequence(); + } + + @Override + public List getY() { + return getSignalData(); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(this.getTimeSequence(), Range.UNLIMITED); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 879a51ce..f4577d98 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -6,6 +6,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; @@ -58,7 +59,8 @@ public final RTECalculationStatus getCalculationStatus() { public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; if (calculationStatus != RTECalculationStatus.NORMAL) { - throw new SolverException(calculationStatus.toString()); + throw new SolverException(calculationStatus.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 64e33e1b..59c92208 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -12,8 +12,6 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; -import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,11 +91,8 @@ public void copyFrom(DifferenceScheme df) { protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - } - else { - discretePulse.recalculate(); - } - + } + discretePulse.recalculate(); clearArrays(); } diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index f2c3a1ac..1c359a4c 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -3,6 +3,7 @@ import static java.lang.Math.abs; import java.util.Arrays; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.FINITE_DIFFERENCE_ERROR; /** * @see Wiki @@ -13,14 +14,15 @@ public interface FixedPointIterations { /** * Performs iterations until the convergence criterion is satisfied.The - latter consists in having a difference two consequent iterations of V - less than the specified error. At the end of each iteration, calls - {@code finaliseIteration()}. + * latter consists in having a difference two consequent iterations of V + * less than the specified error. At the end of each iteration, calls + * {@code finaliseIteration()}. * * @param V the calculation array * @param error used in the convergence criterion * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed * @see finaliseIteration() * @see iteration() */ @@ -28,8 +30,9 @@ public default void doIterations(double[] V, final double error, final int m) th final int N = V.length - 1; - for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error - || abs(V[N] - V_N) > error; finaliseIteration(V)) { + for (double V_0 = error + 1, V_N = error + 1; + abs(V[0] - V_0)/abs(V[0] + V_0 + 1e-16) > error + || abs(V[N] - V_N)/abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { V_N = V[N]; V_0 = V[0]; @@ -42,7 +45,8 @@ public default void doIterations(double[] V, final double error, final int m) th * Performs an iteration at time {@code m} * * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public void iteration(final int m) throws SolverException; @@ -50,13 +54,16 @@ public default void doIterations(double[] V, final double error, final int m) th * Finalises the current iteration.By default, does nothing. * * @param V the current iteration - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public default void finaliseIteration(double[] V) throws SolverException { final double threshold = 1E6; double sum = Arrays.stream(V).sum(); - if( sum > threshold || !Double.isFinite(sum) ) - throw new SolverException("Invalid solution values in V array"); + if (sum > threshold || !Double.isFinite(sum)) { + throw new SolverException("Invalid solution values in V array", + FINITE_DIFFERENCE_ERROR); + } } } diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index 8d852846..217cb285 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -77,14 +77,14 @@ protected void prepare(Problem problem) throws SolverException { @Override public void timeStep(final int m) throws SolverException { - leftBoundary(); + leftBoundary(m); final var V = getCurrentSolution(); final int N = V.length - 1; setSolutionAt(N, evalRightBoundary(tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); tridiagonal.sweep(V); } - public void leftBoundary() { + public void leftBoundary(int m) { tridiagonal.setBeta(1, firstBeta()); tridiagonal.evaluateBeta(getPreviousSolution()); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 88c7c658..9d98c309 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -3,7 +3,7 @@ /** * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving * systems of linear equations. Applicable to such systems where the forming - * matrix has a tridiagonal form. + * matrix has a tridiagonal form: Ai*xi-1 - Bi xi + Ci xi+1 = -Fi. * */ public class TridiagonalMatrixAlgorithm { diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index d50b442c..39e504c1 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -9,6 +9,7 @@ import pulse.problem.schemes.RadiativeTransferCoupling; import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -147,7 +148,8 @@ public Class[] domain() { public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 98c3dbb0..e9e4f603 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -10,6 +10,7 @@ import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; @@ -103,7 +104,8 @@ public void solve(ParticipatingMedium problem) throws SolverException { var status = getCalculationStatus(); if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 920e4fbd..5343958c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -69,10 +69,10 @@ public void prepare(Problem problem) throws SolverException { } @Override - public void leftBoundary() { + public void leftBoundary(int m) { var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); tridiagonal.setGamma(1, -zN_1 / z0); - super.leftBoundary(); + super.leftBoundary(m); } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 5020d2c2..16a80d11 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -42,17 +42,19 @@ * both the heat equation and the boundary conditions. *

* + * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ -public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { - - private double Bi1HTAU; +public class ImplicitLinearisedSolver extends ImplicitScheme + implements Solver { private int N; - private double tau; - - private double HH; - private double _2HTAU; + + protected double Bi1HTAU; + protected double tau; + protected double HH; + protected double _2HTAU; + private double zeta; public ImplicitLinearisedSolver() { diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java new file mode 100644 index 00000000..1a09949e --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -0,0 +1,226 @@ +package pulse.problem.schemes.solvers; + +import java.util.Set; +import static pulse.problem.schemes.DistributedDetection.evaluateSignal; +import static pulse.problem.statements.model.SpectralRange.LASER; +import static pulse.ui.Messages.getString; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.schemes.TridiagonalMatrixAlgorithm; +import pulse.problem.statements.Problem; +import pulse.problem.statements.TwoTemperatureModel; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.TwoTemperatureProperties; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; + +public class ImplicitTwoTemperatureSolver extends ImplicitScheme + implements Solver, FixedPointIterations { + + private AbsorptionModel absorption; + private TridiagonalMatrixAlgorithm gasSolver; + + private int N; + private double hBi; + private double hBiPrime; + private double HH; + private double tau; + private double _05HH_TAU; + + private double[] gasTemp; + + private double diffRatio; + private double g; + private double gPrime; + + private double nonlinearPrecision; + + public ImplicitTwoTemperatureSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitTwoTemperatureSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + private void initSolidPart() { + var grid = getGrid(); + final double hx = grid.getXStep(); + + var solid = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return getCurrentPulseValue() * absorption.absorption(LASER, i * hx) + + g * gasTemp[i]; + } + + }; + + solid.setCoefA(1.0 / HH); + solid.setCoefB(1.0 / tau + 2.0 / HH + g); + solid.setCoefC(1.0 / HH); + + solid.setAlpha(1, 1.0 / (1.0 + hBi + _05HH_TAU + 0.5 * HH * g)); + solid.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(solid); + } + + private void initGasPart() { + var grid = getGrid(); + + gasTemp = new double[N + 1]; + var solidTemp = this.getCurrentSolution(); + + gasSolver = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return gPrime * solidTemp[i]; + } + + @Override + public void evaluateAlpha() { + setAlpha(1, 1.0 / (1.0 + hBiPrime + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime))); + super.evaluateAlpha(); + } + + @Override + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + setBeta(1, diffRatio * (0.5 * HH * phi(0) + _05HH_TAU * U[0]) * getAlpha()[1]); + super.evaluateBeta(U, start, endExclusive); + } + + }; + + double invDiffRatio = 1.0 / diffRatio; + gasSolver.setCoefA(invDiffRatio / HH); + gasSolver.setCoefB(1.0 / tau + gPrime + 2.0 / HH * invDiffRatio); + gasSolver.setCoefC(invDiffRatio / HH); + + gasSolver.evaluateAlpha(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + if (!(problem instanceof TwoTemperatureModel)) { + throw new IllegalArgumentException("Illegal model type"); + } + + super.prepare(problem); + var model = (TwoTemperatureModel) problem; + var ttp = (TwoTemperatureProperties) model.getProperties(); + + double hx = getGrid().getXStep(); + tau = getGrid().getTimeStep(); + N = (int) getGrid().getGridDensity().getValue(); + + HH = hx * hx; + _05HH_TAU = 0.5 * HH / tau; + hBi = (double) ttp.getHeatLoss().getValue() * hx; + hBiPrime = (double) ttp.getGasHeatLoss().getValue() * hx; + + g = (double) ttp.getSolidExchangeCoefficient().getValue(); + absorption = model.getAbsorptionModel(); + + diffRatio = model.diffusivityRatio(); + gPrime = (double) ttp.getGasExchangeCoefficient().getValue(); + + initGasPart(); + initSolidPart(); + } + + @Override + public void solve(TwoTemperatureModel problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(gasTemp, nonlinearPrecision, m); + } + + @Override + public void iteration(int m) throws SolverException { + //first solve for the solid + super.timeStep(m); + //then for the gas + gasSolver.evaluateBeta(gasTemp); + gasTemp[N] = (diffRatio * (0.5 * HH * gasSolver.phi(N) + _05HH_TAU * gasTemp[N]) + + gasSolver.getBeta()[N]) / (1.0 + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime) + + hBiPrime - gasSolver.getAlpha()[N]); + gasSolver.sweep(gasTemp); + } + + @Override + public double signal() { + return evaluateSignal(absorption, getGrid(), getCurrentSolution()); + } + + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[N] + 0.5 * HH * tridiagonal.phi(N) + betaN) + / (1.0 + _05HH_TAU + 0.5 * HH * g + hBi - alphaN); + } + + @Override + public double firstBeta() { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[0] + 0.5 * HH * tridiagonal.phi(0)) + * tridiagonal.getAlpha()[1]; + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitTwoTemperatureSolver(grid.getGridDensity(), + grid.getTimeFactor(), getTimeLimit()); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + @Override + public Class[] domain() { + return new Class[]{TwoTemperatureModel.class}; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 28bddf45..8aacbe04 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -2,9 +2,28 @@ @SuppressWarnings("serial") public class SolverException extends Exception { + + private final SolverExceptionType type; - public SolverException(String status) { + public SolverException(String status, SolverExceptionType type) { super(status); + this.type = type; + } + + public SolverException(SolverExceptionType type) { + this(type.toString(), type); + } + + public SolverExceptionType getType() { + return type; + } + + public enum SolverExceptionType { + RTE_SOLVER_ERROR, + OPTIMISATION_ERROR, + OPTIMISATION_TIMEOUT, + FINITE_DIFFERENCE_ERROR, + ILLEGAL_PARAMETERS, } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 72796ccc..72439856 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,7 +1,7 @@ package pulse.problem.statements; -import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -95,19 +94,18 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - super.optimisationVector(output, flags); + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, bias); + p.setTransform(new StickTransform(bounds)); + p.setValue(bias); } } @@ -117,10 +115,10 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.get(i); - var key = params.getIndex(i); + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { setGeometricFactor(derive(SOURCE_GEOMETRIC_FACTOR, value)); diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 278fe8ea..74b74e0f 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -19,7 +19,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; @@ -69,14 +68,14 @@ public DiscretePulse discretePulseOn(Grid grid) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ExtendedThermalProperties) getProperties(); double value; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Transformable transform = new InvDiamTransform(properties); var bounds = Segment.boundsFrom(key); @@ -102,9 +101,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, transform); - output.setParameterBounds(i, bounds); - output.set(i, value); + p.setTransform(transform); + p.setBounds(bounds); + p.setValue(value); } @@ -116,20 +115,20 @@ public void assign(ParameterVector params) throws SolverException { var properties = (ExtendedThermalProperties) getProperties(); // TODO one-to-one mapping for FOV and SPOT_DIAMETER - for (int i = 0, size = params.dimension(); i < size; i++) { - var type = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var type = p.getIdentifier().getKeyword(); switch (type) { case FOV_OUTER: case FOV_INNER: case HEAT_LOSS_SIDE: case HEAT_LOSS_COMBINED: - properties.set(type, derive(type, params.inverseTransform(i))); + properties.set(type, derive(type, p.inverseTransform())); break; case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, params.inverseTransform(i))); + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + p.inverseTransform())); break; default: - continue; } } } diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 488eafdb..dc4bf9b0 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,7 +13,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.DiathermicProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import pulse.ui.Messages; @@ -34,7 +33,6 @@ */ public class DiathermicMedium extends ClassicalProblem { - public DiathermicMedium() { super(); } @@ -54,15 +52,15 @@ public void initProperties(ThermalProperties properties) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); - Segment bounds = null; - double value = 0; + var key = p.getIdentifier().getKeyword(); + Segment bounds; + double value; switch (key) { case DIATHERMIC_COEFFICIENT: @@ -83,9 +81,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, new StickTransform(bounds)); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); } @@ -96,17 +94,19 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var key = params.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + p.inverseTransform())); break; case HEAT_LOSS_CONVECTIVE: - properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, params.inverseTransform(i))); + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + p.inverseTransform())); break; default: } diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 28b2857e..f21b3b89 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,6 +1,5 @@ package pulse.problem.statements; -import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; @@ -11,13 +10,13 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; @@ -82,10 +81,10 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); getProperties().calculateEmissivity(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.inverseTransform(i); - NumericPropertyKeyword key = params.getIndex(i); + double value = p.inverseTransform(); + NumericPropertyKeyword key = p.getIdentifier().getKeyword(); if (key == LASER_ENERGY) { this.getPulse().setLaserEnergy(derive(key, value)); @@ -104,18 +103,18 @@ public void assign(ParameterVector params) throws SolverException { */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if(key == LASER_ENERGY) { var bounds = Segment.boundsFrom(LASER_ENERGY); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, (double) getPulse().getLaserEnergy().getValue()); + p.setBounds(bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue( (double) getPulse().getLaserEnergy().getValue()); } } @@ -132,4 +131,4 @@ public Problem copy() { return new NonlinearProblem(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index ad0d177b..e8497e23 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -33,10 +33,10 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ThermoOpticalProperties) getProperties(); - properties.optimisationVector(output, flags); + properties.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index b93e68ff..d08d17cf 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.AbsorptionModel; import pulse.problem.statements.model.BeerLambertAbsorption; -import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.properties.Property; @@ -21,20 +20,22 @@ public class PenetrationProblem extends ClassicalProblem { private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); + private AbsorptionModel absorption; public PenetrationProblem() { super(); instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); + absorption = instanceDescriptor.newInstance(AbsorptionModel.class); absorption.setParent(this); } public PenetrationProblem(PenetrationProblem p) { super(p); - instanceDescriptor.setSelectedDescriptor((String) p.getAbsorptionSelector().getValue()); + instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); - initAbsorption(); + this.absorption = p.getAbsorptionModel().copy(); + this.absorption.setParent(this); } private void initAbsorption() { @@ -70,9 +71,9 @@ public InstanceDescriptor getAbsorptionSelector() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - absorption.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + absorption.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index d2bde96c..f2285151 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -7,23 +7,25 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.HeatingCurve; import pulse.baseline.Baseline; +import pulse.baseline.FlatBaseline; import pulse.baseline.LinearBaseline; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -57,9 +59,9 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim private static boolean hideDetailedAdjustment = true; private ProblemComplexity complexity = ProblemComplexity.LOW; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Baseline Selector", Baseline.class); + "Baseline Selector", Baseline.class); /** * Creates a {@code Problem} with default parameters (as found in the .XML @@ -74,10 +76,8 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim protected Problem() { initProperties(); setHeatingCurve(new HeatingCurve()); - - instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); addListeners(); - initBaseline(); + instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); } /** @@ -88,13 +88,10 @@ protected Problem() { */ public Problem(Problem p) { initProperties(p.getProperties().copy()); - setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); curve.setNumPoints(p.getHeatingCurve().getNumPoints()); - - instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); + setBaseline(p.getBaseline()); addListeners(); - setBaseline( p.getBaseline().copy() ); } public abstract Problem copy(); @@ -133,7 +130,7 @@ private void addListeners() { public final List availableSolutions() { var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); return allSchemes.stream().filter(scheme -> scheme instanceof Solver) - .filter(s -> Arrays.asList(s.domain()).contains(this.getClass()) ) + .filter(s -> Arrays.asList(s.domain()).contains(this.getClass())) .collect(Collectors.toList()); } @@ -165,7 +162,7 @@ public final Pulse getPulse() { */ public final void setPulse(Pulse pulse) { this.pulse = pulse; - pulse.setParent(this); + this.pulse.setParent(this); } /** @@ -176,7 +173,7 @@ public final void setPulse(Pulse pulse) { * @param c the {@code ExperimentalData} object */ public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); // used to estimate the floor of the signal range + baseline.fitTo(c); estimateSignalRange(c); updateProperties(this, c.getMetadata()); properties.useTheoreticalEstimates(c); @@ -194,9 +191,11 @@ public void retrieveData(ExperimentalData c) { * @see pulse.input.ExperimentalData.maxTemperature() */ public void estimateSignalRange(ExperimentalData c) { - var maxPoint = c.maxAdjustedSignal(); - final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); - properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); + var maxPoint = c.getHalfTimeCalculator().getFilteredMaximum(); + var flatBaseline = new FlatBaseline(); + flatBaseline.fitTo(c); + final double signalSpan = maxPoint.getY() - flatBaseline.valueAt(maxPoint.getX()); + properties.setMaximumTemperature(derive(MAXTEMP, signalSpan)); } /** @@ -217,29 +216,25 @@ public void estimateSignalRange(ExperimentalData c) { * class, or putting them in the XML file */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - baseline.optimisationVector(output, flags); + baseline.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Segment bounds = Segment.boundsFrom(key); - double value = 0; - + double value; + switch (key) { case THICKNESS: value = (double) properties.getSampleThickness().getValue(); break; case DIFFUSIVITY: - final double a = (double) properties.getDiffusivity().getValue(); - output.setTransform(i, new InvLenSqTransform(properties)); - bounds = new Segment(0.01 * a, 20.0 * a); - output.setParameterBounds(i, bounds); - output.set(i, a); - //custom transform here -- skip assigning StickTransform - continue; + value = (double) properties.getDiffusivity().getValue(); + bounds = new Segment(0.01 * value, 20.0 * value); + break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); bounds = new Segment(0.5 * signalHeight, 1.5 * signalHeight); @@ -247,7 +242,7 @@ public void optimisationVector(ParameterVector output, List flags) { break; case HEAT_LOSS: value = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, new StickTransform(bounds)); + p.setTransform(new StickTransform(bounds)); break; case TIME_SHIFT: double magnitude = 0.25 * properties.timeFactor(); @@ -257,11 +252,11 @@ public void optimisationVector(ParameterVector output, List flags) { default: continue; } - - output.setTransform(i, new StickTransform(bounds)); - output.setParameterBounds(i, bounds); - output.set(i, value); - + + p.setTransform(new StickTransform(bounds)); + p.setBounds(bounds); + p.setValue(value); + } } @@ -276,25 +271,25 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { baseline.assign(params); - + List malformedList = params.findMalformedElements(); - - if(!malformedList.isEmpty()) { + + if (!malformedList.isEmpty()) { StringBuilder sb = new StringBuilder("Cannot assign values: "); - malformedList.forEach(p -> - sb.append(String.format("%n %-25s", p.toString())) + malformedList.forEach(p + -> sb.append(String.format("%n %-25s", p.toString())) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } - - for (int i = 0, size = params.dimension(); i < size; i++) { - double value = params.inverseTransform(i); - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); switch (key) { case THICKNESS: - properties.setSampleThickness(derive(THICKNESS, value )); + properties.setSampleThickness(derive(THICKNESS, value)); break; case DIFFUSIVITY: properties.setDiffusivity(derive(DIFFUSIVITY, value)); @@ -412,16 +407,10 @@ public Baseline getBaseline() { * @see pulse.baseline.Baseline.apply(Baseline) */ public final void setBaseline(Baseline baseline) { - this.baseline = baseline; - curve.apply(baseline); - - baseline.setParent(this); - - var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); - if (searchTask != null) { - var experimentalData = searchTask.getExperimentalCurve(); - baseline.fitTo(experimentalData); - } + instanceDescriptor.setSelectedDescriptor(baseline.getClass().getSimpleName()); + this.baseline = baseline.copy(); + this.baseline.setParent(this); + curve.apply(this.baseline); } public final InstanceDescriptor getBaselineDescriptor() { @@ -429,8 +418,14 @@ public final InstanceDescriptor getBaselineDescriptor() { } private void initBaseline() { - var baseline = instanceDescriptor.newInstance(Baseline.class); - setBaseline(baseline); + setBaseline(instanceDescriptor.newInstance(Baseline.class)); + var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); + if (searchTask != null) { + var experimentalData = (ExperimentalData) searchTask.getInput(); + Executors.newSingleThreadExecutor().submit(() + -> baseline.fitTo(experimentalData) + ); + } parameterListChanged(); } @@ -451,4 +446,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index d2b53249..1b0fbf34 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -7,6 +7,7 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import java.util.List; +import java.util.Objects; import java.util.Set; import pulse.input.ExperimentalData; @@ -92,13 +93,12 @@ private void addListeners() { .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); - var range = corrTask.getExperimentalCurve().getRange(); + var range = ( (ExperimentalData) corrTask.getInput() ).getRange(); if( range.getLowerBound().compareTo(pw) < 0 ) { //update lower bound of the range for that SearchTask - corrTask.getExperimentalCurve().getRange() - .setLowerBound(pw); + range.setLowerBound(pw); } @@ -141,9 +141,9 @@ public void setPulseWidth(NumericProperty pulseWidth) { //validate -- do not update if the new pulse width is greater than 2 half-times SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); - ExperimentalData data = task.getExperimentalCurve(); + ExperimentalData data = (ExperimentalData) task.getInput(); - if(newValue < 2.0 * data.getHalfTime()) { + if(newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { this.pulseWidth = (double) pulseWidth.getValue(); firePropertyChanged(this, pulseWidth); } diff --git a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java new file mode 100644 index 00000000..72298d72 --- /dev/null +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -0,0 +1,176 @@ +package pulse.problem.statements; + +import java.util.List; +import pulse.math.Parameter; +import static pulse.properties.NumericProperties.derive; + +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.solvers.ImplicitTwoTemperatureSolver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.Gas; +import pulse.problem.statements.model.Helium; +import pulse.problem.statements.model.ThermalProperties; +import pulse.problem.statements.model.TwoTemperatureProperties; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.properties.Property; +import pulse.ui.Messages; +import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; + +public class TwoTemperatureModel extends PenetrationProblem { + + private Gas gas; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>("Gas Selector", Gas.class); + + public TwoTemperatureModel() { + super(); + setComplexity(ProblemComplexity.MODERATE); + instanceDescriptor.setSelectedDescriptor(Helium.class.getSimpleName()); + setGas(instanceDescriptor.newInstance(Gas.class)); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + public TwoTemperatureModel(TwoTemperatureModel p) { + super(p); + this.gas = p.gas; + instanceDescriptor.setSelectedDescriptor(gas.getClass().getSimpleName()); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> setGas(instanceDescriptor.newInstance(Gas.class))); + this.getProperties().addListener((PropertyEvent event) -> { + pulse.properties.Property p1 = event.getProperty(); + if (p1 instanceof NumericProperty) { + pulse.properties.NumericPropertyKeyword npType = ((NumericProperty) p1).getType(); + if (npType == TEST_TEMPERATURE) { + gas.evaluate((double) p1.getValue()); + } + } + }); + } + + @Override + public void initProperties() { + setProperties(new TwoTemperatureProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new TwoTemperatureProperties(properties)); + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + Segment bounds = Segment.boundsFrom(p.getIdentifier().getKeyword()); + double value; + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + value = (double) ttp.getSolidExchangeCoefficient().getValue(); + break; + case GAS_EXCHANGE_COEFFICIENT: + value = (double) ttp.getGasExchangeCoefficient().getValue(); + break; + case HEAT_LOSS_GAS: + value = (double) ttp.getGasHeatLoss().getValue(); + break; + default: + continue; + } + + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + var np = derive(key, p.inverseTransform()); + + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + ttp.setSolidExchangeCoefficient(np); + break; + case GAS_EXCHANGE_COEFFICIENT: + ttp.setGasExchangeCoefficient(np); + break; + case HEAT_LOSS_GAS: + ttp.setGasHeatLoss(np); + break; + default: + } + + } + + } + + @Override + public String toString() { + return Messages.getString("TwoTemperatureModel.Descriptor"); + } + + @Override + public Class defaultScheme() { + return ImplicitTwoTemperatureSolver.class; + } + + @Override + public TwoTemperatureModel copy() { + return new TwoTemperatureModel(this); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + public InstanceDescriptor getGasSelector() { + return instanceDescriptor; + } + + public Gas getGas() { + return gas; + } + + public final void setGas(Gas gas) { + this.gas = gas; + gas.evaluate((double) getProperties().getTestTemperature().getValue()); + firePropertyChanged(this, instanceDescriptor); + } + + /** + * Diffusivity of solid over diffusivity of gas + * + * @return + */ + public double diffusivityRatio() { + return (double) getProperties().getDiffusivity().getValue() + / gas.thermalDiffusivity(); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 3560fafd..9165b081 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -7,15 +7,13 @@ import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -35,6 +33,11 @@ protected AbsorptionModel() { absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); } + + protected AbsorptionModel(AbsorptionModel c) { + this.absorptionMap = new HashMap<>(); + this.absorptionMap.putAll(c.absorptionMap); + } public abstract double absorption(SpectralRange range, double x); @@ -105,9 +108,9 @@ public Set listedKeywords() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); double value = 0; Transformable transform = ABS; @@ -127,9 +130,8 @@ public void optimisationVector(ParameterVector output, List flags) { } //do this for the listed key values - output.setParameterBounds(i, Segment.boundsFrom(key)); - output.setTransform(i, transform); - output.set(i, value); + p.setTransform(transform); + p.setValue(value); } @@ -139,14 +141,14 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) throws SolverException { double value; - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var key = p.getIdentifier().getKeyword(); switch (key) { case LASER_ABSORPTIVITY: case THERMAL_ABSORPTIVITY: case COMBINED_ABSORPTIVITY: - value = params.inverseTransform(i); + value = p.inverseTransform(); break; default: continue; @@ -156,5 +158,7 @@ public void assign(ParameterVector params) throws SolverException { } } + + public abstract AbsorptionModel copy(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 31713356..3aebab03 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -6,10 +6,19 @@ public BeerLambertAbsorption() { super(); } + public BeerLambertAbsorption(AbsorptionModel m) { + super(m); + } + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); return a * Math.exp(-a * y); } + @Override + public AbsorptionModel copy() { + return new BeerLambertAbsorption(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Gas.java b/src/main/java/pulse/problem/statements/model/Gas.java new file mode 100644 index 00000000..7442ebd8 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -0,0 +1,75 @@ +package pulse.problem.statements.model; + +import pulse.util.Descriptive; +import pulse.util.Reflexive; + +public abstract class Gas implements Reflexive, Descriptive { + + private double conductivity; + private double thermalMass; + private final int atoms; + private final double mass; + + /** + * Universal gas constant. + */ + + public final static double R = 8.314; //J/K/mol + + private final static double ROOM_TEMPERATURE = 300; + private final static double NORMAL_PRESSURE = 1E5; + + public Gas(int atoms, double atomicWeight) { + evaluate(ROOM_TEMPERATURE, NORMAL_PRESSURE); + this.atoms = atoms; + this.mass = atoms * atomicWeight/1e3; + } + + public final void evaluate(double temperature, double pressure) { + this.conductivity = thermalConductivity(temperature); + this.thermalMass = cp() * density(temperature, pressure); + } + + public final void evaluate(double temperature) { + evaluate(temperature, NORMAL_PRESSURE); + } + + public final double thermalDiffusivity() { + return conductivity/thermalMass; + } + + public abstract double thermalConductivity(double t); + + public double cp() { + return (1.5 + atoms) * R / mass; + } + + public double density(double temperature, double pressure) { + return pressure * mass / (R * temperature); + } + + public double getThermalMass() { + return thermalMass; + } + + public double getConductivity() { + return conductivity; + } + + public double getNumberOfAtoms() { + return atoms; + } + + public double getMolarMass() { + return mass; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append(String.format(" : conductivity = %3.4f; thermal mass = %3.4f; ", conductivity, thermalMass)); + sb.append(String.format("atoms per molecule = %d; atomic weight = %1.4f", atoms, mass)); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Helium.java b/src/main/java/pulse/problem/statements/model/Helium.java new file mode 100644 index 00000000..96b71f0e --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Helium extends Gas { + + public Helium() { + super(1, 4); + } + + @Override + public double thermalConductivity(double t) { + return 0.415 + 0.283E-3 * (t - 1200); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 0cbba0cd..88c5972e 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -17,6 +17,15 @@ public Insulator() { super(); R = (double) def(REFLECTANCE).getValue(); } + + public Insulator(AbsorptionModel m) { + super(m); + if(m instanceof Insulator) { + R = (double) ((Insulator) m).getReflectance().getValue(); + } else { + R = (double) def(REFLECTANCE).getValue(); + } + } @Override public double absorption(SpectralRange spectrum, double x) { @@ -48,4 +57,9 @@ public Set listedKeywords() { return set; } + @Override + public AbsorptionModel copy() { + return new Insulator(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Nitrogen.java b/src/main/java/pulse/problem/statements/model/Nitrogen.java new file mode 100644 index 00000000..acaef03b --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Nitrogen extends Gas { + + public Nitrogen() { + super(2, 14); + } + + @Override + public double thermalConductivity(double t) { + return Math.sqrt(t) * (-92.39/t + 1.647 + 5.255E-4*t) * 1E-3; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index c3691eb6..153faa10 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -66,6 +66,7 @@ public ThermalProperties(ThermalProperties p) { this.a = p.a; this.Bi = p.Bi; this.T = p.T; + this.signalHeight = p.signalHeight; this.emissivity = p.emissivity; initListeners(); fill(); @@ -280,7 +281,7 @@ public Set listedKeywords() { } public final double thermalConductivity() { - return a * cP * rho; + return a * getThermalMass(); } public NumericProperty getThermalConductivity() { @@ -327,6 +328,10 @@ public double maxRadiationBiot() { public double timeFactor() { return l * l / a; } + + public double getThermalMass() { + return cP * rho; + } /** * Calculates the half-rise time t1/2 of {@code c} and @@ -338,7 +343,7 @@ public double timeFactor() { * @see pulse.input.ExperimentalData.halfRiseTime() */ public void useTheoreticalEstimates(ExperimentalData c) { - final double t0 = c.getHalfTime(); + final double t0 = c.getHalfTimeCalculator().getHalfTime(); this.a = PARKERS_COEFFICIENT * l * l / t0; if (areThermalPropertiesLoaded()) { Bi = radiationBiot(); @@ -352,7 +357,7 @@ public final boolean areThermalPropertiesLoaded() { public double maximumHeating(Pulse2D pulse) { final double Q = (double) pulse.getLaserEnergy().getValue(); final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * cP * rho); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass() ); } public NumericProperty getEmissivity() { diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 67db88ba..371fadc3 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -1,6 +1,5 @@ package pulse.problem.statements.model; -import java.util.List; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperty.requireType; @@ -10,12 +9,12 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -189,14 +188,14 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds = null; double value; Transformable transform; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case PLANCK_NUMBER: @@ -230,9 +229,9 @@ public void optimisationVector(ParameterVector output, List flags) { } transform = new StickTransform(bounds); - output.setTransform(i, transform); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(transform); + p.setValue(value); + p.setBounds(bounds); } @@ -241,9 +240,9 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var type = params.getIndex(i); + var type = p.getIdentifier().getKeyword(); switch (type) { @@ -252,7 +251,7 @@ public void assign(ParameterVector params) throws SolverException { case SCATTERING_ANISOTROPY: case OPTICAL_THICKNESS: case HEAT_LOSS_CONVECTIVE: - set(type, derive(type, params.inverseTransform(i))); + set(type, derive(type, p.inverseTransform())); break; default: break; diff --git a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java new file mode 100644 index 00000000..c879327f --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java @@ -0,0 +1,110 @@ +package pulse.problem.statements.model; + +import java.util.Set; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.GAS_EXCHANGE_COEFFICIENT; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_GAS; +import static pulse.properties.NumericPropertyKeyword.SOLID_EXCHANGE_COEFFICIENT; + +public class TwoTemperatureProperties extends ThermalProperties { + + private double exchangeSolid; + private double exchangeGas; + private double gasHeatLoss; + + public TwoTemperatureProperties() { + super(); + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + + public TwoTemperatureProperties(ThermalProperties p) { + super(p); + if (p instanceof TwoTemperatureProperties) { + var np = (TwoTemperatureProperties) p; + this.exchangeSolid = np.exchangeSolid; + this.exchangeGas = np.exchangeGas; + this.gasHeatLoss = np.gasHeatLoss; + } + else { + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + } + + @Override + public ThermalProperties copy() { + return new TwoTemperatureProperties(this); + } + + /** + * Used to change the parameter values of this {@code Problem}. It is only + * allowed to use those types of {@code NumericPropery} that are listed by + * the {@code listedParameters()}. + * + * @param type + * @param value + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + switch (type) { + case SOLID_EXCHANGE_COEFFICIENT: + setSolidExchangeCoefficient(value); + break; + case GAS_EXCHANGE_COEFFICIENT: + setGasExchangeCoefficient(value); + break; + case HEAT_LOSS_GAS: + setGasHeatLoss(value); + break; + default: + super.set(type, value); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(HEAT_LOSS_GAS); + set.add(SOLID_EXCHANGE_COEFFICIENT); + set.add(GAS_EXCHANGE_COEFFICIENT); + return set; + } + + public NumericProperty getSolidExchangeCoefficient() { + return derive(SOLID_EXCHANGE_COEFFICIENT, exchangeSolid); + } + + public NumericProperty getGasExchangeCoefficient() { + return derive(GAS_EXCHANGE_COEFFICIENT, exchangeGas); + } + + public void setSolidExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, SOLID_EXCHANGE_COEFFICIENT); + this.exchangeSolid = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public void setGasExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, GAS_EXCHANGE_COEFFICIENT); + this.exchangeGas = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public NumericProperty getGasHeatLoss() { + return derive(HEAT_LOSS_GAS, gasHeatLoss); + } + + public void setGasHeatLoss(NumericProperty p) { + NumericProperty.requireType(p, HEAT_LOSS_GAS); + this.gasHeatLoss = (double) p.getValue(); + firePropertyChanged(this, p); + } + +} diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 830f8ab1..94b6ce3f 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -24,9 +24,17 @@ public class Flag implements Property { * {@code Flag} */ public Flag(NumericPropertyKeyword type) { - this.index = type; - value = false; + this(type, false); + } + + public Flag(Flag f) { + this(f.index, f.value); } + + public Flag(NumericPropertyKeyword type, boolean flag) { + this.index = type; + this.value = flag; + } /** * Creates a {@code Flag} with the following pre-specified parameters: type diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 10aee7e8..734a70f2 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -1,8 +1,5 @@ package pulse.properties; -import java.text.ParseException; -import java.util.logging.Level; -import java.util.logging.Logger; import pulse.math.Segment; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.derive; diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 539a832d..a657171c 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -360,8 +360,38 @@ public enum NumericPropertyKeyword { * compared to rear face. Can be a number between zero and unity. */ - SOURCE_GEOMETRIC_FACTOR; - + SOURCE_GEOMETRIC_FACTOR, + + /** + * Max. no. of high-frequency waves in the sinusoidal baseline. + */ + + MAX_HIGH_FREQ_WAVES, + + /** + * Max. no. of low-frequency waves in the sinusoidal baseline. + */ + + MAX_LOW_FREQ_WAVES, + + /** + * Energy exchange coefficient in the two-temperature model (g). + */ + + SOLID_EXCHANGE_COEFFICIENT, + + /** + * Energy exchange coefficient in the two-temperature model (g'). + */ + + GAS_EXCHANGE_COEFFICIENT, + + /** + * Heat loss for the gas in the 2T-model. + */ + + HEAT_LOSS_GAS; + public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); } diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java new file mode 100644 index 00000000..44c64ac0 --- /dev/null +++ b/src/main/java/pulse/search/GeneralTask.java @@ -0,0 +1,217 @@ +package pulse.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.tasks.processing.Buffer; +import static pulse.tasks.processing.Buffer.getSize; +import pulse.util.Accessible; + +public abstract class GeneralTask + extends Accessible implements Runnable { + + private IterativeState path; //current sate + private IterativeState best; //best state + + private final Buffer buffer; + private PathOptimiser optimiser; + + public GeneralTask() { + buffer = new Buffer(); + buffer.setParent(this); + } + + public abstract List activeParameters(); + + /** + * Creates a search vector populated by parameters that + * are included in the optimisation routine. + * @return the parameter vector with optimisation parameters + */ + + public abstract ParameterVector searchVector(); + + /** + * Tries to assign a selected set of parameters to the search vector + * used in optimisation. + * @param pv a parameter vector containing all of the optimisation parameters + * whose values will be assigned to this task + * @throws SolverException + */ + + public abstract void assign(ParameterVector pv) throws SolverException; + + /** + *

+ * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, + * will do nothing. After making some preparatory steps, will initiate a + * loop with successive calls to {@code PathSolver.iteration(this)}, filling + * the buffer and notifying any data change listeners in parallel. This loop + * will go on until either converging results are obtained, or a timeout is + * reached, or if an execution error happens. Whether the run has been + * successful will be determined by comparing the associated + * R2 value with the {@code SUCCESS_CUTOFF}. + *

+ */ + @Override + public void run() { + setDefaultOptimiser(); + setIterativeState( optimiser.initState(this) ); + + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + int bufferSize = (Integer) getSize().getValue(); + buffer.init(); + //correlationBuffer.clear(); + + /* search cycle */ + /* sets an independent thread for manipulating the buffer */ + List> bufferFutures = new ArrayList<>(bufferSize); + var singleThreadExecutor = Executors.newSingleThreadExecutor(); + + var response = getResponse(); + + try { + response.objectiveFunction(this); + } catch (SolverException e1) { + onSolverException(e1); + } + + outer: + do { + + bufferFutures.clear(); + + for (var i = 0; i < bufferSize; i++) { + + try { + for (boolean finished = false; !finished;) { + finished = optimiser.iteration(this); + } + } catch (SolverException e) { + onSolverException(e); + break outer; + } + + //if global best is better than the converged value + if (best != null && best.getCost() < path.getCost()) { + try { + //assign the global best parameters + assign(path.getParameters()); + //and try to re-calculate + response.objectiveFunction(this); + } catch (SolverException ex) { + onSolverException(ex); + } + } + + final var j = i; + + bufferFutures.add(CompletableFuture.runAsync(() -> { + buffer.fill(this, j); + intermediateProcessing(); + }, singleThreadExecutor)); + + } + + bufferFutures.forEach(future -> future.join()); + + } while (buffer.isErrorTooHigh(errorTolerance) + && isInProgress()); + + singleThreadExecutor.shutdown(); + + if (isInProgress()) { + postProcessing(); + } + + } + + public abstract boolean isInProgress(); + + /** + * Override this to add intermediate processing of results e.g. + * with a correlation test. + */ + + public void intermediateProcessing() { + //empty + } + + /** + * Specifies what should be done when a solver exception is encountered. + * Empty by default + * @param e1 a solver exception + */ + + public void onSolverException(SolverException e1) { + //empty + } + + /** + * Override this to add post-processing checks + * e.g. normality tests or range checking. + */ + + public void postProcessing() { + //empty + } + + public final Buffer getBuffer() { + return buffer; + } + + public void setIterativeState(IterativeState state) { + this.path = state; + } + + public IterativeState getIterativeState() { + return path; + } + + public IterativeState getBestState() { + return best; + } + + /** + * Update the best state. The instance of this class stores two objects of + * the type IterativeState: the current state of the optimiser and the + * global best state. Calling this method will check if a new global best is + * found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search + * task, comparing the converged result to the global best, and selecting + * whichever has the lowest cost. Such routine is required due to the + * possibility of some optimisers going uphill. + */ + public void storeState() { + if (best == null || best.getCost() > path.getCost()) { + best = new IterativeState(path); + } + } + + public final void setOptimiser(PathOptimiser optimiser) { + this.optimiser = optimiser; + } + + public void setDefaultOptimiser() { + var instance = PathOptimiser.getInstance(); + if(optimiser == null || optimiser != instance) { + setOptimiser(PathOptimiser.getInstance()); + } + } + + public double objectiveFunction() throws SolverException { + return getResponse().objectiveFunction(this); + } + + public abstract I getInput(); + public abstract R getResponse(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 2bbf6b90..44666f54 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,10 +1,8 @@ package pulse.search; -import java.util.List; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; /** * An interface for dealing with optimisation variables. The variables are @@ -19,13 +17,13 @@ public interface Optimisable { * updated, the types of which are listed as indices in the {@code params} * vector. * - * @param params the optimisation vector, containing a similar set of + * @param input the optimisation vector, containing a similar set of * parameters to this {@code Problem} * @throws SolverException if {@code params} contains invalid parameter * values * @see pulse.util.PropertyHolder.listedTypes() */ - public void assign(ParameterVector params) throws SolverException; + public void assign(ParameterVector input) throws SolverException; /** * Calculates the vector argument defined on @@ -33,9 +31,7 @@ public interface Optimisable { * to the scalar objective function for this {@code Optimisable}. * * @param output the output vector where the result will be stored - * @param flags a list of {@code Flag} objects, which determine the basis of - * the search */ - public void optimisationVector(ParameterVector output, List flags); + public void optimisationVector(ParameterVector output); } diff --git a/src/main/java/pulse/search/SimpleOptimisationTask.java b/src/main/java/pulse/search/SimpleOptimisationTask.java new file mode 100644 index 00000000..4fbce664 --- /dev/null +++ b/src/main/java/pulse/search/SimpleOptimisationTask.java @@ -0,0 +1,93 @@ +package pulse.search; + +import java.util.List; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.ActiveFlags; +import static pulse.search.direction.ActiveFlags.selectActiveAndListed; +import pulse.search.direction.LMOptimiser; +import pulse.search.direction.PathOptimiser; +import pulse.util.PropertyHolder; + +/** + * Generic optimisation class. + * + * @param an optimisable object + */ +public abstract class SimpleOptimisationTask + extends GeneralTask { + + private final T optimisable; + private final DiscreteInput input; + + public SimpleOptimisationTask(T optimisable, DiscreteInput input) { + this.input = input; + this.optimisable = optimisable; + } + + @Override + public void run() { + var optimiser = PathOptimiser.getInstance(); + if(optimiser == null) { + PathOptimiser.setInstance(LMOptimiser.getInstance()); + } + super.run(); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id + -> new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + optimisable.optimisationVector(optimisationVector); + + return optimisationVector; + } + + @Override + public void assign(ParameterVector pv) throws SolverException { + optimisable.assign(pv); + } + + @Override + public boolean isInProgress() { + return false; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + optimisable.set(type, property); + } + + @Override + public List activeParameters() { + return selectActiveAndListed(ActiveFlags.getAllFlags(), optimisable); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(LMOptimiser.getInstance()); + } + + @Override + public DiscreteInput getInput() { + return input; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/SimpleResponse.java b/src/main/java/pulse/search/SimpleResponse.java new file mode 100644 index 00000000..9b658713 --- /dev/null +++ b/src/main/java/pulse/search/SimpleResponse.java @@ -0,0 +1,35 @@ +package pulse.search; + +import pulse.Response; +import pulse.math.Segment; +import pulse.search.statistics.OptimiserStatistic; + +public abstract class SimpleResponse implements Response { + + private OptimiserStatistic rs; + + public SimpleResponse(OptimiserStatistic rs) { + setOptimiserStatistic(rs); + } + + @Override + public final OptimiserStatistic getOptimiserStatistic() { + return rs; + } + + public final void setOptimiserStatistic(OptimiserStatistic statistic) { + this.rs = statistic; + } + + @Override + public double objectiveFunction(GeneralTask task) { + rs.evaluate(task); + return (double) rs.getStatistic().getValue(); + } + + @Override + public Segment accessibleRange() { + return Segment.UNBOUNDED; + } + +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index e13af6e1..81103d15 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -3,12 +3,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import pulse.problem.statements.Problem; +import pulse.input.ExperimentalData; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; @@ -41,12 +42,12 @@ public static Set availableProperties() { return set; } - var p = t.getCurrentCalculation().getProblem(); + var p = ( (Calculation) t.getResponse() ).getProblem(); if (p != null) { var fullList = p.listedKeywords(); - fullList.addAll(t.getExperimentalCurve().listedKeywords()); + fullList.addAll( ( (ExperimentalData) t.getInput() ).listedKeywords()); NumericPropertyKeyword key; for (Flag property : flags) { @@ -61,28 +62,45 @@ public static Set availableProperties() { return set; } - + + public static Flag get(NumericPropertyKeyword key) { + var flag = flags.stream().filter(f -> f.getType() == key).findAny(); + return flag.isPresent() ? flag.get() : null; + } + /** - * Finds what properties are being altered in the search - * - * @param t task for which the active parameters should be listed - * @return a {@code List} of property types represented by - * {@code NumericPropertyKeyword}s + * Creates a deep copy of the flags collection. + * @return a deep copy of the flags */ - public static List activeParameters(SearchTask t) { - var c = t.getCurrentCalculation(); - //problem dependent - var allActiveParams = selectActiveAndListed(flags, c.getProblem()); - //problem independent (lower/upper bound) - var listed = selectActiveAndListed(flags, t.getExperimentalCurve().getRange() ); - allActiveParams.addAll(listed); - return allActiveParams; + + public static List storeState() { + var copy = new ArrayList(); + for(Flag f : flags) { + copy.add(new Flag(f)); + } + return copy; + } + + /** + * Loads the argument into the current list of flags. + * This will update any matching flags and assign values correpon + * @param flags + */ + + public static void loadState(List flags) { + for(Flag f : ActiveFlags.flags) { + Optional existingFlag = flags.stream().filter(fl -> + fl.getType() == f.getType()).findFirst(); + if(existingFlag.isPresent()) { + f.setValue((boolean) existingFlag.get().getValue()); + } + } } public static List selectActiveAndListed(List flags, PropertyHolder listed) { //return empty list if(listed == null) { - return new ArrayList(); + return new ArrayList<>(); } return selectActiveTypes(flags).stream() diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 239760e9..273b9361 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -6,7 +6,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -53,7 +53,7 @@ private BFGSOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); //p[k] diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index da33dddb..e6fc8a3b 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -3,6 +3,7 @@ import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.math.linear.SquareMatrix; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -18,7 +19,7 @@ public class ComplexPath extends GradientGuidedPath { private SquareMatrix hessian; private SquareMatrix inverseHessian; - protected ComplexPath(SearchTask task) { + protected ComplexPath(GeneralTask task) { super(task); } @@ -29,8 +30,8 @@ protected ComplexPath(SearchTask task) { * @param task */ @Override - public void configure(SearchTask task) { - hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); + public void configure(GeneralTask task) { + hessian = createIdentityMatrix(this.getParameters().dimension()); inverseHessian = createIdentityMatrix(hessian.getData().length); super.configure(task); } diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 3b78913d..7976646e 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -7,17 +7,17 @@ import pulse.math.ParameterVector; import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.linear.LinearOptimiser; import pulse.search.linear.WolfeOptimiser; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.util.InstanceDescriptor; public abstract class CompositePathOptimiser extends GradientBasedOptimiser { private InstanceDescriptor instanceDescriptor - = new InstanceDescriptor( + = new InstanceDescriptor<>( "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; @@ -46,7 +46,7 @@ private void initLinearOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task boolean accept = true; @@ -56,11 +56,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.getResponse().objectiveFunction(task); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -70,11 +70,15 @@ public boolean iteration(SearchTask task) throws SolverException { p.setLinearStep(step); // new set of parameters determined through search - var candidateParams = parameters.sum(dir.multiply(step)); + var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateVector = new ParameterVector(parameters, candidateParams); - task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters - - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + if(candidateVector.findMalformedElements().isEmpty()) { + task.assign(candidateVector); // assign new parameters + } + + double newCost = task.getResponse().objectiveFunction(task); + // calculate the sum of squared residuals if (newCost > initialCost - EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS @@ -134,8 +138,7 @@ public List listedTypes() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index a3db7fc6..abee584f 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -2,7 +2,6 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.isDiscrete; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; @@ -14,15 +13,15 @@ import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public abstract class GradientBasedOptimiser extends PathOptimiser { private double gradientResolution; private double gradientStep; - - private final static double resolutionHigh = (double)def(GRADIENT_RESOLUTION).getValue(); - private final static double resolutionLow = 5E-2; //TODO + + private final static double RESOLUTION_HIGH = (double) def(GRADIENT_RESOLUTION).getValue(); + private final static double RESOLUTION_LOW = 5E-2; //TODO /** * Abstract constructor that sets up the default @@ -34,6 +33,7 @@ public abstract class GradientBasedOptimiser extends PathOptimiser { */ protected GradientBasedOptimiser() { super(); + this.gradientResolution = gradientStep = RESOLUTION_HIGH; } /** @@ -44,9 +44,11 @@ protected GradientBasedOptimiser() { * * @see pulse.properties.Flag.defaultList() */ + @Override public void reset() { super.reset(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); + gradientResolution = RESOLUTION_HIGH; + gradientStep = gradientResolution; } /** @@ -74,26 +76,29 @@ public void reset() { * @return the gradient of the target function * @throws SolverException */ - public Vector gradient(SearchTask task) throws SolverException { + public Vector gradient(GeneralTask task) throws SolverException { final var params = task.searchVector(); + final var pVector = params.toVector(); var grad = new Vector(params.dimension()); - - for (int i = 0; i < params.dimension(); i++) { - NumericProperty defProp = NumericProperties.def(params.getIndex(i)); - double dx = dx(defProp, params.get(i)); + final var ps = params.getParameters(); + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = ps.get(i).getIdentifier().getKeyword(); + var defProp = key != null ? NumericProperties.def(key) : null; + double dx = dx(defProp, ps.get(i).inverseTransform()); final var shift = new Vector(params.dimension()); shift.set(i, 0.5 * dx); - task.assign(new ParameterVector(params, params.sum(shift))); - final double ss2 = task.solveProblemAndCalculateCost(); + var shiftVector = new ParameterVector(params, pVector.sum(shift)); + task.assign(shiftVector); + final double ss2 = task.objectiveFunction(); - task.assign(new ParameterVector(params, params.subtract(shift))); - final double ss1 = task.solveProblemAndCalculateCost(); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + final double ss1 = task.objectiveFunction(); grad.set(i, (ss2 - ss1) / dx); - } task.assign(params); @@ -101,35 +106,29 @@ public Vector gradient(SearchTask task) throws SolverException { return grad; } - + /** - * Calculates the gradient step. Ensures dx is not zero even if the parameter values is. - * Applicable to discrete properties. - * @param defProp the default property + * Calculates the gradient step. Ensures dx is not zero even if the + * parameter values is. Applicable to discrete properties. + * + * @param defProp the default property * @param value the value of the parameter under the optimisation vector * @return the gradient step */ - protected double dx(NumericProperty defProp, double value) { - boolean discrete = defProp.isDiscrete(); - return (discrete ? resolutionLow : resolutionHigh) - * (Math.abs(value) < 1E-20 - ? defProp.getMaximum().doubleValue() - : value); - } + double result; + + if (defProp == null) { + result = gradientResolution * (Math.abs(value) < 1E-20 ? 0.01 : value); + } else { + boolean discrete = defProp.isDiscrete(); + result = (discrete ? RESOLUTION_LOW : gradientResolution) + * (Math.abs(value) < 1E-20 + ? defProp.getMaximum().doubleValue() + : value); + } - /** - * Checks whether a discrete property is being optimised and selects the - * gradient step best suited to the optimisation strategy. Should be called - * before creating the optimisation path. - * - * @param task the search task defining the search vector - */ - public void configure(SearchTask task) { - var params = task.searchVector(); - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + return result; } public void setGradientResolution(NumericProperty resolution) { diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 9dcab909..7b9a06cc 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -4,6 +4,8 @@ import java.util.logging.Logger; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.tasks.logs.Status; @@ -32,7 +34,8 @@ public class GradientGuidedPath extends IterativeState { private Vector gradient; private double minimumPoint; - protected GradientGuidedPath(SearchTask t) { + protected GradientGuidedPath(GeneralTask t) { + super(t); configure(t); } @@ -41,15 +44,15 @@ protected GradientGuidedPath(SearchTask t) { * direction of search.Sets the minimum point to 0.0. * * @param t the {@code SearchTask}, for which this {@code Path} is created. - * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.search.direction.PathSolver.direction(Path) */ - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.reset(); try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); } catch (SolverException ex) { - t.notifyFailedStatus(ex); + t.onSolverException( new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + ex.printStackTrace(); } minimumPoint = 0.0; } diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index 15b9f9a9..7aee8422 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -5,6 +5,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; public interface HessianDirectionSolver extends DirectionSolver { @@ -38,7 +39,7 @@ public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { var dirv = new DMatrixRMaj(dimg, 1); if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { - throw new SolverException("Singular matrix!"); + throw new SolverException("Singular matrix!", OPTIMISATION_ERROR); } result = new Vector(dirv.getData()); diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 1db8ba2e..05fbacab 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -5,6 +5,7 @@ import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.properties.NumericProperty; +import pulse.search.GeneralTask; public class IterativeState { @@ -23,6 +24,10 @@ public IterativeState(IterativeState other) { this.cost = other.cost; } + public IterativeState(GeneralTask t) { + this.parameters = t.searchVector(); + } + //default constructor public IterativeState() {} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 80bd5e5a..4c3247aa 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -16,15 +16,16 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import static pulse.search.direction.CompositePathOptimiser.EPS; import pulse.search.statistics.OptimiserStatistic; -import pulse.search.statistics.ResidualStatistic; import pulse.search.statistics.SumOfSquares; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.ui.Messages; /** @@ -35,7 +36,7 @@ */ public class LMOptimiser extends GradientBasedOptimiser { - private static LMOptimiser instance = new LMOptimiser(); + private static final LMOptimiser instance = new LMOptimiser(); private double dampingRatio; /** @@ -53,7 +54,7 @@ private LMOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); // the previous path of the task boolean accept = true; //accept the step by default @@ -63,11 +64,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.objectiveFunction(); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -76,16 +77,17 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); - var candidate = parameters.sum(lmDirection); + var candidate = parameters.toVector().sum(lmDirection); if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { - throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + throw new SolverException("Illegal candidate parameters: not finite! " + + p.getIteration(), ILLEGAL_PARAMETERS); } task.assign(new ParameterVector( parameters, candidate)); // assign new parameters - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals /* * Delayed gratification @@ -115,11 +117,12 @@ public boolean iteration(SearchTask task) throws SolverException { * Hessian matrix. */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); + var rs = task.getResponse().getOptimiserStatistic(); //store residual vector at current parameters - p.setResidualVector(new Vector(residualVector(task.getCurrentCalculation().getOptimiserStatistic()))); + p.setResidualVector(new Vector(rs.residualsArray())); // Calculate the Jacobian -- if needed if (p.isComputeJacobian()) { @@ -132,7 +135,8 @@ public void prepare(SearchTask task) throws SolverException { p.setGradient(g1); if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { - throw new SolverException("Could not calculate objective function gradient"); + throw new SolverException("Could not calculate objective function gradient", + OPTIMISATION_ERROR); } // the Hessian is then regularised by adding labmda*I @@ -166,39 +170,51 @@ public void prepare(SearchTask task) throws SolverException { * @throws SolverException * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() */ - public RectangularMatrix jacobian(SearchTask task) throws SolverException { + public RectangularMatrix jacobian(GeneralTask task) throws SolverException { - var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); + var residualCalculator = task.getResponse().getOptimiserStatistic(); var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); + final var pVector = params.toVector(); final int numPoints = p.getResidualVector().dimension(); final int numParams = params.dimension(); var jacobian = new double[numPoints][numParams]; - + var ps = params.getParameters(); + for (int i = 0; i < numParams; i++) { - double dx = dx( NumericProperties.def(params.getIndex(i)), params.get(i)); + var key = ps.get(i).getIdentifier().getKeyword(); + double dx = dx( + key != null ? NumericProperties.def(key) : null, + ps.get(i).inverseTransform()); final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); // + shift - task.assign(new ParameterVector(params, params.sum(shift))); - task.solveProblemAndCalculateCost(); - var r1 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.sum(shift))); + task.objectiveFunction(); + var r = residualCalculator.getResiduals(); + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { + jacobian[j][i] = r.get(j) / dx; + + } + // - shift - task.assign(new ParameterVector(params, params.subtract(shift))); - task.solveProblemAndCalculateCost(); - var r2 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + task.objectiveFunction(); - for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { - jacobian[j][i] = (r1[j] - r2[j]) / dx; + jacobian[j][i] -= r.get(j) / dx; } @@ -210,14 +226,9 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { return Matrices.createMatrix(jacobian); } - - private static double[] residualVector(ResidualStatistic rs) { - return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); - } - + @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new LMPath(t); } diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 3f006e19..2528968b 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -3,7 +3,7 @@ import pulse.math.linear.RectangularMatrix; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; class LMPath extends ComplexPath { @@ -13,12 +13,12 @@ class LMPath extends ComplexPath { private double lambda; private boolean computeJacobian; - public LMPath(SearchTask t) { + public LMPath(GeneralTask t) { super(t); } @Override - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.configure(t); this.lambda = 1.0; computeJacobian = true; diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index d3571159..cb5d4fa6 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -14,8 +14,8 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.statistics.OptimiserStatistic; -import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,15 +93,15 @@ public void reset() { * @see direction(Path) * @see pulse.search.linear.LinearOptimiser */ - public abstract boolean iteration(SearchTask task) throws SolverException; - + public abstract boolean iteration(GeneralTask task) throws SolverException; + /** * Defines a set of procedures to be run at the end of the search iteration. * * @param task the {@code SearchTask} undergoing optimisation * @throws SolverException */ - public abstract void prepare(SearchTask task) throws SolverException; + public abstract void prepare(GeneralTask task) throws SolverException; public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); @@ -232,6 +232,6 @@ public boolean compatibleWith(OptimiserStatistic os) { * @param t the task, the optimisation path of which will be tracked * @return a {@code Path} instance */ - public abstract IterativeState initState(SearchTask t); + public abstract IterativeState initState(GeneralTask t); } diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index bf89b075..8b2f7081 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -7,6 +7,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -35,7 +36,7 @@ private SR1Optimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 390437f7..8fcbc31b 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -2,7 +2,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -36,7 +36,7 @@ private SteepestDescentOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { ((GradientGuidedPath) task.getIterativeState()).setGradient(gradient(task)); } @@ -63,9 +63,8 @@ public static SteepestDescentOptimiser getInstance() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new GradientGuidedPath(t); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java new file mode 100644 index 00000000..f19d824f --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java @@ -0,0 +1,49 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class ConstrictionMover implements Mover { + + private double c1; //social + private double c2; //cognitive + private double chi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_C = 1.49618; + + public ConstrictionMover() { + chi = DEFAULT_CHI; + c1 = c2 = DEFAULT_C; + } + + @Override + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { + var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + Vector nsum = new Vector(n); + + var localBest = p.getBestState().getPosition(); //best position by local particle + var localBestV = localBest.toVector(); + var globalBest = gBest.getPosition(); //best position by any particle + var globalBestV = globalBest.toVector(); + + nsum = nsum.sum(Vector.random(n, 0.0, c1) + .multComponents(localBestV.subtract(curPosV)) + ); + + nsum = nsum.sum(Vector.random(n, 0.0, c2) + .multComponents(globalBestV.subtract(curPosV)) + ); + + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + + return new ParticleState( + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); + } + +} diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index ab4ca1f8..b6869ec6 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -16,29 +16,30 @@ public FIPSMover() { } @Override - public ParticleState attemptMove(Particle p, Particle[] neighbours) { + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + final double nLength = (double) neighbours.length; - var pos = current.getPosition(); - - final int n = pos.dimension(); - var nsum = new Vector(n); + Vector nsum = new Vector(n); for (var neighbour : neighbours) { - var nPos = neighbour.getCurrentState().getPosition(); - nsum = nsum.sum(Vector.random(n, 0.0, phi).multComponents(nPos.subtract(pos))); + var nBestPos = neighbour.getBestState().getPosition(); //best position ever achieved so far by the neighbour + nsum = nsum.sum(Vector.random(n, 0.0, phi/nLength) + .multComponents(nBestPos.toVector().subtract(curPosV)) + ); } - nsum = nsum.multiply(1.0 / ((double) neighbours.length)); - - var newVelocity = (current.getVelocity().sum(nsum)).multiply(chi); - var newPosition = pos.sum(newVelocity); - System.out.println(newPosition); - + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + return new ParticleState( - new ParameterVector(pos, newPosition), - new ParameterVector(pos, newVelocity)); + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index 6dd7387d..d1db1d0c 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -2,6 +2,6 @@ public interface Mover { - public ParticleState attemptMove(Particle p, Particle[] neighbours); + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java index e5384505..d5031dbb 100644 --- a/src/main/java/pulse/search/direction/pso/Particle.java +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -19,6 +19,7 @@ package pulse.search.direction.pso; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -43,10 +44,10 @@ public void adopt(ParticleState state) { this.current = state; } - public void evaluate(SearchTask t) throws SolverException { + public void evaluate(GeneralTask t) throws SolverException { var params = t.searchVector(); t.assign(current.getPosition()); - current.setFitness(t.solveProblemAndCalculateCost()); + current.setFitness(t.objectiveFunction()); t.assign(params); if (current.isBetterThan(pbest)) { diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index e5fcdc09..68f68c13 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -1,6 +1,7 @@ package pulse.search.direction.pso; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; public class ParticleState { @@ -13,10 +14,8 @@ public ParticleState(ParameterVector cur) { this.velocity = new ParameterVector(cur); //set initial velocity to zero - for (int i = 0, n = velocity.dimension(); i < n; i++) { - velocity.set(i, 0.0); - } - + velocity.setValues(new Vector(cur.dimension())); + this.fitness = Double.MAX_VALUE; } @@ -35,22 +34,16 @@ public boolean isBetterThan(ParticleState s) { return this.fitness < s.fitness; } - public void randomise(ParameterVector pos) { - - this.position = new ParameterVector(pos); - - for (int i = 0, n = position.dimension(); i < n; i++) { - - var bounds = position.getBounds(); - - double max = bounds[i].getMaximum(); - double min = bounds[i].getMinimum(); - - double value = min + Math.random() * (max - min); - position.set(i, value); - - } + public final void randomise(ParameterVector pos) { + double[] randomValues = pos.getParameters().stream().mapToDouble(p -> { + double min = p.getBounds().getMinimum(); + double max = p.getBounds().getMaximum(); + return min + Math.random() * (max - min); + }).toArray(); + + Vector randomVector = new Vector(randomValues); + position.setValues(randomVector); } public ParameterVector getPosition() { diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index badabe99..6f39f8b0 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -1,58 +1,72 @@ package pulse.search.direction.pso; -public class ParticleSwarmOptimiser //extends PathOptimiser -{ +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.search.statistics.OptimiserStatistic; + +public class ParticleSwarmOptimiser extends PathOptimiser { private SwarmState swarmState; private Mover mover; public ParticleSwarmOptimiser() { swarmState = new SwarmState(); - mover = new FIPSMover(); + mover = new ConstrictionMover(); } protected void moveParticles() { var topology = swarmState.getNeighborhoodTopology(); for (var p : swarmState.getParticles()) { - p.adopt(mover.attemptMove(p, topology.neighbours(p, swarmState))); + p.adopt(mover.attemptMove(p, + topology.neighbours(p, swarmState), + swarmState.getBestSoFar())); + var data = p.getCurrentState().getPosition().toVector().getData(); + StringBuilder sb = new StringBuilder().append(p.getId()).append(" "); + for(var d : data) { + sb.append(d).append(" "); + } + System.err.println(sb.toString()); } } /** * Iterates the swarm. * - * @param max_iterations max number of iterations to be computed by the - * swarm. */ + @Override + public boolean iteration(GeneralTask task) throws SolverException { + this.prepare(task); - /* - @Override - public boolean iteration(SearchTask task) throws SolverException { - this.prepare(task); - - swarmState.evaluate(task); - moveParticles(); - - swarmState.incrementStep(); - - task.assign( swarmState.bestSoFar().getPosition() ); - task.solveProblemAndCalculateCost(); - - return true; - } - */ + swarmState.evaluate(task); + swarmState.bestSoFar(); + moveParticles(); + + swarmState.incrementStep(); + + task.assign(swarmState.getBestSoFar().getPosition()); + task.objectiveFunction(); + + return true; + } + + @Override + public void prepare(GeneralTask task) throws SolverException { + swarmState.prepare(task); + } + + @Override + public IterativeState initState(GeneralTask t) { + swarmState.prepare(t); + swarmState.create(); + return swarmState; + } + + //TODO + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return false; + } - /* - @Override - public void prepare(SearchTask task) throws SolverException { - swarmState.prepare(task); - } - - @Override - public IterativeState initState(SearchTask t) { - swarmState.prepare(t); - swarmState.create(); - return swarmState; - } - */ } diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 99f31bdd..8fa5c245 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -32,7 +32,7 @@ public class StaticTopologies { final int latticeParameter = (int) Math.sqrt(ps.length); - final int row = i % latticeParameter; + final int row = i / latticeParameter; final int column = i - row * latticeParameter; final int above = column + (row > 0 diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 4a2f4244..7baa955f 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -2,8 +2,8 @@ import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.IterativeState; -import pulse.tasks.SearchTask; public class SwarmState extends IterativeState { @@ -12,13 +12,13 @@ public class SwarmState extends IterativeState { private Particle[] particles; private NeighbourhoodTopology neighborhoodTopology; - private Particle bestSoFar; + private ParticleState bestSoFar; private int bestSoFarIndex; private final static int DEFAULT_PARTICLES = 16; public SwarmState() { - this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); + this(DEFAULT_PARTICLES, StaticTopologies.RING); } public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { @@ -28,13 +28,13 @@ public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopol this.bestSoFarIndex = -1; } - public void evaluate(SearchTask t) throws SolverException { - for (var p : particles) { + public void evaluate(GeneralTask t) throws SolverException { + for (Particle p : particles) { p.evaluate(t); } } - public void prepare(SearchTask t) { + public void prepare(GeneralTask t) { seed = t.searchVector(); } @@ -47,9 +47,8 @@ public void create() { /** * Returns the best state achieved by any particle so far. * - * @return State object. */ - public ParticleState bestSoFar() { + public void bestSoFar() { int bestIndex = 0; double fitness = 0; @@ -65,11 +64,16 @@ public ParticleState bestSoFar() { } } - - this.bestSoFar = particles[bestIndex]; - this.bestSoFarIndex = bestIndex; - - return bestSoFar.getBestState(); + + //determine the current best + ParticleState curBest = particles[bestIndex].getCurrentState(); + + //is curBest the best so far? + if(bestSoFar == null || curBest.isBetterThan(bestSoFar) ) { + this.bestSoFar = curBest; + this.bestSoFarIndex = bestIndex; + } + } public NeighbourhoodTopology getNeighborhoodTopology() { @@ -93,11 +97,11 @@ public void setParticles(Particle[] particles) { this.particles = particles; } - public Particle getBestSoFar() { + public ParticleState getBestSoFar() { return bestSoFar; } - public void setBestSoFar(Particle bestSoFar) { + public void setBestSoFar(ParticleState bestSoFar) { this.bestSoFar = bestSoFar; } @@ -109,4 +113,4 @@ public void setBestSoFarIndex(int bestSoFarIndex) { this.bestSoFarIndex = bestSoFarIndex; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index a4229d27..4c9f1bea 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -3,6 +3,7 @@ import pulse.math.ParameterVector; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientGuidedPath; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -46,11 +47,12 @@ private GoldenSectionOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { final double EPS = 1e-14; final var params = task.searchVector(); + var vParams = params.toVector(); final Vector direction = ((GradientGuidedPath) task.getIterativeState()).getDirection(); var segment = domain(params, direction); @@ -61,13 +63,13 @@ public double linearStep(SearchTask task) throws SolverException { final double alpha = segment.getMinimum() + t; final double one_minus_alpha = segment.getMaximum() - t; - final var newParams1 = params.sum(direction.multiply(alpha)); // alpha + final var newParams1 = vParams.sum(direction.multiply(alpha)); // alpha task.assign(new ParameterVector(params, newParams1)); - final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) + final double ss2 = task.objectiveFunction(); // f(alpha) - final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + final var newParams2 = vParams.sum(direction.multiply(one_minus_alpha)); // 1 - alpha task.assign(new ParameterVector(params, newParams2)); - final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) + final double ss1 = task.objectiveFunction(); // f(1-alpha) task.assign(new ParameterVector(params, newParams2)); // return to old position diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index 891b42bb..a82b9188 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,6 +14,7 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -45,7 +47,7 @@ protected LinearOptimiser() { * to a lower SSR value of this {@code task} * @throws SolverException */ - public abstract double linearStep(SearchTask task) throws SolverException; + public abstract double linearStep(GeneralTask task) throws SolverException; /** * Sets the domain for this linear search on {@code p}. @@ -68,29 +70,32 @@ public static Segment domain(ParameterVector x, Vector p) { double alphaMax = Double.POSITIVE_INFINITY; double alpha; - for (int i = 0; i < x.dimension(); i++) { + var params = x.getParameters(); - final double component = p.get(i); + for (Parameter xp : params) { + + final double component = p.get(params.indexOf(xp)); //check if zero - if (component < EPS && component > -EPS) { - continue; - } + if (Math.abs(component) > EPS) { - var bound = x.getTransformedBounds(i); + var bound = xp.getTransformedBounds(); - alpha = abs( - ((component > 0 ? bound.getMaximum() : bound.getMinimum()) - x.get(i)) - / component); + alpha = abs( + ((component > 0 ? bound.getMaximum() + : bound.getMinimum()) - xp.inverseTransform()) + / component); + + if (Double.isFinite(alpha) && alpha < alphaMax) { + alphaMax = alpha; + } - if (Double.isFinite(alpha) && alpha < alphaMax) { - alphaMax = alpha; } } //check that alphaMax is not zero! otherwise the optimise will crash - return new Segment(0.0, + return new Segment(0.0, Math.max(alphaMax, 1E-10)); } @@ -135,7 +140,7 @@ public Set listedKeywords() { set.add(LINEAR_RESOLUTION); return set; } - + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == LINEAR_RESOLUTION) { diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 751d7d67..af3a2cd3 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -6,6 +6,7 @@ import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientBasedOptimiser; import pulse.search.direction.GradientGuidedPath; import pulse.search.direction.PathOptimiser; @@ -64,7 +65,7 @@ private WolfeOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); @@ -75,9 +76,10 @@ public double linearStep(SearchTask task) throws SolverException { final double G1P_ABS = abs(G1P); var params = task.searchVector(); + var vParams = params.toVector(); Segment segment = domain(params, direction); - double cost1 = task.solveProblemAndCalculateCost(); + double cost1 = task.objectiveFunction(); double randomConfinedValue = 0; double g2p; @@ -88,11 +90,11 @@ public double linearStep(SearchTask task) throws SolverException { randomConfinedValue = segment.randomValue(); - final var newParams = params.sum(direction.multiply(randomConfinedValue)); + final var newParams = vParams.sum(direction.multiply(randomConfinedValue)); task.assign(new ParameterVector(params, newParams)); - final double cost2 = task.solveProblemAndCalculateCost(); + final double cost2 = task.objectiveFunction(); /** * Checks if the first Armijo inequality is not satisfied. In this diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 948adfe5..7113c14c 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -1,10 +1,9 @@ package pulse.search.statistics; -import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; /** * A statistical optimality criterion relying on absolute deviations or the L1 @@ -26,11 +25,13 @@ public AbsoluteDeviations(AbsoluteDeviations another) { /** * Calculates the L1 norm statistic, which simply sums up the absolute * values of residuals. + * @param t */ @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> abs(r[1])).reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream() + .mapToDouble(a -> Math.abs(a)).average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 99ac0048..be695931 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -4,8 +4,8 @@ import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; import umontreal.ssj.gof.GofStat; import umontreal.ssj.probdist.NormalDist; @@ -21,12 +21,14 @@ public class AndersonDarlingTest extends NormalityTest { * test with the input parameters formed by the {@code task} residuals and a * normal distribution with zero mean and variance equal to the residuals * variance. + * @param task + * @return */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { calculateResiduals(task); - double[] residuals = super.transformResiduals(); + double[] residuals = residualsArray(); var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); var testResult = GofStat.andersonDarling(residuals, nd); @@ -42,7 +44,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { test(t); } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index c9775447..670d7e58 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -20,7 +20,7 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv "Correlation Test Selector", CorrelationTest.class); static { - instanceDescriptor.setSelectedDescriptor(PearsonCorrelation.class.getSimpleName()); + instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); } public CorrelationTest() { diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index f4925014..573280c7 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -1,6 +1,6 @@ package pulse.search.statistics; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public class EmptyTest extends NormalityTest { @@ -8,7 +8,7 @@ public class EmptyTest extends NormalityTest { * Always returns true */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { return true; } @@ -18,7 +18,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { // deliberately empty } diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 672bdc16..9a39bc41 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.search.statistics; import org.apache.commons.math3.distribution.FDistribution; @@ -21,7 +6,6 @@ /** * A static class for testing two calculations based on the Fischer test (F-Test) * implemented in Apache Commons Math. - * @author Artem Lunev */ public class FTest { diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index ceb4ceb2..7d5a854d 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -6,8 +6,7 @@ import org.apache.commons.math3.distribution.NormalDistribution; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.apache.commons.math3.stat.inference.TestUtils; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The Kolmogorov-Smirnov normality test as implemented in @@ -20,7 +19,7 @@ public class KSTest extends NormalityTest { private NormalDistribution nd; @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); this.setStatistic(derive(TEST_STATISTIC, @@ -29,9 +28,9 @@ public boolean test(SearchTask task) { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - residuals = transformResiduals(); + residuals = residualsArray(); final double sd = (new StandardDeviation()).evaluate(residuals); nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 324acc5e..fbba1ccb 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -12,7 +12,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyEvent; /** @@ -38,8 +38,8 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { } @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of parameters + public void evaluate(GeneralTask t) { + kq = t.searchVector().dimension(); //number of parameters calcCriterion(); } @@ -104,11 +104,11 @@ public OptimiserStatistic getOptimiserStatistic() { return os; } - public void setOptimiserStatistic(OptimiserStatistic os) { + public final void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; } - public void setStatistic(NumericProperty p) { + public final void setStatistic(NumericProperty p) { requireType(p, MODEL_CRITERION); this.criterion = (double) p.getValue(); } diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index a8de54c4..f383a83f 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -8,6 +8,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -44,7 +45,7 @@ public static void setStatisticalSignificance(NumericProperty alpha) { NormalityTest.significance = (double) alpha.getValue(); } - public abstract boolean test(SearchTask task); + public abstract boolean test(GeneralTask task); @Override public NumericProperty getStatistic() { diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index b29f6b16..0feceed6 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -1,13 +1,13 @@ package pulse.search.statistics; import static java.lang.Math.pow; +import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; -import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The coefficient of determination represents the goodness of fit that a @@ -25,7 +25,7 @@ public RSquaredTest() { } @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); sos = new SumOfSquares(); return getStatistic().compareTo(signifiance) > 0; @@ -48,35 +48,25 @@ public boolean test(SearchTask task) { * page
*/ @Override - public void evaluate(SearchTask t) { - var reference = t.getExperimentalCurve(); - + public void evaluate(GeneralTask t) { + var yr = t.getInput().getY(); sos.evaluate(t); - final int start = reference.getIndexRange().getLowerBound(); - final int end = reference.getIndexRange().getUpperBound(); - - final double mean = mean(reference, start, end); + final double mean = mean(yr); double TSS = 0; - - for (int i = start; i < end; i++) { - TSS += pow(reference.signalAt(i) - mean, 2); + int size = yr.size(); + + for (int i = 0; i < size; i++) { + TSS += pow(yr.get(i) - mean, 2); } - TSS /= (end - start); - + TSS /= size; + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); } - private double mean(ExperimentalData data, final int start, final int end) { - double mean = 0; - - for (int i = start; i < end; i++) { - mean += data.signalAt(i); - } - - mean /= (end - start); - return mean; + private double mean(List input) { + return input.stream().mapToDouble(d -> d).average().getAsDouble(); } public SumOfSquares getSumOfSquares() { diff --git a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java new file mode 100644 index 00000000..af8710fe --- /dev/null +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -0,0 +1,60 @@ +package pulse.search.statistics; + +import pulse.input.IndexRange; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; + +/** + * This is an experimental feature. + * + */ +public class RangePenalisedLeastSquares extends SumOfSquares { + + private double lambda = 0.1; + + public RangePenalisedLeastSquares() { + super(); + } + + public RangePenalisedLeastSquares(RangePenalisedLeastSquares rls) { + super(rls); + this.lambda = rls.lambda; + } + + /** + * The lambda is the regularisation strength. + * + * @return the lambda factor. + */ + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + var x = t.getInput().getX(); + double partialRange = t.getInput().bounds().length(); + double fullRange = x.get(x.size() - 1) - x.get(IndexRange.closestLeft(0.0, x)); + final double statistic = ssr + lambda * (fullRange - partialRange)/fullRange; + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Range-Penalised Least Squares"; + } + + @Override + public OptimiserStatistic copy() { + return new RangePenalisedLeastSquares(this); + } + +} diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index a2205258..6fcd8936 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * This is an experimental feature. The objective function here is equal to the @@ -12,19 +11,16 @@ * dimensionality are favoured. * */ -public class RegularisedLeastSquares extends OptimiserStatistic { +public class RegularisedLeastSquares extends SumOfSquares { private double lambda = 1e-4; - private SumOfSquares sos; - + public RegularisedLeastSquares() { super(); - sos = new SumOfSquares(); } public RegularisedLeastSquares(RegularisedLeastSquares rls) { super(rls); - sos = new SumOfSquares(rls.sos); this.lambda = rls.lambda; } @@ -47,10 +43,11 @@ public void setLambda(double lambda) { * @see pulse.search.statistics.SumOfSquares */ @Override - public void evaluate(SearchTask t) { - sos.evaluate(t); - final double ssr = (double) sos.getStatistic().getValue(); - final double statistic = ssr + lambda * t.searchVector().lengthSq(); + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + final double statistic = ssr + lambda * t.searchVector().toVector().lengthSq(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } @@ -59,11 +56,6 @@ public String getDescriptor() { return "L2 Regularised Least Squares"; } - @Override - public double variance() { - return (double) sos.getStatistic().getValue(); - } - @Override public OptimiserStatistic copy() { return new RegularisedLeastSquares(this); diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 01ed160b..a00ad587 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -1,19 +1,18 @@ package pulse.search.statistics; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static pulse.input.IndexRange.closestLeft; -import static pulse.input.IndexRange.closestRight; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; -import java.util.ArrayList; import java.util.List; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * An abstract statistic (= a numeric value resulting from a statistical @@ -29,30 +28,29 @@ public abstract class ResidualStatistic extends Statistic { private double statistic; - private List residuals; + private List rx; + private List ry; public ResidualStatistic() { super(); - residuals = new ArrayList<>(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); setPrefix("Residuals"); } public ResidualStatistic(ResidualStatistic another) { this.statistic = another.statistic; - this.residuals = new ArrayList<>(another.residuals); - } - - public double[] transformResiduals() { - return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); } /** * This will calculate the residuals for the {@code task} using the time - * sequence defined by the {@code ExperimentalData} object. The residuals - * are calculated between the model, which was previously used to populate - * the {@code HeatingCurve} and the experimental data. The temperature value - * of the model at the reference time is - * ti. and unknown a + * sequence defined by the {@code ExperimentalData} object.The residuals are + * calculated between the model, which was previously used to populate the + * {@code HeatingCurve}and the experimental data.The temperature value of + * the model at the reference time is + * ti.and unknown a * priori. Therefore, it needs to be interpolated based on the discrete * dataset generated by the solver. The interpolation is currently done * using natural cubic splines, which are re-constructed each time a new @@ -62,50 +60,72 @@ public double[] transformResiduals() { * {@code ExperimentalData} reference. The output of this method is stored * in the field of the {@code residuals} object. * - * @param task the optimisation task + * @param reference + * @param estimate * @see pulse.input.ExperimentalData * @see pulse.HeatingCurve */ - public void calculateResiduals(SearchTask task) { - var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); - var reference = task.getExperimentalCurve(); + public final void calculateResiduals(DiscreteInput reference, Response estimate, int min, int max) { + var y = reference.getY(); + var x = reference.getX(); + + //if size has not changed, use the old list + + if (ry.size() == max - min + 1) { + + for (int i = min; i < max; i++) { - residuals.clear(); - var indexRange = reference.getIndexRange(); - var time = reference.getTimeSequence(); + ry.set(i - min, y.get(i) - estimate.evaluate(x.get(i))); - var s = estimate.getSplineInterpolation(); + } - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); + } + + //else create a new list + + else { - double interpolated; + rx = x.subList(min, max); + ry.clear(); - for (int i = startIndex; i <= endIndex; i++) { - /* - * find the point on the calculated heating curve which has the closest time - * value smaller than the experimental points' time value - */ + for (int i = min; i < max; i++) { - interpolated = s.value(reference.timeAt(i)); + ry.add(y.get(i) - estimate.evaluate(x.get(i))); - residuals.add(new double[]{reference.timeAt(i), - reference.signalAt(i) - interpolated}); // y_exp - y* + } } } + + public void calculateResiduals(DiscreteInput reference, Response estimate) { + var y = reference.getY(); + var x = reference.getX(); + + var estimateRange = estimate.accessibleRange(); + + int min = (int) Math.max(reference.getIndexRange().getLowerBound(), + IndexRange.closestLeft(estimateRange.getMinimum(), x) ); + int max = (int) Math.min(reference.getIndexRange().getUpperBound(), + IndexRange.closestRight(estimateRange.getMaximum(), x) ); + + calculateResiduals(reference, estimate, min, max); + } + + public double[] residualsArray() { + return ry.stream().mapToDouble(d -> d).toArray(); + } - public List getResiduals() { - return residuals; + public final void calculateResiduals(GeneralTask task) { + calculateResiduals(task.getInput(), task.getResponse()); } - public double residualUpperBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); + public List getResiduals() { + return ry; } - public double residualLowerBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); + public List getTimeSequence() { + return rx; } public NumericProperty getStatistic() { @@ -117,10 +137,6 @@ public void setStatistic(NumericProperty statistic) { this.statistic = (double) statistic.getValue(); } - public void incrementStatistic(final double increment) { - this.statistic += increment; - } - @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == OPTIMISER_STATISTIC) { diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 7a3c5e1d..46a7f215 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,5 +1,6 @@ package pulse.search.statistics; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -11,6 +12,6 @@ */ public abstract class Statistic extends PropertyHolder implements Reflexive { - public abstract void evaluate(SearchTask t); - -} + public abstract void evaluate(GeneralTask t); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index c4905ef8..f264ed84 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The standard optimality criterion of the L2 norm condition, or simply @@ -39,11 +38,12 @@ public SumOfSquares(SumOfSquares sos) { * @param t The task containing the reference and calculated curves * @see calculateResiduals() */ + @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> r[1] * r[1]) - .reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream().mapToDouble(r -> r * r) + .average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index af33470b..4755a1db 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -10,15 +10,19 @@ import java.util.List; import java.util.stream.Collectors; +import pulse.Response; import pulse.input.ExperimentalData; import pulse.input.Metadata; +import pulse.math.Segment; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.statistics.BICStatistic; import pulse.search.statistics.FTest; import pulse.search.statistics.ModelSelectionCriterion; @@ -30,7 +34,7 @@ import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; -public class Calculation extends PropertyHolder implements Comparable { +public class Calculation extends PropertyHolder implements Comparable, Response { private Status status; public final static double RELATIVE_TIME_MARGIN = 1.01; @@ -96,7 +100,6 @@ public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; problem.setParent(this); problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); addProblemListeners(problem, curve); } @@ -165,7 +168,7 @@ public void process() throws SolverException { list.forEach(np -> sb.append(String.format("%n %-25s", np)) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } ((Solver) scheme).solve(problem); } @@ -248,6 +251,7 @@ public void setOptimiserStatistic(OptimiserStatistic os) { initModelCriterion(); } + @Override public OptimiserStatistic getOptimiserStatistic() { return os; } @@ -351,4 +355,33 @@ public void setResult(Result result) { } } -} + @Override + public double evaluate(double t) { + return problem.getHeatingCurve().interpolateSignalAt(t); + } + + @Override + public Segment accessibleRange() { + var hc = problem.getHeatingCurve(); + return new Segment(hc.timeAt(0), hc.timeLimit()); + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code SearchTask} and calculate the SSR value + * showing how well (or bad) the calculated solution describes the + * {@code ExperimentalData}. + * + * @param task + * @return the value of SSR (sum of squared residuals). + * @throws pulse.problem.schemes.solvers.SolverException + */ + + @Override + public double objectiveFunction(GeneralTask task) throws SolverException { + process(); + os.evaluate(task); + return (double) os.getStatistic().getValue(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index b17a6dba..733a939c 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -2,12 +2,10 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import static pulse.search.direction.ActiveFlags.activeParameters; import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; import static pulse.tasks.logs.Details.INCOMPATIBLE_OPTIMISER; import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Details.MAX_ITERATIONS_REACHED; import static pulse.tasks.logs.Details.MISSING_BUFFER; import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; @@ -21,25 +19,23 @@ import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.READY; -import static pulse.tasks.processing.Buffer.getSize; import static pulse.util.Reflexive.instantiate; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.direction.ActiveFlags; -import pulse.search.direction.IterativeState; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -47,16 +43,12 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; -import pulse.tasks.logs.Details; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; import pulse.tasks.logs.Status; -import pulse.tasks.processing.Buffer; import pulse.tasks.processing.CorrelationBuffer; -import pulse.util.Accessible; import static pulse.tasks.logs.Status.AWAITING_TERMINATION; -import static pulse.tasks.logs.Status.TERMINATED; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -68,15 +60,11 @@ * * @see pulse.tasks.TaskManager */ -public class SearchTask extends Accessible implements Runnable { +public class SearchTask extends GeneralTask { private Calculation current; private List stored; private ExperimentalData curve; - - private IterativeState path; //current sate - private IterativeState best; //best state - private Buffer buffer; private Log log; private final CorrelationBuffer correlationBuffer; @@ -89,8 +77,8 @@ public class SearchTask extends Accessible implements Runnable { * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private List listeners; - private List statusChangeListeners; + private final List listeners; + private final List statusChangeListeners; /** *

@@ -104,6 +92,7 @@ public class SearchTask extends Accessible implements Runnable { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { + super(); this.statusChangeListeners = new CopyOnWriteArrayList<>(); this.listeners = new CopyOnWriteArrayList<>(); current = new Calculation(this); @@ -114,23 +103,7 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - - /** - * Update the best state. The instance of this class stores two objects of - * the type IterativeState: the current state of the optimiser and the - * global best state. Calling this method will check if a new global best is - * found, and if so, this will store its parameters in the corresponding - * variable. This will then be used at the final stage of running the search - * task, comparing the converged result to the global best, and selecting - * whichever has the lowest cost. Such routine is required due to the - * possibility of some optimisers going uphill. - */ - public void storeState() { - if (best == null || best.getCost() > path.getCost()) { - best = new IterativeState(path); - } - } - + private void addListeners() { InterpolationDataset.addListener(e -> { if (current.getProblem() != null) { @@ -171,218 +144,22 @@ private void addListeners() { public void clear() { stored = new ArrayList<>(); curve.resetRanges(); - buffer = new Buffer(); correlationBuffer.clear(); - buffer.setParent(this); log = new Log(this); initCorrelationTest(); initNormalityTest(); - this.path = null; + //this.path = null; current.clear(); this.checkProblems(true); } - - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code SearchTask} and calculate the SSR value - * showing how well (or bad) the calculated solution describes the - * {@code ExperimentalData}. - * - * @return the value of SSR (sum of squared residuals). - * @throws SolverException - */ - public double solveProblemAndCalculateCost() throws SolverException { - current.process(); - var rs = current.getOptimiserStatistic(); - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - + public List alteredParameters() { - return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); - } - - /** - * Generates a search vector (= optimisation vector) using the search flags - * set by the {@code PathSolver}. - * - * @return an {@code IndexedVector} with search parameters of this - * {@code SearchTaks} - * @see pulse.search.direction.PathSolver.getSearchFlags() - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - public ParameterVector searchVector() { - var flags = ActiveFlags.getAllFlags(); - var keywords = activeParameters(this); - var optimisationVector = new ParameterVector(keywords); - - current.getProblem().optimisationVector(optimisationVector, flags); - curve.getRange().optimisationVector(optimisationVector, flags); - - return optimisationVector; - } - - /** - * Assigns the values of the parameters of this {@code SearchTask} to - * {@code searchParameters}. - * - * @param searchParameters an {@code IndexedVector} with relevant search - * parameters - * @throws pulse.problem.schemes.solvers.SolverException - * @see pulse.problem.statements.Problem.assign(IndexedVector) - */ - public void assign(ParameterVector searchParameters) throws SolverException { - current.getProblem().assign(searchParameters); - curve.getRange().assign(searchParameters); - } - - /** - *

- * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, - * will do nothing. After making some preparatory steps, will initiate a - * loop with successive calls to {@code PathSolver.iteration(this)}, filling - * the buffer and notifying any data change listeners in parallel. This loop - * will go on until either converging results are obtained, or a timeout is - * reached, or if an execution error happens. Whether the run has been - * successful will be determined by comparing the associated - * R2 value with the {@code SUCCESS_CUTOFF}. - *

- */ - @Override - public void run() { - - current.setResult(null); - - /* check of status */ - switch (current.getStatus()) { - case READY: - case QUEUED: - setStatus(IN_PROGRESS); - break; - default: - return; - } - - /* preparatory steps */ - current.getProblem().parameterListChanged(); // get updated list of parameters - - var optimiser = getInstance(); - - path = optimiser.initState(this); - - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); - int bufferSize = (Integer) getSize().getValue(); - buffer.init(); - correlationBuffer.clear(); - - /* search cycle */ - /* sets an independent thread for manipulating the buffer */ - List> bufferFutures = new ArrayList<>(bufferSize); - var singleThreadExecutor = Executors.newSingleThreadExecutor(); - - try { - solveProblemAndCalculateCost(); - } catch (SolverException e1) { - notifyFailedStatus(e1); - } - - outer: - do { - - bufferFutures.clear(); - - for (var i = 0; i < bufferSize; i++) { - - try { - for (boolean finished = false; !finished;) { - finished = optimiser.iteration(this); - } - } catch (SolverException e) { - notifyFailedStatus(e); - break outer; - } - - //if global best is better than the converged value - if (best != null && best.getCost() < path.getCost()) { - try { - //assign the global best parameters - assign(path.getParameters()); - //and try to re-calculate - solveProblemAndCalculateCost(); - } catch (SolverException ex) { - notifyFailedStatus(ex); - } - } - - final var j = i; - - bufferFutures.add(CompletableFuture.runAsync(() -> { - buffer.fill(this, j); - correlationBuffer.inflate(this); - notifyDataListeners(new DataLogEntry(this)); - }, singleThreadExecutor)); - - } - - bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance) - && current.getStatus() == IN_PROGRESS); - - singleThreadExecutor.shutdown(); - - if (current.getStatus() == IN_PROGRESS) { - runChecks(); - } - - } - - private void runChecks() { - - if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed - var status = FAILED; - status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); - setStatus(status); - } else { - - var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected - // correlations - notifyDataListeners(new CorrelationLogEntry(this)); - - if (test) { - var status = AMBIGUOUS; - status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); - setStatus(status); - } else { - // lastly, check if the parameter values estimated in this procedure are - // reasonable - - var properties = alteredParameters(); - - if (properties.stream().anyMatch(np -> !np.validate())) { - var status = FAILED; - status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - } else { - current.getModelSelectionCriterion().evaluate(this); - setStatus(DONE); - } - - } - - } - } - - public void notifyFailedStatus(SolverException e1) { - var status = Status.FAILED; - status.setDetails(Details.SOLVER_ERROR); - status.setDetailedMessage(e1.getMessage()); - e1.printStackTrace(); - setStatus(status); - } + return activeParameters().stream().map(key -> + this.numericProperty(key)).collect(Collectors.toList()); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -404,15 +181,7 @@ public void removeStatusChangeListeners() { public String toString() { return getIdentifier().toString(); } - - public ExperimentalData getExperimentalCurve() { - return curve; - } - - public IterativeState getIterativeState() { - return path; - } - + /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -427,28 +196,6 @@ public void setExperimentalCurve(ExperimentalData curve) { } - /** - * Will return {@code true} if status could be updated. - * - * @param status the status of the task - * @return {@code} true if status has been updated. {@code false} if the - * status was already set to {@code status} previously, or if it could not - * be updated at this time. - * @see Calculation.setStatus() - */ - public boolean setStatus(Status status) { - Objects.requireNonNull(status); - - Status oldStatus = current.getStatus(); - boolean changed = current.setStatus(status) - && (oldStatus != current.getStatus()); - if (changed) { - notifyStatusListeners(new StateEntry(this, status)); - } - - return changed; - } - /** *

* Checks if this {@code SearchTask} is ready to be run.Performs basic check @@ -465,7 +212,7 @@ public boolean setStatus(Status status) { * @param updateStatus */ public void checkProblems(boolean updateStatus) { - var status = current.getStatus(); + var status = getStatus(); if (status == DONE) { return; @@ -484,7 +231,7 @@ public void checkProblems(boolean updateStatus) { s.setDetails(MISSING_HEATING_CURVE); } else if (pathSolver == null) { s.setDetails(MISSING_OPTIMISER); - } else if (buffer == null) { + } else if (getBuffer() == null) { s.setDetails(MISSING_BUFFER); } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { s.setDetails(INCOMPATIBLE_OPTIMISER); @@ -516,24 +263,28 @@ private void notifyStatusListeners(StateEntry e) { l.onStatusChange(e); } } - + @Override - public String describe() { - - var sb = new StringBuilder(); - sb.append(TaskManager.getManagerInstance().getSampleName()); - sb.append("_Task_"); - var extId = curve.getMetadata().getExternalID(); - if (extId < 0) { - sb.append("IntID_").append(identifier.getValue()); - } else { - sb.append("ExtID_").append(extId); + public void run() { + correlationBuffer.clear(); + current.setResult(null); + + /* check of status */ + switch (getStatus()) { + case READY: + case QUEUED: + setStatus(IN_PROGRESS); + break; + default: + return; } - - return sb.toString(); - + + current.getProblem().parameterListChanged(); // get updated list of parameters + setDefaultOptimiser(); + + super.run(); } - + /** * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or * {@code READY}, terminates it by setting its status to {@code TERMINATED}. @@ -541,39 +292,15 @@ public String describe() { * running). */ public void terminate() { - switch (current.getStatus()) { + switch (getStatus()) { case IN_PROGRESS: case QUEUED: - case READY: setStatus(AWAITING_TERMINATION); break; default: } } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - /** - * A {@code SearchTask} is deemed equal to another one if it has the same - * {@code ExperimentalData}. - */ - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (!(o instanceof SearchTask)) { - return false; - } - - return curve.equals(((SearchTask) o).getExperimentalCurve()); - - } - public NormalityTest getNormalityTest() { return normalityTest; } @@ -595,22 +322,17 @@ public CorrelationBuffer getCorrelationBuffer() { public CorrelationTest getCorrelationTest() { return correlationTest; } - - public Calculation getCurrentCalculation() { - return current; - } - + public List getStoredCalculations() { return this.stored; - } + } public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); - } + } public void switchTo(Calculation calc) { - current.setParent(null); current = calc; current.setParent(this); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); @@ -641,5 +363,191 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { l.onTaskListChanged(e); } } + + @Override + public boolean isInProgress() { + return getStatus() == IN_PROGRESS; + } + + @Override + public void intermediateProcessing() { + correlationBuffer.inflate(this); + notifyDataListeners(new DataLogEntry(this)); + } + + @Override + public void onSolverException(SolverException e) { + setStatus(Status.troubleshoot(e)); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id -> + new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + current.getProblem().optimisationVector(optimisationVector); + curve.getRange().optimisationVector(optimisationVector); + + return optimisationVector; + } + + /** + * Assigns the values of the parameters of this {@code SearchTask} to + * {@code searchParameters}. + * + * @param searchParameters an {@code IndexedVector} with relevant search + * parameters + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.statements.Problem.assign(IndexedVector) + */ + @Override + public void assign(ParameterVector searchParameters) throws SolverException { + current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); + } + + @Override + public void postProcessing() { + + if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed + var status = FAILED; + status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + setStatus(status); + } else { + + var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected + // correlations + notifyDataListeners(new CorrelationLogEntry(this)); + + if (test) { + var status = AMBIGUOUS; + status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + setStatus(status); + } else { + // lastly, check if the parameter values estimated in this procedure are + // reasonable + + var properties = this.getIterativeState().getParameters(); + + if (properties.findMalformedElements().size() > 0) { + var status = FAILED; + status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + } else { + current.getModelSelectionCriterion().evaluate(this); + setStatus(DONE); + } + + } + + } + } + + + /** + * Finds what properties are being altered in the search of this SearchTask. + * + * @return a {@code List} of property types represented by + * {@code NumericPropertyKeyword}s + */ + @Override + public List activeParameters() { + var flags = ActiveFlags.getAllFlags(); + //problem dependent + var allActiveParams = ActiveFlags.selectActiveAndListed + (flags, current.getProblem()); + //problem independent (lower/upper bound) + var listed = ActiveFlags.selectActiveAndListed + (flags, curve.getRange() ); + allActiveParams.addAll(listed); + return allActiveParams; + } + + /** + * Will return {@code true} if status could be updated. + * + * @param status the status of the task + * @return {@code} true if status has been updated. {@code false} if the + * status was already set to {@code status} previously, or if it could not + * be updated at this time. + * @see Calculation.setStatus() + */ + + public boolean setStatus(Status status) { + Objects.requireNonNull(status); + + Status oldStatus = getStatus(); + boolean changed = current.setStatus(status) + && (oldStatus != getStatus()); + if (changed) { + notifyStatusListeners(new StateEntry(this, status)); + } + + return changed; + } + + public Status getStatus() { + return current.getStatus(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + /** + * A {@code SearchTask} is deemed equal to another one if it has the same + * {@code ExperimentalData}. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof SearchTask)) { + return false; + } + + return curve.equals(((SearchTask) o).curve); + + } + + @Override + public String describe() { + + var sb = new StringBuilder(); + sb.append(TaskManager.getManagerInstance().getSampleName()); + sb.append("_Task_"); + var extId = curve.getMetadata().getExternalID(); + if (extId < 0) { + sb.append("IntID_").append(identifier.getValue()); + } else { + sb.append("ExtID_").append(extId); + } + + return sb.toString(); + + } + + @Override + public ExperimentalData getInput() { + return curve; + } + + @Override + public Calculation getResponse() { + return current; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index f8801c24..358403df 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -95,14 +95,6 @@ private TaskManager() { selectionListeners = new CopyOnWriteArrayList<>(); taskRepositoryListeners = new CopyOnWriteArrayList<>(); addHierarchyListener(statementListener); - /* - Calculate the half-time once data is loaded. - */ - addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { - getTask(e.getId()).getExperimentalCurve().calculateHalfTime(); - } - }); } /** @@ -123,35 +115,40 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(t.getCurrentCalculation().getStatus() != Status.DONE); + t.checkProblems(t.getStatus() != Status.DONE); //try to start cmputation // notify listeners computation is about to start if (!t.setStatus(QUEUED)) { return; } - + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - + // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - var current = t.getCurrentCalculation(); + Calculation current = (Calculation)t.getResponse(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - if (current.getStatus() == DONE) { - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned + if (null == current.getStatus()) { notifyListeners(e); - t.storeCalculation(); } - else if(current.getStatus() == AWAITING_TERMINATION) { - t.setStatus(Status.TERMINATED); - } - else { - notifyListeners(e); + else switch (current.getStatus()) { + case DONE: + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + t.storeCalculation(); + break; + case AWAITING_TERMINATION: + t.setStatus(Status.TERMINATED); + break; + default: + notifyListeners(e); + break; } }); - + } /** @@ -174,7 +171,7 @@ public void notifyListeners(TaskRepositoryEvent e) { public void executeAll() { var queue = tasks.stream().filter(t -> { - switch (t.getCurrentCalculation().getStatus()) { + switch (t.getStatus()) { case IN_PROGRESS: case EXECUTION_ERROR: return false; @@ -198,7 +195,7 @@ public void executeAll() { */ public boolean isTaskQueueEmpty() { return !tasks.stream().anyMatch(t -> { - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); return status == QUEUED || status == IN_PROGRESS; }); } @@ -262,7 +259,8 @@ public SampleName getSampleName() { return null; } - return optional.get().getExperimentalCurve().getMetadata().getSampleName(); + return ( (ExperimentalData) optional.get().getInput() ) + .getMetadata().getSampleName(); } /** @@ -308,7 +306,8 @@ public SearchTask getTask(Identifier id) { */ public SearchTask getTask(int externalId) { var o = tasks.stream().filter(t - -> Integer.compare(t.getExperimentalCurve().getMetadata().getExternalID(), + -> Integer.compare( ( (ExperimentalData) t.getInput()) + .getMetadata().getExternalID(), externalId) == 0).findFirst(); return o.isPresent() ? o.get() : null; } @@ -340,7 +339,7 @@ public void generateTask(File file) { curves.stream().forEach((ExperimentalData curve) -> { var task = new SearchTask(curve); addTask(task); - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); if (!data.isAcquisitionTimeSensible()) { data.truncate(); } @@ -374,7 +373,6 @@ public void generateTasks(List files) { }; Executors.newSingleThreadExecutor().submit(loader); - } /** @@ -512,8 +510,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { - var properties = t.getCurrentCalculation().getProblem().getProperties(); - var c = t.getExperimentalCurve(); + var properties = ( (Calculation) t.getResponse() ).getProblem().getProperties(); + var c = (ExperimentalData)t.getInput(); properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index cacac2f2..f6785bdf 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -1,8 +1,8 @@ package pulse.tasks.logs; +import pulse.math.ParameterIdentifier; import static pulse.properties.NumericProperties.def; -import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.ImmutablePair; @@ -32,16 +32,20 @@ public String toString() { sb.append("

"); sb.append(""); - for (ImmutablePair key : map.keySet()) { + for (ImmutablePair key : map.keySet()) { sb.append("
Correlation table
x y Correlation
"); - sb.append(def(key.getFirst()).getAbbreviation(false)); + sb.append(def(key.getFirst().getKeyword()).getAbbreviation(false)); + if(key.getFirst().getIndex() > 0) + sb.append(" - ").append(key.getFirst().getIndex()); sb.append(""); - sb.append(def(key.getSecond()).getAbbreviation(false)); + sb.append(def(key.getSecond().getKeyword()).getAbbreviation(false)); + if(key.getSecond().getIndex() > 0) + sb.append(" - ").append(key.getSecond().getIndex()); sb.append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } - sb.append("" + String.format("%3.2f", map.get(key)) + ""); + sb.append("").append(String.format("%3.2f", map.get(key))).append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 8448b5fb..2fad022e 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -1,10 +1,11 @@ package pulse.tasks.logs; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; import java.util.List; +import pulse.math.Parameter; +import pulse.math.ParameterIdentifier; +import pulse.properties.NumericProperties; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.ui.Messages; @@ -19,7 +20,7 @@ */ public class DataLogEntry extends LogEntry { - private List entry; + private List entry; /** * Creates a new {@code DataLogEntry} based on the current values of the @@ -52,13 +53,14 @@ public DataLogEntry(SearchTask task) { */ private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { var task = TaskManager.getManagerInstance().getTask(getIdentifier()); - - entry = task.alteredParameters(); - Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); - entry.add(0, task.getIterativeState().getIteration()); + entry = task.searchVector().getParameters(); + var pval = task.getIterativeState().getIteration(); + var pid = new Parameter(new ParameterIdentifier(pval.getType())); + pid.setValue( (int) pval.getValue() ); + entry.add(0, pid); } - public List getData() { + public List getData() { return entry; } @@ -83,13 +85,26 @@ public String toString() { */ sb.append(""); - for (NumericProperty p : entry) { + for (Parameter p : entry) { sb.append("<
"); diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index c8780bac..5b519531 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -9,7 +9,6 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; -import pulse.tasks.listeners.StatusChangeListener; import pulse.ui.Messages; import pulse.util.Group; @@ -23,8 +22,8 @@ public class Log extends Group { private List logEntries; private LocalTime start; private LocalTime end; - private Identifier id; - private List listeners; + private final Identifier id; + private final List listeners; private static boolean verbose = false; /** @@ -49,38 +48,32 @@ public Log(SearchTask task) { /** * Do these actions each time data has been collected for this task. */ - if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { + if (task.getStatus() != Status.INCOMPLETE && verbose) { logEntries.add(le); notifyListeners(le); } }); - task.addStatusChangeListener(new StatusChangeListener() { - - /** - * Do these actions every time the task status has changed. - */ - @Override - public void onStatusChange(StateEntry e) { - logEntries.add(e); - - if (e.getStatus() == Status.IN_PROGRESS) { - start = e.getTime(); - end = null; - } else { - end = e.getTime(); - } - - notifyListeners(e); - - if (e.getState() == Status.DONE) { - logFinished(); - } - + task.addStatusChangeListener((StateEntry e) -> { + logEntries.add(e); + + if (e.getStatus() == Status.IN_PROGRESS) { + start = e.getTime(); + end = null; + } else { + end = e.getTime(); } - - }); + + notifyListeners(e); + + if (e.getState() == Status.DONE) { + logFinished(); + } + } /** + * Do these actions every time the task status has changed. + */ + ); } @@ -92,15 +85,15 @@ private void notifyListeners(LogEntry logEntry) { listeners.stream().forEach(l -> l.onNewEntry(logEntry)); } - public List getListeners() { + public final List getListeners() { return listeners; } - public void addListener(LogEntryListener l) { + public final void addListener(LogEntryListener l) { listeners.add(l); } - public Identifier getIdentifier() { + public final Identifier getIdentifier() { return id; } @@ -128,10 +121,12 @@ public String toString() { sb.append(newLine); sb.append(newLine); - for (LogEntry le : logEntries) { + logEntries.stream().map(le -> { sb.append(le); + return le; + }).forEachOrdered(_item -> { sb.append(newLine); - } + }); return sb.toString(); diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 87e226d9..e2d7017f 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -1,6 +1,9 @@ package pulse.tasks.logs; import java.awt.Color; +import java.util.Objects; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.schemes.solvers.SolverException.SolverExceptionType; /** * An enum that represents the different states in which a {@code SearchTask} @@ -130,5 +133,20 @@ public String getMessage() { } return sb.toString(); } + + public static Status troubleshoot(SolverException e1) { + Objects.requireNonNull(e1, "Solver exception cannot be null when calling troubleshoot!"); + Status status = null; + if(e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { + status = Status.FAILED; + status.setDetails(Details.SOLVER_ERROR); + status.setDetailedMessage(e1.getMessage()); + } + else { + status = Status.TIMEOUT; + status.setDetails(Details.MAX_ITERATIONS_REACHED); + } + return status; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index bcfbac0e..e3a16843 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -13,7 +13,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyHolder; /** @@ -61,8 +61,9 @@ public void init() { * @param t the {@code SearchTask} * @param bufferElement the {@code bufferElement} which will be written over */ - public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); + public final void fill(GeneralTask t, int bufferElement) { + statistic[bufferElement] = (double) t.getResponse() + .getOptimiserStatistic().getStatistic().getValue(); data[bufferElement] = t.searchVector(); } @@ -80,7 +81,7 @@ public boolean isErrorTooHigh(double errorTolerance) { boolean result = false; for (int i = 0; i < e.length && (!result); i++) { - var index = data[0].getIndex(i); + var index = data[0].getParameters().get(i).getIdentifier().getKeyword(); final double av = average(index); e[i] = variance(index) / (av * av); @@ -105,7 +106,7 @@ public double average(NumericPropertyKeyword index) { double av = 0; for (ParameterVector v : data) { - av += v.getParameterValue(index); + av += v.getParameterValue(index, 0); } return av / data.length; @@ -142,7 +143,7 @@ public double variance(NumericPropertyKeyword index) { double av = average(index); for (ParameterVector v : data) { - final double s = v.getParameterValue(index) - av; + final double s = v.getParameterValue(index, 0) - av; sd += s * s; } @@ -188,7 +189,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { */ @Override public List listedTypes() { - return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); + return new ArrayList<>(Arrays.asList(def(BUFFER_SIZE))); } } diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 31d3ef15..fc0c0f79 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -7,8 +7,10 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; import pulse.properties.NumericPropertyKeyword; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.EmptyCorrelationTest; @@ -18,9 +20,9 @@ public class CorrelationBuffer { - private List params; - private static Set> excludePairList; - private static Set excludeSingleList; + private final List params; + private static final Set> excludePairList; + private static final Set excludeSingleList; private final static double DEFAULT_THRESHOLD = 1E-3; @@ -58,17 +60,18 @@ private void truncate(double threshold) { for(i = 0; i < size - 1; i = i + 2) { - ParameterVector diff = new ParameterVector( params.get(i), params.get(i + 1).subtract(params.get(i) )); - if(diff.lengthSq()/params.get(i).lengthSq() < thresholdSq) + Vector vParams = params.get(i).toVector(); + Vector vPlusOneParams = params.get(i + 1).toVector(); + Vector vDiff = vPlusOneParams.subtract(vParams); + if(vDiff.lengthSq()/vParams.lengthSq() < thresholdSq) break; } for(int j = size - 1; j > i; j--) - params.remove(j); - + params.remove(j); } - public Map, Double> evaluate(CorrelationTest t) { + public Map, Double> evaluate(CorrelationTest t) { if (params.isEmpty()) { throw new IllegalStateException("Zero number of entries in parameter list"); } @@ -79,24 +82,40 @@ public Map, Double> evaluate(CorrelationTe truncate(DEFAULT_THRESHOLD); - var indices = params.get(0).getIndices(); - var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) + List indices = params.get(0).getParameters().stream() + .map(ps -> ps.getIdentifier()).collect(Collectors.toList()); + Map map = indices.stream() + .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble( + v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); - var correlationMap = new HashMap, Double>(); - ImmutablePair pair = null; + var correlationMap = new HashMap, Double>(); + ImmutablePair pair; for (int i = 0; i < indicesSize; i++) { - if (!excludeSingleList.contains(indices.get(i))) { + var iKey = indices.get(i).getKeyword(); + + if (!excludeSingleList.contains(iKey)) { + for (int j = i + 1; j < indicesSize; j++) { - pair = new ImmutablePair<>(indices.get(i), indices.get(j)); - if (!excludeSingleList.contains(indices.get(j)) && !excludePairList.contains(pair)) { - correlationMap.put(pair, t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + + var jKey = indices.get(j).getKeyword(); + + pair = new ImmutablePair<>(iKey, jKey); + + if (!excludeSingleList.contains(jKey) + && !excludePairList.contains(pair)) { + + correlationMap.put( + new ImmutablePair<>(indices.get(i), indices.get(j)), + t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } + } + } } @@ -112,6 +131,8 @@ public boolean test(CorrelationTest t) { return false; } + var values = map.values(); + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 8350aedd..626e292b 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -28,7 +29,7 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); } - setParent(task.getCurrentCalculation()); + setParent((Calculation)task.getResponse()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index acadc0bb..de978325 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -87,7 +87,7 @@ public static void main(String[] args) { }); } else { - System.out.println("An instance of PULsE is already running!"); + System.out.println(Messages.getString("msg.running")); } } diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 9d966b43..95c4ab1d 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -4,6 +4,8 @@ import static pulse.ui.frames.MainGraphFrame.getChart; import java.awt.Dimension; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.swing.JTable; import javax.swing.SwingUtilities; @@ -23,9 +25,11 @@ public class CalculationTable extends JTable { private final static int HEADER_HEIGHT = 30; private TaskTableRenderer taskTableRenderer; + private ExecutorService plotExecutor; public CalculationTable() { super(); + plotExecutor = Executors.newSingleThreadExecutor(); setDefaultEditor(Object.class, null); taskTableRenderer = new TaskTableRenderer(); this.setRowSelectionAllowed(true); @@ -67,7 +71,7 @@ public void update(SearchTask t) { } public void identifySelection(SearchTask t) { - int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); + int modelIndex = t.getStoredCalculations().indexOf(t.getResponse()); if (modelIndex > -1) { this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); } @@ -81,10 +85,12 @@ public void initListeners() { var task = TaskManager.getManagerInstance().getSelectedTask(); var id = convertRowIndexToModel(this.getSelectedRow()); if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { - - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); - + + plotExecutor.submit(() -> { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + }); + } }); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 753b8784..68dda2d3 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -41,6 +41,7 @@ import pulse.input.IndexRange; import pulse.input.Range; import pulse.input.listeners.DataEvent; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperties; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; @@ -90,10 +91,9 @@ public void mouseDragged(MouseEvent e) { } SwingUtilities.invokeLater(() -> { - + //process dragged events - Range range = instance.getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ((ExperimentalData) (instance.getSelectedTask().getInput())).getRange(); double value = xCoord(e) / factor; //convert to seconds back from ms -- if needed if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { @@ -107,7 +107,7 @@ public void mouseDragged(MouseEvent e) { } else { super.mouseDragged(e); } - + }); } @@ -118,12 +118,13 @@ public void mouseDragged(MouseEvent e) { //for each new task var eventTask = instance.getTask(e.getId()); if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + var data = (ExperimentalData) eventTask.getInput(); //add passive data listener - eventTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { //that will be triggered only when this task is selected if (instance.getSelectedTask() == eventTask) { //update marker values - var segment = eventTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerMarker.setValue(segment.getMinimum() * factor); //convert to ms -- if needed upperMarker.setValue(segment.getMaximum() * factor); //convert to ms -- if needed } @@ -221,7 +222,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.setDataset(i, null); } - var rawData = task.getExperimentalCurve(); + var rawData = (ExperimentalData) task.getInput(); var segment = rawData.getRange().getSegment(); adjustAxisLabel(segment.getMaximum()); @@ -239,7 +240,7 @@ public void plot(SearchTask task, boolean extendedCurve) { lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - final double margin = (lowerMarker.getValue() + upperMarker.getValue())/20.0; + final double margin = (lowerMarker.getValue() + upperMarker.getValue()) / 20.0; //add listener to handle range adjustment var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); @@ -251,7 +252,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.addDomainMarker(upperMarker); plot.addDomainMarker(lowerMarker); - var calc = task.getCurrentCalculation(); + var calc = (Calculation) task.getResponse(); var problem = calc.getProblem(); if (problem != null) { @@ -259,6 +260,15 @@ public void plot(SearchTask task, boolean extendedCurve) { var solution = problem.getHeatingCurve(); var scheme = calc.getScheme(); + if (solution != null && !solution.isFull()) { + try { + calc.process(); + } catch (SolverException ex) { + System.out.println("Could not plot solution! See details in debug."); + ex.printStackTrace(); + } + } + if (solution != null && scheme != null) { var solutionDataset = new XYSeriesCollection(); @@ -298,7 +308,7 @@ public void plot(SearchTask task, boolean extendedCurve) { public void plotSingle(HeatingCurve curve) { requireNonNull(curve); - var plot = chart.getXYPlot(); + plot = chart.getXYPlot(); var classicDataset = new XYSeriesCollection(); @@ -339,6 +349,7 @@ public XYSeries residuals(Calculation calc) { var problem = calc.getProblem(); var baseline = problem.getBaseline(); + var time = calc.getOptimiserStatistic().getTimeSequence(); var residuals = calc.getOptimiserStatistic().getResiduals(); var size = residuals.size(); @@ -348,7 +359,7 @@ public XYSeries residuals(Calculation calc) { var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); for (var i = 0; i < size; i++) { - series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); + series.add(factor * time.get(i), (Number) (residuals.get(i) + offset)); } return series; diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index d6b6539d..34f9f970 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -15,12 +15,14 @@ import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileNameExtensionFilter; +import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -99,7 +101,7 @@ public static void loadMetadataDialog() { // attempt to fill metadata and problem for (SearchTask task : instance.getTaskList()) { - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); try { handler.populate(file, data.getMetadata()); @@ -109,7 +111,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = task.getCurrentCalculation().getProblem(); + var p = ( (Calculation) task.getResponse() ).getProblem(); if (p != null) { p.retrieveData(data); } @@ -146,7 +148,7 @@ public static void loadPulseDialog() { if (task != null) { pool.submit(() -> { - var metadata = task.getExperimentalCurve().getMetadata(); + var metadata = ((ExperimentalData) task.getInput()).getMetadata(); metadata.setPulseData(pulseData); metadata.getPulseDescriptor() .setSelectedDescriptor( diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index 0fb17161..5af4215a 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -90,7 +90,7 @@ public void printAll() { log.getLogEntries().stream().forEach(entry -> post(entry)); - if (task.getCurrentCalculation().getStatus() == DONE) { + if (task.getStatus() == DONE) { printTimeTaken(log); } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index 54513d97..e4c2e0de 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -14,6 +14,7 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.ProblemComplexity; +import pulse.tasks.Calculation; import pulse.ui.components.controllers.ProblemCellRenderer; import pulse.ui.components.listeners.ProblemSelectionEvent; import pulse.ui.components.listeners.ProblemSelectionListener; @@ -21,7 +22,7 @@ @SuppressWarnings("serial") public class ProblemTree extends JTree { - private List selectionListeners; + private final List selectionListeners; public ProblemTree(List allProblems) { super(); @@ -66,7 +67,7 @@ private void addListeners() { }); instance.addSelectionListener(e -> { - var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); + var current = ( (Calculation) instance.getSelectedTask().getResponse() ).getProblem(); // select appropriate problem type from list setSelectedProblem(current); @@ -108,7 +109,7 @@ public void setSelectedProblem(Problem p) { }); } - public void addProblemSelectionListener(ProblemSelectionListener l) { + public final void addProblemSelectionListener(ProblemSelectionListener l) { selectionListeners.add(l); } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 74aeeab0..216770e5 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -44,6 +44,7 @@ import pulse.search.statistics.NormalityTest; import pulse.search.statistics.OptimiserStatistic; import pulse.search.statistics.SumOfSquares; +import pulse.tasks.Calculation; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; @@ -244,7 +245,8 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t -> + ( (Calculation) t.getResponse() ).initOptimiser()); } }); diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java index 47cacf11..a146e556 100644 --- a/src/main/java/pulse/ui/components/RangeTextFields.java +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -1,30 +1,15 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.ui.components; import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFormattedTextField; import javax.swing.text.NumberFormatter; +import pulse.input.ExperimentalData; import pulse.input.Range; import pulse.input.listeners.DataEvent; import pulse.tasks.SearchTask; @@ -36,7 +21,6 @@ /** * Two JFormattedTextFields used to display the range of the currently * selected task. - * @author Artem Lunev */ public final class RangeTextFields { @@ -69,7 +53,7 @@ public RangeTextFields() { //when a new task is selected instance.addSelectionListener((TaskSelectionEvent e) -> { var task = instance.getSelectedTask(); - var segment = task.getExperimentalCurve().getRange().getSegment(); + var segment = ( (ExperimentalData) task.getInput() ).getRange().getSegment(); //update the textfield values lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); @@ -109,8 +93,8 @@ private NumberFormatter initFormatter() { */ private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { - Range range = TaskManager.getManagerInstance().getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ( (ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput() ).getRange(); double candidateValue = 0.0; try { @@ -195,10 +179,11 @@ public void focusLost(FocusEvent arg0) { } private void updateTextfieldsFromTask(SearchTask newTask) { + var data = (ExperimentalData) newTask.getInput(); //add data listeners in case when the range of the selected task is changed - newTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { if (TaskManager.getManagerInstance().getSelectedTask() == newTask) { - var segment = newTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); } diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 614d786b..7a78ee24 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -30,10 +30,10 @@ public void plot(ResidualStatistic stat) { var pulseDataset = new HistogramDataset(); pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); - var residuals = stat.transformResiduals(); + var residuals = stat.residualsArray(); if (residuals.length > 0) { - pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); + pulseDataset.addSeries("H1", residuals, binCount); } getPlot().setDataset(0, pulseDataset); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 520aa0e0..d718926f 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -17,6 +17,7 @@ import javax.swing.table.TableRowSorter; import pulse.properties.NumericProperty; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -77,35 +78,41 @@ public ResultTable(ResultFormat fmt) { */ TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { var t = instance.getTask(e.getId()); - switch (e.getState()) { - case TASK_FINISHED: - var r = t.getCurrentCalculation().getResult(); - var resultTableModel = (ResultTableModel) getModel(); - Objects.requireNonNull(r, "Task finished with a null result!"); - invokeLater(() -> resultTableModel.addRow(r)); - break; - case TASK_REMOVED: - case TASK_RESET: - ((ResultTableModel) getModel()).removeAll(e.getId()); - getSelectionModel().clearSelection(); - break; - case BEST_MODEL_SELECTED: - for (var c : t.getStoredCalculations()) { - if (c.getResult() != null && c != t.getCurrentCalculation()) { - ((ResultTableModel) getModel()).remove(c.getResult()); + + if(t != null) { + + var cc = (Calculation) t.getResponse(); + + switch (e.getState()) { + case TASK_FINISHED: + var r = cc.getResult(); + var resultTableModel = (ResultTableModel) getModel(); + Objects.requireNonNull(r, "Task finished with a null result!"); + invokeLater(() -> resultTableModel.addRow(r)); + break; + case TASK_REMOVED: + case TASK_RESET: + ((ResultTableModel) getModel()).removeAll(e.getId()); + getSelectionModel().clearSelection(); + break; + case BEST_MODEL_SELECTED: + for (var c : t.getStoredCalculations()) { + if (c.getResult() != null && c != cc) { + ((ResultTableModel) getModel()).remove(c.getResult()); + } } - } - this.select(t.getCurrentCalculation().getResult()); - break; - case TASK_MODEL_SWITCH: - var c = t.getCurrentCalculation(); - this.getSelectionModel().clearSelection(); - if (c != null && c.getResult() != null) { - select(c.getResult()); - } - break; - default: - break; + this.select(cc.getResult()); + break; + case TASK_MODEL_SWITCH: + this.getSelectionModel().clearSelection(); + if (cc != null && cc.getResult() != null) { + select(cc.getResult()); + } + break; + default: + break; + } + } }); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index d89e43d8..5dc2c26e 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -27,9 +27,11 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import pulse.input.ExperimentalData; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Result; @@ -69,8 +71,9 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { + var input = (ExperimentalData) t.getInput(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); + input.getMetadata().toString(), "Metadata", PLAIN_MESSAGE); } }); @@ -78,14 +81,14 @@ public TaskPopupMenu() { instance.addSelectionListener(event -> { instance.getSelectedTask().checkProblems(false); - var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); + var details = instance.getSelectedTask().getStatus().getDetails(); itemShowStatus.setEnabled((details != null) & (details != NONE)); }); itemShowStatus.addActionListener((ActionEvent e) -> { var t = instance.getSelectedTask(); if (t != null) { - var d = t.getCurrentCalculation().getStatus().getDetails(); + var d = t.getStatus().getDetails(); showMessageDialog(getWindowAncestor((Component) e.getSource()), "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); } @@ -100,7 +103,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ } else { t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == DONE) { var dialogButton = YES_NO_OPTION; @@ -115,7 +118,7 @@ public TaskPopupMenu() { } } else if (status != READY) { showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ + t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ ERROR_MESSAGE); } else { @@ -136,7 +139,7 @@ public TaskPopupMenu() { if (t == null) { return; } - var current = t.getCurrentCalculation(); + var current = (Calculation) t.getResponse(); if (current != null) { var r = new Result(t, getInstance()); current.setResult(r); @@ -175,8 +178,8 @@ public void plot(boolean extended) { getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - var calc = t.getCurrentCalculation(); - var statusDetails = calc.getStatus().getDetails(); + var calc = (Calculation) t.getResponse(); + var statusDetails = t.getStatus().getDetails(); if (statusDetails == MISSING_HEATING_CURVE) { diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 37b84d58..30ea042c 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -50,12 +50,12 @@ public ExecutionButton() { } var problematicTask = instance.getTaskList().stream().filter(t -> { t.checkProblems(true); - return t.getCurrentCalculation().getStatus() == INCOMPLETE; + return t.getStatus() == INCOMPLETE; }).findFirst(); if (problematicTask.isPresent()) { var t = problematicTask.get(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", + t + " is " + t.getStatus().getMessage(), "Problems found", ERROR_MESSAGE); } else { instance.executeAll(); diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index ac0c3bd0..526823f7 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -32,8 +32,10 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean try { descriptor.attemptUpdate(e.getItem()); } catch(NullPointerException npe) { - System.out.println("Error updating " + descriptor.getDescriptor(false) - + ". Cannot be set to " + e.getItem()); + String text = "Error updating " + descriptor.getDescriptor(false) + + ". Cannot be set to " + e.getItem(); + System.out.println(text); + npe.printStackTrace(); } } }); diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index ffd35db1..8592285c 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -223,8 +223,7 @@ public void addRow(AbstractResult result) { if (result instanceof Result) { //result must have a valid ancestor! - var ancestor = Objects.requireNonNull( - result.specificAncestor(SearchTask.class), + var ancestor = Objects.requireNonNull(result.specificAncestor(SearchTask.class), "Result " + result.toString() + " does not belong a SearchTask!"); //the ancestor then has the SearchTask type @@ -232,8 +231,7 @@ public void addRow(AbstractResult result) { //any old result asssociated withis this task var oldResult = results.stream().filter(r - -> r.specificAncestor( - SearchTask.class) == parentTask).findAny(); + -> r.specificAncestor(SearchTask.class) == parentTask).findAny(); //check the following only if the old result is present if (oldResult.isPresent()) { @@ -249,7 +247,8 @@ public void addRow(AbstractResult result) { Status status = Status.DONE; //better result than already present -- update table - if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + var c = (Calculation) parentTask.getResponse(); + if (c.isBetterThan(oldCalculation.get())) { remove(oldResultExisting); status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); parentTask.setStatus(status); diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 77d51b81..86eef588 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -11,6 +11,8 @@ import static pulse.ui.Messages.getString; import javax.swing.table.DefaultTableModel; +import pulse.input.ExperimentalData; +import pulse.tasks.Calculation; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -51,11 +53,12 @@ public TaskTableModel() { } public void addTask(SearchTask t) { - var temperature = t.getExperimentalCurve() + var temperature = ( (ExperimentalData) t.getInput() ) .getMetadata().numericProperty(TEST_TEMPERATURE); + var calc = (Calculation) t.getResponse(); var data = new Object[]{t.getIdentifier(), temperature, - t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus()}; + calc.getOptimiserStatistic().getStatistic(), + t.getNormalityTest().getStatistic(), t.getStatus()}; invokeLater(() -> super.addRow(data)); @@ -68,7 +71,7 @@ public void addTask(SearchTask t) { }); t.addTaskListener((LogEntry e) -> { - setValueAt(t.getCurrentCalculation().getOptimiserStatistic() + setValueAt(calc.getOptimiserStatistic() .getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); }); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 011d2f71..b4d5e548 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -26,6 +26,7 @@ import pulse.input.ExperimentalData; import pulse.input.Range; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Messages; import pulse.ui.components.RangeTextFields; @@ -64,12 +65,13 @@ public final void initComponents() { pdfBtn.addActionListener(e -> { var task = TaskManager.getManagerInstance().getSelectedTask(); + var calc = (Calculation) task.getResponse(); - if (task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { + if (task != null && calc.getModelSelectionCriterion() != null) { chFrame.setLocationRelativeTo(null); chFrame.setVisible(true); - chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); + chFrame.plot(calc.getOptimiserStatistic()); } @@ -130,7 +132,7 @@ private void validateRange(double a, double b) { return; } - var expCurve = task.getExperimentalCurve(); + var expCurve = (ExperimentalData) task.getInput(); if (expCurve == null) { return; @@ -169,7 +171,7 @@ private void validateRange(double a, double b) { // set range for all available experimental datasets TaskManager.getManagerInstance().getTaskList() .stream().forEach((aTask) - -> setRange(aTask.getExperimentalCurve(), a, b) + -> setRange( (ExperimentalData) aTask.getInput(), a, b) ); } diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 0aeedc80..92e42cab 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -20,6 +20,7 @@ import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.LoaderButton; import pulse.ui.frames.MainGraphFrame; @@ -61,15 +62,16 @@ public static void plot(ActionEvent e) { var t = instance.getSelectedTask(); - var calc = t.getCurrentCalculation(); + var calc = (Calculation) t.getResponse(); t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == INCOMPLETE && !status.checkProblemStatementSet()) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), + showMessageDialog(getWindowAncestor((Component) e.getSource()), + calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java index f91f8a7b..9debcdc8 100644 --- a/src/main/java/pulse/ui/frames/HistogramFrame.java +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -9,6 +9,7 @@ import javax.swing.JSlider; import pulse.search.statistics.ResidualStatistic; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.AuxPlotter; import pulse.ui.components.ResidualsChart; @@ -29,7 +30,9 @@ public HistogramFrame(AuxPlotter chart, int width, int height getContentPane().add(panel, SOUTH); slider.addChangeListener(e -> { ((ResidualsChart) chart).setBinCount(slider.getValue()); - plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic()); + var c = (Calculation) TaskManager.getManagerInstance().getSelectedTask() + .getResponse(); + plot(c.getOptimiserStatistic()); info.setText("Number of bins: " + slider.getValue()); }); } diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index fff33ecc..e4400d43 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -4,8 +4,11 @@ import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JInternalFrame; +import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.logs.Status; @@ -46,7 +49,7 @@ private void initComponents() { public void plot() { var task = TaskManager.getManagerInstance().getSelectedTask(); //do not plot tasks that are not finished - if (task != null && task.getCurrentCalculation().getStatus() != Status.IN_PROGRESS) { + if (task != null && task.getStatus() != Status.IN_PROGRESS) { Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); } } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 41d5bbf5..6acd7dab 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -14,8 +14,10 @@ import java.awt.BorderLayout; import java.awt.GridLayout; +import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; @@ -32,9 +34,11 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; +import pulse.input.ExperimentalData; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.statements.Problem; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; @@ -44,6 +48,7 @@ import pulse.ui.components.panels.ProblemToolbar; import pulse.ui.components.panels.SettingsToolBar; import pulse.ui.frames.TaskControlFrame.Mode; +import pulse.ui.frames.dialogs.ProgressDialog; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { @@ -108,22 +113,25 @@ public ProblemStatementFrame() { /* * Scheme list and scroller */ - schemeSelectionList = new JList(); + schemeSelectionList = new JList<>(); schemeSelectionList.setSelectionMode(SINGLE_SELECTION); - schemeSelectionList.setModel(new DefaultListModel()); + schemeSelectionList.setModel(new DefaultListModel<>()); schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { - if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) { - return; - } + if (TaskControlFrame.getInstance().getMode() == Mode.PROBLEM) { - var selectedValue = schemeSelectionList.getSelectedValue(); + var selectedValue = schemeSelectionList.getSelectedValue(); + + if (selectedValue != null) { + + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + } else { + changeSchemes(selectedValue); + } + + } - if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - } - else { - changeSchemes(selectedValue); } }); @@ -195,7 +203,7 @@ public void setSelectionPath(TreePath path) { //for all tasks instance.getTaskList().stream(). //select the problem statement of the current calculation - map(t -> t.getCurrentCalculation().getProblem()) + map(t -> ((Calculation) t.getResponse()).getProblem()) //that is non-null .filter(problem -> problem != null) //for each problem, update its properties in a separete thread @@ -215,13 +223,9 @@ public void update() { } private void update(SearchTask selectedTask) { - - if(selectedTask == null) - return; - - var calc = selectedTask.getCurrentCalculation(); - var selectedProblem = selectedTask == null ? null : calc.getProblem(); - var selectedScheme = selectedTask == null ? null : calc.getScheme(); + var calc = (Calculation) selectedTask.getResponse(); + var selectedProblem = calc.getProblem(); + var selectedScheme = calc.getScheme(); // problem if (selectedProblem == null) { @@ -237,95 +241,139 @@ private void update(SearchTask selectedTask) { setSelectedElement(schemeSelectionList, selectedScheme); schemeTable.setPropertyHolder(selectedScheme); } - } private void changeSchemes(DifferenceScheme newScheme) { var instance = TaskManager.getManagerInstance(); var selectedTask = instance.getSelectedTask(); + + var schemeLoaderTracker = new ProgressDialog(); + schemeLoaderTracker.setTitle("Initialising solution schemes..."); + schemeLoaderTracker.setLocationRelativeTo(null); + schemeLoaderTracker.setAlwaysOnTop(true); + + List> callableList; + if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public DifferenceScheme call() throws Exception { changeScheme(t, newScheme); - return t.getCurrentCalculation().getScheme(); + schemeLoaderTracker.incrementProgress(); + return ((Calculation) t.getResponse()).getScheme(); } }).collect(Collectors.toList()); + } else { + callableList = Arrays.asList(() -> { + changeScheme(selectedTask, newScheme); + return selectedTask.getResponse().getScheme(); + }); + } + + schemeLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { schemeListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } + }).thenRun(() -> { - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); - if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); - } + var c = (Calculation) selectedTask.getResponse(); + schemeTable.setPropertyHolder(c.getScheme()); + if (c.getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), + "High complexity", INFORMATION_MESSAGE); + } + Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); + }); } private void changeProblems(Problem newlySelectedProblem, Object source) { var instance = TaskManager.getManagerInstance(); - var selectedTask = instance.getSelectedTask(); + var task = instance.getSelectedTask(); + var selectedCalc = ((Calculation) task.getResponse()); + + var problemLoaderTracker = new ProgressDialog(); + problemLoaderTracker.setTitle("Changing problem statements..."); + problemLoaderTracker.setLocationRelativeTo(null); + problemLoaderTracker.setAlwaysOnTop(true); + + List> callableList; if (source != instance) { + //apply to all tasks if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public Problem call() throws Exception { changeProblem(t, newlySelectedProblem); - return t.getCurrentCalculation().getProblem(); + var result = ((Calculation) t.getResponse()).getProblem(); + problemLoaderTracker.incrementProgress(); + return result; } }).collect(Collectors.toList()); + + } //apply only to this task + else { + callableList = Arrays.asList(() -> { + changeProblem(task, newlySelectedProblem); + return ((Calculation) task.getResponse()).getProblem(); + }); + } + + problemLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { problemListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } - - } else { - changeProblem(selectedTask, newlySelectedProblem); } + ).thenRun(() -> { + problemTable.setPropertyHolder(selectedCalc.getProblem()); + // after problem is selected for this task, show available difference schemes + var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + schemes.forEach(s -> defaultModel.addElement(s)); + selectDefaultScheme(schemeSelectionList, selectedCalc.getProblem()); + schemeSelectionList.setToolTipText(null); + }); } - problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); - // after problem is selected for this task, show available difference schemes - var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); - defaultModel.clear(); - var schemes = newlySelectedProblem.availableSolutions(); - schemes.forEach(s -> defaultModel.addElement(s)); - selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); - schemeSelectionList.setToolTipText(null); - - Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - } private void changeProblem(SearchTask task, Problem newProblem) { - var data = task.getExperimentalCurve(); - var calc = task.getCurrentCalculation(); + var data = (ExperimentalData) task.getInput(); + var calc = (Calculation) task.getResponse(); var oldProblem = calc.getProblem(); // stores previous information var np = newProblem.copy(); if (oldProblem != null) { np.initProperties(oldProblem.getProperties().copy()); np.getPulse().initFrom(oldProblem.getPulse()); + np.setBaseline(oldProblem.getBaseline()); + np.updateProperties(np, data.getMetadata()); } calc.setProblem(np, data); // copies information from old problem to new problem type + if (oldProblem == null) { + np.retrieveData(data); + } + task.checkProblems(true); toolbar.highlightButtons(!np.isReady()); - } private static void selectDefaultScheme(JList list, Problem p) { @@ -348,8 +396,8 @@ private static void selectDefaultScheme(JList list, Problem p) private void changeScheme(SearchTask task, DifferenceScheme newScheme) { // TODO - var calc = task.getCurrentCalculation(); - var data = task.getExperimentalCurve(); + var calc = (Calculation) task.getResponse(); + var data = (ExperimentalData) task.getInput(); if (calc.getScheme() == null) { calc.setScheme(newScheme.copy(), data); diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 2d7eddab..567c4494 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -32,6 +32,7 @@ import pulse.search.direction.LMOptimiser; import pulse.search.direction.PathOptimiser; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.controllers.SearchListRenderer; @@ -132,15 +133,14 @@ public void update() { //model for the flags list already created if (rightTblModel instanceof SelectedKeysModel) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var searchKeys = activeTask.activeParameters(); ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); ((SelectedKeysModel) rightTblModel).update(searchKeys); } //Create a new model for the flags list else { - if (activeTask != null - && activeTask.getCurrentCalculation() != null - && activeTask.getCurrentCalculation().getProblem() != null) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var c = (Calculation)activeTask.getResponse(); + if (c != null && c.getProblem() != null) { + var searchKeys = activeTask.activeParameters(); rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); /* diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 3bcccead..f04f0504 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -113,8 +113,12 @@ private void initListeners() { @Override public void onProblemStatementShowRequest() { - problemStatementFrame.update(); - setProblemStatementFrameVisible(true); + if (TaskManager.getManagerInstance().getSelectedTask() != null) { + problemStatementFrame.update(); + setProblemStatementFrameVisible(true); + } else { + System.out.println("Please select a task"); + } } @Override diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 07936b10..2b1f888e 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -25,7 +25,9 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) + + if (m.getParameterCount() > 0 + || !Group.class.isAssignableFrom(m.getReturnType()) || m.getReturnType().isAssignableFrom(getClass())) { continue; } @@ -39,7 +41,7 @@ public List subgroups() { e.printStackTrace(); } - /* Ignore null, factor/instance methods returning same accessibles */ + /* Ignore null, factory/instance methods returning same accessibles */ if (a == null || a.getDescriptor().equals(getDescriptor())) { continue; } diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index 70bc5e28..ab8e7444 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -19,21 +19,21 @@ public abstract class UpwardsNavigable implements Descriptive { private UpwardsNavigable parent; - private List listeners = new ArrayList(); + private final List listeners = new ArrayList<>(); - public void removeHierarchyListeners() { + public final void removeHierarchyListeners() { this.listeners.clear(); } - public void removeHierarchyListener(HierarchyListener l) { + public final void removeHierarchyListener(HierarchyListener l) { this.listeners.remove(l); } - public void addHierarchyListener(HierarchyListener l) { + public final void addHierarchyListener(HierarchyListener l) { this.listeners.add(l); } - public List getHierarchyListeners() { + public final List getHierarchyListeners() { return listeners; } @@ -76,7 +76,6 @@ public UpwardsNavigable specificAncestor(Class aClas if (aClass.equals(this.getClass())) { return this; } - var parent = this.getParent(); UpwardsNavigable result = null; if (parent != null) { result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); From 8da53f24436c9dee8c4cabb64e371562459ebe6a Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:35:04 +0300 Subject: [PATCH 099/116] Pull update --- pom.xml | 2 +- .../pulse/math/filters/PolylineOptimiser.java | 2 +- .../transforms/StandardTransformations.java | 3 - src/main/resources/NumericProperty.xml | 46 +- src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 4 +- src/main/resources/test/fft.txt | 1024 +++++++++++++++++ 7 files changed, 1069 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/test/fft.txt diff --git a/pom.xml b/pom.xml index ecbe713f..e5118865 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.95 + 1.97 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java index 06246988..dde0142a 100644 --- a/src/main/java/pulse/math/filters/PolylineOptimiser.java +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -87,4 +87,4 @@ public double evaluate(double t) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index c5b95034..a8206fdb 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,9 +19,6 @@ public class StandardTransformations { @Override public double transform(double a) { - if(a < 0) { - System.err.println(a); - } return log(a); } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 3d4b61ad..edf471e6 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -21,21 +21,31 @@ dimensionfactor="1" discreet="false" keyword="MODEL_WEIGHT" maximum="1" minimum="0" primitive-type="double" value="0"> + + + + + maximum="5" minimum="0" primitive-type="double" value="0.1" default-search-variable="false"> + + - + + Diathermic Sample with Grey Walls (1D) LinearizedProblem2D.Descriptor=Classical 2D Problem Statement
  • Based on 1D formulation, except:
  • Allows heat losses from side surface
  • Allows radial heat flow
UniformlyCoatedSample.Descriptor=Core-Shell 2D Problem Statement
  • Based on the classical 2D problem, except:
  • Explicitly accounts for a coating that covers front, rear, and side surfaces
  • Allows for axial, radial, and circumferential heat fluxes
NonlinearProblem.Descriptor=Nonlinear Heat Sink (1D) Problem Statement
  • Precise calculation of heat losses (front and rear only)
  • Cp and ρ data required
+TwoTemperatureModel.Descriptor=Two-Temperature Penetration Model (1D) Problem Statement
  • Different temperatures for solid and gas phase
  • Energy exchange between phases
  • Extended light penetration
  • Cp and ρ data required
Problem.6=Laser Pulse Problem.7=Heating Curve DATReader.0=dat @@ -289,4 +290,5 @@ MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
TextWrap.0=

TextWrap.1=

-TextWrap.2=

\ No newline at end of file +TextWrap.2=

+msg.running=An instance of PULsE appears to be running. Please switch back to the running version or delete the pulse.lock file found in the PULsE directory. \ No newline at end of file diff --git a/src/main/resources/test/fft.txt b/src/main/resources/test/fft.txt new file mode 100644 index 00000000..194936e8 --- /dev/null +++ b/src/main/resources/test/fft.txt @@ -0,0 +1,1024 @@ +1.8623883939948125 +1.440191140077873 +0.0856911188761609 +0.3177405081698117 +0.10853351481231108 +0.11844768772657704 +0.14359180808670602 +0.09056412538979447 +0.10023722372433143 +0.05012708645229069 +0.06281734105869234 +0.049631166773682844 +0.05334748215923658 +0.07407767757133589 +0.03971008132098835 +0.04416964802114836 +0.040107973059110145 +0.033383489903923126 +0.053510587820655715 +0.029764726896007627 +0.028897396016806746 +0.028375440254179138 +0.02608494923958421 +0.02848092345249004 +0.03098610667355402 +0.019704730700161342 +0.02205971150558122 +0.021542133187751365 +0.021717439637497112 +0.020225860751051376 +0.019475979475189267 +0.014608596264644893 +0.01835870883217293 +0.017665011148277704 +0.01752138440835296 +0.015525900169506836 +0.015595410516248704 +0.015566570463898585 +0.015953238738805293 +0.01599587158897621 +0.013360695667264524 +0.015002089502846793 +0.014731537902388517 +0.013800515283259545 +0.014315157889845456 +0.013575152863046239 +0.012352341075970283 +0.012855780651818575 +0.012137860345243811 +0.012335928227005669 +0.011322310696797096 +0.011236261387870374 +0.011574641436270015 +0.011701043926932157 +0.011357150931082414 +0.009886326266142687 +0.009786382245722685 +0.010208387344388233 +0.009714230475277096 +0.009956827366738348 +0.009664725534625166 +0.009191624259773164 +0.009872561989745792 +0.009201888705471856 +0.008827948804843581 +0.008959893796151698 +0.00945443404186388 +0.008745592717618387 +0.009110905334280571 +0.008596123252820742 +0.008184929886419213 +0.008608854455027131 +0.008307320168098539 +0.008117648970713836 +0.008024758407971955 +0.007628751198035526 +0.007083062409124776 +0.007751030347403762 +0.007249154231345055 +0.007175520037985863 +0.00707064335177111 +0.00711871897750612 +0.007305437461793674 +0.007072070255306337 +0.006707680163861008 +0.006303859175803904 +0.007416579081448052 +0.0070619057661997445 +0.006854023369349145 +0.006455665945038045 +0.006835208781953551 +0.0065725374488924135 +0.006131013167403963 +0.006444591376811081 +0.0056394266037035545 +0.006066231052847979 +0.006303991327230198 +0.005949270797656248 +0.0063450056763406275 +0.006196167505123584 +0.005858153424762702 +0.0053913180909300855 +0.0058747255763572604 +0.006090643974924875 +0.005589115745925811 +0.005505074654993644 +0.005588245995291544 +0.005250392382850327 +0.005369156668180935 +0.005194106014717803 +0.005358717441455797 +0.005321043318596023 +0.005012422951166697 +0.005505553842319726 +0.0053432974110541155 +0.004971133806504812 +0.005037290685910062 +0.004765936894177503 +0.005141737892182391 +0.005410339758037131 +0.004885897265174704 +0.004922942306263837 +0.0045790685496497835 +0.00458229361635885 +0.004708660507341414 +0.0048042899663832475 +0.004625127808914531 +0.004743025594503575 +0.004598808630414814 +0.004626386499782302 +0.004459853592652658 +0.004377955368359365 +0.0046513163879235405 +0.0043256859045455115 +0.004512982427236372 +0.004588981897250833 +0.004295974465089781 +0.0043551312599052466 +0.004100096307988168 +0.004353555159813722 +0.00413193523732321 +0.004119654488401856 +0.0035986600134537334 +0.00430772639995229 +0.004058684144930945 +0.004194927813093337 +0.004135454329628857 +0.003707416777758124 +0.004024729209517794 +0.004139371147329603 +0.004167089013471642 +0.003761231388360764 +0.003952355678601933 +0.004066041106296513 +0.004136911700384993 +0.003932359264558167 +0.003542614608770456 +0.003746570018068218 +0.00395090631076722 +0.003537222144720005 +0.0035297026799832177 +0.003925348847332404 +0.0036757151959072244 +0.003602224343978626 +0.0036161028088550077 +0.0036307723158452575 +0.00368144084363016 +0.0036600294498210996 +0.00363782935999298 +0.003405756152194927 +0.003431252110629916 +0.0035649169954907745 +0.003236389910029101 +0.0034433473114116307 +0.0034932650584519062 +0.0035634560282200027 +0.0034296977806162633 +0.0034319461285292714 +0.003312308846263632 +0.003722618636988596 +0.0032536729682297472 +0.0031759378052392275 +0.00314708262406618 +0.003381194017340103 +0.0034226632338035484 +0.003201761963155763 +0.0034019628513818913 +0.0032004395904046603 +0.003102610835125887 +0.003323853973090416 +0.00301143619457104 +0.0033706851832347637 +0.002997554103076301 +0.0034025072727111427 +0.003206086938481629 +0.0031066962307518854 +0.002786437309626743 +0.0030835387904124883 +0.003031043385463557 +0.003011272769112621 +0.003026614277513354 +0.0031038064873766267 +0.002914867534237071 +0.0032106014711124264 +0.002960737750691456 +0.002848084333438608 +0.0029144693542782463 +0.002917497771325989 +0.0027129790526007544 +0.002612885902673647 +0.0029181831060856893 +0.0029055373553426543 +0.002714169194398066 +0.002863600775792233 +0.002684118883481437 +0.0028921148049106745 +0.0028302475704821105 +0.0028594112091045497 +0.0028091941754447122 +0.002761917483210222 +0.002831998224106552 +0.0029082049049603256 +0.0025433232627721726 +0.002744779635531415 +0.002610372762493025 +0.00269604297513204 +0.002522668560461302 +0.0027325824152729257 +0.0024340793422731313 +0.0026319425441122444 +0.002568112067959159 +0.0027683254158293864 +0.0024581501074080517 +0.00272139350102542 +0.0024097746095871524 +0.0026713014097986495 +0.002608528482915483 +0.002757952020574316 +0.002473940039239647 +0.0025698875343990245 +0.0024206272171112667 +0.002527643420140039 +0.002485348316653174 +0.002501106850185462 +0.0028036894150320207 +0.0023487069284162656 +0.002494732633732202 +0.002543346916671682 +0.002434448718146823 +0.002353903482083443 +0.0024849214295775495 +0.002280687444001982 +0.0021460731943987506 +0.0022744284159968734 +0.002355282996760084 +0.002374879375501902 +0.0025636565520782252 +0.0021429362225892585 +0.002512462546679564 +0.002457432015923903 +0.0022275839408261297 +0.002050220061718769 +0.0023897363016175868 +0.002280185276629757 +0.002551552138995319 +0.0020281057349859953 +0.0021776305132253397 +0.0021716442295787192 +0.0022024922812540998 +0.0022643433828360574 +0.0023171079460284592 +0.002209610418455076 +0.002091761906587569 +0.0024699303868929044 +0.0021924456147592924 +0.0025132238234213977 +0.0021413493664871534 +0.0023797515636802095 +0.002116464198887065 +0.0021010965208245326 +0.002200893207135162 +0.0022888366645033406 +0.0022506744392961795 +0.0020200323013424798 +0.0020269379217901146 +0.002235674958617841 +0.0021589634253941837 +0.002007694399579814 +0.002028468326489692 +0.001973178947286009 +0.0021722784832706923 +0.002152321588552352 +0.0020541661208640333 +0.0018443215090906614 +0.0023040970417671667 +0.0022070756163091905 +0.0021458535206313593 +0.0021234981725089773 +0.0020196842506682777 +0.002081325554159696 +0.0019622403312532185 +0.0018956765850871688 +0.002011758678159863 +0.002048584744472413 +0.001967968629034845 +0.0017890536700308419 +0.0018274200657948042 +0.0019495255858672576 +0.0019763604713690084 +0.002007665612620955 +0.002038019646262002 +0.0019323145911434045 +0.0018931617505309171 +0.0017176101935718177 +0.002035717753676598 +0.001997221376476268 +0.00208445761810857 +0.0018213990827714713 +0.0020335566706469687 +0.0019222800995692716 +0.0018730106550425844 +0.0018957021931545856 +0.0020131587529492274 +0.0019006911064656951 +0.0019006385154010802 +0.0018669836596750347 +0.0018387111891236968 +0.001860803645995648 +0.001875487905261806 +0.0019805330604071637 +0.0019333574999443324 +0.0017493923270269086 +0.0018635151864636905 +0.0018839497921766995 +0.001779170384859654 +0.0017369124667888725 +0.0018692706792106512 +0.0018555909898186722 +0.0019861219387481703 +0.0016768772425769986 +0.00173425585005027 +0.0017368104770272986 +0.0018444509218926088 +0.0018344780148905706 +0.0017421482008388048 +0.0019574501495635828 +0.0018485551997456673 +0.0016478397506516388 +0.0019006059767611814 +0.00178908230798387 +0.0017626449686498368 +0.001732149103600908 +0.0017420128347331559 +0.0016694600796223398 +0.0016892814923382899 +0.0017537019545246677 +0.0018336543315263922 +0.0018812436665203323 +0.0017781051942758364 +0.0016246718077691606 +0.0016673577866322385 +0.001759831952508731 +0.0017473637258567304 +0.00173104355552501 +0.001805140539073113 +0.0015973095237779042 +0.0015894420732170923 +0.001654768691098651 +0.0017328672602817553 +0.0018102375834557266 +0.0016478489084596247 +0.0016625501301660536 +0.001755668369649966 +0.0015363102784954816 +0.0017291672122388942 +0.0017614055969386695 +0.001711801977088871 +0.0016645828526783177 +0.0017291523364988042 +0.0017553659401122418 +0.0017300557169120786 +0.0016118911737583585 +0.0016013197537592275 +0.0017380041852213207 +0.0014933333031327699 +0.001559188637981803 +0.0017126653445467563 +0.0015807009808334307 +0.0016628973432414047 +0.0016047005870487945 +0.0017559387040599565 +0.0016243236914338785 +0.0015765644690180533 +0.0015828719203620945 +0.0015398177507824152 +0.0015032959585347633 +0.0016490341843532685 +0.001617396634735605 +0.0015957274117611951 +0.0015794738352842284 +0.0015388573781546031 +0.0016154513019219219 +0.0014816121634873749 +0.0015287905271643165 +0.0014617249768153456 +0.0015711554607759192 +0.0014682188208443925 +0.0015796458655902039 +0.001689735159818995 +0.001433704731383778 +0.0016166759882060668 +0.0014391892059119086 +0.0013712295490970739 +0.0016812167351267391 +0.0015319778370590715 +0.0014236368531158066 +0.0015087839252398833 +0.0016085941140473177 +0.0014820936165921505 +0.0014688802804647845 +0.0014354622391669247 +0.0015967583162015352 +0.0014216344753546506 +0.0015161040951119505 +0.0015096607386122969 +0.001384397608374938 +0.0015245746520588476 +0.0014081823605333326 +0.0016024041035034328 +0.001452871449554933 +0.0014308607906196105 +0.001522136599814874 +0.001481124047817481 +0.0015255580662159325 +0.001309843943187161 +0.0014586938481273024 +0.0014656459708439035 +0.0012726184983539426 +0.001367916494362877 +0.0013395100945080495 +0.0014389087123870105 +0.0015167920773429093 +0.0014722043865967177 +0.0014019882776771784 +0.0014049321693303425 +0.0013847814076527866 +0.0012478724504164254 +0.001459103728716397 +0.001406587374258575 +0.0014418827202656941 +0.0014096520586717536 +0.0014492409154258758 +0.001409394534879737 +0.0014705862502728947 +0.0012983605478659292 +0.001359781891861031 +0.0013736087171087959 +0.0015844689960084271 +0.001284557520511467 +0.0014926221038714146 +0.0013154469364888553 +0.0013159171219375757 +0.0013195726347157337 +0.0013224667986831112 +0.0013970767109930781 +0.0013792554501893374 +0.001302973911800565 +0.0014168082576272456 +0.0012576820832382774 +0.001542675666003404 +0.001307463747149029 +0.0012699109134222467 +0.0014539429326068782 +0.0013921268253928327 +0.0012829264103424237 +0.0014453813185194514 +0.0013361946194903225 +0.0012859127293610348 +0.001219864587298067 +0.0013729822159914303 +0.0013387240271219942 +0.001269166882994578 +0.0013816792929721383 +0.0013093666697172612 +0.0013326007495184746 +0.001334993151008571 +0.001235037425193347 +0.0011362030225542878 +0.0014377371705801458 +0.0013853954016113952 +0.0013471913518237851 +0.0012495283227950647 +0.001266562246997323 +0.0013227908159747438 +0.0012995488131381518 +0.0013904367286179792 +0.0012906469430955612 +0.0013315397480469341 +0.0012934821261761714 +0.0013250673575971874 +0.0013469150077112225 +0.0013202875781665269 +0.0012146174018993268 +0.00122881580627127 +0.0014129481339015366 +0.0011903810318464286 +0.0012449686959895507 +0.0012651176467834173 +0.0013046110131927327 +0.001237171432692225 +0.0011075767342333515 +0.0012758702965103852 +0.0011920086165592336 +0.0011866092015483018 +0.0012707921781073572 +0.001219579012657507 +0.0013616560614411427 +0.0012936725062240194 +0.00123301890710047 +0.0013025018941970037 +0.0011961146586798811 +0.0012808758077574244 +0.001190999826367067 +0.0012141207065111215 +0.0011843613569923832 +0.001335136462186405 +0.0011539681513207233 +0.0013632022792263935 +0.0013805744039978088 +0.0011881868755503888 +0.0011939077843169824 +0.0011270966583446721 +0.0012274172725854615 +0.0012672896373819246 +0.0012091250192438592 +0.001260421275046162 +0.0013151663232838697 +0.0012249004166542665 +0.00117899397310745 +0.0011734446482045359 +0.0012624596819513108 +0.0012764495812893665 +0.0010964856284655289 +0.001252761077726981 +0.0012529236282346054 +0.0012716297422796393 +0.0012861540922519037 +0.0011956481563782898 +0.0011121730631293352 +0.0012784895399416258 +0.0012202804971150033 +0.001045606751447345 +0.0013908900453571335 +0.001197171130379206 +0.0011705475688395964 +0.001269776840408273 +0.0011822952280807005 +0.0011066157789244133 +0.0012798660547691063 +0.0012809823962816405 +0.001109278093988003 +0.0011861159004747741 +0.0010835188502740056 +0.0011746191332898408 +0.001230977749834832 +0.0010795055496552108 +0.0012635419825219678 +0.0012756553936316552 +0.0011898806099122639 +0.0011608250848784943 +0.0012017305666559433 +0.001133310479022337 +0.0012358558242260653 +0.0011754317432781062 +0.0011033932179006088 +0.0011222424420222205 +0.0010375205291658766 +0.0012348160575316306 +0.0011826927315491012 +0.0010671410415516137 +0.0011711615448849848 +0.0011226251500075733 +0.00109441076055885 +0.001094864463168803 +0.0012022274437243083 +0.0011448479964942628 +0.001177852369182545 +0.0011002194983556388 +0.0011536796847610848 +0.0012020752603471268 +0.0010927386293982887 +0.00119823947194137 +0.0010685209532565016 +0.0011808365130782394 +0.0011372096566268058 +0.0011557260888021865 +0.0011304444819921215 +0.0011073395461831943 +0.0011146527005709973 +0.0011737068014750476 +0.001196673453050192 +0.001099569016246178 +0.0010796933440538885 +0.001043599943288889 +0.001153815061186113 +0.00131400849101086 +0.0011597212267390755 +0.0012451816466449049 +0.0010969945781010115 +0.0011004892551955102 +0.0011490190194486621 +0.0012283203493037387 +0.0010586820817033659 +0.0011891430899622388 +0.0011420664067813631 +0.0010229478025710005 +0.0010691985495450338 +0.0012006413404640646 +0.0011247227156228196 +0.0011907207706990607 +0.0010624252987184526 +0.0010228569653310151 +0.0011311460363274618 +0.0010831867629922491 +0.001078372575538281 +0.0011211665776477088 +0.0011605348662726803 +0.0010468906446104121 +0.001111427826815859 +0.0010188359106680675 +0.0011832758640857235 +0.0010596513093145692 +0.0011528677436051875 +0.0011282187354133004 +0.0010452271074060323 +0.0011074716717346302 +0.0011205642580078064 +9.867064467871667E-4 +0.001075272432523398 +9.872992855691574E-4 +0.0011157348160055031 +0.0011666183330647387 +0.001134882543661718 +0.0010855425213214857 +9.486135179973545E-4 +0.001022739916990013 +0.0011230768811140068 +0.0011034279475247624 +0.0011256491710883978 +0.0010678857770924672 +0.0010462878905578591 +0.001126936721613097 +0.0010518572445067302 +0.00100850428956523 +0.0011262068223730414 +0.0011005048073512705 +0.0011543348908842977 +0.0010939783368006336 +0.001022253007938055 +9.327735142685685E-4 +0.0011040492611731388 +0.001031427321278705 +0.0011174208044228532 +0.0010711547316751779 +0.0010263748860923343 +9.271388117916817E-4 +0.0010119339016748938 +0.0010729189004375487 +0.001017358352006335 +0.0012325543901770487 +0.0010559322713433975 +0.0010388280188947873 +0.0010112348874334633 +0.0011041523292409918 +0.001139488720022 +9.478706332814315E-4 +0.0010618470148916553 +0.0010535115515057064 +0.0011410732587352308 +0.0010390307941775675 +0.0011029457801466742 +0.00113809700552862 +0.0010230097035513453 +9.54459369975072E-4 +9.575993594545408E-4 +0.0010089179546782145 +0.0010742472276899845 +0.0010508183691489978 +0.001015706339611075 +9.63931367881156E-4 +9.892104999012375E-4 +0.0011395912499243275 +0.001027489144123793 +9.938574333827525E-4 +0.0010141638067099764 +0.0010850567240691322 +0.0010567209240592832 +0.0010546186206946042 +0.0010591449258845588 +0.0010788103431173524 +0.001001327795102536 +0.0010537475604692989 +9.776562827771178E-4 +0.0010301238724556479 +9.999903298850048E-4 +9.996640973755774E-4 +9.982351690180635E-4 +0.0010282243521071899 +9.793735100862085E-4 +9.937214763986947E-4 +0.0010560997662210044 +0.001064940358969091 +9.321880074986722E-4 +0.0010448277305721279 +9.336692191825015E-4 +0.001083214198713888 +0.0010289052566263427 +0.0010878579899422471 +9.628443269300946E-4 +9.148432953590753E-4 +0.0010009806857296107 +0.001064634768220053 +0.0010917456341344672 +0.001039660018309393 +9.640620972111818E-4 +9.473501095787988E-4 +8.898399478407448E-4 +0.0010752641810434242 +0.0010225882851613213 +0.001031186267761093 +0.001032388300708419 +9.692852639524189E-4 +0.0010636290585673226 +0.0011157012652095268 +9.445338312394765E-4 +9.796550027818063E-4 +9.380874632594835E-4 +9.171090871220963E-4 +0.0010338819871365346 +9.817352980271506E-4 +0.001035548610723382 +0.0010366089040039462 +0.0010023017609384433 +0.0010350418131665217 +0.0010078566274789762 +8.68331298898284E-4 +8.974661539826759E-4 +9.705357497685943E-4 +0.0010949970765370904 +9.495183412072032E-4 +9.737454775958202E-4 +0.0010217995814455307 +9.522448537898191E-4 +9.779127147104816E-4 +9.86232475982084E-4 +0.0010298898005799344 +0.0010131585301009062 +0.0010574032640673276 +9.48474613345645E-4 +0.0010620791774603989 +9.276998623420052E-4 +9.204849451858387E-4 +0.0010399404511545424 +9.264433947557337E-4 +9.089041102056008E-4 +8.985993184669925E-4 +0.0010315295220899943 +0.0010147607153646264 +9.377201100029269E-4 +9.537576069215683E-4 +0.0010009682437537631 +9.99695015129562E-4 +9.242726798056872E-4 +9.24635450522338E-4 +9.956002157419526E-4 +0.0010611750592811704 +0.0010763265510399733 +9.299215963015973E-4 +9.559145806317533E-4 +8.895396926441966E-4 +0.0010214904500228136 +0.0010665758733511713 +9.38328912522674E-4 +9.979567946888498E-4 +9.265333791378834E-4 +9.839671786840895E-4 +0.001028895479833451 +8.506542503867044E-4 +0.0010133621193351137 +8.787126554211259E-4 +9.963469146455363E-4 +9.615126345157179E-4 +9.85958165485008E-4 +9.00999168671934E-4 +9.22395160595154E-4 +9.847387915653623E-4 +9.550681987435555E-4 +0.0010157534718449773 +9.033087045773685E-4 +0.0010196936306393514 +9.886304333696812E-4 +9.621123908614798E-4 +9.961358429027196E-4 +9.185365383486628E-4 +9.802937997516852E-4 +0.0010325104413239838 +8.856756388963145E-4 +9.561355684135135E-4 +9.657359083146397E-4 +9.274753354606738E-4 +9.817087748994818E-4 +9.290601376764568E-4 +9.549805031394914E-4 +0.0010053984111506784 +0.0010171563035885468 +8.490199507419586E-4 +9.56482243200863E-4 +9.605373194334563E-4 +0.0010119157832749685 +8.622504120430778E-4 +9.827176707024432E-4 +9.373533932955863E-4 +9.675303640829435E-4 +0.0010249931272057681 +9.61126280284176E-4 +9.184629421665977E-4 +0.0010050193148524005 +9.787415813740507E-4 +8.972025579781401E-4 +9.687533583789295E-4 +8.925667325497291E-4 +9.642009967372524E-4 +9.053263729012275E-4 +9.921545067133972E-4 +8.908052742353586E-4 +9.467385459143673E-4 +8.734896034648221E-4 +0.001006776295415798 +9.648248427972446E-4 +0.0010178447964271453 +0.0010334043744897557 +9.623872532366822E-4 +8.08721329670184E-4 +8.981291571899335E-4 +8.965830799635602E-4 +9.411649803398181E-4 +9.233563139416925E-4 +9.427443681121117E-4 +9.942239823302458E-4 +9.088242408932741E-4 +9.540261346786916E-4 +9.741512437633195E-4 +9.305784543443058E-4 +8.936810810671528E-4 +8.922716098607077E-4 +9.078055011613465E-4 +9.380441154268395E-4 +9.293086318591539E-4 +9.975803217654006E-4 +8.729079377154703E-4 +8.944054849967194E-4 +9.603926066129352E-4 +9.359497708437436E-4 +8.584287049822975E-4 +9.084124785513469E-4 +9.690922729424672E-4 +9.577562696746035E-4 +8.787112588322701E-4 +9.25262999202752E-4 +9.069952161436504E-4 +8.553040119121029E-4 +9.441837660562317E-4 +8.881766062200255E-4 +9.182443930143199E-4 +9.210270704674465E-4 +0.0010207690003883872 +9.212511483642543E-4 +9.697071438700654E-4 +8.728908036006088E-4 +0.0010346924699559093 +9.98847382922486E-4 +9.243479071810528E-4 +8.86103374277685E-4 +8.924994979962734E-4 +8.810799504326165E-4 +0.0010062064982125846 +9.448553865605595E-4 +9.382799635233933E-4 +9.304974738814165E-4 +0.0010457539120059794 +8.357527786592987E-4 +9.217727903643321E-4 +9.179572581649916E-4 +9.168779812731295E-4 +9.622433504513817E-4 +9.32675306480942E-4 +8.553908706400959E-4 +9.83197977629328E-4 +8.380823090224836E-4 +9.098683129447309E-4 +9.521735462902145E-4 +9.234869018250256E-4 +9.101871270324486E-4 +9.571374853617023E-4 +9.014654684161298E-4 +9.454236030554885E-4 +8.743210842609578E-4 +0.001009335913540478 +8.942776328813976E-4 +9.46445773117879E-4 +9.261167832501032E-4 +9.36900494161455E-4 +0.0010312031257503273 +9.900728853623566E-4 +8.952595563375166E-4 +9.834533909209228E-4 +8.663718595624797E-4 +9.043261594713736E-4 +9.279968431679345E-4 +8.750985392750564E-4 +9.371156720928401E-4 +8.549939108391614E-4 +9.106702956319355E-4 +9.733885260125103E-4 +9.251988897112464E-4 +9.163237448419129E-4 +9.763413853652612E-4 +8.17850953219026E-4 +9.578846196673777E-4 +8.365217856229663E-4 +0.0010026936481388117 +7.928175701311635E-4 +8.893189554431385E-4 +9.017435287623222E-4 +8.783729172775835E-4 +8.669714891290667E-4 +8.950098150254109E-4 +9.102292374057512E-4 +8.678038819327828E-4 +9.072342094860982E-4 +9.204681033012889E-4 +9.140102265789373E-4 +9.679079074939713E-4 +8.97266035411225E-4 +9.253785287178447E-4 +8.87548440924595E-4 +8.982166442089941E-4 +9.331063397107384E-4 +9.793688190172947E-4 +8.571873137309112E-4 +7.653171049075817E-4 +9.437126053661114E-4 +9.130674200589031E-4 +9.921485769821788E-4 +9.309642233887024E-4 +9.060627123548954E-4 +8.980914435685737E-4 +9.652877389717347E-4 +9.047117932972571E-4 +9.667537814851369E-4 +9.535766035259877E-4 +8.941375274267972E-4 +8.984040549051778E-4 +8.652377377593744E-4 +9.20640529777493E-4 +9.180878222942158E-4 +9.559498677603602E-4 +9.036754182526869E-4 +8.948543811167884E-4 +8.577419937245519E-4 +9.328693608319587E-4 +9.448588934203852E-4 +8.874946478023135E-4 +9.201634267392057E-4 +8.218524536406166E-4 +9.554481063991601E-4 +8.727141872122932E-4 +0.0010440163697008847 +8.73836572999781E-4 +8.474705098276823E-4 +9.242788766551806E-4 +9.6628533033844E-4 +9.552653779506281E-4 +8.541963891119773E-4 +9.447177794880269E-4 +8.826859272186537E-4 +8.329850473356033E-4 +8.450126312152971E-4 +8.878943253854854E-4 +9.061636989099398E-4 +9.572319794719509E-4 +9.622037024296709E-4 +9.500723242444433E-4 +9.32405414821388E-4 +8.565420408414186E-4 +8.399901318746076E-4 +9.695118362994088E-4 +8.550086248119928E-4 +8.940615541457234E-4 +0.0010099358754753034 +9.203713767686853E-4 +8.886241205073025E-4 +9.380363126489836E-4 +8.853380504739757E-4 +9.403908197696909E-4 +8.631159768530599E-4 +9.595645004578227E-4 +9.477911078217122E-4 +8.587532061341414E-4 +9.110737725752171E-4 +8.817375500176881E-4 +8.506326823980858E-4 +0.0010001005385404018 +9.101279499969762E-4 +8.321551714226473E-4 +9.509451733575742E-4 +8.358075345119111E-4 +9.362701841463229E-4 +8.750119106584959E-4 +8.856057393331677E-4 +9.750659213866944E-4 +9.699331289208207E-4 +8.291897553302293E-4 +9.72959515072471E-4 \ No newline at end of file From fbf3c6d0ecaed2ca75eaa2c0ca44bdf4adde8b4e Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:24:57 +0300 Subject: [PATCH 100/116] v1.97 --- src/main/java/pulse/AbstractData.java | 2 +- src/main/java/pulse/DiscreteInput.java | 45 ++ src/main/java/pulse/HeatingCurve.java | 31 +- src/main/java/pulse/Response.java | 25 + .../pulse/baseline/AdjustableBaseline.java | 69 ++- src/main/java/pulse/baseline/Baseline.java | 99 +--- .../java/pulse/baseline/FlatBaseline.java | 22 +- .../java/pulse/baseline/LinearBaseline.java | 95 +-- .../pulse/baseline/SinusoidalBaseline.java | 461 ++++++++++----- .../java/pulse/input/ExperimentalData.java | 198 ++----- src/main/java/pulse/input/IndexRange.java | 27 +- src/main/java/pulse/input/Range.java | 84 ++- .../io/export/ResidualStatisticExporter.java | 10 +- .../pulse/io/readers/NetzschCSVReader.java | 101 ++-- src/main/java/pulse/math/FFTTransformer.java | 136 +++++ src/main/java/pulse/math/Harmonic.java | 266 +++++++++ src/main/java/pulse/math/Parameter.java | 91 +++ .../java/pulse/math/ParameterIdentifier.java | 53 ++ src/main/java/pulse/math/ParameterVector.java | 248 +++----- src/main/java/pulse/math/Segment.java | 2 + src/main/java/pulse/math/Window.java | 60 ++ src/main/java/pulse/math/ZScore.java | 134 +++++ .../math/filters/AssignmentListener.java | 7 + src/main/java/pulse/math/filters/Filter.java | 14 + .../math/filters/HalfTimeCalculator.java | 95 +++ .../math/filters/OptimisablePolyline.java | 62 ++ .../math/filters/OptimisedRunningAverage.java | 26 + .../pulse/math/filters/PolylineOptimiser.java | 90 +++ .../java/pulse/math/filters/Randomiser.java | 26 + .../pulse/math/filters/RunningAverage.java | 131 +++++ src/main/java/pulse/math/linear/Vector.java | 5 +- .../math/transforms/InvLenSqTransform.java | 27 - .../math/transforms/InvLenTransform.java | 26 - .../math/transforms/PeriodicTransform.java | 38 ++ .../transforms/StandardTransformations.java | 3 + .../pulse/math/transforms/StickTransform.java | 2 - .../pulse/problem/laser/DiscretePulse.java | 9 +- .../pulse/problem/laser/NumericPulse.java | 6 +- .../pulse/problem/laser/NumericPulseData.java | 21 +- .../schemes/CoupledImplicitScheme.java | 4 +- .../problem/schemes/DifferenceScheme.java | 9 +- .../problem/schemes/FixedPointIterations.java | 27 +- .../pulse/problem/schemes/ImplicitScheme.java | 4 +- .../schemes/TridiagonalMatrixAlgorithm.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 4 +- .../solvers/ImplicitDiathermicSolver.java | 4 +- .../solvers/ImplicitLinearisedSolver.java | 16 +- .../solvers/ImplicitTwoTemperatureSolver.java | 226 ++++++++ .../schemes/solvers/SolverException.java | 23 +- .../problem/statements/ClassicalProblem.java | 22 +- .../statements/ClassicalProblem2D.java | 27 +- .../problem/statements/DiathermicMedium.java | 32 +- .../problem/statements/NonlinearProblem.java | 25 +- .../statements/ParticipatingMedium.java | 6 +- .../statements/PenetrationProblem.java | 15 +- .../pulse/problem/statements/Problem.java | 115 ++-- .../java/pulse/problem/statements/Pulse.java | 10 +- .../statements/TwoTemperatureModel.java | 176 ++++++ .../statements/model/AbsorptionModel.java | 30 +- .../model/BeerLambertAbsorption.java | 9 + .../pulse/problem/statements/model/Gas.java | 75 +++ .../problem/statements/model/Helium.java | 14 + .../problem/statements/model/Insulator.java | 14 + .../problem/statements/model/Nitrogen.java | 14 + .../statements/model/ThermalProperties.java | 11 +- .../model/ThermoOpticalProperties.java | 21 +- .../model/TwoTemperatureProperties.java | 110 ++++ src/main/java/pulse/properties/Flag.java | 12 +- .../pulse/properties/NumericProperty.java | 3 - .../properties/NumericPropertyKeyword.java | 34 +- src/main/java/pulse/search/GeneralTask.java | 217 +++++++ src/main/java/pulse/search/Optimisable.java | 10 +- .../pulse/search/SimpleOptimisationTask.java | 93 +++ .../java/pulse/search/SimpleResponse.java | 35 ++ .../pulse/search/direction/ActiveFlags.java | 56 +- .../pulse/search/direction/BFGSOptimiser.java | 4 +- .../pulse/search/direction/ComplexPath.java | 7 +- .../direction/CompositePathOptimiser.java | 27 +- .../direction/GradientBasedOptimiser.java | 77 ++- .../search/direction/GradientGuidedPath.java | 11 +- .../direction/HessianDirectionSolver.java | 3 +- .../search/direction/IterativeState.java | 5 + .../pulse/search/direction/LMOptimiser.java | 75 +-- .../java/pulse/search/direction/LMPath.java | 6 +- .../pulse/search/direction/PathOptimiser.java | 10 +- .../pulse/search/direction/SR1Optimiser.java | 3 +- .../direction/SteepestDescentOptimiser.java | 9 +- .../direction/pso/ConstrictionMover.java | 49 ++ .../pulse/search/direction/pso/FIPSMover.java | 33 +- .../pulse/search/direction/pso/Mover.java | 4 +- .../pulse/search/direction/pso/Particle.java | 5 +- .../search/direction/pso/ParticleState.java | 31 +- .../direction/pso/ParticleSwarmOptimiser.java | 84 +-- .../direction/pso/StaticTopologies.java | 2 +- .../search/direction/pso/SwarmState.java | 36 +- .../search/linear/GoldenSectionOptimiser.java | 12 +- .../pulse/search/linear/LinearOptimiser.java | 33 +- .../pulse/search/linear/WolfeOptimiser.java | 10 +- .../search/statistics/AbsoluteDeviations.java | 9 +- .../statistics/AndersonDarlingTest.java | 10 +- .../search/statistics/CorrelationTest.java | 2 +- .../pulse/search/statistics/EmptyTest.java | 6 +- .../java/pulse/search/statistics/FTest.java | 16 - .../java/pulse/search/statistics/KSTest.java | 9 +- .../statistics/ModelSelectionCriterion.java | 10 +- .../search/statistics/NormalityTest.java | 3 +- .../pulse/search/statistics/RSquaredTest.java | 38 +- .../RangePenalisedLeastSquares.java | 60 ++ .../statistics/RegularisedLeastSquares.java | 24 +- .../search/statistics/ResidualStatistic.java | 110 ++-- .../pulse/search/statistics/Statistic.java | 7 +- .../pulse/search/statistics/SumOfSquares.java | 10 +- src/main/java/pulse/tasks/Calculation.java | 41 +- src/main/java/pulse/tasks/SearchTask.java | 542 ++++++++---------- src/main/java/pulse/tasks/TaskManager.java | 58 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 14 +- .../java/pulse/tasks/logs/DataLogEntry.java | 37 +- src/main/java/pulse/tasks/logs/Log.java | 61 +- src/main/java/pulse/tasks/logs/Status.java | 20 +- .../java/pulse/tasks/processing/Buffer.java | 15 +- .../tasks/processing/CorrelationBuffer.java | 55 +- .../java/pulse/tasks/processing/Result.java | 3 +- src/main/java/pulse/ui/Launcher.java | 2 +- .../pulse/ui/components/CalculationTable.java | 16 +- src/main/java/pulse/ui/components/Chart.java | 33 +- .../java/pulse/ui/components/DataLoader.java | 8 +- .../java/pulse/ui/components/LogPane.java | 2 +- .../java/pulse/ui/components/ProblemTree.java | 7 +- .../pulse/ui/components/PulseMainMenu.java | 4 +- .../pulse/ui/components/RangeTextFields.java | 29 +- .../pulse/ui/components/ResidualsChart.java | 4 +- .../java/pulse/ui/components/ResultTable.java | 63 +- .../pulse/ui/components/TaskPopupMenu.java | 19 +- .../components/buttons/ExecutionButton.java | 4 +- .../controllers/InstanceCellEditor.java | 6 +- .../components/models/ResultTableModel.java | 9 +- .../ui/components/models/TaskTableModel.java | 11 +- .../ui/components/panels/ChartToolbar.java | 10 +- .../ui/components/panels/ProblemToolbar.java | 8 +- .../java/pulse/ui/frames/HistogramFrame.java | 5 +- .../java/pulse/ui/frames/MainGraphFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 154 +++-- .../pulse/ui/frames/SearchOptionsFrame.java | 10 +- .../pulse/ui/frames/TaskControlFrame.java | 8 +- src/main/java/pulse/util/Group.java | 6 +- .../java/pulse/util/UpwardsNavigable.java | 11 +- 147 files changed, 4497 insertions(+), 1989 deletions(-) create mode 100644 src/main/java/pulse/DiscreteInput.java create mode 100644 src/main/java/pulse/Response.java create mode 100644 src/main/java/pulse/math/FFTTransformer.java create mode 100644 src/main/java/pulse/math/Harmonic.java create mode 100644 src/main/java/pulse/math/Parameter.java create mode 100644 src/main/java/pulse/math/ParameterIdentifier.java create mode 100644 src/main/java/pulse/math/Window.java create mode 100644 src/main/java/pulse/math/ZScore.java create mode 100644 src/main/java/pulse/math/filters/AssignmentListener.java create mode 100644 src/main/java/pulse/math/filters/Filter.java create mode 100644 src/main/java/pulse/math/filters/HalfTimeCalculator.java create mode 100644 src/main/java/pulse/math/filters/OptimisablePolyline.java create mode 100644 src/main/java/pulse/math/filters/OptimisedRunningAverage.java create mode 100644 src/main/java/pulse/math/filters/PolylineOptimiser.java create mode 100644 src/main/java/pulse/math/filters/Randomiser.java create mode 100644 src/main/java/pulse/math/filters/RunningAverage.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenSqTransform.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenTransform.java create mode 100644 src/main/java/pulse/math/transforms/PeriodicTransform.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java create mode 100644 src/main/java/pulse/problem/statements/TwoTemperatureModel.java create mode 100644 src/main/java/pulse/problem/statements/model/Gas.java create mode 100644 src/main/java/pulse/problem/statements/model/Helium.java create mode 100644 src/main/java/pulse/problem/statements/model/Nitrogen.java create mode 100644 src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java create mode 100644 src/main/java/pulse/search/GeneralTask.java create mode 100644 src/main/java/pulse/search/SimpleOptimisationTask.java create mode 100644 src/main/java/pulse/search/SimpleResponse.java create mode 100644 src/main/java/pulse/search/direction/pso/ConstrictionMover.java create mode 100644 src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index 1fd62645..a1d276c7 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -306,4 +306,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java new file mode 100644 index 00000000..c6bb8d85 --- /dev/null +++ b/src/main/java/pulse/DiscreteInput.java @@ -0,0 +1,45 @@ +package pulse; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.input.IndexRange; +import pulse.math.Segment; + +public interface DiscreteInput { + + public List getX(); + public List getY(); + public IndexRange getIndexRange(); + + public static List convert(double[] x, double[] y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.length; i < size; i++) { + ps.add(new Point2D.Double(x[i], y[i])); + } + + return ps; + + } + + public static List convert(List x, List y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.size(); i < size; i++) { + ps.add(new Point2D.Double(x.get(i), y.get(i))); + } + + return ps; + + } + + public default Segment bounds() { + var ir = getIndexRange(); + var x = getX(); + return new Segment(x.get(ir.getLowerBound()), x.get(ir.getUpperBound())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 8fb0f556..86e14621 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -10,6 +10,7 @@ import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.ArrayList; +import static java.util.Collections.max; import java.util.List; import java.util.Set; @@ -48,8 +49,8 @@ public class HeatingCurve extends AbstractData { private final List listeners = new ArrayList<>(); - private UnivariateInterpolator splineInterpolator; - private UnivariateFunction splineInterpolation; + private UnivariateInterpolator interpolator; + private UnivariateFunction interpolation; protected HeatingCurve(List time, List signal, final double startTime, String name) { super(time, name); @@ -64,7 +65,7 @@ protected HeatingCurve(List time, List signal, final double star public HeatingCurve() { super(); adjustedSignal = new ArrayList<>((int) this.getNumPoints().getValue()); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } /** @@ -78,8 +79,8 @@ public HeatingCurve(HeatingCurve c) { super(c); this.adjustedSignal = new ArrayList<>(c.adjustedSignal); this.startTime = c.startTime; - splineInterpolator = new SplineInterpolator(); - if (c.splineInterpolation != null) { + interpolator = new SplineInterpolator(); + if (c.interpolation != null) { this.refreshInterpolation(); } } @@ -100,7 +101,7 @@ public HeatingCurve(NumericProperty count) { adjustedSignal = new ArrayList<>((int) count.getValue()); startTime = (double) def(TIME_SHIFT).getValue(); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } //TODO @@ -163,7 +164,7 @@ public double signalAt(int index) { */ public void scale(double scale) { final int count = this.actualNumPoints(); - for (int i = 0; i < count; i++) { + for (int i = 0, max = Math.min(count, signal.size()); i < max; i++) { signal.set(i, signal.get(i) * scale); } var dataEvent = new CurveEvent(RESCALED, this); @@ -201,7 +202,8 @@ private void refreshInterpolation() { /* * Submit to spline interpolation */ - splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); + + interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); } /** @@ -346,8 +348,8 @@ public void setTimeShift(NumericProperty startTime) { firePropertyChanged(this, startTime); } - public UnivariateFunction getSplineInterpolation() { - return splineInterpolation; + public UnivariateFunction getInterpolation() { + return interpolation; } public List getBaselineCorrectedData() { @@ -377,5 +379,12 @@ public boolean equals(Object o) { return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); } + + public double interpolateSignalAt(double x) { + double min = this.timeAt(0); + double max = timeLimit(); + return min < x && max > x ? interpolation.value(x) + : (x < min ? signalAt(0) : signalAt(actualNumPoints() - 1)); + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/Response.java b/src/main/java/pulse/Response.java new file mode 100644 index 00000000..cc4f23ce --- /dev/null +++ b/src/main/java/pulse/Response.java @@ -0,0 +1,25 @@ +package pulse; + +import pulse.math.Segment; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.statistics.OptimiserStatistic; + +public interface Response { + + public double evaluate(double t); + public Segment accessibleRange(); + + /** + * Calculates the value of the objective function used to identify + * the current state of the optimiser. + * @param task + * @return the value of the objective function in the current state + * @throws pulse.problem.schemes.solvers.SolverException + */ + + public double objectiveFunction(GeneralTask task) throws SolverException; + + public OptimiserStatistic getOptimiserStatistic(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/AdjustableBaseline.java b/src/main/java/pulse/baseline/AdjustableBaseline.java index bb69ca91..12464c76 100644 --- a/src/main/java/pulse/baseline/AdjustableBaseline.java +++ b/src/main/java/pulse/baseline/AdjustableBaseline.java @@ -1,16 +1,16 @@ package pulse.baseline; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.List; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import pulse.util.PropertyHolder; /** @@ -21,27 +21,31 @@ public abstract class AdjustableBaseline extends Baseline { private double intercept; + private double slope; /** * Creates a flat baseline equal to the argument. * * @param intercept the constant baseline value. */ - public AdjustableBaseline(double intercept) { + public AdjustableBaseline(double intercept, double slope) { this.intercept = intercept; + this.slope = slope; } /** - * @return the constant value of this {@code FlatBaseline} + * Calculates the linear function {@code g(x) = intercept + slope*time} + * + * @param x the argument of the linear function + * @return the result of this simple calculation */ @Override public double valueAt(double x) { - return intercept; + return intercept + x * slope; } protected double mean(List x) { - double sum = x.stream().reduce((a, b) -> a + b).get(); - return sum / x.size(); + return x.stream().mapToDouble(d -> d).average().getAsDouble(); } /** @@ -69,6 +73,29 @@ public void setIntercept(NumericProperty intercept) { firePropertyChanged(this, intercept); } + /** + * Provides getter accessibility to the slope as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop + */ + public NumericProperty getSlope() { + return derive(BASELINE_SLOPE, slope); + } + + /** + * Checks whether {@code slope} is a baseline slope property and updates the + * respective value of this baseline. + * + * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type + * @see set + */ + public void setSlope(NumericProperty slope) { + requireType(slope, BASELINE_SLOPE); + this.slope = (double) slope.getValue(); + firePropertyChanged(this, slope); + } + /** * Lists the {@code intercept} as accessible property for this * {@code FlatBaseline}. @@ -91,13 +118,17 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + + if (p != null) { + + var key = p.getIdentifier().getKeyword(); - var key = output.getIndex(i); + if (key == BASELINE_INTERCEPT) { + p.setValue(intercept); + } - if (key == BASELINE_INTERCEPT) { - output.set(i, intercept, key); } } @@ -106,10 +137,12 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_INTERCEPT) { - setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); + if (p.getIdentifier().getKeyword() == BASELINE_INTERCEPT) { + setIntercept( + derive(BASELINE_INTERCEPT, p.inverseTransform()) + ); } } diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 20c2ab94..d42c31d1 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,15 +1,11 @@ package pulse.baseline; -import static java.lang.Double.NEGATIVE_INFINITY; -import static java.lang.Math.min; - -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import pulse.AbstractData; +import pulse.DiscreteInput; import pulse.input.ExperimentalData; import pulse.input.IndexRange; +import pulse.input.Range; import pulse.search.Optimisable; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -27,6 +23,8 @@ */ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { + public final static int MIN_BASELINE_POINTS = 15; + public abstract Baseline copy(); /** @@ -48,74 +46,10 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti * values, triggering whatever events are associated with them. *

* - * @param x a list of independent variable values - * @param y a list of dependent variable values - * @param size the size of the region + * @param x + * @param y */ - protected abstract void doFit(List x, List y, int size); - - /** - * Selects part of the {@code data} that can be used for baseline estimation - * (typically, this means selecting 'negative' time values and the - * corresponding signal) data and runs the fitting algorithms, - * - * @param data the experimental data - * @param rangeMin the minimum of the time range - * @param rangeMax the maximum of the time range - */ - public void fitTo(ExperimentalData data, double rangeMin, double rangeMax) { - var indexRange = data.getIndexRange(); - - Objects.requireNonNull(indexRange); - - if (!indexRange.isValid()) { - throw new IllegalArgumentException("Index range not valid: " + indexRange); - } - - List x = new ArrayList<>(); - List y = new ArrayList<>(); - - int size = 0; - - for (int i = IndexRange.closestLeft(rangeMin, data.getTimeSequence()) + 1, max = min(indexRange.getLowerBound(), - IndexRange.closestRight(rangeMax, data.getTimeSequence())); i < max; i++, size++) { - - x.add(data.timeAt(i)); - y.add(data.signalAt(i)); - - } - - if (size > 0) // do fitting only if data is present - { - doFit(x, y, size); - } - - } - - /** - * Fit to an abstract set of data, using only the subset corresponding to the negative time range. - * @param data a dataset - */ - - public void fitNegative(AbstractData data) { - final int MIN_POINTS = 15; - - var time = data.getTimeSequence(); - var signal = data.getSignalData(); - - var subsetTime = new ArrayList(); - var subsetSignal = new ArrayList(); - - int i; - - for(i = 0; time.get(i) < 0; i++) { - subsetTime.add(time.get(i)); - subsetSignal.add(signal.get(i)); - } - - if(i > MIN_POINTS) - doFit(subsetTime, subsetSignal, i); - } + protected abstract void doFit(List x, List y); /** * Calls {@code fitTo} using the default time range for the data: @@ -125,9 +59,20 @@ public void fitNegative(AbstractData data) { * @param data the experimental data stretching to negative time values * @see fitTo(ExperimentalData,double,double) */ - public void fitTo(ExperimentalData data) { - final double ZERO_LEFT = -1E-5; - fitTo(data, NEGATIVE_INFINITY, ZERO_LEFT); + public void fitTo(DiscreteInput data) { + var filtered = Range.NEGATIVE.filter(data); + if(filtered[0].size() > MIN_BASELINE_POINTS) { + doFit(filtered[0], filtered[1]); + } + } + + public void fitTo(List x, List y) { + int index = IndexRange.closestLeft(0, x); + var xx = x.subList(0, index + 1); + var yy = y.subList(0, index + 1); + if(xx.size() > MIN_BASELINE_POINTS) { + doFit(xx, yy); + } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 56e1d7cc..9500c8f6 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.baseline; import static java.lang.String.format; @@ -22,7 +7,6 @@ /** * A flat baseline. - * @author Artem Lunev */ public class FlatBaseline extends AdjustableBaseline { @@ -41,12 +25,12 @@ public FlatBaseline() { * @param intercept the constant baseline value. */ public FlatBaseline(double intercept) { - super(intercept); + super(intercept, 0.0); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double intercept = mean(y); set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); } @@ -61,4 +45,4 @@ public String toString() { return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index c6381ebc..d3d899e4 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -2,15 +2,13 @@ import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; @@ -31,43 +29,26 @@ */ public class LinearBaseline extends AdjustableBaseline { - private double slope; - /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. */ public LinearBaseline() { - super(0.0); + super(0.0, 0.0); } - - /** - * A constructor, which allows to specify all three parameters in one go. - * - * @param intercept the intercept is the value of the Baseline's linear - * function at {@code x = 0} - * @param slope the slope determines the inclination angle of the Baseline's - * graph. - */ + public LinearBaseline(double intercept, double slope) { - super(intercept); - this.slope = slope; + super(intercept, slope); } - - /** - * Calculates the linear function {@code g(x) = intercept + slope*time} - * - * @param x the argument of the linear function - * @return the result of this simple calculation - */ - @Override - public double valueAt(double x) { - final double intercept = (double) getIntercept().getValue(); - return intercept + x * slope; + + public LinearBaseline(LinearBaseline baseline) { + super( (double) baseline.getIntercept().getValue(), + (double) baseline.getSlope().getValue() + ); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double meanx = mean(x); double meany = mean(y); @@ -76,46 +57,25 @@ protected void doFit(List x, List y, int size) { double xxbar = 0.0; double xybar = 0.0; - for (int i = 0; i < size; i++) { + for (int i = 0, size = x.size(); i < size; i++) { x1 = x.get(i); y1 = y.get(i); xxbar += (x1 - meanx) * (x1 - meanx); xybar += (x1 - meanx) * (y1 - meany); } - - slope = xybar / xxbar; + + double slope = xybar / xxbar; double intercept = meany - slope * meanx; set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); } - /** - * Provides getter accessibility to the slope as a NumericProperty - * - * @return a NumericProperty derived from - * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop - */ - public NumericProperty getSlope() { - return derive(BASELINE_SLOPE, slope); - } - - /** - * Checks whether {@code slope} is a baseline slope property and updates the - * respective value of this baseline. - * - * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type - * @see set - */ - public void setSlope(NumericProperty slope) { - requireType(slope, BASELINE_SLOPE); - this.slope = (double) slope.getValue(); - firePropertyChanged(this, slope); - } - @Override public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + var slope = getSlope().getValue(); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); } @Override @@ -129,15 +89,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == BASELINE_SLOPE) { - output.set(i, slope, BASELINE_SLOPE); + double slope = (double) getSlope().getValue(); + p.setValue(slope); } } @@ -157,10 +118,12 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_SLOPE) { - setSlope(derive(BASELINE_SLOPE, params.get(i))); + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_SLOPE) { + setSlope( derive(BASELINE_SLOPE, p.inverseTransform() )); } } @@ -180,7 +143,7 @@ public Set listedKeywords() { @Override public Baseline copy() { - return new LinearBaseline((double) this.getIntercept().getValue(), this.slope); + return new LinearBaseline(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index c55ec0ed..9074cdc4 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,197 +1,400 @@ package pulse.baseline; -import static java.lang.Math.sin; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; -import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; -import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.math.FFTTransformer; +import pulse.math.Harmonic; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; - +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; +import pulse.input.Range; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.math.transforms.StickTransform; +import pulse.math.ZScore; +import pulse.math.filters.Filter; +import pulse.math.filters.OptimisedRunningAverage; +import pulse.math.filters.Randomiser; +import pulse.math.filters.RunningAverage; import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.ActiveFlags; +import pulse.search.statistics.SumOfSquares; +import pulse.util.Group; +import static pulse.properties.NumericPropertyKeyword.MAX_HIGH_FREQ_WAVES; +import static pulse.properties.NumericPropertyKeyword.MAX_LOW_FREQ_WAVES; /** - * A simple sinusoidal baseline. - *

- * It is given by the expression y = y0 + - * A sin(2πf t + φ) , where f is the - * frequency (in Hz), A is the amplitude, φ is the phase shift. - * Extends the {@code FlatBaseline} class and thus inherits the - * {@code BASELINE_INTERCEPT} property. The sinusoidal baseline is useful to - * mitigate electromagnetic interferences, with the frequencies usually in the - * range of 25 to 60 Hz. - *

+ * A multiple-harmonic baseline. Replaces the Sinusoidal baseline in previous + * version. * */ -public class SinusoidalBaseline extends AdjustableBaseline { +public class SinusoidalBaseline extends LinearBaseline { + + private List hiFreq; + private List loFreq; + private List active; - private double frequency; - private double phaseShift; - private double amplitude; - private final static double _2PI = 2.0 * Math.PI; + private int maxHighFreqHarmonics; + private int maxLowFreqHarmonics; + + private final static double FREQUENCY_THRESHOLD = 400; /** * Creates a sinusoidal baseline with default properties. */ public SinusoidalBaseline() { - super(0.0); - setFrequency(def(BASELINE_FREQUENCY)); - setAmplitude(def(BASELINE_AMPLITUDE)); - setPhaseShift(def(BASELINE_PHASE_SHIFT)); + super(0.0, 0.0); + maxHighFreqHarmonics = (int) def(MAX_HIGH_FREQ_WAVES).getValue(); + maxLowFreqHarmonics = (int) def(MAX_LOW_FREQ_WAVES).getValue(); + hiFreq = new ArrayList<>(); + active = new ArrayList<>(); + loFreq = new ArrayList<>(); } @Override public double valueAt(double x) { - var intercept = (double) getIntercept().getValue(); - return intercept + amplitude * sin(_2PI * x * frequency + phaseShift); + return super.valueAt(x) + + active.stream().mapToDouble(h -> h.valueAt(x)).sum(); } - /** - * Listed properties include the frequency, amplitude, phase shift, and - * intercept. - */ @Override - public Set listedKeywords() { - var set = super.listedKeywords(); - set.add(BASELINE_FREQUENCY); - set.add(BASELINE_AMPLITUDE); - set.add(BASELINE_PHASE_SHIFT); - return set; + public Baseline copy() { + var baseline = new SinusoidalBaseline(); + baseline.setIntercept(this.getIntercept()); + baseline.setSlope(this.getSlope()); + baseline.hiFreq = new ArrayList<>(); + baseline.maxHighFreqHarmonics = this.maxHighFreqHarmonics; + baseline.maxLowFreqHarmonics = this.maxLowFreqHarmonics; + for (Harmonic h : active) { + var newH = new Harmonic(h); + baseline.active.add(newH); + newH.setParent(baseline); + } + for (Harmonic h : hiFreq) { + baseline.hiFreq.add(new Harmonic(h)); + } + for (Harmonic h : loFreq) { + baseline.loFreq.add(new Harmonic(h)); + } + return baseline; } @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + active.forEach(h -> h.optimisationVector(output) ); + } - switch (type) { - case BASELINE_FREQUENCY: - setFrequency(property); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(property); - break; - case BASELINE_AMPLITUDE: - setAmplitude(property); - break; - default: - super.set(type, property); + @Override + public void assign(ParameterVector output) { + super.assign(output); + active.forEach(h + -> h.assign(output) + ); + } + + private void guessHarmonics(double[] x, double[] y) { + var fft = new FFTTransformer(y); + fft.transform(); + double[] sampling = fft.sampling(x); + + var amplitude = fft.getAmpltiudeSpectrum(); + var phase = fft.getPhaseSpectrum(); + + var zscore = new ZScore(); + zscore.process(amplitude); + + var signals = zscore.getSignals(); + double maxAmp = 0; + + hiFreq = new ArrayList<>(); + + double span = x[x.length - 1] - x[0]; + double lowerFrequency = 4.0 / span; + + for (int i = 0; i < sampling.length; i++) { + if (signals[i] > 0) { + if (sampling[i] < FREQUENCY_THRESHOLD && sampling[i] > lowerFrequency) { + var h = new Harmonic(amplitude[i], sampling[i], phase[i]); + hiFreq.add(h); + maxAmp = Math.max(maxAmp, amplitude[i]); + } + } } + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); } - public NumericProperty getFrequency() { - return derive(BASELINE_FREQUENCY, frequency); + private List sort(List hs, int limit) { + var tmp = new ArrayList<>(hs); + tmp.sort(null); + Collections.reverse(tmp); + //leave out a maximum of n harmonics + return tmp.subList(0, Math.min(tmp.size(), limit)); } - public NumericProperty getAmplitude() { - return derive(BASELINE_AMPLITUDE, amplitude); + private void labelActive() { + for (int i = 0, size = active.size(); i < size; i++) { + active.get(i).setRank(i); + active.get(i).setParent(this); + } } - public NumericProperty getPhaseShift() { - return derive(BASELINE_PHASE_SHIFT, phaseShift); - } + private void fitHarmonics(DiscreteInput input) { - public void setFrequency(NumericProperty frequency) { - requireType(frequency, BASELINE_FREQUENCY); - this.frequency = (double) frequency.getValue(); - firePropertyChanged(this, frequency); - } + var sos = new SumOfSquares() { - public void setAmplitude(NumericProperty amplitude) { - requireType(amplitude, BASELINE_AMPLITUDE); - this.amplitude = (double) amplitude.getValue(); - firePropertyChanged(this, amplitude); - } + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + + SimpleResponse response = new SimpleResponse(sos) { + + @Override + public double evaluate(double t) { + return valueAt(t); + } + + }; + + var task = new SimpleOptimisationTask(this, input) { + + @Override + public Response getResponse() { + return response; + } + + }; + + //adjust optimisation flags + var flagList = new ArrayList(); + flagList.add(new Flag(BASELINE_AMPLITUDE, false)); + flagList.add(new Flag(BASELINE_FREQUENCY, true)); + flagList.add(new Flag(BASELINE_PHASE_SHIFT, true)); + flagList.add(new Flag(BASELINE_INTERCEPT, false)); + flagList.add(new Flag(BASELINE_SLOPE, true)); + + var oldState = ActiveFlags.storeState(); + ActiveFlags.loadState(flagList); + + CompletableFuture.runAsync(task).thenRun(() -> { + flagList.stream().filter(f -> f.getType() == BASELINE_AMPLITUDE) + .findFirst().get().setValue(true); + task.run(); + ActiveFlags.loadState(oldState); + } + ); - public void setPhaseShift(NumericProperty phaseShift) { - requireType(phaseShift, BASELINE_PHASE_SHIFT); - this.phaseShift = (double) phaseShift.getValue(); - firePropertyChanged(this, phaseShift); } /** - * The optimisation vector can include the amplitude, frequency and phase - * shift of a sinusoid, and a baseline intercept value of the superclass. + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case BASELINE_FREQUENCY: - output.set(i, frequency, BASELINE_FREQUENCY); - break; - case BASELINE_PHASE_SHIFT: - output.set(i, phaseShift, BASELINE_PHASE_SHIFT); - break; - case BASELINE_AMPLITUDE: - output.set(i, amplitude, BASELINE_AMPLITUDE); - break; - default: - continue; + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAX_HIGH_FREQ_WAVES); + set.add(MAX_LOW_FREQ_WAVES); + return set; + } + + @Override + public List subgroups() { + return getHarmonics() == null ? new ArrayList<>() + : getHarmonics().stream().map(h -> (Group) h).collect(Collectors.toList()); + } + + public List getHarmonics() { + return active; + } + + public NumericProperty getHiFreqMax() { + return derive(MAX_HIGH_FREQ_WAVES, maxHighFreqHarmonics); + } + + public void setHiFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_HIGH_FREQ_WAVES); + int oldValue = this.maxHighFreqHarmonics; + + if ((int) maxHarmonics.getValue() != oldValue) { + + var lowFreq = new ArrayList(); + int size = active.size(); + + if(maxHighFreqHarmonics < size) { + lowFreq = new ArrayList<>(active.subList(maxHighFreqHarmonics, size)); } - output.setTransform(i, new StickTransform(output.getParameterBounds(i))); - + this.maxHighFreqHarmonics = (int) maxHarmonics.getValue(); + active.clear(); + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); + active.addAll(lowFreq); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } + + } + + public NumericProperty getLowFreqMax() { + return derive(MAX_LOW_FREQ_WAVES, maxLowFreqHarmonics); + } + public void setLowFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_LOW_FREQ_WAVES); + int oldValue = this.maxLowFreqHarmonics; + if ((int) maxHarmonics.getValue() != oldValue) { + this.maxLowFreqHarmonics = (int) maxHarmonics.getValue(); + active = active.subList(0, maxHighFreqHarmonics); + active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } } @Override - public void assign(ParameterVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - switch (params.getIndex(i)) { - case BASELINE_FREQUENCY: - setFrequency(derive(BASELINE_FREQUENCY, params.inverseTransform(i))); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.inverseTransform(i))); - break; - case BASELINE_AMPLITUDE: - setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i))); - break; - default: - break; - } + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + switch (type) { + + case MAX_HIGH_FREQ_WAVES: + setHiFreqMax(property); + break; + case MAX_LOW_FREQ_WAVES: + setLowFreqMax(property); + break; + default: } } @Override - public Baseline copy() { - var baseline = new SinusoidalBaseline(); - baseline.setIntercept(this.getIntercept()); - baseline.amplitude = this.amplitude; - baseline.frequency = this.frequency; - baseline.phaseShift = this.phaseShift; - return baseline; + public void fitTo(DiscreteInput input) { + //fit the linear part first + super.fitTo(input); + //then fit the harmonics -- full range is needed here + + DiscreteInputImpl filtered = (DiscreteInputImpl) filter(input); + + var x = filtered.getXasArray(); + var y = filtered.getYasArray(); + + active.clear(); + guessHarmonics(x, y); + labelActive(); + fitHarmonics(new DiscreteInputImpl(x, y)); + addLowFreq(input); + labelActive(); } - @Override - protected void doFit(List x, List y, int size) { - var flatBaseline = new FlatBaseline(); - flatBaseline.doFit(x, y, size); - //TODO Fourier transform + private DiscreteInput filter(DiscreteInput full) { + var x = full.getX().stream().mapToDouble(d -> d).toArray(); + var y = full.getY().stream().mapToDouble(d -> d).toArray(); + + Filter f = new OptimisedRunningAverage(); + Filter fr = new Randomiser(1.0); + var runningAverage = fr.process(f.process(full)); + + var xAv = runningAverage.stream().mapToDouble(p -> p.getX()).toArray(); + var yAv = runningAverage.stream().mapToDouble(p -> p.getY()).toArray(); + + var spline = new SplineInterpolator(); + var interp = spline.interpolate(xAv, yAv); + + for (int i = 0; i < x.length; i++) { + y[i] -= interp.value(x[i]); + //System.err.println(x[i] + " " + interp.value(x[i]) + " " + y[i]); + } + + return new DiscreteInputImpl(x, y); + + } + + private void addLowFreq(DiscreteInput input) { + double amp = !hiFreq.isEmpty() + ? (double) hiFreq.get(0).getAmplitude().getValue() + : Collections.max(input.getY()) / 2.0; + + double span = input.getX().get(input.getX().size() - 1) - input.getX().get(0); + double freq = RunningAverage.DEFAULT_BINS / span; + + loFreq.clear(); + + /* + These harmonics are inaccessible by FFT + */ + + for (double f = freq; f > 1.0 / (2.0 * span); f /= 2.0) { + loFreq.add(new Harmonic(amp, f, 0.0)); + } + + active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); } @Override public String toString() { return getClass().getSimpleName(); - } + } + + private class DiscreteInputImpl implements DiscreteInput { + + private final double[] x; + private final double[] y; + + public DiscreteInputImpl(double[] x, double[] y) { + this.x = x; + this.y = y; + } + + @Override + public List getX() { + return convert(x); + } + + @Override + public List getY() { + return convert(y); + } + + public double[] getXasArray() { + return x; + } + + public double[] getYasArray() { + return y; + } + + private List convert(double[] a) { + return DoubleStream.of(a).boxed().collect(Collectors.toList()); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(getX(), Range.UNLIMITED); + } + } } diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 15776db3..f38a8e0a 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -1,25 +1,20 @@ package pulse.input; -import static java.lang.Double.valueOf; -import static java.util.Collections.max; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; -import java.awt.geom.Point2D; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import pulse.AbstractData; -import pulse.baseline.FlatBaseline; +import pulse.DiscreteInput; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; import pulse.input.listeners.DataListener; -import pulse.ui.Messages; +import pulse.math.filters.HalfTimeCalculator; import pulse.util.PropertyHolderListener; /** @@ -30,13 +25,13 @@ * {@code CurveReader}s. Any manipulation (e.g. truncation) of the data triggers * an event associated with this {@code ExperimentalData}. */ -public class ExperimentalData extends AbstractData { +public class ExperimentalData extends AbstractData implements DiscreteInput { + private HalfTimeCalculator calculator; private Metadata metadata; private IndexRange indexRange; private Range range; private List dataListeners; - private double halfTime; /** * This is the cutoff factor which is used as a criterion for data @@ -45,22 +40,6 @@ public class ExperimentalData extends AbstractData { */ public final static double CUTOFF_FACTOR = 7.2; - /** - * The binning factor used to build a crude approximation of the heating - * curve. Described in Lunev, A., & Heymer, R. (2020). Review of - * Scientific Instruments, 91(6), 064902. - */ - public final static int REDUCTION_FACTOR = 32; - - public final static int MAX_REDUCTION_FACTOR = 256; - - /** - * A fail-safe factor. - */ - public final static double FAIL_SAFE_FACTOR = 10.0; - - private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); - /** * Constructs an {@code ExperimentalData} object using the superclass * constructor and creating a new list of data listeners. The number of @@ -73,9 +52,12 @@ public ExperimentalData() { dataListeners = new ArrayList<>(); setPrefix("RawData"); setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(); - this.addDataListener((DataEvent e) -> calculateHalfTime() ); - + indexRange = new IndexRange(0,0); + this.addDataListener((DataEvent e) -> { + if (e.getType() == DataEventType.DATA_LOADED) { + preprocess(); + } + }); } public final void addDataListener(DataListener listener) { @@ -127,127 +109,6 @@ public void addPoint(double time, double signal) { incrementCount(); } - /** - * Constructs a deliberately crude representation of this heating curve by - * calculating a running average. - *

- * This is done using a binning algorithm, which will group the - * time-temperature data associated with this {@code ExperimentalData} in - * {@code count/reductionFactor - 1} bins, calculate the average value for - * time and temperature within each bin, and collect those values in a - * {@code List}. This is useful to cancel out the effect of signal - * outliers, e.g. when calculating the half-rise time. - *

- * - * The algorithm is described in more detail in Lunev, A., & Heymer, R. - * (2020). Review of Scientific Instruments, 91(6), 064902. - * - * @param reductionFactor the factor, by which the number of points - * {@code count} will be reduced for this {@code ExperimentalData}. - * @return a {@code List}, representing the degraded - * {@code ExperimentalData}. - * @see halfRiseTime() - * @see pulse.AbstractData.maxTemperature() - */ - public List runningAverage(int reductionFactor) { - - int count = (int) getNumPoints().getValue(); - - List crudeAverage = new ArrayList<>(count / reductionFactor); - - int start = indexRange.getLowerBound(); - int end = indexRange.getUpperBound(); - - int step = (end - start) / (count / reductionFactor); - double av = 0; - - int i1, i2; - - for (int i = 0, max = (count / reductionFactor) - 1; i < max; i++) { - i1 = start + step * i; - i2 = i1 + step; - - av = 0; - - for (int j = i1; j < i2; j++) { - av += signalAt(j); - } - - av /= step; - - crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); - - } - - return crudeAverage; - - } - - /** - * Instead of returning the simple maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the maximum of the - * {@code runningAverage} using the default reduction factor - * {@value REDUCTION_FACTOR}. - * - * @return a {@code Point2D} object containing the coordinates of the - * adjusted maximum. - * @see - * pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) - */ - public Point2D maxAdjustedSignal() { - var degraded = runningAverage(REDUCTION_FACTOR); - return max(degraded, pointComparator); - } - - /** - * Calculates the approximate half-rise time used for crude estimation of - * thermal diffusivity. - *

- * This uses the {@code runningAverage} method by applying the default - * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based - * on finding the approximate value corresponding to the half-maximum of the - * temperature. The latter is calculated using the running average curve. - * The index corresponding to the closest temperature value available for - * that curve is used to retrieve the half-rise time (which also has the - * same index). If this fails, i.e. the associated index is less than 1, - * this will print out a warning message and still assign a value to the - * half-time variable equal to the acquisition time divided by a fail-safe factor - * {@value FAIL_SAFE_FACTOR}. - *

- * @see getHalfTime() - */ - public void calculateHalfTime() { - var baseline = new FlatBaseline(); - baseline.fitTo(this); - - int curRedFactor = REDUCTION_FACTOR/2; // reduced twofold since first operation - // in the while loop will increase it likewise - int cutoffIndex = 0; - List degraded = null; //running average - Point2D max = null; - - do { - curRedFactor *= 2; - degraded = runningAverage(curRedFactor); - max = (max(degraded, pointComparator)); - cutoffIndex = degraded.indexOf(max); - } while(cutoffIndex < 1 && curRedFactor < MAX_REDUCTION_FACTOR); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - degraded = degraded.subList(0, cutoffIndex); - - int index = IndexRange.closestLeft(halfMax, - degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - - if (index < 1) { - System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); - halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; - } - else - halfTime = degraded.get(index).getX(); - - } - /** * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. * @@ -284,7 +145,7 @@ public boolean equals(Object o) { * threshold, {@code false} otherwise. */ public boolean isAcquisitionTimeSensible() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); final int count = (int) getNumPoints().getValue(); double d = getTimeSequence().get(count - 1); return getTimeSequence().get(count - 1) < cutoff; @@ -306,7 +167,7 @@ public boolean isAcquisitionTimeSensible() { * @see fireDataChanged */ public void truncate() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); } @@ -335,16 +196,6 @@ private void doSetMetadata() { } } - - /** - * Retrieves the half-time value of this dataset, which is equal to the - * time needed to reach half of the signal maximum. - * @return the half-time value. - */ - - public double getHalfTime() { - return halfTime; - } /** * Gets the time sequence element corresponding to the lower bound of the @@ -382,6 +233,7 @@ public Range getRange() { * * @return the index range */ + @Override public IndexRange getIndexRange() { return indexRange; } @@ -422,6 +274,28 @@ private void doSetRange() { @Override public double timeLimit() { return timeAt(indexRange.getUpperBound()); - } + } + + public HalfTimeCalculator getHalfTimeCalculator() { + return calculator; + } + + public void preprocess() { + if (calculator == null) { + calculator = new HalfTimeCalculator(this); + } + + calculator.calculate(); + } + + @Override + public List getX() { + return this.getTimeSequence(); + } + + @Override + public List getY() { + return this.getSignalData(); + } } \ No newline at end of file diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index bdf4288d..93a92806 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -20,13 +20,14 @@ public class IndexRange { private int iStart; private int iEnd; - /** - * Construct an empty index range where the start index is set to -1 and the - * end index is set to 0. - */ - public IndexRange() { - iStart = -1; - iEnd = 0; + public IndexRange(IndexRange other) { + iStart = other.iStart; + iEnd = other.iEnd; + } + + public IndexRange(int start, int end) { + this.iStart = start; + this.iEnd = end; } /** @@ -73,7 +74,7 @@ protected void reset(List data) { * @see closestLeft * @see closestRight */ - public void setLowerBound(List data, double a) { + public final void setLowerBound(List data, double a) { iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); } @@ -90,7 +91,7 @@ public void setLowerBound(List data, double a) { * @see closestLeft * @see closestRight */ - public void setUpperBound(List data, double b) { + public final void setUpperBound(List data, double b) { iEnd = closestRight(b, data); } @@ -104,9 +105,9 @@ public void setUpperBound(List data, double b) { * @see setLowerBound * @see setUpperBound */ - public void set(List data, Range range) { + public final void set(List data, Range range) { var segment = range.getSegment(); - setLowerBound(data, Math.max(0.0, segment.getMinimum())); + setLowerBound(data, segment.getMinimum()); setUpperBound(data, segment.getMaximum()); } @@ -116,7 +117,7 @@ public void set(List data, Range range) { * * @return the start index */ - public int getLowerBound() { + public final int getLowerBound() { return iStart; } @@ -126,7 +127,7 @@ public int getLowerBound() { * * @return the end index */ - public int getUpperBound() { + public final int getUpperBound() { return iEnd; } diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index ab95fa08..fd6e4e94 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -1,6 +1,7 @@ package pulse.input; import static java.lang.Math.max; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -10,6 +11,8 @@ import java.util.List; import java.util.Set; +import pulse.DiscreteInput; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -30,7 +33,11 @@ public class Range extends PropertyHolder implements Optimisable { private Segment segment; - + + public final static Range UNLIMITED = new Range (Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + public final static Range NEGATIVE = new Range(Double.NEGATIVE_INFINITY, -1E-16); + public final static Range POSITIVE = new Range(1e-16, Double.POSITIVE_INFINITY); + /** * Constructs a {@code Range} from the minimum and maximum values of * {@code data}. @@ -53,6 +60,45 @@ public Range(List data) { public Range(double a, double b) { this.segment = new Segment(a, b); } + + /** + * Contains a data double array ([0] - x, [1] - y), + * where the data points have been filtered so that + * each x fits into this range. + * @param input + * @return a [2][...] array containing filtered x and y values + */ + + public List[] filter(DiscreteInput input) { + var x = input.getX(); + var y = input.getY(); + + if(x.size() != y.size()) { + throw new IllegalArgumentException("x.length != y.length"); + } + + var xf = new ArrayList(); + var yf = new ArrayList(); + + double min = segment.getMinimum(); + double max = segment.getMaximum(); + + final double eps = 1E-10; + + for(int i = 0, size = x.size(); i < size; i++) { + + if(x.get(i) > min && x.get(i) < max + eps) { + + xf.add(x.get(i)); + yf.add(y.get(i)); + + } + + } + + return new List[]{xf, yf}; + + } /** * Resets the minimum and maximum values of this range to those specified by @@ -181,7 +227,7 @@ public Segment boundLimits(boolean isUpperBound) { var curve = (ExperimentalData) this.getParent(); var seq = curve.getTimeSequence(); - double tHalf = curve.getHalfTime(); + double tHalf = curve.getHalfTimeCalculator().getHalfTime(); Segment result = null; if(isUpperBound) @@ -201,25 +247,25 @@ public Segment boundLimits(boolean isUpperBound) { * absolute constraints equal to a fourth of their values. * * @param output the vector to be updated - * @param flags a list of active flags */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds; - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); + double value; + switch (key) { case UPPER_BOUND: - output.set(i, segment.getMaximum()); bounds = boundLimits(true); + value = segment.getMaximum(); break; case LOWER_BOUND: - output.set(i, segment.getMinimum()); bounds = boundLimits(false); + value = segment.getMinimum(); break; default: continue; @@ -227,8 +273,9 @@ public void optimisationVector(ParameterVector output, List flags) { var transform = new StickTransform(bounds); - output.setParameterBounds(i, bounds); - output.setTransform(i, transform); + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); } @@ -242,18 +289,17 @@ public void optimisationVector(ParameterVector output, List flags) { */ @Override public void assign(ParameterVector params) throws SolverException { - NumericProperty p = null; - - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - p = derive( params.getIndex(i), params.inverseTransform(i) ); + var key = p.getIdentifier().getKeyword(); + var np = derive( key, p.inverseTransform() ); - switch (params.getIndex(i)) { + switch (key) { case UPPER_BOUND: - setUpperBound(p); + setUpperBound(np); break; case LOWER_BOUND: - setLowerBound(p); + setLowerBound(np); break; default: } @@ -267,4 +313,4 @@ public String toString() { return "Range given by: " + segment.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index 1de1e4b6..4430ec50 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -59,6 +59,7 @@ public Extension[] getSupportedExtensions() { private void printHTML(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); stream.print(getString("ResultTableExporter.style")); @@ -71,8 +72,8 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { stream.print(""); for (int i = 0; i < residualsLength; i++) { - double tr = residuals.get(i)[0]; - double Tr = residuals.get(i)[1]; + double tr = time.get(i); + double Tr = residuals.get(i); stream.printf("%n
", tr, Tr); } @@ -83,6 +84,7 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { private void printCSV(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); final String TIME_LABEL = getString("HeatingCurve.6"); @@ -90,9 +92,9 @@ private void printCSV(ResidualStatistic hc, FileOutputStream fos) { stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); double tr, Tr; for (int i = 0; i < residualsLength; i++) { - tr = residuals.get(i)[0]; + tr = time.get(i); stream.printf("%n%3.8f", tr); - Tr = residuals.get(i)[1]; + Tr = residuals.get(i); stream.printf("\t%3.8f", Tr); } } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 4445add9..85c4f1c8 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -47,23 +47,25 @@ public class NetzschCSVReader implements CurveReader { private final static String THICKNESS = "Thickness_RT"; private final static String DETECTOR_SPOT_SIZE = "Spotsize"; private final static String DIAMETER = "Diameter"; + private final static String L_PULSE_WIDTH = "Laser_pulse_width"; + private final static String PULSE_WIDTH = "Pulse_width"; /** * Note comma is included as a delimiter character here. */ private final static String ENGLISH_DELIMS = "[#(),;/°Cx%^]+"; private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; - + private static String delims; //default number format (British format) private static Locale locale; - + private static NumberFormat format; - + private NetzschCSVReader() { //do nothing } - + protected void setDefaultLocale() { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; @@ -105,26 +107,25 @@ public String getSupportedExtension() { public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); ExperimentalData curve = new ExperimentalData(); - + setDefaultLocale(); //always start with a default locale - - //gets the number format for this locale + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); - - String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); - + + String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); + double spotSize = 0; - if(spot != null) { + if (spot != null) { var spotTokens = spot.split(delims); spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; } - + String tempLine = findLineByLabel(reader, THICKNESS, false); var tempTokens = tempLine.split(delims); - + final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, DIAMETER, false).split(delims); @@ -133,6 +134,19 @@ public List read(File file) throws IOException { tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, false).split(delims); final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; + var line = findLineByLabel(reader, L_PULSE_WIDTH, DETECTOR, false); + if (line == null) { + line = findLineByLabel(reader, PULSE_WIDTH, DETECTOR, false); + } + + double pulseWidth = 0; + + if (line != null) { + tempTokens = line.split(delims); + pulseWidth = format.parse(tempTokens[tempTokens.length - 1]) + .doubleValue() * TO_SECONDS; + } + /* * Finds the detector keyword. */ @@ -147,6 +161,9 @@ public List read(File file) throws IOException { populate(curve, reader); var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + if (pulseWidth > 1e-10) { + met.set(NumericPropertyKeyword.PULSE_WIDTH, derive(NumericPropertyKeyword.PULSE_WIDTH, pulseWidth)); + } met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); @@ -154,7 +171,7 @@ public List read(File file) throws IOException { curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); - return new ArrayList<>(Arrays.asList(curve)); + return new ArrayList<>(Arrays.asList(curve)); } catch (ParseException ex) { Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); @@ -163,26 +180,24 @@ public List read(File file) throws IOException { return null; } - + /** * Note: the {@code line} must contain a decimal-separated number. - * @param line a line containing number with a decimal separator + * + * @param line a line containing number with a decimal separator */ - private static void guessLocaleAndFormat(String line) { - - if(line.contains(".")) { + + if (line.contains(".")) { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; - } - - else { + } else { delims = GERMAN_DELIMS; locale = Locale.GERMAN; } - + format = DecimalFormat.getInstance(locale); - format.setGroupingUsed(false); + format.setGroupingUsed(false); } protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { @@ -192,12 +207,12 @@ protected static void populate(AbstractData data, BufferedReader reader) throws for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - - if(tokens.length < 2) { + + if (tokens.length < 2) { guessLocaleAndFormat(line); tokens = line.split(delims); } - + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); @@ -210,7 +225,7 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO String[] shotID = shotIDLine.split(delims); int id; - + //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { throw new IllegalArgumentException(file.getName() @@ -226,37 +241,39 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO protected static String findLineByLabel(BufferedReader reader, String label, boolean ignoreLocale) throws IOException { return findLineByLabel(reader, label, "!!!", ignoreLocale); } - + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, boolean ignoreLocale) throws IOException { String line = ""; String[] tokens; reader.mark(1000); - + //find keyword outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { - if(line.isBlank()) + if (line.isBlank()) { continue; - - if(!ignoreLocale) + } + + if (!ignoreLocale) { guessLocaleAndFormat(line); + } tokens = line.split(delims); for (String token : tokens) { - + if (token.equalsIgnoreCase(label)) { break outer; } - - if(token.equalsIgnoreCase(stopLabel)) { + + if (token.equalsIgnoreCase(stopLabel)) { line = null; reader.reset(); break outer; } - + } } @@ -264,7 +281,7 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } - + /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. @@ -274,14 +291,14 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str public static CurveReader getInstance() { return instance; } - + /** * Get the standard delimiter chars. + * * @return delims */ - public static String getDelims() { return delims; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java new file mode 100644 index 00000000..7e5390de --- /dev/null +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -0,0 +1,136 @@ +package pulse.math; + +import org.apache.commons.math3.complex.Complex; +import org.apache.commons.math3.transform.DftNormalization; +import org.apache.commons.math3.transform.FastFourierTransformer; +import org.apache.commons.math3.transform.TransformType; + +public class FFTTransformer { + + private double[] amplitudeSpec; + private double[] phaseSpec; + + private int n; //number of input points + private Complex[] buffer; + + private Window window; + + public FFTTransformer(double[] realInput) { + this(Window.HANN, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput) { + this(window, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput, double[] imagInput) { + this.window = window; + n = realInput.length; + + if (realInput.length != imagInput.length) { + throw new IllegalArgumentException( + String.format("Invalid data array lengths: %5d and %5d", + realInput.length, imagInput.length)); + } + + //if the input array is a power of two, simply make a shallow copy of the input array + if (IsPowerOfTwo(realInput.length)) { + buffer = new Complex[realInput.length]; + fill(realInput, imagInput, realInput.length); + } else { + int pow2 = numBits(realInput.length); + int nextPowerOfTwo = (int) Math.pow(2, pow2 + 1); + int previousPowerOfTwo = (int) Math.pow(2, pow2); + + final double TOLERANCE_FACTOR = 0.25; + + /* + * if we cut the tails, do we end up removing less elements than the number + * of zeros we had to add to reach next power of two? + */ + if ((nextPowerOfTwo - realInput.length + > realInput.length - previousPowerOfTwo) + && //in this case, do we have to add too many zeros? + (nextPowerOfTwo - realInput.length + > TOLERANCE_FACTOR * realInput.length)) { + cutTails(realInput, imagInput, previousPowerOfTwo); + } else { + zeroPad(realInput, imagInput, nextPowerOfTwo); + } + + } + //create power and phase arrays + amplitudeSpec = new double[buffer.length / 2]; + phaseSpec = new double[buffer.length / 2]; + + } + + public double[] sampling(double[] x) { + final double totalTime = x[n - 2] - x[0]; + double[] sample = new double[buffer.length / 2]; + double fs = n/totalTime; //sampling rate + for (int i = 0; i < sample.length; i++) { + sample[i] = i * fs / buffer.length; + } + return sample; + } + + public void transform() { + FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD); + + Complex[] result = fft.transform(buffer, TransformType.FORWARD); + + final double _2_N = 2.0 / amplitudeSpec.length; + + amplitudeSpec[0] = result[0].abs() / amplitudeSpec.length; + phaseSpec[0] = result[0].getArgument(); + + for (int i = 1; i < amplitudeSpec.length; i++) { + amplitudeSpec[i] = _2_N * result[i].abs(); + phaseSpec[i] = result[i].getArgument(); + } + + } + + private void fill(double[] realInput, double[] imagInput, int size) { + for (int i = 0; i < size; i++) { + buffer[i] = new Complex( + window.evaluate(i, realInput.length) * realInput[i], + imagInput[i]); + } + } + + private void cutTails(double[] realInput, double[] imagInput, int previousPowerOfTwo) { + buffer = new Complex[previousPowerOfTwo]; + fill(realInput, imagInput, previousPowerOfTwo); + } + + private void zeroPad(double[] realInput, double[] imagInput, int nextPowerOfTwo) { + buffer = new Complex[nextPowerOfTwo]; + fill(realInput, imagInput, realInput.length); + for (int i = realInput.length; i < nextPowerOfTwo; i++) { + buffer[i] = new Complex(0.0, 0.0); + } + } + + /** + * Checks if the argument (positive integer) is a power of 2. Returns trues + * if the argument is zero. + */ + private static boolean IsPowerOfTwo(int x) { + return x > 0 && ((x & (x - 1)) == 0); + } + + private int numBits(int value) { + return (int) (Math.log(value) / Math.log(2)); + } + + public double[] getAmpltiudeSpectrum() { + return amplitudeSpec; + } + + public double[] getPhaseSpectrum() { + return phaseSpec; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/Harmonic.java b/src/main/java/pulse/math/Harmonic.java new file mode 100644 index 00000000..06e36043 --- /dev/null +++ b/src/main/java/pulse/math/Harmonic.java @@ -0,0 +1,266 @@ +package pulse.math; + +import static java.lang.Math.cos; +import java.util.Set; +import pulse.math.transforms.PeriodicTransform; +import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +/** + * + * Harmonic class. + *

+ * It is given by the expression y = y0 + + * A cos(2πf t + φ) , where f is the + * frequency (in Hz), A is the amplitude, φ is the phase shift. + *

+ * + * + */ +public class Harmonic extends PropertyHolder implements Optimisable, Comparable { + + private int rank = -1; + + private double amplitude; + private double frequency; + private double phaseShift; + + private final static double _2PI = 2.0 * Math.PI; + + public Harmonic() { + setFrequency(def(BASELINE_FREQUENCY)); + setAmplitude(def(BASELINE_AMPLITUDE)); + setPhaseShift(def(BASELINE_PHASE_SHIFT)); + } + + public Harmonic(double amplitude, double frequency, double phaseShift) { + this.amplitude = amplitude; + this.frequency = frequency; + this.phaseShift = phaseShift; + } + + public Harmonic(Harmonic h) { + this.amplitude = h.amplitude; + this.frequency = h.frequency; + this.phaseShift = h.phaseShift; + this.rank = h.rank; + } + + public NumericProperty getFrequency() { + return derive(BASELINE_FREQUENCY, frequency); + } + + public NumericProperty getAmplitude() { + return derive(BASELINE_AMPLITUDE, amplitude); + } + + public NumericProperty getPhaseShift() { + return derive(BASELINE_PHASE_SHIFT, phaseShift); + } + + public final void setFrequency(NumericProperty frequency) { + requireType(frequency, BASELINE_FREQUENCY); + this.frequency = (double) frequency.getValue(); + firePropertyChanged(this, frequency); + } + + public final void setAmplitude(NumericProperty amplitude) { + requireType(amplitude, BASELINE_AMPLITUDE); + this.amplitude = (double) amplitude.getValue(); + firePropertyChanged(this, amplitude); + } + + public final void setPhaseShift(NumericProperty phaseShift) { + requireType(phaseShift, BASELINE_PHASE_SHIFT); + this.phaseShift = (double) phaseShift.getValue(); + firePropertyChanged(this, phaseShift); + } + + /** + * Amplitude form of the Fourier harmonic + * + * @param x + * @return + */ + public double valueAt(double x) { + return amplitude * cos(_2PI * x * frequency + phaseShift); + } + + /** + * Listed properties include the frequency, amplitude, phase shift, and + * intercept. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_FREQUENCY); + set.add(BASELINE_AMPLITUDE); + set.add(BASELINE_PHASE_SHIFT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case BASELINE_FREQUENCY: + setFrequency(property); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(property); + break; + case BASELINE_AMPLITUDE: + setAmplitude(property); + break; + } + + } + + /** + * The optimisation vector can include the amplitude, frequency and phase + * shift of a sinusoid, and a baseline intercept value of the superclass. + */ + @Override + public void optimisationVector(ParameterVector output) { + + var params = output.getParameters(); + + for (int i = 0, size = params.size(); i < size; i++) { + + var p = params.get(i); + var id = p.getIdentifier(); + var bounds = Segment.boundsFrom(id.getKeyword()); + + double value; + + Transformable transform = null; + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + value = frequency; + transform = StandardTransformations.ABS; + break; + case BASELINE_PHASE_SHIFT: + value = phaseShift; + transform = new PeriodicTransform(bounds); + break; + case BASELINE_AMPLITUDE: + value = amplitude; + transform = new StickTransform(bounds); + break; + default: + continue; + } + + var newId = new ParameterIdentifier(id.getKeyword(), rank); + + if (id.getIndex() == rank) { + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); + } else if (rank > -1) { + + boolean matchFound = output.getParameters().stream().anyMatch(pp -> { + var key = pp.getIdentifier().getKeyword(); + int index = pp.getIdentifier().getIndex(); + return key == id.getKeyword() && rank == index; + }); + + if (!matchFound) { + + var newParam = new Parameter(newId, transform, bounds); + newParam.setValue(value); + params.add(newParam); + + } + + } + + } + + } + + @Override + public void assign(ParameterVector params) { + + for (Parameter p : params.getParameters()) { + + var id = p.getIdentifier(); + + if (id.getIndex() == rank) { + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + setFrequency(derive(BASELINE_FREQUENCY, p.inverseTransform())); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(derive(BASELINE_PHASE_SHIFT, p.inverseTransform())); + break; + case BASELINE_AMPLITUDE: + setAmplitude(derive(BASELINE_AMPLITUDE, p.inverseTransform())); + break; + default: + break; + } + + } + + } + + } + + public void setRank(int rank) { + this.rank = rank; + } + + public int getRank() { + return rank; + } + + public Harmonic increaseAmplitudeBy(int amplitudeFactor) { + var h = new Harmonic(this); + h.amplitude *= amplitudeFactor; + return h; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Harmonic)) { + return false; + } + + Harmonic oH = (Harmonic) o; + + final double tolerance = 1E-3; + + return Math.abs(oH.amplitude - this.amplitude) + / Math.abs(oH.amplitude + this.amplitude) < tolerance + && Math.abs(oH.frequency - this.frequency) + / Math.abs(oH.frequency + this.frequency) < tolerance + && Math.abs(oH.phaseShift - this.phaseShift) + / Math.abs(oH.phaseShift + this.phaseShift) < tolerance; + } + + @Override + public int compareTo(Harmonic o) { + return this.getAmplitude().compareTo(o.getAmplitude()); + } + + @Override + public String toString() { + return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", + rank, frequency, amplitude, phaseShift); + } + +} diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java new file mode 100644 index 00000000..5556cd15 --- /dev/null +++ b/src/main/java/pulse/math/Parameter.java @@ -0,0 +1,91 @@ +package pulse.math; + +import pulse.math.transforms.Transformable; + +/** + * Parameter class + */ +public class Parameter { + + private ParameterIdentifier index; + private Transformable transform; + private Segment bound; + private double value; + + public Parameter(ParameterIdentifier index, Transformable transform, Segment bound) { + this.index = index; + this.transform = transform; + this.bound = bound; + } + + public Parameter(ParameterIdentifier index) { + if(index.getKeyword() != null) { + bound = Segment.boundsFrom(index.getKeyword()); + } + this.index = index; + } + + public Parameter(Parameter p) { + this.index = p.index; + this.transform = p.transform; + this.bound = p.bound; + this.value = p.value; + } + + public ParameterIdentifier getIdentifier() { + return index; + } + + public void setBounds(Segment bounds) { + this.bound = bounds; + } + + public Segment getBounds() { + return bound; + } + + /** + * If transform of {@code i} is not null, applies the transformation to the + * component bounds + * + * @param i the index of the component + * @return the transformed bounds + */ + public Segment getTransformedBounds() { + return transform != null + ? new Segment(transform.transform(bound.getMinimum()), + transform.transform(bound.getMaximum())) + : bound; + } + + public Transformable getTransform() { + return transform; + } + + public void setTransform(Transformable transform) { + this.transform = transform; + } + + public double inverseTransform() { + return transform != null ? transform.inverse(value) : value; + } + + public Parameter copy() { + return new Parameter(index, transform, bound); + } + + public double getApparentValue() { + return value; + } + + public void setValue(double value, boolean ignoreTransform) { + this.value = transform == null || ignoreTransform + ? value + : transform.transform(value); + } + + public void setValue(double value) { + setValue(value, false); + } + +} diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java new file mode 100644 index 00000000..b96847da --- /dev/null +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -0,0 +1,53 @@ +package pulse.math; + +import pulse.properties.NumericPropertyKeyword; + +public class ParameterIdentifier { + + private NumericPropertyKeyword keyword; + private int index; + + public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { + this.keyword = keyword; + this.index = index; + } + + public ParameterIdentifier(NumericPropertyKeyword keyword) { + this(keyword, 0); + } + + public ParameterIdentifier(int index) { + this.index = index; + } + + public NumericPropertyKeyword getKeyword() { + return keyword; + } + + public int getIndex() { + return index; + } + + @Override + public boolean equals(Object id) { + if(!id.getClass().equals(ParameterIdentifier.class)) { + return false; + } + + var pid = (ParameterIdentifier) id; + + boolean result = true; + + if(keyword != pid.keyword || index != pid.index) + result = false; + + return result; + + } + + @Override + public String toString() { + return keyword + " # " + index; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 4b852216..6bd17373 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,51 +1,50 @@ package pulse.math; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import pulse.math.linear.Vector; -import pulse.math.transforms.Transformable; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; /** - * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific + * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific * components of the vector. Used when constructing the optimisation vector. */ -public class ParameterVector extends Vector { +public class ParameterVector { - private NumericPropertyKeyword[] indices; - private Transformable[] transforms; - private Segment[] bounds; + private List params; /** * Constructs an {@code IndexedVector} with the specified list of keywords. * * @param indices a list of keywords */ - public ParameterVector(List indices) { - this(indices.size()); - assign(indices); + public ParameterVector(List indices) { + params = indices.stream().map(ind + -> new Parameter(ind)).collect(Collectors.toList()); } /** * Constructs an {@code IndexedVector} based on {@code v} and a list of * keyword {@code indices} * + * @param proto prototype vector * @param v the vector to be copied - * @param prototype the prototype of the parameter vector */ public ParameterVector(ParameterVector proto, Vector v) { - super(v); - this.indices = new NumericPropertyKeyword[proto.indices.length]; - System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); - this.bounds = new Segment[proto.bounds.length]; - System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); - this.transforms = new Transformable[proto.transforms.length]; - System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); - + params = new ArrayList<>(); + var protoParams = proto.params; + for (Parameter p : protoParams) { + var pp = new Parameter(p); //copy + pp.setValue(v.get( + protoParams.indexOf(p))); //set new value + params.add(pp); //add + } } /** @@ -54,198 +53,79 @@ public ParameterVector(ParameterVector proto, Vector v) { * @param v another vector */ public ParameterVector(ParameterVector v) { - this(v.dimension()); - final int n = dimension(); - for (int i = 0; i < n; i++) { - this.set(i, v.get(i)); + params = new ArrayList<>(v.params); + for (Parameter p : params) { + p.setValue(p.getApparentValue(), true); } - System.arraycopy(v.indices, 0, indices, 0, n); - System.arraycopy(v.transforms, 0, transforms, 0, n); - System.arraycopy(v.bounds, 0, bounds, 0, n); - } - - /** - * Creates an empty ParameterVector with a dimension of {@code n} - * - * @param n dimension - */ - private ParameterVector(final int n) { - super(n); - indices = new NumericPropertyKeyword[n]; - transforms = new Transformable[n]; - bounds = new Segment[n]; - } - - @Override - public void set(final int i, final double x) { - set(i, x, false); - } - - /** - * Sets the i-th parameter value to {@code x} without applying the - * transform. Sets the bound for this value as the default bound for {@code key}. - * @param i the index of the parameter - * @param x value to be set - * @param key type of property - */ - - public void set(final int i, final double x, NumericPropertyKeyword key) { - set(i, x); - setParameterBounds(i, Segment.boundsFrom(key)); - } - - /** - * Sets the i-component of this vector to {@code x} or - * its corresponding transform, if the latter is defined and - * {@code ignoreTransform} is {@code false}. - * - * @param i index of the value and its transform - * @param x the non-transformed value, which needs to be assigned to the - * i-th component - * @param ignoreTransform if {@code} false, will ignore exiting transform. - */ - public void set(final int i, final double x, boolean ignoreTransform) { - final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); - super.set(i, t); - } - - /** - * Retrieves the keyword associated with the {@code dataIndex} - * - * @param dataIndex an index pointing to a component of this vector - * @return a keyword describing this component - */ - public NumericPropertyKeyword getIndex(final int dataIndex) { - return indices[dataIndex]; - } - - /** - * Gets the data index that corresponds to the keyword {@code index} - * - * @param index a keyword-index of the component - * @return a numeric index associated with the original {@code Vector} - */ - private int indexOf(NumericPropertyKeyword index) { - return getIndices().indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - public double getParameterValue(NumericPropertyKeyword index) { - return super.get(indexOf(index)); } - /** - * Performs an inverse transform corresponding to the index {@code i} of - * this vector. - * - * @param i the index of the transform - * @return the inverse transform of {@code get(i) } if the transform is - * defined, {@code get(i)} otherwise. - */ - public double inverseTransform(final int i) { - return transforms[i] != null ? transforms[i].inverse(get(i)) : get(i); + public void add(Parameter p) { + params.add(p); } - /** - * Gets the transformable of the i-th component - * - * @param i index of the component - * @return the corresponding {@code Transforamble} - */ - public Transformable getTransform(final int i) { - return transforms[i]; - } - - public void setTransform(final int i, Transformable transformable) { - transforms[i] = transformable; - } - - public Segment getParameterBounds(final int i) { - return bounds[i]; - } - - /** - * If transform of {@code i} is not null, applies the transformation to the - * component bounds - * - * @param i the index of the component - * @return the transformed bounds - */ - public Segment getTransformedBounds(final int i) { - return transforms[i] != null - ? new Segment(transforms[i].transform(bounds[i].getMinimum()), - transforms[i].transform(bounds[i].getMaximum())) - : getParameterBounds(i); - } - - /** - * Sets the bounds of i-th component of this vector. - * - * @param i the index of the component - * @param segment new parameter bounds - */ - public void setParameterBounds(int i, Segment segment) { - bounds[i] = segment; - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - public List getIndices() { - return Arrays.asList(indices); - } - - /** - * This will assign a new list of indices to this vector - * - * @param indices a list of indices - */ - private void assign(List indices) { - this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); - bounds = new Segment[this.indices.length]; - transforms = new Transformable[this.indices.length]; + public double getParameterValue(NumericPropertyKeyword key, int index) { + return params.stream().filter(p -> { + var pid = p.getIdentifier(); + return pid.getKeyword() == key && pid.getIndex() == index; + } + ).findAny().get().getApparentValue(); } @Override public String toString() { var sb = new StringBuilder(); sb.append("Indices: "); - for (var key : indices) { - sb.append(key).append(" ; "); + for (var key : params) { + sb.append(key.getIdentifier()).append(" ; "); } sb.append(System.lineSeparator()); sb.append(" Values: ").append(super.toString()); return sb.toString(); } - + /** * Finds any elements of this vector which do not pass sanity checks. + * * @return a list of malformed numeric properties * @see pulse.properties.NumericProperties.isValueSensible() */ - public List findMalformedElements() { var list = new ArrayList(); - - for (int i = 0; i < dimension(); i++) { - var property = NumericProperties.derive(getIndex(i), inverseTransform(i)); - if (!property.validate()) { - list.add(property); - } + + params.stream().filter(p -> (p.getIdentifier().getKeyword() != null)) + .map(p -> NumericProperties.derive(p.getIdentifier().getKeyword(), + p.inverseTransform())) + .filter(property -> (!property.validate())) + .forEachOrdered(property -> { + list.add(property); + }); + + return list; + } + + public void setValues(Vector v) { + int dim = v.dimension(); + if (dim != this.dimension()) { + throw new IllegalArgumentException("Illegal vector dimension: " + + dim + " != " + this.dimension()); } - return list; + for(int i = 0; i < dim; i++) { + params.get(i).setValue(v.get(i)); + } + + } + + public int dimension() { + return params.size(); + } + + public List getParameters() { + return params; } - public Segment[] getBounds() { - return bounds; + public Vector toVector() { + return new Vector(params.stream().mapToDouble(p -> p.inverseTransform()).toArray()); } } diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index 9691ab34..c1e0c762 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -13,6 +13,8 @@ public class Segment { private double a; private double b; + + public final static Segment UNBOUNDED = new Segment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); /** * Creates a {@code Segment} bounded by {@code a} and {@code b}. diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java new file mode 100644 index 00000000..48fbd4ff --- /dev/null +++ b/src/main/java/pulse/math/Window.java @@ -0,0 +1,60 @@ +package pulse.math; + +public interface Window { + + public final static Window NONE = (n, N) -> 1.0; + public final static Window HANN = (n, N) -> Math.pow( Math.sin(Math.PI * n / ((double) N)), 2); + public final static Window HAMMING = (n, N) -> 0.54 + 0.46*Math.cos(2.0 * Math.PI * n / ((double) N)); + public final static Window BLACKMANN_HARRIS = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.35875 - 0.48829*Math.cos(x) + 0.14128*Math.cos(2.0*x) - 0.01168*Math.cos(3.0*x); + }; + public final static Window FLAT_TOP = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.21557895 - 0.41663158*Math.cos(x) + 0.277263158*Math.cos(2.0*x) + - 0.083578947*Math.cos(3.0*x) + 0.006947368 * Math.cos(4.0 * x); + }; + public final static Window TUKEY = new Window() { + + private final static double alpha = 0.6; + + @Override + public double evaluate(int n, int N) { + + double result = 0; + + if(n < 0.5*alpha*N) { + result = 0.5 * ( 1 - Math.cos(2.0*Math.PI*n/(alpha*N))); + } + + else if(n <= N/2) { + result = 1.0; + } + + else { + result = TUKEY.evaluate(N - n,N); + } + + return result; + + } + }; + + public final static Window HANN_POISSON = (n, N) -> { + + final double alpha = 2.0; + return HANN.evaluate(n, N) * Math.exp( - alpha * (N - 2 * n) / N); + + }; + + public default double[] apply(double[] input) { + double[] output = new double[input.length]; + for(int i = 0; i < output.length; i++) { + output[i] = input[i] * evaluate(i, input.length); + } + return output; + } + + public abstract double evaluate(int n, int N); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java new file mode 100644 index 00000000..fa13fad1 --- /dev/null +++ b/src/main/java/pulse/math/ZScore.java @@ -0,0 +1,134 @@ +package pulse.math; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; + +/** + * This class finds peaks in data using the Z-score algorithm: + * https://en.wikipedia.org/wiki/Standard_score This splits the data into a + * number of population defined by the 'lag' number. A standard score is + * calculated as the difference of the current value and population mean divided + * by the population standard deviation. + */ + +public class ZScore { + + private double[] avgFilter; + private double[] stdFilter; + private int[] signals; + + private int lag; + private double threshold; + private double influence; + + public ZScore(int lag, double threshold, double influence) { + this.lag = lag; + this.threshold = threshold; + this.influence = influence; + } + + public ZScore() { + this(40, 3.5, 0.3); + } + + public void process(double[] input) { + + signals = new int[input.length]; + List filteredY = DoubleStream.of(input).boxed().collect(Collectors.toList()); + + var initialWindow = filteredY.subList(input.length - lag, input.length - 1); + + avgFilter = new double[input.length]; + stdFilter = new double[input.length]; + + avgFilter[input.length - lag + 1] = mean(initialWindow); + stdFilter[input.length - lag + 1] = stdev(initialWindow); + + for (int i = input.length - lag; i > 0; i--) { + + if (Math.abs(input[i] - avgFilter[i + 1]) > threshold * stdFilter[i + 1]) { + + signals[i] = (input[i] > avgFilter[i + 1]) ? 1 : -1; + filteredY.set(i, influence * input[i] + + (1 - influence) * filteredY.get(i + 1)); + + } else { + + signals[i] = 0; + filteredY.set(i, input[i]); + + } + + // Update rolling average and deviation + var slidingWindow = filteredY.subList(i, i + lag - 1); + + avgFilter[i] = mean(slidingWindow); + stdFilter[i] = stdev(slidingWindow); + } + + } + + private static double mean(List list) { + return list.stream().mapToDouble(d -> d).average().getAsDouble(); + } + + private static double stdev(List values) { + double ret = 0; + int size = values.size(); + if (size > 0) { + double avg = mean(values); + double sum = values.stream().mapToDouble(d -> Math.pow(d - avg, 2)).sum(); + ret = Math.sqrt(sum / (size - 1)); + } + return ret; + } + + public int[] getSignals() { + return signals; + } + + public double[] getFilteredAverage() { + return avgFilter; + } + + public double[] getFilteredStdev() { + return stdFilter; + } + + /* + public static void main(String[] args) { + Scanner sc = null; + try { + sc = new Scanner(new File("fft.txt")); + } catch (FileNotFoundException ex) { + Logger.getLogger(ZScore.class.getName()).log(Level.SEVERE, null, ex); + } + + // we just need to use \\Z as delimiter + sc.useDelimiter("\\n"); + + var list = new ArrayList(); + + while(sc.hasNext()) { + list.add(sc.nextDouble()); + } + + var zscore = new ZScore(); + zscore.process(list.stream().mapToDouble(d -> d).toArray()); + var signals = zscore.getSignals(); + + for(int i = 0; i < signals.length; i++) { + System.out.println(list.get(i) + " " + signals[i]); + } + + } + */ + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/AssignmentListener.java b/src/main/java/pulse/math/filters/AssignmentListener.java new file mode 100644 index 00000000..251884a3 --- /dev/null +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -0,0 +1,7 @@ +package pulse.math.filters; + +public interface AssignmentListener { + + public void onValueAssigned(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/Filter.java b/src/main/java/pulse/math/filters/Filter.java new file mode 100644 index 00000000..067ab3e6 --- /dev/null +++ b/src/main/java/pulse/math/filters/Filter.java @@ -0,0 +1,14 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public interface Filter { + + public List process(List input); + public default List process(DiscreteInput input) { + return process(DiscreteInput.convert(input.getX(), input.getY())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/HalfTimeCalculator.java b/src/main/java/pulse/math/filters/HalfTimeCalculator.java new file mode 100644 index 00000000..e2ca9ce1 --- /dev/null +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -0,0 +1,95 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import static java.lang.Double.valueOf; +import static java.util.Collections.max; +import java.util.Comparator; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.baseline.FlatBaseline; +import pulse.input.IndexRange; + +public class HalfTimeCalculator { + + private final Filter filter; + private final DiscreteInput data; + private Point2D max; + private double halfTime; + + /** + * A fail-safe factor. + */ + public final static double FAIL_SAFE_FACTOR = 10.0; + + private static final Comparator pointComparator = + (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + + public HalfTimeCalculator(DiscreteInput input) { + this.data = input; + this.filter = new RunningAverage(); + } + + /** + * Calculates the approximate half-rise time used for crude estimation of + * thermal diffusivity. + *

+ * This uses the {@code runningAverage} method by applying the default + * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based + * on finding the approximate value corresponding to the half-maximum of the + * temperature. The latter is calculated using the running average curve. + * The index corresponding to the closest temperature value available for + * that curve is used to retrieve the half-rise time (which also has the + * same index). If this fails, i.e. the associated index is less than 1, + * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe factor + * {@value FAIL_SAFE_FACTOR}. + *

+ * @see getHalfTime() + */ + public void calculate() { + var baseline = new FlatBaseline(); + baseline.fitTo(data); + + var filtered = filter.process(data); + + max = max(filtered, pointComparator); + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + + int indexLeft = IndexRange.closestLeft(halfMax, + filtered.stream().map(point -> point.getY()) + .collect(Collectors.toList())); + + if (indexLeft < 1 || indexLeft > filtered.size() - 2) { + halfTime = filtered.get(filtered.size() - 1).getX() / FAIL_SAFE_FACTOR; + } + else { + //extrapolate + Point2D p1 = filtered.get(indexLeft); + Point2D p2 = filtered.get(indexLeft + 1); + + halfTime = (halfMax - p1.getY())/(p2.getY() - p1.getY()) + *(p2.getX() - p1.getX()) + p1.getX(); + } + + } + + + /** + * Retrieves the half-time value of this dataset, which is equal to the + * time needed to reach half of the signal maximum. + * @return the half-time value. + */ + + public final double getHalfTime() { + return halfTime; + } + + public final Point2D getFilteredMaximum() { + return max; + } + + public DiscreteInput getData() { + return data; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisablePolyline.java b/src/main/java/pulse/math/filters/OptimisablePolyline.java new file mode 100644 index 00000000..4d7a8d6a --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -0,0 +1,62 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +public class OptimisablePolyline extends PropertyHolder implements Optimisable { + + private final double[] x; + private final double[] y; + private final List listeners; + + public OptimisablePolyline(List data) { + x = data.stream().mapToDouble(d -> d.getX()).toArray(); + y = data.stream().mapToDouble(d -> d.getY()).toArray(); + listeners = new ArrayList<>(); + } + + @Override + public void assign(ParameterVector input) throws SolverException { + var ps = input.getParameters(); + for(int i = 0, size = ps.size(); i < size; i++) { + y[i] = ps.get(i).getApparentValue(); + } + listeners.stream().forEach(l -> l.onValueAssigned()); + } + + @Override + public void optimisationVector(ParameterVector output) { + output.setValues(new Vector(y)); + } + + public List points() { + return DiscreteInput.convert(x, y); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + public double[] getX() { + return x; + } + + public double[] getY() { + return y; + } + + public void addAssignmentListener(AssignmentListener listener) { + listeners.add(listener); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java new file mode 100644 index 00000000..46c004bb --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public class OptimisedRunningAverage extends RunningAverage { + + public OptimisedRunningAverage() { + super(); + } + + public OptimisedRunningAverage(int reductionFactor) { + super(reductionFactor); + } + + @Override + public List process(DiscreteInput input) { + var p = super.process(input); + var optimisableCurve = new OptimisablePolyline(p); + var task = new PolylineOptimiser(input, optimisableCurve); + task.run(); + return optimisableCurve.points(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java new file mode 100644 index 00000000..06246988 --- /dev/null +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -0,0 +1,90 @@ +package pulse.math.filters; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.linear.Vector; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.BFGSOptimiser; +import pulse.search.statistics.AbsoluteDeviations; +import pulse.search.statistics.OptimiserStatistic; + +public class PolylineOptimiser extends SimpleOptimisationTask { + + private final OptimiserStatistic sos; + private final PolylineResponse response; + private final OptimisablePolyline optimisableCurve; + + public PolylineOptimiser(DiscreteInput di, OptimisablePolyline optimisableCurve) { + super(optimisableCurve, di); + this.sos = new AbsoluteDeviations() { + + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + this.optimisableCurve = optimisableCurve; + response = new PolylineResponse(sos); + optimisableCurve.addAssignmentListener(() -> response.update(optimisableCurve)); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(BFGSOptimiser.getInstance()); + } + + @Override + public Response getResponse() { + return response; + } + + @Override + public ParameterVector searchVector() { + var y = optimisableCurve.getY(); + List ids + = IntStream.range(0, optimisableCurve.getX().length).sequential() + .mapToObj(i -> new ParameterIdentifier(i)) + .collect(Collectors.toList()); + var pv = new ParameterVector(ids); + pv.setValues(new Vector(y)); + var pvParams = pv.getParameters(); + for (int i = 0; i < pv.dimension(); i++) { + pvParams.get(i).setBounds(new Segment(y[i] - 2, y[i] + 2)); + } + return pv; + } + + public class PolylineResponse extends SimpleResponse { + + UnivariateInterpolator interp; + UnivariateFunction func; + + public PolylineResponse(OptimiserStatistic os) { + super(os); + } + + public void update(OptimisablePolyline impl) { + interp = new SplineInterpolator(); + func = interp.interpolate(impl.getX(), impl.getY()); + } + + @Override + public double evaluate(double t) { + return func.value(t); + } + } + +} diff --git a/src/main/java/pulse/math/filters/Randomiser.java b/src/main/java/pulse/math/filters/Randomiser.java new file mode 100644 index 00000000..3d15453e --- /dev/null +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; + +public class Randomiser implements Filter { + + private final double amplitude; + + public Randomiser(double amplitude) { + this.amplitude = amplitude; + } + + @Override + public List process(List input) { + input.forEach(p -> + ((Point2D.Double)p).y += (Math.random() - 0.5) * amplitude + ); + return input; + } + + public double getAmplitude() { + return amplitude; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/RunningAverage.java b/src/main/java/pulse/math/filters/RunningAverage.java new file mode 100644 index 00000000..37a08a20 --- /dev/null +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -0,0 +1,131 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; + +public class RunningAverage implements Filter { + + private int bins; + + /** + * The binning factor used to build a crude approximation of the heating + * curve. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + + public static final int DEFAULT_BINS = 16; + public final static int MIN_BINS = 4; + + /** + * @param reductionFactor the factor, by which the number of points + * {@code count} will be reduced for this {@code ExperimentalData}. + */ + + public RunningAverage(int reductionFactor) { + this.bins = reductionFactor; + } + + public RunningAverage() { + this.bins = DEFAULT_BINS; + } + + /** + * Constructs a deliberately crude representation of this heating curve by + * calculating a running average. + *

+ * This is done using a binning algorithm, which will group the + * time-temperature data associated with this {@code ExperimentalData} in + * {@code count/reductionFactor - 1} bins, calculate the average value for + * time and temperature within each bin, and collect those values in a + * {@code List}. This is useful to cancel out the effect of signal + * outliers, e.g. when calculating the half-rise time. + *

+ * + * The algorithm is described in more detail in Lunev, A., & Heymer, R. + * (2020). Review of Scientific Instruments, 91(6), 064902. + * + * @param points + * @param input + * @return a {@code List}, representing the degraded + * {@code ExperimentalData}. + * @see halfRiseTime() + * @see pulse.AbstractData.maxTemperature() + */ + + @Override + public List process(List points) { + var x = points.stream().mapToDouble(p -> p.getX()).toArray(); + var y = points.stream().mapToDouble(p -> p.getY()).toArray(); + + int size = x.length; + int step = size / bins; + List movingAverage = new ArrayList<>(bins); + + for (int i = 0; i < bins; i++) { + int i1 = step*i; + int i2 = step*(i+1); + + double av = 0; + int j; + + for (j = i1; j < i2 && j < size; j++) { + av += y[j]; + } + + av /= j - i1; + i2 = j - 1; + + movingAverage.add(new Point2D.Double( + (x[i1] + x[i2])/ 2.0, av)); + + } + + addBoundaryPoints(movingAverage, x[0], x[size - 1]); + + /* + for(int i = 0; i < movingAverage.size(); i++) { + System.err.println(movingAverage.get(i)); + } + */ + + return movingAverage; + + } + + private static void addBoundaryPoints(List d, double minTime, double maxTime) { + int max = d.size(); + + d.add( + extrapolate(d.get(max - 1), + d.get(max - 2), + maxTime) + ); + + d.add( 0, + extrapolate(d.get(0), + d.get(1), + minTime) + ); + + } + + private static Point2D extrapolate(Point2D a, Point2D b, double x) { + double y1 = a.getY(); + double y2 = b.getY(); + double x1 = a.getX(); + double x2 = b.getX(); + + return new Point2D.Double(x, y1 + (x - x1)/(x2 - x1)*(y2 - y1)); + } + + public final int getNumberOfBins() { + return bins; + } + + public final void setNumberOfBins(int no) { + this.bins = no > MIN_BINS - 1 ? no : MIN_BINS; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 3b493829..39d06531 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -2,6 +2,7 @@ import static java.lang.Math.abs; import static java.lang.Math.sqrt; +import java.util.List; import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; import static pulse.math.linear.ArithmeticOperations.DIFF_SQUARED; import static pulse.math.linear.ArithmeticOperations.PRODUCT; @@ -63,7 +64,7 @@ public Vector inverted() { * * @return the integer dimension */ - public int dimension() { + public final int dimension() { return x.length; } @@ -121,7 +122,7 @@ public static Vector random(int n, double min, double max) { } return v; } - + /** * Component-wise vector multiplication */ diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java deleted file mode 100644 index 8b512871..00000000 --- a/src/main/java/pulse/math/transforms/InvLenSqTransform.java +++ /dev/null @@ -1,27 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the squared length of the - * sample. - */ -public class InvLenSqTransform implements Transformable { - - private double l; - - public InvLenSqTransform(ThermalProperties tp) { - this.l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return Math.abs(value) / (l * l); - } - - @Override - public double inverse(double t) { - return Math.abs(t) * (l * l); - } - -} diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java deleted file mode 100644 index 571bdd81..00000000 --- a/src/main/java/pulse/math/transforms/InvLenTransform.java +++ /dev/null @@ -1,26 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the length of the sample. - */ -public class InvLenTransform implements Transformable { - - private double l; - - public InvLenTransform(ThermalProperties tp) { - l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return value / l; - } - - @Override - public double inverse(double t) { - return t * l; - } - -} diff --git a/src/main/java/pulse/math/transforms/PeriodicTransform.java b/src/main/java/pulse/math/transforms/PeriodicTransform.java new file mode 100644 index 00000000..31cee068 --- /dev/null +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -0,0 +1,38 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +public class PeriodicTransform extends BoundedParameterTransform { + + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public PeriodicTransform(Segment bounds) { + super(bounds); + } + + /** + * @param a + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + double max = getBounds().getMaximum(); + double min = getBounds().getMinimum(); + double len = max - min; + + return a > max ? transform(a - len) : (a < min ? transform(a + len) : a); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return t; + } +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index a8206fdb..c5b95034 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,6 +19,9 @@ public class StandardTransformations { @Override public double transform(double a) { + if(a < 0) { + System.err.println(a); + } return log(a); } diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index 00239cbf..f5487615 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -15,8 +15,6 @@ */ package pulse.math.transforms; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import pulse.math.Segment; /** diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index b27e4eb9..e1a283f6 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,8 +7,6 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import pulse.tasks.SearchTask; /** @@ -36,7 +34,7 @@ public class DiscretePulse { * tc * is the time factor defined in the {@code Problem} class. */ - private final static int WIDTH_TOLERANCE_FACTOR = 1000; + private final static int WIDTH_TOLERANCE_FACTOR = 10000; /** * This creates a one-dimensional discrete pulse on a {@code grid}. @@ -58,7 +56,8 @@ public DiscretePulse(Problem problem, Grid grid) { = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), "Problem has not been assigned to a SearchTask"); - ExperimentalData data = ((SearchTask) ancestor).getExperimentalCurve(); + ExperimentalData data = + (ExperimentalData) ( ((SearchTask) ancestor).getInput() ); init(data); pulse.addListener(e -> { @@ -119,6 +118,8 @@ public final void recalculate() { setDiscreteWidth(nominalWidth); } + invTotalEnergy = 1.0/totalEnergy(); + } /** diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 265ac2a5..e834f024 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -13,6 +13,7 @@ import pulse.tasks.SearchTask; import pulse.baseline.FlatBaseline; +import pulse.tasks.Calculation; /** * A numeric pulse is given by a set of discrete {@code NumericPulseData} @@ -58,7 +59,8 @@ public void init(ExperimentalData data, DiscretePulse pulse) { baselineSubtractedFrom(data); //notify host pulse object of a new pulse width - var problem = ((SearchTask) data.getParent()).getCurrentCalculation().getProblem(); + var problem = ( (Calculation) ((SearchTask) data.getParent()) + .getResponse() ).getProblem(); setPulseWidthOf(problem); //convert to dimensionless time and interpolate @@ -77,7 +79,7 @@ private void baselineSubtractedFrom(ExperimentalData data) { //subtracts a horizontal baseline from the pulse data var baseline = new FlatBaseline(); - baseline.fitNegative(pulseData); + baseline.fitTo(pulseData); for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) pulseData.setSignalAt(i, diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index e23c023d..5a53fd0a 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -1,6 +1,10 @@ package pulse.problem.laser; +import java.util.List; import pulse.AbstractData; +import pulse.DiscreteInput; +import pulse.input.IndexRange; +import pulse.input.Range; /** * An instance of the {@code AbstractData} class, which also declares an @@ -8,7 +12,7 @@ * measurement imported from an external source. * */ -public class NumericPulseData extends AbstractData { +public class NumericPulseData extends AbstractData implements DiscreteInput { private final int externalID; @@ -55,4 +59,19 @@ public double pulseWidth() { return super.timeLimit(); } + @Override + public List getX() { + return getTimeSequence(); + } + + @Override + public List getY() { + return getSignalData(); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(this.getTimeSequence(), Range.UNLIMITED); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 879a51ce..f4577d98 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -6,6 +6,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; @@ -58,7 +59,8 @@ public final RTECalculationStatus getCalculationStatus() { public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; if (calculationStatus != RTECalculationStatus.NORMAL) { - throw new SolverException(calculationStatus.toString()); + throw new SolverException(calculationStatus.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 64e33e1b..59c92208 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -12,8 +12,6 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; -import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,11 +91,8 @@ public void copyFrom(DifferenceScheme df) { protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - } - else { - discretePulse.recalculate(); - } - + } + discretePulse.recalculate(); clearArrays(); } diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index f2c3a1ac..1c359a4c 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -3,6 +3,7 @@ import static java.lang.Math.abs; import java.util.Arrays; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.FINITE_DIFFERENCE_ERROR; /** * @see Wiki @@ -13,14 +14,15 @@ public interface FixedPointIterations { /** * Performs iterations until the convergence criterion is satisfied.The - latter consists in having a difference two consequent iterations of V - less than the specified error. At the end of each iteration, calls - {@code finaliseIteration()}. + * latter consists in having a difference two consequent iterations of V + * less than the specified error. At the end of each iteration, calls + * {@code finaliseIteration()}. * * @param V the calculation array * @param error used in the convergence criterion * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed * @see finaliseIteration() * @see iteration() */ @@ -28,8 +30,9 @@ public default void doIterations(double[] V, final double error, final int m) th final int N = V.length - 1; - for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error - || abs(V[N] - V_N) > error; finaliseIteration(V)) { + for (double V_0 = error + 1, V_N = error + 1; + abs(V[0] - V_0)/abs(V[0] + V_0 + 1e-16) > error + || abs(V[N] - V_N)/abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { V_N = V[N]; V_0 = V[0]; @@ -42,7 +45,8 @@ public default void doIterations(double[] V, final double error, final int m) th * Performs an iteration at time {@code m} * * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public void iteration(final int m) throws SolverException; @@ -50,13 +54,16 @@ public default void doIterations(double[] V, final double error, final int m) th * Finalises the current iteration.By default, does nothing. * * @param V the current iteration - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public default void finaliseIteration(double[] V) throws SolverException { final double threshold = 1E6; double sum = Arrays.stream(V).sum(); - if( sum > threshold || !Double.isFinite(sum) ) - throw new SolverException("Invalid solution values in V array"); + if (sum > threshold || !Double.isFinite(sum)) { + throw new SolverException("Invalid solution values in V array", + FINITE_DIFFERENCE_ERROR); + } } } diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index 8d852846..217cb285 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -77,14 +77,14 @@ protected void prepare(Problem problem) throws SolverException { @Override public void timeStep(final int m) throws SolverException { - leftBoundary(); + leftBoundary(m); final var V = getCurrentSolution(); final int N = V.length - 1; setSolutionAt(N, evalRightBoundary(tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); tridiagonal.sweep(V); } - public void leftBoundary() { + public void leftBoundary(int m) { tridiagonal.setBeta(1, firstBeta()); tridiagonal.evaluateBeta(getPreviousSolution()); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 88c7c658..9d98c309 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -3,7 +3,7 @@ /** * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving * systems of linear equations. Applicable to such systems where the forming - * matrix has a tridiagonal form. + * matrix has a tridiagonal form: Ai*xi-1 - Bi xi + Ci xi+1 = -Fi. * */ public class TridiagonalMatrixAlgorithm { diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index d50b442c..39e504c1 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -9,6 +9,7 @@ import pulse.problem.schemes.RadiativeTransferCoupling; import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -147,7 +148,8 @@ public Class[] domain() { public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 98c3dbb0..e9e4f603 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -10,6 +10,7 @@ import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; @@ -103,7 +104,8 @@ public void solve(ParticipatingMedium problem) throws SolverException { var status = getCalculationStatus(); if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 920e4fbd..5343958c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -69,10 +69,10 @@ public void prepare(Problem problem) throws SolverException { } @Override - public void leftBoundary() { + public void leftBoundary(int m) { var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); tridiagonal.setGamma(1, -zN_1 / z0); - super.leftBoundary(); + super.leftBoundary(m); } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 5020d2c2..16a80d11 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -42,17 +42,19 @@ * both the heat equation and the boundary conditions. *

* + * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ -public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { - - private double Bi1HTAU; +public class ImplicitLinearisedSolver extends ImplicitScheme + implements Solver { private int N; - private double tau; - - private double HH; - private double _2HTAU; + + protected double Bi1HTAU; + protected double tau; + protected double HH; + protected double _2HTAU; + private double zeta; public ImplicitLinearisedSolver() { diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java new file mode 100644 index 00000000..1a09949e --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -0,0 +1,226 @@ +package pulse.problem.schemes.solvers; + +import java.util.Set; +import static pulse.problem.schemes.DistributedDetection.evaluateSignal; +import static pulse.problem.statements.model.SpectralRange.LASER; +import static pulse.ui.Messages.getString; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.schemes.TridiagonalMatrixAlgorithm; +import pulse.problem.statements.Problem; +import pulse.problem.statements.TwoTemperatureModel; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.TwoTemperatureProperties; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; + +public class ImplicitTwoTemperatureSolver extends ImplicitScheme + implements Solver, FixedPointIterations { + + private AbsorptionModel absorption; + private TridiagonalMatrixAlgorithm gasSolver; + + private int N; + private double hBi; + private double hBiPrime; + private double HH; + private double tau; + private double _05HH_TAU; + + private double[] gasTemp; + + private double diffRatio; + private double g; + private double gPrime; + + private double nonlinearPrecision; + + public ImplicitTwoTemperatureSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitTwoTemperatureSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + private void initSolidPart() { + var grid = getGrid(); + final double hx = grid.getXStep(); + + var solid = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return getCurrentPulseValue() * absorption.absorption(LASER, i * hx) + + g * gasTemp[i]; + } + + }; + + solid.setCoefA(1.0 / HH); + solid.setCoefB(1.0 / tau + 2.0 / HH + g); + solid.setCoefC(1.0 / HH); + + solid.setAlpha(1, 1.0 / (1.0 + hBi + _05HH_TAU + 0.5 * HH * g)); + solid.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(solid); + } + + private void initGasPart() { + var grid = getGrid(); + + gasTemp = new double[N + 1]; + var solidTemp = this.getCurrentSolution(); + + gasSolver = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return gPrime * solidTemp[i]; + } + + @Override + public void evaluateAlpha() { + setAlpha(1, 1.0 / (1.0 + hBiPrime + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime))); + super.evaluateAlpha(); + } + + @Override + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + setBeta(1, diffRatio * (0.5 * HH * phi(0) + _05HH_TAU * U[0]) * getAlpha()[1]); + super.evaluateBeta(U, start, endExclusive); + } + + }; + + double invDiffRatio = 1.0 / diffRatio; + gasSolver.setCoefA(invDiffRatio / HH); + gasSolver.setCoefB(1.0 / tau + gPrime + 2.0 / HH * invDiffRatio); + gasSolver.setCoefC(invDiffRatio / HH); + + gasSolver.evaluateAlpha(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + if (!(problem instanceof TwoTemperatureModel)) { + throw new IllegalArgumentException("Illegal model type"); + } + + super.prepare(problem); + var model = (TwoTemperatureModel) problem; + var ttp = (TwoTemperatureProperties) model.getProperties(); + + double hx = getGrid().getXStep(); + tau = getGrid().getTimeStep(); + N = (int) getGrid().getGridDensity().getValue(); + + HH = hx * hx; + _05HH_TAU = 0.5 * HH / tau; + hBi = (double) ttp.getHeatLoss().getValue() * hx; + hBiPrime = (double) ttp.getGasHeatLoss().getValue() * hx; + + g = (double) ttp.getSolidExchangeCoefficient().getValue(); + absorption = model.getAbsorptionModel(); + + diffRatio = model.diffusivityRatio(); + gPrime = (double) ttp.getGasExchangeCoefficient().getValue(); + + initGasPart(); + initSolidPart(); + } + + @Override + public void solve(TwoTemperatureModel problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(gasTemp, nonlinearPrecision, m); + } + + @Override + public void iteration(int m) throws SolverException { + //first solve for the solid + super.timeStep(m); + //then for the gas + gasSolver.evaluateBeta(gasTemp); + gasTemp[N] = (diffRatio * (0.5 * HH * gasSolver.phi(N) + _05HH_TAU * gasTemp[N]) + + gasSolver.getBeta()[N]) / (1.0 + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime) + + hBiPrime - gasSolver.getAlpha()[N]); + gasSolver.sweep(gasTemp); + } + + @Override + public double signal() { + return evaluateSignal(absorption, getGrid(), getCurrentSolution()); + } + + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[N] + 0.5 * HH * tridiagonal.phi(N) + betaN) + / (1.0 + _05HH_TAU + 0.5 * HH * g + hBi - alphaN); + } + + @Override + public double firstBeta() { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[0] + 0.5 * HH * tridiagonal.phi(0)) + * tridiagonal.getAlpha()[1]; + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitTwoTemperatureSolver(grid.getGridDensity(), + grid.getTimeFactor(), getTimeLimit()); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + @Override + public Class[] domain() { + return new Class[]{TwoTemperatureModel.class}; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 28bddf45..8aacbe04 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -2,9 +2,28 @@ @SuppressWarnings("serial") public class SolverException extends Exception { + + private final SolverExceptionType type; - public SolverException(String status) { + public SolverException(String status, SolverExceptionType type) { super(status); + this.type = type; + } + + public SolverException(SolverExceptionType type) { + this(type.toString(), type); + } + + public SolverExceptionType getType() { + return type; + } + + public enum SolverExceptionType { + RTE_SOLVER_ERROR, + OPTIMISATION_ERROR, + OPTIMISATION_TIMEOUT, + FINITE_DIFFERENCE_ERROR, + ILLEGAL_PARAMETERS, } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 72796ccc..72439856 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,7 +1,7 @@ package pulse.problem.statements; -import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -95,19 +94,18 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - super.optimisationVector(output, flags); + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, bias); + p.setTransform(new StickTransform(bounds)); + p.setValue(bias); } } @@ -117,10 +115,10 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.get(i); - var key = params.getIndex(i); + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { setGeometricFactor(derive(SOURCE_GEOMETRIC_FACTOR, value)); diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 278fe8ea..74b74e0f 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -19,7 +19,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; @@ -69,14 +68,14 @@ public DiscretePulse discretePulseOn(Grid grid) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ExtendedThermalProperties) getProperties(); double value; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Transformable transform = new InvDiamTransform(properties); var bounds = Segment.boundsFrom(key); @@ -102,9 +101,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, transform); - output.setParameterBounds(i, bounds); - output.set(i, value); + p.setTransform(transform); + p.setBounds(bounds); + p.setValue(value); } @@ -116,20 +115,20 @@ public void assign(ParameterVector params) throws SolverException { var properties = (ExtendedThermalProperties) getProperties(); // TODO one-to-one mapping for FOV and SPOT_DIAMETER - for (int i = 0, size = params.dimension(); i < size; i++) { - var type = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var type = p.getIdentifier().getKeyword(); switch (type) { case FOV_OUTER: case FOV_INNER: case HEAT_LOSS_SIDE: case HEAT_LOSS_COMBINED: - properties.set(type, derive(type, params.inverseTransform(i))); + properties.set(type, derive(type, p.inverseTransform())); break; case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, params.inverseTransform(i))); + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + p.inverseTransform())); break; default: - continue; } } } diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 488eafdb..dc4bf9b0 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,7 +13,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.DiathermicProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import pulse.ui.Messages; @@ -34,7 +33,6 @@ */ public class DiathermicMedium extends ClassicalProblem { - public DiathermicMedium() { super(); } @@ -54,15 +52,15 @@ public void initProperties(ThermalProperties properties) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); - Segment bounds = null; - double value = 0; + var key = p.getIdentifier().getKeyword(); + Segment bounds; + double value; switch (key) { case DIATHERMIC_COEFFICIENT: @@ -83,9 +81,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, new StickTransform(bounds)); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); } @@ -96,17 +94,19 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var key = params.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + p.inverseTransform())); break; case HEAT_LOSS_CONVECTIVE: - properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, params.inverseTransform(i))); + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + p.inverseTransform())); break; default: } diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 28b2857e..f21b3b89 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,6 +1,5 @@ package pulse.problem.statements; -import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; @@ -11,13 +10,13 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; @@ -82,10 +81,10 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); getProperties().calculateEmissivity(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.inverseTransform(i); - NumericPropertyKeyword key = params.getIndex(i); + double value = p.inverseTransform(); + NumericPropertyKeyword key = p.getIdentifier().getKeyword(); if (key == LASER_ENERGY) { this.getPulse().setLaserEnergy(derive(key, value)); @@ -104,18 +103,18 @@ public void assign(ParameterVector params) throws SolverException { */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if(key == LASER_ENERGY) { var bounds = Segment.boundsFrom(LASER_ENERGY); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, (double) getPulse().getLaserEnergy().getValue()); + p.setBounds(bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue( (double) getPulse().getLaserEnergy().getValue()); } } @@ -132,4 +131,4 @@ public Problem copy() { return new NonlinearProblem(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index ad0d177b..e8497e23 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -33,10 +33,10 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ThermoOpticalProperties) getProperties(); - properties.optimisationVector(output, flags); + properties.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index b93e68ff..d08d17cf 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.AbsorptionModel; import pulse.problem.statements.model.BeerLambertAbsorption; -import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.properties.Property; @@ -21,20 +20,22 @@ public class PenetrationProblem extends ClassicalProblem { private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); + private AbsorptionModel absorption; public PenetrationProblem() { super(); instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); + absorption = instanceDescriptor.newInstance(AbsorptionModel.class); absorption.setParent(this); } public PenetrationProblem(PenetrationProblem p) { super(p); - instanceDescriptor.setSelectedDescriptor((String) p.getAbsorptionSelector().getValue()); + instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); - initAbsorption(); + this.absorption = p.getAbsorptionModel().copy(); + this.absorption.setParent(this); } private void initAbsorption() { @@ -70,9 +71,9 @@ public InstanceDescriptor getAbsorptionSelector() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - absorption.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + absorption.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index d2bde96c..f2285151 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -7,23 +7,25 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.HeatingCurve; import pulse.baseline.Baseline; +import pulse.baseline.FlatBaseline; import pulse.baseline.LinearBaseline; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -57,9 +59,9 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim private static boolean hideDetailedAdjustment = true; private ProblemComplexity complexity = ProblemComplexity.LOW; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Baseline Selector", Baseline.class); + "Baseline Selector", Baseline.class); /** * Creates a {@code Problem} with default parameters (as found in the .XML @@ -74,10 +76,8 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim protected Problem() { initProperties(); setHeatingCurve(new HeatingCurve()); - - instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); addListeners(); - initBaseline(); + instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); } /** @@ -88,13 +88,10 @@ protected Problem() { */ public Problem(Problem p) { initProperties(p.getProperties().copy()); - setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); curve.setNumPoints(p.getHeatingCurve().getNumPoints()); - - instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); + setBaseline(p.getBaseline()); addListeners(); - setBaseline( p.getBaseline().copy() ); } public abstract Problem copy(); @@ -133,7 +130,7 @@ private void addListeners() { public final List availableSolutions() { var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); return allSchemes.stream().filter(scheme -> scheme instanceof Solver) - .filter(s -> Arrays.asList(s.domain()).contains(this.getClass()) ) + .filter(s -> Arrays.asList(s.domain()).contains(this.getClass())) .collect(Collectors.toList()); } @@ -165,7 +162,7 @@ public final Pulse getPulse() { */ public final void setPulse(Pulse pulse) { this.pulse = pulse; - pulse.setParent(this); + this.pulse.setParent(this); } /** @@ -176,7 +173,7 @@ public final void setPulse(Pulse pulse) { * @param c the {@code ExperimentalData} object */ public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); // used to estimate the floor of the signal range + baseline.fitTo(c); estimateSignalRange(c); updateProperties(this, c.getMetadata()); properties.useTheoreticalEstimates(c); @@ -194,9 +191,11 @@ public void retrieveData(ExperimentalData c) { * @see pulse.input.ExperimentalData.maxTemperature() */ public void estimateSignalRange(ExperimentalData c) { - var maxPoint = c.maxAdjustedSignal(); - final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); - properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); + var maxPoint = c.getHalfTimeCalculator().getFilteredMaximum(); + var flatBaseline = new FlatBaseline(); + flatBaseline.fitTo(c); + final double signalSpan = maxPoint.getY() - flatBaseline.valueAt(maxPoint.getX()); + properties.setMaximumTemperature(derive(MAXTEMP, signalSpan)); } /** @@ -217,29 +216,25 @@ public void estimateSignalRange(ExperimentalData c) { * class, or putting them in the XML file */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - baseline.optimisationVector(output, flags); + baseline.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Segment bounds = Segment.boundsFrom(key); - double value = 0; - + double value; + switch (key) { case THICKNESS: value = (double) properties.getSampleThickness().getValue(); break; case DIFFUSIVITY: - final double a = (double) properties.getDiffusivity().getValue(); - output.setTransform(i, new InvLenSqTransform(properties)); - bounds = new Segment(0.01 * a, 20.0 * a); - output.setParameterBounds(i, bounds); - output.set(i, a); - //custom transform here -- skip assigning StickTransform - continue; + value = (double) properties.getDiffusivity().getValue(); + bounds = new Segment(0.01 * value, 20.0 * value); + break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); bounds = new Segment(0.5 * signalHeight, 1.5 * signalHeight); @@ -247,7 +242,7 @@ public void optimisationVector(ParameterVector output, List flags) { break; case HEAT_LOSS: value = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, new StickTransform(bounds)); + p.setTransform(new StickTransform(bounds)); break; case TIME_SHIFT: double magnitude = 0.25 * properties.timeFactor(); @@ -257,11 +252,11 @@ public void optimisationVector(ParameterVector output, List flags) { default: continue; } - - output.setTransform(i, new StickTransform(bounds)); - output.setParameterBounds(i, bounds); - output.set(i, value); - + + p.setTransform(new StickTransform(bounds)); + p.setBounds(bounds); + p.setValue(value); + } } @@ -276,25 +271,25 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { baseline.assign(params); - + List malformedList = params.findMalformedElements(); - - if(!malformedList.isEmpty()) { + + if (!malformedList.isEmpty()) { StringBuilder sb = new StringBuilder("Cannot assign values: "); - malformedList.forEach(p -> - sb.append(String.format("%n %-25s", p.toString())) + malformedList.forEach(p + -> sb.append(String.format("%n %-25s", p.toString())) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } - - for (int i = 0, size = params.dimension(); i < size; i++) { - double value = params.inverseTransform(i); - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); switch (key) { case THICKNESS: - properties.setSampleThickness(derive(THICKNESS, value )); + properties.setSampleThickness(derive(THICKNESS, value)); break; case DIFFUSIVITY: properties.setDiffusivity(derive(DIFFUSIVITY, value)); @@ -412,16 +407,10 @@ public Baseline getBaseline() { * @see pulse.baseline.Baseline.apply(Baseline) */ public final void setBaseline(Baseline baseline) { - this.baseline = baseline; - curve.apply(baseline); - - baseline.setParent(this); - - var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); - if (searchTask != null) { - var experimentalData = searchTask.getExperimentalCurve(); - baseline.fitTo(experimentalData); - } + instanceDescriptor.setSelectedDescriptor(baseline.getClass().getSimpleName()); + this.baseline = baseline.copy(); + this.baseline.setParent(this); + curve.apply(this.baseline); } public final InstanceDescriptor getBaselineDescriptor() { @@ -429,8 +418,14 @@ public final InstanceDescriptor getBaselineDescriptor() { } private void initBaseline() { - var baseline = instanceDescriptor.newInstance(Baseline.class); - setBaseline(baseline); + setBaseline(instanceDescriptor.newInstance(Baseline.class)); + var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); + if (searchTask != null) { + var experimentalData = (ExperimentalData) searchTask.getInput(); + Executors.newSingleThreadExecutor().submit(() + -> baseline.fitTo(experimentalData) + ); + } parameterListChanged(); } @@ -451,4 +446,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index d2b53249..1b0fbf34 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -7,6 +7,7 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import java.util.List; +import java.util.Objects; import java.util.Set; import pulse.input.ExperimentalData; @@ -92,13 +93,12 @@ private void addListeners() { .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); - var range = corrTask.getExperimentalCurve().getRange(); + var range = ( (ExperimentalData) corrTask.getInput() ).getRange(); if( range.getLowerBound().compareTo(pw) < 0 ) { //update lower bound of the range for that SearchTask - corrTask.getExperimentalCurve().getRange() - .setLowerBound(pw); + range.setLowerBound(pw); } @@ -141,9 +141,9 @@ public void setPulseWidth(NumericProperty pulseWidth) { //validate -- do not update if the new pulse width is greater than 2 half-times SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); - ExperimentalData data = task.getExperimentalCurve(); + ExperimentalData data = (ExperimentalData) task.getInput(); - if(newValue < 2.0 * data.getHalfTime()) { + if(newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { this.pulseWidth = (double) pulseWidth.getValue(); firePropertyChanged(this, pulseWidth); } diff --git a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java new file mode 100644 index 00000000..72298d72 --- /dev/null +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -0,0 +1,176 @@ +package pulse.problem.statements; + +import java.util.List; +import pulse.math.Parameter; +import static pulse.properties.NumericProperties.derive; + +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.solvers.ImplicitTwoTemperatureSolver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.Gas; +import pulse.problem.statements.model.Helium; +import pulse.problem.statements.model.ThermalProperties; +import pulse.problem.statements.model.TwoTemperatureProperties; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.properties.Property; +import pulse.ui.Messages; +import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; + +public class TwoTemperatureModel extends PenetrationProblem { + + private Gas gas; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>("Gas Selector", Gas.class); + + public TwoTemperatureModel() { + super(); + setComplexity(ProblemComplexity.MODERATE); + instanceDescriptor.setSelectedDescriptor(Helium.class.getSimpleName()); + setGas(instanceDescriptor.newInstance(Gas.class)); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + public TwoTemperatureModel(TwoTemperatureModel p) { + super(p); + this.gas = p.gas; + instanceDescriptor.setSelectedDescriptor(gas.getClass().getSimpleName()); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> setGas(instanceDescriptor.newInstance(Gas.class))); + this.getProperties().addListener((PropertyEvent event) -> { + pulse.properties.Property p1 = event.getProperty(); + if (p1 instanceof NumericProperty) { + pulse.properties.NumericPropertyKeyword npType = ((NumericProperty) p1).getType(); + if (npType == TEST_TEMPERATURE) { + gas.evaluate((double) p1.getValue()); + } + } + }); + } + + @Override + public void initProperties() { + setProperties(new TwoTemperatureProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new TwoTemperatureProperties(properties)); + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + Segment bounds = Segment.boundsFrom(p.getIdentifier().getKeyword()); + double value; + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + value = (double) ttp.getSolidExchangeCoefficient().getValue(); + break; + case GAS_EXCHANGE_COEFFICIENT: + value = (double) ttp.getGasExchangeCoefficient().getValue(); + break; + case HEAT_LOSS_GAS: + value = (double) ttp.getGasHeatLoss().getValue(); + break; + default: + continue; + } + + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + var np = derive(key, p.inverseTransform()); + + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + ttp.setSolidExchangeCoefficient(np); + break; + case GAS_EXCHANGE_COEFFICIENT: + ttp.setGasExchangeCoefficient(np); + break; + case HEAT_LOSS_GAS: + ttp.setGasHeatLoss(np); + break; + default: + } + + } + + } + + @Override + public String toString() { + return Messages.getString("TwoTemperatureModel.Descriptor"); + } + + @Override + public Class defaultScheme() { + return ImplicitTwoTemperatureSolver.class; + } + + @Override + public TwoTemperatureModel copy() { + return new TwoTemperatureModel(this); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + public InstanceDescriptor getGasSelector() { + return instanceDescriptor; + } + + public Gas getGas() { + return gas; + } + + public final void setGas(Gas gas) { + this.gas = gas; + gas.evaluate((double) getProperties().getTestTemperature().getValue()); + firePropertyChanged(this, instanceDescriptor); + } + + /** + * Diffusivity of solid over diffusivity of gas + * + * @return + */ + public double diffusivityRatio() { + return (double) getProperties().getDiffusivity().getValue() + / gas.thermalDiffusivity(); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 3560fafd..9165b081 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -7,15 +7,13 @@ import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -35,6 +33,11 @@ protected AbsorptionModel() { absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); } + + protected AbsorptionModel(AbsorptionModel c) { + this.absorptionMap = new HashMap<>(); + this.absorptionMap.putAll(c.absorptionMap); + } public abstract double absorption(SpectralRange range, double x); @@ -105,9 +108,9 @@ public Set listedKeywords() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); double value = 0; Transformable transform = ABS; @@ -127,9 +130,8 @@ public void optimisationVector(ParameterVector output, List flags) { } //do this for the listed key values - output.setParameterBounds(i, Segment.boundsFrom(key)); - output.setTransform(i, transform); - output.set(i, value); + p.setTransform(transform); + p.setValue(value); } @@ -139,14 +141,14 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) throws SolverException { double value; - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var key = p.getIdentifier().getKeyword(); switch (key) { case LASER_ABSORPTIVITY: case THERMAL_ABSORPTIVITY: case COMBINED_ABSORPTIVITY: - value = params.inverseTransform(i); + value = p.inverseTransform(); break; default: continue; @@ -156,5 +158,7 @@ public void assign(ParameterVector params) throws SolverException { } } + + public abstract AbsorptionModel copy(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 31713356..3aebab03 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -6,10 +6,19 @@ public BeerLambertAbsorption() { super(); } + public BeerLambertAbsorption(AbsorptionModel m) { + super(m); + } + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); return a * Math.exp(-a * y); } + @Override + public AbsorptionModel copy() { + return new BeerLambertAbsorption(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Gas.java b/src/main/java/pulse/problem/statements/model/Gas.java new file mode 100644 index 00000000..7442ebd8 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -0,0 +1,75 @@ +package pulse.problem.statements.model; + +import pulse.util.Descriptive; +import pulse.util.Reflexive; + +public abstract class Gas implements Reflexive, Descriptive { + + private double conductivity; + private double thermalMass; + private final int atoms; + private final double mass; + + /** + * Universal gas constant. + */ + + public final static double R = 8.314; //J/K/mol + + private final static double ROOM_TEMPERATURE = 300; + private final static double NORMAL_PRESSURE = 1E5; + + public Gas(int atoms, double atomicWeight) { + evaluate(ROOM_TEMPERATURE, NORMAL_PRESSURE); + this.atoms = atoms; + this.mass = atoms * atomicWeight/1e3; + } + + public final void evaluate(double temperature, double pressure) { + this.conductivity = thermalConductivity(temperature); + this.thermalMass = cp() * density(temperature, pressure); + } + + public final void evaluate(double temperature) { + evaluate(temperature, NORMAL_PRESSURE); + } + + public final double thermalDiffusivity() { + return conductivity/thermalMass; + } + + public abstract double thermalConductivity(double t); + + public double cp() { + return (1.5 + atoms) * R / mass; + } + + public double density(double temperature, double pressure) { + return pressure * mass / (R * temperature); + } + + public double getThermalMass() { + return thermalMass; + } + + public double getConductivity() { + return conductivity; + } + + public double getNumberOfAtoms() { + return atoms; + } + + public double getMolarMass() { + return mass; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append(String.format(" : conductivity = %3.4f; thermal mass = %3.4f; ", conductivity, thermalMass)); + sb.append(String.format("atoms per molecule = %d; atomic weight = %1.4f", atoms, mass)); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Helium.java b/src/main/java/pulse/problem/statements/model/Helium.java new file mode 100644 index 00000000..96b71f0e --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Helium extends Gas { + + public Helium() { + super(1, 4); + } + + @Override + public double thermalConductivity(double t) { + return 0.415 + 0.283E-3 * (t - 1200); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 0cbba0cd..88c5972e 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -17,6 +17,15 @@ public Insulator() { super(); R = (double) def(REFLECTANCE).getValue(); } + + public Insulator(AbsorptionModel m) { + super(m); + if(m instanceof Insulator) { + R = (double) ((Insulator) m).getReflectance().getValue(); + } else { + R = (double) def(REFLECTANCE).getValue(); + } + } @Override public double absorption(SpectralRange spectrum, double x) { @@ -48,4 +57,9 @@ public Set listedKeywords() { return set; } + @Override + public AbsorptionModel copy() { + return new Insulator(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Nitrogen.java b/src/main/java/pulse/problem/statements/model/Nitrogen.java new file mode 100644 index 00000000..acaef03b --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Nitrogen extends Gas { + + public Nitrogen() { + super(2, 14); + } + + @Override + public double thermalConductivity(double t) { + return Math.sqrt(t) * (-92.39/t + 1.647 + 5.255E-4*t) * 1E-3; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index c3691eb6..153faa10 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -66,6 +66,7 @@ public ThermalProperties(ThermalProperties p) { this.a = p.a; this.Bi = p.Bi; this.T = p.T; + this.signalHeight = p.signalHeight; this.emissivity = p.emissivity; initListeners(); fill(); @@ -280,7 +281,7 @@ public Set listedKeywords() { } public final double thermalConductivity() { - return a * cP * rho; + return a * getThermalMass(); } public NumericProperty getThermalConductivity() { @@ -327,6 +328,10 @@ public double maxRadiationBiot() { public double timeFactor() { return l * l / a; } + + public double getThermalMass() { + return cP * rho; + } /** * Calculates the half-rise time t1/2 of {@code c} and @@ -338,7 +343,7 @@ public double timeFactor() { * @see pulse.input.ExperimentalData.halfRiseTime() */ public void useTheoreticalEstimates(ExperimentalData c) { - final double t0 = c.getHalfTime(); + final double t0 = c.getHalfTimeCalculator().getHalfTime(); this.a = PARKERS_COEFFICIENT * l * l / t0; if (areThermalPropertiesLoaded()) { Bi = radiationBiot(); @@ -352,7 +357,7 @@ public final boolean areThermalPropertiesLoaded() { public double maximumHeating(Pulse2D pulse) { final double Q = (double) pulse.getLaserEnergy().getValue(); final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * cP * rho); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass() ); } public NumericProperty getEmissivity() { diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 67db88ba..371fadc3 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -1,6 +1,5 @@ package pulse.problem.statements.model; -import java.util.List; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperty.requireType; @@ -10,12 +9,12 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -189,14 +188,14 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds = null; double value; Transformable transform; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case PLANCK_NUMBER: @@ -230,9 +229,9 @@ public void optimisationVector(ParameterVector output, List flags) { } transform = new StickTransform(bounds); - output.setTransform(i, transform); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(transform); + p.setValue(value); + p.setBounds(bounds); } @@ -241,9 +240,9 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var type = params.getIndex(i); + var type = p.getIdentifier().getKeyword(); switch (type) { @@ -252,7 +251,7 @@ public void assign(ParameterVector params) throws SolverException { case SCATTERING_ANISOTROPY: case OPTICAL_THICKNESS: case HEAT_LOSS_CONVECTIVE: - set(type, derive(type, params.inverseTransform(i))); + set(type, derive(type, p.inverseTransform())); break; default: break; diff --git a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java new file mode 100644 index 00000000..c879327f --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java @@ -0,0 +1,110 @@ +package pulse.problem.statements.model; + +import java.util.Set; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.GAS_EXCHANGE_COEFFICIENT; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_GAS; +import static pulse.properties.NumericPropertyKeyword.SOLID_EXCHANGE_COEFFICIENT; + +public class TwoTemperatureProperties extends ThermalProperties { + + private double exchangeSolid; + private double exchangeGas; + private double gasHeatLoss; + + public TwoTemperatureProperties() { + super(); + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + + public TwoTemperatureProperties(ThermalProperties p) { + super(p); + if (p instanceof TwoTemperatureProperties) { + var np = (TwoTemperatureProperties) p; + this.exchangeSolid = np.exchangeSolid; + this.exchangeGas = np.exchangeGas; + this.gasHeatLoss = np.gasHeatLoss; + } + else { + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + } + + @Override + public ThermalProperties copy() { + return new TwoTemperatureProperties(this); + } + + /** + * Used to change the parameter values of this {@code Problem}. It is only + * allowed to use those types of {@code NumericPropery} that are listed by + * the {@code listedParameters()}. + * + * @param type + * @param value + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + switch (type) { + case SOLID_EXCHANGE_COEFFICIENT: + setSolidExchangeCoefficient(value); + break; + case GAS_EXCHANGE_COEFFICIENT: + setGasExchangeCoefficient(value); + break; + case HEAT_LOSS_GAS: + setGasHeatLoss(value); + break; + default: + super.set(type, value); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(HEAT_LOSS_GAS); + set.add(SOLID_EXCHANGE_COEFFICIENT); + set.add(GAS_EXCHANGE_COEFFICIENT); + return set; + } + + public NumericProperty getSolidExchangeCoefficient() { + return derive(SOLID_EXCHANGE_COEFFICIENT, exchangeSolid); + } + + public NumericProperty getGasExchangeCoefficient() { + return derive(GAS_EXCHANGE_COEFFICIENT, exchangeGas); + } + + public void setSolidExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, SOLID_EXCHANGE_COEFFICIENT); + this.exchangeSolid = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public void setGasExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, GAS_EXCHANGE_COEFFICIENT); + this.exchangeGas = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public NumericProperty getGasHeatLoss() { + return derive(HEAT_LOSS_GAS, gasHeatLoss); + } + + public void setGasHeatLoss(NumericProperty p) { + NumericProperty.requireType(p, HEAT_LOSS_GAS); + this.gasHeatLoss = (double) p.getValue(); + firePropertyChanged(this, p); + } + +} diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 830f8ab1..94b6ce3f 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -24,9 +24,17 @@ public class Flag implements Property { * {@code Flag} */ public Flag(NumericPropertyKeyword type) { - this.index = type; - value = false; + this(type, false); + } + + public Flag(Flag f) { + this(f.index, f.value); } + + public Flag(NumericPropertyKeyword type, boolean flag) { + this.index = type; + this.value = flag; + } /** * Creates a {@code Flag} with the following pre-specified parameters: type diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 10aee7e8..734a70f2 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -1,8 +1,5 @@ package pulse.properties; -import java.text.ParseException; -import java.util.logging.Level; -import java.util.logging.Logger; import pulse.math.Segment; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.derive; diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 539a832d..a657171c 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -360,8 +360,38 @@ public enum NumericPropertyKeyword { * compared to rear face. Can be a number between zero and unity. */ - SOURCE_GEOMETRIC_FACTOR; - + SOURCE_GEOMETRIC_FACTOR, + + /** + * Max. no. of high-frequency waves in the sinusoidal baseline. + */ + + MAX_HIGH_FREQ_WAVES, + + /** + * Max. no. of low-frequency waves in the sinusoidal baseline. + */ + + MAX_LOW_FREQ_WAVES, + + /** + * Energy exchange coefficient in the two-temperature model (g). + */ + + SOLID_EXCHANGE_COEFFICIENT, + + /** + * Energy exchange coefficient in the two-temperature model (g'). + */ + + GAS_EXCHANGE_COEFFICIENT, + + /** + * Heat loss for the gas in the 2T-model. + */ + + HEAT_LOSS_GAS; + public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); } diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java new file mode 100644 index 00000000..44c64ac0 --- /dev/null +++ b/src/main/java/pulse/search/GeneralTask.java @@ -0,0 +1,217 @@ +package pulse.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.tasks.processing.Buffer; +import static pulse.tasks.processing.Buffer.getSize; +import pulse.util.Accessible; + +public abstract class GeneralTask + extends Accessible implements Runnable { + + private IterativeState path; //current sate + private IterativeState best; //best state + + private final Buffer buffer; + private PathOptimiser optimiser; + + public GeneralTask() { + buffer = new Buffer(); + buffer.setParent(this); + } + + public abstract List activeParameters(); + + /** + * Creates a search vector populated by parameters that + * are included in the optimisation routine. + * @return the parameter vector with optimisation parameters + */ + + public abstract ParameterVector searchVector(); + + /** + * Tries to assign a selected set of parameters to the search vector + * used in optimisation. + * @param pv a parameter vector containing all of the optimisation parameters + * whose values will be assigned to this task + * @throws SolverException + */ + + public abstract void assign(ParameterVector pv) throws SolverException; + + /** + *

+ * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, + * will do nothing. After making some preparatory steps, will initiate a + * loop with successive calls to {@code PathSolver.iteration(this)}, filling + * the buffer and notifying any data change listeners in parallel. This loop + * will go on until either converging results are obtained, or a timeout is + * reached, or if an execution error happens. Whether the run has been + * successful will be determined by comparing the associated + * R2 value with the {@code SUCCESS_CUTOFF}. + *

+ */ + @Override + public void run() { + setDefaultOptimiser(); + setIterativeState( optimiser.initState(this) ); + + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + int bufferSize = (Integer) getSize().getValue(); + buffer.init(); + //correlationBuffer.clear(); + + /* search cycle */ + /* sets an independent thread for manipulating the buffer */ + List> bufferFutures = new ArrayList<>(bufferSize); + var singleThreadExecutor = Executors.newSingleThreadExecutor(); + + var response = getResponse(); + + try { + response.objectiveFunction(this); + } catch (SolverException e1) { + onSolverException(e1); + } + + outer: + do { + + bufferFutures.clear(); + + for (var i = 0; i < bufferSize; i++) { + + try { + for (boolean finished = false; !finished;) { + finished = optimiser.iteration(this); + } + } catch (SolverException e) { + onSolverException(e); + break outer; + } + + //if global best is better than the converged value + if (best != null && best.getCost() < path.getCost()) { + try { + //assign the global best parameters + assign(path.getParameters()); + //and try to re-calculate + response.objectiveFunction(this); + } catch (SolverException ex) { + onSolverException(ex); + } + } + + final var j = i; + + bufferFutures.add(CompletableFuture.runAsync(() -> { + buffer.fill(this, j); + intermediateProcessing(); + }, singleThreadExecutor)); + + } + + bufferFutures.forEach(future -> future.join()); + + } while (buffer.isErrorTooHigh(errorTolerance) + && isInProgress()); + + singleThreadExecutor.shutdown(); + + if (isInProgress()) { + postProcessing(); + } + + } + + public abstract boolean isInProgress(); + + /** + * Override this to add intermediate processing of results e.g. + * with a correlation test. + */ + + public void intermediateProcessing() { + //empty + } + + /** + * Specifies what should be done when a solver exception is encountered. + * Empty by default + * @param e1 a solver exception + */ + + public void onSolverException(SolverException e1) { + //empty + } + + /** + * Override this to add post-processing checks + * e.g. normality tests or range checking. + */ + + public void postProcessing() { + //empty + } + + public final Buffer getBuffer() { + return buffer; + } + + public void setIterativeState(IterativeState state) { + this.path = state; + } + + public IterativeState getIterativeState() { + return path; + } + + public IterativeState getBestState() { + return best; + } + + /** + * Update the best state. The instance of this class stores two objects of + * the type IterativeState: the current state of the optimiser and the + * global best state. Calling this method will check if a new global best is + * found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search + * task, comparing the converged result to the global best, and selecting + * whichever has the lowest cost. Such routine is required due to the + * possibility of some optimisers going uphill. + */ + public void storeState() { + if (best == null || best.getCost() > path.getCost()) { + best = new IterativeState(path); + } + } + + public final void setOptimiser(PathOptimiser optimiser) { + this.optimiser = optimiser; + } + + public void setDefaultOptimiser() { + var instance = PathOptimiser.getInstance(); + if(optimiser == null || optimiser != instance) { + setOptimiser(PathOptimiser.getInstance()); + } + } + + public double objectiveFunction() throws SolverException { + return getResponse().objectiveFunction(this); + } + + public abstract I getInput(); + public abstract R getResponse(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 2bbf6b90..44666f54 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,10 +1,8 @@ package pulse.search; -import java.util.List; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; /** * An interface for dealing with optimisation variables. The variables are @@ -19,13 +17,13 @@ public interface Optimisable { * updated, the types of which are listed as indices in the {@code params} * vector. * - * @param params the optimisation vector, containing a similar set of + * @param input the optimisation vector, containing a similar set of * parameters to this {@code Problem} * @throws SolverException if {@code params} contains invalid parameter * values * @see pulse.util.PropertyHolder.listedTypes() */ - public void assign(ParameterVector params) throws SolverException; + public void assign(ParameterVector input) throws SolverException; /** * Calculates the vector argument defined on @@ -33,9 +31,7 @@ public interface Optimisable { * to the scalar objective function for this {@code Optimisable}. * * @param output the output vector where the result will be stored - * @param flags a list of {@code Flag} objects, which determine the basis of - * the search */ - public void optimisationVector(ParameterVector output, List flags); + public void optimisationVector(ParameterVector output); } diff --git a/src/main/java/pulse/search/SimpleOptimisationTask.java b/src/main/java/pulse/search/SimpleOptimisationTask.java new file mode 100644 index 00000000..4fbce664 --- /dev/null +++ b/src/main/java/pulse/search/SimpleOptimisationTask.java @@ -0,0 +1,93 @@ +package pulse.search; + +import java.util.List; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.ActiveFlags; +import static pulse.search.direction.ActiveFlags.selectActiveAndListed; +import pulse.search.direction.LMOptimiser; +import pulse.search.direction.PathOptimiser; +import pulse.util.PropertyHolder; + +/** + * Generic optimisation class. + * + * @param an optimisable object + */ +public abstract class SimpleOptimisationTask + extends GeneralTask { + + private final T optimisable; + private final DiscreteInput input; + + public SimpleOptimisationTask(T optimisable, DiscreteInput input) { + this.input = input; + this.optimisable = optimisable; + } + + @Override + public void run() { + var optimiser = PathOptimiser.getInstance(); + if(optimiser == null) { + PathOptimiser.setInstance(LMOptimiser.getInstance()); + } + super.run(); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id + -> new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + optimisable.optimisationVector(optimisationVector); + + return optimisationVector; + } + + @Override + public void assign(ParameterVector pv) throws SolverException { + optimisable.assign(pv); + } + + @Override + public boolean isInProgress() { + return false; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + optimisable.set(type, property); + } + + @Override + public List activeParameters() { + return selectActiveAndListed(ActiveFlags.getAllFlags(), optimisable); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(LMOptimiser.getInstance()); + } + + @Override + public DiscreteInput getInput() { + return input; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/SimpleResponse.java b/src/main/java/pulse/search/SimpleResponse.java new file mode 100644 index 00000000..9b658713 --- /dev/null +++ b/src/main/java/pulse/search/SimpleResponse.java @@ -0,0 +1,35 @@ +package pulse.search; + +import pulse.Response; +import pulse.math.Segment; +import pulse.search.statistics.OptimiserStatistic; + +public abstract class SimpleResponse implements Response { + + private OptimiserStatistic rs; + + public SimpleResponse(OptimiserStatistic rs) { + setOptimiserStatistic(rs); + } + + @Override + public final OptimiserStatistic getOptimiserStatistic() { + return rs; + } + + public final void setOptimiserStatistic(OptimiserStatistic statistic) { + this.rs = statistic; + } + + @Override + public double objectiveFunction(GeneralTask task) { + rs.evaluate(task); + return (double) rs.getStatistic().getValue(); + } + + @Override + public Segment accessibleRange() { + return Segment.UNBOUNDED; + } + +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index e13af6e1..81103d15 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -3,12 +3,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import pulse.problem.statements.Problem; +import pulse.input.ExperimentalData; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; @@ -41,12 +42,12 @@ public static Set availableProperties() { return set; } - var p = t.getCurrentCalculation().getProblem(); + var p = ( (Calculation) t.getResponse() ).getProblem(); if (p != null) { var fullList = p.listedKeywords(); - fullList.addAll(t.getExperimentalCurve().listedKeywords()); + fullList.addAll( ( (ExperimentalData) t.getInput() ).listedKeywords()); NumericPropertyKeyword key; for (Flag property : flags) { @@ -61,28 +62,45 @@ public static Set availableProperties() { return set; } - + + public static Flag get(NumericPropertyKeyword key) { + var flag = flags.stream().filter(f -> f.getType() == key).findAny(); + return flag.isPresent() ? flag.get() : null; + } + /** - * Finds what properties are being altered in the search - * - * @param t task for which the active parameters should be listed - * @return a {@code List} of property types represented by - * {@code NumericPropertyKeyword}s + * Creates a deep copy of the flags collection. + * @return a deep copy of the flags */ - public static List activeParameters(SearchTask t) { - var c = t.getCurrentCalculation(); - //problem dependent - var allActiveParams = selectActiveAndListed(flags, c.getProblem()); - //problem independent (lower/upper bound) - var listed = selectActiveAndListed(flags, t.getExperimentalCurve().getRange() ); - allActiveParams.addAll(listed); - return allActiveParams; + + public static List storeState() { + var copy = new ArrayList(); + for(Flag f : flags) { + copy.add(new Flag(f)); + } + return copy; + } + + /** + * Loads the argument into the current list of flags. + * This will update any matching flags and assign values correpon + * @param flags + */ + + public static void loadState(List flags) { + for(Flag f : ActiveFlags.flags) { + Optional existingFlag = flags.stream().filter(fl -> + fl.getType() == f.getType()).findFirst(); + if(existingFlag.isPresent()) { + f.setValue((boolean) existingFlag.get().getValue()); + } + } } public static List selectActiveAndListed(List flags, PropertyHolder listed) { //return empty list if(listed == null) { - return new ArrayList(); + return new ArrayList<>(); } return selectActiveTypes(flags).stream() diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 239760e9..273b9361 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -6,7 +6,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -53,7 +53,7 @@ private BFGSOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); //p[k] diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index da33dddb..e6fc8a3b 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -3,6 +3,7 @@ import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.math.linear.SquareMatrix; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -18,7 +19,7 @@ public class ComplexPath extends GradientGuidedPath { private SquareMatrix hessian; private SquareMatrix inverseHessian; - protected ComplexPath(SearchTask task) { + protected ComplexPath(GeneralTask task) { super(task); } @@ -29,8 +30,8 @@ protected ComplexPath(SearchTask task) { * @param task */ @Override - public void configure(SearchTask task) { - hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); + public void configure(GeneralTask task) { + hessian = createIdentityMatrix(this.getParameters().dimension()); inverseHessian = createIdentityMatrix(hessian.getData().length); super.configure(task); } diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 3b78913d..7976646e 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -7,17 +7,17 @@ import pulse.math.ParameterVector; import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.linear.LinearOptimiser; import pulse.search.linear.WolfeOptimiser; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.util.InstanceDescriptor; public abstract class CompositePathOptimiser extends GradientBasedOptimiser { private InstanceDescriptor instanceDescriptor - = new InstanceDescriptor( + = new InstanceDescriptor<>( "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; @@ -46,7 +46,7 @@ private void initLinearOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task boolean accept = true; @@ -56,11 +56,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.getResponse().objectiveFunction(task); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -70,11 +70,15 @@ public boolean iteration(SearchTask task) throws SolverException { p.setLinearStep(step); // new set of parameters determined through search - var candidateParams = parameters.sum(dir.multiply(step)); + var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateVector = new ParameterVector(parameters, candidateParams); - task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters - - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + if(candidateVector.findMalformedElements().isEmpty()) { + task.assign(candidateVector); // assign new parameters + } + + double newCost = task.getResponse().objectiveFunction(task); + // calculate the sum of squared residuals if (newCost > initialCost - EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS @@ -134,8 +138,7 @@ public List listedTypes() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index a3db7fc6..abee584f 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -2,7 +2,6 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.isDiscrete; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; @@ -14,15 +13,15 @@ import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public abstract class GradientBasedOptimiser extends PathOptimiser { private double gradientResolution; private double gradientStep; - - private final static double resolutionHigh = (double)def(GRADIENT_RESOLUTION).getValue(); - private final static double resolutionLow = 5E-2; //TODO + + private final static double RESOLUTION_HIGH = (double) def(GRADIENT_RESOLUTION).getValue(); + private final static double RESOLUTION_LOW = 5E-2; //TODO /** * Abstract constructor that sets up the default @@ -34,6 +33,7 @@ public abstract class GradientBasedOptimiser extends PathOptimiser { */ protected GradientBasedOptimiser() { super(); + this.gradientResolution = gradientStep = RESOLUTION_HIGH; } /** @@ -44,9 +44,11 @@ protected GradientBasedOptimiser() { * * @see pulse.properties.Flag.defaultList() */ + @Override public void reset() { super.reset(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); + gradientResolution = RESOLUTION_HIGH; + gradientStep = gradientResolution; } /** @@ -74,26 +76,29 @@ public void reset() { * @return the gradient of the target function * @throws SolverException */ - public Vector gradient(SearchTask task) throws SolverException { + public Vector gradient(GeneralTask task) throws SolverException { final var params = task.searchVector(); + final var pVector = params.toVector(); var grad = new Vector(params.dimension()); - - for (int i = 0; i < params.dimension(); i++) { - NumericProperty defProp = NumericProperties.def(params.getIndex(i)); - double dx = dx(defProp, params.get(i)); + final var ps = params.getParameters(); + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = ps.get(i).getIdentifier().getKeyword(); + var defProp = key != null ? NumericProperties.def(key) : null; + double dx = dx(defProp, ps.get(i).inverseTransform()); final var shift = new Vector(params.dimension()); shift.set(i, 0.5 * dx); - task.assign(new ParameterVector(params, params.sum(shift))); - final double ss2 = task.solveProblemAndCalculateCost(); + var shiftVector = new ParameterVector(params, pVector.sum(shift)); + task.assign(shiftVector); + final double ss2 = task.objectiveFunction(); - task.assign(new ParameterVector(params, params.subtract(shift))); - final double ss1 = task.solveProblemAndCalculateCost(); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + final double ss1 = task.objectiveFunction(); grad.set(i, (ss2 - ss1) / dx); - } task.assign(params); @@ -101,35 +106,29 @@ public Vector gradient(SearchTask task) throws SolverException { return grad; } - + /** - * Calculates the gradient step. Ensures dx is not zero even if the parameter values is. - * Applicable to discrete properties. - * @param defProp the default property + * Calculates the gradient step. Ensures dx is not zero even if the + * parameter values is. Applicable to discrete properties. + * + * @param defProp the default property * @param value the value of the parameter under the optimisation vector * @return the gradient step */ - protected double dx(NumericProperty defProp, double value) { - boolean discrete = defProp.isDiscrete(); - return (discrete ? resolutionLow : resolutionHigh) - * (Math.abs(value) < 1E-20 - ? defProp.getMaximum().doubleValue() - : value); - } + double result; + + if (defProp == null) { + result = gradientResolution * (Math.abs(value) < 1E-20 ? 0.01 : value); + } else { + boolean discrete = defProp.isDiscrete(); + result = (discrete ? RESOLUTION_LOW : gradientResolution) + * (Math.abs(value) < 1E-20 + ? defProp.getMaximum().doubleValue() + : value); + } - /** - * Checks whether a discrete property is being optimised and selects the - * gradient step best suited to the optimisation strategy. Should be called - * before creating the optimisation path. - * - * @param task the search task defining the search vector - */ - public void configure(SearchTask task) { - var params = task.searchVector(); - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + return result; } public void setGradientResolution(NumericProperty resolution) { diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 9dcab909..7b9a06cc 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -4,6 +4,8 @@ import java.util.logging.Logger; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.tasks.logs.Status; @@ -32,7 +34,8 @@ public class GradientGuidedPath extends IterativeState { private Vector gradient; private double minimumPoint; - protected GradientGuidedPath(SearchTask t) { + protected GradientGuidedPath(GeneralTask t) { + super(t); configure(t); } @@ -41,15 +44,15 @@ protected GradientGuidedPath(SearchTask t) { * direction of search.Sets the minimum point to 0.0. * * @param t the {@code SearchTask}, for which this {@code Path} is created. - * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.search.direction.PathSolver.direction(Path) */ - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.reset(); try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); } catch (SolverException ex) { - t.notifyFailedStatus(ex); + t.onSolverException( new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + ex.printStackTrace(); } minimumPoint = 0.0; } diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index 15b9f9a9..7aee8422 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -5,6 +5,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; public interface HessianDirectionSolver extends DirectionSolver { @@ -38,7 +39,7 @@ public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { var dirv = new DMatrixRMaj(dimg, 1); if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { - throw new SolverException("Singular matrix!"); + throw new SolverException("Singular matrix!", OPTIMISATION_ERROR); } result = new Vector(dirv.getData()); diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 1db8ba2e..05fbacab 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -5,6 +5,7 @@ import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.properties.NumericProperty; +import pulse.search.GeneralTask; public class IterativeState { @@ -23,6 +24,10 @@ public IterativeState(IterativeState other) { this.cost = other.cost; } + public IterativeState(GeneralTask t) { + this.parameters = t.searchVector(); + } + //default constructor public IterativeState() {} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 80bd5e5a..4c3247aa 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -16,15 +16,16 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import static pulse.search.direction.CompositePathOptimiser.EPS; import pulse.search.statistics.OptimiserStatistic; -import pulse.search.statistics.ResidualStatistic; import pulse.search.statistics.SumOfSquares; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.ui.Messages; /** @@ -35,7 +36,7 @@ */ public class LMOptimiser extends GradientBasedOptimiser { - private static LMOptimiser instance = new LMOptimiser(); + private static final LMOptimiser instance = new LMOptimiser(); private double dampingRatio; /** @@ -53,7 +54,7 @@ private LMOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); // the previous path of the task boolean accept = true; //accept the step by default @@ -63,11 +64,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.objectiveFunction(); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -76,16 +77,17 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); - var candidate = parameters.sum(lmDirection); + var candidate = parameters.toVector().sum(lmDirection); if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { - throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + throw new SolverException("Illegal candidate parameters: not finite! " + + p.getIteration(), ILLEGAL_PARAMETERS); } task.assign(new ParameterVector( parameters, candidate)); // assign new parameters - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals /* * Delayed gratification @@ -115,11 +117,12 @@ public boolean iteration(SearchTask task) throws SolverException { * Hessian matrix. */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); + var rs = task.getResponse().getOptimiserStatistic(); //store residual vector at current parameters - p.setResidualVector(new Vector(residualVector(task.getCurrentCalculation().getOptimiserStatistic()))); + p.setResidualVector(new Vector(rs.residualsArray())); // Calculate the Jacobian -- if needed if (p.isComputeJacobian()) { @@ -132,7 +135,8 @@ public void prepare(SearchTask task) throws SolverException { p.setGradient(g1); if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { - throw new SolverException("Could not calculate objective function gradient"); + throw new SolverException("Could not calculate objective function gradient", + OPTIMISATION_ERROR); } // the Hessian is then regularised by adding labmda*I @@ -166,39 +170,51 @@ public void prepare(SearchTask task) throws SolverException { * @throws SolverException * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() */ - public RectangularMatrix jacobian(SearchTask task) throws SolverException { + public RectangularMatrix jacobian(GeneralTask task) throws SolverException { - var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); + var residualCalculator = task.getResponse().getOptimiserStatistic(); var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); + final var pVector = params.toVector(); final int numPoints = p.getResidualVector().dimension(); final int numParams = params.dimension(); var jacobian = new double[numPoints][numParams]; - + var ps = params.getParameters(); + for (int i = 0; i < numParams; i++) { - double dx = dx( NumericProperties.def(params.getIndex(i)), params.get(i)); + var key = ps.get(i).getIdentifier().getKeyword(); + double dx = dx( + key != null ? NumericProperties.def(key) : null, + ps.get(i).inverseTransform()); final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); // + shift - task.assign(new ParameterVector(params, params.sum(shift))); - task.solveProblemAndCalculateCost(); - var r1 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.sum(shift))); + task.objectiveFunction(); + var r = residualCalculator.getResiduals(); + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { + jacobian[j][i] = r.get(j) / dx; + + } + // - shift - task.assign(new ParameterVector(params, params.subtract(shift))); - task.solveProblemAndCalculateCost(); - var r2 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + task.objectiveFunction(); - for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { - jacobian[j][i] = (r1[j] - r2[j]) / dx; + jacobian[j][i] -= r.get(j) / dx; } @@ -210,14 +226,9 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { return Matrices.createMatrix(jacobian); } - - private static double[] residualVector(ResidualStatistic rs) { - return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); - } - + @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new LMPath(t); } diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 3f006e19..2528968b 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -3,7 +3,7 @@ import pulse.math.linear.RectangularMatrix; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; class LMPath extends ComplexPath { @@ -13,12 +13,12 @@ class LMPath extends ComplexPath { private double lambda; private boolean computeJacobian; - public LMPath(SearchTask t) { + public LMPath(GeneralTask t) { super(t); } @Override - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.configure(t); this.lambda = 1.0; computeJacobian = true; diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index d3571159..cb5d4fa6 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -14,8 +14,8 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.statistics.OptimiserStatistic; -import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,15 +93,15 @@ public void reset() { * @see direction(Path) * @see pulse.search.linear.LinearOptimiser */ - public abstract boolean iteration(SearchTask task) throws SolverException; - + public abstract boolean iteration(GeneralTask task) throws SolverException; + /** * Defines a set of procedures to be run at the end of the search iteration. * * @param task the {@code SearchTask} undergoing optimisation * @throws SolverException */ - public abstract void prepare(SearchTask task) throws SolverException; + public abstract void prepare(GeneralTask task) throws SolverException; public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); @@ -232,6 +232,6 @@ public boolean compatibleWith(OptimiserStatistic os) { * @param t the task, the optimisation path of which will be tracked * @return a {@code Path} instance */ - public abstract IterativeState initState(SearchTask t); + public abstract IterativeState initState(GeneralTask t); } diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index bf89b075..8b2f7081 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -7,6 +7,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -35,7 +36,7 @@ private SR1Optimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 390437f7..8fcbc31b 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -2,7 +2,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -36,7 +36,7 @@ private SteepestDescentOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { ((GradientGuidedPath) task.getIterativeState()).setGradient(gradient(task)); } @@ -63,9 +63,8 @@ public static SteepestDescentOptimiser getInstance() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new GradientGuidedPath(t); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java new file mode 100644 index 00000000..f19d824f --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java @@ -0,0 +1,49 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class ConstrictionMover implements Mover { + + private double c1; //social + private double c2; //cognitive + private double chi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_C = 1.49618; + + public ConstrictionMover() { + chi = DEFAULT_CHI; + c1 = c2 = DEFAULT_C; + } + + @Override + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { + var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + Vector nsum = new Vector(n); + + var localBest = p.getBestState().getPosition(); //best position by local particle + var localBestV = localBest.toVector(); + var globalBest = gBest.getPosition(); //best position by any particle + var globalBestV = globalBest.toVector(); + + nsum = nsum.sum(Vector.random(n, 0.0, c1) + .multComponents(localBestV.subtract(curPosV)) + ); + + nsum = nsum.sum(Vector.random(n, 0.0, c2) + .multComponents(globalBestV.subtract(curPosV)) + ); + + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + + return new ParticleState( + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); + } + +} diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index ab4ca1f8..b6869ec6 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -16,29 +16,30 @@ public FIPSMover() { } @Override - public ParticleState attemptMove(Particle p, Particle[] neighbours) { + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + final double nLength = (double) neighbours.length; - var pos = current.getPosition(); - - final int n = pos.dimension(); - var nsum = new Vector(n); + Vector nsum = new Vector(n); for (var neighbour : neighbours) { - var nPos = neighbour.getCurrentState().getPosition(); - nsum = nsum.sum(Vector.random(n, 0.0, phi).multComponents(nPos.subtract(pos))); + var nBestPos = neighbour.getBestState().getPosition(); //best position ever achieved so far by the neighbour + nsum = nsum.sum(Vector.random(n, 0.0, phi/nLength) + .multComponents(nBestPos.toVector().subtract(curPosV)) + ); } - nsum = nsum.multiply(1.0 / ((double) neighbours.length)); - - var newVelocity = (current.getVelocity().sum(nsum)).multiply(chi); - var newPosition = pos.sum(newVelocity); - System.out.println(newPosition); - + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + return new ParticleState( - new ParameterVector(pos, newPosition), - new ParameterVector(pos, newVelocity)); + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index 6dd7387d..d1db1d0c 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -2,6 +2,6 @@ public interface Mover { - public ParticleState attemptMove(Particle p, Particle[] neighbours); + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java index e5384505..d5031dbb 100644 --- a/src/main/java/pulse/search/direction/pso/Particle.java +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -19,6 +19,7 @@ package pulse.search.direction.pso; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -43,10 +44,10 @@ public void adopt(ParticleState state) { this.current = state; } - public void evaluate(SearchTask t) throws SolverException { + public void evaluate(GeneralTask t) throws SolverException { var params = t.searchVector(); t.assign(current.getPosition()); - current.setFitness(t.solveProblemAndCalculateCost()); + current.setFitness(t.objectiveFunction()); t.assign(params); if (current.isBetterThan(pbest)) { diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index e5fcdc09..68f68c13 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -1,6 +1,7 @@ package pulse.search.direction.pso; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; public class ParticleState { @@ -13,10 +14,8 @@ public ParticleState(ParameterVector cur) { this.velocity = new ParameterVector(cur); //set initial velocity to zero - for (int i = 0, n = velocity.dimension(); i < n; i++) { - velocity.set(i, 0.0); - } - + velocity.setValues(new Vector(cur.dimension())); + this.fitness = Double.MAX_VALUE; } @@ -35,22 +34,16 @@ public boolean isBetterThan(ParticleState s) { return this.fitness < s.fitness; } - public void randomise(ParameterVector pos) { - - this.position = new ParameterVector(pos); - - for (int i = 0, n = position.dimension(); i < n; i++) { - - var bounds = position.getBounds(); - - double max = bounds[i].getMaximum(); - double min = bounds[i].getMinimum(); - - double value = min + Math.random() * (max - min); - position.set(i, value); - - } + public final void randomise(ParameterVector pos) { + double[] randomValues = pos.getParameters().stream().mapToDouble(p -> { + double min = p.getBounds().getMinimum(); + double max = p.getBounds().getMaximum(); + return min + Math.random() * (max - min); + }).toArray(); + + Vector randomVector = new Vector(randomValues); + position.setValues(randomVector); } public ParameterVector getPosition() { diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index badabe99..6f39f8b0 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -1,58 +1,72 @@ package pulse.search.direction.pso; -public class ParticleSwarmOptimiser //extends PathOptimiser -{ +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.search.statistics.OptimiserStatistic; + +public class ParticleSwarmOptimiser extends PathOptimiser { private SwarmState swarmState; private Mover mover; public ParticleSwarmOptimiser() { swarmState = new SwarmState(); - mover = new FIPSMover(); + mover = new ConstrictionMover(); } protected void moveParticles() { var topology = swarmState.getNeighborhoodTopology(); for (var p : swarmState.getParticles()) { - p.adopt(mover.attemptMove(p, topology.neighbours(p, swarmState))); + p.adopt(mover.attemptMove(p, + topology.neighbours(p, swarmState), + swarmState.getBestSoFar())); + var data = p.getCurrentState().getPosition().toVector().getData(); + StringBuilder sb = new StringBuilder().append(p.getId()).append(" "); + for(var d : data) { + sb.append(d).append(" "); + } + System.err.println(sb.toString()); } } /** * Iterates the swarm. * - * @param max_iterations max number of iterations to be computed by the - * swarm. */ + @Override + public boolean iteration(GeneralTask task) throws SolverException { + this.prepare(task); - /* - @Override - public boolean iteration(SearchTask task) throws SolverException { - this.prepare(task); - - swarmState.evaluate(task); - moveParticles(); - - swarmState.incrementStep(); - - task.assign( swarmState.bestSoFar().getPosition() ); - task.solveProblemAndCalculateCost(); - - return true; - } - */ + swarmState.evaluate(task); + swarmState.bestSoFar(); + moveParticles(); + + swarmState.incrementStep(); + + task.assign(swarmState.getBestSoFar().getPosition()); + task.objectiveFunction(); + + return true; + } + + @Override + public void prepare(GeneralTask task) throws SolverException { + swarmState.prepare(task); + } + + @Override + public IterativeState initState(GeneralTask t) { + swarmState.prepare(t); + swarmState.create(); + return swarmState; + } + + //TODO + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return false; + } - /* - @Override - public void prepare(SearchTask task) throws SolverException { - swarmState.prepare(task); - } - - @Override - public IterativeState initState(SearchTask t) { - swarmState.prepare(t); - swarmState.create(); - return swarmState; - } - */ } diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 99f31bdd..8fa5c245 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -32,7 +32,7 @@ public class StaticTopologies { final int latticeParameter = (int) Math.sqrt(ps.length); - final int row = i % latticeParameter; + final int row = i / latticeParameter; final int column = i - row * latticeParameter; final int above = column + (row > 0 diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 4a2f4244..7baa955f 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -2,8 +2,8 @@ import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.IterativeState; -import pulse.tasks.SearchTask; public class SwarmState extends IterativeState { @@ -12,13 +12,13 @@ public class SwarmState extends IterativeState { private Particle[] particles; private NeighbourhoodTopology neighborhoodTopology; - private Particle bestSoFar; + private ParticleState bestSoFar; private int bestSoFarIndex; private final static int DEFAULT_PARTICLES = 16; public SwarmState() { - this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); + this(DEFAULT_PARTICLES, StaticTopologies.RING); } public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { @@ -28,13 +28,13 @@ public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopol this.bestSoFarIndex = -1; } - public void evaluate(SearchTask t) throws SolverException { - for (var p : particles) { + public void evaluate(GeneralTask t) throws SolverException { + for (Particle p : particles) { p.evaluate(t); } } - public void prepare(SearchTask t) { + public void prepare(GeneralTask t) { seed = t.searchVector(); } @@ -47,9 +47,8 @@ public void create() { /** * Returns the best state achieved by any particle so far. * - * @return State object. */ - public ParticleState bestSoFar() { + public void bestSoFar() { int bestIndex = 0; double fitness = 0; @@ -65,11 +64,16 @@ public ParticleState bestSoFar() { } } - - this.bestSoFar = particles[bestIndex]; - this.bestSoFarIndex = bestIndex; - - return bestSoFar.getBestState(); + + //determine the current best + ParticleState curBest = particles[bestIndex].getCurrentState(); + + //is curBest the best so far? + if(bestSoFar == null || curBest.isBetterThan(bestSoFar) ) { + this.bestSoFar = curBest; + this.bestSoFarIndex = bestIndex; + } + } public NeighbourhoodTopology getNeighborhoodTopology() { @@ -93,11 +97,11 @@ public void setParticles(Particle[] particles) { this.particles = particles; } - public Particle getBestSoFar() { + public ParticleState getBestSoFar() { return bestSoFar; } - public void setBestSoFar(Particle bestSoFar) { + public void setBestSoFar(ParticleState bestSoFar) { this.bestSoFar = bestSoFar; } @@ -109,4 +113,4 @@ public void setBestSoFarIndex(int bestSoFarIndex) { this.bestSoFarIndex = bestSoFarIndex; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index a4229d27..4c9f1bea 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -3,6 +3,7 @@ import pulse.math.ParameterVector; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientGuidedPath; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -46,11 +47,12 @@ private GoldenSectionOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { final double EPS = 1e-14; final var params = task.searchVector(); + var vParams = params.toVector(); final Vector direction = ((GradientGuidedPath) task.getIterativeState()).getDirection(); var segment = domain(params, direction); @@ -61,13 +63,13 @@ public double linearStep(SearchTask task) throws SolverException { final double alpha = segment.getMinimum() + t; final double one_minus_alpha = segment.getMaximum() - t; - final var newParams1 = params.sum(direction.multiply(alpha)); // alpha + final var newParams1 = vParams.sum(direction.multiply(alpha)); // alpha task.assign(new ParameterVector(params, newParams1)); - final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) + final double ss2 = task.objectiveFunction(); // f(alpha) - final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + final var newParams2 = vParams.sum(direction.multiply(one_minus_alpha)); // 1 - alpha task.assign(new ParameterVector(params, newParams2)); - final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) + final double ss1 = task.objectiveFunction(); // f(1-alpha) task.assign(new ParameterVector(params, newParams2)); // return to old position diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index 891b42bb..a82b9188 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,6 +14,7 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -45,7 +47,7 @@ protected LinearOptimiser() { * to a lower SSR value of this {@code task} * @throws SolverException */ - public abstract double linearStep(SearchTask task) throws SolverException; + public abstract double linearStep(GeneralTask task) throws SolverException; /** * Sets the domain for this linear search on {@code p}. @@ -68,29 +70,32 @@ public static Segment domain(ParameterVector x, Vector p) { double alphaMax = Double.POSITIVE_INFINITY; double alpha; - for (int i = 0; i < x.dimension(); i++) { + var params = x.getParameters(); - final double component = p.get(i); + for (Parameter xp : params) { + + final double component = p.get(params.indexOf(xp)); //check if zero - if (component < EPS && component > -EPS) { - continue; - } + if (Math.abs(component) > EPS) { - var bound = x.getTransformedBounds(i); + var bound = xp.getTransformedBounds(); - alpha = abs( - ((component > 0 ? bound.getMaximum() : bound.getMinimum()) - x.get(i)) - / component); + alpha = abs( + ((component > 0 ? bound.getMaximum() + : bound.getMinimum()) - xp.inverseTransform()) + / component); + + if (Double.isFinite(alpha) && alpha < alphaMax) { + alphaMax = alpha; + } - if (Double.isFinite(alpha) && alpha < alphaMax) { - alphaMax = alpha; } } //check that alphaMax is not zero! otherwise the optimise will crash - return new Segment(0.0, + return new Segment(0.0, Math.max(alphaMax, 1E-10)); } @@ -135,7 +140,7 @@ public Set listedKeywords() { set.add(LINEAR_RESOLUTION); return set; } - + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == LINEAR_RESOLUTION) { diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 751d7d67..af3a2cd3 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -6,6 +6,7 @@ import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientBasedOptimiser; import pulse.search.direction.GradientGuidedPath; import pulse.search.direction.PathOptimiser; @@ -64,7 +65,7 @@ private WolfeOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); @@ -75,9 +76,10 @@ public double linearStep(SearchTask task) throws SolverException { final double G1P_ABS = abs(G1P); var params = task.searchVector(); + var vParams = params.toVector(); Segment segment = domain(params, direction); - double cost1 = task.solveProblemAndCalculateCost(); + double cost1 = task.objectiveFunction(); double randomConfinedValue = 0; double g2p; @@ -88,11 +90,11 @@ public double linearStep(SearchTask task) throws SolverException { randomConfinedValue = segment.randomValue(); - final var newParams = params.sum(direction.multiply(randomConfinedValue)); + final var newParams = vParams.sum(direction.multiply(randomConfinedValue)); task.assign(new ParameterVector(params, newParams)); - final double cost2 = task.solveProblemAndCalculateCost(); + final double cost2 = task.objectiveFunction(); /** * Checks if the first Armijo inequality is not satisfied. In this diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 948adfe5..7113c14c 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -1,10 +1,9 @@ package pulse.search.statistics; -import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; /** * A statistical optimality criterion relying on absolute deviations or the L1 @@ -26,11 +25,13 @@ public AbsoluteDeviations(AbsoluteDeviations another) { /** * Calculates the L1 norm statistic, which simply sums up the absolute * values of residuals. + * @param t */ @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> abs(r[1])).reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream() + .mapToDouble(a -> Math.abs(a)).average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 99ac0048..be695931 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -4,8 +4,8 @@ import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; import umontreal.ssj.gof.GofStat; import umontreal.ssj.probdist.NormalDist; @@ -21,12 +21,14 @@ public class AndersonDarlingTest extends NormalityTest { * test with the input parameters formed by the {@code task} residuals and a * normal distribution with zero mean and variance equal to the residuals * variance. + * @param task + * @return */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { calculateResiduals(task); - double[] residuals = super.transformResiduals(); + double[] residuals = residualsArray(); var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); var testResult = GofStat.andersonDarling(residuals, nd); @@ -42,7 +44,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { test(t); } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index c9775447..670d7e58 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -20,7 +20,7 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv "Correlation Test Selector", CorrelationTest.class); static { - instanceDescriptor.setSelectedDescriptor(PearsonCorrelation.class.getSimpleName()); + instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); } public CorrelationTest() { diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index f4925014..573280c7 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -1,6 +1,6 @@ package pulse.search.statistics; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public class EmptyTest extends NormalityTest { @@ -8,7 +8,7 @@ public class EmptyTest extends NormalityTest { * Always returns true */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { return true; } @@ -18,7 +18,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { // deliberately empty } diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 672bdc16..9a39bc41 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.search.statistics; import org.apache.commons.math3.distribution.FDistribution; @@ -21,7 +6,6 @@ /** * A static class for testing two calculations based on the Fischer test (F-Test) * implemented in Apache Commons Math. - * @author Artem Lunev */ public class FTest { diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index ceb4ceb2..7d5a854d 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -6,8 +6,7 @@ import org.apache.commons.math3.distribution.NormalDistribution; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.apache.commons.math3.stat.inference.TestUtils; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The Kolmogorov-Smirnov normality test as implemented in @@ -20,7 +19,7 @@ public class KSTest extends NormalityTest { private NormalDistribution nd; @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); this.setStatistic(derive(TEST_STATISTIC, @@ -29,9 +28,9 @@ public boolean test(SearchTask task) { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - residuals = transformResiduals(); + residuals = residualsArray(); final double sd = (new StandardDeviation()).evaluate(residuals); nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 324acc5e..fbba1ccb 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -12,7 +12,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyEvent; /** @@ -38,8 +38,8 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { } @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of parameters + public void evaluate(GeneralTask t) { + kq = t.searchVector().dimension(); //number of parameters calcCriterion(); } @@ -104,11 +104,11 @@ public OptimiserStatistic getOptimiserStatistic() { return os; } - public void setOptimiserStatistic(OptimiserStatistic os) { + public final void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; } - public void setStatistic(NumericProperty p) { + public final void setStatistic(NumericProperty p) { requireType(p, MODEL_CRITERION); this.criterion = (double) p.getValue(); } diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index a8de54c4..f383a83f 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -8,6 +8,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -44,7 +45,7 @@ public static void setStatisticalSignificance(NumericProperty alpha) { NormalityTest.significance = (double) alpha.getValue(); } - public abstract boolean test(SearchTask task); + public abstract boolean test(GeneralTask task); @Override public NumericProperty getStatistic() { diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index b29f6b16..0feceed6 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -1,13 +1,13 @@ package pulse.search.statistics; import static java.lang.Math.pow; +import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; -import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The coefficient of determination represents the goodness of fit that a @@ -25,7 +25,7 @@ public RSquaredTest() { } @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); sos = new SumOfSquares(); return getStatistic().compareTo(signifiance) > 0; @@ -48,35 +48,25 @@ public boolean test(SearchTask task) { * page
*/ @Override - public void evaluate(SearchTask t) { - var reference = t.getExperimentalCurve(); - + public void evaluate(GeneralTask t) { + var yr = t.getInput().getY(); sos.evaluate(t); - final int start = reference.getIndexRange().getLowerBound(); - final int end = reference.getIndexRange().getUpperBound(); - - final double mean = mean(reference, start, end); + final double mean = mean(yr); double TSS = 0; - - for (int i = start; i < end; i++) { - TSS += pow(reference.signalAt(i) - mean, 2); + int size = yr.size(); + + for (int i = 0; i < size; i++) { + TSS += pow(yr.get(i) - mean, 2); } - TSS /= (end - start); - + TSS /= size; + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); } - private double mean(ExperimentalData data, final int start, final int end) { - double mean = 0; - - for (int i = start; i < end; i++) { - mean += data.signalAt(i); - } - - mean /= (end - start); - return mean; + private double mean(List input) { + return input.stream().mapToDouble(d -> d).average().getAsDouble(); } public SumOfSquares getSumOfSquares() { diff --git a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java new file mode 100644 index 00000000..af8710fe --- /dev/null +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -0,0 +1,60 @@ +package pulse.search.statistics; + +import pulse.input.IndexRange; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; + +/** + * This is an experimental feature. + * + */ +public class RangePenalisedLeastSquares extends SumOfSquares { + + private double lambda = 0.1; + + public RangePenalisedLeastSquares() { + super(); + } + + public RangePenalisedLeastSquares(RangePenalisedLeastSquares rls) { + super(rls); + this.lambda = rls.lambda; + } + + /** + * The lambda is the regularisation strength. + * + * @return the lambda factor. + */ + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + var x = t.getInput().getX(); + double partialRange = t.getInput().bounds().length(); + double fullRange = x.get(x.size() - 1) - x.get(IndexRange.closestLeft(0.0, x)); + final double statistic = ssr + lambda * (fullRange - partialRange)/fullRange; + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Range-Penalised Least Squares"; + } + + @Override + public OptimiserStatistic copy() { + return new RangePenalisedLeastSquares(this); + } + +} diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index a2205258..6fcd8936 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * This is an experimental feature. The objective function here is equal to the @@ -12,19 +11,16 @@ * dimensionality are favoured. * */ -public class RegularisedLeastSquares extends OptimiserStatistic { +public class RegularisedLeastSquares extends SumOfSquares { private double lambda = 1e-4; - private SumOfSquares sos; - + public RegularisedLeastSquares() { super(); - sos = new SumOfSquares(); } public RegularisedLeastSquares(RegularisedLeastSquares rls) { super(rls); - sos = new SumOfSquares(rls.sos); this.lambda = rls.lambda; } @@ -47,10 +43,11 @@ public void setLambda(double lambda) { * @see pulse.search.statistics.SumOfSquares */ @Override - public void evaluate(SearchTask t) { - sos.evaluate(t); - final double ssr = (double) sos.getStatistic().getValue(); - final double statistic = ssr + lambda * t.searchVector().lengthSq(); + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + final double statistic = ssr + lambda * t.searchVector().toVector().lengthSq(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } @@ -59,11 +56,6 @@ public String getDescriptor() { return "L2 Regularised Least Squares"; } - @Override - public double variance() { - return (double) sos.getStatistic().getValue(); - } - @Override public OptimiserStatistic copy() { return new RegularisedLeastSquares(this); diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 01ed160b..a00ad587 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -1,19 +1,18 @@ package pulse.search.statistics; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static pulse.input.IndexRange.closestLeft; -import static pulse.input.IndexRange.closestRight; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; -import java.util.ArrayList; import java.util.List; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * An abstract statistic (= a numeric value resulting from a statistical @@ -29,30 +28,29 @@ public abstract class ResidualStatistic extends Statistic { private double statistic; - private List residuals; + private List rx; + private List ry; public ResidualStatistic() { super(); - residuals = new ArrayList<>(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); setPrefix("Residuals"); } public ResidualStatistic(ResidualStatistic another) { this.statistic = another.statistic; - this.residuals = new ArrayList<>(another.residuals); - } - - public double[] transformResiduals() { - return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); } /** * This will calculate the residuals for the {@code task} using the time - * sequence defined by the {@code ExperimentalData} object. The residuals - * are calculated between the model, which was previously used to populate - * the {@code HeatingCurve} and the experimental data. The temperature value - * of the model at the reference time is - * ti. and unknown a + * sequence defined by the {@code ExperimentalData} object.The residuals are + * calculated between the model, which was previously used to populate the + * {@code HeatingCurve}and the experimental data.The temperature value of + * the model at the reference time is + * ti.and unknown a * priori. Therefore, it needs to be interpolated based on the discrete * dataset generated by the solver. The interpolation is currently done * using natural cubic splines, which are re-constructed each time a new @@ -62,50 +60,72 @@ public double[] transformResiduals() { * {@code ExperimentalData} reference. The output of this method is stored * in the field of the {@code residuals} object. * - * @param task the optimisation task + * @param reference + * @param estimate * @see pulse.input.ExperimentalData * @see pulse.HeatingCurve */ - public void calculateResiduals(SearchTask task) { - var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); - var reference = task.getExperimentalCurve(); + public final void calculateResiduals(DiscreteInput reference, Response estimate, int min, int max) { + var y = reference.getY(); + var x = reference.getX(); + + //if size has not changed, use the old list + + if (ry.size() == max - min + 1) { + + for (int i = min; i < max; i++) { - residuals.clear(); - var indexRange = reference.getIndexRange(); - var time = reference.getTimeSequence(); + ry.set(i - min, y.get(i) - estimate.evaluate(x.get(i))); - var s = estimate.getSplineInterpolation(); + } - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); + } + + //else create a new list + + else { - double interpolated; + rx = x.subList(min, max); + ry.clear(); - for (int i = startIndex; i <= endIndex; i++) { - /* - * find the point on the calculated heating curve which has the closest time - * value smaller than the experimental points' time value - */ + for (int i = min; i < max; i++) { - interpolated = s.value(reference.timeAt(i)); + ry.add(y.get(i) - estimate.evaluate(x.get(i))); - residuals.add(new double[]{reference.timeAt(i), - reference.signalAt(i) - interpolated}); // y_exp - y* + } } } + + public void calculateResiduals(DiscreteInput reference, Response estimate) { + var y = reference.getY(); + var x = reference.getX(); + + var estimateRange = estimate.accessibleRange(); + + int min = (int) Math.max(reference.getIndexRange().getLowerBound(), + IndexRange.closestLeft(estimateRange.getMinimum(), x) ); + int max = (int) Math.min(reference.getIndexRange().getUpperBound(), + IndexRange.closestRight(estimateRange.getMaximum(), x) ); + + calculateResiduals(reference, estimate, min, max); + } + + public double[] residualsArray() { + return ry.stream().mapToDouble(d -> d).toArray(); + } - public List getResiduals() { - return residuals; + public final void calculateResiduals(GeneralTask task) { + calculateResiduals(task.getInput(), task.getResponse()); } - public double residualUpperBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); + public List getResiduals() { + return ry; } - public double residualLowerBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); + public List getTimeSequence() { + return rx; } public NumericProperty getStatistic() { @@ -117,10 +137,6 @@ public void setStatistic(NumericProperty statistic) { this.statistic = (double) statistic.getValue(); } - public void incrementStatistic(final double increment) { - this.statistic += increment; - } - @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == OPTIMISER_STATISTIC) { diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 7a3c5e1d..46a7f215 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,5 +1,6 @@ package pulse.search.statistics; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -11,6 +12,6 @@ */ public abstract class Statistic extends PropertyHolder implements Reflexive { - public abstract void evaluate(SearchTask t); - -} + public abstract void evaluate(GeneralTask t); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index c4905ef8..f264ed84 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The standard optimality criterion of the L2 norm condition, or simply @@ -39,11 +38,12 @@ public SumOfSquares(SumOfSquares sos) { * @param t The task containing the reference and calculated curves * @see calculateResiduals() */ + @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> r[1] * r[1]) - .reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream().mapToDouble(r -> r * r) + .average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index af33470b..4755a1db 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -10,15 +10,19 @@ import java.util.List; import java.util.stream.Collectors; +import pulse.Response; import pulse.input.ExperimentalData; import pulse.input.Metadata; +import pulse.math.Segment; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.statistics.BICStatistic; import pulse.search.statistics.FTest; import pulse.search.statistics.ModelSelectionCriterion; @@ -30,7 +34,7 @@ import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; -public class Calculation extends PropertyHolder implements Comparable { +public class Calculation extends PropertyHolder implements Comparable, Response { private Status status; public final static double RELATIVE_TIME_MARGIN = 1.01; @@ -96,7 +100,6 @@ public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; problem.setParent(this); problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); addProblemListeners(problem, curve); } @@ -165,7 +168,7 @@ public void process() throws SolverException { list.forEach(np -> sb.append(String.format("%n %-25s", np)) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } ((Solver) scheme).solve(problem); } @@ -248,6 +251,7 @@ public void setOptimiserStatistic(OptimiserStatistic os) { initModelCriterion(); } + @Override public OptimiserStatistic getOptimiserStatistic() { return os; } @@ -351,4 +355,33 @@ public void setResult(Result result) { } } -} + @Override + public double evaluate(double t) { + return problem.getHeatingCurve().interpolateSignalAt(t); + } + + @Override + public Segment accessibleRange() { + var hc = problem.getHeatingCurve(); + return new Segment(hc.timeAt(0), hc.timeLimit()); + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code SearchTask} and calculate the SSR value + * showing how well (or bad) the calculated solution describes the + * {@code ExperimentalData}. + * + * @param task + * @return the value of SSR (sum of squared residuals). + * @throws pulse.problem.schemes.solvers.SolverException + */ + + @Override + public double objectiveFunction(GeneralTask task) throws SolverException { + process(); + os.evaluate(task); + return (double) os.getStatistic().getValue(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index b17a6dba..733a939c 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -2,12 +2,10 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import static pulse.search.direction.ActiveFlags.activeParameters; import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; import static pulse.tasks.logs.Details.INCOMPATIBLE_OPTIMISER; import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Details.MAX_ITERATIONS_REACHED; import static pulse.tasks.logs.Details.MISSING_BUFFER; import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; @@ -21,25 +19,23 @@ import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.READY; -import static pulse.tasks.processing.Buffer.getSize; import static pulse.util.Reflexive.instantiate; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.direction.ActiveFlags; -import pulse.search.direction.IterativeState; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -47,16 +43,12 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; -import pulse.tasks.logs.Details; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; import pulse.tasks.logs.Status; -import pulse.tasks.processing.Buffer; import pulse.tasks.processing.CorrelationBuffer; -import pulse.util.Accessible; import static pulse.tasks.logs.Status.AWAITING_TERMINATION; -import static pulse.tasks.logs.Status.TERMINATED; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -68,15 +60,11 @@ * * @see pulse.tasks.TaskManager */ -public class SearchTask extends Accessible implements Runnable { +public class SearchTask extends GeneralTask { private Calculation current; private List stored; private ExperimentalData curve; - - private IterativeState path; //current sate - private IterativeState best; //best state - private Buffer buffer; private Log log; private final CorrelationBuffer correlationBuffer; @@ -89,8 +77,8 @@ public class SearchTask extends Accessible implements Runnable { * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private List listeners; - private List statusChangeListeners; + private final List listeners; + private final List statusChangeListeners; /** *

@@ -104,6 +92,7 @@ public class SearchTask extends Accessible implements Runnable { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { + super(); this.statusChangeListeners = new CopyOnWriteArrayList<>(); this.listeners = new CopyOnWriteArrayList<>(); current = new Calculation(this); @@ -114,23 +103,7 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - - /** - * Update the best state. The instance of this class stores two objects of - * the type IterativeState: the current state of the optimiser and the - * global best state. Calling this method will check if a new global best is - * found, and if so, this will store its parameters in the corresponding - * variable. This will then be used at the final stage of running the search - * task, comparing the converged result to the global best, and selecting - * whichever has the lowest cost. Such routine is required due to the - * possibility of some optimisers going uphill. - */ - public void storeState() { - if (best == null || best.getCost() > path.getCost()) { - best = new IterativeState(path); - } - } - + private void addListeners() { InterpolationDataset.addListener(e -> { if (current.getProblem() != null) { @@ -171,218 +144,22 @@ private void addListeners() { public void clear() { stored = new ArrayList<>(); curve.resetRanges(); - buffer = new Buffer(); correlationBuffer.clear(); - buffer.setParent(this); log = new Log(this); initCorrelationTest(); initNormalityTest(); - this.path = null; + //this.path = null; current.clear(); this.checkProblems(true); } - - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code SearchTask} and calculate the SSR value - * showing how well (or bad) the calculated solution describes the - * {@code ExperimentalData}. - * - * @return the value of SSR (sum of squared residuals). - * @throws SolverException - */ - public double solveProblemAndCalculateCost() throws SolverException { - current.process(); - var rs = current.getOptimiserStatistic(); - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - + public List alteredParameters() { - return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); - } - - /** - * Generates a search vector (= optimisation vector) using the search flags - * set by the {@code PathSolver}. - * - * @return an {@code IndexedVector} with search parameters of this - * {@code SearchTaks} - * @see pulse.search.direction.PathSolver.getSearchFlags() - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - public ParameterVector searchVector() { - var flags = ActiveFlags.getAllFlags(); - var keywords = activeParameters(this); - var optimisationVector = new ParameterVector(keywords); - - current.getProblem().optimisationVector(optimisationVector, flags); - curve.getRange().optimisationVector(optimisationVector, flags); - - return optimisationVector; - } - - /** - * Assigns the values of the parameters of this {@code SearchTask} to - * {@code searchParameters}. - * - * @param searchParameters an {@code IndexedVector} with relevant search - * parameters - * @throws pulse.problem.schemes.solvers.SolverException - * @see pulse.problem.statements.Problem.assign(IndexedVector) - */ - public void assign(ParameterVector searchParameters) throws SolverException { - current.getProblem().assign(searchParameters); - curve.getRange().assign(searchParameters); - } - - /** - *

- * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, - * will do nothing. After making some preparatory steps, will initiate a - * loop with successive calls to {@code PathSolver.iteration(this)}, filling - * the buffer and notifying any data change listeners in parallel. This loop - * will go on until either converging results are obtained, or a timeout is - * reached, or if an execution error happens. Whether the run has been - * successful will be determined by comparing the associated - * R2 value with the {@code SUCCESS_CUTOFF}. - *

- */ - @Override - public void run() { - - current.setResult(null); - - /* check of status */ - switch (current.getStatus()) { - case READY: - case QUEUED: - setStatus(IN_PROGRESS); - break; - default: - return; - } - - /* preparatory steps */ - current.getProblem().parameterListChanged(); // get updated list of parameters - - var optimiser = getInstance(); - - path = optimiser.initState(this); - - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); - int bufferSize = (Integer) getSize().getValue(); - buffer.init(); - correlationBuffer.clear(); - - /* search cycle */ - /* sets an independent thread for manipulating the buffer */ - List> bufferFutures = new ArrayList<>(bufferSize); - var singleThreadExecutor = Executors.newSingleThreadExecutor(); - - try { - solveProblemAndCalculateCost(); - } catch (SolverException e1) { - notifyFailedStatus(e1); - } - - outer: - do { - - bufferFutures.clear(); - - for (var i = 0; i < bufferSize; i++) { - - try { - for (boolean finished = false; !finished;) { - finished = optimiser.iteration(this); - } - } catch (SolverException e) { - notifyFailedStatus(e); - break outer; - } - - //if global best is better than the converged value - if (best != null && best.getCost() < path.getCost()) { - try { - //assign the global best parameters - assign(path.getParameters()); - //and try to re-calculate - solveProblemAndCalculateCost(); - } catch (SolverException ex) { - notifyFailedStatus(ex); - } - } - - final var j = i; - - bufferFutures.add(CompletableFuture.runAsync(() -> { - buffer.fill(this, j); - correlationBuffer.inflate(this); - notifyDataListeners(new DataLogEntry(this)); - }, singleThreadExecutor)); - - } - - bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance) - && current.getStatus() == IN_PROGRESS); - - singleThreadExecutor.shutdown(); - - if (current.getStatus() == IN_PROGRESS) { - runChecks(); - } - - } - - private void runChecks() { - - if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed - var status = FAILED; - status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); - setStatus(status); - } else { - - var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected - // correlations - notifyDataListeners(new CorrelationLogEntry(this)); - - if (test) { - var status = AMBIGUOUS; - status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); - setStatus(status); - } else { - // lastly, check if the parameter values estimated in this procedure are - // reasonable - - var properties = alteredParameters(); - - if (properties.stream().anyMatch(np -> !np.validate())) { - var status = FAILED; - status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - } else { - current.getModelSelectionCriterion().evaluate(this); - setStatus(DONE); - } - - } - - } - } - - public void notifyFailedStatus(SolverException e1) { - var status = Status.FAILED; - status.setDetails(Details.SOLVER_ERROR); - status.setDetailedMessage(e1.getMessage()); - e1.printStackTrace(); - setStatus(status); - } + return activeParameters().stream().map(key -> + this.numericProperty(key)).collect(Collectors.toList()); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -404,15 +181,7 @@ public void removeStatusChangeListeners() { public String toString() { return getIdentifier().toString(); } - - public ExperimentalData getExperimentalCurve() { - return curve; - } - - public IterativeState getIterativeState() { - return path; - } - + /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -427,28 +196,6 @@ public void setExperimentalCurve(ExperimentalData curve) { } - /** - * Will return {@code true} if status could be updated. - * - * @param status the status of the task - * @return {@code} true if status has been updated. {@code false} if the - * status was already set to {@code status} previously, or if it could not - * be updated at this time. - * @see Calculation.setStatus() - */ - public boolean setStatus(Status status) { - Objects.requireNonNull(status); - - Status oldStatus = current.getStatus(); - boolean changed = current.setStatus(status) - && (oldStatus != current.getStatus()); - if (changed) { - notifyStatusListeners(new StateEntry(this, status)); - } - - return changed; - } - /** *

* Checks if this {@code SearchTask} is ready to be run.Performs basic check @@ -465,7 +212,7 @@ public boolean setStatus(Status status) { * @param updateStatus */ public void checkProblems(boolean updateStatus) { - var status = current.getStatus(); + var status = getStatus(); if (status == DONE) { return; @@ -484,7 +231,7 @@ public void checkProblems(boolean updateStatus) { s.setDetails(MISSING_HEATING_CURVE); } else if (pathSolver == null) { s.setDetails(MISSING_OPTIMISER); - } else if (buffer == null) { + } else if (getBuffer() == null) { s.setDetails(MISSING_BUFFER); } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { s.setDetails(INCOMPATIBLE_OPTIMISER); @@ -516,24 +263,28 @@ private void notifyStatusListeners(StateEntry e) { l.onStatusChange(e); } } - + @Override - public String describe() { - - var sb = new StringBuilder(); - sb.append(TaskManager.getManagerInstance().getSampleName()); - sb.append("_Task_"); - var extId = curve.getMetadata().getExternalID(); - if (extId < 0) { - sb.append("IntID_").append(identifier.getValue()); - } else { - sb.append("ExtID_").append(extId); + public void run() { + correlationBuffer.clear(); + current.setResult(null); + + /* check of status */ + switch (getStatus()) { + case READY: + case QUEUED: + setStatus(IN_PROGRESS); + break; + default: + return; } - - return sb.toString(); - + + current.getProblem().parameterListChanged(); // get updated list of parameters + setDefaultOptimiser(); + + super.run(); } - + /** * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or * {@code READY}, terminates it by setting its status to {@code TERMINATED}. @@ -541,39 +292,15 @@ public String describe() { * running). */ public void terminate() { - switch (current.getStatus()) { + switch (getStatus()) { case IN_PROGRESS: case QUEUED: - case READY: setStatus(AWAITING_TERMINATION); break; default: } } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - /** - * A {@code SearchTask} is deemed equal to another one if it has the same - * {@code ExperimentalData}. - */ - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (!(o instanceof SearchTask)) { - return false; - } - - return curve.equals(((SearchTask) o).getExperimentalCurve()); - - } - public NormalityTest getNormalityTest() { return normalityTest; } @@ -595,22 +322,17 @@ public CorrelationBuffer getCorrelationBuffer() { public CorrelationTest getCorrelationTest() { return correlationTest; } - - public Calculation getCurrentCalculation() { - return current; - } - + public List getStoredCalculations() { return this.stored; - } + } public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); - } + } public void switchTo(Calculation calc) { - current.setParent(null); current = calc; current.setParent(this); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); @@ -641,5 +363,191 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { l.onTaskListChanged(e); } } + + @Override + public boolean isInProgress() { + return getStatus() == IN_PROGRESS; + } + + @Override + public void intermediateProcessing() { + correlationBuffer.inflate(this); + notifyDataListeners(new DataLogEntry(this)); + } + + @Override + public void onSolverException(SolverException e) { + setStatus(Status.troubleshoot(e)); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id -> + new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + current.getProblem().optimisationVector(optimisationVector); + curve.getRange().optimisationVector(optimisationVector); + + return optimisationVector; + } + + /** + * Assigns the values of the parameters of this {@code SearchTask} to + * {@code searchParameters}. + * + * @param searchParameters an {@code IndexedVector} with relevant search + * parameters + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.statements.Problem.assign(IndexedVector) + */ + @Override + public void assign(ParameterVector searchParameters) throws SolverException { + current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); + } + + @Override + public void postProcessing() { + + if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed + var status = FAILED; + status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + setStatus(status); + } else { + + var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected + // correlations + notifyDataListeners(new CorrelationLogEntry(this)); + + if (test) { + var status = AMBIGUOUS; + status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + setStatus(status); + } else { + // lastly, check if the parameter values estimated in this procedure are + // reasonable + + var properties = this.getIterativeState().getParameters(); + + if (properties.findMalformedElements().size() > 0) { + var status = FAILED; + status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + } else { + current.getModelSelectionCriterion().evaluate(this); + setStatus(DONE); + } + + } + + } + } + + + /** + * Finds what properties are being altered in the search of this SearchTask. + * + * @return a {@code List} of property types represented by + * {@code NumericPropertyKeyword}s + */ + @Override + public List activeParameters() { + var flags = ActiveFlags.getAllFlags(); + //problem dependent + var allActiveParams = ActiveFlags.selectActiveAndListed + (flags, current.getProblem()); + //problem independent (lower/upper bound) + var listed = ActiveFlags.selectActiveAndListed + (flags, curve.getRange() ); + allActiveParams.addAll(listed); + return allActiveParams; + } + + /** + * Will return {@code true} if status could be updated. + * + * @param status the status of the task + * @return {@code} true if status has been updated. {@code false} if the + * status was already set to {@code status} previously, or if it could not + * be updated at this time. + * @see Calculation.setStatus() + */ + + public boolean setStatus(Status status) { + Objects.requireNonNull(status); + + Status oldStatus = getStatus(); + boolean changed = current.setStatus(status) + && (oldStatus != getStatus()); + if (changed) { + notifyStatusListeners(new StateEntry(this, status)); + } + + return changed; + } + + public Status getStatus() { + return current.getStatus(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + /** + * A {@code SearchTask} is deemed equal to another one if it has the same + * {@code ExperimentalData}. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof SearchTask)) { + return false; + } + + return curve.equals(((SearchTask) o).curve); + + } + + @Override + public String describe() { + + var sb = new StringBuilder(); + sb.append(TaskManager.getManagerInstance().getSampleName()); + sb.append("_Task_"); + var extId = curve.getMetadata().getExternalID(); + if (extId < 0) { + sb.append("IntID_").append(identifier.getValue()); + } else { + sb.append("ExtID_").append(extId); + } + + return sb.toString(); + + } + + @Override + public ExperimentalData getInput() { + return curve; + } + + @Override + public Calculation getResponse() { + return current; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index f8801c24..358403df 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -95,14 +95,6 @@ private TaskManager() { selectionListeners = new CopyOnWriteArrayList<>(); taskRepositoryListeners = new CopyOnWriteArrayList<>(); addHierarchyListener(statementListener); - /* - Calculate the half-time once data is loaded. - */ - addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { - getTask(e.getId()).getExperimentalCurve().calculateHalfTime(); - } - }); } /** @@ -123,35 +115,40 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(t.getCurrentCalculation().getStatus() != Status.DONE); + t.checkProblems(t.getStatus() != Status.DONE); //try to start cmputation // notify listeners computation is about to start if (!t.setStatus(QUEUED)) { return; } - + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - + // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - var current = t.getCurrentCalculation(); + Calculation current = (Calculation)t.getResponse(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - if (current.getStatus() == DONE) { - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned + if (null == current.getStatus()) { notifyListeners(e); - t.storeCalculation(); } - else if(current.getStatus() == AWAITING_TERMINATION) { - t.setStatus(Status.TERMINATED); - } - else { - notifyListeners(e); + else switch (current.getStatus()) { + case DONE: + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + t.storeCalculation(); + break; + case AWAITING_TERMINATION: + t.setStatus(Status.TERMINATED); + break; + default: + notifyListeners(e); + break; } }); - + } /** @@ -174,7 +171,7 @@ public void notifyListeners(TaskRepositoryEvent e) { public void executeAll() { var queue = tasks.stream().filter(t -> { - switch (t.getCurrentCalculation().getStatus()) { + switch (t.getStatus()) { case IN_PROGRESS: case EXECUTION_ERROR: return false; @@ -198,7 +195,7 @@ public void executeAll() { */ public boolean isTaskQueueEmpty() { return !tasks.stream().anyMatch(t -> { - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); return status == QUEUED || status == IN_PROGRESS; }); } @@ -262,7 +259,8 @@ public SampleName getSampleName() { return null; } - return optional.get().getExperimentalCurve().getMetadata().getSampleName(); + return ( (ExperimentalData) optional.get().getInput() ) + .getMetadata().getSampleName(); } /** @@ -308,7 +306,8 @@ public SearchTask getTask(Identifier id) { */ public SearchTask getTask(int externalId) { var o = tasks.stream().filter(t - -> Integer.compare(t.getExperimentalCurve().getMetadata().getExternalID(), + -> Integer.compare( ( (ExperimentalData) t.getInput()) + .getMetadata().getExternalID(), externalId) == 0).findFirst(); return o.isPresent() ? o.get() : null; } @@ -340,7 +339,7 @@ public void generateTask(File file) { curves.stream().forEach((ExperimentalData curve) -> { var task = new SearchTask(curve); addTask(task); - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); if (!data.isAcquisitionTimeSensible()) { data.truncate(); } @@ -374,7 +373,6 @@ public void generateTasks(List files) { }; Executors.newSingleThreadExecutor().submit(loader); - } /** @@ -512,8 +510,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { - var properties = t.getCurrentCalculation().getProblem().getProperties(); - var c = t.getExperimentalCurve(); + var properties = ( (Calculation) t.getResponse() ).getProblem().getProperties(); + var c = (ExperimentalData)t.getInput(); properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index cacac2f2..f6785bdf 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -1,8 +1,8 @@ package pulse.tasks.logs; +import pulse.math.ParameterIdentifier; import static pulse.properties.NumericProperties.def; -import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.ImmutablePair; @@ -32,16 +32,20 @@ public String toString() { sb.append("

"); - sb.append(p.getAbbreviation(false)); + var def = NumericProperties.def(p.getIdentifier().getKeyword()); + boolean b = def.getValue() instanceof Integer; + Number val; + if(b) { + val = (int) Math.rint(p.getApparentValue()); + } else{ + val = p.getApparentValue(); + } + def.setValue(val); + sb.append(def.getAbbreviation(false)); + int index = p.getIdentifier().getIndex(); + if(index > 0) { + sb.append(" - ").append(index); + } sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ sb.append(""); - sb.append(p.formattedOutput()); + sb.append(def.formattedOutput()); sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ sb.append("
%.8f%.8f
"); sb.append(""); - for (ImmutablePair key : map.keySet()) { + for (ImmutablePair key : map.keySet()) { sb.append("
Correlation table
x y Correlation
"); - sb.append(def(key.getFirst()).getAbbreviation(false)); + sb.append(def(key.getFirst().getKeyword()).getAbbreviation(false)); + if(key.getFirst().getIndex() > 0) + sb.append(" - ").append(key.getFirst().getIndex()); sb.append(""); - sb.append(def(key.getSecond()).getAbbreviation(false)); + sb.append(def(key.getSecond().getKeyword()).getAbbreviation(false)); + if(key.getSecond().getIndex() > 0) + sb.append(" - ").append(key.getSecond().getIndex()); sb.append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } - sb.append("" + String.format("%3.2f", map.get(key)) + ""); + sb.append("").append(String.format("%3.2f", map.get(key))).append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 8448b5fb..2fad022e 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -1,10 +1,11 @@ package pulse.tasks.logs; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; import java.util.List; +import pulse.math.Parameter; +import pulse.math.ParameterIdentifier; +import pulse.properties.NumericProperties; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.ui.Messages; @@ -19,7 +20,7 @@ */ public class DataLogEntry extends LogEntry { - private List entry; + private List entry; /** * Creates a new {@code DataLogEntry} based on the current values of the @@ -52,13 +53,14 @@ public DataLogEntry(SearchTask task) { */ private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { var task = TaskManager.getManagerInstance().getTask(getIdentifier()); - - entry = task.alteredParameters(); - Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); - entry.add(0, task.getIterativeState().getIteration()); + entry = task.searchVector().getParameters(); + var pval = task.getIterativeState().getIteration(); + var pid = new Parameter(new ParameterIdentifier(pval.getType())); + pid.setValue( (int) pval.getValue() ); + entry.add(0, pid); } - public List getData() { + public List getData() { return entry; } @@ -83,13 +85,26 @@ public String toString() { */ sb.append(""); - for (NumericProperty p : entry) { + for (Parameter p : entry) { sb.append("<
"); diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index c8780bac..5b519531 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -9,7 +9,6 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; -import pulse.tasks.listeners.StatusChangeListener; import pulse.ui.Messages; import pulse.util.Group; @@ -23,8 +22,8 @@ public class Log extends Group { private List logEntries; private LocalTime start; private LocalTime end; - private Identifier id; - private List listeners; + private final Identifier id; + private final List listeners; private static boolean verbose = false; /** @@ -49,38 +48,32 @@ public Log(SearchTask task) { /** * Do these actions each time data has been collected for this task. */ - if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { + if (task.getStatus() != Status.INCOMPLETE && verbose) { logEntries.add(le); notifyListeners(le); } }); - task.addStatusChangeListener(new StatusChangeListener() { - - /** - * Do these actions every time the task status has changed. - */ - @Override - public void onStatusChange(StateEntry e) { - logEntries.add(e); - - if (e.getStatus() == Status.IN_PROGRESS) { - start = e.getTime(); - end = null; - } else { - end = e.getTime(); - } - - notifyListeners(e); - - if (e.getState() == Status.DONE) { - logFinished(); - } - + task.addStatusChangeListener((StateEntry e) -> { + logEntries.add(e); + + if (e.getStatus() == Status.IN_PROGRESS) { + start = e.getTime(); + end = null; + } else { + end = e.getTime(); } - - }); + + notifyListeners(e); + + if (e.getState() == Status.DONE) { + logFinished(); + } + } /** + * Do these actions every time the task status has changed. + */ + ); } @@ -92,15 +85,15 @@ private void notifyListeners(LogEntry logEntry) { listeners.stream().forEach(l -> l.onNewEntry(logEntry)); } - public List getListeners() { + public final List getListeners() { return listeners; } - public void addListener(LogEntryListener l) { + public final void addListener(LogEntryListener l) { listeners.add(l); } - public Identifier getIdentifier() { + public final Identifier getIdentifier() { return id; } @@ -128,10 +121,12 @@ public String toString() { sb.append(newLine); sb.append(newLine); - for (LogEntry le : logEntries) { + logEntries.stream().map(le -> { sb.append(le); + return le; + }).forEachOrdered(_item -> { sb.append(newLine); - } + }); return sb.toString(); diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 87e226d9..e2d7017f 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -1,6 +1,9 @@ package pulse.tasks.logs; import java.awt.Color; +import java.util.Objects; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.schemes.solvers.SolverException.SolverExceptionType; /** * An enum that represents the different states in which a {@code SearchTask} @@ -130,5 +133,20 @@ public String getMessage() { } return sb.toString(); } + + public static Status troubleshoot(SolverException e1) { + Objects.requireNonNull(e1, "Solver exception cannot be null when calling troubleshoot!"); + Status status = null; + if(e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { + status = Status.FAILED; + status.setDetails(Details.SOLVER_ERROR); + status.setDetailedMessage(e1.getMessage()); + } + else { + status = Status.TIMEOUT; + status.setDetails(Details.MAX_ITERATIONS_REACHED); + } + return status; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index bcfbac0e..e3a16843 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -13,7 +13,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyHolder; /** @@ -61,8 +61,9 @@ public void init() { * @param t the {@code SearchTask} * @param bufferElement the {@code bufferElement} which will be written over */ - public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); + public final void fill(GeneralTask t, int bufferElement) { + statistic[bufferElement] = (double) t.getResponse() + .getOptimiserStatistic().getStatistic().getValue(); data[bufferElement] = t.searchVector(); } @@ -80,7 +81,7 @@ public boolean isErrorTooHigh(double errorTolerance) { boolean result = false; for (int i = 0; i < e.length && (!result); i++) { - var index = data[0].getIndex(i); + var index = data[0].getParameters().get(i).getIdentifier().getKeyword(); final double av = average(index); e[i] = variance(index) / (av * av); @@ -105,7 +106,7 @@ public double average(NumericPropertyKeyword index) { double av = 0; for (ParameterVector v : data) { - av += v.getParameterValue(index); + av += v.getParameterValue(index, 0); } return av / data.length; @@ -142,7 +143,7 @@ public double variance(NumericPropertyKeyword index) { double av = average(index); for (ParameterVector v : data) { - final double s = v.getParameterValue(index) - av; + final double s = v.getParameterValue(index, 0) - av; sd += s * s; } @@ -188,7 +189,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { */ @Override public List listedTypes() { - return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); + return new ArrayList<>(Arrays.asList(def(BUFFER_SIZE))); } } diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 31d3ef15..fc0c0f79 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -7,8 +7,10 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; import pulse.properties.NumericPropertyKeyword; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.EmptyCorrelationTest; @@ -18,9 +20,9 @@ public class CorrelationBuffer { - private List params; - private static Set> excludePairList; - private static Set excludeSingleList; + private final List params; + private static final Set> excludePairList; + private static final Set excludeSingleList; private final static double DEFAULT_THRESHOLD = 1E-3; @@ -58,17 +60,18 @@ private void truncate(double threshold) { for(i = 0; i < size - 1; i = i + 2) { - ParameterVector diff = new ParameterVector( params.get(i), params.get(i + 1).subtract(params.get(i) )); - if(diff.lengthSq()/params.get(i).lengthSq() < thresholdSq) + Vector vParams = params.get(i).toVector(); + Vector vPlusOneParams = params.get(i + 1).toVector(); + Vector vDiff = vPlusOneParams.subtract(vParams); + if(vDiff.lengthSq()/vParams.lengthSq() < thresholdSq) break; } for(int j = size - 1; j > i; j--) - params.remove(j); - + params.remove(j); } - public Map, Double> evaluate(CorrelationTest t) { + public Map, Double> evaluate(CorrelationTest t) { if (params.isEmpty()) { throw new IllegalStateException("Zero number of entries in parameter list"); } @@ -79,24 +82,40 @@ public Map, Double> evaluate(CorrelationTe truncate(DEFAULT_THRESHOLD); - var indices = params.get(0).getIndices(); - var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) + List indices = params.get(0).getParameters().stream() + .map(ps -> ps.getIdentifier()).collect(Collectors.toList()); + Map map = indices.stream() + .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble( + v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); - var correlationMap = new HashMap, Double>(); - ImmutablePair pair = null; + var correlationMap = new HashMap, Double>(); + ImmutablePair pair; for (int i = 0; i < indicesSize; i++) { - if (!excludeSingleList.contains(indices.get(i))) { + var iKey = indices.get(i).getKeyword(); + + if (!excludeSingleList.contains(iKey)) { + for (int j = i + 1; j < indicesSize; j++) { - pair = new ImmutablePair<>(indices.get(i), indices.get(j)); - if (!excludeSingleList.contains(indices.get(j)) && !excludePairList.contains(pair)) { - correlationMap.put(pair, t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + + var jKey = indices.get(j).getKeyword(); + + pair = new ImmutablePair<>(iKey, jKey); + + if (!excludeSingleList.contains(jKey) + && !excludePairList.contains(pair)) { + + correlationMap.put( + new ImmutablePair<>(indices.get(i), indices.get(j)), + t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } + } + } } @@ -112,6 +131,8 @@ public boolean test(CorrelationTest t) { return false; } + var values = map.values(); + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 8350aedd..626e292b 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -28,7 +29,7 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); } - setParent(task.getCurrentCalculation()); + setParent((Calculation)task.getResponse()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index acadc0bb..de978325 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -87,7 +87,7 @@ public static void main(String[] args) { }); } else { - System.out.println("An instance of PULsE is already running!"); + System.out.println(Messages.getString("msg.running")); } } diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 9d966b43..95c4ab1d 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -4,6 +4,8 @@ import static pulse.ui.frames.MainGraphFrame.getChart; import java.awt.Dimension; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.swing.JTable; import javax.swing.SwingUtilities; @@ -23,9 +25,11 @@ public class CalculationTable extends JTable { private final static int HEADER_HEIGHT = 30; private TaskTableRenderer taskTableRenderer; + private ExecutorService plotExecutor; public CalculationTable() { super(); + plotExecutor = Executors.newSingleThreadExecutor(); setDefaultEditor(Object.class, null); taskTableRenderer = new TaskTableRenderer(); this.setRowSelectionAllowed(true); @@ -67,7 +71,7 @@ public void update(SearchTask t) { } public void identifySelection(SearchTask t) { - int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); + int modelIndex = t.getStoredCalculations().indexOf(t.getResponse()); if (modelIndex > -1) { this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); } @@ -81,10 +85,12 @@ public void initListeners() { var task = TaskManager.getManagerInstance().getSelectedTask(); var id = convertRowIndexToModel(this.getSelectedRow()); if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { - - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); - + + plotExecutor.submit(() -> { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + }); + } }); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 753b8784..68dda2d3 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -41,6 +41,7 @@ import pulse.input.IndexRange; import pulse.input.Range; import pulse.input.listeners.DataEvent; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperties; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; @@ -90,10 +91,9 @@ public void mouseDragged(MouseEvent e) { } SwingUtilities.invokeLater(() -> { - + //process dragged events - Range range = instance.getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ((ExperimentalData) (instance.getSelectedTask().getInput())).getRange(); double value = xCoord(e) / factor; //convert to seconds back from ms -- if needed if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { @@ -107,7 +107,7 @@ public void mouseDragged(MouseEvent e) { } else { super.mouseDragged(e); } - + }); } @@ -118,12 +118,13 @@ public void mouseDragged(MouseEvent e) { //for each new task var eventTask = instance.getTask(e.getId()); if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + var data = (ExperimentalData) eventTask.getInput(); //add passive data listener - eventTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { //that will be triggered only when this task is selected if (instance.getSelectedTask() == eventTask) { //update marker values - var segment = eventTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerMarker.setValue(segment.getMinimum() * factor); //convert to ms -- if needed upperMarker.setValue(segment.getMaximum() * factor); //convert to ms -- if needed } @@ -221,7 +222,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.setDataset(i, null); } - var rawData = task.getExperimentalCurve(); + var rawData = (ExperimentalData) task.getInput(); var segment = rawData.getRange().getSegment(); adjustAxisLabel(segment.getMaximum()); @@ -239,7 +240,7 @@ public void plot(SearchTask task, boolean extendedCurve) { lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - final double margin = (lowerMarker.getValue() + upperMarker.getValue())/20.0; + final double margin = (lowerMarker.getValue() + upperMarker.getValue()) / 20.0; //add listener to handle range adjustment var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); @@ -251,7 +252,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.addDomainMarker(upperMarker); plot.addDomainMarker(lowerMarker); - var calc = task.getCurrentCalculation(); + var calc = (Calculation) task.getResponse(); var problem = calc.getProblem(); if (problem != null) { @@ -259,6 +260,15 @@ public void plot(SearchTask task, boolean extendedCurve) { var solution = problem.getHeatingCurve(); var scheme = calc.getScheme(); + if (solution != null && !solution.isFull()) { + try { + calc.process(); + } catch (SolverException ex) { + System.out.println("Could not plot solution! See details in debug."); + ex.printStackTrace(); + } + } + if (solution != null && scheme != null) { var solutionDataset = new XYSeriesCollection(); @@ -298,7 +308,7 @@ public void plot(SearchTask task, boolean extendedCurve) { public void plotSingle(HeatingCurve curve) { requireNonNull(curve); - var plot = chart.getXYPlot(); + plot = chart.getXYPlot(); var classicDataset = new XYSeriesCollection(); @@ -339,6 +349,7 @@ public XYSeries residuals(Calculation calc) { var problem = calc.getProblem(); var baseline = problem.getBaseline(); + var time = calc.getOptimiserStatistic().getTimeSequence(); var residuals = calc.getOptimiserStatistic().getResiduals(); var size = residuals.size(); @@ -348,7 +359,7 @@ public XYSeries residuals(Calculation calc) { var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); for (var i = 0; i < size; i++) { - series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); + series.add(factor * time.get(i), (Number) (residuals.get(i) + offset)); } return series; diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index d6b6539d..34f9f970 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -15,12 +15,14 @@ import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileNameExtensionFilter; +import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -99,7 +101,7 @@ public static void loadMetadataDialog() { // attempt to fill metadata and problem for (SearchTask task : instance.getTaskList()) { - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); try { handler.populate(file, data.getMetadata()); @@ -109,7 +111,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = task.getCurrentCalculation().getProblem(); + var p = ( (Calculation) task.getResponse() ).getProblem(); if (p != null) { p.retrieveData(data); } @@ -146,7 +148,7 @@ public static void loadPulseDialog() { if (task != null) { pool.submit(() -> { - var metadata = task.getExperimentalCurve().getMetadata(); + var metadata = ((ExperimentalData) task.getInput()).getMetadata(); metadata.setPulseData(pulseData); metadata.getPulseDescriptor() .setSelectedDescriptor( diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index 0fb17161..5af4215a 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -90,7 +90,7 @@ public void printAll() { log.getLogEntries().stream().forEach(entry -> post(entry)); - if (task.getCurrentCalculation().getStatus() == DONE) { + if (task.getStatus() == DONE) { printTimeTaken(log); } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index 54513d97..e4c2e0de 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -14,6 +14,7 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.ProblemComplexity; +import pulse.tasks.Calculation; import pulse.ui.components.controllers.ProblemCellRenderer; import pulse.ui.components.listeners.ProblemSelectionEvent; import pulse.ui.components.listeners.ProblemSelectionListener; @@ -21,7 +22,7 @@ @SuppressWarnings("serial") public class ProblemTree extends JTree { - private List selectionListeners; + private final List selectionListeners; public ProblemTree(List allProblems) { super(); @@ -66,7 +67,7 @@ private void addListeners() { }); instance.addSelectionListener(e -> { - var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); + var current = ( (Calculation) instance.getSelectedTask().getResponse() ).getProblem(); // select appropriate problem type from list setSelectedProblem(current); @@ -108,7 +109,7 @@ public void setSelectedProblem(Problem p) { }); } - public void addProblemSelectionListener(ProblemSelectionListener l) { + public final void addProblemSelectionListener(ProblemSelectionListener l) { selectionListeners.add(l); } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 74aeeab0..216770e5 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -44,6 +44,7 @@ import pulse.search.statistics.NormalityTest; import pulse.search.statistics.OptimiserStatistic; import pulse.search.statistics.SumOfSquares; +import pulse.tasks.Calculation; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; @@ -244,7 +245,8 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t -> + ( (Calculation) t.getResponse() ).initOptimiser()); } }); diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java index 47cacf11..a146e556 100644 --- a/src/main/java/pulse/ui/components/RangeTextFields.java +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -1,30 +1,15 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.ui.components; import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFormattedTextField; import javax.swing.text.NumberFormatter; +import pulse.input.ExperimentalData; import pulse.input.Range; import pulse.input.listeners.DataEvent; import pulse.tasks.SearchTask; @@ -36,7 +21,6 @@ /** * Two JFormattedTextFields used to display the range of the currently * selected task. - * @author Artem Lunev */ public final class RangeTextFields { @@ -69,7 +53,7 @@ public RangeTextFields() { //when a new task is selected instance.addSelectionListener((TaskSelectionEvent e) -> { var task = instance.getSelectedTask(); - var segment = task.getExperimentalCurve().getRange().getSegment(); + var segment = ( (ExperimentalData) task.getInput() ).getRange().getSegment(); //update the textfield values lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); @@ -109,8 +93,8 @@ private NumberFormatter initFormatter() { */ private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { - Range range = TaskManager.getManagerInstance().getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ( (ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput() ).getRange(); double candidateValue = 0.0; try { @@ -195,10 +179,11 @@ public void focusLost(FocusEvent arg0) { } private void updateTextfieldsFromTask(SearchTask newTask) { + var data = (ExperimentalData) newTask.getInput(); //add data listeners in case when the range of the selected task is changed - newTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { if (TaskManager.getManagerInstance().getSelectedTask() == newTask) { - var segment = newTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); } diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 614d786b..7a78ee24 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -30,10 +30,10 @@ public void plot(ResidualStatistic stat) { var pulseDataset = new HistogramDataset(); pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); - var residuals = stat.transformResiduals(); + var residuals = stat.residualsArray(); if (residuals.length > 0) { - pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); + pulseDataset.addSeries("H1", residuals, binCount); } getPlot().setDataset(0, pulseDataset); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 520aa0e0..d718926f 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -17,6 +17,7 @@ import javax.swing.table.TableRowSorter; import pulse.properties.NumericProperty; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -77,35 +78,41 @@ public ResultTable(ResultFormat fmt) { */ TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { var t = instance.getTask(e.getId()); - switch (e.getState()) { - case TASK_FINISHED: - var r = t.getCurrentCalculation().getResult(); - var resultTableModel = (ResultTableModel) getModel(); - Objects.requireNonNull(r, "Task finished with a null result!"); - invokeLater(() -> resultTableModel.addRow(r)); - break; - case TASK_REMOVED: - case TASK_RESET: - ((ResultTableModel) getModel()).removeAll(e.getId()); - getSelectionModel().clearSelection(); - break; - case BEST_MODEL_SELECTED: - for (var c : t.getStoredCalculations()) { - if (c.getResult() != null && c != t.getCurrentCalculation()) { - ((ResultTableModel) getModel()).remove(c.getResult()); + + if(t != null) { + + var cc = (Calculation) t.getResponse(); + + switch (e.getState()) { + case TASK_FINISHED: + var r = cc.getResult(); + var resultTableModel = (ResultTableModel) getModel(); + Objects.requireNonNull(r, "Task finished with a null result!"); + invokeLater(() -> resultTableModel.addRow(r)); + break; + case TASK_REMOVED: + case TASK_RESET: + ((ResultTableModel) getModel()).removeAll(e.getId()); + getSelectionModel().clearSelection(); + break; + case BEST_MODEL_SELECTED: + for (var c : t.getStoredCalculations()) { + if (c.getResult() != null && c != cc) { + ((ResultTableModel) getModel()).remove(c.getResult()); + } } - } - this.select(t.getCurrentCalculation().getResult()); - break; - case TASK_MODEL_SWITCH: - var c = t.getCurrentCalculation(); - this.getSelectionModel().clearSelection(); - if (c != null && c.getResult() != null) { - select(c.getResult()); - } - break; - default: - break; + this.select(cc.getResult()); + break; + case TASK_MODEL_SWITCH: + this.getSelectionModel().clearSelection(); + if (cc != null && cc.getResult() != null) { + select(cc.getResult()); + } + break; + default: + break; + } + } }); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index d89e43d8..5dc2c26e 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -27,9 +27,11 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import pulse.input.ExperimentalData; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Result; @@ -69,8 +71,9 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { + var input = (ExperimentalData) t.getInput(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); + input.getMetadata().toString(), "Metadata", PLAIN_MESSAGE); } }); @@ -78,14 +81,14 @@ public TaskPopupMenu() { instance.addSelectionListener(event -> { instance.getSelectedTask().checkProblems(false); - var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); + var details = instance.getSelectedTask().getStatus().getDetails(); itemShowStatus.setEnabled((details != null) & (details != NONE)); }); itemShowStatus.addActionListener((ActionEvent e) -> { var t = instance.getSelectedTask(); if (t != null) { - var d = t.getCurrentCalculation().getStatus().getDetails(); + var d = t.getStatus().getDetails(); showMessageDialog(getWindowAncestor((Component) e.getSource()), "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); } @@ -100,7 +103,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ } else { t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == DONE) { var dialogButton = YES_NO_OPTION; @@ -115,7 +118,7 @@ public TaskPopupMenu() { } } else if (status != READY) { showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ + t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ ERROR_MESSAGE); } else { @@ -136,7 +139,7 @@ public TaskPopupMenu() { if (t == null) { return; } - var current = t.getCurrentCalculation(); + var current = (Calculation) t.getResponse(); if (current != null) { var r = new Result(t, getInstance()); current.setResult(r); @@ -175,8 +178,8 @@ public void plot(boolean extended) { getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - var calc = t.getCurrentCalculation(); - var statusDetails = calc.getStatus().getDetails(); + var calc = (Calculation) t.getResponse(); + var statusDetails = t.getStatus().getDetails(); if (statusDetails == MISSING_HEATING_CURVE) { diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 37b84d58..30ea042c 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -50,12 +50,12 @@ public ExecutionButton() { } var problematicTask = instance.getTaskList().stream().filter(t -> { t.checkProblems(true); - return t.getCurrentCalculation().getStatus() == INCOMPLETE; + return t.getStatus() == INCOMPLETE; }).findFirst(); if (problematicTask.isPresent()) { var t = problematicTask.get(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", + t + " is " + t.getStatus().getMessage(), "Problems found", ERROR_MESSAGE); } else { instance.executeAll(); diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index ac0c3bd0..526823f7 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -32,8 +32,10 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean try { descriptor.attemptUpdate(e.getItem()); } catch(NullPointerException npe) { - System.out.println("Error updating " + descriptor.getDescriptor(false) - + ". Cannot be set to " + e.getItem()); + String text = "Error updating " + descriptor.getDescriptor(false) + + ". Cannot be set to " + e.getItem(); + System.out.println(text); + npe.printStackTrace(); } } }); diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index ffd35db1..8592285c 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -223,8 +223,7 @@ public void addRow(AbstractResult result) { if (result instanceof Result) { //result must have a valid ancestor! - var ancestor = Objects.requireNonNull( - result.specificAncestor(SearchTask.class), + var ancestor = Objects.requireNonNull(result.specificAncestor(SearchTask.class), "Result " + result.toString() + " does not belong a SearchTask!"); //the ancestor then has the SearchTask type @@ -232,8 +231,7 @@ public void addRow(AbstractResult result) { //any old result asssociated withis this task var oldResult = results.stream().filter(r - -> r.specificAncestor( - SearchTask.class) == parentTask).findAny(); + -> r.specificAncestor(SearchTask.class) == parentTask).findAny(); //check the following only if the old result is present if (oldResult.isPresent()) { @@ -249,7 +247,8 @@ public void addRow(AbstractResult result) { Status status = Status.DONE; //better result than already present -- update table - if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + var c = (Calculation) parentTask.getResponse(); + if (c.isBetterThan(oldCalculation.get())) { remove(oldResultExisting); status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); parentTask.setStatus(status); diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 77d51b81..86eef588 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -11,6 +11,8 @@ import static pulse.ui.Messages.getString; import javax.swing.table.DefaultTableModel; +import pulse.input.ExperimentalData; +import pulse.tasks.Calculation; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -51,11 +53,12 @@ public TaskTableModel() { } public void addTask(SearchTask t) { - var temperature = t.getExperimentalCurve() + var temperature = ( (ExperimentalData) t.getInput() ) .getMetadata().numericProperty(TEST_TEMPERATURE); + var calc = (Calculation) t.getResponse(); var data = new Object[]{t.getIdentifier(), temperature, - t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus()}; + calc.getOptimiserStatistic().getStatistic(), + t.getNormalityTest().getStatistic(), t.getStatus()}; invokeLater(() -> super.addRow(data)); @@ -68,7 +71,7 @@ public void addTask(SearchTask t) { }); t.addTaskListener((LogEntry e) -> { - setValueAt(t.getCurrentCalculation().getOptimiserStatistic() + setValueAt(calc.getOptimiserStatistic() .getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); }); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 011d2f71..b4d5e548 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -26,6 +26,7 @@ import pulse.input.ExperimentalData; import pulse.input.Range; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Messages; import pulse.ui.components.RangeTextFields; @@ -64,12 +65,13 @@ public final void initComponents() { pdfBtn.addActionListener(e -> { var task = TaskManager.getManagerInstance().getSelectedTask(); + var calc = (Calculation) task.getResponse(); - if (task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { + if (task != null && calc.getModelSelectionCriterion() != null) { chFrame.setLocationRelativeTo(null); chFrame.setVisible(true); - chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); + chFrame.plot(calc.getOptimiserStatistic()); } @@ -130,7 +132,7 @@ private void validateRange(double a, double b) { return; } - var expCurve = task.getExperimentalCurve(); + var expCurve = (ExperimentalData) task.getInput(); if (expCurve == null) { return; @@ -169,7 +171,7 @@ private void validateRange(double a, double b) { // set range for all available experimental datasets TaskManager.getManagerInstance().getTaskList() .stream().forEach((aTask) - -> setRange(aTask.getExperimentalCurve(), a, b) + -> setRange( (ExperimentalData) aTask.getInput(), a, b) ); } diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 0aeedc80..92e42cab 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -20,6 +20,7 @@ import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.LoaderButton; import pulse.ui.frames.MainGraphFrame; @@ -61,15 +62,16 @@ public static void plot(ActionEvent e) { var t = instance.getSelectedTask(); - var calc = t.getCurrentCalculation(); + var calc = (Calculation) t.getResponse(); t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == INCOMPLETE && !status.checkProblemStatementSet()) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), + showMessageDialog(getWindowAncestor((Component) e.getSource()), + calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java index f91f8a7b..9debcdc8 100644 --- a/src/main/java/pulse/ui/frames/HistogramFrame.java +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -9,6 +9,7 @@ import javax.swing.JSlider; import pulse.search.statistics.ResidualStatistic; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.AuxPlotter; import pulse.ui.components.ResidualsChart; @@ -29,7 +30,9 @@ public HistogramFrame(AuxPlotter chart, int width, int height getContentPane().add(panel, SOUTH); slider.addChangeListener(e -> { ((ResidualsChart) chart).setBinCount(slider.getValue()); - plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic()); + var c = (Calculation) TaskManager.getManagerInstance().getSelectedTask() + .getResponse(); + plot(c.getOptimiserStatistic()); info.setText("Number of bins: " + slider.getValue()); }); } diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index fff33ecc..e4400d43 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -4,8 +4,11 @@ import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JInternalFrame; +import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.logs.Status; @@ -46,7 +49,7 @@ private void initComponents() { public void plot() { var task = TaskManager.getManagerInstance().getSelectedTask(); //do not plot tasks that are not finished - if (task != null && task.getCurrentCalculation().getStatus() != Status.IN_PROGRESS) { + if (task != null && task.getStatus() != Status.IN_PROGRESS) { Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); } } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 41d5bbf5..6acd7dab 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -14,8 +14,10 @@ import java.awt.BorderLayout; import java.awt.GridLayout; +import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; @@ -32,9 +34,11 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; +import pulse.input.ExperimentalData; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.statements.Problem; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; @@ -44,6 +48,7 @@ import pulse.ui.components.panels.ProblemToolbar; import pulse.ui.components.panels.SettingsToolBar; import pulse.ui.frames.TaskControlFrame.Mode; +import pulse.ui.frames.dialogs.ProgressDialog; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { @@ -108,22 +113,25 @@ public ProblemStatementFrame() { /* * Scheme list and scroller */ - schemeSelectionList = new JList(); + schemeSelectionList = new JList<>(); schemeSelectionList.setSelectionMode(SINGLE_SELECTION); - schemeSelectionList.setModel(new DefaultListModel()); + schemeSelectionList.setModel(new DefaultListModel<>()); schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { - if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) { - return; - } + if (TaskControlFrame.getInstance().getMode() == Mode.PROBLEM) { - var selectedValue = schemeSelectionList.getSelectedValue(); + var selectedValue = schemeSelectionList.getSelectedValue(); + + if (selectedValue != null) { + + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + } else { + changeSchemes(selectedValue); + } + + } - if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - } - else { - changeSchemes(selectedValue); } }); @@ -195,7 +203,7 @@ public void setSelectionPath(TreePath path) { //for all tasks instance.getTaskList().stream(). //select the problem statement of the current calculation - map(t -> t.getCurrentCalculation().getProblem()) + map(t -> ((Calculation) t.getResponse()).getProblem()) //that is non-null .filter(problem -> problem != null) //for each problem, update its properties in a separete thread @@ -215,13 +223,9 @@ public void update() { } private void update(SearchTask selectedTask) { - - if(selectedTask == null) - return; - - var calc = selectedTask.getCurrentCalculation(); - var selectedProblem = selectedTask == null ? null : calc.getProblem(); - var selectedScheme = selectedTask == null ? null : calc.getScheme(); + var calc = (Calculation) selectedTask.getResponse(); + var selectedProblem = calc.getProblem(); + var selectedScheme = calc.getScheme(); // problem if (selectedProblem == null) { @@ -237,95 +241,139 @@ private void update(SearchTask selectedTask) { setSelectedElement(schemeSelectionList, selectedScheme); schemeTable.setPropertyHolder(selectedScheme); } - } private void changeSchemes(DifferenceScheme newScheme) { var instance = TaskManager.getManagerInstance(); var selectedTask = instance.getSelectedTask(); + + var schemeLoaderTracker = new ProgressDialog(); + schemeLoaderTracker.setTitle("Initialising solution schemes..."); + schemeLoaderTracker.setLocationRelativeTo(null); + schemeLoaderTracker.setAlwaysOnTop(true); + + List> callableList; + if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public DifferenceScheme call() throws Exception { changeScheme(t, newScheme); - return t.getCurrentCalculation().getScheme(); + schemeLoaderTracker.incrementProgress(); + return ((Calculation) t.getResponse()).getScheme(); } }).collect(Collectors.toList()); + } else { + callableList = Arrays.asList(() -> { + changeScheme(selectedTask, newScheme); + return selectedTask.getResponse().getScheme(); + }); + } + + schemeLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { schemeListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } + }).thenRun(() -> { - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); - if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); - } + var c = (Calculation) selectedTask.getResponse(); + schemeTable.setPropertyHolder(c.getScheme()); + if (c.getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), + "High complexity", INFORMATION_MESSAGE); + } + Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); + }); } private void changeProblems(Problem newlySelectedProblem, Object source) { var instance = TaskManager.getManagerInstance(); - var selectedTask = instance.getSelectedTask(); + var task = instance.getSelectedTask(); + var selectedCalc = ((Calculation) task.getResponse()); + + var problemLoaderTracker = new ProgressDialog(); + problemLoaderTracker.setTitle("Changing problem statements..."); + problemLoaderTracker.setLocationRelativeTo(null); + problemLoaderTracker.setAlwaysOnTop(true); + + List> callableList; if (source != instance) { + //apply to all tasks if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public Problem call() throws Exception { changeProblem(t, newlySelectedProblem); - return t.getCurrentCalculation().getProblem(); + var result = ((Calculation) t.getResponse()).getProblem(); + problemLoaderTracker.incrementProgress(); + return result; } }).collect(Collectors.toList()); + + } //apply only to this task + else { + callableList = Arrays.asList(() -> { + changeProblem(task, newlySelectedProblem); + return ((Calculation) task.getResponse()).getProblem(); + }); + } + + problemLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { problemListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } - - } else { - changeProblem(selectedTask, newlySelectedProblem); } + ).thenRun(() -> { + problemTable.setPropertyHolder(selectedCalc.getProblem()); + // after problem is selected for this task, show available difference schemes + var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + schemes.forEach(s -> defaultModel.addElement(s)); + selectDefaultScheme(schemeSelectionList, selectedCalc.getProblem()); + schemeSelectionList.setToolTipText(null); + }); } - problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); - // after problem is selected for this task, show available difference schemes - var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); - defaultModel.clear(); - var schemes = newlySelectedProblem.availableSolutions(); - schemes.forEach(s -> defaultModel.addElement(s)); - selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); - schemeSelectionList.setToolTipText(null); - - Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - } private void changeProblem(SearchTask task, Problem newProblem) { - var data = task.getExperimentalCurve(); - var calc = task.getCurrentCalculation(); + var data = (ExperimentalData) task.getInput(); + var calc = (Calculation) task.getResponse(); var oldProblem = calc.getProblem(); // stores previous information var np = newProblem.copy(); if (oldProblem != null) { np.initProperties(oldProblem.getProperties().copy()); np.getPulse().initFrom(oldProblem.getPulse()); + np.setBaseline(oldProblem.getBaseline()); + np.updateProperties(np, data.getMetadata()); } calc.setProblem(np, data); // copies information from old problem to new problem type + if (oldProblem == null) { + np.retrieveData(data); + } + task.checkProblems(true); toolbar.highlightButtons(!np.isReady()); - } private static void selectDefaultScheme(JList list, Problem p) { @@ -348,8 +396,8 @@ private static void selectDefaultScheme(JList list, Problem p) private void changeScheme(SearchTask task, DifferenceScheme newScheme) { // TODO - var calc = task.getCurrentCalculation(); - var data = task.getExperimentalCurve(); + var calc = (Calculation) task.getResponse(); + var data = (ExperimentalData) task.getInput(); if (calc.getScheme() == null) { calc.setScheme(newScheme.copy(), data); diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 2d7eddab..567c4494 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -32,6 +32,7 @@ import pulse.search.direction.LMOptimiser; import pulse.search.direction.PathOptimiser; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.controllers.SearchListRenderer; @@ -132,15 +133,14 @@ public void update() { //model for the flags list already created if (rightTblModel instanceof SelectedKeysModel) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var searchKeys = activeTask.activeParameters(); ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); ((SelectedKeysModel) rightTblModel).update(searchKeys); } //Create a new model for the flags list else { - if (activeTask != null - && activeTask.getCurrentCalculation() != null - && activeTask.getCurrentCalculation().getProblem() != null) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var c = (Calculation)activeTask.getResponse(); + if (c != null && c.getProblem() != null) { + var searchKeys = activeTask.activeParameters(); rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); /* diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 3bcccead..f04f0504 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -113,8 +113,12 @@ private void initListeners() { @Override public void onProblemStatementShowRequest() { - problemStatementFrame.update(); - setProblemStatementFrameVisible(true); + if (TaskManager.getManagerInstance().getSelectedTask() != null) { + problemStatementFrame.update(); + setProblemStatementFrameVisible(true); + } else { + System.out.println("Please select a task"); + } } @Override diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 07936b10..2b1f888e 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -25,7 +25,9 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) + + if (m.getParameterCount() > 0 + || !Group.class.isAssignableFrom(m.getReturnType()) || m.getReturnType().isAssignableFrom(getClass())) { continue; } @@ -39,7 +41,7 @@ public List subgroups() { e.printStackTrace(); } - /* Ignore null, factor/instance methods returning same accessibles */ + /* Ignore null, factory/instance methods returning same accessibles */ if (a == null || a.getDescriptor().equals(getDescriptor())) { continue; } diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index 70bc5e28..ab8e7444 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -19,21 +19,21 @@ public abstract class UpwardsNavigable implements Descriptive { private UpwardsNavigable parent; - private List listeners = new ArrayList(); + private final List listeners = new ArrayList<>(); - public void removeHierarchyListeners() { + public final void removeHierarchyListeners() { this.listeners.clear(); } - public void removeHierarchyListener(HierarchyListener l) { + public final void removeHierarchyListener(HierarchyListener l) { this.listeners.remove(l); } - public void addHierarchyListener(HierarchyListener l) { + public final void addHierarchyListener(HierarchyListener l) { this.listeners.add(l); } - public List getHierarchyListeners() { + public final List getHierarchyListeners() { return listeners; } @@ -76,7 +76,6 @@ public UpwardsNavigable specificAncestor(Class aClas if (aClass.equals(this.getClass())) { return this; } - var parent = this.getParent(); UpwardsNavigable result = null; if (parent != null) { result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); From 00ba73373c4069dc48dc15d6246927442cee9ae0 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:35:04 +0300 Subject: [PATCH 101/116] Pull update --- pom.xml | 2 +- .../pulse/math/filters/PolylineOptimiser.java | 2 +- .../transforms/StandardTransformations.java | 3 - src/main/resources/NumericProperty.xml | 46 +- src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 4 +- src/main/resources/test/fft.txt | 1024 +++++++++++++++++ 7 files changed, 1069 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/test/fft.txt diff --git a/pom.xml b/pom.xml index ecbe713f..e5118865 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.95 + 1.97 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java index 06246988..dde0142a 100644 --- a/src/main/java/pulse/math/filters/PolylineOptimiser.java +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -87,4 +87,4 @@ public double evaluate(double t) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index c5b95034..a8206fdb 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,9 +19,6 @@ public class StandardTransformations { @Override public double transform(double a) { - if(a < 0) { - System.err.println(a); - } return log(a); } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 3d4b61ad..edf471e6 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -21,21 +21,31 @@ dimensionfactor="1" discreet="false" keyword="MODEL_WEIGHT" maximum="1" minimum="0" primitive-type="double" value="0"> + + + + + maximum="5" minimum="0" primitive-type="double" value="0.1" default-search-variable="false"> + + - + + Diathermic Sample with Grey Walls (1D) LinearizedProblem2D.Descriptor=Classical 2D Problem Statement
  • Based on 1D formulation, except:
  • Allows heat losses from side surface
  • Allows radial heat flow
UniformlyCoatedSample.Descriptor=Core-Shell 2D Problem Statement
  • Based on the classical 2D problem, except:
  • Explicitly accounts for a coating that covers front, rear, and side surfaces
  • Allows for axial, radial, and circumferential heat fluxes
NonlinearProblem.Descriptor=Nonlinear Heat Sink (1D) Problem Statement
  • Precise calculation of heat losses (front and rear only)
  • Cp and ρ data required
+TwoTemperatureModel.Descriptor=Two-Temperature Penetration Model (1D) Problem Statement
  • Different temperatures for solid and gas phase
  • Energy exchange between phases
  • Extended light penetration
  • Cp and ρ data required
Problem.6=Laser Pulse Problem.7=Heating Curve DATReader.0=dat @@ -289,4 +290,5 @@ MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
TextWrap.0=

TextWrap.1=

-TextWrap.2=

\ No newline at end of file +TextWrap.2=

+msg.running=An instance of PULsE appears to be running. Please switch back to the running version or delete the pulse.lock file found in the PULsE directory. \ No newline at end of file diff --git a/src/main/resources/test/fft.txt b/src/main/resources/test/fft.txt new file mode 100644 index 00000000..194936e8 --- /dev/null +++ b/src/main/resources/test/fft.txt @@ -0,0 +1,1024 @@ +1.8623883939948125 +1.440191140077873 +0.0856911188761609 +0.3177405081698117 +0.10853351481231108 +0.11844768772657704 +0.14359180808670602 +0.09056412538979447 +0.10023722372433143 +0.05012708645229069 +0.06281734105869234 +0.049631166773682844 +0.05334748215923658 +0.07407767757133589 +0.03971008132098835 +0.04416964802114836 +0.040107973059110145 +0.033383489903923126 +0.053510587820655715 +0.029764726896007627 +0.028897396016806746 +0.028375440254179138 +0.02608494923958421 +0.02848092345249004 +0.03098610667355402 +0.019704730700161342 +0.02205971150558122 +0.021542133187751365 +0.021717439637497112 +0.020225860751051376 +0.019475979475189267 +0.014608596264644893 +0.01835870883217293 +0.017665011148277704 +0.01752138440835296 +0.015525900169506836 +0.015595410516248704 +0.015566570463898585 +0.015953238738805293 +0.01599587158897621 +0.013360695667264524 +0.015002089502846793 +0.014731537902388517 +0.013800515283259545 +0.014315157889845456 +0.013575152863046239 +0.012352341075970283 +0.012855780651818575 +0.012137860345243811 +0.012335928227005669 +0.011322310696797096 +0.011236261387870374 +0.011574641436270015 +0.011701043926932157 +0.011357150931082414 +0.009886326266142687 +0.009786382245722685 +0.010208387344388233 +0.009714230475277096 +0.009956827366738348 +0.009664725534625166 +0.009191624259773164 +0.009872561989745792 +0.009201888705471856 +0.008827948804843581 +0.008959893796151698 +0.00945443404186388 +0.008745592717618387 +0.009110905334280571 +0.008596123252820742 +0.008184929886419213 +0.008608854455027131 +0.008307320168098539 +0.008117648970713836 +0.008024758407971955 +0.007628751198035526 +0.007083062409124776 +0.007751030347403762 +0.007249154231345055 +0.007175520037985863 +0.00707064335177111 +0.00711871897750612 +0.007305437461793674 +0.007072070255306337 +0.006707680163861008 +0.006303859175803904 +0.007416579081448052 +0.0070619057661997445 +0.006854023369349145 +0.006455665945038045 +0.006835208781953551 +0.0065725374488924135 +0.006131013167403963 +0.006444591376811081 +0.0056394266037035545 +0.006066231052847979 +0.006303991327230198 +0.005949270797656248 +0.0063450056763406275 +0.006196167505123584 +0.005858153424762702 +0.0053913180909300855 +0.0058747255763572604 +0.006090643974924875 +0.005589115745925811 +0.005505074654993644 +0.005588245995291544 +0.005250392382850327 +0.005369156668180935 +0.005194106014717803 +0.005358717441455797 +0.005321043318596023 +0.005012422951166697 +0.005505553842319726 +0.0053432974110541155 +0.004971133806504812 +0.005037290685910062 +0.004765936894177503 +0.005141737892182391 +0.005410339758037131 +0.004885897265174704 +0.004922942306263837 +0.0045790685496497835 +0.00458229361635885 +0.004708660507341414 +0.0048042899663832475 +0.004625127808914531 +0.004743025594503575 +0.004598808630414814 +0.004626386499782302 +0.004459853592652658 +0.004377955368359365 +0.0046513163879235405 +0.0043256859045455115 +0.004512982427236372 +0.004588981897250833 +0.004295974465089781 +0.0043551312599052466 +0.004100096307988168 +0.004353555159813722 +0.00413193523732321 +0.004119654488401856 +0.0035986600134537334 +0.00430772639995229 +0.004058684144930945 +0.004194927813093337 +0.004135454329628857 +0.003707416777758124 +0.004024729209517794 +0.004139371147329603 +0.004167089013471642 +0.003761231388360764 +0.003952355678601933 +0.004066041106296513 +0.004136911700384993 +0.003932359264558167 +0.003542614608770456 +0.003746570018068218 +0.00395090631076722 +0.003537222144720005 +0.0035297026799832177 +0.003925348847332404 +0.0036757151959072244 +0.003602224343978626 +0.0036161028088550077 +0.0036307723158452575 +0.00368144084363016 +0.0036600294498210996 +0.00363782935999298 +0.003405756152194927 +0.003431252110629916 +0.0035649169954907745 +0.003236389910029101 +0.0034433473114116307 +0.0034932650584519062 +0.0035634560282200027 +0.0034296977806162633 +0.0034319461285292714 +0.003312308846263632 +0.003722618636988596 +0.0032536729682297472 +0.0031759378052392275 +0.00314708262406618 +0.003381194017340103 +0.0034226632338035484 +0.003201761963155763 +0.0034019628513818913 +0.0032004395904046603 +0.003102610835125887 +0.003323853973090416 +0.00301143619457104 +0.0033706851832347637 +0.002997554103076301 +0.0034025072727111427 +0.003206086938481629 +0.0031066962307518854 +0.002786437309626743 +0.0030835387904124883 +0.003031043385463557 +0.003011272769112621 +0.003026614277513354 +0.0031038064873766267 +0.002914867534237071 +0.0032106014711124264 +0.002960737750691456 +0.002848084333438608 +0.0029144693542782463 +0.002917497771325989 +0.0027129790526007544 +0.002612885902673647 +0.0029181831060856893 +0.0029055373553426543 +0.002714169194398066 +0.002863600775792233 +0.002684118883481437 +0.0028921148049106745 +0.0028302475704821105 +0.0028594112091045497 +0.0028091941754447122 +0.002761917483210222 +0.002831998224106552 +0.0029082049049603256 +0.0025433232627721726 +0.002744779635531415 +0.002610372762493025 +0.00269604297513204 +0.002522668560461302 +0.0027325824152729257 +0.0024340793422731313 +0.0026319425441122444 +0.002568112067959159 +0.0027683254158293864 +0.0024581501074080517 +0.00272139350102542 +0.0024097746095871524 +0.0026713014097986495 +0.002608528482915483 +0.002757952020574316 +0.002473940039239647 +0.0025698875343990245 +0.0024206272171112667 +0.002527643420140039 +0.002485348316653174 +0.002501106850185462 +0.0028036894150320207 +0.0023487069284162656 +0.002494732633732202 +0.002543346916671682 +0.002434448718146823 +0.002353903482083443 +0.0024849214295775495 +0.002280687444001982 +0.0021460731943987506 +0.0022744284159968734 +0.002355282996760084 +0.002374879375501902 +0.0025636565520782252 +0.0021429362225892585 +0.002512462546679564 +0.002457432015923903 +0.0022275839408261297 +0.002050220061718769 +0.0023897363016175868 +0.002280185276629757 +0.002551552138995319 +0.0020281057349859953 +0.0021776305132253397 +0.0021716442295787192 +0.0022024922812540998 +0.0022643433828360574 +0.0023171079460284592 +0.002209610418455076 +0.002091761906587569 +0.0024699303868929044 +0.0021924456147592924 +0.0025132238234213977 +0.0021413493664871534 +0.0023797515636802095 +0.002116464198887065 +0.0021010965208245326 +0.002200893207135162 +0.0022888366645033406 +0.0022506744392961795 +0.0020200323013424798 +0.0020269379217901146 +0.002235674958617841 +0.0021589634253941837 +0.002007694399579814 +0.002028468326489692 +0.001973178947286009 +0.0021722784832706923 +0.002152321588552352 +0.0020541661208640333 +0.0018443215090906614 +0.0023040970417671667 +0.0022070756163091905 +0.0021458535206313593 +0.0021234981725089773 +0.0020196842506682777 +0.002081325554159696 +0.0019622403312532185 +0.0018956765850871688 +0.002011758678159863 +0.002048584744472413 +0.001967968629034845 +0.0017890536700308419 +0.0018274200657948042 +0.0019495255858672576 +0.0019763604713690084 +0.002007665612620955 +0.002038019646262002 +0.0019323145911434045 +0.0018931617505309171 +0.0017176101935718177 +0.002035717753676598 +0.001997221376476268 +0.00208445761810857 +0.0018213990827714713 +0.0020335566706469687 +0.0019222800995692716 +0.0018730106550425844 +0.0018957021931545856 +0.0020131587529492274 +0.0019006911064656951 +0.0019006385154010802 +0.0018669836596750347 +0.0018387111891236968 +0.001860803645995648 +0.001875487905261806 +0.0019805330604071637 +0.0019333574999443324 +0.0017493923270269086 +0.0018635151864636905 +0.0018839497921766995 +0.001779170384859654 +0.0017369124667888725 +0.0018692706792106512 +0.0018555909898186722 +0.0019861219387481703 +0.0016768772425769986 +0.00173425585005027 +0.0017368104770272986 +0.0018444509218926088 +0.0018344780148905706 +0.0017421482008388048 +0.0019574501495635828 +0.0018485551997456673 +0.0016478397506516388 +0.0019006059767611814 +0.00178908230798387 +0.0017626449686498368 +0.001732149103600908 +0.0017420128347331559 +0.0016694600796223398 +0.0016892814923382899 +0.0017537019545246677 +0.0018336543315263922 +0.0018812436665203323 +0.0017781051942758364 +0.0016246718077691606 +0.0016673577866322385 +0.001759831952508731 +0.0017473637258567304 +0.00173104355552501 +0.001805140539073113 +0.0015973095237779042 +0.0015894420732170923 +0.001654768691098651 +0.0017328672602817553 +0.0018102375834557266 +0.0016478489084596247 +0.0016625501301660536 +0.001755668369649966 +0.0015363102784954816 +0.0017291672122388942 +0.0017614055969386695 +0.001711801977088871 +0.0016645828526783177 +0.0017291523364988042 +0.0017553659401122418 +0.0017300557169120786 +0.0016118911737583585 +0.0016013197537592275 +0.0017380041852213207 +0.0014933333031327699 +0.001559188637981803 +0.0017126653445467563 +0.0015807009808334307 +0.0016628973432414047 +0.0016047005870487945 +0.0017559387040599565 +0.0016243236914338785 +0.0015765644690180533 +0.0015828719203620945 +0.0015398177507824152 +0.0015032959585347633 +0.0016490341843532685 +0.001617396634735605 +0.0015957274117611951 +0.0015794738352842284 +0.0015388573781546031 +0.0016154513019219219 +0.0014816121634873749 +0.0015287905271643165 +0.0014617249768153456 +0.0015711554607759192 +0.0014682188208443925 +0.0015796458655902039 +0.001689735159818995 +0.001433704731383778 +0.0016166759882060668 +0.0014391892059119086 +0.0013712295490970739 +0.0016812167351267391 +0.0015319778370590715 +0.0014236368531158066 +0.0015087839252398833 +0.0016085941140473177 +0.0014820936165921505 +0.0014688802804647845 +0.0014354622391669247 +0.0015967583162015352 +0.0014216344753546506 +0.0015161040951119505 +0.0015096607386122969 +0.001384397608374938 +0.0015245746520588476 +0.0014081823605333326 +0.0016024041035034328 +0.001452871449554933 +0.0014308607906196105 +0.001522136599814874 +0.001481124047817481 +0.0015255580662159325 +0.001309843943187161 +0.0014586938481273024 +0.0014656459708439035 +0.0012726184983539426 +0.001367916494362877 +0.0013395100945080495 +0.0014389087123870105 +0.0015167920773429093 +0.0014722043865967177 +0.0014019882776771784 +0.0014049321693303425 +0.0013847814076527866 +0.0012478724504164254 +0.001459103728716397 +0.001406587374258575 +0.0014418827202656941 +0.0014096520586717536 +0.0014492409154258758 +0.001409394534879737 +0.0014705862502728947 +0.0012983605478659292 +0.001359781891861031 +0.0013736087171087959 +0.0015844689960084271 +0.001284557520511467 +0.0014926221038714146 +0.0013154469364888553 +0.0013159171219375757 +0.0013195726347157337 +0.0013224667986831112 +0.0013970767109930781 +0.0013792554501893374 +0.001302973911800565 +0.0014168082576272456 +0.0012576820832382774 +0.001542675666003404 +0.001307463747149029 +0.0012699109134222467 +0.0014539429326068782 +0.0013921268253928327 +0.0012829264103424237 +0.0014453813185194514 +0.0013361946194903225 +0.0012859127293610348 +0.001219864587298067 +0.0013729822159914303 +0.0013387240271219942 +0.001269166882994578 +0.0013816792929721383 +0.0013093666697172612 +0.0013326007495184746 +0.001334993151008571 +0.001235037425193347 +0.0011362030225542878 +0.0014377371705801458 +0.0013853954016113952 +0.0013471913518237851 +0.0012495283227950647 +0.001266562246997323 +0.0013227908159747438 +0.0012995488131381518 +0.0013904367286179792 +0.0012906469430955612 +0.0013315397480469341 +0.0012934821261761714 +0.0013250673575971874 +0.0013469150077112225 +0.0013202875781665269 +0.0012146174018993268 +0.00122881580627127 +0.0014129481339015366 +0.0011903810318464286 +0.0012449686959895507 +0.0012651176467834173 +0.0013046110131927327 +0.001237171432692225 +0.0011075767342333515 +0.0012758702965103852 +0.0011920086165592336 +0.0011866092015483018 +0.0012707921781073572 +0.001219579012657507 +0.0013616560614411427 +0.0012936725062240194 +0.00123301890710047 +0.0013025018941970037 +0.0011961146586798811 +0.0012808758077574244 +0.001190999826367067 +0.0012141207065111215 +0.0011843613569923832 +0.001335136462186405 +0.0011539681513207233 +0.0013632022792263935 +0.0013805744039978088 +0.0011881868755503888 +0.0011939077843169824 +0.0011270966583446721 +0.0012274172725854615 +0.0012672896373819246 +0.0012091250192438592 +0.001260421275046162 +0.0013151663232838697 +0.0012249004166542665 +0.00117899397310745 +0.0011734446482045359 +0.0012624596819513108 +0.0012764495812893665 +0.0010964856284655289 +0.001252761077726981 +0.0012529236282346054 +0.0012716297422796393 +0.0012861540922519037 +0.0011956481563782898 +0.0011121730631293352 +0.0012784895399416258 +0.0012202804971150033 +0.001045606751447345 +0.0013908900453571335 +0.001197171130379206 +0.0011705475688395964 +0.001269776840408273 +0.0011822952280807005 +0.0011066157789244133 +0.0012798660547691063 +0.0012809823962816405 +0.001109278093988003 +0.0011861159004747741 +0.0010835188502740056 +0.0011746191332898408 +0.001230977749834832 +0.0010795055496552108 +0.0012635419825219678 +0.0012756553936316552 +0.0011898806099122639 +0.0011608250848784943 +0.0012017305666559433 +0.001133310479022337 +0.0012358558242260653 +0.0011754317432781062 +0.0011033932179006088 +0.0011222424420222205 +0.0010375205291658766 +0.0012348160575316306 +0.0011826927315491012 +0.0010671410415516137 +0.0011711615448849848 +0.0011226251500075733 +0.00109441076055885 +0.001094864463168803 +0.0012022274437243083 +0.0011448479964942628 +0.001177852369182545 +0.0011002194983556388 +0.0011536796847610848 +0.0012020752603471268 +0.0010927386293982887 +0.00119823947194137 +0.0010685209532565016 +0.0011808365130782394 +0.0011372096566268058 +0.0011557260888021865 +0.0011304444819921215 +0.0011073395461831943 +0.0011146527005709973 +0.0011737068014750476 +0.001196673453050192 +0.001099569016246178 +0.0010796933440538885 +0.001043599943288889 +0.001153815061186113 +0.00131400849101086 +0.0011597212267390755 +0.0012451816466449049 +0.0010969945781010115 +0.0011004892551955102 +0.0011490190194486621 +0.0012283203493037387 +0.0010586820817033659 +0.0011891430899622388 +0.0011420664067813631 +0.0010229478025710005 +0.0010691985495450338 +0.0012006413404640646 +0.0011247227156228196 +0.0011907207706990607 +0.0010624252987184526 +0.0010228569653310151 +0.0011311460363274618 +0.0010831867629922491 +0.001078372575538281 +0.0011211665776477088 +0.0011605348662726803 +0.0010468906446104121 +0.001111427826815859 +0.0010188359106680675 +0.0011832758640857235 +0.0010596513093145692 +0.0011528677436051875 +0.0011282187354133004 +0.0010452271074060323 +0.0011074716717346302 +0.0011205642580078064 +9.867064467871667E-4 +0.001075272432523398 +9.872992855691574E-4 +0.0011157348160055031 +0.0011666183330647387 +0.001134882543661718 +0.0010855425213214857 +9.486135179973545E-4 +0.001022739916990013 +0.0011230768811140068 +0.0011034279475247624 +0.0011256491710883978 +0.0010678857770924672 +0.0010462878905578591 +0.001126936721613097 +0.0010518572445067302 +0.00100850428956523 +0.0011262068223730414 +0.0011005048073512705 +0.0011543348908842977 +0.0010939783368006336 +0.001022253007938055 +9.327735142685685E-4 +0.0011040492611731388 +0.001031427321278705 +0.0011174208044228532 +0.0010711547316751779 +0.0010263748860923343 +9.271388117916817E-4 +0.0010119339016748938 +0.0010729189004375487 +0.001017358352006335 +0.0012325543901770487 +0.0010559322713433975 +0.0010388280188947873 +0.0010112348874334633 +0.0011041523292409918 +0.001139488720022 +9.478706332814315E-4 +0.0010618470148916553 +0.0010535115515057064 +0.0011410732587352308 +0.0010390307941775675 +0.0011029457801466742 +0.00113809700552862 +0.0010230097035513453 +9.54459369975072E-4 +9.575993594545408E-4 +0.0010089179546782145 +0.0010742472276899845 +0.0010508183691489978 +0.001015706339611075 +9.63931367881156E-4 +9.892104999012375E-4 +0.0011395912499243275 +0.001027489144123793 +9.938574333827525E-4 +0.0010141638067099764 +0.0010850567240691322 +0.0010567209240592832 +0.0010546186206946042 +0.0010591449258845588 +0.0010788103431173524 +0.001001327795102536 +0.0010537475604692989 +9.776562827771178E-4 +0.0010301238724556479 +9.999903298850048E-4 +9.996640973755774E-4 +9.982351690180635E-4 +0.0010282243521071899 +9.793735100862085E-4 +9.937214763986947E-4 +0.0010560997662210044 +0.001064940358969091 +9.321880074986722E-4 +0.0010448277305721279 +9.336692191825015E-4 +0.001083214198713888 +0.0010289052566263427 +0.0010878579899422471 +9.628443269300946E-4 +9.148432953590753E-4 +0.0010009806857296107 +0.001064634768220053 +0.0010917456341344672 +0.001039660018309393 +9.640620972111818E-4 +9.473501095787988E-4 +8.898399478407448E-4 +0.0010752641810434242 +0.0010225882851613213 +0.001031186267761093 +0.001032388300708419 +9.692852639524189E-4 +0.0010636290585673226 +0.0011157012652095268 +9.445338312394765E-4 +9.796550027818063E-4 +9.380874632594835E-4 +9.171090871220963E-4 +0.0010338819871365346 +9.817352980271506E-4 +0.001035548610723382 +0.0010366089040039462 +0.0010023017609384433 +0.0010350418131665217 +0.0010078566274789762 +8.68331298898284E-4 +8.974661539826759E-4 +9.705357497685943E-4 +0.0010949970765370904 +9.495183412072032E-4 +9.737454775958202E-4 +0.0010217995814455307 +9.522448537898191E-4 +9.779127147104816E-4 +9.86232475982084E-4 +0.0010298898005799344 +0.0010131585301009062 +0.0010574032640673276 +9.48474613345645E-4 +0.0010620791774603989 +9.276998623420052E-4 +9.204849451858387E-4 +0.0010399404511545424 +9.264433947557337E-4 +9.089041102056008E-4 +8.985993184669925E-4 +0.0010315295220899943 +0.0010147607153646264 +9.377201100029269E-4 +9.537576069215683E-4 +0.0010009682437537631 +9.99695015129562E-4 +9.242726798056872E-4 +9.24635450522338E-4 +9.956002157419526E-4 +0.0010611750592811704 +0.0010763265510399733 +9.299215963015973E-4 +9.559145806317533E-4 +8.895396926441966E-4 +0.0010214904500228136 +0.0010665758733511713 +9.38328912522674E-4 +9.979567946888498E-4 +9.265333791378834E-4 +9.839671786840895E-4 +0.001028895479833451 +8.506542503867044E-4 +0.0010133621193351137 +8.787126554211259E-4 +9.963469146455363E-4 +9.615126345157179E-4 +9.85958165485008E-4 +9.00999168671934E-4 +9.22395160595154E-4 +9.847387915653623E-4 +9.550681987435555E-4 +0.0010157534718449773 +9.033087045773685E-4 +0.0010196936306393514 +9.886304333696812E-4 +9.621123908614798E-4 +9.961358429027196E-4 +9.185365383486628E-4 +9.802937997516852E-4 +0.0010325104413239838 +8.856756388963145E-4 +9.561355684135135E-4 +9.657359083146397E-4 +9.274753354606738E-4 +9.817087748994818E-4 +9.290601376764568E-4 +9.549805031394914E-4 +0.0010053984111506784 +0.0010171563035885468 +8.490199507419586E-4 +9.56482243200863E-4 +9.605373194334563E-4 +0.0010119157832749685 +8.622504120430778E-4 +9.827176707024432E-4 +9.373533932955863E-4 +9.675303640829435E-4 +0.0010249931272057681 +9.61126280284176E-4 +9.184629421665977E-4 +0.0010050193148524005 +9.787415813740507E-4 +8.972025579781401E-4 +9.687533583789295E-4 +8.925667325497291E-4 +9.642009967372524E-4 +9.053263729012275E-4 +9.921545067133972E-4 +8.908052742353586E-4 +9.467385459143673E-4 +8.734896034648221E-4 +0.001006776295415798 +9.648248427972446E-4 +0.0010178447964271453 +0.0010334043744897557 +9.623872532366822E-4 +8.08721329670184E-4 +8.981291571899335E-4 +8.965830799635602E-4 +9.411649803398181E-4 +9.233563139416925E-4 +9.427443681121117E-4 +9.942239823302458E-4 +9.088242408932741E-4 +9.540261346786916E-4 +9.741512437633195E-4 +9.305784543443058E-4 +8.936810810671528E-4 +8.922716098607077E-4 +9.078055011613465E-4 +9.380441154268395E-4 +9.293086318591539E-4 +9.975803217654006E-4 +8.729079377154703E-4 +8.944054849967194E-4 +9.603926066129352E-4 +9.359497708437436E-4 +8.584287049822975E-4 +9.084124785513469E-4 +9.690922729424672E-4 +9.577562696746035E-4 +8.787112588322701E-4 +9.25262999202752E-4 +9.069952161436504E-4 +8.553040119121029E-4 +9.441837660562317E-4 +8.881766062200255E-4 +9.182443930143199E-4 +9.210270704674465E-4 +0.0010207690003883872 +9.212511483642543E-4 +9.697071438700654E-4 +8.728908036006088E-4 +0.0010346924699559093 +9.98847382922486E-4 +9.243479071810528E-4 +8.86103374277685E-4 +8.924994979962734E-4 +8.810799504326165E-4 +0.0010062064982125846 +9.448553865605595E-4 +9.382799635233933E-4 +9.304974738814165E-4 +0.0010457539120059794 +8.357527786592987E-4 +9.217727903643321E-4 +9.179572581649916E-4 +9.168779812731295E-4 +9.622433504513817E-4 +9.32675306480942E-4 +8.553908706400959E-4 +9.83197977629328E-4 +8.380823090224836E-4 +9.098683129447309E-4 +9.521735462902145E-4 +9.234869018250256E-4 +9.101871270324486E-4 +9.571374853617023E-4 +9.014654684161298E-4 +9.454236030554885E-4 +8.743210842609578E-4 +0.001009335913540478 +8.942776328813976E-4 +9.46445773117879E-4 +9.261167832501032E-4 +9.36900494161455E-4 +0.0010312031257503273 +9.900728853623566E-4 +8.952595563375166E-4 +9.834533909209228E-4 +8.663718595624797E-4 +9.043261594713736E-4 +9.279968431679345E-4 +8.750985392750564E-4 +9.371156720928401E-4 +8.549939108391614E-4 +9.106702956319355E-4 +9.733885260125103E-4 +9.251988897112464E-4 +9.163237448419129E-4 +9.763413853652612E-4 +8.17850953219026E-4 +9.578846196673777E-4 +8.365217856229663E-4 +0.0010026936481388117 +7.928175701311635E-4 +8.893189554431385E-4 +9.017435287623222E-4 +8.783729172775835E-4 +8.669714891290667E-4 +8.950098150254109E-4 +9.102292374057512E-4 +8.678038819327828E-4 +9.072342094860982E-4 +9.204681033012889E-4 +9.140102265789373E-4 +9.679079074939713E-4 +8.97266035411225E-4 +9.253785287178447E-4 +8.87548440924595E-4 +8.982166442089941E-4 +9.331063397107384E-4 +9.793688190172947E-4 +8.571873137309112E-4 +7.653171049075817E-4 +9.437126053661114E-4 +9.130674200589031E-4 +9.921485769821788E-4 +9.309642233887024E-4 +9.060627123548954E-4 +8.980914435685737E-4 +9.652877389717347E-4 +9.047117932972571E-4 +9.667537814851369E-4 +9.535766035259877E-4 +8.941375274267972E-4 +8.984040549051778E-4 +8.652377377593744E-4 +9.20640529777493E-4 +9.180878222942158E-4 +9.559498677603602E-4 +9.036754182526869E-4 +8.948543811167884E-4 +8.577419937245519E-4 +9.328693608319587E-4 +9.448588934203852E-4 +8.874946478023135E-4 +9.201634267392057E-4 +8.218524536406166E-4 +9.554481063991601E-4 +8.727141872122932E-4 +0.0010440163697008847 +8.73836572999781E-4 +8.474705098276823E-4 +9.242788766551806E-4 +9.6628533033844E-4 +9.552653779506281E-4 +8.541963891119773E-4 +9.447177794880269E-4 +8.826859272186537E-4 +8.329850473356033E-4 +8.450126312152971E-4 +8.878943253854854E-4 +9.061636989099398E-4 +9.572319794719509E-4 +9.622037024296709E-4 +9.500723242444433E-4 +9.32405414821388E-4 +8.565420408414186E-4 +8.399901318746076E-4 +9.695118362994088E-4 +8.550086248119928E-4 +8.940615541457234E-4 +0.0010099358754753034 +9.203713767686853E-4 +8.886241205073025E-4 +9.380363126489836E-4 +8.853380504739757E-4 +9.403908197696909E-4 +8.631159768530599E-4 +9.595645004578227E-4 +9.477911078217122E-4 +8.587532061341414E-4 +9.110737725752171E-4 +8.817375500176881E-4 +8.506326823980858E-4 +0.0010001005385404018 +9.101279499969762E-4 +8.321551714226473E-4 +9.509451733575742E-4 +8.358075345119111E-4 +9.362701841463229E-4 +8.750119106584959E-4 +8.856057393331677E-4 +9.750659213866944E-4 +9.699331289208207E-4 +8.291897553302293E-4 +9.72959515072471E-4 \ No newline at end of file From 4cb45c7b3553843301d0f849e1bc583ef3b5cda1 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 16:11:46 +0300 Subject: [PATCH 102/116] Updated splash screen --- src/main/resources/images/splash.png | Bin 67897 -> 67108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index d8ce89ad9ca02f2e8a043e095b8ab888cbce8813..c9583b6ef18dc2c1130fd57b2d9804f368219730 100644 GIT binary patch literal 67108 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 literal 67897 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 N@O1TaS?83{1OQJbFdhH^ From f1cb16876f68efd30db61bd4b3db6e6360fb702e Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 16:11:46 +0300 Subject: [PATCH 103/116] Updated splash screen --- src/main/resources/images/splash.png | Bin 67897 -> 67108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index d8ce89ad9ca02f2e8a043e095b8ab888cbce8813..c9583b6ef18dc2c1130fd57b2d9804f368219730 100644 GIT binary patch literal 67108 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 literal 67897 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 N@O1TaS?83{1OQJbFdhH^ From 475d09b60f8ffbfcdba2fb774e8a59d830dc2874 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sun, 25 Dec 2022 12:17:04 +0300 Subject: [PATCH 104/116] - Made an AbstractLogger superclass - Moved text logging capabilities to TextLogPaneExporter - Added reference to specific Calculation in log entries - Separated MVC functions in logging --- ...Exporter.java => TextLogPaneExporter.java} | 24 ++-- .../java/pulse/search/statistics/FTest.java | 2 +- .../java/pulse/tasks/logs/DataLogEntry.java | 1 + src/main/java/pulse/tasks/logs/Log.java | 13 ++ src/main/java/pulse/tasks/logs/LogEntry.java | 9 +- .../pulse/ui/components/AbstractLogger.java | 52 +++++++ .../java/pulse/ui/components/LogPane.java | 136 ------------------ .../java/pulse/ui/components/TextLogPane.java | 108 ++++++++++++++ src/main/java/pulse/ui/frames/LogFrame.java | 28 ++-- .../pulse/ui/frames/TaskControlFrame.java | 4 +- 10 files changed, 212 insertions(+), 165 deletions(-) rename src/main/java/pulse/io/export/{LogPaneExporter.java => TextLogPaneExporter.java} (67%) create mode 100644 src/main/java/pulse/ui/components/AbstractLogger.java delete mode 100644 src/main/java/pulse/ui/components/LogPane.java create mode 100644 src/main/java/pulse/ui/components/TextLogPane.java diff --git a/src/main/java/pulse/io/export/LogPaneExporter.java b/src/main/java/pulse/io/export/TextLogPaneExporter.java similarity index 67% rename from src/main/java/pulse/io/export/LogPaneExporter.java rename to src/main/java/pulse/io/export/TextLogPaneExporter.java index ade0e0fb..e6ea39e1 100644 --- a/src/main/java/pulse/io/export/LogPaneExporter.java +++ b/src/main/java/pulse/io/export/TextLogPaneExporter.java @@ -4,22 +4,23 @@ import java.io.FileOutputStream; import java.io.IOException; +import javax.swing.JEditorPane; import javax.swing.text.BadLocationException; import javax.swing.text.html.HTMLEditorKit; -import pulse.ui.components.LogPane; +import pulse.ui.components.TextLogPane; /** * Similar to a {@code LogExporter}, except that it works only on the contents * of a {@code LogPane} currently being displayed to the user. * */ -public class LogPaneExporter implements Exporter { +public class TextLogPaneExporter implements Exporter { - private static LogPaneExporter instance = new LogPaneExporter(); + private static TextLogPaneExporter instance = new TextLogPaneExporter(); - private LogPaneExporter() { + private TextLogPaneExporter() { // intentionally blank } @@ -29,10 +30,11 @@ private LogPaneExporter() { * argument is ignored. After exporting, the stream is explicitly closed. */ @Override - public void printToStream(LogPane pane, FileOutputStream fos, Extension extension) { - var kit = (HTMLEditorKit) pane.getEditorKit(); + public void printToStream(TextLogPane pane, FileOutputStream fos, Extension extension) { + var editorPane = (JEditorPane) pane.getGUIComponent(); + var kit = (HTMLEditorKit) editorPane.getEditorKit(); try { - kit.write(fos, pane.getDocument(), 0, pane.getDocument().getLength()); + kit.write(fos, editorPane.getDocument(), 0, editorPane.getDocument().getLength()); } catch (IOException | BadLocationException e) { System.err.println("Could not export the log pane!"); e.printStackTrace(); @@ -50,7 +52,7 @@ public void printToStream(LogPane pane, FileOutputStream fos, Extension extensio * * @return an instance of{@code LogPaneExporter}. */ - public static LogPaneExporter getInstance() { + public static TextLogPaneExporter getInstance() { return instance; } @@ -58,8 +60,8 @@ public static LogPaneExporter getInstance() { * @return {@code LogPane.class}. */ @Override - public Class target() { - return LogPane.class; + public Class target() { + return TextLogPane.class; } /** @@ -70,4 +72,4 @@ public Extension[] getSupportedExtensions() { return new Extension[]{HTML}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 4d9da690..4723f5f6 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -137,4 +137,4 @@ public static Calculation findNested(Calculation a, Calculation b) { return aParams > bParams ? b : a; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 2fad022e..4e17c87c 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -2,6 +2,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; +import pulse.Response; import pulse.math.Parameter; import pulse.math.ParameterIdentifier; import pulse.properties.NumericProperties; diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 5b519531..11534996 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -1,6 +1,8 @@ package pulse.tasks.logs; import java.time.LocalTime; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -186,5 +188,16 @@ public static boolean isVerbose() { public static void setVerbose(boolean verbose) { Log.verbose = verbose; } + + /** + * Time taken where the first array element contains seconds [0] and the second contains milliseconds [1]. + * @return an array of long values that sum um to the time taken to process a task + */ + + public long[] timeTaken() { + var seconds = SECONDS.between(getStart(), getEnd()); + var ms = MILLIS.between(getStart(), getEnd()) - 1000L * seconds; + return new long[] {seconds, ms}; + } } diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index 7841a910..bfd84fbd 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Objects; +import pulse.Response; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -21,7 +22,8 @@ public class LogEntry { private Identifier identifier; private LocalTime time; - + private final Response response; + /** *

* Creates a {@code LogEntry} from this {@code SearchTask}. The data of the @@ -34,6 +36,11 @@ public LogEntry(SearchTask t) { Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); + this.response = t.getResponse(); + } + + public Response getResponse() { + return response; } public Identifier getIdentifier() { diff --git a/src/main/java/pulse/ui/components/AbstractLogger.java b/src/main/java/pulse/ui/components/AbstractLogger.java new file mode 100644 index 00000000..916e678c --- /dev/null +++ b/src/main/java/pulse/ui/components/AbstractLogger.java @@ -0,0 +1,52 @@ +package pulse.ui.components; + +import java.util.concurrent.ExecutorService; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import javax.swing.JComponent; +import pulse.tasks.TaskManager; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; +import pulse.util.Descriptive; + +public abstract class AbstractLogger implements Descriptive { + + private final ExecutorService updateExecutor = newSingleThreadExecutor(); + + public synchronized void update() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task == null) { + return; + } + + var log = task.getLog(); + + if (!log.isStarted()) { + return; + } + + post(log.lastEntry()); + } + + public ExecutorService getUpdateExecutor() { + return updateExecutor; + } + + public synchronized void callUpdate() { + updateExecutor.submit(() -> update()); + } + + public abstract JComponent getGUIComponent(); + public abstract void printTimeTaken(Log log); + public abstract void post(LogEntry logEntry); + public abstract void post(String text); + public abstract void postAll(); + public abstract void clear(); + public abstract boolean isEmpty(); + + @Override + public String describe() { + return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java deleted file mode 100644 index 5af4215a..00000000 --- a/src/main/java/pulse/ui/components/LogPane.java +++ /dev/null @@ -1,136 +0,0 @@ -package pulse.ui.components; - -import static java.lang.System.err; -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; -import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; -import static pulse.tasks.logs.Status.DONE; -import static pulse.ui.Messages.getString; - -import java.io.IOException; -import java.util.concurrent.ExecutorService; - -import javax.swing.JEditorPane; -import javax.swing.text.BadLocationException; -import javax.swing.text.DefaultCaret; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; - -import pulse.tasks.TaskManager; -import pulse.tasks.logs.Log; -import pulse.tasks.logs.LogEntry; -import pulse.util.Descriptive; - -@SuppressWarnings("serial") -public class LogPane extends JEditorPane implements Descriptive { - - private ExecutorService updateExecutor = newSingleThreadExecutor(); - - public LogPane() { - super(); - setContentType("text/html"); - setEditable(false); - var c = (DefaultCaret) getCaret(); - c.setUpdatePolicy(ALWAYS_UPDATE); - } - - private void post(LogEntry logEntry) { - post(logEntry.toString()); - } - - /* - private void postError(String text) { - var sb = new StringBuilder(); - sb.append(getString("DataLogEntry.FontTagError")); - sb.append(text); - sb.append(getString("DataLogEntry.FontTagClose")); - post(sb.toString()); - }*/ - private void post(String text) { - - final var doc = (HTMLDocument) getDocument(); - final var kit = (HTMLEditorKit) this.getEditorKit(); - try { - kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); - } catch (BadLocationException e) { - err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ - e.printStackTrace(); - } catch (IOException e) { - err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ - e.printStackTrace(); - } - - } - - public void printTimeTaken(Log log) { - var seconds = SECONDS.between(log.getStart(), log.getEnd()); - var ms = MILLIS.between(log.getStart(), log.getEnd()) - 1000L * seconds; - var sb = new StringBuilder(); - sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ - sb.append(seconds + getString("LogPane.Seconds")); //$NON-NLS-1$ - sb.append(ms + getString("LogPane.Milliseconds")); //$NON-NLS-1$ - post(sb.toString()); - } - - public synchronized void callUpdate() { - updateExecutor.submit(() -> update()); - } - - public void printAll() { - clear(); - - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task != null) { - - var log = task.getLog(); - - if (log.isStarted()) { - - log.getLogEntries().stream().forEach(entry -> post(entry)); - - if (task.getStatus() == DONE) { - printTimeTaken(log); - } - - } - - } - - } - - private synchronized void update() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task == null) { - return; - } - - var log = task.getLog(); - - if (!log.isStarted()) { - return; - } - - post(log.lastEntry()); - } - - public void clear() { - try { - getDocument().remove(0, getDocument().getLength()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } - - public ExecutorService getUpdateExecutor() { - return updateExecutor; - } - - @Override - public String describe() { - return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); - } - -} diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java new file mode 100644 index 00000000..aaf318ae --- /dev/null +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -0,0 +1,108 @@ +package pulse.ui.components; + +import static java.lang.System.err; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; +import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; +import static pulse.tasks.logs.Status.DONE; +import static pulse.ui.Messages.getString; + +import java.io.IOException; +import javax.swing.JComponent; + +import javax.swing.JEditorPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultCaret; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.HTMLEditorKit; + +import pulse.tasks.TaskManager; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; + +@SuppressWarnings("serial") +public class TextLogPane extends AbstractLogger { + + private final JEditorPane editor; + + public TextLogPane() { + editor = new JEditorPane(); + editor.setContentType("text/html"); + editor.setEditable(false); + ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + } + + @Override + public void post(LogEntry logEntry) { + post(logEntry.toString()); + } + + @Override + public void post(String text) { + + final var doc = (HTMLDocument) editor.getDocument(); + final var kit = (HTMLEditorKit) editor.getEditorKit(); + try { + kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); + } catch (BadLocationException e) { + err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ + } catch (IOException e) { + err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ + } + + } + + + public void printTimeTaken(Log log) { + var time = log.timeTaken(); + var sb = new StringBuilder(); + sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ + sb.append(time[0]).append(getString("LogPane.Seconds")); //$NON-NLS-1$ + sb.append(time[1]).append(getString("LogPane.Milliseconds")); //$NON-NLS-1$ + post(sb.toString()); + } + + @Override + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + log.getLogEntries().stream().forEach(entry -> post(entry)); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public void clear() { + try { + editor.getDocument().remove(0, editor.getDocument().getLength()); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + @Override + public JComponent getGUIComponent() { + return editor; + } + + @Override + public boolean isEmpty() { + return editor.getDocument().getLength() < 1; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 6410fb7d..4f45590c 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -22,14 +22,15 @@ import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -import pulse.ui.components.LogPane; +import pulse.ui.components.AbstractLogger; +import pulse.ui.components.TextLogPane; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; @SuppressWarnings("serial") public class LogFrame extends JInternalFrame { - private LogPane logTextPane; + private AbstractLogger logger; public LogFrame() { super("Log", true, false, true, true); @@ -39,9 +40,9 @@ public LogFrame() { } private void initComponents() { - logTextPane = new LogPane(); + logger = new TextLogPane(); var logScroller = new JScrollPane(); - logScroller.setViewportView(logTextPane); + logScroller.setViewportView(logger.getGUIComponent()); getContentPane().setLayout(new BorderLayout()); getContentPane().add(logScroller, CENTER); @@ -54,8 +55,8 @@ private void initComponents() { var logToolbar = new LogToolbar(); logToolbar.addLogExportListener(() -> { - if (logTextPane.getDocument().getLength() > 0) { - askToExport(logTextPane, (JFrame) getWindowAncestor(this), + if (!logger.isEmpty()) { + askToExport(logger, (JFrame) getWindowAncestor(this), getString("LogToolBar.FileFormatDescriptor")); } }); @@ -65,7 +66,7 @@ private void initComponents() { private void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logTextPane.printAll()); + instance.addSelectionListener(e -> logger.postAll()); instance.addTaskRepositoryListener(event -> { if (event.getState() != TASK_ADDED) { @@ -81,13 +82,12 @@ public void onLogFinished(Log log) { if (instance.getSelectedTask() == task) { try { - logTextPane.getUpdateExecutor().awaitTermination(10, MILLISECONDS); + logger.getUpdateExecutor().awaitTermination(10, MILLISECONDS); } catch (InterruptedException e) { err.println("Log not finished in time"); - e.printStackTrace(); } - logTextPane.printTimeTaken(log); + logger.printTimeTaken(log); } } @@ -95,7 +95,7 @@ public void onLogFinished(Log log) { @Override public void onNewEntry(LogEntry e) { if (instance.getSelectedTask() == task) { - logTextPane.callUpdate(); + logger.callUpdate(); } } @@ -105,8 +105,8 @@ public void onNewEntry(LogEntry e) { }); } - public LogPane getLogTextPane() { - return logTextPane; + public AbstractLogger getLogger() { + return logger; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index f04f0504..880e10d0 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -157,13 +157,13 @@ public void onRemoveRequest() { @Override public void onClearRequest() { - logFrame.getLogTextPane().clear(); + logFrame.getLogger().clear(); resultsFrame.getResultTable().clear(); } @Override public void onResetRequest() { - logFrame.getLogTextPane().clear(); + logFrame.getLogger().clear(); resultsFrame.getResultTable().removeAll(); } From 0434e4cb0b25d7104b646b6f09023e06808542f7 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sun, 25 Dec 2022 12:22:38 +0300 Subject: [PATCH 105/116] Moved AbstractLogger to logs package --- .../pulse/{ui/components => tasks/logs}/AbstractLogger.java | 4 +--- src/main/java/pulse/ui/components/TextLogPane.java | 1 + src/main/java/pulse/ui/frames/LogFrame.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) rename src/main/java/pulse/{ui/components => tasks/logs}/AbstractLogger.java (93%) diff --git a/src/main/java/pulse/ui/components/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java similarity index 93% rename from src/main/java/pulse/ui/components/AbstractLogger.java rename to src/main/java/pulse/tasks/logs/AbstractLogger.java index 916e678c..bed70f7e 100644 --- a/src/main/java/pulse/ui/components/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -1,11 +1,9 @@ -package pulse.ui.components; +package pulse.tasks.logs; import java.util.concurrent.ExecutorService; import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; import pulse.tasks.TaskManager; -import pulse.tasks.logs.Log; -import pulse.tasks.logs.LogEntry; import pulse.util.Descriptive; public abstract class AbstractLogger implements Descriptive { diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index aaf318ae..e8f18ff2 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -1,5 +1,6 @@ package pulse.ui.components; +import pulse.tasks.logs.AbstractLogger; import static java.lang.System.err; import static java.time.temporal.ChronoUnit.MILLIS; import static java.time.temporal.ChronoUnit.SECONDS; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 4f45590c..996f1f37 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -22,7 +22,7 @@ import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -import pulse.ui.components.AbstractLogger; +import pulse.tasks.logs.AbstractLogger; import pulse.ui.components.TextLogPane; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; From 18b3dd0e401be9a77a09cf4384609f847a7035bf Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sat, 31 Dec 2022 18:59:57 +0300 Subject: [PATCH 106/116] Graphical log pane & new LAF - New Look and Feel (faster and better-looking) - Graphical logs - No "verbose" log option anymore ("on" by default in text mode) - Fixed glitches with GUI and task execution --- .../listeners/{LogExportListener.java => LogListener.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/pulse/ui/components/listeners/{LogExportListener.java => LogListener.java} (100%) diff --git a/src/main/java/pulse/ui/components/listeners/LogExportListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java similarity index 100% rename from src/main/java/pulse/ui/components/listeners/LogExportListener.java rename to src/main/java/pulse/ui/components/listeners/LogListener.java From a92b260eb75f56b9553a95fed552d04309f2417d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sat, 31 Dec 2022 18:59:57 +0300 Subject: [PATCH 107/116] Graphical log pane & new LAF - New Look and Feel (faster and better-looking) - Graphical logs - No "verbose" log option anymore ("on" by default in text mode) - Fixed glitches with GUI and task execution --- .../java/pulse/math/ParameterIdentifier.java | 31 ++- .../properties/NumericPropertyKeyword.java | 8 +- src/main/java/pulse/search/GeneralTask.java | 1 + .../pulse/search/direction/ComplexPath.java | 1 - .../direction/CompositePathOptimiser.java | 4 +- .../search/direction/IterativeState.java | 1 + .../pulse/search/direction/LMOptimiser.java | 4 +- .../direction/pso/ParticleSwarmOptimiser.java | 3 +- src/main/java/pulse/tasks/Calculation.java | 15 +- src/main/java/pulse/tasks/SearchTask.java | 7 +- .../java/pulse/tasks/logs/AbstractLogger.java | 66 ++++-- .../java/pulse/tasks/logs/DataLogEntry.java | 20 +- src/main/java/pulse/tasks/logs/Log.java | 18 +- .../java/pulse/tasks/processing/Buffer.java | 18 +- src/main/java/pulse/ui/ColorGenerator.java | 45 ++++ src/main/java/pulse/ui/Launcher.java | 8 +- .../java/pulse/ui/components/AuxPlotter.java | 87 +++++--- src/main/java/pulse/ui/components/Chart.java | 19 +- .../pulse/ui/components/GraphicalLogPane.java | 92 ++++++++ .../java/pulse/ui/components/LogChart.java | 199 ++++++++++++++++++ .../java/pulse/ui/components/PulseChart.java | 7 +- .../pulse/ui/components/ResidualsChart.java | 15 +- .../java/pulse/ui/components/TaskBox.java | 4 +- .../java/pulse/ui/components/TextLogPane.java | 34 +-- .../ui/components/buttons/LoaderButton.java | 27 ++- .../controllers/AccessibleTableRenderer.java | 2 - .../controllers/InstanceCellEditor.java | 14 +- .../controllers/NumericPropertyRenderer.java | 17 +- .../controllers/ProblemCellRenderer.java | 8 +- .../controllers/SearchListRenderer.java | 4 +- .../controllers/TaskTableRenderer.java | 3 - .../listeners/LogExportListener.java | 7 - .../ui/components/listeners/LogListener.java | 8 + .../ui/components/panels/ChartToolbar.java | 2 +- .../components/panels/DoubleTablePanel.java | 17 -- .../ui/components/panels/LogToolbar.java | 31 +-- .../ui/components/panels/ModelToolbar.java | 2 +- .../ui/components/panels/ProblemToolbar.java | 1 - src/main/java/pulse/ui/frames/LogFrame.java | 48 +++-- .../java/pulse/ui/frames/PreviewFrame.java | 10 +- .../pulse/ui/frames/SearchOptionsFrame.java | 2 - .../pulse/ui/frames/TaskControlFrame.java | 20 +- 42 files changed, 665 insertions(+), 265 deletions(-) create mode 100644 src/main/java/pulse/ui/ColorGenerator.java create mode 100644 src/main/java/pulse/ui/components/GraphicalLogPane.java create mode 100644 src/main/java/pulse/ui/components/LogChart.java delete mode 100644 src/main/java/pulse/ui/components/listeners/LogExportListener.java create mode 100644 src/main/java/pulse/ui/components/listeners/LogListener.java diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java index b96847da..3fb7bc49 100644 --- a/src/main/java/pulse/math/ParameterIdentifier.java +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -1,5 +1,6 @@ package pulse.math; +import java.util.Objects; import pulse.properties.NumericPropertyKeyword; public class ParameterIdentifier { @@ -15,6 +16,14 @@ public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { public ParameterIdentifier(NumericPropertyKeyword keyword) { this(keyword, 0); } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + Objects.hashCode(this.keyword); + hash = 29 * hash + this.index; + return hash; + } public ParameterIdentifier(int index) { this.index = index; @@ -30,24 +39,28 @@ public int getIndex() { @Override public boolean equals(Object id) { - if(!id.getClass().equals(ParameterIdentifier.class)) { + if(id.getClass() == null) { return false; } - var pid = (ParameterIdentifier) id; - - boolean result = true; + var classA = id.getClass(); + var classB = this.getClass(); - if(keyword != pid.keyword || index != pid.index) - result = false; - - return result; + if(classA != classB) { + return false; + } + var pid = (ParameterIdentifier) id; + return keyword == pid.keyword && Math.abs(index - pid.index) < 1; } @Override public String toString() { - return keyword + " # " + index; + StringBuilder sb = new StringBuilder("").append(keyword); + if(index > 0) { + sb.append(" # ").append(index); + } + return sb.toString(); } } \ No newline at end of file diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index a657171c..36b16662 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -390,7 +390,13 @@ public enum NumericPropertyKeyword { * Heat loss for the gas in the 2T-model. */ - HEAT_LOSS_GAS; + HEAT_LOSS_GAS, + + /** + * Value of objective function. + */ + + OBJECTIVE_FUNCTION; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java index 44c64ac0..3bd42d0a 100644 --- a/src/main/java/pulse/search/GeneralTask.java +++ b/src/main/java/pulse/search/GeneralTask.java @@ -64,6 +64,7 @@ public GeneralTask() { @Override public void run() { setDefaultOptimiser(); + best = null; setIterativeState( optimiser.initState(this) ); var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index e6fc8a3b..bfe1ef84 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -4,7 +4,6 @@ import pulse.math.linear.SquareMatrix; import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; /** *

diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 7976646e..48766c5a 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -61,6 +61,7 @@ public boolean iteration(GeneralTask task) throws SolverException { } else { double initialCost = task.getResponse().objectiveFunction(task); + p.setCost(initialCost); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -94,6 +95,7 @@ public boolean iteration(GeneralTask task) throws SolverException { task.storeState(); p.resetFailedAttempts(); this.prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] + p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps } @@ -142,4 +144,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 05fbacab..9045d46f 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -41,6 +41,7 @@ public void setCost(double cost) { public void reset() { iteration = 0; + setCost(Double.POSITIVE_INFINITY); } public NumericProperty getIteration() { diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 4c3247aa..86ee9e4b 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -69,6 +69,7 @@ public boolean iteration(GeneralTask task) throws SolverException { } else { double initialCost = task.objectiveFunction(); + p.setCost(initialCost); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -88,7 +89,7 @@ public boolean iteration(GeneralTask task) throws SolverException { parameters, candidate)); // assign new parameters double newCost = task.objectiveFunction(); // calculate the sum of squared residuals - + /* * Delayed gratification */ @@ -103,6 +104,7 @@ public boolean iteration(GeneralTask task) throws SolverException { p.resetFailedAttempts(); p.setLambda(p.getLambda() / 3.0); p.setComputeJacobian(false); + p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps } diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index 6f39f8b0..323dfab2 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -46,7 +46,8 @@ public boolean iteration(GeneralTask task) throws SolverException { swarmState.incrementStep(); task.assign(swarmState.getBestSoFar().getPosition()); - task.objectiveFunction(); + double cost = task.objectiveFunction(); + swarmState.setCost(cost); return true; } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 51bc76ed..5c673d32 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -34,6 +34,7 @@ import pulse.util.InstanceDescriptor; import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; +import pulse.util.UpwardsNavigable; public class Calculation extends PropertyHolder implements Comparable, Response { @@ -78,14 +79,14 @@ public Calculation(Calculation c) { instanceDescriptor.addListener(() -> initModelCriterion(rs)); } - public void assumeOwnership() { - problem.setParent(this); - scheme.setParent(this); - rs.setParent(this); - os.setParent(this); - result.setParent(this); + public void conformTo(UpwardsNavigable owner) { + problem.setParent(owner); + scheme.setParent(owner); + rs.setParent(owner); + os.setParent(owner); + result.setParent(owner); } - + public void clear() { this.status = INCOMPLETE; this.problem = null; diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index bc8277a6..91633c81 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -280,8 +280,7 @@ public void run() { } current.getProblem().parameterListChanged(); // get updated list of parameters - setDefaultOptimiser(); - + super.run(); } @@ -333,9 +332,11 @@ public void storeCalculation() { } public void switchTo(Calculation calc) { + current.setParent(null); + current.conformTo(null); current = new Calculation(calc); - current.assumeOwnership(); current.setParent(this); + calc.conformTo(calc); current.setStatus(Status.READY); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); diff --git a/src/main/java/pulse/tasks/logs/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java index bed70f7e..f128837b 100644 --- a/src/main/java/pulse/tasks/logs/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -4,12 +4,17 @@ import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; import pulse.tasks.TaskManager; +import static pulse.tasks.logs.Status.DONE; import pulse.util.Descriptive; public abstract class AbstractLogger implements Descriptive { - private final ExecutorService updateExecutor = newSingleThreadExecutor(); - + private final ExecutorService updateExecutor; + + public AbstractLogger() { + updateExecutor = newSingleThreadExecutor(); + } + public synchronized void update() { var task = TaskManager.getManagerInstance().getSelectedTask(); @@ -19,32 +24,59 @@ public synchronized void update() { var log = task.getLog(); - if (!log.isStarted()) { - return; + if (log.isStarted()) { + post(log.lastEntry()); } - - post(log.lastEntry()); + } - + public ExecutorService getUpdateExecutor() { return updateExecutor; } - + public synchronized void callUpdate() { updateExecutor.submit(() -> update()); } - + + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + log.getLogEntries().stream().forEach(entry -> post(entry)); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public String describe() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + return "Log" + (task == null ? "" : "_" + task.getIdentifier().getValue()); + } + public abstract JComponent getGUIComponent(); + public abstract void printTimeTaken(Log log); + public abstract void post(LogEntry logEntry); + public abstract void post(String text); - public abstract void postAll(); + public abstract void clear(); + public abstract boolean isEmpty(); - - @Override - public String describe() { - return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); - } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 4e17c87c..c3c5ab3d 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -2,10 +2,11 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; -import pulse.Response; import pulse.math.Parameter; import pulse.math.ParameterIdentifier; import pulse.properties.NumericProperties; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.OBJECTIVE_FUNCTION; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; @@ -55,10 +56,19 @@ public DataLogEntry(SearchTask task) { private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { var task = TaskManager.getManagerInstance().getTask(getIdentifier()); entry = task.searchVector().getParameters(); + //iteration var pval = task.getIterativeState().getIteration(); var pid = new Parameter(new ParameterIdentifier(pval.getType())); - pid.setValue( (int) pval.getValue() ); + pid.setValue((int) pval.getValue()); + //cost + var costId = new Parameter(new ParameterIdentifier(OBJECTIVE_FUNCTION)); + var costval = task.getIterativeState().getCost(); + // entry.add(0, pid); + if (NumericProperties.isValueSensible(def(OBJECTIVE_FUNCTION), costval)) { + costId.setValue(costval); + entry.add(costId); + } } public List getData() { @@ -91,15 +101,15 @@ public String toString() { var def = NumericProperties.def(p.getIdentifier().getKeyword()); boolean b = def.getValue() instanceof Integer; Number val; - if(b) { + if (b) { val = (int) Math.rint(p.getApparentValue()); - } else{ + } else { val = p.getApparentValue(); } def.setValue(val); sb.append(def.getAbbreviation(false)); int index = p.getIdentifier().getIndex(); - if(index > 0) { + if (index > 0) { sb.append(" - ").append(index); } sb.append("<

"); - sb.append(p.getAbbreviation(false)); + var def = NumericProperties.def(p.getIdentifier().getKeyword()); + boolean b = def.getValue() instanceof Integer; + Number val; + if(b) { + val = (int) Math.rint(p.getApparentValue()); + } else{ + val = p.getApparentValue(); + } + def.setValue(val); + sb.append(def.getAbbreviation(false)); + int index = p.getIdentifier().getIndex(); + if(index > 0) { + sb.append(" - ").append(index); + } sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ sb.append(""); - sb.append(p.formattedOutput()); + sb.append(def.formattedOutput()); sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ sb.append("
"); diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 11534996..21a32df8 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -26,7 +26,7 @@ public class Log extends Group { private LocalTime end; private final Identifier id; private final List listeners; - private static boolean verbose = false; + private static boolean graphical = true; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -50,7 +50,7 @@ public Log(SearchTask task) { /** * Do these actions each time data has been collected for this task. */ - if (task.getStatus() != Status.INCOMPLETE && verbose) { + if (task.getStatus() != Status.INCOMPLETE) { logEntries.add(le); notifyListeners(le); } @@ -107,7 +107,7 @@ public final Identifier getIdentifier() { * @return {@code true} if the start time is not {@code null} */ public boolean isStarted() { - return start != null; + return logEntries.size() > 0; } /** @@ -175,18 +175,18 @@ public LogEntry lastEntry() { * * @return {@code true} if the verbose flag is on */ - public static boolean isVerbose() { - return verbose; + public static boolean isGraphicalLog() { + return graphical; } /** * Sets the verbose flag to {@code verbose} * * @param verbose the new value of the flag - * @see isVerbose() + * @see #isGraphicalLog() */ - public static void setVerbose(boolean verbose) { - Log.verbose = verbose; + public static void setGraphicalLog(boolean verbose) { + Log.graphical = verbose; } /** @@ -200,4 +200,4 @@ public long[] timeTaken() { return new long[] {seconds, ms}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index e3a16843..90ef5bde 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -8,10 +8,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; +import org.apache.commons.math3.stat.regression.SimpleRegression; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.OBJECTIVE_FUNCTION; import pulse.properties.Property; import pulse.search.GeneralTask; import pulse.util.PropertyHolder; @@ -48,7 +52,7 @@ public ParameterVector[] getData() { /* * Re-inits the storage. */ - public void init() { + public final void init() { this.data = new ParameterVector[size]; statistic = new double[size]; } @@ -105,11 +109,17 @@ public double average(NumericPropertyKeyword index) { double av = 0; - for (ParameterVector v : data) { - av += v.getParameterValue(index, 0); + if (index == OBJECTIVE_FUNCTION) { + av = Arrays.stream(statistic).average().getAsDouble(); + } else { + + for (ParameterVector v : data) { + av += v.getParameterValue(index, 0); + } + av /= data.length; } - return av / data.length; + return av; } diff --git a/src/main/java/pulse/ui/ColorGenerator.java b/src/main/java/pulse/ui/ColorGenerator.java new file mode 100644 index 00000000..2e2388df --- /dev/null +++ b/src/main/java/pulse/ui/ColorGenerator.java @@ -0,0 +1,45 @@ +package pulse.ui; + +import java.awt.Color; +import static java.awt.Color.BLUE; +import static java.awt.Color.GREEN; +import static java.awt.Color.RED; +import java.util.ArrayList; +import java.util.Collections; + +public class ColorGenerator { + + private Color a, b, c; + + public ColorGenerator() { + a = RED; + b = GREEN; + c = BLUE; + } + + public Color[] random(int number) { + var list = new ArrayList(); + for(int i = 0; i < number; i++) { + list.add(sample(i/(double)(number - 1))); + } + //Collections.shuffle(list); + return list.toArray(new Color[list.size()]); + } + + public Color sample(double seed) { + return seed < 0.5 ? + mix(a, b, (float) (seed*2)) + : mix(b, c,(float)((seed-0.5)*2)); + } + + private static Color mix(Color a, Color b, float ratio) { + float[] aRgb = a.getRGBComponents(null); + float[] bRgb = b.getRGBComponents(null); + float[] cRgb = new float[3]; + for(int i = 0; i < cRgb.length; i++) { + cRgb[i] = aRgb[i] * (1.0f - ratio) + bRgb[i] * ratio; + } + return new Color(cRgb[0], cRgb[1], cRgb[2]); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index de978325..ee238ac1 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -17,8 +17,7 @@ import javax.swing.JOptionPane; import javax.swing.UIManager; -import com.alee.laf.WebLookAndFeel; -import com.alee.skin.dark.WebDarkSkin; +import com.formdev.flatlaf.FlatDarkLaf; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -65,9 +64,10 @@ public static void main(String[] args) { splashScreen(); - WebLookAndFeel.install(WebDarkSkin.class); + //WebLookAndFeel.install(WebDarkSkin.class); + FlatDarkLaf.setup(); try { - UIManager.setLookAndFeel(new WebLookAndFeel()); + UIManager.setLookAndFeel(new FlatDarkLaf()); } catch (Exception ex) { System.err.println("Failed to initialize LaF"); } diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 8150c4c4..602a3d63 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -1,58 +1,85 @@ package pulse.ui.components; +import java.awt.Color; import java.awt.Font; +import javax.swing.JLabel; import javax.swing.UIManager; +import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.CombinedDomainXYPlot; +import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import org.jfree.chart.plot.XYPlot; public abstract class AuxPlotter { - + private ChartPanel chartPanel; private JFreeChart chart; private XYPlot plot; - + + public AuxPlotter() { + //empty + } + public AuxPlotter(String xLabel, String yLabel) { - createChart(xLabel, yLabel); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + setChart( ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false) ); + + setPlot( chart.getXYPlot() ); + chart.removeLegend(); - plot = chart.getXYPlot(); setFonts(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); } - - public void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 16); - var plot = getPlot(); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); + + public final void setFonts() { + var jlabel = new JLabel(); + var label = jlabel.getFont().deriveFont(20f); + var ticks = jlabel.getFont().deriveFont(16f); + chart.getTitle().setFont(jlabel.getFont().deriveFont(20f)); + + if (plot instanceof CombinedDomainXYPlot) { + var combinedPlot = (CombinedDomainXYPlot) plot; + combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot)sp, label, ticks)); + } else { + setFontsForPlot(plot, label, ticks); + } + } - - public abstract void createChart(String xLabel, String yLabel); - + + private void setFontsForPlot(XYPlot p, Font label, Font ticks) { + var foreColor = UIManager.getColor("Label.foreground"); + var domainAxis = p.getDomainAxis(); + Chart.setAxisFontColor(domainAxis, foreColor); + var rangeAxis = p.getRangeAxis(); + Chart.setAxisFontColor(rangeAxis, foreColor); + } + public abstract void plot(T t); - - public ChartPanel getChartPanel() { + + public final ChartPanel getChartPanel() { return chartPanel; } - - public JFreeChart getChart() { + + public final JFreeChart getChart() { return chart; } - - public XYPlot getPlot() { + + public final XYPlot getPlot() { return plot; } - - public void setChart(JFreeChart chart) { + + public final void setPlot(XYPlot plot) { + this.plot = plot; + plot.setBackgroundPaint(chart.getBackgroundPaint()); + } + + public final void setChart(JFreeChart chart) { this.chart = chart; + var color = UIManager.getLookAndFeelDefaults().getColor("TextPane.background"); + chart.setBackgroundPaint(color); + chartPanel = new ChartPanel(chart); + this.plot = chart.getXYPlot(); } - -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 68dda2d3..7fe372e5 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -26,6 +26,7 @@ import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; +import org.jfree.chart.axis.Axis; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; @@ -81,7 +82,7 @@ public Chart() { final TaskManager instance = TaskManager.getManagerInstance(); chart.removeLegend(); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + chart.setBackgroundPaint(UIManager.getColor("TextPane.background")); chartPanel = new ChartPanel(chart) { @Override @@ -146,16 +147,18 @@ public double xCoord(MouseEvent e) { } private void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 14); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); + var foreColor = UIManager.getColor("Label.foreground"); + setAxisFontColor(plot.getDomainAxis(), foreColor); + setAxisFontColor(plot.getRangeAxis(), foreColor); + } + + public static void setAxisFontColor(Axis axis, Color color) { + axis.setLabelPaint(color); + axis.setTickLabelPaint(color); } private void setBackgroundAndGrid() { - // plot.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setBackgroundPaint(UIManager.getColor("TextPane.background")); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(GRAY); diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java new file mode 100644 index 00000000..a50e4941 --- /dev/null +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -0,0 +1,92 @@ +package pulse.ui.components; + +import javax.swing.JComponent; +import static pulse.properties.NumericPropertyKeyword.ITERATION; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.logs.AbstractLogger; +import pulse.tasks.logs.DataLogEntry; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; +import static pulse.tasks.logs.Status.DONE; + +@SuppressWarnings("serial") +public class GraphicalLogPane extends AbstractLogger { + + private final LogChart chart; + + public GraphicalLogPane() { + chart = new LogChart(); + TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { + if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { + chart.clear(); + } + }); + } + + @Override + public JComponent getGUIComponent() { + return chart.getChartPanel(); + } + + @Override + public void printTimeTaken(Log log) { + long[] time = log.timeTaken(); + StringBuilder sb = new StringBuilder("Finished in "); + sb.append(time[0]).append(" s ").append(time[1]).append(" ms."); + } + + @Override + public void post(LogEntry logEntry) { + if(logEntry instanceof DataLogEntry) { + var dle = (DataLogEntry) logEntry; + double iteration = dle.getData().stream() + .filter(p -> p.getIdentifier().getKeyword() == ITERATION) + .findAny().get().getApparentValue(); + chart.changeAxis(true); + chart.plot((DataLogEntry)logEntry, iteration); + } + } + + @Override + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + chart.clear(); + chart.changeAxis(false); + chart.plot(log); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public void post(String text) { + //not supported + } + + @Override + public void clear() { + chart.clear(); + } + + @Override + public boolean isEmpty() { + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java new file mode 100644 index 00000000..1f180ca9 --- /dev/null +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -0,0 +1,199 @@ +package pulse.ui.components; + +import static java.util.Objects.requireNonNull; + +import java.awt.BasicStroke; +import java.awt.Color; +import static java.awt.Color.WHITE; +import static java.awt.Color.black; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import javax.swing.SwingUtilities; + +import org.jfree.chart.JFreeChart; +import org.jfree.chart.annotations.XYTitleAnnotation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.NumberTickUnit; +import org.jfree.chart.block.BlockBorder; +import org.jfree.chart.plot.CombinedDomainXYPlot; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.ui.RectangleAnchor; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.data.Range; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import pulse.Response; +import pulse.math.ParameterIdentifier; + +import static pulse.properties.NumericPropertyKeyword.ITERATION; +import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.logs.DataLogEntry; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.Status; +import pulse.tasks.processing.Buffer; +import pulse.ui.ColorGenerator; + +public class LogChart extends AuxPlotter { + + private final Map plots; + private Color[] colors; + private static final ColorGenerator cg = new ColorGenerator(); + private Response r; + + public LogChart() { + var plot = new CombinedDomainXYPlot(new NumberAxis("Iteration")); + plot.setGap(10.0); + plot.setOrientation(PlotOrientation.VERTICAL); + var chart = new JFreeChart("", JFreeChart.DEFAULT_TITLE_FONT, plot, true); + setChart(chart); + plots = new HashMap<>(); + getChart().removeLegend(); + } + + public final void clear() { + var p = (CombinedDomainXYPlot) getPlot(); + p.getDomainAxis().setAutoRange(true); + if (p != null) { + plots.values().stream().forEach(pp -> p.remove(pp)); + } + plots.clear(); + colors = new Color[0]; + r = null; + } + + private void setLegendTitle(Plot plot) { + var lt = new LegendTitle(plot); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.0, 0.8, lt, RectangleAnchor.LEFT); + ta.setMaxWidth(0.58); + ((XYPlot) plot).addAnnotation(ta); + } + + public final void add(ParameterIdentifier key, int no) { + var plot = new XYPlot(); + var axis = new NumberAxis(); + axis.setAutoRangeIncludesZero(false); + plot.setRangeAxis(axis); + + plot.setBackgroundPaint(getChart().getBackgroundPaint()); + + plots.put(key, plot); + ((CombinedDomainXYPlot) getPlot()).add(plot); + + var dataset = new XYSeriesCollection(); + var series = new XYSeries(key.toString()); + + dataset.addSeries(series); + dataset.addSeries(new XYSeries("Running average")); + plot.setDataset(dataset); + setLegendTitle(plot); + + setRenderer(plot, colors[no]); + setFonts(); + } + + private void setRenderer(XYPlot plt, Color clr) { + var renderer = new XYLineAndShapeRenderer(true, false); + renderer.setSeriesPaint(0, clr); + renderer.setSeriesPaint(1, WHITE); + var dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, 10.0f, new float[]{10.0f}, 0.0f); + renderer.setSeriesStroke(1, dashed); + renderer.setSeriesVisibleInLegend(1, Boolean.FALSE); + plt.setRenderer(renderer); + } + + public void changeAxis(boolean iterationMode) { + var domainAxis = (NumberAxis) getPlot().getDomainAxis(); + domainAxis.setLabel(iterationMode ? "Iteration" : "Time (ms)"); + domainAxis.setAutoRange(!iterationMode); + if(iterationMode) { + domainAxis.setTickUnit(new NumberTickUnit(1)); + } else { + domainAxis.setAutoTickUnitSelection(true); + } + } + + @Override + public void plot(Log l) { + requireNonNull(l); + + l.getLogEntries().stream() + .filter(le -> le instanceof DataLogEntry) + .forEach(d -> plot((DataLogEntry) d, + Duration.between(l.getStart(), d.getTime()).toMillis())); + } + + private static void adjustRange(XYPlot pl, int iteration, int bufSize) { + int lower = (iteration / bufSize) * bufSize; + + var domainAxis = pl.getDomainAxis(); + var r = domainAxis.getRange(); + var newR = new Range(lower, lower + bufSize); + + if (!r.equals(newR) && iteration > lower) { + ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + } + } + + public final void plot(DataLogEntry dle, double iterationOrTime) { + requireNonNull(dle); + + var data = dle.getData(); + int size = data.size(); + + if (colors == null || colors.length < size) { + colors = cg.random(size - 1); + } + + SearchTask task = TaskManager.getManagerInstance().getTask(dle.getIdentifier()); + Buffer buf = task.getBuffer(); + final int bufSize = buf.getData().length; + + for (int i = 0, j = 0; i < size; i++) { + var p = data.get(i); + var np = p.getIdentifier(); + + if (np.getKeyword() == ITERATION) { + continue; + } + + double value = p.getApparentValue(); + + if (!plots.containsKey(np)) { + add(np, j++); + } + + Plot pl = plots.get(np); + + var dataset = (XYSeriesCollection) ((XYPlot) pl).getDataset(); + XYSeries series = (XYSeries) dataset.getSeries(0); + series.add(iterationOrTime, value); + + if (task.getStatus() == Status.IN_PROGRESS) { + + XYSeries runningAverage = dataset.getSeries(1); + if (iterationOrTime > buf.getData().length - 1) { + runningAverage.add(iterationOrTime, buf.average(np.getKeyword())); + } + + SwingUtilities.invokeLater(() -> adjustRange((XYPlot)pl, (int)iterationOrTime, bufSize)); + + } else { + var domainAxis = ((XYPlot) pl).getDomainAxis(); + domainAxis.setAutoRange(true); + } + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index bb336469..db6e7478 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -41,16 +41,11 @@ private void setRenderer() { getPlot().setRenderer(rendererPulse); } - @Override - public void createChart(String xLabel, String yLabel) { - setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); - } - private void setLegendTitle() { var plot = getPlot(); var lt = new LegendTitle(plot); lt.setItemFont(new Font("Dialog", PLAIN, 16)); - //lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); lt.setFrame(new BlockBorder(black)); lt.setPosition(RectangleEdge.RIGHT); var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 7a78ee24..7543707d 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -1,9 +1,8 @@ package pulse.ui.components; import static java.util.Objects.requireNonNull; -import static org.jfree.chart.plot.PlotOrientation.VERTICAL; - import org.jfree.chart.ChartFactory; +import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import org.jfree.data.statistics.HistogramDataset; import org.jfree.data.statistics.HistogramType; @@ -14,15 +13,13 @@ public class ResidualsChart extends AuxPlotter { private int binCount; public ResidualsChart(String xLabel, String yLabel) { - super(xLabel, yLabel); - binCount = 32; - } - - @Override - public void createChart(String xLabel, String yLabel) { setChart(ChartFactory.createHistogram("", xLabel, yLabel, null, VERTICAL, true, true, false)); + setPlot(getChart().getXYPlot()); + getChart().removeLegend(); + setFonts(); + binCount = 32; } - + @Override public void plot(ResidualStatistic stat) { requireNonNull(stat); diff --git a/src/main/java/pulse/ui/components/TaskBox.java b/src/main/java/pulse/ui/components/TaskBox.java index 9be85ed4..13b84459 100644 --- a/src/main/java/pulse/ui/components/TaskBox.java +++ b/src/main/java/pulse/ui/components/TaskBox.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Color.WHITE; import static java.awt.event.ItemEvent.SELECTED; import static pulse.ui.Messages.getString; @@ -46,11 +45,10 @@ public TaskBox() { }); } - public void init() { + public final void init() { setMaximumSize(new Dimension(32767, 24)); setMinimumSize(new Dimension(250, 20)); setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ - setBackground(WHITE); } } diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index e8f18ff2..f1b7dfcd 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -2,22 +2,19 @@ import pulse.tasks.logs.AbstractLogger; import static java.lang.System.err; -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; -import static pulse.tasks.logs.Status.DONE; import static pulse.ui.Messages.getString; import java.io.IOException; import javax.swing.JComponent; import javax.swing.JEditorPane; +import javax.swing.JScrollPane; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultCaret; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; -import pulse.tasks.TaskManager; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; @@ -25,12 +22,15 @@ public class TextLogPane extends AbstractLogger { private final JEditorPane editor; + private final JScrollPane pane; public TextLogPane() { editor = new JEditorPane(); editor.setContentType("text/html"); editor.setEditable(false); ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + pane = new JScrollPane(); + pane.setViewportView(editor); } @Override @@ -63,30 +63,6 @@ public void printTimeTaken(Log log) { post(sb.toString()); } - @Override - public void postAll() { - clear(); - - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task != null) { - - var log = task.getLog(); - - if (log.isStarted()) { - - log.getLogEntries().stream().forEach(entry -> post(entry)); - - if (task.getStatus() == DONE) { - printTimeTaken(log); - } - - } - - } - - } - @Override public void clear() { try { @@ -98,7 +74,7 @@ public void clear() { @Override public JComponent getGUIComponent() { - return editor; + return pane; } @Override diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 46c9b435..cad1066c 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -19,7 +19,6 @@ import java.io.File; import java.io.IOException; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.UIManager; @@ -27,7 +26,6 @@ import org.apache.commons.math3.exception.OutOfRangeException; import pulse.input.InterpolationDataset; -import pulse.ui.Messages; import pulse.util.ImageUtils; @SuppressWarnings("serial") @@ -36,8 +34,8 @@ public class LoaderButton extends JButton { private InterpolationDataset.StandartType dataType; private static File dir; - private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); - private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.75f); + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); public LoaderButton() { super(); @@ -49,7 +47,7 @@ public LoaderButton(String str) { init(); } - public void init() { + public final void init() { InterpolationDataset.addListener(e -> { if (dataType == e) { @@ -86,18 +84,17 @@ public void init() { showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ getString("LoaderButton.IOError"), //$NON-NLS-1$ ERROR_MESSAGE); - } - catch(OutOfRangeException ofre) { + } catch (OutOfRangeException ofre) { getDefaultToolkit().beep(); StringBuilder sb = new StringBuilder(getString("TextWrap.0")); - sb.append(getString("LoaderButton.OFRErrorDescriptor") ); + sb.append(getString("LoaderButton.OFRErrorDescriptor")); sb.append(ofre.getMessage()); sb.append(getString("LoaderButton.OFRErrorDescriptor2")); sb.append(getString("TextWrap.1")); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - sb.toString(), + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), + sb.toString(), getString("LoaderButton.OFRError"), //$NON-NLS-1$ - ERROR_MESSAGE); + ERROR_MESSAGE); } var size = getDataset(dataType).getData().size(); var label = ""; @@ -113,8 +110,10 @@ public void init() { default: throw new IllegalStateException("Unknown data type: " + dataType); } + StringBuilder sb = new StringBuilder(""); + sb.append(label).append(" data loaded! A total of ").append(size).append(" data points loaded."); showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - "" + label + " data loaded! A total of " + size + " data points loaded.", + sb.toString(), "Data loaded", INFORMATION_MESSAGE); }); } @@ -124,11 +123,11 @@ public void setDataType(InterpolationDataset.StandartType dataType) { } public void highlight(boolean highlighted) { - setBorder(highlighted ? BorderFactory.createLineBorder(HIGHLIGHTED) : null); + setBackground(highlighted ? HIGHLIGHTED : NOT_HIGHLIGHTED); } public void highlightIfNeeded() { highlight(getDataset(dataType) == null); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index b297d1a5..04727c06 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,6 +1,5 @@ package pulse.ui.components.controllers; -import static java.awt.Color.RED; import java.awt.Component; import java.awt.Font; @@ -8,7 +7,6 @@ import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JTable; -import javax.swing.UIManager; import pulse.properties.Flag; import pulse.properties.NumericProperty; diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 526823f7..e9611d77 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -1,13 +1,11 @@ package pulse.ui.components.controllers; -import com.alee.utils.swing.PopupMenuAdapter; import java.awt.Component; import java.awt.event.ItemEvent; import javax.swing.DefaultCellEditor; import javax.swing.JComboBox; import javax.swing.JTable; -import javax.swing.event.PopupMenuEvent; import pulse.util.InstanceDescriptor; @@ -39,17 +37,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean } } }); - - combobox.addPopupMenuListener(new PopupMenuAdapter() { - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { - fireEditingCanceled(); - } - - } - ); - + return combobox; } diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index e655f9b1..5f05afc5 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -7,10 +7,8 @@ import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.table.DefaultTableCellRenderer; -import pulse.math.Segment; import pulse.properties.NumericProperty; -import pulse.properties.Property; import pulse.properties.NumericPropertyFormatter; @SuppressWarnings("serial") @@ -29,25 +27,22 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole if (value instanceof NumericProperty) { var jtf = initTextField((NumericProperty) value, table.isRowSelected(row)); - if (table.getEditorComponent() != null) { result = jtf; } else { - result = new JLabel(jtf.getText(), JLabel.RIGHT); - jtf = null; + result = (JLabel) super.getTableCellRendererComponent(table, + jtf.getText(), isSelected, hasFocus, row, column); + ((JLabel) result).setHorizontalAlignment(RIGHT); } - } else { var superRenderer = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); superRenderer.setHorizontalAlignment(JLabel.LEFT); - superRenderer.setBackground( - isSelected - ? UIManager.getColor("JFormattedTextField.selectionBackground") - : UIManager.getColor("JFormattedTextField.background")); result = superRenderer; } + + result.setForeground(UIManager.getColor("List.foreground")); return result; } @@ -59,4 +54,4 @@ private static JFormattedTextField initTextField(NumericProperty np, boolean row return jtf; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java index 7c360774..4827624b 100644 --- a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -4,20 +4,20 @@ import javax.swing.ImageIcon; import javax.swing.JTree; -import javax.swing.UIManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; -import com.alee.managers.icon.LazyIcon; +//import com.alee.managers.icon.LazyIcon; import pulse.problem.statements.Problem; import pulse.util.ImageUtils; +import static pulse.util.ImageUtils.loadIcon; @SuppressWarnings("serial") public class ProblemCellRenderer extends DefaultTreeCellRenderer { - private static ImageIcon defaultIcon = (ImageIcon) ((LazyIcon) UIManager.getIcon("Tree.leafIcon")).getIcon(); - + private static ImageIcon defaultIcon = loadIcon("leaf.png", 16); + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index e74951a2..cfc884cf 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -2,7 +2,6 @@ import static javax.swing.BorderFactory.createEmptyBorder; -import java.awt.Color; import java.awt.Component; import javax.swing.DefaultListCellRenderer; @@ -18,8 +17,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - renderer.setForeground(isSelected ? Color.DARK_GRAY : Color.white); - + return renderer; } diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index 85d3ba62..33ccdcb8 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -3,16 +3,13 @@ import static java.awt.Font.BOLD; import java.awt.Component; -import java.awt.Font; import javax.swing.JLabel; import javax.swing.JTable; import pulse.properties.NumericProperty; -import pulse.properties.Property; import pulse.tasks.Identifier; import pulse.tasks.logs.Status; -import pulse.util.PropertyHolder; @SuppressWarnings("serial") public class TaskTableRenderer extends NumericPropertyRenderer { diff --git a/src/main/java/pulse/ui/components/listeners/LogExportListener.java b/src/main/java/pulse/ui/components/listeners/LogExportListener.java deleted file mode 100644 index 99a32ae9..00000000 --- a/src/main/java/pulse/ui/components/listeners/LogExportListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package pulse.ui.components.listeners; - -public interface LogExportListener { - - public void onLogExportRequest(); - -} diff --git a/src/main/java/pulse/ui/components/listeners/LogListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java new file mode 100644 index 00000000..5a07f5dc --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/LogListener.java @@ -0,0 +1,8 @@ +package pulse.ui.components.listeners; + +public interface LogListener { + + public void onLogExportRequest(); + public void onLogModeChanged(boolean graphical); + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index b4d5e548..dca6e625 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -38,7 +38,7 @@ public final class ChartToolbar extends JToolBar { private final static int ICON_SIZE = 16; - private List listeners; + private final List listeners; private RangeTextFields rtf; diff --git a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java index 11a5c192..ebb6bf66 100644 --- a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 kotik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.ui.components.panels; import java.awt.GridBagConstraints; @@ -87,7 +72,6 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderLeft = createTitledBorder(titleLeft); leftScroller.setBorder(borderLeft); - borderLeft.setTitleColor(java.awt.Color.WHITE); leftTable.setRowHeight(80); @@ -102,7 +86,6 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderRight = createTitledBorder(titleRight); rightScroller.setBorder(borderRight); - borderRight.setTitleColor(java.awt.Color.WHITE); rightTable.setRowHeight(80); rightScroller.setViewportView(rightTable); diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index a9fd6a16..5db207b5 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -1,7 +1,5 @@ package pulse.ui.components.panels; -import static pulse.tasks.logs.Log.isVerbose; -import static pulse.tasks.logs.Log.setVerbose; import static pulse.ui.Messages.getString; import static pulse.util.ImageUtils.loadIcon; @@ -14,13 +12,15 @@ import javax.swing.JCheckBox; import javax.swing.JToolBar; -import pulse.ui.components.listeners.LogExportListener; +import static pulse.tasks.logs.Log.setGraphicalLog; +import static pulse.tasks.logs.Log.isGraphicalLog; +import pulse.ui.components.listeners.LogListener; @SuppressWarnings("serial") public class LogToolbar extends JToolBar { private final static int ICON_SIZE = 16; - private List listeners; + private List listeners; public LogToolbar() { super(); @@ -35,24 +35,25 @@ public void initComponents() { var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); saveLogBtn.setToolTipText("Save"); - var verboseCheckBox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ - verboseCheckBox.setSelected(isVerbose()); - verboseCheckBox.setHorizontalAlignment(CENTER); + var logmodeCheckbox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ + logmodeCheckbox.setSelected(isGraphicalLog()); + logmodeCheckbox.setHorizontalAlignment(CENTER); - verboseCheckBox.addActionListener(event -> setVerbose(verboseCheckBox.isSelected())); + logmodeCheckbox.addActionListener(event -> { + boolean selected = logmodeCheckbox.isSelected(); + setGraphicalLog(selected); + listeners.stream().forEach(l -> l.onLogModeChanged(selected)); + }); - saveLogBtn.addActionListener(e -> notifyLog()); + saveLogBtn.addActionListener(e -> listeners.stream().forEach(l -> l.onLogExportRequest())); add(saveLogBtn); - add(verboseCheckBox); - } - public void notifyLog() { - listeners.stream().forEach(l -> l.onLogExportRequest()); + add(logmodeCheckbox); } - public void addLogExportListener(LogExportListener l) { + public void addLogListener(LogListener l) { listeners.add(l); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index 7c0b5508..770734eb 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -18,7 +18,7 @@ @SuppressWarnings("serial") public class ModelToolbar extends JToolBar { - private final static int ICON_SIZE = 20; + private final static int ICON_SIZE = 16; public ModelToolbar() { super(); diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 92e42cab..e06abd02 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -13,7 +13,6 @@ import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; -import java.util.concurrent.Executors; import javax.swing.JButton; import javax.swing.JToolBar; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 996f1f37..7eeae66d 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -6,7 +6,6 @@ import static java.awt.GridBagConstraints.WEST; import static java.lang.System.err; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static javax.swing.SwingUtilities.getWindowAncestor; import static pulse.io.export.ExportManager.askToExport; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; import static pulse.ui.Messages.getString; @@ -14,16 +13,17 @@ import java.awt.BorderLayout; import java.awt.GridBagConstraints; -import javax.swing.JFrame; import javax.swing.JInternalFrame; -import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.AbstractLogger; +import pulse.ui.components.GraphicalLogPane; import pulse.ui.components.TextLogPane; +import pulse.ui.components.listeners.LogListener; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; @@ -31,6 +31,8 @@ public class LogFrame extends JInternalFrame { private AbstractLogger logger; + private final static AbstractLogger graphical = new GraphicalLogPane(); + private final static AbstractLogger text = new TextLogPane(); public LogFrame() { super("Log", true, false, true, true); @@ -40,12 +42,10 @@ public LogFrame() { } private void initComponents() { - logger = new TextLogPane(); - var logScroller = new JScrollPane(); - logScroller.setViewportView(logger.getGUIComponent()); + logger = Log.isGraphicalLog() ? graphical : text; getContentPane().setLayout(new BorderLayout()); - getContentPane().add(logScroller, CENTER); + getContentPane().add(logger.getGUIComponent(), CENTER); var gridBagConstraints = new GridBagConstraints(); gridBagConstraints.anchor = WEST; @@ -54,12 +54,24 @@ private void initComponents() { getContentPane().add(new SystemPanel(), PAGE_END); var logToolbar = new LogToolbar(); - logToolbar.addLogExportListener(() -> { - if (!logger.isEmpty()) { - askToExport(logger, (JFrame) getWindowAncestor(this), - getString("LogToolBar.FileFormatDescriptor")); + + var lel = new LogListener() { + @Override + public void onLogExportRequest() { + if (logger == text) { + askToExport(logger, null, getString("LogToolBar.FileFormatDescriptor")); + } else { + System.out.println("To export the log entries, please switch to text mode first!"); + } } - }); + + @Override + public void onLogModeChanged(boolean graphical) { + SwingUtilities.invokeLater(() -> setGraphicalLogger(graphical)); + } + }; + + logToolbar.addLogListener(lel); getContentPane().add(logToolbar, NORTH); } @@ -109,4 +121,16 @@ public AbstractLogger getLogger() { return logger; } + private void setGraphicalLogger(boolean graphicalLog) { + var old = logger; + logger = graphicalLog ? graphical : text; + + if (old != logger) { + getContentPane().remove(old.getGUIComponent()); + getContentPane().add(logger.getGUIComponent(), BorderLayout.CENTER); + logger.postAll(); + } + + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index 76a249da..508d5a77 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -15,6 +15,7 @@ import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; +import static java.awt.Color.WHITE; import java.awt.GridLayout; import java.awt.Rectangle; import java.util.ArrayList; @@ -46,6 +47,7 @@ import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import pulse.tasks.processing.ResultFormat; +import pulse.ui.components.Chart; @SuppressWarnings("serial") public class PreviewFrame extends JInternalFrame { @@ -93,7 +95,6 @@ private void init() { toolbar.add(selectX); selectXBox = new JComboBox<>(); - selectXBox.setFont(selectXBox.getFont().deriveFont(11)); toolbar.add(selectXBox); toolbar.add(new JSeparator()); @@ -102,7 +103,6 @@ private void init() { toolbar.add(selectY); selectYBox = new JComboBox<>(); - selectYBox.setFont(selectYBox.getFont().deriveFont(11)); toolbar.add(selectYBox); var drawSmoothBtn = new JToggleButton(); @@ -229,6 +229,9 @@ private static ChartPanel createEmptyPanel() { //plot.setRangeGridlinesVisible(false); //plot.setDomainGridlinesVisible(false); + var fore = UIManager.getColor("Label.foreground"); + plot.setDomainGridlinePaint(fore); + plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); plot.getRenderer(0).setSeriesShape(0, @@ -244,6 +247,9 @@ private static ChartPanel createEmptyPanel() { cp.setMinimumDrawHeight(10); chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setBackgroundPaint(chart.getBackgroundPaint()); + Chart.setAxisFontColor(plot.getDomainAxis(), fore); + Chart.setAxisFontColor(plot.getRangeAxis(), fore); return cp; } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 567c4494..f638f4d7 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -48,7 +48,6 @@ public class SearchOptionsFrame extends JInternalFrame { private final JTable rightTable; private final PathSolversList pathList; - private final static Font FONT = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); private final static List pathSolvers = instancesOf(PathOptimiser.class); private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{MAXTEMP}; @@ -196,7 +195,6 @@ public PathOptimiser getElementAt(int index) { } }); - setFont(FONT); setSelectionMode(SINGLE_SELECTION); setCellRenderer(new SearchListRenderer()); diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 880e10d0..f56d6ed1 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -55,6 +55,8 @@ public class TaskControlFrame extends JFrame { private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; + + private final static int ICON_SIZE = 16; public static TaskControlFrame getInstance() { return instance; @@ -188,26 +190,26 @@ private void initComponents() { setJMenuBar(mainMenu); logFrame = new LogFrame(); - logFrame.setFrameIcon(loadIcon("log.png", 20, Color.white)); + logFrame.setFrameIcon(loadIcon("log.png", ICON_SIZE, Color.white)); resultsFrame = new ResultFrame(); - resultsFrame.setFrameIcon(loadIcon("result.png", 20, Color.white)); + resultsFrame.setFrameIcon(loadIcon("result.png", ICON_SIZE, Color.white)); previewFrame = new PreviewFrame(); - previewFrame.setFrameIcon(loadIcon("preview.png", 20, Color.white)); + previewFrame.setFrameIcon(loadIcon("preview.png", ICON_SIZE, Color.white)); taskManagerFrame = new TaskManagerFrame(); - taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20, Color.white)); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", ICON_SIZE, Color.white)); graphFrame = MainGraphFrame.getInstance(); - graphFrame.setFrameIcon(loadIcon("curves.png", 20, Color.white)); + graphFrame.setFrameIcon(loadIcon("curves.png", ICON_SIZE, Color.white)); problemStatementFrame = new ProblemStatementFrame(); - problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20, Color.white)); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", ICON_SIZE, Color.white)); modelFrame = new ModelSelectionFrame(); - modelFrame.setFrameIcon(loadIcon("stored.png", 20, Color.white)); + modelFrame.setFrameIcon(loadIcon("stored.png", ICON_SIZE, Color.white)); searchOptionsFrame = new SearchOptionsFrame(); - searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); + searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", ICON_SIZE, Color.white)); pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); + pulseFrame.setFrameIcon(loadIcon("pulse.png", ICON_SIZE, Color.white)); pulseFrame.setVisible(false); /* From caf3237e7539d5bdd2a4c8130c17a5a0d2684413 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 9 Jan 2023 00:19:47 +0300 Subject: [PATCH 108/116] Pulse & logs - Better handling of pulse discretisation - Fixed graphical log updates - Better pulse visualisation --- pom.xml | 13 +- .../pulse/problem/laser/DiscretePulse.java | 172 +++++++++++------- .../pulse/problem/laser/DiscretePulse2D.java | 4 +- .../pulse/problem/laser/NumericPulse.java | 2 +- .../pulse/problem/laser/RectangularPulse.java | 4 +- .../pulse/problem/laser/TrapezoidalPulse.java | 2 +- .../problem/schemes/DifferenceScheme.java | 7 +- src/main/java/pulse/problem/schemes/Grid.java | 34 +--- .../java/pulse/problem/schemes/Grid2D.java | 1 - .../problem/statements/AdiabaticSolution.java | 2 +- .../pulse/problem/statements/Problem.java | 5 +- .../java/pulse/problem/statements/Pulse.java | 4 +- .../statements/model/ThermalProperties.java | 2 +- .../statistics/ModelSelectionCriterion.java | 29 +++ .../search/statistics/ResidualStatistic.java | 36 +++- src/main/java/pulse/tasks/Calculation.java | 55 +++--- .../java/pulse/tasks/logs/DataLogEntry.java | 2 +- src/main/java/pulse/tasks/logs/LogEntry.java | 20 +- .../pulse/ui/components/GraphicalLogPane.java | 8 +- .../java/pulse/ui/components/LogChart.java | 43 +++-- .../java/pulse/ui/components/PulseChart.java | 10 +- src/main/resources/NumericProperty.xml | 9 +- src/main/resources/Version.txt | 2 +- src/main/resources/images/leaf.png | Bin 0 -> 24476 bytes src/main/resources/messages.properties | 2 +- 25 files changed, 294 insertions(+), 174 deletions(-) create mode 100644 src/main/resources/images/leaf.png diff --git a/pom.xml b/pom.xml index e5118865..9605d72d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.97 + 1.97b PULsE Processing Unit for Laser flash Experiments @@ -18,8 +18,7 @@ The Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt - - + org.jfree @@ -27,11 +26,11 @@ 1.5.0 - com.weblookandfeel - weblaf-ui - 1.2.13 + com.formdev + flatlaf + 3.0 - + org.apache.commons commons-math3 3.6.1 diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 51f58059..88913244 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,7 +7,10 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.tasks.SearchTask; +import pulse.util.PropertyHolderListener; /** * A {@code DiscretePulse} is an object that acts as a medium between the @@ -20,9 +23,10 @@ public class DiscretePulse { private final Grid grid; private final Pulse pulse; - + private final ExperimentalData data; + private double widthOnGrid; - private double timeConversionFactor; + private double characteristicTime; private double invTotalEnergy; //normalisation factor /** @@ -49,29 +53,28 @@ public class DiscretePulse { */ public DiscretePulse(Problem problem, Grid grid) { this.grid = grid; - timeConversionFactor = problem.getProperties().timeFactor(); + characteristicTime = problem.getProperties().characteristicTime(); this.pulse = problem.getPulse(); Object ancestor = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), "Problem has not been assigned to a SearchTask"); - ExperimentalData data = - (ExperimentalData) ( ((SearchTask) ancestor).getInput() ); - init(data); - + data = (ExperimentalData) (((SearchTask) ancestor).getInput()); + init(); + + PropertyHolderListener phl = e -> { + characteristicTime = problem.getProperties().characteristicTime(); + widthOnGrid = 0; + init(); + }; + pulse.addListener(e -> { - timeConversionFactor = problem.getProperties().timeFactor(); - init(data); + widthOnGrid = 0; + init(); }); - - } - - private void init(ExperimentalData data) { - widthOnGrid = 0; - recalculate(); - pulse.getPulseShape().init(data, this); - invTotalEnergy = 1.0/totalEnergy(); + problem.addListener(phl); + } /** @@ -91,39 +94,77 @@ public double laserPowerAt(double time) { * * @see pulse.problem.schemes.Grid.gridTime(double,double) */ - public final void recalculate() { - final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - final double resolvedWidth = timeConversionFactor / getWidthToleranceFactor(); + public final void init() { + final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); + final double resolvedWidth = resolvedPulseWidthSeconds(); final double EPS = 1E-10; - + + double oldValue = widthOnGrid; + this.widthOnGrid = pulseWidthGrid(); + /** * The pulse is too short, which makes calculations too expensive. Can * we replace it with a rectangular pulse shape instead? */ - - if (nominalWidth < resolvedWidth - EPS && widthOnGrid < EPS) { + if (nominalWidth < resolvedWidth - EPS && oldValue < EPS) { //change shape to rectangular var shape = new RectangularPulse(); - pulse.setPulseShape(shape); - //change pulse width - setDiscreteWidth(resolvedWidth); + pulse.setPulseShape(shape); shape.init(null, this); - //adjust the pulse object to update the visualised pulse - } else if(nominalWidth > resolvedWidth + EPS) { - setDiscreteWidth(nominalWidth); - } - - invTotalEnergy = 1.0/totalEnergy(); - + } else { + pulse.getPulseShape().init(data, this); + } + + invTotalEnergy = 1.0 / totalEnergy(); } - + /** - * Calculates the total pulse energy using a numerical integrator.The - * normalisation factor is then equal to the inverse total energy. - * @return the total pulse energy, assuming sample area fully covered by the beam + * Optimises the {@code Grid} parameters so that the timestep is + * sufficiently small to enable accurate pulse correction. + *

+ * This can change the {@code tauFactor} and {@code tau} variables in the + * {@code Grid} object if {@code discretePulseWidth/(M - 1) < grid.tau}, + * where M is the required number of pulse calculations. + *

+ * + * @see PulseTemporalShape.getRequiredDiscretisation() */ + public double pulseWidthGrid() { + //minimum number of points for pulse calculation + int reqPoints = pulse.getPulseShape().getRequiredDiscretisation(); + //physical pulse width in time units + double experimentalWidth = (double) pulse.getPulseWidth().getValue(); + + //minimum resolved pulse width in time units for that specific problem + double resolvedWidth = resolvedPulseWidthSeconds(); + + double pWidth = Math.max(experimentalWidth, resolvedWidth); + + final double EPS = 1E-10; + double newTau = pWidth / characteristicTime / reqPoints; + + double result = 0; + + if (newTau < grid.getTimeStep() - EPS) { + double newTauFactor = (double) grid.getTimeFactor().getValue() / 2.0; + grid.setTimeFactor(derive(TAU_FACTOR, newTauFactor)); + result = pulseWidthGrid(); + } else { + result = grid.gridTime(pWidth, characteristicTime); + } + + return result; + } + + /** + * Calculates the total pulse energy using a numerical integrator.The + * normalisation factor is then equal to the inverse total energy. + * + * @return the total pulse energy, assuming sample area fully covered by the + * beam + */ public final double totalEnergy() { var pulseShape = pulse.getPulseShape(); @@ -140,27 +181,22 @@ public double integrand(double... vars) { } /** - * Gets the discrete dimensionless pulse width, which is a multiplier of the current - * grid timestep. The pulse width is converted to the dimensionless pulse width by - * dividing the real value by l2/a. + * Gets the discrete dimensionless pulse width, which is a multiplier of the + * current grid timestep. The pulse width is converted to the dimensionless + * pulse width by dividing the real value by l2/a. * * @return the dimensionless pulse width mapped to the grid. */ public double getDiscreteWidth() { return widthOnGrid; } - - private void setDiscreteWidth(double width) { - widthOnGrid = grid.gridTime(width, timeConversionFactor); - grid.adjustTimeStep(this); - } /** * Gets the physical {@code Pulse} * * @return the {@code Pulse} object */ - public Pulse getPulse() { + public Pulse getPhysicalPulse() { return pulse; } @@ -172,36 +208,38 @@ public Pulse getPulse() { public Grid getGrid() { return grid; } - + /** - * Gets the dimensional factor required to convert real time variable into - * a dimensional variable, defined in the {@code Problem} class + * Gets the dimensional factor required to convert real time variable into a + * dimensional variable, defined in the {@code Problem} class + * * @return the conversion factor */ - - public double getConversionFactor() { - return timeConversionFactor; + public double getCharacteristicTime() { + return characteristicTime; } - + /** - * Gets the minimal resolved pulse width defined by the {@code WIDTH_TOLERANCE_FACTOR} - * and the characteristic time given by the {@code getConversionFactor}. - * @return + * Gets the minimal resolved pulse width defined by the + * {@code WIDTH_TOLERANCE_FACTOR} and the characteristic time given by the + * {@code getConversionFactor}. + * + * @return */ - - public double resolvedPulseWidth() { - return timeConversionFactor / getWidthToleranceFactor(); + public double resolvedPulseWidthSeconds() { + return characteristicTime / getWidthToleranceFactor(); } - - /** - * Assuming a characteristic time is divided by the return value of this method - * and is set to the minimal resolved pulse width, shows how small a pulse width - * can be to enable finite pulse correction. - * @return the smallest fraction of a characteristic time resolved as a finite pulse. + + /** + * Assuming a characteristic time is divided by the return value of this + * method and is set to the minimal resolved pulse width, shows how small a + * pulse width can be to enable finite pulse correction. + * + * @return the smallest fraction of a characteristic time resolved as a + * finite pulse. */ - public int getWidthToleranceFactor() { return WIDTH_TOLERANCE_FACTOR; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 00f884fd..02a60f09 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -26,7 +26,7 @@ public class DiscretePulse2D extends DiscretePulse { * This had to be decreased for the 2d pulses. */ - private final static int WIDTH_TOLERANCE_FACTOR = 2000; + private final static int WIDTH_TOLERANCE_FACTOR = 1000; /** * The constructor for {@code DiscretePulse2D}. @@ -90,7 +90,7 @@ private void calcPulseSpot(ExtendedThermalProperties properties) { * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) */ public final void evalPulseSpot() { - var pulse = (Pulse2D) getPulse(); + var pulse = (Pulse2D) getPhysicalPulse(); var grid2d = (Grid2D) getGrid(); final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index e834f024..9dbaadb6 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -64,7 +64,7 @@ public void init(ExperimentalData data, DiscretePulse pulse) { setPulseWidthOf(problem); //convert to dimensionless time and interpolate - double timeFactor = problem.getProperties().timeFactor(); + double timeFactor = problem.getProperties().characteristicTime(); doInterpolation(timeFactor); } diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index 2ba6b9da..583a92e1 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -14,7 +14,7 @@ */ public class RectangularPulse extends PulseTemporalShape { - private final static int MIN_POINTS = 4; + private final static int MIN_POINTS = 2; /** * @param time the time measured from the start of the laser pulse. @@ -41,4 +41,4 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index c305b816..a061a875 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -22,7 +22,7 @@ public class TrapezoidalPulse extends PulseTemporalShape { private double fall; private double h; - private final static int MIN_POINTS = 6; + private final static int MIN_POINTS = 8; /** * Constructs a trapezoidal pulse using a default segmentation principle. diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 59c92208..8ad8dfd8 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import java.util.Objects; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -92,7 +93,7 @@ protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); } - discretePulse.recalculate(); + discretePulse.init(); clearArrays(); } @@ -114,13 +115,13 @@ public void runTimeSequence(Problem problem, final double offset, final double e int numPoints = (int) curve.getNumPoints().getValue(); final double startTime = (double) curve.getTimeShift().getValue(); - final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); + final double timeSegment = (endTime - startTime - offset) / problem.getProperties().characteristicTime(); double tau = grid.getTimeStep(); final double dt = timeSegment / (numPoints - 1); timeInterval = Math.max( (int) (dt / tau), 1); - double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); + double wFactor = timeInterval * tau * problem.getProperties().characteristicTime(); // First point (index = 0) is always (0.0, 0.0) curve.addPoint(0.0, 0.0); diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index f68467bf..2d7128d0 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -11,6 +11,7 @@ import java.util.Set; import pulse.problem.laser.DiscretePulse; +import pulse.problem.statements.Pulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.util.PropertyHolder; @@ -63,37 +64,6 @@ public Grid copy() { return new Grid(getGridDensity(), getTimeFactor()); } - /** - * Optimises the {@code Grid} parameters so that the timestep is - * sufficiently small to enable accurate pulse correction. - *

- * This can change the {@code tauFactor} and {@code tau} variables in the - * {@code Grid} object if {@code discretePulseWidth/(M - 1) < grid.tau}, - * where M is the required number of pulse calculations. - *

- * - * @param pulse the discrete pulse representation - * @see PulseTemporalShape.getRequiredDiscretisation() - */ - public final void adjustTimeStep(DiscretePulse pulse) { - double timeFactor = pulse.getConversionFactor(); - - final int reqPoints = pulse.getPulse().getPulseShape().getRequiredDiscretisation(); - - double pNominalWidth = (double) pulse.getPulse().getPulseWidth().getValue(); - double pResolvedWidth = pulse.resolvedPulseWidth(); - double pWidth = pNominalWidth < pResolvedWidth ? pResolvedWidth : pNominalWidth; - - double newTau = pWidth / timeFactor / (reqPoints > 1 ? reqPoints - 1 : 1); - double newTauFactor = newTau / (hx * hx); - - final double EPS = 1E-10; - if (newTauFactor < tauFactor - EPS) { - setTimeFactor(derive(TAU_FACTOR, newTauFactor)); - } - - } - /** * The listed properties include {@code GRID_DENSITY} and * {@code TAU_FACTOR}. @@ -223,7 +193,7 @@ public void setTimeFactor(NumericProperty timeFactor) { * @return a double representing the time on the finite grid */ public final double gridTime(double time, double dimensionFactor) { - return rint((time / dimensionFactor) / tau) * tau; + return ( (int) (time / dimensionFactor / tau) ) * tau; } /** diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index 6b104197..af2284bc 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -68,7 +68,6 @@ public void adjustStepSize(DiscretePulse pulse) { adjustStepSize(pulse); } - adjustTimeStep(pulse); } @Override diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index a335facd..5fbb5a84 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -75,7 +75,7 @@ public static HeatingCurve classicSolution(Problem p, double timeLimit, int prec private final static double solutionAt(ThermalProperties p, double time, int precision) { final double EPS = 1E-8; - final double Fo = time / p.timeFactor(); + final double Fo = time / p.characteristicTime(); if (time < EPS) { return 0; diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index f2285151..a9752e23 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -245,7 +246,7 @@ public void optimisationVector(ParameterVector output) { p.setTransform(new StickTransform(bounds)); break; case TIME_SHIFT: - double magnitude = 0.25 * properties.timeFactor(); + double magnitude = 0.25 * properties.characteristicTime(); bounds = new Segment(-magnitude, magnitude); value = (double) curve.getTimeShift().getValue(); break; @@ -446,4 +447,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 1b0fbf34..eac31f91 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -7,7 +7,6 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import java.util.List; -import java.util.Objects; import java.util.Set; import pulse.input.ExperimentalData; @@ -217,7 +216,8 @@ public PulseTemporalShape getPulseShape() { public void setPulseShape(PulseTemporalShape pulseShape) { this.pulseShape = pulseShape; - pulseShape.setParent(this); + pulseShape.setParent(this); + } } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 153faa10..295f6b86 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -325,7 +325,7 @@ public double maxRadiationBiot() { * * @return the time factor */ - public double timeFactor() { + public double characteristicTime() { return l * l / a; } diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index fbba1ccb..69518450 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -37,6 +37,35 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { this.criterion = another.criterion; } + @Override + public int hashCode() { + int hash = 7; + hash = 43 * hash + this.kq; + hash = 43 * hash + (int) (Double.doubleToLongBits(this.criterion) ^ (Double.doubleToLongBits(this.criterion) >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ModelSelectionCriterion other = (ModelSelectionCriterion) obj; + if (this.kq != other.kq) { + return false; + } + if (Double.doubleToLongBits(this.criterion) != Double.doubleToLongBits(other.criterion)) { + return false; + } + return true; + } + @Override public void evaluate(GeneralTask t) { kq = t.searchVector().dimension(); //number of parameters diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index e4c92a30..5b9d3e53 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; import java.util.List; +import java.util.Objects; import pulse.DiscreteInput; import pulse.Response; import pulse.input.IndexRange; @@ -31,6 +32,39 @@ public abstract class ResidualStatistic extends Statistic { private List rx; private List ry; + @Override + public int hashCode() { + int hash = 5; + hash = 53 * hash + (int) (Double.doubleToLongBits(this.statistic) ^ (Double.doubleToLongBits(this.statistic) >>> 32)); + hash = 53 * hash + Objects.hashCode(this.rx); + hash = 53 * hash + Objects.hashCode(this.ry); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ResidualStatistic other = (ResidualStatistic) obj; + if (Double.doubleToLongBits(this.statistic) != Double.doubleToLongBits(other.statistic)) { + return false; + } + if (!Objects.equals(this.rx, other.rx)) { + return false; + } + if (!Objects.equals(this.ry, other.ry)) { + return false; + } + return true; + } + public ResidualStatistic() { super(); ry = new ArrayList<>(); @@ -144,4 +178,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 5c673d32..2e38a798 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -9,6 +9,7 @@ import static pulse.util.Reflexive.instantiate; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import pulse.Response; @@ -330,27 +331,6 @@ public int compareTo(Calculation arg0) { return sThis.compareTo(sAnother); } - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (o == null) { - return false; - } - - if (!(o instanceof Calculation)) { - return false; - } - - var c = (Calculation) o; - - return (os.getStatistic().equals(c.getOptimiserStatistic().getStatistic()) - && rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())); - - } - public static InstanceDescriptor getModelSelectionDescriptor() { return instanceDescriptor; } @@ -395,4 +375,37 @@ public double objectiveFunction(GeneralTask task) throws SolverException { return (double) os.getStatistic().getValue(); } + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.problem); + hash = 79 * hash + Objects.hashCode(this.scheme); + hash = 79 * hash + Objects.hashCode(this.result); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Calculation other = (Calculation) obj; + if (!Objects.equals(this.problem, other.problem)) { + return false; + } + if (!Objects.equals(this.scheme, other.scheme)) { + return false; + } + if (!Objects.equals(this.result, other.result)) { + return false; + } + return true; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index c3c5ab3d..94b87746 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -127,4 +127,4 @@ public String toString() { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index bfd84fbd..f311fd16 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -20,9 +20,9 @@ */ public class LogEntry { - private Identifier identifier; - private LocalTime time; - private final Response response; + private final Identifier identifier; + private final LocalTime time; + private final LogEntry previous; /** *

@@ -36,11 +36,17 @@ public LogEntry(SearchTask t) { Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); - this.response = t.getResponse(); + var list = t.getLog().getLogEntries(); + if(list != null && !list.isEmpty()) { + previous = list.get(list.size() - 1); + } + else { + previous = null; + } } - public Response getResponse() { - return response; + public LogEntry getPreviousEntry() { + return previous; } public Identifier getIdentifier() { @@ -51,4 +57,4 @@ public LocalTime getTime() { return time; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index a50e4941..82f84248 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -1,6 +1,8 @@ package pulse.ui.components; import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -14,9 +16,13 @@ public class GraphicalLogPane extends AbstractLogger { private final LogChart chart; + private final JScrollPane pane; public GraphicalLogPane() { + pane = new JScrollPane(); + pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); chart = new LogChart(); + pane.setViewportView(chart.getChartPanel()); TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { chart.clear(); @@ -26,7 +32,7 @@ public GraphicalLogPane() { @Override public JComponent getGUIComponent() { - return chart.getChartPanel(); + return pane; } @Override diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java index 1f180ca9..ba40ae40 100644 --- a/src/main/java/pulse/ui/components/LogChart.java +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -6,9 +6,14 @@ import java.awt.Color; import static java.awt.Color.WHITE; import static java.awt.Color.black; +import java.awt.Dimension; import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.swing.SwingUtilities; import org.jfree.chart.JFreeChart; @@ -35,6 +40,7 @@ import pulse.tasks.TaskManager; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Log; +import pulse.tasks.logs.StateEntry; import pulse.tasks.logs.Status; import pulse.tasks.processing.Buffer; import pulse.ui.ColorGenerator; @@ -44,7 +50,8 @@ public class LogChart extends AuxPlotter { private final Map plots; private Color[] colors; private static final ColorGenerator cg = new ColorGenerator(); - private Response r; + public final static int HEIGHT_FACTOR = 75; + public final static int MARGIN = 10; public LogChart() { var plot = new CombinedDomainXYPlot(new NumberAxis("Iteration")); @@ -58,13 +65,14 @@ public LogChart() { public final void clear() { var p = (CombinedDomainXYPlot) getPlot(); - p.getDomainAxis().setAutoRange(true); if (p != null) { + if (p.getDomainAxis() != null) { + p.getDomainAxis().setAutoRange(true); + } plots.values().stream().forEach(pp -> p.remove(pp)); } plots.clear(); colors = new Color[0]; - r = null; } private void setLegendTitle(Plot plot) { @@ -88,6 +96,11 @@ public final void add(ParameterIdentifier key, int no) { plots.put(key, plot); ((CombinedDomainXYPlot) getPlot()).add(plot); + int height = HEIGHT_FACTOR * plots.size(); + int width = getChartPanel().getParent().getWidth() - MARGIN; + getChartPanel().setPreferredSize(new Dimension(width, height)); + getChartPanel().revalidate(); + var dataset = new XYSeriesCollection(); var series = new XYSeries(key.toString()); @@ -115,7 +128,7 @@ public void changeAxis(boolean iterationMode) { var domainAxis = (NumberAxis) getPlot().getDomainAxis(); domainAxis.setLabel(iterationMode ? "Iteration" : "Time (ms)"); domainAxis.setAutoRange(!iterationMode); - if(iterationMode) { + if (iterationMode) { domainAxis.setTickUnit(new NumberTickUnit(1)); } else { domainAxis.setAutoTickUnitSelection(true); @@ -126,10 +139,18 @@ public void changeAxis(boolean iterationMode) { public void plot(Log l) { requireNonNull(l); - l.getLogEntries().stream() - .filter(le -> le instanceof DataLogEntry) - .forEach(d -> plot((DataLogEntry) d, - Duration.between(l.getStart(), d.getTime()).toMillis())); + List startTimes = l.getLogEntries().stream() + .filter(le -> le instanceof DataLogEntry && le.getPreviousEntry() instanceof StateEntry) + .map(entry -> entry.getTime()).collect(Collectors.toList()); + + if (!startTimes.isEmpty()) { + var recentStart = startTimes.get(startTimes.size() - 1); + l.getLogEntries().stream().filter(le -> le.getTime().isAfter(recentStart)) + .filter(e -> e instanceof DataLogEntry).forEach(dle + -> plot((DataLogEntry) dle, + Duration.between(recentStart, dle.getTime()).toMillis())); + } + } private static void adjustRange(XYPlot pl, int iteration, int bufSize) { @@ -138,7 +159,7 @@ private static void adjustRange(XYPlot pl, int iteration, int bufSize) { var domainAxis = pl.getDomainAxis(); var r = domainAxis.getRange(); var newR = new Range(lower, lower + bufSize); - + if (!r.equals(newR) && iteration > lower) { ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); } @@ -185,7 +206,7 @@ public final void plot(DataLogEntry dle, double iterationOrTime) { runningAverage.add(iterationOrTime, buf.average(np.getKeyword())); } - SwingUtilities.invokeLater(() -> adjustRange((XYPlot)pl, (int)iterationOrTime, bufSize)); + SwingUtilities.invokeLater(() -> adjustRange((XYPlot) pl, (int) iterationOrTime, bufSize)); } else { var domainAxis = ((XYPlot) pl).getDomainAxis(); @@ -196,4 +217,4 @@ public final void plot(DataLogEntry dle, double iterationOrTime) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index db6e7478..5b4151a2 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -4,13 +4,11 @@ import static java.awt.Color.black; import static java.awt.Font.PLAIN; import static java.util.Objects.requireNonNull; -import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; -import org.jfree.chart.ChartFactory; import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.renderer.xy.XYDifferenceRenderer; @@ -63,8 +61,8 @@ public void plot(Calculation c) { var pulseDataset = new XYSeriesCollection(); - pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep(), - problem.getProperties().timeFactor(), startTime)); + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep()/20.0, + problem.getProperties().characteristicTime(), startTime)); getPlot().setDataset(0, pulseDataset); } @@ -76,8 +74,8 @@ private static XYSeries series(Pulse pulse, double dx, double timeFactor, double double timeLimit = pulseShape.getPulseWidth(); double x = startTime/timeFactor; - series.add(TO_MILLIS * (startTime - dx * timeFactor / 10.), 0.0); - series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 10.)), 0.0); + series.add(TO_MILLIS * (startTime - dx * timeFactor / 100.), 0.0); + series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 100.)), 0.0); for (int i = 0, numPoints = (int) (timeLimit/dx); i < numPoints; i++) { series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime/timeFactor)); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 9640ce21..06c47f7d 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,10 @@ + + 1l_GFkuWY3;; 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
Time taken: LogToolBar.FileFormatDescriptor=HTML Log Files LogToolBar.SaveButton=Save Log -LogToolBar.Verbose=Verbose log +LogToolBar.Verbose=Graphical NumberEditor.EditText=Edit NumberEditor.IllegalTableEntry=Illegal table entry NumberEditor.InvalidText=Invalid Text Entered From dd0a6aa8f76740bedc81d770d6ddc2dc5eddaa22 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Wed, 11 Jan 2023 12:44:35 +0300 Subject: [PATCH 109/116] General usability changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed sample name parsing in Netzsch files - Made some changes to the text displayed in the GUI - Tooltips now more informative and well-formatted - Added missing SwingUtilities.invokeLater(…) method when updating graphical logs - Added safety checks for graphical logging - Added ‘finished’ boolean in the Log class, which helps graphical logs to be displayed correctly - Migrated to JfreeChart 1.5.4, which fixes issues with graphs in resized windows - Switched from using ExecutionServices and ForkJoinPools to using CompletableFutures. Removed redundant blocks where concurrency has been poorly handled. - Removed argument from checkProblems() in SearchTask - Fixed execution of queued tasks hanging because of conflicting ForkJoinPools - Fixed tracker dialogs not closing on end of scheme selection - Fixed focus lost problem in ProblemStatementFrame --- pom.xml | 4 +- src/main/java/pulse/input/Metadata.java | 4 +- .../pulse/io/readers/NetzschCSVReader.java | 6 + src/main/java/pulse/problem/schemes/Grid.java | 12 +- .../java/pulse/problem/schemes/Grid2D.java | 5 - .../java/pulse/problem/statements/Pulse.java | 10 +- .../pulse/problem/statements/Pulse2D.java | 3 +- .../statements/model/AbsorptionModel.java | 5 +- .../java/pulse/properties/SampleName.java | 34 +++-- .../pulse/search/direction/LMOptimiser.java | 2 +- src/main/java/pulse/tasks/Calculation.java | 70 ++++++---- src/main/java/pulse/tasks/SearchTask.java | 117 ++++++++-------- src/main/java/pulse/tasks/TaskManager.java | 65 ++++----- src/main/java/pulse/tasks/logs/Log.java | 7 + .../pulse/ui/components/GraphicalLogPane.java | 24 ++-- .../java/pulse/ui/components/LogChart.java | 10 +- .../pulse/ui/components/TaskPopupMenu.java | 4 +- .../components/buttons/ExecutionButton.java | 2 +- .../controllers/AccessibleTableRenderer.java | 44 +++--- .../ui/components/panels/ProblemToolbar.java | 2 +- src/main/java/pulse/ui/frames/LogFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 132 ++++++++---------- .../pulse/ui/frames/SearchOptionsFrame.java | 2 +- src/main/java/pulse/util/Accessible.java | 19 +-- .../java/pulse/util/InstanceDescriptor.java | 5 +- src/main/resources/NumericProperty.xml | 6 +- 26 files changed, 293 insertions(+), 306 deletions(-) diff --git a/pom.xml b/pom.xml index 9605d72d..f7140a29 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.97b + 1.97c PULsE Processing Unit for Laser flash Experiments @@ -23,7 +23,7 @@ org.jfree jfreechart - 1.5.0 + 1.5.4 com.formdev diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index cd9369cb..ae00b92e 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -61,7 +61,7 @@ public class Metadata extends PropertyHolder implements Reflexive { * experimental setup. */ public Metadata(NumericProperty temperature, int externalId) { - sampleName = new SampleName(); + sampleName = new SampleName(null); setExternalID(externalId); pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); data = new TreeSet<>(); @@ -186,7 +186,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public List listedTypes() { List list = super.listedTypes(); - list.add(new SampleName()); + list.add(new SampleName("")); list.add(pulseDescriptor); return list; } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 85c4f1c8..d9837f13 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -23,6 +23,7 @@ import pulse.input.Metadata; import pulse.input.Range; import pulse.properties.NumericPropertyKeyword; +import pulse.properties.SampleName; import pulse.ui.Messages; /** @@ -49,6 +50,7 @@ public class NetzschCSVReader implements CurveReader { private final static String DIAMETER = "Diameter"; private final static String L_PULSE_WIDTH = "Laser_pulse_width"; private final static String PULSE_WIDTH = "Pulse_width"; + private final static String MATERIAL = "Material"; /** * Note comma is included as a delimiter character here. @@ -115,6 +117,9 @@ public List read(File file) throws IOException { int shotId = determineShotID(reader, file); + String name = findLineByLabel(reader, MATERIAL, DETECTOR_SPOT_SIZE, true) + .substring(MATERIAL.length() + 1) + .replaceAll(delims, ""); String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); double spotSize = 0; @@ -164,6 +169,7 @@ public List read(File file) throws IOException { if (pulseWidth > 1e-10) { met.set(NumericPropertyKeyword.PULSE_WIDTH, derive(NumericPropertyKeyword.PULSE_WIDTH, pulseWidth)); } + met.setSampleName(new SampleName(name)); met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 2d7128d0..7bbfc9a8 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -211,15 +211,9 @@ public final double gridAxialDistance(double distance, double lengthFactor) { @Override public String toString() { - var sb = new StringBuilder(); - sb.append(""). - append(getClass().getSimpleName()) - .append(": hx=") - .append(format("%3.2e", hx)) - .append("; "). - append("τ=") - .append(format("%3.2e", tau)) - .append("; "); + var sb = new StringBuilder("Grid"); + sb.append(String.format("%n %-25s", this.getGridDensity())); + sb.append(String.format("%n %-25s", this.getTimeFactor())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index af2284bc..e35a6e33 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -99,11 +99,6 @@ public double gridRadialDistance(double radial, double lengthFactor) { return rint((radial / lengthFactor) / hy) * hy; } - @Override - public String toString() { - return super.toString() + "hy=" + format("%3.3f", hy); - } - public double getYStep() { return hy; } diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index eac31f91..2b5b988e 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -163,12 +163,10 @@ public void setLaserEnergy(NumericProperty laserEnergy) { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(getPulseShape()); - sb.append(" ; "); - sb.append(getPulseWidth()); - sb.append(" ; "); - sb.append(getLaserEnergy()); + StringBuilder sb = new StringBuilder("Pulse:"); + sb.append(String.format("%n %-25s", getPulseShape())); + sb.append(String.format("%n %-25s", getPulseWidth())); + sb.append(String.format("%n %-25s", getLaserEnergy())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index 16895b60..11d4bef3 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -60,8 +60,7 @@ public void setSpotDiameter(NumericProperty spotDiameter) { @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); - sb.append(" ; "); - sb.append(getSpotDiameter()); + sb.append(String.format("%n %-25s", getSpotDiameter())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 9165b081..aacbd12f 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -95,7 +95,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public String toString() { - return getClass().getSimpleName() + " : " + absorptionMap.get(LASER) + " ; " + absorptionMap.get(THERMAL); + var sb = new StringBuilder(getSimpleName()); + sb.append(String.format("%n %-25s", absorptionMap.get(LASER))); + sb.append(String.format("%n %-25s", absorptionMap.get(THERMAL))); + return sb.toString(); } @Override diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 5ef74195..2f9f6cc0 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -6,8 +6,8 @@ public class SampleName implements Property { private String name; - public SampleName() { - //null name + public SampleName(String name) { + this.name = name; } @Override @@ -41,22 +41,28 @@ public String toString() { } @Override - public boolean equals(Object o) { - if (o == this) { + public int hashCode() { + int hash = 5; + hash = 43 * hash + Objects.hashCode(this.name); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { return true; } - - if (o == null) { + if (obj == null) { return false; } - - boolean result = false; - - if (o instanceof SampleName) { - result = name.equals(((SampleName) o).getValue()); + if (getClass() != obj.getClass()) { + return false; } - - return result; + final SampleName other = (SampleName) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + return true; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 86ee9e4b..d3b22c74 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -43,7 +43,7 @@ public class LMOptimiser extends GradientBasedOptimiser { * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 4; + public final static int MAX_FAILED_ATTEMPTS = 5; private LMOptimiser() { super(); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 2e38a798..40d857d8 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -79,7 +79,7 @@ public Calculation(Calculation c) { } instanceDescriptor.addListener(() -> initModelCriterion(rs)); } - + public void conformTo(UpwardsNavigable owner) { problem.setParent(owner); scheme.setParent(owner); @@ -87,13 +87,13 @@ public void conformTo(UpwardsNavigable owner) { os.setParent(owner); result.setParent(owner); } - + public void clear() { this.status = INCOMPLETE; this.problem = null; this.scheme = null; } - + /** *

* After setting and adopting the {@code problem} by this @@ -201,31 +201,40 @@ public Status getStatus() { */ public boolean setStatus(Status status) { - boolean changeStatus = true; - - switch (this.status) { - case QUEUED: - case IN_PROGRESS: - switch (status) { - case QUEUED: - case READY: - case INCOMPLETE: - changeStatus = false; - break; - default: - } - break; - case FAILED: - case EXECUTION_ERROR: - case INCOMPLETE: - //if the TaskManager attempts to run this calculation - changeStatus = status != Status.QUEUED; - break; - default: - } + boolean changeStatus = false; + + if (this.getStatus() != status) { + + changeStatus = true; + + //current status is given by ** this.status ** + //new status is the ** argument ** of this method + switch (this.status) { + case QUEUED: + //do not change status to queued, ready or incomplete if already in progress + case IN_PROGRESS: + switch (status) { + case QUEUED: + case READY: + case INCOMPLETE: + changeStatus = false; + break; + default: + } + break; + case FAILED: + case EXECUTION_ERROR: + case INCOMPLETE: + //if the TaskManager attempts to run this calculation + changeStatus = status != Status.QUEUED; + break; + default: + } + + if (changeStatus) { + this.status = status; + } - if (changeStatus) { - this.status = status; } return changeStatus; @@ -350,14 +359,14 @@ public void setResult(Result result) { public double evaluate(double t) { return problem.getHeatingCurve().interpolateSignalAt(t); } - + @Override public Segment accessibleRange() { var hc = problem.getHeatingCurve(); return new Segment(hc.timeAt(0), hc.timeLimit()); } - /** + /** * This will use the current {@code DifferenceScheme} to solve the * {@code Problem} for this {@code SearchTask} and calculate the SSR value * showing how well (or bad) the calculated solution describes the @@ -367,7 +376,6 @@ public Segment accessibleRange() { * @return the value of SSR (sum of squared residuals). * @throws pulse.problem.schemes.solvers.SolverException */ - @Override public double objectiveFunction(GeneralTask task) throws SolverException { process(); @@ -408,4 +416,4 @@ public boolean equals(Object obj) { return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 91633c81..9cf3e226 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -103,7 +103,7 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - + private void addListeners() { InterpolationDataset.addListener(e -> { if (current.getProblem() != null) { @@ -153,13 +153,13 @@ public void clear() { //this.path = null; current.clear(); - this.checkProblems(true); + this.checkProblems(); } - + public List alteredParameters() { - return activeParameters().stream().map(key -> - this.numericProperty(key)).collect(Collectors.toList()); - } + return activeParameters().stream().map(key + -> this.numericProperty(key)).collect(Collectors.toList()); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -181,7 +181,7 @@ public void removeStatusChangeListeners() { public String toString() { return getIdentifier().toString(); } - + /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -209,39 +209,37 @@ public void setExperimentalCurve(ExperimentalData curve) { * using the {@code status.getDetails()} method. *

* - * @param updateStatus */ - public void checkProblems(boolean updateStatus) { + public void checkProblems() { var status = getStatus(); - if (status == DONE) { - return; - } - - var pathSolver = getInstance(); - var s = INCOMPLETE; - - if (current.getProblem() == null) { - s.setDetails(MISSING_PROBLEM_STATEMENT); - } else if (!current.getProblem().isReady()) { - s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); - } else if (current.getScheme() == null) { - s.setDetails(MISSING_DIFFERENCE_SCHEME); - } else if (curve == null) { - s.setDetails(MISSING_HEATING_CURVE); - } else if (pathSolver == null) { - s.setDetails(MISSING_OPTIMISER); - } else if (getBuffer() == null) { - s.setDetails(MISSING_BUFFER); - } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { - s.setDetails(INCOMPATIBLE_OPTIMISER); - } else { - s = READY; - } + if (status != DONE) { + + var pathSolver = getInstance(); + var s = INCOMPLETE; + + if (current.getProblem() == null) { + s.setDetails(MISSING_PROBLEM_STATEMENT); + } else if (!current.getProblem().isReady()) { + s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); + } else if (current.getScheme() == null) { + s.setDetails(MISSING_DIFFERENCE_SCHEME); + } else if (curve == null) { + s.setDetails(MISSING_HEATING_CURVE); + } else if (pathSolver == null) { + s.setDetails(MISSING_OPTIMISER); + } else if (getBuffer() == null) { + s.setDetails(MISSING_BUFFER); + } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { + s.setDetails(INCOMPATIBLE_OPTIMISER); + } else { + s = READY; + } - if (updateStatus) { setStatus(s); + } + } public Identifier getIdentifier() { @@ -263,12 +261,12 @@ private void notifyStatusListeners(StateEntry e) { l.onStatusChange(e); } } - + @Override public void run() { correlationBuffer.clear(); current.setResult(null); - + /* check of status */ switch (getStatus()) { case READY: @@ -278,12 +276,12 @@ public void run() { default: return; } - + current.getProblem().parameterListChanged(); // get updated list of parameters super.run(); } - + /** * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or * {@code READY}, terminates it by setting its status to {@code TERMINATED}. @@ -321,15 +319,15 @@ public CorrelationBuffer getCorrelationBuffer() { public CorrelationTest getCorrelationTest() { return correlationTest; } - + public List getStoredCalculations() { return this.stored; - } + } public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); - } + } public void switchTo(Calculation calc) { current.setParent(null); @@ -366,19 +364,19 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { l.onTaskListChanged(e); } } - + @Override public boolean isInProgress() { return getStatus() == IN_PROGRESS; } - + @Override public void intermediateProcessing() { correlationBuffer.inflate(this); notifyDataListeners(new DataLogEntry(this)); } - - @Override + + @Override public void onSolverException(SolverException e) { setStatus(Status.troubleshoot(e)); } @@ -394,8 +392,8 @@ public void onSolverException(SolverException e) { */ @Override public ParameterVector searchVector() { - var ids = activeParameters().stream().map(id -> - new ParameterIdentifier(id)).collect(Collectors.toList()); + var ids = activeParameters().stream().map(id + -> new ParameterIdentifier(id)).collect(Collectors.toList()); var optimisationVector = new ParameterVector(ids); current.getProblem().optimisationVector(optimisationVector); @@ -455,8 +453,7 @@ public void postProcessing() { } } - - + /** * Finds what properties are being altered in the search of this SearchTask. * @@ -467,16 +464,14 @@ public void postProcessing() { public List activeParameters() { var flags = ActiveFlags.getAllFlags(); //problem dependent - var allActiveParams = ActiveFlags.selectActiveAndListed - (flags, current.getProblem()); + var allActiveParams = ActiveFlags.selectActiveAndListed(flags, current.getProblem()); //problem independent (lower/upper bound) - var listed = ActiveFlags.selectActiveAndListed - (flags, curve.getRange() ); - allActiveParams.addAll(listed); + var listed = ActiveFlags.selectActiveAndListed(flags, curve.getRange()); + allActiveParams.addAll(listed); return allActiveParams; } - - /** + + /** * Will return {@code true} if status could be updated. * * @param status the status of the task @@ -485,20 +480,18 @@ public List activeParameters() { * be updated at this time. * @see Calculation.setStatus() */ - public boolean setStatus(Status status) { Objects.requireNonNull(status); Status oldStatus = getStatus(); - boolean changed = current.setStatus(status) - && (oldStatus != getStatus()); + boolean changed = current.setStatus(status) && oldStatus != status; if (changed) { notifyStatusListeners(new StateEntry(this, status)); } return changed; } - + public Status getStatus() { return current.getStatus(); } @@ -525,7 +518,7 @@ public boolean equals(Object o) { return curve.equals(((SearchTask) o).curve); } - + @Override public String describe() { @@ -553,4 +546,4 @@ public Calculation getResponse() { return current; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 358403df..397971dd 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -24,7 +24,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,8 +65,6 @@ public class TaskManager extends UpwardsNavigable { private boolean singleStatement = true; - private final ForkJoinPool taskPool; - private final List selectionListeners; private final List taskRepositoryListeners; @@ -91,7 +88,6 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList<>(); - taskPool = new ForkJoinPool(ResourceMonitor.getInstance().getThreadsAvailable()); selectionListeners = new CopyOnWriteArrayList<>(); taskRepositoryListeners = new CopyOnWriteArrayList<>(); addHierarchyListener(statementListener); @@ -115,40 +111,41 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(t.getStatus() != Status.DONE); + t.checkProblems(); - //try to start cmputation + // try to start computation // notify listeners computation is about to start - if (!t.setStatus(QUEUED)) { + if (t.getStatus() != QUEUED && !t.setStatus(QUEUED)) { return; } - + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - + // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - Calculation current = (Calculation)t.getResponse(); + Calculation current = (Calculation) t.getResponse(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); if (null == current.getStatus()) { notifyListeners(e); - } - else switch (current.getStatus()) { - case DONE: - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned - notifyListeners(e); - t.storeCalculation(); - break; - case AWAITING_TERMINATION: - t.setStatus(Status.TERMINATED); - break; - default: - notifyListeners(e); - break; + } else { + switch (current.getStatus()) { + case DONE: + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + t.storeCalculation(); + break; + case AWAITING_TERMINATION: + t.setStatus(Status.TERMINATED); + break; + default: + notifyListeners(e); + break; + } } }); - + } /** @@ -170,7 +167,7 @@ public void notifyListeners(TaskRepositoryEvent e) { */ public void executeAll() { - var queue = tasks.stream().filter(t -> { + tasks.stream().filter(t -> { switch (t.getStatus()) { case IN_PROGRESS: case EXECUTION_ERROR: @@ -178,11 +175,9 @@ public void executeAll() { default: return true; } - }).collect(toList()); - - for (SearchTask t : queue) { - taskPool.submit(() -> execute(t)); - } + }).forEach(t -> { + execute(t); + }); } @@ -259,7 +254,7 @@ public SampleName getSampleName() { return null; } - return ( (ExperimentalData) optional.get().getInput() ) + return ((ExperimentalData) optional.get().getInput()) .getMetadata().getSampleName(); } @@ -306,7 +301,7 @@ public SearchTask getTask(Identifier id) { */ public SearchTask getTask(int externalId) { var o = tasks.stream().filter(t - -> Integer.compare( ( (ExperimentalData) t.getInput()) + -> Integer.compare(((ExperimentalData) t.getInput()) .getMetadata().getExternalID(), externalId) == 0).findFirst(); return o.isPresent() ? o.get() : null; @@ -510,8 +505,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { - var properties = ( (Calculation) t.getResponse() ).getProblem().getProperties(); - var c = (ExperimentalData)t.getInput(); + var properties = ((Calculation) t.getResponse()).getProblem().getProperties(); + var c = (ExperimentalData) t.getInput(); properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 21a32df8..b4c74367 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -27,6 +27,7 @@ public class Log extends Group { private final Identifier id; private final List listeners; private static boolean graphical = true; + private boolean finished; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -80,10 +81,12 @@ public Log(SearchTask task) { } private void logFinished() { + finished = true; listeners.stream().forEach(l -> l.onLogFinished(this)); } private void notifyListeners(LogEntry logEntry) { + finished = false; listeners.stream().forEach(l -> l.onNewEntry(logEntry)); } @@ -109,6 +112,10 @@ public final Identifier getIdentifier() { public boolean isStarted() { return logEntries.size() > 0; } + + public boolean isFinished() { + return finished; + } /** * Outputs all log entries consecutively. diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index 82f84248..bc69d235 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -14,17 +14,17 @@ @SuppressWarnings("serial") public class GraphicalLogPane extends AbstractLogger { - + private final LogChart chart; private final JScrollPane pane; - + public GraphicalLogPane() { pane = new JScrollPane(); pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); chart = new LogChart(); pane.setViewportView(chart.getChartPanel()); - TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { - if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { + TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { chart.clear(); } }); @@ -44,16 +44,18 @@ public void printTimeTaken(Log log) { @Override public void post(LogEntry logEntry) { - if(logEntry instanceof DataLogEntry) { + if (logEntry instanceof DataLogEntry) { var dle = (DataLogEntry) logEntry; double iteration = dle.getData().stream() .filter(p -> p.getIdentifier().getKeyword() == ITERATION) .findAny().get().getApparentValue(); + chart.changeAxis(true); - chart.plot((DataLogEntry)logEntry, iteration); + chart.plot((DataLogEntry) logEntry, iteration); + } } - + @Override public void postAll() { clear(); @@ -64,18 +66,18 @@ public void postAll() { var log = task.getLog(); - if (log.isStarted()) { + if (log.isStarted() && log.isFinished()) { chart.clear(); chart.changeAxis(false); chart.plot(log); - + if (task.getStatus() == DONE) { printTimeTaken(log); } } - + } } @@ -95,4 +97,4 @@ public boolean isEmpty() { return false; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java index ba40ae40..f1c896cf 100644 --- a/src/main/java/pulse/ui/components/LogChart.java +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -157,11 +157,13 @@ private static void adjustRange(XYPlot pl, int iteration, int bufSize) { int lower = (iteration / bufSize) * bufSize; var domainAxis = pl.getDomainAxis(); - var r = domainAxis.getRange(); - var newR = new Range(lower, lower + bufSize); + if (domainAxis != null) { + var r = domainAxis.getRange(); + var newR = new Range(lower, lower + bufSize); - if (!r.equals(newR) && iteration > lower) { - ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + if (!r.equals(newR) && iteration > lower) { + ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + } } } diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 5dc2c26e..c29818f3 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -80,7 +80,7 @@ public TaskPopupMenu() { var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); instance.addSelectionListener(event -> { - instance.getSelectedTask().checkProblems(false); + instance.getSelectedTask().checkProblems(); var details = instance.getSelectedTask().getStatus().getDetails(); itemShowStatus.setEnabled((details != null) & (details != NONE)); }); @@ -102,7 +102,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - t.checkProblems(true); + t.checkProblems(); var status = t.getStatus(); if (status == DONE) { diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 30ea042c..5a06530c 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -49,7 +49,7 @@ public ExecutionButton() { return; } var problematicTask = instance.getTaskList().stream().filter(t -> { - t.checkProblems(true); + t.checkProblems(); return t.getStatus() == INCOMPLETE; }).findFirst(); if (problematicTask.isPresent()) { diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 04727c06..6f98ec37 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -26,33 +26,29 @@ public AccessibleTableRenderer() { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - Component renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - + Component result = null; + if (value instanceof Flag) { - renderer = new IconCheckBox((boolean) ((Property) value).getValue()); - ((IconCheckBox) renderer).setHorizontalAlignment(CENTER); - } else if (value instanceof PropertyHolder) { - renderer = initButton(value.toString()); + result = new IconCheckBox((boolean) ((Property) value).getValue()); + ((IconCheckBox) result).setHorizontalAlignment(CENTER); } - else if (value instanceof NumericProperty) { - //default - } - else if (value instanceof Property) { - var label = (JLabel) super.getTableCellRendererComponent(table, - ((Property) value).getDescriptor(true), isSelected, - hasFocus, row, column); - label.setHorizontalAlignment(JLabel.CENTER); - label.setFont(label.getFont().deriveFont(Font.BOLD)); - return label; + + else if (value instanceof PropertyHolder) { + var sb = new StringBuilder("Click to Edit/View "); + sb.append(((PropertyHolder) value).getSimpleName()); + sb.append("..."); + result = new JButton(sb.toString()); + ((JButton)result).setToolTipText(value.toString()); + ((JButton)result).setHorizontalAlignment(LEFT); } - return renderer; - } - - private JButton initButton(String str) { - var button = new JButton(str); - button.setToolTipText(str); - return button; + else { + result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } + + return result; + } + -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index e06abd02..ca36f67c 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -63,7 +63,7 @@ public static void plot(ActionEvent e) { var calc = (Calculation) t.getResponse(); - t.checkProblems(true); + t.checkProblems(); var status = t.getStatus(); if (status == INCOMPLETE && !status.checkProblemStatementSet()) { diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 7eeae66d..096bac20 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -78,7 +78,8 @@ public void onLogModeChanged(boolean graphical) { private void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logger.postAll()); + instance.addSelectionListener( + e -> SwingUtilities.invokeLater(() -> logger.postAll())); instance.addTaskRepositoryListener(event -> { if (event.getState() != TASK_ADDED) { @@ -128,7 +129,7 @@ private void setGraphicalLogger(boolean graphicalLog) { if (old != logger) { getContentPane().remove(old.getGUIComponent()); getContentPane().add(logger.getGUIComponent(), BorderLayout.CENTER); - logger.postAll(); + SwingUtilities.invokeLater(() -> logger.postAll()); } } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 6acd7dab..525ac3e2 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -14,14 +14,10 @@ import java.awt.BorderLayout; import java.awt.GridLayout; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import javax.swing.DefaultListModel; @@ -247,98 +243,62 @@ private void changeSchemes(DifferenceScheme newScheme) { var instance = TaskManager.getManagerInstance(); var selectedTask = instance.getSelectedTask(); - var schemeLoaderTracker = new ProgressDialog(); - schemeLoaderTracker.setTitle("Initialising solution schemes..."); - schemeLoaderTracker.setLocationRelativeTo(null); - schemeLoaderTracker.setAlwaysOnTop(true); + var tracker = new ProgressDialog(); + tracker.setTitle("Initialising solution schemes..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); - List> callableList; + tracker.trackProgress(instance.isSingleStatement() ? instance.getTaskList().size() : 1); + + Runnable finishingTouch = () -> { + var c = (Calculation) selectedTask.getResponse(); + schemeTable.setPropertyHolder(c.getScheme()); + if (c.getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), + "High complexity", INFORMATION_MESSAGE); + } + ProblemToolbar.plot(null); + tracker.setVisible(false); + problemTable.requestFocus(); + }; if (instance.isSingleStatement()) { - callableList = instance.getTaskList().stream().map(t -> new Callable() { + var runnables = instance.getTaskList().stream().map(t -> new Runnable() { @Override - public DifferenceScheme call() throws Exception { + public void run() { changeScheme(t, newScheme); - schemeLoaderTracker.incrementProgress(); - return ((Calculation) t.getResponse()).getScheme(); + tracker.incrementProgress(); } }).collect(Collectors.toList()); + CompletableFuture.runAsync(() + -> runnables.parallelStream().forEach(c -> c.run())) + .thenRun(finishingTouch); + } else { - callableList = Arrays.asList(() -> { - changeScheme(selectedTask, newScheme); - return selectedTask.getResponse().getScheme(); - }); + CompletableFuture.runAsync(() -> changeScheme(selectedTask, newScheme)).thenRun(finishingTouch); } - schemeLoaderTracker.trackProgress(callableList.size() - 1); - - CompletableFuture.runAsync(() -> { - try { - schemeListExecutor.invokeAll(callableList); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - }).thenRun(() -> { - - var c = (Calculation) selectedTask.getResponse(); - schemeTable.setPropertyHolder(c.getScheme()); - if (c.getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), - "High complexity", INFORMATION_MESSAGE); - } - Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - }); } private void changeProblems(Problem newlySelectedProblem, Object source) { var instance = TaskManager.getManagerInstance(); - var task = instance.getSelectedTask(); - var selectedCalc = ((Calculation) task.getResponse()); - - var problemLoaderTracker = new ProgressDialog(); - problemLoaderTracker.setTitle("Changing problem statements..."); - problemLoaderTracker.setLocationRelativeTo(null); - problemLoaderTracker.setAlwaysOnTop(true); + var selectedProblem = instance.getSelectedTask(); - List> callableList; + var tracker = new ProgressDialog(); + tracker.setTitle("Changing problem statements..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); if (source != instance) { - //apply to all tasks - if (instance.isSingleStatement()) { - - callableList = instance.getTaskList().stream().map(t -> new Callable() { - @Override - public Problem call() throws Exception { - changeProblem(t, newlySelectedProblem); - var result = ((Calculation) t.getResponse()).getProblem(); - problemLoaderTracker.incrementProgress(); - return result; - } - }).collect(Collectors.toList()); - - } //apply only to this task - else { - callableList = Arrays.asList(() -> { - changeProblem(task, newlySelectedProblem); - return ((Calculation) task.getResponse()).getProblem(); - }); - } + tracker.trackProgress(instance.isSingleStatement() ? instance.getTaskList().size() : 1); - problemLoaderTracker.trackProgress(callableList.size() - 1); - - CompletableFuture.runAsync(() -> { - try { - problemListExecutor.invokeAll(callableList); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - } - ).thenRun(() -> { + Runnable finishingTouch = () -> { + var selectedCalc = (Calculation) selectedProblem.getResponse(); problemTable.setPropertyHolder(selectedCalc.getProblem()); // after problem is selected for this task, show available difference schemes var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); @@ -347,7 +307,27 @@ public Problem call() throws Exception { schemes.forEach(s -> defaultModel.addElement(s)); selectDefaultScheme(schemeSelectionList, selectedCalc.getProblem()); schemeSelectionList.setToolTipText(null); - }); + tracker.setVisible(false); + }; + + if (instance.isSingleStatement()) { + + var runnables = instance.getTaskList().stream().map(t -> new Runnable() { + @Override + public void run() { + changeProblem(t, newlySelectedProblem); + tracker.incrementProgress(); + } + + }).collect(Collectors.toList()); + + CompletableFuture.runAsync(() + -> runnables.parallelStream().forEach(c -> c.run())) + .thenRun(finishingTouch); + + } else { + CompletableFuture.runAsync(() -> changeProblem(selectedProblem, newlySelectedProblem)).thenRun(finishingTouch); + } } @@ -372,7 +352,7 @@ private void changeProblem(SearchTask task, Problem newProblem) { np.retrieveData(data); } - task.checkProblems(true); + task.checkProblems(); toolbar.highlightButtons(!np.isReady()); } @@ -413,7 +393,7 @@ private void changeScheme(SearchTask task, DifferenceScheme newScheme) { } - task.checkProblems(true); + task.checkProblems(); } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index f638f4d7..7d2c9db2 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -209,7 +209,7 @@ public PathOptimiser getElementAt(int index) { pathTable.setPropertyHolder(optimiser); for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(true); + t.checkProblems(); } }); diff --git a/src/main/java/pulse/util/Accessible.java b/src/main/java/pulse/util/Accessible.java index b9cdf8c2..9cc28326 100644 --- a/src/main/java/pulse/util/Accessible.java +++ b/src/main/java/pulse/util/Accessible.java @@ -101,17 +101,18 @@ public List genericProperties() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0) { - continue; - } + //getters only + if (m.getParameterCount() == 0) { - if (Property.class.isAssignableFrom(m.getReturnType()) - && !NumericProperty.class.isAssignableFrom(m.getReturnType())) + if (Property.class.isAssignableFrom(m.getReturnType()) + && !NumericProperty.class.isAssignableFrom(m.getReturnType())) try { - fields.add((Property) m.invoke(this)); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - err.println("Error invoking method " + m); - e.printStackTrace(); + fields.add((Property) m.invoke(this)); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + err.println("Error invoking method " + m); + e.printStackTrace(); + } + } } diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index bead9379..644ad028 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -55,9 +55,10 @@ public boolean attemptUpdate(Object object) { return false; } - if(!allDescriptors.contains(string)) + if(!allDescriptors.contains(string)) { throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); - + } + this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); return true; diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 06c47f7d..cfc99845 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -441,10 +441,10 @@ descriptor="Buffer size" dimensionfactor="1" keyword="BUFFER_SIZE" maximum="32" minimum="4" value="5" primitive-type="int" discreet="false"/> - Date: Wed, 11 Jan 2023 14:15:15 +0300 Subject: [PATCH 110/116] Minor changes - Fixed buffer size not updating after commit to buffer dialog - Fixed baseline descriptor --- src/main/java/pulse/baseline/Baseline.java | 5 +++++ src/main/java/pulse/ui/components/PulseMainMenu.java | 7 ++++--- src/main/resources/Version.txt | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index d42c31d1..1efbda2d 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -75,4 +75,9 @@ public void fitTo(List x, List y) { } } + @Override + public String getDescriptor() { + return "Baseline"; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 216770e5..31d05441 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -71,8 +71,8 @@ public class PulseMainMenu extends JMenuBar { private static JMenuItem loadPulseItem; private static JMenuItem modelSettingsItem; - private static ExportDialog exportDialog = new ExportDialog(); - private static FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); + private static final ExportDialog exportDialog = new ExportDialog(); + private static final FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); private static File dir; @@ -80,7 +80,8 @@ public class PulseMainMenu extends JMenuBar { private List exitListeners; public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> Buffer.setSize(def(BUFFER_SIZE))); + bufferDialog.setConfirmAction(() -> + Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); initComponents(); initListeners(); diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 25789640..96882818 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.97b \ No newline at end of file +1.97c \ No newline at end of file From 1eccdb275506132cab21f5e23be86fbeb111d65d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Feb 2023 12:21:23 +0300 Subject: [PATCH 111/116] Added serialization - Added serialization capabilities to PULsE sessions - Modified the GUI (main menu) to accomodate the new capabilities of saving/loading sessions --- src/main/java/pulse/AbstractData.java | 4 +- src/main/java/pulse/DiscreteInput.java | 37 +++-- src/main/java/pulse/HeatingCurve.java | 61 +++++-- src/main/java/pulse/HeatingCurveListener.java | 4 +- src/main/java/pulse/Response.java | 20 ++- src/main/java/pulse/baseline/Baseline.java | 27 +-- .../java/pulse/baseline/FlatBaseline.java | 16 +- .../java/pulse/baseline/LinearBaseline.java | 24 +-- .../pulse/baseline/SinusoidalBaseline.java | 32 ++-- .../java/pulse/input/ExperimentalData.java | 21 ++- src/main/java/pulse/input/IndexRange.java | 6 +- .../pulse/input/InterpolationDataset.java | 107 ++++-------- src/main/java/pulse/input/Metadata.java | 10 +- src/main/java/pulse/input/Range.java | 122 +++++++------- .../pulse/input/listeners/CurveEvent.java | 19 +-- .../pulse/input/listeners/CurveEventType.java | 6 +- .../java/pulse/input/listeners/DataEvent.java | 3 +- .../pulse/input/listeners/DataEventType.java | 2 - .../pulse/input/listeners/DataListener.java | 4 +- .../listeners/ExternalDatasetListener.java | 13 +- .../java/pulse/io/export/ExportManager.java | 2 +- .../pulse/io/export/TextLogPaneExporter.java | 2 +- .../java/pulse/io/readers/AbstractReader.java | 1 + .../io/readers/NetzschPulseCSVReader.java | 4 +- .../java/pulse/io/readers/ReaderManager.java | 19 ++- .../java/pulse/math/AbstractIntegrator.java | 3 +- src/main/java/pulse/math/FFTTransformer.java | 14 +- .../pulse/math/FixedIntervalIntegrator.java | 1 + .../pulse/math/FunctionWithInterpolation.java | 33 +++- src/main/java/pulse/math/Harmonic.java | 10 +- src/main/java/pulse/math/LegendrePoly.java | 4 +- .../java/pulse/math/MidpointIntegrator.java | 2 + src/main/java/pulse/math/Parameter.java | 12 +- .../java/pulse/math/ParameterIdentifier.java | 36 ++-- src/main/java/pulse/math/ParameterVector.java | 14 +- src/main/java/pulse/math/Segment.java | 21 ++- .../java/pulse/math/SimpsonIntegrator.java | 2 + src/main/java/pulse/math/Window.java | 68 ++++---- src/main/java/pulse/math/ZScore.java | 44 +++-- .../math/filters/AssignmentListener.java | 8 +- src/main/java/pulse/math/filters/Filter.java | 12 +- .../math/filters/HalfTimeCalculator.java | 63 +++---- .../math/filters/OptimisablePolyline.java | 15 +- .../math/filters/OptimisedRunningAverage.java | 6 +- .../pulse/math/filters/PolylineOptimiser.java | 3 +- .../java/pulse/math/filters/Randomiser.java | 17 +- .../pulse/math/filters/RunningAverage.java | 79 +++++---- src/main/java/pulse/math/linear/Matrix2.java | 2 + src/main/java/pulse/math/linear/Matrix3.java | 2 + src/main/java/pulse/math/linear/Matrix4.java | 2 + .../pulse/math/linear/RectangularMatrix.java | 4 +- src/main/java/pulse/math/linear/Vector.java | 9 +- .../pulse/math/transforms/AtanhTransform.java | 2 + .../math/transforms/InvDiamTransform.java | 1 + .../math/transforms/PeriodicTransform.java | 6 +- .../pulse/math/transforms/StickTransform.java | 10 +- .../pulse/math/transforms/Transformable.java | 4 +- .../pulse/problem/laser/DiscretePulse.java | 4 +- .../pulse/problem/laser/DiscretePulse2D.java | 38 ++--- .../laser/ExponentiallyModifiedGaussian.java | 5 +- .../pulse/problem/laser/NumericPulse.java | 68 +++++--- .../pulse/problem/laser/NumericPulseData.java | 7 +- .../problem/laser/PulseTemporalShape.java | 7 +- .../pulse/problem/laser/RectangularPulse.java | 9 +- .../pulse/problem/laser/TrapezoidalPulse.java | 9 +- .../java/pulse/problem/schemes/ADIScheme.java | 14 +- .../problem/schemes/BlockMatrixAlgorithm.java | 5 +- .../schemes/CoupledImplicitScheme.java | 17 +- .../problem/schemes/DifferenceScheme.java | 18 +- .../problem/schemes/DistributedDetection.java | 7 +- .../pulse/problem/schemes/ExplicitScheme.java | 5 + .../problem/schemes/FixedPointIterations.java | 10 +- src/main/java/pulse/problem/schemes/Grid.java | 16 +- .../java/pulse/problem/schemes/Grid2D.java | 13 +- .../pulse/problem/schemes/ImplicitScheme.java | 19 ++- .../pulse/problem/schemes/MixedScheme.java | 5 + .../problem/schemes/OneDimensionalScheme.java | 5 +- .../schemes/RadiativeTransferCoupling.java | 9 +- .../schemes/TridiagonalMatrixAlgorithm.java | 25 +-- .../schemes/rte/BlackbodySpectrum.java | 39 ++++- .../schemes/rte/DerivativeCalculator.java | 4 +- .../pulse/problem/schemes/rte/Fluxes.java | 8 +- .../rte/FluxesAndExplicitDerivatives.java | 6 +- .../rte/FluxesAndImplicitDerivatives.java | 2 + .../schemes/rte/RTECalculationListener.java | 4 +- .../schemes/rte/RTECalculationStatus.java | 4 +- .../schemes/rte/dom/ButcherTableau.java | 4 +- .../rte/dom/CompositeGaussianQuadrature.java | 5 +- .../schemes/rte/dom/CornetteSchanksPF.java | 16 +- .../rte/dom/DiscreteOrdinatesMethod.java | 15 +- .../schemes/rte/dom/DiscreteQuantities.java | 5 +- .../schemes/rte/dom/Discretisation.java | 1 + .../schemes/rte/dom/ExplicitRungeKutta.java | 1 + .../schemes/rte/dom/FixedIterations.java | 2 + .../schemes/rte/dom/HenyeyGreensteinPF.java | 2 +- .../schemes/rte/dom/HermiteInterpolator.java | 5 +- .../schemes/rte/dom/LinearAnisotropicPF.java | 3 +- .../schemes/rte/dom/ODEIntegrator.java | 4 +- .../problem/schemes/rte/dom/OrdinateSet.java | 4 +- .../schemes/rte/dom/PhaseFunction.java | 16 +- .../schemes/rte/dom/StretchedGrid.java | 6 +- .../rte/dom/SuccessiveOverrelaxation.java | 4 +- .../pulse/problem/schemes/rte/dom/TRBDF2.java | 1 + .../rte/exact/ChandrasekharsQuadrature.java | 14 +- .../rte/exact/ExponentialIntegral.java | 1 + .../rte/exact/NewtonCotesQuadrature.java | 5 +- .../NonscatteringAnalyticalDerivatives.java | 6 +- .../NonscatteringDiscreteDerivatives.java | 2 + .../exact/NonscatteringRadiativeTransfer.java | 9 +- .../schemes/solvers/ADILinearisedSolver.java | 11 +- .../solvers/ExplicitCoupledSolver.java | 16 +- .../solvers/ExplicitCoupledSolverNL.java | 22 +-- .../solvers/ExplicitLinearisedSolver.java | 5 +- .../solvers/ExplicitNonlinearSolver.java | 1 + .../solvers/ExplicitTranslucentSolver.java | 3 +- .../solvers/ImplicitCoupledSolver.java | 12 +- .../solvers/ImplicitCoupledSolverNL.java | 11 +- .../solvers/ImplicitDiathermicSolver.java | 8 +- .../solvers/ImplicitLinearisedSolver.java | 16 +- .../solvers/ImplicitNonlinearSolver.java | 3 +- .../solvers/ImplicitTranslucentSolver.java | 15 +- .../solvers/ImplicitTwoTemperatureSolver.java | 1 + .../schemes/solvers/MixedCoupledSolver.java | 22 +-- .../schemes/solvers/MixedCoupledSolverNL.java | 11 +- .../solvers/MixedLinearisedSolver.java | 15 +- .../pulse/problem/schemes/solvers/Solver.java | 3 +- .../schemes/solvers/SolverException.java | 10 +- .../problem/statements/AdiabaticSolution.java | 4 +- .../problem/statements/ClassicalProblem.java | 4 + .../statements/ClassicalProblem2D.java | 13 +- .../problem/statements/DiathermicMedium.java | 24 +-- .../problem/statements/NonlinearProblem.java | 35 ++-- .../statements/ParticipatingMedium.java | 7 +- .../statements/PenetrationProblem.java | 5 +- .../pulse/problem/statements/Problem.java | 14 +- .../java/pulse/problem/statements/Pulse.java | 63 +++---- .../pulse/problem/statements/Pulse2D.java | 1 + .../statements/TwoTemperatureModel.java | 10 +- .../statements/model/AbsorptionModel.java | 13 +- .../model/BeerLambertAbsorption.java | 6 +- .../model/DiathermicProperties.java | 5 +- .../model/ExtendedThermalProperties.java | 1 + .../pulse/problem/statements/model/Gas.java | 42 ++--- .../problem/statements/model/Helium.java | 8 +- .../problem/statements/model/Insulator.java | 5 +- .../problem/statements/model/Nitrogen.java | 10 +- .../statements/model/ThermalProperties.java | 81 +++++---- .../model/ThermoOpticalProperties.java | 45 ++--- .../model/TwoTemperatureProperties.java | 10 +- src/main/java/pulse/properties/Flag.java | 7 +- .../pulse/properties/NumericProperties.java | 9 +- .../pulse/properties/NumericProperty.java | 35 ++-- .../properties/NumericPropertyFormatter.java | 9 +- .../properties/NumericPropertyKeyword.java | 26 +-- src/main/java/pulse/properties/Property.java | 4 +- .../java/pulse/properties/SampleName.java | 3 +- .../pulse/properties/ScientificFormat.java | 1 + src/main/java/pulse/search/GeneralTask.java | 91 +++++----- src/main/java/pulse/search/Optimisable.java | 1 - .../pulse/search/SimpleOptimisationTask.java | 14 +- .../pulse/search/direction/ActiveFlags.java | 42 ++--- .../pulse/search/direction/BFGSOptimiser.java | 8 +- .../pulse/search/direction/ComplexPath.java | 1 + .../direction/CompositePathOptimiser.java | 45 +++-- .../search/direction/DirectionSolver.java | 3 +- .../direction/GradientBasedOptimiser.java | 2 +- .../search/direction/GradientGuidedPath.java | 10 +- .../search/direction/IterativeState.java | 26 +-- .../pulse/search/direction/LMOptimiser.java | 48 +++--- .../java/pulse/search/direction/LMPath.java | 1 + .../pulse/search/direction/PathOptimiser.java | 4 +- .../pulse/search/direction/SR1Optimiser.java | 2 + .../direction/SteepestDescentOptimiser.java | 6 +- .../direction/pso/ConstrictionMover.java | 28 ++-- .../pulse/search/direction/pso/FIPSMover.java | 10 +- .../pulse/search/direction/pso/Mover.java | 2 +- .../search/direction/pso/ParticleState.java | 4 +- .../direction/pso/ParticleSwarmOptimiser.java | 8 +- .../direction/pso/StaticTopologies.java | 2 +- .../search/direction/pso/SwarmState.java | 12 +- .../search/linear/GoldenSectionOptimiser.java | 5 + .../pulse/search/linear/WolfeOptimiser.java | 47 +++--- .../pulse/search/statistics/AICStatistic.java | 2 + .../search/statistics/AbsoluteDeviations.java | 4 +- .../statistics/AndersonDarlingTest.java | 7 +- .../pulse/search/statistics/BICStatistic.java | 2 + .../search/statistics/CorrelationTest.java | 6 +- .../statistics/EmptyCorrelationTest.java | 2 + .../pulse/search/statistics/EmptyTest.java | 2 + .../java/pulse/search/statistics/FTest.java | 2 +- .../java/pulse/search/statistics/KSTest.java | 8 +- .../search/statistics/NormalityTest.java | 5 +- .../search/statistics/PearsonCorrelation.java | 2 + .../pulse/search/statistics/RSquaredTest.java | 5 +- .../RangePenalisedLeastSquares.java | 7 +- .../statistics/RegularisedLeastSquares.java | 3 +- .../search/statistics/ResidualStatistic.java | 22 +-- .../statistics/SpearmansCorrelationTest.java | 2 + .../pulse/search/statistics/Statistic.java | 4 +- .../pulse/search/statistics/SumOfSquares.java | 3 +- src/main/java/pulse/tasks/Calculation.java | 5 +- src/main/java/pulse/tasks/Identifier.java | 5 +- src/main/java/pulse/tasks/SearchTask.java | 52 +++--- src/main/java/pulse/tasks/TaskManager.java | 156 ++++++++++++++---- .../listeners/DataCollectionListener.java | 4 +- .../tasks/listeners/LogEntryListener.java | 3 +- .../tasks/listeners/ResultFormatEvent.java | 3 +- .../tasks/listeners/ResultFormatListener.java | 4 +- .../tasks/listeners/SessionListener.java | 7 + .../tasks/listeners/StatusChangeListener.java | 3 +- .../tasks/listeners/TaskRepositoryEvent.java | 7 +- .../listeners/TaskRepositoryListener.java | 7 +- .../tasks/listeners/TaskSelectionEvent.java | 5 - .../listeners/TaskSelectionListener.java | 6 +- .../java/pulse/tasks/logs/AbstractLogger.java | 7 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 6 +- .../java/pulse/tasks/logs/DataLogEntry.java | 3 +- src/main/java/pulse/tasks/logs/Details.java | 15 +- src/main/java/pulse/tasks/logs/Log.java | 64 +++++-- src/main/java/pulse/tasks/logs/LogEntry.java | 16 +- .../java/pulse/tasks/logs/StateEntry.java | 3 +- src/main/java/pulse/tasks/logs/Status.java | 18 +- .../pulse/tasks/processing/AverageResult.java | 25 +-- .../java/pulse/tasks/processing/Buffer.java | 4 + .../tasks/processing/CorrelationBuffer.java | 67 ++++---- .../java/pulse/tasks/processing/Result.java | 18 +- .../pulse/tasks/processing/ResultFormat.java | 24 ++- .../tasks/processing/ResultStatistics.java | 35 ++-- src/main/java/pulse/ui/ColorGenerator.java | 21 ++- src/main/java/pulse/ui/Launcher.java | 7 +- .../java/pulse/ui/components/AuxPlotter.java | 42 ++--- src/main/java/pulse/ui/components/Chart.java | 9 +- .../java/pulse/ui/components/DataLoader.java | 26 ++- .../pulse/ui/components/GraphicalLogPane.java | 8 +- .../java/pulse/ui/components/ProblemTree.java | 2 +- .../ui/components/PropertyHolderTable.java | 7 +- .../java/pulse/ui/components/PulseChart.java | 14 +- .../pulse/ui/components/PulseMainMenu.java | 102 ++++++++---- .../pulse/ui/components/RangeTextFields.java | 47 +++--- .../pulse/ui/components/ResidualsChart.java | 2 +- .../java/pulse/ui/components/ResultTable.java | 22 ++- .../pulse/ui/components/TaskPopupMenu.java | 44 +++-- .../java/pulse/ui/components/TaskTable.java | 58 ++++--- .../java/pulse/ui/components/TextLogPane.java | 9 +- .../components/buttons/ExecutionButton.java | 11 +- .../ui/components/buttons/LoaderButton.java | 51 +++--- .../controllers/AccessibleTableRenderer.java | 30 ++-- .../controllers/InstanceCellEditor.java | 4 +- .../components/controllers/NumberEditor.java | 8 - .../controllers/NumericPropertyRenderer.java | 1 - .../controllers/ProblemCellRenderer.java | 3 +- .../controllers/SearchListRenderer.java | 2 +- .../controllers/TaskTableRenderer.java | 4 +- .../FrameVisibilityRequestListener.java | 3 +- .../ui/components/listeners/LogListener.java | 3 +- .../listeners/MouseOnMarkerListener.java | 30 ++-- .../components/listeners/ResultListener.java | 2 +- .../models/ParameterTableModel.java | 24 ++- .../components/models/ResultTableModel.java | 40 +++-- .../components/models/SelectedKeysModel.java | 31 ++-- .../ui/components/models/TaskBoxModel.java | 4 - .../ui/components/models/TaskTableModel.java | 20 ++- .../ui/components/panels/ChartToolbar.java | 2 +- .../components/panels/DoubleTablePanel.java | 12 +- .../ui/components/panels/LogToolbar.java | 2 +- .../ui/components/panels/ProblemToolbar.java | 14 +- .../ui/components/panels/SettingsToolBar.java | 2 - .../ui/components/panels/TaskToolbar.java | 10 ++ src/main/java/pulse/ui/frames/DataFrame.java | 1 - src/main/java/pulse/ui/frames/LogFrame.java | 5 +- .../java/pulse/ui/frames/MainGraphFrame.java | 3 - .../pulse/ui/frames/ModelSelectionFrame.java | 6 +- .../java/pulse/ui/frames/PreviewFrame.java | 23 ++- .../ui/frames/ProblemStatementFrame.java | 16 +- .../java/pulse/ui/frames/ResultFrame.java | 14 +- .../pulse/ui/frames/SearchOptionsFrame.java | 32 ++-- .../pulse/ui/frames/TaskControlFrame.java | 34 ++-- .../pulse/ui/frames/TaskManagerFrame.java | 28 ++-- .../pulse/ui/frames/dialogs/ExportDialog.java | 6 +- .../ui/frames/dialogs/ProgressDialog.java | 29 +++- .../ui/frames/dialogs/ResultChangeDialog.java | 30 ++-- .../pulse/util/DescriptorChangeListener.java | 4 +- .../java/pulse/util/FunctionSerializer.java | 31 ++++ src/main/java/pulse/util/Group.java | 4 +- .../java/pulse/util/HierarchyListener.java | 4 +- .../java/pulse/util/ImmutableDataEntry.java | 4 +- src/main/java/pulse/util/ImmutablePair.java | 4 +- .../java/pulse/util/InstanceDescriptor.java | 10 +- src/main/java/pulse/util/PropertyEvent.java | 3 +- src/main/java/pulse/util/PropertyHolder.java | 23 ++- .../pulse/util/PropertyHolderListener.java | 4 +- src/main/java/pulse/util/Serializer.java | 118 +++++++++++++ .../java/pulse/util/UpwardsNavigable.java | 13 +- 293 files changed, 2655 insertions(+), 1882 deletions(-) create mode 100644 src/main/java/pulse/tasks/listeners/SessionListener.java create mode 100644 src/main/java/pulse/util/FunctionSerializer.java create mode 100644 src/main/java/pulse/util/Serializer.java diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index a1d276c7..58fbceb3 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -258,7 +258,7 @@ public void remove(int i) { public boolean ignoreSiblings() { return true; } - + public boolean isFull() { return actualNumPoints() >= count; } @@ -306,4 +306,4 @@ public boolean equals(Object o) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java index c6bb8d85..3b29c1f7 100644 --- a/src/main/java/pulse/DiscreteInput.java +++ b/src/main/java/pulse/DiscreteInput.java @@ -1,45 +1,48 @@ package pulse; import java.awt.geom.Point2D; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import pulse.input.IndexRange; import pulse.math.Segment; -public interface DiscreteInput { - +public interface DiscreteInput extends Serializable { + public List getX(); + public List getY(); + public IndexRange getIndexRange(); - + public static List convert(double[] x, double[] y) { - + var ps = new ArrayList(); - - for(int i = 0, size = x.length; i < size; i++) { + + for (int i = 0, size = x.length; i < size; i++) { ps.add(new Point2D.Double(x[i], y[i])); } - + return ps; - + } - + public static List convert(List x, List y) { - + var ps = new ArrayList(); - - for(int i = 0, size = x.size(); i < size; i++) { + + for (int i = 0, size = x.size(); i < size; i++) { ps.add(new Point2D.Double(x.get(i), y.get(i))); } - + return ps; - + } - + public default Segment bounds() { var ir = getIndexRange(); var x = getX(); return new Segment(x.get(ir.getLowerBound()), x.get(ir.getUpperBound())); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 86e14621..f6823062 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -1,6 +1,8 @@ package pulse; -import static java.util.Collections.max; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import static java.util.stream.Collectors.toList; import static pulse.input.listeners.CurveEventType.RESCALED; import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; @@ -17,12 +19,14 @@ import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import pulse.baseline.Baseline; import pulse.input.ExperimentalData; import pulse.input.listeners.CurveEvent; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.util.FunctionSerializer; /** * The {@code HeatingCurve} represents a time-temperature profile (a @@ -42,20 +46,30 @@ */ public class HeatingCurve extends AbstractData { - private final List adjustedSignal; + /** + * + */ + private static final long serialVersionUID = 7071147065094996971L; + private List adjustedSignal; private List lastCalculation; private double startTime; - private final List listeners - = new ArrayList<>(); + private List listeners; - private UnivariateInterpolator interpolator; - private UnivariateFunction interpolation; + private transient UnivariateInterpolator interpolator; + private transient UnivariateFunction interpolation; protected HeatingCurve(List time, List signal, final double startTime, String name) { super(time, name); this.adjustedSignal = signal; this.startTime = startTime; + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); + listeners = new ArrayList<>(); } /** @@ -167,7 +181,7 @@ public void scale(double scale) { for (int i = 0, max = Math.min(count, signal.size()); i < max; i++) { signal.set(i, signal.get(i) * scale); } - var dataEvent = new CurveEvent(RESCALED, this); + var dataEvent = new CurveEvent(RESCALED); fireCurveEvent(dataEvent); } @@ -195,14 +209,13 @@ private void refreshInterpolation() { } final double alpha = -1.0; - adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] + adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate // linearly /* * Submit to spline interpolation */ - interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); } @@ -343,7 +356,7 @@ public NumericProperty getTimeShift() { public void setTimeShift(NumericProperty startTime) { requireType(startTime, TIME_SHIFT); this.startTime = (double) startTime.getValue(); - var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED, this); + var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED); fireCurveEvent(dataEvent); firePropertyChanged(this, startTime); } @@ -357,11 +370,14 @@ public List getBaselineCorrectedData() { } public void addHeatingCurveListener(HeatingCurveListener l) { + if (listeners == null) { + listeners = new ArrayList<>(); + } this.listeners.add(l); } @Override - public void removeHeatingCurveListeners() { + public void removeListeners() { listeners.clear(); } @@ -379,7 +395,7 @@ public boolean equals(Object o) { return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); } - + public double interpolateSignalAt(double x) { double min = this.timeAt(0); double max = timeLimit(); @@ -387,4 +403,23 @@ public double interpolateSignalAt(double x) { : (x < min ? signalAt(0) : signalAt(actualNumPoints() - 1)); } -} \ No newline at end of file + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + this.interpolator = new SplineInterpolator(); + } + +} diff --git a/src/main/java/pulse/HeatingCurveListener.java b/src/main/java/pulse/HeatingCurveListener.java index 7dd972c6..12d80648 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -1,15 +1,17 @@ package pulse; +import java.io.Serializable; import pulse.input.listeners.CurveEvent; /** * An interface used to listen to data events related to {@code HeatingCurve}. * */ -public interface HeatingCurveListener { +public interface HeatingCurveListener extends Serializable { /** * Signals that a {@code CurveEvent} has occurred. + * * @param event */ public void onCurveEvent(CurveEvent event); diff --git a/src/main/java/pulse/Response.java b/src/main/java/pulse/Response.java index cc4f23ce..e138950d 100644 --- a/src/main/java/pulse/Response.java +++ b/src/main/java/pulse/Response.java @@ -1,25 +1,27 @@ package pulse; +import java.io.Serializable; import pulse.math.Segment; import pulse.problem.schemes.solvers.SolverException; import pulse.search.GeneralTask; import pulse.search.statistics.OptimiserStatistic; -public interface Response { - +public interface Response extends Serializable { + public double evaluate(double t); + public Segment accessibleRange(); - + /** - * Calculates the value of the objective function used to identify - * the current state of the optimiser. + * Calculates the value of the objective function used to identify the + * current state of the optimiser. + * * @param task * @return the value of the objective function in the current state * @throws pulse.problem.schemes.solvers.SolverException */ - public double objectiveFunction(GeneralTask task) throws SolverException; - + public OptimiserStatistic getOptimiserStatistic(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 1efbda2d..29a88865 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,5 +1,6 @@ package pulse.baseline; +import java.util.ArrayList; import java.util.List; import pulse.DiscreteInput; @@ -24,7 +25,7 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { public final static int MIN_BASELINE_POINTS = 15; - + public abstract Baseline copy(); /** @@ -60,24 +61,24 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti * @see fitTo(ExperimentalData,double,double) */ public void fitTo(DiscreteInput data) { - var filtered = Range.NEGATIVE.filter(data); - if(filtered[0].size() > MIN_BASELINE_POINTS) { + var filtered = Range.NEGATIVE.filter(data); + if (filtered[0].size() > MIN_BASELINE_POINTS) { doFit(filtered[0], filtered[1]); - } + } } - + public void fitTo(List x, List y) { - int index = IndexRange.closestLeft(0, x); - var xx = x.subList(0, index + 1); - var yy = y.subList(0, index + 1); - if(xx.size() > MIN_BASELINE_POINTS) { + int index = IndexRange.closestLeft(0, x); + var xx = new ArrayList<>(x.subList(0, index + 1)); + var yy = new ArrayList<>(y.subList(0, index + 1)); + if (xx.size() > MIN_BASELINE_POINTS) { doFit(xx, yy); - } + } } - + @Override public String getDescriptor() { return "Baseline"; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 9500c8f6..1ca0743b 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -8,10 +8,11 @@ /** * A flat baseline. */ - public class FlatBaseline extends AdjustableBaseline { - - /** + + private static final long serialVersionUID = -4867631788950622739L; + + /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. */ @@ -27,8 +28,7 @@ public FlatBaseline() { public FlatBaseline(double intercept) { super(intercept, 0.0); } - - + @Override protected void doFit(List x, List y) { double intercept = mean(y); @@ -37,12 +37,12 @@ protected void doFit(List x, List y) { @Override public Baseline copy() { - return new FlatBaseline((double)getIntercept().getValue()); + return new FlatBaseline((double) getIntercept().getValue()); } @Override public String toString() { return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index c4afb45f..a04354d8 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -29,6 +29,8 @@ */ public class LinearBaseline extends AdjustableBaseline { + private static final long serialVersionUID = -7334390731462268504L; + /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. @@ -36,19 +38,19 @@ public class LinearBaseline extends AdjustableBaseline { public LinearBaseline() { super(0.0, 0.0); } - + public LinearBaseline(double intercept, double slope) { super(intercept, slope); } - + public LinearBaseline(AdjustableBaseline baseline) { - super( (double) baseline.getIntercept().getValue(), - (double) baseline.getSlope().getValue() - ); + super((double) baseline.getIntercept().getValue(), + (double) baseline.getSlope().getValue() + ); } @Override - protected void doFit(List x, List y) { + protected void doFit(List x, List y) { double meanx = mean(x); double meany = mean(y); @@ -63,7 +65,7 @@ protected void doFit(List x, List y) { xxbar += (x1 - meanx) * (x1 - meanx); xybar += (x1 - meanx) * (y1 - meany); } - + double slope = xybar / xxbar; double intercept = meany - slope * meanx; @@ -74,8 +76,8 @@ protected void doFit(List x, List y) { @Override public String toString() { var slope = getSlope().getValue(); - return getClass().getSimpleName() + " = " + - format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); } @Override @@ -121,9 +123,9 @@ public void assign(ParameterVector params) { for (Parameter p : params.getParameters()) { var key = p.getIdentifier().getKeyword(); - + if (key == BASELINE_SLOPE) { - setSlope( derive(BASELINE_SLOPE, p.inverseTransform() )); + setSlope(derive(BASELINE_SLOPE, p.inverseTransform())); } } diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index 9074cdc4..94b92c9f 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -46,6 +46,7 @@ */ public class SinusoidalBaseline extends LinearBaseline { + private static final long serialVersionUID = -6858521208790195992L; private List hiFreq; private List loFreq; private List active; @@ -87,10 +88,10 @@ public Baseline copy() { newH.setParent(baseline); } for (Harmonic h : hiFreq) { - baseline.hiFreq.add(new Harmonic(h)); + baseline.hiFreq.add(new Harmonic(h)); } for (Harmonic h : loFreq) { - baseline.loFreq.add(new Harmonic(h)); + baseline.loFreq.add(new Harmonic(h)); } return baseline; } @@ -98,7 +99,7 @@ public Baseline copy() { @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); - active.forEach(h -> h.optimisationVector(output) ); + active.forEach(h -> h.optimisationVector(output)); } @Override @@ -124,7 +125,7 @@ private void guessHarmonics(double[] x, double[] y) { double maxAmp = 0; hiFreq = new ArrayList<>(); - + double span = x[x.length - 1] - x[0]; double lowerFrequency = 4.0 / span; @@ -146,7 +147,7 @@ private List sort(List hs, int limit) { tmp.sort(null); Collections.reverse(tmp); //leave out a maximum of n harmonics - return tmp.subList(0, Math.min(tmp.size(), limit)); + return new ArrayList<>(tmp.subList(0, Math.min(tmp.size(), limit))); } private void labelActive() { @@ -237,25 +238,25 @@ public NumericProperty getHiFreqMax() { public void setHiFreqMax(NumericProperty maxHarmonics) { NumericProperty.requireType(maxHarmonics, MAX_HIGH_FREQ_WAVES); int oldValue = this.maxHighFreqHarmonics; - + if ((int) maxHarmonics.getValue() != oldValue) { - + var lowFreq = new ArrayList(); int size = active.size(); - - if(maxHighFreqHarmonics < size) { + + if (maxHighFreqHarmonics < size) { lowFreq = new ArrayList<>(active.subList(maxHighFreqHarmonics, size)); } - + this.maxHighFreqHarmonics = (int) maxHarmonics.getValue(); active.clear(); active.addAll(sort(hiFreq, maxHighFreqHarmonics)); active.addAll(lowFreq); this.labelActive(); this.firePropertyChanged(this, maxHarmonics); - + } - + } public NumericProperty getLowFreqMax() { @@ -267,7 +268,7 @@ public void setLowFreqMax(NumericProperty maxHarmonics) { int oldValue = this.maxLowFreqHarmonics; if ((int) maxHarmonics.getValue() != oldValue) { this.maxLowFreqHarmonics = (int) maxHarmonics.getValue(); - active = active.subList(0, maxHighFreqHarmonics); + active = new ArrayList<>(active.subList(0, maxHighFreqHarmonics)); active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); this.labelActive(); this.firePropertyChanged(this, maxHarmonics); @@ -279,7 +280,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); switch (type) { - + case MAX_HIGH_FREQ_WAVES: setHiFreqMax(property); break; @@ -346,12 +347,11 @@ private void addLowFreq(DiscreteInput input) { /* These harmonics are inaccessible by FFT */ - for (double f = freq; f > 1.0 / (2.0 * span); f /= 2.0) { loFreq.add(new Harmonic(amp, f, 0.0)); } - active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); + active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); } @Override diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index f38a8e0a..4be76e71 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -27,11 +27,15 @@ */ public class ExperimentalData extends AbstractData implements DiscreteInput { + /** + * + */ + private static final long serialVersionUID = 7950893319753173094L; private HalfTimeCalculator calculator; private Metadata metadata; private IndexRange indexRange; private Range range; - private List dataListeners; + private transient List dataListeners; /** * This is the cutoff factor which is used as a criterion for data @@ -49,10 +53,16 @@ public class ExperimentalData extends AbstractData implements DiscreteInput { */ public ExperimentalData() { super(); - dataListeners = new ArrayList<>(); setPrefix("RawData"); setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(0,0); + indexRange = new IndexRange(0, 0); + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); + dataListeners = new ArrayList<>(); this.addDataListener((DataEvent e) -> { if (e.getType() == DataEventType.DATA_LOADED) { preprocess(); @@ -61,6 +71,9 @@ public ExperimentalData() { } public final void addDataListener(DataListener listener) { + if(dataListeners == null) { + dataListeners = new ArrayList<>(); + } dataListeners.add(listener); } @@ -298,4 +311,4 @@ public List getY() { return this.getSignalData(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 93a92806..cb19490c 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -1,5 +1,6 @@ package pulse.input; +import java.io.Serializable; import static java.util.Objects.requireNonNull; import java.util.List; @@ -15,8 +16,9 @@ * @see pulse.input.Range * */ -public class IndexRange { +public class IndexRange implements Serializable { + private static final long serialVersionUID = 7983756487957427969L; private int iStart; private int iEnd; @@ -24,7 +26,7 @@ public IndexRange(IndexRange other) { iStart = other.iStart; iEnd = other.iEnd; } - + public IndexRange(int start, int end) { this.iStart = start; this.iEnd = end; diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 45f244ed..4e0ec64a 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -1,20 +1,18 @@ package pulse.input; -import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.util.ArrayList; -import java.util.EnumMap; import java.util.List; -import java.util.Map; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; -import pulse.input.listeners.ExternalDatasetListener; -import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; +import pulse.util.FunctionSerializer; import pulse.util.ImmutableDataEntry; /** @@ -27,19 +25,19 @@ * * @see pulse.input.listeners.ExternalDatasetListener */ -public class InterpolationDataset { +public class InterpolationDataset implements Serializable { - private UnivariateFunction interpolation; + /** + * + */ + private static final long serialVersionUID = 7439474910490135034L; + private transient UnivariateFunction interpolation; private final List> dataset; - private static final Map standartDatasets - = new EnumMap(StandartType.class); - private static final List listeners = new ArrayList<>(); - /** - * Creates an empty {@code InterpolationDataset}. - */ - - public InterpolationDataset() { + /** + * Creates an empty {@code InterpolationDataset}. + */ + public InterpolationDataset() { dataset = new ArrayList<>(); } @@ -66,7 +64,7 @@ public void add(ImmutableDataEntry entry) { } /** - * Constructs a new spline interpolator and uses the available dataset to + * Constructs a new Akima spline interpolator and uses the available dataset to * produce a {@code SplineInterpolation}. */ public void doInterpolation() { @@ -84,65 +82,22 @@ public List> getData() { return dataset; } - /** - * Retrieves a standard dataset previously loaded by the respective reader. - * - * @param type the standard dataset type - * @return an {@code InterpolationDataset} corresponding to {@code type} - */ - public static InterpolationDataset getDataset(StandartType type) { - return standartDatasets.get(type); - } - - /** - * Puts a datset specified by {@code type} into the static hash map of this - * class, using {@code type} as key. Triggers {@code onDensityDataLoaded} - * - * @param dataset a dataset to be appended to the static hash map - * @param type the dataset type + /* + * Serialization */ - public static void setDataset(InterpolationDataset dataset, StandartType type) { - standartDatasets.put(type, dataset); - listeners.stream().forEach(l -> l.onDataLoaded(type)); + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); } - /** - * Creates a list of property keywords that can be derived with help of the - * loaded data. For example, if heat capacity and density data is available, - * the returned list will contain {@code CONDUCTIVITY}. - * - * @return - */ - public static List derivableProperties() { - var list = new ArrayList(); - if (standartDatasets.containsKey(StandartType.HEAT_CAPACITY)) { - list.add(SPECIFIC_HEAT); - } - if (standartDatasets.containsKey(StandartType.DENSITY)) { - list.add(DENSITY); - } - if (list.contains(SPECIFIC_HEAT) && list.contains(DENSITY)) { - list.add(CONDUCTIVITY); - list.add(EMISSIVITY); - } - return list; - } - - public static void addListener(ExternalDatasetListener l) { - listeners.add(l); - } - - public enum StandartType { - - /** - * A keyword for the heat capacity dataset (in J/kg/K). - */ - HEAT_CAPACITY, - /** - * A keyword for the density dataset (in kg/m3). - */ - DENSITY; - + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index ae00b92e..6840ff58 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -42,11 +42,12 @@ */ public class Metadata extends PropertyHolder implements Reflexive { + private static final long serialVersionUID = -7954252611294551707L; private Set data; private SampleName sampleName; private int externalID; - private InstanceDescriptor pulseDescriptor + private InstanceDescriptor pulseDescriptor = new InstanceDescriptor<>("Pulse Shape Selector", PulseTemporalShape.class); private NumericPulseData pulseData; @@ -117,13 +118,14 @@ public void setSampleName(SampleName sampleName) { public final void setPulseData(NumericPulseData pulseData) { this.pulseData = pulseData; - this.set(PULSE_WIDTH, derive(PULSE_WIDTH, pulseData.pulseWidth()) ); + this.set(PULSE_WIDTH, derive(PULSE_WIDTH, pulseData.pulseWidth())); } /** * If a Numerical Pulse has been loaded (for example, when importing from * Proteus), this will return an object describing this data. - * @return + * + * @return */ public final NumericPulseData getPulseData() { return pulseData; @@ -272,4 +274,4 @@ public boolean equals(Object o) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index fd6e4e94..09b247d5 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -18,7 +18,6 @@ import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.Optimisable; @@ -32,12 +31,14 @@ */ public class Range extends PropertyHolder implements Optimisable { - private Segment segment; - - public final static Range UNLIMITED = new Range (Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + private static final long serialVersionUID = 5326569416384623525L; + + private final Segment segment; + + public final static Range UNLIMITED = new Range(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); public final static Range NEGATIVE = new Range(Double.NEGATIVE_INFINITY, -1E-16); public final static Range POSITIVE = new Range(1e-16, Double.POSITIVE_INFINITY); - + /** * Constructs a {@code Range} from the minimum and maximum values of * {@code data}. @@ -60,44 +61,43 @@ public Range(List data) { public Range(double a, double b) { this.segment = new Segment(a, b); } - + /** - * Contains a data double array ([0] - x, [1] - y), - * where the data points have been filtered so that - * each x fits into this range. + * Contains a data double array ([0] - x, [1] - y), where the data points + * have been filtered so that each x fits into this range. + * * @param input * @return a [2][...] array containing filtered x and y values */ - public List[] filter(DiscreteInput input) { - var x = input.getX(); - var y = input.getY(); - - if(x.size() != y.size()) { - throw new IllegalArgumentException("x.length != y.length"); - } - - var xf = new ArrayList(); - var yf = new ArrayList(); - - double min = segment.getMinimum(); - double max = segment.getMaximum(); - - final double eps = 1E-10; - - for(int i = 0, size = x.size(); i < size; i++) { - - if(x.get(i) > min && x.get(i) < max + eps) { - - xf.add(x.get(i)); - yf.add(y.get(i)); - - } - - } - - return new List[]{xf, yf}; - + var x = input.getX(); + var y = input.getY(); + + if (x.size() != y.size()) { + throw new IllegalArgumentException("x.length != y.length"); + } + + var xf = new ArrayList(); + var yf = new ArrayList(); + + double min = segment.getMinimum(); + double max = segment.getMaximum(); + + final double eps = 1E-10; + + for (int i = 0, size = x.size(); i < size; i++) { + + if (x.get(i) > min && x.get(i) < max + eps) { + + xf.add(x.get(i)); + yf.add(y.get(i)); + + } + + } + + return new List[]{xf, yf}; + } /** @@ -139,12 +139,12 @@ public NumericProperty getUpperBound() { */ public void setLowerBound(NumericProperty p) { requireType(p, LOWER_BOUND); - - if( boundLimits(false).contains( ((Number)p.getValue()).doubleValue())) { + + if (boundLimits(false).contains(((Number) p.getValue()).doubleValue())) { segment.setMinimum((double) p.getValue()); firePropertyChanged(this, p); } - + } /** @@ -154,12 +154,12 @@ public void setLowerBound(NumericProperty p) { */ public void setUpperBound(NumericProperty p) { requireType(p, UPPER_BOUND); - - if( boundLimits(true).contains( ((Number)p.getValue()).doubleValue()) ) { + + if (boundLimits(true).contains(((Number) p.getValue()).doubleValue())) { segment.setMaximum((double) p.getValue()); firePropertyChanged(this, p); } - + } /** @@ -215,26 +215,28 @@ public Set listedKeywords() { set.add(LOWER_BOUND); set.add(UPPER_BOUND); return set; - } - + } + /** * Calculates the allowed range for either the upper or lower bound. - * @param isUpperBound if {@code true}, will calculate the range for the upper bound, otherwise -- for the lower one., + * + * @param isUpperBound if {@code true}, will calculate the range for the + * upper bound, otherwise -- for the lower one., * @return a Segment range of limits for the specific bound */ - public Segment boundLimits(boolean isUpperBound) { - + var curve = (ExperimentalData) this.getParent(); var seq = curve.getTimeSequence(); double tHalf = curve.getHalfTimeCalculator().getHalfTime(); - + Segment result = null; - if(isUpperBound) + if (isUpperBound) { result = new Segment(2.5 * tHalf, seq.get(seq.size() - 1)); - else - result = new Segment( Math.max(-0.15 * tHalf, seq.get(0)), 0.75 * tHalf); - + } else { + result = new Segment(Math.max(-0.15 * tHalf, seq.get(0)), 0.75 * tHalf); + } + return result; } @@ -250,14 +252,14 @@ public Segment boundLimits(boolean isUpperBound) { */ @Override public void optimisationVector(ParameterVector output) { - + Segment bounds; - + for (Parameter p : output.getParameters()) { var key = p.getIdentifier().getKeyword(); double value; - + switch (key) { case UPPER_BOUND: bounds = boundLimits(true); @@ -270,7 +272,7 @@ public void optimisationVector(ParameterVector output) { default: continue; } - + var transform = new StickTransform(bounds); p.setBounds(bounds); @@ -292,7 +294,7 @@ public void assign(ParameterVector params) throws SolverException { for (Parameter p : params.getParameters()) { var key = p.getIdentifier().getKeyword(); - var np = derive( key, p.inverseTransform() ); + var np = derive(key, p.inverseTransform()); switch (key) { case UPPER_BOUND: @@ -313,4 +315,4 @@ public String toString() { return "Range given by: " + segment.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index a0b69f9b..61413bf0 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -1,6 +1,6 @@ package pulse.input.listeners; -import pulse.HeatingCurve; +import java.io.Serializable; /** * A {@code CurveEvent} is associated with an {@code HeatingCurve} object. @@ -8,21 +8,18 @@ * @see pulse.HeatingCurve * */ -public class CurveEvent { +public class CurveEvent implements Serializable { private CurveEventType type; - private HeatingCurve data; /** * Constructs a {@code CurveEvent} object, combining the {@code type} and * associated {@code data} * * @param type the type of this event - * @param data the source of the event */ - public CurveEvent(CurveEventType type, HeatingCurve data) { + public CurveEvent(CurveEventType type) { this.type = type; - this.data = data; } /** @@ -34,14 +31,4 @@ public CurveEventType getType() { return type; } - /** - * Used to get the {@code HeatingCurve} object that has undergone certain - * changes specified by this event type. - * - * @return the associated data - */ - public HeatingCurve getData() { - return data; - } - } diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 3dcb5d0d..40a55d3d 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -19,12 +19,10 @@ public enum CurveEventType { * procedure. */ TIME_ORIGIN_CHANGED, - /** - * A calculation associated with this curve has finished and - * the required arrays have been filled. + * A calculation associated with this curve has finished and the required + * arrays have been filled. */ - CALCULATION_FINISHED; } diff --git a/src/main/java/pulse/input/listeners/DataEvent.java b/src/main/java/pulse/input/listeners/DataEvent.java index d42c1a20..7fefe1c0 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -1,5 +1,6 @@ package pulse.input.listeners; +import java.io.Serializable; import pulse.AbstractData; /** @@ -7,7 +8,7 @@ * {@code ExperimentalData}. * */ -public class DataEvent { +public class DataEvent implements Serializable { private DataEventType type; private AbstractData data; diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index da2160c1..52c80851 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -15,11 +15,9 @@ public enum DataEventType { */ RANGE_CHANGED, - /** * All data points loaded and are ready for processing. */ - DATA_LOADED; } diff --git a/src/main/java/pulse/input/listeners/DataListener.java b/src/main/java/pulse/input/listeners/DataListener.java index cb1f3d4c..28170c53 100644 --- a/src/main/java/pulse/input/listeners/DataListener.java +++ b/src/main/java/pulse/input/listeners/DataListener.java @@ -1,11 +1,13 @@ package pulse.input.listeners; +import java.io.Serializable; + /** * A listener interface, which is used to listen to {@code DataEvent}s occurring * with an {@code ExperimentalData} object. * */ -public interface DataListener { +public interface DataListener extends Serializable { /** * Triggered when a certain {@code DataEvent} specified by its diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java index 1bb85392..a73d3d13 100644 --- a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -1,7 +1,6 @@ package pulse.input.listeners; -import pulse.input.InterpolationDataset.StandartType; - +import java.io.Serializable; /** * A listener associated with the {@code InterpolationDataset} static repository * of interpolations. @@ -9,11 +8,7 @@ */ public interface ExternalDatasetListener { - /** - * Triggered when a data {@code type} has been loaded. - * - * @param type a type of the dataset, for which an interpolation is created. - */ - public void onDataLoaded(StandartType type); + public void onSpecificHeatDataLoaded(); + public void onDensityDataLoaded(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 2064c501..892402c1 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -21,7 +21,7 @@ * */ public class ExportManager { - + //current working dir private static File cwd = null; diff --git a/src/main/java/pulse/io/export/TextLogPaneExporter.java b/src/main/java/pulse/io/export/TextLogPaneExporter.java index e6ea39e1..317af504 100644 --- a/src/main/java/pulse/io/export/TextLogPaneExporter.java +++ b/src/main/java/pulse/io/export/TextLogPaneExporter.java @@ -72,4 +72,4 @@ public Extension[] getSupportedExtensions() { return new Extension[]{HTML}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/AbstractReader.java b/src/main/java/pulse/io/readers/AbstractReader.java index d98036ef..29bbc625 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -13,6 +13,7 @@ * lists, arrays and containers may (and usually will) change as a result of * using the reader. *

+ * * @param */ public interface AbstractReader extends AbstractHandler { diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 7d759e6f..bfc07fe1 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -55,8 +55,8 @@ public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); NumericPulseData data = null; - - ( (NetzschCSVReader) NetzschCSVReader.getInstance() ) + + ((NetzschCSVReader) NetzschCSVReader.getInstance()) .setDefaultLocale(); //always start with a default locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 0f8201bb..5d168a93 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -214,27 +214,28 @@ public static Set readDirectory(List> readers, File dir } var es = Executors.newSingleThreadExecutor(); - + var callableList = new ArrayList>(); - + for (File f : directory.listFiles()) { Callable callable = () -> read(readers, f); callableList.add(callable); } - + Set result = new HashSet<>(); - + try { List> futures = es.invokeAll(callableList); - - for(Future f : futures) + + for (Future f : futures) { result.add(f.get()); - + } + } catch (InterruptedException ex) { - Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, "Reading interrupted when loading files from " + directory.toString(), ex); } catch (ExecutionException ex) { - Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, "Error executing read operation using concurrency", ex); } diff --git a/src/main/java/pulse/math/AbstractIntegrator.java b/src/main/java/pulse/math/AbstractIntegrator.java index 1fadb3aa..b56c00ea 100644 --- a/src/main/java/pulse/math/AbstractIntegrator.java +++ b/src/main/java/pulse/math/AbstractIntegrator.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -10,7 +11,7 @@ * or more variables and the other to actually do the integration. * */ -public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive { +public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive, Serializable { private Segment integrationBounds; diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java index 7e5390de..dea0d65b 100644 --- a/src/main/java/pulse/math/FFTTransformer.java +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -1,15 +1,17 @@ package pulse.math; +import java.io.Serializable; import org.apache.commons.math3.complex.Complex; import org.apache.commons.math3.transform.DftNormalization; import org.apache.commons.math3.transform.FastFourierTransformer; import org.apache.commons.math3.transform.TransformType; -public class FFTTransformer { +public class FFTTransformer implements Serializable { + private static final long serialVersionUID = -5424502578926616928L; private double[] amplitudeSpec; private double[] phaseSpec; - + private int n; //number of input points private Complex[] buffer; @@ -18,7 +20,7 @@ public class FFTTransformer { public FFTTransformer(double[] realInput) { this(Window.HANN, realInput, new double[realInput.length]); } - + public FFTTransformer(Window window, double[] realInput) { this(window, realInput, new double[realInput.length]); } @@ -68,7 +70,7 @@ public FFTTransformer(Window window, double[] realInput, double[] imagInput) { public double[] sampling(double[] x) { final double totalTime = x[n - 2] - x[0]; double[] sample = new double[buffer.length / 2]; - double fs = n/totalTime; //sampling rate + double fs = n / totalTime; //sampling rate for (int i = 0; i < sample.length; i++) { sample[i] = i * fs / buffer.length; } @@ -132,5 +134,5 @@ public double[] getAmpltiudeSpectrum() { public double[] getPhaseSpectrum() { return phaseSpec; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/FixedIntervalIntegrator.java b/src/main/java/pulse/math/FixedIntervalIntegrator.java index ac77ccb4..8631efb2 100644 --- a/src/main/java/pulse/math/FixedIntervalIntegrator.java +++ b/src/main/java/pulse/math/FixedIntervalIntegrator.java @@ -20,6 +20,7 @@ */ public abstract class FixedIntervalIntegrator extends AbstractIntegrator { + private static final long serialVersionUID = -5304597610450009326L; private int integrationSegments; /** diff --git a/src/main/java/pulse/math/FunctionWithInterpolation.java b/src/main/java/pulse/math/FunctionWithInterpolation.java index 9cbf3d45..41971b76 100644 --- a/src/main/java/pulse/math/FunctionWithInterpolation.java +++ b/src/main/java/pulse/math/FunctionWithInterpolation.java @@ -1,18 +1,25 @@ package pulse.math; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import pulse.util.FunctionSerializer; /** * An abstract class for univariate functions with the capacity of spline * interpolation. * */ -public abstract class FunctionWithInterpolation { +public abstract class FunctionWithInterpolation implements Serializable { + private static final long serialVersionUID = -303222542756574714L; private Segment tBounds; private int lookupTableSize; - private UnivariateFunction interpolation; + private transient UnivariateFunction interpolation; public final static int NUM_PARTITIONS = 8192; @@ -114,4 +121,26 @@ private void interpolate(double[] lookupTable) { interpolation = splineInterpolation.interpolate(tArray, lookupTable); } + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + oos.writeObject(tBounds); + oos.writeInt(lookupTableSize); + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.tBounds = (Segment) ois.readObject(); + this.lookupTableSize = ois.readInt(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } diff --git a/src/main/java/pulse/math/Harmonic.java b/src/main/java/pulse/math/Harmonic.java index 06e36043..b5207f32 100644 --- a/src/main/java/pulse/math/Harmonic.java +++ b/src/main/java/pulse/math/Harmonic.java @@ -30,6 +30,8 @@ */ public class Harmonic extends PropertyHolder implements Optimisable, Comparable { + private static final long serialVersionUID = 3732379391172485157L; + private int rank = -1; private double amplitude; @@ -135,7 +137,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public void optimisationVector(ParameterVector output) { var params = output.getParameters(); - + for (int i = 0, size = params.size(); i < size; i++) { var p = params.get(i); @@ -182,13 +184,13 @@ public void optimisationVector(ParameterVector output) { var newParam = new Parameter(newId, transform, bounds); newParam.setValue(value); params.add(newParam); - + } } } - + } @Override @@ -259,7 +261,7 @@ public int compareTo(Harmonic o) { @Override public String toString() { - return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", + return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", rank, frequency, amplitude, phaseShift); } diff --git a/src/main/java/pulse/math/LegendrePoly.java b/src/main/java/pulse/math/LegendrePoly.java index b8095072..29a82bf1 100644 --- a/src/main/java/pulse/math/LegendrePoly.java +++ b/src/main/java/pulse/math/LegendrePoly.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import static pulse.math.MathUtils.fastPowInt; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; @@ -21,8 +22,9 @@ * @see Wiki * page */ -public class LegendrePoly { +public class LegendrePoly implements Serializable { + private static final long serialVersionUID = -6859690814783610846L; private double[] c; private int n; diff --git a/src/main/java/pulse/math/MidpointIntegrator.java b/src/main/java/pulse/math/MidpointIntegrator.java index cc66763b..6109de57 100644 --- a/src/main/java/pulse/math/MidpointIntegrator.java +++ b/src/main/java/pulse/math/MidpointIntegrator.java @@ -11,6 +11,8 @@ */ public abstract class MidpointIntegrator extends FixedIntervalIntegrator { + private static final long serialVersionUID = -5434607461290096748L; + public MidpointIntegrator(Segment bounds, NumericProperty segments) { super(bounds, segments); } diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java index 5556cd15..b46e02ee 100644 --- a/src/main/java/pulse/math/Parameter.java +++ b/src/main/java/pulse/math/Parameter.java @@ -1,12 +1,14 @@ package pulse.math; +import java.io.Serializable; import pulse.math.transforms.Transformable; /** * Parameter class */ -public class Parameter { +public class Parameter implements Serializable { + private static final long serialVersionUID = 3222166682943107207L; private ParameterIdentifier index; private Transformable transform; private Segment bound; @@ -17,9 +19,9 @@ public Parameter(ParameterIdentifier index, Transformable transform, Segment bou this.transform = transform; this.bound = bound; } - + public Parameter(ParameterIdentifier index) { - if(index.getKeyword() != null) { + if (index.getKeyword() != null) { bound = Segment.boundsFrom(index.getKeyword()); } this.index = index; @@ -80,8 +82,8 @@ public double getApparentValue() { public void setValue(double value, boolean ignoreTransform) { this.value = transform == null || ignoreTransform - ? value - : transform.transform(value); + ? value + : transform.transform(value); } public void setValue(double value) { diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java index 3fb7bc49..31a66f7f 100644 --- a/src/main/java/pulse/math/ParameterIdentifier.java +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -1,18 +1,20 @@ package pulse.math; +import java.io.Serializable; import java.util.Objects; import pulse.properties.NumericPropertyKeyword; -public class ParameterIdentifier { - +public class ParameterIdentifier implements Serializable { + + private static final long serialVersionUID = 5288875329862605319L; private NumericPropertyKeyword keyword; private int index; - + public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { this.keyword = keyword; this.index = index; } - + public ParameterIdentifier(NumericPropertyKeyword keyword) { this(keyword, 0); } @@ -24,43 +26,43 @@ public int hashCode() { hash = 29 * hash + this.index; return hash; } - + public ParameterIdentifier(int index) { this.index = index; } - + public NumericPropertyKeyword getKeyword() { return keyword; } - + public int getIndex() { return index; } - + @Override public boolean equals(Object id) { - if(id.getClass() == null) { + if (id.getClass() == null) { return false; } - + var classA = id.getClass(); var classB = this.getClass(); - - if(classA != classB) { + + if (classA != classB) { return false; } - + var pid = (ParameterIdentifier) id; return keyword == pid.keyword && Math.abs(index - pid.index) < 1; } - + @Override public String toString() { StringBuilder sb = new StringBuilder("").append(keyword); - if(index > 0) { + if (index > 0) { sb.append(" # ").append(index); } return sb.toString(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 6bd17373..f9d27fba 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,9 +1,8 @@ package pulse.math; +import java.io.Serializable; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import pulse.math.linear.Vector; @@ -15,9 +14,10 @@ * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific * components of the vector. Used when constructing the optimisation vector. */ -public class ParameterVector { +public class ParameterVector implements Serializable { - private List params; + private static final long serialVersionUID = -4678286597080149891L; + private final List params; /** * Constructs an {@code IndexedVector} with the specified list of keywords. @@ -109,11 +109,11 @@ public void setValues(Vector v) { throw new IllegalArgumentException("Illegal vector dimension: " + dim + " != " + this.dimension()); } - - for(int i = 0; i < dim; i++) { + + for (int i = 0; i < dim; i++) { params.get(i).setValue(v.get(i)); } - + } public int dimension() { diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index c1e0c762..95e6c232 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import java.util.Random; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericProperties.def; @@ -9,11 +10,15 @@ * that {@code a < b}. * */ -public class Segment { +public class Segment implements Serializable { + /** + * + */ + private static final long serialVersionUID = -1373763811823628708L; private double a; private double b; - + public final static Segment UNBOUNDED = new Segment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); /** @@ -36,17 +41,17 @@ public Segment(Segment segment) { this.a = segment.a; this.b = segment.b; } - + /** - * Creates a segment representing the bounds of {@code p}, i.e. the range - * in which the property value is allowed to change + * Creates a segment representing the bounds of {@code p}, i.e. the range in + * which the property value is allowed to change + * * @param p a property keyword to extract default bounds * @return a {@code Segment} with the bounds */ - public static Segment boundsFrom(NumericPropertyKeyword p) { - return new Segment(def(p).getMinimum().doubleValue(), - def(p).getMaximum().doubleValue()); + return new Segment(def(p).getMinimum().doubleValue(), + def(p).getMaximum().doubleValue()); } /** diff --git a/src/main/java/pulse/math/SimpsonIntegrator.java b/src/main/java/pulse/math/SimpsonIntegrator.java index 55ddda7b..8a367d6c 100644 --- a/src/main/java/pulse/math/SimpsonIntegrator.java +++ b/src/main/java/pulse/math/SimpsonIntegrator.java @@ -11,6 +11,8 @@ */ public abstract class SimpsonIntegrator extends FixedIntervalIntegrator { + private static final long serialVersionUID = -7800272372472765906L; + public SimpsonIntegrator(Segment bounds) { super(bounds); } diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java index 48fbd4ff..a230ccb8 100644 --- a/src/main/java/pulse/math/Window.java +++ b/src/main/java/pulse/math/Window.java @@ -1,60 +1,58 @@ package pulse.math; -public interface Window { +import java.io.Serializable; + +public interface Window extends Serializable { public final static Window NONE = (n, N) -> 1.0; - public final static Window HANN = (n, N) -> Math.pow( Math.sin(Math.PI * n / ((double) N)), 2); - public final static Window HAMMING = (n, N) -> 0.54 + 0.46*Math.cos(2.0 * Math.PI * n / ((double) N)); + public final static Window HANN = (n, N) -> Math.pow(Math.sin(Math.PI * n / ((double) N)), 2); + public final static Window HAMMING = (n, N) -> 0.54 + 0.46 * Math.cos(2.0 * Math.PI * n / ((double) N)); public final static Window BLACKMANN_HARRIS = (n, N) -> { - final double x = 2.0*Math.PI*n/ ((double)N); - return 0.35875 - 0.48829*Math.cos(x) + 0.14128*Math.cos(2.0*x) - 0.01168*Math.cos(3.0*x); - }; - public final static Window FLAT_TOP = (n, N) -> { - final double x = 2.0*Math.PI*n/ ((double)N); - return 0.21557895 - 0.41663158*Math.cos(x) + 0.277263158*Math.cos(2.0*x) - - 0.083578947*Math.cos(3.0*x) + 0.006947368 * Math.cos(4.0 * x); - }; + final double x = 2.0 * Math.PI * n / ((double) N); + return 0.35875 - 0.48829 * Math.cos(x) + 0.14128 * Math.cos(2.0 * x) - 0.01168 * Math.cos(3.0 * x); + }; + public final static Window FLAT_TOP = (n, N) -> { + final double x = 2.0 * Math.PI * n / ((double) N); + return 0.21557895 - 0.41663158 * Math.cos(x) + 0.277263158 * Math.cos(2.0 * x) + - 0.083578947 * Math.cos(3.0 * x) + 0.006947368 * Math.cos(4.0 * x); + }; public final static Window TUKEY = new Window() { - + private final static double alpha = 0.6; - + @Override public double evaluate(int n, int N) { - + double result = 0; - - if(n < 0.5*alpha*N) { - result = 0.5 * ( 1 - Math.cos(2.0*Math.PI*n/(alpha*N))); - } - - else if(n <= N/2) { + + if (n < 0.5 * alpha * N) { + result = 0.5 * (1 - Math.cos(2.0 * Math.PI * n / (alpha * N))); + } else if (n <= N / 2) { result = 1.0; + } else { + result = TUKEY.evaluate(N - n, N); } - - else { - result = TUKEY.evaluate(N - n,N); - } - + return result; - + } }; - + public final static Window HANN_POISSON = (n, N) -> { - + final double alpha = 2.0; - return HANN.evaluate(n, N) * Math.exp( - alpha * (N - 2 * n) / N); - + return HANN.evaluate(n, N) * Math.exp(-alpha * (N - 2 * n) / N); + }; - + public default double[] apply(double[] input) { double[] output = new double[input.length]; - for(int i = 0; i < output.length; i++) { + for (int i = 0; i < output.length; i++) { output[i] = input[i] * evaluate(i, input.length); } return output; } - + public abstract double evaluate(int n, int N); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java index fa13fad1..83be83a8 100644 --- a/src/main/java/pulse/math/ZScore.java +++ b/src/main/java/pulse/math/ZScore.java @@ -17,23 +17,22 @@ * calculated as the difference of the current value and population mean divided * by the population standard deviation. */ - public class ZScore { - + private double[] avgFilter; private double[] stdFilter; private int[] signals; - + private int lag; private double threshold; private double influence; - + public ZScore(int lag, double threshold, double influence) { this.lag = lag; this.threshold = threshold; this.influence = influence; } - + public ZScore() { this(40, 3.5, 0.3); } @@ -42,32 +41,32 @@ public void process(double[] input) { signals = new int[input.length]; List filteredY = DoubleStream.of(input).boxed().collect(Collectors.toList()); - - var initialWindow = filteredY.subList(input.length - lag, input.length - 1); + + var initialWindow = new ArrayList<>(filteredY.subList(input.length - lag, input.length - 1)); avgFilter = new double[input.length]; stdFilter = new double[input.length]; - + avgFilter[input.length - lag + 1] = mean(initialWindow); stdFilter[input.length - lag + 1] = stdev(initialWindow); for (int i = input.length - lag; i > 0; i--) { - + if (Math.abs(input[i] - avgFilter[i + 1]) > threshold * stdFilter[i + 1]) { - + signals[i] = (input[i] > avgFilter[i + 1]) ? 1 : -1; - filteredY.set(i, influence * input[i] - + (1 - influence) * filteredY.get(i + 1)); - + filteredY.set(i, influence * input[i] + + (1 - influence) * filteredY.get(i + 1)); + } else { - + signals[i] = 0; filteredY.set(i, input[i]); - + } // Update rolling average and deviation - var slidingWindow = filteredY.subList(i, i + lag - 1); + var slidingWindow = new ArrayList<>(filteredY.subList(i, i + lag - 1)); avgFilter[i] = mean(slidingWindow); stdFilter[i] = stdev(slidingWindow); @@ -89,19 +88,19 @@ private static double stdev(List values) { } return ret; } - + public int[] getSignals() { return signals; } - + public double[] getFilteredAverage() { return avgFilter; } - + public double[] getFilteredStdev() { return stdFilter; } - + /* public static void main(String[] args) { Scanner sc = null; @@ -129,6 +128,5 @@ public static void main(String[] args) { } } - */ - -} \ No newline at end of file + */ +} diff --git a/src/main/java/pulse/math/filters/AssignmentListener.java b/src/main/java/pulse/math/filters/AssignmentListener.java index 251884a3..6020aa6f 100644 --- a/src/main/java/pulse/math/filters/AssignmentListener.java +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -1,7 +1,9 @@ package pulse.math.filters; -public interface AssignmentListener { - +import java.io.Serializable; + +public interface AssignmentListener extends Serializable { + public void onValueAssigned(); - + } \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/Filter.java b/src/main/java/pulse/math/filters/Filter.java index 067ab3e6..0bba2aeb 100644 --- a/src/main/java/pulse/math/filters/Filter.java +++ b/src/main/java/pulse/math/filters/Filter.java @@ -1,14 +1,16 @@ package pulse.math.filters; import java.awt.geom.Point2D; +import java.io.Serializable; import java.util.List; import pulse.DiscreteInput; -public interface Filter { +public interface Filter extends Serializable { + + public List process(List input); - public List process(List input); public default List process(DiscreteInput input) { return process(DiscreteInput.convert(input.getX(), input.getY())); - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/pulse/math/filters/HalfTimeCalculator.java b/src/main/java/pulse/math/filters/HalfTimeCalculator.java index e2ca9ce1..7f98f3e4 100644 --- a/src/main/java/pulse/math/filters/HalfTimeCalculator.java +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -1,6 +1,7 @@ package pulse.math.filters; import java.awt.geom.Point2D; +import java.io.Serializable; import static java.lang.Double.valueOf; import static java.util.Collections.max; import java.util.Comparator; @@ -9,20 +10,21 @@ import pulse.baseline.FlatBaseline; import pulse.input.IndexRange; -public class HalfTimeCalculator { - +public class HalfTimeCalculator implements Serializable { + + private static final long serialVersionUID = 8302980290467110065L; private final Filter filter; private final DiscreteInput data; private Point2D max; private double halfTime; - + /** * A fail-safe factor. */ public final static double FAIL_SAFE_FACTOR = 10.0; - private static final Comparator pointComparator = - (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + private static final Comparator pointComparator + = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); public HalfTimeCalculator(DiscreteInput input) { this.data = input; @@ -40,56 +42,55 @@ public HalfTimeCalculator(DiscreteInput input) { * The index corresponding to the closest temperature value available for * that curve is used to retrieve the half-rise time (which also has the * same index). If this fails, i.e. the associated index is less than 1, - * this will print out a warning message and still assign a value to the - * half-time variable equal to the acquisition time divided by a fail-safe factor - * {@value FAIL_SAFE_FACTOR}. - *

+ * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe + * factor {@value FAIL_SAFE_FACTOR}. + *

+ * * @see getHalfTime() */ public void calculate() { var baseline = new FlatBaseline(); baseline.fitTo(data); - + var filtered = filter.process(data); - + max = max(filtered, pointComparator); - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + int indexLeft = IndexRange.closestLeft(halfMax, - filtered.stream().map(point -> point.getY()) - .collect(Collectors.toList())); - + filtered.stream().map(point -> point.getY()) + .collect(Collectors.toList())); + if (indexLeft < 1 || indexLeft > filtered.size() - 2) { halfTime = filtered.get(filtered.size() - 1).getX() / FAIL_SAFE_FACTOR; - } - else { + } else { //extrapolate Point2D p1 = filtered.get(indexLeft); Point2D p2 = filtered.get(indexLeft + 1); - - halfTime = (halfMax - p1.getY())/(p2.getY() - p1.getY()) - *(p2.getX() - p1.getX()) + p1.getX(); + + halfTime = (halfMax - p1.getY()) / (p2.getY() - p1.getY()) + * (p2.getX() - p1.getX()) + p1.getX(); } - + } - - + /** - * Retrieves the half-time value of this dataset, which is equal to the - * time needed to reach half of the signal maximum. + * Retrieves the half-time value of this dataset, which is equal to the time + * needed to reach half of the signal maximum. + * * @return the half-time value. */ - public final double getHalfTime() { return halfTime; } - + public final Point2D getFilteredMaximum() { return max; } - + public DiscreteInput getData() { return data; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/filters/OptimisablePolyline.java b/src/main/java/pulse/math/filters/OptimisablePolyline.java index 4d7a8d6a..4356b4f5 100644 --- a/src/main/java/pulse/math/filters/OptimisablePolyline.java +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -14,9 +14,10 @@ public class OptimisablePolyline extends PropertyHolder implements Optimisable { + private static final long serialVersionUID = 418264754603533971L; private final double[] x; private final double[] y; - private final List listeners; + private List listeners; public OptimisablePolyline(List data) { x = data.stream().mapToDouble(d -> d.getX()).toArray(); @@ -27,7 +28,7 @@ public OptimisablePolyline(List data) { @Override public void assign(ParameterVector input) throws SolverException { var ps = input.getParameters(); - for(int i = 0, size = ps.size(); i < size; i++) { + for (int i = 0, size = ps.size(); i < size; i++) { y[i] = ps.get(i).getApparentValue(); } listeners.stream().forEach(l -> l.onValueAssigned()); @@ -37,7 +38,7 @@ public void assign(ParameterVector input) throws SolverException { public void optimisationVector(ParameterVector output) { output.setValues(new Vector(y)); } - + public List points() { return DiscreteInput.convert(x, y); } @@ -46,17 +47,17 @@ public List points() { public void set(NumericPropertyKeyword type, NumericProperty property) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + public double[] getX() { return x; } - + public double[] getY() { return y; } - + public void addAssignmentListener(AssignmentListener listener) { listeners.add(listener); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java index 46c004bb..0f9ce5ad 100644 --- a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -6,6 +6,8 @@ public class OptimisedRunningAverage extends RunningAverage { + private static final long serialVersionUID = 1272276960302188392L; + public OptimisedRunningAverage() { super(); } @@ -17,10 +19,10 @@ public OptimisedRunningAverage(int reductionFactor) { @Override public List process(DiscreteInput input) { var p = super.process(input); - var optimisableCurve = new OptimisablePolyline(p); + var optimisableCurve = new OptimisablePolyline(p); var task = new PolylineOptimiser(input, optimisableCurve); task.run(); return optimisableCurve.points(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java index dde0142a..c2f7ffb5 100644 --- a/src/main/java/pulse/math/filters/PolylineOptimiser.java +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -20,6 +20,7 @@ public class PolylineOptimiser extends SimpleOptimisationTask { + private static final long serialVersionUID = -9056678836812293655L; private final OptimiserStatistic sos; private final PolylineResponse response; private final OptimisablePolyline optimisableCurve; @@ -87,4 +88,4 @@ public double evaluate(double t) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/Randomiser.java b/src/main/java/pulse/math/filters/Randomiser.java index 3d15453e..cfd6d714 100644 --- a/src/main/java/pulse/math/filters/Randomiser.java +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -4,23 +4,24 @@ import java.util.List; public class Randomiser implements Filter { - + + private static final long serialVersionUID = 3390706390237573886L; private final double amplitude; - + public Randomiser(double amplitude) { this.amplitude = amplitude; } @Override - public List process(List input) { - input.forEach(p -> - ((Point2D.Double)p).y += (Math.random() - 0.5) * amplitude + public List process(List input) { + input.forEach(p + -> ((Point2D.Double) p).y += (Math.random() - 0.5) * amplitude ); return input; } - + public double getAmplitude() { return amplitude; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/filters/RunningAverage.java b/src/main/java/pulse/math/filters/RunningAverage.java index 37a08a20..2a2b82b9 100644 --- a/src/main/java/pulse/math/filters/RunningAverage.java +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -3,30 +3,29 @@ import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; -import pulse.DiscreteInput; public class RunningAverage implements Filter { - + + private static final long serialVersionUID = -6134297308468858848L; + private int bins; - - /** + + /** * The binning factor used to build a crude approximation of the heating * curve. Described in Lunev, A., & Heymer, R. (2020). Review of * Scientific Instruments, 91(6), 064902. */ - public static final int DEFAULT_BINS = 16; public final static int MIN_BINS = 4; - + /** * @param reductionFactor the factor, by which the number of points * {@code count} will be reduced for this {@code ExperimentalData}. */ - public RunningAverage(int reductionFactor) { this.bins = reductionFactor; } - + public RunningAverage() { this.bins = DEFAULT_BINS; } @@ -53,7 +52,6 @@ public RunningAverage() { * @see halfRiseTime() * @see pulse.AbstractData.maxTemperature() */ - @Override public List process(List points) { var x = points.stream().mapToDouble(p -> p.getX()).toArray(); @@ -62,14 +60,14 @@ public List process(List points) { int size = x.length; int step = size / bins; List movingAverage = new ArrayList<>(bins); - + for (int i = 0; i < bins; i++) { - int i1 = step*i; - int i2 = step*(i+1); + int i1 = step * i; + int i2 = step * (i + 1); double av = 0; int j; - + for (j = i1; j < i2 && j < size; j++) { av += y[j]; } @@ -77,55 +75,54 @@ public List process(List points) { av /= j - i1; i2 = j - 1; - movingAverage.add(new Point2D.Double( - (x[i1] + x[i2])/ 2.0, av)); + movingAverage.add(new Point2D.Double( + (x[i1] + x[i2]) / 2.0, av)); } - + addBoundaryPoints(movingAverage, x[0], x[size - 1]); - + /* for(int i = 0; i < movingAverage.size(); i++) { System.err.println(movingAverage.get(i)); } - */ - + */ return movingAverage; - } - + } + private static void addBoundaryPoints(List d, double minTime, double maxTime) { int max = d.size(); - + d.add( - extrapolate(d.get(max - 1), - d.get(max - 2), - maxTime) - ); - - d.add( 0, - extrapolate(d.get(0), - d.get(1), - minTime) - ); - + extrapolate(d.get(max - 1), + d.get(max - 2), + maxTime) + ); + + d.add(0, + extrapolate(d.get(0), + d.get(1), + minTime) + ); + } - - private static Point2D extrapolate(Point2D a, Point2D b, double x) { + + private static Point2D extrapolate(Point2D a, Point2D b, double x) { double y1 = a.getY(); double y2 = b.getY(); double x1 = a.getX(); double x2 = b.getX(); - - return new Point2D.Double(x, y1 + (x - x1)/(x2 - x1)*(y2 - y1)); + + return new Point2D.Double(x, y1 + (x - x1) / (x2 - x1) * (y2 - y1)); } - + public final int getNumberOfBins() { return bins; } - + public final void setNumberOfBins(int no) { this.bins = no > MIN_BINS - 1 ? no : MIN_BINS; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/linear/Matrix2.java b/src/main/java/pulse/math/linear/Matrix2.java index aa579847..00cf4095 100644 --- a/src/main/java/pulse/math/linear/Matrix2.java +++ b/src/main/java/pulse/math/linear/Matrix2.java @@ -6,6 +6,8 @@ */ class Matrix2 extends SquareMatrix { + private static final long serialVersionUID = 6015187791989387058L; + protected Matrix2(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/Matrix3.java b/src/main/java/pulse/math/linear/Matrix3.java index 09e799f6..3835e329 100644 --- a/src/main/java/pulse/math/linear/Matrix3.java +++ b/src/main/java/pulse/math/linear/Matrix3.java @@ -6,6 +6,8 @@ */ class Matrix3 extends SquareMatrix { + private static final long serialVersionUID = -2671066600560428989L; + protected Matrix3(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/Matrix4.java b/src/main/java/pulse/math/linear/Matrix4.java index 970a7a9a..cc34b17d 100644 --- a/src/main/java/pulse/math/linear/Matrix4.java +++ b/src/main/java/pulse/math/linear/Matrix4.java @@ -6,6 +6,8 @@ */ class Matrix4 extends SquareMatrix { + private static final long serialVersionUID = -1355372261335732541L; + protected Matrix4(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/RectangularMatrix.java b/src/main/java/pulse/math/linear/RectangularMatrix.java index 8f325dc7..8ac8470c 100644 --- a/src/main/java/pulse/math/linear/RectangularMatrix.java +++ b/src/main/java/pulse/math/linear/RectangularMatrix.java @@ -1,5 +1,6 @@ package pulse.math.linear; +import java.io.Serializable; import static pulse.math.MathUtils.approximatelyEquals; import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; import static pulse.math.linear.ArithmeticOperations.SUM; @@ -7,8 +8,9 @@ import pulse.ui.Messages; -public class RectangularMatrix { +public class RectangularMatrix implements Serializable { + private static final long serialVersionUID = -8184303238440935851L; protected final double[][] x; protected RectangularMatrix(double[][] args) { diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 39d06531..483cf0c5 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -1,5 +1,6 @@ package pulse.math.linear; +import java.io.Serializable; import static java.lang.Math.abs; import static java.lang.Math.sqrt; import java.util.List; @@ -16,8 +17,12 @@ * and ODE solvers. *

*/ -public class Vector { +public class Vector implements Serializable { + /** + * + */ + private static final long serialVersionUID = 5560069982536341831L; private double[] x; /** @@ -122,7 +127,7 @@ public static Vector random(int n, double min, double max) { } return v; } - + /** * Component-wise vector multiplication */ diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java index b65577e9..253fd0e1 100644 --- a/src/main/java/pulse/math/transforms/AtanhTransform.java +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -11,6 +11,8 @@ */ public class AtanhTransform extends BoundedParameterTransform { + private static final long serialVersionUID = -6322775329000050307L; + /** * Only the upper bound of the argument is used. * diff --git a/src/main/java/pulse/math/transforms/InvDiamTransform.java b/src/main/java/pulse/math/transforms/InvDiamTransform.java index c8810814..22345dfb 100644 --- a/src/main/java/pulse/math/transforms/InvDiamTransform.java +++ b/src/main/java/pulse/math/transforms/InvDiamTransform.java @@ -8,6 +8,7 @@ */ public class InvDiamTransform implements Transformable { + private static final long serialVersionUID = 1809584085307619279L; private double d; public InvDiamTransform(ExtendedThermalProperties etp) { diff --git a/src/main/java/pulse/math/transforms/PeriodicTransform.java b/src/main/java/pulse/math/transforms/PeriodicTransform.java index 31cee068..c788a9b6 100644 --- a/src/main/java/pulse/math/transforms/PeriodicTransform.java +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -4,6 +4,8 @@ public class PeriodicTransform extends BoundedParameterTransform { + private static final long serialVersionUID = 4564881912462997982L; + /** * Only the upper bound of the argument is used. * @@ -23,7 +25,7 @@ public double transform(double a) { double max = getBounds().getMaximum(); double min = getBounds().getMinimum(); double len = max - min; - + return a > max ? transform(a - len) : (a < min ? transform(a + len) : a); } @@ -35,4 +37,4 @@ public double transform(double a) { public double inverse(double t) { return t; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index f5487615..ce5fd4a5 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -18,16 +18,18 @@ import pulse.math.Segment; /** - * A simple bounded transform which makes the parameter stick to the - * boundaries upon reaching them. For insatnce, when a parameter x + * A simple bounded transform which makes the parameter stick to the boundaries + * upon reaching them. For insatnce, when a parameter x * attempts to escape its bounds due to a larger increment then allowed, this * transform will return it directly to the respective boundary, where it will * "stick". + * * @author Artem Lunev */ - public class StickTransform extends BoundedParameterTransform { + private static final long serialVersionUID = -8709273330809657074L; + /** * Only the upper bound of the argument is used. * @@ -57,5 +59,5 @@ public double transform(double a) { public double inverse(double t) { return transform(t); } - + } diff --git a/src/main/java/pulse/math/transforms/Transformable.java b/src/main/java/pulse/math/transforms/Transformable.java index 88e9d13c..150f6c30 100644 --- a/src/main/java/pulse/math/transforms/Transformable.java +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -1,11 +1,13 @@ package pulse.math.transforms; +import java.io.Serializable; + /** * An interface for performing reversible one-to-one mapping of the model * parameters. * */ -public interface Transformable { +public interface Transformable extends Serializable { /** * Performs the selected transform with {@code value} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 88913244..5ec9be1e 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -1,5 +1,6 @@ package pulse.problem.laser; +import java.io.Serializable; import java.util.Objects; import pulse.input.ExperimentalData; import pulse.math.MidpointIntegrator; @@ -19,8 +20,9 @@ * * @see pulse.problem.statements.Pulse */ -public class DiscretePulse { +public class DiscretePulse implements Serializable { + private static final long serialVersionUID = 5826506918603729615L; private final Grid grid; private final Pulse pulse; private final ExperimentalData data; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 02a60f09..21be2b89 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -18,14 +18,14 @@ */ public class DiscretePulse2D extends DiscretePulse { + private static final long serialVersionUID = 6203222036852037146L; private double discretePulseSpot; private double sampleRadius; private double normFactor; - + /** * This had to be decreased for the 2d pulses. */ - private final static int WIDTH_TOLERANCE_FACTOR = 1000; /** @@ -43,9 +43,9 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { super(problem, grid); var properties = (ExtendedThermalProperties) problem.getProperties(); calcPulseSpot(properties); - properties.addListener(e -> calcPulseSpot(properties) ); + properties.addListener(e -> calcPulseSpot(properties)); } - + /** * This calculates the dimensionless, discretised pulse function at a * dimensionless radial coordinate {@code coord}. @@ -61,58 +61,58 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { * {@code coord > spotDiameter}. * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) */ - public double evaluateAt(double time, double radialCoord) { - return laserPowerAt(time) + return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); } - + /** - * Calculates the laser power at a give moment in time. The total laser - * energy is normalised over a beam partially illuminating the sample surface. + * Calculates the laser power at a give moment in time. The total laser + * energy is normalised over a beam partially illuminating the sample + * surface. + * * @param time a moment in time (in dimensionless units) * @return the laser power in arbitrary units */ - @Override public double laserPowerAt(double time) { return normFactor * super.laserPowerAt(time); } - + private void calcPulseSpot(ExtendedThermalProperties properties) { - sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; + sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; evalPulseSpot(); } /** - * Calculates the {@code discretePulseSpot} using the {@code gridRadialDistance} method. + * Calculates the {@code discretePulseSpot} using the + * {@code gridRadialDistance} method. * * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) */ public final void evalPulseSpot() { var pulse = (Pulse2D) getPhysicalPulse(); var grid2d = (Grid2D) getGrid(); - final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; + final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); grid2d.adjustStepSize(this); - normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; + normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; } public final double getDiscretePulseSpot() { return discretePulseSpot; } - + public final double getRadialConversionFactor() { return sampleRadius; } - + /** * A smaller tolerance factor is set for 2D calculations */ - @Override public int getWidthToleranceFactor() { return WIDTH_TOLERANCE_FACTOR; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index 379ab97f..d4490d2b 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -24,10 +24,11 @@ */ public class ExponentiallyModifiedGaussian extends PulseTemporalShape { + private static final long serialVersionUID = -4437794069527301235L; private double mu; private double sigma; private double lambda; - + private final static int MIN_POINTS = 10; /** @@ -159,4 +160,4 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 9dbaadb6..a71d65fd 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -1,10 +1,14 @@ package pulse.problem.laser; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import pulse.input.ExperimentalData; import pulse.problem.statements.Problem; @@ -14,6 +18,7 @@ import pulse.baseline.FlatBaseline; import pulse.tasks.Calculation; +import pulse.util.FunctionSerializer; /** * A numeric pulse is given by a set of discrete {@code NumericPulseData} @@ -24,9 +29,10 @@ */ public class NumericPulse extends PulseTemporalShape { + private static final long serialVersionUID = 6088261629992349844L; private NumericPulseData pulseData; - private UnivariateFunction interpolation; - + private transient UnivariateFunction interpolation; + private final static int MIN_POINTS = 20; public NumericPulse() { @@ -57,40 +63,42 @@ public NumericPulse(NumericPulse pulse) { public void init(ExperimentalData data, DiscretePulse pulse) { //generate baseline-subtracted numeric data from ExperimentalData baselineSubtractedFrom(data); - + //notify host pulse object of a new pulse width - var problem = ( (Calculation) ((SearchTask) data.getParent()) - .getResponse() ).getProblem(); + var problem = ((Calculation) ((SearchTask) data.getParent()) + .getResponse()).getProblem(); setPulseWidthOf(problem); //convert to dimensionless time and interpolate double timeFactor = problem.getProperties().characteristicTime(); doInterpolation(timeFactor); } - + /** - * Copies the numeric pulse from metadata and subtracts a horizontal baseline - * from the data points assigned to {@code pulseData}. - * @param data the experimental data containing the metadata with numeric pulse data. + * Copies the numeric pulse from metadata and subtracts a horizontal + * baseline from the data points assigned to {@code pulseData}. + * + * @param data the experimental data containing the metadata with numeric + * pulse data. */ - private void baselineSubtractedFrom(ExperimentalData data) { pulseData = new NumericPulseData(data.getMetadata().getPulseData()); - + //subtracts a horizontal baseline from the pulse data var baseline = new FlatBaseline(); baseline.fitTo(pulseData); - - for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) - pulseData.setSignalAt(i, + + for (int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) { + pulseData.setSignalAt(i, pulseData.signalAt(i) - baseline.valueAt(pulseData.timeAt(i))); + } } private void setPulseWidthOf(Problem problem) { - var timeSequence = pulseData.getTimeSequence(); - double pulseWidth = timeSequence.get(timeSequence.size() - 1); + var timeSequence = pulseData.getTimeSequence(); + double pulseWidth = timeSequence.get(timeSequence.size() - 1); - var pulseObject = problem.getPulse(); + var pulseObject = problem.getPulse(); pulseObject.setPulseWidth(derive(PULSE_WIDTH, pulseWidth)); } @@ -98,11 +106,11 @@ private void setPulseWidthOf(Problem problem) { private void doInterpolation(double timeFactor) { var interpolator = new AkimaSplineInterpolator(); - var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); - var powerList = pulseData.getSignalData(); + var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); + var powerList = pulseData.getSignalData(); this.setPulseWidth(timeList[timeList.length - 1]); - + interpolation = interpolator.interpolate(timeList, powerList.stream().mapToDouble(d -> d).toArray()); } @@ -135,7 +143,7 @@ public NumericPulseData getData() { public void setData(NumericPulseData pulseData) { this.pulseData = pulseData; - + } public UnivariateFunction getInterpolation() { @@ -147,4 +155,22 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } + /* + Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index 5a53fd0a..7bbfeeed 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -14,6 +14,7 @@ */ public class NumericPulseData extends AbstractData implements DiscreteInput { + private static final long serialVersionUID = 8142129124831241206L; private final int externalID; /** @@ -54,7 +55,7 @@ public void addPoint(double time, double power) { public int getExternalID() { return externalID; } - + public double pulseWidth() { return super.timeLimit(); } @@ -73,5 +74,5 @@ public List getY() { public IndexRange getIndexRange() { return new IndexRange(this.getTimeSequence(), Range.UNLIMITED); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index 6c3814e4..9a74c09c 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -1,6 +1,5 @@ package pulse.problem.laser; - import pulse.input.ExperimentalData; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -23,7 +22,7 @@ public PulseTemporalShape() { public PulseTemporalShape(PulseTemporalShape another) { this.width = another.width; } - + /** * This evaluates the dimensionless, discretised pulse function on a * {@code grid} needed to evaluate the heat source in the difference scheme. @@ -64,7 +63,7 @@ public double getPulseWidth() { public void setPulseWidth(double width) { this.width = width; } - + public abstract int getRequiredDiscretisation(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index 583a92e1..7bc5dfbd 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -14,11 +14,12 @@ */ public class RectangularPulse extends PulseTemporalShape { + private static final long serialVersionUID = 8207478409316696745L; private final static int MIN_POINTS = 2; - + /** * @param time the time measured from the start of the laser pulse. - * @return + * @return */ @Override public double evaluateAt(double time) { @@ -35,10 +36,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new RectangularPulse(); } - + @Override public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index a061a875..908e2cfc 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -18,12 +18,13 @@ */ public class TrapezoidalPulse extends PulseTemporalShape { + private static final long serialVersionUID = 2089809680713225034L; private double rise; private double fall; private double h; private final static int MIN_POINTS = 8; - + /** * Constructs a trapezoidal pulse using a default segmentation principle. * The reader is referred to the {@code .xml} file containing the default @@ -42,7 +43,7 @@ public TrapezoidalPulse(TrapezoidalPulse another) { this.fall = another.fall; this.h = another.h; } - + /** * Calculates the height of the trapezium which under current segmentation * will yield an area of unity. @@ -125,10 +126,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new TrapezoidalPulse(this); } - + @Override public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index db0c7e48..b20b34db 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -14,6 +14,11 @@ */ public abstract class ADIScheme extends DifferenceScheme { + /** + * + */ + private static final long serialVersionUID = 4772650159522354367L; + /** * Creates a new {@code ADIScheme} with default values of grid density and * time factor. @@ -56,13 +61,14 @@ public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty public String toString() { return getString("ADIScheme.4"); } - + /** - * Contains only an empty statement, as the pulse needs to be calculated not only - * for the time step {@code m} but also accounting for the radial coordinate + * Contains only an empty statement, as the pulse needs to be calculated not + * only for the time step {@code m} but also accounting for the radial + * coordinate + * * @param m thte time step */ - @Override public void prepareStep(int m) { //do nothing diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index acaa5cd1..450db9db 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -11,6 +11,7 @@ */ public class BlockMatrixAlgorithm extends TridiagonalMatrixAlgorithm { + private static final long serialVersionUID = -6553638438386098008L; private final double[] gamma; private final double[] p; private final double[] q; @@ -36,7 +37,7 @@ public void evaluateBeta(final double[] U) { super.evaluateBeta(U); var alpha = getAlpha(); var beta = getBeta(); - + final int N = getGridPoints(); p[N - 1] = beta[N]; @@ -51,7 +52,7 @@ public void evaluateBeta(final double[] U) { @Override public void evaluateBeta(final double[] U, final int start, final int endExclusive) { var alpha = getAlpha(); - + final double h = this.getGridStep(); final double HX2_TAU = h * h / this.getTimeStep(); diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index f4577d98..17976a27 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -14,6 +14,7 @@ public abstract class CoupledImplicitScheme extends ImplicitScheme { + private static final long serialVersionUID = 1974655675470727643L; private RadiativeTransferCoupling coupling; private RTECalculationStatus calculationStatus; private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers @@ -29,11 +30,11 @@ public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, Nume this(N, timeFactor); setTimeLimit(timeLimit); } - + @Override public void finaliseStep() throws SolverException { super.finaliseStep(); - if(autoUpdateFluxes) { + if (autoUpdateFluxes) { var rte = this.getCoupling().getRadiativeTransferEquation(); setCalculationStatus(rte.compute(getCurrentSolution())); } @@ -59,11 +60,11 @@ public final RTECalculationStatus getCalculationStatus() { public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; if (calculationStatus != RTECalculationStatus.NORMAL) { - throw new SolverException(calculationStatus.toString(), + throw new SolverException(calculationStatus.toString(), RTE_SOLVER_ERROR); } } - + public final RadiativeTransferCoupling getCoupling() { return coupling; } @@ -72,18 +73,18 @@ public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } - + public final boolean isAutoUpdateFluxes() { return this.autoUpdateFluxes; } - + public final void setAutoUpdateFluxes(boolean auto) { this.autoUpdateFluxes = auto; } - + @Override public Class[] domain() { return new Class[]{ParticipatingMedium.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 8ad8dfd8..64de3f8c 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -29,7 +29,7 @@ */ public abstract class DifferenceScheme extends PropertyHolder implements Reflexive { - private DiscretePulse discretePulse; + private transient DiscretePulse discretePulse; private Grid grid; private double timeLimit; @@ -92,7 +92,7 @@ public void copyFrom(DifferenceScheme df) { protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - } + } discretePulse.init(); clearArrays(); } @@ -114,12 +114,12 @@ public void runTimeSequence(Problem problem, final double offset, final double e int numPoints = (int) curve.getNumPoints().getValue(); - final double startTime = (double) curve.getTimeShift().getValue(); + final double startTime = (double) curve.getTimeShift().getValue(); final double timeSegment = (endTime - startTime - offset) / problem.getProperties().characteristicTime(); double tau = grid.getTimeStep(); final double dt = timeSegment / (numPoints - 1); - timeInterval = Math.max( (int) (dt / tau), 1); + timeInterval = Math.max((int) (dt / tau), 1); double wFactor = timeInterval * tau * problem.getProperties().characteristicTime(); @@ -297,16 +297,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { setTimeLimit(property); } } - + public abstract double signal(); - + public abstract void clearArrays(); public abstract void timeStep(int m) throws SolverException; public abstract void finaliseStep() throws SolverException; - /** + /** * Retrieves all problem statements that can be solved with this * implementation of the difference scheme. * @@ -322,5 +322,5 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { * @return an exact copy of this {@code DifferenceScheme}. */ public abstract DifferenceScheme copy(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index 88831e4c..3d179465 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import java.io.Serializable; import java.util.stream.IntStream; import pulse.problem.statements.model.AbsorptionModel; @@ -11,7 +12,9 @@ * {@code AbsorptionModel}. * */ -public class DistributedDetection { +public class DistributedDetection implements Serializable { + + private static final long serialVersionUID = 3587781877001360511L; /** * Calculates the effective signal registered by the detector, which takes @@ -35,4 +38,4 @@ public static double evaluateSignal(final AbsorptionModel absorption, final Grid return signal * 0.5 * hx; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ExplicitScheme.java b/src/main/java/pulse/problem/schemes/ExplicitScheme.java index 70a47ba3..e4b858cd 100644 --- a/src/main/java/pulse/problem/schemes/ExplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ExplicitScheme.java @@ -19,6 +19,11 @@ */ public abstract class ExplicitScheme extends OneDimensionalScheme { + /** + * + */ + private static final long serialVersionUID = -3025913683505686334L; + /** * Constructs a default explicit scheme using the default values of * {@code GRID_DENSITY} and {@code TAU_FACTOR}. diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index 1c359a4c..dbf5992f 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -1,6 +1,8 @@ package pulse.problem.schemes; import static java.lang.Math.abs; + +import java.io.Serializable; import java.util.Arrays; import pulse.problem.schemes.solvers.SolverException; import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.FINITE_DIFFERENCE_ERROR; @@ -10,7 +12,7 @@ * page * */ -public interface FixedPointIterations { +public interface FixedPointIterations extends Serializable { /** * Performs iterations until the convergence criterion is satisfied.The @@ -30,9 +32,9 @@ public default void doIterations(double[] V, final double error, final int m) th final int N = V.length - 1; - for (double V_0 = error + 1, V_N = error + 1; - abs(V[0] - V_0)/abs(V[0] + V_0 + 1e-16) > error - || abs(V[N] - V_N)/abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { + for (double V_0 = error + 1, V_N = error + 1; + abs(V[0] - V_0) / abs(V[0] + V_0 + 1e-16) > error + || abs(V[N] - V_N) / abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { V_N = V[N]; V_0 = V[0]; diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 7bbfc9a8..d8a0b331 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -2,7 +2,6 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; -import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; @@ -10,8 +9,6 @@ import java.util.Set; -import pulse.problem.laser.DiscretePulse; -import pulse.problem.statements.Pulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.util.PropertyHolder; @@ -28,6 +25,7 @@ */ public class Grid extends PropertyHolder { + private static final long serialVersionUID = -4293010190416468656L; private double hx; private double tau; private double tauFactor; @@ -47,7 +45,7 @@ public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { this.N = (int) gridDensity.getValue(); this.tauFactor = (double) timeFactor.getValue(); hx = 1. / N; - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); } protected Grid() { @@ -121,7 +119,7 @@ public final double getTimeStep() { protected final void setTimeStep(double tau) { this.tau = tau; - + } /** @@ -166,7 +164,7 @@ public void setGridDensity(NumericProperty gridDensity) { requireType(gridDensity, GRID_DENSITY); this.N = (int) gridDensity.getValue(); hx = 1. / N; - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); firePropertyChanged(this, gridDensity); } @@ -179,7 +177,7 @@ public void setGridDensity(NumericProperty gridDensity) { public void setTimeFactor(NumericProperty timeFactor) { requireType(timeFactor, TAU_FACTOR); this.tauFactor = (double) timeFactor.getValue(); - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); firePropertyChanged(this, timeFactor); } @@ -193,7 +191,7 @@ public void setTimeFactor(NumericProperty timeFactor) { * @return a double representing the time on the finite grid */ public final double gridTime(double time, double dimensionFactor) { - return ( (int) (time / dimensionFactor / tau) ) * tau; + return ((int) (time / dimensionFactor / tau)) * tau; } /** @@ -213,7 +211,7 @@ public final double gridAxialDistance(double distance, double lengthFactor) { public String toString() { var sb = new StringBuilder("Grid"); sb.append(String.format("%n %-25s", this.getGridDensity())); - sb.append(String.format("%n %-25s", this.getTimeFactor())); + sb.append(String.format("%n %-25s", this.getTimeFactor())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index e35a6e33..35dac537 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -2,7 +2,6 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; -import static java.lang.String.format; import pulse.problem.laser.DiscretePulse; import pulse.problem.laser.DiscretePulse2D; @@ -19,6 +18,7 @@ */ public class Grid2D extends Grid { + private static final long serialVersionUID = 564113358979595637L; private double hy; protected Grid2D() { @@ -46,7 +46,7 @@ public Grid2D copy() { @Override public void setTimeFactor(NumericProperty timeFactor) { super.setTimeFactor(timeFactor); - setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2)) ); + setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); } /** @@ -56,18 +56,17 @@ public void setTimeFactor(NumericProperty timeFactor) { * * @param pulse the discrete puls representation */ - public void adjustStepSize(DiscretePulse pulse) { - var pulse2d = (DiscretePulse2D)pulse; + var pulse2d = (DiscretePulse2D) pulse; double pulseSpotSize = pulse2d.getDiscretePulseSpot(); - if(hy > pulseSpotSize) { + if (hy > pulseSpotSize) { final int INCREMENT = 5; final int newN = getGridDensityValue() + INCREMENT; setGridDensityValue(newN); adjustStepSize(pulse); } - + } @Override @@ -103,4 +102,4 @@ public double getYStep() { return hy; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index 217cb285..0d72003c 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -18,6 +18,10 @@ */ public abstract class ImplicitScheme extends OneDimensionalScheme { + /** + * + */ + private static final long serialVersionUID = 2785615380656900783L; private TridiagonalMatrixAlgorithm tridiagonal; /** @@ -67,14 +71,15 @@ protected void prepare(Problem problem) throws SolverException { } /** - * Calculates the solution at the boundaries using the boundary conditions - * specific to the problem statement and runs the tridiagonal matrix algorithm - * to evaluate solution at the intermediate grid points. + * Calculates the solution at the boundaries using the boundary conditions + * specific to the problem statement and runs the tridiagonal matrix + * algorithm to evaluate solution at the intermediate grid points. + * * @param m the time step - * @throws SolverException if the calculation failed - * @see leftBoundary(), evalRightBoundary(), pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() + * @throws SolverException if the calculation failed + * @see leftBoundary(), evalRightBoundary(), + * pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() */ - @Override public void timeStep(final int m) throws SolverException { leftBoundary(m); @@ -111,4 +116,4 @@ public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal this.tridiagonal = tridiagonal; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/MixedScheme.java b/src/main/java/pulse/problem/schemes/MixedScheme.java index a02bfe3a..fa53e697 100644 --- a/src/main/java/pulse/problem/schemes/MixedScheme.java +++ b/src/main/java/pulse/problem/schemes/MixedScheme.java @@ -17,6 +17,11 @@ */ public abstract class MixedScheme extends ImplicitScheme { + /** + * + */ + private static final long serialVersionUID = -770528855578192638L; + /** * Constructs a default semi-implicit scheme using the default values of * {@code GRID_DENSITY} and {@code TAU_FACTOR}. diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index 5483b5a2..b58bf0fe 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -15,8 +15,7 @@ protected OneDimensionalScheme() { protected OneDimensionalScheme(NumericProperty timeLimit) { super(timeLimit); } - - + @Override public void clearArrays() { final int N = (int) getGrid().getGridDensity().getValue(); @@ -32,9 +31,9 @@ public double signal() { /** * Overwrites previously calculated temperature values with the calculations * made at the current time step + * * @throws SolverException if the calculation failed */ - @Override public void finaliseStep() throws SolverException { System.arraycopy(V, 0, U, 0, V.length); diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index 189548a1..e507e00d 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -4,7 +4,6 @@ import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.rte.dom.DiscreteOrdinatesMethod; -import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -16,9 +15,11 @@ public class RadiativeTransferCoupling extends PropertyHolder { + private static final long serialVersionUID = -8969606772435213260L; private RadiativeTransferSolver rte; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "RTE Solver Selector", RadiativeTransferSolver.class); + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "RTE Solver Selector", RadiativeTransferSolver.class); public RadiativeTransferCoupling() { instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); @@ -34,7 +35,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public void init(ParticipatingMedium problem, Grid grid) { if (rte == null) { - + if (!(problem.getProperties() instanceof ThermoOpticalProperties)) { throw new IllegalArgumentException("Illegal problem type: " + problem); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 9d98c309..0b76d966 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -1,13 +1,17 @@ package pulse.problem.schemes; +import java.io.Serializable; + /** * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving * systems of linear equations. Applicable to such systems where the forming - * matrix has a tridiagonal form: Ai*xi-1 - Bi xi + Ci xi+1 = -Fi. + * matrix has a tridiagonal form: Ai*xi-1 - Bi + * xi + Ci xi+1 = -Fi. * */ -public class TridiagonalMatrixAlgorithm { +public class TridiagonalMatrixAlgorithm implements Serializable { + private static final long serialVersionUID = 8201903787985856087L; private final double tau; private final double h; @@ -21,10 +25,10 @@ public class TridiagonalMatrixAlgorithm { public TridiagonalMatrixAlgorithm(Grid grid) { tau = grid.getTimeStep(); - N = grid.getGridDensityValue(); - h = grid.getXStep(); - alpha = new double[N + 2]; - beta = new double[N + 2]; + N = grid.getGridDensityValue(); + h = grid.getXStep(); + alpha = new double[N + 2]; + beta = new double[N + 2]; } /** @@ -59,6 +63,7 @@ public void evaluateBeta(final double[] U) { /** * Calculates the {@code beta} coefficients as part of the tridiagonal * matrix algorithm. + * * @param U * @param start * @param endExclusive @@ -116,17 +121,17 @@ protected double getCoefB() { protected double getCoefC() { return c; } - + public final double getTimeStep() { return tau; } - + public final int getGridPoints() { return N; } - + public final double getGridStep() { return h; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java index f395d007..5f65f01e 100644 --- a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java +++ b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java @@ -1,11 +1,15 @@ package pulse.problem.schemes.rte; - +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import static pulse.math.MathUtils.fastPowLoop; import pulse.problem.statements.NonlinearProblem; import pulse.problem.statements.Pulse2D; - +import pulse.util.FunctionSerializer; /** * Contains methods for calculating the integral spectral characteristics of a @@ -14,10 +18,11 @@ * {@code SplineInterpolator}. * */ -public class BlackbodySpectrum { +public class BlackbodySpectrum implements Serializable { - private UnivariateFunction interpolation; - private final double reductionFactor; + private static final long serialVersionUID = 4628793608666198231L; + private transient UnivariateFunction interpolation; + private double reductionFactor; /** * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor @@ -36,16 +41,16 @@ public BlackbodySpectrum(NonlinearProblem p) { public String toString() { return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; } - + /** * Calculates the emissive power. This is equal to * 0.25 T0Tm [1 * +δTm /T0 θ (x) * ]4, where θ is the reduced temperature. + * * @param reducedTemperature the dimensionless reduced temperature * @return the amount of emissive power */ - public double emissivePower(double reducedTemperature) { return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); } @@ -63,7 +68,7 @@ public double radianceAt(double x) { } /** - * Calculates the emissive power at the given coordinate. + * Calculates the emissive power at the given coordinate. * * @param x the geometric coordinate inside the sample * @return the local emissive power value @@ -90,4 +95,22 @@ public final double radiance(double reducedTemperature) { return emissivePower(reducedTemperature) / Math.PI; } + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java index 8ad2b582..23a128fa 100644 --- a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java +++ b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java @@ -1,11 +1,13 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** * This is basically a coupling interface between a {@code Solver} and a * {@code RadiativeTransferSolver}. * */ -public interface DerivativeCalculator { +public interface DerivativeCalculator extends Serializable { /** * Calculates the average value of the flux derivatives at the diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index 7c3bc78a..be74c53d 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -23,13 +23,13 @@ public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { public void store() { System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results } - + /** - * Checks whether all stored values are finite. This is equivalent to summing - * all elements and checking whether the sum if finite. + * Checks whether all stored values are finite. This is equivalent to + * summing all elements and checking whether the sum if finite. + * * @return {@code true} if the elements are finite. */ - public RTECalculationStatus checkArrays() { double sum = Arrays.stream(fluxes).sum() + Arrays.stream(storedFluxes).sum(); return Double.isFinite(sum) ? NORMAL : INVALID_FLUXES; diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 2b4ac0f4..f569717d 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -1,19 +1,17 @@ package pulse.problem.schemes.rte; -import java.util.Arrays; -import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; -import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public class FluxesAndExplicitDerivatives extends Fluxes { + private static final long serialVersionUID = -6308711091434946173L; private double fd[]; private double fdStored[]; public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } - + @Override public void init() { super.init(); diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java index b2fd4e32..038bc2e2 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java @@ -4,6 +4,8 @@ public class FluxesAndImplicitDerivatives extends Fluxes { + private static final long serialVersionUID = -4161296401342482405L; + public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java index 71da5f96..f761f7c4 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java @@ -1,11 +1,13 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** * Used to listed to status updates in {@code RadiativeTransferSolver} * subclasses. * */ -public interface RTECalculationListener { +public interface RTECalculationListener extends Serializable { /** * Invoked when a sub-step of the RTE solution has finished. diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index f579004d..83054ca1 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -22,11 +22,9 @@ public enum RTECalculationStatus { * The grid density required to reach the error threshold was too large. */ GRID_TOO_LARGE, - /** * The radiative fluxes contain illegal values. */ - INVALID_FLUXES; - + } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java index 9c557e3d..5d86e8f0 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import pulse.math.linear.Matrices; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; @@ -10,8 +11,9 @@ * Variable names correspond to the standard notations. * */ -public class ButcherTableau implements Descriptive { +public class ButcherTableau implements Descriptive, Serializable { + private static final long serialVersionUID = -8856270519744473886L; private Vector b; private Vector bHat; private Vector c; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java index a242ef40..4e3de939 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import pulse.math.LegendrePoly; import pulse.math.MathUtils; @@ -10,7 +11,9 @@ * @author Teymur Aliev, Vadim Zborovskii, Artem Lunev * */ -public class CompositeGaussianQuadrature { +public class CompositeGaussianQuadrature implements Serializable { + + private static final long serialVersionUID = 780827333372523309L; private LegendrePoly poly; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java index f2a695e6..5dfccb4d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java @@ -6,14 +6,16 @@ import pulse.problem.statements.model.ThermoOpticalProperties; /** - * The single-parameter Cornette-Schanks scattering phase function. - * It converges to the Rayleigh phase function as 〈μ〉 → 0 and approaches - * the Henyey–Greenstein phase function as |〈μ〉| → 1 + * The single-parameter Cornette-Schanks scattering phase function. It converges + * to the Rayleigh phase function as 〈μ〉 → 0 and approaches the + * Henyey–Greenstein phase function as |〈μ〉| → 1 + * * @see https://doi.org/10.1364/ao.31.003152 * */ public class CornetteSchanksPF extends PhaseFunction { + private static final long serialVersionUID = -4371291780762389604L; private double anisoFactor; private double onePlusGSq; private double g2; @@ -29,14 +31,14 @@ public void init(ThermoOpticalProperties top) { g2 = 2.0 * anisotropy; final double aSq = anisotropy * anisotropy; onePlusGSq = 1.0 + aSq; - anisoFactor = 1.5*(1.0 - aSq)/(2.0 + aSq); + anisoFactor = 1.5 * (1.0 - aSq) / (2.0 + aSq); } @Override public double function(final int i, final int k) { - double cosine = cosineTheta(i,k); + double cosine = cosineTheta(i, k); final double f = onePlusGSq - g2 * cosine; - return anisoFactor * (1.0 + cosine*cosine) / (f * sqrt(f)); + return anisoFactor * (1.0 + cosine * cosine) / (f * sqrt(f)); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index af59449a..0e3e5d47 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -23,6 +23,7 @@ */ public class DiscreteOrdinatesMethod extends RadiativeTransferSolver { + private static final long serialVersionUID = 2881363894773388976L; private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( "Integrator selector", AdaptiveIntegrator.class); private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( @@ -55,7 +56,6 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); - phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); initPhaseFunction(properties, discrete); init(problem, grid); @@ -66,7 +66,8 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { iterativeSolverSelector .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); - + + phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); } @Override @@ -78,7 +79,7 @@ public RTECalculationStatus compute(double[] tempArray) { if (status == RTECalculationStatus.NORMAL) { fluxesAndDerivatives(tempArray.length); } - + fireStatusUpdate(status); return status; } @@ -94,10 +95,10 @@ private void fluxesAndDerivatives(final int nExclusive) { for (int i = 0; i < nExclusive; i++) { double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); fluxes.setFlux(i, flux); - fluxes.setFluxDerivative(i, + fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); } - + } @Override @@ -108,8 +109,8 @@ public String getDescriptor() { @Override public void init(ParticipatingMedium problem, Grid grid) { super.init(problem, grid); - var top = (ThermoOpticalProperties)problem.getProperties(); - initPhaseFunction(top, + var top = (ThermoOpticalProperties) problem.getProperties(); + initPhaseFunction(top, integrator.getDiscretisation()); integrator.init(problem); integrator.getPhaseFunction().init(top); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index 846dc1bc..e81389a9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -1,12 +1,15 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; + /** * Defines the main quantities calculated within the discrete ordinates method. * This includes the various intensity and flux arrays used internally by the * integrators. */ -public class DiscreteQuantities { +public class DiscreteQuantities implements Serializable { + private static final long serialVersionUID = -3997479317699236996L; private double[][] I; private double[][] Ik; private double[][] f; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index 932d7d69..91be63d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -23,6 +23,7 @@ */ public class Discretisation extends PropertyHolder { + private static final long serialVersionUID = 7069987686586911101L; private DiscreteQuantities quantities; private StretchedGrid grid; private OrdinateSet ordinates; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java index 648096ec..42b98b9a 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java @@ -16,6 +16,7 @@ */ public class ExplicitRungeKutta extends AdaptiveIntegrator { + private static final long serialVersionUID = -2478658861611086402L; private ButcherTableau tableau; private DiscreteSelector tableauSelector; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java index 08cea6dc..2a9ea834 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java @@ -6,6 +6,8 @@ public class FixedIterations extends IterativeSolver { + private static final long serialVersionUID = -7308041206602757928L; + @Override public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java index f1e0d617..12af9709 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java @@ -2,7 +2,6 @@ import static java.lang.Math.sqrt; -import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.model.ThermoOpticalProperties; /** @@ -11,6 +10,7 @@ */ public class HenyeyGreensteinPF extends PhaseFunction { + private static final long serialVersionUID = -2196189314681832809L; private double a1; private double a2; private double b1; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java index 1157850a..93c4131d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java @@ -1,5 +1,7 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; + /** * A globally C1 Hermite interpolator used to interpolate intensities * and derivatives in discrete ordinates method when solving the radiative @@ -8,8 +10,9 @@ * @author Vadim Zborovskii, Artem Lunev * */ -public class HermiteInterpolator { +public class HermiteInterpolator implements Serializable { + private static final long serialVersionUID = -1973954803574711053L; protected double y1; protected double y0; protected double d1; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java index 7c382c65..4693eeb4 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java @@ -9,6 +9,7 @@ */ public class LinearAnisotropicPF extends PhaseFunction { + private static final long serialVersionUID = 7074989018933263351L; private double g; public LinearAnisotropicPF(ThermoOpticalProperties top, Discretisation intensities) { @@ -29,7 +30,7 @@ public double partialSum(final int i, final int j, final int n1, final int n2Exc @Override public double function(final int i, final int k) { - return 1.0 + g * cosineTheta(i,k); + return 1.0 + g * cosineTheta(i, k); } } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java index 1785a2a7..3dc388f9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -21,9 +21,9 @@ public ODEIntegrator(Discretisation intensities) { protected void init(NonlinearProblem problem) { extract((ThermoOpticalProperties) problem.getProperties()); - setEmissionFunction( new BlackbodySpectrum(problem) ); + setEmissionFunction(new BlackbodySpectrum(problem)); } - + protected void extract(ThermoOpticalProperties properties) { discretisation.setEmissivity((double) properties.getEmissivity().getValue()); discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java index 68662f6f..fc09890d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import static pulse.math.MathUtils.approximatelyEquals; import java.util.Arrays; @@ -11,8 +12,9 @@ * discretisation of a radiative transfer equation. * */ -public class OrdinateSet implements Descriptive { +public class OrdinateSet implements Descriptive, Serializable { + private static final long serialVersionUID = 4850346144315192409L; private double[] mu; private double[] w; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index e8123f8f..21374fb6 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -1,10 +1,10 @@ package pulse.problem.schemes.rte.dom; -import pulse.problem.statements.ParticipatingMedium; +import java.io.Serializable; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Reflexive; -public abstract class PhaseFunction implements Reflexive { +public abstract class PhaseFunction implements Reflexive, Serializable { private final Discretisation intensities; private double anisotropy; @@ -14,15 +14,15 @@ public PhaseFunction(ThermoOpticalProperties top, Discretisation intensities) { this.intensities = intensities; init(top); } - - /** - * Calculates the cosine of the scattering angle as the product - * of the two discrete cosine nodes. + + /** + * Calculates the cosine of the scattering angle as the product of the two + * discrete cosine nodes. + * * @param i * @param k - * @return + * @return */ - public final double cosineTheta(int i, int k) { final var ordinates = getDiscreteIntensities().getOrdinates(); return ordinates.getNode(k) * ordinates.getNode(i); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java index 335d1ca1..012eb5d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java @@ -5,18 +5,16 @@ import static pulse.properties.NumericPropertyKeyword.DOM_GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.GRID_STRETCHING_FACTOR; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; import pulse.util.PropertyHolder; public class StretchedGrid extends PropertyHolder { + private static final long serialVersionUID = -7987714138817824037L; + private double[] nodes; private double stretchingFactor; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java index a051ce9b..1c44c065 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java @@ -5,17 +5,15 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.RELAXATION_PARAMETER; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; public class SuccessiveOverrelaxation extends IterativeSolver { + private static final long serialVersionUID = 1135563981945852881L; private double W; public SuccessiveOverrelaxation() { diff --git a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java index fa1bc4f2..5ce22402 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java @@ -14,6 +14,7 @@ */ public class TRBDF2 extends AdaptiveIntegrator { + private static final long serialVersionUID = 5488454845395333565L; /* * Coefficients of the Butcher tableau as originally defined in M.E. Hosea, L.E * Shampine/Applied Numerical Mathematics 20 (1996) 21-37 diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java index 208ab29b..535a603e 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java @@ -11,9 +11,7 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.stream.IntStream; @@ -25,8 +23,6 @@ import pulse.math.linear.Vector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; /** * This quadrature methods of evaluating the composition product of the @@ -39,10 +35,11 @@ */ public class ChandrasekharsQuadrature extends CompositionProduct { + private static final long serialVersionUID = 3282258803373408111L; private int m; private double expLower; private double expUpper; - private LaguerreSolver solver; + private transient LaguerreSolver solver; private double[] moments; /** @@ -127,8 +124,8 @@ private SquareMatrix xMatrix(final double[] roots) { /** * Calculates \int_{r_{min}}^{r_{max}}{x^{l+1}exp(-x)dx}. * - * @param l an integer such that 0 <= l <= 2*m - 1. - * @return the value of this definite integral. + * @param l an integer such that 0 <= l <= 2*m - 1. @re + * turn the value of this definite integral. */ private static double auxilliaryIntegral(final double x, final int lPlusN, final double exp) { @@ -261,6 +258,9 @@ private double[] roots() { break; default: // use LaguerreSolver + if (solver == null) { + solver = new LaguerreSolver(); + } roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); } diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java index 340b5f2f..b3833ac3 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java @@ -16,6 +16,7 @@ */ class ExponentialIntegral extends MidpointIntegrator { + private static final long serialVersionUID = -5818633555456309668L; private double t; private int order; diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java index f2f23282..546bb2cf 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java @@ -2,13 +2,11 @@ import static java.lang.Math.max; import static java.lang.Math.min; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_CUTOFF; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import java.util.List; import java.util.Set; import pulse.math.FixedIntervalIntegrator; @@ -16,8 +14,6 @@ import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; /** * A class for evaluating the composition product using a simple Newton-Cotes @@ -26,6 +22,7 @@ */ public class NewtonCotesQuadrature extends CompositionProduct { + private static final long serialVersionUID = -177127670003926420L; private final static int DEFAULT_SEGMENTS = 64; private final static double DEFAULT_CUTOFF = 16.0; private FixedIntervalIntegrator integrator; diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java index bbcf0d3a..dbc23a8c 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -20,6 +20,7 @@ */ public class NonscatteringAnalyticalDerivatives extends NonscatteringRadiativeTransfer { + private static final long serialVersionUID = -7549047672012708753L; private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { @@ -31,8 +32,9 @@ public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid /** * Evaluates fluxes and their derivatives using analytical formulae and the * selected numerical quadrature.Usually works best with the - {@code ChandrasekharsQuadrature} - * @return + * {@code ChandrasekharsQuadrature} + * + * @return */ @Override public RTECalculationStatus compute(double U[]) { diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java index b620d91b..c781488e 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java @@ -14,6 +14,8 @@ */ public class NonscatteringDiscreteDerivatives extends NonscatteringRadiativeTransfer { + private static final long serialVersionUID = -6919734351838124553L; + public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { super(problem, grid); var properties = (ThermoOpticalProperties) problem.getProperties(); diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java index a4583b5b..3afd8920 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -17,6 +17,8 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSolver { + private static final long serialVersionUID = 4934841542530728191L; + private static final FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); private double emissivity; @@ -27,9 +29,9 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSo private double radiosityFront; private double radiosityRear; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Quadrature Selector", CompositionProduct.class); + "Quadrature Selector", CompositionProduct.class); protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { super(); @@ -50,6 +52,7 @@ public void init(ParticipatingMedium p, Grid grid) { * The superclass method will update the interpolation that the blackbody * spectrum uses to evaluate the temperature profile and calculate the * radiosities.A {@code NORMAL}status is always returned. + * * @param array */ @Override @@ -182,7 +185,7 @@ public double getRadiosityRear() { */ private void radiosities() { final double doubleReflectivity = 2.0 * (1.0 - emissivity); - + final double b = b(doubleReflectivity); final double sq = 1.0 - b * b; final double a1 = a1(doubleReflectivity); diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index d2472df5..27196d50 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -17,6 +17,8 @@ */ public class ADILinearisedSolver extends ADIScheme implements Solver { + private static final long serialVersionUID = 5354981341912770336L; + private TridiagonalMatrixAlgorithm tridiagonal; private int N; @@ -77,12 +79,12 @@ public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { super(N, timeFactor, timeLimit); } - + @Override public void clearArrays() { - N = (int) getGrid().getGridDensity().getValue(); - U1 = new double[N + 1][N + 1]; - U2 = new double[N + 1][N + 1]; + N = (int) getGrid().getGridDensity().getValue(); + U1 = new double[N + 1][N + 1]; + U2 = new double[N + 1][N + 1]; U1_E = new double[N + 3][N + 3]; U2_E = new double[N + 3][N + 3]; @@ -117,7 +119,6 @@ public void prepare(Problem problem) throws SolverException { l = (double) properties.getSampleThickness().getValue(); // end - // a[i]*u[i-1] - b[i]*u[i] + c[i]*u[i+1] = F[i] lastIndex = (int) (fovOuter / d / hx); lastIndex = lastIndex > N ? N : lastIndex; diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 39e504c1..5b706a42 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -32,7 +32,7 @@ public abstract class ExplicitCoupledSolver extends ExplicitScheme private double zeta; private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers - + public ExplicitCoupledSolver() { this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); } @@ -49,7 +49,7 @@ public void prepare(Problem problem) throws SolverException { var grid = getGrid(); - coupling.init((ParticipatingMedium)problem, grid); + coupling.init((ParticipatingMedium) problem, grid); fluxes = coupling.getRadiativeTransferEquation().getFluxes(); setCalculationStatus(fluxes.checkArrays()); @@ -69,7 +69,7 @@ public void prepare(Problem problem) throws SolverException { HX_NP = hx / Np; prefactor = tau * opticalThickness / Np; - zeta = (double) ((ParticipatingMedium)problem).getGeometricFactor().getValue(); + zeta = (double) ((ParticipatingMedium) problem).getGeometricFactor().getValue(); } @Override @@ -139,7 +139,7 @@ public final void setCoupling(RadiativeTransferCoupling coupling) { public String toString() { return getString("ExplicitScheme.4"); } - + @Override public Class[] domain() { return new Class[]{ParticipatingMedium.class}; @@ -148,17 +148,17 @@ public Class[] domain() { public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString(), + throw new SolverException(status.toString(), RTE_SOLVER_ERROR); } } - + public final boolean isAutoUpdateFluxes() { return this.autoUpdateFluxes; } - + public final void setAutoUpdateFluxes(boolean auto) { this.autoUpdateFluxes = auto; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java index ab30eeba..6d40c3b8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java @@ -28,24 +28,24 @@ * * @author Artem Lunev */ -public class ExplicitCoupledSolverNL extends ExplicitCoupledSolver - implements FixedPointIterations -{ +public class ExplicitCoupledSolverNL extends ExplicitCoupledSolver + implements FixedPointIterations { + private static final long serialVersionUID = 4214528397984492532L; private double nonlinearPrecision; - + public ExplicitCoupledSolverNL() { super(); nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + public ExplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor) { super(N, timeFactor); nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -61,13 +61,13 @@ public void finaliseIteration(double[] V) throws SolverException { FixedPointIterations.super.finaliseIteration(V); setCalculationStatus(this.getCoupling().getRadiativeTransferEquation().compute(V)); } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new ExplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor()); } - + public final NumericProperty getNonlinearPrecision() { return derive(NONLINEAR_PRECISION, nonlinearPrecision); } @@ -84,10 +84,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public String toString() { return Messages.getString("ExplicitScheme.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index 7b7f0aeb..cd667beb 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -46,6 +46,7 @@ */ public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { + private static final long serialVersionUID = 3084350485569519036L; private int N; private double hx; private double a; @@ -66,8 +67,8 @@ public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, N @Override public void prepare(Problem problem) throws SolverException { super.prepare(problem); - - zeta = (double) ( (ClassicalProblem) problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); N = (int) getGrid().getGridDensity().getValue(); hx = getGrid().getXStep(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java index ed8e7fa3..546abc03 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java @@ -19,6 +19,7 @@ public class ExplicitNonlinearSolver extends ExplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = -5392648684733420360L; private int N; private double hx; diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index 56e747c8..ac48b368 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -13,6 +13,7 @@ public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver { + private static final long serialVersionUID = -3693226611473383024L; private int N; private double hx; private double tau; @@ -33,7 +34,7 @@ public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); - model = ((PenetrationProblem)problem).getAbsorptionModel(); + model = ((PenetrationProblem) problem).getAbsorptionModel(); N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index e9e4f603..a42bac2c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -17,7 +17,7 @@ import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme +public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { private RadiativeTransferSolver rte; @@ -50,7 +50,7 @@ public void prepare(Problem problem) throws SolverException { final var grid = getGrid(); var coupling = getCoupling(); - coupling.init( (ParticipatingMedium) problem, grid); + coupling.init((ParticipatingMedium) problem, grid); rte = coupling.getRadiativeTransferEquation(); N = (int) getGrid().getGridDensity().getValue(); @@ -72,7 +72,7 @@ public void prepare(Problem problem) throws SolverException { v1 = 1.0 + HX2_2TAU + hx * Bi1; fluxes = rte.getFluxes(); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -91,8 +91,8 @@ public double phi(int i) { tridiagonal.evaluateAlpha(); setTridiagonalMatrixAlgorithm(tridiagonal); - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); } @Override @@ -104,7 +104,7 @@ public void solve(ParticipatingMedium problem) throws SolverException { var status = getCalculationStatus(); if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString(), + throw new SolverException(status.toString(), RTE_SOLVER_ERROR); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java index 389d148a..af0d6e11 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java @@ -30,6 +30,7 @@ */ public class ImplicitCoupledSolverNL extends ImplicitCoupledSolver implements FixedPointIterations { + private static final long serialVersionUID = -3993380888844448942L; private double nonlinearPrecision; public ImplicitCoupledSolverNL() { @@ -43,7 +44,7 @@ public ImplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, Nu nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(final int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -77,16 +78,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new ImplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } - + @Override public String toString() { return Messages.getString("ImplicitScheme.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 5343958c..587271b8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -18,7 +18,7 @@ public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver[] domain() { return new Class[]{DiathermicMedium.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 16a80d11..62b5709a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -45,16 +45,18 @@ * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ -public class ImplicitLinearisedSolver extends ImplicitScheme +public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { + private static final long serialVersionUID = -5182202341972279175L; + private int N; - + protected double Bi1HTAU; protected double tau; protected double HH; protected double _2HTAU; - + private double zeta; public ImplicitLinearisedSolver() { @@ -78,8 +80,8 @@ public void prepare(Problem problem) throws SolverException { N = (int) grid.getGridDensity().getValue(); final double hx = grid.getXStep(); tau = grid.getTimeStep(); - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); @@ -114,7 +116,7 @@ public double firstBeta() { @Override public double evalRightBoundary(final double alphaN, final double betaN) { - return (HH * getPreviousSolution()[N] + 2. * tau * betaN + return (HH * getPreviousSolution()[N] + 2. * tau * betaN + _2HTAU * (1.0 - zeta) * getCurrentPulseValue() //additional term due to stray light ) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); } @@ -130,4 +132,4 @@ public Class[] domain() { return new Class[]{ClassicalProblem.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java index 0cc048ac..20224b8a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java @@ -17,6 +17,7 @@ public class ImplicitNonlinearSolver extends ImplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = -6263519219698662707L; private int N; private double HH; private double tau; @@ -139,7 +140,7 @@ public void iteration(int m) throws SolverException { @Override public double evalRightBoundary(double alphaN, double betaN) { - return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index d5005bae..7ace1f8d 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -14,6 +14,7 @@ public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { + private static final long serialVersionUID = -2207434474904484692L; private AbsorptionModel absorption; private int N; @@ -40,12 +41,12 @@ public void prepare(Problem problem) throws SolverException { final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); final double hx = grid.getXStep(); - absorption = ((PenetrationProblem)problem).getAbsorptionModel(); - + absorption = ((PenetrationProblem) problem).getAbsorptionModel(); + HH = hx * hx; _2Bi1HTAU = 2.0 * Bi1H * tau; b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -54,7 +55,7 @@ public double phi(final int i) { } }; - + // coefficients for difference equation tridiagonal.setCoefA(1. / HH); tridiagonal.setCoefB(1. / tau + 2. / HH); @@ -81,7 +82,7 @@ public double evalRightBoundary(final double alphaN, final double betaN) { final double tau = getGrid().getTimeStep(); var tridiagonal = this.getTridiagonalMatrixAlgorithm(); - return (HH * getPreviousSolution()[N] + HH*tau*tridiagonal.phi(N) + return (HH * getPreviousSolution()[N] + HH * tau * tridiagonal.phi(N) + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); } @@ -89,7 +90,7 @@ public double evalRightBoundary(final double alphaN, final double betaN) { public double firstBeta() { var tridiagonal = this.getTridiagonalMatrixAlgorithm(); double tau = getGrid().getTimeStep(); - return (getPreviousSolution()[0] + tau*tridiagonal.phi(0))* b11; + return (getPreviousSolution()[0] + tau * tridiagonal.phi(0)) * b11; } @Override @@ -113,4 +114,4 @@ public Class[] domain() { return new Class[]{PenetrationProblem.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java index 1a09949e..b31f7fd2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -22,6 +22,7 @@ public class ImplicitTwoTemperatureSolver extends ImplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = 7955478815933535623L; private AbsorptionModel absorption; private TridiagonalMatrixAlgorithm gasSolver; diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index fc3e5167..4ba9ea26 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -19,7 +19,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -public abstract class MixedCoupledSolver extends CoupledImplicitScheme +public abstract class MixedCoupledSolver extends CoupledImplicitScheme implements Solver { private RadiativeTransferSolver rte; @@ -74,13 +74,13 @@ public void prepare(Problem problem) throws SolverException { hx = grid.getXStep(); tau = grid.getTimeStep(); - var properties = (ThermoOpticalProperties)problem.getProperties(); + var properties = (ThermoOpticalProperties) problem.getProperties(); //combined biot - Bi1 = (double) properties.getHeatLoss().getValue() + - (double) properties.getConvectiveLosses().getValue(); + Bi1 = (double) properties.getHeatLoss().getValue() + + (double) properties.getConvectiveLosses().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); - zeta = (double) ( (ClassicalProblem)problem ).getGeometricFactor().getValue(); - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -95,7 +95,7 @@ public double beta(final double f, final double phi, final int i) { var U = getPreviousSolution(); return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); } - + @Override public void evaluateBeta(final double[] U) { var fluxes = rte.getFluxes(); @@ -168,10 +168,10 @@ public double firstBeta() { var U = getPreviousSolution(); final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); return (_2TAUHX - * (getCurrentPulseValue() * zeta - SIGMA_NP * fluxes.getFlux(0) + * (getCurrentPulseValue() * zeta - SIGMA_NP * fluxes.getFlux(0) - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) - + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * - (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; + + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA + * (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; } @Override @@ -215,4 +215,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java index 77d7b192..94ad40d9 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java @@ -30,6 +30,7 @@ */ public class MixedCoupledSolverNL extends MixedCoupledSolver implements FixedPointIterations { + private static final long serialVersionUID = -8344384560376683594L; private double nonlinearPrecision; public MixedCoupledSolverNL() { @@ -43,7 +44,7 @@ public MixedCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, Numer nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(final int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -77,16 +78,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new MixedCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } - + @Override public String toString() { return Messages.getString("MixedScheme2.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 56a74f0a..e2fc25c2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -50,12 +50,13 @@ */ public class MixedLinearisedSolver extends MixedScheme implements Solver { + private static final long serialVersionUID = 2233988060956648641L; private double b1; private double b2; private double b3; private double c1; private double c2; - + private double zeta; private final static double EPS = 1e-7; // a small value ensuring numeric stability @@ -93,8 +94,8 @@ public void prepare(Problem problem) throws SolverException { b3 = hx * tau; c1 = b2; c2 = Bi1HTAU + HH; - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @@ -128,15 +129,15 @@ public double evalRightBoundary(final double alphaN, final double betaN) { final double tau = grid.getTimeStep(); final int N = (int) grid.getGridDensity().getValue(); - return ( c1 * U[N] + tau * betaN + b3 * (1.0 - zeta) * getCurrentPulseValue() - - tau * (U[N] - U[N - 1]) ) / (c2 - tau * (alphaN - 1)); + return (c1 * U[N] + tau * betaN + b3 * (1.0 - zeta) * getCurrentPulseValue() + - tau * (U[N] - U[N - 1])) / (c2 - tau * (alphaN - 1)); } - + @Override public double pulse(int m) { final double tau = getGrid().getTimeStep(); var pulse = getDiscretePulse(); - return pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); + return pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/Solver.java b/src/main/java/pulse/problem/schemes/solvers/Solver.java index f371b189..654f8970 100644 --- a/src/main/java/pulse/problem/schemes/solvers/Solver.java +++ b/src/main/java/pulse/problem/schemes/solvers/Solver.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.solvers; +import java.io.Serializable; import pulse.problem.statements.Problem; /** @@ -9,7 +10,7 @@ * * @param an instance of Problem */ -public interface Solver { +public interface Solver extends Serializable { /** * Calculates the solution of the {@code t} and stores it in the respective diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 8aacbe04..ef97e328 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -2,22 +2,22 @@ @SuppressWarnings("serial") public class SolverException extends Exception { - + private final SolverExceptionType type; public SolverException(String status, SolverExceptionType type) { super(status); this.type = type; } - + public SolverException(SolverExceptionType type) { this(type.toString(), type); } - + public SolverExceptionType getType() { return type; } - + public enum SolverExceptionType { RTE_SOLVER_ERROR, OPTIMISATION_ERROR, @@ -26,4 +26,4 @@ public enum SolverExceptionType { ILLEGAL_PARAMETERS, } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index 5fbb5a84..84293749 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -1,5 +1,6 @@ package pulse.problem.statements; +import java.io.Serializable; import static java.lang.Math.PI; import static java.lang.Math.exp; import static java.lang.Math.pow; @@ -9,8 +10,9 @@ import pulse.HeatingCurve; import pulse.problem.statements.model.ThermalProperties; -public class AdiabaticSolution { +public class AdiabaticSolution implements Serializable { + private static final long serialVersionUID = 4240406501288696621L; public final static int DEFAULT_CLASSIC_PRECISION = 200; public final static int DEFAULT_POINTS = 100; diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 72439856..2f5fd36a 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -24,6 +24,10 @@ */ public class ClassicalProblem extends Problem { + /** + * + */ + private static final long serialVersionUID = -1915004757733565502L; private double bias; public ClassicalProblem() { diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 9960a8bc..3ea99b1d 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -30,6 +30,11 @@ */ public class ClassicalProblem2D extends ClassicalProblem { + /** + * + */ + private static final long serialVersionUID = 8974995052071820422L; + public ClassicalProblem2D() { super(); setPulse(new Pulse2D()); @@ -66,7 +71,7 @@ public String toString() { public DiscretePulse discretePulseOn(Grid grid) { return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); } - + @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); @@ -78,7 +83,7 @@ public void optimisationVector(ParameterVector output) { var key = p.getIdentifier().getKeyword(); Transformable transform = new InvDiamTransform(properties); var bounds = Segment.boundsFrom(key); - + switch (key) { case FOV_OUTER: value = (double) properties.getFOVOuter().getValue(); @@ -127,7 +132,7 @@ public void assign(ParameterVector params) throws SolverException { properties.set(type, derive(type, p.inverseTransform())); break; case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, p.inverseTransform())); break; default: @@ -145,4 +150,4 @@ public Problem copy() { return new ClassicalProblem2D(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index dc4bf9b0..008a54ff 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -33,6 +33,8 @@ */ public class DiathermicMedium extends ClassicalProblem { + private static final long serialVersionUID = -98674255799114512L; + public DiathermicMedium() { super(); } @@ -69,18 +71,18 @@ public void optimisationVector(ParameterVector output) { break; case HEAT_LOSS_CONVECTIVE: bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); - value = (double) properties.getConvectiveLosses().getValue(); - break; + value = (double) properties.getConvectiveLosses().getValue(); + break; case HEAT_LOSS: - if(properties.areThermalPropertiesLoaded()) { - value = (double) properties.getHeatLoss().getValue(); - bounds = new Segment(0.0, properties.maxRadiationBiot() ); - break; - } + if (properties.areThermalPropertiesLoaded()) { + value = (double) properties.getHeatLoss().getValue(); + bounds = new Segment(0.0, properties.maxRadiationBiot()); + break; + } default: continue; } - + p.setTransform(new StickTransform(bounds)); p.setValue(value); p.setBounds(bounds); @@ -101,11 +103,11 @@ public void assign(ParameterVector params) throws SolverException { switch (key) { case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, p.inverseTransform())); break; case HEAT_LOSS_CONVECTIVE: - properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, p.inverseTransform())); break; default: @@ -130,4 +132,4 @@ public DiathermicMedium copy() { return new DiathermicMedium(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index f21b3b89..d3e01870 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -25,6 +25,11 @@ public class NonlinearProblem extends ClassicalProblem { + /** + * + */ + private static final long serialVersionUID = -5266939533182313886L; + public NonlinearProblem() { super(); setPulse(new Pulse2D()); @@ -68,14 +73,15 @@ public NumericProperty getThermalConductivity() { } /** - * - * Does the same as super-class method plus updates the laser energy, if needed. + * + * Does the same as super-class method plus updates the laser energy, if + * needed. + * * @param params * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.problem.statements.Problem.getPulse() - * - */ - + * + */ @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); @@ -92,29 +98,30 @@ public void assign(ParameterVector params) throws SolverException { } } - + /** - * - * Does the same as super-class method plus extracts the laser energy and stores it in the {@code output}, if needed. + * + * Does the same as super-class method plus extracts the laser energy and + * stores it in the {@code output}, if needed. + * * @param output * @param flags * @see pulse.problem.statements.Problem.getPulse() - * + * */ - @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); - + for (Parameter p : output.getParameters()) { var key = p.getIdentifier().getKeyword(); - if(key == LASER_ENERGY) { + if (key == LASER_ENERGY) { var bounds = Segment.boundsFrom(LASER_ENERGY); p.setBounds(bounds); p.setTransform(new StickTransform(bounds)); - p.setValue( (double) getPulse().getLaserEnergy().getValue()); + p.setValue((double) getPulse().getLaserEnergy().getValue()); } } @@ -131,4 +138,4 @@ public Problem copy() { return new NonlinearProblem(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index e8497e23..844c904d 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,7 +1,5 @@ package pulse.problem.statements; - -import java.util.List; import java.util.Set; import pulse.math.ParameterVector; @@ -10,13 +8,14 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; -import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { + private static final long serialVersionUID = -8227061869299826343L; + public ParticipatingMedium() { super(); setComplexity(ProblemComplexity.HIGH); @@ -73,4 +72,4 @@ public Problem copy() { return new ParticipatingMedium(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index d08d17cf..588e2773 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -17,6 +17,7 @@ public class PenetrationProblem extends ClassicalProblem { + private static final long serialVersionUID = -6760177658036060627L; private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); @@ -58,7 +59,7 @@ public List listedTypes() { list.add(instanceDescriptor); return list; } - + @Override public Set listedKeywords() { var set = super.listedKeywords(); @@ -97,4 +98,4 @@ public Problem copy() { return new PenetrationProblem(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index a9752e23..7392e27f 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,5 +1,6 @@ package pulse.problem.statements; +import java.io.Serializable; import java.util.Arrays; import static pulse.input.listeners.CurveEventType.RESCALED; import static pulse.properties.NumericProperties.derive; @@ -52,6 +53,10 @@ */ public abstract class Problem extends PropertyHolder implements Reflexive, Optimisable { + /** + * + */ + private static final long serialVersionUID = 7275327427201737684L; private ThermalProperties properties; private HeatingCurve curve; private Baseline baseline; @@ -174,7 +179,7 @@ public final void setPulse(Pulse pulse) { * @param c the {@code ExperimentalData} object */ public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); + baseline.fitTo(c); estimateSignalRange(c); updateProperties(this, c.getMetadata()); properties.useTheoreticalEstimates(c); @@ -423,9 +428,8 @@ private void initBaseline() { var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); if (searchTask != null) { var experimentalData = (ExperimentalData) searchTask.getInput(); - Executors.newSingleThreadExecutor().submit(() - -> baseline.fitTo(experimentalData) - ); + Runnable baselineFitter = (Runnable & Serializable) () -> baseline.fitTo(experimentalData); + Executors.newSingleThreadExecutor().submit(baselineFitter); } parameterListChanged(); } @@ -447,4 +451,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 2b5b988e..f177b20f 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -30,6 +30,10 @@ */ public class Pulse extends PropertyHolder { + /** + * + */ + private static final long serialVersionUID = 3564455042006771282L; private double pulseWidth; private double laserEnergy; @@ -50,11 +54,8 @@ public Pulse() { laserEnergy = (double) def(LASER_ENERGY).getValue(); instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); initShape(); - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - addListeners(); + addInstanceListener(); + initListeners(); } /** @@ -67,14 +68,20 @@ public Pulse(Pulse p) { this.pulseShape = p.getPulseShape(); this.pulseWidth = p.pulseWidth; this.laserEnergy = p.laserEnergy; - addListeners(); + addInstanceListener(); + initListeners(); } - private void addListeners() { + private void addInstanceListener() { instanceDescriptor.addListener(() -> { initShape(); this.firePropertyChanged(instanceDescriptor, instanceDescriptor); }); + } + + @Override + public void initListeners() { + super.initListeners(); addListener((PropertyEvent event) -> { //when a property of the pulse is changed @@ -91,16 +98,16 @@ private void addListeners() { NumericProperty pw = NumericProperties .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); - - var range = ( (ExperimentalData) corrTask.getInput() ).getRange(); - - if( range.getLowerBound().compareTo(pw) < 0 ) { - - //update lower bound of the range for that SearchTask - range.setLowerBound(pw); - + + var range = ((ExperimentalData) corrTask.getInput()).getRange(); + + if (range.getLowerBound().compareTo(pw) < 0) { + + //update lower bound of the range for that SearchTask + range.setLowerBound(pw); + } - + } } @@ -131,24 +138,24 @@ public void setPulseWidth(NumericProperty pulseWidth) { requireType(pulseWidth, PULSE_WIDTH); double newValue = (double) pulseWidth.getValue(); - + double relChange = Math.abs((newValue - this.pulseWidth) / (this.pulseWidth + newValue)); final double EPS = 1E-3; - + //do not update -- if new value is the same as the previous one if (relChange > EPS && newValue > 0) { - + //validate -- do not update if the new pulse width is greater than 2 half-times - SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); - ExperimentalData data = (ExperimentalData) task.getInput(); - - if(newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { + SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); + ExperimentalData data = (ExperimentalData) task.getInput(); + + if (newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { this.pulseWidth = (double) pulseWidth.getValue(); firePropertyChanged(this, pulseWidth); } - + } - + } public NumericProperty getLaserEnergy() { @@ -214,8 +221,8 @@ public PulseTemporalShape getPulseShape() { public void setPulseShape(PulseTemporalShape pulseShape) { this.pulseShape = pulseShape; - pulseShape.setParent(this); - + pulseShape.setParent(this); + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index 11d4bef3..7dd4c37c 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -12,6 +12,7 @@ public class Pulse2D extends Pulse { + private static final long serialVersionUID = 8753396877032808678L; private double spotDiameter; /** diff --git a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java index 72298d72..980814fd 100644 --- a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -23,11 +23,13 @@ public class TwoTemperatureModel extends PenetrationProblem { + private static final long serialVersionUID = 2567125396986165234L; + private Gas gas; private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>("Gas Selector", Gas.class); - + public TwoTemperatureModel() { super(); setComplexity(ProblemComplexity.MODERATE); @@ -92,7 +94,7 @@ public void optimisationVector(ParameterVector output) { continue; } - p.setTransform(new StickTransform(bounds)); + p.setTransform(new StickTransform(bounds)); p.setValue(value); p.setBounds(bounds); @@ -109,7 +111,7 @@ public void assign(ParameterVector params) throws SolverException { var key = p.getIdentifier().getKeyword(); var np = derive(key, p.inverseTransform()); - + switch (key) { case SOLID_EXCHANGE_COEFFICIENT: ttp.setSolidExchangeCoefficient(np); @@ -124,7 +126,7 @@ public void assign(ParameterVector params) throws SolverException { } } - + } @Override diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index aacbd12f..9e895e49 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -25,15 +25,16 @@ public abstract class AbsorptionModel extends PropertyHolder implements Reflexive, Optimisable { + private static final long serialVersionUID = -8937860285061335131L; private Map absorptionMap; - + protected AbsorptionModel() { setPrefix("Absorption model"); absorptionMap = new HashMap<>(); absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); } - + protected AbsorptionModel(AbsorptionModel c) { this.absorptionMap = new HashMap<>(); this.absorptionMap.putAll(c.absorptionMap); @@ -109,7 +110,7 @@ public Set listedKeywords() { set.add(COMBINED_ABSORPTIVITY); return set; } - + @Override public void optimisationVector(ParameterVector output) { for (Parameter p : output.getParameters()) { @@ -117,7 +118,7 @@ public void optimisationVector(ParameterVector output) { double value = 0; Transformable transform = ABS; - + switch (key) { case LASER_ABSORPTIVITY: value = (double) (getLaserAbsorptivity()).getValue(); @@ -161,7 +162,7 @@ public void assign(ParameterVector params) throws SolverException { } } - + public abstract AbsorptionModel copy(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 3aebab03..6e3a956d 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,14 +2,16 @@ public class BeerLambertAbsorption extends AbsorptionModel { + private static final long serialVersionUID = -7996852815508481089L; + public BeerLambertAbsorption() { super(); } - + public BeerLambertAbsorption(AbsorptionModel m) { super(m); } - + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java index bc0a9a56..09f6d17d 100644 --- a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -11,6 +11,7 @@ public class DiathermicProperties extends ThermalProperties { + private static final long serialVersionUID = 1294930368429607512L; private double diathermicCoefficient; private double convectiveLosses; @@ -42,7 +43,7 @@ public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); } - + public NumericProperty getConvectiveLosses() { return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); } @@ -76,4 +77,4 @@ public Set listedKeywords() { return set; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index 4ff6ff7a..fc720e0a 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -19,6 +19,7 @@ public class ExtendedThermalProperties extends ThermalProperties { + private static final long serialVersionUID = 452882822074681811L; private double d; private double Bi3; private double fovOuter; diff --git a/src/main/java/pulse/problem/statements/model/Gas.java b/src/main/java/pulse/problem/statements/model/Gas.java index 7442ebd8..ad050e36 100644 --- a/src/main/java/pulse/problem/statements/model/Gas.java +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -1,69 +1,69 @@ package pulse.problem.statements.model; +import java.io.Serializable; import pulse.util.Descriptive; import pulse.util.Reflexive; -public abstract class Gas implements Reflexive, Descriptive { - +public abstract class Gas implements Reflexive, Descriptive, Serializable { + private double conductivity; private double thermalMass; private final int atoms; private final double mass; - + /** * Universal gas constant. */ - public final static double R = 8.314; //J/K/mol - + private final static double ROOM_TEMPERATURE = 300; private final static double NORMAL_PRESSURE = 1E5; - + public Gas(int atoms, double atomicWeight) { evaluate(ROOM_TEMPERATURE, NORMAL_PRESSURE); this.atoms = atoms; - this.mass = atoms * atomicWeight/1e3; + this.mass = atoms * atomicWeight / 1e3; } - + public final void evaluate(double temperature, double pressure) { this.conductivity = thermalConductivity(temperature); this.thermalMass = cp() * density(temperature, pressure); } - + public final void evaluate(double temperature) { evaluate(temperature, NORMAL_PRESSURE); } public final double thermalDiffusivity() { - return conductivity/thermalMass; + return conductivity / thermalMass; } - + public abstract double thermalConductivity(double t); - + public double cp() { return (1.5 + atoms) * R / mass; } - + public double density(double temperature, double pressure) { return pressure * mass / (R * temperature); } - + public double getThermalMass() { return thermalMass; } - + public double getConductivity() { return conductivity; - } - + } + public double getNumberOfAtoms() { return atoms; } - + public double getMolarMass() { return mass; } - + @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); @@ -71,5 +71,5 @@ public String toString() { sb.append(String.format("atoms per molecule = %d; atomic weight = %1.4f", atoms, mass)); return sb.toString(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/Helium.java b/src/main/java/pulse/problem/statements/model/Helium.java index 96b71f0e..206a2bf7 100644 --- a/src/main/java/pulse/problem/statements/model/Helium.java +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -1,7 +1,9 @@ package pulse.problem.statements.model; public class Helium extends Gas { - + + private static final long serialVersionUID = -4697854426333597653L; + public Helium() { super(1, 4); } @@ -10,5 +12,5 @@ public Helium() { public double thermalConductivity(double t) { return 0.415 + 0.283E-3 * (t - 1200); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 88c5972e..25137f72 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -11,16 +11,17 @@ public class Insulator extends AbsorptionModel { + private static final long serialVersionUID = -3519941060924868530L; private double R; public Insulator() { super(); R = (double) def(REFLECTANCE).getValue(); } - + public Insulator(AbsorptionModel m) { super(m); - if(m instanceof Insulator) { + if (m instanceof Insulator) { R = (double) ((Insulator) m).getReflectance().getValue(); } else { R = (double) def(REFLECTANCE).getValue(); diff --git a/src/main/java/pulse/problem/statements/model/Nitrogen.java b/src/main/java/pulse/problem/statements/model/Nitrogen.java index acaef03b..2b00b8ed 100644 --- a/src/main/java/pulse/problem/statements/model/Nitrogen.java +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -1,14 +1,16 @@ package pulse.problem.statements.model; public class Nitrogen extends Gas { - + + private static final long serialVersionUID = -8593450360265855427L; + public Nitrogen() { super(2, 14); } @Override public double thermalConductivity(double t) { - return Math.sqrt(t) * (-92.39/t + 1.647 + 5.255E-4*t) * 1E-3; + return Math.sqrt(t) * (-92.39 / t + 1.647 + 5.255E-4 * t) * 1E-3; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 295f6b86..5e2f135a 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -2,8 +2,6 @@ import static java.lang.Math.PI; import java.util.List; -import static pulse.input.InterpolationDataset.getDataset; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -13,17 +11,18 @@ import java.util.stream.Collectors; import pulse.input.ExperimentalData; -import pulse.input.InterpolationDataset; -import pulse.input.InterpolationDataset.StandartType; +import pulse.input.listeners.ExternalDatasetListener; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; public class ThermalProperties extends PropertyHolder { + private static final long serialVersionUID = 1868313258119863995L; private double a; private double l; private double Bi; @@ -56,7 +55,6 @@ public ThermalProperties() { signalHeight = (double) def(MAXTEMP).getValue(); T = (double) def(TEST_TEMPERATURE).getValue(); emissivity = (double) def(EMISSIVITY).getValue(); - initListeners(); fill(); } @@ -68,7 +66,6 @@ public ThermalProperties(ThermalProperties p) { this.T = p.T; this.signalHeight = p.signalHeight; this.emissivity = p.emissivity; - initListeners(); fill(); } @@ -78,17 +75,6 @@ public List findMalformedProperties() { return list; } - private void fill() { - var rhoCurve = getDataset(StandartType.DENSITY); - var cpCurve = getDataset(StandartType.HEAT_CAPACITY); - if (rhoCurve != null) { - rho = rhoCurve.interpolateAt(T); - } - if (cpCurve != null) { - cP = cpCurve.interpolateAt(T); - } - } - /** * Calculates some or all of the following properties: * Cp, ρ, &labmda;, @@ -99,21 +85,31 @@ private void fill() { * {@code TaskManager}. *

*/ - private void initListeners() { + + private void fill() { + var i = TaskManager.getManagerInstance(); + var rhoCurve = i.getDensityDataset(); + var cpCurve = i.getSpecificHeatDataset(); + if (rhoCurve != null) { + rho = rhoCurve.interpolateAt(T); + } + if (cpCurve != null) { + cP = cpCurve.interpolateAt(T); + } - InterpolationDataset.addListener(e -> { - if (getParent() == null) { - return; + i.addExternalDatasetListener(new ExternalDatasetListener() { + @Override + public void onSpecificHeatDataLoaded() { + cP = i.getSpecificHeatDataset().interpolateAt(T); } - if (e == StandartType.DENSITY) { - rho = getDataset(StandartType.DENSITY).interpolateAt(T); - } else if (e == StandartType.HEAT_CAPACITY) { - cP = getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); + @Override + public void onDensityDataLoaded() { + rho = i.getDensityDataset().interpolateAt(T); } - + }); - + } public ThermalProperties copy() { @@ -170,7 +166,7 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { public void setHeatLoss(NumericProperty Bi) { requireType(Bi, HEAT_LOSS); this.Bi = (double) Bi.getValue(); - if(areThermalPropertiesLoaded()) { + if (areThermalPropertiesLoaded()) { calculateEmissivity(); } firePropertyChanged(this, Bi); @@ -248,13 +244,14 @@ public void setTestTemperature(NumericProperty T) { requireType(T, TEST_TEMPERATURE); this.T = (double) T.getValue(); - var heatCapacity = getDataset(HEAT_CAPACITY); + var i = TaskManager.getManagerInstance(); + var heatCapacity = i.getSpecificHeatDataset(); if (heatCapacity != null) { cP = heatCapacity.interpolateAt(this.T); } - var density = getDataset(StandartType.DENSITY); + var density = i.getDensityDataset(); if (density != null) { rho = density.interpolateAt(this.T); @@ -291,31 +288,31 @@ public NumericProperty getThermalConductivity() { public void calculateEmissivity() { double newEmissivity = Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN); var transform = new StickTransform(Segment.boundsFrom(EMISSIVITY)); - setEmissivity(derive(EMISSIVITY, + setEmissivity(derive(EMISSIVITY, transform.transform(newEmissivity)) ); } - + /** * Calculates the radiative Biot number. + * * @return the radiative Biot number. */ - public double radiationBiot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; } - + /** - * Calculates the maximum Biot number at these conditions, which - * corresponds to an emissivity of unity. If emissivity is non-positive, - * returns the maximum Biot number defined in the XML file. + * Calculates the maximum Biot number at these conditions, which corresponds + * to an emissivity of unity. If emissivity is non-positive, returns the + * maximum Biot number defined in the XML file. + * * @return the maximum Biot number */ - public double maxRadiationBiot() { double absMax = Segment.boundsFrom(HEAT_LOSS).getMaximum(); - return emissivity > 0 ? radiationBiot() / emissivity : absMax; + return emissivity > 0 ? radiationBiot() / emissivity : absMax; } /** @@ -328,7 +325,7 @@ public double maxRadiationBiot() { public double characteristicTime() { return l * l / a; } - + public double getThermalMass() { return cP * rho; } @@ -357,7 +354,7 @@ public final boolean areThermalPropertiesLoaded() { public double maximumHeating(Pulse2D pulse) { final double Q = (double) pulse.getLaserEnergy().getValue(); final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass() ); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass()); } public NumericProperty getEmissivity() { @@ -385,4 +382,4 @@ public String toString() { return sb.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 371fadc3..ff844eb6 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -25,6 +25,7 @@ public class ThermoOpticalProperties extends ThermalProperties implements Optimisable { + private static final long serialVersionUID = 3573682830421468534L; private double opticalThickness; private double planckNumber; private double scatteringAlbedo; @@ -33,29 +34,29 @@ public class ThermoOpticalProperties extends ThermalProperties implements Optimi public ThermoOpticalProperties() { super(); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); + this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermalProperties p) { super(p); - opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); + opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermoOpticalProperties p) { super(p); - this.opticalThickness = p.opticalThickness; - this.planckNumber = p.planckNumber; - this.scatteringAlbedo = p.scatteringAlbedo; - this.scatteringAnisotropy = p.scatteringAnisotropy; - this.convectiveLosses = p.convectiveLosses; + this.opticalThickness = p.opticalThickness; + this.planckNumber = p.planckNumber; + this.scatteringAlbedo = p.scatteringAlbedo; + this.scatteringAnisotropy = p.scatteringAnisotropy; + this.convectiveLosses = p.convectiveLosses; } @Override @@ -135,7 +136,7 @@ public void setScatteringAnisotropy(NumericProperty A1) { this.scatteringAnisotropy = (double) A1.getValue(); firePropertyChanged(this, A1); } - + public void setConvectiveLosses(NumericProperty losses) { requireType(losses, HEAT_LOSS_CONVECTIVE); this.convectiveLosses = (double) losses.getValue(); @@ -145,7 +146,7 @@ public void setConvectiveLosses(NumericProperty losses) { public NumericProperty getConvectiveLosses() { return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); } - + public NumericProperty getScatteringAlbedo() { return derive(SCATTERING_ALBEDO, scatteringAlbedo); } @@ -173,7 +174,7 @@ public void useTheoreticalEstimates(ExperimentalData c) { public String getDescriptor() { return "Thermo-Physical & Optical Properties"; } - + @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); @@ -186,7 +187,7 @@ public String toString() { sb.append(String.format("%n %-25s", this.getDensity())); return sb.toString(); } - + @Override public void optimisationVector(ParameterVector output) { Segment bounds = null; @@ -220,9 +221,9 @@ public void optimisationVector(ParameterVector output) { bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); break; case HEAT_LOSS: - value = (double) getHeatLoss().getValue(); - bounds = new Segment(0.0, maxRadiationBiot() ); - break; + value = (double) getHeatLoss().getValue(); + bounds = new Segment(0.0, maxRadiationBiot()); + break; default: continue; diff --git a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java index c879327f..099fde4b 100644 --- a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java +++ b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java @@ -11,10 +11,11 @@ public class TwoTemperatureProperties extends ThermalProperties { + private static final long serialVersionUID = 4157382023954200467L; private double exchangeSolid; private double exchangeGas; private double gasHeatLoss; - + public TwoTemperatureProperties() { super(); exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); @@ -29,8 +30,7 @@ public TwoTemperatureProperties(ThermalProperties p) { this.exchangeSolid = np.exchangeSolid; this.exchangeGas = np.exchangeGas; this.gasHeatLoss = np.gasHeatLoss; - } - else { + } else { exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); @@ -80,7 +80,7 @@ public Set listedKeywords() { public NumericProperty getSolidExchangeCoefficient() { return derive(SOLID_EXCHANGE_COEFFICIENT, exchangeSolid); } - + public NumericProperty getGasExchangeCoefficient() { return derive(GAS_EXCHANGE_COEFFICIENT, exchangeGas); } @@ -90,7 +90,7 @@ public void setSolidExchangeCoefficient(NumericProperty p) { this.exchangeSolid = (double) p.getValue(); firePropertyChanged(this, p); } - + public void setGasExchangeCoefficient(NumericProperty p) { NumericProperty.requireType(p, GAS_EXCHANGE_COEFFICIENT); this.exchangeGas = (double) p.getValue(); diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 94b6ce3f..d6c6f10a 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -12,6 +12,7 @@ */ public class Flag implements Property { + private static final long serialVersionUID = 4927536419752406797L; private NumericPropertyKeyword index; private boolean value; private String descriptor; @@ -26,15 +27,15 @@ public class Flag implements Property { public Flag(NumericPropertyKeyword type) { this(type, false); } - + public Flag(Flag f) { this(f.index, f.value); } - + public Flag(NumericPropertyKeyword type, boolean flag) { this.index = type; this.value = flag; - } + } /** * Creates a {@code Flag} with the following pre-specified parameters: type diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index 738885a6..3f0f5ff1 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -41,8 +41,8 @@ public static boolean isValueSensible(NumericProperty property, Number val) { double v = val.doubleValue(); final double EPS = 1E-12; boolean ok = true; - - if( !Double.isFinite(v) + + if (!Double.isFinite(v) || v > property.getMaximum().doubleValue() + EPS || v < property.getMinimum().doubleValue() - EPS) { ok = false; @@ -91,7 +91,7 @@ public static int compare(NumericProperty a, NumericProperty b) { Double d1 = ((Number) a.getValue()).doubleValue(); Double d2 = ((Number) b.getValue()).doubleValue(); - final double eps = 1E-8 * (d1 + d2) / 2.0; + final double eps = 1E-8 * Math.abs(d1 + d2) / 2.0; return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); } @@ -99,8 +99,7 @@ public static int compare(NumericProperty a, NumericProperty b) { /** * Searches for the default {@code NumericProperty} corresponding to * {@code keyword} in the list of pre-defined properties loaded from the - * respective {@code .xml} file, and if found creates a new - * { + * respective {@code .xml} file, and if found creates a new { * * @NumericProperty} which will replicate all field of the latter, but will * set its value to {@code value}. diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 734a70f2..495d1a18 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -22,6 +22,11 @@ */ public class NumericProperty implements Property, Comparable { + /** + * + */ + private static final long serialVersionUID = -7132274623596750984L; + private Number value; private Number minimum; @@ -263,6 +268,7 @@ public boolean equals(Object o) { @Override public int compareTo(NumericProperty arg0) { final int result = this.getType().compareTo(arg0.getType()); + int res = compare(this, arg0); return result != 0 ? result : compare(this, arg0); } @@ -310,34 +316,35 @@ public void setDefaultSearchVariable(boolean defaultSearchVariable) { public void setOptimisable(boolean optimisable) { this.optimisable = optimisable; } - + public Number getDimensionDelta() { - if(type == NumericPropertyKeyword.TEST_TEMPERATURE) + if (type == NumericPropertyKeyword.TEST_TEMPERATURE) { return -273.15; - else + } else { return 0.0; + } } - + /** - * Represents the bounds specified for this numeric property - * as a {@code Segment} object. The bound numbers are taken by - * their double values and assigned to the segment. + * Represents the bounds specified for this numeric property as a + * {@code Segment} object. The bound numbers are taken by their double + * values and assigned to the segment. + * * @return the bounds in which this property is allowed to change */ - public Segment getBounds() { return new Segment(minimum.doubleValue(), maximum.doubleValue()); } - + /** * Uses a {@code NumericPropertyFormatter} to generate a formatted output - * @return a formatted string output with the value (and error -- if available) - * of this numeric property + * + * @return a formatted string output with the value (and error -- if + * available) of this numeric property */ - public String formattedOutput() { var num = new NumericPropertyFormatter(this, true, true); return num.numberFormat(this).format(value); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java index f3c83cf0..065f0602 100644 --- a/src/main/java/pulse/properties/NumericPropertyFormatter.java +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -30,6 +30,7 @@ */ public class NumericPropertyFormatter extends AbstractFormatter { + private static final long serialVersionUID = -7733589481239097566L; private NumericPropertyKeyword key; private Segment bounds; private boolean convertDimension = true; @@ -74,9 +75,9 @@ public NumberFormat numberFormat(NumericProperty p) { : (double) value; double absAdjustedValue = Math.abs(adjustedValue); - if (addHtmlTags && - ( (absAdjustedValue > UPPER_LIMIT) - || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) ) { + if (addHtmlTags + && ((absAdjustedValue > UPPER_LIMIT) + || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO))) { //format with scientific notations f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); } else { @@ -85,7 +86,7 @@ public NumberFormat numberFormat(NumericProperty p) { } } - + return f; } diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 36b16662..344bdbea 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -157,13 +157,10 @@ public enum NumericPropertyKeyword { * sample (1D and 2D problems). */ HEAT_LOSS, - /** * The convective heat loss in diathermic and participating medium problems. */ - HEAT_LOSS_CONVECTIVE, - /** * A directive for the optimiser to maintain equal heat losses on all * surfaces of the sample. Note that the dimensionless heat losses, i.e. @@ -224,7 +221,6 @@ public enum NumericPropertyKeyword { * Statistical significance for calculating the critical value. */ SIGNIFICANCE, - /** * Optimiser statistic (usually, RSS). */ @@ -354,50 +350,36 @@ public enum NumericPropertyKeyword { * damping. A value of 1 gives pure Marquardt damping. */ DAMPING_RATIO, - /** * Determines how much weight is attributed to the front-face light source - * compared to rear face. Can be a number between zero and unity. + * compared to rear face. Can be a number between zero and unity. */ - SOURCE_GEOMETRIC_FACTOR, - /** * Max. no. of high-frequency waves in the sinusoidal baseline. */ - MAX_HIGH_FREQ_WAVES, - /** - * Max. no. of low-frequency waves in the sinusoidal baseline. - */ - + * Max. no. of low-frequency waves in the sinusoidal baseline. + */ MAX_LOW_FREQ_WAVES, - /** * Energy exchange coefficient in the two-temperature model (g). */ - SOLID_EXCHANGE_COEFFICIENT, - /** * Energy exchange coefficient in the two-temperature model (g'). */ - GAS_EXCHANGE_COEFFICIENT, - /** * Heat loss for the gas in the 2T-model. */ - HEAT_LOSS_GAS, - /** * Value of objective function. */ - OBJECTIVE_FUNCTION; - + public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); } diff --git a/src/main/java/pulse/properties/Property.java b/src/main/java/pulse/properties/Property.java index a1215d74..0ff78043 100644 --- a/src/main/java/pulse/properties/Property.java +++ b/src/main/java/pulse/properties/Property.java @@ -1,10 +1,12 @@ package pulse.properties; +import java.io.Serializable; + /** * The basic interface for properties. The only declared functionality consists * in the ability to report the associated value and deliver text description. */ -public interface Property { +public interface Property extends Serializable { /** * Retrieves the value of this {@code Property}. diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 2f9f6cc0..0e355f2b 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -4,6 +4,7 @@ public class SampleName implements Property { + private static final long serialVersionUID = -965821128124753850L; private String name; public SampleName(String name) { @@ -65,4 +66,4 @@ public boolean equals(Object obj) { return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/properties/ScientificFormat.java b/src/main/java/pulse/properties/ScientificFormat.java index cd9bacaf..a61108c6 100644 --- a/src/main/java/pulse/properties/ScientificFormat.java +++ b/src/main/java/pulse/properties/ScientificFormat.java @@ -30,6 +30,7 @@ */ public class ScientificFormat extends NumberFormat { + private static final long serialVersionUID = -6509402151736747913L; private final int dimensionFactor; private final double dimensionDelta; diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java index 3bd42d0a..426b067f 100644 --- a/src/main/java/pulse/search/GeneralTask.java +++ b/src/main/java/pulse/search/GeneralTask.java @@ -15,41 +15,41 @@ import static pulse.tasks.processing.Buffer.getSize; import pulse.util.Accessible; -public abstract class GeneralTask +public abstract class GeneralTask extends Accessible implements Runnable { private IterativeState path; //current sate private IterativeState best; //best state - + private final Buffer buffer; private PathOptimiser optimiser; - + public GeneralTask() { buffer = new Buffer(); buffer.setParent(this); } - + public abstract List activeParameters(); /** - * Creates a search vector populated by parameters that - * are included in the optimisation routine. + * Creates a search vector populated by parameters that are included in the + * optimisation routine. + * * @return the parameter vector with optimisation parameters */ - public abstract ParameterVector searchVector(); - + /** - * Tries to assign a selected set of parameters to the search vector - * used in optimisation. - * @param pv a parameter vector containing all of the optimisation parameters - * whose values will be assigned to this task - * @throws SolverException + * Tries to assign a selected set of parameters to the search vector used in + * optimisation. + * + * @param pv a parameter vector containing all of the optimisation + * parameters whose values will be assigned to this task + * @throws SolverException */ - public abstract void assign(ParameterVector pv) throws SolverException; - - /** + + /** *

* Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, * will do nothing. After making some preparatory steps, will initiate a @@ -65,9 +65,9 @@ public GeneralTask() { public void run() { setDefaultOptimiser(); best = null; - setIterativeState( optimiser.initState(this) ); + setIterativeState(optimiser.initState(this)); - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + double errorTolerance = (double) optimiser.getErrorTolerance().getValue(); int bufferSize = (Integer) getSize().getValue(); buffer.init(); //correlationBuffer.clear(); @@ -78,7 +78,7 @@ public void run() { var singleThreadExecutor = Executors.newSingleThreadExecutor(); var response = getResponse(); - + try { response.objectiveFunction(this); } catch (SolverException e1) { @@ -100,7 +100,7 @@ public void run() { onSolverException(e); break outer; } - + //if global best is better than the converged value if (best != null && best.getCost() < path.getCost()) { try { @@ -114,7 +114,7 @@ public void run() { } final var j = i; - + bufferFutures.add(CompletableFuture.runAsync(() -> { buffer.fill(this, j); intermediateProcessing(); @@ -123,52 +123,50 @@ public void run() { } bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance) + + } while (buffer.isErrorTooHigh(errorTolerance) && isInProgress()); singleThreadExecutor.shutdown(); if (isInProgress()) { postProcessing(); - } + } - } + } + + public abstract boolean isInProgress(); - public abstract boolean isInProgress(); - /** - * Override this to add intermediate processing of results e.g. - * with a correlation test. + * Override this to add intermediate processing of results e.g. with a + * correlation test. */ - public void intermediateProcessing() { //empty } - + /** * Specifies what should be done when a solver exception is encountered. * Empty by default + * * @param e1 a solver exception */ - public void onSolverException(SolverException e1) { //empty } - + /** - * Override this to add post-processing checks - * e.g. normality tests or range checking. + * Override this to add post-processing checks e.g. normality tests or range + * checking. */ - public void postProcessing() { //empty } - + public final Buffer getBuffer() { return buffer; } - + public void setIterativeState(IterativeState state) { this.path = state; } @@ -196,23 +194,24 @@ public void storeState() { best = new IterativeState(path); } } - + public final void setOptimiser(PathOptimiser optimiser) { this.optimiser = optimiser; } - + public void setDefaultOptimiser() { var instance = PathOptimiser.getInstance(); - if(optimiser == null || optimiser != instance) { + if (optimiser == null || optimiser != instance) { setOptimiser(PathOptimiser.getInstance()); } } - + public double objectiveFunction() throws SolverException { return getResponse().objectiveFunction(this); } - - public abstract I getInput(); + + public abstract I getInput(); + public abstract R getResponse(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 44666f54..53c29d43 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,6 +1,5 @@ package pulse.search; - import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; diff --git a/src/main/java/pulse/search/SimpleOptimisationTask.java b/src/main/java/pulse/search/SimpleOptimisationTask.java index 4fbce664..ec0fd4ed 100644 --- a/src/main/java/pulse/search/SimpleOptimisationTask.java +++ b/src/main/java/pulse/search/SimpleOptimisationTask.java @@ -30,11 +30,11 @@ public SimpleOptimisationTask(T optimisable, DiscreteInput input) { this.input = input; this.optimisable = optimisable; } - + @Override public void run() { var optimiser = PathOptimiser.getInstance(); - if(optimiser == null) { + if (optimiser == null) { PathOptimiser.setInstance(LMOptimiser.getInstance()); } super.run(); @@ -74,20 +74,20 @@ public boolean isInProgress() { public void set(NumericPropertyKeyword type, NumericProperty property) { optimisable.set(type, property); } - + @Override - public List activeParameters() { + public List activeParameters() { return selectActiveAndListed(ActiveFlags.getAllFlags(), optimisable); } - + @Override public void setDefaultOptimiser() { setOptimiser(LMOptimiser.getInstance()); } - + @Override public DiscreteInput getInput() { return input; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 81103d15..1ad72f30 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -13,8 +14,9 @@ import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; -public class ActiveFlags { +public class ActiveFlags implements Serializable { + private static final long serialVersionUID = -8711073682010113698L; private static List flags; static { @@ -42,12 +44,12 @@ public static Set availableProperties() { return set; } - var p = ( (Calculation) t.getResponse() ).getProblem(); + var p = ((Calculation) t.getResponse()).getProblem(); if (p != null) { var fullList = p.listedKeywords(); - fullList.addAll( ( (ExperimentalData) t.getInput() ).listedKeywords()); + fullList.addAll(((ExperimentalData) t.getInput()).listedKeywords()); NumericPropertyKeyword key; for (Flag property : flags) { @@ -62,36 +64,36 @@ public static Set availableProperties() { return set; } - + public static Flag get(NumericPropertyKeyword key) { var flag = flags.stream().filter(f -> f.getType() == key).findAny(); return flag.isPresent() ? flag.get() : null; - } - + } + /** * Creates a deep copy of the flags collection. + * * @return a deep copy of the flags */ - public static List storeState() { var copy = new ArrayList(); - for(Flag f : flags) { + for (Flag f : flags) { copy.add(new Flag(f)); } return copy; } - + /** - * Loads the argument into the current list of flags. - * This will update any matching flags and assign values correpon - * @param flags + * Loads the argument into the current list of flags. This will update any + * matching flags and assign values correpon + * + * @param flags */ - public static void loadState(List flags) { - for(Flag f : ActiveFlags.flags) { - Optional existingFlag = flags.stream().filter(fl -> - fl.getType() == f.getType()).findFirst(); - if(existingFlag.isPresent()) { + for (Flag f : ActiveFlags.flags) { + Optional existingFlag = flags.stream().filter(fl + -> fl.getType() == f.getType()).findFirst(); + if (existingFlag.isPresent()) { f.setValue((boolean) existingFlag.get().getValue()); } } @@ -99,15 +101,15 @@ public static void loadState(List flags) { public static List selectActiveAndListed(List flags, PropertyHolder listed) { //return empty list - if(listed == null) { + if (listed == null) { return new ArrayList<>(); } - + return selectActiveTypes(flags).stream() .filter(type -> listed.isListedNumericType(type)) .collect(Collectors.toList()); } - + public static List selectActiveTypes(List flags) { return Flag.selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); } diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 273b9361..89119266 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -30,6 +30,10 @@ */ public class BFGSOptimiser extends CompositePathOptimiser { + /** + * + */ + private static final long serialVersionUID = -8542438015176648987L; private static BFGSOptimiser instance = new BFGSOptimiser(); private BFGSOptimiser() { @@ -68,9 +72,9 @@ public void prepare(GeneralTask task) throws SolverException { p.setHessian(hessian); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 p.setGradient(g1); // set g1 as the new gradient for next step } - + /** - * Uses the BFGS formula to calculate the Hessian. + * Uses the BFGS formula to calculate the Hessian. * * @param g1 gradient at step k * @param g2 gradient at step k+1 diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index bfe1ef84..8340c81c 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -15,6 +15,7 @@ */ public class ComplexPath extends GradientGuidedPath { + private static final long serialVersionUID = -1520823504831702183L; private SquareMatrix hessian; private SquareMatrix inverseHessian; diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 48766c5a..1e91de69 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -16,23 +16,22 @@ public abstract class CompositePathOptimiser extends GradientBasedOptimiser { - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Linear Optimiser Selector", LinearOptimiser.class); + "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; - - /** - * Maximum number of consequent failed iterations that can be rejected. - * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. + + /** + * Maximum number of consequent failed iterations that can be rejected. Up + * to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 2; - + /** * For numerical comparison. */ - public final static double EPS = 1e-10; + public final static double EPS = 1e-10; public CompositePathOptimiser() { instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); @@ -48,7 +47,7 @@ private void initLinearOptimiser() { @Override public boolean iteration(GeneralTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task - + boolean accept = true; /* @@ -62,7 +61,7 @@ public boolean iteration(GeneralTask task) throws SolverException { double initialCost = task.getResponse().objectiveFunction(task); p.setCost(initialCost); - var parameters = task.searchVector(); + var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -71,24 +70,24 @@ public boolean iteration(GeneralTask task) throws SolverException { p.setLinearStep(step); // new set of parameters determined through search - var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateParams = parameters.toVector().sum(dir.multiply(step)); var candidateVector = new ParameterVector(parameters, candidateParams); - - if(candidateVector.findMalformedElements().isEmpty()) { + + if (candidateVector.findMalformedElements().isEmpty()) { task.assign(candidateVector); // assign new parameters } - - double newCost = task.getResponse().objectiveFunction(task); + + double newCost = task.getResponse().objectiveFunction(task); // calculate the sum of squared residuals - if (newCost > initialCost - EPS - && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS - && p instanceof ComplexPath) { - var complexPath = (ComplexPath)p; + if (newCost > initialCost - EPS + && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS + && p instanceof ComplexPath) { + var complexPath = (ComplexPath) p; task.assign(parameters); // roll back if cost increased // attempt to reset -> in case of Hessian-based methods, // this will change the Hessian) { - complexPath.setHessian( createIdentityMatrix(parameters.dimension()) ); + complexPath.setHessian(createIdentityMatrix(parameters.dimension())); p.incrementFailedAttempts(); accept = false; } else { @@ -97,7 +96,7 @@ public boolean iteration(GeneralTask task) throws SolverException { this.prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps - } + } } @@ -144,4 +143,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java index c8e55a1e..f96c1910 100644 --- a/src/main/java/pulse/search/direction/DirectionSolver.java +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -1,9 +1,10 @@ package pulse.search.direction; +import java.io.Serializable; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -public interface DirectionSolver { +public interface DirectionSolver extends Serializable { /** * Finds the direction of the minimum using the previously calculated values diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index abee584f..c55405ec 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -82,7 +82,7 @@ public Vector gradient(GeneralTask task) throws SolverException { final var pVector = params.toVector(); var grad = new Vector(params.dimension()); final var ps = params.getParameters(); - + for (int i = 0, size = params.dimension(); i < size; i++) { var key = ps.get(i).getIdentifier().getKeyword(); var defProp = key != null ? NumericProperties.def(key) : null; diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 7b9a06cc..f4dd4f8f 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -30,6 +30,10 @@ */ public class GradientGuidedPath extends IterativeState { + /** + * + */ + private static final long serialVersionUID = -6450999613326096767L; private Vector direction; private Vector gradient; private double minimumPoint; @@ -51,7 +55,7 @@ public void configure(GeneralTask t) { try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); } catch (SolverException ex) { - t.onSolverException( new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + t.onSolverException(new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); ex.printStackTrace(); } minimumPoint = 0.0; @@ -79,6 +83,6 @@ public double getMinimumPoint() { public void setLinearStep(double min) { minimumPoint = min; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 9045d46f..6d258076 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.io.Serializable; import pulse.math.ParameterVector; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.ITERATION; @@ -7,38 +8,41 @@ import pulse.properties.NumericProperty; import pulse.search.GeneralTask; -public class IterativeState { +public class IterativeState implements Serializable { + private static final long serialVersionUID = -3924087865736298552L; private ParameterVector parameters; private double cost = Double.POSITIVE_INFINITY; private int iteration; private int failedAttempts; /** - * Stores the parameter vector and cost function value associated with the specified state. + * Stores the parameter vector and cost function value associated with the + * specified state. + * * @param other another state of the optimiser */ - public IterativeState(IterativeState other) { this.parameters = new ParameterVector(other.parameters); this.cost = other.cost; } - + public IterativeState(GeneralTask t) { this.parameters = t.searchVector(); } - + //default constructor - public IterativeState() {} - + public IterativeState() { + } + public double getCost() { return cost; } - + public void setCost(double cost) { this.cost = cost; } - + public void reset() { iteration = 0; setCost(Double.POSITIVE_INFINITY); @@ -63,7 +67,7 @@ public void resetFailedAttempts() { public void incrementFailedAttempts() { failedAttempts++; } - + public ParameterVector getParameters() { return parameters; } @@ -72,4 +76,4 @@ public void setParameters(ParameterVector parameters) { this.parameters = parameters; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index d3b22c74..9d4c1e55 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -36,13 +36,13 @@ */ public class LMOptimiser extends GradientBasedOptimiser { + private static final long serialVersionUID = -7954867240278082038L; private static final LMOptimiser instance = new LMOptimiser(); private double dampingRatio; - + /** * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 5; private LMOptimiser() { @@ -79,17 +79,17 @@ public boolean iteration(GeneralTask task) throws SolverException { var lmDirection = getSolver().direction(p); var candidate = parameters.toVector().sum(lmDirection); - - if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + + if (Arrays.stream(candidate.getData()).anyMatch(el -> !Double.isFinite(el))) { throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration(), ILLEGAL_PARAMETERS); } - + task.assign(new ParameterVector( parameters, candidate)); // assign new parameters - + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals - + /* * Delayed gratification */ @@ -135,12 +135,12 @@ public void prepare(GeneralTask task) throws SolverException { // the Jacobian is then used to calculate the 'gradient' Vector g1 = halfGradient(p); // g1 p.setGradient(g1); - - if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { + + if (Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { throw new SolverException("Could not calculate objective function gradient", - OPTIMISATION_ERROR); + OPTIMISATION_ERROR); } - + // the Hessian is then regularised by adding labmda*I var hessian = p.getNonregularisedHessian(); var damping = (levenbergDamping(hessian).multiply(dampingRatio) @@ -175,7 +175,7 @@ public void prepare(GeneralTask task) throws SolverException { public RectangularMatrix jacobian(GeneralTask task) throws SolverException { var residualCalculator = task.getResponse().getOptimiserStatistic(); - + var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); @@ -186,12 +186,12 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { var jacobian = new double[numPoints][numParams]; var ps = params.getParameters(); - + for (int i = 0; i < numParams; i++) { var key = ps.get(i).getIdentifier().getKeyword(); - double dx = dx( - key != null ? NumericProperties.def(key) : null, + double dx = dx( + key != null ? NumericProperties.def(key) : null, ps.get(i).inverseTransform()); final var shift = new Vector(numParams); @@ -201,19 +201,19 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { task.assign(new ParameterVector(params, pVector.sum(shift))); task.objectiveFunction(); var r = residualCalculator.getResiduals(); - - for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); - j < realNumPoints; j++) { + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { jacobian[j][i] = r.get(j) / dx; } - + // - shift task.assign(new ParameterVector(params, pVector.subtract(shift))); task.objectiveFunction(); - for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); j < realNumPoints; j++) { jacobian[j][i] -= r.get(j) / dx; @@ -221,14 +221,14 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { } } - + // revert to original params task.assign(params); return Matrices.createMatrix(jacobian); } - + @Override public GradientGuidedPath initState(GeneralTask t) { return new LMPath(t); @@ -260,8 +260,8 @@ private SquareMatrix levenbergDamping(SquareMatrix hessian) { private SquareMatrix marquardtDamping(SquareMatrix hessian) { return hessian.blockDiagonal(); } - - @Override + + @Override public Set listedKeywords() { var set = super.listedKeywords(); set.add(DAMPING_RATIO); diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 2528968b..39e99855 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -7,6 +7,7 @@ class LMPath extends ComplexPath { + private static final long serialVersionUID = -7154616034580697035L; private Vector residualVector; private RectangularMatrix jacobian; private SquareMatrix nonregularisedHessian; diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index cb5d4fa6..21618a8d 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -94,7 +94,7 @@ public void reset() { * @see pulse.search.linear.LinearOptimiser */ public abstract boolean iteration(GeneralTask task) throws SolverException; - + /** * Defines a set of procedures to be run at the end of the search iteration. * @@ -216,7 +216,7 @@ protected final void setSolver(DirectionSolver solver) { /** * Checks if this optimiser is compatible with the statistic passed to the * method as its argument.By default, this will accept any - {@code OptimiserStatistic} + * {@code OptimiserStatistic} * * @param os a selected optimiser metric * @return {@code true}, if not specified otherwise by its subclass diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index 8b2f7081..716753f2 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -13,6 +13,8 @@ public class SR1Optimiser extends CompositePathOptimiser { + private static final long serialVersionUID = -3041166132227281210L; + private static SR1Optimiser instance = new SR1Optimiser(); private final static double r = 1E-8; diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 8fcbc31b..ee9b68e2 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -16,6 +16,10 @@ */ public class SteepestDescentOptimiser extends CompositePathOptimiser { + /** + * + */ + private static final long serialVersionUID = -6868259511333467862L; private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); private SteepestDescentOptimiser() { @@ -67,4 +71,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new GradientGuidedPath(t); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java index f19d824f..db1d89b2 100644 --- a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java +++ b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java @@ -4,16 +4,16 @@ import pulse.math.linear.Vector; public class ConstrictionMover implements Mover { - + private double c1; //social private double c2; //cognitive - private double chi; + private double chi; public final static double DEFAULT_CHI = 0.7298; public final static double DEFAULT_C = 1.49618; - + public ConstrictionMover() { - chi = DEFAULT_CHI; - c1 = c2 = DEFAULT_C; + chi = DEFAULT_CHI; + c1 = c2 = DEFAULT_C; } @Override @@ -23,24 +23,24 @@ public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleStat var curPosV = curPos.toVector(); final int n = curPos.dimension(); - Vector nsum = new Vector(n); + Vector nsum = new Vector(n); - var localBest = p.getBestState().getPosition(); //best position by local particle + var localBest = p.getBestState().getPosition(); //best position by local particle var localBestV = localBest.toVector(); var globalBest = gBest.getPosition(); //best position by any particle var globalBestV = globalBest.toVector(); - + nsum = nsum.sum(Vector.random(n, 0.0, c1) - .multComponents(localBestV.subtract(curPosV)) - ); - + .multComponents(localBestV.subtract(curPosV)) + ); + nsum = nsum.sum(Vector.random(n, 0.0, c2) - .multComponents(globalBestV.subtract(curPosV)) - ); + .multComponents(globalBestV.subtract(curPosV)) + ); var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); var newPosition = curPosV.sum(newVelocity); - + return new ParticleState( new ParameterVector(curPos, newPosition), new ParameterVector(curPos, newVelocity)); diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index b6869ec6..09fe7a69 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -20,26 +20,26 @@ public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleStat var current = p.getCurrentState(); var curPos = current.getPosition(); var curPosV = curPos.toVector(); - + final int n = curPos.dimension(); final double nLength = (double) neighbours.length; - Vector nsum = new Vector(n); + Vector nsum = new Vector(n); for (var neighbour : neighbours) { var nBestPos = neighbour.getBestState().getPosition(); //best position ever achieved so far by the neighbour - nsum = nsum.sum(Vector.random(n, 0.0, phi/nLength) + nsum = nsum.sum(Vector.random(n, 0.0, phi / nLength) .multComponents(nBestPos.toVector().subtract(curPosV)) ); } var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); var newPosition = curPosV.sum(newVelocity); - + return new ParticleState( new ParameterVector(curPos, newPosition), new ParameterVector(curPos, newVelocity)); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index d1db1d0c..1d7ecc21 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -4,4 +4,4 @@ public interface Mover { public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index 68f68c13..01985e48 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -15,7 +15,7 @@ public ParticleState(ParameterVector cur) { //set initial velocity to zero velocity.setValues(new Vector(cur.dimension())); - + this.fitness = Double.MAX_VALUE; } @@ -41,7 +41,7 @@ public final void randomise(ParameterVector pos) { double max = p.getBounds().getMaximum(); return min + Math.random() * (max - min); }).toArray(); - + Vector randomVector = new Vector(randomValues); position.setValues(randomVector); } diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index 323dfab2..f8cbf29e 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -19,12 +19,12 @@ public ParticleSwarmOptimiser() { protected void moveParticles() { var topology = swarmState.getNeighborhoodTopology(); for (var p : swarmState.getParticles()) { - p.adopt(mover.attemptMove(p, - topology.neighbours(p, swarmState), + p.adopt(mover.attemptMove(p, + topology.neighbours(p, swarmState), swarmState.getBestSoFar())); var data = p.getCurrentState().getPosition().toVector().getData(); StringBuilder sb = new StringBuilder().append(p.getId()).append(" "); - for(var d : data) { + for (var d : data) { sb.append(d).append(" "); } System.err.println(sb.toString()); @@ -63,7 +63,7 @@ public IterativeState initState(GeneralTask t) { swarmState.create(); return swarmState; } - + //TODO @Override public boolean compatibleWith(OptimiserStatistic os) { diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 8fa5c245..c466e8f2 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -32,7 +32,7 @@ public class StaticTopologies { final int latticeParameter = (int) Math.sqrt(ps.length); - final int row = i / latticeParameter; + final int row = i / latticeParameter; final int column = i - row * latticeParameter; final int above = column + (row > 0 diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 7baa955f..df5b7041 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -64,16 +64,16 @@ public void bestSoFar() { } } - + //determine the current best - ParticleState curBest = particles[bestIndex].getCurrentState(); - + ParticleState curBest = particles[bestIndex].getCurrentState(); + //is curBest the best so far? - if(bestSoFar == null || curBest.isBetterThan(bestSoFar) ) { + if (bestSoFar == null || curBest.isBetterThan(bestSoFar)) { this.bestSoFar = curBest; this.bestSoFarIndex = bestIndex; } - + } public NeighbourhoodTopology getNeighborhoodTopology() { @@ -113,4 +113,4 @@ public void setBestSoFarIndex(int bestSoFarIndex) { this.bestSoFarIndex = bestSoFarIndex; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index 4c9f1bea..85eab130 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -18,6 +18,11 @@ */ public class GoldenSectionOptimiser extends LinearOptimiser { + /** + * + */ + private static final long serialVersionUID = -369106060533186038L; + /** * The golden section φ, which is approximately equal to 0.618033989. */ diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index af3a2cd3..d540ba3b 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -25,7 +25,12 @@ * page */ public class WolfeOptimiser extends LinearOptimiser { - + + /** + * + */ + private static final long serialVersionUID = 5200832276052099700L; + private static WolfeOptimiser instance = new WolfeOptimiser(); /** @@ -38,7 +43,7 @@ public class WolfeOptimiser extends LinearOptimiser { * gradient projection, equal to {@value C2}. */ public final static double C2 = 0.8; - + private WolfeOptimiser() { super(); } @@ -66,34 +71,34 @@ private WolfeOptimiser() { */ @Override public double linearStep(GeneralTask task) throws SolverException { - + GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); - + final Vector direction = p.getDirection(); final Vector g1 = p.getGradient(); - + final double G1P = g1.dot(direction); final double G1P_ABS = abs(G1P); - + var params = task.searchVector(); var vParams = params.toVector(); Segment segment = domain(params, direction); - + double cost1 = task.objectiveFunction(); - + double randomConfinedValue = 0; double g2p; - + var optimiser = (GradientBasedOptimiser) PathOptimiser.getInstance(); - + for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { - + randomConfinedValue = segment.randomValue(); - + final var newParams = vParams.sum(direction.multiply(randomConfinedValue)); - + task.assign(new ParameterVector(params, newParams)); - + final double cost2 = task.objectiveFunction(); /** @@ -105,7 +110,7 @@ public double linearStep(GeneralTask task) throws SolverException { segment.setMaximum(randomConfinedValue); continue; } - + final var g2 = optimiser.gradient(task); g2p = g2.dot(direction); @@ -121,16 +126,16 @@ public double linearStep(GeneralTask task) throws SolverException { * if( g2p >= C2*G1P ) break; */ segment.setMinimum(randomConfinedValue); - + } - + task.assign(params); p.setGradient(g1); - + return randomConfinedValue; - + } - + @Override public String toString() { return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ @@ -145,5 +150,5 @@ public String toString() { public static WolfeOptimiser getInstance() { return instance; } - + } diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 32794614..5e04b383 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -7,6 +7,8 @@ */ public class AICStatistic extends ModelSelectionCriterion { + private static final long serialVersionUID = 8549601688520099629L; + public AICStatistic(OptimiserStatistic os) { super(os); } diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 7113c14c..37b2561f 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -4,7 +4,6 @@ import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; import pulse.search.GeneralTask; - /** * A statistical optimality criterion relying on absolute deviations or the L1 * norm condition. Similar to the least squares technique, it attempts to find a @@ -14,6 +13,8 @@ */ public class AbsoluteDeviations extends OptimiserStatistic { + private static final long serialVersionUID = 3385019714627583467L; + public AbsoluteDeviations() { super(); } @@ -25,6 +26,7 @@ public AbsoluteDeviations(AbsoluteDeviations another) { /** * Calculates the L1 norm statistic, which simply sums up the absolute * values of residuals. + * * @param t */ @Override diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index be695931..84d51043 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -16,13 +16,16 @@ */ public class AndersonDarlingTest extends NormalityTest { + private static final long serialVersionUID = -7471878404063688512L; + /** * This uses the SSJ statistical library to calculate the Anderson-Darling * test with the input parameters formed by the {@code task} residuals and a * normal distribution with zero mean and variance equal to the residuals * variance. + * * @param task - * @return + * @return */ @Override public boolean test(GeneralTask task) { @@ -33,7 +36,7 @@ public boolean test(GeneralTask task) { var testResult = GofStat.andersonDarling(residuals, nd); this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - + //compare the p-value and the significance return testResult[1] > significance; } diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java index 72409f03..c0d09aac 100644 --- a/src/main/java/pulse/search/statistics/BICStatistic.java +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -11,6 +11,8 @@ */ public class BICStatistic extends ModelSelectionCriterion { + private static final long serialVersionUID = 737642724262758403L; + public BICStatistic(ModelSelectionCriterion another) { super(another); } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index 670d7e58..ca7c9423 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -22,15 +22,15 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv static { instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); } - + public CorrelationTest() { //intentionally blank } public static CorrelationTest init() { - return instanceDescriptor.newInstance(CorrelationTest.class); + return instanceDescriptor.newInstance(CorrelationTest.class); } - + public final static InstanceDescriptor getTestDescriptor() { return instanceDescriptor; } diff --git a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java index ba6195fb..c69dbb3b 100644 --- a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java @@ -2,6 +2,8 @@ public class EmptyCorrelationTest extends CorrelationTest { + private static final long serialVersionUID = -2462666081516562018L; + @Override public double evaluate(double[] x, double[] y) { return 0; diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index 573280c7..4434f83f 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -4,6 +4,8 @@ public class EmptyTest extends NormalityTest { + private static final long serialVersionUID = 5919796302195242667L; + /** * Always returns true */ diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 4723f5f6..4d9da690 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -137,4 +137,4 @@ public static Calculation findNested(Calculation a, Calculation b) { return aParams > bParams ? b : a; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index 7d5a854d..70350d30 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -21,10 +21,10 @@ public class KSTest extends NormalityTest { @Override public boolean test(GeneralTask task) { evaluate(task); - - this.setStatistic(derive(TEST_STATISTIC, - TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); - return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); + + this.setStatistic(derive(TEST_STATISTIC, + TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); + return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); } @Override diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index f383a83f..498a7f08 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -20,9 +20,10 @@ * residuals. As this is the pre-requisite for optimisers based on the ordinary * least-square statistic, the normality test can also be used to estimate if a * fit 'failed' or 'succeeded' in describing the data. - * + * * The test consists in testing the relation statistic < critValue, - * where the critical value is determined based on a given level of significance. + * where the critical value is determined based on a given level of + * significance. * */ public abstract class NormalityTest extends ResidualStatistic { diff --git a/src/main/java/pulse/search/statistics/PearsonCorrelation.java b/src/main/java/pulse/search/statistics/PearsonCorrelation.java index 401413cf..50ffe90b 100644 --- a/src/main/java/pulse/search/statistics/PearsonCorrelation.java +++ b/src/main/java/pulse/search/statistics/PearsonCorrelation.java @@ -9,6 +9,8 @@ */ public class PearsonCorrelation extends CorrelationTest { + private static final long serialVersionUID = 4819197257434836120L; + @Override public double evaluate(double[] x, double[] y) { return (new PearsonsCorrelation()).correlation(x, y); diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index 0feceed6..807c39d9 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -16,6 +16,7 @@ */ public class RSquaredTest extends NormalityTest { + private static final long serialVersionUID = -2022982190434832373L; private SumOfSquares sos; private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); @@ -55,13 +56,13 @@ public void evaluate(GeneralTask t) { final double mean = mean(yr); double TSS = 0; int size = yr.size(); - + for (int i = 0; i < size; i++) { TSS += pow(yr.get(i) - mean, 2); } TSS /= size; - + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); } diff --git a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java index af8710fe..68943a74 100644 --- a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -11,8 +11,9 @@ */ public class RangePenalisedLeastSquares extends SumOfSquares { + private static final long serialVersionUID = 4068238957339821770L; private double lambda = 0.1; - + public RangePenalisedLeastSquares() { super(); } @@ -43,7 +44,7 @@ public void evaluate(GeneralTask t) { var x = t.getInput().getX(); double partialRange = t.getInput().bounds().length(); double fullRange = x.get(x.size() - 1) - x.get(IndexRange.closestLeft(0.0, x)); - final double statistic = ssr + lambda * (fullRange - partialRange)/fullRange; + final double statistic = ssr + lambda * (fullRange - partialRange) / fullRange; setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } @@ -51,7 +52,7 @@ public void evaluate(GeneralTask t) { public String getDescriptor() { return "Range-Penalised Least Squares"; } - + @Override public OptimiserStatistic copy() { return new RangePenalisedLeastSquares(this); diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index 6fcd8936..06bf1ff4 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -13,8 +13,9 @@ */ public class RegularisedLeastSquares extends SumOfSquares { + private static final long serialVersionUID = -7398979361944447180L; private double lambda = 1e-4; - + public RegularisedLeastSquares() { super(); } diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 5b9d3e53..7185d5ae 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -102,9 +102,8 @@ public ResidualStatistic(ResidualStatistic another) { public final void calculateResiduals(DiscreteInput reference, Response estimate, int min, int max) { var y = reference.getY(); var x = reference.getX(); - + //if size has not changed, use the old list - if (ry.size() == max - min + 1) { for (int i = min; i < max; i++) { @@ -113,13 +112,10 @@ public final void calculateResiduals(DiscreteInput reference, Response estimate, } - } - - //else create a new list - + } //else create a new list else { - rx = x.subList(min, max); + rx = new ArrayList<>(x.subList(min, max)); ry.clear(); for (int i = min; i < max; i++) { @@ -131,18 +127,18 @@ public final void calculateResiduals(DiscreteInput reference, Response estimate, } } - + public void calculateResiduals(DiscreteInput reference, Response estimate) { var y = reference.getY(); var x = reference.getX(); - + var estimateRange = estimate.accessibleRange(); int min = (int) Math.max(reference.getIndexRange().getLowerBound(), - IndexRange.closestLeft(estimateRange.getMinimum(), x) ); + IndexRange.closestLeft(estimateRange.getMinimum(), x)); int max = (int) Math.min(reference.getIndexRange().getUpperBound(), - IndexRange.closestRight(estimateRange.getMaximum(), x) ); - + IndexRange.closestRight(estimateRange.getMaximum(), x)); + calculateResiduals(reference, estimate, min, max); } @@ -178,4 +174,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java index 8bb45072..c073c64a 100644 --- a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java @@ -9,6 +9,8 @@ */ public class SpearmansCorrelationTest extends CorrelationTest { + private static final long serialVersionUID = -8027167403407629716L; + @Override public double evaluate(double[] x, double[] y) { return (new SpearmansCorrelation()).correlation(x, y); diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 46a7f215..30d056ee 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -13,5 +13,5 @@ public abstract class Statistic extends PropertyHolder implements Reflexive { public abstract void evaluate(GeneralTask t); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index f264ed84..e72e41f1 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -11,6 +11,8 @@ */ public class SumOfSquares extends OptimiserStatistic { + private static final long serialVersionUID = 3959714755977689591L; + public SumOfSquares() { super(); } @@ -38,7 +40,6 @@ public SumOfSquares(SumOfSquares sos) { * @param t The task containing the reference and calculated curves * @see calculateResiduals() */ - @Override public void evaluate(GeneralTask t) { calculateResiduals(t); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 40d857d8..db6b25df 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -39,6 +39,7 @@ public class Calculation extends PropertyHolder implements Comparable, Response { + private static final long serialVersionUID = 8098141563821512602L; private Status status; public final static double RELATIVE_TIME_MARGIN = 1.01; @@ -111,7 +112,7 @@ public void clear() { public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; problem.setParent(this); - problem.removeHeatingCurveListeners(); + problem.removeListeners(); addProblemListeners(problem, curve); } @@ -206,7 +207,7 @@ public boolean setStatus(Status status) { if (this.getStatus() != status) { changeStatus = true; - + //current status is given by ** this.status ** //new status is the ** argument ** of this method switch (this.status) { diff --git a/src/main/java/pulse/tasks/Identifier.java b/src/main/java/pulse/tasks/Identifier.java index f640fbd3..2a14adca 100644 --- a/src/main/java/pulse/tasks/Identifier.java +++ b/src/main/java/pulse/tasks/Identifier.java @@ -15,6 +15,7 @@ */ public class Identifier extends NumericProperty { + private static final long serialVersionUID = 3751417739136256453L; private static int lastId = -1; private Identifier(int value, boolean addToList) { @@ -57,5 +58,5 @@ public static Identifier externalIdentifier(int id) { public String toString() { return Messages.getString("Identifier.Tag") + " " + getValue(); } - -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 9cf3e226..ed814aee 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -29,6 +29,7 @@ import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; +import pulse.input.listeners.ExternalDatasetListener; import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; @@ -62,23 +63,27 @@ */ public class SearchTask extends GeneralTask { + /** + * + */ + private static final long serialVersionUID = -6763815749875446528L; private Calculation current; private List stored; private ExperimentalData curve; private Log log; - private final CorrelationBuffer correlationBuffer; + private CorrelationBuffer correlationBuffer; private CorrelationTest correlationTest; private NormalityTest normalityTest; - private final Identifier identifier; + private Identifier identifier; /** * If {@code SearchTask} finishes, and its R2 value is * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private final List listeners; - private final List statusChangeListeners; + private transient List listeners; + private transient List statusChangeListeners; /** *

@@ -92,27 +97,29 @@ public class SearchTask extends GeneralTask { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { - super(); - this.statusChangeListeners = new CopyOnWriteArrayList<>(); - this.listeners = new CopyOnWriteArrayList<>(); current = new Calculation(this); this.identifier = new Identifier(); this.curve = curve; curve.setParent(this); correlationBuffer = new CorrelationBuffer(); + initListeners(); clear(); - addListeners(); } - private void addListeners() { - InterpolationDataset.addListener(e -> { - if (current.getProblem() != null) { - var p = current.getProblem().getProperties(); - if (p.areThermalPropertiesLoaded()) { - p.useTheoreticalEstimates(curve); - } + private void updateThermalProperties() { + if (current.getProblem() != null) { + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) { + p.useTheoreticalEstimates(curve); } - }); + } + } + + @Override + public void initListeners() { + super.initListeners(); + this.statusChangeListeners = new CopyOnWriteArrayList<>(); + this.listeners = new CopyOnWriteArrayList<>(); /** * Sets the difference scheme's time limit to the upper bound of the @@ -237,7 +244,7 @@ public void checkProblems() { } setStatus(s); - + } } @@ -278,7 +285,7 @@ public void run() { } current.getProblem().parameterListChanged(); // get updated list of parameters - + super.run(); } @@ -353,9 +360,12 @@ public Calculation findBestCalculation() { } public void switchToBestModel() { - this.switchTo(findBestCalculation()); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); - fireRepositoryEvent(e); + var best = findBestCalculation(); + if (current != best && best != null) { + this.switchTo(best); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); + fireRepositoryEvent(e); + } } private void fireRepositoryEvent(TaskRepositoryEvent e) { diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 397971dd..f3394fb9 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -3,7 +3,6 @@ import static java.time.LocalDateTime.now; import static java.time.format.DateTimeFormatter.ISO_WEEK_DATE; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; import static pulse.io.readers.ReaderManager.curveReaders; import static pulse.io.readers.ReaderManager.read; import static pulse.tasks.listeners.TaskRepositoryEvent.State.SHUTDOWN; @@ -18,6 +17,8 @@ import static pulse.util.Group.contents; import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -28,11 +29,19 @@ import java.util.logging.Level; import java.util.logging.Logger; import pulse.input.ExperimentalData; +import pulse.input.InterpolationDataset; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; +import pulse.input.listeners.ExternalDatasetListener; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; +import static pulse.properties.NumericPropertyKeyword.DENSITY; +import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; +import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import pulse.properties.SampleName; import pulse.search.direction.PathOptimiser; +import pulse.tasks.listeners.SessionListener; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.listeners.TaskRepositoryListener; import pulse.tasks.listeners.TaskSelectionEvent; @@ -44,7 +53,6 @@ import pulse.util.Group; import pulse.util.HierarchyListener; import pulse.util.PropertyHolder; -import pulse.util.ResourceMonitor; import pulse.util.UpwardsNavigable; /** @@ -56,40 +64,76 @@ *

* */ -public class TaskManager extends UpwardsNavigable { - - private static final TaskManager instance = new TaskManager(); +public final class TaskManager extends UpwardsNavigable { + /** + * + */ + private static final long serialVersionUID = -4255751786167667650L; private List tasks; private SearchTask selectedTask; - private boolean singleStatement = true; + private HierarchyListener statementListener; - private final List selectionListeners; - private final List taskRepositoryListeners; + private transient List selectionListeners; + private transient List taskRepositoryListeners; + private transient List externalListeners; - private final static String DEFAULT_NAME = "Measurement_" + now().format(ISO_WEEK_DATE); + private static TaskManager instance = new TaskManager(); + + private static List globalListeners = new ArrayList<>(); + + private InterpolationDataset cpDataset; + private InterpolationDataset rhoDataset; - private final HierarchyListener statementListener = e -> { + private TaskManager() { + tasks = new ArrayList<>(); + initListeners(); + } + + /** + * Creates a list of property keywords that can be derived with help of the + * loaded data. For example, if heat capacity and density data is available, + * the returned list will contain {@code CONDUCTIVITY}. + * + * @return + */ + public List derivableProperties() { + var list = new ArrayList(); + if (cpDataset != null) { + list.add(SPECIFIC_HEAT); + } + if (rhoDataset != null) { + list.add(DENSITY); + } + if (rhoDataset != null && cpDataset != null) { + list.add(CONDUCTIVITY); + list.add(EMISSIVITY); + } + return list; + } + + @Override + public void initListeners() { + super.initListeners(); + selectionListeners = new CopyOnWriteArrayList<>(); + taskRepositoryListeners = new CopyOnWriteArrayList<>(); + externalListeners = new CopyOnWriteArrayList<>(); + statementListener = e -> { - if (!(e.getSource() instanceof PropertyHolder)) { + if (!(e.getSource() instanceof PropertyHolder)) { - var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); - for (SearchTask t : tasks) { - if (t == task) { - continue; + var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); + for (SearchTask t : tasks) { + if (t == task) { + continue; + } + t.update(e.getProperty()); } - t.update(e.getProperty()); - } - - } - }; + } - private TaskManager() { - tasks = new ArrayList<>(); - selectionListeners = new CopyOnWriteArrayList<>(); - taskRepositoryListeners = new CopyOnWriteArrayList<>(); + }; addHierarchyListener(statementListener); } @@ -211,7 +255,7 @@ public void cancelAllTasks() { } - private void fireTaskSelected(Object source) { + public void fireTaskSelected(Object source) { var e = new TaskSelectionEvent(source); for (var l : selectionListeners) { l.onSelectionChanged(e); @@ -349,7 +393,7 @@ public void generateTask(File file) { */ public void generateTasks(List files) { requireNonNull(files, "Null list of files passed to generatesTasks(...)"); - + //this is the loader runnable submitted to the executor service Runnable loader = () -> { var pool = Executors.newSingleThreadExecutor(); @@ -463,8 +507,8 @@ public final void addTaskRepositoryListener(TaskRepositoryListener listener) { taskRepositoryListeners.add(listener); } - public TaskSelectionListener[] getSelectionListeners() { - return (TaskSelectionListener[]) selectionListeners.toArray(); + public List getSelectionListeners() { + return selectionListeners; } public void removeSelectionListeners() { @@ -500,7 +544,9 @@ public List getTaskRepositoryListeners() { @Override public String describe() { var name = getSampleName(); - return name == null || name.getValue() == null ? DEFAULT_NAME : name.toString(); + return name == null || name.getValue() == null + ? "Measurement_" + now().format(ISO_WEEK_DATE) + : name.toString(); } public void evaluate() { @@ -530,6 +576,24 @@ public Set allGrouppedContents() { public boolean isSingleStatement() { return singleStatement; } + + public static void assumeNewState(TaskManager loaded) { + TaskManager.instance = null; + TaskManager.instance = loaded; + globalListeners.stream().forEach(l -> l.onNewSessionLoaded()); + } + + public void addExternalDatasetListener(ExternalDatasetListener edl) { + this.externalListeners.add(edl); + } + + public static void addSessionListener(SessionListener sl) { + globalListeners.add(sl); + } + + public static void removeSessionListeners() { + globalListeners.clear(); + } /** * Sets the flag to isolate or inter-connects changes in all instances of @@ -547,5 +611,35 @@ public void setSingleStatement(boolean singleStatement) { this.addHierarchyListener(statementListener); } } - -} + + /* + Serialization + */ + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + } + + public InterpolationDataset getDensityDataset() { + return rhoDataset; + } + + public InterpolationDataset getSpecificHeatDataset() { + return cpDataset; + } + + public void setDensityDataset(InterpolationDataset dataset) { + this.rhoDataset = dataset; + this.externalListeners.stream().forEach(l -> l.onDensityDataLoaded()); + evaluate(); + } + + public void setSpecificHeatDataset(InterpolationDataset dataset) { + this.cpDataset = dataset; + this.externalListeners.stream().forEach(l -> l.onSpecificHeatDataLoaded()); + evaluate(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java index 5bbdafa3..b1f2e4a1 100644 --- a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java +++ b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java @@ -1,8 +1,10 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.LogEntry; -public interface DataCollectionListener { +public interface DataCollectionListener extends Serializable { public void onDataCollected(LogEntry e); + } diff --git a/src/main/java/pulse/tasks/listeners/LogEntryListener.java b/src/main/java/pulse/tasks/listeners/LogEntryListener.java index 660ce187..e2d03925 100644 --- a/src/main/java/pulse/tasks/listeners/LogEntryListener.java +++ b/src/main/java/pulse/tasks/listeners/LogEntryListener.java @@ -1,9 +1,10 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -public interface LogEntryListener { +public interface LogEntryListener extends Serializable { public void onNewEntry(LogEntry e); diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java index 54077573..edd94e99 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java @@ -1,8 +1,9 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.processing.ResultFormat; -public class ResultFormatEvent { +public class ResultFormatEvent implements Serializable { private ResultFormat rf; diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java index bfa0c1c4..732563c2 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java @@ -1,6 +1,8 @@ package pulse.tasks.listeners; -public interface ResultFormatListener { +import java.io.Serializable; + +public interface ResultFormatListener extends Serializable { public void resultFormatChanged(ResultFormatEvent rfe); diff --git a/src/main/java/pulse/tasks/listeners/SessionListener.java b/src/main/java/pulse/tasks/listeners/SessionListener.java new file mode 100644 index 00000000..30d13009 --- /dev/null +++ b/src/main/java/pulse/tasks/listeners/SessionListener.java @@ -0,0 +1,7 @@ +package pulse.tasks.listeners; + +public interface SessionListener { + + public void onNewSessionLoaded(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java index 61b767ca..3d9ba946 100644 --- a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java +++ b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java @@ -1,8 +1,9 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.StateEntry; -public interface StatusChangeListener { +public interface StatusChangeListener extends Serializable { public void onStatusChange(StateEntry e); } diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 0c232abd..f234b6a4 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -21,7 +21,6 @@ public Identifier getId() { } public enum State { - /** * Indicates a task has been added to the repository. */ @@ -59,6 +58,10 @@ public enum State { * Indicates the task has discarded superfluous calculations. */ BEST_MODEL_SELECTED, + /** + * A new state has been loaded. + */ + NEW_STATE, /** * The repository has been shut down/ */ @@ -66,4 +69,4 @@ public enum State { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java index d30cf038..639d9892 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java @@ -1,6 +1,9 @@ package pulse.tasks.listeners; -public interface TaskRepositoryListener { +import java.io.Serializable; + +public interface TaskRepositoryListener extends Serializable { public void onTaskListChanged(TaskRepositoryEvent e); -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java index 89ed0659..c2ce45f9 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java @@ -4,11 +4,6 @@ public class TaskSelectionEvent extends EventObject { - /** - * - */ - private static final long serialVersionUID = 4278832926994139917L; - public TaskSelectionEvent(Object source) { super(source); // TODO Auto-generated constructor stub diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java index fb422b47..a4c3f1a5 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java @@ -1,7 +1,9 @@ package pulse.tasks.listeners; -public interface TaskSelectionListener { +import java.io.Serializable; + +public interface TaskSelectionListener extends Serializable { public void onSelectionChanged(TaskSelectionEvent e); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java index f128837b..966351a9 100644 --- a/src/main/java/pulse/tasks/logs/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -1,5 +1,6 @@ package pulse.tasks.logs; +import java.io.Serializable; import java.util.concurrent.ExecutorService; import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; @@ -7,9 +8,9 @@ import static pulse.tasks.logs.Status.DONE; import pulse.util.Descriptive; -public abstract class AbstractLogger implements Descriptive { +public abstract class AbstractLogger implements Descriptive, Serializable { - private final ExecutorService updateExecutor; + private ExecutorService updateExecutor; public AbstractLogger() { updateExecutor = newSingleThreadExecutor(); @@ -27,7 +28,7 @@ public synchronized void update() { if (log.isStarted()) { post(log.lastEntry()); } - + } public ExecutorService getUpdateExecutor() { diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index f6785bdf..f87f078b 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -35,12 +35,14 @@ public String toString() { for (ImmutablePair key : map.keySet()) { sb.append("
"); sb.append(def(key.getFirst().getKeyword()).getAbbreviation(false)); - if(key.getFirst().getIndex() > 0) + if (key.getFirst().getIndex() > 0) { sb.append(" - ").append(key.getFirst().getIndex()); + } sb.append(""); sb.append(def(key.getSecond().getKeyword()).getAbbreviation(false)); - if(key.getSecond().getIndex() > 0) + if (key.getSecond().getIndex() > 0) { sb.append(" - ").append(key.getSecond().getIndex()); + } sb.append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 94b87746..45532729 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -22,6 +22,7 @@ */ public class DataLogEntry extends LogEntry { + private static final long serialVersionUID = -8995240410369870205L; private List entry; /** @@ -127,4 +128,4 @@ public String toString() { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index acaf6dfd..af2854e1 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -42,22 +42,17 @@ public enum Details { PARAMETER_VALUES_NOT_SENSIBLE, MAX_ITERATIONS_REACHED, ABNORMAL_DISTRIBUTION_OF_RESIDUALS, - /** - * Indicates that the result table had not been updated, as the selected - * model produced results worse than expected by the model selection criterion. + * Indicates that the result table had not been updated, as the selected + * model produced results worse than expected by the model selection + * criterion. */ - CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED, - - /** - * Indicates that the result table had been updated, as the current - * model selection criterion showed better result than already present. + * Indicates that the result table had been updated, as the current model + * selection criterion showed better result than already present. */ - BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED, - SOLVER_ERROR; @Override diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index b4c74367..acdcde6c 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -11,6 +11,8 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.listeners.TaskRepositoryEvent.State; import pulse.ui.Messages; import pulse.util.Group; @@ -21,13 +23,15 @@ */ public class Log extends Group { + private static final long serialVersionUID = 420096365502122145L; private List logEntries; private LocalTime start; private LocalTime end; - private final Identifier id; - private final List listeners; - private static boolean graphical = true; + private Identifier id; private boolean finished; + private transient List listeners; + + private static boolean graphical = true; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -44,8 +48,37 @@ public Log(SearchTask task) { id = task.getIdentifier(); this.logEntries = new CopyOnWriteArrayList<>(); + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); listeners = new CopyOnWriteArrayList<>(); + var instance = TaskManager.getManagerInstance(); + var existingTask = instance.getTask(id); + + if (existingTask != null) { + //task already exists - add listeners nwo + doAddListeners(existingTask); + } else { + //wait until task is added into repository + instance.addTaskRepositoryListener(event -> { + + if (event.getState() == State.TASK_ADDED && event.getId().equals(id)) { + + var task = TaskManager.getManagerInstance().getTask(id); + doAddListeners(task); + } + } + ); + + } + + } + + private void doAddListeners(SearchTask task) { task.addTaskListener(le -> { /** @@ -60,24 +93,23 @@ public Log(SearchTask task) { task.addStatusChangeListener((StateEntry e) -> { logEntries.add(e); - + if (e.getStatus() == Status.IN_PROGRESS) { start = e.getTime(); end = null; } else { end = e.getTime(); } - + notifyListeners(e); - + if (e.getState() == Status.DONE) { logFinished(); } } /** * Do these actions every time the task status has changed. - */ + */ ); - } private void logFinished() { @@ -112,7 +144,7 @@ public final Identifier getIdentifier() { public boolean isStarted() { return logEntries.size() > 0; } - + public boolean isFinished() { return finished; } @@ -195,16 +227,18 @@ public static boolean isGraphicalLog() { public static void setGraphicalLog(boolean verbose) { Log.graphical = verbose; } - + /** - * Time taken where the first array element contains seconds [0] and the second contains milliseconds [1]. - * @return an array of long values that sum um to the time taken to process a task + * Time taken where the first array element contains seconds [0] and the + * second contains milliseconds [1]. + * + * @return an array of long values that sum um to the time taken to process + * a task */ - public long[] timeTaken() { var seconds = SECONDS.between(getStart(), getEnd()); var ms = MILLIS.between(getStart(), getEnd()) - 1000L * seconds; - return new long[] {seconds, ms}; + return new long[]{seconds, ms}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index f311fd16..6a6d6442 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -1,9 +1,9 @@ package pulse.tasks.logs; +import java.io.Serializable; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Objects; -import pulse.Response; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -18,12 +18,13 @@ *

* */ -public class LogEntry { +public class LogEntry implements Serializable { + private static final long serialVersionUID = -6797821686964650045L; private final Identifier identifier; private final LocalTime time; private final LogEntry previous; - + /** *

* Creates a {@code LogEntry} from this {@code SearchTask}. The data of the @@ -37,14 +38,13 @@ public LogEntry(SearchTask t) { time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); var list = t.getLog().getLogEntries(); - if(list != null && !list.isEmpty()) { + if (list != null && !list.isEmpty()) { previous = list.get(list.size() - 1); - } - else { + } else { previous = null; } } - + public LogEntry getPreviousEntry() { return previous; } @@ -57,4 +57,4 @@ public LocalTime getTime() { return time; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index 17333a67..e2ea9a55 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -8,6 +8,7 @@ public class StateEntry extends LogEntry { + private static final long serialVersionUID = 8380229394939453079L; private Status status; public StateEntry(SearchTask task, Status status) { @@ -41,7 +42,7 @@ public String toString() { if (status.getDetails() != NONE) { sb.append(" due to " + status.getDetails() + ""); } - if(status.getDetailedMessage().length() > 0) { + if (status.getDetailedMessage().length() > 0) { sb.append(" Details: "); sb.append(status.getDetailedMessage()); } diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index e2d7017f..d479bc23 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -37,13 +37,10 @@ public enum Status { * Termination requested. */ AWAITING_TERMINATION(Color.DARK_GRAY), - /** * Task terminated */ - TERMINATED(Color.DARK_GRAY), - /** * Task has been queued and is waiting to be executed. */ @@ -82,11 +79,11 @@ public Details getDetails() { public void setDetails(Details details) { this.details = details; } - + public String getDetailedMessage() { return message; } - + public void setDetailedMessage(String str) { this.message = str; } @@ -133,20 +130,19 @@ public String getMessage() { } return sb.toString(); } - + public static Status troubleshoot(SolverException e1) { Objects.requireNonNull(e1, "Solver exception cannot be null when calling troubleshoot!"); Status status = null; - if(e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { + if (e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { status = Status.FAILED; status.setDetails(Details.SOLVER_ERROR); - status.setDetailedMessage(e1.getMessage()); - } - else { + status.setDetailedMessage(e1.getMessage()); + } else { status = Status.TIMEOUT; status.setDetails(Details.MAX_ITERATIONS_REACHED); } return status; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/processing/AverageResult.java b/src/main/java/pulse/tasks/processing/AverageResult.java index f5cdf85b..f6186b07 100644 --- a/src/main/java/pulse/tasks/processing/AverageResult.java +++ b/src/main/java/pulse/tasks/processing/AverageResult.java @@ -18,6 +18,8 @@ */ public class AverageResult extends AbstractResult { + private static final long serialVersionUID = 5279249996318155238L; + private final List results; public final static int SIGNIFICANT_FIGURES = 2; @@ -28,11 +30,10 @@ public class AverageResult extends AbstractResult { *

* It will also use the {@code resultFormat}. A method will be invoked to: * (a) calculate the mean values of the list of {@code NumericProperty} - * according to the {@code resultFormat}; (b) calculate the statistical error; - * (c) create a {@code BigDecimal} representation - * of the values and the errors, so that only {@value SIGNIFICANT_FIGURES} - * significant figures are left for consistency between the {@code value} - * and the {@code error}. + * according to the {@code resultFormat}; (b) calculate the statistical + * error; (c) create a {@code BigDecimal} representation of the values and + * the errors, so that only {@value SIGNIFICANT_FIGURES} significant figures + * are left for consistency between the {@code value} and the {@code error}. *

* * @param res a list of {@code AbstractResult}s that are going to be @@ -72,16 +73,16 @@ private void calculate() { if (!Double.isFinite(err[j])) { p = derive(key, av[j]); // ignore error as the value is not finite - } else if(NumericProperties.def(key).getValue() instanceof Double) { - var stdBig = new BigDecimal(err[j]); - var avBig = new BigDecimal(av[j]); + } else if (NumericProperties.def(key).getValue() instanceof Double) { + var stdBig = new BigDecimal(err[j]); + var avBig = new BigDecimal(av[j]); var error = stdBig.setScale( SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), RoundingMode.HALF_UP); - var mean = stdBig.precision() > 1 ? - avBig.setScale(error.scale(), RoundingMode.CEILING) + var mean = stdBig.precision() > 1 + ? avBig.setScale(error.scale(), RoundingMode.CEILING) : avBig; p = derive(key, mean.doubleValue()); @@ -89,8 +90,8 @@ private void calculate() { } else { //if integer - p = derive(key, (int)Math.round( av[j] ) ); - p.setError((int)Math.round( err[j] ) ); + p = derive(key, (int) Math.round(av[j])); + p.setError((int) Math.round(err[j])); } addProperty(p); diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index 90ef5bde..b257e59d 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -29,6 +29,10 @@ */ public class Buffer extends PropertyHolder { + /** + * + */ + private static final long serialVersionUID = 3613745885879508057L; private ParameterVector[] data; private double[] statistic; private static int size = (int) def(BUFFER_SIZE).getValue(); diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index fc0c0f79..9a79788f 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -18,14 +19,15 @@ import pulse.util.ImmutableDataEntry; import pulse.util.ImmutablePair; -public class CorrelationBuffer { +public class CorrelationBuffer implements Serializable { - private final List params; + private static final long serialVersionUID = 1672281370094463238L; + private List params; private static final Set> excludePairList; private static final Set excludeSingleList; private final static double DEFAULT_THRESHOLD = 1E-3; - + static { excludePairList = new HashSet<>(); excludeSingleList = new HashSet<>(); @@ -48,27 +50,28 @@ public void inflate(SearchTask t) { public void clear() { params.clear(); } - + /** * Truncates the buffer by excluding nearly-converged results. */ - private void truncate(double threshold) { int i = 0; int size = params.size(); - final double thresholdSq = threshold*threshold; - - for(i = 0; i < size - 1; i = i + 2) { - + final double thresholdSq = threshold * threshold; + + for (i = 0; i < size - 1; i = i + 2) { + Vector vParams = params.get(i).toVector(); Vector vPlusOneParams = params.get(i + 1).toVector(); Vector vDiff = vPlusOneParams.subtract(vParams); - if(vDiff.lengthSq()/vParams.lengthSq() < thresholdSq) - break; + if (vDiff.lengthSq() / vParams.lengthSq() < thresholdSq) { + break; + } + } + + for (int j = size - 1; j > i; j--) { + params.remove(j); } - - for(int j = size - 1; j > i; j--) - params.remove(j); } public Map, Double> evaluate(CorrelationTest t) { @@ -81,12 +84,12 @@ public Map, Double> evaluate(CorrelationTest } truncate(DEFAULT_THRESHOLD); - + List indices = params.get(0).getParameters().stream() .map(ps -> ps.getIdentifier()).collect(Collectors.toList()); Map map = indices.stream() .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble( - v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) + v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); @@ -96,26 +99,26 @@ public Map, Double> evaluate(CorrelationTest for (int i = 0; i < indicesSize; i++) { var iKey = indices.get(i).getKeyword(); - - if (!excludeSingleList.contains(iKey)) { - + + if (!excludeSingleList.contains(iKey)) { + for (int j = i + 1; j < indicesSize; j++) { - + var jKey = indices.get(j).getKeyword(); - + pair = new ImmutablePair<>(iKey, jKey); - - if (!excludeSingleList.contains(jKey) - && !excludePairList.contains(pair)) { - - correlationMap.put( - new ImmutablePair<>(indices.get(i), indices.get(j)), - t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); - + + if (!excludeSingleList.contains(jKey) + && !excludePairList.contains(pair)) { + + correlationMap.put( + new ImmutablePair<>(indices.get(i), indices.get(j)), + t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } - + } - + } } @@ -132,7 +135,7 @@ public boolean test(CorrelationTest t) { } var values = map.values(); - + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 626e292b..083b82f2 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,6 +1,7 @@ package pulse.tasks.processing; import pulse.tasks.Calculation; +import pulse.tasks.Identifier; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -13,6 +14,12 @@ */ public class Result extends AbstractResult { + /** + * + */ + private static final long serialVersionUID = 471531411060979791L; + private Identifier id; + /** * Creates an individual {@code Result} related to the current state of * {@code task} using the specified {@code format}. @@ -29,14 +36,19 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); } - setParent((Calculation)task.getResponse()); + id = task.getIdentifier(); + setParent((Calculation) task.getResponse()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); - } public Result(Result r) { super(r); + id = r.getTaskIdentifier(); + } + + public Identifier getTaskIdentifier() { + return id; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultFormat.java b/src/main/java/pulse/tasks/processing/ResultFormat.java index 6611a84c..3c0cd56f 100644 --- a/src/main/java/pulse/tasks/processing/ResultFormat.java +++ b/src/main/java/pulse/tasks/processing/ResultFormat.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static pulse.properties.NumericProperties.def; @@ -11,6 +12,7 @@ import java.util.List; import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.tasks.listeners.ResultFormatEvent; import pulse.tasks.listeners.ResultFormatListener; @@ -22,7 +24,12 @@ * characters. *

*/ -public class ResultFormat { +public class ResultFormat implements Serializable { + + /** + * + */ + private static final long serialVersionUID = -3155104011585735097L; private List nameMap; @@ -45,15 +52,22 @@ private ResultFormat() { private ResultFormat(List keys) { nameMap = new ArrayList<>(); - keys.forEach(key -> - nameMap.add(key) + keys.forEach(key + -> nameMap.add(key) ); + TaskManager.addSessionListener(() -> format = this); } - + public static void addResultFormatListener(ResultFormatListener rfl) { listeners.add(rfl); } + public static void removeListeners() { + if (listeners != null) { + listeners.clear(); + } + } + public static ResultFormat generateFormat(List keys) { format = new ResultFormat(keys); @@ -161,4 +175,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultStatistics.java b/src/main/java/pulse/tasks/processing/ResultStatistics.java index 807b2f57..d6d09118 100644 --- a/src/main/java/pulse/tasks/processing/ResultStatistics.java +++ b/src/main/java/pulse/tasks/processing/ResultStatistics.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -16,14 +17,15 @@ * * @author Artem Lunev */ -class ResultStatistics { +class ResultStatistics implements Serializable { + private static final long serialVersionUID = 4617029204359661289L; private double[] av; private double[] err; /** * Confidence level of {@value CONFIDENCE_LEVEL} for error calculation using - * t-distribution quantiles. + * t-distribution quantiles. */ public final static double CONFIDENCE_LEVEL = 0.95; @@ -41,8 +43,9 @@ public ResultStatistics() { * assuming a {@value CONFIDENCE_LEVEL} confidence level, by calculating a * standard deviation for each {@code NumericPropertyKeyword} and * multiplying the result by the quantile value of the - * t-distribution. The inverse cumulative distribution function of - * Student distribution is calculated using {@code ApacheCommonsMath} library. + * t-distribution. The inverse cumulative distribution function of + * Student distribution is calculated using {@code ApacheCommonsMath} + * library. * * @param results a list of individual (or average) results to be processed */ @@ -60,23 +63,21 @@ public void process(List results) { The number of elements in the parameter list. This ASSUMES that the input list contains results with the same number of output parameters! */ - StandardDeviation sd = new StandardDeviation(true); //bias-corrected sd double sqrtn = Math.sqrt(results.size()); //calculate average values - var stats = ResultFormat.getInstance().getKeywords().stream() - .map(key -> map.get(key)) //preserve the original order of keywods - .map(c -> { - double mean = openStream(c).average().orElse(0.0); //fail-safe, in case if avg is undefined - return new ImmutablePair( - mean, //the mean value - sd.evaluate(openStream(c).toArray(), mean) //that would be the sample standard deviation - / sqrtn //however, since we are calculating the std of the MEAN, - //we need to divide the result by sqrtn - ); - }).collect(Collectors.toList()); + .map(key -> map.get(key)) //preserve the original order of keywods + .map(c -> { + double mean = openStream(c).average().orElse(0.0); //fail-safe, in case if avg is undefined + return new ImmutablePair( + mean, //the mean value + sd.evaluate(openStream(c).toArray(), mean) //that would be the sample standard deviation + / sqrtn //however, since we are calculating the std of the MEAN, + //we need to divide the result by sqrtn + ); + }).collect(Collectors.toList()); av = stats.stream().mapToDouble(pair -> pair.getFirst()).toArray(); //store mean values @@ -90,7 +91,7 @@ public void process(List results) { ).toArray(); //store errors } - + private DoubleStream openStream(List input) { return input.stream().mapToDouble(d -> d); } diff --git a/src/main/java/pulse/ui/ColorGenerator.java b/src/main/java/pulse/ui/ColorGenerator.java index 2e2388df..ac56b59f 100644 --- a/src/main/java/pulse/ui/ColorGenerator.java +++ b/src/main/java/pulse/ui/ColorGenerator.java @@ -5,10 +5,9 @@ import static java.awt.Color.GREEN; import static java.awt.Color.RED; import java.util.ArrayList; -import java.util.Collections; public class ColorGenerator { - + private Color a, b, c; public ColorGenerator() { @@ -16,30 +15,30 @@ public ColorGenerator() { b = GREEN; c = BLUE; } - + public Color[] random(int number) { var list = new ArrayList(); - for(int i = 0; i < number; i++) { - list.add(sample(i/(double)(number - 1))); + for (int i = 0; i < number; i++) { + list.add(sample(i / (double) (number - 1))); } //Collections.shuffle(list); return list.toArray(new Color[list.size()]); } public Color sample(double seed) { - return seed < 0.5 ? - mix(a, b, (float) (seed*2)) - : mix(b, c,(float)((seed-0.5)*2)); + return seed < 0.5 + ? mix(a, b, (float) (seed * 2)) + : mix(b, c, (float) ((seed - 0.5) * 2)); } - + private static Color mix(Color a, Color b, float ratio) { float[] aRgb = a.getRGBComponents(null); float[] bRgb = b.getRGBComponents(null); float[] cRgb = new float[3]; - for(int i = 0; i < cRgb.length; i++) { + for (int i = 0; i < cRgb.length; i++) { cRgb[i] = aRgb[i] * (1.0f - ratio) + bRgb[i] * ratio; } return new Color(cRgb[0], cRgb[1], cRgb[2]); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index ee238ac1..3744e9e9 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; +import pulse.util.Serializer; /** *

@@ -51,7 +52,7 @@ private Launcher() { */ public static void main(String[] args) { new Launcher(); - + if (!LOCK.exists()) { try { @@ -59,7 +60,7 @@ public static void main(String[] args) { } catch (IOException ex) { Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, "Unable to create lock file", ex); } - + LOCK.deleteOnExit(); splashScreen(); @@ -148,7 +149,7 @@ private void createShutdownHook() { errorLog.delete(); } //delete lock explicitly on abnormal termination - if(LOCK.exists()) { + if (LOCK.exists()) { LOCK.delete(); } }; diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 602a3d63..f35d34db 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -14,66 +14,66 @@ import org.jfree.chart.plot.XYPlot; public abstract class AuxPlotter { - + private ChartPanel chartPanel; private JFreeChart chart; private XYPlot plot; - + public AuxPlotter() { //empty } - + public AuxPlotter(String xLabel, String yLabel) { - setChart( ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false) ); - - setPlot( chart.getXYPlot() ); + setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); + + setPlot(chart.getXYPlot()); chart.removeLegend(); setFonts(); } - + public final void setFonts() { var jlabel = new JLabel(); var label = jlabel.getFont().deriveFont(20f); var ticks = jlabel.getFont().deriveFont(16f); chart.getTitle().setFont(jlabel.getFont().deriveFont(20f)); - + if (plot instanceof CombinedDomainXYPlot) { var combinedPlot = (CombinedDomainXYPlot) plot; - combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot)sp, label, ticks)); + combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot) sp, label, ticks)); } else { - setFontsForPlot(plot, label, ticks); + setFontsForPlot(plot, label, ticks); } - + } - + private void setFontsForPlot(XYPlot p, Font label, Font ticks) { var foreColor = UIManager.getColor("Label.foreground"); var domainAxis = p.getDomainAxis(); - Chart.setAxisFontColor(domainAxis, foreColor); + Chart.setAxisFontColor(domainAxis, foreColor); var rangeAxis = p.getRangeAxis(); Chart.setAxisFontColor(rangeAxis, foreColor); } - + public abstract void plot(T t); - + public final ChartPanel getChartPanel() { return chartPanel; } - + public final JFreeChart getChart() { return chart; } - + public final XYPlot getPlot() { return plot; } - + public final void setPlot(XYPlot plot) { this.plot = plot; plot.setBackgroundPaint(chart.getBackgroundPaint()); } - + public final void setChart(JFreeChart chart) { this.chart = chart; var color = UIManager.getLookAndFeelDefaults().getColor("TextPane.background"); @@ -81,5 +81,5 @@ public final void setChart(JFreeChart chart) { chartPanel = new ChartPanel(chart); this.plot = chart.getXYPlot(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 7fe372e5..0801ac8f 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -19,6 +19,7 @@ import java.awt.Color; import java.awt.Font; import java.awt.event.MouseEvent; +import java.io.Serializable; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -52,10 +53,10 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.ui.components.listeners.MouseOnMarkerListener; -public class Chart { +public class Chart implements Serializable { - private ChartPanel chartPanel; - private JFreeChart chart; + private final ChartPanel chartPanel; + private final JFreeChart chart; private XYPlot plot; private float opacity = 0.15f; @@ -151,7 +152,7 @@ private void setFonts() { setAxisFontColor(plot.getDomainAxis(), foreColor); setAxisFontColor(plot.getRangeAxis(), foreColor); } - + public static void setAxisFontColor(Axis axis, Color color) { axis.setLabelPaint(color); axis.setTickLabelPaint(color); diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index 34f9f970..2baa8148 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -9,7 +9,6 @@ import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.concurrent.Executors; import javax.swing.JFileChooser; @@ -17,8 +16,6 @@ import javax.swing.filechooser.FileNameExtensionFilter; import pulse.input.ExperimentalData; -import pulse.input.InterpolationDataset; -import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; @@ -111,7 +108,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = ( (Calculation) task.getResponse() ).getProblem(); + var p = ((Calculation) task.getResponse()).getProblem(); if (p != null) { p.retrieveData(data); } @@ -175,14 +172,25 @@ public static void loadPulseDialog() { * * @param f a {@code File} containing a property specified by the * {@code type} - * @param type the type of the loaded data * @throws IOException if file cannot be read * @see pulse.tasks.TaskManager.evaluate() */ - public static void load(StandartType type, File f) throws IOException { - Objects.requireNonNull(f); - InterpolationDataset.setDataset(read(datasetReaders(), f), type); - TaskManager.getManagerInstance().evaluate(); + public static void loadDensity(File f) throws IOException { + TaskManager.getManagerInstance().setDensityDataset(read(datasetReaders(), f)); + } + + /** + * Uses the {@code ReaderManager} to create an {@code InterpolationDataset} + * from data stored in {@code f} and updates the associated properties of + * each task. + * + * @param f a {@code File} containing a property specified by the + * {@code type} + * @throws IOException if file cannot be read + * @see pulse.tasks.TaskManager.evaluate() + */ + public static void loadSpecificHeat(File f) throws IOException { + TaskManager.getManagerInstance().setSpecificHeatDataset(read(datasetReaders(), f)); } private static List userInput(String descriptor, List extensions) { diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index bc69d235..1b643f53 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -49,10 +49,10 @@ public void post(LogEntry logEntry) { double iteration = dle.getData().stream() .filter(p -> p.getIdentifier().getKeyword() == ITERATION) .findAny().get().getApparentValue(); - + chart.changeAxis(true); chart.plot((DataLogEntry) logEntry, iteration); - + } } @@ -71,13 +71,13 @@ public void postAll() { chart.clear(); chart.changeAxis(false); chart.plot(log); - + if (task.getStatus() == DONE) { printTimeTaken(log); } } - + } } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index e4c2e0de..0ac58e52 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -67,7 +67,7 @@ private void addListeners() { }); instance.addSelectionListener(e -> { - var current = ( (Calculation) instance.getSelectedTask().getResponse() ).getProblem(); + var current = ((Calculation) instance.getSelectedTask().getResponse()).getProblem(); // select appropriate problem type from list setSelectedProblem(current); diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index 05413b65..21577895 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -155,11 +155,6 @@ public TableCellEditor getCellEditor(int row, int column) { return new DefaultCellEditor((JComboBox) value); } - if (value instanceof Enum) { - return new DefaultCellEditor( - new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); - } - if (value instanceof InstanceDescriptor) { return new InstanceCellEditor((InstanceDescriptor) value); } @@ -187,7 +182,7 @@ public TableCellEditor getCellEditor(int row, int column) { value, false, false, row, column), ((Flag) value).getType()); } - return getDefaultEditor(value.getClass()); + return super.getCellEditor(row, column); } diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index 5b4151a2..b3c7d06f 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -60,8 +60,8 @@ public void plot(Calculation c) { double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); var pulseDataset = new XYSeriesCollection(); - - pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep()/20.0, + + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep() / 20.0, problem.getProperties().characteristicTime(), startTime)); getPlot().setDataset(0, pulseDataset); @@ -70,15 +70,15 @@ public void plot(Calculation c) { private static XYSeries series(Pulse pulse, double dx, double timeFactor, double startTime) { var series = new XYSeries(pulse.getPulseShape().toString()); var pulseShape = pulse.getPulseShape(); - + double timeLimit = pulseShape.getPulseWidth(); - double x = startTime/timeFactor; + double x = startTime / timeFactor; series.add(TO_MILLIS * (startTime - dx * timeFactor / 100.), 0.0); - series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 100.)), 0.0); + series.add(TO_MILLIS * (startTime + timeFactor * (timeLimit + dx / 100.)), 0.0); - for (int i = 0, numPoints = (int) (timeLimit/dx); i < numPoints; i++) { - series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime/timeFactor)); + for (int i = 0, numPoints = (int) (timeLimit / dx); i < numPoints; i++) { + series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime / timeFactor)); x += dx; } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 31d05441..caec94a7 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -30,6 +30,8 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.AbstractButton; import javax.swing.ButtonGroup; @@ -39,6 +41,7 @@ import javax.swing.JMenuItem; import javax.swing.JRadioButtonMenuItem; import javax.swing.JSeparator; +import pulse.util.Serializer; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; @@ -80,25 +83,39 @@ public class PulseMainMenu extends JMenuBar { private List exitListeners; public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> - Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); + bufferDialog.setConfirmAction(() + -> Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); initComponents(); initListeners(); assignMenuFunctions(); - addListeners(); + reset(); listeners = new ArrayList<>(); exitListeners = new ArrayList<>(); } - private void addListeners() { - getManagerInstance().addTaskRepositoryListener(event -> { + private void enableIfNeeded() { + var instance = getManagerInstance(); + boolean enabled = instance.getTaskList().size() > 0; + loadMetadataItem.setEnabled(enabled); + loadPulseItem.setEnabled(enabled); + modelSettingsItem.setEnabled(enabled); + searchSettingsItem.setEnabled(enabled); + } + + public final void reset() { + var instance = getManagerInstance(); + + instance.addTaskRepositoryListener(event -> { if (event.getState() == TASK_ADDED) { exportCurrentItem.setEnabled(true); exportAllItem.setEnabled(true); } }); + + enableIfNeeded(); + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> enableIfNeeded()); } private void initListeners() { @@ -171,7 +188,35 @@ private void initComponents() { fileMenu.add(exportCurrentItem); fileMenu.add(exportAllItem); fileMenu.add(new JSeparator()); + + var serializeItem = new JMenuItem("Save Session..."); + fileMenu.add(serializeItem); + serializeItem.addActionListener(e -> { + try { + Serializer.serialize(); + } catch (IOException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } + }); + var deserializeItem = new JMenuItem("Load Session..."); + + fileMenu.add(deserializeItem); + deserializeItem.addActionListener(e -> { + + try { + Serializer.deserialize(); + } catch (IOException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } + + }); + fileMenu.add(exitItem); + add(fileMenu); settingsMenu.add(modelSettingsItem); @@ -246,8 +291,8 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> - ( (Calculation) t.getResponse() ).initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t + -> ((Calculation) t.getResponse()).initOptimiser()); } }); @@ -275,29 +320,30 @@ private JMenu initAnalysisSubmenu() { JRadioButtonMenuItem corrItem = null; var ct = CorrelationTest.init(); - + for (var corrName : allDescriptors(CorrelationTest.class)) { corrItem = new JRadioButtonMenuItem(corrName); corrItems.add(corrItem); correlationsSubMenu.add(corrItem); - - if(ct.getDescriptor().equalsIgnoreCase(corrName)) + + if (ct.getDescriptor().equalsIgnoreCase(corrName)) { corrItem.setSelected(true); - + } + corrItem.addItemListener(e -> { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); var allTests = Reflexive.instancesOf(CorrelationTest.class); - var optionalTest = allTests.stream().filter(test -> - test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); - - if(optionalTest.isPresent()) { + var optionalTest = allTests.stream().filter(test + -> test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); + + if (optionalTest.isPresent()) { CorrelationTest.getTestDescriptor() .setSelectedDescriptor(optionalTest.get().getClass().getSimpleName()); getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); } - + } }); @@ -337,20 +383,6 @@ private void assignMenuFunctions() { changeDialog.setVisible(true); }); - getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (getManagerInstance().getTaskList().size() > 0) { - loadMetadataItem.setEnabled(true); - loadPulseItem.setEnabled(true);; - modelSettingsItem.setEnabled(true); - searchSettingsItem.setEnabled(true); - } else { - loadMetadataItem.setEnabled(false); - loadPulseItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - } - }); - exportAllItem.setEnabled(true); exportAllItem.addActionListener(e -> { exportDialog.setLocationRelativeTo(null); @@ -370,6 +402,16 @@ private void assignMenuFunctions() { } + public void removeAllListeners() { + if (listeners != null) { + listeners.clear(); + } + if (exitListeners != null) { + exitListeners.clear(); + } + + } + public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { listeners.add(l); } diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java index a146e556..dbfcd7c7 100644 --- a/src/main/java/pulse/ui/components/RangeTextFields.java +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -3,6 +3,7 @@ import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.io.Serializable; import java.text.DecimalFormat; import java.text.ParseException; import java.util.logging.Level; @@ -19,20 +20,19 @@ import pulse.ui.components.panels.ChartToolbar; /** - * Two JFormattedTextFields used to display the range of the currently - * selected task. + * Two JFormattedTextFields used to display the range of the currently selected + * task. */ -public final class RangeTextFields { +public final class RangeTextFields implements Serializable { private JFormattedTextField lowerLimitField; private JFormattedTextField upperLimitField; /** - * Creates textfield objects, which may be accessed with getters from this instance. - * Additionally, binds listeners to all current and future tasks in order to observe - * and reflect upon the changes with the textfield. + * Creates textfield objects, which may be accessed with getters from this + * instance. Additionally, binds listeners to all current and future tasks + * in order to observe and reflect upon the changes with the textfield. */ - public RangeTextFields() { initTextFields(); @@ -53,18 +53,17 @@ public RangeTextFields() { //when a new task is selected instance.addSelectionListener((TaskSelectionEvent e) -> { var task = instance.getSelectedTask(); - var segment = ( (ExperimentalData) task.getInput() ).getRange().getSegment(); + var segment = ((ExperimentalData) task.getInput()).getRange().getSegment(); //update the textfield values lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); }); } - + /* Creates a formatter for the textfields - */ - + */ private NumberFormatter initFormatter() { var format = new DecimalFormat(); format.setMinimumFractionDigits(1); @@ -83,18 +82,19 @@ private NumberFormatter initFormatter() { formatter.setOverwriteMode(true); return formatter; } - + /** * Checks if the candidate value produced by the formatter is sensible, i.e. - * if it lies within the bounds defined in the Range class. + * if it lies within the bounds defined in the Range class. + * * @param jtf the textfield containing the candidate value as text - * @param upperBound whether the upper bound is checked ({@code false} if the lower bound is checked) + * @param upperBound whether the upper bound is checked ({@code false} if + * the lower bound is checked) * @return {@code true} if the edit may proceed */ - private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { - Range range = ( (ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() - .getInput() ).getRange(); + Range range = ((ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput()).getRange(); double candidateValue = 0.0; try { @@ -107,12 +107,11 @@ private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) return range.boundLimits(upperBound).contains(candidateValue); } - + /** - * Creates a formatter and initialised the textfields, setting up rules - * for edit validation. + * Creates a formatter and initialised the textfields, setting up rules for + * edit validation. */ - private void initTextFields() { var instance = TaskManager.getManagerInstance(); @@ -153,7 +152,7 @@ public void commitEdit() throws ParseException { } }; - + var fl = new FocusListener() { @Override public void focusGained(FocusEvent arg0) { @@ -189,11 +188,11 @@ private void updateTextfieldsFromTask(SearchTask newTask) { } }); } - + public JFormattedTextField getLowerLimitField() { return lowerLimitField; } - + public JFormattedTextField getUpperLimitField() { return upperLimitField; } diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 7543707d..2f311088 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -19,7 +19,7 @@ public ResidualsChart(String xLabel, String yLabel) { setFonts(); binCount = 32; } - + @Override public void plot(ResidualStatistic stat) { requireNonNull(stat); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index d718926f..e2b2b93e 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -44,7 +44,6 @@ public ResultTable(ResultFormat fmt) { var model = new ResultTableModel(fmt); setModel(model); - setRowSorter(sorter()); model.addListener(event -> setRowSorter(sorter())); @@ -60,6 +59,12 @@ public ResultTable(ResultFormat fmt) { headersSize.height = RESULTS_HEADER_HEIGHT; getTableHeader().setPreferredSize(headersSize); + resetSession(); + } + + public void resetSession() { + ((ResultTableModel)getModel()).resetSession(); + /* * Listen to TaskTable and select appropriate results when task selection * changes @@ -76,10 +81,10 @@ public ResultTable(ResultFormat fmt) { * Automatically add finished tasks to this result table Automatically remove * results if corresponding task is removed */ - TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { var t = instance.getTask(e.getId()); - - if(t != null) { + + if (t != null) { var cc = (Calculation) t.getResponse(); @@ -112,10 +117,12 @@ public ResultTable(ResultFormat fmt) { default: break; } - + } }); - + + setRowSorter(sorter()); + } public void clear() { @@ -124,7 +131,8 @@ public void clear() { } private TableRowSorter sorter() { - var sorter = new TableRowSorter((ResultTableModel) getModel()); + var model = (ResultTableModel) getModel(); + var sorter = new TableRowSorter(model); var list = new ArrayList(); Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index c29818f3..621210cb 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -27,6 +27,8 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import pulse.input.ExperimentalData; import pulse.problem.schemes.solvers.Solver; @@ -61,10 +63,9 @@ public TaskPopupMenu() { ICON_GRAPH); itemExtendedChart.addActionListener(e -> plot(true)); - var instance = TaskManager.getManagerInstance(); - var itemShowMeta = new JMenuItem("Show metadata", ICON_METADATA); itemShowMeta.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { showMessageDialog(getWindowAncestor((Component) e.getSource()), @@ -79,13 +80,8 @@ public TaskPopupMenu() { var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); - instance.addSelectionListener(event -> { - instance.getSelectedTask().checkProblems(); - var details = instance.getSelectedTask().getStatus().getDetails(); - itemShowStatus.setEnabled((details != null) & (details != NONE)); - }); - itemShowStatus.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t != null) { var d = t.getStatus().getDetails(); @@ -96,6 +92,7 @@ public TaskPopupMenu() { var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ itemExecute.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { showMessageDialog(getWindowAncestor((Component) e.getSource()), @@ -130,11 +127,12 @@ public TaskPopupMenu() { var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); - itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); + itemReset.addActionListener((ActionEvent arg0) -> TaskManager.getManagerInstance().getSelectedTask().clear()); var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); itemGenerateResult.addActionListener((ActionEvent arg0) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { return; @@ -152,8 +150,32 @@ public TaskPopupMenu() { itemViewStored.setEnabled(false); - itemViewStored.addActionListener(arg0 -> instance.notifyListeners( - new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier()))); + itemViewStored.addActionListener(arg0 -> { + var instance = TaskManager.getManagerInstance(); + instance.notifyListeners( + new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier())); + }); + + this.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + var instance = TaskManager.getManagerInstance(); + instance.getSelectedTask().checkProblems(); + var details = instance.getSelectedTask().getStatus().getDetails(); + itemShowStatus.setEnabled((details != null) & (details != NONE)); + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + //. + } + + }); add(itemShowMeta); add(itemShowStatus); diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index c7cff7b6..e69ba896 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -56,32 +56,11 @@ public TaskTable() { setSelectionMode(SINGLE_INTERVAL_SELECTION); setShowHorizontalLines(false); - var model = new TaskTableModel(); - setModel(model); - var th = new TableHeader(getColumnModel()); setTableHeader(th); - getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); - setAutoCreateRowSorter(true); - var sorter = new TableRowSorter(); - sorter.setModel(model); - var list = new ArrayList(); - - for (var i = 0; i < this.getModel().getColumnCount(); i++) { - list.add(new RowSorter.SortKey(i, ASCENDING)); - if (i == TaskTableModel.STATUS_COLUMN) { - sorter.setComparator(i, statusComparator); - } else { - sorter.setComparator(i, numericComparator); - } - } - - sorter.setSortKeys(list); - setRowSorter(sorter); - initListeners(); menu = new TaskPopupMenu(); @@ -89,8 +68,6 @@ public TaskTable() { public void initListeners() { - var instance = TaskManager.getManagerInstance(); - /* * mouse listener */ @@ -99,6 +76,8 @@ public void initListeners() { @Override public void mouseClicked(MouseEvent e) { + var instance = TaskManager.getManagerInstance(); + if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { var task = instance.getSelectedTask(); menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); @@ -116,21 +95,50 @@ public void mouseClicked(MouseEvent e) { var reference = this; lsm.addListSelectionListener((ListSelectionEvent e) -> { + var instance = TaskManager.getManagerInstance(); if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); instance.selectTask(id, reference); } }); + resetSession(); + + } + + public void resetSession() { + var model = new TaskTableModel(); + setModel(model); + + var sorter = new TableRowSorter(); + sorter.setModel(model); + var list = new ArrayList(); + + for (var i = 0; i < this.getModel().getColumnCount(); i++) { + list.add(new RowSorter.SortKey(i, ASCENDING)); + if (i == TaskTableModel.STATUS_COLUMN) { + sorter.setComparator(i, statusComparator); + } else { + sorter.setComparator(i, numericComparator); + } + } + + sorter.setSortKeys(list); + setRowSorter(sorter); + + var instance = TaskManager.getManagerInstance(); instance.addSelectionListener((TaskSelectionEvent e) -> { + // simply ignore call if event is triggered by taskTable - if (e.getSource() instanceof TaskTable) { + if (e.getSource() == this) { return; } var id = instance.getSelectedTask().getIdentifier(); Identifier idFromTable = null; int i = 0; + + clearSelection(); for (i = 0; i < getRowCount() && !id.equals(idFromTable); i++) { idFromTable = (Identifier) getValueAt(i, 0); @@ -139,9 +147,7 @@ public void mouseClicked(MouseEvent e) { if (i < getRowCount()) { setRowSelectionInterval(i, i); } - clearSelection(); }); - } @Override diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index f1b7dfcd..de791299 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -23,12 +23,12 @@ public class TextLogPane extends AbstractLogger { private final JEditorPane editor; private final JScrollPane pane; - + public TextLogPane() { editor = new JEditorPane(); editor.setContentType("text/html"); editor.setEditable(false); - ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + ((DefaultCaret) editor.getCaret()).setUpdatePolicy(ALWAYS_UPDATE); pane = new JScrollPane(); pane.setViewportView(editor); } @@ -52,7 +52,6 @@ public void post(String text) { } } - public void printTimeTaken(Log log) { var time = log.timeTaken(); @@ -71,7 +70,7 @@ public void clear() { e.printStackTrace(); } } - + @Override public JComponent getGUIComponent() { return pane; @@ -82,4 +81,4 @@ public boolean isEmpty() { return editor.getDocument().getLength() < 1; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 5a06530c..8ef13f4b 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -28,9 +28,9 @@ public ExecutionButton() { setIcon(state.getIcon()); setToolTipText(state.getMessage()); - var instance = TaskManager.getManagerInstance(); - this.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); + /* * STOP PRESSED? */ @@ -62,6 +62,12 @@ public ExecutionButton() { } }); + resetSession(); + + } + + public void resetSession() { + var instance = TaskManager.getManagerInstance(); instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { switch (e.getState()) { case TASK_SUBMITTED: @@ -81,7 +87,6 @@ public ExecutionButton() { return; } }); - } public void setExecutionState(ExecutionState state) { diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index cad1066c..09733d9b 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -6,12 +6,8 @@ import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.getDataset; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.io.readers.ReaderManager.getDatasetExtensions; import static pulse.ui.Messages.getString; -import static pulse.ui.components.DataLoader.load; import java.awt.Color; import java.awt.Component; @@ -26,34 +22,34 @@ import org.apache.commons.math3.exception.OutOfRangeException; import pulse.input.InterpolationDataset; +import pulse.tasks.TaskManager; +import static pulse.ui.components.DataLoader.loadDensity; +import static pulse.ui.components.DataLoader.loadSpecificHeat; import pulse.util.ImageUtils; @SuppressWarnings("serial") public class LoaderButton extends JButton { - private InterpolationDataset.StandartType dataType; + private final DataType dataType; private static File dir; - private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); - private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); - public LoaderButton() { + public LoaderButton(DataType type) { super(); + this.dataType = type; init(); } - public LoaderButton(String str) { + public LoaderButton(DataType type, String str) { super(str); + this.dataType = type; init(); } - + public final void init() { - - InterpolationDataset.addListener(e -> { - if (dataType == e) { - highlight(false); - } - }); + highlight(false); addActionListener((ActionEvent arg0) -> { var fileChooser = new JFileChooser(); @@ -71,10 +67,10 @@ public final void init() { try { switch (dataType) { case HEAT_CAPACITY: - load(HEAT_CAPACITY, fileChooser.getSelectedFile()); + loadSpecificHeat(fileChooser.getSelectedFile()); break; case DENSITY: - load(DENSITY, fileChooser.getSelectedFile()); + loadDensity(fileChooser.getSelectedFile()); break; default: throw new IllegalStateException("Unrecognised type: " + dataType); @@ -96,7 +92,7 @@ public final void init() { getString("LoaderButton.OFRError"), //$NON-NLS-1$ ERROR_MESSAGE); } - var size = getDataset(dataType).getData().size(); + int size = getDataset().getData().size(); var label = ""; switch (dataType) { case HEAT_CAPACITY: @@ -115,19 +111,26 @@ public final void init() { showMessageDialog(getWindowAncestor((Component) arg0.getSource()), sb.toString(), "Data loaded", INFORMATION_MESSAGE); + highlight(false); }); } - - public void setDataType(InterpolationDataset.StandartType dataType) { - this.dataType = dataType; + + public InterpolationDataset getDataset() { + var i = TaskManager.getManagerInstance(); + return dataType == DataType.HEAT_CAPACITY ? + i.getSpecificHeatDataset() : i.getDensityDataset(); } - + public void highlight(boolean highlighted) { setBackground(highlighted ? HIGHLIGHTED : NOT_HIGHLIGHTED); } public void highlightIfNeeded() { - highlight(getDataset(dataType) == null); + highlight(getDataset() == null); + } + + public enum DataType { + HEAT_CAPACITY, DENSITY; } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 6f98ec37..4c363b56 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,6 +1,5 @@ package pulse.ui.components.controllers; - import java.awt.Component; import java.awt.Font; @@ -27,28 +26,23 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole int row, int column) { Component result = null; - + if (value instanceof Flag) { result = new IconCheckBox((boolean) ((Property) value).getValue()); ((IconCheckBox) result).setHorizontalAlignment(CENTER); - } - - else if (value instanceof PropertyHolder) { - var sb = new StringBuilder("Click to Edit/View "); - sb.append(((PropertyHolder) value).getSimpleName()); - sb.append("..."); - result = new JButton(sb.toString()); - ((JButton)result).setToolTipText(value.toString()); - ((JButton)result).setHorizontalAlignment(LEFT); - } - - else { + } else if (value instanceof PropertyHolder) { + var sb = new StringBuilder("Click to Edit/View "); + sb.append(((PropertyHolder) value).getSimpleName()); + sb.append("..."); + result = new JButton(sb.toString()); + ((JButton) result).setToolTipText(value.toString()); + ((JButton) result).setHorizontalAlignment(LEFT); + } else { result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } - + return result; - + } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index e9611d77..9f01e291 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -29,7 +29,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean if (e.getStateChange() == ItemEvent.SELECTED) { try { descriptor.attemptUpdate(e.getItem()); - } catch(NullPointerException npe) { + } catch (NullPointerException npe) { String text = "Error updating " + descriptor.getDescriptor(false) + ". Cannot be set to " + e.getItem(); System.out.println(text); @@ -37,7 +37,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean } } }); - + return combobox; } diff --git a/src/main/java/pulse/ui/components/controllers/NumberEditor.java b/src/main/java/pulse/ui/components/controllers/NumberEditor.java index c51120f2..3aa41496 100644 --- a/src/main/java/pulse/ui/components/controllers/NumberEditor.java +++ b/src/main/java/pulse/ui/components/controllers/NumberEditor.java @@ -61,10 +61,6 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/ public class NumberEditor extends DefaultCellEditor { - /** - * - */ - private static final long serialVersionUID = 1L; JFormattedTextField ftf; NumberFormat numberFormat; private boolean DEBUG = false; @@ -104,10 +100,6 @@ public NumberEditor(NumericProperty property) { // JFormattedTextField's focusLostBehavior property.) ftf.getInputMap().put(getKeyStroke(VK_ENTER, 0), "check"); ftf.getActionMap().put("check", new AbstractAction() { - /** - * - */ - private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index 5f05afc5..2d07657f 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -41,7 +41,6 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole result = superRenderer; } - result.setForeground(UIManager.getColor("List.foreground")); return result; diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java index 4827624b..dd621b96 100644 --- a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -8,7 +8,6 @@ import javax.swing.tree.DefaultTreeCellRenderer; //import com.alee.managers.icon.LazyIcon; - import pulse.problem.statements.Problem; import pulse.util.ImageUtils; import static pulse.util.ImageUtils.loadIcon; @@ -17,7 +16,7 @@ public class ProblemCellRenderer extends DefaultTreeCellRenderer { private static ImageIcon defaultIcon = loadIcon("leaf.png", 16); - + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index cfc884cf..402bc41a 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -17,7 +17,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - + return renderer; } diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index 33ccdcb8..8bc83691 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -34,11 +34,11 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole superRenderer.setForeground(((Status) value).getColor()); superRenderer.setFont(superRenderer.getFont().deriveFont(BOLD)); - ((JLabel)superRenderer).setHorizontalAlignment(JLabel.CENTER); + ((JLabel) superRenderer).setHorizontalAlignment(JLabel.CENTER); return superRenderer; - } + } return superRenderer; diff --git a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java index fe791695..121f70b6 100644 --- a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java @@ -3,7 +3,6 @@ public interface FrameVisibilityRequestListener { public void onProblemStatementShowRequest(); - public void onSearchSettingsShowRequest(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/listeners/LogListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java index 5a07f5dc..a498817a 100644 --- a/src/main/java/pulse/ui/components/listeners/LogListener.java +++ b/src/main/java/pulse/ui/components/listeners/LogListener.java @@ -3,6 +3,7 @@ public interface LogListener { public void onLogExportRequest(); + public void onLogModeChanged(boolean graphical); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java index 834dc1cc..b619cafe 100644 --- a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -29,12 +29,12 @@ public class MouseOnMarkerListener implements ChartMouseListener { private final MovableValueMarker lower; private final MovableValueMarker upper; - + private final Chart chart; private final double margin; - + private final static Cursor CROSSHAIR = new Cursor(Cursor.CROSSHAIR_CURSOR); - private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); + private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); public MouseOnMarkerListener(Chart chart, MovableValueMarker lower, MovableValueMarker upper, double margin) { this.chart = chart; @@ -51,33 +51,31 @@ public void chartMouseClicked(ChartMouseEvent arg0) { @Override public void chartMouseMoved(ChartMouseEvent arg0) { double xCoord = chart.xCoord(arg0.getTrigger()); - highlightMarker(xCoord); + highlightMarker(xCoord); } private void highlightMarker(double xCoord) { if (xCoord > (lower.getValue() - margin) & xCoord < (lower.getValue() + margin)) { - - lower.setState(MovableValueMarker.State.SELECTED); + + lower.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - - } - else if (xCoord > (upper.getValue() - margin) + + } else if (xCoord > (upper.getValue() - margin) & xCoord < (upper.getValue() + margin)) { - + upper.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - - } - else { - + + } else { + lower.setState(MovableValueMarker.State.IDLE); upper.setState(MovableValueMarker.State.IDLE); chart.getChartPanel().setCursor(CROSSHAIR); - + } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/listeners/ResultListener.java b/src/main/java/pulse/ui/components/listeners/ResultListener.java index 17ba3bf3..4ba96d26 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultListener.java @@ -6,4 +6,4 @@ public interface ResultListener { public void onFormatChanged(ResultFormatEvent fme); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/ParameterTableModel.java b/src/main/java/pulse/ui/components/models/ParameterTableModel.java index 66aa0cb4..ec1bf9c3 100644 --- a/src/main/java/pulse/ui/components/models/ParameterTableModel.java +++ b/src/main/java/pulse/ui/components/models/ParameterTableModel.java @@ -9,19 +9,15 @@ import javax.swing.table.AbstractTableModel; -import pulse.input.InterpolationDataset; import pulse.properties.Flag; import pulse.properties.NumericProperties; import pulse.properties.NumericPropertyKeyword; import pulse.search.direction.ActiveFlags; +import pulse.tasks.TaskManager; import pulse.ui.Messages; public class ParameterTableModel extends AbstractTableModel { - - /** - * - */ - private static final long serialVersionUID = 1L; + protected List elements; private final boolean extendedList; @@ -39,7 +35,7 @@ public final void populateWithAllProperties() { elements.add(OPTIMISER_STATISTIC); elements.add(TEST_STATISTIC); elements.add(IDENTIFIER); - elements.addAll(InterpolationDataset.derivableProperties()); + elements.addAll(TaskManager.getManagerInstance().derivableProperties()); } } @@ -55,19 +51,19 @@ public int getColumnCount() { @Override public Object getValueAt(int i, int i1) { - if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + if (i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { var p = NumericProperties.def(elements.get(i)); - return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + - p.getDescriptor(false) + Messages.getString("TextWrap.1"); - } - else + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } else { return null; + } } - + public boolean contains(NumericPropertyKeyword key) { return elements.contains(key); } - + public NumericPropertyKeyword getElementAt(int index) { return elements.get(index); } diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 8592285c..c131f9cc 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import static javax.swing.SwingUtilities.invokeLater; import javax.swing.table.DefaultTableModel; @@ -17,6 +18,7 @@ import pulse.tasks.Identifier; import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; import pulse.tasks.listeners.ResultFormatEvent; import pulse.tasks.logs.Details; import pulse.tasks.logs.Status; @@ -36,8 +38,8 @@ public class ResultTableModel extends DefaultTableModel { public ResultTableModel(ResultFormat fmt, int rowCount) { super(fmt.abbreviations().toArray(), rowCount); - this.fmt = fmt; results = new ArrayList<>(); + this.fmt = fmt; tooltips = tooltips(); listeners = new ArrayList<>(); } @@ -46,6 +48,14 @@ public ResultTableModel(ResultFormat fmt) { this(fmt, 0); } + public void resetSession() { + clear(); + changeFormat(ResultFormat.getInstance()); + var repo = TaskManager.getManagerInstance(); + repo.getTaskList().stream() + .forEach(t -> addRow(t.findBestCalculation().getResult())); + } + public void addListener(ResultListener listener) { listeners.add(listener); } @@ -219,24 +229,28 @@ private List tooltips() { public void addRow(AbstractResult result) { Objects.requireNonNull(result, "Entry added to the results table must not be null"); + var instance = TaskManager.getManagerInstance(); + //ignore average results if (result instanceof Result) { //result must have a valid ancestor! - var ancestor = Objects.requireNonNull(result.specificAncestor(SearchTask.class), - "Result " + result.toString() + " does not belong a SearchTask!"); - - //the ancestor then has the SearchTask type - SearchTask parentTask = (SearchTask) ancestor; + var id = ((Result) result).getTaskIdentifier(); + SearchTask parentTask = instance.getTask(id); //any old result asssociated withis this task - var oldResult = results.stream().filter(r - -> r.specificAncestor(SearchTask.class) == parentTask).findAny(); - - //check the following only if the old result is present - if (oldResult.isPresent()) { - - AbstractResult oldResultExisting = oldResult.get(); + var linkedResults = results.stream() + .filter(r -> r instanceof Result) + .filter(rr -> ((Result) rr).getTaskIdentifier().equals(parentTask.getIdentifier())) + .collect(Collectors.toList()); + + if (linkedResults.size() > 1) { + //table can't contain more than one result associated with a task + throw new IllegalStateException("More than one result found associated with " + parentTask.getIdentifier()); + } else if (linkedResults.size() == 1) { + //check the following only if the old result is present + AbstractResult oldResultExisting = linkedResults.get(0); + //find specific calculation for this result Optional oldCalculation = parentTask.getStoredCalculations().stream() .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); diff --git a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java index 98491bf7..f9098f20 100644 --- a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java +++ b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java @@ -1,6 +1,5 @@ package pulse.ui.components.models; - import java.util.ArrayList; import java.util.List; @@ -12,10 +11,6 @@ public class SelectedKeysModel extends DefaultTableModel { - /** - * - */ - private static final long serialVersionUID = 1L; private final List elements; private final List referenceList; private final NumericPropertyKeyword[] mandatorySelection; @@ -36,7 +31,7 @@ public void update(List keys) { elements.clear(); elements.addAll(keys); } - + @Override public int getRowCount() { return elements != null ? elements.size() : 0; @@ -46,29 +41,29 @@ public int getRowCount() { public int getColumnCount() { return 2; } - + @Override public Object getValueAt(int i, int i1) { - if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + if (i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { var p = NumericProperties.def(elements.get(i)); - return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + - p.getDescriptor(false) + Messages.getString("TextWrap.1"); - } - else + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } else { return null; + } } - + public void addElement(NumericPropertyKeyword key) { elements.add(key); var e = NumericProperties.def(key); int index = elements.size() - 1; super.fireTableRowsInserted(index, index); } - + public boolean contains(NumericPropertyKeyword key) { return elements.contains(key); } - + public List getData() { return elements; } @@ -76,7 +71,7 @@ public List getData() { public NumericPropertyKeyword getElementAt(int index) { return elements.get(index); } - + public boolean removeElement(NumericPropertyKeyword key) { if (!elements.contains(key)) { @@ -88,11 +83,11 @@ public boolean removeElement(NumericPropertyKeyword key) { return false; } } - + var index = elements.indexOf(key); super.fireTableRowsDeleted(index, index); elements.remove(key); return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/models/TaskBoxModel.java b/src/main/java/pulse/ui/components/models/TaskBoxModel.java index 5230f284..317d367d 100644 --- a/src/main/java/pulse/ui/components/models/TaskBoxModel.java +++ b/src/main/java/pulse/ui/components/models/TaskBoxModel.java @@ -17,10 +17,6 @@ */ public class TaskBoxModel extends AbstractListModel implements ComboBoxModel { - /** - * - */ - private static final long serialVersionUID = 5394433933807306979L; protected SearchTask selectedTask; public TaskBoxModel() { diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 86eef588..2145d57f 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -31,14 +31,21 @@ public class TaskTableModel extends DefaultTableModel { public TaskTableModel() { super(new Object[][]{}, - new String[]{def(IDENTIFIER).getAbbreviation(true), + new String[]{def(IDENTIFIER).getAbbreviation(true), def(TEST_TEMPERATURE).getAbbreviation(true), - def(OPTIMISER_STATISTIC).getAbbreviation(true), + def(OPTIMISER_STATISTIC).getAbbreviation(true), def(TEST_STATISTIC).getAbbreviation(true), getString("TaskTable.Status")}); + resetSession(); + } + + public void resetSession() { + //clear all rows + this.setRowCount(0); var instance = TaskManager.getManagerInstance(); - + instance.getTaskList().stream().forEach(t -> addTask(t)); + /* * task removed/added listener */ @@ -49,14 +56,13 @@ public TaskTableModel() { addTask(instance.getTask(e.getId())); } }); - } public void addTask(SearchTask t) { - var temperature = ( (ExperimentalData) t.getInput() ) + var temperature = ((ExperimentalData) t.getInput()) .getMetadata().numericProperty(TEST_TEMPERATURE); var calc = (Calculation) t.getResponse(); - var data = new Object[]{t.getIdentifier(), temperature, + var data = new Object[]{t.getIdentifier(), temperature, calc.getOptimiserStatistic().getStatistic(), t.getNormalityTest().getStatistic(), t.getStatus()}; @@ -65,7 +71,7 @@ public void addTask(SearchTask t) { t.addStatusChangeListener((StateEntry e) -> { setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); if (t.getNormalityTest() != null) { - setValueAt(t.getNormalityTest().getStatistic(), + setValueAt(t.getNormalityTest().getStatistic(), searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); } }); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index dca6e625..bb54a90e 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -171,7 +171,7 @@ private void validateRange(double a, double b) { // set range for all available experimental datasets TaskManager.getManagerInstance().getTaskList() .stream().forEach((aTask) - -> setRange( (ExperimentalData) aTask.getInput(), a, b) + -> setRange((ExperimentalData) aTask.getInput(), a, b) ); } diff --git a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java index ebb6bf66..d685da97 100644 --- a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -20,13 +20,13 @@ public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, S super(); initComponents(leftTable, titleLeft, rightTable, titleRight); - + moveRightBtn.addActionListener(e -> { var model = (SelectedKeysModel) rightTable.getModel(); - NumericPropertyKeyword key = ( (ParameterTableModel) leftTable.getModel() ) - .getElementAt(leftTable - .convertRowIndexToModel(leftTable.getSelectedRow())); + NumericPropertyKeyword key = ((ParameterTableModel) leftTable.getModel()) + .getElementAt(leftTable + .convertRowIndexToModel(leftTable.getSelectedRow())); if (key != null) { if (!model.contains(key)) { @@ -57,7 +57,7 @@ public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, S } }); - + } public void initComponents(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { @@ -72,7 +72,7 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderLeft = createTitledBorder(titleLeft); leftScroller.setBorder(borderLeft); - + leftTable.setRowHeight(80); leftScroller.setViewportView(leftTable); diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index 5db207b5..84879f15 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -56,4 +56,4 @@ public void addLogListener(LogListener l) { listeners.add(l); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index ca36f67c..0ddf940b 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -5,8 +5,6 @@ import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.ui.Messages.getString; @@ -22,6 +20,8 @@ import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.LoaderButton; +import static pulse.ui.components.buttons.LoaderButton.DataType.DENSITY; +import static pulse.ui.components.buttons.LoaderButton.DataType.HEAT_CAPACITY; import pulse.ui.frames.MainGraphFrame; import pulse.ui.frames.TaskControlFrame; @@ -40,12 +40,12 @@ public ProblemToolbar() { btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ add(btnSimulate); - btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ - btnLoadCv.setDataType(HEAT_CAPACITY); + btnLoadCv = new LoaderButton(HEAT_CAPACITY, + getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ add(btnLoadCv); - btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ - btnLoadDensity.setDataType(DENSITY); + btnLoadDensity = new LoaderButton(DENSITY, + getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ add(btnLoadDensity); btnSimulate.addActionListener((ActionEvent e) @@ -69,7 +69,7 @@ public static void plot(ActionEvent e) { if (status == INCOMPLETE && !status.checkProblemStatementSet()) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), + showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); diff --git a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java index 405e5453..a86cee52 100644 --- a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java +++ b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java @@ -19,8 +19,6 @@ public class SettingsToolBar extends JToolBar { - private static final long serialVersionUID = -1171612225785102673L; - private JCheckBox cbSingleStatement, cbHideDetails; public SettingsToolBar(PropertyHolderTable... tables) { diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index 786178ef..522a6f3b 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -68,6 +68,10 @@ private void initComponents() { execBtn.setToolTipText("Execute All Tasks"); add(execBtn); } + + public void resetSession() { + ((ExecutionButton)execBtn).resetSession(); + } public void setRemoveEnabled(boolean b) { removeBtn.setEnabled(b); @@ -118,5 +122,11 @@ public void notifyGraph() { public void addTaskActionListener(TaskActionListener l) { listeners.add(l); } + + public void removeListeners() { + if(listeners != null) { + listeners.clear(); + } + } } diff --git a/src/main/java/pulse/ui/frames/DataFrame.java b/src/main/java/pulse/ui/frames/DataFrame.java index 492df291..152b1a70 100644 --- a/src/main/java/pulse/ui/frames/DataFrame.java +++ b/src/main/java/pulse/ui/frames/DataFrame.java @@ -16,7 +16,6 @@ public class DataFrame extends JFrame { - private static final long serialVersionUID = 1L; private JPanel contentPane; private PropertyHolderTable dataTable; private Component ancestorFrame; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 096bac20..0db89c6b 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -27,7 +27,6 @@ import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; -@SuppressWarnings("serial") public class LogFrame extends JInternalFrame { private AbstractLogger logger; @@ -76,7 +75,7 @@ public void onLogModeChanged(boolean graphical) { } - private void scheduleLogEvents() { + public void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); instance.addSelectionListener( e -> SwingUtilities.invokeLater(() -> logger.postAll())); @@ -134,4 +133,4 @@ private void setGraphicalLogger(boolean graphicalLog) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index e4400d43..d31f5700 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -4,11 +4,8 @@ import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.swing.JInternalFrame; -import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.logs.Status; diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java index 68856aba..1d247024 100644 --- a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -24,13 +24,17 @@ public ModelSelectionFrame() { setSize(new Dimension(400, 400)); setTitle("Stored Calculations"); getContentPane().add(new ModelToolbar(), BorderLayout.SOUTH); + resetSession(); + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + } + + public void resetSession() { var instance = TaskManager.getManagerInstance(); instance.addTaskRepositoryListener(e -> { if (e.getState() == TASK_BROWSING_REQUEST) { table.update(instance.getTask(e.getId())); } }); - this.setDefaultCloseOperation(HIDE_ON_CLOSE); } } diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index 508d5a77..87a156bd 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -85,7 +85,7 @@ private void init() { getContentPane().add(createEmptyPanel(), CENTER); - var toolbar = new JToolBar(); + var toolbar = new JToolBar(); toolbar.setFloatable(false); toolbar.setLayout(new GridLayout()); @@ -118,7 +118,7 @@ private void init() { selectXBox.addItemListener(e -> replot(chart)); selectYBox.addItemListener(e -> replot(chart)); - this.setDefaultCloseOperation(EXIT_ON_CLOSE); + this.setDefaultCloseOperation(EXIT_ON_CLOSE); } private void replot(JFreeChart chart) { @@ -146,7 +146,7 @@ private void replot(JFreeChart chart) { if (drawSmooth) { drawSmooth(plot, selectedX, selectedY); } - + } private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { @@ -161,7 +161,7 @@ private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { //Akima spline for small number of points var interpolator = new AkimaSplineInterpolator(); interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); - } catch( NonMonotonicSequenceException e) { + } catch (NonMonotonicSequenceException e) { //do not draw if points not strictly increasing return; } @@ -175,7 +175,7 @@ private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { x[i] = data[selectedX][0][0] + dx * i; try { y[i] = interpolation.value(x[i]); - } catch(OutOfRangeException e) { + } catch (OutOfRangeException e) { y[i] = Double.NaN; } } @@ -192,14 +192,14 @@ public void update(ResultFormat fmt, double[][][] data) { propertyNames = new ArrayList<>(size); String tmp; - + selectXBox.removeAllItems(); selectYBox.removeAllItems(); for (var s : descriptors) { selectXBox.addItem(s); selectYBox.addItem(s); - } + } selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); @@ -228,14 +228,13 @@ private static ChartPanel createEmptyPanel() { //plot.setRangeGridlinesVisible(false); //plot.setDomainGridlinesVisible(false); - var fore = UIManager.getColor("Label.foreground"); plot.setDomainGridlinePaint(fore); - + plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); - plot.getRenderer(0).setSeriesShape(0, - new Rectangle(-MARKER_SIZE/2, -MARKER_SIZE/2, MARKER_SIZE, MARKER_SIZE)); + plot.getRenderer(0).setSeriesShape(0, + new Rectangle(-MARKER_SIZE / 2, -MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); chart.removeLegend(); @@ -250,7 +249,7 @@ private static ChartPanel createEmptyPanel() { plot.setBackgroundPaint(chart.getBackgroundPaint()); Chart.setAxisFontColor(plot.getDomainAxis(), fore); Chart.setAxisFontColor(plot.getRangeAxis(), fore); - + return cp; } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 525ac3e2..9c0bd4ba 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -37,7 +37,6 @@ import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; -import pulse.tasks.listeners.TaskSelectionEvent; import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.listeners.ProblemSelectionEvent; @@ -91,8 +90,6 @@ public ProblemStatementFrame() { problemTree = new ProblemTree(knownProblems); contentPane.add(new JScrollPane(problemTree)); - var instance = getManagerInstance(); - problemListExecutor = Executors.newCachedThreadPool(); schemeListExecutor = Executors.newCachedThreadPool(); propertyExecutor = Executors.newCachedThreadPool(); @@ -188,12 +185,12 @@ public void setSelectionPath(TreePath path) { getContentPane().add(contentPane, CENTER); getContentPane().add(toolbar, SOUTH); - /* - * listeners - */ - instance.addSelectionListener((TaskSelectionEvent e) -> update(instance.getSelectedTask())); - - getManagerInstance().addHierarchyListener(event -> { + resetSession(); + } + + public void resetSession() { + var instance = getManagerInstance(); + instance.addHierarchyListener(event -> { if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) { //for all tasks @@ -211,7 +208,6 @@ public void setSelectionPath(TreePath path) { } }); - } public void update() { diff --git a/src/main/java/pulse/ui/frames/ResultFrame.java b/src/main/java/pulse/ui/frames/ResultFrame.java index 07031274..93aa7e8d 100644 --- a/src/main/java/pulse/ui/frames/ResultFrame.java +++ b/src/main/java/pulse/ui/frames/ResultFrame.java @@ -122,13 +122,19 @@ public void addFrameCreationListener(PreviewFrameCreationListener l) { private void showInputDialog() { averageWindowDialog.setLocationRelativeTo(null); averageWindowDialog.setVisible(true); - averageWindowDialog.setConfirmAction(() -> - ((ResultTableModel)resultTable.getModel()) - .merge(averageWindowDialog.value().doubleValue())); + averageWindowDialog.setConfirmAction(() + -> ((ResultTableModel) resultTable.getModel()) + .merge(averageWindowDialog.value().doubleValue())); } public ResultTable getResultTable() { return resultTable; } -} + public void removeAllListeners() { + if (listeners != null) { + listeners.clear(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 7d2c9db2..94306df5 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -88,11 +88,11 @@ public SearchOptionsFrame() { leftTable.setModel(new ParameterTableModel(false)); leftTable.setTableHeader(null); leftTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - + rightTable = new javax.swing.JTable(); rightTable.setTableHeader(null); rightTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - + var mainContainer = new DoubleTablePanel(leftTable, "All Parameters", rightTable, "Optimised Parameters"); getContentPane().add(pathListScroller, gbc); @@ -109,7 +109,7 @@ public SearchOptionsFrame() { tableScroller.setBorder( createTitledBorder("Select search variables and settings")); getContentPane().add(tableScroller, gbc); - + } public void update() { @@ -126,18 +126,18 @@ public void update() { leftTable.setAutoCreateRowSorter(true); leftTable.getRowSorter().toggleSortOrder(0); - + var rightTblModel = rightTable.getModel(); var activeTask = TaskManager.getManagerInstance().getSelectedTask(); //model for the flags list already created if (rightTblModel instanceof SelectedKeysModel) { var searchKeys = activeTask.activeParameters(); - ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); + ((ParameterTableModel) leftTable.getModel()).populateWithAllProperties(); ((SelectedKeysModel) rightTblModel).update(searchKeys); } //Create a new model for the flags list else { - var c = (Calculation)activeTask.getResponse(); + var c = (Calculation) activeTask.getResponse(); if (c != null && c.getProblem() != null) { var searchKeys = activeTask.activeParameters(); rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); @@ -148,28 +148,28 @@ public void update() { rightTable.getModel().addTableModelListener(new TableModelListener() { private void updateFlag(TableModelEvent arg0, boolean value) { - var source = (NumericPropertyKeyword) - ( (SelectedKeysModel)rightTable.getModel() ) - .getElementAt(arg0.getFirstRow()); + var source = (NumericPropertyKeyword) ((SelectedKeysModel) rightTable.getModel()) + .getElementAt(arg0.getFirstRow()); var flag = new Flag(source); flag.setValue(value); PathOptimiser.getInstance().update(flag); } - + @Override public void tableChanged(TableModelEvent tme) { - if(tme.getType() == TableModelEvent.INSERT) + if (tme.getType() == TableModelEvent.INSERT) { updateFlag(tme, true); - else if(tme.getType() == TableModelEvent.DELETE) + } else if (tme.getType() == TableModelEvent.DELETE) { updateFlag(tme, false); + } } - + }); } } pathTable.updateTable(); - + } class PathSolversList extends JList { @@ -179,10 +179,6 @@ public PathSolversList() { super(); setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -7683200230096704268L; @Override public int getSize() { diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index f56d6ed1..2c17b04c 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -18,6 +18,7 @@ import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.Objects; import javax.swing.JDesktopPane; import javax.swing.JFrame; @@ -25,7 +26,6 @@ import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; -import pulse.problem.statements.Pulse; import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Version; @@ -55,7 +55,7 @@ public class TaskControlFrame extends JFrame { private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; - + private final static int ICON_SIZE = 16; public static TaskControlFrame getInstance() { @@ -75,6 +75,27 @@ private TaskControlFrame() { setIconImage(loadIcon("logo.png", 32).getImage()); addListeners(); setDefaultCloseOperation(EXIT_ON_CLOSE); + TaskManager.addSessionListener(() -> resetSession()); + } + + public void resetSession() { + Objects.requireNonNull(mainMenu, "Menu not created"); + var s = TaskManager.getManagerInstance(); + s.addSelectionListener(e -> graphFrame.plot()); + problemStatementFrame.resetSession(); + taskManagerFrame.resetSession(); + modelFrame.resetSession(); + resultsFrame.getResultTable().resetSession(); + logFrame.getLogger().clear(); + logFrame.scheduleLogEvents(); + mainMenu.reset(); + s.addTaskRepositoryListener(e + -> { + if (e.getState() == TASK_BROWSING_REQUEST) { + setModelSelectionFrameVisible(true); + } + } + ); } private void addListeners() { @@ -136,15 +157,6 @@ public void onSearchSettingsShowRequest() { } }); - var manager = TaskManager.getManagerInstance(); - manager.addTaskRepositoryListener(e - -> { - if (e.getState() == TASK_BROWSING_REQUEST) { - setModelSelectionFrameVisible(true); - } - } - ); - addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) .changeFormat(rfe.getResultFormat())); diff --git a/src/main/java/pulse/ui/frames/TaskManagerFrame.java b/src/main/java/pulse/ui/frames/TaskManagerFrame.java index 033edf96..a24f9c0f 100644 --- a/src/main/java/pulse/ui/frames/TaskManagerFrame.java +++ b/src/main/java/pulse/ui/frames/TaskManagerFrame.java @@ -28,6 +28,12 @@ public TaskManagerFrame() { setVisible(true); } + public void resetSession() { + taskTable.resetSession(); + taskToolbar.resetSession(); + adjustEnabledControls(); + } + private void manageListeners() { taskToolbar.addTaskActionListener(new TaskActionListener() { @@ -62,21 +68,21 @@ private void initComponents() { taskToolbar = new TaskToolbar(); getContentPane().add(taskToolbar, PAGE_START); } + + private void enableIfNeeded() { + var ttm = (TaskTableModel) taskTable.getModel(); + + boolean enabled = ttm.getRowCount() > 0; + taskToolbar.setClearEnabled(enabled); + taskToolbar.setResetEnabled(enabled); + taskToolbar.setExecEnabled(enabled); + } private void adjustEnabledControls() { var ttm = (TaskTableModel) taskTable.getModel(); - ttm.addTableModelListener((TableModelEvent arg0) -> { - if (ttm.getRowCount() < 1) { - taskToolbar.setClearEnabled(false); - taskToolbar.setResetEnabled(false); - taskToolbar.setExecEnabled(false); - } else { - taskToolbar.setClearEnabled(true); - taskToolbar.setResetEnabled(true); - taskToolbar.setExecEnabled(true); - } - }); + enableIfNeeded(); + ttm.addTableModelListener((TableModelEvent arg0) -> enableIfNeeded() ); taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { var selection = taskTable.getSelectedRows(); diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index ed0f7eed..bdfe6109 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -85,11 +85,11 @@ private File directoryQuery() { var returnVal = fileChooser.showOpenDialog(this); File f = null; - + if (returnVal == APPROVE_OPTION) { dir = f = fileChooser.getSelectedFile(); } - + return f; } @@ -250,7 +250,7 @@ public void removeUpdate(DocumentEvent e) { var browseBtn = new JButton("Browse..."); browseBtn.addActionListener(e -> directoryField.setText(directoryQuery() - .getPath() + separator + projectName + separator) ); + .getPath() + separator + projectName + separator)); var exportBtn = new JButton("Export"); diff --git a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java index 179ab1f0..82f2cf94 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java @@ -6,10 +6,16 @@ import java.awt.Dimension; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JDialog; import javax.swing.JProgressBar; import javax.swing.SwingWorker; +import pulse.util.Serializer; +import static pulse.util.Serializer.deserialize; @SuppressWarnings("serial") public class ProgressDialog extends JDialog implements PropertyChangeListener { @@ -84,4 +90,25 @@ protected void done() { progressWorker.execute(); } -} + public interface ProgressWorker { + + public default void work() { + var dialog = new ProgressDialog(); + dialog.setLocationRelativeTo(null); + dialog.trackProgress(1); + CompletableFuture.runAsync(() -> { + try { + action(); + } catch (Exception ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to load session", ex); + System.err.println("Failed to load session."); + } + }) + .thenRun(() -> dialog.incrementProgress()); + } + + public void action(); + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index b0554c3a..b1965877 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -14,10 +14,6 @@ public class ResultChangeDialog extends JDialog { - /** - * - */ - private static final long serialVersionUID = 1L; private final static int WIDTH = 1000; private final static int HEIGHT = 600; @@ -51,34 +47,36 @@ private void initComponents() { setDefaultCloseOperation(HIDE_ON_CLOSE); leftTbl = new javax.swing.JTable() { - + @Override - public boolean isCellEditable(int row, int column) { - return false; - }; - + public boolean isCellEditable(int row, int column) { + return false; + } + ; + }; leftTbl.setModel(new ParameterTableModel(true)); leftTbl.setTableHeader(null); rightTbl = new javax.swing.JTable() { - + @Override - public boolean isCellEditable(int row, int column) { - return false; - }; - + public boolean isCellEditable(int row, int column) { + return false; + } + ; + }; rightTbl.setModel(new SelectedKeysModel( ResultFormat.getInstance().getKeywords(), ResultFormat.getMinimalArray())); rightTbl.setTableHeader(null); - + MainContainer = new DoubleTablePanel(leftTbl, "All Parameters", rightTbl, "Output"); - + getContentPane().add(MainContainer, BorderLayout.CENTER); MainToolbar.setFloatable(false); diff --git a/src/main/java/pulse/util/DescriptorChangeListener.java b/src/main/java/pulse/util/DescriptorChangeListener.java index 4c522b7d..4edbe51d 100644 --- a/src/main/java/pulse/util/DescriptorChangeListener.java +++ b/src/main/java/pulse/util/DescriptorChangeListener.java @@ -1,6 +1,8 @@ package pulse.util; -public interface DescriptorChangeListener { +import java.io.Serializable; + +public interface DescriptorChangeListener extends Serializable { public void onDescriptorChanged(); diff --git a/src/main/java/pulse/util/FunctionSerializer.java b/src/main/java/pulse/util/FunctionSerializer.java new file mode 100644 index 00000000..65911a8b --- /dev/null +++ b/src/main/java/pulse/util/FunctionSerializer.java @@ -0,0 +1,31 @@ +package pulse.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import org.apache.commons.math3.analysis.polynomials.PolynomialFunction; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +public class FunctionSerializer { + + private FunctionSerializer() { + //empty + } + + public static void writeSplineFunction(PolynomialSplineFunction f, ObjectOutputStream oos) + throws IOException { + // write the object + double[] knots = f != null ? f.getKnots() : null; + PolynomialFunction[] funcs = f != null ? f.getPolynomials() : null; + oos.writeObject(knots); + oos.writeObject(funcs); + } + + public static PolynomialSplineFunction readSplineFunction(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + var knots = (double[]) ois.readObject(); // knots + var funcs = (PolynomialFunction[]) ois.readObject(); + return knots != null & funcs != null ? new PolynomialSplineFunction(knots, funcs) : null; + } + +} diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 2b1f888e..cbdfc35c 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -25,8 +25,8 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - - if (m.getParameterCount() > 0 + + if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) || m.getReturnType().isAssignableFrom(getClass())) { continue; diff --git a/src/main/java/pulse/util/HierarchyListener.java b/src/main/java/pulse/util/HierarchyListener.java index c6c3ef8c..c5a87c60 100644 --- a/src/main/java/pulse/util/HierarchyListener.java +++ b/src/main/java/pulse/util/HierarchyListener.java @@ -1,5 +1,7 @@ package pulse.util; +import java.io.Serializable; + /** * An hierarchy listener, which listens to any changes happening with the * children of an {@code UpwardsNavigable}. @@ -7,7 +9,7 @@ * @see pulse.util.UpwardsNavigable * */ -public interface HierarchyListener { +public interface HierarchyListener extends Serializable { /** * This is invoked by the {@code UpwardsNavigable} when an event resulting diff --git a/src/main/java/pulse/util/ImmutableDataEntry.java b/src/main/java/pulse/util/ImmutableDataEntry.java index 7a55587b..b7648294 100644 --- a/src/main/java/pulse/util/ImmutableDataEntry.java +++ b/src/main/java/pulse/util/ImmutableDataEntry.java @@ -1,5 +1,7 @@ package pulse.util; +import java.io.Serializable; + /** * A {@code DataEntry} is an immutable ordered pair of an instance of {@code T}, * which is considered to be the 'key', and an instance of {@code R}, which is @@ -8,7 +10,7 @@ * @param the key * @param the value */ -public class ImmutableDataEntry { +public class ImmutableDataEntry implements Serializable { private T key; private R value; diff --git a/src/main/java/pulse/util/ImmutablePair.java b/src/main/java/pulse/util/ImmutablePair.java index f264614f..f01f9a49 100644 --- a/src/main/java/pulse/util/ImmutablePair.java +++ b/src/main/java/pulse/util/ImmutablePair.java @@ -1,6 +1,8 @@ package pulse.util; -public class ImmutablePair { +import java.io.Serializable; + +public class ImmutablePair implements Serializable { private T anElement; private T anotherElement; diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 644ad028..a8769a57 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -30,7 +30,7 @@ public InstanceDescriptor(String generalDescriptor, Class c, Object... argume allDescriptors = nameMap.get(c); selectedDescriptor = allDescriptors.iterator().next(); this.generalDescriptor = generalDescriptor; - listeners = new ArrayList(); + listeners = new ArrayList<>(); } public InstanceDescriptor(Class c, Object... arguments) { @@ -50,15 +50,15 @@ public Object getValue() { @Override public boolean attemptUpdate(Object object) { var string = object.toString(); - + if (selectedDescriptor.equals(string)) { return false; } - - if(!allDescriptors.contains(string)) { + + if (!allDescriptors.contains(string)) { throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); } - + this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); return true; diff --git a/src/main/java/pulse/util/PropertyEvent.java b/src/main/java/pulse/util/PropertyEvent.java index 70c486a5..0c74fc75 100644 --- a/src/main/java/pulse/util/PropertyEvent.java +++ b/src/main/java/pulse/util/PropertyEvent.java @@ -1,5 +1,6 @@ package pulse.util; +import java.io.Serializable; import pulse.properties.Property; /** @@ -7,7 +8,7 @@ * {@code PropertyHolder}. * */ -public class PropertyEvent { +public class PropertyEvent implements Serializable { private Object source; private PropertyHolder propertyHolder; diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index 05cef73c..4eff7a05 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -22,7 +22,7 @@ public abstract class PropertyHolder extends Accessible { private List parameters = listedTypes(); - private List listeners; + private transient List listeners; private String prefix; /** @@ -63,8 +63,9 @@ public List listedTypes() { return listedKeywords().stream().map(key -> def(key)).collect(Collectors.toList()); } - public PropertyHolder() { - this.listeners = new ArrayList<>(); + public void initListeners() { + super.initListeners(); + listeners = new ArrayList<>(); } /** @@ -195,7 +196,9 @@ public boolean updateProperty(Object sourceComponent, Property updatedProperty) public void firePropertyChanged(Object source, Property property) { var event = new PropertyEvent(source, this, property); - listeners.forEach(l -> l.onPropertyChanged(event)); + if (listeners != null) { + listeners.forEach(l -> l.onPropertyChanged(event)); + } /* * If the changes are triggered by an external GUI component (such as @@ -220,11 +223,19 @@ public void updateProperties(Object sourceComponent, PropertyHolder propertyHold propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); } - public void removeHeatingCurveListeners() { - this.listeners.clear(); + public void removeListeners() { + if(listeners == null) { + listeners = new ArrayList<>(); + } + else { + listeners.clear(); + } } public void addListener(PropertyHolderListener l) { + if (listeners == null) { + this.listeners = new ArrayList<>(); + } this.listeners.add(l); } diff --git a/src/main/java/pulse/util/PropertyHolderListener.java b/src/main/java/pulse/util/PropertyHolderListener.java index de5017b8..aa3e2b72 100644 --- a/src/main/java/pulse/util/PropertyHolderListener.java +++ b/src/main/java/pulse/util/PropertyHolderListener.java @@ -1,10 +1,12 @@ package pulse.util; +import java.io.Serializable; + /** * A listener used by {@code PropertyHolder}s to track changes with the * associated {@code Propert}ies. */ -public interface PropertyHolderListener { +public interface PropertyHolderListener extends Serializable { /** * This event is triggered by any {@code PropertyHolder}, the properties of diff --git a/src/main/java/pulse/util/Serializer.java b/src/main/java/pulse/util/Serializer.java new file mode 100644 index 00000000..ccd7833f --- /dev/null +++ b/src/main/java/pulse/util/Serializer.java @@ -0,0 +1,118 @@ +package pulse.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JFileChooser; +import static javax.swing.JFileChooser.APPROVE_OPTION; +import static javax.swing.JFileChooser.FILES_ONLY; +import javax.swing.filechooser.FileNameExtensionFilter; +import pulse.tasks.TaskManager; +import pulse.ui.frames.dialogs.ProgressDialog.ProgressWorker; + +public class Serializer { + + private static final FileNameExtensionFilter filter = new FileNameExtensionFilter( + "Saved sessions (.pulse)", "pulse"); + + private Serializer() { + // + } + + public static void serialize() throws IOException, FileNotFoundException, ClassNotFoundException { + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(FILES_ONLY); + fileChooser.setFileFilter(filter); + File f = new File("./Saved/"); + if (!f.exists()) { + f.mkdir(); + } + fileChooser.setCurrentDirectory(f); + + int returnVal = fileChooser.showSaveDialog(null); + + if (returnVal == APPROVE_OPTION) { + String ext = filter.getExtensions()[0]; + File fileToBeSaved; + if (!fileChooser.getSelectedFile().getAbsolutePath().endsWith(ext)) { + fileToBeSaved = new File(fileChooser.getSelectedFile() + ext); + } else { + fileToBeSaved = fileChooser.getSelectedFile(); + } + + ProgressWorker worker = () -> { + try { + serialize(fileToBeSaved); + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to save session", ex); + System.err.println("Failed to save session."); + } + }; + + worker.work(); + } + + } + + public static void deserialize() throws FileNotFoundException { + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(FILES_ONLY); + fileChooser.setFileFilter(filter); + File f = new File("./Saved/"); + if (f.exists()) { + fileChooser.setCurrentDirectory(f); + } + + int returnVal = fileChooser.showOpenDialog(null); + + if (returnVal == APPROVE_OPTION) { + + ProgressWorker worker = () -> { + try { + deserialize(fileChooser.getSelectedFile()); + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to load session", ex); + System.err.println("Failed to load session."); + } + }; + + worker.work(); + + } + + } + + public static void serialize(File fname) throws FileNotFoundException, IOException, ClassNotFoundException { + FileOutputStream fileOutputStream = new FileOutputStream(fname); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) { + var instance = TaskManager.getManagerInstance(); + objectOutputStream.writeObject(instance); + } + } + + public static void deserialize(File fname) throws FileNotFoundException, IOException, ClassNotFoundException { + FileInputStream fis = new FileInputStream(fname); + TaskManager state; + try (ObjectInputStream ois = new ObjectInputStream(fis)) { + state = (TaskManager) ois.readObject(); + } + //close stream + state.initListeners(); + state.getTaskList().stream().forEach(t -> { + t.initListeners(); + t.children().stream().forEach(c -> c.initListeners()); + } + ); + TaskManager.assumeNewState(state); + state.fireTaskSelected(state); + } + +} diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index ab8e7444..1d4992c7 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -1,5 +1,6 @@ package pulse.util; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -16,10 +17,14 @@ *

* */ -public abstract class UpwardsNavigable implements Descriptive { +public abstract class UpwardsNavigable implements Descriptive, Serializable { private UpwardsNavigable parent; - private final List listeners = new ArrayList<>(); + private transient List listeners; + + public void initListeners() { + listeners = new ArrayList<>(); + } public final void removeHierarchyListeners() { this.listeners.clear(); @@ -48,7 +53,9 @@ public final List getHierarchyListeners() { */ public void tellParent(PropertyEvent e) { if (parent != null) { - parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + if (parent.listeners != null) { + parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + } parent.tellParent(e); } } From 7b9b47285d89bf7430929761d93ddd72e14b3ad2 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Feb 2023 12:41:27 +0300 Subject: [PATCH 112/116] PULsE v1.98 --- src/main/java/pulse/ui/components/PulseMainMenu.java | 8 ++------ src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index caec94a7..ccf0f89d 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -194,11 +194,9 @@ private void initComponents() { serializeItem.addActionListener(e -> { try { Serializer.serialize(); - } catch (IOException ex) { - Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } catch (ClassNotFoundException ex) { + } catch (IOException | ClassNotFoundException ex) { Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } + } }); var deserializeItem = new JMenuItem("Load Session..."); @@ -209,8 +207,6 @@ private void initComponents() { Serializer.deserialize(); } catch (IOException ex) { Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } catch (ClassNotFoundException ex) { - Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); } }); diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 96882818..92888258 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.97c \ No newline at end of file +1.98 \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b9f7d322..1a4346a2 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -291,4 +291,4 @@ MixedScheme2.5=Increased Accuracy Semi-implicit Scheme (NL)

@@ -35,18 +38,18 @@ protected GradientGuidedPath(SearchTask t) { /** * Resets the {@code Path}: calculates the current gradient and the - * direction of search. Sets the minimum point to 0.0. + * direction of search.Sets the minimum point to 0.0. * * @param t the {@code SearchTask}, for which this {@code Path} is created. + * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.search.direction.PathSolver.direction(Path) */ public void configure(SearchTask t) { super.reset(); try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); - } catch (SolverException e) { - System.err.println("Failed on gradient calculation while resetting optimiser..."); - e.printStackTrace(); + } catch (SolverException ex) { + t.notifyFailedStatus(ex); } minimumPoint = 0.0; } diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index b11adc0d..d3571159 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -6,16 +6,13 @@ import static pulse.properties.NumericPropertyKeyword.ERROR_TOLERANCE; import static pulse.properties.NumericPropertyKeyword.ITERATION_LIMIT; -import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import pulse.properties.Property; import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.SearchTask; diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index 5588f782..891b42bb 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -5,9 +5,6 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Set; import pulse.math.ParameterVector; @@ -16,8 +13,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; -import pulse.properties.Property; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -71,7 +66,7 @@ protected LinearOptimiser() { */ public static Segment domain(ParameterVector x, Vector p) { double alphaMax = Double.POSITIVE_INFINITY; - double alpha = 0.0; + double alpha; for (int i = 0; i < x.dimension(); i++) { @@ -94,7 +89,9 @@ public static Segment domain(ParameterVector x, Vector p) { } - return new Segment(0.0, alphaMax); + //check that alphaMax is not zero! otherwise the optimise will crash + return new Segment(0.0, + Math.max(alphaMax, 1E-10)); } diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index b4db6d7a..751d7d67 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -24,7 +24,7 @@ * page */ public class WolfeOptimiser extends LinearOptimiser { - + private static WolfeOptimiser instance = new WolfeOptimiser(); /** @@ -37,7 +37,7 @@ public class WolfeOptimiser extends LinearOptimiser { * gradient projection, equal to {@value C2}. */ public final static double C2 = 0.8; - + private WolfeOptimiser() { super(); } @@ -65,32 +65,33 @@ private WolfeOptimiser() { */ @Override public double linearStep(SearchTask task) throws SolverException { - + GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); - + final Vector direction = p.getDirection(); final Vector g1 = p.getGradient(); - + final double G1P = g1.dot(direction); final double G1P_ABS = abs(G1P); - + var params = task.searchVector(); Segment segment = domain(params, direction); - + double cost1 = task.solveProblemAndCalculateCost(); - + double randomConfinedValue = 0; double g2p; - - var instance = (GradientBasedOptimiser) PathOptimiser.getInstance(); - + + var optimiser = (GradientBasedOptimiser) PathOptimiser.getInstance(); + for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { - + randomConfinedValue = segment.randomValue(); - + final var newParams = params.sum(direction.multiply(randomConfinedValue)); + task.assign(new ParameterVector(params, newParams)); - + final double cost2 = task.solveProblemAndCalculateCost(); /** @@ -102,8 +103,8 @@ public double linearStep(SearchTask task) throws SolverException { segment.setMaximum(randomConfinedValue); continue; } - - final var g2 = instance.gradient(task); + + final var g2 = optimiser.gradient(task); g2p = g2.dot(direction); /** @@ -118,16 +119,16 @@ public double linearStep(SearchTask task) throws SolverException { * if( g2p >= C2*G1P ) break; */ segment.setMinimum(randomConfinedValue); - + } - + task.assign(params); p.setGradient(g1); - + return randomConfinedValue; - + } - + @Override public String toString() { return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ @@ -142,5 +143,5 @@ public String toString() { public static WolfeOptimiser getInstance() { return instance; } - + } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index 67191bcf..c9775447 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -15,8 +15,8 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - private static InstanceDescriptor instanceDescriptor - = new InstanceDescriptor( + private static final InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( "Correlation Test Selector", CorrelationTest.class); static { diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 7943c11a..af33470b 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -160,10 +160,11 @@ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { @SuppressWarnings({"unchecked", "rawtypes"}) public void process() throws SolverException { var list = problem.getProperties().findMalformedProperties(); - if(!list.isEmpty()) { + if (!list.isEmpty()) { StringBuilder sb = new StringBuilder("Illegal values:"); - for(NumericProperty np : list) - sb.append(String.format("%n %-25s", np)); + list.forEach(np + -> sb.append(String.format("%n %-25s", np)) + ); throw new SolverException(sb.toString()); } ((Solver) scheme).solve(problem); @@ -208,9 +209,10 @@ public boolean setStatus(Status status) { default: } - if(changeStatus) + if (changeStatus) { this.status = status; - + } + return changeStatus; } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 717b7bca..b17a6dba 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -21,7 +21,6 @@ import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.READY; -import static pulse.tasks.logs.Status.TERMINATED; import static pulse.tasks.processing.Buffer.getSize; import static pulse.util.Reflexive.instantiate; @@ -31,8 +30,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import pulse.input.ExperimentalData; @@ -51,7 +48,6 @@ import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Details; -import static pulse.tasks.logs.Details.SOLVER_ERROR; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; @@ -59,6 +55,8 @@ import pulse.tasks.processing.Buffer; import pulse.tasks.processing.CorrelationBuffer; import pulse.util.Accessible; +import static pulse.tasks.logs.Status.AWAITING_TERMINATION; +import static pulse.tasks.logs.Status.TERMINATED; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -81,7 +79,7 @@ public class SearchTask extends Accessible implements Runnable { private Buffer buffer; private Log log; - private CorrelationBuffer correlationBuffer; + private final CorrelationBuffer correlationBuffer; private CorrelationTest correlationTest; private NormalityTest normalityTest; @@ -171,7 +169,7 @@ private void addListeners() { *

@@ -106,8 +105,9 @@ public class SearchTask extends Accessible implements Runnable { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { - current = new Calculation(); - current.setParent(this); + this.statusChangeListeners = new CopyOnWriteArrayList<>(); + this.listeners = new CopyOnWriteArrayList<>(); + current = new Calculation(this); this.identifier = new Identifier(); this.curve = curve; curve.setParent(this); @@ -285,7 +285,7 @@ public void run() { /* search cycle */ - /* sets an independent thread for manipulating the buffer */ + /* sets an independent thread for manipulating the buffer */ List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); @@ -598,7 +598,7 @@ public void initNormalityTest() { } public void initCorrelationTest() { - correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); + correlationTest = CorrelationTest.init(); correlationTest.setParent(this); } @@ -617,6 +617,11 @@ public Calculation getCurrentCalculation() { public List getStoredCalculations() { return this.stored; } + + public void storeCalculation() { + var copy = new Calculation(current); + stored.add(copy); + } public void switchTo(Calculation calc) { current.setParent(null); @@ -625,10 +630,20 @@ public void switchTo(Calculation calc) { var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); } + + /** + * Finds the best calculation by comparing those already stored by their + * model selection statistics. + * @return the calculation showing the optimal value of the model selection statistic. + */ + + public Calculation findBestCalculation() { + var c = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); + return c.isPresent() ? c.get() : null; + } public void switchToBestModel() { - var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); - this.switchTo(best.get()); + this.switchTo(findBestCalculation()); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); fireRepositoryEvent(e); } @@ -640,4 +655,4 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 44fa9c25..03aa3341 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -38,6 +38,7 @@ import pulse.tasks.listeners.TaskRepositoryListener; import pulse.tasks.listeners.TaskSelectionEvent; import pulse.tasks.listeners.TaskSelectionListener; +import pulse.tasks.logs.Status; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.util.Group; @@ -120,7 +121,7 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(true); + t.checkProblems(t.getCurrentCalculation().getStatus() != Status.DONE); //try to start cmputation // notify listeners computation is about to start @@ -139,12 +140,10 @@ public void execute(SearchTask t) { current.setResult(new Result(t, ResultFormat.getInstance())); //notify listeners before the task is re-assigned notifyListeners(e); - current.setParent(null); - t.getStoredCalculations().add(current.copy()); - current.setParent(t); - } else { - notifyListeners(e); + t.storeCalculation(); } + else + notifyListeners(e); }); } @@ -170,7 +169,6 @@ public void executeAll() { var queue = tasks.stream().filter(t -> { switch (t.getCurrentCalculation().getStatus()) { - case DONE: case IN_PROGRESS: case EXECUTION_ERROR: return false; diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 15529097..9c8f46a6 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -41,7 +41,22 @@ public enum Details { SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, PARAMETER_VALUES_NOT_SENSIBLE, MAX_ITERATIONS_REACHED, - ABNORMAL_DISTRIBUTION_OF_RESIDUALS; + ABNORMAL_DISTRIBUTION_OF_RESIDUALS, + + /** + * Indicates that the result table had not been updated, as the selected + * model produced results worse than expected by the model selection criterion. + */ + + CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED, + + + /** + * Indicates that the result table had been updated, as the current + * model selection criterion showed better result than already present. + */ + + BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED; @Override public String toString() { diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 5464396a..31d3ef15 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -22,6 +22,8 @@ public class CorrelationBuffer { private static Set> excludePairList; private static Set excludeSingleList; + private final static double DEFAULT_THRESHOLD = 1E-3; + static { excludePairList = new HashSet<>(); excludeSingleList = new HashSet<>(); @@ -44,6 +46,27 @@ public void inflate(SearchTask t) { public void clear() { params.clear(); } + + /** + * Truncates the buffer by excluding nearly-converged results. + */ + + private void truncate(double threshold) { + int i = 0; + int size = params.size(); + final double thresholdSq = threshold*threshold; + + for(i = 0; i < size - 1; i = i + 2) { + + ParameterVector diff = new ParameterVector( params.get(i), params.get(i + 1).subtract(params.get(i) )); + if(diff.lengthSq()/params.get(i).lengthSq() < thresholdSq) + break; + } + + for(int j = size - 1; j > i; j--) + params.remove(j); + + } public Map, Double> evaluate(CorrelationTest t) { if (params.isEmpty()) { @@ -54,6 +77,8 @@ public Map, Double> evaluate(CorrelationTe return null; } + truncate(DEFAULT_THRESHOLD); + var indices = params.get(0).getIndices(); var map = indices.stream() .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 992157d5..74aeeab0 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -51,6 +51,7 @@ import pulse.ui.frames.dialogs.ExportDialog; import pulse.ui.frames.dialogs.FormattedInputDialog; import pulse.ui.frames.dialogs.ResultChangeDialog; +import pulse.util.Reflexive; @SuppressWarnings("serial") public class PulseMainMenu extends JMenuBar { @@ -270,16 +271,30 @@ private JMenu initAnalysisSubmenu() { JRadioButtonMenuItem corrItem = null; + var ct = CorrelationTest.init(); + for (var corrName : allDescriptors(CorrelationTest.class)) { corrItem = new JRadioButtonMenuItem(corrName); corrItems.add(corrItem); correlationsSubMenu.add(corrItem); + + if(ct.getDescriptor().equalsIgnoreCase(corrName)) + corrItem.setSelected(true); + corrItem.addItemListener(e -> { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); - CorrelationTest.setSelectedTestDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + var allTests = Reflexive.instancesOf(CorrelationTest.class); + var optionalTest = allTests.stream().filter(test -> + test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); + + if(optionalTest.isPresent()) { + CorrelationTest.getTestDescriptor() + .setSelectedDescriptor(optionalTest.get().getClass().getSimpleName()); + getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + } + } }); @@ -294,8 +309,6 @@ private JMenu initAnalysisSubmenu() { correlationsSubMenu.add(thrItem); thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); - correlationsSubMenu.getItem(0).setSelected(true); - analysisSubMenu.add(correlationsSubMenu); return analysisSubMenu; } diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index b47a0eeb..141d0f4e 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -8,6 +8,7 @@ import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Comparator; +import java.util.Objects; import javax.swing.JTable; import javax.swing.RowSorter; @@ -79,7 +80,9 @@ public ResultTable(ResultFormat fmt) { switch (e.getState()) { case TASK_FINISHED: var r = t.getCurrentCalculation().getResult(); - invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); + var resultTableModel = (ResultTableModel) getModel(); + Objects.requireNonNull(r, "Task finished with a null result!"); + invokeLater(() -> resultTableModel.addRow(r)); break; case TASK_REMOVED: case TASK_RESET: @@ -134,7 +137,7 @@ public double[][][] data() { for (var i = 0; i < data.length; i++) { for (var j = 0; j < data[0][0].length; j++) { - property = (NumericProperty) getValueAt(j, i) ; + property = (NumericProperty) getValueAt(j, i); data[i][0][j] = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue() + property.getDimensionDelta().doubleValue(); data[i][1][j] = property.getError() == null ? 0 @@ -230,6 +233,7 @@ public void undo() { var instance = TaskManager.getManagerInstance(); instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) + .filter(Objects::nonNull) .forEach(c -> dtm.addRow(c.getResult())); } diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 943ce3e8..37b84d58 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -32,14 +32,14 @@ public ExecutionButton() { this.addActionListener((ActionEvent e) -> { /* - * STOP PRESSED? + * STOP PRESSED? */ if (state == STOP) { instance.cancelAllTasks(); return; } /* - * EXECUTE PRESSED? + * EXECUTE PRESSED? */ if (instance.getTaskList().isEmpty()) { showMessageDialog(getWindowAncestor((Component) e.getSource()), diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 4bb5ef18..5910a002 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -1,23 +1,25 @@ package pulse.ui.components.models; import static java.lang.Math.abs; -import static java.util.stream.Collectors.toList; import static pulse.tasks.processing.AbstractResult.filterProperties; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Optional; import static javax.swing.SwingUtilities.invokeLater; import javax.swing.table.DefaultTableModel; import pulse.properties.NumericProperties; -import pulse.properties.NumericProperty; import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.tasks.Calculation; import pulse.tasks.Identifier; +import pulse.tasks.SearchTask; import pulse.tasks.listeners.ResultFormatEvent; +import pulse.tasks.logs.Details; +import pulse.tasks.logs.Status; import pulse.tasks.processing.AbstractResult; import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; @@ -78,9 +80,7 @@ public void changeFormat(ResultFormat fmt) { results.clear(); this.setColumnIdentifiers(fmt.abbreviations().toArray()); - for (var r : oldResults) { - addRow(r); - } + oldResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); } else { this.setColumnIdentifiers(fmt.abbreviations().toArray()); @@ -93,21 +93,22 @@ public void changeFormat(ResultFormat fmt) { } /** - * Transforms the result model by merging individual results which: - * (a) correspond to test temperatures within a specified {@code temperatureDelta} - * (b) form a single sequence of measurements - * @param temperatureDelta the maximum difference between the test temperature of two results being merged + * Transforms the result model by merging individual results which: (a) + * correspond to test temperatures within a specified + * {@code temperatureDelta} (b) form a single sequence of measurements + * + * @param temperatureDelta the maximum difference between the test + * temperature of two results being merged */ - public void merge(double temperatureDelta) { List skipList = new ArrayList<>(); List avgResults = new ArrayList<>(); List sortedResults = new ArrayList<>(results); - + /*sort results in the order of their ids * This is essential for the algorithm below which assumes the results * are listed in the order of ascending ids. - */ + */ sortedResults.sort((AbstractResult arg0, AbstractResult arg1) -> { var id1 = arg0.getProperties().get(fmt.indexOf(IDENTIFIER)); var id2 = arg1.getProperties().get(fmt.indexOf(IDENTIFIER)); @@ -146,22 +147,23 @@ public void merge(double temperatureDelta) { invokeLater(() -> { setRowCount(0); results.clear(); - avgResults.stream().forEach(r -> addRow(r)); + avgResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); }); } - + /** - * Takes a list of results, which should be mandatory sorted in the order of ascending id values, - * and searches for those results that can be merged with {@code r}, satisfying these criteria: - * (a) these results correspond to test temperatures within a specified {@code temperatureDelta} - * (b) they form a single sequence of measurements + * Takes a list of results, which should be mandatory sorted in the order of + * ascending id values, and searches for those results that can be merged + * with {@code r}, satisfying these criteria: (a) these results correspond + * to test temperatures within a specified {@code temperatureDelta} (b) they + * form a single sequence of measurements + * * @param listOfResults an orderer list of results, as explained above - * @param r the result of interest - * @param propertyInterval an interval for the temperature merging - * @return a group of results + * @param r the result of interest + * @param propertyInterval an interval for the temperature merging + * @return a group of results */ - public List group(List listOfResults, AbstractResult r, double propertyInterval) { List selection = new ArrayList<>(); @@ -214,8 +216,52 @@ private List tooltips() { } public void addRow(AbstractResult result) { - if (result == null) { - return; + Objects.requireNonNull(result, "Entry added to the results table must not be null"); + + //result must have a valid ancestor! + var ancestor = Objects.requireNonNull( + result.specificAncestor(SearchTask.class), + "Result " + result.toString() + " does not belong a SearchTask!"); + + //the ancestor then has the SearchTask type + SearchTask parentTask = (SearchTask) ancestor; + + //any old result asssociated withis this task + var oldResult = results.stream().filter(r + -> r.specificAncestor( + SearchTask.class) == parentTask).findAny(); + + //ignore average results + if (result instanceof Result && oldResult.isPresent()) { + AbstractResult oldResultExisting = oldResult.get(); + Optional oldCalculation = parentTask.getStoredCalculations().stream() + .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); + + //old calculation found + if (oldCalculation.isPresent()) { + + //since the task has already been completed anyway + Status status = Status.DONE; + + //better result than already present -- update table + if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + remove(oldResultExisting); + status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + } else { + //do not remove result and do not add new result + status.setDetails(Details.CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + return; + } + + } else { + //calculation has been purged -- delete previous result + + remove(oldResultExisting); + + } + } var propertyList = filterProperties(result, fmt); diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java index e82d734e..83255e35 100644 --- a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -32,8 +32,10 @@ public void update(SearchTask t) { var list = t.getStoredCalculations(); for (Calculation c : list) { - var problem = c.getProblem(); - var baseline = c.getProblem().getBaseline(); + //we assume all problem descriptions contain the word Problem after their titles + String problem = c.getProblem().toString().split("Problem")[0] + ""; + //likewise -- for baselines containing Baseline + String baseline = c.getProblem().getBaseline().getSimpleName().split("Baseline")[0]; var optimiser = c.getOptimiserStatistic(); var criterion = c.getModelSelectionCriterion(); var parameters = c.getModelSelectionCriterion().getNumVariables(); diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 54ee4dbf..bead9379 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -50,10 +50,13 @@ public Object getValue() { @Override public boolean attemptUpdate(Object object) { var string = object.toString(); - - if (selectedDescriptor.equals(string) || !allDescriptors.contains(string)) { + + if (selectedDescriptor.equals(string)) { return false; } + + if(!allDescriptors.contains(string)) + throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index 7a3079c5..70bc5e28 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -90,7 +90,7 @@ public UpwardsNavigable specificAncestor(Class aClas * @param parent the new parent that will adopt this * {@code UpwardsNavigable}. */ - public void setParent(UpwardsNavigable parent) { + public final void setParent(UpwardsNavigable parent) { this.parent = parent; } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 9c78b74a..e5ca9830 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -272,13 +272,20 @@ descriptor="Thermal diffusivity, <i>a</i> (mm<sup>2</sup>s<sup>-1</sup>) " dimensionfactor="1000000.0" keyword="DIFFUSIVITY" maximum="0.001" minimum="1.0E-10" value="1.0E-6" primitive-type="double" - discreet="false" default-search-variable="true" /> + discreet="false" default-search-variable="true"> + + THICKNESS + + + + DIFFUSIVITY + Date: Mon, 25 Apr 2022 00:09:26 +0300 Subject: [PATCH 093/116] PULsE v1.94 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #ParameterVector.java -Changed findMalformedElements to conform with the transforms defined by the class instance and simplified the method calls. #Solver implementations and subclasses of DifferenceScheme: -Enabled throwing SolverException in solve(), timeStep() and iteration() #ThermalProperties.java -Introduced findMalformedProperties() for automated property validation -Fixed an error with density and heat capacity values not being assigned to the corresponding variables in fill() - Fixed a caveat in the calculateEmissivity() method where, in some extreme cases, the emissivity values could be greater than unity or smaller than zero, which led to critical breakdown of radiative transfer calculations. - Created a toString() method with a formatted output of the thermal properties. #CoupledImplicitScheme.java -Added missing call to super method FixedPointIterations.super.finaliseIteration() -Added a throws SolverException line in setCalculationStatus(…) when the status is not NORMAL #Status.java -Added optional message to further customise the status updates. The message is accessible via getDetailedMessage() #ExplicitLinearisedSolver.java, ImplicitLinearisedSolver.java -Experimental feature: rear-surface heat source introduced through the ‘zeta’ factor. This slightly changes the right boundary expressions and the timeStep(…) method #RTECalculationStatus.java -Added a new status, ‘INVALID_FLUXES’, which indicates the RTE calculation resulted in invalid flux values. #StateEntry.java -In case of a Status having a non-empty detailed message, the message will be printed in the toString() method, which has been modified accordingly #ParticipatingMedium.java -Changed bounds for the parameters, including e.g. the Planck number and optical thickness -Replaced all transforms for all optical parameters to StickTransform. This has proven to be more fail-proof #ClassicalProblem.java -Introduced the bias property, which refers to the bias in the source power between the front and rear surfaces #Problem.java -Added ABS transformation for the MAXTEMP property in optimisationVector() -Added parameter bounds for the HEAT_LOSS property, linking them to the parameter bounds retrieved from the XML file -Removed unnecessary method calls and conditional statement in setHeatLossParameter() -Added check for malformed property values in assign(…). This is expected to be called by all subclasses of Problem #NonlinearProblem.java -Emissivity is now calculated following every call to assign(…), not just for selected properties #RadiativeTransferSolver.java -Added flux.init() call in init(…), when fluxes are not null #FixedPointIterations.java -Added check for malformed temperature value array. This checks for non-finite elements or sum of elements exceeding a maximum threshold and throws a SolverException #ExplicitCoupledSolver, ImplicitCoupledSolver, MixedCoupledSolver -Improved exception handling and monitoring of calculation health #IndexRange.java - Fixed an erratic statement in the closest(…) method where an exception would be thrown when sizeMinusOne were less than 1 #Fluxes.java - Added checkArrays() method, which checks whether the fluxes are finite - Introduced an overridable init() method #FluxesAndExplicitDerivatives.java - Instead of having an overriden setDensity() method, the latter is set final, with the init() method now overriding superclass method - Introduced #ThermoOpticalProperties - New overriden toString() method that outputs all thermo-optical properties #ExperimentalData - Changed MAX_REDUCTION_FACTOR to 256 - Changed calculateHalfTime(), where the running-average curve would be calculated iteratively changing the reduction factor until certain conditions are met #SearchTask.java - Added null check in addListeners(…) - run() in several places: replaced confusing System.err on SolverException by a detailed status update with the notifyFailedStatus(…) call #DifferenceScheme.java - Important change to runTimeSequence(…), where, after filling the solution curve array, the nominal number of points is replaced by the real number of points, should that number be smaller than the former. #Launcher.java - Added file lock to prevent launching multiple instances of PULsE simultaneously #NonlinearProblem.java - Added LASER_ENERGY as a possible search variable #LMOptimiser.java - Added check for malformed parameter vectors and gradient vectors, causing a SolverException to be thrown if found #Calculation.java - process() will first check for malformed properties before proceeding with the calculation #Details.java - added SOLVER_ERROR #NumericProperties.java - added checks for non-finite values in isValueSensible(...) #Other classes: Made some getter and setter methods final to improve encapsulation --- .../java/pulse/input/ExperimentalData.java | 30 +++-- src/main/java/pulse/input/IndexRange.java | 29 +++-- .../pulse/input/InterpolationDataset.java | 1 - .../pulse/io/readers/NetzschCSVReader.java | 1 - src/main/java/pulse/math/ParameterVector.java | 5 +- .../schemes/CoupledImplicitScheme.java | 21 ++-- .../problem/schemes/DifferenceScheme.java | 47 ++++---- .../problem/schemes/FixedPointIterations.java | 26 +++-- .../pulse/problem/schemes/ImplicitScheme.java | 12 +- .../problem/schemes/OneDimensionalScheme.java | 9 +- .../schemes/RadiativeTransferCoupling.java | 4 +- .../pulse/problem/schemes/rte/Fluxes.java | 22 +++- .../rte/FluxesAndExplicitDerivatives.java | 9 +- .../schemes/rte/RTECalculationStatus.java | 9 +- .../schemes/rte/RadiativeTransferSolver.java | 1 + .../rte/dom/DiscreteOrdinatesMethod.java | 6 +- .../schemes/rte/dom/DiscreteQuantities.java | 2 +- .../schemes/rte/dom/IterativeSolver.java | 4 - .../schemes/rte/dom/PhaseFunction.java | 2 +- .../schemes/solvers/ADILinearisedSolver.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 30 ++--- .../solvers/ExplicitLinearisedSolver.java | 10 +- .../solvers/ExplicitNonlinearSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 4 +- .../solvers/ImplicitDiathermicSolver.java | 2 +- .../solvers/ImplicitLinearisedSolver.java | 11 +- .../solvers/ImplicitNonlinearSolver.java | 9 +- .../solvers/ImplicitTranslucentSolver.java | 4 +- .../schemes/solvers/MixedCoupledSolver.java | 31 ++--- .../solvers/MixedLinearisedSolver.java | 2 +- .../problem/statements/ClassicalProblem.java | 78 +++++++++++++ .../problem/statements/DiathermicMedium.java | 2 +- .../problem/statements/NonlinearProblem.java | 76 ++++++------- .../statements/ParticipatingMedium.java | 30 ++--- .../pulse/problem/statements/Problem.java | 36 +++--- .../statements/model/ThermalProperties.java | 37 ++++-- .../model/ThermoOpticalProperties.java | 20 ++-- .../pulse/properties/NumericProperties.java | 17 +-- .../properties/NumericPropertyKeyword.java | 9 +- .../direction/CompositePathOptimiser.java | 5 + .../pulse/search/direction/LMOptimiser.java | 17 ++- .../pulse/search/direction/PathOptimiser.java | 4 +- src/main/java/pulse/tasks/Calculation.java | 7 ++ src/main/java/pulse/tasks/SearchTask.java | 107 ++++++++++-------- src/main/java/pulse/tasks/logs/Details.java | 4 +- .../java/pulse/tasks/logs/StateEntry.java | 4 + src/main/java/pulse/tasks/logs/Status.java | 9 ++ src/main/java/pulse/ui/Launcher.java | 55 ++++++--- src/main/resources/NumericProperty.xml | 17 ++- src/main/resources/Version.txt | 2 +- src/main/resources/images/splash.png | Bin 68380 -> 67179 bytes 51 files changed, 568 insertions(+), 317 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index bbbb2e6a..bdbeabf6 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import pulse.AbstractData; @@ -53,11 +52,13 @@ public class ExperimentalData extends AbstractData { * Scientific Instruments, 91(6), 064902. */ public final static int REDUCTION_FACTOR = 32; + + public final static int MAX_REDUCTION_FACTOR = 256; /** * A fail-safe factor. */ - public final static double FAIL_SAFE_FACTOR = 3.0; + public final static double FAIL_SAFE_FACTOR = 10.0; private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); @@ -217,19 +218,28 @@ public Point2D maxAdjustedSignal() { * @see getHalfTime() */ public void calculateHalfTime() { - var degraded = runningAverage(REDUCTION_FACTOR); - var max = (max(degraded, pointComparator)); var baseline = new FlatBaseline(); baseline.fitTo(this); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - - int cutoffIndex = degraded.indexOf(max); + + int curRedFactor = REDUCTION_FACTOR/2; // reduced twofold since first operation + // in the while loop will increase it likewise + int cutoffIndex = 0; + List degraded = null; //running average + Point2D max = null; + + do { + curRedFactor *= 2; + degraded = runningAverage(curRedFactor); + max = (max(degraded, pointComparator)); + cutoffIndex = degraded.indexOf(max); + } while(cutoffIndex < 1 && curRedFactor < MAX_REDUCTION_FACTOR); + + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; degraded = degraded.subList(0, cutoffIndex); - + int index = IndexRange.closestLeft(halfMax, degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - + if (index < 1) { System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 742d77e6..bdf4288d 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -186,24 +186,31 @@ public static int closestRight(double of, List in) { } private static int closest(double of, List in, boolean reverseOrder) { - int sizeMinusOne = Math.max( in.size() - 1, 0); //has to be non-negative - - if (of > in.get(sizeMinusOne)) { - return sizeMinusOne; - } + int sizeMinusOne = in.size() - 1; //has to be non-negative + + int result = 0; + + if (sizeMinusOne < 1) { + result = 0; + } else if (of > in.get(sizeMinusOne)) { + result = sizeMinusOne; + } else { + + int start = reverseOrder ? sizeMinusOne - 1 : 0; + int increment = reverseOrder ? -1 : 1; - int start = reverseOrder ? sizeMinusOne - 1 : 0; - int increment = reverseOrder ? -1 : 1; + for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { - for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { + if (between(of, in.get(i), in.get(i + 1))) { + result = i; + break; + } - if (between(of, in.get(i), in.get(i + 1))) { - return i; } } - return 0; + return result; } diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 63c98270..3db3182d 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -52,7 +52,6 @@ public InterpolationDataset() { */ public double interpolateAt(double key) { return interpolation.value(key); - } /** diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index c2e0b2ad..dae82be0 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -8,7 +8,6 @@ import java.io.FileReader; import java.io.IOException; import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index ca0cb49b..295f5a34 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -236,9 +236,8 @@ public List findMalformedElements() { var list = new ArrayList(); for (int i = 0; i < dimension(); i++) { - var property = def(getIndex(i)); - boolean sensible = NumericProperties.isValueSensible(property, get(i)); - if (!sensible) { + var property = NumericProperties.derive(getIndex(i), inverseTransform(i)); + if (!property.validate()) { list.add(property); } } diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 6981853b..848482bb 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -4,16 +4,14 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; -import pulse.properties.Property; public abstract class CoupledImplicitScheme extends ImplicitScheme implements FixedPointIterations { @@ -37,17 +35,20 @@ public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, Nume } @Override - public void timeStep(final int m) { + public void timeStep(final int m) throws SolverException { pls = pulse(m); doIterations(getCurrentSolution(), nonlinearPrecision, m); } @Override - public void iteration(final int m) { + public void iteration(final int m) throws SolverException { super.timeStep(m); } - public void finaliseIteration(double[] V) { + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = coupling.getRadiativeTransferEquation(); setCalculationStatus(coupling.getRadiativeTransferEquation().compute(V)); } @@ -55,13 +56,13 @@ public RadiativeTransferCoupling getCoupling() { return coupling; } - public void setCoupling(RadiativeTransferCoupling coupling) { + public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { super.finaliseStep(); coupling.getRadiativeTransferEquation().getFluxes().store(); } @@ -104,8 +105,10 @@ public RTECalculationStatus getCalculationStatus() { return calculationStatus; } - public void setCalculationStatus(RTECalculationStatus calculationStatus) { + public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; + if(calculationStatus != RTECalculationStatus.NORMAL) + throw new SolverException(calculationStatus.toString()); } public double getCurrentPulseValue() { diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 9f54d46b..09ea1cf6 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -5,16 +5,14 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.problem.laser.DiscretePulse; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -119,16 +117,14 @@ protected void prepare(Problem problem) { hc.clear(); } - public void runTimeSequence(Problem problem) { + public void runTimeSequence(Problem problem) throws SolverException { runTimeSequence(problem, 0, timeLimit); var curve = problem.getHeatingCurve(); final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); curve.scale(maxTemp / curve.apparentMaximum()); } - public void runTimeSequence(Problem problem, final double offset, final double endTime) { - final var grid = getGrid(); - + public void runTimeSequence(Problem problem, final double offset, final double endTime) throws SolverException { var curve = problem.getHeatingCurve(); int adjustedNumPoints = (int) curve.getNumPoints().getValue(); @@ -137,7 +133,7 @@ public void runTimeSequence(Problem problem, final double offset, final double e final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); final double tau = grid.getTimeStep(); - for (double dt = 0, factor = 1.0; dt < tau; adjustedNumPoints *= factor) { + for (double dt = 0, factor; dt < tau; adjustedNumPoints *= factor) { dt = timeSegment / (adjustedNumPoints - 1); factor = dt / tau; timeInterval = (int) factor; @@ -164,10 +160,19 @@ public void runTimeSequence(Problem problem, final double offset, final double e curve.addPoint(nextTime, signal()); } + + /** + * If the total number of points added by the procedure + * is actually less than the pre-set number of points -- change that number + */ + + if(curve.actualNumPoints() < (int)curve.getNumPoints().getValue()) { + curve.setNumPoints(derive(NUMPOINTS, curve.actualNumPoints())); + } } - private void timeSegment(final int m1, final int m2) { + private void timeSegment(final int m1, final int m2) throws SolverException { for (int m = m1; m < m2 && normalOperation(); m++) { timeStep(m); finaliseStep(); @@ -180,9 +185,9 @@ public double pulse(final int m) { public abstract double signal(); - public abstract void timeStep(final int m); + public abstract void timeStep(final int m) throws SolverException; - public abstract void finaliseStep(); + public abstract void finaliseStep() throws SolverException; public boolean normalOperation() { return true; @@ -209,7 +214,7 @@ public String toString() { * @return the discrete pulse * @see pulse.problem.statements.Pulse */ - public DiscretePulse getDiscretePulse() { + public final DiscretePulse getDiscretePulse() { return discretePulse; } @@ -219,7 +224,7 @@ public DiscretePulse getDiscretePulse() { * * @return the grid */ - public Grid getGrid() { + public final Grid getGrid() { return grid; } @@ -228,7 +233,7 @@ public Grid getGrid() { * * @param grid the grid */ - public void setGrid(Grid grid) { + public final void setGrid(Grid grid) { this.grid = grid; this.grid.setParent(this); } @@ -240,7 +245,7 @@ public void setGrid(Grid grid) { * * @return the time interval */ - public int getTimeInterval() { + public final int getTimeInterval() { return timeInterval; } @@ -249,7 +254,7 @@ public int getTimeInterval() { * * @param timeInterval a positive integer. */ - public void setTimeInterval(int timeInterval) { + public final void setTimeInterval(int timeInterval) { this.timeInterval = timeInterval; } @@ -259,7 +264,7 @@ public void setTimeInterval(int timeInterval) { * need to be displayed. */ @Override - public boolean areDetailsHidden() { + public final boolean areDetailsHidden() { return hideDetailedAdjustment; } @@ -269,7 +274,7 @@ public boolean areDetailsHidden() { * * @param b a boolean. */ - public static void setDetailsHidden(boolean b) { + public final static void setDetailsHidden(boolean b) { hideDetailedAdjustment = b; } @@ -281,7 +286,7 @@ public static void setDetailsHidden(boolean b) { * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} * @see pulse.properties.NumericPropertyKeyword */ - public NumericProperty getTimeLimit() { + public final NumericProperty getTimeLimit() { return derive(TIME_LIMIT, timeLimit); } @@ -294,7 +299,7 @@ public NumericProperty getTimeLimit() { * {@code TIME_LIMIT} * @see pulse.properties.NumericPropertyKeyword */ - public void setTimeLimit(NumericProperty timeLimit) { + public final void setTimeLimit(NumericProperty timeLimit) { requireType(timeLimit, TIME_LIMIT); this.timeLimit = (double) timeLimit.getValue(); firePropertyChanged(this, timeLimit); diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index 221fe505..f2c3a1ac 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -1,6 +1,8 @@ package pulse.problem.schemes; import static java.lang.Math.abs; +import java.util.Arrays; +import pulse.problem.schemes.solvers.SolverException; /** * @see Wiki @@ -10,18 +12,19 @@ public interface FixedPointIterations { /** - * Performs iterations until the convergence criterion is satisfied. The - * latter consists in having a difference two consequent iterations of V - * less than the specified error. At the end of each iteration, calls - * {@code finaliseIteration()}. + * Performs iterations until the convergence criterion is satisfied.The + latter consists in having a difference two consequent iterations of V + less than the specified error. At the end of each iteration, calls + {@code finaliseIteration()}. * * @param V the calculation array * @param error used in the convergence criterion * @param m time step + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed * @see finaliseIteration() * @see iteration() */ - public default void doIterations(double[] V, final double error, final int m) { + public default void doIterations(double[] V, final double error, final int m) throws SolverException { final int N = V.length - 1; @@ -39,16 +42,21 @@ public default void doIterations(double[] V, final double error, final int m) { * Performs an iteration at time {@code m} * * @param m time step + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed */ - public void iteration(final int m); + public void iteration(final int m) throws SolverException; /** - * Finalises the current iteration. By default, does nothing. + * Finalises the current iteration.By default, does nothing. * * @param V the current iteration + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed */ - public default void finaliseIteration(double[] V) { - // do nothing + public default void finaliseIteration(double[] V) throws SolverException { + final double threshold = 1E6; + double sum = Arrays.stream(V).sum(); + if( sum > threshold || !Double.isFinite(sum) ) + throw new SolverException("Invalid solution values in V array"); } } diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index f7b73bbe..ae80435f 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import pulse.problem.schemes.solvers.SolverException; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; @@ -65,8 +66,17 @@ protected void prepare(Problem problem) { tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); } + /** + * Calculates the solution at the boundaries using the boundary conditions + * specific to the problem statement and runs the tridiagonal matrix algorithm + * to evaluate solution at the intermediate grid points. + * @param m the time step + * @throws SolverException if the calculation failed + * @see leftBoundary(), evalRightBoundary(), pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() + */ + @Override - public void timeStep(final int m) { + public void timeStep(final int m) throws SolverException { leftBoundary(m); final var V = getCurrentSolution(); final int N = V.length - 1; diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index d8992045..54b33954 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; @@ -29,8 +30,14 @@ public double signal() { return V[V.length - 1]; } + /** + * Overwrites previously calculated temperature values with the calculations + * made at the current time step + * @throws SolverException if the calculation failed + */ + @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { System.arraycopy(V, 0, U, 0, V.length); } diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index e3e6a8a8..73801b7f 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -1,7 +1,5 @@ package pulse.problem.schemes; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import pulse.problem.schemes.rte.RadiativeTransferSolver; @@ -38,7 +36,7 @@ public void init(ParticipatingMedium problem, Grid grid) { newRTE(problem, grid); rte.init(problem, grid); }); - + } else { rte.init(problem, grid); } diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index 0dff5461..7c3bc78a 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -1,5 +1,8 @@ package pulse.problem.schemes.rte; +import java.util.Arrays; +import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; +import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public abstract class Fluxes implements DerivativeCalculator { @@ -20,6 +23,17 @@ public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { public void store() { System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results } + + /** + * Checks whether all stored values are finite. This is equivalent to summing + * all elements and checking whether the sum if finite. + * @return {@code true} if the elements are finite. + */ + + public RTECalculationStatus checkArrays() { + double sum = Arrays.stream(fluxes).sum() + Arrays.stream(storedFluxes).sum(); + return Double.isFinite(sum) ? NORMAL : INVALID_FLUXES; + } /** * Retrieves the currently calculated flux at the {@code i} grid point @@ -63,13 +77,17 @@ public double getOpticalThickness() { return opticalThickness; } - public void setDensity(NumericProperty gridDensity) { + public final void setDensity(NumericProperty gridDensity) { this.N = (int) gridDensity.getValue(); + init(); + } + + public void init() { fluxes = new double[N + 1]; storedFluxes = new double[N + 1]; } - public void setOpticalThickness(NumericProperty opticalThickness) { + public final void setOpticalThickness(NumericProperty opticalThickness) { this.opticalThickness = (double) opticalThickness.getValue(); } diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 0d164037..2b4ac0f4 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -1,5 +1,8 @@ package pulse.problem.schemes.rte; +import java.util.Arrays; +import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; +import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public class FluxesAndExplicitDerivatives extends Fluxes { @@ -10,10 +13,10 @@ public class FluxesAndExplicitDerivatives extends Fluxes { public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } - + @Override - public void setDensity(NumericProperty gridDensity) { - super.setDensity(gridDensity); + public void init() { + super.init(); fd = new double[getDensity() + 1]; fdStored = new double[getDensity() + 1]; } diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index 06d7b40d..f579004d 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -21,5 +21,12 @@ public enum RTECalculationStatus { /** * The grid density required to reach the error threshold was too large. */ - GRID_TOO_LARGE; + GRID_TOO_LARGE, + + /** + * The radiative fluxes contain illegal values. + */ + + INVALID_FLUXES; + } diff --git a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index 98dd9030..bb23d099 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -55,6 +55,7 @@ public RadiativeTransferSolver() { public void init(ParticipatingMedium p, Grid grid) { if (fluxes != null) { fluxes.setDensity(grid.getGridDensity()); + fluxes.init(); var properties = (ThermoOpticalProperties) p.getProperties(); fluxes.setOpticalThickness(properties.getOpticalThickness()); } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index 92031b5e..5ad9d0d4 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -77,7 +77,7 @@ public RTECalculationStatus compute(double[] tempArray) { if (status == RTECalculationStatus.NORMAL) { fluxesAndDerivatives(tempArray.length); } - + fireStatusUpdate(status); return status; } @@ -90,9 +90,11 @@ private void fluxesAndDerivatives(final int nExclusive) { var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); for (int i = 0; i < nExclusive; i++) { - fluxes.setFlux(i, DOUBLE_PI * discrete.firstMoment(interpolation[0], i)); + double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); + fluxes.setFlux(i, flux); fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); } + } @Override diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index bdcf5d3d..548e4885 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -5,7 +5,7 @@ * This includes the various intensity and flux arrays used internally by the * integrators. */ -class DiscreteQuantities { +public class DiscreteQuantities { private double[][] I; private double[][] Ik; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java index 66cf0f03..6bfb45c1 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java @@ -6,15 +6,11 @@ import static pulse.properties.NumericPropertyKeyword.DOM_ITERATION_ERROR; import static pulse.properties.NumericPropertyKeyword.RTE_MAX_ITERATIONS; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index 6ed02e47..0892aabe 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -6,7 +6,7 @@ public abstract class PhaseFunction implements Reflexive { - private Discretisation intensities; + private final Discretisation intensities; private double anisotropy; private double halfAlbedo; diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index e24120f4..7ce79f98 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -162,7 +162,7 @@ private void initConst() { } @Override - public void solve(ClassicalProblem2D problem) { + public void solve(ClassicalProblem2D problem) throws SolverException { prepare(problem); runTimeSequence(problem); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index b41cb031..40927d79 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -47,7 +47,7 @@ public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { status = RTECalculationStatus.NORMAL; } - private void prepare(ParticipatingMedium problem) { + private void prepare(ParticipatingMedium problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -55,7 +55,8 @@ private void prepare(ParticipatingMedium problem) { coupling.init(problem, grid); rte = coupling.getRadiativeTransferEquation(); fluxes = coupling.getRadiativeTransferEquation().getFluxes(); - + setCalculationStatus(fluxes.checkArrays()); + N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); @@ -74,13 +75,9 @@ private void prepare(ParticipatingMedium problem) { @Override public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - status = coupling.getRadiativeTransferEquation().compute(getPreviousSolution()); + this.prepare(problem); + setCalculationStatus(coupling.getRadiativeTransferEquation().compute(getPreviousSolution())); runTimeSequence(problem); - - if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); - } } @Override @@ -89,7 +86,7 @@ public boolean normalOperation() { } @Override - public void timeStep(int m) { + public void timeStep(int m) throws SolverException { pls = pulse(m); doIterations(getCurrentSolution(), nonlinearPrecision, m); } @@ -111,8 +108,9 @@ public void iteration(final int m) { } @Override - public void finaliseIteration(double[] V) { - status = rte.compute(V); + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + setCalculationStatus(rte.compute(V)); } @Override @@ -121,7 +119,7 @@ public double phi(final int i) { } @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { super.finaliseStep(); coupling.getRadiativeTransferEquation().getFluxes().store(); } @@ -130,7 +128,7 @@ public RadiativeTransferCoupling getCoupling() { return coupling; } - public void setCoupling(RadiativeTransferCoupling coupling) { + public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } @@ -155,5 +153,11 @@ public DifferenceScheme copy() { public Class domain() { return ParticipatingMedium.class; } + + public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { + this.status = calculationStatus; + if(status != RTECalculationStatus.NORMAL) + throw new SolverException(status.toString()); + } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index 3e0fc4c2..5d3a6f9f 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -49,6 +49,7 @@ public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { private RadiativeTransferSolver rte; - private Fluxes fluxes; private int N; private double hx; @@ -64,7 +58,7 @@ public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, Numeric sigma = (double) def(SCHEME_WEIGHT).getValue(); } - private void prepare(ParticipatingMedium problem) { + private void prepare(ParticipatingMedium problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -73,31 +67,30 @@ private void prepare(ParticipatingMedium problem) { coupling.init(problem, grid); rte = coupling.getRadiativeTransferEquation(); - var U = getPreviousSolution(); - N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); tau = grid.getTimeStep(); Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - fluxes = rte.getFluxes(); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override public double phi(int i) { + var fluxes = rte.getFluxes(); return A * fluxes.meanFluxDerivative(i) + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); } @Override public double beta(final double f, final double phi, final int i) { + var U = getPreviousSolution(); return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); } - + @Override public void evaluateBeta(final double[] U) { + var fluxes = rte.getFluxes(); final double phiSecond = A * fluxes.meanFluxDerivative(1) + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); setBeta(2, beta(U[1] / tau, phiSecond, 2)); @@ -150,15 +143,7 @@ private void initConst(ParticipatingMedium problem) { public void solve(ParticipatingMedium problem) throws SolverException { this.prepare(problem); initConst(problem); - - setCalculationStatus(rte.compute(getPreviousSolution())); this.runTimeSequence(problem); - - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); - } - } @Override @@ -171,6 +156,7 @@ public double pulse(final int m) { @Override public double firstBeta(final int m) { + var fluxes = rte.getFluxes(); var U = getPreviousSolution(); final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); return (_2TAUHX @@ -180,6 +166,7 @@ public double firstBeta(final int m) { @Override public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + var fluxes = rte.getFluxes(); final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); final var U = getPreviousSolution(); return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi @@ -229,4 +216,4 @@ public DifferenceScheme copy() { return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 28166846..979d50f7 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -139,7 +139,7 @@ public double firstBeta(final int m) { } @Override - public void solve(ClassicalProblem problem) { + public void solve(ClassicalProblem problem) throws SolverException { this.prepare(problem); runTimeSequence(problem); } diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 381e33a3..72796ccc 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,8 +1,21 @@ package pulse.problem.statements; +import java.util.List; +import java.util.Set; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; +import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; /** @@ -12,16 +25,27 @@ */ public class ClassicalProblem extends Problem { + private double bias; + public ClassicalProblem() { super(); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); setPulse(new Pulse()); } public ClassicalProblem(Problem p) { super(p); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); setPulse(new Pulse(p.getPulse())); } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SOURCE_GEOMETRIC_FACTOR); + return set; + } + @Override public Class defaultScheme() { return ImplicitLinearisedSolver.class; @@ -52,4 +76,58 @@ public Problem copy() { return new ClassicalProblem(this); } + public NumericProperty getGeometricFactor() { + return derive(SOURCE_GEOMETRIC_FACTOR, bias); + } + + public void setGeometricFactor(NumericProperty bias) { + requireType(bias, SOURCE_GEOMETRIC_FACTOR); + this.bias = (double) bias.getValue(); + firePropertyChanged(this, bias); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + super.set(type, value); + if (type == SOURCE_GEOMETRIC_FACTOR) { + setGeometricFactor(value); + } + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == SOURCE_GEOMETRIC_FACTOR) { + var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, bias); + } + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + for (int i = 0, size = params.dimension(); i < size; i++) { + + double value = params.get(i); + var key = params.getIndex(i); + + if (key == SOURCE_GEOMETRIC_FACTOR) { + setGeometricFactor(derive(SOURCE_GEOMETRIC_FACTOR, value)); + } + + } + + } + } diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index a34c7441..b2aca281 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -93,7 +93,7 @@ public void assign(ParameterVector params) throws SolverException { break; case HEAT_LOSS: if (properties.areThermalPropertiesLoaded()) { - properties.emissivity(); + properties.calculateEmissivity(); final double emissivity = (double) properties.getEmissivity().getValue(); properties .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 3bfc634d..5097ab31 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,26 +1,26 @@ package pulse.problem.statements; +import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import java.util.List; import java.util.Set; import pulse.input.ExperimentalData; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; import pulse.ui.Messages; public class NonlinearProblem extends ClassicalProblem { @@ -66,54 +66,54 @@ public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, getProperties().thermalConductivity()); } + /** + * + * Does the same as super-class method plus updates the laser energy, if needed. + * @param params + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.statements.Problem.getPulse() + * + */ + @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - int size = output.dimension(); - var properties = getProperties(); - - for (int i = 0; i < size; i++) { - - var key = output.getIndex(i); + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + getProperties().calculateEmissivity(); - if (key == HEAT_LOSS) { + for (int i = 0, size = params.dimension(); i < size; i++) { - var bounds = new Segment(0.0, properties.maxBiot()); - final double Bi1 = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, StandardTransformations.ABS); - output.set(i, Bi1); - output.setParameterBounds(i, bounds); + double value = params.inverseTransform(i); + NumericPropertyKeyword key = params.getIndex(i); + if (key == LASER_ENERGY) { + this.getPulse().setLaserEnergy(derive(key, value)); } } - } - + /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types - * of which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of - * parameters to this {@code Problem} - * @throws SolverException - * @see listedTypes() + * + * Does the same as super-class method plus extracts the laser energy and stores it in the {@code output}, if needed. + * @param output + * @param flags + * @see pulse.problem.statements.Problem.getPulse() + * */ + @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var p = getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - var key = params.getIndex(i); - - if (key == HEAT_LOSS) { + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { - p.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); - p.emissivity(); + var key = output.getIndex(i); + if(key == LASER_ENERGY) { + var bounds = Segment.boundsFrom(LASER_ENERGY); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, (double) getPulse().getLaserEnergy().getValue()); } } diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 2d60b1dd..5674dcc1 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,14 +1,11 @@ package pulse.problem.statements; -import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.List; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.AtanhTransform; import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; @@ -17,6 +14,8 @@ import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; +import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; +import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { @@ -41,8 +40,8 @@ public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (ThermoOpticalProperties) getProperties(); - Segment bounds; - double value = 0; + Segment bounds = null; + double value; Transformable transform; for (int i = 0, size = output.dimension(); i < size; i++) { @@ -51,30 +50,28 @@ public void optimisationVector(ParameterVector output, List flags) { switch (key) { case PLANCK_NUMBER: - bounds = new Segment(1E-5, properties.maxNp()); + final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); + bounds = new Segment(lowerBound, properties.maxNp()); value = (double) properties.getPlanckNumber().getValue(); - transform = new AtanhTransform(bounds); break; case OPTICAL_THICKNESS: value = (double) properties.getOpticalThickness().getValue(); - bounds = new Segment(1E-8, 1E5); - transform = LOG; - break; + bounds = Segment.boundsFrom(OPTICAL_THICKNESS); + break; case SCATTERING_ALBEDO: value = (double) properties.getScatteringAlbedo().getValue(); bounds = new Segment(0.0, 1.0); - transform = new StickTransform(bounds); break; case SCATTERING_ANISOTROPY: value = (double) properties.getScatteringAnisostropy().getValue(); bounds = new Segment(-1.0, 1.0); - transform = new StickTransform(bounds); break; default: continue; } + transform = new StickTransform(bounds); output.setTransform(i, transform); output.set(i, value); output.setParameterBounds(i, bounds); @@ -86,8 +83,9 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); + var properties = (ThermoOpticalProperties) getProperties(); - + for (int i = 0, size = params.dimension(); i < size; i++) { var type = params.getIndex(i); @@ -100,17 +98,13 @@ public void assign(ParameterVector params) throws SolverException { case OPTICAL_THICKNESS: properties.set(type, derive(type, params.inverseTransform(i))); break; - case HEAT_LOSS: - case DIFFUSIVITY: - properties.emissivity(); - break; default: break; } } - + } @Override diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 4b47ebdb..d0e8adc3 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -16,6 +16,7 @@ import pulse.math.Segment; import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StandardTransformations; +import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; @@ -24,7 +25,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; -import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -156,7 +156,7 @@ public HeatingCurve getHeatingCurve() { return curve; } - public Pulse getPulse() { + public final Pulse getPulse() { return pulse; } @@ -166,7 +166,7 @@ public Pulse getPulse() { * * @param pulse a {@code Pulse} object */ - public void setPulse(Pulse pulse) { + public final void setPulse(Pulse pulse) { this.pulse = pulse; pulse.setParent(this); } @@ -244,11 +244,13 @@ public void optimisationVector(ParameterVector output, List flags) { break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output.set(i, signalHeight); + output.setTransform(i, ABS); output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); + output.set(i, signalHeight); break; case HEAT_LOSS: final double Bi = (double) properties.getHeatLoss().getValue(); + output.setParameterBounds(i, Segment.boundsFrom(HEAT_LOSS)); setHeatLossParameter(output, i, Bi); break; case TIME_SHIFT: @@ -257,22 +259,14 @@ public void optimisationVector(ParameterVector output, List flags) { output.setParameterBounds(i, new Segment(-magnitude, magnitude)); break; default: - continue; } } } - //TODO remove atanh transform and replace with abs protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { - if (output.getTransform(i) == null) { - final double min = (double) def(HEAT_LOSS).getMinimum(); - final double max = (double) def(HEAT_LOSS).getMaximum(); - var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); - output.setTransform(i, StandardTransformations.ABS); - output.setParameterBounds(i, bounds); - } + output.setTransform(i, StandardTransformations.ABS); output.set(i, Bi); } @@ -286,6 +280,17 @@ protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { @Override public void assign(ParameterVector params) throws SolverException { baseline.assign(params); + + List malformedList = params.findMalformedElements(); + + if(!malformedList.isEmpty()) { + StringBuilder sb = new StringBuilder("Cannot assign values: "); + malformedList.forEach(p -> + sb.append(String.format("%n %-25s", p.toString())) + ); + throw new SolverException(sb.toString()); + } + for (int i = 0, size = params.dimension(); i < size; i++) { double value = params.get(i); @@ -308,7 +313,6 @@ public void assign(ParameterVector params) throws SolverException { curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); break; default: - continue; } } @@ -390,11 +394,11 @@ public String toString() { return this.getClass().getSimpleName(); } - public ProblemComplexity getComplexity() { + public final ProblemComplexity getComplexity() { return complexity; } - public void setComplexity(ProblemComplexity complexity) { + public final void setComplexity(ProblemComplexity complexity) { this.complexity = complexity; } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 8cce847e..1039f258 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -1,6 +1,7 @@ package pulse.problem.statements.model; import static java.lang.Math.PI; +import java.util.List; import static pulse.input.InterpolationDataset.getDataset; import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.properties.NumericProperties.def; @@ -9,10 +10,13 @@ import static pulse.properties.NumericPropertyKeyword.*; import java.util.Set; +import java.util.stream.Collectors; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -67,14 +71,20 @@ public ThermalProperties(ThermalProperties p) { fill(); } + public List findMalformedProperties() { + var list = this.numericData().stream() + .filter(np -> !np.validate()).collect(Collectors.toList()); + return list; + } + private void fill() { var rhoCurve = getDataset(StandartType.DENSITY); var cpCurve = getDataset(StandartType.HEAT_CAPACITY); if (rhoCurve != null) { - rhoCurve.interpolateAt(T); + rho = rhoCurve.interpolateAt(T); } if (cpCurve != null) { - cpCurve.interpolateAt(T); + cP = cpCurve.interpolateAt(T); } } @@ -270,15 +280,14 @@ public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, thermalConductivity()); } - public void emissivity() { - setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); - } - - public double maxBiot() { - double lambda = thermalConductivity(); - return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; + public void calculateEmissivity() { + double newEmissivity = Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN); + var transform = new StickTransform(new Segment(0.01, 1.0)); + setEmissivity(derive(EMISSIVITY, + transform.transform(newEmissivity)) + ); } - + public double biot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; @@ -329,7 +338,6 @@ public NumericProperty getEmissivity() { public void setEmissivity(NumericProperty e) { requireType(e, EMISSIVITY); this.emissivity = (double) e.getValue(); - setHeatLoss(derive(HEAT_LOSS, biot())); } @Override @@ -339,7 +347,12 @@ public String getDescriptor() { @Override public String toString() { - return "Show Details..."; + StringBuilder sb = new StringBuilder(getDescriptor()); + sb.append(":"); + sb.append(String.format("%n %-25s", this.getDiffusivity())); + sb.append(String.format("%n %-25s", this.getMaximumTemperature())); + sb.append(String.format("%n %-25s", this.getHeatLoss())); + return sb.toString(); } } diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index f6304e3f..feafb90f 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -9,19 +9,11 @@ import static pulse.properties.NumericPropertyKeyword.SCATTERING_ALBEDO; import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; -import java.util.List; import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; -import pulse.properties.Property; public class ThermoOpticalProperties extends ThermalProperties { @@ -155,5 +147,17 @@ public void useTheoreticalEstimates(ExperimentalData c) { public String getDescriptor() { return "Thermo-Physical & Optical Properties"; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(String.format("%n %-25s", this.getOpticalThickness())); + sb.append(String.format("%n %-25s", this.getPlanckNumber())); + sb.append(String.format("%n %-25s", this.getScatteringAlbedo())); + sb.append(String.format("%n %-25s", this.getScatteringAnisostropy())); + sb.append(String.format("%n %-25s", this.getSpecificHeat())); + sb.append(String.format("%n %-25s", this.getDensity())); + return sb.toString(); + } } diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index cdda6226..cd5279e9 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -42,15 +42,16 @@ public static boolean isValueSensible(NumericProperty property, Number val) { } double v = val.doubleValue(); - final double EPS = 1E-12; - - if (v > property.getMaximum().doubleValue() + EPS) { - return false; + boolean ok = true; + + if( !Double.isFinite(v) + || v > property.getMaximum().doubleValue() + EPS + || v < property.getMinimum().doubleValue() - EPS) { + ok = false; } - return v >= property.getMinimum().doubleValue() - EPS; - + return ok; } public static String printRangeAndNumber(NumericProperty p, Number value) { @@ -102,7 +103,9 @@ public static int compare(NumericProperty a, NumericProperty b) { * Searches for the default {@code NumericProperty} corresponding to * {@code keyword} in the list of pre-defined properties loaded from the * respective {@code .xml} file, and if found creates a new - * {@NumericProperty} which will replicate all field of the latter, but will + * { + * + * @NumericProperty} which will replicate all field of the latter, but will * set its value to {@code value}. * * @param keyword one of the constant {@code NumericPropertyKeyword}s diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 65cdf46b..c9e63cbe 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -346,7 +346,14 @@ public enum NumericPropertyKeyword { * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg * damping. A value of 1 gives pure Marquardt damping. */ - DAMPING_RATIO; + DAMPING_RATIO, + + /** + * Determines how much weight is attributed to the front-face light source + * compared to rear face. Can be a number between zero and unity. + */ + + SOURCE_GEOMETRIC_FACTOR; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 99a243f9..9b4c0bf0 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.util.Arrays; import static pulse.properties.NumericProperties.compare; import java.util.List; @@ -70,6 +71,10 @@ public boolean iteration(SearchTask task) throws SolverException { // new set of parameters determined through search var candidateParams = parameters.sum(dir.multiply(step)); + if( Arrays.stream( candidateParams.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + } + task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 72c8b83c..80bd5e5a 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.util.Arrays; import static pulse.math.linear.SquareMatrix.asSquareMatrix; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.def; @@ -76,8 +77,14 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); var candidate = parameters.sum(lmDirection); + + if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + } + task.assign(new ParameterVector( parameters, candidate)); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals /* @@ -123,7 +130,11 @@ public void prepare(SearchTask task) throws SolverException { // the Jacobian is then used to calculate the 'gradient' Vector g1 = halfGradient(p); // g1 p.setGradient(g1); - + + if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { + throw new SolverException("Could not calculate objective function gradient"); + } + // the Hessian is then regularised by adding labmda*I var hessian = p.getNonregularisedHessian(); var damping = (levenbergDamping(hessian).multiply(dampingRatio) @@ -158,7 +169,7 @@ public void prepare(SearchTask task) throws SolverException { public RectangularMatrix jacobian(SearchTask task) throws SolverException { var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); - + var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); @@ -192,7 +203,7 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { } } - + // revert to original params task.assign(params); diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index d033dc81..b11adc0d 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -208,11 +208,11 @@ public static void setInstance(PathOptimiser selectedPathOptimiser) { selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); } - protected DirectionSolver getSolver() { + protected final DirectionSolver getSolver() { return solver; } - protected void setSolver(DirectionSolver solver) { + protected final void setSolver(DirectionSolver solver) { this.solver = solver; } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 3863df18..7943c11a 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -159,6 +159,13 @@ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { */ @SuppressWarnings({"unchecked", "rawtypes"}) public void process() throws SolverException { + var list = problem.getProperties().findMalformedProperties(); + if(!list.isEmpty()) { + StringBuilder sb = new StringBuilder("Illegal values:"); + for(NumericProperty np : list) + sb.append(String.format("%n %-25s", np)); + throw new SolverException(sb.toString()); + } ((Solver) scheme).solve(problem); } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index d657875b..717b7bca 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -51,6 +51,7 @@ import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Details; +import static pulse.tasks.logs.Details.SOLVER_ERROR; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; @@ -115,30 +116,32 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - + /** - * Update the best state. The instance of this class stores two objects - * of the type IterativeState: the current state of the optimiser and - * the global best state. Calling this method will check if a new global - * best is found, and if so, this will store its parameters in the corresponding - * variable. This will then be used at the final stage of running the search task, - * comparing the converged result to the global best, and selecting whichever - * has the lowest cost. Such routine is required due to the possibility of - * some optimisers going uphill. + * Update the best state. The instance of this class stores two objects of + * the type IterativeState: the current state of the optimiser and the + * global best state. Calling this method will check if a new global best is + * found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search + * task, comparing the converged result to the global best, and selecting + * whichever has the lowest cost. Such routine is required due to the + * possibility of some optimisers going uphill. */ - public void storeState() { - if(best == null || best.getCost() > path.getCost()) + if (best == null || best.getCost() > path.getCost()) { best = new IterativeState(path); + } } private void addListeners() { InterpolationDataset.addListener(e -> { - var p = current.getProblem().getProperties(); - if (p.areThermalPropertiesLoaded()) { - p.useTheoreticalEstimates(curve); + if (current.getProblem() != null) { + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) { + p.useTheoreticalEstimates(curve); + } } - }); + }); /** * Sets the difference scheme's time limit to the upper bound of the @@ -237,10 +240,7 @@ public void assign(ParameterVector searchParameters) { current.getProblem().assign(searchParameters); curve.getRange().assign(searchParameters); } catch (SolverException e) { - var status = FAILED; - status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - e.printStackTrace(); + notifyFailedStatus(e); } } @@ -285,15 +285,14 @@ public void run() { /* search cycle */ - /* sets an independent thread for manipulating the buffer */ + /* sets an independent thread for manipulating the buffer */ List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); try { solveProblemAndCalculateCost(); } catch (SolverException e1) { - System.err.println("Failed on first calculation. Details:"); - e1.printStackTrace(); + notifyFailedStatus(e1); } final int maxIterations = (int) getInstance().getMaxIterations().getValue(); @@ -316,9 +315,7 @@ public void run() { finished = optimiser.iteration(this); } } catch (SolverException e) { - setStatus(FAILED); - System.err.println(this + " failed during execution. Details: "); - e.printStackTrace(); + notifyFailedStatus(e); break outer; } @@ -327,16 +324,16 @@ public void run() { fail.setDetails(MAX_ITERATIONS_REACHED); setStatus(fail); } - + //if global best is better than the converged value - if(best != null && best.getCost() < path.getCost()) { + if (best != null && best.getCost() < path.getCost()) { //assign the global best parameters assign(path.getParameters()); //and try to re-calculate try { solveProblemAndCalculateCost(); } catch (SolverException ex) { - Logger.getLogger(SearchTask.class.getName()).log(Level.SEVERE, null, ex); + notifyFailedStatus(ex); } } @@ -398,6 +395,13 @@ private void runChecks() { } } + + private void notifyFailedStatus(SolverException e1) { + var status = Status.FAILED; + status.setDetails(Details.SOLVER_ERROR); + status.setDetailedMessage(e1.getMessage()); + setStatus(status); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -441,41 +445,43 @@ public void setExperimentalCurve(ExperimentalData curve) { } } - + /** - * Will return {@code true} if status could be updated. + * Will return {@code true} if status could be updated. + * * @param status the status of the task - * @return {@code} true if status has been updated. {@code false} if - * the status was already set to {@code status} previously, or if it could - * not be updated at this time. + * @return {@code} true if status has been updated. {@code false} if the + * status was already set to {@code status} previously, or if it could not + * be updated at this time. * @see Calculation.setStatus() */ - public boolean setStatus(Status status) { Objects.requireNonNull(status); - + Status oldStatus = current.getStatus(); - boolean changed = current.setStatus(status) + boolean changed = current.setStatus(status) && (oldStatus != current.getStatus()); if (changed) { notifyStatusListeners(new StateEntry(this, status)); - } - + } + return changed; } /** *

Date of release: 10/10/2020
Lead developer:
Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii