From 7b48d003da8a767cbe4134323c877d6dc59bb712 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:23:32 +0200 Subject: [PATCH 1/7] Revised association contribution (#150) * More generic association models * Rewrite association contribution to be more generic * Michael L. Michelsen to the rescue once again * Add hard-sphere theory guide * update parameter files * cleanup * add changelog --- CHANGELOG.md | 3 + docs/theory/models/FeOs_Association.png | Bin 0 -> 29101 bytes docs/theory/models/association.md | 83 ++++ docs/theory/models/hard_spheres.md | 57 +++ docs/theory/models/index.md | 3 + parameters/pcsaft/gross2002.json | 72 +++- parameters/pcsaft/loetgeringlin2015_homo.json | 4 + parameters/pcsaft/loetgeringlin2018.json | 72 ++++ parameters/pcsaft/rehner2020.json | 94 ++-- parameters/pcsaft/sauer2014_hetero.json | 8 +- .../pcsaft/sauer2014_hetero_joback.json | 8 +- parameters/pcsaft/sauer2014_homo.json | 28 +- parameters/pcsaft/sauer2014_homo_joback.json | 8 +- src/association/dft.rs | 193 ++++++--- src/association/mod.rs | 405 +++++++++++------- src/association/python.rs | 20 +- src/gc_pcsaft/dft/mod.rs | 2 +- src/gc_pcsaft/dft/parameter.rs | 8 +- src/gc_pcsaft/eos/mod.rs | 2 +- src/gc_pcsaft/eos/parameter.rs | 50 ++- src/gc_pcsaft/python/mod.rs | 2 +- src/pcsaft/dft/mod.rs | 2 +- src/pcsaft/dft/pure_saft_functional.rs | 35 +- src/pcsaft/eos/mod.rs | 2 +- src/pcsaft/eos/qspr.rs | 9 +- src/pcsaft/parameters.rs | 87 ++-- src/pcsaft/python.rs | 15 +- tests/gc_pcsaft/dft.rs | 30 +- tests/pcsaft/test_parameters.json | 4 +- 29 files changed, 879 insertions(+), 427 deletions(-) create mode 100644 docs/theory/models/FeOs_Association.png create mode 100644 docs/theory/models/association.md create mode 100644 docs/theory/models/hard_spheres.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f60aba6f..335a4bbdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) +- To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) ## [0.4.3] - 2023-03-20 - Python only: Release the changes introduced in `feos-core` 0.4.2. diff --git a/docs/theory/models/FeOs_Association.png b/docs/theory/models/FeOs_Association.png new file mode 100644 index 0000000000000000000000000000000000000000..dd43fd2f9092ee84f49c454e5858ae9bed022c62 GIT binary patch literal 29101 zcmd?Rc|4R~_&=;qMI}^9MW|#cgG$I=DUujj8@n?0CF@`eeNsubGTbt@>{Q4-7>uP7 znwd}tW6f^L*crR$9?|FfdtT39&+GZ;_x$Gd>Q!^keXetz>s;qt@AJN{Q>d#?xxLNNdA*$F)nQIpya{k6kWPvaU(e&Y!m^ktXbRqd-REQL|*tJb?& zSpHGay=`y?TI#=eV zu9*ufqDsfb1X#{p zwb+&OELW~vxn_G#O^EZlwu-v^1$AfGO<5LAcgb6#T(=CaYojjc zT#?mPV$n6#)D4i<6F+G1NXAHA$@m`JI7II5H5P=_AyeoRu3&!qs=0%qMYNLDO%^LF zD{JY)_a2zsi&H`BYao-Z*vs+Q>#*3PEFIF+oO5otAa1(`A|EIoePF=yAWy^F>W=pl zCm)q#z9uZ_@CN}%Bg{44AS;%jprC)TKEY0APt^HBY*|8Z0bw52;kWoBQvZ!|X2JT} z#^?yf96ZB3d1UaYpvb-*dPJ7~>Zge`U@>O;q%NrA+H({uEpOXq6vw#-D zU~~XTNhdsKArJ^<_e9E?bIV^ns(!&zP0jy+6#JAMQv0*09$@+6aF&!A-sB?Ql*sb^ zZFIXktUZl|3dZ$%OZMfk^p_@$_(_fCvy4@x(J>b$i&!QnC#Pz2809RBl)Q}(EE^je z5;b$?kn8OB($mpr>7&oBXix`VLl5>oxQXy&VY&E$`DcfijY$^^i|cRA>sJkYE$BTP zgJ%peqbrqiG&JeO6LZ(aw6cGQl%!hWho}3`iKoT8BSa1@9XfL{-^1OSgY)r)%dE!K zBSII(PVe1Ob4-#w`pDG-#~!nT8(nmU{Nx*zY>OY3I^N!lrLRhYs$*W>hNuf2@ZXf& zH$nff@SWfB@Zg#Va>scE@;_DqF}pu3g5av)#sn9t!Dw(bIHyH5X}wFHaX@cWXG13t zt*Te8yr#GmQ3S&Vt!%6{@#Bk22PIl*1GMTSwaHW7_RA=6VU| z&KzP|d17H{us8a^1_JuNd;7a7z}tMU55D!j9UDP7@3JB|(X7xo4@pC1(vubuk%FIQyqlM;(L@j%VfG*1I@k zz&xwADsD^e2!IT6C@jc?6{o?TgE>n`s=3Ly>2}kr2p@oFMQ^LAi2-1qSzCqOYRUs3 zHK3*$TTN?`RBe-SzU`)(2%l)4m22BgjR5nE+N!9nrT~CsgPNvqH62D$15L)ewyfg# z1awxGSU|>P8pYhW!&7_Zjlc`(%#8SZ8qKf|dt)^+9^I3_7 zBTm3DHCVcD!GSpasNjkG*|v-#n44ucG5!J7(8oJrgekXt$lCd~dRx{kN1g6fd) zAhjYbRD1MjsJBgeX+^NEn_-xB6V*@Lqaog2gvHjRn{vtLOt2Eb@X|_SL|@srxRUoh zU?;9y)=HZ-4tdpB1iOOC%Dek5mSpp#5#f#~dBL-?SWKRIaJQz8w>qs@dEobfq{B{n zfx8$D?1OQVUh}d(p+=_InFb&gxpccXmN5xOsjK{ycByUfGj8k+dbvSLq)Q(;D@7S; zxtCIijvVyx*VPOHz` z=E$T{k>7xD&+nyjIJ;!`Cv^&T-TZ2KJ2sDWJd;mCjXYE9wJ~UtVlO%CfLi_?L7a8o zP?zq+?LS9(3TauXj%dcewMA8rDh6=y5>2rMi@Zev%PdY49GK79yC-eC_NmV0^2H;QOnNWhcpw%`x7C&ax5lEm;x&UC|1-;(U_fzj9Bm9vvIDt?va)>V~nG2xyUfGOtnX+ zv{_VLV{kfRW>B`KJ4HaIv3;mR!Rg|A7@u|+W@-xjjDIWlcq4o@A}{Q>ZVm2pLupq{ zkotNZ%TBU~{%YgNz7xLf*{n-&jF9VW!NtCLhl%|u&bQh#N?{r;LVQUnvxUB^!aWky z46Pc%jUKgPcWsdVDwacnA_BRGVkyl1O2@F&(~ak=NiJbd9)ZeE9;a80=bl#^x?Vx# zNA?}}QvV+GSv27-rjs?(X{G}1Ni)rK(lZ0Z2N}EUP$=1f{)kiQEvz@2_9gw`zlv|f z?U$smLY7u6f(81_hsJ#hx*@t+DG|m8=W54`xINi;ZZSh3$Hoo!*m(ZjpVD@d%grXl zO7^rlc3gL+Rl!Nm1ZetP5UMiCZF)hmtITU`WXgQPHl@SFPWx#b($k?3&Tj%-zB%!R z(A+mWW%*rU0B06>kKuHPjbt8h&lss$&`y(#cRhR9buY(@d`%SbjPfh9a*-#Hv6XH8 zWy^qsrxG~j_3QnLpLKfPjzx|7x#=nJ+SHT+U^lvyt1H*_QyqO=Onfqej zoI`|H#o9;|KEuf)1@4qlGb|gBDBQDC-USsW1IKi>e@gU;gP*8ZKvE4%#--T+Vb184 z&!*JLr5iz>Zw2;PVhndP^`nYaPKAv52Dm+(0W(6h?FNS)jqjNl2_a`XUE~uzx-|6K z^8qRs1iM=}U6Xh(?Q?-?G|L?KdF_*5Oa@bnIy5-NDeUgp;U}vMrB&lgX2fZO@3`?d zFj}ob(gl0!m*R%Lcf#gaV+@YsTKG-AlxToJ;C9%L6y;g?bi`U%oL{JG@kdf`n~ZaZ z#0n>y7i&F7t)Z)g0%kc{RO$ZO11HNKSYKV#pS$bp_G|#G5U9RT`Z-&sYN(}Q1kRD^ z6di>{PgW8!2zflHL9%`$#p zvdiYy4UY2keM`-NF|zArCFesG-7i&su$oUQJCbJ(aAee~xZXqvu#1CI`~T^3L9DL3 zSgTnl_RZ?fwhi^4w}ytDk?lFFc9s?aYg&wlcfa=1PM&u7ZlZx8lcOV$)aD1Jfz-Re z_ja2#Fg%54rDzA!Zx~OHDoRj7OQ+{z7tgKAcLpM&8%mwE1;X{Ah@>T}BiSYCC*QM^ zmi&hLaA|PJLI*C-br%~AzQbr`e$N-g6rqk zap8A>LTG^-TJWe6dF2p3CV=yxLpCzLTBISvBNM5qkuBVQU<0OD2R<=R4bYC(%E!x; zy$>+9fAbnf?dkFt32c`PH0cQ)jsr_j=E6ZC{kG*;o(TwI9_cKeZCPl89dY}gzY!H1o`zdH>F}OcJ_j_o_Cr=%)L7gpiz&C;D70x zQmx`mszici?`9cEQP!pt(hRv8oFDhGigZGX8US}7MGaP~SABivR~H-yrY?5QOv?AN zWTb6CW^vj9v7Q8yI%sgs2KFxmGa_hXS?i#kSQ z)>%{b^(80XjwlGGuP0;dJDs(oMfZ;TYDYto!mn5(cvMfjf8f5%YFZ&8h_KT(lG5^d zh3wX1VdtNU-2(|qk41hi>ZpeV4N`xE*~wkmTOh=Dzgn5C2GuBwfPC7gJ%f0hfCX&` z`9=g6DG5F^><>z`K=)-zHoY#Z-KGKzkKZPsHk0l=gY6HC@i1cX{OVt?uO69@dUGf24c#%G(m|lL{i^r=(#N&h&OAC`s z-+z6XM-qAZPZZ?B&G!WLj3D>GsWgcSRO~NVLN8qPpfNWT@OWH4X{vrtOl)S5SLbh9 zXm6epzk4sPVrkxH=BEsz3>GN2YHx}vrx#Dl9LK}%XV$8uzk^MWJC>88*Plx=G}kQT z16LP)Q8qJd)j$C$bg*D4MLjLST^tzB9MlYlHYSTm%cB{LhXKm+RN@@l+QMv;YF`=c zYMGzYjIGB-e-chfbuw{|j4Znl2)|^%pY**Zge%H@+NwmvBY`~3**6&U%B~d*kKtK) zmIf|Te|V``*IItRbj#&c-P)_fjM-XgKsq^{;=F6WnRI#7XQ$ zseqNp(p2fP)t~g)e2SzvCGopMi=fxV32%}_w*aB*%@CaXxYY!%BB{GrnD0!t{|YID zQzpv3a%e(SMpZm+xO4H;_ZKx;gigA!v0?`Pt`7_0q#>g(z-l8fD1(GB?!LT{B{>Kn z6N@@T569LD5_qMEo^NA^7l*AQzmxUKMWj>?7+cx)1rHWw+DS{Ab7R6k+((_+N&9NJ zW-K2YD_prlYdr&;`xqM5$f!C&aBY*MtjcJ@dA%q6yK zdz82sDCU@d82!CZ@!Cfr6{I=R$s+`w*ge$|CZ#m9H|WCJ)u;y@^>WE0vrB0U=iYh> zTwd>-4(W=B^YnM^HZ=g`21j@GuszDiem2sRb8&pn?jBRmIFlj~n!2XS$x@8#c(y>$ z(vNyOxQ(KmWq&N?;s)s%~`I3EqMW4SPVL!=)lGYu;Jl)?tRC}xBJu1fPj zW~G-qk4UAZ>m5Y1X|Xp!p;@{KV|s=jsr%j)!IMXV5P>jVTh>nIE9aMYWPTZkG+5y82pz$n)!EMVMy9SGjbU@ebu8!Mw2Vt z*69%CBA?K23Y??5{_0SI`_UENOC$M^l*(rwMk3XA25C7k+MVYUAHRDgWR8lFoi)Vn z5@2C%XI#vxL<(VcMRo~x+4=ZcL@>E|1wAg;b#R{#0-)-L+D09w@!JzZ!FLHfrar^W zbvsM!5_W%qX_e3_7di|@8}4cSu8l#r#VpNl`T(`b&nIr@nsQvIscHj9iU|#8BN}@j zvC!ay8Q@RP^4g^X+2gxI&&QKAH{~*St(b{DyMF`74%}z(441MK7s)2X-&j&>8ExTr zK$9uo9QeU|loEXc>yYBwj8-d;h(R_Kb)-Stqe`|oe~myUl7y1P@ux1Jl=Kh*FA|;I zGO91Bf%l8+YQZx{aCFY4$6sy9 zpoNJu$&fN&XN;vxn3(_p-;6Kol~4YAw9j9*!l6zqj4L{!2mks4gK?twW>7g@<%>UZ zy5Ta_*Te|0`Aw@mZ+)vk>fBi^1%h}C%8{&M#d8o=9F{?N@V!~EntWfGDqKC-h@uV;aiA7{Z%jmM0Ub!rE(>X5q!<#- zrLQ4IT$LnDu#rBPHr_J1Ankyr3wKwe#?d}hpK$TBTIC7f*GMG|ucAW^sbKOqR=ZBW zLjZ>DL*2q~Wyf|=#b%-h!gfPWHGXUle(~$4rp32)N!b{M^^fqzaD8B|{8SI$bEknm zML0V5+T^RB5wS=}mwS*d3_&loQ!C1-z#F2t^ve4{ZF|P(yT(T{GH8*TgyZ2YF!B0lm&hSxcVZ@J&BRfp&AX_A^tS z)lLGoi1#862p(6SBzxX=X=4a-K?}rbt=Ur*k1xNM0m4kMURdNu+%9_Fv6;Mgu@n!O zA1pOICQMHDRgnDm93-MtBsobqj$0<>+s3;k4gmSU#5on}@jhkQ^#`z?Ji7K=9Trxg z*~bxUgus4?ih_07nS9zvU2Z!g5vFRJXNe~p(*z7P5ffo4hrDE2n|~i{!Izs0#X_BC zd`I|u>#f*~1>{WpLTYjrc`;5!)`dME32H1wYPMakOaGj@p(QEm=$6q{-BLsX_iKkq z;Pnu=^c{4uw(HC2WQO%%tgy;ranQ4_b54g6C#5;6SLO|*yYt<&o5MnRFrg{M$!0%L zlvj_y3^utLPsyh2CQhQwN6cxBb5I0bLX?Xfl_cIr;xm6xClHk|lFm((ZiV3M$~wz8 z?v&_vnvD39feL=>xLw+~-o9!F{C9Bw?Jx=O7Q(_I@HsW_A2Wq2OmAfJ@L;!s0SF3fFpC(HH(V&f;C%$idkXBmU_ybc00p1|(W;Pi~ z3+|Q^(eecS*AU=yJvS60iW^Lwg?|Y*1-!IpOc`>0cwZKC{VeM1?rvMkMVLg%=W|F0 zc(YGE4mWc1>iJQEf(fEC#kp^{D%D$i_HBQ_*)Bl3~y1HQGFl#5z7dCN?@Our;f=r7A~>EO_{3t5zl zXC!1dX23HcLi6EamNhM7Wyc>LhvH_6XWzlj)qlpyhzd>1O@Y(YpTf~HH2Fr*OS`i= ziZle2-Cs76pb+uI_*d*)+QUbW;}U?x@Z;$)@q7|^A2wMz5n?m<)k_o6T={|&D<`ND zu=qiSbVG@K;;5Vt0$d?Kky)B6CJnQ(Pnw}VOUb^fKF23Td05mjP9e`Y`=3BsGDa7# zFXxwK+Tm?8z>04Z4{y_dt54*PyK?Bu&E=C!3#eQ^j{T%`VGA=9bd0#ocv{t5odsdy z_zB@B7|GTIHJ@-@V3dxk{`zDM^-Qz!_r`VQ(WvKSk4yaP=Xk<(_$pybog28+6xFI`^r`$)LPM1?fRuUcbh%Q zADd-p>%w-(__|z%5&A8JGH-lRIvVJ?q<^_hN^4~=JG9p{e8g38x7l;4Z!OpglS$1u?$vCe97^i|;g@x;We6^8r2L^@8XRT*C04X8dG{38LAvtTMj}O{QI@eU^=T#Q}xqW z@{FUInK@?cTk49ya{s5UgGlX|+P0zRt5tfG_M!aJU+gtt5_mYP*47+sxEOh%1W`A2E&P*-_3LOFLyLchxU9UVPp@c&bnSz88gZZ$jxrzL?4bU&oKE*PNu07f}3Fg;v@VQ6YYD z?`nO>dH_$ttid^oxOB_{R!qP46r7<4`tJnU^&?TEY~`H*^rSu)2>e8Q(Z?=Zjvu5>w;|BgfbB1s03 zZHD;N(Y4wr;S+k_KMc^%f>|HYX_Bm0@i*F#C?S{tM_TUT6Hg=evLP0bN5>wAoO2r% z3>t}i&rADKe!ligrltY zqwM+QeV__D0}-Pv?~G2KE-x zKL#b(Wf49`z}KZp6rw{1QDviQbAZ|VEh;jP94#-9kwR7CB{%6I3uxOxh)2cuyrWh z*zW#0xjCzLJ$OrR$8%a(9Q9S;jv|K_yu;Hpz!T(5*FHmJYeT$fL=F0j}8 zW|G{RqF>6!+DZSuxk($J#Nn%|>(!DY{TD`7rY8ab^-Z4JM|>%v#6nMl!!V2Vd`JCbsYJ#H2!B#K?~>GQZqF=ssm; zz{S|f|37HVTMUEqC;GK!b67cD+nTdIRp`*vNi_ATQKndLb4uEBv@J0l*Pb^SHi#V5ub~w9lIWcbmjM2&VRd@Rx zBjD(aHV@eY+)TZHCwqiRq?C4Kb6JILrTJU$BIRAfvc)Kxn^gyO=d@0oL7^9$9)gJ( z%sFz*06$;#Fs?kjF5WRZx&Oa*n^Xw=3^IPvbZ1BN_Aqo1P=e%bZyHQgl{w+OnJ|@` z6S?nT1&ay8&JNp3X2Kx0UcwED^dt4b?6kI3ImTpHq;vuO9h>931JUWO5Lp_F#f|F$^CrGW zI6BtdOD=7#y*vbu;F)&Ul*h&gk@E6PFK)#lLxYEK+Q2&)*J6gpe{UK?Z!(@$e+P1S0)1d_shKa!^6TI}SCuvmHCbQQ61dUJLyfQdmP;z+D8#53Zp z&?i!WH?ZU_+01=J-@?%6$?|Fnd$y{$iB`p1ocIs>B6VO^l|8FQUK1!|$ZWZ?Ps z3EJ(Y#87Ia?v?Eh?n0JA=`h@pY&AnUie|z+2lt6lEH+KElQIxIG&G?zONoX$+1AsV zIYc~B^wLP2@MC1(PN_>Pcl7_4AN@NQpDwCTy@Q9r)a4#0WiMEGcM*gWU(WRsa8D3vcBlH>)@W_C|QnmA6B6Ai@GKwQr=SvAK7< zz=s|uT(%drez+E>ys@>-no3f{qcY;IPZAlvP10dvT0b_Y84WFspC1?YXbmI=vk~h} zoiA=JVV44!2P&?OUqz!P>&_o=u^qWO*AI3NJ8N&b@&6Hl>wn*1t#65_dXO~RE^Rq0 zXJWb5)(Y>PB4{v?$JO{>z_xDGegvFFD57V&E?dr?{hhuk)*QLy=YM8c5OZ$; zyWVC~GC7M#xmfl=^j*{@7V?g*Ezwj7NM4h&eKYzPj`n33tDI7fn~Q`am`iXs^36Gl z=R$R?V9>X9&s&t4$ZF}OSMXQMPvF0L(7a3t_2{Bu?C|}T*rkqw1aLJEp0s!Pg5|QYx1{ySKoJp+4eMWJ6RJxcWu3sVKw=OIZId>Yj*9SC^m|E*y7_ zhyt151lkjhoslz-TY~5|4&lLpz;Ucd%G_RPRE}RkC2PU&r8T0Qn@GXX0C<1nqKmpd zFu6DvKf*HMx851>FfPUuPkij53-a4lKHF#7jRFds#=) zKBO(F{$`mrZy}7UOSA|ADQ}X&$5%Z{5~K+*`reVDB&n4Q5+mT#9%v{9u!2Olly3@S z*gp_T))Ww2p>t*dE_zTC%VJd-px{0t_)hRQcE2v2z zfCP6=_N6t2)U28SD4g0k6`hGB!Qy`2$+5gtm}3#xeUBn4$)N4*V8T5<2`zt&b(8i% zy%ObtIPos7u8!~B28tfTN?pWTvLPyRBMpcwXgqIWQXK=iVQUea=Ok?an_CkhvfjqRge^Z2b(G8-l42@4>7@5~GmZf{~=iKAnaQ-1*U{wb}}>NX@u2p?=MAVu?1d%Oi7(DPJE- zdr!+zs7y$iAg&d-+V-`Je;=P59+Q#EGt6}2J`GiZXBz;2>jMapXQ@v5!LXYc~#v^p_L7-RtUm|=tWW#H#SmlR>PjSR>`Fso}h z{GxFaXJiuOv{)&JA?zE@#E>ma0N3(uzYP;dxj-5GMLL@uAvfvQ#u_1bfe6l$=$Fg9 zSj^GN136rp88FAkppuhTU33QWc%c3!WOKZaTza#HP6>G1(|W#=sAgS2!!A6wZ3je% zg)Y$4ROXcR*nU?u&Hoq7IY@?zpn9BwGGL zS6xPeXoIji27cgpB$zW#ftt|SvmZ5sN(X!SKQZxKrvYNUbG^7r0tfCaQyxUtoGcqZ zlv)C#zF%8-sO@1mJMQrAgdO#LF~#F_vO6cUmPUX>|LG=5mNf)QwWNDANy)YE2qhd4 zu=&c{oNS>((CKvdpgFvLRgf!<`E(h=$p2yHha9oO0@cb*B~qRDPwA6fM9jb33^bf2 ze0rzvfbdG}Hb7sutq?8XC8xDXP{c<&JQ^N3AOu3Otbckm0DMCd5UeB{#CPn@%l&T4Ao*wKE(BK@zl$O zU5y5IC^1)WujIq6Dx*Tw`lVR-t*5IS6&#fMx(dT#CJtx9ekWN32&J5{`4^DyG+pL{Oa}3OXHc=# zxP1jeWDk$dn}~twu7z;8KV278oFmX(n~m)>tb*p#Z6>&usi}h5DKCgt3y)uXhsJD# zSap9(dR2P)RB4ZBES`A^lDz|1)(#TrZdJ->6lJ>j$yv)qmeqsYyw=>KeCyXRGa5pI z_x3{5o6W?pR2xmo_BrK;2xPP8WZy0c$p8dseHG8v{isW8E6 z?VZs*8JX)nr=Sx^TqfagXa$T7^K0m-MxbKbh0HJD~0h1(dR2o9cvVQLS-bM4$~K!jkFALAn|~Sm4g&M#)}C4M}MF2+x|1 z`5|ypQ`~L(fg4X^w4p`HTL}wXm8}k-GG3&;ED$^)2puGtry5DsS6We(i-~t&lv?_3 z^}ss+UtKE5Iws*2VF8I>s9Aqwt=gtOPgH}X!@}G=6I*(I+H+;!Mn>%pbt`JWUhRFl zLe~GtLv5(&o|g!#@@~eod7v3``YRI$kawV?Q^E2)vrZBO|rA3(YREw1< zD##Kwwdk9Y#W&OG%Bh8=Fq$`udqwwQB-OM|QmcY|zIrV~Q&J~C5cLeq**tTB%ma1r zfv%h+Y-MUCR8X_%t-72zJSQSiJjn^gjvUCBbmS1d<4G?>0&v`mvQI@iY!?ant5 zkzMbwq^jh8Je12-;ThVU;xMx}<;8o_=IgaleYR0t9#n0kU%h``hfa4*W7K!B@&xQ+ zNmDDs$}izI!coSRIlYbi`1J8caqmrKU&Eq06ZJ8}H>E^O=B(+*x~mkMs9t2fw|6K5 z^JAFvs|8)ARGlA{iq{baeh3Svv~Q>r!pd^31nn04r#~&e7B`|Ba*(0*Zlm4*3iqC9 zH{5#Dm2f=5VhVdAE=i$2d*1$6Q32y?Q6W9hW|IjFZ?N(1A-N;&d#qqlV@@x273~&_H>~R59bjq zrZK7LHSf4e4M?AR2(Tv!IkQ4&eFEmBfl5}hXC&cWGY0ghd$Iz~ADmwKR0qBEkqr8zf})GF2!sgUVyZ=%2!loYnfFF0SRX6EF;*9DT=y zykjbv25~se>os=GJkw3o2YCmWA(kEewSmn0bCEE(XuY`OwCC z!~X>hHo1TjL!KZDCqch~c^`+?ElxkG%i;p8x)j{Gf^piz)XuGg}NZ zrrMRtsE~QGw@ASYl$pnv>w}yrmmf-Or(=YG;m@@T2RWd##wI&>kTUZgb1iI}GOhqC zJgP9)e!qqbeE2J8n(R@4`LG`;uer&M+9UlOQvR3kWwij@Y@+3V@Zayi3ahYe$ptyq z|2Xk?Oh^u0_JD5G{9&xsHyP{cu>R^tnDs`MlFM6bGeV!qvVPX;uRa~*^dC=Lw|QXr zVG1#<8cgOcJq98lm0!jV%LL&Q{}QR`P=$Fk@mC%pdJu+- z2gFlVp3i^z;(7ASUQ>GycOvN;LML!@!}@=Ko12;>fr+KvKp_+GJ+@!GfAzmnX2aG? ze|Q}WN<0K5E&iy%G;y3Ol#`@rMJfAEoj^0V*{Ww;1a}FIW40XCB%Rr8Tm|N?_d05~ ze4tx5#z0FXNNF+L-tmI4wbuo|pnT!B^M6FXtk2t*aseu9{SnqbjGsQ%V9@mbC=)hpUcU@x z9!Mkxq}43|Ph5J5%3<4EQNLkr$Gpj4M3!jiQ{btorgdZTSJhkn+|2 zEw}ym$f2cx;S)hJ!*+lOmM`E_I1{&Rt}N)r%~cPu@+BaQ{;3Po^3bhSO?B%I`}_M} zz8i=UE(=By6_2^pcq|Cg7vs);-M(cl#S~7!9e)l)0g~@m) zx`Gwn8ooj%s~=JqE>rHSSZ^ZDP0xDG)Un5gM4pL(|3(Ej{lJR;9s4HdJ@omTfeG*g znu3G*T8Y0%SSGIB)ch1vbN?|JDGC8&Qea#6)(iV&D~PeiWXiv7F^=q7Q^t?g2+aE} zOY)_`44I@@&J4JCd~}^nE_?Lpa*0D4H`mIMP`SA$+f5-Zd}3;@r!+S=Qj@iNL}0qC zktSbvZ+x2>J|{^LlC^}YVxc0Z7Nn}nc*t}V?9>!AZjZG^ri^bup|1cGQ%^i z^6UArGE1ZfBfDVEWp>ws9LQ$H{SrQe966ylNL(aMh@+-nuIKUmO>;aa zLliu=REd8-!=R~0jN#oR=si-VMEvKq_2gDGHMoPw;HSNe@hJ^_GkT0ZJ;)VU+eg=o z`kU!~lL3>W!g4XRULF5WWj}&E*9jK5p6hWJO3|Ci(O7|x{1gKH_w6Q`e`MpgGuMUy z)#1!U6G&iEm#z8anKt|a=3nQc{~!PY(d#Jb(?*o|?~ys8v;O&)3ub@gKT%u@jhHhI zxJD7#N?5l}kefM)SgGQ_5F`)UgylLS;TGQXOcsx|m;VEI2#2RD_F3+te^EHz@5a5I zUwexrxsD2^fE}nXmxNo}*hYdvUw3>{85?FHZ==8cDiNtEYl0IGIo4BMzc~SmVf^N%G7R2s^jj<@^eVVl_(f8O4hE?rTef9%A>$S7d=lbj(J4N*+g5Ka zAJY3EpF-$$#V1u&b$6p&ERp0_l8bJFMML7#1q|d?-$d<{9^X_qLBiV$qZC zrZ(|l)PCm++wW$>CVOlsm-4IVskvR8<^u(M(EnNR7N5DDYs~|dpr0+~LnkW(`;M!e z^`~YReC64$MS*Go^5N2Mwjoxov$^8Jn|rlCTMu}%89Pjnnhd|*1&8llDl(;AqWNTCYZc{q zlV?DQf&2xBh`DZQqKNIOpX?kBgg2 z*_-T9EHR*92jGQDGuwqbFK+J?(9PxaBHCqnY?5~)*My`UW&2>qi}dRS9>^^f)nx~i zTkoIHe8SAEwuxyelvLw?{#KC}tI`aU*uK;66f-Pq`bl}Pr8_PFq$NjLGy>aZlYz`8 z(LV!Sex!{_-zwlugL!Ypt$w8651t20J|v2R=PfB-99X+;H(x|E2i7@;-s1FhJBmGx z8&)y>AiU-~o1oHQ_R2E5^t5U8U%wFM7ab);px2hNTkaFEQn8UXp?@8O*9<_ptUt*Y zDEfF2$-Z4U@e|A#99j-=?7%+_{|nNCDbdi?^pL<2JcqU5cFhs=FcPbObp4wh-zSxa zTV1lo2`d)|hCh3Pic6%O^G5EdxXZSG^O^u&1W`dkba%~6xE#hL zMQ-qd6&d`q`F{W_5>yThnWr4fM9poK!}TWv+g1Ms$1iL`nfEZ-<+4VOyUicV?1sL_ z?^V2ZwTb5N7me(s%wQ6_yaktECSw-ZpDP4th>evU>ts@Lo8I_=X`*tMn<)$3aygls zbp!&N+tp4=bfW2m%7HVArCpvr_9e`^1m(^8Bk1-Azi2z=iN1a8Xqy6Bv_15|Dn??6qV1cvef8(idE;>?rTr$6<8$}py*Gk;-^Gau_N z3mtwGl@3E6Vd8y`v(Rl!VZrK++skdj?2_JDu(iPH?n9G{;fkB4Q6onu8>`f0--yb# zmM3har9K1aR_-op#ZTTQ>oIB57-4>O^q1^-xfW|ltTaz=RSG!gFS?opo4SzVj@m?+ zR}jsqYb5@=$xQ-Lm*vwH8-+Cx{>n$!b$AwWO{I#=sfF&%qTE1vKEY51T{UKcH9kQh z-Hj-wRWSLu8cPbE@b^$Ks+ zN5HgXeX0k4sS%Xtd>OL35S647S>Z20Pl>f+TrD^m_*}ZvmhakU6 znHeHFLsS_`OH`4RU&tlLMM}IQYolQ>Qw~(YNzL<`oISfYA>a*k56%g>drO1jbnvS{ zhVRqf(`1p90c5UE;t3>84biGYp7HX(Pw@=ua0%1y2}Ak~n(E~91&H>dR0(5V9_V)R z^18sL8&Qycv)@wH=%;|TCoQ?8X`Y0lqm<^}T{=eVf@=|xv-vAVo@Le{eZ?LZmz+{` zxi)q6K!8L~yJ$O!PKxqfz*3gr*c{7+a{0w5te*F~IGrD>ugcgv&rI=)kuTy(SR3df z+-?CM`+j{#S?d-L4vA?AZt{t-9k}sIC@tM3g8dz!cW$!_hg#Itrljk$P)y4~m>qlRZx{{Gl1iZMy+ z*%wdm`^s7M#kzC#-;>^d${;UBqQ=?U7hl6~7opWi3%7}}LK-H&_(e4kP!8kfAVDTt6ZnKKnl)Y(D&7NUr{klAUl@rQS0MPr=T1Qv?faRCGg*#CP~xR z8$vLT9O_HG+-_1M!W^uV76za{pFmFdZdmSXzSAX_uvH#gB1nAVky6&FT z2j{XY-CHWaj;Qa6)QPoH;ty%V|>IaBxB%X}&VEdYp5DY1*2 zpVJ-wTs3JO>)-mn*Y*X z1qCvtqIZ>XK}|iSzc*{_Vs6y#SZFGi!zTXf-G~#R)3;3CXKm{DE&dRN?|^13tgc zh-<0nG!|#4EGPdmllikE%%|(lmZ)MmoW3WRhX_?H*>~BBi&jRJL}C*hCrcX;qgZC` zP^Ng>snwrdrdDuLgx;t1+f}4yKv?l-*X_gztd_u@Xc>F>pN*;c;3S$q?4`wYcMt#b z;30(&OWd;zxk)#+5s`Ywn!-M-N%P6uf2Ki5pgKZB)wani<<~f8ZC<3#u}<(zleiNX z{%d5;YDLenW#_5}{Lw!KCoN5P`IT9ww>O4*KOde7*bh^P?ihN+>H1!!o%%*8=n@fc_$u_A*xmHW4moe9KL|gxi`h005nj) zT|lAt{Sj$UQ6VpH)MXLv{0qI%B>BfinoyeD;cBzmIka2FxX9MC#fH9Sq6Hz!Qr4Pl z)L@fDjuAcxB^%|8Ns(=!;-x=bBkRPYyY)TJVICL#^YM8p#16_Gbj`d_?ngt&KuX*)I+H;+EvJE z4vW*KWk>XVnr``oh-on+o)+W)%F>m-^0x|%dOe$_jzI}!X2+xTZgxk7N}AC|w&`O! zP)!aq0wnf57p~U)zskGvsHU!N-`1+NR;W{PC{RUYFp7eJfC*ZPKv6+aGz_9bM3689 zgaQH%C|b&(KoA+CGC45?2$K*i$dCYGQVFxnND>4>0%YKwo8$)iz4!g`)_QBb_160{ z_ny1&KKq<|&e^}ce>-+fC_KGD_t*i9cvtY6T>Cu80-cQf+tHPVyyNd9ZoECD?ZrV} zW!2#YRxA{Ih4#61b3us#Ws|A1=X@wo)D-IWD*h~%)7EC~OQTppv0>3t(+9;tr}Y(X zd1cN+aXqMuzmQVB_g&mo<~;Pg0z^?ETfF0URh&#o@1VH1;9kH2_XUG!_s53&3YYcX zL%~Dn)kiTFXnWTQ1DZe~_sn>&G1f>oWIAlqUMs5SO- zsiU7Zl1$*bG}ErRTFwG2*%dM~$RueANdn`E(u#aa45GV!%rc=5|1n}J^m?jX-g zbb$DvMjasC9sf|NvXVPjaGEal(T}5Btc;RV3q0lruMZ$pMCS;QYI3G?d$!ikdkk*c z2G~E9y8l;z`~Q1lWQ-OaJ;Lu};YdG70NSh*_z~=8+36htC z+G&R;kJ-3wgYlA7L=k;mwpEnIoKq`Ou{>!voF@$2eLLH?Lt!UV*w2UmYI%7_)x29W z;O7dLcL86E?W%t?oBi~On(be(s~eAD9Vl^biAeYwERbUC`V+=%B3EEU(3u(54SH0v zmwepIn6yKpFbFr$4wU#2LiCZo1|=@--iQCysYhut5`KwIcZaO44N@)+l%Nzh<3g21 zIhP`RJki8S-o+;GJ6kwk!w^5zQ15D86LOp5QZGNn2DQP-?|q6Lal&GzBQQ@4b?QjzT`TaA|1MumKr!_c_l`{Lm-jE~)~3kT zwr^N`AKTMx1k|PYyPMCjL{M)ty%Okn3*6Qfk`#e0*7zPTTttCBdf#hAen-7bol{XH zaTRR*SBSC4aYm@D``zcZk@xPyE9-Guj;Qi}OoZ{4f8YU|ZWHbLHIDMmZl@5Pp{1%# z3FUnJcNRT^|*k>#EC_J_{`BYwcXB7n9;$k*noKeu;b+D?3oA2KV6^g8AwG z^9kPm_#@6^HHBIuHLh;o!;@Bw5U6nT*;(dzAdCMpdWSAD&Cxd5L zKDRKM{w5SX2wq$&ov9V1TtTWy8eEEBZw-tKIY^UEGKt z!X{qHGcsgEawi6i(C*_~&7D2q=*{a5dvaUM7uJlkY6cKV(UTpSi#j^NU{gMRLL z&h?oi_uOP)DZWr5idOJ&&P~sIj`IyVFjo)+HXs7712OZ~eg)Ywld)qOOl^rxz&)=W z=27fD3mlkV1?n`;#mlGbKFZ?HB5z_NIz>m6iU{Ny#hcW)}{9 z9sq$GddW7*&Hq2_$$yjC5`o5-$vA}cy-DYB9lZPAILjzwvWf{lQDpVS zJ70)D=PuuZlTl@;CJ$+RPk#!p=f#MIF~Xs!%ur-Hm2bSwHdgZWt%x7eOzch8Ht0RL zf@dnR4gm8Fu$&`Wzq;?>+=nF;AQW@y-ElC?sn3{ zcBLZV1J5f2AZ1)whLy{eg_9pHfJk!k-qCduV;J7BqM$Ntk>1uy zmQUHkAULaFjcT~zq|%m<{a_ImvFJe6?trhjJ_PH*aNl(H%^adE9Ao(!V@A;Qxo@${ zbC8m|14B=R4uD`cq<_~n<%Ro5P>4@M1d3cpP$pI(F>i7+q4-6v(gs9vbaM|ZM4Nij z*R|F!^ybze8`;1_iMc4$DdWVv6Z=VIx+riV@>hp2b=fAbsVdp$#Ssw5!voAiTzo_O zS^oo2-7ydr5ncRnu(7`tPw@@?hC(zE?Y;i!x3c6jP13?q#s36(E4nJrT2RlGWL*a{HRkRP*fXeBTQaxoy{pX+^QE zO2waki0+d8Tq`G#jm3b1-XN9_tYcxI`}gH{$H1<%9>hOHsOO5rU5b3dPjW^P#B zw1x_xT*`~wEmiY*#`~!Gv^!BYC3FXM3h^VPcw02_=LBp)5}xp_tIXf^iib?l9DT?s z1XIB$b;yRWVgCiRr=cp`9Av{Z$Da318}hV`Sgy;?gr%!f^U5CFD&~100~qasFb}uH zyf2P`G%CUdDk(TduHDMm`Pa1ffEjvKbh^())UfI%?nQ@JVgq6A@8E~po5yKH<$xsL z+q9N2xNR|_m6R4{-DUi-tG(Pw71j}CKn;Jnu`HfNC}q59yRdmY2Y%xfd^%v2jzbgX zA4J6$4~TpLK`^Em#CJHhlRP~c6~ea=_Iq4&b&vBuau{OWKU1enoe4Q_nABFY@OQ6k zmL|GgS8ueC+Q)Mvg%7z&hl%@0FnR));ia8hN6uCFv{cSTPW zjuZKnVGLczU6W@%&GZU_uO(PJ#`#zOCVGv0{16E41#4P`u&oGFtq<>~Z2@A{#bQZ_ zl)G*lg~LT|Z(l)|v$63*+j(0+sfp1G9-??&Az-K_2+n5&s~{WJ%e<(6kw-Z=(1Alo zl0dd>&tYA-Su2qr)Xsi5uSNKg@0{2IDR6`|M673d(@So-Q^eniYbM6!&NIHXj^+^A z*1QAiLM_P#uTec>VB*|;a6)H~BrB*4LG7i}-tqG+M#1|WJ5QonhMr8J0H;{Woghyg z`lG|U5lXrSNI^Vi&Wcy^aDlNt0ElZRox%q)S_AFJymz>X$);t9a9MM7v28vOlnPZv zXp^B%aXDA^k;g@kjDkl}!$wM|Bc_sFt+0~Fzb&w-zd7N6NomH;e8>7zl4H&%xi7*?g0F0(DvL!YfVTfHZWFgQc;PVtcn-o*d0~45t#-Jf?NP}6^Crq`V z^0$U~H@gr{4{w$iRcg>VgJ&NqhB3z9yE*dA?*$kONp84uV z9lYdoqYVB1vS+;{KEhl5XB-Ap@7N}jm8>mdd~rOxmA)rKCGrwopB{qpUTJ$bh?ppQaFJibp6liBUt zc7&`GZW1JZx%Amc*f^*W^y`m!mzQ9UDv77o3d)o&XV=4{Z)0kN>O#E--P;J>r9m-n z(V=0n4bZnEZMQC}S9o}K+M?4|_ZWaE8LE_!FRQG)CehlU=XRo=Zy_TT){{eX%Z)j) zEE$1O;O9)F3e#_#AR0tb?T%-Y)O8BOs5$vwzdBaNFJ(px1Bx9gaY40_sr({CkKbcY zK_8;DsX-H?v`T6FalbHOjYX^D>w5)I2X~4(7y(Lf_Rs4IHOrbwESY115#N{{ac7|E zVkDZx&D3?W*}=6A+6E@z`TAZ#jZZt(X^I!ZcQ&j&l5-TSU}r_csqG6fEC@U*`%a7% zn4`_T3wvQ=CbxaR3`Sir26?i(--%;3^94#G(b1b zaJ7})0?QI37K#HxvGZMd^cZBuFiYlrsAzzd;+0brh4vvE({)`Edg`wIhsd(j`X(gNTW^V1JGj9My6RMyR0hlwvJQX2sXSo?wp&h`r(E9Qa zV#(>VE6V+2o`*;tcf=1FaSRtK*783Ew@1tafE6ma0>~f0RE&#N_=%myGY}FBeSB3! z5L6j^cQ=bOS)VZvzv|3S1*O_Wp{qfH`;HN)@)sJ^;)g4rq@6A z0yX$mRtNkt8#Mp(V@{pCp5Zq6_Yf8gZ9~nVlA`r+SBIK}iv%q&fDjwZ_jTV-Oq}2* zqh5R`HmW-Kmh2lGW`B(nck?V{JByIhsf=K(jHn4n5cl=V91@X$AVZjfla8KUHVf{8 z6`nEHu7%n*&jX}C-vx&o2luVE77rJ+eJ`n21H6ft)uPux+wFg+x=l5L zY9E$k=-JmI;N|Ff_GyKY!GCGw!l{PO@+i2$>RmLVg(-u~`Jg0p-)#F(a)7H=qh=}Q zrUxbkwT-A=SPh@|H(?Sw=OWZnXtqQ@kPtD<080<#WN|6 zK!;9;<2yg{?3VaF`3SDVrSOtg!`7o%T}KvMZOOeHgkO0t9(Sf|!YWGM0B4ir(-FO_ zco8LLv9Es%n2u4^E^>2JO)37FWrbJDA5iOdEG(+N_i=XVofG&}Ir{c&?{%Z5|FsJ< zs$hAKXU3g!;XPE z-~wgV*`vG;a-PF}R2sqr`=Pr17CM9B2&-?3mg#&IaiQoux)vo(!-`cdd(IgN>c6|R z9jEh^dwa6GTs{r{=GQU~szdc68cO%{iIA~x34b$eqUrB-WulWZ4rx;3as9miDvBm9fZ?qXVL|C<}; z&MH|WU7fw+;}&oTK;_wVT}`J4!jmHnQInnPpUN0Kz8;kSy`|sbMyxMOYL?b&ISgK3(n)cv8END^ zc@^$4y5K^%!fGgC!uTjawH^JGS9)oby+6KBQFv`CcjUHWD!~oC>ACuDj-I+)ip@R&r7{<8lqBvhkKe3ss8OH_ zrmrZv1>!BOB)i^o#IAsajUP2$H*_UW{z845Ybi-)-GWIdUHGuR;MsHlwyWrWzdDYiQ67pt;Y!7UYamw|n~pGv32eH`wkKbf=HTxFD+H>{p|`vk z3&ZNzRD?;V$@Ui&o!m~TtRrZ5QdCN+hiSHyVS|lL_djc@NKkf^O4|y+yFXCiinLyu zt=>)B+W)IiFu-RP^evZxdU`wu%S@1S&*Y~pY`*`V;~-Q!xqt%|^T?!smf(Yc;EUjo k7r*+duWi~wShpa1U*%%(10^{-5}\\ +\frac{3}{2}+\frac{8}{3}n_3+\frac{15}{4}n_3^2+\frac{24}{5}n_3^3+\frac{35}{6}n_3^4&\text{else}\end{cases}$$ + +The weighted densities $n_k(\mathbf{r})$ are calculated by convolving the density profiles $\rho_\alpha(\mathbf{r})$ with weight functions $\omega_k^\alpha(\mathbf{r})$ + +$$n_k(\mathbf{r})=\sum_\alpha n_k^\alpha(\mathbf{r})=\sum_\alpha\int\rho_\alpha(\mathbf{r}')\omega_k^\alpha(\mathbf{r}-\mathbf{r}')\mathrm{d}\mathbf{r}'$$ + +which differ between the different FMT versions. + +||WhiteBear/AntiSymWhiteBear|KierlikRosinberg| +|-|:-:|:-:| +|$\omega_0^\alpha(\mathbf{r})$|$\frac{C_{0,\alpha}}{\pi\sigma_\alpha^2}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|$C_{0,\alpha}\left(-\frac{1}{8\pi}\,\delta''\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)+\frac{1}{2\pi\|\mathbf{r}\|}\,\delta'\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)\right)$| +|$\omega_1^\alpha(\mathbf{r})$|$\frac{C_{1,\alpha}}{2\pi\sigma_\alpha}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|$\frac{C_{1,\alpha}}{8\pi}\,\delta'\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$| +|$\omega_2^\alpha(\mathbf{r})$|$C_{2,\alpha}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|$C_{2,\alpha}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$| +|$\omega_3^\alpha(\mathbf{r})$|$C_{3,\alpha}\,\Theta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|$C_{3,\alpha}\,\Theta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$| +|$\vec\omega_1^\alpha(\mathbf{r})$|$C_{3,\alpha}\frac{\mathbf{r}}{2\pi\sigma_\alpha\|\mathbf{r}\|}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|-| +|$\vec\omega_2^\alpha(\mathbf{r})$|$C_{3,\alpha}\frac{\mathbf{r}}{\|\mathbf{r}\|}\,\delta\!\left(\frac{d_\alpha}{2}-\|\mathbf{r}\|\right)$|-| \ No newline at end of file diff --git a/docs/theory/models/index.md b/docs/theory/models/index.md index 148f360b4..efccaf1cd 100644 --- a/docs/theory/models/index.md +++ b/docs/theory/models/index.md @@ -6,4 +6,7 @@ It is currently still under construction. You can help by [contributing](https:/ ```{eval-rst} .. toctree:: :maxdepth: 1 + + hard_spheres + association ``` \ No newline at end of file diff --git a/parameters/pcsaft/gross2002.json b/parameters/pcsaft/gross2002.json index b888201d8..3b8737154 100644 --- a/parameters/pcsaft/gross2002.json +++ b/parameters/pcsaft/gross2002.json @@ -13,7 +13,9 @@ "sigma": 3.23, "epsilon_k": 188.9, "kappa_ab": 0.035176, - "epsilon_k_ab": 2899.5 + "epsilon_k_ab": 2899.5, + "na": 1.0, + "nb": 1.0 }, "molarweight": 32.042 }, @@ -31,7 +33,9 @@ "sigma": 3.1771, "epsilon_k": 198.24, "kappa_ab": 0.032384, - "epsilon_k_ab": 2653.4 + "epsilon_k_ab": 2653.4, + "na": 1.0, + "nb": 1.0 }, "molarweight": 46.069 }, @@ -49,7 +53,9 @@ "sigma": 3.2522, "epsilon_k": 233.4, "kappa_ab": 0.015268, - "epsilon_k_ab": 2276.8 + "epsilon_k_ab": 2276.8, + "na": 1.0, + "nb": 1.0 }, "molarweight": 60.096 }, @@ -67,7 +73,9 @@ "sigma": 3.6139, "epsilon_k": 259.59, "kappa_ab": 0.006692, - "epsilon_k_ab": 2544.6 + "epsilon_k_ab": 2544.6, + "na": 1.0, + "nb": 1.0 }, "molarweight": 74.123 }, @@ -85,7 +93,9 @@ "sigma": 3.4508, "epsilon_k": 247.28, "kappa_ab": 0.010319, - "epsilon_k_ab": 2252.1 + "epsilon_k_ab": 2252.1, + "na": 1.0, + "nb": 1.0 }, "molarweight": 88.15 }, @@ -103,7 +113,9 @@ "sigma": 3.6735, "epsilon_k": 262.32, "kappa_ab": 0.005747, - "epsilon_k_ab": 2538.9 + "epsilon_k_ab": 2538.9, + "na": 1.0, + "nb": 1.0 }, "molarweight": 102.177 }, @@ -121,7 +133,9 @@ "sigma": 3.545, "epsilon_k": 253.46, "kappa_ab": 0.001155, - "epsilon_k_ab": 2878.5 + "epsilon_k_ab": 2878.5, + "na": 1.0, + "nb": 1.0 }, "molarweight": 116.203 }, @@ -139,7 +153,9 @@ "sigma": 3.7145, "epsilon_k": 262.74, "kappa_ab": 0.002197, - "epsilon_k_ab": 2754.8 + "epsilon_k_ab": 2754.8, + "na": 1.0, + "nb": 1.0 }, "molarweight": 130.23 }, @@ -157,7 +173,9 @@ "sigma": 3.7292, "epsilon_k": 263.64, "kappa_ab": 0.001427, - "epsilon_k_ab": 2941.9 + "epsilon_k_ab": 2941.9, + "na": 1.0, + "nb": 1.0 }, "molarweight": 144.257 }, @@ -175,7 +193,9 @@ "sigma": 3.2085, "epsilon_k": 208.42, "kappa_ab": 0.024675, - "epsilon_k_ab": 2253.9 + "epsilon_k_ab": 2253.9, + "na": 1.0, + "nb": 1.0 }, "molarweight": 60.096 }, @@ -193,7 +213,9 @@ "sigma": 3.9053, "epsilon_k": 266.01, "kappa_ab": 0.001863, - "epsilon_k_ab": 2618.8 + "epsilon_k_ab": 2618.8, + "na": 1.0, + "nb": 1.0 }, "molarweight": 88.15 }, @@ -211,7 +233,9 @@ "sigma": 3.0007, "epsilon_k": 366.51, "kappa_ab": 0.034868, - "epsilon_k_ab": 2500.7 + "epsilon_k_ab": 2500.7, + "na": 1.0, + "nb": 1.0 }, "molarweight": 18.015 }, @@ -229,7 +253,9 @@ "sigma": 2.8906, "epsilon_k": 214.94, "kappa_ab": 0.095103, - "epsilon_k_ab": 684.3 + "epsilon_k_ab": 684.3, + "na": 1.0, + "nb": 1.0 }, "molarweight": 31.06 }, @@ -247,7 +273,9 @@ "sigma": 3.1343, "epsilon_k": 221.53, "kappa_ab": 0.017275, - "epsilon_k_ab": 854.7 + "epsilon_k_ab": 854.7, + "na": 1.0, + "nb": 1.0 }, "molarweight": 45.09 }, @@ -265,7 +293,9 @@ "sigma": 3.5347, "epsilon_k": 250.52, "kappa_ab": 0.022674, - "epsilon_k_ab": 1028.1 + "epsilon_k_ab": 1028.1, + "na": 1.0, + "nb": 1.0 }, "molarweight": 59.11 }, @@ -283,7 +313,9 @@ "sigma": 3.4777, "epsilon_k": 231.8, "kappa_ab": 0.02134, - "epsilon_k_ab": 932.2 + "epsilon_k_ab": 932.2, + "na": 1.0, + "nb": 1.0 }, "molarweight": 59.11 }, @@ -301,7 +333,9 @@ "sigma": 3.7021, "epsilon_k": 335.47, "kappa_ab": 0.074883, - "epsilon_k_ab": 1351.6 + "epsilon_k_ab": 1351.6, + "na": 1.0, + "nb": 1.0 }, "molarweight": 93.13 }, @@ -319,7 +353,9 @@ "sigma": 3.8582, "epsilon_k": 211.59, "kappa_ab": 0.07555, - "epsilon_k_ab": 3044.4 + "epsilon_k_ab": 3044.4, + "na": 1.0, + "nb": 1.0 }, "molarweight": 60.053 } diff --git a/parameters/pcsaft/loetgeringlin2015_homo.json b/parameters/pcsaft/loetgeringlin2015_homo.json index ff9df6b16..030332969 100644 --- a/parameters/pcsaft/loetgeringlin2015_homo.json +++ b/parameters/pcsaft/loetgeringlin2015_homo.json @@ -277,6 +277,8 @@ "epsilon_k": 488.66, "epsilon_k_ab": 2517.0, "kappa_ab": 0.006825, + "na": 1.0, + "nb": 1.0, "viscosity": [ -15.7583e-3, -2.5654e-1, @@ -294,6 +296,8 @@ "epsilon_k": 467.59, "epsilon_k_ab": 1064.6, "kappa_ab": 0.026662, + "na": 1.0, + "nb": 1.0, "viscosity": [ -4.4048e-3, -0.6089e-1, diff --git a/parameters/pcsaft/loetgeringlin2018.json b/parameters/pcsaft/loetgeringlin2018.json index 989e3439c..153e40ed3 100644 --- a/parameters/pcsaft/loetgeringlin2018.json +++ b/parameters/pcsaft/loetgeringlin2018.json @@ -896,6 +896,8 @@ "mu": 0.9204, "kappa_ab": 0.03, "epsilon_k_ab": 1547.4422, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.6824, -2.129, @@ -921,6 +923,8 @@ "mu": 1.0313, "kappa_ab": 0.03, "epsilon_k_ab": 959.5053, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.3807, 2.1585, @@ -946,6 +950,8 @@ "mu": 1.07, "kappa_ab": 0.03, "epsilon_k_ab": 39.2881, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.8987, -2.1083, @@ -1040,6 +1046,8 @@ "mu": 1.391, "kappa_ab": 0.03, "epsilon_k_ab": 1497.5173, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.8691, -2.3216, @@ -1065,6 +1073,8 @@ "mu": 1.6698, "kappa_ab": 0.03, "epsilon_k_ab": 1846.2029, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.0065, -1.88906, @@ -1090,6 +1100,8 @@ "mu": 1.6608, "kappa_ab": 0.03, "epsilon_k_ab": 1718.0488, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.9864, -1.3818, @@ -1363,6 +1375,8 @@ "mu": 1.409, "kappa_ab": 0.03, "epsilon_k_ab": 2644.5966, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3992, -3.2065, @@ -1388,6 +1402,8 @@ "mu": 1.6189, "kappa_ab": 0.03, "epsilon_k_ab": 1967.8312, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.5371, -2.97397, @@ -1526,6 +1542,8 @@ "mu": 1.6908, "kappa_ab": 0.03, "epsilon_k_ab": 2002.9613, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.6684, -3.9471, @@ -1639,6 +1657,8 @@ "mu": 2.4103, "kappa_ab": 0.03, "epsilon_k_ab": 2711.6591, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.7689, -0.9258, @@ -1664,6 +1684,8 @@ "mu": 1.6908, "kappa_ab": 0.03, "epsilon_k_ab": 2514.0609, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.08153, -1.1998, @@ -1870,6 +1892,8 @@ "mu": 1.6608, "kappa_ab": 0.03, "epsilon_k_ab": 2220.2939, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.1698, -3.4917, @@ -1895,6 +1919,8 @@ "mu": 1.7388, "kappa_ab": 0.03, "epsilon_k_ab": 1956.8424, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3075, -2.24414, @@ -1920,6 +1946,8 @@ "mu": 1.6488, "kappa_ab": 0.03, "epsilon_k_ab": 1878.5922, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3001, -1.7492, @@ -2080,6 +2108,8 @@ "mu": 1.6698, "kappa_ab": 0.03, "epsilon_k_ab": 2140.2545, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.8993, -3.0227, @@ -2127,6 +2157,8 @@ "mu": 1.5889, "kappa_ab": 0.03, "epsilon_k_ab": 82.5108, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.0803, -2.3348, @@ -2152,6 +2184,8 @@ "mu": 1.6488, "kappa_ab": 0.03, "epsilon_k_ab": 1930.6231, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.218, -2.0634, @@ -2177,6 +2211,8 @@ "mu": 1.6578, "kappa_ab": 0.03, "epsilon_k_ab": 1793.3615, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.2075, -1.7111, @@ -2291,6 +2327,8 @@ "mu": 1.3101, "kappa_ab": 0.03, "epsilon_k_ab": 1103.6088, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.369, -1.2753, @@ -2338,6 +2376,8 @@ "mu": 1.6998, "kappa_ab": 0.03, "epsilon_k_ab": 2519.7116, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.577, -0.44059, @@ -2454,6 +2494,8 @@ "mu": 0.8994, "kappa_ab": 0.03, "epsilon_k_ab": 2731.5672, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3279, -3.1517, @@ -2479,6 +2521,8 @@ "mu": 1.6099, "kappa_ab": 0.03, "epsilon_k_ab": 2073.3555, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.4657, -2.71167, @@ -2592,6 +2636,8 @@ "mu": 1.6608, "kappa_ab": 0.03, "epsilon_k_ab": 2150.7482, + "na": 1.0, + "nb": 1.0, "viscosity": [ -2.0036, -3.7363, @@ -2639,6 +2685,8 @@ "mu": 1.421, "kappa_ab": 0.03, "epsilon_k_ab": 2476.9879, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.2518, -2.9995, @@ -2664,6 +2712,8 @@ "mu": 1.6488, "kappa_ab": 0.03, "epsilon_k_ab": 1869.8446, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3896, -2.58867, @@ -2689,6 +2739,8 @@ "mu": 1.6488, "kappa_ab": 0.03, "epsilon_k_ab": 1721.3869, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.3845, -1.1782, @@ -2781,6 +2833,8 @@ "mu": 1.5499, "kappa_ab": 0.03, "epsilon_k_ab": 1923.3059, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.9812, -2.6533, @@ -2806,6 +2860,8 @@ "mu": 1.7, "kappa_ab": 0.03, "epsilon_k_ab": 1835.0315, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.1188, -2.22062, @@ -2831,6 +2887,8 @@ "mu": 1.6668, "kappa_ab": 0.03, "epsilon_k_ab": 1713.2429, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.1043, -1.0389, @@ -2856,6 +2914,8 @@ "mu": 1.6399, "kappa_ab": 0.03, "epsilon_k_ab": 1806.0011, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.1043, -0.9823, @@ -2971,6 +3031,8 @@ "mu": 1.1692, "kappa_ab": 0.03, "epsilon_k_ab": 86.0572, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.7383, -1.7723, @@ -2996,6 +3058,8 @@ "mu": 1.6788, "kappa_ab": 0.03, "epsilon_k_ab": 2044.5298, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.8756, -1.47721, @@ -3021,6 +3085,8 @@ "mu": 1.6608, "kappa_ab": 0.03, "epsilon_k_ab": 1871.8788, + "na": 1.0, + "nb": 1.0, "viscosity": [ -0.847, -1.0735, @@ -3181,6 +3247,8 @@ "mu": 1.55, "kappa_ab": 0.03, "epsilon_k_ab": 2238.1838, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.7882, -2.8133, @@ -3294,6 +3362,8 @@ "mu": 1.6488, "kappa_ab": 0.03, "epsilon_k_ab": 2212.4364, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.7295, -2.8003, @@ -3341,6 +3411,8 @@ "mu": 1.6698, "kappa_ab": 0.03, "epsilon_k_ab": 1888.8681, + "na": 1.0, + "nb": 1.0, "viscosity": [ -1.6044, -3.44231, diff --git a/parameters/pcsaft/rehner2020.json b/parameters/pcsaft/rehner2020.json index 87d9d42e6..df289c35b 100644 --- a/parameters/pcsaft/rehner2020.json +++ b/parameters/pcsaft/rehner2020.json @@ -14,7 +14,9 @@ "sigma": 2.937523956051823, "epsilon_k": 272.02757407828676, "kappa_ab": 0.04448012165716923, - "epsilon_k_ab": 3125.3202766200056 + "epsilon_k_ab": 3125.3202766200056, + "na": 1.0, + "nb": 1.0 } }, { @@ -33,7 +35,8 @@ "epsilon_k": 238.31794591948915, "kappa_ab": 0.037807044304069184, "epsilon_k_ab": 2749.004567423258, - "na": 2 + "na": 2.0, + "nb": 1.0 } }, { @@ -52,8 +55,8 @@ "epsilon_k": 169.7751872692369, "kappa_ab": 0.13373757842938191, "epsilon_k_ab": 1772.0393059972052, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -72,7 +75,9 @@ "epsilon_k": 166.6147951235982, "mu": 1.6152087869692175, "kappa_ab": 0.09819448826630345, - "epsilon_k_ab": 2667.2518268470913 + "epsilon_k_ab": 2667.2518268470913, + "na": 1.0, + "nb": 1.0 } }, { @@ -92,7 +97,8 @@ "mu": 1.9373715367486852, "kappa_ab": 0.038235772849856756, "epsilon_k_ab": 2377.871413812318, - "na": 2 + "na": 2.0, + "nb": 1.0 } }, { @@ -112,8 +118,8 @@ "mu": 1.5050185176397637, "kappa_ab": 0.082906656369032, "epsilon_k_ab": 1784.1459137848506, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -132,8 +138,8 @@ "epsilon_k": 101.0845669390466, "kappa_ab": 0.11953491759977186, "epsilon_k_ab": 1834.845540500216, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -152,8 +158,8 @@ "epsilon_k": 124.58467252505255, "kappa_ab": 0.10067627754508424, "epsilon_k_ab": 1810.4338396651838, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -172,8 +178,8 @@ "epsilon_k": 144.37733584205245, "kappa_ab": 0.05810650285845608, "epsilon_k_ab": 1959.8294504365563, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -192,8 +198,8 @@ "epsilon_k": 163.6648110273199, "kappa_ab": 0.07216895211562263, "epsilon_k_ab": 1862.0561323915151, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -212,8 +218,8 @@ "epsilon_k": 179.754303082289, "kappa_ab": 0.0762520371571717, "epsilon_k_ab": 1824.6154575116475, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -231,7 +237,9 @@ "sigma": 3.7482827652225086, "epsilon_k": 270.86189939448803, "kappa_ab": 0.002566318648210027, - "epsilon_k_ab": 2778.9261790720657 + "epsilon_k_ab": 2778.9261790720657, + "na": 1.0, + "nb": 1.0 } }, { @@ -249,7 +257,9 @@ "sigma": 3.620930465025628, "epsilon_k": 252.7630330258869, "kappa_ab": 0.003060843493411822, - "epsilon_k_ab": 2700.937487295063 + "epsilon_k_ab": 2700.937487295063, + "na": 1.0, + "nb": 1.0 } }, { @@ -267,7 +277,9 @@ "sigma": 3.8281719743325215, "epsilon_k": 268.72917896983984, "kappa_ab": 0.0031104598923094376, - "epsilon_k_ab": 2804.2762044751535 + "epsilon_k_ab": 2804.2762044751535, + "na": 1.0, + "nb": 1.0 } }, { @@ -285,7 +297,9 @@ "sigma": 3.942380472821992, "epsilon_k": 278.68277806898686, "kappa_ab": 0.0014907242131781597, - "epsilon_k_ab": 3103.6530564736395 + "epsilon_k_ab": 3103.6530564736395, + "na": 1.0, + "nb": 1.0 } }, { @@ -303,7 +317,9 @@ "sigma": 4.042094734564122, "epsilon_k": 281.2912655243266, "kappa_ab": 0.002493036173908781, - "epsilon_k_ab": 3023.609567972825 + "epsilon_k_ab": 3023.609567972825, + "na": 1.0, + "nb": 1.0 } }, { @@ -321,7 +337,9 @@ "sigma": 3.977088533521953, "epsilon_k": 274.5543622578429, "kappa_ab": 0.0012672291257527345, - "epsilon_k_ab": 3223.361822388517 + "epsilon_k_ab": 3223.361822388517, + "na": 1.0, + "nb": 1.0 } }, { @@ -340,8 +358,8 @@ "epsilon_k": 136.16495641117777, "kappa_ab": 0.09762722816926367, "epsilon_k_ab": 1718.8359393009746, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -360,8 +378,8 @@ "epsilon_k": 149.97423748959227, "kappa_ab": 0.16261464675774262, "epsilon_k_ab": 1469.6277485887485, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -380,8 +398,8 @@ "epsilon_k": 159.24901703474268, "kappa_ab": 0.0787091926637352, "epsilon_k_ab": 1771.398524849797, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -400,8 +418,8 @@ "epsilon_k": 162.73433804276743, "kappa_ab": 0.12199296128478557, "epsilon_k_ab": 1516.0966359637514, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -420,8 +438,8 @@ "epsilon_k": 211.90069259634961, "kappa_ab": 0.041207808637705026, "epsilon_k_ab": 2550.2519514335218, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -440,8 +458,8 @@ "epsilon_k": 183.09717953416344, "kappa_ab": 0.08499573407848357, "epsilon_k_ab": 2268.8453113675146, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } }, { @@ -460,8 +478,8 @@ "epsilon_k": 299.7826857254255, "kappa_ab": 0.019561736415810844, "epsilon_k_ab": 3069.7332849899476, - "na": 2, - "nb": 2 + "na": 2.0, + "nb": 2.0 } } ] \ No newline at end of file diff --git a/parameters/pcsaft/sauer2014_hetero.json b/parameters/pcsaft/sauer2014_hetero.json index da9755997..fc5485f57 100644 --- a/parameters/pcsaft/sauer2014_hetero.json +++ b/parameters/pcsaft/sauer2014_hetero.json @@ -192,7 +192,9 @@ "sigma": 2.7702, "epsilon_k": 334.29, "epsilon_k_ab": 2575.9, - "kappa_ab": 0.009583 + "kappa_ab": 0.009583, + "na": 1.0, + "nb": 1.0 }, "molarweight": 17.00734 }, @@ -203,7 +205,9 @@ "sigma": 3.1129, "epsilon_k": 309.93, "epsilon_k_ab": 1471.5, - "kappa_ab": 0.005769 + "kappa_ab": 0.005769, + "na": 1.0, + "nb": 1.0 }, "molarweight": 16.02238 } diff --git a/parameters/pcsaft/sauer2014_hetero_joback.json b/parameters/pcsaft/sauer2014_hetero_joback.json index 9198b27bf..2ddb5dd7e 100644 --- a/parameters/pcsaft/sauer2014_hetero_joback.json +++ b/parameters/pcsaft/sauer2014_hetero_joback.json @@ -332,7 +332,9 @@ "sigma": 2.7702, "epsilon_k": 334.29, "epsilon_k_ab": 2575.9, - "kappa_ab": 0.009583 + "kappa_ab": 0.009583, + "na": 1.0, + "nb": 1.0 }, "molarweight": 17.00734, "ideal_gas_record": { @@ -350,7 +352,9 @@ "sigma": 3.1129, "epsilon_k": 309.93, "epsilon_k_ab": 1471.5, - "kappa_ab": 0.005769 + "kappa_ab": 0.005769, + "na": 1.0, + "nb": 1.0 }, "molarweight": 16.02238, "ideal_gas_record": { diff --git a/parameters/pcsaft/sauer2014_homo.json b/parameters/pcsaft/sauer2014_homo.json index 67aa45680..cb92a9fb0 100644 --- a/parameters/pcsaft/sauer2014_homo.json +++ b/parameters/pcsaft/sauer2014_homo.json @@ -17,7 +17,6 @@ }, "molarweight": 14.02658 }, - { "identifier": ">CH", "model_record": { @@ -57,7 +56,7 @@ { "identifier": "=C<", "model_record": { - "m": 0.86367, + "m": 0.86367, "sigma": 3.1815, "epsilon_k": 156.31 }, @@ -66,7 +65,7 @@ { "identifier": "C≡CH", "model_record": { - "m": 1.3279, + "m": 1.3279, "sigma": 2.9421, "epsilon_k": 223.05 }, @@ -75,7 +74,7 @@ { "identifier": "CH2_hex", "model_record": { - "m": 0.39496, + "m": 0.39496, "sigma": 3.9126, "epsilon_k": 289.03 }, @@ -84,7 +83,7 @@ { "identifier": "CH_hex", "model_record": { - "m": 0.02880, + "m": 0.02880, "sigma": 8.9779, "epsilon_k": 1306.7 }, @@ -93,7 +92,7 @@ { "identifier": "CH2_pent", "model_record": { - "m": 0.46742, + "m": 0.46742, "sigma": 3.7272, "epsilon_k": 267.16 }, @@ -102,7 +101,7 @@ { "identifier": "CH_pent", "model_record": { - "m": 0.03314, + "m": 0.03314, "sigma": 7.7190, "epsilon_k": 1297.7 }, @@ -111,7 +110,7 @@ { "identifier": "CH_arom", "model_record": { - "m": 0.42335, + "m": 0.42335, "sigma": 3.7270, "epsilon_k": 274.41 }, @@ -169,7 +168,7 @@ { "identifier": "HCOO", "model_record": { - "m": 1.7525, + "m": 1.7525, "sigma": 2.9043, "epsilon_k": 229.63, "mu": 2.7916 @@ -193,7 +192,9 @@ "sigma": 3.2859, "epsilon_k": 488.66, "epsilon_k_ab": 2517.0, - "kappa_ab": 0.006825 + "kappa_ab": 0.006825, + "na": 1.0, + "nb": 1.0 }, "molarweight": 17.00734 }, @@ -204,9 +205,10 @@ "sigma": 3.6456, "epsilon_k": 467.59, "epsilon_k_ab": 1064.6, - "kappa_ab": 0.026662 + "kappa_ab": 0.026662, + "na": 1.0, + "nb": 1.0 }, "molarweight": 16.02238 } -] - +] \ No newline at end of file diff --git a/parameters/pcsaft/sauer2014_homo_joback.json b/parameters/pcsaft/sauer2014_homo_joback.json index 534c9ae75..c56e8632b 100644 --- a/parameters/pcsaft/sauer2014_homo_joback.json +++ b/parameters/pcsaft/sauer2014_homo_joback.json @@ -332,7 +332,9 @@ "sigma": 3.2859, "epsilon_k": 488.66, "epsilon_k_ab": 2517.0, - "kappa_ab": 0.006825 + "kappa_ab": 0.006825, + "na": 1.0, + "nb": 1.0 }, "molarweight": 17.00734, "ideal_gas_record": { @@ -350,7 +352,9 @@ "sigma": 3.6456, "epsilon_k": 467.59, "epsilon_k_ab": 1064.6, - "kappa_ab": 0.026662 + "kappa_ab": 0.026662, + "na": 1.0, + "nb": 1.0 }, "molarweight": 16.02238, "ideal_gas_record": { diff --git a/src/association/dft.rs b/src/association/dft.rs index e824efb24..05c92b378 100644 --- a/src/association/dft.rs +++ b/src/association/dft.rs @@ -52,9 +52,6 @@ where // number of segments let n = self.association_parameters.component_index.len(); - // number of associating segments - let nassoc = self.association_parameters.assoc_comp.len(); - // number of dimensions let dim = (weighted_densities.shape()[0] - 1) / n - 1; @@ -70,7 +67,7 @@ where .collect(); let n3 = weighted_densities.index_axis(Axis(0), n * (dim + 1)); - // calculate rho0 (only associating segments) + // calculate rho0 let [_, _, c2, _] = p.geometry_coefficients(temperature); let diameter = p.hs_diameter(temperature); let mut n2i = n0i.to_owned(); @@ -88,9 +85,6 @@ where *rho0 = n0i; } }); - let rho0 = Array2::from_shape_fn((nassoc, n3.len()), |(i, j)| { - rho0[(self.association_parameters.assoc_comp[i], j)] - }); // calculate xi let n2v: Vec<_> = n2vi.iter().map(|n2vi| n2vi.sum_axis(Axis(0))).collect(); @@ -111,56 +105,143 @@ where // auxiliary variables let n3i = n3.mapv(|n3| (-n3 + 1.0).recip()); - // only one associating component - if nassoc == 1 { - // association strength - let k = &n2 * &n3i * diameter[self.association_parameters.assoc_comp[0]] * 0.5; - let deltarho = (((&k / 18.0 + 0.5) * &k * xi + 1.0) * n3i) - * ((temperature.recip() * self.association_parameters.epsilon_k_aibj[(0, 0)]) - .exp_m1() - * self.association_parameters.sigma3_kappa_aibj[(0, 0)]) - * rho0.index_axis(Axis(0), 0); - - let na = self.association_parameters.na[0]; - let nb = self.association_parameters.nb[0]; - let f = |x: N| x.ln() - x * 0.5 + 0.5; - if nb > 0.0 { - // no cross association, two association sites - let xa = deltarho.mapv(|d| Self::assoc_site_frac_ab(d, na, nb)); - let xb = (&xa - 1.0) * (na / nb) + 1.0; - Ok((xa.mapv(f) * na + xb.mapv(f) * nb) * rho0.index_axis(Axis(0), 0)) - } else { - // no cross association, one association site - let xa = deltarho.mapv(|d| Self::assoc_site_frac_a(d, na)); - - Ok(xa.mapv(f) * na * rho0.index_axis(Axis(0), 0)) + self.calculate_helmholtz_energy_density(temperature, &rho0, &n2, &n3i, &xi) + } +} + +impl Association

{ + pub fn calculate_helmholtz_energy_density< + N: DualNum + ScalarOperand, + S: Data, + >( + &self, + temperature: N, + rho0: &Array2, + n2: &ArrayBase, + n3i: &Array1, + xi: &Array1, + ) -> EosResult> { + let a = &self.association_parameters; + + let d = self.parameters.hs_diameter(temperature); + + match ( + a.sites_a.len() * a.sites_b.len(), + a.sites_c.len(), + self.force_cross_association, + ) { + (0, 0, _) => Ok(Array::zeros(n3i.len())), + (1, 0, false) => { + Ok(self.helmholtz_energy_density_ab_analytic(temperature, rho0, &d, n2, n3i, xi)) + } + (0, 1, false) => { + Ok(self.helmholtz_energy_density_cc_analytic(temperature, rho0, &d, n2, n3i, xi)) + } + (1, 1, false) => { + Ok( + self.helmholtz_energy_density_ab_analytic(temperature, rho0, &d, n2, n3i, xi) + + self.helmholtz_energy_density_cc_analytic( + temperature, + rho0, + &d, + n2, + n3i, + xi, + ), + ) + } + _ => { + let mut x: Array1 = Array::from_elem(a.sites_a.len() + a.sites_b.len(), 0.2); + let (assoc_comp_ab, n_ab): (Vec<_>, Vec<_>) = a + .sites_a + .iter() + .chain(a.sites_b.iter()) + .map(|s| (s.assoc_comp, s.n)) + .unzip(); + let rhoab = Array2::from_shape_fn((x.len(), n3i.len()), |(i, j)| { + rho0[(assoc_comp_ab[i], j)] * n_ab[i] + }); + rhoab + .axis_iter(Axis(1)) + .zip(n2.iter()) + .zip(n3i.iter()) + .zip(xi.iter()) + .map(|(((rho, &n2), &n3i), &xi)| { + let [delta_ab, delta_cc] = + self.association_strength(temperature, &d, n2, n3i, xi); + Self::helmholtz_energy_density_cross_association( + &rho, + &delta_ab, + &delta_cc, + self.max_iter, + self.tol, + Some(&mut x), + ) + }) + .collect() } - } else { - let mut x: Array1 = Array::from_elem(2 * nassoc, 0.2); - Ok(rho0 - .view() - .into_shape([nassoc, rho0.len() / nassoc]) - .unwrap() - .axis_iter(Axis(1)) - .zip(n2.iter()) - .zip(n3i.iter()) - .zip(xi.iter()) - .map(|(((rho0, &n2), &n3i), &xi)| { - self.helmholtz_energy_density_cross_association( - temperature, - &rho0, - &diameter, - n2, - n3i, - xi, - self.max_iter, - self.tol, - Some(&mut x), - ) - }) - .collect::, _>>()? - .into_shape(n2.raw_dim()) - .unwrap()) } } + + fn helmholtz_energy_density_ab_analytic + ScalarOperand, S: Data>( + &self, + temperature: N, + rho0: &Array2, + diameter: &Array1, + n2: &ArrayBase, + n3i: &Array1, + xi: &Array1, + ) -> Array1 { + let a = &self.association_parameters; + + // site densities + let i = a.sites_a[0].assoc_comp; + let j = a.sites_b[0].assoc_comp; + let rhoa = &rho0.index_axis(Axis(0), i) * a.sites_a[0].n; + let rhob = &rho0.index_axis(Axis(0), j) * a.sites_b[0].n; + + // association strength + let di = diameter[i]; + let dj = diameter[j]; + let k = n2 * n3i * (di * dj / (di + dj)); + let delta = (((&k / 18.0 + 0.5) * &k * xi + 1.0) * n3i) + * ((temperature.recip() * a.epsilon_k_ab[(0, 0)]).exp_m1() * a.sigma3_kappa_ab[(0, 0)]); + + // no cross association, two association sites + let aux = &delta * (&rhob - &rhoa) + 1.0; + let xa = ((&aux * &aux + &delta * &rhoa * 4.0).map(N::sqrt) + &aux).map(N::recip) * 2.0; + let aux = -aux + 2.0; + let xb = ((&aux * &aux + delta * &rhob * 4.0).map(N::sqrt) + aux).map(N::recip) * 2.0; + + let f = |x: N| x.ln() - x * 0.5 + 0.5; + rhoa * xa.mapv(f) + rhob * xb.mapv(f) + } + + fn helmholtz_energy_density_cc_analytic + ScalarOperand, S: Data>( + &self, + temperature: N, + rho0: &Array2, + diameter: &Array1, + n2: &ArrayBase, + n3i: &Array1, + xi: &Array1, + ) -> Array1 { + let a = &self.association_parameters; + + // site densities + let i = a.sites_c[0].assoc_comp; + let rhoc = &rho0.index_axis(Axis(0), i) * a.sites_c[0].n; + + // association strength + let di = diameter[i]; + let k = n2 * n3i * (di * 0.5); + let delta = (((&k / 18.0 + 0.5) * &k * xi + 1.0) * n3i) + * ((temperature.recip() * a.epsilon_k_cc[(0, 0)]).exp_m1() * a.sigma3_kappa_cc[(0, 0)]); + + // no cross association, two association sites + let xc = ((delta * 4.0 * &rhoc + 1.0).map(N::sqrt) + 1.0).map(N::recip) * 2.0; + + let f = |x: N| x.ln() - x * 0.5 + 0.5; + rhoc * xc.mapv(f) + } } diff --git a/src/association/mod.rs b/src/association/mod.rs index aa1b74bb7..8dfcbc9de 100644 --- a/src/association/mod.rs +++ b/src/association/mod.rs @@ -1,13 +1,13 @@ //! Generic implementation of the SAFT association contribution //! that can be used across models. use crate::hard_sphere::HardSphereProperties; -use feos_core::{EosError, HelmholtzEnergyDual, StateHD}; +use feos_core::{EosError, EosResult, HelmholtzEnergyDual, StateHD}; use ndarray::*; use num_dual::linalg::{norm, LU}; use num_dual::*; +use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::fmt; -use std::ops::SubAssign; use std::sync::Arc; #[cfg(feature = "dft")] @@ -17,6 +17,25 @@ mod python; #[cfg(feature = "python")] pub use python::PyAssociationRecord; +#[derive(Clone, Copy, Debug)] +struct AssociationSite { + assoc_comp: usize, + n: f64, + kappa_ab: f64, + epsilon_k_ab: f64, +} + +impl AssociationSite { + fn new(assoc_comp: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64) -> Self { + Self { + assoc_comp, + n, + kappa_ab, + epsilon_k_ab, + } + } +} + /// Pure component association parameters. #[derive(Serialize, Deserialize, Clone, Copy, Default)] pub struct AssociationRecord { @@ -25,20 +44,27 @@ pub struct AssociationRecord { /// Association energy parameter in units of Kelvin pub epsilon_k_ab: f64, /// \# of association sites of type A - #[serde(skip_serializing_if = "Option::is_none")] - pub na: Option, + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub na: f64, /// \# of association sites of type B - #[serde(skip_serializing_if = "Option::is_none")] - pub nb: Option, + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub nb: f64, + /// \# of association sites of type C + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub nc: f64, } impl AssociationRecord { - pub fn new(kappa_ab: f64, epsilon_k_ab: f64, na: Option, nb: Option) -> Self { + pub fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { Self { kappa_ab, epsilon_k_ab, na, nb, + nc, } } } @@ -47,8 +73,16 @@ impl fmt::Display for AssociationRecord { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "AssociationRecord(kappa_ab={}", self.kappa_ab)?; write!(f, ", epsilon_k_ab={}", self.epsilon_k_ab)?; - write!(f, ", na={}", self.na.unwrap_or(1.0))?; - write!(f, ", nb={})", self.nb.unwrap_or(1.0)) + if self.na > 0.0 { + write!(f, ", na={}", self.na)?; + } + if self.nb > 0.0 { + write!(f, ", nb={}", self.nb)?; + } + if self.nc > 0.0 { + write!(f, ", nc={}", self.nc)?; + } + write!(f, ")") } } @@ -57,61 +91,88 @@ impl fmt::Display for AssociationRecord { #[derive(Clone)] pub struct AssociationParameters { component_index: Array1, - pub assoc_comp: Array1, - pub kappa_ab: Array1, - pub epsilon_k_ab: Array1, - pub sigma3_kappa_aibj: Array2, - pub epsilon_k_aibj: Array2, - pub na: Array1, - pub nb: Array1, + sites_a: Array1, + sites_b: Array1, + sites_c: Array1, + pub sigma3_kappa_ab: Array2, + pub sigma3_kappa_cc: Array2, + pub epsilon_k_ab: Array2, + pub epsilon_k_cc: Array2, } impl AssociationParameters { pub fn new( - records: &[Option], + records: &[Vec], sigma: &Array1, component_index: Option<&Array1>, ) -> Self { - let mut assoc_comp = Vec::new(); - let mut sigma_assoc = Vec::new(); - let mut kappa_ab = Vec::new(); - let mut epsilon_k_ab = Vec::new(); - let mut na = Vec::new(); - let mut nb = Vec::new(); + let mut sites_a = Vec::new(); + let mut sites_b = Vec::new(); + let mut sites_c = Vec::new(); for (i, record) in records.iter().enumerate() { - if let Some(record) = record.as_ref() { - if record.kappa_ab > 0.0 && record.epsilon_k_ab > 0.0 { - assoc_comp.push(i); - sigma_assoc.push(sigma[i]); - kappa_ab.push(record.kappa_ab); - epsilon_k_ab.push(record.epsilon_k_ab); - na.push(record.na.unwrap_or(1.0)); - nb.push(record.nb.unwrap_or(1.0)); + for site in record { + if site.kappa_ab > 0.0 && site.epsilon_k_ab > 0.0 { + if site.na > 0.0 { + sites_a.push(AssociationSite::new( + i, + site.na, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nb > 0.0 { + sites_b.push(AssociationSite::new( + i, + site.nb, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nc > 0.0 { + sites_c.push(AssociationSite::new( + i, + site.nc, + site.kappa_ab, + site.epsilon_k_ab, + )); + } } } } - let sigma3_kappa_aibj = Array2::from_shape_fn([kappa_ab.len(); 2], |(i, j)| { - (sigma_assoc[i] * sigma_assoc[j]).powf(1.5) * (kappa_ab[i] * kappa_ab[j]).sqrt() + let sigma3_kappa_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + (sigma[sites_a[i].assoc_comp] * sigma[sites_b[j].assoc_comp]).powf(1.5) + * (sites_a[i].kappa_ab * sites_b[j].kappa_ab).sqrt() + }); + let sigma3_kappa_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + (sigma[sites_c[i].assoc_comp] * sigma[sites_c[j].assoc_comp]).powf(1.5) + * (sites_c[i].kappa_ab * sites_c[j].kappa_ab).sqrt() + }); + let epsilon_k_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + 0.5 * (sites_a[i].epsilon_k_ab + sites_b[j].epsilon_k_ab) }); - let epsilon_k_aibj = Array2::from_shape_fn([epsilon_k_ab.len(); 2], |(i, j)| { - 0.5 * (epsilon_k_ab[i] + epsilon_k_ab[j]) + let epsilon_k_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + 0.5 * (sites_c[i].epsilon_k_ab + sites_c[j].epsilon_k_ab) }); Self { component_index: component_index .cloned() .unwrap_or_else(|| Array1::from_shape_fn(records.len(), |i| i)), - assoc_comp: Array1::from_vec(assoc_comp), - kappa_ab: Array1::from_vec(kappa_ab), - epsilon_k_ab: Array1::from_vec(epsilon_k_ab), - sigma3_kappa_aibj, - epsilon_k_aibj, - na: Array1::from_vec(na), - nb: Array1::from_vec(nb), + sites_a: Array1::from_vec(sites_a), + sites_b: Array1::from_vec(sites_b), + sites_c: Array1::from_vec(sites_c), + sigma3_kappa_ab, + sigma3_kappa_cc, + epsilon_k_ab, + epsilon_k_cc, } } + + pub fn is_empty(&self) -> bool { + (self.sites_a.is_empty() | self.sites_b.is_empty()) & self.sites_c.is_empty() + } } /// Implementation of the SAFT association Helmholtz energy @@ -158,17 +219,25 @@ impl Association

{ n2: D, n3i: D, xi: D, - ) -> Array2 { - // Calculate association strength - let ac = &self.association_parameters.assoc_comp; - Array2::from_shape_fn([ac.len(); 2], |(i, j)| { - let k = diameter[ac[i]] * diameter[ac[j]] / (diameter[ac[i]] + diameter[ac[j]]) - * (n2 * n3i); + ) -> [Array2; 2] { + let p = &self.association_parameters; + let delta_ab = Array2::from_shape_fn([p.sites_a.len(), p.sites_b.len()], |(i, j)| { + let di = diameter[p.sites_a[i].assoc_comp]; + let dj = diameter[p.sites_b[j].assoc_comp]; + let k = di * dj / (di + dj) * (n2 * n3i); n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) - * self.association_parameters.sigma3_kappa_aibj[(i, j)] - * (temperature.recip() * self.association_parameters.epsilon_k_aibj[(i, j)]) - .exp_m1() - }) + * p.sigma3_kappa_ab[(i, j)] + * (temperature.recip() * p.epsilon_k_ab[(i, j)]).exp_m1() + }); + let delta_cc = Array2::from_shape_fn([p.sites_c.len(); 2], |(i, j)| { + let di = diameter[p.sites_c[i].assoc_comp]; + let dj = diameter[p.sites_c[j].assoc_comp]; + let k = di * dj / (di + dj) * (n2 * n3i); + n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) + * p.sigma3_kappa_cc[(i, j)] + * (temperature.recip() * p.epsilon_k_cc[(i, j)]).exp_m1() + }); + [delta_ab, delta_cc] } } @@ -177,6 +246,7 @@ impl + ScalarOperand, P: HardSphereProperties> HelmholtzEnergyDu { fn helmholtz_energy(&self, state: &StateHD) -> D { let p: &P = &self.parameters; + let a = &self.association_parameters; // temperature dependent segment diameter let diameter = p.hs_diameter(state.temperature); @@ -186,48 +256,43 @@ impl + ScalarOperand, P: HardSphereProperties> HelmholtzEnergyDu let n2 = zeta2 * 6.0; let n3i = (-n3 + 1.0).recip(); - if self.association_parameters.assoc_comp.len() > 1 || self.force_cross_association { - // extract densities of associating segments - let rho_assoc = self - .association_parameters - .assoc_comp - .mapv(|a| state.partial_density[self.association_parameters.component_index[a]]); - - // Helmholtz energy - self.helmholtz_energy_density_cross_association( - state.temperature, - &rho_assoc, - &diameter, - n2, - n3i, - D::one(), - self.max_iter, - self.tol, - None, - ) - .unwrap_or_else(|_| D::from(std::f64::NAN)) - * state.volume - } else { - // association strength - let c = self.association_parameters.component_index - [self.association_parameters.assoc_comp[0]]; - let deltarho = - self.association_strength(state.temperature, &diameter, n2, n3i, D::one())[(0, 0)] - * state.partial_density[c]; - - let na = self.association_parameters.na[0]; - let nb = self.association_parameters.nb[0]; - if nb > 0.0 { - // no cross association, two association sites - let xa = Self::assoc_site_frac_ab(deltarho, na, nb); - let xb = (xa - 1.0) * (na / nb) + 1.0; - - state.moles[c] * ((xa.ln() - xa * 0.5 + 0.5) * na + (xb.ln() - xb * 0.5 + 0.5) * nb) - } else { - // no cross association, one association site - let xa = Self::assoc_site_frac_a(deltarho, na); - - state.moles[c] * (xa.ln() - xa * 0.5 + 0.5) * na + // association strength + let [delta_ab, delta_cc] = + self.association_strength(state.temperature, &diameter, n2, n3i, D::one()); + + match ( + a.sites_a.len() * a.sites_b.len(), + a.sites_c.len(), + self.force_cross_association, + ) { + (0, 0, _) => D::zero(), + (1, 0, false) => self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]), + (0, 1, false) => self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]), + (1, 1, false) => { + self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]) + + self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]) + } + _ => { + // extract site densities of associating segments + let rho: Array1<_> = a + .sites_a + .iter() + .chain(a.sites_b.iter()) + .chain(a.sites_c.iter()) + .map(|s| state.partial_density[a.component_index[s.assoc_comp]] * s.n) + .collect(); + + // Helmholtz energy + Self::helmholtz_energy_density_cross_association( + &rho, + &delta_ab, + &delta_cc, + self.max_iter, + self.tol, + None, + ) + .unwrap_or_else(|_| D::from(std::f64::NAN)) + * state.volume } } } @@ -240,66 +305,72 @@ impl

fmt::Display for Association

{ } impl Association

{ - pub fn assoc_site_frac_ab>(deltarho: D, na: f64, nb: f64) -> D { - (((deltarho * (na - nb) + 1.0).powi(2) + deltarho * nb * 4.0).sqrt() - + (deltarho * (nb - na) + 1.0)) - .recip() - * 2.0 + fn helmholtz_energy_ab_analytic>(&self, state: &StateHD, delta: D) -> D { + let a = &self.association_parameters; + + // site densities + let rhoa = + state.partial_density[a.component_index[a.sites_a[0].assoc_comp]] * a.sites_a[0].n; + let rhob = + state.partial_density[a.component_index[a.sites_b[0].assoc_comp]] * a.sites_b[0].n; + + // fraction of non-bonded association sites + let sqrt = ((delta * (rhoa - rhob) + 1.0).powi(2) + delta * rhob * 4.0).sqrt(); + let xa = (sqrt + (delta * (rhob - rhoa) + 1.0)).recip() * 2.0; + let xb = (sqrt + (delta * (rhoa - rhob) + 1.0)).recip() * 2.0; + + (rhoa * (xa.ln() - xa * 0.5 + 0.5) + rhob * (xb.ln() - xb * 0.5 + 0.5)) * state.volume } - pub fn assoc_site_frac_a>(deltarho: D, na: f64) -> D { - ((deltarho * 4.0 * na + 1.0).sqrt() + 1.0).recip() * 2.0 + fn helmholtz_energy_cc_analytic>(&self, state: &StateHD, delta: D) -> D { + let a = &self.association_parameters; + + // site density + let rhoc = + state.partial_density[a.component_index[a.sites_c[0].assoc_comp]] * a.sites_c[0].n; + + // fraction of non-bonded association sites + let xc = ((delta * 4.0 * rhoc + 1.0).sqrt() + 1.0).recip() * 2.0; + + rhoc * (xc.ln() - xc * 0.5 + 0.5) * state.volume } #[allow(clippy::too_many_arguments)] fn helmholtz_energy_density_cross_association< - S: Data, D: DualNum + ScalarOperand, + S: Data, >( - &self, - temperature: D, - density: &ArrayBase, - diameter: &Array1, - n2: D, - n3i: D, - xi: D, + rho: &ArrayBase, + delta_ab: &Array2, + delta_cc: &Array2, max_iter: usize, tol: f64, x0: Option<&mut Array1>, - ) -> Result { + ) -> EosResult { // check if density is close to 0 - if density.sum().re() < f64::EPSILON { + if rho.sum().re() < f64::EPSILON { if let Some(x0) = x0 { x0.fill(1.0); } return Ok(D::zero()); } - let assoc_comp = &self.association_parameters.assoc_comp; - let nassoc = assoc_comp.len(); - - // association strength - let delta = self.association_strength(temperature, diameter, n2, n3i, xi); - - // extract parameters of associating components - let na = &self.association_parameters.na; - let nb = &self.association_parameters.nb; - // cross-association according to Michelsen2006 // initialize monomer fraction let mut x = match &x0 { Some(x0) => (*x0).clone(), - None => Array::from_elem(2 * nassoc, 0.2), + None => Array::from_elem(rho.len(), 0.2), }; + let delta_ab_re = delta_ab.map(D::re); + let delta_cc_re = delta_cc.map(D::re); + let rho_re = rho.map(D::re); for k in 0..max_iter { - if Self::newton_step_cross_association::<_, f64>( - nassoc, + if Self::newton_step_cross_association( &mut x, - &delta.map(D::re), - na, - nb, - &density.map(D::re), + &delta_ab_re, + &delta_cc_re, + &rho_re, tol, )? { break; @@ -312,7 +383,7 @@ impl Association

{ // calculate derivatives let mut x_dual = x.mapv(D::from); for _ in 0..D::NDERIV { - Self::newton_step_cross_association(nassoc, &mut x_dual, &delta, na, nb, density, tol)?; + Self::newton_step_cross_association(&mut x_dual, delta_ab, delta_cc, rho, tol)?; } // save monomer fraction @@ -321,48 +392,72 @@ impl Association

{ } // Helmholtz energy density - let xa = x_dual.slice(s![..nassoc]); - let xb = x_dual.slice(s![nassoc..]); let f = |x: D| x.ln() - x * 0.5 + 0.5; - Ok((density * (xa.mapv(f) * na + xb.mapv(f) * nb)).sum()) + Ok((rho * x_dual.mapv(f)).sum()) } - fn newton_step_cross_association, D: DualNum + ScalarOperand>( - nassoc: usize, + fn newton_step_cross_association + ScalarOperand, S: Data>( x: &mut Array1, - delta: &Array2, - na: &Array1, - nb: &Array1, + delta_ab: &Array2, + delta_cc: &Array2, rho: &ArrayBase, tol: f64, - ) -> Result { + ) -> EosResult { + let nassoc = x.len(); // gradient let mut g = x.map(D::recip); // Hessian - let mut h: Array2 = Array::zeros((2 * nassoc, 2 * nassoc)); + let mut h: Array2 = Array::zeros([nassoc; 2]); - // split x array - let (xa, xb) = x.view().split_at(Axis(0), nassoc); + // split arrays + let &[a, b] = delta_ab.shape() else { panic!("wrong shape!") }; + let c = delta_cc.shape()[0]; + let (xa, xc) = x.view().split_at(Axis(0), a + b); + let (xa, xb) = xa.split_at(Axis(0), a); + let (rhoa, rhoc) = rho.view().split_at(Axis(0), a + b); + let (rhoa, rhob) = rhoa.split_at(Axis(0), a); - // calculate gradients and approximate Hessian for i in 0..nassoc { - let d = &delta.index_axis(Axis(0), i) * rho; - - let dnx = (&xb * nb * &d).sum() + 1.0; + // calculate gradients + let (d, dnx) = if i < a { + let d = delta_ab.index_axis(Axis(0), i); + (d, (&xb * &rhob * d).sum() + 1.0) + } else if i < a + b { + let d = delta_ab.index_axis(Axis(1), i - a); + (d, (&xa * &rhoa * d).sum() + 1.0) + } else { + let d = delta_cc.index_axis(Axis(0), i - a - b); + (d, (&xc * &rhoc * d).sum() + 1.0) + }; g[i] -= dnx; - for j in 0..nassoc { - h[(i, nassoc + j)] = -d[j] * nb[j]; - h[(nassoc + i, j)] = -d[j] * na[j]; - } - h[(i, i)] = -dnx / xa[i]; - let dnx = (&xa * na * &d).sum() + 1.0; - g[nassoc + i] -= dnx; - h[(nassoc + i, nassoc + i)] = -dnx / xb[i]; + // approximate hessian + h[(i, i)] = -dnx / x[i]; + if i < a { + for j in 0..b { + h[(i, a + j)] = -d[j] * rhob[j]; + } + } else if i < a + b { + for j in 0..a { + h[(i, j)] = -d[j] * rhoa[j]; + } + } else { + for j in 0..c { + h[(i, a + b + j)] -= d[j] * rhoc[j]; + } + } } // Newton step - x.sub_assign(&LU::new(h)?.solve(&g)); + // avoid stepping to negative values for x (see Michelsen 2006) + let delta_x = LU::new(h)?.solve(&g); + Zip::from(x).and(&delta_x).for_each(|x, &delta_x| { + if delta_x.re() < x.re() * 0.8 { + *x -= delta_x + } else { + *x *= 0.2 + } + }); // check convergence Ok(norm(&g.map(D::re)) < tol) @@ -407,7 +502,7 @@ mod tests_pcsaft { let mut params = water_parameters(); let mut record = params.pure_records.pop().unwrap(); let mut association_record = record.model_record.association_record.unwrap(); - association_record.na = Some(2.0); + association_record.na = 2.0; record.model_record.association_record = Some(association_record); let params = Arc::new(PcSaftParameters::new_pure(record)); let assoc = Association::new(¶ms, ¶ms.association, 50, 1e-10); diff --git a/src/association/python.rs b/src/association/python.rs index cbc8ed680..9744f5552 100644 --- a/src/association/python.rs +++ b/src/association/python.rs @@ -4,19 +4,16 @@ use feos_core::parameter::ParameterError; use pyo3::prelude::*; /// Pure component association parameters -#[pyclass( - name = "AssociationRecord", - text_signature = "(kappa_ab, epsilon_k_ab, na=None, nb=None)" -)] +#[pyclass(name = "AssociationRecord")] #[derive(Clone)] pub struct PyAssociationRecord(pub AssociationRecord); #[pymethods] impl PyAssociationRecord { - #[pyo3(signature = (kappa_ab, epsilon_k_ab, na=None, nb=None))] #[new] - fn new(kappa_ab: f64, epsilon_k_ab: f64, na: Option, nb: Option) -> Self { - Self(AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb)) + #[pyo3(signature = (kappa_ab, epsilon_k_ab, na=0.0, nb=0.0, nc=0.0))] + fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { + Self(AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc)) } #[getter] @@ -30,15 +27,20 @@ impl PyAssociationRecord { } #[getter] - fn get_na(&self) -> Option { + fn get_na(&self) -> f64 { self.0.na } #[getter] - fn get_nb(&self) -> Option { + fn get_nb(&self) -> f64 { self.0.nb } + #[getter] + fn get_nc(&self) -> f64 { + self.0.nc + } + fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index 438af9ba2..e8e502d65 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -56,7 +56,7 @@ impl GcPcSaftFunctional { contributions.push(Box::new(att)); // Association - if !parameters.association.assoc_comp.is_empty() { + if !parameters.association.is_empty() { let assoc = Association::new( ¶meters, ¶meters.association, diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index d6c27d9df..d0931dfac 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -80,7 +80,13 @@ impl ParameterHetero for GcPcSaftFunctionalParameters { sigma.push(segment.model_record.sigma); epsilon_k.push(segment.model_record.epsilon_k); - association_records.push(segment.model_record.association_record); + association_records.push( + segment + .model_record + .association_record + .into_iter() + .collect(), + ); psi_dft.push(segment.model_record.psi_dft.unwrap_or(PSI_GC_DFT)); diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index c6e0f9906..eec50fea4 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -60,7 +60,7 @@ impl GcPcSaft { contributions.push(Box::new(Dispersion { parameters: parameters.clone(), })); - if !parameters.association.assoc_comp.is_empty() { + if !parameters.association.is_empty() { contributions.push(Box::new(Association::new( ¶meters, ¶meters.association, diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index c25add9ad..795ea2792 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -140,10 +140,10 @@ impl ParameterHetero for GcPcSaftEosParameters { let mut assoc = segment.model_record.association_record; if let Some(mut assoc) = assoc.as_mut() { - assoc.na = Some(assoc.na.unwrap_or(1.0) * count); - assoc.nb = Some(assoc.nb.unwrap_or(1.0) * count); + assoc.na *= count; + assoc.nb *= count; }; - association_records.push(assoc); + association_records.push(assoc.into_iter().collect()); m_i += segment.model_record.m * count; sigma_i += segment.model_record.m * segment.model_record.sigma.powi(3) * count; @@ -287,11 +287,17 @@ impl HardSphereProperties for GcPcSaftEosParameters { impl GcPcSaftEosParameters { pub fn to_markdown(&self) -> String { + let gorup_dict: HashMap<&String, &GcPcSaftRecord> = self + .segment_records + .iter() + .map(|r| (&r.identifier, &r.model_record)) + .collect(); + let mut output = String::new(); let o = &mut output; write!( o, - "|component|molarweight|dipole moment|group|$m$|$\\sigma$|$\\varepsilon$|$\\kappa_{{AB}}$|$\\varepsilon_{{AB}}$|$N_A$|$N_B$|\n|-|-|-|-|-|-|-|-|-|-|-|" + "|component|molarweight|dipole moment|group|$m$|$\\sigma$|$\\varepsilon$|$\\kappa_{{AB}}$|$\\varepsilon_{{AB}}$|$N_A$|$N_B$|$N_C$|\n|-|-|-|-|-|-|-|-|-|-|-|-|" ) .unwrap(); for i in 0..self.m.len() { @@ -305,37 +311,39 @@ impl GcPcSaftEosParameters { .as_ref() .unwrap_or(&format!("Component {}", self.component_index[i] + 1)), self.molarweight[self.component_index[i]], - if let Some(d) = self.dipole_comp.iter().position(|&d| d == i) { + if let Some(d) = self + .dipole_comp + .iter() + .position(|&d| d == self.component_index[i]) + { format!("{}", self.mu[d]) } else { "".into() } ) }; - let association = - if let Some(a) = self.association.assoc_comp.iter().position(|&a| a == i) { - format!( - "{}|{}|{}|{}", - self.association.kappa_ab[a], - self.association.epsilon_k_ab[a], - self.association.na[a], - self.association.nb[a] - ) - } else { - "|||".to_string() - }; + let record = gorup_dict[&self.identifiers[i]]; + let association = if let Some(a) = record.association_record { + format!( + "{}|{}|{}|{}|{}", + a.kappa_ab, a.epsilon_k_ab, a.na, a.nb, a.nc + ) + } else { + "||||".to_string() + }; write!( o, "\n|{}|{}|{}|{}|{}|{}|", component, self.identifiers[i], - self.m[i], - self.sigma[i], - self.epsilon_k[i], + record.m, + record.sigma, + record.epsilon_k, association ) .unwrap(); } + write!(o, "\n\n|component|group 1|group 2|bonds|\n|-|-|-|-|").unwrap(); let mut last_component = None; @@ -426,7 +434,7 @@ pub mod test { 2.7702, 334.29, None, - Some(AssociationRecord::new(0.009583, 2575.9, None, None)), + Some(AssociationRecord::new(0.009583, 2575.9, 1.0, 1.0, 0.0)), None, ), None, diff --git a/src/gc_pcsaft/python/mod.rs b/src/gc_pcsaft/python/mod.rs index 6ad10432a..171254a1e 100644 --- a/src/gc_pcsaft/python/mod.rs +++ b/src/gc_pcsaft/python/mod.rs @@ -67,7 +67,7 @@ impl PyGcPcSaftRecord { #[getter] fn get_association_record(&self) -> Option { - self.0.association_record.clone().map(PyAssociationRecord) + self.0.association_record.map(PyAssociationRecord) } fn __repr__(&self) -> PyResult { diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index fab80baad..deefc121d 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -76,7 +76,7 @@ impl PcSaftFunctional { contributions.push(Box::new(att)); // Association - if !parameters.association.assoc_comp.is_empty() { + if !parameters.association.is_empty() { let assoc = Association::new( ¶meters, ¶meters.association, diff --git a/src/pcsaft/dft/pure_saft_functional.rs b/src/pcsaft/dft/pure_saft_functional.rs index 66279ef3d..801a079a8 100644 --- a/src/pcsaft/dft/pure_saft_functional.rs +++ b/src/pcsaft/dft/pure_saft_functional.rs @@ -18,16 +18,18 @@ const PI36M1: f64 = 1.0 / (36.0 * PI); const N3_CUTOFF: f64 = 1e-5; const N0_CUTOFF: f64 = 1e-9; -#[derive(Clone)] pub struct PureFMTAssocFunctional { parameters: Arc, + association: Association, version: FMTVersion, } impl PureFMTAssocFunctional { pub fn new(parameters: Arc, version: FMTVersion) -> Self { + let association = Association::new(¶meters, ¶meters.association, 50, 1e-10); Self { parameters, + association, version, } } @@ -113,33 +115,22 @@ impl + ScalarOperand> FunctionalContributionDual for PureFMTA // association let a = &p.association; - if a.assoc_comp.len() == 1 { + if !a.is_empty() { let mut xi = -(&n2v * &n2v).sum_axis(Axis(0)) / (&n2 * &n2) + 1.0; xi.iter_mut().zip(&n2).for_each(|(xi, &n2)| { if n2.re() < N0_CUTOFF * 4.0 * PI * p.m[0] * r.re().powi(2) { *xi = N::one(); } }); - - let k = &n2 * &n3m1rec * r; - let deltarho = (((&k / 18.0 + 0.5) * &k * &xi + 1.0) * n3m1rec) - * ((temperature.recip() * a.epsilon_k_aibj[(0, 0)]).exp_m1() - * a.sigma3_kappa_aibj[(0, 0)]) - * (&n0 / p.m[0] * &xi); - - let f = |x: N| x.ln() - x * 0.5 + 0.5; - phi = phi - + if a.nb[0] > 0.0 { - let xa = deltarho.mapv(|d| { - Association::::assoc_site_frac_ab(d, a.na[0], a.nb[0]) - }); - let xb = (xa.clone() - 1.0) * a.na[0] / a.nb[0] + 1.0; - (n0 / p.m[0] * xi) * (xa.mapv(f) * a.na[0] + xb.mapv(f) * a.nb[0]) - } else { - let xa = deltarho - .mapv(|d| Association::::assoc_site_frac_a(d, a.na[0])); - n0 / p.m[0] * xi * (xa.mapv(f) * a.na[0]) - }; + let rho0 = (&n0 / p.m[0] * &xi).insert_axis(Axis(0)); + + phi += &(self.association.calculate_helmholtz_energy_density( + temperature, + &rho0, + &n2, + &n3m1rec, + &xi, + ))?; } Ok(phi) diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index 48f0b05fa..c11a7714a 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -86,7 +86,7 @@ impl PcSaft { variant: options.dq_variant, })); }; - if !parameters.association.assoc_comp.is_empty() { + if !parameters.association.is_empty() { contributions.push(Box::new(Association::new( ¶meters, ¶meters.association, diff --git a/src/pcsaft/eos/qspr.rs b/src/pcsaft/eos/qspr.rs index baa2e6436..0f2ce9b4b 100644 --- a/src/pcsaft/eos/qspr.rs +++ b/src/pcsaft/eos/qspr.rs @@ -70,12 +70,13 @@ pub struct QSPR { impl> IdealGasContributionDual for QSPR { fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = match self.parameters.association.assoc_comp.len() { - 0 => match self.parameters.ndipole + self.parameters.nquadpole { + let (c_300, c_400) = if self.parameters.association.is_empty() { + match self.parameters.ndipole + self.parameters.nquadpole { 0 => (NA_NP_300, NA_NP_400), _ => (NA_P_300, NA_P_400), - }, - _ => (AP_300, AP_400), + } + } else { + (AP_300, AP_400) }; Array1::from_shape_fn(components, |i| { diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 7053aa6a6..5cfef1564 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -7,7 +7,6 @@ use feos_core::parameter::{ }; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; -use num_traits::Zero; use quantity::si::{JOULE, KB, KELVIN}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -70,14 +69,23 @@ impl FromSegments for PcSaftRecord { [ record.kappa_ab * n, record.epsilon_k_ab * n, - record.na.unwrap_or(1.0) * n, - record.nb.unwrap_or(1.0) * n, + record.na * n, + record.nb * n, + record.nc * n, ] }) }) - .reduce(|a, b| [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]) - .map(|[kappa_ab, epsilon_k_ab, na, nb]| { - AssociationRecord::new(kappa_ab, epsilon_k_ab, Some(na), Some(nb)) + .reduce(|a, b| { + [ + a[0] + b[0], + a[1] + b[1], + a[2] + b[2], + a[3] + b[3], + a[4] + b[4], + ] + }) + .map(|[kappa_ab, epsilon_k_ab, na, nb, nc]| { + AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc) }); // entropy scaling @@ -151,13 +159,23 @@ impl FromSegments for PcSaftRecord { impl FromSegments for PcSaftRecord { fn from_segments(segments: &[(Self, usize)]) -> Result { // We do not allow more than a single segment for q, mu, kappa_ab, epsilon_k_ab + let polar_segments: usize = segments + .iter() + .filter_map(|(s, n)| { + if s.q.is_some() || s.mu.is_some() || s.association_record.is_some() { + Some(n) + } else { + None + } + }) + .sum(); let quadpole_segments: usize = segments.iter().filter_map(|(s, n)| s.q.map(|_| n)).sum(); let dipole_segments: usize = segments.iter().filter_map(|(s, n)| s.mu.map(|_| n)).sum(); let assoc_segments: usize = segments .iter() .filter_map(|(s, n)| s.association_record.map(|_| n)) .sum(); - if quadpole_segments + dipole_segments + assoc_segments > 1 { + if polar_segments > 1 { return Err(ParameterError::IncompatibleParameters(format!( "Too many polar/associating segments (dipolar: {dipole_segments}, quadrupolar {quadpole_segments}, associating: {assoc_segments})." ))); @@ -209,14 +227,19 @@ impl PcSaftRecord { epsilon_k_ab: Option, na: Option, nb: Option, + nc: Option, viscosity: Option<[f64; 4]>, diffusion: Option<[f64; 5]>, thermal_conductivity: Option<[f64; 4]>, ) -> PcSaftRecord { let association_record = match (kappa_ab, epsilon_k_ab) { - (Some(kappa_ab), Some(epsilon_k_ab)) => { - Some(AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb)) - } + (Some(kappa_ab), Some(epsilon_k_ab)) => Some(AssociationRecord::new( + kappa_ab, + epsilon_k_ab, + na.unwrap_or(0.0), + nb.unwrap_or(0.0), + nc.unwrap_or(0.0), + )), (None, None) => None, _ => { panic!("To model association, both kappa_ab and epsilon_k_ab need to be specified.") @@ -328,7 +351,7 @@ impl Parameter for PcSaftParameters { epsilon_k[i] = r.epsilon_k; mu[i] = r.mu.unwrap_or(0.0); q[i] = r.q.unwrap_or(0.0); - association_records.push(r.association_record); + association_records.push(r.association_record.into_iter().collect()); viscosity.push(r.viscosity); diffusion.push(r.diffusion); thermal_conductivity.push(r.thermal_conductivity); @@ -460,7 +483,7 @@ impl PcSaftParameters { let o = &mut output; write!( o, - "|component|molarweight|$m$|$\\sigma$|$\\varepsilon$|$\\mu$|$Q$|$\\kappa_{{AB}}$|$\\varepsilon_{{AB}}$|$N_A$|$N_B$|\n|-|-|-|-|-|-|-|-|-|-|-|" + "|component|molarweight|$m$|$\\sigma$|$\\varepsilon$|$\\mu$|$Q$|$\\kappa_{{AB}}$|$\\varepsilon_{{AB}}$|$N_A$|$N_B$|$N_C$|\n|-|-|-|-|-|-|-|-|-|-|-|-|" ) .unwrap(); for (i, record) in self.pure_records.iter().enumerate() { @@ -469,10 +492,10 @@ impl PcSaftParameters { let association = record .model_record .association_record - .unwrap_or_else(|| AssociationRecord::new(0.0, 0.0, None, None)); + .unwrap_or_else(|| AssociationRecord::new(0.0, 0.0, 0.0, 0.0, 0.0)); write!( o, - "\n|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|", + "\n|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|", component, record.molarweight, record.model_record.m, @@ -482,8 +505,9 @@ impl PcSaftParameters { record.model_record.q.unwrap_or(0.0), association.kappa_ab, association.epsilon_k_ab, - association.na.unwrap_or(1.0), - association.nb.unwrap_or(1.0) + association.na, + association.nb, + association.nc ) .unwrap(); } @@ -492,33 +516,6 @@ impl PcSaftParameters { } } -impl std::fmt::Display for PcSaftParameters { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "PcSaftParameters(")?; - write!(f, "\n\tmolarweight={}", self.molarweight)?; - write!(f, "\n\tm={}", self.m)?; - write!(f, "\n\tsigma={}", self.sigma)?; - write!(f, "\n\tepsilon_k={}", self.epsilon_k)?; - if !self.dipole_comp.is_empty() { - write!(f, "\n\tmu={}", self.mu)?; - } - if !self.quadpole_comp.is_empty() { - write!(f, "\n\tq={}", self.q)?; - } - if !self.association.assoc_comp.is_empty() { - write!(f, "\n\tassociating={}", self.association.assoc_comp)?; - write!(f, "\n\tkappa_ab={}", self.association.kappa_ab)?; - write!(f, "\n\tepsilon_k_ab={}", self.association.epsilon_k_ab)?; - write!(f, "\n\tna={}", self.association.na)?; - write!(f, "\n\tnb={}", self.association.nb)?; - } - if !self.k_ij.iter().all(|k| k.is_zero()) { - write!(f, "\n\tk_ij=\n{}", self.k_ij)?; - } - write!(f, "\n)") - } -} - #[cfg(test)] pub mod utils { use super::*; @@ -639,7 +636,9 @@ pub mod utils { "sigma": 3.000683, "epsilon_k": 366.5121, "kappa_ab": 0.034867983, - "epsilon_k_ab": 2500.6706 + "epsilon_k_ab": 2500.6706, + "na": 1.0, + "nb": 1.0 }, "molarweight": 18.0152 }"#; diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 658722422..077e4d7e6 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -36,6 +36,7 @@ impl PyPcSaftRecord { epsilon_k_ab: Option, na: Option, nb: Option, + nc: Option, viscosity: Option<[f64; 4]>, diffusion: Option<[f64; 5]>, thermal_conductivity: Option<[f64; 4]>, @@ -50,6 +51,7 @@ impl PyPcSaftRecord { epsilon_k_ab, na, nb, + nc, viscosity, diffusion, thermal_conductivity, @@ -93,12 +95,17 @@ impl PyPcSaftRecord { #[getter] fn get_na(&self) -> Option { - self.0.association_record.and_then(|a| a.na) + self.0.association_record.map(|a| a.na) } #[getter] fn get_nb(&self) -> Option { - self.0.association_record.and_then(|a| a.nb) + self.0.association_record.map(|a| a.nb) + } + + #[getter] + fn get_nc(&self) -> Option { + self.0.association_record.map(|a| a.nc) } #[getter] @@ -172,10 +179,6 @@ impl PyPcSaftParameters { fn _repr_markdown_(&self) -> String { self.0.to_markdown() } - - fn __repr__(&self) -> PyResult { - Ok(self.0.to_string()) - } } #[pymodule] diff --git a/tests/gc_pcsaft/dft.rs b/tests/gc_pcsaft/dft.rs index b777405b5..cc5a76827 100644 --- a/tests/gc_pcsaft/dft.rs +++ b/tests/gc_pcsaft/dft.rs @@ -120,13 +120,13 @@ fn test_bulk_association() -> Result<(), Box> { segment_records.clone(), None, )?); - let eos = Arc::new(GcPcSaft::new(eos_parameters.clone())); + let eos = Arc::new(GcPcSaft::new(eos_parameters)); let func_parameters = Arc::new(GcPcSaftFunctionalParameters::from_segments( vec![ethylene_glycol], segment_records, None, )?); - let func = Arc::new(GcPcSaftFunctional::new(func_parameters.clone())); + let func = Arc::new(GcPcSaftFunctional::new(func_parameters)); let t = 200.0 * KELVIN; let v = 0.002 * METER.powi(3); @@ -135,35 +135,9 @@ fn test_bulk_association() -> Result<(), Box> { let state_func = State::new_nvt(&func, t, v, &n)?; let p_eos = state_eos.pressure_contributions(); let p_func = state_func.pressure_contributions(); - println!( - "Equation of state: - \tcomps: {} - \tkappa_ab: {} - \tepsilon_k_ab: {} - \tna: {} - \tnb: {}", - eos_parameters.association.assoc_comp, - eos_parameters.association.kappa_ab, - eos_parameters.association.epsilon_k_ab, - eos_parameters.association.na, - eos_parameters.association.nb, - ); for (s, x) in &p_eos { println!("{s:18}: {x:21.16}"); } - println!( - "\nHelmholtz energy functional: - \tcomps: {} - \tkappa_ab: {} - \tepsilon_k_ab: {} - \tna: {} - \tnb: {}", - func_parameters.association.assoc_comp, - func_parameters.association.kappa_ab, - func_parameters.association.epsilon_k_ab, - func_parameters.association.na, - func_parameters.association.nb, - ); for (s, x) in &p_func { println!("{s:26}: {x:21.16}"); } diff --git a/tests/pcsaft/test_parameters.json b/tests/pcsaft/test_parameters.json index 988fcb3b8..c49a5de31 100644 --- a/tests/pcsaft/test_parameters.json +++ b/tests/pcsaft/test_parameters.json @@ -93,7 +93,9 @@ "sigma": 3.000683, "epsilon_k": 366.5121, "kappa_ab": 0.034867983, - "epsilon_k_ab": 2500.6706 + "epsilon_k_ab": 2500.6706, + "na": 1.0, + "nb": 1.0 }, "molarweight": 18.0152 }, From 16787018882e52e622ad33ebb1fdf7687f471cdb Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Tue, 30 May 2023 14:56:50 +0200 Subject: [PATCH 2/7] Revive phi in hetero gc PC-SAFT (#157) --- CHANGELOG.md | 1 + src/gc_pcsaft/eos/parameter.rs | 18 +++++++++++++++++- src/gc_pcsaft/python/mod.rs | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 335a4bbdc..296c660a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) - To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) +- Added the possibility to specify a pure component correction parameter `phi` for the heterosegmented gc PC-SAFT equation of state. [#157](https://github.com/feos-org/feos/pull/157) ## [0.4.3] - 2023-03-20 - Python only: Release the changes introduced in `feos-core` 0.4.2. diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index 795ea2792..efc5185bf 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -19,6 +19,7 @@ pub struct GcPcSaftChemicalRecord { pub identifier: Identifier, pub segments: HashMap, pub bonds: HashMap<[String; 2], f64>, + phi: f64, } impl GcPcSaftChemicalRecord { @@ -26,11 +27,13 @@ impl GcPcSaftChemicalRecord { identifier: Identifier, segments: HashMap, bonds: HashMap<[String; 2], f64>, + phi: f64, ) -> Self { Self { identifier, segments, bonds, + phi, } } } @@ -53,11 +56,13 @@ impl From for GcPcSaftChemicalRecord { chemical_record.identifier.clone(), chemical_record.segment_count(), chemical_record.bond_count(), + 1.0, ) } } /// Parameter set required for the gc-PC-SAFT equation of state. +#[derive(Clone)] pub struct GcPcSaftEosParameters { pub molarweight: Array1, pub component_index: Array1, @@ -116,11 +121,14 @@ impl ParameterHetero for GcPcSaftEosParameters { let mut sigma_mix = Vec::new(); let mut epsilon_k_mix = Vec::new(); + let mut phi = Vec::new(); + let mut joback_records = Vec::new(); for (i, chemical_record) in chemical_records.iter().cloned().enumerate() { let mut segment_indices = IndexMap::with_capacity(segment_records.len()); let segment_map = chemical_record.segment_map(&segment_records)?; + phi.push(chemical_record.phi); let mut m_i = 0.0; let mut sigma_i = 0.0; @@ -214,7 +222,7 @@ impl ParameterHetero for GcPcSaftEosParameters { let sigma_ij = Array2::from_shape_fn([sigma.len(); 2], |(i, j)| 0.5 * (sigma[i] + sigma[j])); let epsilon_k_ij = Array2::from_shape_fn([epsilon_k.len(); 2], |(i, j)| { - (epsilon_k[i] * epsilon_k[j]).sqrt() + (epsilon_k[i] * phi[component_index[i]] * epsilon_k[j] * phi[component_index[j]]).sqrt() }) * (1.0 - &k_ij); // Combining rules polar @@ -271,6 +279,14 @@ impl ParameterHetero for GcPcSaftEosParameters { } } +impl GcPcSaftEosParameters { + pub fn phi(self, phi: &[f64]) -> Result { + let mut cr = self.chemical_records; + cr.iter_mut().zip(phi.iter()).for_each(|(c, &p)| c.phi = p); + Self::from_segments(cr, self.segment_records, self.binary_segment_records) + } +} + impl HardSphereProperties for GcPcSaftEosParameters { fn monomer_shape>(&self, _: N) -> MonomerShape { let m = self.m.mapv(N::from); diff --git a/src/gc_pcsaft/python/mod.rs b/src/gc_pcsaft/python/mod.rs index 171254a1e..eed44690d 100644 --- a/src/gc_pcsaft/python/mod.rs +++ b/src/gc_pcsaft/python/mod.rs @@ -95,6 +95,10 @@ impl_parameter_from_segments!(GcPcSaftEosParameters, PyGcPcSaftEosParameters); #[pymethods] impl PyGcPcSaftEosParameters { + fn phi(&self, phi: Vec) -> PyResult { + Ok(Self(Arc::new((*self.0).clone().phi(&phi)?))) + } + fn _repr_markdown_(&self) -> String { self.0.to_markdown() } From 263dd916134461efc596c7c483673c6aa810b5f8 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Wed, 31 May 2023 13:41:04 +0200 Subject: [PATCH 3/7] Update num-dual dependency to 0.7 (#137) --- .github/workflows/test.yml | 4 +- .github/workflows/wheels.yml | 4 +- CHANGELOG.md | 3 + Cargo.toml | 2 +- benches/dual_numbers.rs | 2 +- feos-core/CHANGELOG.md | 2 + feos-core/Cargo.toml | 5 +- feos-core/src/cubic.rs | 2 +- feos-core/src/equation_of_state.rs | 111 +++++----- feos-core/src/joback.rs | 2 +- feos-core/src/python/user_defined.rs | 31 +-- feos-core/src/state/cache.rs | 22 +- feos-core/src/state/critical_point.rs | 195 +++++++++--------- feos-core/src/state/mod.rs | 32 +-- feos-core/src/state/properties.rs | 20 +- feos-dft/CHANGELOG.md | 2 + feos-dft/Cargo.toml | 3 +- feos-dft/src/convolver/mod.rs | 4 +- feos-dft/src/functional.rs | 22 +- feos-dft/src/functional_contribution.rs | 63 +++--- feos-dft/src/ideal_chain_contribution.rs | 2 +- feos-dft/src/pdgt.rs | 15 +- feos-dft/src/profile.rs | 10 +- feos-dft/src/solver.rs | 9 +- feos-dft/src/weight_functions.rs | 4 +- src/association/dft.rs | 14 +- src/association/mod.rs | 32 +-- src/gc_pcsaft/dft/dispersion.rs | 4 +- src/gc_pcsaft/dft/hard_chain.rs | 2 +- src/gc_pcsaft/dft/mod.rs | 2 +- src/gc_pcsaft/eos/dispersion.rs | 10 +- src/gc_pcsaft/eos/hard_chain.rs | 10 +- src/gc_pcsaft/eos/mod.rs | 20 +- src/gc_pcsaft/eos/parameter.rs | 2 +- src/gc_pcsaft/eos/polar.rs | 6 +- src/hard_sphere/dft.rs | 2 +- src/hard_sphere/mod.rs | 8 +- src/pcsaft/dft/dispersion.rs | 6 +- src/pcsaft/dft/hard_chain.rs | 2 +- src/pcsaft/dft/polar.rs | 10 +- src/pcsaft/dft/pure_saft_functional.rs | 8 +- src/pcsaft/eos/dispersion.rs | 2 +- src/pcsaft/eos/hard_chain.rs | 2 +- src/pcsaft/eos/polar.rs | 17 +- src/pcsaft/eos/qspr.rs | 2 +- src/pcsaft/parameters.rs | 2 +- src/pets/dft/dispersion.rs | 6 +- src/pets/dft/pure_pets_functional.rs | 4 +- src/pets/eos/dispersion.rs | 2 +- src/pets/eos/qspr.rs | 2 +- src/pets/parameters.rs | 2 +- src/saftvrqmie/dft/dispersion.rs | 6 +- src/saftvrqmie/dft/mod.rs | 2 +- src/saftvrqmie/dft/non_additive_hs.rs | 4 +- src/saftvrqmie/eos/dispersion.rs | 146 +++++++------ src/saftvrqmie/eos/hard_sphere.rs | 72 ++++--- src/saftvrqmie/eos/non_additive_hs.rs | 4 +- .../eos/attractive_perturbation_bh.rs | 18 +- .../eos/attractive_perturbation_uvb3.rs | 29 ++- .../eos/attractive_perturbation_wca.rs | 18 +- src/uvtheory/eos/hard_sphere_bh.rs | 23 ++- src/uvtheory/eos/hard_sphere_wca.rs | 29 ++- src/uvtheory/eos/reference_perturbation_bh.rs | 2 +- .../eos/reference_perturbation_uvb3.rs | 2 +- .../eos/reference_perturbation_wca.rs | 2 +- src/uvtheory/parameters.rs | 4 +- 66 files changed, 589 insertions(+), 522 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 73c300ab2..1f06c7322 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [main] + branches: [main, development] pull_request: - branches: [main] + branches: [main, development] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8e167f82a..4ccc9a432 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,9 +1,9 @@ name: Build Wheels on: push: - branches: [main] + branches: [main, development] pull_request: - branches: [main] + branches: [main, development] jobs: linux: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 296c660a0..ba4edacde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) - Added the possibility to specify a pure component correction parameter `phi` for the heterosegmented gc PC-SAFT equation of state. [#157](https://github.com/feos-org/feos/pull/157) +### Packaging +- Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) + ## [0.4.3] - 2023-03-20 - Python only: Release the changes introduced in `feos-core` 0.4.2. diff --git a/Cargo.toml b/Cargo.toml index 357ee68d0..ff44725de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ crate-type = ["rlib", "cdylib"] [dependencies] quantity = "0.6" -num-dual = "0.6" +num-dual = "0.7" feos-core = { version = "0.4", path = "feos-core" } feos-dft = { version = "0.4", path = "feos-dft", optional = true } feos-derive = { version = "0.2", path = "feos-derive" } diff --git a/benches/dual_numbers.rs b/benches/dual_numbers.rs index 0eff389fa..f78ca03f7 100644 --- a/benches/dual_numbers.rs +++ b/benches/dual_numbers.rs @@ -28,7 +28,7 @@ fn state_pcsaft(parameters: PcSaftParameters) -> State { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res, E: EquationOfState>(inp: (&Arc, &StateHD)) -> D +fn a_res + Copy, E: EquationOfState>(inp: (&Arc, &StateHD)) -> D where (dyn HelmholtzEnergy + 'static): HelmholtzEnergyDual, { diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index b0e933596..7c14acda2 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Packaging +- Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) ## [0.4.2] - 2023-04-03 ### Fixed diff --git a/feos-core/Cargo.toml b/feos-core/Cargo.toml index 4b3dc2940..4788e6690 100644 --- a/feos-core/Cargo.toml +++ b/feos-core/Cargo.toml @@ -19,8 +19,9 @@ features = [ "rayon" ] [dependencies] quantity = "0.6" -num-dual = { version = "0.6", features = ["linalg"] } +num-dual = { version = "0.7", features = ["linalg"] } ndarray = { version = "0.15", features = ["serde"] } +nalgebra = "0.32" num-traits = "0.2" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } @@ -37,4 +38,4 @@ approx = "0.4" [features] default = [] rayon = ["dep:rayon", "ndarray/rayon"] -python = ["pyo3", "numpy", "quantity/python", "num-dual/python", "rayon"] +python = ["pyo3", "numpy", "quantity/python", "num-dual/python_macro", "rayon"] diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index 2732ea0a2..f0dc05862 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -170,7 +170,7 @@ struct PengRobinsonContribution { parameters: Arc, } -impl> HelmholtzEnergyDual for PengRobinsonContribution { +impl + Copy> HelmholtzEnergyDual for PengRobinsonContribution { fn helmholtz_energy(&self, state: &StateHD) -> D { // temperature dependent a parameter let p = &self.parameters; diff --git a/feos-core/src/equation_of_state.rs b/feos-core/src/equation_of_state.rs index abb500012..6d7b523fc 100644 --- a/feos-core/src/equation_of_state.rs +++ b/feos-core/src/equation_of_state.rs @@ -3,9 +3,10 @@ use crate::state::StateHD; use crate::EosUnit; use ndarray::prelude::*; use num_dual::{ - Dual, Dual2_64, Dual3, Dual3_64, Dual64, DualNum, DualVec64, HyperDual, HyperDual64, + first_derivative, second_derivative, third_derivative, Dual, Dual2, Dual2_64, Dual3, Dual3_64, + Dual64, DualNum, DualSVec64, HyperDual, HyperDual64, }; -use num_traits::{One, Zero}; +use num_traits::Zero; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; @@ -28,16 +29,17 @@ pub trait HelmholtzEnergyDual> { pub trait HelmholtzEnergy: HelmholtzEnergyDual + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + HelmholtzEnergyDual + HelmholtzEnergyDual + HelmholtzEnergyDual + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + fmt::Display + Send + Sync @@ -47,16 +49,17 @@ pub trait HelmholtzEnergy: impl HelmholtzEnergy for T where T: HelmholtzEnergyDual + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + HelmholtzEnergyDual + HelmholtzEnergyDual + HelmholtzEnergyDual + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + fmt::Display + Send + Sync @@ -70,7 +73,7 @@ impl HelmholtzEnergy for T where /// the specific types in the supertraits of [IdealGasContribution] /// so that the implementor can be used as an ideal gas /// contribution in the equation of state. -pub trait IdealGasContributionDual> { +pub trait IdealGasContributionDual + Copy> { /// The thermal de Broglie wavelength of each component in the form $\ln\left(\frac{\Lambda^3}{\AA^3}\right)$ fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1; @@ -101,16 +104,17 @@ pub trait IdealGasContributionDual> { pub trait IdealGasContribution: IdealGasContributionDual + IdealGasContributionDual - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + IdealGasContributionDual + IdealGasContributionDual + IdealGasContributionDual + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual> + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + fmt::Display { } @@ -118,22 +122,23 @@ pub trait IdealGasContribution: impl IdealGasContribution for T where T: IdealGasContributionDual + IdealGasContributionDual - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + IdealGasContributionDual + IdealGasContributionDual + IdealGasContributionDual + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual> + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + + IdealGasContributionDual, f64>> + fmt::Display { } struct DefaultIdealGasContribution; -impl> IdealGasContributionDual for DefaultIdealGasContribution { +impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { Array1::zeros(components) } @@ -175,7 +180,7 @@ pub trait EquationOfState: Send + Sync { fn residual(&self) -> &[Box]; /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. - fn evaluate_residual>(&self, state: &StateHD) -> D + fn evaluate_residual + Copy>(&self, state: &StateHD) -> D where dyn HelmholtzEnergy: HelmholtzEnergyDual, { @@ -187,7 +192,7 @@ pub trait EquationOfState: Send + Sync { /// Evaluate the reduced Helmholtz energy of each individual contribution /// and return them together with a string representation of the contribution. - fn evaluate_residual_contributions>( + fn evaluate_residual_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(String, D)> @@ -252,12 +257,10 @@ pub trait EquationOfState: Send + Sync { ) -> EosResult { let mr = self.validate_moles(moles)?; let x = mr.to_reduced(mr.sum())?; - let mut rho = HyperDual64::zero(); - rho.eps1[0] = 1.0; - rho.eps2[0] = 1.0; - let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); + let (_, _, b) = second_derivative(a_res, 0.0); + Ok(b * 0.5 / SIUnit::reference_density()) } /// Calculate the third virial coefficient $C(T)$ @@ -268,10 +271,10 @@ pub trait EquationOfState: Send + Sync { ) -> EosResult { let mr = self.validate_moles(moles)?; let x = mr.to_reduced(mr.sum())?; - let rho = Dual3_64::zero().derive(); - let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); + let (_, _, _, c) = third_derivative(a_res, 0.0); + Ok(c / 3.0 / SIUnit::reference_density().powi(2)) } /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ @@ -282,15 +285,16 @@ pub trait EquationOfState: Send + Sync { ) -> EosResult { let mr = self.validate_moles(moles)?; let x = mr.to_reduced(mr.sum())?; - let mut rho = HyperDual::zero(); - rho.eps1[0] = Dual64::one(); - rho.eps2[0] = Dual64::one(); - let t = HyperDual::from_re( - Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), - ); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)].eps[0] * 0.5 - / (SIUnit::reference_density() * SIUnit::reference_temperature())) + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + let b = |t| { + let a_res = |rho: Dual2| { + self.evaluate_residual(&StateHD::new_virial(Dual2::from_re(t), rho, x)) + }; + let (_, _, b) = second_derivative(a_res, Dual64::zero()); + b + }; + let (_, b_t) = first_derivative(b, t); + Ok(b_t * 0.5 / (SIUnit::reference_density() * SIUnit::reference_temperature())) } /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ @@ -301,14 +305,15 @@ pub trait EquationOfState: Send + Sync { ) -> EosResult { let mr = self.validate_moles(moles)?; let x = mr.to_reduced(mr.sum())?; - let rho = Dual3::zero().derive(); - let t = Dual3::from_re( - Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), - ); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).v3.eps[0] - / 3.0 - / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + let c = |t| { + let a_res = + |rho| self.evaluate_residual(&StateHD::new_virial(Dual3::from_re(t), rho, x)); + let (_, _, _, c) = third_derivative(a_res, Dual64::zero()); + c + }; + let (_, c_t) = first_derivative(c, t); + Ok(c_t / 3.0 / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) } } diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index 643f7bc66..b8d73e90e 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -107,7 +107,7 @@ const P0: f64 = 1.0e5; const A3: f64 = 1e-30; const KB: f64 = 1.38064852e-23; -impl> IdealGasContributionDual for Joback { +impl + Copy> IdealGasContributionDual for Joback { fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { let t = temperature; let t2 = t * t; diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index 52c34e10c..ace9713ac 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -2,11 +2,11 @@ use crate::{EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, MolarWeight, use ndarray::Array1; use num_dual::*; use numpy::convert::IntoPyArray; -use numpy::{PyReadonlyArrayDyn, PyArray}; +use numpy::{PyArray, PyReadonlyArrayDyn}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; -use quantity::si::{SIArray1}; +use quantity::si::SIArray1; use std::fmt; struct PyHelmholtzEnergy(Py); @@ -205,35 +205,36 @@ state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); -dual_number!(PyDualVec3, DualVec64<3>, f64); +dual_number!(PyDualVec3, DualSVec64<3>, f64); impl_dual_state_helmholtz_energy!( PyStateDualDualVec3, PyDualDualVec3, - Dual, f64>, + Dual, f64>, PyDualVec3 ); -impl_dual_state_helmholtz_energy!( - PyStateHD, - PyHyperDual64, - HyperDual64, - f64 -); +impl_dual_state_helmholtz_energy!(PyStateHD, PyHyperDual64, HyperDual64, f64); impl_dual_state_helmholtz_energy!(PyStateD2, PyDual2_64, Dual2_64, f64); impl_dual_state_helmholtz_energy!(PyStateD3, PyDual3_64, Dual3_64, f64); impl_dual_state_helmholtz_energy!(PyStateHDD, PyHyperDualDual64, HyperDual, PyDual64); -dual_number!(PyDualVec2, DualVec64<2>, f64); +dual_number!(PyDualVec2, DualSVec64<2>, f64); impl_dual_state_helmholtz_energy!( PyStateHDDVec2, PyHyperDualVec2, - HyperDual, f64>, + HyperDual, f64>, PyDualVec2 ); impl_dual_state_helmholtz_energy!( PyStateHDDVec3, PyHyperDualVec3, - HyperDual, f64>, + HyperDual, f64>, PyDualVec3 ); +impl_dual_state_helmholtz_energy!( + PyStateD2D, + PyDual2Dual64, + Dual2, + PyDual64 +); impl_dual_state_helmholtz_energy!( PyStateD3D, PyDual3Dual64, @@ -243,12 +244,12 @@ impl_dual_state_helmholtz_energy!( impl_dual_state_helmholtz_energy!( PyStateD3DVec2, PyDual3DualVec2, - Dual3, f64>, + Dual3, f64>, PyDualVec2 ); impl_dual_state_helmholtz_energy!( PyStateD3DVec3, PyDual3DualVec3, - Dual3, f64>, + Dual3, f64>, PyDualVec3 ); diff --git a/feos-core/src/state/cache.rs b/feos-core/src/state/cache.rs index f0c41eabd..20f272923 100644 --- a/feos-core/src/state/cache.rs +++ b/feos-core/src/state/cache.rs @@ -45,8 +45,8 @@ impl Cache { let value = f(); self.map.insert(PartialDerivative::Zeroth, value.re); self.map - .insert(PartialDerivative::First(derivative), value.eps[0]); - value.eps[0] + .insert(PartialDerivative::First(derivative), value.eps); + value.eps } } @@ -66,12 +66,12 @@ impl Cache { let value = f(); self.map.insert(PartialDerivative::Zeroth, value.re); self.map - .insert(PartialDerivative::First(derivative), value.v1[0]); + .insert(PartialDerivative::First(derivative), value.v1); self.map.insert( PartialDerivative::SecondMixed(derivative, derivative), - value.v2[0], + value.v2, ); - value.v2[0] + value.v2 } } @@ -91,14 +91,12 @@ impl Cache { let value = f(); self.map.insert(PartialDerivative::Zeroth, value.re); self.map - .insert(PartialDerivative::First(derivative1), value.eps1[0]); + .insert(PartialDerivative::First(derivative1), value.eps1); self.map - .insert(PartialDerivative::First(derivative2), value.eps2[0]); - self.map.insert( - PartialDerivative::SecondMixed(d1, d2), - value.eps1eps2[(0, 0)], - ); - value.eps1eps2[(0, 0)] + .insert(PartialDerivative::First(derivative2), value.eps2); + self.map + .insert(PartialDerivative::SecondMixed(d1, d2), value.eps1eps2); + value.eps1eps2 } } diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index b2cb1b3c3..c215e928e 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -3,9 +3,12 @@ use crate::equation_of_state::EquationOfState; use crate::errors::{EosError, EosResult}; use crate::phase_equilibria::{SolverOptions, Verbosity}; use crate::{DensityInitialization, EosUnit}; -use ndarray::{arr1, arr2, Array1, Array2}; -use num_dual::linalg::{norm, smallest_ev, LU}; -use num_dual::{Dual, Dual3, Dual64, DualNum, DualVec64, HyperDual, StaticVec}; +use nalgebra::{DMatrix, DVector, SVector, SymmetricEigen}; +use ndarray::{arr1, Array1}; +use num_dual::{ + first_derivative, try_first_derivative, try_jacobian, Dual, Dual3, Dual64, DualNum, DualSVec64, + DualVec, HyperDual, +}; use num_traits::{One, Zero}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; @@ -123,16 +126,12 @@ impl State { for i in 1..=max_iter { // calculate residuals and derivatives w.r.t. temperature and density - let [t_dual, rho_dual] = *StaticVec::new_vec([t, rho]) - .map(DualVec64::<2>::from_re) - .derive() - .raw_array(); - let res = critical_point_objective(eos, t_dual, rho_dual, &n)?; - let h = arr2(res.jacobian().raw_data()); - let res = arr1(res.map(|r| r.re()).raw_array()); + let res = |x: SVector, 2>| critical_point_objective(eos, x[0], x[1], &n); + let (res, jac) = try_jacobian(res, SVector::from([t, rho]))?; // calculate Newton step - let mut delta = LU::new(h)?.solve(&res); + let delta = jac.lu().solve(&res); + let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?; // reduce step if necessary if delta[0].abs() > 0.25 * t { @@ -151,13 +150,13 @@ impl State { verbosity, " {:4} | {:14.8e} | {:13.8} | {:12.8}", i, - norm(&res), + res.norm(), t * SIUnit::reference_temperature(), rho * SIUnit::reference_density(), ); // check convergence - if norm(&res) < tol { + if res.norm() < tol { log_result!( verbosity, "Critical point calculation converged in {} step(s)\n", @@ -188,9 +187,9 @@ impl State { options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT); let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let x = StaticVec::new_vec(initial_molefracs.unwrap_or([0.5, 0.5])); + let x = SVector::from(initial_molefracs.unwrap_or([0.5, 0.5])); let max_density = eos - .max_density(Some(&(arr1(x.raw_array()) * SIUnit::reference_moles())))? + .max_density(Some(&(arr1(&x.data.0[0]) * SIUnit::reference_moles())))? .to_reduced(SIUnit::reference_density())?; let mut rho = x * 0.3 * max_density; @@ -209,17 +208,12 @@ impl State { for i in 1..=max_iter { // calculate residuals and derivatives w.r.t. partial densities - let r = StaticVec::new_vec([DualVec64::from_re(rho[0]), DualVec64::from_re(rho[1])]) - .derive(); - let res = critical_point_objective_t(eos, t, r)?; + let res = |rho| critical_point_objective_t(eos, t, rho); + let (res, jac) = try_jacobian(res, rho)?; // calculate Newton step - let h = res.jacobian(); - let res = res.map(|r| r.re); - let mut delta = StaticVec::new_vec([ - h[(1, 1)] * res[0] - h[(0, 1)] * res[1], - h[(0, 0)] * res[1] - h[(1, 0)] * res[0], - ]) / (h[(0, 0)] * h[(1, 1)] - h[(0, 1)] * h[(1, 0)]); + let delta = jac.lu().solve(&res); + let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?; // reduce step if necessary for i in 0..2 { @@ -253,7 +247,7 @@ impl State { eos, t * SIUnit::reference_temperature(), SIUnit::reference_volume(), - &(arr1(rho.raw_array()) * SIUnit::reference_moles()), + &(arr1(&rho.data.0[0]) * SIUnit::reference_moles()), ); } } @@ -279,9 +273,9 @@ impl State { .map(|t| t.to_reduced(SIUnit::reference_temperature())) .transpose()? .unwrap_or(300.0); - let x = StaticVec::new_vec(initial_molefracs.unwrap_or([0.5, 0.5])); + let x = SVector::from(initial_molefracs.unwrap_or([0.5, 0.5])); let max_density = eos - .max_density(Some(&(arr1(x.raw_array()) * SIUnit::reference_moles())))? + .max_density(Some(&(arr1(&x.data.0[0]) * SIUnit::reference_moles())))? .to_reduced(SIUnit::reference_density())?; let mut rho = x * 0.3 * max_density; @@ -301,19 +295,15 @@ impl State { for i in 1..=max_iter { // calculate residuals and derivatives w.r.t. temperature and partial densities - let x = StaticVec::new_vec([ - DualVec64::from_re(t), - DualVec64::from_re(rho[0]), - DualVec64::from_re(rho[1]), - ]) - .derive(); - let r = StaticVec::new_vec([x[1], x[2]]); - let res = critical_point_objective_p(eos, p, x[0], r)?; + let res = |x: SVector, 3>| { + let r = SVector::from([x[1], x[2]]); + critical_point_objective_p(eos, p, x[0], r) + }; + let (res, jac) = try_jacobian(res, SVector::from([t, rho[0], rho[1]]))?; // calculate Newton step - let h = arr2(res.jacobian().raw_data()); - let res = arr1(res.map(|r| r.re).raw_array()); - let mut delta = LU::new(h)?.solve(&res); + let delta = jac.lu().solve(&res); + let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?; // reduce step if necessary if delta[0].abs() > 0.25 * t { @@ -337,14 +327,14 @@ impl State { verbosity, " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", i, - norm(&res), + res.norm(), t * SIUnit::reference_temperature(), rho[0] * SIUnit::reference_density(), rho[1] * SIUnit::reference_density(), ); // check convergence - if norm(&res) < tol { + if res.norm() < tol { log_result!( verbosity, "Critical point calculation converged in {} step(s)\n", @@ -354,7 +344,7 @@ impl State { eos, t * SIUnit::reference_temperature(), SIUnit::reference_volume(), - &(arr1(rho.raw_array()) * SIUnit::reference_moles()), + &(arr1(&rho.data.0[0]) * SIUnit::reference_moles()), ); } } @@ -427,10 +417,11 @@ impl State { for i in 1..=max_iter { // calculate residuals and derivative w.r.t. density - let res = spinodal_objective(eos, Dual64::from(t), Dual64::from(rho).derive(), &n)?; + let (f, df) = + try_first_derivative(|rho| spinodal_objective(eos, t.into(), rho, &n), rho)?; // calculate Newton step - let mut delta = res.re / res.eps[0]; + let mut delta = f / df; // reduce step if necessary if delta.abs() > 0.03 * max_density { @@ -445,12 +436,12 @@ impl State { verbosity, " {:4} | {:14.8e} | {:12.8}", i, - res.re.abs(), + f.abs(), rho * SIUnit::reference_density(), ); // check convergence - if res.re.abs() < tol { + if f.abs() < tol { log_result!( verbosity, "Spinodal calculation converged in {} step(s)\n", @@ -470,20 +461,19 @@ impl State { fn critical_point_objective( eos: &Arc, - temperature: DualVec64<2>, - density: DualVec64<2>, + temperature: DualSVec64<2>, + density: DualSVec64<2>, moles: &Array1, -) -> EosResult, 2>> { +) -> EosResult, 2>> { // calculate second partial derivatives w.r.t. moles let t = HyperDual::from_re(temperature); let v = HyperDual::from_re(density.recip() * moles.sum()); - let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| { + let qij = DMatrix::from_fn(eos.components(), eos.components(), |i, j| { let mut m = moles.mapv(HyperDual::from); - m[i].eps1[0] = DualVec64::one(); - m[j].eps2[0] = DualVec64::one(); + m[i].eps1 = DualSVec64::one(); + m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2[(0, 0)] - + eos.ideal_gas().evaluate(&state).eps1eps2[(0, 0)]) + (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) * (moles[i] * moles[j]).sqrt() }); @@ -493,10 +483,10 @@ fn critical_point_objective( // evaluate third partial derivative w.r.t. s let moles_hd = Array1::from_shape_fn(eos.components(), |i| { Dual3::new( - DualVec64::from(moles[i]), + DualSVec64::from(moles[i]), evec[i] * moles[i].sqrt(), - DualVec64::zero(), - DualVec64::zero(), + DualSVec64::zero(), + DualSVec64::zero(), ) }); let state_s = StateHD::new( @@ -505,24 +495,23 @@ fn critical_point_objective( moles_hd, ); let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(StaticVec::new_vec([eval, res.v3])) + Ok(SVector::from([eval, res.v3])) } fn critical_point_objective_t( eos: &Arc, temperature: f64, - density: StaticVec, 2>, -) -> EosResult, 2>> { + density: SVector, 2>, +) -> EosResult, 2>> { // calculate second partial derivatives w.r.t. moles let t = HyperDual::from(temperature); let v = HyperDual::from(1.0); - let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| { + let qij = DMatrix::from_fn(eos.components(), eos.components(), |i, j| { let mut m = density.map(HyperDual::from_re); - m[i].eps1[0] = DualVec64::one(); - m[j].eps2[0] = DualVec64::one(); + m[i].eps1 = DualSVec64::one(); + m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2[(0, 0)] - + eos.ideal_gas().evaluate(&state).eps1eps2[(0, 0)]) + (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) * (density[i] * density[j]).sqrt() }); @@ -534,31 +523,30 @@ fn critical_point_objective_t( Dual3::new( density[i], evec[i] * density[i].sqrt(), - DualVec64::zero(), - DualVec64::zero(), + DualSVec64::zero(), + DualSVec64::zero(), ) }); let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd); let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(StaticVec::new_vec([eval, res.v3])) + Ok(SVector::from([eval, res.v3])) } fn critical_point_objective_p( eos: &Arc, pressure: f64, - temperature: DualVec64<3>, - density: StaticVec, 2>, -) -> EosResult, 3>> { + temperature: DualSVec64<3>, + density: SVector, 2>, +) -> EosResult, 3>> { // calculate second partial derivatives w.r.t. moles let t = HyperDual::from_re(temperature); let v = HyperDual::from(1.0); - let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| { + let qij = DMatrix::from_fn(eos.components(), eos.components(), |i, j| { let mut m = density.map(HyperDual::from_re); - m[i].eps1[0] = DualVec64::one(); - m[j].eps2[0] = DualVec64::one(); + m[i].eps1 = DualSVec64::one(); + m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2[(0, 0)] - + eos.ideal_gas().evaluate(&state).eps1eps2[(0, 0)]) + (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) * (density[i] * density[j]).sqrt() }); @@ -570,24 +558,22 @@ fn critical_point_objective_p( Dual3::new( density[i], evec[i] * density[i].sqrt(), - DualVec64::zero(), - DualVec64::zero(), + DualSVec64::zero(), + DualSVec64::zero(), ) }); let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd); let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); // calculate pressure - let v = Dual::from(1.0).derive(); - let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); - let state_p = StateHD::new(Dual::from_re(temperature), v, m); - let p = eos.evaluate_residual(&state_p) + eos.ideal_gas().evaluate(&state_p); - - Ok(StaticVec::new_vec([ - eval, - res.v3, - p.eps[0] * temperature + pressure, - ])) + let a = |v| { + let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); + let state_p = StateHD::new(Dual::from_re(temperature), v, m); + eos.evaluate_residual(&state_p) + eos.ideal_gas().evaluate(&state_p) + }; + let (_, p) = first_derivative(a, DualVec::one()); + + Ok(SVector::from([eval, res.v3, p * temperature + pressure])) } fn spinodal_objective( @@ -599,18 +585,41 @@ fn spinodal_objective( // calculate second partial derivatives w.r.t. moles let t = HyperDual::from_re(temperature); let v = HyperDual::from_re(density.recip() * moles.sum()); - let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| { + let qij = DMatrix::from_fn(eos.components(), eos.components(), |i, j| { let mut m = moles.mapv(HyperDual::from); - m[i].eps1[0] = Dual64::one(); - m[j].eps2[0] = Dual64::one(); + m[i].eps1 = Dual64::one(); + m[j].eps2 = Dual64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2[(0, 0)] - + eos.ideal_gas().evaluate(&state).eps1eps2[(0, 0)]) + (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) * (moles[i] * moles[j]).sqrt() }); // calculate smallest eigenvalue of q - let (eval, _) = smallest_ev(qij); + let (eval, _) = smallest_ev_scalar(qij); Ok(eval) } + +fn smallest_ev( + m: DMatrix>, +) -> (DualSVec64, DVector>) { + let eig = SymmetricEigen::new(m); + let (e, ev) = eig + .eigenvalues + .iter() + .zip(eig.eigenvectors.column_iter()) + .reduce(|e1, e2| if e1.0 < e2.0 { e1 } else { e2 }) + .unwrap(); + (*e, ev.into()) +} + +fn smallest_ev_scalar(m: DMatrix) -> (Dual64, DVector) { + let eig = SymmetricEigen::new(m); + let (e, ev) = eig + .eigenvalues + .iter() + .zip(eig.eigenvectors.column_iter()) + .reduce(|e1, e2| if e1.0 < e2.0 { e1 } else { e2 }) + .unwrap(); + (*e, ev.into()) +} diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 2133a0165..ca566add3 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -58,7 +58,7 @@ pub struct StateHD> { pub partial_density: Array1, } -impl> StateHD { +impl + Copy> StateHD { /// Create a new `StateHD` for given temperature volume and moles. pub fn new(temperature: D, volume: D, moles: Array1) -> Self { let total_moles = moles.sum(); @@ -686,9 +686,9 @@ impl State { let mut v = Dual64::from(self.reduced_volume); let mut n = self.reduced_moles.mapv(Dual64::from); match derivative { - Derivative::DT => t = t.derive(), - Derivative::DV => v = v.derive(), - Derivative::DN(i) => n[i] = n[i].derive(), + Derivative::DT => t = t.derivative(), + Derivative::DV => v = v.derivative(), + Derivative::DN(i) => n[i] = n[i].derivative(), } StateHD::new(t, v, n) } @@ -699,9 +699,9 @@ impl State { let mut v = Dual2_64::from(self.reduced_volume); let mut n = self.reduced_moles.mapv(Dual2_64::from); match derivative { - Derivative::DT => t = t.derive(), - Derivative::DV => v = v.derive(), - Derivative::DN(i) => n[i] = n[i].derive(), + Derivative::DT => t = t.derivative(), + Derivative::DV => v = v.derivative(), + Derivative::DN(i) => n[i] = n[i].derivative(), } StateHD::new(t, v, n) } @@ -716,14 +716,14 @@ impl State { let mut v = HyperDual64::from(self.reduced_volume); let mut n = self.reduced_moles.mapv(HyperDual64::from); match derivative1 { - Derivative::DT => t = t.derive1(), - Derivative::DV => v = v.derive1(), - Derivative::DN(i) => n[i] = n[i].derive1(), + Derivative::DT => t = t.derivative1(), + Derivative::DV => v = v.derivative1(), + Derivative::DN(i) => n[i] = n[i].derivative1(), } match derivative2 { - Derivative::DT => t = t.derive2(), - Derivative::DV => v = v.derive2(), - Derivative::DN(i) => n[i] = n[i].derive2(), + Derivative::DT => t = t.derivative2(), + Derivative::DV => v = v.derivative2(), + Derivative::DN(i) => n[i] = n[i].derivative2(), } StateHD::new(t, v, n) } @@ -734,9 +734,9 @@ impl State { let mut v = Dual3_64::from(self.reduced_volume); let mut n = self.reduced_moles.mapv(Dual3_64::from); match derivative { - Derivative::DT => t = t.derive(), - Derivative::DV => v = v.derive(), - Derivative::DN(i) => n[i] = n[i].derive(), + Derivative::DT => t = t.derivative(), + Derivative::DV => v = v.derivative(), + Derivative::DN(i) => n[i] = n[i].derivative(), }; StateHD::new(t, v, n) } diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index baf749718..dcaee2010 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -47,18 +47,18 @@ impl State { } PartialDerivative::First(v) => { let new_state = self.derive1(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps[0] + -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps * (SIUnit::reference_energy() / v.reference()) } PartialDerivative::Second(v) => { let new_state = self.derive2(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2[0] + -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 * (SIUnit::reference_energy() / (v.reference() * v.reference())) } PartialDerivative::SecondMixed(v1, v2) => { let new_state = self.derive2_mixed(v1, v2); -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - .eps1eps2[(0, 0)] + .eps1eps2 * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) } PartialDerivative::Third(v) => { @@ -123,20 +123,19 @@ impl State { } PartialDerivative::First(v) => { let new_state = self.derive1(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps[0] + (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_energy() / v.reference() } PartialDerivative::Second(v) => { let new_state = self.derive2(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v2[0] + (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v2 * SIUnit::reference_energy() / (v.reference() * v.reference()) } PartialDerivative::SecondMixed(v1, v2) => { let new_state = self.derive2_mixed(v1, v2); (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps1eps2 - [(0, 0)] * SIUnit::reference_energy() / (v1.reference() * v2.reference()) } @@ -541,13 +540,12 @@ impl State { let ig = self.eos.ideal_gas(); res.push(( ig.to_string(), - -(ig.evaluate(&new_state) * new_state.temperature).eps[0] - * SIUnit::reference_pressure(), + -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), )); for (s, v) in contributions { res.push(( s, - -(v * new_state.temperature).eps[0] * SIUnit::reference_pressure(), + -(v * new_state.temperature).eps * SIUnit::reference_pressure(), )); } res @@ -561,13 +559,13 @@ impl State { let ig = self.eos.ideal_gas(); res.push(( ig.to_string(), - (ig.evaluate(&new_state) * new_state.temperature).eps[0] + (ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); for (s, v) in contributions { res.push(( s, - (v * new_state.temperature).eps[0] * SIUnit::reference_molar_energy(), + (v * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); } res diff --git a/feos-dft/CHANGELOG.md b/feos-dft/CHANGELOG.md index 6f542968a..9a8fd9824 100644 --- a/feos-dft/CHANGELOG.md +++ b/feos-dft/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Packaging +- Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) ## [0.4.1] - 2023-03-20 ### Added diff --git a/feos-dft/Cargo.toml b/feos-dft/Cargo.toml index 02e7145d4..f9f1b7af2 100644 --- a/feos-dft/Cargo.toml +++ b/feos-dft/Cargo.toml @@ -18,9 +18,10 @@ features = [ "rayon" ] [dependencies] quantity = { version = "0.6", features = ["linalg"] } -num-dual = "0.6" +num-dual = "0.7" feos-core = { version = "0.4", path = "../feos-core" } ndarray = "0.15" +nalgebra = "0.32" rustdct = "0.7" rustfft = "6.0" ang = "0.6" diff --git a/feos-dft/src/convolver/mod.rs b/feos-dft/src/convolver/mod.rs index 5cdaa56d2..f609916cc 100644 --- a/feos-dft/src/convolver/mod.rs +++ b/feos-dft/src/convolver/mod.rs @@ -40,7 +40,7 @@ pub(crate) struct BulkConvolver { weight_constants: Vec>, } -impl> BulkConvolver { +impl + Copy + Send + Sync> BulkConvolver { pub(crate) fn new(weight_functions: Vec>) -> Arc> { let weight_constants = weight_functions .into_iter() @@ -50,7 +50,7 @@ impl> BulkConvolver { } } -impl> Convolver for BulkConvolver +impl + Copy + Send + Sync> Convolver for BulkConvolver where Array2: Dot, Output = Array1>, { diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 2c037060a..430b6e608 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -51,7 +51,7 @@ impl MolarWeight for DFT { } struct DefaultIdealGasContribution(); -impl> IdealGasContributionDual for DefaultIdealGasContribution { +impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { Array1::zeros(components) } @@ -80,7 +80,7 @@ impl EquationOfState for DFT { unreachable!() } - fn evaluate_residual>(&self, state: &StateHD) -> D + fn evaluate_residual + Copy>(&self, state: &StateHD) -> D where dyn HelmholtzEnergy: HelmholtzEnergyDual, { @@ -91,7 +91,7 @@ impl EquationOfState for DFT { + self.ideal_chain_contribution().helmholtz_energy(state) } - fn evaluate_residual_contributions>( + fn evaluate_residual_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(String, D)> @@ -278,7 +278,7 @@ impl DFT { convolver: &Arc>, ) -> EosResult> where - N: DualNum + ScalarOperand, + N: DualNum + Copy + ScalarOperand, dyn FunctionalContribution: FunctionalContributionDual, D: Dimension, D::Larger: Dimension, @@ -318,7 +318,7 @@ impl DFT { D: Dimension, D::Larger: Dimension, { - let temperature_dual = Dual64::from(temperature).derive(); + let temperature_dual = Dual64::from(temperature).derivative(); let mut helmholtz_energy_density = self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; match contributions { @@ -328,7 +328,7 @@ impl DFT { Contributions::ResidualNpt|Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), Contributions::ResidualNvt => (), } - Ok(helmholtz_energy_density.mapv(|f| -f.eps[0])) + Ok(helmholtz_energy_density.mapv(|f| -f.eps)) } /// Calculate the individual contributions to the entropy density. @@ -346,7 +346,7 @@ impl DFT { ::Larger: Dimension, { let density_dual = density.mapv(Dual64::from); - let temperature_dual = Dual64::from(temperature).derive(); + let temperature_dual = Dual64::from(temperature).derivative(); let weighted_densities = convolver.weighted_densities(&density_dual); let functional_contributions = self.contributions(); let mut helmholtz_energy_density: Vec> = @@ -370,7 +370,7 @@ impl DFT { } Ok(helmholtz_energy_density .iter() - .map(|v| v.mapv(|f| -(f * temperature_dual).eps[0])) + .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) .collect()) } @@ -389,7 +389,7 @@ impl DFT { D: Dimension, D::Larger: Dimension, { - let temperature_dual = Dual64::from(temperature).derive(); + let temperature_dual = Dual64::from(temperature).derivative(); let mut helmholtz_energy_density_dual = self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; match contributions { @@ -400,7 +400,7 @@ impl DFT { Contributions::ResidualNvt => (), } let helmholtz_energy_density = helmholtz_energy_density_dual - .mapv(|f| f.re - f.eps[0] * temperature) + .mapv(|f| f.re - f.eps * temperature) + (external_potential * density).sum_axis(Axis(0)) * temperature; Ok(helmholtz_energy_density) } @@ -452,7 +452,7 @@ impl DFT { D: Dimension, D::Larger: Dimension, { - let temperature_dual = Dual64::from(temperature).derive(); + let temperature_dual = Dual64::from(temperature).derivative(); let density_dual = density.mapv(Dual64::from); let weighted_densities = convolver.weighted_densities(&density_dual); let contributions = self.contributions(); diff --git a/feos-dft/src/functional_contribution.rs b/feos-dft/src/functional_contribution.rs index 67541246d..89bbf8499 100644 --- a/feos-dft/src/functional_contribution.rs +++ b/feos-dft/src/functional_contribution.rs @@ -34,16 +34,17 @@ macro_rules! impl_helmholtz_energy { impl_helmholtz_energy!(f64); impl_helmholtz_energy!(Dual64); -impl_helmholtz_energy!(Dual, f64>); +impl_helmholtz_energy!(Dual, f64>); impl_helmholtz_energy!(HyperDual64); impl_helmholtz_energy!(Dual2_64); impl_helmholtz_energy!(Dual3_64); impl_helmholtz_energy!(HyperDual); -impl_helmholtz_energy!(HyperDual, f64>); -impl_helmholtz_energy!(HyperDual, f64>); +impl_helmholtz_energy!(HyperDual, f64>); +impl_helmholtz_energy!(HyperDual, f64>); +impl_helmholtz_energy!(Dual2); impl_helmholtz_energy!(Dual3); -impl_helmholtz_energy!(Dual3, f64>); -impl_helmholtz_energy!(Dual3, f64>); +impl_helmholtz_energy!(Dual3, f64>); +impl_helmholtz_energy!(Dual3, f64>); /// Individual functional contribution that can /// be evaluated using generalized (hyper) dual numbers. @@ -76,16 +77,17 @@ pub trait FunctionalContribution: FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual> - + FunctionalContributionDual, f64>> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual> + FunctionalContributionDual> - + FunctionalContributionDual, f64>> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + Display + Sync + Send @@ -102,14 +104,12 @@ pub trait FunctionalContribution: let mut phi = Array::zeros(weighted_densities.raw_dim().remove_axis(Axis(0))); for i in 0..wd.shape()[0] { - wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps[0] = 1.0); + wd.index_axis_mut(Axis(0), i).map_inplace(|x| x.eps = 1.0); phi = self.calculate_helmholtz_energy_density(t, wd.view())?; first_partial_derivative .index_axis_mut(Axis(0), i) - .assign(&phi.mapv(|p| p.eps[0])); - wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps[0] = 0.0); + .assign(&phi.mapv(|p| p.eps)); + wd.index_axis_mut(Axis(0), i).map_inplace(|x| x.eps = 0.0); } helmholtz_energy_density.assign(&phi.mapv(|p| p.re)); Ok(()) @@ -128,13 +128,13 @@ pub trait FunctionalContribution: for i in 0..wd.shape()[0] { wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps[0] = Dual64::one()); + .map_inplace(|x| x.eps = Dual::one()); phi = self.calculate_helmholtz_energy_density(t, wd.view())?; first_partial_derivative .index_axis_mut(Axis(0), i) - .assign(&phi.mapv(|p| p.eps[0])); + .assign(&phi.mapv(|p| p.eps)); wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps[0] = Dual64::zero()); + .map_inplace(|x| x.eps = Dual::zero()); } helmholtz_energy_density.assign(&phi.mapv(|p| p.re)); Ok(()) @@ -153,13 +153,11 @@ pub trait FunctionalContribution: let mut phi = Array::zeros(weighted_densities.raw_dim().remove_axis(Axis(0))); for i in 0..wd.shape()[0] { - wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps1[0] = 1.0); + wd.index_axis_mut(Axis(0), i).map_inplace(|x| x.eps1 = 1.0); for j in 0..=i { - wd.index_axis_mut(Axis(0), j) - .map_inplace(|x| x.eps2[0] = 1.0); + wd.index_axis_mut(Axis(0), j).map_inplace(|x| x.eps2 = 1.0); phi = self.calculate_helmholtz_energy_density(t, wd.view())?; - let p = phi.mapv(|p| p.eps1eps2[(0, 0)]); + let p = phi.mapv(|p| p.eps1eps2); second_partial_derivative .index_axis_mut(Axis(0), i) .index_axis_mut(Axis(0), j) @@ -170,14 +168,12 @@ pub trait FunctionalContribution: .index_axis_mut(Axis(0), i) .assign(&p); } - wd.index_axis_mut(Axis(0), j) - .map_inplace(|x| x.eps2[0] = 0.0); + wd.index_axis_mut(Axis(0), j).map_inplace(|x| x.eps2 = 0.0); } first_partial_derivative .index_axis_mut(Axis(0), i) - .assign(&phi.mapv(|p| p.eps1[0])); - wd.index_axis_mut(Axis(0), i) - .map_inplace(|x| x.eps1[0] = 0.0); + .assign(&phi.mapv(|p| p.eps1)); + wd.index_axis_mut(Axis(0), i).map_inplace(|x| x.eps1 = 0.0); } helmholtz_energy_density.assign(&phi.mapv(|p| p.re)); Ok(()) @@ -188,16 +184,17 @@ impl FunctionalContribution for T where T: FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual + FunctionalContributionDual> - + FunctionalContributionDual, f64>> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual> + FunctionalContributionDual> - + FunctionalContributionDual, f64>> - + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + + FunctionalContributionDual, f64>> + Display + Sync + Send diff --git a/feos-dft/src/ideal_chain_contribution.rs b/feos-dft/src/ideal_chain_contribution.rs index 9eece53aa..39847458e 100644 --- a/feos-dft/src/ideal_chain_contribution.rs +++ b/feos-dft/src/ideal_chain_contribution.rs @@ -19,7 +19,7 @@ impl IdealChainContribution { } } -impl> HelmholtzEnergyDual for IdealChainContribution { +impl + Copy> HelmholtzEnergyDual for IdealChainContribution { fn helmholtz_energy(&self, state: &StateHD) -> D { let segments = self.component_index.len(); if self.component_index[segments - 1] + 1 != segments { diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index bdea179f2..44dc07a9d 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -3,20 +3,15 @@ use super::functional_contribution::FunctionalContribution; use super::weight_functions::WeightFunctionInfo; use feos_core::{Contributions, EosResult, EosUnit, EquationOfState, PhaseEquilibrium}; use ndarray::*; -use num_dual::HyperDual64; -// use quantity::{SIArray2, SINumber}; +use num_dual::Dual2_64; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; use std::ops::AddAssign; -impl WeightFunctionInfo { +impl WeightFunctionInfo { fn pdgt_weight_constants(&self) -> (Array2, Array2, Array2) { - let k = HyperDual64::from(0.0).derive1().derive2(); + let k = Dual2_64::from(0.0).derivative(); let w = self.weight_constants(k, 1); - ( - w.mapv(|w| w.re), - w.mapv(|w| -w.eps1[0]), - w.mapv(|w| -0.5 * w.eps1eps2[(0, 0)]), - ) + (w.mapv(|w| w.re), w.mapv(|w| -w.v1), w.mapv(|w| -0.5 * w.v2)) } } @@ -32,7 +27,7 @@ impl dyn FunctionalContribution { influence_matrix: Option<&mut Array3>, ) -> EosResult<()> { // calculate weighted densities - let weight_functions = self.weight_functions_pdgt(HyperDual64::from(temperature)); + let weight_functions = self.weight_functions_pdgt(Dual2_64::from(temperature)); let (w0, w1, w2) = weight_functions.pdgt_weight_constants(); let weighted_densities = w0.dot(density); diff --git a/feos-dft/src/profile.rs b/feos-dft/src/profile.rs index 86ab4479e..1c8ae407e 100644 --- a/feos-dft/src/profile.rs +++ b/feos-dft/src/profile.rs @@ -485,7 +485,7 @@ where let functional_contributions = self.dft.contributions(); let weight_functions: Vec> = functional_contributions .iter() - .map(|c| c.weight_functions(Dual64::from(t).derive())) + .map(|c| c.weight_functions(Dual64::from(t).derivative())) .collect(); let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); @@ -518,7 +518,7 @@ where let functional_contributions = self.dft.contributions(); let weight_functions: Vec> = functional_contributions .iter() - .map(|c| c.weight_functions(Dual64::from(t).derive())) + .map(|c| c.weight_functions(Dual64::from(t).derivative())) .collect(); let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); @@ -617,7 +617,7 @@ where let functional_contributions = self.dft.contributions(); let weight_functions: Vec> = functional_contributions .iter() - .map(|c| c.weight_functions(Dual64::from(t).derive())) + .map(|c| c.weight_functions(Dual64::from(t).derivative())) .collect(); let convolver: Arc> = ConvolverFFT::plan(&self.grid, &weight_functions, None); @@ -638,11 +638,11 @@ where let x = (self.bulk.partial_molar_volume(Contributions::Total) * self.bulk.dp_dt(Contributions::Total)) .to_reduced(SIUnit::reference_molar_entropy())?; - let mut lhs = dfdrhodt.mapv(|d| d.eps[0]); + let mut lhs = dfdrhodt.mapv(|d| d.eps); lhs.outer_iter_mut() .zip(dfdrhodt_bulk.into_iter()) .zip(x.into_iter()) - .for_each(|((mut lhs, d), x)| lhs -= d.eps[0] - x); + .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); lhs.outer_iter_mut() .zip(rho.outer_iter()) .zip(rho_bulk.into_iter()) diff --git a/feos-dft/src/solver.rs b/feos-dft/src/solver.rs index 3da93b1f8..47bd4851d 100644 --- a/feos-dft/src/solver.rs +++ b/feos-dft/src/solver.rs @@ -1,8 +1,8 @@ use crate::{DFTProfile, HelmholtzEnergyFunctional, WeightFunction, WeightFunctionShape}; use feos_core::{log_iter, log_result, EosError, EosResult, EosUnit, Verbosity}; +use nalgebra::{DMatrix, DVector}; use ndarray::prelude::*; use ndarray::RemoveAxis; -use num_dual::linalg::LU; use petgraph::graph::Graph; use petgraph::visit::EdgeRef; use petgraph::Directed; @@ -398,7 +398,7 @@ where } // calculate alpha - r = Array::from_shape_fn((m + 1, m + 1), |(i, j)| match (i == m, j == m) { + r = DMatrix::from_fn(m + 1, m + 1, |i, j| match (i == m, j == m) { (false, false) => { let (resi, resi_bulk, _) = &resm[i]; let (resj, resj_bulk, _) = &resm[j]; @@ -407,9 +407,10 @@ where (true, true) => 0.0, _ => 1.0, }); - alpha = Array::zeros(m + 1); + alpha = DVector::zeros(m + 1); alpha[m] = 1.0; - alpha = LU::new(r)?.solve(&alpha); + let alpha = r.lu().solve(&alpha); + let alpha = alpha.ok_or(EosError::Error("alpha matrix is not invertible".into()))?; // update solution rho.fill(0.0); diff --git a/feos-dft/src/weight_functions.rs b/feos-dft/src/weight_functions.rs index 871a0b59d..4f4c0665e 100644 --- a/feos-dft/src/weight_functions.rs +++ b/feos-dft/src/weight_functions.rs @@ -15,7 +15,7 @@ pub struct WeightFunction { pub shape: WeightFunctionShape, } -impl> WeightFunction { +impl + Copy> WeightFunction { /// Create a new weight function without prefactor pub fn new_unscaled(kernel_radius: Array1, shape: WeightFunctionShape) -> Self { Self { @@ -276,7 +276,7 @@ impl WeightFunctionInfo { } } -impl> WeightFunctionInfo { +impl + Copy> WeightFunctionInfo { /// calculates the matrix of weight constants for this set of weighted densities pub fn weight_constants(&self, k: T, dimensions: usize) -> Array2 { let segments = self.component_index.len(); diff --git a/src/association/dft.rs b/src/association/dft.rs index 05c92b378..5a8838a2c 100644 --- a/src/association/dft.rs +++ b/src/association/dft.rs @@ -12,7 +12,7 @@ pub const N0_CUTOFF: f64 = 1e-9; impl FunctionalContributionDual for Association

where - N: DualNum + ScalarOperand, + N: DualNum + Copy + ScalarOperand, P: HardSphereProperties, { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { @@ -111,7 +111,7 @@ where impl Association

{ pub fn calculate_helmholtz_energy_density< - N: DualNum + ScalarOperand, + N: DualNum + Copy + ScalarOperand, S: Data, >( &self, @@ -183,7 +183,10 @@ impl Association

{ } } - fn helmholtz_energy_density_ab_analytic + ScalarOperand, S: Data>( + fn helmholtz_energy_density_ab_analytic< + N: DualNum + Copy + ScalarOperand, + S: Data, + >( &self, temperature: N, rho0: &Array2, @@ -217,7 +220,10 @@ impl Association

{ rhoa * xa.mapv(f) + rhob * xb.mapv(f) } - fn helmholtz_energy_density_cc_analytic + ScalarOperand, S: Data>( + fn helmholtz_energy_density_cc_analytic< + N: DualNum + Copy + ScalarOperand, + S: Data, + >( &self, temperature: N, rho0: &Array2, diff --git a/src/association/mod.rs b/src/association/mod.rs index 8dfcbc9de..afecb2731 100644 --- a/src/association/mod.rs +++ b/src/association/mod.rs @@ -212,7 +212,7 @@ impl Association

{ res } - fn association_strength>( + fn association_strength + Copy>( &self, temperature: D, diameter: &Array1, @@ -241,7 +241,7 @@ impl Association

{ } } -impl + ScalarOperand, P: HardSphereProperties> HelmholtzEnergyDual +impl + Copy + ScalarOperand, P: HardSphereProperties> HelmholtzEnergyDual for Association

{ fn helmholtz_energy(&self, state: &StateHD) -> D { @@ -305,7 +305,11 @@ impl

fmt::Display for Association

{ } impl Association

{ - fn helmholtz_energy_ab_analytic>(&self, state: &StateHD, delta: D) -> D { + fn helmholtz_energy_ab_analytic + Copy>( + &self, + state: &StateHD, + delta: D, + ) -> D { let a = &self.association_parameters; // site densities @@ -322,7 +326,11 @@ impl Association

{ (rhoa * (xa.ln() - xa * 0.5 + 0.5) + rhob * (xb.ln() - xb * 0.5 + 0.5)) * state.volume } - fn helmholtz_energy_cc_analytic>(&self, state: &StateHD, delta: D) -> D { + fn helmholtz_energy_cc_analytic + Copy>( + &self, + state: &StateHD, + delta: D, + ) -> D { let a = &self.association_parameters; // site density @@ -337,7 +345,7 @@ impl Association

{ #[allow(clippy::too_many_arguments)] fn helmholtz_energy_density_cross_association< - D: DualNum + ScalarOperand, + D: DualNum + Copy + ScalarOperand, S: Data, >( rho: &ArrayBase, @@ -396,7 +404,7 @@ impl Association

{ Ok((rho * x_dual.mapv(f)).sum()) } - fn newton_step_cross_association + ScalarOperand, S: Data>( + fn newton_step_cross_association + Copy + ScalarOperand, S: Data>( x: &mut Array1, delta_ab: &Array2, delta_cc: &Array2, @@ -541,11 +549,11 @@ mod tests_gc_pcsaft { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); } @@ -561,11 +569,11 @@ mod tests_gc_pcsaft { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); } @@ -583,11 +591,11 @@ mod tests_gc_pcsaft { .unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), moles.mapv(Dual64::from_re), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -26.105606376765632 * PASCAL, max_relative = 1e-10); } } diff --git a/src/gc_pcsaft/dft/dispersion.rs b/src/gc_pcsaft/dft/dispersion.rs index 4eade834f..f9baea129 100644 --- a/src/gc_pcsaft/dft/dispersion.rs +++ b/src/gc_pcsaft/dft/dispersion.rs @@ -24,7 +24,9 @@ impl AttractiveFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for AttractiveFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual + for AttractiveFunctional +{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; diff --git a/src/gc_pcsaft/dft/hard_chain.rs b/src/gc_pcsaft/dft/hard_chain.rs index b7dc74793..c3552821f 100644 --- a/src/gc_pcsaft/dft/hard_chain.rs +++ b/src/gc_pcsaft/dft/hard_chain.rs @@ -23,7 +23,7 @@ impl ChainFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for ChainFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for ChainFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let d = p.hs_diameter(temperature); diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index e8e502d65..2b0f43b38 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -128,7 +128,7 @@ impl HardSphereProperties for GcPcSaftFunctionalParameters { MonomerShape::Heterosegmented([m.clone(), m.clone(), m.clone(), m], &self.component_index) } - fn hs_diameter>(&self, temperature: D) -> Array1 { + fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { let ti = temperature.recip() * -3.0; Array1::from_shape_fn(self.sigma.len(), |i| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] diff --git a/src/gc_pcsaft/eos/dispersion.rs b/src/gc_pcsaft/eos/dispersion.rs index a104b35c7..2992a5fa0 100644 --- a/src/gc_pcsaft/eos/dispersion.rs +++ b/src/gc_pcsaft/eos/dispersion.rs @@ -66,7 +66,7 @@ pub struct Dispersion { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Dispersion { +impl + Copy> HelmholtzEnergyDual for Dispersion { fn helmholtz_energy(&self, state: &StateHD) -> D { // auxiliary variables let p = &self.parameters; @@ -151,11 +151,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -2.846724434944439 * PASCAL, max_relative = 1e-10); } @@ -173,11 +173,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -5.432173507270732 * PASCAL, max_relative = 1e-10); } } diff --git a/src/gc_pcsaft/eos/hard_chain.rs b/src/gc_pcsaft/eos/hard_chain.rs index 419ac92bc..5d17e4548 100644 --- a/src/gc_pcsaft/eos/hard_chain.rs +++ b/src/gc_pcsaft/eos/hard_chain.rs @@ -10,7 +10,7 @@ pub struct HardChain { pub parameters: Arc, } -impl> HelmholtzEnergyDual for HardChain { +impl + Copy> HelmholtzEnergyDual for HardChain { fn helmholtz_energy(&self, state: &StateHD) -> D { // temperature dependent segment diameter let diameter = self.parameters.hs_diameter(state.temperature); @@ -66,11 +66,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!( pressure, -7.991735636207462e-1 * PASCAL, @@ -92,11 +92,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -1.2831486124723626 * PASCAL, max_relative = 1e-10); } } diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index eec50fea4..67be9e25c 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -139,11 +139,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, 1.5285037907989527 * PASCAL, max_relative = 1e-10); } @@ -159,11 +159,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, 2.3168212018200243 * PASCAL, max_relative = 1e-10); } @@ -179,11 +179,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); } @@ -200,11 +200,11 @@ mod test { let moles = (1.5 * MOL).to_reduced(EosUnit::reference_moles()).unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), arr1(&[Dual64::from_re(moles)]), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); } @@ -222,11 +222,11 @@ mod test { .unwrap(); let state = StateHD::new( Dual64::from_re(temperature), - Dual64::from_re(volume).derive(), + Dual64::from_re(volume).derivative(), moles.mapv(Dual64::from_re), ); let pressure = - -contrib.helmholtz_energy(&state).eps[0] * temperature * EosUnit::reference_pressure(); + -contrib.helmholtz_energy(&state).eps * temperature * EosUnit::reference_pressure(); assert_relative_eq!(pressure, -26.105606376765632 * PASCAL, max_relative = 1e-10); } } diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index efc5185bf..e71bca0b0 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -293,7 +293,7 @@ impl HardSphereProperties for GcPcSaftEosParameters { MonomerShape::Heterosegmented([m.clone(), m.clone(), m.clone(), m], &self.component_index) } - fn hs_diameter>(&self, temperature: D) -> Array1 { + fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { let ti = temperature.recip() * -3.0; Array1::from_shape_fn(self.sigma.len(), |i| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] diff --git a/src/gc_pcsaft/eos/polar.rs b/src/gc_pcsaft/eos/polar.rs index e72dc2f37..b36f47b59 100644 --- a/src/gc_pcsaft/eos/polar.rs +++ b/src/gc_pcsaft/eos/polar.rs @@ -33,7 +33,7 @@ pub const CD: [[f64; 3]; 4] = [ pub const PI_SQ_43: f64 = 4.0 * PI * FRAC_PI_3; -fn pair_integral_ij>(mij1: f64, mij2: f64, eta: D, eps_ij_t: D) -> D { +fn pair_integral_ij + Copy>(mij1: f64, mij2: f64, eta: D, eps_ij_t: D) -> D { let eta2 = eta * eta; let etas = [D::one(), eta, eta2, eta2 * eta, eta2 * eta2]; (0..AD.len()) @@ -45,7 +45,7 @@ fn pair_integral_ij>(mij1: f64, mij2: f64, eta: D, eps_ij_t: D) .sum() } -fn triplet_integral_ijk>(mijk1: f64, mijk2: f64, eta: D) -> D { +fn triplet_integral_ijk + Copy>(mijk1: f64, mijk2: f64, eta: D) -> D { let eta2 = eta * eta; let etas = [D::one(), eta, eta2, eta2 * eta]; (0..CD.len()) @@ -118,7 +118,7 @@ impl Dipole { } } -impl> HelmholtzEnergyDual for Dipole { +impl + Copy> HelmholtzEnergyDual for Dipole { fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; let ndipole = p.dipole_comp.len(); diff --git a/src/hard_sphere/dft.rs b/src/hard_sphere/dft.rs index b261b41dd..c886e2c95 100644 --- a/src/hard_sphere/dft.rs +++ b/src/hard_sphere/dft.rs @@ -84,7 +84,7 @@ impl

FMTContribution

{ } } -impl> FunctionalContributionDual +impl + Copy> FunctionalContributionDual for FMTContribution

{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { diff --git a/src/hard_sphere/mod.rs b/src/hard_sphere/mod.rs index 92d96e7a2..51a9da45d 100644 --- a/src/hard_sphere/mod.rs +++ b/src/hard_sphere/mod.rs @@ -31,7 +31,7 @@ pub trait HardSphereProperties { fn monomer_shape>(&self, temperature: D) -> MonomerShape; /// The temperature dependent hard-sphere diameters of every segment. - fn hs_diameter>(&self, temperature: D) -> Array1; + fn hs_diameter + Copy>(&self, temperature: D) -> Array1; /// For every segment, the index of the component that it is on. fn component_index(&self) -> Cow> { @@ -55,7 +55,7 @@ pub trait HardSphereProperties { } /// The packing fractions $\zeta_k$. - fn zeta, const N: usize>( + fn zeta + Copy, const N: usize>( &self, temperature: D, partial_density: &Array1, @@ -77,7 +77,7 @@ pub trait HardSphereProperties { } /// The fraction $\frac{\zeta_2}{\zeta_3}$ evaluated in a way to avoid a division by 0 when the density is 0. - fn zeta_23>(&self, temperature: D, molefracs: &Array1) -> D { + fn zeta_23 + Copy>(&self, temperature: D, molefracs: &Array1) -> D { let component_index = self.component_index(); let geometry_coefficients = self.geometry_coefficients(temperature); let diameter = self.hs_diameter(temperature); @@ -116,7 +116,7 @@ impl

HardSphere

{ } } -impl, P: HardSphereProperties> HelmholtzEnergyDual for HardSphere

{ +impl + Copy, P: HardSphereProperties> HelmholtzEnergyDual for HardSphere

{ fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; let zeta = p.zeta(state.temperature, &state.partial_density, [0, 1, 2, 3]); diff --git a/src/pcsaft/dft/dispersion.rs b/src/pcsaft/dft/dispersion.rs index 05908daac..f3d7c7181 100644 --- a/src/pcsaft/dft/dispersion.rs +++ b/src/pcsaft/dft/dispersion.rs @@ -28,7 +28,7 @@ impl AttractiveFunctional { } } -fn att_weight_functions + ScalarOperand>( +fn att_weight_functions + Copy + ScalarOperand>( p: &PcSaftParameters, psi: f64, temperature: N, @@ -40,7 +40,9 @@ fn att_weight_functions + ScalarOperand>( ) } -impl + ScalarOperand> FunctionalContributionDual for AttractiveFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual + for AttractiveFunctional +{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { att_weight_functions(&self.parameters, PSI_DFT, temperature) } diff --git a/src/pcsaft/dft/hard_chain.rs b/src/pcsaft/dft/hard_chain.rs index df8d2013a..7119e2e40 100644 --- a/src/pcsaft/dft/hard_chain.rs +++ b/src/pcsaft/dft/hard_chain.rs @@ -20,7 +20,7 @@ impl ChainFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for ChainFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for ChainFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let d = p.hs_diameter(temperature); diff --git a/src/pcsaft/dft/polar.rs b/src/pcsaft/dft/polar.rs index 606e77c4d..efb20adf7 100644 --- a/src/pcsaft/dft/polar.rs +++ b/src/pcsaft/dft/polar.rs @@ -8,7 +8,7 @@ use ndarray::*; use num_dual::DualNum; use std::f64::consts::{FRAC_PI_3, PI}; -pub(super) fn calculate_helmholtz_energy_density_polar + ScalarOperand>( +pub(super) fn calculate_helmholtz_energy_density_polar + Copy + ScalarOperand>( parameters: &PcSaftParameters, temperature: N, density: ArrayView2, @@ -38,7 +38,7 @@ pub(super) fn calculate_helmholtz_energy_density_polar + ScalarO Ok(phi) } -pub fn pair_integral_ij + ScalarOperand>( +pub fn pair_integral_ij + Copy + ScalarOperand>( mij1: f64, mij2: f64, eta: &Array1, @@ -93,7 +93,7 @@ fn triplet_integral_ijk_dq + ScalarOperand>( integral } -fn phi_polar_dipole + ScalarOperand>( +fn phi_polar_dipole + Copy + ScalarOperand>( p: &PcSaftParameters, temperature: N, density: ArrayView2, @@ -181,7 +181,7 @@ fn phi_polar_dipole + ScalarOperand>( Ok(result) } -fn phi_polar_quadrupole + ScalarOperand>( +fn phi_polar_quadrupole + Copy + ScalarOperand>( p: &PcSaftParameters, temperature: N, density: ArrayView2, @@ -269,7 +269,7 @@ fn phi_polar_quadrupole + ScalarOperand>( Ok(result) } -fn phi_polar_dipole_quadrupole + ScalarOperand>( +fn phi_polar_dipole_quadrupole + Copy + ScalarOperand>( p: &PcSaftParameters, temperature: N, density: ArrayView2, diff --git a/src/pcsaft/dft/pure_saft_functional.rs b/src/pcsaft/dft/pure_saft_functional.rs index 801a079a8..d8b0291a1 100644 --- a/src/pcsaft/dft/pure_saft_functional.rs +++ b/src/pcsaft/dft/pure_saft_functional.rs @@ -35,7 +35,9 @@ impl PureFMTAssocFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for PureFMTAssocFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual + for PureFMTAssocFunctional +{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let r = self.parameters.hs_diameter(temperature) * 0.5; WeightFunctionInfo::new(arr1(&[0]), false).extend( @@ -154,7 +156,7 @@ impl PureChainFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for PureChainFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for PureChainFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); WeightFunctionInfo::new(arr1(&[0]), true) @@ -206,7 +208,7 @@ impl PureAttFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for PureAttFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for PureAttFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); const PSI: f64 = 1.3862; // Homosegmented DFT (Sauer2017) diff --git a/src/pcsaft/eos/dispersion.rs b/src/pcsaft/eos/dispersion.rs index a8a4f0c9d..416778685 100644 --- a/src/pcsaft/eos/dispersion.rs +++ b/src/pcsaft/eos/dispersion.rs @@ -65,7 +65,7 @@ pub struct Dispersion { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Dispersion { +impl + Copy> HelmholtzEnergyDual for Dispersion { fn helmholtz_energy(&self, state: &StateHD) -> D { // auxiliary variables let n = self.parameters.m.len(); diff --git a/src/pcsaft/eos/hard_chain.rs b/src/pcsaft/eos/hard_chain.rs index ea604e26d..a046c893c 100644 --- a/src/pcsaft/eos/hard_chain.rs +++ b/src/pcsaft/eos/hard_chain.rs @@ -10,7 +10,7 @@ pub struct HardChain { pub parameters: Arc, } -impl> HelmholtzEnergyDual for HardChain { +impl + Copy> HelmholtzEnergyDual for HardChain { fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; let d = self.parameters.hs_diameter(state.temperature); diff --git a/src/pcsaft/eos/polar.rs b/src/pcsaft/eos/polar.rs index b94df9087..9efe1c428 100644 --- a/src/pcsaft/eos/polar.rs +++ b/src/pcsaft/eos/polar.rs @@ -127,7 +127,7 @@ impl MeanSegmentNumbers { } } -fn pair_integral_ij>( +fn pair_integral_ij + Copy>( mij1: f64, mij2: f64, etas: &[D], @@ -144,13 +144,18 @@ fn pair_integral_ij>( .sum() } -fn triplet_integral_ijk>(mijk1: f64, mijk2: f64, etas: &[D], c: &[[f64; 3]]) -> D { +fn triplet_integral_ijk + Copy>( + mijk1: f64, + mijk2: f64, + etas: &[D], + c: &[[f64; 3]], +) -> D { (0..c.len()) .map(|i| etas[i] * (c[i][0] + mijk1 * c[i][1] + mijk2 * c[i][2])) .sum() } -fn triplet_integral_ijk_dq>(mijk: f64, etas: &[D], c: &[[f64; 2]]) -> D { +fn triplet_integral_ijk_dq + Copy>(mijk: f64, etas: &[D], c: &[[f64; 2]]) -> D { (0..c.len()) .map(|i| etas[i] * (c[i][0] + mijk * c[i][1])) .sum() @@ -160,7 +165,7 @@ pub struct Dipole { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Dipole { +impl + Copy> HelmholtzEnergyDual for Dipole { fn helmholtz_energy(&self, state: &StateHD) -> D { let m = MeanSegmentNumbers::new(&self.parameters, Multipole::Dipole); let p = &self.parameters; @@ -237,7 +242,7 @@ pub struct Quadrupole { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Quadrupole { +impl + Copy> HelmholtzEnergyDual for Quadrupole { fn helmholtz_energy(&self, state: &StateHD) -> D { let m = MeanSegmentNumbers::new(&self.parameters, Multipole::Quadrupole); let p = &self.parameters; @@ -324,7 +329,7 @@ pub struct DipoleQuadrupole { pub variant: DQVariants, } -impl> HelmholtzEnergyDual for DipoleQuadrupole { +impl + Copy> HelmholtzEnergyDual for DipoleQuadrupole { fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; diff --git a/src/pcsaft/eos/qspr.rs b/src/pcsaft/eos/qspr.rs index 0f2ce9b4b..844c21ce5 100644 --- a/src/pcsaft/eos/qspr.rs +++ b/src/pcsaft/eos/qspr.rs @@ -68,7 +68,7 @@ pub struct QSPR { pub parameters: Arc, } -impl> IdealGasContributionDual for QSPR { +impl + Copy> IdealGasContributionDual for QSPR { fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { let (c_300, c_400) = if self.parameters.association.is_empty() { match self.parameters.ndipole + self.parameters.nquadpole { diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 5cfef1564..892cbee6b 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -469,7 +469,7 @@ impl HardSphereProperties for PcSaftParameters { MonomerShape::NonSpherical(self.m.mapv(N::from)) } - fn hs_diameter>(&self, temperature: D) -> Array1 { + fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { let ti = temperature.recip() * -3.0; Array::from_shape_fn(self.sigma.len(), |i| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] diff --git a/src/pets/dft/dispersion.rs b/src/pets/dft/dispersion.rs index 79c648497..89e556aee 100644 --- a/src/pets/dft/dispersion.rs +++ b/src/pets/dft/dispersion.rs @@ -27,7 +27,7 @@ impl AttractiveFunctional { } } -fn att_weight_functions + ScalarOperand>( +fn att_weight_functions + Copy + ScalarOperand>( p: &PetsParameters, psi: f64, temperature: N, @@ -39,7 +39,9 @@ fn att_weight_functions + ScalarOperand>( ) } -impl + ScalarOperand> FunctionalContributionDual for AttractiveFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual + for AttractiveFunctional +{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { att_weight_functions(&self.parameters, PSI_DFT, temperature) } diff --git a/src/pets/dft/pure_pets_functional.rs b/src/pets/dft/pure_pets_functional.rs index b7aaaa798..85ec7c910 100644 --- a/src/pets/dft/pure_pets_functional.rs +++ b/src/pets/dft/pure_pets_functional.rs @@ -29,7 +29,7 @@ impl PureFMTFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for PureFMTFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for PureFMTFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let r = self.parameters.hs_diameter(temperature) * 0.5; WeightFunctionInfo::new(arr1(&[0]), false).extend( @@ -125,7 +125,7 @@ impl PureAttFunctional { } } -impl + ScalarOperand> FunctionalContributionDual for PureAttFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual for PureAttFunctional { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); const PSI: f64 = 1.21; // Homosegmented DFT (Heier2018) diff --git a/src/pets/eos/dispersion.rs b/src/pets/eos/dispersion.rs index 09db1c17c..4b656d412 100644 --- a/src/pets/eos/dispersion.rs +++ b/src/pets/eos/dispersion.rs @@ -30,7 +30,7 @@ pub struct Dispersion { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Dispersion { +impl + Copy> HelmholtzEnergyDual for Dispersion { fn helmholtz_energy(&self, state: &StateHD) -> D { // auxiliary variables let n = self.parameters.sigma.len(); diff --git a/src/pets/eos/qspr.rs b/src/pets/eos/qspr.rs index 78f9e8572..e4d5aa6e4 100644 --- a/src/pets/eos/qspr.rs +++ b/src/pets/eos/qspr.rs @@ -68,7 +68,7 @@ pub struct QSPR { pub parameters: Arc, } -impl> IdealGasContributionDual for QSPR { +impl + Copy> IdealGasContributionDual for QSPR { fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { let (c_300, c_400) = (NA_NP_300, NA_NP_400); diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 096f6e724..436c9dc3a 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -237,7 +237,7 @@ impl HardSphereProperties for PetsParameters { MonomerShape::Spherical(self.sigma.len()) } - fn hs_diameter>(&self, temperature: D) -> Array1 { + fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { let ti = temperature.recip() * -3.052785558; Array::from_shape_fn(self.sigma.len(), |i| { -((ti * self.epsilon_k[i]).exp() * 0.127112544 - 1.0) * self.sigma[i] diff --git a/src/saftvrqmie/dft/dispersion.rs b/src/saftvrqmie/dft/dispersion.rs index c824385bf..4b21d0538 100644 --- a/src/saftvrqmie/dft/dispersion.rs +++ b/src/saftvrqmie/dft/dispersion.rs @@ -26,7 +26,7 @@ impl AttractiveFunctional { } } -fn att_weight_functions + ScalarOperand>( +fn att_weight_functions + Copy + ScalarOperand>( p: &SaftVRQMieParameters, psi: f64, temperature: N, @@ -38,7 +38,9 @@ fn att_weight_functions + ScalarOperand>( ) } -impl + ScalarOperand> FunctionalContributionDual for AttractiveFunctional { +impl + Copy + ScalarOperand> FunctionalContributionDual + for AttractiveFunctional +{ fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { att_weight_functions(&self.parameters, PSI_DFT, temperature) } diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index da9d32855..d728fe9db 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -116,7 +116,7 @@ impl HardSphereProperties for SaftVRQMieParameters { MonomerShape::Spherical(self.m.len()) } - fn hs_diameter>(&self, temperature: D) -> Array1 { + fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { self.hs_diameter(temperature) } } diff --git a/src/saftvrqmie/dft/non_additive_hs.rs b/src/saftvrqmie/dft/non_additive_hs.rs index fe94e362e..f7517c700 100644 --- a/src/saftvrqmie/dft/non_additive_hs.rs +++ b/src/saftvrqmie/dft/non_additive_hs.rs @@ -24,7 +24,7 @@ impl NonAddHardSphereFunctional { impl FunctionalContributionDual for NonAddHardSphereFunctional where - N: DualNum + ScalarOperand, + N: DualNum + Copy + ScalarOperand, { fn weight_functions(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; @@ -146,7 +146,7 @@ where } } -pub fn non_additive_hs_energy_density + ScalarOperand>( +pub fn non_additive_hs_energy_density + Copy + ScalarOperand>( parameters: &SaftVRQMieParameters, d_hs_ij: &Array2, d_hs_add_ij: &Array2, diff --git a/src/saftvrqmie/eos/dispersion.rs b/src/saftvrqmie/eos/dispersion.rs index 1db59440e..8bff9d590 100644 --- a/src/saftvrqmie/eos/dispersion.rs +++ b/src/saftvrqmie/eos/dispersion.rs @@ -34,7 +34,7 @@ pub struct Alpha> { alpha_ij: Array2, } -impl> Alpha { +impl + Copy> Alpha { pub fn new( parameters: &SaftVRQMieParameters, sigma_eff_ij: &Array2, @@ -76,7 +76,7 @@ pub struct Dispersion { pub parameters: Arc, } -impl> HelmholtzEnergyDual for Dispersion { +impl + Copy> HelmholtzEnergyDual for Dispersion { fn helmholtz_energy(&self, state: &StateHD) -> D { // auxiliary variables let n = self.parameters.m.len(); @@ -136,7 +136,7 @@ impl> HelmholtzEnergyDual for Dispersion { } #[cfg(feature = "dft")] -pub fn dispersion_energy_density>( +pub fn dispersion_energy_density + Copy>( parameters: &SaftVRQMieParameters, d_hs_ij: &Array2, s_eff_ij: &Array2, @@ -172,7 +172,7 @@ pub fn dispersion_energy_density>( rho_s * (a1 * inv_t + a2 * inv_t.powi(2) + a3 * inv_t.powi(3)) } -fn zeta_saft_vrq_mie>( +fn zeta_saft_vrq_mie + Copy>( m: &Array1, x_s: &Array1, diameter: &Array2, @@ -187,7 +187,7 @@ fn zeta_saft_vrq_mie>( zeta * FRAC_PI_6 * rho_s } -fn first_order_perturbation>( +fn first_order_perturbation + Copy>( parameters: &SaftVRQMieParameters, x_s: &Array1, zeta: D, @@ -223,7 +223,7 @@ fn first_order_perturbation>( a1 } -fn first_order_perturbation_ij>( +fn first_order_perturbation_ij + Copy>( lambda_a: f64, lambda_r: f64, epsilon_k: f64, @@ -244,7 +244,7 @@ fn first_order_perturbation_ij>( (int_qa * qa1 - int_qr * qr1 + int_a - int_r) * c } -fn eta_eff>(lambda: f64, zeta: D) -> D { +fn eta_eff + Copy>(lambda: f64, zeta: D) -> D { let inv_lambda = Array1::from(vec![ 1.0, 1.0 / lambda, @@ -260,7 +260,7 @@ fn eta_eff>(lambda: f64, zeta: D) -> D { zeta * (zeta * (zeta * (zeta * c[3] + c[2]) + c[1]) + c[0]) } -fn sutherland>(lambda: f64, epsilon_k: f64, zeta: D, x0: D) -> D { +fn sutherland + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D) -> D { let ef = eta_eff(lambda, zeta); (-ef * 0.5 + 1.0) * -12.0 * x0.powf(lambda) * epsilon_k / (lambda - 3.0) / (-ef + 1.0).powi(3) } @@ -278,7 +278,7 @@ fn jlambda>(lambda: f64, x0: D) -> D { /// B is divided by the packing fraction /// /// \author Morten Hammer, February 2018 -fn b>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff: D) -> D { +fn b + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff: D) -> D { let ilambda = ilambda(lambda, x0_eff); let jlambda = jlambda(lambda, x0_eff); let denum = (-zeta + 1.0).powi(3); @@ -289,7 +289,7 @@ fn b>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff: D) -> } #[inline] -fn combine_sutherland_and_b>( +fn combine_sutherland_and_b + Copy>( lambda: f64, epsilon_k: f64, zeta: D, @@ -301,7 +301,7 @@ fn combine_sutherland_and_b>( int_as + int_b } -fn second_order_perturbation>( +fn second_order_perturbation + Copy>( parameters: &SaftVRQMieParameters, alpha: &Alpha, x_s: &Array1, @@ -353,7 +353,7 @@ fn quantum_prefactor(lambda: f64) -> f64 { lambda * (lambda - 1.0) } -fn second_order_perturbation_ij>( +fn second_order_perturbation_ij + Copy>( lambda_a: f64, lambda_r: f64, epsilon_k: f64, @@ -398,7 +398,7 @@ fn second_order_perturbation_ij>( a2_ij * 0.5 * epsilon_k * c.powi(2) } -fn third_order_perturbation>( +fn third_order_perturbation + Copy>( parameters: &SaftVRQMieParameters, alpha: &Alpha, x_s: &Array1, @@ -417,7 +417,7 @@ fn third_order_perturbation>( a3 } -fn third_order_perturbation_ij>( +fn third_order_perturbation_ij + Copy>( i: usize, j: usize, epsilon_k_eff: D, @@ -444,9 +444,6 @@ mod tests { use crate::saftvrqmie::parameters::utils::hydrogen_fh1; use approx::assert_relative_eq; use ndarray::arr1; - use num_dual::Dual2; - use num_dual::DualNum; - use num_traits::Zero; #[test] fn test_eta_eff() { @@ -466,7 +463,7 @@ mod tests { #[test] fn test_alpha() { - let temperature = Dual2::from_re(26.7060).derive(); + let temperature = 26.7060; let parameters = hydrogen_fh1(); let n = 1; let s_eff_ij = Array2::from_shape_fn((n, n), |(i, j)| { @@ -476,46 +473,42 @@ mod tests { parameters.calc_epsilon_k_eff_ij(i, j, temperature) }); let alpha = Alpha::new(¶meters, &s_eff_ij, &epsilon_k_eff_ij, temperature); - assert_relative_eq!( - alpha.alpha_ij[[0, 0]].re(), - 1.0239374984636636, - epsilon = 5e-8 - ); + assert_relative_eq!(alpha.alpha_ij[[0, 0]], 1.0239374984636636, epsilon = 5e-8); } #[test] fn test_sutherland() { - let x0 = Dual2::from_re(1.1).derive(); - let zeta = Dual2::from_re(0.333).derive(); + let x0 = 1.1; + let zeta = 0.333; let lambda = 13.77; let eps_div_k = 13.88; let asa = sutherland(lambda, eps_div_k, zeta, x0); - assert_relative_eq!(asa.re(), -122.12017536923423, epsilon = 1e-12); + assert_relative_eq!(asa, -122.12017536923423, epsilon = 1e-12); } #[test] fn test_b() { - let x0 = Dual2::from_re(1.1).derive(); - let zeta = Dual2::from_re(0.333).derive(); + let x0 = 1.1; + let zeta = 0.333; let lambda = 13.77; let eps_div_k = 13.88; let ba = b(lambda, eps_div_k, zeta, x0, x0); - assert_relative_eq!(ba.re(), 93.436438943866293, epsilon = 1e-12); + assert_relative_eq!(ba, 93.436438943866293, epsilon = 1e-12); } #[test] fn test_quantum_d_ij() { let p = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); + let temperature = 26.7060; let dq_ij = p.quantum_d_ij(0, 0, temperature); - assert_relative_eq!(dq_ij.re(), 7.5092605940987542e-2, epsilon = 5e-8); + assert_relative_eq!(dq_ij, 7.5092605940987542e-2, epsilon = 5e-8); } #[test] fn test_first_order_perturbation_ij() { let p = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); - let zeta = Dual2::from_re(0.333).derive(); + let temperature = 26.7060; + let zeta = 0.333; let dq_div_s2 = p.quantum_d_ij(0, 0, temperature) / p.sigma_ij[[0, 0]].powi(2); let s_eff = p.calc_sigma_eff_ij(0, 0, temperature); let d_hs = p.hs_diameter_ij(0, 0, temperature, s_eff); @@ -532,15 +525,15 @@ mod tests { p.c_ij[[0, 0]], dq_div_s2, ); - let rel_err = (a1_ij.re() + 332.00915966785539) / 332.00915966785539; + let rel_err = (a1_ij + 332.00915966785539) / 332.00915966785539; assert_relative_eq!(rel_err, 0.0, epsilon = 1e-7); } #[test] fn test_second_order_perturbation_ij() { let p = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); - let zeta = Dual2::from_re(0.333).derive(); + let temperature = 26.7060; + let zeta = 0.333; let dq_div_s2 = p.quantum_d_ij(0, 0, temperature) / p.sigma_ij[[0, 0]].powi(2); let s_eff = p.calc_sigma_eff_ij(0, 0, temperature); let d_hs = p.hs_diameter_ij(0, 0, temperature, s_eff); @@ -557,15 +550,15 @@ mod tests { p.c_ij[[0, 0]], dq_div_s2, ); - let rel_err = (a2_ij.re() + 1907.5055256805874) / 1907.5055256805874; + let rel_err = (a2_ij + 1907.5055256805874) / 1907.5055256805874; assert_relative_eq!(rel_err, 0.0, epsilon = 1e-7); } #[test] fn test_third_order_perturbation_ij() { let p = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); - let zeta_bar = Dual2::from_re(0.333).derive(); + let temperature = 26.7060; + let zeta_bar = 0.333; let n = 1; let s_eff_ij = Array2::from_shape_fn((n, n), |(i, j)| p.calc_sigma_eff_ij(i, j, temperature)); @@ -575,16 +568,16 @@ mod tests { let a3_ij = third_order_perturbation_ij(0, 0, epsilon_k_eff_ij[[0, 0]], &alpha, zeta_bar); - let rel_err = (a3_ij.re() + 25.807966819127916) / 25.807966819127916; + let rel_err = (a3_ij + 25.807966819127916) / 25.807966819127916; assert_relative_eq!(rel_err, 0.0, epsilon = 5e-7); } #[test] fn test_zeta_saft_vrq_mie() { let p = hydrogen_fh1(); - let t = Dual2::from_re(26.7060).derive(); - let v = Dual2::from_re(1.0e26).derive(); - let n = Dual2::from_re(6.02214076e23).derive(); + let t = 26.7060; + let v = 1.0e26; + let n = 6.02214076e23; let state = StateHD::new(t, v, arr1(&[n])); let nc = 1; // temperature dependent sigma @@ -603,23 +596,23 @@ mod tests { x_s[i] *= inv_x_s_sum; } // Segment density - let mut rho_s = Dual2::zero(); + let mut rho_s = 0.0; for i in 0..nc { rho_s += state.partial_density[i] * p.m[i]; } // packing fractions let zeta = zeta_saft_vrq_mie(&p.m, &x_s, &d_hs_ij, rho_s); let zeta_bar = zeta_saft_vrq_mie(&p.m, &x_s, &s_eff_ij, rho_s); - assert_relative_eq!(zeta.re(), 9.7717457994590765E-002, epsilon = 5e-9); - assert_relative_eq!(zeta_bar.re(), 0.10864364645845238, epsilon = 5e-9); + assert_relative_eq!(zeta, 9.7717457994590765E-002, epsilon = 5e-9); + assert_relative_eq!(zeta_bar, 0.10864364645845238, epsilon = 5e-9); } #[test] fn test_perturbation_terms() { let p = hydrogen_fh1(); - let t = Dual2::from_re(26.7060).derive(); - let v = Dual2::from_re(1.0e26).derive(); - let n = Dual2::from_re(6.02214076e23).derive(); + let t = 26.7060; + let v = 1.0e26; + let n = 6.02214076e23; let state = StateHD::new(t, v, arr1(&[n])); let nc = 1; // temperature dependent sigma @@ -639,7 +632,7 @@ mod tests { } // Segment density - let mut rho_s = Dual2::zero(); + let mut rho_s = 0.0; for i in 0..nc { rho_s += state.partial_density[i] * p.m[i]; } @@ -666,12 +659,12 @@ mod tests { ); let a3 = third_order_perturbation(&p, &alpha, &x_s, zeta_bar, &epsilon_k_eff_ij); - let rel_err_a1 = (a1.re() + 30.702499892515764) / 30.702499892515764; - let rel_err_a2 = (a2.re() + 67.046957636607587) / 67.046957636607587; - let rel_err_a3 = (a3.re() + 470.96241656623727) / 470.96241656623727; - assert_relative_eq!(rel_err_a1.re(), 0.0, epsilon = 5e-7); - assert_relative_eq!(rel_err_a2.re(), 0.0, epsilon = 5e-7); - assert_relative_eq!(rel_err_a3.re(), 0.0, epsilon = 5e-7); + let rel_err_a1 = (a1 + 30.702499892515764) / 30.702499892515764; + let rel_err_a2 = (a2 + 67.046957636607587) / 67.046957636607587; + let rel_err_a3 = (a3 + 470.96241656623727) / 470.96241656623727; + assert_relative_eq!(rel_err_a1, 0.0, epsilon = 5e-7); + assert_relative_eq!(rel_err_a2, 0.0, epsilon = 5e-7); + assert_relative_eq!(rel_err_a3, 0.0, epsilon = 5e-7); } #[test] @@ -688,19 +681,18 @@ mod tests { ]; let na = 6.02214076e23; for (it, &a) in a_ref.iter().enumerate() { - let t = Dual2::from_re(26.7060 * (it + 1) as f64).derive(); - let v = Dual2::from_re(1.0e26).derive(); - let n = Dual2::from_re(na).derive(); - let state = StateHD::new(t, v, arr1(&[n])); + let t = 26.7060 * (it + 1) as f64; + let v = 1.0e26; + let state = StateHD::new(t, v, arr1(&[na])); let a_disp = disp.helmholtz_energy(&state) / na; - assert_relative_eq!(a_disp.re(), a, epsilon = 1e-7); + assert_relative_eq!(a_disp, a, epsilon = 1e-7); } - let t = Dual2::from_re(26.7060).derive(); - let v = Dual2::from_re(1.0e26 * 2.0).derive(); - let n = Dual2::from_re(na * 2.0).derive(); + let t = 26.7060; + let v = 1.0e26 * 2.0; + let n = na * 2.0; let state = StateHD::new(t, v, arr1(&[n])); let a_disp = disp.helmholtz_energy(&state) / na; - assert_relative_eq!(a_disp.re(), a_ref[0] * 2.0, epsilon = 1e-7); + assert_relative_eq!(a_disp, a_ref[0] * 2.0, epsilon = 1e-7); } #[test] @@ -728,21 +720,21 @@ mod tests { -0.84210863940206726, ]; let na = 6.02214076e23; - let n = [Dual2::from_re(1.1 * na), Dual2::from_re(1.0 * na)]; - let v = Dual2::from_re(1.0e26).derive(); + let n = [1.1 * na, 1.0 * na]; + let v = 1.0e26; for (it, &a) in a_ref.iter().enumerate() { - let t = Dual2::from_re(30.0 * (it + 1) as f64).derive(); + let t = 30.0 * (it + 1) as f64; let state = StateHD::new(t, v, arr1(&n)); let a_disp = disp.helmholtz_energy(&state) / na; dbg!(it); - assert_relative_eq!(a_disp.re(), a, epsilon = 1e-7); + assert_relative_eq!(a_disp, a, epsilon = 1e-7); } - let t = Dual2::from_re(30.0).derive(); - let v = Dual2::from_re(1.0e26 * 2.0).derive(); - let n = [Dual2::from_re(2.2 * na), Dual2::from_re(2.0 * na)]; + let t = 30.0; + let v = 1.0e26 * 2.0; + let n = [2.2 * na, 2.0 * na]; let state = StateHD::new(t, v, arr1(&n)); let a_disp = disp.helmholtz_energy(&state) / na; - assert_relative_eq!(a_disp.re(), a_ref[0] * 2.0, epsilon = 1e-7); + assert_relative_eq!(a_disp, a_ref[0] * 2.0, epsilon = 1e-7); } #[cfg(feature = "dft")] @@ -753,8 +745,8 @@ mod tests { }; let p = &disp.parameters; let n = p.m.len(); - let rho = Array1::from_shape_fn(n, |_i| Dual2::from_re(0.01)); - let t = Dual2::from_re(25.0).derive(); + let rho = Array1::from_shape_fn(n, |_i| 0.01); + let t = 25.0; // temperature dependent segment radius // calc & store this in struct let s_eff_ij = Array2::from_shape_fn((n, n), |(i, j)| p.calc_sigma_eff_ij(i, j, t)); @@ -783,7 +775,7 @@ mod tests { ); dbg!(rho); - dbg!(a_disp.re()); - assert_relative_eq!(a_disp.re(), -0.022349175545184223, epsilon = 1e-7); + dbg!(a_disp); + assert_relative_eq!(a_disp, -0.022349175545184223, epsilon = 1e-7); } } diff --git a/src/saftvrqmie/eos/hard_sphere.rs b/src/saftvrqmie/eos/hard_sphere.rs index 452339a35..dcff28074 100644 --- a/src/saftvrqmie/eos/hard_sphere.rs +++ b/src/saftvrqmie/eos/hard_sphere.rs @@ -62,7 +62,7 @@ const W_K21: [f64; 21] = [ impl SaftVRQMieParameters { #[inline] - pub fn hs_diameter>(&self, temperature: D) -> Array1 { + pub fn hs_diameter + Copy>(&self, temperature: D) -> Array1 { Array1::from_shape_fn(self.m.len(), |i| -> D { let sigma_eff = self.calc_sigma_eff_ij(i, i, temperature); self.hs_diameter_ij(i, i, temperature, sigma_eff) @@ -70,7 +70,7 @@ impl SaftVRQMieParameters { } #[inline] - pub fn hs_diameter_ij>( + pub fn hs_diameter_ij + Copy>( &self, i: usize, j: usize, @@ -89,7 +89,7 @@ impl SaftVRQMieParameters { d_hs } - pub fn zero_integrand>( + pub fn zero_integrand + Copy>( &self, i: usize, j: usize, @@ -118,13 +118,18 @@ impl SaftVRQMieParameters { } #[inline] - pub fn epsilon_k_eff>(&self, temperature: D) -> Array1 { + pub fn epsilon_k_eff + Copy>(&self, temperature: D) -> Array1 { Array1::from_shape_fn(self.m.len(), |i| -> D { self.calc_epsilon_k_eff_ij(i, i, temperature) }) } - pub fn calc_epsilon_k_eff_ij>(&self, i: usize, j: usize, temperature: D) -> D { + pub fn calc_epsilon_k_eff_ij + Copy>( + &self, + i: usize, + j: usize, + temperature: D, + ) -> D { let mut r = D::one() * self.sigma_ij[[i, j]]; let mut u_vec = [D::zero(), D::zero(), D::zero()]; for _k in 1..20 { @@ -141,13 +146,18 @@ impl SaftVRQMieParameters { } #[inline] - pub fn sigma_eff>(&self, temperature: D) -> Array1 { + pub fn sigma_eff + Copy>(&self, temperature: D) -> Array1 { Array1::from_shape_fn(self.m.len(), |i| -> D { self.calc_sigma_eff_ij(i, i, temperature) }) } - pub fn calc_sigma_eff_ij>(&self, i: usize, j: usize, temperature: D) -> D { + pub fn calc_sigma_eff_ij + Copy>( + &self, + i: usize, + j: usize, + temperature: D, + ) -> D { let mut r = D::one() * self.sigma_ij[[i, j]]; let mut u_vec = [D::zero(), D::zero(), D::zero()]; for _k in 1..20 { @@ -169,7 +179,7 @@ impl SaftVRQMieParameters { } /// Feynman-Hibbs corrected potential - pub fn qmie_potential_ij>( + pub fn qmie_potential_ij + Copy>( &self, i: usize, j: usize, @@ -219,7 +229,7 @@ pub struct HardSphere { pub parameters: Arc, } -impl> HelmholtzEnergyDual for HardSphere { +impl + Copy> HelmholtzEnergyDual for HardSphere { fn helmholtz_energy(&self, state: &StateHD) -> D { let d = self.parameters.hs_diameter(state.temperature); let zeta = zeta(&self.parameters.m, &state.partial_density, &d); @@ -238,7 +248,7 @@ impl fmt::Display for HardSphere { } } -pub fn zeta>( +pub fn zeta + Copy>( m: &Array1, partial_density: &Array1, diameter: &Array1, @@ -254,7 +264,11 @@ pub fn zeta>( zeta } -pub fn zeta_23>(m: &Array1, molefracs: &Array1, diameter: &Array1) -> D { +pub fn zeta_23 + Copy>( + m: &Array1, + molefracs: &Array1, + diameter: &Array1, +) -> D { let mut zeta: [D; 2] = [D::zero(), D::zero()]; for i in 0..m.len() { for (k, z) in zeta.iter_mut().enumerate() { @@ -271,59 +285,59 @@ mod tests { use crate::saftvrqmie::parameters::utils::hydrogen_fh1; use approx::assert_relative_eq; use ndarray::arr1; - use num_dual::Dual2; + use num_dual::Dual64; #[test] fn test_quantum_d_mass() { let parameters = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); - let r = Dual2::from_re(3.5); + let temperature = 26.7060; + let r = 3.5; let u0 = parameters.qmie_potential_ij(0, 0, r, temperature); let eps = 1.0e-5; let u2 = parameters.qmie_potential_ij(0, 0, r + eps, temperature); let u1 = parameters.qmie_potential_ij(0, 0, r - eps, temperature); - let dudr_num = (u2[0].re() - u1[0].re()) / eps / 2.0; - let d2udr2_num = (u2[1].re() - u1[1].re()) / eps / 2.0; - assert!(((dudr_num - u0[1].re()) / u0[1].re()).abs() < 1.0e-9); - assert!(((d2udr2_num - u0[2].re()) / u0[2].re()).abs() < 1.0e-9); + let dudr_num = (u2[0] - u1[0]) / eps / 2.0; + let d2udr2_num = (u2[1] - u1[1]) / eps / 2.0; + assert!(((dudr_num - u0[1]) / u0[1]).abs() < 1.0e-9); + assert!(((d2udr2_num - u0[2]) / u0[2]).abs() < 1.0e-9); } #[test] fn test_sigma_effective() { let parameters = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); + let temperature = 26.7060; let sigma_eff = parameters.calc_sigma_eff_ij(0, 0, temperature); - println!("{}", sigma_eff.re() - 3.2540054024660556); - assert!((sigma_eff.re() - 3.2540054024660556).abs() < 5.0e-7) + println!("{}", sigma_eff - 3.2540054024660556); + assert!((sigma_eff - 3.2540054024660556).abs() < 5.0e-7) } #[test] fn test_eps_div_k_effective() { let parameters = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); + let temperature = 26.7060; let epsilon_k_eff = parameters.calc_epsilon_k_eff_ij(0, 0, temperature); - println!("{}", epsilon_k_eff.re() - 21.654396207986697); - assert!((epsilon_k_eff.re() - 21.654396207986697).abs() < 1.0e-6) + println!("{}", epsilon_k_eff - 21.654396207986697); + assert!((epsilon_k_eff - 21.654396207986697).abs() < 1.0e-6) } #[test] fn test_zero_integrand() { let parameters = hydrogen_fh1(); - let temperature = Dual2::from_re(26.706).derive(); + let temperature = 26.706; let sigma_eff = parameters.calc_sigma_eff_ij(0, 0, temperature); let r0 = parameters.zero_integrand(0, 0, temperature, sigma_eff); - println!("{}", r0.re() - 2.5265031901173732); - assert!((r0.re() - 2.5265031901173732).abs() < 5.0e-7) + println!("{}", r0 - 2.5265031901173732); + assert!((r0 - 2.5265031901173732).abs() < 5.0e-7) } #[test] fn test_hs_diameter() { let parameters = hydrogen_fh1(); - let temperature = Dual2::from_re(26.7060).derive(); + let temperature = Dual64::from(26.7060).derivative(); let sigma_eff = parameters.calc_sigma_eff_ij(0, 0, temperature); let d_hs = parameters.hs_diameter_ij(0, 0, temperature, sigma_eff); assert!((d_hs.re() - 3.1410453883283341).abs() < 5.0e-8); - assert!((d_hs.v1[0] + 8.4528823966252661e-3).abs() < 1.0e-9); + assert!((d_hs.eps + 8.4528823966252661e-3).abs() < 1.0e-9); } #[test] diff --git a/src/saftvrqmie/eos/non_additive_hs.rs b/src/saftvrqmie/eos/non_additive_hs.rs index 520a5a975..ae8c8cdea 100644 --- a/src/saftvrqmie/eos/non_additive_hs.rs +++ b/src/saftvrqmie/eos/non_additive_hs.rs @@ -11,7 +11,7 @@ pub struct NonAddHardSphere { pub parameters: Arc, } -impl> HelmholtzEnergyDual for NonAddHardSphere { +impl + Copy> HelmholtzEnergyDual for NonAddHardSphere { fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; let n = p.m.len(); @@ -40,7 +40,7 @@ impl fmt::Display for NonAddHardSphere { } } -pub fn reduced_non_additive_hs_energy>( +pub fn reduced_non_additive_hs_energy + Copy>( parameters: &SaftVRQMieParameters, d_hs_ij: &Array2, d_hs_add_ij: &Array2, diff --git a/src/uvtheory/eos/attractive_perturbation_bh.rs b/src/uvtheory/eos/attractive_perturbation_bh.rs index c99580cb5..b4be61452 100644 --- a/src/uvtheory/eos/attractive_perturbation_bh.rs +++ b/src/uvtheory/eos/attractive_perturbation_bh.rs @@ -51,7 +51,7 @@ impl fmt::Display for AttractivePerturbationBH { } } -impl> HelmholtzEnergyDual for AttractivePerturbationBH { +impl + Copy> HelmholtzEnergyDual for AttractivePerturbationBH { /// Helmholtz energy for attractive perturbation, eq. 52 fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; @@ -87,7 +87,7 @@ fn delta_b12u>(t_x: D, mean_field_constant_x: D, weighted_sigma3 -mean_field_constant_x / t_x * 2.0 * PI * weighted_sigma3_ij } -fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, t: D) -> D { +fn residual_virial_coefficient + Copy>(p: &UVParameters, x: &Array1, t: D) -> D { let mut delta_b2bar = D::zero(); for i in 0..p.ncomponents { let xi = x[i]; @@ -101,7 +101,7 @@ fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, delta_b2bar } -fn correlation_integral_bh>( +fn correlation_integral_bh + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -116,7 +116,7 @@ fn correlation_integral_bh>( /// U-fraction according to Barker-Henderson division. /// Eq. 15 -fn u_fraction_bh>(rep_x: D, reduced_density: D, one_fluid_beta: D) -> D { +fn u_fraction_bh + Copy>(rep_x: D, reduced_density: D, one_fluid_beta: D) -> D { let mut c = [D::zero(); 4]; let inv_rep = rep_x.recip(); for i in 0..4 { @@ -130,11 +130,11 @@ fn u_fraction_bh>(rep_x: D, reduced_density: D, one_fluid_beta: /// Activation function used for u-fraction according to Barker-Henderson division. /// Eq. 16 -fn activation>(c: D, one_fluid_beta: D) -> D { +fn activation + Copy>(c: D, one_fluid_beta: D) -> D { one_fluid_beta * c.sqrt() / (one_fluid_beta.powi(2) * c + 1.0).sqrt() } -fn one_fluid_properties>( +fn one_fluid_properties + Copy>( p: &UVParameters, x: &Array1, t: D, @@ -172,7 +172,7 @@ fn one_fluid_properties>( ) } -fn coefficients_bh>(rep: D, att: D, d: D) -> [D; 3] { +fn coefficients_bh + Copy>(rep: D, att: D, d: D) -> [D; 3] { let c11 = d.powd(-rep + 6.0) * ((D::one() * 2.0f64).powd(-rep + 3.0) - d.powd(rep - 3.0)) / (-rep + 3.0) + (-d.powi(3) * 8.0 + 1.0) / 24.0; @@ -190,14 +190,14 @@ fn coefficients_bh>(rep: D, att: D, d: D) -> [D; 3] { [c1, c2, c3] } -fn delta_b2>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { let rc = 5.0; let alpha = mean_field_constant(rep, att, rc); let yeff = y_eff(reduced_temperature, rep, att); -(yeff * (rc.powi(3) - 1.0) / 3.0 + reduced_temperature.recip() * alpha) * 2.0 * PI } -fn y_eff>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { // optimize: move this part to parameter initialization let rc = 5.0; let rs = 1.0; diff --git a/src/uvtheory/eos/attractive_perturbation_uvb3.rs b/src/uvtheory/eos/attractive_perturbation_uvb3.rs index c703dee6a..af8aab27c 100644 --- a/src/uvtheory/eos/attractive_perturbation_uvb3.rs +++ b/src/uvtheory/eos/attractive_perturbation_uvb3.rs @@ -90,7 +90,7 @@ impl fmt::Display for AttractivePerturbationUVB3 { } } -impl> HelmholtzEnergyDual for AttractivePerturbationUVB3 { +impl + Copy> HelmholtzEnergyDual for AttractivePerturbationUVB3 { /// Helmholtz energy for attractive perturbation fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; @@ -131,7 +131,7 @@ impl> HelmholtzEnergyDual for AttractivePerturbationUVB3 { } } -fn delta_b12u>( +fn delta_b12u + Copy>( t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D, @@ -144,7 +144,7 @@ fn delta_b12u>( * weighted_sigma3_ij } -fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, t: D) -> D { +fn residual_virial_coefficient + Copy>(p: &UVParameters, x: &Array1, t: D) -> D { let mut delta_b2bar = D::zero(); for i in 0..p.ncomponents { @@ -163,7 +163,7 @@ fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, } delta_b2bar } -fn residual_third_virial_coefficient>( +fn residual_third_virial_coefficient + Copy>( p: &UVParameters, x: &Array1, t: D, @@ -192,7 +192,7 @@ fn residual_third_virial_coefficient>( } delta_b3bar } -fn correlation_integral_wca>( +fn correlation_integral_wca + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -209,7 +209,7 @@ fn correlation_integral_wca>( } /// U-fraction with low temperature correction omega -fn u_fraction_wca>(rep_x: D, reduced_density: D, t_x: D) -> D { +fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D, t_x: D) -> D { let omega = if t_x.re() < 175.0 { (-t_x * CU_WCA[5] * (reduced_density - CU_WCA[6]).powi(2)).exp() * ((t_x * CU_WCA[7]).tanh().recip() - 1.0).powi(2) @@ -223,7 +223,7 @@ fn u_fraction_wca>(rep_x: D, reduced_density: D, t_x: D) -> D { } // Coefficients for IWCA -fn coefficients_wca>(rep: D, att: D, d: D) -> [D; 6] { +fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { let rep_inv = rep.recip(); let rs_x = (rep / att).powd((rep - att).recip()); let tau_x = -d + rs_x; @@ -261,7 +261,7 @@ fn factorial(num: u64) -> u64 { (1..=num).product() } -fn delta_b2>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { let rm = (rep / att).powd((rep - att).recip()); let beta = reduced_temperature.recip(); let b20 = q.powi(3) * 2.0 / 3.0 * PI; @@ -289,13 +289,13 @@ fn delta_b2>(reduced_temperature: D, rep: f64, att: f64, q: D) - for i in 2..16 { let k = factorial(i as u64) as f64 * i as f64; - sum_beta += beta.powi(i as i32) / k + sum_beta += beta.powi(i) / k } (b20 - rm.powi(3) * 2.0 / 3.0 * PI - c1) * y - sum_beta * c2 - beta * c3 - beta.powi(2) * c4 } -fn delta_b31u>( +fn delta_b31u + Copy>( t_x: D, weighted_sigma3_ij: D, rm_x: D, @@ -313,7 +313,14 @@ fn delta_b31u>( t_x.recip() * 4.0 * mie_prefactor(rep_x, att_x) * PI * k1 * weighted_sigma3_ij.powi(2) } -fn delta_b3>(t_x: D, rm_x: f64, rep_x: f64, _att_x: f64, d_x: D, q_x: D) -> D { +fn delta_b3 + Copy>( + t_x: D, + rm_x: f64, + rep_x: f64, + _att_x: f64, + d_x: D, + q_x: D, +) -> D { let beta = t_x.recip(); let b30 = (q_x.powi(3) * PI / 6.0).powi(2) * 10.0; diff --git a/src/uvtheory/eos/attractive_perturbation_wca.rs b/src/uvtheory/eos/attractive_perturbation_wca.rs index 9904e61a4..cc31bab5f 100644 --- a/src/uvtheory/eos/attractive_perturbation_wca.rs +++ b/src/uvtheory/eos/attractive_perturbation_wca.rs @@ -77,7 +77,7 @@ impl fmt::Display for AttractivePerturbationWCA { } } -impl> HelmholtzEnergyDual for AttractivePerturbationWCA { +impl + Copy> HelmholtzEnergyDual for AttractivePerturbationWCA { /// Helmholtz energy for attractive perturbation, eq. 52 fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; @@ -110,7 +110,7 @@ impl> HelmholtzEnergyDual for AttractivePerturbationWCA { } // (S43) & (S53) -fn delta_b12u>( +fn delta_b12u + Copy>( t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D, @@ -123,7 +123,7 @@ fn delta_b12u>( * weighted_sigma3_ij } -fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, t: D) -> D { +fn residual_virial_coefficient + Copy>(p: &UVParameters, x: &Array1, t: D) -> D { let mut delta_b2bar = D::zero(); for i in 0..p.ncomponents { @@ -145,7 +145,7 @@ fn residual_virial_coefficient>(p: &UVParameters, x: &Array1, delta_b2bar } -fn correlation_integral_wca>( +fn correlation_integral_wca + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -163,13 +163,13 @@ fn correlation_integral_wca>( /// U-fraction according to Barker-Henderson division. /// Eq. 15 -fn u_fraction_wca>(rep_x: D, reduced_density: D) -> D { +fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D) -> D { (reduced_density * CU_WCA[0] + reduced_density.powi(2) * (rep_x.recip() * CU_WCA[2] + CU_WCA[1])) .tanh() } -pub(super) fn one_fluid_properties>( +pub(super) fn one_fluid_properties + Copy>( p: &UVParameters, x: &Array1, t: D, @@ -212,7 +212,7 @@ pub(super) fn one_fluid_properties>( } // Coefficients for IWCA from eq. (S55) -fn coefficients_wca>(rep: D, att: D, d: D) -> [D; 6] { +fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { let rep_inv = rep.recip(); let rs_x = (rep / att).powd((rep - att).recip()); let tau_x = -d + rs_x; @@ -244,7 +244,7 @@ fn coefficients_wca>(rep: D, att: D, d: D) -> [D; 6] { [c1, c2, c3, c4, c5, c6] } -fn delta_b2>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { let rm = (rep / att).powf(1.0 / (rep - att)); // Check mixing rule!! let rc = 5.0; let alpha = mean_field_constant(rep, att, rc); @@ -256,7 +256,7 @@ fn delta_b2>(reduced_temperature: D, rep: f64, att: f64, q: D) - * PI } -fn y_eff>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { // optimize: move this part to parameter initialization let rc = 5.0; let rs = (rep / att).powf(1.0 / (rep - att)); diff --git a/src/uvtheory/eos/hard_sphere_bh.rs b/src/uvtheory/eos/hard_sphere_bh.rs index a3001cb4b..984577b97 100644 --- a/src/uvtheory/eos/hard_sphere_bh.rs +++ b/src/uvtheory/eos/hard_sphere_bh.rs @@ -23,7 +23,7 @@ pub struct HardSphereBH { pub parameters: Arc, } -impl> HelmholtzEnergyDual for HardSphereBH { +impl + Copy> HelmholtzEnergyDual for HardSphereBH { /// Helmholtz energy for hard spheres, eq. 19 (check Volume) fn helmholtz_energy(&self, state: &StateHD) -> D { let d = diameter_bh(&self.parameters, state.temperature); @@ -46,7 +46,10 @@ impl fmt::Display for HardSphereBH { /// Dimensionless Hard-sphere diameter according to Barker-Henderson division. /// Eq. S23 and S24. /// -pub(super) fn diameter_bh>(parameters: &UVParameters, temperature: D) -> Array1 { +pub(super) fn diameter_bh + Copy>( + parameters: &UVParameters, + temperature: D, +) -> Array1 { parameters .cd_bh_pure .iter() @@ -54,14 +57,16 @@ pub(super) fn diameter_bh>(parameters: &UVParameters, temperatur .map(|(i, c)| { let t = temperature / parameters.epsilon_k[i]; let d = t.powf(0.25) * c[1] + t.powf(0.75) * c[2] + t.powf(1.25) * c[3]; - (t * c[0] + d * (t + 1.0).ln() + t.powi(2) * c[4] + 1.0) - .powf(-0.5 / parameters.rep[i] as f64) + (t * c[0] + d * (t + 1.0).ln() + t.powi(2) * c[4] + 1.0).powf(-0.5 / parameters.rep[i]) * parameters.sigma[i] }) .collect() } -pub(super) fn zeta>(partial_density: &Array1, diameter: &Array1) -> [D; 4] { +pub(super) fn zeta + Copy>( + partial_density: &Array1, + diameter: &Array1, +) -> [D; 4] { let mut zeta: [D; 4] = [D::zero(), D::zero(), D::zero(), D::zero()]; for i in 0..partial_density.len() { for k in 0..4 { @@ -72,7 +77,7 @@ pub(super) fn zeta>(partial_density: &Array1, diameter: &Arra zeta } -pub(super) fn packing_fraction>( +pub(super) fn packing_fraction + Copy>( partial_density: &Array1, diameter: &Array1, ) -> D { @@ -81,7 +86,7 @@ pub(super) fn packing_fraction>( }) } -pub(super) fn zeta_23>(molefracs: &Array1, diameter: &Array1) -> D { +pub(super) fn zeta_23 + Copy>(molefracs: &Array1, diameter: &Array1) -> D { let mut zeta: [D; 2] = [D::zero(), D::zero()]; for i in 0..molefracs.len() { for k in 0..2 { @@ -91,7 +96,7 @@ pub(super) fn zeta_23>(molefracs: &Array1, diameter: &Array1< zeta[0] / zeta[1] } -pub(super) fn packing_fraction_b>( +pub(super) fn packing_fraction_b + Copy>( parameters: &UVParameters, diameter: &Array1, eta: D, @@ -111,7 +116,7 @@ pub(super) fn packing_fraction_b>( }) } -pub(super) fn packing_fraction_a>( +pub(super) fn packing_fraction_a + Copy>( parameters: &UVParameters, diameter: &Array1, eta: D, diff --git a/src/uvtheory/eos/hard_sphere_wca.rs b/src/uvtheory/eos/hard_sphere_wca.rs index 8a8e57270..ed636eb6b 100644 --- a/src/uvtheory/eos/hard_sphere_wca.rs +++ b/src/uvtheory/eos/hard_sphere_wca.rs @@ -53,7 +53,7 @@ pub struct HardSphereWCA { pub parameters: Arc, } -impl> HelmholtzEnergyDual for HardSphereWCA { +impl + Copy> HelmholtzEnergyDual for HardSphereWCA { /// Helmholtz energy for hard spheres, eq. 19 (check Volume) fn helmholtz_energy(&self, state: &StateHD) -> D { let d = diameter_wca(&self.parameters, state.temperature); @@ -74,7 +74,7 @@ impl fmt::Display for HardSphereWCA { } /// Dimensionless Hard-sphere diameter according to Weeks-Chandler-Andersen division. -pub(super) fn diameter_wca>( +pub(super) fn diameter_wca + Copy>( parameters: &UVParameters, temperature: D, ) -> Array1 { @@ -96,7 +96,11 @@ pub(super) fn diameter_wca>( .collect() } -pub(super) fn dimensionless_diameter_q_wca>(t_x: D, rep_x: D, att_x: D) -> D { +pub(super) fn dimensionless_diameter_q_wca + Copy>( + t_x: D, + rep_x: D, + att_x: D, +) -> D { let nu = rep_x; let n = att_x; let rs = (nu / n).powd((nu - n).recip()); @@ -122,7 +126,10 @@ pub(super) fn dimensionless_diameter_q_wca>(t_x: D, rep_x: D, at * rs } -pub(super) fn zeta>(partial_density: &Array1, diameter: &Array1) -> [D; 4] { +pub(super) fn zeta + Copy>( + partial_density: &Array1, + diameter: &Array1, +) -> [D; 4] { let mut zeta: [D; 4] = [D::zero(), D::zero(), D::zero(), D::zero()]; for i in 0..partial_density.len() { for k in 0..4 { @@ -133,7 +140,7 @@ pub(super) fn zeta>(partial_density: &Array1, diameter: &Arra zeta } -pub(super) fn packing_fraction>( +pub(super) fn packing_fraction + Copy>( partial_density: &Array1, diameter: &Array1, ) -> D { @@ -142,7 +149,7 @@ pub(super) fn packing_fraction>( }) } -pub(super) fn zeta_23>(molefracs: &Array1, diameter: &Array1) -> D { +pub(super) fn zeta_23 + Copy>(molefracs: &Array1, diameter: &Array1) -> D { let mut zeta: [D; 2] = [D::zero(), D::zero()]; for i in 0..molefracs.len() { for k in 0..2 { @@ -153,7 +160,7 @@ pub(super) fn zeta_23>(molefracs: &Array1, diameter: &Array1< } #[inline] -pub(super) fn dimensionless_length_scale>( +pub(super) fn dimensionless_length_scale + Copy>( parameters: &UVParameters, temperature: D, ) -> Array1 { @@ -172,7 +179,7 @@ pub(super) fn dimensionless_length_scale>( #[inline] -pub(super) fn packing_fraction_b>( +pub(super) fn packing_fraction_b + Copy>( parameters: &UVParameters, eta: D, temperature: D, @@ -194,7 +201,7 @@ pub(super) fn packing_fraction_b>( }) } -pub(super) fn packing_fraction_b_uvb3>( +pub(super) fn packing_fraction_b_uvb3 + Copy>( parameters: &UVParameters, eta: D, temperature: D, @@ -216,7 +223,7 @@ pub(super) fn packing_fraction_b_uvb3>( }) } -pub(super) fn packing_fraction_a>( +pub(super) fn packing_fraction_a + Copy>( parameters: &UVParameters, eta: D, temperature: D, @@ -245,7 +252,7 @@ pub(super) fn packing_fraction_a>( }) } -pub(super) fn packing_fraction_a_uvb3>( +pub(super) fn packing_fraction_a_uvb3 + Copy>( parameters: &UVParameters, eta: D, temperature: D, diff --git a/src/uvtheory/eos/reference_perturbation_bh.rs b/src/uvtheory/eos/reference_perturbation_bh.rs index e54ca0c2e..37d7f67a9 100644 --- a/src/uvtheory/eos/reference_perturbation_bh.rs +++ b/src/uvtheory/eos/reference_perturbation_bh.rs @@ -18,7 +18,7 @@ impl fmt::Display for ReferencePerturbationBH { } } -impl> HelmholtzEnergyDual for ReferencePerturbationBH { +impl + Copy> HelmholtzEnergyDual for ReferencePerturbationBH { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; diff --git a/src/uvtheory/eos/reference_perturbation_uvb3.rs b/src/uvtheory/eos/reference_perturbation_uvb3.rs index 30d79d126..8ebfac45e 100644 --- a/src/uvtheory/eos/reference_perturbation_uvb3.rs +++ b/src/uvtheory/eos/reference_perturbation_uvb3.rs @@ -19,7 +19,7 @@ impl fmt::Display for ReferencePerturbationUVB3 { } } -impl> HelmholtzEnergyDual for ReferencePerturbationUVB3 { +impl + Copy> HelmholtzEnergyDual for ReferencePerturbationUVB3 { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; diff --git a/src/uvtheory/eos/reference_perturbation_wca.rs b/src/uvtheory/eos/reference_perturbation_wca.rs index 89fb58801..4b709424e 100644 --- a/src/uvtheory/eos/reference_perturbation_wca.rs +++ b/src/uvtheory/eos/reference_perturbation_wca.rs @@ -19,7 +19,7 @@ impl fmt::Display for ReferencePerturbationWCA { } } -impl> HelmholtzEnergyDual for ReferencePerturbationWCA { +impl + Copy> HelmholtzEnergyDual for ReferencePerturbationWCA { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 fn helmholtz_energy(&self, state: &StateHD) -> D { let p = &self.parameters; diff --git a/src/uvtheory/parameters.rs b/src/uvtheory/parameters.rs index 4b54c22a7..f602084d6 100644 --- a/src/uvtheory/parameters.rs +++ b/src/uvtheory/parameters.rs @@ -93,12 +93,12 @@ lazy_static! { } #[inline] -pub fn mie_prefactor>(rep: D, att: D) -> D { +pub fn mie_prefactor + Copy>(rep: D, att: D) -> D { rep / (rep - att) * (rep / att).powd(att / (rep - att)) } #[inline] -pub fn mean_field_constant>(rep: D, att: D, x: D) -> D { +pub fn mean_field_constant + Copy>(rep: D, att: D, x: D) -> D { mie_prefactor(rep, att) * (x.powd(-att + 3.0) / (att - 3.0) - x.powd(-rep + 3.0) / (rep - 3.0)) } From d48b09a9a7a717b90e6838b22e1ec43a09a2703c Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 7 Jul 2023 13:22:52 +0200 Subject: [PATCH 4/7] Revised handling of ideal gas contribution (#158) Co-authored-by: Gernot Bauer --- CHANGELOG.md | 6 + benches/contributions.rs | 2 +- benches/dual_numbers.rs | 6 +- benches/state_creation.rs | 18 +- benches/state_properties.rs | 36 +- docs/theory/eos/properties.md | 117 +-- examples/core_user_defined_eos.ipynb | 59 +- examples/pcsaft_state.ipynb | 116 ++- feos-core/CHANGELOG.md | 19 + feos-core/src/cubic.rs | 58 +- feos-core/src/density_iteration.rs | 6 +- feos-core/src/equation_of_state.rs | 343 --------- .../src/equation_of_state/helmholtz_energy.rs | 59 ++ feos-core/src/equation_of_state/ideal_gas.rs | 90 +++ feos-core/src/equation_of_state/mod.rs | 128 ++++ feos-core/src/equation_of_state/residual.rs | 172 +++++ feos-core/src/joback.rs | 253 +++++-- feos-core/src/lib.rs | 338 ++++++++- feos-core/src/parameter/chemical_record.rs | 8 +- feos-core/src/parameter/mod.rs | 48 +- feos-core/src/parameter/model_record.rs | 44 +- feos-core/src/parameter/segment.rs | 25 +- feos-core/src/phase_equilibria/bubble_dew.rs | 80 ++- feos-core/src/phase_equilibria/mod.rs | 112 +-- .../phase_equilibria/phase_diagram_binary.rs | 83 ++- .../phase_equilibria/phase_diagram_pure.rs | 9 +- .../src/phase_equilibria/phase_envelope.rs | 10 +- .../phase_equilibria/stability_analysis.rs | 8 +- feos-core/src/phase_equilibria/tp_flash.rs | 11 +- feos-core/src/phase_equilibria/vle_pure.rs | 52 +- feos-core/src/python/cubic.rs | 9 +- feos-core/src/python/joback.rs | 54 +- feos-core/src/python/parameter.rs | 40 +- feos-core/src/python/phase_equilibria.rs | 14 - feos-core/src/python/state.rs | 35 +- feos-core/src/python/user_defined.rs | 116 ++- feos-core/src/state/builder.rs | 97 ++- feos-core/src/state/critical_point.rs | 79 +- feos-core/src/state/mod.rs | 216 +++--- feos-core/src/state/properties.rs | 678 ++---------------- feos-core/src/state/residual_properties.rs | 439 ++++++++++++ feos-core/src/state/statevec.rs | 103 +++ feos-derive/src/components.rs | 59 ++ feos-derive/src/dft.rs | 22 - feos-derive/src/ideal_gas.rs | 41 ++ feos-derive/src/lib.rs | 30 +- feos-derive/src/residual.rs | 175 +++++ feos-dft/CHANGELOG.md | 12 + feos-dft/src/adsorption/mod.rs | 100 +-- feos-dft/src/adsorption/pore.rs | 19 +- feos-dft/src/functional.rs | 328 ++------- feos-dft/src/lib.rs | 1 + feos-dft/src/pdgt.rs | 21 +- feos-dft/src/{profile.rs => profile/mod.rs} | 220 +----- feos-dft/src/profile/properties.rs | 397 ++++++++++ feos-dft/src/python/adsorption/mod.rs | 5 - feos-dft/src/python/profile.rs | 5 - src/dft.rs | 4 +- src/eos.rs | 19 +- src/estimator/binary_vle.rs | 23 +- src/estimator/dataset.rs | 6 +- src/estimator/diffusion.rs | 4 +- src/estimator/estimator.rs | 8 +- src/estimator/liquid_density.rs | 7 +- src/estimator/thermal_conductivity.rs | 4 +- src/estimator/vapor_pressure.rs | 4 +- src/estimator/viscosity.rs | 4 +- src/gc_pcsaft/dft/mod.rs | 20 +- src/gc_pcsaft/dft/parameter.rs | 10 +- src/gc_pcsaft/eos/mod.rs | 20 +- src/gc_pcsaft/eos/parameter.rs | 37 +- src/gc_pcsaft/python/mod.rs | 10 +- src/hard_sphere/dft.rs | 21 +- src/lib.rs | 4 +- src/pcsaft/dft/mod.rs | 28 +- src/pcsaft/eos/mod.rs | 58 +- src/pcsaft/eos/qspr.rs | 132 ---- src/pcsaft/parameters.rs | 37 +- src/pcsaft/python.rs | 7 +- src/pets/dft/mod.rs | 30 +- src/pets/eos/mod.rs | 479 ++++++------- src/pets/eos/qspr.rs | 122 ---- src/pets/parameters.rs | 28 +- src/pets/python.rs | 7 +- src/python/cubic.rs | 2 - src/python/dft.rs | 64 +- src/python/eos.rs | 117 ++- src/python/ideal_gas.rs | 8 + src/python/mod.rs | 4 + src/saftvrqmie/dft/mod.rs | 28 +- src/saftvrqmie/eos/mod.rs | 13 +- src/saftvrqmie/parameters.rs | 23 +- src/saftvrqmie/python.rs | 10 +- src/uvtheory/eos/mod.rs | 36 +- src/uvtheory/parameters.rs | 17 +- src/uvtheory/python.rs | 4 +- tests/pcsaft/dft.rs | 22 +- tests/pcsaft/properties.rs | 2 +- tests/pcsaft/state_creation_mixture.rs | 34 +- tests/pcsaft/state_creation_pure.rs | 96 ++- tests/pcsaft/test_parameters_joback.json | 135 ++++ 101 files changed, 4100 insertions(+), 3279 deletions(-) delete mode 100644 feos-core/src/equation_of_state.rs create mode 100644 feos-core/src/equation_of_state/helmholtz_energy.rs create mode 100644 feos-core/src/equation_of_state/ideal_gas.rs create mode 100644 feos-core/src/equation_of_state/mod.rs create mode 100644 feos-core/src/equation_of_state/residual.rs create mode 100644 feos-core/src/state/residual_properties.rs create mode 100644 feos-core/src/state/statevec.rs create mode 100644 feos-derive/src/components.rs create mode 100644 feos-derive/src/ideal_gas.rs create mode 100644 feos-derive/src/residual.rs rename feos-dft/src/{profile.rs => profile/mod.rs} (61%) create mode 100644 feos-dft/src/profile/properties.rs delete mode 100644 src/pcsaft/eos/qspr.rs delete mode 100644 src/pets/eos/qspr.rs create mode 100644 src/python/ideal_gas.rs create mode 100644 tests/pcsaft/test_parameters_joback.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ba4edacde..3d4d9932f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added `IdealGasModel` enum that collects all implementors of the `IdealGas` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Added `feos.ideal_gas` module in Python from which (currently) `Joback` and `JobackParameters` are available. [#158](https://github.com/feos-org/feos/pull/158) + ### Changed - Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) - To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) - Added the possibility to specify a pure component correction parameter `phi` for the heterosegmented gc PC-SAFT equation of state. [#157](https://github.com/feos-org/feos/pull/157) +- Renamed `EosVariant` to `ResidualModel`. [#158](https://github.com/feos-org/feos/pull/158) +- Added methods to add an ideal gas contribution to an initialized equation of state object in Python. [#158](https://github.com/feos-org/feos/pull/158) ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/benches/contributions.rs b/benches/contributions.rs index 268ad6681..a572ed597 100644 --- a/benches/contributions.rs +++ b/benches/contributions.rs @@ -8,7 +8,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{DensityInitialization, Derivative, EquationOfState, State}; +use feos_core::{DensityInitialization, Derivative, Residual, State}; use ndarray::arr1; use quantity::si::*; use std::sync::Arc; diff --git a/benches/dual_numbers.rs b/benches/dual_numbers.rs index f78ca03f7..9b447a0b2 100644 --- a/benches/dual_numbers.rs +++ b/benches/dual_numbers.rs @@ -7,7 +7,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Derivative, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, State, StateHD, + Derivative, HelmholtzEnergy, HelmholtzEnergyDual, Residual, State, StateHD, }; use ndarray::{arr1, Array}; use num_dual::DualNum; @@ -28,7 +28,7 @@ fn state_pcsaft(parameters: PcSaftParameters) -> State { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res + Copy, E: EquationOfState>(inp: (&Arc, &StateHD)) -> D +fn a_res + Copy, E: Residual>(inp: (&Arc, &StateHD)) -> D where (dyn HelmholtzEnergy + 'static): HelmholtzEnergyDual, { @@ -36,7 +36,7 @@ where } /// Benchmark for evaluation of the Helmholtz energy for different dual number types. -fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { +fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { let mut group = c.benchmark_group(group_name); group.bench_function("a_f64", |b| { b.iter(|| a_res((&state.eos, &state.derive0()))) diff --git a/benches/state_creation.rs b/benches/state_creation.rs index 68c7b0904..b79627f32 100644 --- a/benches/state_creation.rs +++ b/benches/state_creation.rs @@ -2,14 +2,14 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, + Contributions, DensityInitialization, PhaseEquilibrium, Residual, State, }; use ndarray::{Array, Array1}; use quantity::si::*; use std::sync::Arc; /// Evaluate NPT constructor -fn npt( +fn npt( (eos, t, p, n, rho0): ( &Arc, SINumber, @@ -22,26 +22,26 @@ fn npt( } /// Evaluate critical point constructor -fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { +fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { State::critical_point(eos, n, None, Default::default()).unwrap(); } /// Evaluate critical point constructor for binary systems at given T or p -fn critical_point_binary((eos, tp): (&Arc, SINumber)) { +fn critical_point_binary((eos, tp): (&Arc, SINumber)) { State::critical_point_binary(eos, tp, None, None, Default::default()).unwrap(); } /// VLE for pure substance for given temperature or pressure -fn pure((eos, t_or_p): (&Arc, SINumber)) { +fn pure((eos, t_or_p): (&Arc, SINumber)) { PhaseEquilibrium::pure(eos, t_or_p, None, Default::default()).unwrap(); } /// Evaluate temperature, pressure flash. -fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { +fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { PhaseEquilibrium::tp_flash(eos, t, p, feed, None, Default::default(), None).unwrap(); } -fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { +fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::bubble_point( eos, t, @@ -53,7 +53,7 @@ fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1((eos, t, y): (&Arc, SINumber, &Array1)) { +fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::dew_point( eos, t, @@ -65,7 +65,7 @@ fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) .unwrap(); } -fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { +fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { let ncomponents = eos.components(); let x = Array::from_elem(ncomponents, 1.0 / ncomponents as f64); let n = &x * 100.0 * MOL; diff --git a/benches/state_properties.rs b/benches/state_properties.rs index 06c419fcb..43f0d2587 100644 --- a/benches/state_properties.rs +++ b/benches/state_properties.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, EquationOfState, State, + Contributions, Residual, State, }; use ndarray::arr1; use quantity::si::*; @@ -12,7 +12,7 @@ type S = State; /// Evaluate a property of a state given the EoS, the property to compute, /// temperature, volume, moles, and the contributions to consider. -fn property, Contributions) -> T>( +fn property, Contributions) -> T>( (eos, property, t, v, n, contributions): ( &Arc, F, @@ -28,7 +28,7 @@ fn property, Contributions) -> T>( /// Evaluate a property with of a state given the EoS, the property to compute, /// temperature, volume, moles. -fn property_no_contributions) -> T>( +fn property_no_contributions) -> T>( (eos, property, t, v, n): (&Arc, F, SINumber, SINumber, &SIArray1), ) -> T { let state = State::new_nvt(eos, t, v, n).unwrap(); @@ -52,7 +52,7 @@ fn properties_pcsaft(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_methane_ethane_propane"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -61,19 +61,10 @@ fn properties_pcsaft(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } @@ -94,7 +85,7 @@ fn properties_pcsaft_polar(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_polar"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -103,19 +94,10 @@ fn properties_pcsaft_polar(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } diff --git a/docs/theory/eos/properties.md b/docs/theory/eos/properties.md index 5826c5db6..ae1486042 100644 --- a/docs/theory/eos/properties.md +++ b/docs/theory/eos/properties.md @@ -42,7 +42,7 @@ $$X^\mathrm{res,p}=\mathcal{D}\left(A\right)-\mathcal{D}\left(A^\mathrm{ig,p}\ri For linear operators $\mathcal{D}$ eqs. {eq}`eqn:a_ig` and {eq}`eqn:a_res` can be used to simplify the expression -$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A\right)-\mathcal{D}\left(nRT\ln Z\right)$$ +$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A^\mathrm{ig,V}\right)-\mathcal{D}\left(nRT\ln Z\right)$$ with the compressiblity factor $Z=\frac{pV}{nRT}$. @@ -52,62 +52,69 @@ For details on how the evaluation of properties from Helmholtz energy models is The table below lists all properties that are available in $\text{FeO}_\text{s}$, their definition, and whether they can be evaluated as residual contributions as well. -| Name | definition | residual? | -|-|:-:|-| -| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | yes | -| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | yes | -| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | yes | -| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | yes | -| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | yes | -| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | -| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | -| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | -| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | -| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | -| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | -| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | -| Molar entropy $s$ | $\frac{S}{n}$ | yes | -| Enthalpy $H$ | $A+TS+pV$ | yes | -| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | -| Helmholtz energy $A$ | | yes | -| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | -| Internal energy $U$ | $A+TS$ | yes | -| Molar internal energy $u$ | $\frac{U}{n}$ | yes | -| Gibbs energy $G$ | $A+pV$ | yes | -| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | -| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | no | -| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | no | -| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | -| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | +In general, the evaluation of (total) Helmholtz energies and their derivatives requires a model for the residual Helmholtz energy and a model for the ideal gas contribution, specifically for the temperature dependence of the thermal de Broglie wavelength $\Lambda_i$. However, for many properties like the pressure including its derivatives and fugacity coefficients, the de Broglie wavelength cancels out. + +Due to different language paradigms, $\text{FeO}_\text{s}$ handles the ideal gas term slightly different in Rust and Python. +- In **Rust**, if no ideal gas model is provided, users can only evaluate properties for which no ideal gas model is required because the de Broglie wavelength cancels. For those properties that require an ideal gas model but the table below indicates that they can be evaluated as residual, extra functions are provided. +- In **Python**, no additional functions are required, instead the property evaluation will throw an exception if an ideal gas contribution is required but not provided. + +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | no | yes | +| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | no | yes | +| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | no | yes | +| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | no | yes | +| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | no | yes | +| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | no | no | +| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | no | yes | +| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | no | +| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | no | +| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | no | +| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | no | +| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | yes | +| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Second partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,n_i}$ | yes | yes | +| Molar entropy $s$ | $\frac{S}{n}$ | yes | yes +| Enthalpy $H$ | $A+TS+pV$ | yes | yes | +| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | yes | +| Helmholtz energy $A$ | | yes | yes | +| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | yes | +| Internal energy $U$ | $A+TS$ | yes | yes | +| Molar internal energy $u$ | $\frac{U}{n}$ | yes | yes | +| Gibbs energy $G$ | $A+pV$ | yes | yes | +| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | yes | +| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | yes | no | +| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | yes | no | +| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | no | +| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | no | ## Additional properties for fluids with known molar weights If the Helmholtz energy model includes information about the molar weigt $MW_i$ of each species, additional properties are available in $\text{FeO}_\text{s}$ -| Name | definition | residual? | -|-|:-:|-| -| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | -| Mass of each component $m_i$ | $n_iMW_i$ | no | -| Total mass $m$ | $\sum_im_i=nMW$ | no | -| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | -| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | -| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | -| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | -| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | -| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | -| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | -| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | no | \ No newline at end of file +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | no | +| Mass of each component $m_i$ | $n_iMW_i$ | no | no | +| Total mass $m$ | $\sum_im_i=nMW$ | no | no | +| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | no | +| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | no | +| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | yes | +| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | yes | +| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | yes | +| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | yes | +| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | yes | +| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | yes | no | \ No newline at end of file diff --git a/examples/core_user_defined_eos.ipynb b/examples/core_user_defined_eos.ipynb index 2c67ffc0e..1d8fe99ca 100644 --- a/examples/core_user_defined_eos.ipynb +++ b/examples/core_user_defined_eos.ipynb @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -218,9 +218,16 @@ "\n", "# create an instance of our python class and hand it over to rust\n", "pr = PyPengRobinson(tc, pc, omega, molar_weight)\n", - "eos = EquationOfState.python(pr)" + "eos = EquationOfState.python_residual(pr)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -234,7 +241,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -309,7 +316,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -321,7 +328,7 @@ "1.6605390671738466e-24 mol" ] }, - "execution_count": 23, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -334,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -346,7 +353,7 @@ "1 mol" ] }, - "execution_count": 24, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -370,14 +377,40 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "thread '' panicked at 'No ideal gas model initialized!', src/eos.rs:" + ] + }, + { + "ename": "PanicException", + "evalue": "No ideal gas model initialized!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mPanicException\u001b[0m Traceback (most recent call last)", + "Input \u001b[0;32mIn [9]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m s_ph \u001b[38;5;241m=\u001b[39m \u001b[43mState\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43meos\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mpressure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mBAR\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43ms_pt\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mContributions\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mResidual\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m)\u001b[49m\n", + "\u001b[0;31mPanicException\u001b[0m: No ideal gas model initialized!" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "49:10\n" + ] + } + ], "source": [ "s_ph = State(\n", " eos, \n", " pressure=1*BAR, \n", - " molar_enthalpy=s_pt.molar_enthalpy()\n", + " molar_enthalpy=s_pt.molar_enthalpy(Contributions.Residual)\n", ")" ] }, @@ -1248,7 +1281,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1262,7 +1295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/examples/pcsaft_state.ipynb b/examples/pcsaft_state.ipynb index 1bcc42050..4a631e857 100644 --- a/examples/pcsaft_state.ipynb +++ b/examples/pcsaft_state.ipynb @@ -99,6 +99,118 @@ "state_nvt" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([-1.52564778]), array([-1.52564778]))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "state_nvt.chemical_potential(Contributions.ResidualNvt) / RGAS / state_nvt.temperature - np.log(state_nvt.compressibility()), state_nvt.ln_phi()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.07682387846270378] K^-1" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beta = 1 / (RGAS * state_nvt.temperature)\n", + "a1 = - state_nvt.chemical_potential(Contributions.ResidualNvt) * beta / state_nvt.temperature\n", + "a2 = beta * state_nvt.dmu_dt(Contributions.ResidualNvt)\n", + "a3 = 1 / state_nvt.temperature\n", + "a4 = - state_nvt.dp_dt() / state_nvt.pressure()\n", + "a1 + a2 + a3 #+ a4" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.022509944563452417 K^-1,\n", + " [0.05098226639966782] K^-1,\n", + " 3.331667499583542e-3 K^-1,\n", + " -6.476301239431868 K^-1)" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a1, a2, a3, a4" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([0.0001330105610385462] m³/mol, [0.0001330105610385462] m³/mol)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.partial_molar_volume(), -state_nvt.dp_dni() / state_nvt.dp_dv()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-3.69618646800518 nPa,\n", + " 100.37302562357485 kPa,\n", + " 100.37302562357485 kPa,\n", + " 18.76231432566827 MPa)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.pressure(Contributions.ResidualNpt), state_nvt.pressure(Contributions.ResidualNvt) + state_nvt.pressure(Contributions.IdealGas), state_nvt.pressure(Contributions.Total), state_nvt.pressure(Contributions.IdealGas)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -800,7 +912,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -814,7 +926,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" }, "vscode": { "interpreter": { diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index 7c14acda2..d44de111c 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -5,6 +5,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Added `Components`, `Residual`, `IdealGas` and `DeBroglieWavelength` traits to decouple ideal gas models from residual models. [#158](https://github.com/feos-org/feos/pull/158) +- Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/) + +### Changed +- Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/) +- The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/) +- Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/) +- State creation routines are split into those that can be used with `Residual` and those that require `Residual + IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is renamed to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) +- Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/) +- Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) +- Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/) + +### Removed +- Removed `EquationOfState` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Removed ideal gas dependencies from `PureRecord` and `SegmentRecord`. [#158](https://github.com/feos-org/feos/pull/) +- Removed Python getter and setter functions and optional arguments for ideal gas records in macros. [#158](https://github.com/feos-org/feos/pull/) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index f0dc05862..be399d67d 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -4,10 +4,7 @@ //! of state - with a single contribution to the Helmholtz energy - can be implemented. //! The implementation closely follows the form of the equations given in //! [this wikipedia article](https://en.wikipedia.org/wiki/Cubic_equations_of_state#Peng%E2%80%93Robinson_equation_of_state). -use crate::equation_of_state::{ - EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, -}; -use crate::joback::{Joback, JobackRecord}; +use crate::equation_of_state::{Components, HelmholtzEnergy, HelmholtzEnergyDual, Residual}; use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord}; use crate::si::{GRAM, MOL}; use crate::state::StateHD; @@ -64,9 +61,7 @@ pub struct PengRobinsonParameters { /// Molar weight in units of g/mol molarweight: Array1, /// List of pure component records - pure_records: Vec>, - /// List of ideal gas Joback records - joback_records: Option>, + pure_records: Vec>, } impl std::fmt::Display for PengRobinsonParameters { @@ -102,7 +97,7 @@ impl PengRobinsonParameters { acentric_factor: acentric_factor[i], }; let id = Identifier::default(); - PureRecord::new(id, molarweight[i], record, None) + PureRecord::new(id, molarweight[i], record) }) .collect(); Ok(PengRobinsonParameters::from_records( @@ -114,12 +109,11 @@ impl PengRobinsonParameters { impl Parameter for PengRobinsonParameters { type Pure = PengRobinsonRecord; - type IdealGas = JobackRecord; type Binary = f64; /// Creates parameters from pure component records. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -139,11 +133,6 @@ impl Parameter for PengRobinsonParameters { kappa[i] = 0.37464 + (1.54226 - 0.26992 * r.acentric_factor) * r.acentric_factor; } - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { tc, a, @@ -152,16 +141,10 @@ impl Parameter for PengRobinsonParameters { kappa, molarweight, pure_records, - joback_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.k_ij) } } @@ -207,8 +190,6 @@ impl fmt::Display for PengRobinsonContribution { pub struct PengRobinson { /// Parameters parameters: Arc, - /// Ideal gas contributions to the Helmholtz energy - ideal_gas: Joback, /// Non-ideal contributions to the Helmholtz energy contributions: Vec>, } @@ -216,23 +197,24 @@ pub struct PengRobinson { impl PengRobinson { /// Create a new equation of state from a set of parameters. pub fn new(parameters: Arc) -> Self { - let ideal_gas = parameters.joback_records.as_ref().map_or_else( - || Joback::default(parameters.tc.len()), - |j| Joback::new(j.clone()), - ); let contributions: Vec> = vec![Box::new(PengRobinsonContribution { parameters: parameters.clone(), })]; Self { parameters, - ideal_gas, contributions, } } } -impl EquationOfState for PengRobinson { +impl fmt::Display for PengRobinson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Peng Robinson") + } +} + +impl Components for PengRobinson { fn components(&self) -> usize { self.parameters.b.len() } @@ -240,19 +222,17 @@ impl EquationOfState for PengRobinson { fn subset(&self, component_list: &[usize]) -> Self { Self::new(Arc::new(self.parameters.subset(component_list))) } +} +impl Residual for PengRobinson { fn compute_max_density(&self, moles: &Array1) -> f64 { let b = (moles * &self.parameters.b).sum() / moles.sum(); 0.9 / b } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.ideal_gas - } } impl MolarWeight for PengRobinson { @@ -264,15 +244,13 @@ impl MolarWeight for PengRobinson { #[cfg(test)] mod tests { use super::*; - use crate::phase_equilibria::SolverOptions; - use crate::state::State; - use crate::Contributions; - use crate::{EosResult, Verbosity}; + use crate::state::{Contributions, State}; + use crate::{EosResult, SolverOptions, Verbosity}; use approx::*; use quantity::si::*; use std::sync::Arc; - fn pure_record_vec() -> Vec> { + fn pure_record_vec() -> Vec> { let records = r#"[ { "identifier": { diff --git a/feos-core/src/density_iteration.rs b/feos-core/src/density_iteration.rs index 54c236274..541362493 100644 --- a/feos-core/src/density_iteration.rs +++ b/feos-core/src/density_iteration.rs @@ -1,11 +1,11 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::State; use crate::EosUnit; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::sync::Arc; -pub fn density_iteration( +pub fn density_iteration( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -144,7 +144,7 @@ pub fn density_iteration( } } -fn pressure_spinodal( +fn pressure_spinodal( eos: &Arc, temperature: SINumber, rho_init: SINumber, diff --git a/feos-core/src/equation_of_state.rs b/feos-core/src/equation_of_state.rs deleted file mode 100644 index 6d7b523fc..000000000 --- a/feos-core/src/equation_of_state.rs +++ /dev/null @@ -1,343 +0,0 @@ -use crate::errors::{EosError, EosResult}; -use crate::state::StateHD; -use crate::EosUnit; -use ndarray::prelude::*; -use num_dual::{ - first_derivative, second_derivative, third_derivative, Dual, Dual2, Dual2_64, Dual3, Dual3_64, - Dual64, DualNum, DualSVec64, HyperDual, HyperDual64, -}; -use num_traits::Zero; -use quantity::si::{SIArray1, SINumber, SIUnit}; -use std::fmt; - -/// Individual Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [HelmholtzEnergy] -/// so that the implementor can be used as a Helmholtz energy -/// contribution in the equation of state. -pub trait HelmholtzEnergyDual> { - /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. - fn helmholtz_energy(&self, state: &StateHD) -> D; -} - -/// Object safe version of the [HelmholtzEnergyDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait HelmholtzEnergy: - HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -impl HelmholtzEnergy for T where - T: HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -/// Ideal gas Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [IdealGasContribution] -/// so that the implementor can be used as an ideal gas -/// contribution in the equation of state. -pub trait IdealGasContributionDual + Copy> { - /// The thermal de Broglie wavelength of each component in the form $\ln\left(\frac{\Lambda^3}{\AA^3}\right)$ - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1; - - /// Evaluate the ideal gas contribution for a given state. - /// - /// In some cases it could be advantageous to overwrite this - /// implementation instead of implementing the de Broglie - /// wavelength. - fn evaluate(&self, state: &StateHD) -> D { - let lambda = self.de_broglie_wavelength(state.temperature, state.moles.len()); - ((lambda - + state.partial_density.mapv(|x| { - if x.re() == 0.0 { - D::from(0.0) - } else { - x.ln() - 1.0 - } - })) - * &state.moles) - .sum() - } -} - -/// Object safe version of the [IdealGasContributionDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait IdealGasContribution: - IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -impl IdealGasContribution for T where - T: IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -struct DefaultIdealGasContribution; -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) - } -} - -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") - } -} - -/// Molar weight of all components. -/// -/// The trait is required to be able to calculate (mass) -/// specific properties. -pub trait MolarWeight { - fn molar_weight(&self) -> SIArray1; -} - -/// A general equation of state. -pub trait EquationOfState: Send + Sync { - /// Return the number of components of the equation of state. - fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; - - /// Return the maximum density in Angstrom^-3. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn compute_max_density(&self, moles: &Array1) -> f64; - - /// Return a slice of the individual contributions (excluding the ideal gas) - /// of the equation of state. - fn residual(&self) -> &[Box]; - - /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. - fn evaluate_residual + Copy>(&self, state: &StateHD) -> D - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| c.helmholtz_energy(state)) - .sum() - } - - /// Evaluate the reduced Helmholtz energy of each individual contribution - /// and return them together with a string representation of the contribution. - fn evaluate_residual_contributions + Copy>( - &self, - state: &StateHD, - ) -> Vec<(String, D)> - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| (c.to_string(), c.helmholtz_energy(state))) - .collect() - } - - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of enthalpies) this function - /// has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution - } - - /// Check if the provided optional mole number is consistent with the - /// equation of state. - /// - /// In general, the number of elements in `moles` needs to match the number - /// of components of the equation of state. For a pure component, however, - /// no moles need to be provided. In that case, it is set to the constant - /// reference value. - fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { - let l = moles.map_or(1, |m| m.len()); - if self.components() == l { - match moles { - Some(m) => Ok(m.to_owned()), - None => Ok(Array::ones(1) * SIUnit::reference_moles()), - } - } else { - Err(EosError::IncompatibleComponents(self.components(), l)) - } - } - - /// Calculate the maximum density. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { - let mr = self - .validate_moles(moles)? - .to_reduced(SIUnit::reference_moles())?; - Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) - } - - /// Calculate the second virial coefficient $B(T)$ - fn second_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, b) = second_derivative(a_res, 0.0); - Ok(b * 0.5 / SIUnit::reference_density()) - } - - /// Calculate the third virial coefficient $C(T)$ - fn third_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, _, c) = third_derivative(a_res, 0.0); - Ok(c / 3.0 / SIUnit::reference_density().powi(2)) - } - - /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ - fn second_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let b = |t| { - let a_res = |rho: Dual2| { - self.evaluate_residual(&StateHD::new_virial(Dual2::from_re(t), rho, x)) - }; - let (_, _, b) = second_derivative(a_res, Dual64::zero()); - b - }; - let (_, b_t) = first_derivative(b, t); - Ok(b_t * 0.5 / (SIUnit::reference_density() * SIUnit::reference_temperature())) - } - - /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - fn third_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let c = |t| { - let a_res = - |rho| self.evaluate_residual(&StateHD::new_virial(Dual3::from_re(t), rho, x)); - let (_, _, _, c) = third_derivative(a_res, Dual64::zero()); - c - }; - let (_, c_t) = first_derivative(c, t); - Ok(c_t / 3.0 / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) - } -} - -/// Reference values and residual entropy correlations for entropy scaling. -pub trait EntropyScaling { - fn viscosity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; -} diff --git a/feos-core/src/equation_of_state/helmholtz_energy.rs b/feos-core/src/equation_of_state/helmholtz_energy.rs new file mode 100644 index 000000000..11d2c2703 --- /dev/null +++ b/feos-core/src/equation_of_state/helmholtz_energy.rs @@ -0,0 +1,59 @@ +use crate::StateHD; +use num_dual::*; +use std::fmt; + +/// Individual Helmholtz energy contribution that can +/// be evaluated using generalized (hyper) dual numbers. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [HelmholtzEnergy] +/// so that the implementor can be used as a Helmholtz energy +/// contribution in the equation of state. +pub trait HelmholtzEnergyDual> { + /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. + fn helmholtz_energy(&self, state: &StateHD) -> D; +} + +/// Object safe version of the [HelmholtzEnergyDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. +pub trait HelmholtzEnergy: + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl HelmholtzEnergy for T where + T: HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs new file mode 100644 index 000000000..12a2653be --- /dev/null +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -0,0 +1,90 @@ +use super::Components; +use crate::StateHD; +use ndarray::Array1; +use num_dual::DualNum; +use num_dual::*; +use std::fmt; + +/// Ideal gas Helmholtz energy contribution. +pub trait IdealGas: Components + Sync + Send { + // Return a reference to the implementation of the de Broglie wavelength. + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength; + + /// Evaluate the ideal gas Helmholtz energy contribution for a given state. + /// + /// In some cases it could be advantageous to overwrite this + /// implementation instead of implementing the de Broglie + /// wavelength. + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D + where + for<'a> dyn DeBroglieWavelength + 'a: DeBroglieWavelengthDual, + { + let ln_lambda3 = self.ideal_gas_model().ln_lambda3(state.temperature); + ((ln_lambda3 + + state.partial_density.mapv(|x| { + if x.re() == 0.0 { + D::from(0.0) + } else { + x.ln() - 1.0 + } + })) + * &state.moles) + .sum() + } +} + +/// Implementation of an ideal gas model in terms of the +/// logarithm of the cubic thermal de Broglie wavelength +/// in units ln(A³). +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [DeBroglieWavelength] +/// so that the implementor can be used as an ideal gas +/// contribution in the equation of state. +pub trait DeBroglieWavelengthDual> { + fn ln_lambda3(&self, temperature: D) -> Array1; +} + +/// Object safe version of the [DeBroglieWavelengthDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. +pub trait DeBroglieWavelength: + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl DeBroglieWavelength for T where + T: DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs new file mode 100644 index 000000000..bb7b237cb --- /dev/null +++ b/feos-core/src/equation_of_state/mod.rs @@ -0,0 +1,128 @@ +use crate::EosResult; +use ndarray::Array1; +use quantity::si::{SIArray1, SINumber}; +use std::sync::Arc; + +mod helmholtz_energy; +mod ideal_gas; +mod residual; + +pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; +pub use ideal_gas::{DeBroglieWavelength, DeBroglieWavelengthDual, IdealGas}; +pub use residual::{EntropyScaling, Residual}; + +/// Molar weight of all components. +/// +/// The trait is required to be able to calculate (mass) +/// specific properties. +pub trait MolarWeight { + fn molar_weight(&self) -> SIArray1; +} + +/// The number of components that the model is initialized for. +pub trait Components { + /// Return the number of components of the model. + fn components(&self) -> usize; + + /// Return a model consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; +} + +/// An equation of state consisting of an ideal gas model +/// and a residual Helmholtz energy model. +#[derive(Clone)] +pub struct EquationOfState { + pub ideal_gas: Arc, + pub residual: Arc, +} + +impl EquationOfState { + /// Return a new [EquationOfState] with the given ideal gas + /// and residual models. + pub fn new(ideal_gas: Arc, residual: Arc) -> Self { + Self { + ideal_gas, + residual, + } + } +} + +impl Components for EquationOfState { + fn components(&self) -> usize { + assert_eq!( + self.residual.components(), + self.ideal_gas.components(), + "residual and ideal gas model differ in the number of components" + ); + self.residual.components() + } + + fn subset(&self, component_list: &[usize]) -> Self { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list)), + ) + } +} + +impl IdealGas for EquationOfState { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self.ideal_gas.ideal_gas_model() + } +} + +impl Residual for EquationOfState { + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } + + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } +} + +impl MolarWeight for EquationOfState { + fn molar_weight(&self) -> SIArray1 { + self.residual.molar_weight() + } +} + +impl EntropyScaling for EquationOfState { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .viscosity_reference(temperature, volume, moles) + } + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.viscosity_correlation(s_res, x) + } + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .diffusion_reference(temperature, volume, moles) + } + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.diffusion_correlation(s_res, x) + } + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .thermal_conductivity_reference(temperature, volume, moles) + } + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.thermal_conductivity_correlation(s_res, x) + } +} diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs new file mode 100644 index 000000000..6746c3718 --- /dev/null +++ b/feos-core/src/equation_of_state/residual.rs @@ -0,0 +1,172 @@ +use super::{Components, HelmholtzEnergy, HelmholtzEnergyDual}; +use crate::StateHD; +use crate::{EosError, EosResult, EosUnit}; +use ndarray::prelude::*; +use num_dual::*; +use num_traits::{One, Zero}; +use quantity::si::{SIArray1, SINumber, SIUnit}; + +/// A reisdual Helmholtz energy model. +pub trait Residual: Components + Send + Sync { + /// Return the maximum density in Angstrom^-3. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn compute_max_density(&self, moles: &Array1) -> f64; + + /// Return a slice of the individual contributions (excluding the ideal gas) + /// of the equation of state. + fn contributions(&self) -> &[Box]; + + /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. + fn evaluate_residual + Copy>(&self, state: &StateHD) -> D + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| c.helmholtz_energy(state)) + .sum() + } + + /// Evaluate the reduced Helmholtz energy of each individual contribution + /// and return them together with a string representation of the contribution. + fn evaluate_residual_contributions + Copy>( + &self, + state: &StateHD, + ) -> Vec<(String, D)> + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| (c.to_string(), c.helmholtz_energy(state))) + .collect() + } + + /// Check if the provided optional mole number is consistent with the + /// equation of state. + /// + /// In general, the number of elements in `moles` needs to match the number + /// of components of the equation of state. For a pure component, however, + /// no moles need to be provided. In that case, it is set to the constant + /// reference value. + fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { + let l = moles.map_or(1, |m| m.len()); + if self.components() == l { + match moles { + Some(m) => Ok(m.to_owned()), + None => Ok(Array::ones(1) * SIUnit::reference_moles()), + } + } else { + Err(EosError::IncompatibleComponents(self.components(), l)) + } + } + + /// Calculate the maximum density. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { + let mr = self + .validate_moles(moles)? + .to_reduced(SIUnit::reference_moles())?; + Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) + } + + /// Calculate the second virial coefficient $B(T)$ + fn second_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual64::zero(); + rho.eps1 = 1.0; + rho.eps2 = 1.0; + let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2 * 0.5 / SIUnit::reference_density()) + } + + /// Calculate the third virial coefficient $C(T)$ + fn third_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3_64::zero().derivative(); + let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) + } + + /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ + fn second_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual::zero(); + rho.eps1 = Dual64::one(); + rho.eps2 = Dual64::one(); + let t = HyperDual::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2.eps * 0.5 + / (SIUnit::reference_density() * SIUnit::reference_temperature())) + } + + /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ + fn third_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3::zero().derivative(); + let t = Dual3::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3.eps + / 3.0 + / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) + } +} + +/// Reference values and residual entropy correlations for entropy scaling. +pub trait EntropyScaling { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; +} diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index b8d73e90e..d1a6bdb85 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,17 +1,16 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). +use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; use crate::parameter::*; -use crate::{ - EosResult, EosUnit, EquationOfState, HelmholtzEnergy, IdealGasContribution, - IdealGasContributionDual, -}; +use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; -use ndarray::Array1; +use ndarray::{Array, Array1, Array2}; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt; +use std::sync::Arc; /// Coefficients used in the Joback model. /// @@ -65,99 +64,186 @@ impl> FromSegments for JobackRecord { } } +/// Parameters for one or more components for the Joback and Reid model. +pub struct JobackParameters { + a: Array1, + b: Array1, + c: Array1, + d: Array1, + e: Array1, + pure_records: Vec>, + binary_records: Array2, +} + +impl Parameter for JobackParameters { + type Pure = JobackRecord; + type Binary = JobackBinaryRecord; + + fn from_records( + pure_records: Vec>, + _binary_records: Array2, + ) -> Self { + let n = pure_records.len(); + + let binary_records = Array::from_elem((n, n), JobackBinaryRecord); + let mut a = Array::zeros(n); + let mut b = Array::zeros(n); + let mut c = Array::zeros(n); + let mut d = Array::zeros(n); + let mut e = Array::zeros(n); + + for (i, record) in pure_records.iter().enumerate() { + let r = &record.model_record; + a[i] = r.a; + b[i] = r.b; + c[i] = r.c; + d[i] = r.d; + e[i] = r.e; + } + + Self { + a, + b, + c, + d, + e, + pure_records, + binary_records, + } + } + + fn records(&self) -> (&[PureRecord], &Array2) { + (&self.pure_records, &self.binary_records) + } +} + +/// Dummy implementation to satisfy traits for parameter handling. +/// Not intended to be used. +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct JobackBinaryRecord; + +impl From for JobackBinaryRecord { + fn from(_: f64) -> Self { + Self + } +} + +impl From for f64 { + fn from(_: JobackBinaryRecord) -> Self { + 0.0 // nasty hack - panic crashes Ipython kernel, actual value is never used + } +} + +impl> FromSegmentsBinary for JobackBinaryRecord { + fn from_segments_binary(_segments: &[(Self, T, T)]) -> Result { + Err(ParameterError::IncompatibleParameters( + "No binary interaction parameters implemented for Joback".to_string(), + )) + } +} + +impl std::fmt::Display for JobackBinaryRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + /// The ideal gas contribution according to /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -#[derive(Debug, Clone)] +/// +/// The (cubic) de Broglie wavelength is calculated by integrating +/// the heat capacity with the following reference values: +/// +/// - T = 289.15 K +/// - p = 1e5 Pa +/// - V = 1e-30 A³ pub struct Joback { - pub records: Vec, + pub parameters: Arc, } impl Joback { /// Creates a new Joback contribution. - pub fn new(records: Vec) -> Self { - Self { records } - } - - /// Creates a default ($c_p^\mathrm{ig}=0$) ideal gas contribution for the - /// given number of components. - pub fn default(components: usize) -> Self { - Self::new(vec![JobackRecord::default(); components]) + pub fn new(parameters: Arc) -> Self { + Self { parameters } } /// Directly calculates the ideal gas heat capacity from the Joback model. pub fn c_p(&self, temperature: SINumber, molefracs: &Array1) -> EosResult { let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let mut c_p = 0.0; - for (j, &x) in self.records.iter().zip(molefracs.iter()) { - c_p += x * (j.a + j.b * t + j.c * t.powi(2) + j.d * t.powi(3) + j.e * t.powi(4)); - } + let p = &self.parameters; + let c_p = (molefracs + * &(&p.a + &p.b * t + &p.c * t.powi(2) + &p.d * t.powi(3) + &p.e * t.powi(4))) + .sum(); Ok(c_p / RGAS * SIUnit::gas_constant()) } } -impl fmt::Display for Joback { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (Joback)") +impl Components for Joback { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { + let mut records = Vec::with_capacity(component_list.len()); + component_list + .iter() + .for_each(|&i| records.push(self.parameters.pure_records[i].clone())); + let n = component_list.len(); + Self::new(Arc::new(JobackParameters::from_records( + records, + Array::from_elem((n, n), JobackBinaryRecord), + ))) } } -const RGAS: f64 = 6.022140857 * 1.38064852; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; -const KB: f64 = 1.38064852e-23; +impl IdealGas for Joback { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self + } +} -impl + Copy> IdealGasContributionDual for Joback { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { +impl + Copy> DeBroglieWavelengthDual for Joback { + fn ln_lambda3(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; + let t4 = t2 * t2; let f = (temperature * KB / (P0 * A3)).ln(); - Array1::from_shape_fn(components, |i| { - let j = &self.records[i]; - let h = (t2 - T0 * T0) * 0.5 * j.b - + (t * t2 - T0.powi(3)) * j.c / 3.0 - + (t2 * t2 - T0.powi(4)) * j.d / 4.0 - + (t2 * t2 * t - T0.powi(5)) * j.e / 5.0 + Array1::from_shape_fn(self.parameters.pure_records.len(), |i| { + let j = &self.parameters.pure_records[i].model_record; + let h = (t2 - T0_2) * 0.5 * j.b + + (t * t2 - T0_3) * j.c / 3.0 + + (t4 - T0_4) * j.d / 4.0 + + (t4 * t - T0_5) * j.e / 5.0 + (t - T0) * j.a; let s = (t - T0) * j.b - + (t2 - T0.powi(2)) * 0.5 * j.c - + (t2 * t - T0.powi(3)) * j.d / 3.0 - + (t2 * t2 - T0.powi(4)) * j.e / 4.0 + + (t2 - T0_2) * 0.5 * j.c + + (t2 * t - T0_3) * j.d / 3.0 + + (t4 - T0_4) * j.e / 4.0 + (t / T0).ln() * j.a; (h - t * s) / (t * RGAS) + f }) } } -impl EquationOfState for Joback { - fn components(&self) -> usize { - self.records.len() - } - - fn subset(&self, component_list: &[usize]) -> Self { - let records = component_list - .iter() - .map(|&i| self.records[i].clone()) - .collect(); - Self::new(records) - } - - fn compute_max_density(&self, _moles: &Array1) -> f64 { - 1.0 - } - - fn residual(&self) -> &[Box] { - &[] - } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - self +impl fmt::Display for Joback { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Joback)") } } +const RGAS: f64 = 6.022140857 * 1.38064852; +const T0: f64 = 298.15; +const T0_2: f64 = 298.15 * 298.15; +const T0_3: f64 = T0 * T0_2; +const T0_4: f64 = T0_2 * T0_2; +const T0_5: f64 = T0 * T0_4; +const P0: f64 = 1.0e5; +const A3: f64 = 1e-30; +const KB: f64 = 1.38064852e-23; + #[cfg(test)] mod tests { - use crate::{Contributions, State, StateBuilder}; + use crate::{Contributions, Residual, State, StateBuilder}; use approx::assert_relative_eq; use ndarray::arr1; use quantity::si::*; @@ -165,16 +251,23 @@ mod tests { use super::*; - #[derive(Deserialize, Clone, Debug)] - struct ModelRecord; + // implement Residual to test Joback as equation of state + impl Residual for Joback { + fn compute_max_density(&self, _moles: &Array1) -> f64 { + 1.0 + } + + fn contributions(&self) -> &[Box] { + &[] + } + } #[test] fn paper_example() -> EosResult<()> { let segments_json = r#"[ { "identifier": "-Cl", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": 33.3, "b": -0.0963, "c": 0.000187, @@ -185,8 +278,7 @@ mod tests { }, { "identifier": "-CH=(ring)", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": -2.14, "b": 5.74e-2, "c": -1.64e-6, @@ -197,8 +289,7 @@ mod tests { }, { "identifier": "=CH<(ring)", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": -8.25, "b": 1.01e-1, "c": -1.42e-4, @@ -208,7 +299,7 @@ mod tests { "molarweight": 13.01864 } ]"#; - let segment_records: Vec> = + let segment_records: Vec> = serde_json::from_str(segments_json).expect("Unable to parse json."); let segments = ChemicalRecord::new( Identifier::default(), @@ -230,7 +321,7 @@ mod tests { assert_eq!(segments.get(&segment_records[2]), Some(&2)); let joback_segments: Vec<_> = segments .iter() - .map(|(s, &n)| (s.ideal_gas_record.clone().unwrap(), n)) + .map(|(s, &n)| (s.model_record.clone(), n)) .collect(); let jr = JobackRecord::from_segments(&joback_segments)?; assert_relative_eq!( @@ -255,7 +346,8 @@ mod tests { ); assert_relative_eq!(jr.e, 0.0); - let eos = Arc::new(Joback::new(vec![jr])); + let pr = PureRecord::new(Identifier::default(), 1.0, jr); + let eos = Arc::new(Joback::new(Arc::new(JobackParameters::new_pure(pr)))); let state = State::new_nvt( &eos, 1000.0 * KELVIN, @@ -275,9 +367,18 @@ mod tests { #[test] fn c_p_comparison() -> EosResult<()> { - let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); - let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); - let joback = Arc::new(Joback::new(vec![record1, record2])); + let record1 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005), + ); + let record2 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001), + ); + let parameters = Arc::new(JobackParameters::new_binary(vec![record1, record2], None)); + let joback = Arc::new(Joback::new(parameters)); let temperature = 300.0 * KELVIN; let volume = METER.powi(3); let moles = arr1(&[1.0, 3.0]) * MOL; diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 54cb98ff1..ddea14018 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::reversed_empty_ranges)] #![allow(clippy::many_single_char_names)] #![allow(clippy::too_many_arguments)] +#![allow(deprecated)] use quantity::si::*; use quantity::*; @@ -35,13 +36,11 @@ pub mod parameter; mod phase_equilibria; mod state; pub use equation_of_state::{ - EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, - IdealGasContributionDual, MolarWeight, + Components, DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, + HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; -pub use phase_equilibria::{ - PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, -}; +pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; pub use state::{ Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, }; @@ -49,6 +48,77 @@ pub use state::{ #[cfg(feature = "python")] pub mod python; +/// Level of detail in the iteration output. +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Verbosity { + /// Do not print output. + None, + /// Print information about the success of failure of the iteration. + Result, + /// Print a detailed outpur for every iteration. + Iter, +} + +impl Default for Verbosity { + fn default() -> Self { + Self::None + } +} + +/// Options for the various phase equilibria solvers. +/// +/// If the values are [None], solver specific default +/// values are used. +#[derive(Copy, Clone, Default)] +pub struct SolverOptions { + /// Maximum number of iterations. + pub max_iter: Option, + /// Tolerance. + pub tol: Option, + /// Iteration outpput indicated by the [Verbosity] enum. + pub verbosity: Verbosity, +} + +impl From<(Option, Option, Option)> for SolverOptions { + fn from(options: (Option, Option, Option)) -> Self { + Self { + max_iter: options.0, + tol: options.1, + verbosity: options.2.unwrap_or(Verbosity::None), + } + } +} + +impl SolverOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn max_iter(mut self, max_iter: usize) -> Self { + self.max_iter = Some(max_iter); + self + } + + pub fn tol(mut self, tol: f64) -> Self { + self.tol = Some(tol); + self + } + + pub fn verbosity(mut self, verbosity: Verbosity) -> Self { + self.verbosity = verbosity; + self + } + + pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { + ( + self.max_iter.unwrap_or(max_iter), + self.tol.unwrap_or(tol), + self.verbosity, + ) + } +} + /// Consistent conversions between quantities and reduced properties. pub trait EosUnit: Unit + Send + Sync { fn reference_temperature() -> QuantityScalar; @@ -121,3 +191,261 @@ impl EosUnit for SIUnit { RGAS } } + +#[cfg(test)] +mod tests { + use crate::cubic::*; + use crate::equation_of_state::EquationOfState; + use crate::joback::{Joback, JobackParameters, JobackRecord}; + use crate::parameter::*; + use crate::Contributions; + use crate::EosResult; + use crate::StateBuilder; + use approx::*; + use ndarray::Array2; + use quantity::si::*; + use std::sync::Arc; + + fn pure_record_vec() -> Vec> { + let records = r#"[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "tc": 369.96, + "pc": 4250000.0, + "acentric_factor": 0.153 + }, + "molarweight": 44.0962 + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "tc": 425.2, + "pc": 3800000.0, + "acentric_factor": 0.199 + }, + "molarweight": 58.123 + } + ]"#; + serde_json::from_str(records).expect("Unable to parse json.") + } + + #[test] + fn validate_residual_properties() -> EosResult<()> { + let mixture = pure_record_vec(); + let propane = mixture[0].clone(); + let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); + let joback_parameters = Arc::new(JobackParameters::new_pure(PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(0.0, 0.0, 0.0, 0.0, 0.0), + ))); + let ideal_gas = Arc::new(Joback::new(joback_parameters)); + let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); + + let sr = StateBuilder::new(&residual) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + let s = StateBuilder::new(&eos) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + // pressure + assert_relative_eq!( + s.pressure(Contributions::Total), + sr.pressure(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.pressure(Contributions::Residual), + sr.pressure(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Total), + sr.compressibility(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Residual), + sr.compressibility(Contributions::Residual), + max_relative = 1e-15 + ); + + // residual properties + assert_relative_eq!( + s.helmholtz_energy(Contributions::Residual), + sr.residual_helmholtz_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.entropy(Contributions::Residual), + sr.residual_entropy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.enthalpy(Contributions::Residual), + sr.residual_enthalpy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.internal_energy(Contributions::Residual), + sr.residual_internal_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.gibbs_energy(Contributions::Residual), + sr.residual_gibbs_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.chemical_potential(Contributions::Residual), + sr.residual_chemical_potential(), + max_relative = 1e-15 + ); + + // pressure derivatives + assert_relative_eq!( + s.structure_factor(), + sr.structure_factor(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Total), + sr.dp_dt(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Residual), + sr.dp_dt(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Total), + sr.dp_dv(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Residual), + sr.dp_dv(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Total), + sr.dp_drho(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Residual), + sr.dp_drho(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Total), + sr.d2p_dv2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Residual), + sr.d2p_dv2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Total), + sr.d2p_drho2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Residual), + sr.d2p_drho2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Total), + sr.dp_dni(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Residual), + sr.dp_dni(Contributions::Residual), + max_relative = 1e-15 + ); + + // entropy + assert_relative_eq!( + s.ds_dt(Contributions::Residual), + sr.ds_res_dt(), + max_relative = 1e-15 + ); + + // chemical potential + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dni(Contributions::Residual), + sr.dmu_dni(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); + + // fugacity + assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dnj(), sr.dln_phi_dnj(), max_relative = 1e-15); + assert_relative_eq!( + s.thermodynamic_factor(), + sr.thermodynamic_factor(), + max_relative = 1e-15 + ); + + // residual properties using multiple derivatives + assert_relative_eq!( + s.c_v(Contributions::Residual), + sr.c_v_res(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dc_v_dt(Contributions::Residual), + sr.dc_v_res_dt(), + max_relative = 1e-15 + ); + println!( + "{}\n{}\n{}", + s.c_p(Contributions::Residual), + s.c_p(Contributions::IdealGas), + s.c_p(Contributions::Total) + ); + assert_relative_eq!( + s.c_p(Contributions::Residual), + sr.c_p_res(), + max_relative = 1e-14 + ); + Ok(()) + } +} diff --git a/feos-core/src/parameter/chemical_record.rs b/feos-core/src/parameter/chemical_record.rs index f498b1ba9..900a9ee87 100644 --- a/feos-core/src/parameter/chemical_record.rs +++ b/feos-core/src/parameter/chemical_record.rs @@ -120,13 +120,13 @@ pub trait SegmentCount { /// molecule. /// /// The map contains the segment record as key and the count as value. - fn segment_map( + fn segment_map( &self, - segment_records: &[SegmentRecord], - ) -> Result, Self::Count>, ParameterError> { + segment_records: &[SegmentRecord], + ) -> Result, Self::Count>, ParameterError> { let count = self.segment_count(); let queried: HashSet<_> = count.keys().cloned().collect(); - let mut segments: HashMap> = segment_records + let mut segments: HashMap> = segment_records .iter() .map(|r| (r.identifier.clone(), r.clone())) .collect(); diff --git a/feos-core/src/parameter/mod.rs b/feos-core/src/parameter/mod.rs index 48c62c303..5782db00f 100644 --- a/feos-core/src/parameter/mod.rs +++ b/feos-core/src/parameter/mod.rs @@ -30,17 +30,16 @@ where Self: Sized, { type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned + Default; /// Creates parameters from records for pure substances and possibly binary parameters. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self; /// Creates parameters for a pure component from a pure record. - fn new_pure(pure_record: PureRecord) -> Self { + fn new_pure(pure_record: PureRecord) -> Self { let binary_record = Array2::from_elem([1, 1], Self::Binary::default()); Self::from_records(vec![pure_record], binary_record) } @@ -48,7 +47,7 @@ where /// Creates parameters for a binary system from pure records and an optional /// binary interaction parameter. fn new_binary( - pure_records: Vec>, + pure_records: Vec>, binary_record: Option, ) -> Self { let binary_record = Array2::from_shape_fn([2, 2], |(i, j)| { @@ -63,12 +62,7 @@ where /// Return the original pure and binary records that were used to construct the parameters. #[allow(clippy::type_complexity)] - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ); + fn records(&self) -> (&[PureRecord], &Array2); /// Helper function to build matrix from list of records in correct order. /// @@ -76,7 +70,7 @@ where /// `pure_records`, the `Default` implementation of Self::Binary is used. #[allow(clippy::expect_fun_call)] fn binary_matrix_from_records( - pure_records: &Vec>, + pure_records: &Vec>, binary_records: &[BinaryRecord], search_option: IdentifierOption, ) -> Array2 { @@ -138,8 +132,7 @@ where P: AsRef, { let mut queried: IndexSet = IndexSet::new(); - let mut record_map: HashMap> = - HashMap::new(); + let mut record_map: HashMap> = HashMap::new(); for (substances, file) in input { substances.iter().try_for_each(|identifier| { @@ -154,8 +147,7 @@ where let f = File::open(file)?; let reader = BufReader::new(f); - let pure_records: Vec> = - serde_json::from_reader(reader)?; + let pure_records: Vec> = serde_json::from_reader(reader)?; pure_records .into_iter() @@ -202,12 +194,11 @@ where /// and the ideal gas record. fn from_segments( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result where Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { // update the pure records with model and ideal gas records @@ -276,7 +267,6 @@ where where P: AsRef, Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { let queried: IndexSet = substances @@ -315,7 +305,7 @@ where .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -353,13 +343,12 @@ where pub trait ParameterHetero: Sized { type Chemical: Clone; type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned; /// Creates parameters from the molecular structure and segment information. fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result; @@ -369,7 +358,7 @@ pub trait ParameterHetero: Sized { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ); @@ -419,7 +408,7 @@ pub trait ParameterHetero: Sized { .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -473,7 +462,6 @@ pub enum ParameterError { #[cfg(test)] mod test { use super::*; - use crate::joback::JobackRecord; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -495,16 +483,15 @@ mod test { } struct MyParameter { - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, } impl Parameter for MyParameter { type Pure = MyPureModel; - type IdealGas = JobackRecord; type Binary = MyBinaryModel; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { Self { @@ -513,12 +500,7 @@ mod test { } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } diff --git a/feos-core/src/parameter/model_record.rs b/feos-core/src/parameter/model_record.rs index 382b68936..ee86f61dc 100644 --- a/feos-core/src/parameter/model_record.rs +++ b/feos-core/src/parameter/model_record.rs @@ -10,28 +10,19 @@ use std::path::Path; /// A collection of parameters of a pure substance. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PureRecord { +pub struct PureRecord { pub identifier: Identifier, pub molarweight: f64, pub model_record: M, - #[serde(default = "Default::default")] - #[serde(skip_serializing_if = "Option::is_none")] - pub ideal_gas_record: Option, } -impl PureRecord { +impl PureRecord { /// Create a new `PureRecord`. - pub fn new( - identifier: Identifier, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: Identifier, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } @@ -43,47 +34,29 @@ impl PureRecord { where T: Copy + ValueInto, M: FromSegments, - I: FromSegments, - S: IntoIterator, T)>, + S: IntoIterator, T)>, { let mut molarweight = 0.0; let mut model_segments = Vec::new(); - let mut ideal_gas_segments = Vec::new(); for (s, n) in segments { molarweight += s.molarweight * n.value_into().unwrap(); model_segments.push((s.model_record, n)); - ideal_gas_segments.push(s.ideal_gas_record.map(|ig| (ig, n))); } let model_record = M::from_segments(&model_segments)?; - let ideal_gas_segments: Option> = ideal_gas_segments.into_iter().collect(); - let ideal_gas_record = ideal_gas_segments - .as_deref() - .map(I::from_segments) - .transpose()?; - - Ok(Self::new( - identifier, - molarweight, - model_record, - ideal_gas_record, - )) + Ok(Self::new(identifier, molarweight, model_record)) } } -impl std::fmt::Display for PureRecord +impl std::fmt::Display for PureRecord where M: std::fmt::Display, - I: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PureRecord(")?; write!(f, "\n\tidentifier={},", self.identifier)?; write!(f, "\n\tmolarweight={},", self.molarweight)?; write!(f, "\n\tmodel_record={},", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } @@ -153,7 +126,6 @@ where #[cfg(test)] mod test { use super::*; - use crate::joback::JobackRecord; #[derive(Serialize, Deserialize, Debug, Default, Clone)] struct TestModelRecordSegments { @@ -173,7 +145,7 @@ mod test { } } "#; - let record: PureRecord = + let record: PureRecord = serde_json::from_str(r).expect("Unable to parse json."); assert_eq!(record.identifier.cas, Some("123-4-5".into())) } @@ -201,7 +173,7 @@ mod test { } } ]"#; - let records: Vec> = + let records: Vec> = serde_json::from_str(r).expect("Unable to parse json."); assert_eq!(records[0].identifier.cas, Some("1".into())); assert_eq!(records[1].identifier.cas, Some("2".into())) diff --git a/feos-core/src/parameter/segment.rs b/feos-core/src/parameter/segment.rs index f87b7a8d7..f7718c054 100644 --- a/feos-core/src/parameter/segment.rs +++ b/feos-core/src/parameter/segment.rs @@ -8,60 +8,49 @@ use std::path::Path; /// Parameters describing an individual segment of a molecule. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SegmentRecord { +pub struct SegmentRecord { pub identifier: String, pub molarweight: f64, pub model_record: M, - pub ideal_gas_record: Option, } -impl SegmentRecord { +impl SegmentRecord { /// Creates a new `SegmentRecord`. - pub fn new( - identifier: String, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: String, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } /// Read a list of `SegmentRecord`s from a JSON file. pub fn from_json>(file: P) -> Result, ParameterError> where - I: DeserializeOwned, M: DeserializeOwned, { Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?) } } -impl Hash for SegmentRecord { +impl Hash for SegmentRecord { fn hash(&self, state: &mut H) { self.identifier.hash(state); } } -impl PartialEq for SegmentRecord { +impl PartialEq for SegmentRecord { fn eq(&self, other: &Self) -> bool { self.identifier == other.identifier } } -impl Eq for SegmentRecord {} +impl Eq for SegmentRecord {} -impl std::fmt::Display for SegmentRecord { +impl std::fmt::Display for SegmentRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SegmentRecord(\n\tidentifier={}", self.identifier)?; write!(f, "\n\tmolarweight={}", self.molarweight)?; write!(f, "\n\tmodel_record={}", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index e64920c8e..ee9a133db 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -1,12 +1,12 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, State, StateBuilder, TPSpec, }; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -55,7 +55,7 @@ where } /// # Bubble and dew point calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a given temperature /// or pressure and composition of the liquid phase. pub fn bubble_point( @@ -225,9 +225,9 @@ impl PhaseEquilibrium { let m = liquid_molefracs * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let p_i = (temperature * density * SIUnit::gas_constant() * liquid_molefracs) * (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -251,9 +251,9 @@ impl PhaseEquilibrium { let m = x * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let k = vapor_molefracs / (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -287,7 +287,7 @@ impl PhaseEquilibrium { } } -fn starting_x2_bubble( +fn starting_x2_bubble( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -315,7 +315,7 @@ fn starting_x2_bubble( Ok([liquid_state, vapor_state]) } -fn starting_x2_dew( +fn starting_x2_dew( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -353,7 +353,7 @@ fn starting_x2_dew( Ok([vapor_state, liquid_state]) } -fn bubble_dew( +fn bubble_dew( tp_spec: TPSpec, mut var_tp: TPSpec, mut state1: State, @@ -441,7 +441,7 @@ where } } -fn adjust_t_p( +fn adjust_t_p( var: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -509,7 +509,7 @@ where Ok(f.abs()) } -fn adjust_states( +fn adjust_states( var: &TPSpec, state1: &mut State, state2: &mut State, @@ -536,7 +536,7 @@ fn adjust_states( Ok(()) } -fn adjust_x2( +fn adjust_x2( state1: &State, state2: &mut State, verbosity: Verbosity, @@ -558,7 +558,7 @@ fn adjust_x2( Ok(err_out) } -fn newton_step( +fn newton_step( tp_spec: TPSpec, var: &mut TPSpec, state1: &mut State, @@ -574,7 +574,7 @@ where } } -fn newton_step_t( +fn newton_step_t( pressure: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -593,11 +593,11 @@ where .dot(&state1.molefracs); let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -607,7 +607,12 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p_2])]; + let dmu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let res = concatenate![Axis(0), mu_1_res - mu_2_res + dmu_ig, arr1(&[p_1 - p_2])]; let error = norm(&res); // calculate Jacobian @@ -651,7 +656,7 @@ where Ok(error) } -fn newton_step_p( +fn newton_step_p( pressure: SINumber, temperature: &mut TPSpec, state1: &mut State, @@ -666,12 +671,8 @@ where .dot(&state1.molefracs); let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_1 = state1 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_2 = state2 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_1 = state1.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_2 = state2.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .dot(&state1.molefracs); @@ -683,11 +684,11 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -698,7 +699,18 @@ where let p = pressure.to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p]), arr1(&[p_2 - p])]; + let delta_dmu_ig_dt = (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_mu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_dmu_ig_dt; + let res = concatenate![ + Axis(0), + mu_1_res - mu_2_res + delta_mu_ig, + arr1(&[p_1 - p]), + arr1(&[p_2 - p]) + ]; let error = norm(&res); // calculate Jacobian @@ -717,7 +729,7 @@ where ], concatenate![ Axis(0), - (dmu_dt_1 - dmu_dt_2).insert_axis(Axis(1)), + (dmu_res_dt_1 - dmu_res_dt_2 + delta_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_1], [dp_dt_2]]) ] ]; diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 7c020e352..7f8a27624 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -1,7 +1,7 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State}; -use crate::EosUnit; +use crate::state::{DensityInitialization, State}; +use crate::{Contributions, EosUnit}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; use std::fmt::Write; @@ -17,77 +17,6 @@ mod vle_pure; pub use phase_diagram_binary::PhaseDiagramHetero; pub use phase_diagram_pure::PhaseDiagram; -/// Level of detail in the iteration output. -#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Verbosity { - /// Do not print output. - None, - /// Print information about the success of failure of the iteration. - Result, - /// Print a detailed outpur for every iteration. - Iter, -} - -impl Default for Verbosity { - fn default() -> Self { - Self::None - } -} - -/// Options for the various phase equilibria solvers. -/// -/// If the values are [None], solver specific default -/// values are used. -#[derive(Copy, Clone, Default)] -pub struct SolverOptions { - /// Maximum number of iterations. - pub max_iter: Option, - /// Tolerance. - pub tol: Option, - /// Iteration outpput indicated by the [Verbosity] enum. - pub verbosity: Verbosity, -} - -impl From<(Option, Option, Option)> for SolverOptions { - fn from(options: (Option, Option, Option)) -> Self { - Self { - max_iter: options.0, - tol: options.1, - verbosity: options.2.unwrap_or(Verbosity::None), - } - } -} - -impl SolverOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn max_iter(mut self, max_iter: usize) -> Self { - self.max_iter = Some(max_iter); - self - } - - pub fn tol(mut self, tol: f64) -> Self { - self.tol = Some(tol); - self - } - - pub fn verbosity(mut self, verbosity: Verbosity) -> Self { - self.verbosity = verbosity; - self - } - - pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { - ( - self.max_iter.unwrap_or(max_iter), - self.tol.unwrap_or(tol), - self.verbosity, - ) - } -} - /// A thermodynamic equilibrium state. /// /// The struct is parametrized over the number of phases with most features @@ -109,11 +38,10 @@ impl Clone for PhaseEquilibrium { } } -impl fmt::Display for PhaseEquilibrium +impl fmt::Display for PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, s) in self.0.iter().enumerate() { @@ -123,11 +51,10 @@ where } } -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { pub fn _repr_markdown_(&self) -> String { if self.0[0].eos.components() == 1 { @@ -161,7 +88,7 @@ where } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -171,7 +98,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -185,7 +112,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn from_states(state1: State, state2: State) -> Self { let (vapor, liquid) = if state1.density < state2.density { (state1, state2) @@ -226,7 +153,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn update_pressure( mut self, temperature: SINumber, @@ -261,18 +188,21 @@ impl PhaseEquilibrium { Ok(()) } - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for s in self.0.iter_mut() { - s.update_chemical_potential(chemical_potential)?; - } - Ok(()) - } - + // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) pub(super) fn total_gibbs_energy(&self) -> SINumber { self.0 .iter() .fold(0.0 * SIUnit::reference_energy(), |acc, s| { - acc + s.gibbs_energy(Contributions::Total) + let ln_rho = s + .partial_density + .to_reduced(SIUnit::reference_density()) + .unwrap() + .mapv(f64::ln); + acc + s.residual_helmholtz_energy() + + s.pressure(Contributions::Total) * s.volume + + SIUnit::gas_constant() + * s.temperature + * (s.moles.clone() * (ln_rho - 1.0)).sum() }) } } @@ -280,7 +210,7 @@ impl PhaseEquilibrium { const TRIVIAL_REL_DEVIATION: f64 = 1e-5; /// # Utility functions -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn check_trivial_solution(self) -> EosResult { if Self::is_trivial_solution(self.vapor(), self.liquid()) { Err(EosError::TrivialSolution) diff --git a/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/feos-core/src/phase_equilibria/phase_diagram_binary.rs index 746fc5da0..d9ae810ce 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,8 +1,8 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions}; use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -11,7 +11,7 @@ use std::sync::Arc; const DEFAULT_POINTS: usize = 51; -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid equilibrium. /// @@ -171,7 +171,7 @@ impl PhaseDiagram { } } -fn iterate_vle( +fn iterate_vle( eos: &Arc, tp: TPSpec, x_lim: &[f64], @@ -231,7 +231,7 @@ where vle_vec } -impl State { +impl State { fn tp(&self, tp: TPSpec) -> SINumber { match tp { TPSpec::Temperature(_) => self.pressure(Contributions::Total), @@ -247,7 +247,7 @@ pub struct PhaseDiagramHetero { pub lle: Option>, } -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid/liquid equilibrium. /// @@ -346,7 +346,7 @@ const MAX_ITER_HETERO: usize = 50; const TOL_HETERO: f64 = 1e-8; /// # Heteroazeotropes -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: std::fmt::Display + std::fmt::LowerExp, { @@ -421,14 +421,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -441,10 +441,20 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p_v]), arr1(&[p_l2 - p_v]) ]; @@ -555,12 +565,9 @@ where .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_l1 = (l1.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_l2 = (l2.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_v = (v.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) @@ -573,14 +580,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_dt_v = (v.dp_dt(Contributions::Total)) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -593,10 +600,22 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_dmu_ig_dt = (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_dmu_ig_dt = (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l1v_dmu_ig_dt; + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l2v_dmu_ig_dt; let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p]), arr1(&[p_l2 - p]), arr1(&[p_v - p]) @@ -636,8 +655,8 @@ where ], concatenate![ Axis(0), - (dmu_dt_l1 - &dmu_dt_v).insert_axis(Axis(1)), - (dmu_dt_l2 - &dmu_dt_v).insert_axis(Axis(1)), + (dmu_res_dt_l1 - &dmu_res_dt_v + delta_l1v_dmu_ig_dt).insert_axis(Axis(1)), + (dmu_res_dt_l2 - &dmu_res_dt_v + delta_l2v_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_l1]]), arr2(&[[dp_dt_l2]]), arr2(&[[dp_dt_v]]) diff --git a/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/feos-core/src/phase_equilibria/phase_diagram_pure.rs index c7b823fed..1a25d8ac9 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -1,9 +1,10 @@ -use super::{PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::EosResult; use crate::state::{State, StateVec}; #[cfg(feature = "rayon")] use crate::EosUnit; +use crate::SolverOptions; #[cfg(feature = "rayon")] use ndarray::{Array1, ArrayView1, Axis}; #[cfg(feature = "rayon")] @@ -33,7 +34,7 @@ impl PhaseDiagram { } } -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate a phase diagram for a pure component. pub fn pure( eos: &Arc, @@ -74,7 +75,7 @@ impl PhaseDiagram { } #[cfg(feature = "rayon")] -impl PhaseDiagram { +impl PhaseDiagram { fn solve_temperatures( eos: &Arc, temperatures: ArrayView1, diff --git a/feos-core/src/phase_equilibria/phase_envelope.rs b/feos-core/src/phase_equilibria/phase_envelope.rs index f05ed3afe..9161eb75e 100644 --- a/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/feos-core/src/phase_equilibria/phase_envelope.rs @@ -1,12 +1,12 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::State; -use crate::Contributions; +use crate::state::{Contributions, State}; +use crate::SolverOptions; use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate the bubble point line of a mixture with given composition. pub fn bubble_point_line( eos: &Arc, diff --git a/feos-core/src/phase_equilibria/stability_analysis.rs b/feos-core/src/phase_equilibria/stability_analysis.rs index 47da90f3f..c0df11f98 100644 --- a/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/feos-core/src/phase_equilibria/stability_analysis.rs @@ -1,8 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::smallest_ev; use num_dual::linalg::LU; @@ -18,7 +18,7 @@ const MINIMIZE_KMAX: usize = 100; const ZERO_TPD: f64 = -1E-08; /// # Stability analysis -impl State { +impl State { /// Determine if the state is stable, i.e. if a phase split should /// occur or not. pub fn is_stable(&self, options: SolverOptions) -> EosResult { diff --git a/feos-core/src/phase_equilibria/tp_flash.rs b/feos-core/src/phase_equilibria/tp_flash.rs index ef53cd189..41262bfd0 100644 --- a/feos-core/src/phase_equilibria/tp_flash.rs +++ b/feos-core/src/phase_equilibria/tp_flash.rs @@ -1,7 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; +use crate::{SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::norm; use quantity::si::{SIArray1, SINumber}; @@ -11,7 +12,7 @@ const MAX_ITER_TP: usize = 400; const TOL_TP: f64 = 1e-8; /// # Flash calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Perform a Tp-flash calculation. If no initial values are /// given, the solution is initialized using a stability analysis. /// @@ -38,7 +39,7 @@ impl PhaseEquilibrium { } /// # Flash calculations -impl State { +impl State { /// Perform a Tp-flash calculation using the [State] as feed. /// If no initial values are given, the solution is initialized /// using a stability analysis. @@ -157,7 +158,7 @@ impl State { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { fn accelerated_successive_substitution( &mut self, feed_state: &State, diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index 7d89a62ed..eb3cd2ce5 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -1,8 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State, TPSpec}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::{arr1, Array1}; use quantity::si::{SINumber, SIUnit}; use std::convert::TryFrom; @@ -13,7 +13,7 @@ const MAX_ITER_PURE: usize = 50; const TOL_PURE: f64 = 1e-12; /// # Pure component phase equilibria -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a pure component. pub fn pure( eos: &Arc, @@ -83,21 +83,21 @@ impl PhaseEquilibrium { let (p_l, p_rho_l) = liquid.p_dpdrho(); let (p_v, p_rho_v) = vapor.p_dpdrho(); // calculate the molar Helmholtz energies (already cached) - let a_l = liquid.molar_helmholtz_energy(Contributions::Total); - let a_v = vapor.molar_helmholtz_energy(Contributions::Total); + let a_l_res = liquid.residual_helmholtz_energy() / liquid.total_moles; + let a_v_res = vapor.residual_helmholtz_energy() / vapor.total_moles; // Estimate the new pressure + let kt = SIUnit::gas_constant() * vapor.temperature; let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; - let delta_a = a_v - a_l; + let delta_a = a_v_res - a_l_res + kt * vapor.density.to_reduced(liquid.density)?.ln(); let mut p_new = -delta_a / delta_v; // If the pressure becomes negative, assume the gas phase is ideal. The // resulting pressure is always positive. if p_new.is_sign_negative() { - let mu_v = vapor.chemical_potential(Contributions::Total).get(0); p_new = p_v - * (a_l - mu_v) - .to_reduced(vapor.temperature * SIUnit::gas_constant())? + * (-delta_a - p_v * vapor.volume / vapor.total_moles) + .to_reduced(kt)? .exp(); } @@ -193,20 +193,23 @@ impl PhaseEquilibrium { let p_t_l = vle.liquid().dp_dt(Contributions::Total); let p_t_v = vle.vapor().dp_dt(Contributions::Total); - // calculate the molar entropies (already cached) - let s_l = vle.liquid().molar_entropy(Contributions::Total); - let s_v = vle.vapor().molar_entropy(Contributions::Total); + // calculate the residual molar entropies (already cached) + let s_l_res = vle.liquid().residual_entropy() / vle.liquid().total_moles; + let s_v_res = vle.vapor().residual_entropy() / vle.vapor().total_moles; - // calculate the molar Helmholtz energies (already cached) - let a_l = vle.liquid().molar_helmholtz_energy(Contributions::Total); - let a_v = vle.vapor().molar_helmholtz_energy(Contributions::Total); + // calculate the residual molar Helmholtz energies (already cached) + let a_l_res = vle.liquid().residual_helmholtz_energy() / vle.liquid().total_moles; + let a_v_res = vle.vapor().residual_helmholtz_energy() / vle.vapor().total_moles; // calculate the molar volumes let v_l = 1.0 / vle.liquid().density; let v_v = 1.0 / vle.vapor().density; // estimate the temperature steps - let delta_t = (pressure * (v_v - v_l) + (a_v - a_l)) / (s_v - s_l); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + let ln_rho = v_l.to_reduced(v_v)?.ln(); + let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho)) + / (s_v_res - s_l_res - SIUnit::gas_constant() * ln_rho); let t_new = vle.vapor().temperature + delta_t; // calculate Newton steps for the densities and update state. @@ -319,10 +322,15 @@ impl PhaseEquilibrium { } for _ in 0..20 { - t0 = (e.vapor().enthalpy(Contributions::Total) - - e.liquid().enthalpy(Contributions::Total)) - / (e.vapor().entropy(Contributions::Total) - - e.liquid().entropy(Contributions::Total)); + let h = |s: &State<_>| { + s.residual_enthalpy() + s.total_moles * SIUnit::gas_constant() * s.temperature + }; + t0 = (h(e.vapor()) - h(e.liquid())) + / (e.vapor().residual_entropy() + - e.liquid().residual_entropy() + - SIUnit::gas_constant() + * e.vapor().total_moles + * (e.vapor().density.to_reduced(e.liquid().density)?.ln())); let trial_state = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; if trial_state.density < cp.density { @@ -346,7 +354,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate the pure component vapor pressures of all /// components in the system for the given temperature. pub fn vapor_pressure(eos: &Arc, temperature: SINumber) -> Vec> { diff --git a/feos-core/src/python/cubic.rs b/feos-core/src/python/cubic.rs index 80a82b3f7..1973a1bf1 100644 --- a/feos-core/src/python/cubic.rs +++ b/feos-core/src/python/cubic.rs @@ -1,9 +1,7 @@ use crate::cubic::{PengRobinsonParameters, PengRobinsonRecord}; -use crate::joback::JobackRecord; use crate::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use crate::python::joback::PyJobackRecord; use crate::python::parameter::PyIdentifier; use crate::*; use ndarray::Array2; @@ -32,12 +30,7 @@ impl PyPengRobinsonRecord { impl_json_handling!(PyPengRobinsonRecord); -impl_pure_record!( - PengRobinsonRecord, - PyPengRobinsonRecord, - JobackRecord, - PyJobackRecord -); +impl_pure_record!(PengRobinsonRecord, PyPengRobinsonRecord); impl_binary_record!(); diff --git a/feos-core/src/python/joback.rs b/feos-core/src/python/joback.rs index d6e670ed8..c54257b6c 100644 --- a/feos-core/src/python/joback.rs +++ b/feos-core/src/python/joback.rs @@ -1,7 +1,17 @@ -use crate::impl_json_handling; -use crate::joback::JobackRecord; -use crate::parameter::ParameterError; +use std::sync::Arc; + +use crate::joback::{JobackBinaryRecord, JobackParameters, JobackRecord}; +use crate::parameter::*; +use crate::python::parameter::*; +use crate::{ + impl_binary_record, impl_json_handling, impl_parameter, impl_parameter_from_segments, + impl_pure_record, impl_segment_record, +}; +use ndarray::Array2; +use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; +use std::convert::{TryFrom, TryInto}; /// Create a set of Joback ideal gas heat capacity parameters /// for a segment or a pure component. @@ -44,3 +54,41 @@ impl PyJobackRecord { } impl_json_handling!(PyJobackRecord); +impl_pure_record!(JobackRecord, PyJobackRecord); +impl_segment_record!(JobackRecord, PyJobackRecord); + +#[pyclass(name = "JobackBinaryRecord")] +#[derive(Clone)] +pub struct PyJobackBinaryRecord(pub JobackBinaryRecord); + +impl_binary_record!(JobackBinaryRecord, PyJobackBinaryRecord); +/// Create a set of Joback parameters from records. +/// +/// Parameters +/// ---------- +/// pure_records : List[PureRecord] +/// pure substance records. +/// substances : List[str], optional +/// The substances to use. Filters substances from `pure_records` according to +/// `search_option`. +/// When not provided, all entries of `pure_records` are used. +/// search_option : {'Name', 'Cas', 'Inchi', 'IupacName', 'Formula', 'Smiles'}, optional, defaults to 'Name'. +/// Identifier that is used to search substance. +/// +/// Returns +/// ------- +/// JobackParameters +#[pyclass(name = "JobackParameters")] +#[pyo3(text_signature = "(pure_records, substances=None, search_option='Name')")] +#[derive(Clone)] +pub struct PyJobackParameters(pub Arc); + +impl_parameter!(JobackParameters, PyJobackParameters); +impl_parameter_from_segments!(JobackParameters, PyJobackParameters); + +#[pymethods] +impl PyJobackParameters { + // fn _repr_markdown_(&self) -> String { + // self.0.to_markdown() + // } +} diff --git a/feos-core/src/python/parameter.rs b/feos-core/src/python/parameter.rs index d0a55ad2a..9c0e80e94 100644 --- a/feos-core/src/python/parameter.rs +++ b/feos-core/src/python/parameter.rs @@ -370,7 +370,7 @@ impl_json_handling!(PyBinarySegmentRecord); #[macro_export] macro_rules! impl_pure_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a pure component. /// /// Parameters @@ -381,16 +381,14 @@ macro_rules! impl_pure_record { /// The molar weight (in g/mol) of the pure component. /// model_record : ModelRecord /// The pure component model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The pure component parameters for the ideal gas model. /// /// Returns /// ------- /// PureRecord #[pyclass(name = "PureRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight, model_record)")] #[derive(Clone)] - pub struct PyPureRecord(pub PureRecord<$model_record, $ideal_gas_record>); + pub struct PyPureRecord(pub PureRecord<$model_record>); #[pymethods] impl PyPureRecord { @@ -399,13 +397,11 @@ macro_rules! impl_pure_record { identifier: PyIdentifier, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(PureRecord::new( identifier.0, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -439,16 +435,6 @@ macro_rules! impl_pure_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } @@ -460,7 +446,7 @@ macro_rules! impl_pure_record { #[macro_export] macro_rules! impl_segment_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a single segment. /// /// Parameters @@ -471,16 +457,14 @@ macro_rules! impl_segment_record { /// The molar weight (in g/mol) of the segment. /// model_record : ModelRecord /// The segment model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The segment ideal gas parameters. /// /// Returns /// ------- /// SegmentRecord #[pyclass(name = "SegmentRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight)")] #[derive(Clone)] - pub struct PySegmentRecord(SegmentRecord<$model_record, $ideal_gas_record>); + pub struct PySegmentRecord(SegmentRecord<$model_record>); #[pymethods] impl PySegmentRecord { @@ -489,13 +473,11 @@ macro_rules! impl_segment_record { identifier: String, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(SegmentRecord::new( identifier, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -547,16 +529,6 @@ macro_rules! impl_segment_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } diff --git a/feos-core/src/python/phase_equilibria.rs b/feos-core/src/python/phase_equilibria.rs index 72951802d..c988a429f 100644 --- a/feos-core/src/python/phase_equilibria.rs +++ b/feos-core/src/python/phase_equilibria.rs @@ -237,20 +237,6 @@ macro_rules! impl_phase_equilibrium { PyState(self.0.liquid().clone()) } - /// Calculate a new PhaseEquilibrium with the given chemical potential. - /// The temperature remains constant, but the states are not in - /// a mechanical equilibrium anymore. - /// - /// Parameters - /// ---------- - /// chemical_potential: SIArray1 - /// The new chemical potential - /// - fn update_chemical_potential(slf: &PyCell, chemical_potential: &PySIArray1) -> PyResult<()> { - slf.borrow_mut().0.update_chemical_potential(chemical_potential)?; - Ok(()) - } - /// Calculate the pure component vapor-liquid equilibria for all /// components in the system. /// diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 9e0045cea..59644acdd 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -90,7 +90,7 @@ macro_rules! impl_state { } else { Ok(DensityInitialization::None) }; - let s = State::new( + let s = State::new_full( &eos.0, temperature.map(|t| t.into()), volume.map(|t| t.into()), @@ -459,18 +459,11 @@ macro_rules! impl_state { /// Return partial molar volume of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_volume(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_volume(contributions)) + fn partial_molar_volume(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_volume()) } /// Return chemical potential of each component. @@ -703,18 +696,11 @@ macro_rules! impl_state { /// Return partial molar entropy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_entropy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_entropy(contributions)) + fn partial_molar_entropy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_entropy()) } /// Return enthalpy. @@ -752,18 +738,11 @@ macro_rules! impl_state { /// Return partial molar enthalpy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_enthalpy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_enthalpy(contributions)) + fn partial_molar_enthalpy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_enthalpy()) } /// Return helmholtz_energy. diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index ace9713ac..5e5a97ac5 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,8 +1,11 @@ -use crate::{EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, MolarWeight, StateHD}; +use crate::{ + Components, DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, + IdealGas, MolarWeight, Residual, StateHD, +}; use ndarray::Array1; use num_dual::*; use numpy::convert::IntoPyArray; -use numpy::{PyArray, PyReadonlyArrayDyn}; +use numpy::{PyArray, PyReadonlyArray1, PyReadonlyArrayDyn}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; @@ -11,13 +14,79 @@ use std::fmt; struct PyHelmholtzEnergy(Py); +pub struct PyIdealGas(Py); + +impl PyIdealGas { + pub fn new(obj: Py) -> PyResult { + Python::with_gil(|py| { + let attr = obj.as_ref(py).hasattr("components")?; + if !attr { + panic!("Python Class has to have a method 'components' with signature:\n\tdef signature(self) -> int") + } + let attr = obj.as_ref(py).hasattr("subset")?; + if !attr { + panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") + } + let attr = obj.as_ref(py).hasattr("ln_lambda3")?; + if !attr { + panic!("{}", "Python Class has to have a method 'ln_lambda3' with signature:\n\tdef ln_lambda3(self, temperature: HD) -> HD\nwhere 'HD' has to be any (hyper-) dual number.") + } + Ok(Self(obj)) + }) + } +} + +impl Components for PyIdealGas { + fn components(&self) -> usize { + Python::with_gil(|py| { + let py_result = self.0.as_ref(py).call_method0("components").unwrap(); + if py_result.get_type().name().unwrap() != "int" { + panic!( + "Expected an integer for the components() method signature, got {}", + py_result.get_type().name().unwrap() + ); + } + py_result.extract().unwrap() + }) + } + + fn subset(&self, component_list: &[usize]) -> Self { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("subset", (component_list.to_vec(),)) + .unwrap(); + Self::new(py_result.extract().unwrap()).unwrap() + }) + } +} + +impl IdealGas for PyIdealGas { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self + } +} + +impl fmt::Display for PyIdealGas { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Python)") + } +} + /// Struct containing pointer to Python Class that implements Helmholtz energy. -pub struct PyEoSObj { +pub struct PyResidual { obj: Py, contributions: Vec>, } -impl PyEoSObj { +impl fmt::Display for PyResidual { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python residual") + } +} + +impl PyResidual { pub fn new(obj: Py) -> PyResult { Python::with_gil(|py| { let attr = obj.as_ref(py).hasattr("components")?; @@ -48,7 +117,7 @@ impl PyEoSObj { } } -impl MolarWeight for PyEoSObj { +impl MolarWeight for PyResidual { fn molar_weight(&self) -> SIArray1 { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("molar_weight").unwrap(); @@ -63,7 +132,7 @@ impl MolarWeight for PyEoSObj { } } -impl EquationOfState for PyEoSObj { +impl Components for PyResidual { fn components(&self) -> usize { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); @@ -87,7 +156,9 @@ impl EquationOfState for PyEoSObj { Self::new(py_result.extract().unwrap()).unwrap() }) } +} +impl Residual for PyResidual { fn compute_max_density(&self, moles: &Array1) -> f64 { Python::with_gil(|py| { let py_result = self @@ -99,7 +170,7 @@ impl EquationOfState for PyEoSObj { }) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -192,17 +263,48 @@ macro_rules! helmholtz_energy { }; } +macro_rules! de_broglie_wavelength { + ($py_hd_id:ident, $hd_ty:ty) => { + impl DeBroglieWavelengthDual<$hd_ty> for PyIdealGas { + fn ln_lambda3(&self, temperature: $hd_ty) -> Array1<$hd_ty> { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("ln_lambda3", (<$py_hd_id>::from(temperature),)) + .unwrap(); + + // f64 + let rr = if let Ok(r) = py_result.extract::>() { + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri)) + // anything but f64 + } else if let Ok(r) = py_result.extract::>() { + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) + } else { + panic!("ln_lambda3: data type of result must be one-dimensional numpy ndarray") + }; + rr + }) + } + } + }; +} + macro_rules! impl_dual_state_helmholtz_energy { ($py_state_id:ident, $py_hd_id:ident, $hd_ty:ty, $py_field_ty:ty) => { dual_number!($py_hd_id, $hd_ty, $py_field_ty); state!($py_state_id, $py_hd_id, $hd_ty); helmholtz_energy!($py_state_id, $py_hd_id, $hd_ty); + de_broglie_wavelength!($py_hd_id, $hd_ty); }; } // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); +de_broglie_wavelength!(f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); dual_number!(PyDualVec3, DualSVec64<3>, f64); diff --git a/feos-core/src/state/builder.rs b/feos-core/src/state/builder.rs index 40406862b..b9a598b4c 100644 --- a/feos-core/src/state/builder.rs +++ b/feos-core/src/state/builder.rs @@ -1,5 +1,5 @@ use super::{DensityInitialization, State}; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::EosResult; use ndarray::Array1; use quantity::si::{SIArray1, SINumber}; @@ -52,7 +52,7 @@ use std::sync::Arc; /// # Ok(()) /// # } /// ``` -pub struct StateBuilder<'a, E: EquationOfState> { +pub struct StateBuilder<'a, E, const IG: bool> { eos: Arc, temperature: Option, volume: Option, @@ -69,7 +69,7 @@ pub struct StateBuilder<'a, E: EquationOfState> { initial_temperature: Option, } -impl<'a, E: EquationOfState> StateBuilder<'a, E> { +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Create a new `StateBuilder` for the given equation of state. pub fn new(eos: &Arc) -> Self { StateBuilder { @@ -89,7 +89,9 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { initial_temperature: None, } } +} +impl<'a, E: Residual, const IG: bool> StateBuilder<'a, E, IG> { /// Provide the temperature for the new state. pub fn temperature(mut self, temperature: SINumber) -> Self { self.temperature = Some(temperature); @@ -138,24 +140,6 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self } - /// Provide the molar enthalpy for the new state. - pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> Self { - self.molar_enthalpy = Some(molar_enthalpy); - self - } - - /// Provide the molar entropy for the new state. - pub fn molar_entropy(mut self, molar_entropy: SINumber) -> Self { - self.molar_entropy = Some(molar_entropy); - self - } - - /// Provide the molar internal energy for the new state. - pub fn molar_internal_energy(mut self, molar_internal_energy: SINumber) -> Self { - self.molar_internal_energy = Some(molar_internal_energy); - self - } - /// Specify a vapor state. pub fn vapor(mut self) -> Self { self.density_initialization = DensityInitialization::Vapor; @@ -173,16 +157,81 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self.density_initialization = DensityInitialization::InitialDensity(initial_density); self } +} + +impl<'a, E: Residual + IdealGas, const IG: bool> StateBuilder<'a, E, IG> { + /// Provide the molar enthalpy for the new state. + pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_enthalpy = Some(molar_enthalpy); + self.convert() + } + + /// Provide the molar entropy for the new state. + pub fn molar_entropy(mut self, molar_entropy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_entropy = Some(molar_entropy); + self.convert() + } + + /// Provide the molar internal energy for the new state. + pub fn molar_internal_energy( + mut self, + molar_internal_energy: SINumber, + ) -> StateBuilder<'a, E, true> { + self.molar_internal_energy = Some(molar_internal_energy); + self.convert() + } /// Provide an initial temperature used in the Newton solver. - pub fn initial_temperature(mut self, initial_temperature: SINumber) -> Self { + pub fn initial_temperature( + mut self, + initial_temperature: SINumber, + ) -> StateBuilder<'a, E, true> { self.initial_temperature = Some(initial_temperature); - self + self.convert() + } + + fn convert(self) -> StateBuilder<'a, E, true> { + StateBuilder { + eos: self.eos, + temperature: self.temperature, + volume: self.volume, + density: self.density, + partial_density: self.partial_density, + total_moles: self.total_moles, + moles: self.moles, + molefracs: self.molefracs, + pressure: self.pressure, + molar_enthalpy: self.molar_enthalpy, + molar_entropy: self.molar_entropy, + molar_internal_energy: self.molar_internal_energy, + density_initialization: self.density_initialization, + initial_temperature: self.initial_temperature, + } } +} +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Try to build the state with the given inputs. pub fn build(self) -> EosResult> { State::new( + &self.eos, + self.temperature, + self.volume, + self.density, + self.partial_density, + self.total_moles, + self.moles, + self.molefracs, + self.pressure, + self.density_initialization, + ) + } +} + +impl<'a, E: Residual + IdealGas> StateBuilder<'a, E, true> { + /// Try to build the state with the given inputs. + pub fn build(self) -> EosResult> { + State::new_full( &self.eos, self.temperature, self.volume, @@ -201,7 +250,7 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { } } -impl<'a, E: EquationOfState> Clone for StateBuilder<'a, E> { +impl<'a, E, const IG: bool> Clone for StateBuilder<'a, E, IG> { fn clone(&self) -> Self { Self { eos: self.eos.clone(), diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index c215e928e..d5b5ad103 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -1,8 +1,7 @@ -use super::{State, StateHD, TPSpec}; -use crate::equation_of_state::EquationOfState; +use super::{DensityInitialization, State, StateHD, TPSpec}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::phase_equilibria::{SolverOptions, Verbosity}; -use crate::{DensityInitialization, EosUnit}; +use crate::{EosUnit, SolverOptions, Verbosity}; use nalgebra::{DMatrix, DVector, SVector, SymmetricEigen}; use ndarray::{arr1, Array1}; use num_dual::{ @@ -19,10 +18,10 @@ const MAX_ITER_CRIT_POINT_BINARY: usize = 200; const TOL_CRIT_POINT: f64 = 1e-8; /// # Critical points -impl State { +impl State { /// Calculate the pure component critical point of all components. pub fn critical_point_pure( - eos: &Arc, + eos: &Arc, initial_temperature: Option, options: SolverOptions, ) -> EosResult> @@ -42,7 +41,7 @@ impl State { } pub fn critical_point_binary( - eos: &Arc, + eos: &Arc, temperature_or_pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -67,7 +66,7 @@ impl State { /// Calculate the critical point of a system for given moles. pub fn critical_point( - eos: &Arc, + eos: &Arc, moles: Option<&SIArray1>, initial_temperature: Option, options: SolverOptions, @@ -94,7 +93,7 @@ impl State { } fn critical_point_hkm( - eos: &Arc, + eos: &Arc, moles: &SIArray1, initial_temperature: SINumber, options: SolverOptions, @@ -175,7 +174,7 @@ impl State { /// Calculate the critical point of a binary system for given temperature. fn critical_point_binary_t( - eos: &Arc, + eos: &Arc, temperature: SINumber, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, @@ -256,7 +255,7 @@ impl State { /// Calculate the critical point of a binary system for given pressure. fn critical_point_binary_p( - eos: &Arc, + eos: &Arc, pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -352,7 +351,7 @@ impl State { } pub fn spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: Option<&SIArray1>, options: SolverOptions, @@ -381,7 +380,7 @@ impl State { } fn calculate_spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: &SIArray1, density_initialization: DensityInitialization, @@ -459,8 +458,8 @@ impl State { } } -fn critical_point_objective( - eos: &Arc, +fn critical_point_objective( + eos: &Arc, temperature: DualSVec64<2>, density: DualSVec64<2>, moles: &Array1, @@ -473,8 +472,7 @@ fn critical_point_objective( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -494,12 +492,13 @@ fn critical_point_objective( Dual3::from_re(density.recip() * moles.sum()), moles_hd, ); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(SVector::from([eval, res.v3])) + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); + Ok(SVector::from([eval, (res + ig).v3])) } -fn critical_point_objective_t( - eos: &Arc, +fn critical_point_objective_t( + eos: &Arc, temperature: f64, density: SVector, 2>, ) -> EosResult, 2>> { @@ -511,8 +510,7 @@ fn critical_point_objective_t( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -528,12 +526,13 @@ fn critical_point_objective_t( ) }); let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(SVector::from([eval, res.v3])) + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); + Ok(SVector::from([eval, (res + ig).v3])) } -fn critical_point_objective_p( - eos: &Arc, +fn critical_point_objective_p( + eos: &Arc, pressure: f64, temperature: DualSVec64<3>, density: SVector, 2>, @@ -546,8 +545,7 @@ fn critical_point_objective_p( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -563,21 +561,23 @@ fn critical_point_objective_p( ) }); let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); // calculate pressure let a = |v| { let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); let state_p = StateHD::new(Dual::from_re(temperature), v, m); - eos.evaluate_residual(&state_p) + eos.ideal_gas().evaluate(&state_p) + eos.evaluate_residual(&state_p) }; let (_, p) = first_derivative(a, DualVec::one()); + let p = (p - density.sum()) * temperature; - Ok(SVector::from([eval, res.v3, p * temperature + pressure])) + Ok(SVector::from([eval, (res + ig).v3, p + pressure])) } -fn spinodal_objective( - eos: &Arc, +fn spinodal_objective( + eos: &Arc, temperature: Dual64, density: Dual64, moles: &Array1, @@ -590,8 +590,7 @@ fn spinodal_objective( m[i].eps1 = Dual64::one(); m[j].eps2 = Dual64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue of q @@ -623,3 +622,11 @@ fn smallest_ev_scalar(m: DMatrix) -> (Dual64, DVector) { .unwrap(); (*e, ev.into()) } + +fn kronecker(i: usize, j: usize) -> f64 { + if i == j { + 1.0 + } else { + 0.0 + } +} diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index ca566add3..893952eb1 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -7,12 +7,11 @@ //! //! Internally, all properties are computed using such states as input. use crate::density_iteration::density_iteration; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::{EosError, EosResult}; use crate::EosUnit; use cache::Cache; use ndarray::prelude::*; -use num_dual::linalg::{norm, LU}; use num_dual::*; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; @@ -22,8 +21,24 @@ use std::sync::{Arc, Mutex}; mod builder; mod cache; mod properties; +mod residual_properties; +mod statevec; pub use builder::StateBuilder; -pub use properties::{Contributions, StateVec}; +pub use statevec::StateVec; + +/// Possible contributions that can be computed. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Contributions { + /// Only compute the ideal gas contribution + IdealGas, + /// Only compute the difference between the total and the ideal gas contribution + Residual, + // /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state + // ResidualNpt, + /// Compute ideal gas and residual contributions + Total, +} /// Initial values in a density iteration. #[derive(Clone, Copy)] @@ -166,11 +181,10 @@ impl Clone for State { } } -impl fmt::Display for State +impl fmt::Display for State where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.eos.components() == 1 { @@ -217,7 +231,7 @@ pub(crate) enum PartialDerivative { } /// # State constructors -impl State { +impl State { /// Return a new `State` given a temperature, an array of mole numbers and a volume. /// /// This function will perform a validation of the given properties, i.e. test for signs @@ -290,7 +304,6 @@ impl State { /// is overdetermined, it will choose a method based on the following hierarchy. /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. /// 2. Use a density iteration for a given pressure. - /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ /// /// The [StateBuilder] provides a convenient way of calling this function without the need to provide /// all the optional input values. @@ -308,12 +321,35 @@ impl State { moles: Option<&SIArray1>, molefracs: Option<&Array1>, pressure: Option, - molar_enthalpy: Option, - molar_entropy: Option, - molar_internal_energy: Option, density_initialization: DensityInitialization, - initial_temperature: Option, ) -> EosResult { + Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + )? + .map_err(|_| EosError::UndeterminedState(String::from("Missing input parameters."))) + } + + fn _new( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + density_initialization: DensityInitialization, + ) -> EosResult>> { // Check if the provided densities have correct units. if let DensityInitialization::InitialDensity(rho0) = density_initialization { if !rho0.has_unit(&SIUnit::reference_density()) { @@ -396,36 +432,24 @@ impl State { // check if new state can be created using default constructor if let (Some(v), Some(t), Some(n_i)) = (v, temperature, &n_i) { - return State::new_nvt(eos, t, v, n_i); + return Ok(Ok(State::new_nvt(eos, t, v, n_i)?)); } // Check if new state can be created using density iteration if let (Some(p), Some(t), Some(n_i)) = (pressure, temperature, &n_i) { - return State::new_npt(eos, t, p, n_i, density_initialization); + return Ok(Ok(State::new_npt(eos, t, p, n_i, density_initialization)?)); } if let (Some(p), Some(t), Some(v)) = (pressure, temperature, v) { - return State::new_npvx(eos, t, p, v, &x_u, density_initialization); - } - - // Check if new state can be created using molar_enthalpy and temperature - if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { - return State::new_nph(eos, p, h, n_i, density_initialization, initial_temperature); - } - if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { - return State::new_nps(eos, p, s, n_i, density_initialization, initial_temperature); - } - if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { - return State::new_nth(eos, t, h, n_i, density_initialization); - } - if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { - return State::new_nts(eos, t, s, n_i, density_initialization); - } - if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { - return State::new_nvu(eos, v, u, n_i, initial_temperature); + return Ok(Ok(State::new_npvx( + eos, + t, + p, + v, + &x_u, + density_initialization, + )?)); } - Err(EosError::UndeterminedState(String::from( - "Missing input parameters.", - ))) + Ok(Err(n_i)) } /// Return a new `State` using a density iteration. [DensityInitialization] is used to @@ -479,9 +503,7 @@ impl State { (Ok(_), Err(_)) => liquid, (Err(_), Ok(_)) => vapor, (Ok(l), Ok(v)) => { - if l.molar_gibbs_energy(Contributions::Total) - > v.molar_gibbs_energy(Contributions::Total) - { + if l.residual_gibbs_energy() > v.residual_gibbs_energy() { vapor } else { liquid @@ -510,6 +532,78 @@ impl State { let moles = state.partial_density * volume; Self::new_nvt(eos, temperature, volume, &moles) } +} + +impl State { + /// Return a new `State` for the combination of inputs. + /// + /// The function attempts to create a new state using the given input values. If the state + /// is overdetermined, it will choose a method based on the following hierarchy. + /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. + /// 2. Use a density iteration for a given pressure. + /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ + /// + /// The [StateBuilder] provides a convenient way of calling this function without the need to provide + /// all the optional input values. + /// + /// # Errors + /// + /// When the state cannot be created using the combination of inputs. + pub fn new_full( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + molar_enthalpy: Option, + molar_entropy: Option, + molar_internal_energy: Option, + density_initialization: DensityInitialization, + initial_temperature: Option, + ) -> EosResult { + let state = Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + )?; + + let ti = initial_temperature; + match state { + Ok(state) => Ok(state), + Err(n_i) => { + // Check if new state can be created using molar_enthalpy and temperature + if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { + return State::new_nph(eos, p, h, n_i, density_initialization, ti); + } + if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { + return State::new_nps(eos, p, s, n_i, density_initialization, ti); + } + if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { + return State::new_nth(eos, t, h, n_i, density_initialization); + } + if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { + return State::new_nts(eos, t, s, n_i, density_initialization); + } + if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { + return State::new_nvu(eos, v, u, n_i, ti); + } + Err(EosError::UndeterminedState(String::from( + "Missing input parameters.", + ))) + } + } + } /// Return a new `State` for given pressure $p$ and molar enthalpy $h$. pub fn new_nph( @@ -621,56 +715,14 @@ impl State { }; newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } +} +impl State { /// Update the state with the given temperature pub fn update_temperature(&self, temperature: SINumber) -> EosResult { Self::new_nvt(&self.eos, temperature, self.volume, &self.moles) } - /// Update the state with the given chemical potential. - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for _ in 0..50 { - let dmu_drho = self.dmu_dni(Contributions::Total) * self.volume; - let f = self.chemical_potential(Contributions::Total) - chemical_potential; - let dmu_drho_r = dmu_drho - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let f_r = f.to_reduced(SIUnit::reference_molar_energy())?; - let rho = &self.partial_density - - &(LU::new(dmu_drho_r)?.solve(&f_r) * SIUnit::reference_density()); - *self = State::new_nvt( - &self.eos, - self.temperature, - self.volume, - &(rho * self.volume), - )?; - if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { - return Ok(()); - } - } - Err(EosError::NotConverged( - "State::update_chemical_potential".into(), - )) - } - - /// Update the state with the given molar Gibbs energy. - pub fn update_gibbs_energy(mut self, molar_gibbs_energy: SINumber) -> EosResult { - for _ in 0..50 { - let df = self.volume / self.density * self.dp_dv(Contributions::Total); - let f = self.molar_gibbs_energy(Contributions::Total) - molar_gibbs_energy; - let rho = self.density * (f.to_reduced(df)?).exp(); - self = State::new_nvt( - &self.eos, - self.temperature, - self.total_moles / rho, - &self.moles, - )?; - if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { - return Ok(self); - } - } - Err(EosError::NotConverged("State::update_gibbs_energy".into())) - } - /// Creates a [StateHD] cloning temperature, volume and moles. pub fn derive0(&self) -> StateHD { StateHD::new( @@ -746,7 +798,7 @@ fn is_close(x: SINumber, y: SINumber, atol: SINumber, rtol: f64) -> bool { (x - y).abs() <= atol + rtol * y.abs() } -fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> +fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> where F: FnMut(SINumber) -> EosResult<(SINumber, SINumber, State)>, { diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index dcaee2010..5567bb142 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,147 +1,50 @@ -use super::{Derivative::*, PartialDerivative, State}; -use crate::equation_of_state::{EntropyScaling, EquationOfState, MolarWeight}; -use crate::errors::EosResult; +use super::{Contributions, Derivative::*, PartialDerivative, State}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; use crate::EosUnit; -use ndarray::{arr1, Array1, Array2}; -use num_dual::DualNum; +use ndarray::Array1; use quantity::si::*; -use std::iter::FromIterator; -use std::ops::{Add, Deref, Sub}; -use std::sync::Arc; - -#[derive(Clone, Copy)] -pub(crate) enum Evaluate { - IdealGas, - Residual, - Total, - IdealGasDelta, -} - -/// Possible contributions that can be computed. -#[derive(Clone, Copy)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Contributions { - /// Only compute the ideal gas contribution - IdealGas, - /// Only compute the difference between the total and the ideal gas contribution - ResidualNvt, - /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state - ResidualNpt, - /// Compute ideal gas and residual contributions - Total, -} -/// # State properties -impl State { +impl State { fn get_or_compute_derivative( &self, derivative: PartialDerivative, - evaluate: Evaluate, + contributions: Contributions, ) -> SINumber { - if let Evaluate::IdealGasDelta = evaluate { - return match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps - * (SIUnit::reference_energy() / v.reference()) - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 - * (SIUnit::reference_energy() / (v.reference() * v.reference())) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - .eps1eps2 - * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v3 - * (SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference())) - } - }; - } - - let mut cache = self.cache.lock().unwrap(); - - let residual = match evaluate { - Evaluate::IdealGas => None, - _ => Some(match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() - / v.reference() - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference()) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() - / (v1.reference() * v2.reference()) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference()) - } - }), + let residual = match contributions { + Contributions::IdealGas => None, + _ => Some(self.get_or_compute_derivative_residual(derivative)), }; - let ideal_gas = match evaluate { - Evaluate::Residual => None, + let ideal_gas = match contributions { + Contributions::Residual => None, _ => Some(match derivative { PartialDerivative::Zeroth => { let new_state = self.derive0(); - self.eos.ideal_gas().evaluate(&new_state) + self.eos.evaluate_ideal_gas(&new_state) * SIUnit::reference_energy() * new_state.temperature } PartialDerivative::First(v) => { let new_state = self.derive1(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_energy() / v.reference() } PartialDerivative::Second(v) => { let new_state = self.derive2(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v2 * SIUnit::reference_energy() / (v.reference() * v.reference()) } PartialDerivative::SecondMixed(v1, v2) => { let new_state = self.derive2_mixed(v1, v2); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps1eps2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps1eps2 * SIUnit::reference_energy() / (v1.reference() * v2.reference()) } PartialDerivative::Third(v) => { let new_state = self.derive3(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v3 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v3 * SIUnit::reference_energy() / (v.reference() * v.reference() * v.reference()) } @@ -156,267 +59,56 @@ impl State { } } - fn evaluate_property(&self, f: F, contributions: Contributions, additive: bool) -> R - where - R: Add + Sub, - F: Fn(&Self, Evaluate) -> R, - { - match contributions { - Contributions::IdealGas => f(self, Evaluate::IdealGas), - Contributions::Total => f(self, Evaluate::Total), - Contributions::ResidualNvt => { - if additive { - f(self, Evaluate::Residual) - } else { - f(self, Evaluate::Total) - f(self, Evaluate::IdealGas) - } - } - Contributions::ResidualNpt => { - let p = self.pressure_(Evaluate::Total); - let state_p = Self::new_nvt_unchecked( - &self.eos, - self.temperature, - self.total_moles * SIUnit::gas_constant() * self.temperature / p, - &self.moles, - ); - if additive { - f(self, Evaluate::Residual) + f(self, Evaluate::IdealGasDelta) - - f(&state_p, Evaluate::IdealGasDelta) - } else { - f(self, Evaluate::Total) - f(&state_p, Evaluate::IdealGas) - } - } - } - } - - fn helmholtz_energy_(&self, evaluate: Evaluate) -> SINumber { - self.get_or_compute_derivative(PartialDerivative::Zeroth, evaluate) - } - - fn pressure_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DV), evaluate) - } - - fn entropy_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DT), evaluate) - } - - fn chemical_potential_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::First(DN(i)), evaluate) - }) - } - - fn dp_dv_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DV), evaluate) - } - - fn dp_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DT), evaluate) - } - - fn dp_dni_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DN(i)), evaluate) - }) - } - - fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) - } - - fn dmu_dt_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), evaluate) - }) - } - - fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { - let n = self.eos.components(); - SIArray::from_shape_fn((n, n), |(i, j)| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DN(i), DN(j)), evaluate) - }) - } - - fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) - } - - fn d2s_dt2_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Third(DT), evaluate) - } - - /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ - pub fn pressure(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::pressure_, contributions, true) - } - - /// Compressibility factor: $Z=\frac{pV}{NRT}$ - pub fn compressibility(&self, contributions: Contributions) -> f64 { - (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) - .into_value() - .unwrap() - } - - /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ - pub fn dp_dv(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dv_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ - pub fn dp_drho(&self, contributions: Contributions) -> SINumber { - -self.volume / self.density * self.dp_dv(contributions) - } - - /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ - pub fn dp_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dt_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ - pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dp_dni_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ - pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::d2p_dv2_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ - pub fn d2p_drho2(&self, contributions: Contributions) -> SINumber { - self.volume / (self.density * self.density) - * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) - } - - /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); - self.evaluate_property(func, contributions, false) - } - /// Chemical potential: $\mu_i=\left(\frac{\partial A}{\partial N_i}\right)_{T,V,N_j}$ pub fn chemical_potential(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::chemical_potential_, contributions, true) + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::First(DN(i)), contributions) + }) } /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ pub fn dmu_dt(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dmu_dt_, contributions, true) - } - - /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ - pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { - self.evaluate_property(Self::dmu_dni_, contributions, true) - } - - /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ - pub fn ln_phi(&self) -> Array1 { - (self.chemical_potential(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature)) - .into_value() - .unwrap() - } - - /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. - pub fn ln_phi_pure_liquid(&self) -> EosResult> { - let pressure = self.pressure(Contributions::Total); - (0..self.eos.components()) - .map(|i| { - let eos = Arc::new(self.eos.subset(&[i])); - let state = Self::new_npt( - &eos, - self.temperature, - pressure, - &(arr1(&[1.0]) * SIUnit::reference_moles()), - crate::DensityInitialization::Liquid, - )?; - Ok(state.ln_phi()[0]) - }) - .collect() - } - - /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ - pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { - match self.eos.components() { - 1 => Ok(arr1(&[0.0])), - _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), - } - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. temperature: $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,N_i}$ - pub fn dln_phi_dt(&self) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - (s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - - s.chemical_potential_(evaluate) / self.temperature) - / (SIUnit::gas_constant() * self.temperature) - }; - self.evaluate_property(func, Contributions::ResidualNpt, false) - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. pressure: $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,N_i}$ - pub fn dln_phi_dp(&self) -> SIArray1 { - self.partial_molar_volume(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature) - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$ - pub fn dln_phi_dnj(&self) -> SIArray2 { - let n = self.eos.components(); - let dmu_dni = self.dmu_dni(Contributions::ResidualNvt); - let dp_dni = self.dp_dni(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); - (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) - + 1.0 / self.total_moles - } - - /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ - pub fn thermodynamic_factor(&self) -> Array2 { - let dln_phi_dnj = self - .dln_phi_dnj() - .to_reduced(SIUnit::reference_moles().powi(-1)) - .unwrap(); - let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); - let n = self.eos.components() - 1; - Array2::from_shape_fn((n, n), |(i, j)| { - moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), contributions) }) } /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { - let func = - |s: &Self, evaluate: Evaluate| s.temperature * s.ds_dt_(evaluate) / s.total_moles; - self.evaluate_property(func, contributions, true) + self.temperature * self.ds_dt(contributions) / self.total_moles } /// Partial derivative of the molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V}{\partial T}\right)_{V,N_i}$ pub fn dc_v_dt(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - (s.temperature * s.d2s_dt2_(evaluate) + s.ds_dt_(evaluate)) / s.total_moles - }; - self.evaluate_property(func, contributions, true) + (self.temperature * self.d2s_dt2(contributions) + self.ds_dt(contributions)) + / self.total_moles } /// Molar isobaric heat capacity: $c_p=\left(\frac{\partial h}{\partial T}\right)_{p,N_i}$ pub fn c_p(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature / s.total_moles - * (s.ds_dt_(evaluate) - - s.dp_dt_(evaluate) * s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - }; - self.evaluate_property(func, contributions, false) + match contributions { + Contributions::Residual => self.c_p_res(), + _ => { + self.temperature / self.total_moles + * (self.ds_dt(contributions) + - self.dp_dt(contributions).powi(2) / self.dp_dv(contributions)) + } + } } /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ pub fn entropy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::entropy_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::First(DT), contributions) } /// Partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial S}{\partial T}\right)_{V,N_i}$ pub fn ds_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::ds_dt_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::Second(DT), contributions) + } + + /// Second partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,N_i}$ + pub fn d2s_dt2(&self, contributions: Contributions) -> SINumber { + -self.get_or_compute_derivative(PartialDerivative::Third(DT), contributions) } /// molar entropy: $s=\frac{S}{N}$ @@ -426,12 +118,9 @@ impl State { /// Enthalpy: $H=A+TS+pV$ pub fn enthalpy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) - + s.helmholtz_energy_(evaluate) - + s.pressure_(evaluate) * s.volume - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + + self.helmholtz_energy(contributions) + + self.pressure(contributions) * self.volume } /// molar enthalpy: $h=\frac{H}{N}$ @@ -441,7 +130,7 @@ impl State { /// Helmholtz energy: $A$ pub fn helmholtz_energy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::helmholtz_energy_, contributions, true) + self.get_or_compute_derivative(PartialDerivative::Zeroth, contributions) } /// molar Helmholtz energy: $a=\frac{A}{N}$ @@ -451,10 +140,7 @@ impl State { /// Internal energy: $U=A+TS$ pub fn internal_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + self.helmholtz_energy(contributions) } /// Molar internal energy: $u=\frac{U}{N}$ @@ -464,10 +150,7 @@ impl State { /// Gibbs energy: $G=A+pV$ pub fn gibbs_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.pressure_(evaluate) * s.volume + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.pressure(contributions) * self.volume + self.helmholtz_energy(contributions) } /// Molar Gibbs energy: $g=\frac{G}{N}$ @@ -476,17 +159,15 @@ impl State { } /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_entropy(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - -(s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate))) - }; - self.evaluate_property(func, contributions, false) + pub fn partial_molar_entropy(&self) -> SIArray1 { + let c = Contributions::Total; + -(self.dmu_dt(c) + self.dp_dni(c) * (self.dp_dt(c) / self.dp_dv(c))) } /// Partial molar enthalpy: $h_i=\left(\frac{\partial H}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_enthalpy(&self, contributions: Contributions) -> SIArray1 { - let s = self.partial_molar_entropy(contributions); - let mu = self.chemical_potential(contributions); + pub fn partial_molar_enthalpy(&self) -> SIArray1 { + let s = self.partial_molar_entropy(); + let mu = self.chemical_potential(Contributions::Total); s * self.temperature + mu } @@ -503,28 +184,16 @@ impl State { -self.c_v(c) / (self.c_p(c) * self.dp_dv(c) * self.volume) } - /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ - pub fn isothermal_compressibility(&self) -> SINumber { - let c = Contributions::Total; - -1.0 / (self.dp_dv(c) * self.volume) - } - - /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ - pub fn structure_factor(&self) -> f64 { - -(SIUnit::gas_constant() * self.temperature * self.density) - .to_reduced(self.volume * self.dp_dv(Contributions::Total)) - .unwrap() - } - /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { let new_state = self.derive0(); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); res.push(( - ig.to_string(), - ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), + self.eos.ideal_gas_model().to_string(), + self.eos.evaluate_ideal_gas(&new_state) + * new_state.temperature + * SIUnit::reference_energy(), )); for (s, v) in contributions { res.push((s, v * new_state.temperature * SIUnit::reference_energy())); @@ -532,34 +201,14 @@ impl State { res } - /// Pressure $p$ evaluated for each contribution of the equation of state. - pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { - let new_state = self.derive1(DV); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); - res.push(( - ig.to_string(), - -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - for (s, v) in contributions { - res.push(( - s, - -(v * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - } - res - } - /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { let new_state = self.derive1(DN(component)); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); res.push(( - ig.to_string(), - (ig.evaluate(&new_state) * new_state.temperature).eps + self.eos.ideal_gas_model().to_string(), + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); for (s, v) in contributions { @@ -576,7 +225,7 @@ impl State { /// /// These properties are available for equations of state /// that implement the [MolarWeight] trait. -impl State { +impl State { /// Total molar weight: $MW=\sum_ix_iMW_i$ pub fn total_molar_weight(&self) -> SINumber { (self.eos.molar_weight() * &self.molefracs).sum() @@ -601,7 +250,9 @@ impl State { pub fn massfracs(&self) -> Array1 { self.mass().to_reduced(self.total_mass()).unwrap() } +} +impl State { /// Specific entropy: $s^{(m)}=\frac{S}{m}$ pub fn specific_entropy(&self, contributions: Contributions) -> SINumber { self.molar_entropy(contributions) / self.total_molar_weight() @@ -634,214 +285,3 @@ impl State { .unwrap() } } - -impl State { - // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - ) - } - - // This function is designed specifically for use in spinodal iterations - pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { - let d2p_dv2 = self.d2p_dv2(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), - ) - } -} - -/// # Transport properties -/// -/// These properties are available for equations of state -/// that implement the [EntropyScaling] trait. -impl State { - /// Return the viscosity via entropy scaling. - pub fn viscosity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .viscosity_reference(self.temperature, self.volume, &self.moles)? - * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced viscosity. - /// - /// This term equals the viscosity correlation function - /// that is used for entropy scaling. - pub fn ln_viscosity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.viscosity_correlation(s, &self.molefracs) - } - - /// Return the viscosity reference as used in entropy scaling. - pub fn viscosity_reference(&self) -> EosResult { - self.eos - .viscosity_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the diffusion via entropy scaling. - pub fn diffusion(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .diffusion_reference(self.temperature, self.volume, &self.moles)? - * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced diffusion. - /// - /// This term equals the diffusion correlation function - /// that is used for entropy scaling. - pub fn ln_diffusion_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.diffusion_correlation(s, &self.molefracs) - } - - /// Return the diffusion reference as used in entropy scaling. - pub fn diffusion_reference(&self) -> EosResult { - self.eos - .diffusion_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the thermal conductivity via entropy scaling. - pub fn thermal_conductivity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? - * self - .eos - .thermal_conductivity_correlation(s, &self.molefracs)? - .exp()) - } - - /// Return the logarithm of the reduced thermal conductivity. - /// - /// This term equals the thermal conductivity correlation function - /// that is used for entropy scaling. - pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos - .thermal_conductivity_correlation(s, &self.molefracs) - } - - /// Return the thermal conductivity reference as used in entropy scaling. - pub fn thermal_conductivity_reference(&self) -> EosResult { - self.eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) - } -} - -/// A list of states for a simple access to properties -/// of multiple states. -pub struct StateVec<'a, E>(pub Vec<&'a State>); - -impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { - fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl<'a, E> IntoIterator for StateVec<'a, E> { - type Item = &'a State; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a, E> Deref for StateVec<'a, E> { - type Target = Vec<&'a State>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, E: EquationOfState> StateVec<'a, E> { - pub fn temperature(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) - } - - pub fn pressure(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) - } - - pub fn compressibility(&self) -> Array1 { - Array1::from_shape_fn(self.0.len(), |i| { - self.0[i].compressibility(Contributions::Total) - }) - } - - pub fn density(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) - } - - pub fn moles(&self) -> SIArray2 { - SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].moles.get(j) - }) - } - - pub fn molefracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].molefracs[j] - }) - } - - pub fn molar_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_enthalpy(Contributions::Total) - }) - } - - pub fn molar_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_entropy(Contributions::Total) - }) - } -} - -impl<'a, E: EquationOfState + MolarWeight> StateVec<'a, E> { - pub fn mass_density(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) - } - - pub fn massfracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].massfracs()[j] - }) - } - - pub fn specific_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_enthalpy(Contributions::Total) - }) - } - - pub fn specific_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_entropy(Contributions::Total) - }) - } -} diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs new file mode 100644 index 000000000..42f2f06f1 --- /dev/null +++ b/feos-core/src/state/residual_properties.rs @@ -0,0 +1,439 @@ +use super::{Contributions, Derivative::*, PartialDerivative, State}; +use crate::equation_of_state::{EntropyScaling, Residual}; +use crate::errors::EosResult; +use crate::EosUnit; +use ndarray::{arr1, Array1, Array2}; +use quantity::si::*; +use std::sync::Arc; + +/// # State properties +impl State { + pub(super) fn get_or_compute_derivative_residual( + &self, + derivative: PartialDerivative, + ) -> SINumber { + let mut cache = self.cache.lock().unwrap(); + + match derivative { + PartialDerivative::Zeroth => { + let new_state = self.derive0(); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() + } + PartialDerivative::First(v) => { + let new_state = self.derive1(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() + / v.reference() + } + PartialDerivative::Second(v) => { + let new_state = self.derive2(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference()) + } + PartialDerivative::SecondMixed(v1, v2) => { + let new_state = self.derive2_mixed(v1, v2); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() + / (v1.reference() * v2.reference()) + } + PartialDerivative::Third(v) => { + let new_state = self.derive3(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference() * v.reference()) + } + } + } +} + +impl State { + fn contributions( + ideal_gas: SINumber, + residual: SINumber, + contributions: Contributions, + ) -> SINumber { + match contributions { + Contributions::IdealGas => ideal_gas, + Contributions::Total => ideal_gas + residual, + Contributions::Residual => residual, + } + } + + /// Residual Helmholtz energy $A^\text{res}$ + pub fn residual_helmholtz_energy(&self) -> SINumber { + self.get_or_compute_derivative_residual(PartialDerivative::Zeroth) + } + + /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn residual_entropy(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)) + } + + /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ + pub fn pressure(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant() * self.temperature; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::First(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ + pub fn residual_chemical_potential(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) + }) + } + + /// Compressibility factor: $Z=\frac{pV}{NRT}$ + pub fn compressibility(&self, contributions: Contributions) -> f64 { + (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) + .into_value() + .unwrap() + } + + // pressure derivatives + + /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ + pub fn dp_dv(&self, contributions: Contributions) -> SINumber { + let ideal_gas = -self.density * SIUnit::gas_constant() * self.temperature / self.volume; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ + pub fn dp_drho(&self, contributions: Contributions) -> SINumber { + -self.volume / self.density * self.dp_dv(contributions) + } + + /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ + pub fn dp_dt(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant(); + let residual = + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DT)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ + pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { + match contributions { + Contributions::IdealGas => { + SIArray::from_vec(vec![ + SIUnit::gas_constant() * self.temperature / self.volume; + self.eos.components() + ]) + } + Contributions::Residual => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + }), + Contributions::Total => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + + SIUnit::gas_constant() * self.temperature / self.volume + }), + } + } + + /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ + pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { + let ideal_gas = 2.0 * self.density * SIUnit::gas_constant() * self.temperature + / (self.volume * self.volume); + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Third(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ + pub fn d2p_drho2(&self, contributions: Contributions) -> SINumber { + self.volume / (self.density * self.density) + * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) + } + + /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ + pub fn structure_factor(&self) -> f64 { + -(SIUnit::gas_constant() * self.temperature * self.density) + .to_reduced(self.volume * self.dp_dv(Contributions::Total)) + .unwrap() + } + + // This function is designed specifically for use in density iterations + pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + ) + } + + /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ + pub fn partial_molar_volume(&self) -> SIArray1 { + -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total) + } + + /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { + let n = self.eos.components(); + SIArray::from_shape_fn((n, n), |(i, j)| { + let ideal_gas = if i == j { + SIUnit::gas_constant() * self.temperature / self.moles.get(i) + } else { + 0.0 * SIUnit::reference_molar_energy() / SIUnit::reference_moles() + }; + let residual = self + .get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DN(i), DN(j))); + Self::contributions(ideal_gas, residual, contributions) + }) + } + + // This function is designed specifically for use in spinodal iterations + pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { + let d2p_dv2 = self.d2p_dv2(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), + ) + } + + /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ + pub fn isothermal_compressibility(&self) -> SINumber { + -1.0 / (self.dp_dv(Contributions::Total) * self.volume) + } + + /// Pressure $p$ evaluated for each contribution of the equation of state. + pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { + let new_state = self.derive1(DV); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + "Ideal gas".into(), + self.density * SIUnit::gas_constant() * self.temperature, + )); + for (s, v) in contributions { + res.push(( + s, + -(v * new_state.temperature).eps * SIUnit::reference_pressure(), + )); + } + res + } + + // entropy derivatives + + /// Partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial S^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn ds_res_dt(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)) + } + + /// Second partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial^2S^\text{res}}{\partial T^2}\right)_{V,N_i}$ + pub fn d2s_res_dt2(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)) + } + + /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ + pub fn dmu_res_dt(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DT, DN(i))) + }) + } + + /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ + pub fn ln_phi(&self) -> Array1 { + (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) + .into_value() + .unwrap() + - self.compressibility(Contributions::Total).ln() + } + + /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. + pub fn ln_phi_pure_liquid(&self) -> EosResult> { + let pressure = self.pressure(Contributions::Total); + (0..self.eos.components()) + .map(|i| { + let eos = Arc::new(self.eos.subset(&[i])); + let state = Self::new_npt( + &eos, + self.temperature, + pressure, + &(arr1(&[1.0]) * SIUnit::reference_moles()), + crate::DensityInitialization::Liquid, + )?; + Ok(state.ln_phi()[0]) + }) + .collect() + } + + /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ + pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { + match self.eos.components() { + 1 => Ok(arr1(&[0.0])), + _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), + } + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. temperature: $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,N_i}$ + pub fn dln_phi_dt(&self) -> SIArray1 { + let vi = self.partial_molar_volume(); + (self.dmu_res_dt() + - self.residual_chemical_potential() / self.temperature + - vi * self.dp_dt(Contributions::Total)) + / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.temperature + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. pressure: $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,N_i}$ + pub fn dln_phi_dp(&self) -> SIArray1 { + self.partial_molar_volume() / (SIUnit::gas_constant() * self.temperature) + - 1.0 / self.pressure(Contributions::Total) + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$ + pub fn dln_phi_dnj(&self) -> SIArray2 { + let n = self.eos.components(); + let dmu_dni = self.dmu_dni(Contributions::Residual); + let dp_dni = self.dp_dni(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); + (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.total_moles + } + + /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ + pub fn thermodynamic_factor(&self) -> Array2 { + let dln_phi_dnj = self + .dln_phi_dnj() + .to_reduced(SIUnit::reference_moles().powi(-1)) + .unwrap(); + let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); + let n = self.eos.components() - 1; + Array2::from_shape_fn((n, n), |(i, j)| { + moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } + }) + } + + /// Molar residual isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn c_v_res(&self) -> SINumber { + self.temperature * self.ds_res_dt() / self.total_moles + } + + /// Partial derivative of the molar residual isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn dc_v_res_dt(&self) -> SINumber { + (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles + } + + /// Molar residual isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$ + pub fn c_p_res(&self) -> SINumber { + self.temperature / self.total_moles + * (self.ds_res_dt() + - self.dp_dt(Contributions::Total).powi(2) / self.dp_dv(Contributions::Total)) + - SIUnit::gas_constant() + } + + /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$ + pub fn residual_enthalpy(&self) -> SINumber { + self.temperature * self.residual_entropy() + + self.residual_helmholtz_energy() + + self.pressure(Contributions::Residual) * self.volume + } + + /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$ + pub fn residual_internal_energy(&self) -> SINumber { + self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() + } + + /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+p^\text{res}V-NRT \ln Z$ + pub fn residual_gibbs_energy(&self) -> SINumber { + self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() + - self.total_moles + * SIUnit::gas_constant() + * self.temperature + * self.compressibility(Contributions::Total).ln() + } +} + +/// # Transport properties +/// +/// These properties are available for equations of state +/// that implement the [EntropyScaling] trait. +impl State { + /// Return the viscosity via entropy scaling. + pub fn viscosity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .viscosity_reference(self.temperature, self.volume, &self.moles)? + * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced viscosity. + /// + /// This term equals the viscosity correlation function + /// that is used for entropy scaling. + pub fn ln_viscosity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.viscosity_correlation(s, &self.molefracs) + } + + /// Return the viscosity reference as used in entropy scaling. + pub fn viscosity_reference(&self) -> EosResult { + self.eos + .viscosity_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the diffusion via entropy scaling. + pub fn diffusion(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .diffusion_reference(self.temperature, self.volume, &self.moles)? + * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced diffusion. + /// + /// This term equals the diffusion correlation function + /// that is used for entropy scaling. + pub fn ln_diffusion_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.diffusion_correlation(s, &self.molefracs) + } + + /// Return the diffusion reference as used in entropy scaling. + pub fn diffusion_reference(&self) -> EosResult { + self.eos + .diffusion_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the thermal conductivity via entropy scaling. + pub fn thermal_conductivity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? + * self + .eos + .thermal_conductivity_correlation(s, &self.molefracs)? + .exp()) + } + + /// Return the logarithm of the reduced thermal conductivity. + /// + /// This term equals the thermal conductivity correlation function + /// that is used for entropy scaling. + pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos + .thermal_conductivity_correlation(s, &self.molefracs) + } + + /// Return the thermal conductivity reference as used in entropy scaling. + pub fn thermal_conductivity_reference(&self) -> EosResult { + self.eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) + } +} diff --git a/feos-core/src/state/statevec.rs b/feos-core/src/state/statevec.rs new file mode 100644 index 000000000..cb752fe63 --- /dev/null +++ b/feos-core/src/state/statevec.rs @@ -0,0 +1,103 @@ +use super::{Contributions, State}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; +use ndarray::{Array1, Array2}; +use quantity::si::{SIArray1, SIArray2}; +use std::iter::FromIterator; +use std::ops::Deref; + +/// A list of states for a simple access to properties +/// of multiple states. +pub struct StateVec<'a, E>(pub Vec<&'a State>); + +impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { + fn from_iter>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'a, E> IntoIterator for StateVec<'a, E> { + type Item = &'a State; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, E> Deref for StateVec<'a, E> { + type Target = Vec<&'a State>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, E: Residual> StateVec<'a, E> { + pub fn temperature(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) + } + + pub fn pressure(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) + } + + pub fn compressibility(&self) -> Array1 { + Array1::from_shape_fn(self.0.len(), |i| { + self.0[i].compressibility(Contributions::Total) + }) + } + + pub fn density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) + } + + pub fn moles(&self) -> SIArray2 { + SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].moles.get(j) + }) + } + + pub fn molefracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].molefracs[j] + }) + } +} + +impl<'a, E: Residual + IdealGas> StateVec<'a, E> { + pub fn molar_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_enthalpy(Contributions::Total) + }) + } + + pub fn molar_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_entropy(Contributions::Total) + }) + } +} + +impl<'a, E: Residual + IdealGas + MolarWeight> StateVec<'a, E> { + pub fn mass_density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) + } + + pub fn massfracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].massfracs()[j] + }) + } + + pub fn specific_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_enthalpy(Contributions::Total) + }) + } + + pub fn specific_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_entropy(Contributions::Total) + }) + } +} diff --git a/feos-derive/src/components.rs b/feos-derive/src/components.rs new file mode 100644 index 000000000..aac441690 --- /dev/null +++ b/feos-derive/src/components.rs @@ -0,0 +1,59 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_components(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let components = impl_components(input.ident, variants); + Ok(quote! { + #components + }) +} + +fn impl_components( + ident: syn::Ident, + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let components = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => *n + } + } else { + quote! { + Self::#name(residual) => residual.components() + } + } + }); + let subset = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => Self::#name(component_list.len()) + } + } else { + quote! { + Self::#name(residual) => Self::#name(residual.subset(component_list)) + } + } + }); + + quote! { + impl Components for #ident { + fn components(&self) -> usize { + match self { + #(#components,)* + } + } + fn subset(&self, component_list: &[usize]) -> Self { + match self { + #(#subset,)* + } + } + } + } +} diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 00ce3788d..647ef5110 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -82,12 +82,6 @@ fn impl_from( fn impl_helmholtz_energy_functional( variants: &syn::punctuated::Punctuated, ) -> syn::Result { - let subset = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.subset(component_list).into() - } - }); let molecule_shape = variants.iter().map(|v| { let name = &v.ident; quote! { @@ -106,12 +100,6 @@ fn impl_helmholtz_energy_functional( Self::#name(functional) => functional.contributions() } }); - let ideal_gas = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.ideal_gas() - } - }); let mut bond_lengths = Vec::new(); for v in variants.iter() { @@ -125,11 +113,6 @@ fn impl_helmholtz_energy_functional( Ok(quote! { impl HelmholtzEnergyFunctional for FunctionalVariant { - fn subset(&self, component_list: &[usize]) -> DFT { - match self { - #(#subset,)* - } - } fn molecule_shape(&self) -> MoleculeShape { match self { #(#molecule_shape,)* @@ -145,11 +128,6 @@ fn impl_helmholtz_energy_functional( #(#contributions,)* } } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match self { - #(#ideal_gas,)* - } - } fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { match self { #(#bond_lengths,)* diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs new file mode 100644 index 000000000..69f8d9b63 --- /dev/null +++ b/feos-derive/src/ideal_gas.rs @@ -0,0 +1,41 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_ideal_gas(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let ideal_gas = impl_ideal_gas(variants); + Ok(quote! { + #ideal_gas + }) +} + +fn impl_ideal_gas( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let ideal_gas_model = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(_) => panic!("No ideal gas model initialized!") + } + } else { + quote! { + Self::#name(ideal_gas) => ideal_gas.ideal_gas_model() + } + } + }); + + quote! { + impl IdealGas for IdealGasModel { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + match self { + #(#ideal_gas_model,)* + } + } + } + } +} diff --git a/feos-derive/src/lib.rs b/feos-derive/src/lib.rs index 667b93ec6..e91a82b76 100644 --- a/feos-derive/src/lib.rs +++ b/feos-derive/src/lib.rs @@ -1,13 +1,17 @@ //! This crate provides derive macros used for the EosVariant and //! FunctionalVariant enums in FeOs. The macros implement //! the boilerplate for the EquationOfState and HelmholtzEnergyFunctional traits. +use components::expand_components; use dft::expand_helmholtz_energy_functional; -use eos::expand_equation_of_state; +use ideal_gas::expand_ideal_gas; use proc_macro::TokenStream; +use residual::expand_residual; use syn::{parse_macro_input, DeriveInput}; +mod components; mod dft; -mod eos; +mod ideal_gas; +mod residual; fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn::Result { let syn::Variant { attrs, .. } = variant; @@ -43,10 +47,26 @@ fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn:: implement } -#[proc_macro_derive(EquationOfState, attributes(implement))] -pub fn derive_equation_of_state(input: TokenStream) -> TokenStream { +#[proc_macro_derive(Components)] +pub fn derive_components(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_equation_of_state(input) + expand_components(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(IdealGas)] +pub fn derive_ideal_gas(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_ideal_gas(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(Residual, attributes(implement))] +pub fn derive_residual(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_residual(input) .unwrap_or_else(syn::Error::into_compile_error) .into() } diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs new file mode 100644 index 000000000..6dd9476d6 --- /dev/null +++ b/feos-derive/src/residual.rs @@ -0,0 +1,175 @@ +use super::implement; +use quote::quote; +use syn::DeriveInput; + +// possible additional traits to implement +const OPT_IMPLS: [&str; 2] = ["molar_weight", "entropy_scaling"]; + +pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let residual = impl_residual(variants); + let molar_weight = impl_molar_weight(variants)?; + let entropy_scaling = impl_entropy_scaling(variants)?; + Ok(quote! { + #residual + #molar_weight + #entropy_scaling + }) +} + +fn impl_residual( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let compute_max_density = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.compute_max_density(moles) + } + }); + let contributions = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.contributions() + } + }); + + quote! { + impl Residual for ResidualModel { + fn compute_max_density(&self, moles: &Array1) -> f64 { + match self { + #(#compute_max_density,)* + } + } + fn contributions(&self) -> &[Box] { + match self { + #(#contributions,)* + } + } + } + } +} + +fn impl_molar_weight( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut molar_weight = Vec::new(); + + for v in variants.iter() { + if implement("molar_weight", v, &OPT_IMPLS)? { + let name = &v.ident; + molar_weight.push(quote! { + Self::#name(residual) => residual.molar_weight() + }); + } + } + Ok(quote! { + impl MolarWeight for ResidualModel { + fn molar_weight(&self) -> SIArray1 { + match self { + #(#molar_weight,)* + _ => unimplemented!() + } + } + } + }) +} + +fn impl_entropy_scaling( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut etar = Vec::new(); + let mut etac = Vec::new(); + let mut dr = Vec::new(); + let mut dc = Vec::new(); + let mut thcr = Vec::new(); + let mut thcc = Vec::new(); + + for v in variants.iter() { + if implement("entropy_scaling", v, &OPT_IMPLS)? { + let name = &v.ident; + etar.push(quote! { + Self::#name(eos) => eos.viscosity_reference(temperature, volume, moles) + }); + etac.push(quote! { + Self::#name(eos) => eos.viscosity_correlation(s_res, x) + }); + dr.push(quote! { + Self::#name(eos) => eos.diffusion_reference(temperature, volume, moles) + }); + dc.push(quote! { + Self::#name(eos) => eos.diffusion_correlation(s_res, x) + }); + thcr.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_reference(temperature, volume, moles) + }); + thcc.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_correlation(s_res, x) + }); + } + } + + Ok(quote! { + impl EntropyScaling for ResidualModel { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#etar,)* + _ => unimplemented!(), + } + } + + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#etac,)* + _ => unimplemented!(), + } + } + + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#dr,)* + _ => unimplemented!(), + } + } + + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#dc,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#thcr,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#thcc,)* + _ => unimplemented!(), + } + } + } + }) +} diff --git a/feos-dft/CHANGELOG.md b/feos-dft/CHANGELOG.md index 9a8fd9824..1464fdf1e 100644 --- a/feos-dft/CHANGELOG.md +++ b/feos-dft/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Implemented `HelmholtzEnergyFunctional` for `EquationOfState` to be able to use functionals as equations of state. [#158](https://github.com/feos-org/feos/pull/158) + +### Changed +- `HelmholtzEnergyFunctional`: added `Components` trait as trait bound and removed `ideal_gas` method. [#158](https://github.com/feos-org/feos/pull/158) +- `DFT` now implements `Residual` and furthermore `IdealGas` if `F` implements `IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- What properties (and contributions) of `DFTProfile` are available now depends on whether an ideal gas model is provided or not. [#158](https://github.com/feos-org/feos/pull/158) + +### Removed +- Removed `DefaultIdealGasContribution` [#158](https://github.com/feos-org/feos/pull/158) +- Removed getters for `chemical_potential` (for profiles) and `molar_gibbs_energy` (for `Adsorption1D` and `Adsorption3D`) from Python interface. [#158](https://github.com/feos-org/feos/pull/158) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index 3f517a079..b79227403 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -2,7 +2,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::solver::DFTSolver; use feos_core::{ - Contributions, DensityInitialization, EosError, EosResult, EosUnit, EquationOfState, + Components, Contributions, DensityInitialization, EosError, EosResult, EosUnit, Residual, SolverOptions, State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; @@ -273,36 +273,29 @@ where .moles(&moles) .vapor() .build()?; - let liquid_bulk = StateBuilder::new(functional) + let bulk_init = StateBuilder::new(functional) .temperature(temperature) .pressure(p_max) .moles(&moles) .liquid() .build()?; + let liquid_bulk = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_max) + .moles(&moles) + .vapor() + .build()?; let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; - let mut liquid = pore.initialize(&liquid_bulk, None, None)?.solve(solver)?; + let mut liquid = pore.initialize(&bulk_init, None, None)?.solve(solver)?; // calculate initial value for the molar gibbs energy - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { - Ok(s.grand_potential.unwrap() - + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) - }; - let mut g = (f(&liquid, nl)? - f(&vapor, nv)?) / (nl - nv); + let n_dp_drho_v = (vapor.profile.moles() * vapor_bulk.dp_drho(Contributions::Total)).sum(); + let n_dp_drho_l = + (liquid.profile.moles() * liquid_bulk.dp_drho(Contributions::Total)).sum(); + let mut rho = (vapor.grand_potential.unwrap() + n_dp_drho_v + - (liquid.grand_potential.unwrap() + n_dp_drho_l)) + / (n_dp_drho_v / vapor_bulk.density - n_dp_drho_l / liquid_bulk.density); // update filled pore with limited step size let mut bulk = StateBuilder::new(functional) @@ -311,12 +304,12 @@ where .moles(&moles) .vapor() .build()?; - let g_liquid = liquid.profile.bulk.molar_gibbs_energy(Contributions::Total); - let steps = (10.0 * (g - g_liquid)).to_reduced(g_liquid)?.abs().ceil() as usize; - let delta_g = (g - g_liquid) / steps as f64; + let rho0 = liquid_bulk.density; + let steps = (10.0 * (rho - rho0)).to_reduced(rho0)?.abs().ceil() as usize; + let delta_rho = (rho - rho0) / steps as f64; for i in 1..=steps { - let g_i = g_liquid + i as f64 * delta_g; - bulk = bulk.update_gibbs_energy(g_i)?; + let rho_i = rho0 + i as f64 * delta_rho; + bulk = State::new_nvt(functional, temperature, moles.sum() / rho_i, &moles)?; liquid = liquid.update_bulk(&bulk).solve(solver)?; } @@ -328,30 +321,15 @@ where liquid = liquid.update_bulk(&bulk).solve(solver)?; // calculate moles - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - - // check for a trivial solution - if nl.to_reduced(nv)? - 1.0 < 1e-5 { - return Err(EosError::TrivialSolution); - } + let n_dp_drho = ((liquid.profile.moles() - vapor.profile.moles()) + * bulk.dp_drho(Contributions::Total)) + .sum(); // Newton step - let delta_g = - (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); - if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() + let delta_rho = (liquid.grand_potential.unwrap() - vapor.grand_potential.unwrap()) + / n_dp_drho + * bulk.density; + if delta_rho.to_reduced(SIUnit::reference_density())?.abs() < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) { return Ok(Adsorption::new( @@ -360,10 +338,10 @@ where vec![Ok(vapor), Ok(liquid)], )); } - g += delta_g; + rho += delta_rho; // update bulk phase - bulk = bulk.update_gibbs_energy(g)?; + bulk = State::new_nvt(functional, temperature, moles.sum() / rho, &moles)?; } Err(EosError::NotConverged( "Adsorption::phase_equilibrium".into(), @@ -390,26 +368,6 @@ where }) } - pub fn molar_gibbs_energy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { - Ok(p) => { - if p.profile.bulk.eos.components() > 1 - && !p.profile.bulk.is_stable(SolverOptions::default()).unwrap() - { - p.profile - .bulk - .tp_flash(None, SolverOptions::default(), None) - .unwrap() - .vapor() - .molar_gibbs_energy(Contributions::Total) - } else { - p.profile.bulk.molar_gibbs_energy(Contributions::Total) - } - } - Err(_) => f64::NAN * SIUnit::reference_molar_energy(), - }) - } - pub fn adsorption(&self) -> SIArray2 { SIArray2::from_shape_fn((self.components, self.profiles.len()), |(j, i)| match &self .profiles[i] diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 5f3ac4501..713f6efae 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -5,7 +5,7 @@ use crate::functional_contribution::FunctionalContribution; use crate::geometry::{Axis, Geometry, Grid}; use crate::profile::{DFTProfile, MAX_POTENTIAL}; use crate::solver::DFTSolver; -use feos_core::{Contributions, EosResult, EosUnit, State, StateBuilder}; +use feos_core::{Components, Contributions, EosResult, EosUnit, State, StateBuilder}; use ndarray::prelude::*; use ndarray::Axis as Axis_nd; use ndarray::RemoveAxis; @@ -264,6 +264,7 @@ fn external_potential_1d( const EPSILON_HE: f64 = 10.9; const SIGMA_HE: f64 = 2.64; +#[derive(Clone)] struct Helium { epsilon: Array1, sigma: Array1, @@ -273,7 +274,17 @@ impl Helium { fn new() -> DFT { let epsilon = arr1(&[EPSILON_HE]); let sigma = arr1(&[SIGMA_HE]); - (Self { epsilon, sigma }).into() + DFT(Self { epsilon, sigma }) + } +} + +impl Components for Helium { + fn components(&self) -> usize { + 1 + } + + fn subset(&self, _: &[usize]) -> Self { + self.clone() } } @@ -282,10 +293,6 @@ impl HelmholtzEnergyFunctional for Helium { &[] } - fn subset(&self, _: &[usize]) -> DFT { - Self::new() - } - fn compute_max_density(&self, _: &Array1) -> f64 { 1.0 } diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 430b6e608..9cd3d4e5c 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -1,35 +1,61 @@ +use crate::adsorption::FluidParameters; use crate::convolver::Convolver; use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; +use crate::solvation::PairPotential; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; use feos_core::{ - Contributions, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, - IdealGasContribution, IdealGasContributionDual, MolarWeight, StateHD, + Components, DeBroglieWavelength, EosResult, EquationOfState, HelmholtzEnergy, + HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::*; use num_dual::*; use petgraph::graph::{Graph, UnGraph}; use petgraph::visit::EdgeRef; use petgraph::Directed; -// use quantity::{QuantityArray, SIArray1, SINumber}; -use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; +use quantity::si::SIArray1; use std::borrow::Cow; -use std::fmt; -use std::ops::{AddAssign, Deref, MulAssign}; +use std::ops::{Deref, MulAssign}; use std::sync::Arc; +impl HelmholtzEnergyFunctional + for EquationOfState +{ + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } + + fn molecule_shape(&self) -> MoleculeShape { + self.residual.molecule_shape() + } + + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } +} + +impl PairPotential for EquationOfState { + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2 { + self.residual.pair_potential(i, r, temperature) + } +} + +impl FluidParameters for EquationOfState { + fn epsilon_k_ff(&self) -> Array1 { + self.residual.epsilon_k_ff() + } + + fn sigma_ff(&self) -> &Array1 { + self.residual.sigma_ff() + } +} + /// Wrapper struct for the [HelmholtzEnergyFunctional] trait. /// -/// Needed (for now) to generically implement the `EquationOfState` +/// Needed (for now) to generically implement the `Residual` /// trait for Helmholtz energy functionals. #[derive(Clone)] -pub struct DFT(F); - -impl From for DFT { - fn from(functional: F) -> Self { - Self(functional) - } -} +pub struct DFT(pub F); impl DFT { pub fn into>(self) -> DFT { @@ -44,39 +70,34 @@ impl Deref for DFT { } } -impl MolarWeight for DFT { - fn molar_weight(&self) -> SIArray1 { - (self as &T).molar_weight() - } -} - -struct DefaultIdealGasContribution(); -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) +impl DFT { + pub fn ideal_gas(self, ideal_gas: I) -> DFT> { + DFT(EquationOfState::new(Arc::new(ideal_gas), Arc::new(self.0))) } } -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") +impl MolarWeight for DFT { + fn molar_weight(&self) -> SIArray1 { + self.0.molar_weight() } } -impl EquationOfState for DFT { +impl Components for DFT { fn components(&self) -> usize { - self.component_index()[self.component_index().len() - 1] + 1 + self.0.components() } fn subset(&self, component_list: &[usize]) -> Self { - (self as &T).subset(component_list) + Self(self.0.subset(component_list)) } +} +impl Residual for DFT { fn compute_max_density(&self, moles: &Array1) -> f64 { - (self as &T).compute_max_density(moles) + self.0.compute_max_density(moles) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { unreachable!() } @@ -84,7 +105,8 @@ impl EquationOfState for DFT { where dyn HelmholtzEnergy: HelmholtzEnergyDual, { - self.contributions() + self.0 + .contributions() .iter() .map(|c| (c as &dyn HelmholtzEnergy).helmholtz_energy(state)) .sum::() @@ -99,6 +121,7 @@ impl EquationOfState for DFT { dyn HelmholtzEnergy: HelmholtzEnergyDual, { let mut res: Vec<(String, D)> = self + .0 .contributions() .iter() .map(|c| { @@ -114,9 +137,11 @@ impl EquationOfState for DFT { )); res } +} - fn ideal_gas(&self) -> &dyn IdealGasContribution { - (self as &T).ideal_gas() +impl IdealGas for DFT { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self.0.ideal_gas_model() } } @@ -133,16 +158,13 @@ pub enum MoleculeShape<'a> { } /// A general Helmholtz energy functional. -pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { +pub trait HelmholtzEnergyFunctional: Components + Sized + Send + Sync { /// Return a slice of [FunctionalContribution]s. fn contributions(&self) -> &[Box]; /// Return the shape of the molecules and the necessary specifications. fn molecule_shape(&self) -> MoleculeShape; - /// Return a functional for the specified subset of components. - fn subset(&self, component_list: &[usize]) -> DFT; - /// Return the maximum density in Angstrom^-3. /// /// This value is used as an estimate for a liquid phase for phase @@ -151,18 +173,6 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { /// equation of state anyways). fn compute_max_density(&self, moles: &Array1) -> f64; - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of internal energies) this - /// function has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution() - } - /// Overwrite this, if the functional consists of heterosegmented chains. fn bond_lengths(&self, _temperature: f64) -> UnGraph<(), f64> { Graph::with_capacity(0, 0) @@ -196,218 +206,10 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { fn ideal_chain_contribution(&self) -> IdealChainContribution { IdealChainContribution::new(&self.component_index(), &self.m()) } -} - -impl DFT { - /// Calculate the grand potential density $\omega$. - pub fn grand_potential_density( - &self, - temperature: SINumber, - density: &SIArray, - convolver: &Arc>, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - // Calculate residual Helmholtz energy density and functional derivative - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - let (mut f, dfdrho) = self.functional_derivative(t, &rho, convolver)?; - - // Calculate the grand potential density - for ((rho, dfdrho), &m) in rho - .outer_iter() - .zip(dfdrho.outer_iter()) - .zip(self.m().iter()) - { - f -= &((&dfdrho + m) * &rho); - } - - let bond_lengths = self.bond_lengths(t); - for segment in bond_lengths.node_indices() { - let n = bond_lengths.neighbors(segment).count(); - f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); - } - - Ok(f * t * SIUnit::reference_pressure()) - } - - pub(crate) fn ideal_gas_contribution( - &self, - temperature: f64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (rhoi.ln() + lambda[i] - 1.0) * rhoi); - } - phi * temperature - } - - fn ideal_gas_contribution_dual( - &self, - temperature: Dual64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); - } - phi * temperature - } - - fn intrinsic_helmholtz_energy_density( - &self, - temperature: N, - density: &Array, - convolver: &Arc>, - ) -> EosResult> - where - N: DualNum + Copy + ScalarOperand, - dyn FunctionalContribution: FunctionalContributionDual, - D: Dimension, - D::Larger: Dimension, - { - let density_dual = density.mapv(N::from); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Array = self - .ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(N::from))?; - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density - .view_mut() - .into_shape(ngrid) - .unwrap() - .add_assign(&c.calculate_helmholtz_energy_density( - temperature, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )?); - } - Ok(helmholtz_energy_density * temperature) - } - - /// Calculate the entropy density $s$. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - Ok(helmholtz_energy_density.mapv(|f| -f.eps)) - } - - /// Calculate the individual contributions to the entropy density. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density_contributions( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - ) -> EosResult>> - where - D: Dimension, - D::Larger: Dimension, - ::Larger: Dimension, - { - let density_dual = density.mapv(Dual64::from); - let temperature_dual = Dual64::from(temperature).derivative(); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Vec> = - Vec::with_capacity(functional_contributions.len() + 1); - helmholtz_energy_density.push( - self.ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, - ); - - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density.push( - c.calculate_helmholtz_energy_density( - temperature_dual, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )? - .into_shape(density.raw_dim().remove_axis(Axis(0))) - .unwrap(), - ); - } - Ok(helmholtz_energy_density - .iter() - .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) - .collect()) - } - - /// Calculate the internal energy density $u$. - /// - /// Untested with heterosegmented functionals. - pub fn internal_energy_density( - &self, - temperature: f64, - density: &Array, - external_potential: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density_dual = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - let helmholtz_energy_density = helmholtz_energy_density_dual - .mapv(|f| f.re - f.eps * temperature) - + (external_potential * density).sum_axis(Axis(0)) * temperature; - Ok(helmholtz_energy_density) - } - /// Calculate the (residual) functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. #[allow(clippy::type_complexity)] - pub fn functional_derivative( + fn functional_derivative( &self, temperature: f64, density: &Array, @@ -442,7 +244,7 @@ impl DFT { } #[allow(clippy::type_complexity)] - pub(crate) fn functional_derivative_dual( + fn functional_derivative_dual( &self, temperature: f64, density: &Array, @@ -479,7 +281,7 @@ impl DFT { } /// Calculate the bond integrals $I_{\alpha\alpha'}(\mathbf{r})$ - pub fn bond_integrals( + fn bond_integrals( &self, temperature: f64, exponential: &Array, diff --git a/feos-dft/src/lib.rs b/feos-dft/src/lib.rs index 2962fbeea..4323b2303 100644 --- a/feos-dft/src/lib.rs +++ b/feos-dft/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::suspicious_operation_groupings)] #![allow(clippy::too_many_arguments)] #![allow(clippy::new_ret_no_self)] +#![allow(deprecated)] pub mod adsorption; mod convolver; diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index 44dc07a9d..d9c138660 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -1,7 +1,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::functional_contribution::FunctionalContribution; use super::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosResult, EosUnit, EquationOfState, PhaseEquilibrium}; +use feos_core::{Components, Contributions, EosResult, EosUnit, PhaseEquilibrium}; use ndarray::*; use num_dual::Dual2_64; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; @@ -167,20 +167,15 @@ impl DFT { .ideal_chain_contribution() .helmholtz_energy_density::(vle.vapor().temperature, &density)?; - let t = vle - .vapor() - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - delta_omega += - &(self.ideal_gas_contribution::(t, &rho) * SIUnit::reference_pressure()); - // calculate excess grand potential density - let mu = vle.vapor().chemical_potential(Contributions::Total); + let mu_res = vle.vapor().residual_chemical_potential(); for i in 0..self.components() { - let rhoi = density.index_axis(Axis(0), i); - let mui = mu.get(i); - delta_omega -= &(&rhoi * mui); + let rhoi = density.index_axis(Axis(0), i).to_owned(); + let rhoi_b = vle.vapor().partial_density.get(i); + let mui_res = mu_res.get(i); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + delta_omega += + &(&rhoi * (kt * (rhoi.to_reduced(rhoi_b)?.mapv(f64::ln) - 1.0) - mui_res)); } delta_omega += vle.vapor().pressure(Contributions::Total); diff --git a/feos-dft/src/profile.rs b/feos-dft/src/profile/mod.rs similarity index 61% rename from feos-dft/src/profile.rs rename to feos-dft/src/profile/mod.rs index 1c8ae407e..9886923f2 100644 --- a/feos-dft/src/profile.rs +++ b/feos-dft/src/profile/mod.rs @@ -1,18 +1,16 @@ -use crate::convolver::{BulkConvolver, Convolver, ConvolverFFT}; +use crate::convolver::{BulkConvolver, Convolver}; use crate::functional::{HelmholtzEnergyFunctional, DFT}; use crate::geometry::Grid; use crate::solver::{DFTSolver, DFTSolverLog}; -use crate::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosError, EosResult, EosUnit, EquationOfState, State, Verbosity}; -use ndarray::{ - Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3, RemoveAxis, -}; -use num_dual::Dual64; -use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use feos_core::{Components, EosError, EosResult, EosUnit, State}; +use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3}; +use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; use quantity::Quantity; use std::ops::MulAssign; use std::sync::Arc; +mod properties; + pub(crate) const MAX_POTENTIAL: f64 = 50.0; #[cfg(feature = "rayon")] pub(crate) const CUTOFF_RADIUS: f64 = 14.0; @@ -290,11 +288,6 @@ where pub fn total_moles(&self) -> SINumber { self.moles().sum() } - - /// Return the chemical potential of the system - pub fn chemical_potential(&self) -> SIArray1 { - self.bulk.chemical_potential(Contributions::Total) - } } impl Clone for DFTProfile { @@ -326,16 +319,6 @@ where .weighted_densities(&self.density.to_reduced(SIUnit::reference_density())?)) } - pub fn functional_derivative(&self) -> EosResult> { - let (_, dfdrho) = self.dft.functional_derivative( - self.temperature - .to_reduced(SIUnit::reference_temperature())?, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.convolver, - )?; - Ok(dfdrho) - } - #[allow(clippy::type_complexity)] pub fn residual(&self, log: bool) -> EosResult<(Array, Array1, f64)> { // Read from profile @@ -470,194 +453,3 @@ where Ok(()) } } - -impl DFTProfile -where - D::Larger: Dimension, - D::Smaller: Dimension, - ::Larger: Dimension, -{ - pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - Ok(self.dft.entropy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &convolver, - contributions, - )? * (SIUnit::reference_entropy() / SIUnit::reference_volume())) - } - - pub fn entropy(&self, contributions: Contributions) -> EosResult { - Ok(self.integrate(&self.entropy_density(contributions)?)) - } - - pub fn grand_potential_density(&self) -> EosResult> { - self.dft - .grand_potential_density(self.temperature, &self.density, &self.convolver) - } - - pub fn grand_potential(&self) -> EosResult { - Ok(self.integrate(&self.grand_potential_density()?)) - } - - pub fn internal_energy(&self, contributions: Contributions) -> EosResult { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - let internal_energy_density = self.dft.internal_energy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.external_potential, - &convolver, - contributions, - )? * SIUnit::reference_pressure(); - Ok(self.integrate(&internal_energy_density)) - } - - fn density_derivative(&self, lhs: &Array) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - - let second_partial_derivatives = self.second_partial_derivatives(&rho)?; - let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; - - let rhs = |x: &_| { - let delta_functional_derivative = - self.delta_functional_derivative(x, &second_partial_derivatives); - let mut xm = x.clone(); - xm.outer_iter_mut() - .zip(self.dft.m().iter()) - .for_each(|(mut x, &m)| x *= m); - let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); - xm + (delta_functional_derivative - delta_i) * &rho - }; - let mut log = DFTSolverLog::new(Verbosity::None); - Self::gmres(rhs, lhs, 200, 1e-13, &mut log) - } - - /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ - pub fn drho_dmu(&self) -> EosResult::Larger>> { - let shape = self.density.shape(); - let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); - let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); - for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - for (i, mut l) in lhs.outer_iter_mut().enumerate() { - if i != k { - l.fill(0.0); - } - } - d.assign(&self.density_derivative(&lhs)?); - } - Ok(drho_dmu - * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ - pub fn dn_dmu(&self) -> EosResult { - let drho_dmu = self.drho_dmu()?; - let n = drho_dmu.shape()[0]; - let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { - self.integrate(&drho_dmu.index_axis(Axis_nd(0), i).index_axis(Axis_nd(0), j)) - }); - Ok(dn_dmu) - } - - /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn drho_dp(&self) -> EosResult> { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - let v = self - .bulk - .partial_molar_volume(Contributions::Total) - .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; - for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { - l *= v[c]; - } - self.density_derivative(&lhs) - .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn dn_dp(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dp()?)) - } - - /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn drho_dt(&self) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - - // calculate temperature derivative of functional derivative - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver: Arc> = - ConvolverFFT::plan(&self.grid, &weight_functions, None); - let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; - - // calculate temperature derivative of bulk functional derivative - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - let bulk_convolver = BulkConvolver::new(weight_functions); - let (_, dfdrhodt_bulk) = - self.dft - .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; - - // solve for drho_dt - let x = (self.bulk.partial_molar_volume(Contributions::Total) - * self.bulk.dp_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_entropy())?; - let mut lhs = dfdrhodt.mapv(|d| d.eps); - lhs.outer_iter_mut() - .zip(dfdrhodt_bulk.into_iter()) - .zip(x.into_iter()) - .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); - lhs.outer_iter_mut() - .zip(rho.outer_iter()) - .zip(rho_bulk.into_iter()) - .zip(self.dft.m().iter()) - .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); - - lhs *= &(-&rho / t); - self.density_derivative(&lhs) - .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) - } - - /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn dn_dt(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dt()?)) - } -} diff --git a/feos-dft/src/profile/properties.rs b/feos-dft/src/profile/properties.rs new file mode 100644 index 000000000..2348fbd0f --- /dev/null +++ b/feos-dft/src/profile/properties.rs @@ -0,0 +1,397 @@ +use super::DFTProfile; +use crate::convolver::{BulkConvolver, Convolver}; +use crate::functional_contribution::{FunctionalContribution, FunctionalContributionDual}; +use crate::{ConvolverFFT, DFTSolverLog, HelmholtzEnergyFunctional, WeightFunctionInfo}; +use feos_core::{Contributions, EosResult, EosUnit, IdealGas, Verbosity}; +use ndarray::{Array, Axis, Dimension, RemoveAxis, ScalarOperand}; +use num_dual::{Dual64, DualNum}; +use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use std::ops::AddAssign; +use std::sync::Arc; + +impl DFTProfile +where + D::Larger: Dimension, +{ + /// Calculate the grand potential density $\omega$. + pub fn grand_potential_density(&self) -> EosResult> { + // Calculate residual Helmholtz energy density and functional derivative + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let (mut f, dfdrho) = self.dft.functional_derivative(t, &rho, &self.convolver)?; + + // Calculate the grand potential density + for ((rho, dfdrho), &m) in rho + .outer_iter() + .zip(dfdrho.outer_iter()) + .zip(self.dft.m().iter()) + { + f -= &((&dfdrho + m) * &rho); + } + + let bond_lengths = self.dft.bond_lengths(t); + for segment in bond_lengths.node_indices() { + let n = bond_lengths.neighbors(segment).count(); + f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); + } + + Ok(f * t * SIUnit::reference_pressure()) + } + + /// Calculate the grand potential $\Omega$. + pub fn grand_potential(&self) -> EosResult { + Ok(self.integrate(&self.grand_potential_density()?)) + } + + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + pub fn functional_derivative(&self) -> EosResult> { + let (_, dfdrho) = self.dft.functional_derivative( + self.temperature + .to_reduced(SIUnit::reference_temperature())?, + &self.density.to_reduced(SIUnit::reference_density())?, + &self.convolver, + )?; + Ok(dfdrho) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn intrinsic_helmholtz_energy_density( + &self, + temperature: N, + density: &Array, + convolver: &Arc>, + ) -> EosResult> + where + N: DualNum + Copy + ScalarOperand, + dyn FunctionalContribution: FunctionalContributionDual, + { + let density_dual = density.mapv(N::from); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Array = self + .dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(N::from))?; + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density + .view_mut() + .into_shape(ngrid) + .unwrap() + .add_assign(&c.calculate_helmholtz_energy_density( + temperature, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )?); + } + Ok(helmholtz_energy_density * temperature) + } + + /// Calculate the residual entropy density $s^\mathrm{res}(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn residual_entropy_density(&self) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the individual contributions to the entropy density. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density_contributions( + &self, + temperature: f64, + density: &Array, + convolver: &Arc>, + ) -> EosResult>> { + let density_dual = density.mapv(Dual64::from); + let temperature_dual = Dual64::from(temperature).derivative(); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Vec> = + Vec::with_capacity(functional_contributions.len() + 1); + helmholtz_energy_density.push( + self.dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, + ); + + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density.push( + c.calculate_helmholtz_energy_density( + temperature_dual, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )? + .into_shape(density.raw_dim().remove_axis(Axis(0))) + .unwrap(), + ); + } + Ok(helmholtz_energy_density + .iter() + .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) + .collect()) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn ideal_gas_contribution_dual( + &self, + temperature: Dual64, + density: &Array, + ) -> Array { + let lambda = self.dft.ideal_gas_model().ln_lambda3(temperature); + let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); + for (i, rhoi) in density.outer_iter().enumerate() { + phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); + } + phi * temperature + } + + /// Calculate the entropy density $s(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the entropy $S$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.entropy_density(contributions)?)) + } + + /// Calculate the internal energy density $u(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy_density(&self, contributions: Contributions) -> EosResult> + where + D: Dimension, + D::Larger: Dimension, + { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density_dual = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + let helmholtz_energy_density = helmholtz_energy_density_dual + .mapv(|f| f.re - f.eps * temperature) + + (&self.external_potential * density).sum_axis(Axis(0)) * temperature; + Ok(helmholtz_energy_density * (SIUnit::reference_energy() / SIUnit::reference_volume())) + } + + /// Calculate the internal energy $U$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.internal_energy_density(contributions)?)) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn density_derivative(&self, lhs: &Array) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + + let second_partial_derivatives = self.second_partial_derivatives(&rho)?; + let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; + + let rhs = |x: &_| { + let delta_functional_derivative = + self.delta_functional_derivative(x, &second_partial_derivatives); + let mut xm = x.clone(); + xm.outer_iter_mut() + .zip(self.dft.m().iter()) + .for_each(|(mut x, &m)| x *= m); + let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); + xm + (delta_functional_derivative - delta_i) * &rho + }; + let mut log = DFTSolverLog::new(Verbosity::None); + Self::gmres(rhs, lhs, 200, 1e-13, &mut log) + } + + /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ + pub fn drho_dmu(&self) -> EosResult::Larger>> { + let shape = self.density.shape(); + let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); + let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); + for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + for (i, mut l) in lhs.outer_iter_mut().enumerate() { + if i != k { + l.fill(0.0); + } + } + d.assign(&self.density_derivative(&lhs)?); + } + Ok(drho_dmu + * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ + pub fn dn_dmu(&self) -> EosResult { + let drho_dmu = self.drho_dmu()?; + let n = drho_dmu.shape()[0]; + let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { + self.integrate(&drho_dmu.index_axis(Axis(0), i).index_axis(Axis(0), j)) + }); + Ok(dn_dmu) + } + + /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn drho_dp(&self) -> EosResult> { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + let v = self + .bulk + .partial_molar_volume() + .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; + for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { + l *= v[c]; + } + self.density_derivative(&lhs) + .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn dn_dp(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dp()?)) + } + + /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn drho_dt(&self) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + + // calculate temperature derivative of functional derivative + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(Dual64::from(t).derivative())) + .collect(); + let convolver: Arc> = + ConvolverFFT::plan(&self.grid, &weight_functions, None); + let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; + + // calculate temperature derivative of bulk functional derivative + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + let bulk_convolver = BulkConvolver::new(weight_functions); + let (_, dfdrhodt_bulk) = + self.dft + .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; + + // solve for drho_dt + let x = (self.bulk.partial_molar_volume() * self.bulk.dp_dt(Contributions::Total)) + .to_reduced(SIUnit::reference_molar_entropy())?; + let mut lhs = dfdrhodt.mapv(|d| d.eps); + lhs.outer_iter_mut() + .zip(dfdrhodt_bulk.into_iter()) + .zip(x.into_iter()) + .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); + lhs.outer_iter_mut() + .zip(rho.outer_iter()) + .zip(rho_bulk.into_iter()) + .zip(self.dft.m().iter()) + .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); + + lhs *= &(-&rho / t); + self.density_derivative(&lhs) + .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) + } + + /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn dn_dt(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dt()?)) + } +} diff --git a/feos-dft/src/python/adsorption/mod.rs b/feos-dft/src/python/adsorption/mod.rs index 99970cb55..688b9b0d3 100644 --- a/feos-dft/src/python/adsorption/mod.rs +++ b/feos-dft/src/python/adsorption/mod.rs @@ -229,11 +229,6 @@ macro_rules! impl_adsorption_isotherm { self.0.pressure().into() } - #[getter] - fn get_molar_gibbs_energy(&self) -> PySIArray1 { - self.0.molar_gibbs_energy().into() - } - #[getter] fn get_adsorption(&self) -> PySIArray2 { self.0.adsorption().into() diff --git a/feos-dft/src/python/profile.rs b/feos-dft/src/python/profile.rs index 4223f4dab..46c2f378f 100644 --- a/feos-dft/src/python/profile.rs +++ b/feos-dft/src/python/profile.rs @@ -77,11 +77,6 @@ macro_rules! impl_profile { self.0.profile.external_potential.view().to_pyarray(py) } - #[getter] - fn get_chemical_potential(&self) -> PySIArray1 { - PySIArray1::from(self.0.profile.chemical_potential()) - } - #[getter] fn get_bulk(&self) -> PyState { PyState(self.0.profile.bulk.clone()) diff --git a/src/dft.rs b/src/dft.rs index 352ce0c27..585681f4d 100644 --- a/src/dft.rs +++ b/src/dft.rs @@ -8,7 +8,7 @@ use crate::pets::PetsFunctional; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::SaftVRQMieFunctional; use feos_core::*; -use feos_derive::HelmholtzEnergyFunctional; +use feos_derive::{Components, HelmholtzEnergyFunctional}; use feos_dft::adsorption::*; use feos_dft::solvation::*; use feos_dft::*; @@ -21,7 +21,7 @@ use quantity::si::*; /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(HelmholtzEnergyFunctional)] +#[derive(Components, HelmholtzEnergyFunctional)] pub enum FunctionalVariant { #[cfg(feature = "pcsaft")] #[implement(fluid_parameters, molar_weight, pair_potential)] diff --git a/src/eos.rs b/src/eos.rs index c93505db5..f5cd0d2de 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -9,10 +9,11 @@ use crate::saftvrqmie::SaftVRQMie; #[cfg(feature = "uvtheory")] use crate::uvtheory::UVTheory; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; #[cfg(feature = "python")] -use feos_core::python::user_defined::PyEoSObj; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; -use feos_derive::EquationOfState; +use feos_derive::{Components, IdealGas, Residual}; use ndarray::Array1; use quantity::si::*; @@ -20,8 +21,8 @@ use quantity::si::*; /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(EquationOfState)] -pub enum EosVariant { +#[derive(Components, Residual)] +pub enum ResidualModel { #[cfg(feature = "pcsaft")] #[implement(entropy_scaling, molar_weight)] PcSaft(PcSaft), @@ -32,7 +33,7 @@ pub enum EosVariant { PengRobinson(PengRobinson), #[cfg(feature = "python")] #[implement(molar_weight)] - Python(PyEoSObj), + Python(PyResidual), #[cfg(feature = "saftvrqmie")] #[implement(molar_weight)] SaftVRQMie(SaftVRQMie), @@ -42,3 +43,11 @@ pub enum EosVariant { #[cfg(feature = "uvtheory")] UVTheory(UVTheory), } + +#[derive(Components, IdealGas)] +pub enum IdealGasModel { + NoModel(usize), + Joback(Joback), + #[cfg(feature = "python")] + Python(PyIdealGas), +} diff --git a/src/estimator/binary_vle.rs b/src/estimator/binary_vle.rs index 200925d68..63c668677 100644 --- a/src/estimator/binary_vle.rs +++ b/src/estimator/binary_vle.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - Contributions, DensityInitialization, EosUnit, EquationOfState, PhaseDiagram, PhaseEquilibrium, - State, + Contributions, DensityInitialization, EosUnit, PhaseDiagram, PhaseEquilibrium, Residual, State, }; use ndarray::{arr1, s, Array1, ArrayView1, Axis}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -44,7 +43,7 @@ impl BinaryVleChemicalPotential { } } -impl DataSet for BinaryVleChemicalPotential { +impl DataSet for BinaryVleChemicalPotential { fn target(&self) -> &SIArray1 { &self.target } @@ -73,16 +72,22 @@ impl DataSet for BinaryVleChemicalPotential { { let liquid_moles = arr1(&[xi, 1.0 - xi]) * SIUnit::reference_moles(); let liquid = State::new_npt(eos, t, p, &liquid_moles, DensityInitialization::Liquid)?; - let mu_liquid = liquid.chemical_potential(Contributions::Total); + let mu_res_liquid = liquid.residual_chemical_potential(); let vapor_moles = arr1(&[yi, 1.0 - yi]) * SIUnit::reference_moles(); let vapor = State::new_npt(eos, t, p, &vapor_moles, DensityInitialization::Vapor)?; - let mu_vapor = vapor.chemical_potential(Contributions::Total); + let mu_res_vapor = vapor.residual_chemical_potential(); + let kt = SIUnit::gas_constant() * t; + let rho_frac = (&liquid.partial_density / &vapor.partial_density).into_value()?; prediction.push( - mu_liquid.get(0) - mu_vapor.get(0) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(0) - mu_res_vapor.get(0) + + kt * rho_frac[0].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); prediction.push( - mu_liquid.get(1) - mu_vapor.get(1) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(1) - mu_res_vapor.get(1) + + kt * rho_frac[1].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); } Ok(SIArray1::from_vec(prediction)) @@ -129,7 +134,7 @@ impl BinaryVlePressure { } } -impl DataSet for BinaryVlePressure { +impl DataSet for BinaryVlePressure { fn target(&self) -> &SIArray1 { &self.pressure } @@ -227,7 +232,7 @@ impl BinaryPhaseDiagram { } } -impl DataSet for BinaryPhaseDiagram { +impl DataSet for BinaryPhaseDiagram { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/dataset.rs b/src/estimator/dataset.rs index 4e8f78aa2..cf496b8c0 100644 --- a/src/estimator/dataset.rs +++ b/src/estimator/dataset.rs @@ -3,7 +3,7 @@ //! a `target` which can be values from experimental data or //! other models. use super::{EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::Array1; use quantity::si::SIArray1; use std::collections::HashMap; @@ -14,7 +14,7 @@ use std::sync::Arc; /// /// Functionalities in the context of optimizations of /// parameters of equations of state. -pub trait DataSet: Send + Sync { +pub trait DataSet: Send + Sync { /// Return target quantity. fn target(&self) -> &SIArray1; @@ -61,7 +61,7 @@ pub trait DataSet: Send + Sync { } } -impl fmt::Display for dyn DataSet { +impl fmt::Display for dyn DataSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, diff --git a/src/estimator/diffusion.rs b/src/estimator/diffusion.rs index 25c9db749..d921ead42 100644 --- a/src/estimator/diffusion.rs +++ b/src/estimator/diffusion.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Diffusion { } } -impl DataSet for Diffusion { +impl DataSet for Diffusion { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/estimator.rs b/src/estimator/estimator.rs index 300d26314..69299e5e3 100644 --- a/src/estimator/estimator.rs +++ b/src/estimator/estimator.rs @@ -1,7 +1,7 @@ //! The [`Estimator`] struct can be used to store multiple [`DataSet`]s for convenient parameter //! optimization. use super::{DataSet, EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::{arr1, concatenate, Array1, ArrayView1, Axis}; use quantity::si::SIArray1; use std::fmt; @@ -11,13 +11,13 @@ use std::sync::Arc; /// A collection of [`DataSet`]s and weights that can be used to /// evaluate an equation of state versus experimental data. -pub struct Estimator { +pub struct Estimator { data: Vec>>, weights: Vec, losses: Vec, } -impl Estimator { +impl Estimator { /// Create a new `Estimator` given `DataSet`s and weights. /// /// The weights are normalized and used as multiplicator when the @@ -99,7 +99,7 @@ impl Estimator { } } -impl Display for Estimator { +impl Display for Estimator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for d in self.data.iter() { writeln!(f, "{}", d)?; diff --git a/src/estimator/liquid_density.rs b/src/estimator/liquid_density.rs index 71a2e36eb..f1290b6a9 100644 --- a/src/estimator/liquid_density.rs +++ b/src/estimator/liquid_density.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - DensityInitialization, EosUnit, EquationOfState, MolarWeight, PhaseEquilibrium, SolverOptions, - State, + DensityInitialization, EosUnit, MolarWeight, PhaseEquilibrium, Residual, SolverOptions, State, }; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; @@ -44,7 +43,7 @@ impl LiquidDensity { } } -impl DataSet for LiquidDensity { +impl DataSet for LiquidDensity { fn target(&self) -> &SIArray1 { &self.target } @@ -110,7 +109,7 @@ impl EquilibriumLiquidDensity { } } -impl DataSet for EquilibriumLiquidDensity { +impl DataSet for EquilibriumLiquidDensity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/thermal_conductivity.rs b/src/estimator/thermal_conductivity.rs index 45b3e33f4..ec7003edd 100644 --- a/src/estimator/thermal_conductivity.rs +++ b/src/estimator/thermal_conductivity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl ThermalConductivity { } } -impl DataSet for ThermalConductivity { +impl DataSet for ThermalConductivity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/vapor_pressure.rs b/src/estimator/vapor_pressure.rs index 19d9e2fb7..0545c73da 100644 --- a/src/estimator/vapor_pressure.rs +++ b/src/estimator/vapor_pressure.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{Contributions, EosUnit, EquationOfState, PhaseEquilibrium, SolverOptions, State}; +use feos_core::{Contributions, EosUnit, PhaseEquilibrium, Residual, SolverOptions, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::collections::HashMap; @@ -57,7 +57,7 @@ impl VaporPressure { } } -impl DataSet for VaporPressure { +impl DataSet for VaporPressure { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/viscosity.rs b/src/estimator/viscosity.rs index 8e57c56d4..ad05b168f 100644 --- a/src/estimator/viscosity.rs +++ b/src/estimator/viscosity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Viscosity { } } -impl DataSet for Viscosity { +impl DataSet for Viscosity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index 2b0f43b38..1c66a6716 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -2,7 +2,7 @@ use super::eos::GcPcSaftOptions; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, MonomerShape}; use feos_core::parameter::ParameterHetero; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; use ndarray::Array1; @@ -66,27 +66,33 @@ impl GcPcSaftFunctional { contributions.push(Box::new(assoc)); } - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, }) - .into() } } -impl HelmholtzEnergyFunctional for GcPcSaftFunctional { - fn molecule_shape(&self) -> MoleculeShape { - MoleculeShape::Heterosegmented(&self.parameters.component_index) +impl Components for GcPcSaftFunctional { + fn components(&self) -> usize { + self.parameters.chemical_records.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 + } +} + +impl HelmholtzEnergyFunctional for GcPcSaftFunctional { + fn molecule_shape(&self) -> MoleculeShape { + MoleculeShape::Heterosegmented(&self.parameters.component_index) } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index d0931dfac..43fc9016e 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -1,6 +1,5 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, ChemicalRecord, ParameterError, ParameterHetero, SegmentRecord, }; @@ -26,20 +25,19 @@ pub struct GcPcSaftFunctionalParameters { pub k_ij: Array2, pub sigma_ij: Array2, pub epsilon_k_ij: Array2, - chemical_records: Vec, - segment_records: Vec>, + pub chemical_records: Vec, + segment_records: Vec>, binary_segment_records: Option>>, } impl ParameterHetero for GcPcSaftFunctionalParameters { type Chemical = ChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|cr| cr.into()).collect(); @@ -155,7 +153,7 @@ impl ParameterHetero for GcPcSaftFunctionalParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index 67be9e25c..32b917dc9 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -1,8 +1,7 @@ use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::ParameterHetero; -use feos_core::{EquationOfState, HelmholtzEnergy, IdealGasContribution, MolarWeight}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; use std::f64::consts::FRAC_PI_6; @@ -43,7 +42,6 @@ pub struct GcPcSaft { pub parameters: Arc, options: GcPcSaftOptions, contributions: Vec>, - joback: Joback, } impl GcPcSaft { @@ -72,18 +70,14 @@ impl GcPcSaft { contributions.push(Box::new(Dipole::new(¶meters))) } Self { - parameters: parameters.clone(), + parameters, options, contributions, - joback: parameters.joback_records.clone().map_or_else( - || Joback::default(parameters.chemical_records.len()), - Joback::new, - ), } } } -impl EquationOfState for GcPcSaft { +impl Components for GcPcSaft { fn components(&self) -> usize { self.parameters.molarweight.len() } @@ -94,7 +88,9 @@ impl EquationOfState for GcPcSaft { self.options, ) } +} +impl Residual for GcPcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { let p = &self.parameters; let moles_segments: Array1 = p.component_index.iter().map(|&i| moles[i]).collect(); @@ -102,13 +98,9 @@ impl EquationOfState for GcPcSaft { / (FRAC_PI_6 * &p.m * p.sigma.mapv(|v| v.powi(3)) * moles_segments).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for GcPcSaft { diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index e71bca0b0..537b0ba64 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -1,10 +1,9 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ - BinaryRecord, ChemicalRecord, FromSegments, Identifier, ParameterError, ParameterHetero, - SegmentCount, SegmentRecord, + BinaryRecord, ChemicalRecord, Identifier, ParameterError, ParameterHetero, SegmentCount, + SegmentRecord, }; use indexmap::IndexMap; use ndarray::{Array1, Array2}; @@ -87,20 +86,18 @@ pub struct GcPcSaftEosParameters { pub epsilon_k_ij: Array2, pub chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, - pub joback_records: Option>, } impl ParameterHetero for GcPcSaftEosParameters { type Chemical = GcPcSaftChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|c| c.into()).collect(); @@ -123,8 +120,6 @@ impl ParameterHetero for GcPcSaftEosParameters { let mut phi = Vec::new(); - let mut joback_records = Vec::new(); - for (i, chemical_record) in chemical_records.iter().cloned().enumerate() { let mut segment_indices = IndexMap::with_capacity(segment_records.len()); let segment_map = chemical_record.segment_map(&segment_records)?; @@ -179,18 +174,6 @@ impl ParameterHetero for GcPcSaftEosParameters { *bond += count; } } - - let ideal_gas_segments: Option> = segment_map - .iter() - .map(|(s, &n)| s.ideal_gas_record.clone().map(|ig| (ig, n))) - .collect(); - - joback_records.push( - ideal_gas_segments - .as_ref() - .map(|s| JobackRecord::from_segments(s)) - .transpose()?, - ); } // Binary interaction parameter @@ -260,7 +243,6 @@ impl ParameterHetero for GcPcSaftEosParameters { chemical_records, segment_records, binary_segment_records, - joback_records: joback_records.into_iter().collect(), }) } @@ -268,7 +250,7 @@ impl ParameterHetero for GcPcSaftEosParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( @@ -423,25 +405,23 @@ pub mod test { use crate::association::AssociationRecord; use feos_core::parameter::{ChemicalRecord, Identifier}; - fn ch3() -> SegmentRecord { + fn ch3() -> SegmentRecord { SegmentRecord::new( "CH3".into(), 15.0, GcPcSaftRecord::new(0.77247, 3.6937, 181.49, None, None, None), - None, ) } - fn ch2() -> SegmentRecord { + fn ch2() -> SegmentRecord { SegmentRecord::new( "CH2".into(), 14.0, GcPcSaftRecord::new(0.7912, 3.0207, 157.23, None, None, None), - None, ) } - fn oh() -> SegmentRecord { + fn oh() -> SegmentRecord { SegmentRecord::new( "OH".into(), 0.0, @@ -453,7 +433,6 @@ pub mod test { Some(AssociationRecord::new(0.009583, 2575.9, 1.0, 1.0, 0.0)), None, ), - None, ) } diff --git a/src/gc_pcsaft/python/mod.rs b/src/gc_pcsaft/python/mod.rs index eed44690d..86d3b3074 100644 --- a/src/gc_pcsaft/python/mod.rs +++ b/src/gc_pcsaft/python/mod.rs @@ -3,11 +3,9 @@ use super::dft::GcPcSaftFunctionalParameters; use super::eos::GcPcSaftEosParameters; use super::record::GcPcSaftRecord; use crate::association::PyAssociationRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, IdentifierOption, ParameterError, ParameterHetero, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::{PyBinarySegmentRecord, PyChemicalRecord, PyIdentifier}; use feos_core::{impl_json_handling, impl_parameter_from_segments, impl_segment_record}; #[cfg(feature = "dft")] @@ -77,12 +75,7 @@ impl PyGcPcSaftRecord { impl_json_handling!(PyGcPcSaftRecord); -impl_segment_record!( - GcPcSaftRecord, - PyGcPcSaftRecord, - JobackRecord, - PyJobackRecord -); +impl_segment_record!(GcPcSaftRecord, PyGcPcSaftRecord); #[pyclass(name = "GcPcSaftEosParameters")] #[pyo3( @@ -156,7 +149,6 @@ pub fn gc_pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/hard_sphere/dft.rs b/src/hard_sphere/dft.rs index c886e2c95..720da02a8 100644 --- a/src/hard_sphere/dft.rs +++ b/src/hard_sphere/dft.rs @@ -1,4 +1,4 @@ -use feos_core::EosResult; +use feos_core::{Components, EosResult}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{ @@ -322,26 +322,31 @@ impl FMTFunctional { }); let contributions: Vec> = vec![Box::new(FMTContribution::new(&properties, version))]; - (Self { + DFT(Self { properties, contributions, version, }) - .into() } } -impl HelmholtzEnergyFunctional for FMTFunctional { - fn contributions(&self) -> &[Box] { - &self.contributions +impl Components for FMTFunctional { + fn components(&self) -> usize { + self.properties.sigma.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { let sigma = component_list .iter() .map(|&c| self.properties.sigma[c]) .collect(); - Self::new(&sigma, self.version) + Self::new(&sigma, self.version).0 + } +} + +impl HelmholtzEnergyFunctional for FMTFunctional { + fn contributions(&self) -> &[Box] { + &self.contributions } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/lib.rs b/src/lib.rs index 24af2ff02..ddb0e7cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,12 +33,14 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] +#![allow(deprecated)] + #[cfg(feature = "dft")] mod dft; #[cfg(feature = "dft")] pub use dft::FunctionalVariant; mod eos; -pub use eos::EosVariant; +pub use eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] pub mod estimator; diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index deefc121d..f87d4b0c6 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -2,9 +2,8 @@ use super::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -28,7 +27,6 @@ pub struct PcSaftFunctional { fmt_version: FMTVersion, options: PcSaftOptions, contributions: Vec>, - joback: Joback, } impl PcSaftFunctional { @@ -87,31 +85,31 @@ impl PcSaftFunctional { } } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) - .into() } } -impl HelmholtzEnergyFunctional for PcSaftFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PcSaftFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PcSaftFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) @@ -122,10 +120,6 @@ impl HelmholtzEnergyFunctional for PcSaftFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index c11a7714a..323563377 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -1,32 +1,23 @@ use super::parameters::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; use std::f64::consts::{FRAC_PI_6, PI}; +use std::fmt; use std::sync::Arc; pub(crate) mod dispersion; pub(crate) mod hard_chain; pub(crate) mod polar; -mod qspr; use dispersion::Dispersion; use hard_chain::HardChain; pub use polar::DQVariants; use polar::{Dipole, DipoleQuadrupole, Quadrupole}; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Customization options for the PC-SAFT equation of state and functional. #[derive(Copy, Clone)] @@ -53,7 +44,6 @@ pub struct PcSaft { parameters: Arc, options: PcSaftOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl PcSaft { @@ -95,21 +85,15 @@ impl PcSaft { ))); }; - let joback_records = parameters.joback_records.clone(); - Self { - parameters: parameters.clone(), + parameters, options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for PcSaft { +impl Components for PcSaft { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -120,22 +104,23 @@ impl EquationOfState for PcSaft { self.options, ) } +} +impl Residual for PcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } +} - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } +impl fmt::Display for PcSaft { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PC-SAFT") } } @@ -292,8 +277,7 @@ impl EntropyScaling for PcSaft { let tr = (temperature / p.epsilon_k[i] / KELVIN) .into_value() .unwrap(); - let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) + let s_res_reduced = (state.residual_entropy() / state.total_moles) .to_reduced(RGAS) .unwrap() / p.m[i]; @@ -358,7 +342,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -374,7 +358,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -474,20 +458,6 @@ mod tests { } } - #[test] - fn speed_of_sound() { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let m = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &m, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.speed_of_sound(), - 245.00185709137546 * METER / SECOND, - epsilon = 1e-4 - ) - } - #[test] fn mix_single() { let e1 = Arc::new(PcSaft::new(propane_parameters())); diff --git a/src/pcsaft/eos/qspr.rs b/src/pcsaft/eos/qspr.rs deleted file mode 100644 index 844c21ce5..000000000 --- a/src/pcsaft/eos/qspr.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::PcSaftParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -const NA_P_300: [f64; 6] = [ - 5177.19095226181, - 919.565206504576, - -108.829105648889, - 0.0, - -3.93917830677682, - -13504.5671858292, -]; -const NA_P_400: [f64; 6] = [ - 10656.1018362315, - 1146.10782703748, - -131.023645998081, - 0.0, - -9.93789225413177, - -24430.12952497, -]; -const AP_300: [f64; 6] = [ - 3600.32322462175, - 1006.20461224949, - -151.688378113974, - 7.81876773647109e-07, - 8.01001754473385, - -8959.37140957179, -]; -const AP_400: [f64; 6] = [ - 7248.0697641199, - 1267.44346171358, - -208.738557800023, - 0.000170238690157906, - -6.7841792685616, - -12669.4196622924, -]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = if self.parameters.association.is_empty() { - match self.parameters.ndipole + self.parameters.nquadpole { - 0 => (NA_NP_300, NA_NP_400), - _ => (NA_P_300, NA_P_400), - } - } else { - (AP_300, AP_400) - }; - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt * self.parameters.m[i]; - let p2 = sigma3 * self.parameters.m[i]; - let p3 = epsilon_kt * p2; - let p4 = self.parameters.pure_records[i] - .model_record - .association_record - .as_ref() - .map_or(D::zero(), |a| { - (temperature.recip() * a.epsilon_k_ab).exp_m1() * p2 * sigma3 * a.kappa_ab - }); - let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - + p4 * c_300[3] / T300 - + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - + p4 * c_400[3] / T400 - + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 892cbee6b..097617a5a 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -1,7 +1,6 @@ use crate::association::{AssociationParameters, AssociationRecord}; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use conv::ValueInto; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ FromSegments, FromSegmentsBinary, Parameter, ParameterError, PureRecord, }; @@ -314,19 +313,17 @@ pub struct PcSaftParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for PcSaftParameters { type Pure = PcSaftRecord; - type IdealGas = JobackRecord; type Binary = PcSaftBinaryRecord; fn from_records( - pure_records: Vec>, - binary_records: Array2, + pure_records: Vec>, + binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -422,11 +419,6 @@ impl Parameter for PcSaftParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -450,16 +442,10 @@ impl Parameter for PcSaftParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -519,7 +505,6 @@ impl PcSaftParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use feos_core::parameter::{BinaryRecord, ChemicalRecord, SegmentRecord}; use std::sync::Arc; @@ -544,7 +529,7 @@ pub mod utils { }, "molarweight": 44.0962 }"#; - let propane_record: PureRecord = + let propane_record: PureRecord = serde_json::from_str(propane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(propane_record)) } @@ -568,7 +553,7 @@ pub mod utils { "q": 4.4 } }"#; - let co2_record: PureRecord = + let co2_record: PureRecord = serde_json::from_str(co2_json).expect("Unable to parse json."); PcSaftParameters::new_pure(co2_record) } @@ -591,7 +576,7 @@ pub mod utils { }, "molarweight": 58.123 }"#; - let butane_record: PureRecord = + let butane_record: PureRecord = serde_json::from_str(butane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(butane_record)) } @@ -615,7 +600,7 @@ pub mod utils { }, "molarweight": 46.0688 }"#; - let dme_record: PureRecord = + let dme_record: PureRecord = serde_json::from_str(dme_json).expect("Unable to parse json."); PcSaftParameters::new_pure(dme_record) } @@ -642,7 +627,7 @@ pub mod utils { }, "molarweight": 18.0152 }"#; - let water_record: PureRecord = + let water_record: PureRecord = serde_json::from_str(water_json).expect("Unable to parse json."); PcSaftParameters::new_pure(water_record) } @@ -684,7 +669,7 @@ pub mod utils { } } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); PcSaftParameters::new_binary(binary_record, None) } @@ -729,7 +714,7 @@ pub mod utils { "molarweight": 58.123 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_binary(binary_record, None)) } diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 077e4d7e6..b714962f8 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -1,11 +1,9 @@ use super::parameters::{PcSaftBinaryRecord, PcSaftParameters, PcSaftRecord}; use super::DQVariants; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::*; use ndarray::Array2; @@ -130,8 +128,8 @@ impl PyPcSaftRecord { impl_json_handling!(PyPcSaftRecord); -impl_pure_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); -impl_segment_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PcSaftRecord, PyPcSaftRecord); +impl_segment_record!(PcSaftRecord, PyPcSaftRecord); #[pyclass(name = "PcSaftBinaryRecord")] #[pyo3( @@ -186,7 +184,6 @@ pub fn pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/pets/dft/mod.rs b/src/pets/dft/mod.rs index f14fd3aa5..abf76fa82 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -2,9 +2,8 @@ use super::eos::PetsOptions; use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -25,7 +24,6 @@ pub struct PetsFunctional { fmt_version: FMTVersion, options: PetsOptions, contributions: Vec>, - joback: Joback, } impl PetsFunctional { @@ -74,31 +72,31 @@ impl PetsFunctional { contributions.push(Box::new(att)); } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.sigma.len()), - }; - - Self { + DFT(Self { parameters, fmt_version, options: pets_options, contributions, - joback, - } - .into() + }) } } -impl HelmholtzEnergyFunctional for PetsFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PetsFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PetsFunctional { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(self.parameters.sigma.len()) } @@ -111,10 +109,6 @@ impl HelmholtzEnergyFunctional for PetsFunctional { fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for PetsFunctional { diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 93596d75d..4f806139b 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -1,26 +1,14 @@ use super::parameters::PetsParameters; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, -}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; -use std::f64::consts::{FRAC_PI_6, PI}; +use std::f64::consts::FRAC_PI_6; use std::sync::Arc; pub(crate) mod dispersion; -mod qspr; use dispersion::Dispersion; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Configuration options for the PeTS equation of state and Helmholtz energy functional. /// @@ -43,7 +31,6 @@ pub struct Pets { parameters: Arc, options: PetsOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl Pets { @@ -60,22 +47,15 @@ impl Pets { parameters: parameters.clone(), }), ]; - - let joback_records = parameters.joback_records.clone(); - Self { - parameters: parameters.clone(), + parameters, options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for Pets { +impl Components for Pets { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -86,22 +66,17 @@ impl EquationOfState for Pets { self.options, ) } +} +impl Residual for Pets { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } - } } impl MolarWeight for Pets { @@ -110,217 +85,217 @@ impl MolarWeight for Pets { } } -fn omega11(t: f64) -> f64 { - 1.06036 * t.powf(-0.15610) - + 0.19300 * (-0.47635 * t).exp() - + 1.03587 * (-1.52996 * t).exp() - + 1.76474 * (-3.89411 * t).exp() -} - -fn omega22(t: f64) -> f64 { - 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() - - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -} - -impl EntropyScaling for Pets { - fn viscosity_reference( - &self, - temperature: SINumber, - _: SINumber, - moles: &SIArray1, - ) -> EosResult { - let x = moles.to_reduced(moles.sum())?; - let p = &self.parameters; - let mw = &p.molarweight; - let ce: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 5.0 / 16.0 - * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) - .sqrt() - .unwrap() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi(2) - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().unwrap().sqrt() - * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } - - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) - } - - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) - .sqrt() - .unwrap() - }) - .collect(); - Ok(res[0]) - } - - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - let e: f64 = (&coefficients.row(4) * x).sum(); - Ok(a + b * s_res - - c * (1.0 - s_res.exp()) * s_res.powi(2) - - d * s_res.powi(4) - - e * s_res.powi(8)) - } - - // fn thermal_conductivity_reference( - // &self, - // state: &State, - // ) -> EosResult { - // if self.components() != 1 { - // return Err(EosError::IncompatibleComponents(self.components(), 1)); - // } - // let p = &self.parameters; - // let res: Array1 = (0..self.components()) - // .map(|i| { - // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) - // .into_value() - // .unwrap(); - // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); - // let s_res_cp_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let s_res_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let ref_ce = 0.083235 - // * ((state.temperature / KELVIN).into_value().unwrap() - // / (p.molarweight[0])) - // .sqrt() - // / p.sigma[0] - // / p.sigma[0] - // / omega22(tr); - // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); - // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) - // * (p.sigma[i].powi(3) * p.epsilon_k[0]) - // / 100000.0; - // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN - // }) - // .collect(); - // Ok(res[0]) - // } - - // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let state = State::new_nvt( - &Arc::new(Self::new(self.parameters.clone())), - temperature, - volume, - moles, - )?; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - let ce = 83.235 - * f64::powf(10.0, -1.5) - * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() - / (p.sigma[0] * p.sigma[0]) - / omega22(tr); - ce * WATT / METER / KELVIN - + state.density - * self - .diffusion_reference(temperature, volume, moles) - .unwrap() - * self - .diffusion_correlation( - state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy()) - .unwrap(), - &state.molefracs, - ) - .unwrap() - * (state.c_v(Contributions::Total) - 1.5 * RGAS) - }) - .collect(); - Ok(res[0]) - } - - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) - } -} +// fn omega11(t: f64) -> f64 { +// 1.06036 * t.powf(-0.15610) +// + 0.19300 * (-0.47635 * t).exp() +// + 1.03587 * (-1.52996 * t).exp() +// + 1.76474 * (-3.89411 * t).exp() +// } + +// fn omega22(t: f64) -> f64 { +// 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() +// - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +// } + +// impl EntropyScaling for Pets { +// fn viscosity_reference( +// &self, +// temperature: SINumber, +// _: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// let x = moles.to_reduced(moles.sum())?; +// let p = &self.parameters; +// let mw = &p.molarweight; +// let ce: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 5.0 / 16.0 +// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) +// .sqrt() +// .unwrap() +// / omega22(tr) +// / (p.sigma[i] * ANGSTROM).powi(2) +// }) +// .collect(); +// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; +// for i in 0..self.components() { +// let denom: f64 = (0..self.components()) +// .map(|j| { +// x[j] * (1.0 +// + (ce[i] / ce[j]).into_value().unwrap().sqrt() +// * (mw[j] / mw[i]).powf(1.0 / 4.0)) +// .powi(2) +// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() +// }) +// .sum(); +// ce_mix += ce[i] * x[i] / denom +// } +// Ok(ce_mix) +// } + +// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// let coefficients = self +// .parameters +// .viscosity +// .as_ref() +// .expect("Missing viscosity coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) +// } + +// fn diffusion_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let density = moles.sum() / volume; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) +// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) +// .sqrt() +// .unwrap() +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .diffusion +// .as_ref() +// .expect("Missing diffusion coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// let e: f64 = (&coefficients.row(4) * x).sum(); +// Ok(a + b * s_res +// - c * (1.0 - s_res.exp()) * s_res.powi(2) +// - d * s_res.powi(4) +// - e * s_res.powi(8)) +// } + +// // fn thermal_conductivity_reference( +// // &self, +// // state: &State, +// // ) -> EosResult { +// // if self.components() != 1 { +// // return Err(EosError::IncompatibleComponents(self.components(), 1)); +// // } +// // let p = &self.parameters; +// // let res: Array1 = (0..self.components()) +// // .map(|i| { +// // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) +// // .into_value() +// // .unwrap(); +// // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); +// // let s_res_cp_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let s_res_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let ref_ce = 0.083235 +// // * ((state.temperature / KELVIN).into_value().unwrap() +// // / (p.molarweight[0])) +// // .sqrt() +// // / p.sigma[0] +// // / p.sigma[0] +// // / omega22(tr); +// // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); +// // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) +// // * (p.sigma[i].powi(3) * p.epsilon_k[0]) +// // / 100000.0; +// // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN +// // }) +// // .collect(); +// // Ok(res[0]) +// // } + +// // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 +// fn thermal_conductivity_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let state = State::new_nvt( +// &Arc::new(Self::new(self.parameters.clone())), +// temperature, +// volume, +// moles, +// )?; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// let ce = 83.235 +// * f64::powf(10.0, -1.5) +// * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() +// / (p.sigma[0] * p.sigma[0]) +// / omega22(tr); +// ce * WATT / METER / KELVIN +// + state.density +// * self +// .diffusion_reference(temperature, volume, moles) +// .unwrap() +// * self +// .diffusion_correlation( +// state +// .residual_entropy() +// .to_reduced(SIUnit::reference_molar_entropy() * state.total_moles) +// .unwrap(), +// &state.molefracs, +// ) +// .unwrap() +// * (state.c_v(Contributions::Total) - 1.5 * RGAS) +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .thermal_conductivity +// .as_ref() +// .expect("Missing thermal conductivity coefficients"); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) +// } +// } #[cfg(test)] mod tests { @@ -345,23 +320,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), - s.pressure(Contributions::Total), - epsilon = 1e-10 - ); - } - - #[test] - fn ideal_gas_heat_capacity_joback() { - let e = Arc::new(Pets::new(argon_parameters())); - let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi(3); - let n = arr1(&[1.0]) * MOL; - let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; - assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); - assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); diff --git a/src/pets/eos/qspr.rs b/src/pets/eos/qspr.rs deleted file mode 100644 index e4d5aa6e4..000000000 --- a/src/pets/eos/qspr.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::pets::parameters::PetsParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -// const NA_P_300: [f64; 6] = [ -// 5177.19095226181, -// 919.565206504576, -// -108.829105648889, -// 0.0, -// -3.93917830677682, -// -13504.5671858292, -// ]; -// const NA_P_400: [f64; 6] = [ -// 10656.1018362315, -// 1146.10782703748, -// -131.023645998081, -// 0.0, -// -9.93789225413177, -// -24430.12952497, -// ]; -// const AP_300: [f64; 6] = [ -// 3600.32322462175, -// 1006.20461224949, -// -151.688378113974, -// 7.81876773647109e-07, -// 8.01001754473385, -// -8959.37140957179, -// ]; -// const AP_400: [f64; 6] = [ -// 7248.0697641199, -// 1267.44346171358, -// -208.738557800023, -// 0.000170238690157906, -// -6.7841792685616, -// -12669.4196622924, -// ]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = (NA_NP_300, NA_NP_400); - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt; - let p2 = sigma3; - let p3 = epsilon_kt * p2; - // let p4 = (temperature.recip() * self.parameters.epsilon_k_ab[i]).exp_m1() - // * p2 - // * sigma3 - // * self.parameters.kappa_ab[i]; - // let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - // + p4 * c_300[3] / T300 - // + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - // + p4 * c_400[3] / T400 - // + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 436c9dc3a..36391a037 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -1,5 +1,4 @@ use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; @@ -119,20 +118,17 @@ pub struct PetsParameters { /// thermal conductivity parameters for entropy scaling pub thermal_conductivity: Option>, /// records of all pure substances of the system - pub pure_records: Vec>, - /// records of parameters for Joback method - pub joback_records: Option>, + pub pure_records: Vec>, /// records of all binary interaction parameters pub binary_records: Array2, } impl Parameter for PetsParameters { type Pure = PetsRecord; - type IdealGas = JobackRecord; type Binary = PetsBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -200,11 +196,6 @@ impl Parameter for PetsParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, sigma, @@ -217,17 +208,11 @@ impl Parameter for PetsParameters { diffusion: diffusion_coefficients, thermal_conductivity: thermal_conductivity_coefficients, pure_records, - joback_records, binary_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -285,7 +270,6 @@ impl std::fmt::Display for PetsParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use std::sync::Arc; pub fn argon_parameters() -> Arc { @@ -308,7 +292,7 @@ pub mod utils { }, "molarweight": 39.948 }"#; - let argon_record: PureRecord = + let argon_record: PureRecord = serde_json::from_str(argon_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(argon_record)) } @@ -330,7 +314,7 @@ pub mod utils { }, "molarweight": 83.798 }"#; - let krypton_record: PureRecord = + let krypton_record: PureRecord = serde_json::from_str(krypton_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(krypton_record)) } @@ -374,7 +358,7 @@ pub mod utils { "molarweight": 83.798 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_binary(binary_record, None)) } diff --git a/src/pets/python.rs b/src/pets/python.rs index f0b365fa1..39a78a977 100644 --- a/src/pets/python.rs +++ b/src/pets/python.rs @@ -1,7 +1,5 @@ use super::parameters::*; -use feos_core::joback::JobackRecord; use feos_core::parameter::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::{impl_binary_record, impl_json_handling, impl_parameter, impl_pure_record}; use ndarray::Array2; @@ -69,7 +67,7 @@ impl PyPetsRecord { } impl_json_handling!(PyPetsRecord); -impl_pure_record!(PetsRecord, PyPetsRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PetsRecord, PyPetsRecord); #[pyclass(name = "PetsBinaryRecord")] #[pyo3( @@ -180,7 +178,6 @@ impl PyPetsParameters { identifier, molarweight.as_ref().map_or(1.0, |v| v[i]), model_record, - None, ) // Hier Ideal Gas anstatt None??? }) @@ -239,7 +236,6 @@ impl PyPetsParameters { ), molarweight.map_or(1.0, |v| v), PetsRecord::new(sigma, epsilon_k, viscosity, diffusion, thermal_conductivity), - None, ); Self(Arc::new(PetsParameters::new_pure(pure_record))) } @@ -265,7 +261,6 @@ pub fn pets(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/cubic.rs b/src/python/cubic.rs index adcced3c3..e73ed374e 100644 --- a/src/python/cubic.rs +++ b/src/python/cubic.rs @@ -1,5 +1,4 @@ use feos_core::python::cubic::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use pyo3::prelude::*; @@ -7,7 +6,6 @@ use pyo3::prelude::*; pub fn cubic(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/dft.rs b/src/python/dft.rs index 8be59bcbc..4a2b45ca8 100644 --- a/src/python/dft.rs +++ b/src/python/dft.rs @@ -21,6 +21,7 @@ use crate::saftvrqmie::python::PySaftVRQMieParameters; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::{FeynmanHibbsOrder, SaftVRQMieFunctional, SaftVRQMieOptions}; +use crate::eos::IdealGasModel; use feos_core::*; use feos_dft::adsorption::*; use feos_dft::interface::*; @@ -33,14 +34,28 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2, PySIArray3, PySIArray4}; +use quantity::python::{PySIArray1, PySIArray2, PySIArray3, PySIArray4, PySINumber}; use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; +type Functional = EquationOfState; + #[pyclass(name = "HelmholtzEnergyFunctional")] #[derive(Clone)] -pub struct PyFunctionalVariant(pub Arc>); +pub struct PyFunctionalVariant(pub Arc>); + +impl PyFunctionalVariant { + fn new(functional: DFT) -> Self + where + FunctionalVariant: From, + { + let functional: DFT = functional.into(); + let n = functional.components(); + let eos = functional.ideal_gas(IdealGasModel::NoModel(n)); + Self(Arc::new(eos)) + } +} #[pymethods] impl PyFunctionalVariant { @@ -84,9 +99,8 @@ impl PyFunctionalVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new( - PcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = PcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// (heterosegmented) group contribution PC-SAFT Helmholtz energy functional. @@ -125,9 +139,8 @@ impl PyFunctionalVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new( - GcPcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = GcPcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// PeTS Helmholtz energy functional without simplifications @@ -153,9 +166,8 @@ impl PyFunctionalVariant { )] fn pets(parameters: PyPetsParameters, fmt_version: FMTVersion, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new( - PetsFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = PetsFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// Helmholtz energy functional for hard sphere systems. @@ -172,9 +184,8 @@ impl PyFunctionalVariant { /// HelmholtzEnergyFunctional #[staticmethod] fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { - Self(Arc::new( - FMTFunctional::new(&sigma.to_owned_array(), fmt_version).into(), - )) + let func = FMTFunctional::new(&sigma.to_owned_array(), fmt_version); + Self::new(func) } /// SAFT-VRQ Mie Helmholtz energy functional. @@ -214,29 +225,28 @@ impl PyFunctionalVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new( - SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } } impl_equation_of_state!(PyFunctionalVariant); -impl_state!(DFT, PyFunctionalVariant); -impl_state_molarweight!(DFT, PyFunctionalVariant); -impl_phase_equilibrium!(DFT, PyFunctionalVariant); +impl_state!(DFT, PyFunctionalVariant); +impl_state_molarweight!(DFT, PyFunctionalVariant); +impl_phase_equilibrium!(DFT, PyFunctionalVariant); -impl_planar_interface!(FunctionalVariant); -impl_surface_tension_diagram!(FunctionalVariant); +impl_planar_interface!(Functional); +impl_surface_tension_diagram!(Functional); -impl_pore!(FunctionalVariant, PyFunctionalVariant); -impl_adsorption!(FunctionalVariant, PyFunctionalVariant); +impl_pore!(Functional, PyFunctionalVariant); +impl_adsorption!(Functional, PyFunctionalVariant); -impl_pair_correlation!(FunctionalVariant); -impl_solvation_profile!(FunctionalVariant); +impl_pair_correlation!(Functional); +impl_solvation_profile!(Functional); #[cfg(feature = "estimator")] -impl_estimator!(DFT, PyFunctionalVariant); +impl_estimator!(DFT, PyFunctionalVariant); #[pymodule] pub fn dft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { diff --git a/src/python/eos.rs b/src/python/eos.rs index 94d1fc8a2..5aa19b504 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -1,4 +1,4 @@ -use crate::eos::EosVariant; +use crate::eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] use crate::estimator::*; #[cfg(feature = "gc_pcsaft")] @@ -27,8 +27,10 @@ use crate::uvtheory::python::PyUVParameters; use crate::uvtheory::{Perturbation, UVTheory, UVTheoryOptions, VirialOrder}; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; use feos_core::python::cubic::PyPengRobinsonParameters; -use feos_core::python::user_defined::PyEoSObj; +use feos_core::python::joback::PyJobackParameters; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; use numpy::convert::ToPyArray; use numpy::{PyArray1, PyArray2}; @@ -36,7 +38,7 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2}; +use quantity::python::{PySIArray1, PySIArray2, PySINumber}; use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; @@ -44,10 +46,10 @@ use std::sync::Arc; /// Collection of equations of state. #[pyclass(name = "EquationOfState")] #[derive(Clone)] -pub struct PyEosVariant(pub Arc); +pub struct PyEquationOfState(pub Arc>); #[pymethods] -impl PyEosVariant { +impl PyEquationOfState { /// PC-SAFT equation of state. /// /// Parameters @@ -87,10 +89,12 @@ impl PyEosVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new(EosVariant::PcSaft(PcSaft::with_options( + let residual = Arc::new(ResidualModel::PcSaft(PcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// (heterosegmented) group contribution PC-SAFT equation of state. @@ -128,10 +132,12 @@ impl PyEosVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new(EosVariant::GcPcSaft(GcPcSaft::with_options( + let residual = Arc::new(ResidualModel::GcPcSaft(GcPcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// Peng-Robinson equation of state. @@ -148,25 +154,27 @@ impl PyEosVariant { /// states. #[staticmethod] pub fn peng_robinson(parameters: PyPengRobinsonParameters) -> Self { - Self(Arc::new(EosVariant::PengRobinson(PengRobinson::new( - parameters.0, - )))) + let residual = Arc::new(ResidualModel::PengRobinson(PengRobinson::new(parameters.0))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } - /// Equation of state from a Python class. + /// Residual Helmholtz energy model from a Python class. /// /// Parameters /// ---------- - /// obj : Class + /// residual : Class /// A python class implementing the necessary methods - /// to be used as equation of state. + /// to be used as residual equation of state. /// /// Returns /// ------- /// EquationOfState #[staticmethod] - fn python(obj: Py) -> PyResult { - Ok(Self(Arc::new(EosVariant::Python(PyEoSObj::new(obj)?)))) + fn python_residual(residual: Py) -> PyResult { + let residual = Arc::new(ResidualModel::Python(PyResidual::new(residual)?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// PeTS equation of state. @@ -188,10 +196,12 @@ impl PyEosVariant { #[pyo3(signature = (parameters, max_eta=0.5), text_signature = "(parameters, max_eta=0.5)")] fn pets(parameters: PyPetsParameters, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new(EosVariant::Pets(Pets::with_options( + let residual = Arc::new(ResidualModel::Pets(Pets::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// UV-Theory equation of state. @@ -230,9 +240,12 @@ impl PyEosVariant { perturbation, virial_order, }; - Ok(Self(Arc::new(EosVariant::UVTheory( - UVTheory::with_options(parameters.0, options)?, - )))) + let residual = Arc::new(ResidualModel::UVTheory(UVTheory::with_options( + parameters.0, + options, + )?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// SAFT-VRQ Mie equation of state. @@ -271,32 +284,70 @@ impl PyEosVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new(EosVariant::SaftVRQMie(SaftVRQMie::with_options( + let residual = Arc::new(ResidualModel::SaftVRQMie(SaftVRQMie::with_options( parameters.0, options, + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) + } + + /// Ideal gas equation of state from a Python class. + /// + /// Parameters + /// ---------- + /// ideal_gas : Class + /// A python class implementing the necessary methods + /// to be used as an ideal gas model. + /// + /// Returns + /// ------- + /// EquationOfState + fn python_ideal_gas(&self, ideal_gas: Py) -> PyResult { + let ig = Arc::new(IdealGasModel::Python(PyIdealGas::new(ideal_gas)?)); + Ok(Self(Arc::new(EquationOfState::new( + ig, + self.0.residual.clone(), )))) } + + /// Ideal gas model of Joback and Reid. + /// + /// Parameters + /// ---------- + /// parameters : List[JobackRecord] + /// List containing + /// + /// Returns + /// ------- + /// EquationOfState + fn joback(&self, parameters: PyJobackParameters) -> Self { + let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(parameters.0))); + Self(Arc::new(EquationOfState::new( + ideal_gas, + self.0.residual.clone(), + ))) + } } -impl_equation_of_state!(PyEosVariant); -impl_virial_coefficients!(PyEosVariant); -impl_state!(EosVariant, PyEosVariant); -impl_state_molarweight!(EosVariant, PyEosVariant); -#[cfg(feature = "pcsaft")] -impl_state_entropy_scaling!(EosVariant, PyEosVariant); -impl_phase_equilibrium!(EosVariant, PyEosVariant); +impl_equation_of_state!(PyEquationOfState); +impl_virial_coefficients!(PyEquationOfState); +impl_state!(EquationOfState, PyEquationOfState); +impl_state_molarweight!(EquationOfState, PyEquationOfState); +impl_state_entropy_scaling!(EquationOfState, PyEquationOfState); +impl_phase_equilibrium!(EquationOfState, PyEquationOfState); #[cfg(feature = "estimator")] -impl_estimator!(EosVariant, PyEosVariant); +impl_estimator!(EquationOfState, PyEquationOfState); #[cfg(all(feature = "estimator", feature = "pcsaft"))] -impl_estimator_entropy_scaling!(EosVariant, PyEosVariant); +impl_estimator_entropy_scaling!(EquationOfState, PyEquationOfState); #[pymodule] pub fn eos(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/ideal_gas.rs b/src/python/ideal_gas.rs new file mode 100644 index 000000000..04e0d177c --- /dev/null +++ b/src/python/ideal_gas.rs @@ -0,0 +1,8 @@ +use feos_core::python::joback::{PyJobackParameters, PyJobackRecord}; +use pyo3::prelude::*; + +#[pymodule] +pub fn ideal_gas(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::() +} diff --git a/src/python/mod.rs b/src/python/mod.rs index ec6e9adb0..e4a3e4f8a 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -15,8 +15,10 @@ use quantity::python::quantity as quantity_module; mod cubic; mod eos; +mod ideal_gas; use cubic::cubic as cubic_module; use eos::eos as eos_module; +use ideal_gas::ideal_gas as ideal_gas_module; #[cfg(feature = "dft")] mod dft; @@ -31,6 +33,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(eos_module))?; #[cfg(feature = "dft")] m.add_wrapped(wrap_pymodule!(dft_module))?; + m.add_wrapped(wrap_pymodule!(ideal_gas_module))?; m.add_wrapped(wrap_pymodule!(cubic_module))?; #[cfg(feature = "pcsaft")] m.add_wrapped(wrap_pymodule!(pcsaft_module))?; @@ -51,6 +54,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { set_path(py, m, "feos.dft", "dft")?; #[cfg(all(feature = "dft", feature = "estimator"))] set_path(py, m, "feos.dft.estimator", "dft.estimator_dft")?; + set_path(py, m, "feos.ideal_gas", "ideal_gas")?; set_path(py, m, "feos.cubic", "cubic")?; #[cfg(feature = "pcsaft")] set_path(py, m, "feos.pcsaft", "pcsaft")?; diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index d728fe9db..92bfe3a28 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -2,9 +2,8 @@ use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, Mono use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::SaftVRQMieParameters; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -24,7 +23,6 @@ pub struct SaftVRQMieFunctional { fmt_version: FMTVersion, options: SaftVRQMieOptions, contributions: Vec>, - joback: Joback, } impl SaftVRQMieFunctional { @@ -61,31 +59,31 @@ impl SaftVRQMieFunctional { let att = AttractiveFunctional::new(parameters.clone()); contributions.push(Box::new(att)); - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) - .into() } } -impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for SaftVRQMieFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) @@ -96,10 +94,6 @@ impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/saftvrqmie/eos/mod.rs b/src/saftvrqmie/eos/mod.rs index 42330a9e3..a7b7100e1 100644 --- a/src/saftvrqmie/eos/mod.rs +++ b/src/saftvrqmie/eos/mod.rs @@ -1,8 +1,7 @@ use super::parameters::SaftVRQMieParameters; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - MolarWeight, State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; @@ -85,7 +84,7 @@ impl SaftVRQMie { } } -impl EquationOfState for SaftVRQMie { +impl Components for SaftVRQMie { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -96,14 +95,16 @@ impl EquationOfState for SaftVRQMie { self.options, ) } +} +impl Residual for SaftVRQMie { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -264,8 +265,8 @@ impl EntropyScaling for SaftVRQMie { .into_value() .unwrap(); let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(RGAS) + .residual_entropy() + .to_reduced(RGAS * state.total_moles) .unwrap() / p.m[i]; let ref_ce = chapman_enskog_thermal_conductivity( diff --git a/src/saftvrqmie/parameters.rs b/src/saftvrqmie/parameters.rs index ed59cd9d5..13b49eb7e 100644 --- a/src/saftvrqmie/parameters.rs +++ b/src/saftvrqmie/parameters.rs @@ -1,4 +1,3 @@ -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, ParameterError, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_traits::Zero; @@ -134,18 +133,16 @@ pub struct SaftVRQMieParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for SaftVRQMieParameters { type Pure = SaftVRQMieRecord; - type IdealGas = JobackRecord; type Binary = SaftVRQMieBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -235,11 +232,6 @@ impl Parameter for SaftVRQMieParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -261,14 +253,13 @@ impl Parameter for SaftVRQMieParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ) { (&self.pure_records, &self.binary_records) @@ -431,7 +422,7 @@ pub mod utils { }, "molarweight": 2.0157309551872 }"#; - let hydrogen_record: PureRecord = + let hydrogen_record: PureRecord = serde_json::from_str(hydrogen_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(hydrogen_record)) } @@ -457,7 +448,7 @@ pub mod utils { }, "molarweight": 4.002601643881807 }"#; - let helium_record: PureRecord = + let helium_record: PureRecord = serde_json::from_str(helium_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(helium_record)) } @@ -483,7 +474,7 @@ pub mod utils { }, "molarweight": 20.17969806457545 }"#; - let neon_record: PureRecord = + let neon_record: PureRecord = serde_json::from_str(neon_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(neon_record)) } @@ -527,7 +518,7 @@ pub mod utils { "molarweight": 20.17969806457545 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_binary( binary_record, diff --git a/src/saftvrqmie/python.rs b/src/saftvrqmie/python.rs index 699269d29..8063e4992 100644 --- a/src/saftvrqmie/python.rs +++ b/src/saftvrqmie/python.rs @@ -3,11 +3,9 @@ use crate::saftvrqmie::eos::FeynmanHibbsOrder; use crate::saftvrqmie::parameters::{ SaftVRQMieBinaryRecord, SaftVRQMieParameters, SaftVRQMieRecord, }; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::PyIdentifier; use feos_core::*; use ndarray::Array2; @@ -147,12 +145,7 @@ impl PySaftVRQMieBinaryRecord { pub struct PySaftVRQMieParameters(pub Arc); impl_json_handling!(PySaftVRQMieRecord); -impl_pure_record!( - SaftVRQMieRecord, - PySaftVRQMieRecord, - JobackRecord, - PyJobackRecord -); +impl_pure_record!(SaftVRQMieRecord, PySaftVRQMieRecord); impl_binary_record!(SaftVRQMieBinaryRecord, PySaftVRQMieBinaryRecord); impl_parameter!(SaftVRQMieParameters, PySaftVRQMieParameters); @@ -229,7 +222,6 @@ impl PySaftVRQMieParameters { pub fn saftvrqmie(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/uvtheory/eos/mod.rs b/src/uvtheory/eos/mod.rs index 143b859c0..1be4f9674 100644 --- a/src/uvtheory/eos/mod.rs +++ b/src/uvtheory/eos/mod.rs @@ -2,7 +2,7 @@ #![allow(clippy::needless_range_loop)] use super::parameters::UVParameters; -use feos_core::{parameter::Parameter, EosError, EosResult, EquationOfState, HelmholtzEnergy}; +use feos_core::{parameter::Parameter, Components, EosError, EosResult, HelmholtzEnergy, Residual}; use ndarray::Array1; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -144,7 +144,7 @@ impl UVTheory { } } -impl EquationOfState for UVTheory { +impl Components for UVTheory { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -156,13 +156,15 @@ impl EquationOfState for UVTheory { ) .expect("Not defined for mixture") } +} +impl Residual for UVTheory { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -175,7 +177,7 @@ mod test { use crate::uvtheory::parameters::*; use approx::assert_relative_eq; use feos_core::parameter::{Identifier, Parameter, PureRecord}; - use feos_core::{Contributions, State}; + use feos_core::State; use ndarray::arr1; use quantity::si::{ANGSTROM, KELVIN, MOL, NAV, RGAS}; @@ -192,8 +194,7 @@ mod test { let moles = arr1(&[2.0]) * MOL; let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); assert_relative_eq!(a, 2.972986567516, max_relative = 1e-12); //wca @@ -221,8 +222,7 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); @@ -251,8 +251,8 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * temperature) + .residual_helmholtz_energy() + .to_reduced(RGAS * temperature * s.total_moles) .unwrap(); dbg!(a); assert_relative_eq!(a, 0.37659379124271003, max_relative = 1e-12); @@ -276,8 +276,8 @@ mod test { let j = Identifier::new(None, None, None, None, None, None); ////////////// - let pr1 = PureRecord::new(i, 1.0, r1, None); - let pr2 = PureRecord::new(j, 1.0, r2, None); + let pr1 = PureRecord::new(i, 1.0, r1); + let pr2 = PureRecord::new(j, 1.0, r2); let pure_records = vec![pr1, pr2]; let uv_parameters = UVParameters::new_binary(pure_records, None); // state @@ -301,8 +301,8 @@ mod test { let state_bh = State::new_nvt(&eos_bh, t_x, volume, &moles).unwrap(); let a_bh = state_bh - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_bh.total_moles) .unwrap(); assert_relative_eq!(a_bh, 2.993577305779432, max_relative = 1e-12); @@ -330,8 +330,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.597791038364405, max_relative = 1e-5); @@ -360,8 +360,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.034206207363139396, max_relative = 1e-5); Ok(()) diff --git a/src/uvtheory/parameters.rs b/src/uvtheory/parameters.rs index f602084d6..e6dbd1fec 100644 --- a/src/uvtheory/parameters.rs +++ b/src/uvtheory/parameters.rs @@ -118,17 +118,16 @@ pub struct UVParameters { pub eps_k_ij: Array2, pub cd_bh_pure: Vec>, pub cd_bh_binary: Array2>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, } impl Parameter for UVParameters { type Pure = UVRecord; - type IdealGas = NoRecord; type Binary = UVBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -198,7 +197,7 @@ impl Parameter for UVParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -207,7 +206,7 @@ impl UVParameters { /// Parameters for a single substance with molar weight one and no (default) ideal gas contributions. pub fn new_simple(rep: f64, att: f64, sigma: f64, epsilon_k: f64) -> Self { let model_record = UVRecord::new(rep, att, sigma, epsilon_k); - let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record, None); + let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record); Self::new_pure(pure_record) } @@ -256,7 +255,7 @@ pub mod utils { pub fn test_parameters(rep: f64, att: f64, sigma: f64, epsilon: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, sigma, epsilon); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } @@ -268,11 +267,11 @@ pub mod utils { ) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep[0], att[0], sigma[0], epsilon[0]); - let pr1 = PureRecord::new(identifier, 1.0, model_record, None); + let pr1 = PureRecord::new(identifier, 1.0, model_record); // let identifier2 = Identifier::new(Some("1"), None, None, None, None, None); let model_record2 = UVRecord::new(rep[1], att[1], sigma[1], epsilon[1]); - let pr2 = PureRecord::new(identifier2, 1.0, model_record2, None); + let pr2 = PureRecord::new(identifier2, 1.0, model_record2); let pure_records = vec![pr1, pr2]; UVParameters::new_binary(pure_records, None) } @@ -280,7 +279,7 @@ pub mod utils { pub fn methane_parameters(rep: f64, att: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, 3.7039, 150.03); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } } diff --git a/src/uvtheory/python.rs b/src/uvtheory/python.rs index fa235b6a5..04a4425e0 100644 --- a/src/uvtheory/python.rs +++ b/src/uvtheory/python.rs @@ -94,7 +94,7 @@ impl PyUVParameters { None, ); let model_record = UVRecord::new(rep[i], att[i], sigma[i], epsilon_k[i]); - PureRecord::new(identifier, 1.0, model_record, None) + PureRecord::new(identifier, 1.0, model_record) }) .collect(); let binary = Array2::from_shape_fn((n, n), |(_, _)| UVBinaryRecord { k_ij: 0.0 }); @@ -130,7 +130,7 @@ impl PyUVParameters { } } -impl_pure_record!(UVRecord, PyUVRecord, NoRecord, PyNoRecord); +impl_pure_record!(UVRecord, PyUVRecord); impl_parameter!(UVParameters, PyUVParameters); #[pymodule] diff --git a/tests/pcsaft/dft.rs b/tests/pcsaft/dft.rs index 066f874ff..d993011a0 100644 --- a/tests/pcsaft/dft.rs +++ b/tests/pcsaft/dft.rs @@ -3,6 +3,7 @@ use approx::assert_relative_eq; use feos::hard_sphere::FMTVersion; use feos::pcsaft::{PcSaft, PcSaftFunctional, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; use feos_core::{Contributions, PhaseEquilibrium, State, Verbosity}; use feos_dft::interface::PlanarInterface; @@ -329,12 +330,17 @@ fn test_entropy_bulk_values() -> Result<(), Box> { None, IdentifierOption::Name, )?; - let func = Arc::new(PcSaftFunctional::new(Arc::new(params))); + let joback_params = JobackParameters::from_json( + vec!["water_np"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name + )?; + let joback = Joback::new(Arc::new(joback_params)); + let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).ideal_gas(joback)); let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; - let s_res = profile - .profile - .entropy_density(Contributions::ResidualNvt)?; + let s_res = profile.profile.entropy_density(Contributions::Residual)?; let s_tot = profile.profile.entropy_density(Contributions::Total)?; println!( "Density:\n{}", @@ -348,8 +354,8 @@ fn test_entropy_bulk_values() -> Result<(), Box> { println!("\nResidual:\n{}", s_res); println!( "liquid: {}, vapor: {}", - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume ); println!("\nTotal:\n{}", s_tot); println!( @@ -359,12 +365,12 @@ fn test_entropy_bulk_values() -> Result<(), Box> { ); assert_relative_eq!( s_res.get(0), - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, max_relative = 1e-8, ); assert_relative_eq!( s_res.get(2047), - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume, + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume, max_relative = 1e-8, ); assert_relative_eq!( diff --git a/tests/pcsaft/properties.rs b/tests/pcsaft/properties.rs index f27c33712..70b527065 100644 --- a/tests/pcsaft/properties.rs +++ b/tests/pcsaft/properties.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{EquationOfState, StateBuilder}; +use feos_core::{Residual, StateBuilder}; use ndarray::*; use quantity::si::*; use std::error::Error; diff --git a/tests/pcsaft/state_creation_mixture.rs b/tests/pcsaft/state_creation_mixture.rs index 8b40d8cb6..5497d4b6b 100644 --- a/tests/pcsaft/state_creation_mixture.rs +++ b/tests/pcsaft/state_creation_mixture.rs @@ -1,35 +1,47 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; -use feos_core::{Contributions, StateBuilder}; +use feos_core::{Contributions, EquationOfState, StateBuilder}; use ndarray::prelude::*; use ndarray::Zip; use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_butane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_butane_parameters( +) -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane", "butane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane", "butane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn pressure_entropy_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let (saft_params, joback_params) = propane_butane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = BAR; let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .temperature(temperature) .pressure(pressure) .molefracs(&x) .build()?; let molar_entropy = state.molar_entropy(Contributions::Total); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_entropy(molar_entropy) .molefracs(&x) @@ -50,7 +62,7 @@ fn pressure_entropy_molefracs() -> Result<(), Box> { #[test] fn volume_temperature_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -67,7 +79,7 @@ fn volume_temperature_molefracs() -> Result<(), Box> { #[test] fn temperature_partial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let partial_density = x.clone() * MOL / METER.powi(3); @@ -86,7 +98,7 @@ fn temperature_partial_density() -> Result<(), Box> { #[test] fn temperature_density_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let density = MOL / METER.powi(3); @@ -104,7 +116,7 @@ fn temperature_density_molefracs() -> Result<(), Box> { #[test] fn temperature_pressure_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let pressure = BAR; let x = arr1(&[0.3, 0.7]); diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index 2299d95cf..ea7dcafc7 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -1,25 +1,34 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, StateBuilder, + Contributions, DensityInitialization, EquationOfState, IdealGas, PhaseEquilibrium, Residual, + State, StateBuilder, }; use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_parameters() -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn temperature_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -34,7 +43,7 @@ fn temperature_volume() -> Result<(), Box> { #[test] fn temperature_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let density = MOL / METER.powi(3); let state = StateBuilder::new(&saft) @@ -47,7 +56,7 @@ fn temperature_density() -> Result<(), Box> { #[test] fn temperature_total_moles_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let volume = METER.powi(3); @@ -63,7 +72,7 @@ fn temperature_total_moles_volume() -> Result<(), Box> { #[test] fn temperature_total_moles_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let density = MOL / METER.powi(3); @@ -82,7 +91,7 @@ fn temperature_total_moles_density() -> Result<(), Box> { #[test] fn pressure_temperature() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -99,7 +108,7 @@ fn pressure_temperature() -> Result<(), Box> { #[test] fn pressure_temperature_phase() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -117,7 +126,7 @@ fn pressure_temperature_phase() -> Result<(), Box> { #[test] fn pressure_temperature_initial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -135,10 +144,13 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { #[test] fn pressure_enthalpy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_enthalpy(molar_enthalpy) .vapor() @@ -154,7 +166,7 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .moles(&state.moles) @@ -174,17 +186,20 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { #[test] fn density_internal_energy() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 5.0 * BAR; let temperature = 315.0 * KELVIN; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .temperature(temperature) .total_moles(total_moles) .build()?; let molar_internal_energy = state.molar_internal_energy(Contributions::Total); - let state_nvu = StateBuilder::new(&saft) + let state_nvu = StateBuilder::new(&eos) .volume(state.volume) .molar_internal_energy(molar_internal_energy) .total_moles(total_moles) @@ -201,11 +216,14 @@ fn density_internal_energy() -> Result<(), Box> { #[test] fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_enthalpy(molar_enthalpy) .total_moles(total_moles) @@ -222,7 +240,7 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .total_moles(state.total_moles) @@ -242,10 +260,13 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { #[test] fn pressure_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_entropy = -2.0 * JOULE / MOL / KELVIN; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_entropy(molar_entropy) .vapor() @@ -261,7 +282,7 @@ fn pressure_entropy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .moles(&state.moles) @@ -281,18 +302,21 @@ fn pressure_entropy_vapor() -> Result<(), Box> { #[test] fn temperature_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 3.0 * BAR; let temperature = 315.15 * KELVIN; let total_moles = 3.0 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .temperature(temperature) .pressure(pressure) .total_moles(total_moles) .build()?; let s = State::new_nts( - &saft, + &eos, temperature, state.molar_entropy(Contributions::Total), &state.moles, @@ -307,7 +331,7 @@ fn temperature_entropy_vapor() -> Result<(), Box> { Ok(()) } -fn assert_multiple_states( +fn assert_multiple_states( states: &[(&State, &str)], pressure: SINumber, enthalpy: SINumber, @@ -338,13 +362,15 @@ fn assert_multiple_states( #[test] fn test_consistency() -> Result<(), Box> { - let p = propane_parameters()?; - let saft = Arc::new(PcSaft::new(p)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .temperature(temperature) .build()?; @@ -361,21 +387,21 @@ fn test_consistency() -> Result<(), Box> { let molar_entropy = state.molar_entropy(Contributions::Total); let density = state.density; - let state_tv = StateBuilder::new(&saft) + let state_tv = StateBuilder::new(&eos) .temperature(temperature) .density(density) .build()?; - let vle = PhaseEquilibrium::pure(&saft, temperature, None, Default::default()); + let vle = PhaseEquilibrium::pure(&eos, temperature, None, Default::default()); let builder = if let Ok(ps) = vle { let p_sat = ps.liquid().pressure(Contributions::Total); if pressure > p_sat { - StateBuilder::new(&saft).liquid() + StateBuilder::new(&eos).liquid() } else { - StateBuilder::new(&saft).vapor() + StateBuilder::new(&eos).vapor() } } else { - StateBuilder::new(&saft).vapor() + StateBuilder::new(&eos).vapor() }; let state_ts = builder diff --git a/tests/pcsaft/test_parameters_joback.json b/tests/pcsaft/test_parameters_joback.json new file mode 100644 index 000000000..afb9c81f4 --- /dev/null +++ b/tests/pcsaft/test_parameters_joback.json @@ -0,0 +1,135 @@ +[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 44.0962, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 58.123, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "74-82-8", + "name": "methane", + "iupac_name": "methane", + "smiles": "C", + "inchi": "InChI=1/CH4/h1H4", + "formula": "CH4" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 16.0426 + }, + { + "identifier": { + "cas": "124-38-9", + "name": "carbon-dioxide", + "iupac_name": "carbon dioxide", + "smiles": "O=C=O", + "inchi": "InChI=1/CO2/c2-1-3", + "formula": "CO2" + }, + "molarweight": 44.0098, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + } + }, + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "110-54-3", + "name": "hexane", + "iupac_name": "hexane", + "smiles": "CCCCCC", + "inchi": "InChI=1/C6H14/c1-3-5-6-4-2/h3-6H2,1-2H3", + "formula": "C6H14" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH2", + "CH2", + "CH3" + ] + }, + "molarweight": 86.177 + } +] \ No newline at end of file From 498be2fe832416035fffbf12726ff964b838e390 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 7 Jul 2023 13:56:40 +0200 Subject: [PATCH 5/7] Fixes for the bubble and dew point calculations (#166) --- feos-core/CHANGELOG.md | 20 ++++--- feos-core/src/phase_equilibria/bubble_dew.rs | 60 ++++++++++---------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index d44de111c..a7ed73f82 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -7,22 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - Added `Components`, `Residual`, `IdealGas` and `DeBroglieWavelength` traits to decouple ideal gas models from residual models. [#158](https://github.com/feos-org/feos/pull/158) -- Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/) +- Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/158) ### Changed -- Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/) -- The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/) -- Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/) +- Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/158) +- The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/158) - State creation routines are split into those that can be used with `Residual` and those that require `Residual + IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) - `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is renamed to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) -- Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/) +- Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/158) - Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) -- Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/) +- Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/158) +- Bubble and dew point iterations will not attempt a second iteration if no solution is found for the given initial pressure. [#166](https://github.com/feos-org/feos/pull/166) ### Removed - Removed `EquationOfState` trait. [#158](https://github.com/feos-org/feos/pull/158) -- Removed ideal gas dependencies from `PureRecord` and `SegmentRecord`. [#158](https://github.com/feos-org/feos/pull/) -- Removed Python getter and setter functions and optional arguments for ideal gas records in macros. [#158](https://github.com/feos-org/feos/pull/) +- Removed ideal gas dependencies from `PureRecord` and `SegmentRecord`. [#158](https://github.com/feos-org/feos/pull/158) +- Removed Python getter and setter functions and optional arguments for ideal gas records in macros. [#158](https://github.com/feos-org/feos/pull/158) + +### Fixed +- The vapor and liquid states in a bubble or dew point iteration are assigned correctly according to the inputs, rather than based on the mole density which can be incorrect for mixtures with large differences in molar weights. [#166](https://github.com/feos-org/feos/pull/166) ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index ee9a133db..4a13a65b4 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -119,51 +119,44 @@ impl PhaseEquilibrium { match tp_spec { TPSpec::Temperature(t) => { // First use given initial pressure if applicable - let mut vle = tp_init - .map(|p| { + if let Some(p) = tp_init { + return Self::iterate_bubble_dew( + eos, + tp_spec, + p, + molefracs_spec, + molefracs_init, + bubble, + options, + ); + } + + // Next try to initialize with an ideal gas assumption + let vle = Self::starting_pressure_ideal_gas(eos, t, molefracs_spec, bubble) + .and_then(|(p, x)| { Self::iterate_bubble_dew( eos, tp_spec, p, molefracs_spec, - molefracs_init, + molefracs_init.or(Some(&x)), bubble, options, ) - }) - .and_then(Result::ok); + }); - // Next try to initialize with an ideal gas assumption - vle = vle.or_else(|| { - let (p, x) = - Self::starting_pressure_ideal_gas(eos, t, molefracs_spec, bubble).ok()?; + // Finally use the spinodal to initialize the calculation + vle.or_else(|_| { Self::iterate_bubble_dew( eos, tp_spec, - p, + Self::starting_pressure_spinodal(eos, t, molefracs_spec)?, molefracs_spec, - molefracs_init.or(Some(&x)), + molefracs_init, bubble, options, ) - .ok() - }); - - // Finally use the spinodal to initialize the calculation - vle.map_or_else( - || { - Self::iterate_bubble_dew( - eos, - tp_spec, - Self::starting_pressure_spinodal(eos, t, molefracs_spec)?, - molefracs_spec, - molefracs_init, - bubble, - options, - ) - }, - Ok, - ) + }) } TPSpec::Pressure(_) => { let temperature = tp_init.expect("An initial temperature is required for the calculation of bubble/dew points at given pressure!"); @@ -198,7 +191,7 @@ impl PhaseEquilibrium { } else { starting_x2_dew(eos, t, p, molefracs_spec, molefracs_init) }?; - bubble_dew(tp_spec, var, state1, state2, options) + bubble_dew(tp_spec, var, state1, state2, bubble, options) } fn starting_pressure_ideal_gas( @@ -358,6 +351,7 @@ fn bubble_dew( mut var_tp: TPSpec, mut state1: State, mut state2: State, + bubble: bool, options: (SolverOptions, SolverOptions), ) -> EosResult> where @@ -434,7 +428,11 @@ where "Bubble/dew point: calculation converged in {} step(s)\n", k_out ); - Ok(PhaseEquilibrium::from_states(state1, state2)) + if bubble { + Ok(PhaseEquilibrium([state2, state1])) + } else { + Ok(PhaseEquilibrium([state1, state2])) + } } else { // not converged, return EosError Err(EosError::NotConverged(String::from("bubble-dew-iteration"))) From 88dd0c3ac879b2c51e7c436fc0317bfd0ec98c91 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:52:08 +0200 Subject: [PATCH 6/7] Implement binary association parameters (#167) * Implement binary association parameters * Optional binary records and new parameter constructor (#169) --- CHANGELOG.md | 1 + feos-core/CHANGELOG.md | 3 + feos-core/src/cubic.rs | 17 ++- feos-core/src/joback.rs | 15 +-- feos-core/src/lib.rs | 3 +- feos-core/src/parameter/mod.rs | 77 ++++++----- feos-core/src/python/cubic.rs | 8 +- feos-core/src/python/joback.rs | 9 +- feos-core/src/python/parameter.rs | 77 ++++++----- src/association/mod.rs | 209 +++++++++++++++++++++++++----- src/gc_pcsaft/dft/parameter.rs | 2 +- src/gc_pcsaft/eos/parameter.rs | 2 +- src/pcsaft/parameters.rs | 115 +++++++++++----- src/pcsaft/python.rs | 29 ++++- src/pets/parameters.rs | 23 ++-- src/pets/python.rs | 17 +-- src/saftvrqmie/parameters.rs | 13 +- src/saftvrqmie/python.rs | 10 +- src/uvtheory/parameters.rs | 15 ++- src/uvtheory/python.rs | 6 +- 20 files changed, 453 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4d9932f..20a29fe2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `IdealGasModel` enum that collects all implementors of the `IdealGas` trait. [#158](https://github.com/feos-org/feos/pull/158) - Added `feos.ideal_gas` module in Python from which (currently) `Joback` and `JobackParameters` are available. [#158](https://github.com/feos-org/feos/pull/158) +- Added binary association parameters to PC-SAFT. [#167](https://github.com/feos-org/feos/pull/167) ### Changed - Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index a7ed73f82..633dcf05f 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `Components`, `Residual`, `IdealGas` and `DeBroglieWavelength` traits to decouple ideal gas models from residual models. [#158](https://github.com/feos-org/feos/pull/158) - Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/158) +- Added `Parameter::from_model_records` as a simpler interface to generate parameters. [#169](https://github.com/feos-org/feos/pull/169) ### Changed - Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/158) @@ -19,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) - Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/158) - Bubble and dew point iterations will not attempt a second iteration if no solution is found for the given initial pressure. [#166](https://github.com/feos-org/feos/pull/166) +- Made the binary records in the constructions and getters of the `Parameter` trait optional. [#169](https://github.com/feos-org/feos/pull/169) +- Changed the second argument of `new_binary` in Python from a `BinaryRecord` to the corresponding binary model record (analogous to the Rust implementation). [#169](https://github.com/feos-org/feos/pull/169) ### Removed - Removed `EquationOfState` trait. [#158](https://github.com/feos-org/feos/pull/158) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index be399d67d..e06ed36e1 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -100,10 +100,7 @@ impl PengRobinsonParameters { PureRecord::new(id, molarweight[i], record) }) .collect(); - Ok(PengRobinsonParameters::from_records( - records, - Array2::zeros([pc.len(); 2]), - )) + Ok(PengRobinsonParameters::from_records(records, None)) } } @@ -114,7 +111,7 @@ impl Parameter for PengRobinsonParameters { /// Creates parameters from pure component records. fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { let n = pure_records.len(); @@ -133,19 +130,21 @@ impl Parameter for PengRobinsonParameters { kappa[i] = 0.37464 + (1.54226 - 0.26992 * r.acentric_factor) * r.acentric_factor; } + let k_ij = binary_records.unwrap_or_else(|| Array2::zeros([n; 2])); + Self { tc, a, b, - k_ij: binary_records, + k_ij, kappa, molarweight, pure_records, } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.k_ij) + fn records(&self) -> (&[PureRecord], Option<&Array2>) { + (&self.pure_records, Some(&self.k_ij)) } } @@ -294,7 +293,7 @@ mod tests { let propane = mixture[0].clone(); let tc = propane.model_record.tc; let pc = propane.model_record.pc; - let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let parameters = PengRobinsonParameters::new_pure(propane); let pr = Arc::new(PengRobinson::new(Arc::new(parameters))); let options = SolverOptions::new().verbosity(Verbosity::Iter); let cp = State::critical_point(&pr, None, None, options)?; diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index d1a6bdb85..6aca1e41f 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -72,7 +72,6 @@ pub struct JobackParameters { d: Array1, e: Array1, pure_records: Vec>, - binary_records: Array2, } impl Parameter for JobackParameters { @@ -81,11 +80,10 @@ impl Parameter for JobackParameters { fn from_records( pure_records: Vec>, - _binary_records: Array2, + _binary_records: Option>, ) -> Self { let n = pure_records.len(); - let binary_records = Array::from_elem((n, n), JobackBinaryRecord); let mut a = Array::zeros(n); let mut b = Array::zeros(n); let mut c = Array::zeros(n); @@ -108,12 +106,11 @@ impl Parameter for JobackParameters { d, e, pure_records, - binary_records, } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.binary_records) + fn records(&self) -> (&[PureRecord], Option<&Array2>) { + (&self.pure_records, None) } } @@ -188,11 +185,7 @@ impl Components for Joback { component_list .iter() .for_each(|&i| records.push(self.parameters.pure_records[i].clone())); - let n = component_list.len(); - Self::new(Arc::new(JobackParameters::from_records( - records, - Array::from_elem((n, n), JobackBinaryRecord), - ))) + Self::new(Arc::new(JobackParameters::from_records(records, None))) } } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index ddea14018..93ce524f4 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -202,7 +202,6 @@ mod tests { use crate::EosResult; use crate::StateBuilder; use approx::*; - use ndarray::Array2; use quantity::si::*; use std::sync::Arc; @@ -248,7 +247,7 @@ mod tests { fn validate_residual_properties() -> EosResult<()> { let mixture = pure_record_vec(); let propane = mixture[0].clone(); - let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let parameters = PengRobinsonParameters::new_pure(propane); let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); let joback_parameters = Arc::new(JobackParameters::new_pure(PureRecord::new( Identifier::default(), diff --git a/feos-core/src/parameter/mod.rs b/feos-core/src/parameter/mod.rs index 5782db00f..67a00bd8c 100644 --- a/feos-core/src/parameter/mod.rs +++ b/feos-core/src/parameter/mod.rs @@ -35,13 +35,12 @@ where /// Creates parameters from records for pure substances and possibly binary parameters. fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self; /// Creates parameters for a pure component from a pure record. fn new_pure(pure_record: PureRecord) -> Self { - let binary_record = Array2::from_elem([1, 1], Self::Binary::default()); - Self::from_records(vec![pure_record], binary_record) + Self::from_records(vec![pure_record], None) } /// Creates parameters for a binary system from pure records and an optional @@ -50,19 +49,31 @@ where pure_records: Vec>, binary_record: Option, ) -> Self { - let binary_record = Array2::from_shape_fn([2, 2], |(i, j)| { - if i == j { - Self::Binary::default() - } else { - binary_record.clone().unwrap_or_default() - } + let binary_record = binary_record.map(|br| { + Array2::from_shape_fn([2, 2], |(i, j)| { + if i == j { + Self::Binary::default() + } else { + br.clone() + } + }) }); Self::from_records(pure_records, binary_record) } + /// Creates parameters from model records with default values for the molar weight, + /// identifiers, and binary interaction parameters. + fn from_model_records(model_records: Vec) -> Self { + let pure_records = model_records + .into_iter() + .map(|r| PureRecord::new(Default::default(), Default::default(), r)) + .collect(); + Self::from_records(pure_records, None) + } + /// Return the original pure and binary records that were used to construct the parameters. #[allow(clippy::type_complexity)] - fn records(&self) -> (&[PureRecord], &Array2); + fn records(&self) -> (&[PureRecord], Option<&Array2>); /// Helper function to build matrix from list of records in correct order. /// @@ -73,7 +84,11 @@ where pure_records: &Vec>, binary_records: &[BinaryRecord], search_option: IdentifierOption, - ) -> Array2 { + ) -> Option> { + if binary_records.is_empty() { + return None; + } + // Build Hashmap (id, id) -> BinaryRecord let binary_map: HashMap<(String, String), Self::Binary> = { binary_records @@ -86,7 +101,7 @@ where .collect() }; let n = pure_records.len(); - Array2::from_shape_fn([n, n], |(i, j)| { + Some(Array2::from_shape_fn([n, n], |(i, j)| { let id1 = pure_records[i] .identifier .as_string(search_option) @@ -106,7 +121,7 @@ where .or_else(|| binary_map.get(&(id2, id1))) .cloned() .unwrap_or_default() - }) + })) } /// Creates parameters from substance information stored in json files. @@ -250,7 +265,7 @@ where } } - Ok(Self::from_records(pure_records, binary_records)) + Ok(Self::from_records(pure_records, Some(binary_records))) } /// Creates parameters from segment information stored in json files. @@ -331,8 +346,10 @@ where .map(|&i| pure_records[i].clone()) .collect(); let n = component_list.len(); - let binary_records = Array2::from_shape_fn([n, n], |(i, j)| { - binary_records[(component_list[i], component_list[j])].clone() + let binary_records = binary_records.map(|br| { + Array2::from_shape_fn([n, n], |(i, j)| { + br[(component_list[i], component_list[j])].clone() + }) }); Self::from_records(pure_records, binary_records) @@ -484,7 +501,7 @@ mod test { struct MyParameter { pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, } impl Parameter for MyParameter { @@ -492,7 +509,7 @@ mod test { type Binary = MyBinaryModel; fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { Self { pure_records, @@ -500,8 +517,8 @@ mod test { } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.binary_records) + fn records(&self) -> (&[PureRecord], Option<&Array2>) { + (&self.pure_records, self.binary_records.as_ref()) } } @@ -555,7 +572,7 @@ mod test { assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]].b, 12.0) + assert_eq!(p.binary_records.unwrap()[[0, 1]].b, 12.0) } #[test] @@ -608,8 +625,9 @@ mod test { assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[0, 1]].b, 0.0) + let br = p.binary_records.as_ref().unwrap(); + assert_eq!(br[[0, 1]], MyBinaryModel::default()); + assert_eq!(br[[0, 1]].b, 0.0) } #[test] @@ -672,11 +690,12 @@ mod test { assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[2, 0]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[2, 1]].b, 12.0); - assert_eq!(p.binary_records[[1, 2]].b, 12.0); + let br = p.binary_records.as_ref().unwrap(); + assert_eq!(br[[0, 1]], MyBinaryModel::default()); + assert_eq!(br[[1, 0]], MyBinaryModel::default()); + assert_eq!(br[[0, 2]], MyBinaryModel::default()); + assert_eq!(br[[2, 0]], MyBinaryModel::default()); + assert_eq!(br[[2, 1]].b, 12.0); + assert_eq!(br[[1, 2]].b, 12.0); } } diff --git a/feos-core/src/python/cubic.rs b/feos-core/src/python/cubic.rs index 1973a1bf1..a74802750 100644 --- a/feos-core/src/python/cubic.rs +++ b/feos-core/src/python/cubic.rs @@ -4,7 +4,6 @@ use crate::parameter::{ }; use crate::python::parameter::PyIdentifier; use crate::*; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; @@ -56,7 +55,12 @@ impl_binary_record!(); #[derive(Clone)] pub struct PyPengRobinsonParameters(pub Arc); -impl_parameter!(PengRobinsonParameters, PyPengRobinsonParameters); +impl_parameter!( + PengRobinsonParameters, + PyPengRobinsonParameters, + PyPengRobinsonRecord, + f64 +); #[pymethods] impl PyPengRobinsonParameters { diff --git a/feos-core/src/python/joback.rs b/feos-core/src/python/joback.rs index c54257b6c..de1f05031 100644 --- a/feos-core/src/python/joback.rs +++ b/feos-core/src/python/joback.rs @@ -7,7 +7,6 @@ use crate::{ impl_binary_record, impl_json_handling, impl_parameter, impl_parameter_from_segments, impl_pure_record, impl_segment_record, }; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; @@ -62,6 +61,7 @@ impl_segment_record!(JobackRecord, PyJobackRecord); pub struct PyJobackBinaryRecord(pub JobackBinaryRecord); impl_binary_record!(JobackBinaryRecord, PyJobackBinaryRecord); + /// Create a set of Joback parameters from records. /// /// Parameters @@ -83,7 +83,12 @@ impl_binary_record!(JobackBinaryRecord, PyJobackBinaryRecord); #[derive(Clone)] pub struct PyJobackParameters(pub Arc); -impl_parameter!(JobackParameters, PyJobackParameters); +impl_parameter!( + JobackParameters, + PyJobackParameters, + PyJobackRecord, + PyJobackBinaryRecord +); impl_parameter_from_segments!(JobackParameters, PyJobackParameters); #[pymethods] diff --git a/feos-core/src/python/parameter.rs b/feos-core/src/python/parameter.rs index 9c0e80e94..8a98b145d 100644 --- a/feos-core/src/python/parameter.rs +++ b/feos-core/src/python/parameter.rs @@ -201,6 +201,12 @@ macro_rules! impl_binary_record { #[derive(Clone)] pub struct PyBinaryRecord(pub BinaryRecord); + impl From<$py_model_record> for $model_record { + fn from(record: $py_model_record) -> Self { + record.0 + } + } + #[pymethods] impl PyBinaryRecord { #[new] @@ -540,7 +546,7 @@ macro_rules! impl_segment_record { #[macro_export] macro_rules! impl_parameter { - ($parameter:ty, $py_parameter:ty) => { + ($parameter:ty, $py_parameter:ty, $py_model_record:ty, $py_binary_model_record:ty) => { #[pymethods] impl $py_parameter { /// Creates parameters from records. @@ -565,32 +571,26 @@ macro_rules! impl_parameter { search_option: IdentifierOption, ) -> PyResult { let prs = pure_records.into_iter().map(|pr| pr.0).collect(); - if let Some(binary_records) = binary_records { - let brs = if let Ok(br) = binary_records.extract::>() { - Ok(br.to_owned_array().mapv(|r| r.try_into().unwrap())) - } else if let Ok(br) = binary_records.extract::>() { - let brs: Vec<_> = br.into_iter().map(|br| br.0).collect(); - Ok(<$parameter>::binary_matrix_from_records( - &prs, - &brs, - search_option, - )) - } else { - Err(PyErr::new::(format!( - "Could not parse binary input!" - ))) - }; - Ok(Self(Arc::new(<$parameter>::from_records( - prs, - brs.unwrap(), - )))) - } else { - let n = prs.len(); - Ok(Self(Arc::new(<$parameter>::from_records( - prs, - Array2::from_elem([n, n], <$parameter as Parameter>::Binary::default()), - )))) - } + let binary_records = binary_records + .map(|binary_records| { + if let Ok(br) = binary_records.extract::>() { + Ok(Some(br.to_owned_array().mapv(|r| r.try_into().unwrap()))) + } else if let Ok(br) = binary_records.extract::>() { + let brs: Vec<_> = br.into_iter().map(|br| br.0).collect(); + Ok(<$parameter>::binary_matrix_from_records( + &prs, + &brs, + search_option, + )) + } else { + Err(PyErr::new::(format!( + "Could not parse binary input!" + ))) + } + }) + .transpose()? + .flatten(); + Ok(Self(Arc::new(Parameter::from_records(prs, binary_records)))) } /// Creates parameters for a pure component from a pure record. @@ -624,8 +624,8 @@ macro_rules! impl_parameter { .map(|br| { if let Ok(r) = br.extract::() { Ok(r.try_into()?) - } else if let Ok(r) = br.extract::() { - Ok(r.0.model_record) + } else if let Ok(r) = br.extract::<$py_binary_model_record>() { + Ok(r.into()) } else { Err(PyErr::new::(format!( "Could not parse binary input!" @@ -636,6 +636,19 @@ macro_rules! impl_parameter { Ok(Self(Arc::new(<$parameter>::new_binary(prs, br)))) } + /// Creates parameters from model records with default values for the molar weight, + /// identifiers, and binary interaction parameters. + /// + /// Parameters + /// ---------- + /// model_records : [ModelRecord] + /// A list of model parameters. + #[staticmethod] + fn from_model_records(model_records: Vec<$py_model_record>) -> PyResult { + let mrs = model_records.into_iter().map(|mr| mr.0).collect(); + Ok(Self(Arc::new(<$parameter>::from_model_records(mrs)))) + } + /// Creates parameters from json files. /// /// Parameters @@ -706,13 +719,11 @@ macro_rules! impl_parameter { } #[getter] - fn get_binary_records<'py>(&self, py: Python<'py>) -> &'py PyArray2 { + fn get_binary_records<'py>(&self, py: Python<'py>) -> Option<&'py PyArray2> { self.0 .records() .1 - .mapv(|r| f64::try_from(r).unwrap()) - .view() - .to_pyarray(py) + .map(|r| r.mapv(|r| f64::try_from(r).unwrap()).view().to_pyarray(py)) } } }; diff --git a/src/association/mod.rs b/src/association/mod.rs index afecb2731..86b0ffeba 100644 --- a/src/association/mod.rs +++ b/src/association/mod.rs @@ -7,6 +7,7 @@ use num_dual::linalg::{norm, LU}; use num_dual::*; use num_traits::Zero; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fmt; use std::sync::Arc; @@ -20,15 +21,17 @@ pub use python::PyAssociationRecord; #[derive(Clone, Copy, Debug)] struct AssociationSite { assoc_comp: usize, + site_index: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64, } impl AssociationSite { - fn new(assoc_comp: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64) -> Self { + fn new(assoc_comp: usize, site_index: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64) -> Self { Self { assoc_comp, + site_index, n, kappa_ab, epsilon_k_ab, @@ -37,11 +40,15 @@ impl AssociationSite { } /// Pure component association parameters. -#[derive(Serialize, Deserialize, Clone, Copy, Default)] +#[derive(Serialize, Deserialize, Clone, Copy)] pub struct AssociationRecord { /// Association volume parameter + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] pub kappa_ab: f64, /// Association energy parameter in units of Kelvin + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] pub epsilon_k_ab: f64, /// \# of association sites of type A #[serde(skip_serializing_if = "f64::is_zero")] @@ -86,6 +93,27 @@ impl fmt::Display for AssociationRecord { } } +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct BinaryAssociationRecord { + pub kappa_ab: Option, + pub epsilon_k_ab: Option, + pub site_indices: [usize; 2], +} + +impl BinaryAssociationRecord { + pub fn new( + kappa_ab: Option, + epsilon_k_ab: Option, + site_indices: Option<[usize; 2]>, + ) -> Self { + Self { + kappa_ab, + epsilon_k_ab, + site_indices: site_indices.unwrap_or_default(), + } + } +} + /// Parameter set required for the SAFT association Helmoltz energy /// contribution and functional. #[derive(Clone)] @@ -104,6 +132,7 @@ impl AssociationParameters { pub fn new( records: &[Vec], sigma: &Array1, + binary_records: &[((usize, usize), BinaryAssociationRecord)], component_index: Option<&Array1>, ) -> Self { let mut sites_a = Vec::new(); @@ -111,51 +140,101 @@ impl AssociationParameters { let mut sites_c = Vec::new(); for (i, record) in records.iter().enumerate() { - for site in record { - if site.kappa_ab > 0.0 && site.epsilon_k_ab > 0.0 { - if site.na > 0.0 { - sites_a.push(AssociationSite::new( - i, - site.na, - site.kappa_ab, - site.epsilon_k_ab, - )); - } - if site.nb > 0.0 { - sites_b.push(AssociationSite::new( - i, - site.nb, - site.kappa_ab, - site.epsilon_k_ab, - )); - } - if site.nc > 0.0 { - sites_c.push(AssociationSite::new( - i, - site.nc, - site.kappa_ab, - site.epsilon_k_ab, - )); - } + for (s, site) in record.iter().enumerate() { + if site.na > 0.0 { + sites_a.push(AssociationSite::new( + i, + s, + site.na, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nb > 0.0 { + sites_b.push(AssociationSite::new( + i, + s, + site.nb, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nc > 0.0 { + sites_c.push(AssociationSite::new( + i, + s, + site.nc, + site.kappa_ab, + site.epsilon_k_ab, + )); } } } - let sigma3_kappa_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { - (sigma[sites_a[i].assoc_comp] * sigma[sites_b[j].assoc_comp]).powf(1.5) - * (sites_a[i].kappa_ab * sites_b[j].kappa_ab).sqrt() - }); - let sigma3_kappa_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + let indices_a: HashMap<_, _> = sites_a + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let indices_b: HashMap<_, _> = sites_b + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let indices_c: HashMap<_, _> = sites_c + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let mut sigma3_kappa_ab = + Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + (sigma[sites_a[i].assoc_comp] * sigma[sites_b[j].assoc_comp]).powf(1.5) + * (sites_a[i].kappa_ab * sites_b[j].kappa_ab).sqrt() + }); + let mut sigma3_kappa_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { (sigma[sites_c[i].assoc_comp] * sigma[sites_c[j].assoc_comp]).powf(1.5) * (sites_c[i].kappa_ab * sites_c[j].kappa_ab).sqrt() }); - let epsilon_k_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + let mut epsilon_k_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { 0.5 * (sites_a[i].epsilon_k_ab + sites_b[j].epsilon_k_ab) }); - let epsilon_k_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + let mut epsilon_k_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { 0.5 * (sites_c[i].epsilon_k_ab + sites_c[j].epsilon_k_ab) }); + for &((i, j), record) in binary_records.iter() { + let [a, b] = record.site_indices; + if let (Some(x), Some(y)) = (indices_a.get(&(i, a)), indices_b.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + if let (Some(y), Some(x)) = (indices_b.get(&(i, a)), indices_a.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + if let (Some(x), Some(y)) = (indices_c.get(&(i, a)), indices_c.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_cc[[*x, *y]] = epsilon_k_aibj; + epsilon_k_cc[[*y, *x]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_cc[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + sigma3_kappa_cc[[*y, *x]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + } + Self { component_index: component_index .cloned() @@ -472,6 +551,66 @@ impl Association

{ } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_binary_parameters() { + let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; + let comp2 = vec![AssociationRecord::new(0.2, 1500., 1.0, 1.0, 0.0)]; + let comp3 = vec![AssociationRecord::new(0.3, 500., 0.0, 1.0, 0.0)]; + let comp4 = vec![ + AssociationRecord::new(0.3, 1000., 1.0, 0.0, 0.0), + AssociationRecord::new(0.3, 2000., 0.0, 1.0, 0.0), + ]; + let records = [comp1, comp2, comp3, comp4]; + let sigma = arr1(&[3.0, 3.0, 3.0, 3.0]); + let binary = [ + ( + (0, 1), + BinaryAssociationRecord::new(Some(3.5), Some(1234.), Some([0, 0])), + ), + ( + (0, 2), + BinaryAssociationRecord::new(Some(3.5), Some(3140.), Some([0, 0])), + ), + ( + (1, 3), + BinaryAssociationRecord::new(Some(3.5), Some(3333.), Some([0, 1])), + ), + ]; + let assoc = AssociationParameters::new(&records, &sigma, &binary, None); + println!("{}", assoc.epsilon_k_ab); + let epsilon_k_ab = arr2(&[ + [2500., 1234., 3140., 2250.], + [1234., 1500., 1000., 3333.], + [1750., 1250., 750., 1500.], + ]); + assert_eq!(assoc.epsilon_k_ab, epsilon_k_ab); + } + + #[test] + fn test_induced_association() { + let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; + let comp2 = vec![AssociationRecord::new(0.1, -500., 0.0, 1.0, 0.0)]; + let comp3 = vec![AssociationRecord::new(0.0, 0.0, 0.0, 1.0, 0.0)]; + let sigma = arr1(&[3.0, 3.5]); + let binary = [( + (0, 1), + BinaryAssociationRecord::new(Some(0.1), Some(1000.), None), + )]; + let assoc1 = AssociationParameters::new(&[comp1.clone(), comp2], &sigma, &[], None); + let assoc2 = AssociationParameters::new(&[comp1, comp3], &sigma, &binary, None); + println!("{}", assoc1.epsilon_k_ab); + println!("{}", assoc2.epsilon_k_ab); + assert_eq!(assoc1.epsilon_k_ab, assoc2.epsilon_k_ab); + println!("{}", assoc1.sigma3_kappa_ab); + println!("{}", assoc2.sigma3_kappa_ab); + assert_eq!(assoc1.sigma3_kappa_ab, assoc2.sigma3_kappa_ab); + } +} + #[cfg(test)] #[cfg(feature = "pcsaft")] mod tests_pcsaft { diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index 43fc9016e..68d644b0d 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -128,7 +128,7 @@ impl ParameterHetero for GcPcSaftFunctionalParameters { let sigma = Array1::from_vec(sigma); let component_index = Array1::from_vec(component_index); let association = - AssociationParameters::new(&association_records, &sigma, Some(&component_index)); + AssociationParameters::new(&association_records, &sigma, &[], Some(&component_index)); Ok(Self { molarweight, diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index 537b0ba64..665c19cff 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -220,7 +220,7 @@ impl ParameterHetero for GcPcSaftEosParameters { let sigma = Array1::from_vec(sigma); let component_index = Array1::from_vec(component_index); let association = - AssociationParameters::new(&association_records, &sigma, Some(&component_index)); + AssociationParameters::new(&association_records, &sigma, &[], Some(&component_index)); Ok(Self { molarweight, diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 097617a5a..2738b61ef 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -1,4 +1,4 @@ -use crate::association::{AssociationParameters, AssociationRecord}; +use crate::association::{AssociationParameters, AssociationRecord, BinaryAssociationRecord}; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use conv::ValueInto; use feos_core::parameter::{ @@ -161,7 +161,11 @@ impl FromSegments for PcSaftRecord { let polar_segments: usize = segments .iter() .filter_map(|(s, n)| { - if s.q.is_some() || s.mu.is_some() || s.association_record.is_some() { + if s.q.is_some() + || s.mu.is_some() + || s.association_record + .is_some_and(|r| r.na + r.nb + r.nc > 0.0) + { Some(n) } else { None @@ -231,18 +235,21 @@ impl PcSaftRecord { diffusion: Option<[f64; 5]>, thermal_conductivity: Option<[f64; 4]>, ) -> PcSaftRecord { - let association_record = match (kappa_ab, epsilon_k_ab) { - (Some(kappa_ab), Some(epsilon_k_ab)) => Some(AssociationRecord::new( - kappa_ab, - epsilon_k_ab, - na.unwrap_or(0.0), - nb.unwrap_or(0.0), - nc.unwrap_or(0.0), - )), - (None, None) => None, - _ => { - panic!("To model association, both kappa_ab and epsilon_k_ab need to be specified.") - } + let association_record = if kappa_ab.is_none() + && epsilon_k_ab.is_none() + && na.is_none() + && nb.is_none() + && nc.is_none() + { + None + } else { + Some(AssociationRecord::new( + kappa_ab.unwrap_or_default(), + epsilon_k_ab.unwrap_or_default(), + na.unwrap_or_default(), + nb.unwrap_or_default(), + nc.unwrap_or_default(), + )) }; PcSaftRecord { m, @@ -260,12 +267,16 @@ impl PcSaftRecord { #[derive(Serialize, Deserialize, Clone, Default)] pub struct PcSaftBinaryRecord { - k_ij: f64, + pub k_ij: f64, + association: Option, } impl From for PcSaftBinaryRecord { fn from(k_ij: f64) -> Self { - Self { k_ij } + Self { + k_ij, + association: None, + } } } @@ -275,19 +286,43 @@ impl From for f64 { } } +impl PcSaftBinaryRecord { + pub fn new(k_ij: Option, kappa_ab: Option, epsilon_k_ab: Option) -> Self { + let k_ij = k_ij.unwrap_or_default(); + let association = if kappa_ab.is_none() && epsilon_k_ab.is_none() { + None + } else { + Some(BinaryAssociationRecord::new(kappa_ab, epsilon_k_ab, None)) + }; + Self { k_ij, association } + } +} + impl> FromSegmentsBinary for PcSaftBinaryRecord { fn from_segments_binary(segments: &[(Self, T, T)]) -> Result { let (k_ij, n) = segments.iter().fold((0.0, 0.0), |(k_ij, n), (br, n1, n2)| { let nab = (*n1).value_into().unwrap() * (*n2).value_into().unwrap(); (k_ij + br.k_ij * nab, n + nab) }); - Ok(Self { k_ij: k_ij / n }) + Ok(Self { + k_ij: k_ij / n, + association: None, + }) } } impl std::fmt::Display for PcSaftBinaryRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "PcSaftBinaryRecord(k_ij={})", self.k_ij) + write!(f, "PcSaftBinaryRecord(k_ij={}", self.k_ij)?; + if let Some(association) = self.association { + if let Some(kappa_ab) = association.kappa_ab { + write!(f, ", kappa_ab={}", kappa_ab)?; + } + if let Some(epsilon_k_ab) = association.epsilon_k_ab { + write!(f, ", epsilon_k_ab={}", epsilon_k_ab)?; + } + } + write!(f, ")") } } @@ -302,7 +337,6 @@ pub struct PcSaftParameters { pub mu2: Array1, pub q2: Array1, pub association: AssociationParameters, - pub k_ij: Array2, pub sigma_ij: Array2, pub epsilon_k_ij: Array2, pub e_k_ij: Array2, @@ -314,7 +348,7 @@ pub struct PcSaftParameters { pub diffusion: Option>, pub thermal_conductivity: Option>, pub pure_records: Vec>, - pub binary_records: Array2, + pub binary_records: Option>, } impl Parameter for PcSaftParameters { @@ -323,7 +357,7 @@ impl Parameter for PcSaftParameters { fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { let n = pure_records.len(); @@ -374,19 +408,29 @@ impl Parameter for PcSaftParameters { .collect(); let nquadpole = quadpole_comp.len(); - let association = AssociationParameters::new(&association_records, &sigma, None); + let binary_association: Vec<_> = binary_records + .iter() + .flat_map(|r| { + r.indexed_iter() + .filter_map(|(i, record)| record.association.map(|r| (i, r))) + }) + .collect(); + let association = + AssociationParameters::new(&association_records, &sigma, &binary_association, None); - let k_ij = binary_records.map(|br| br.k_ij); - let mut epsilon_k_ij = Array::zeros((n, n)); + let k_ij = binary_records.as_ref().map(|br| br.map(|br| br.k_ij)); let mut sigma_ij = Array::zeros((n, n)); let mut e_k_ij = Array::zeros((n, n)); for i in 0..n { for j in 0..n { e_k_ij[[i, j]] = (epsilon_k[i] * epsilon_k[j]).sqrt(); - epsilon_k_ij[[i, j]] = (1.0 - k_ij[[i, j]]) * e_k_ij[[i, j]]; sigma_ij[[i, j]] = 0.5 * (sigma[i] + sigma[j]); } } + let mut epsilon_k_ij = e_k_ij.clone(); + if let Some(k_ij) = k_ij.as_ref() { + epsilon_k_ij *= &(1.0 - k_ij) + }; let viscosity_coefficients = if viscosity.iter().any(|v| v.is_none()) { None @@ -429,7 +473,6 @@ impl Parameter for PcSaftParameters { mu2, q2, association, - k_ij, sigma_ij, epsilon_k_ij, e_k_ij, @@ -445,8 +488,13 @@ impl Parameter for PcSaftParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.binary_records) + fn records( + &self, + ) -> ( + &[PureRecord], + Option<&Array2>, + ) { + (&self.pure_records, self.binary_records.as_ref()) } } @@ -735,7 +783,14 @@ pub mod utils { let binary_segment_records = kij .iter() .map(|&(id1, id2, k_ij)| { - BinaryRecord::new(id1.into(), id2.into(), PcSaftBinaryRecord { k_ij }) + BinaryRecord::new( + id1.into(), + id2.into(), + PcSaftBinaryRecord { + k_ij, + association: None, + }, + ) }) .collect(); let params = PcSaftParameters::from_segments( @@ -743,7 +798,7 @@ pub mod utils { segment_records, Some(binary_segment_records), )?; - let k_ij = ¶ms.binary_records; + let k_ij = params.binary_records.as_ref().unwrap(); assert_eq!(k_ij[[0, 0]].k_ij, 0.0); assert_eq!(k_ij[[0, 1]].k_ij, -0.5 / 9.); assert_eq!(k_ij[[1, 0]].k_ij, -0.5 / 9.); diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index b714962f8..90caa218d 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -6,7 +6,6 @@ use feos_core::parameter::{ }; use feos_core::python::parameter::*; use feos_core::*; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; @@ -137,6 +136,19 @@ impl_segment_record!(PcSaftRecord, PyPcSaftRecord); )] #[derive(Clone)] pub struct PyPcSaftBinaryRecord(PcSaftBinaryRecord); + +#[pymethods] +impl PyPcSaftBinaryRecord { + #[new] + fn new(k_ij: Option, kappa_ab: Option, epsilon_k_ab: Option) -> Self { + Self(PcSaftBinaryRecord::new(k_ij, kappa_ab, epsilon_k_ab)) + } + + fn __repr__(&self) -> PyResult { + Ok(self.0.to_string()) + } +} + impl_binary_record!(PcSaftBinaryRecord, PyPcSaftBinaryRecord); /// Create a set of PC-SAFT parameters from records. @@ -164,14 +176,22 @@ impl_binary_record!(PcSaftBinaryRecord, PyPcSaftBinaryRecord); #[derive(Clone)] pub struct PyPcSaftParameters(pub Arc); -impl_parameter!(PcSaftParameters, PyPcSaftParameters); +impl_parameter!( + PcSaftParameters, + PyPcSaftParameters, + PyPcSaftRecord, + PyPcSaftBinaryRecord +); impl_parameter_from_segments!(PcSaftParameters, PyPcSaftParameters); #[pymethods] impl PyPcSaftParameters { #[getter] - fn get_k_ij<'py>(&self, py: Python<'py>) -> &'py PyArray2 { - self.0.k_ij.view().to_pyarray(py) + fn get_k_ij<'py>(&self, py: Python<'py>) -> Option<&'py PyArray2> { + self.0 + .binary_records + .as_ref() + .map(|br| br.map(|br| br.k_ij).view().to_pyarray(py)) } fn _repr_markdown_(&self) -> String { @@ -187,6 +207,7 @@ pub fn pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 36391a037..7867402db 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -2,7 +2,6 @@ use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use feos_core::parameter::{Parameter, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; -use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::Write; @@ -104,7 +103,7 @@ pub struct PetsParameters { /// Lennard-Jones energy parameter in Kelvin pub epsilon_k: Array1, /// binary interaction parameter - pub k_ij: Array2, + pub k_ij: Option>, /// diameter matrix pub sigma_ij: Array2, /// energy parameter matrix including k_ij @@ -120,7 +119,7 @@ pub struct PetsParameters { /// records of all pure substances of the system pub pure_records: Vec>, /// records of all binary interaction parameters - pub binary_records: Array2, + pub binary_records: Option>, } impl Parameter for PetsParameters { @@ -129,7 +128,7 @@ impl Parameter for PetsParameters { fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { let n = pure_records.len(); @@ -153,17 +152,19 @@ impl Parameter for PetsParameters { molarweight[i] = record.molarweight; } - let k_ij = binary_records.map(|br| br.k_ij); - let mut epsilon_k_ij = Array::zeros((n, n)); + let k_ij = binary_records.as_ref().map(|br| br.map(|br| br.k_ij)); let mut sigma_ij = Array::zeros((n, n)); let mut e_k_ij = Array::zeros((n, n)); for i in 0..n { for j in 0..n { e_k_ij[[i, j]] = (epsilon_k[i] * epsilon_k[j]).sqrt(); - epsilon_k_ij[[i, j]] = (1.0 - k_ij[[i, j]]) * e_k_ij[[i, j]]; sigma_ij[[i, j]] = 0.5 * (sigma[i] + sigma[j]); } } + let mut epsilon_k_ij = e_k_ij.clone(); + if let Some(k_ij) = k_ij.as_ref() { + epsilon_k_ij *= &(1.0 - k_ij); + } let viscosity_coefficients = if viscosity.iter().any(|v| v.is_none()) { None @@ -212,8 +213,8 @@ impl Parameter for PetsParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.binary_records) + fn records(&self) -> (&[PureRecord], Option<&Array2>) { + (&self.pure_records, self.binary_records.as_ref()) } } @@ -260,8 +261,8 @@ impl std::fmt::Display for PetsParameters { write!(f, "\n\tmolarweight={}", self.molarweight)?; write!(f, "\n\tsigma={}", self.sigma)?; write!(f, "\n\tepsilon_k={}", self.epsilon_k)?; - if !self.k_ij.iter().all(|k| k.is_zero()) { - write!(f, "\n\tk_ij=\n{}", self.k_ij)?; + if let Some(k_ij) = self.k_ij.as_ref() { + write!(f, "\n\tk_ij=\n{}", k_ij)?; } write!(f, "\n)") } diff --git a/src/pets/python.rs b/src/pets/python.rs index 39a78a977..c7cec78e4 100644 --- a/src/pets/python.rs +++ b/src/pets/python.rs @@ -2,7 +2,6 @@ use super::parameters::*; use feos_core::parameter::*; use feos_core::python::parameter::*; use feos_core::{impl_binary_record, impl_json_handling, impl_parameter, impl_pure_record}; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::*; @@ -183,10 +182,7 @@ impl PyPetsParameters { }) .collect(); - let binary = match k_ij { - Some(v) => v.to_owned_array().mapv(f64::into), - None => Array2::from_shape_fn((n, n), |(_, _)| PetsBinaryRecord::from(0.0)), - }; + let binary = k_ij.map(|v| v.to_owned_array().mapv(f64::into)); Ok(Self(Arc::new(PetsParameters::from_records( pure_records, @@ -241,8 +237,8 @@ impl PyPetsParameters { } #[getter] - fn get_k_ij<'py>(&self, py: Python<'py>) -> &'py PyArray2 { - self.0.k_ij.view().to_pyarray(py) + fn get_k_ij<'py>(&self, py: Python<'py>) -> Option<&'py PyArray2> { + self.0.k_ij.as_ref().map(|k| k.view().to_pyarray(py)) } fn _repr_markdown_(&self) -> String { @@ -254,7 +250,12 @@ impl PyPetsParameters { } } -impl_parameter!(PetsParameters, PyPetsParameters); +impl_parameter!( + PetsParameters, + PyPetsParameters, + PyPetsRecord, + PyPetsBinaryRecord +); #[pymodule] pub fn pets(_py: Python<'_>, m: &PyModule) -> PyResult<()> { diff --git a/src/saftvrqmie/parameters.rs b/src/saftvrqmie/parameters.rs index 13b49eb7e..9f1e9696a 100644 --- a/src/saftvrqmie/parameters.rs +++ b/src/saftvrqmie/parameters.rs @@ -134,7 +134,7 @@ pub struct SaftVRQMieParameters { pub diffusion: Option>, pub thermal_conductivity: Option>, pub pure_records: Vec>, - pub binary_records: Array2, + pub binary_records: Option>, } impl Parameter for SaftVRQMieParameters { @@ -143,7 +143,7 @@ impl Parameter for SaftVRQMieParameters { fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { let n = pure_records.len(); @@ -173,8 +173,9 @@ impl Parameter for SaftVRQMieParameters { molarweight[i] = record.molarweight; } - let k_ij = binary_records.map(|br| br.k_ij); - let l_ij = binary_records.map(|br| br.l_ij); + let br = binary_records.as_ref(); + let k_ij = br.map_or_else(|| Array2::zeros([n; 2]), |br| br.mapv(|br| br.k_ij)); + let l_ij = br.map_or_else(|| Array2::zeros([n; 2]), |br| br.mapv(|br| br.l_ij)); let mut epsilon_k_ij = Array::zeros((n, n)); let mut sigma_ij = Array::zeros((n, n)); let mut e_k_ij = Array::zeros((n, n)); @@ -260,9 +261,9 @@ impl Parameter for SaftVRQMieParameters { &self, ) -> ( &[PureRecord], - &Array2, + Option<&Array2>, ) { - (&self.pure_records, &self.binary_records) + (&self.pure_records, self.binary_records.as_ref()) } } diff --git a/src/saftvrqmie/python.rs b/src/saftvrqmie/python.rs index 8063e4992..0665dc9ed 100644 --- a/src/saftvrqmie/python.rs +++ b/src/saftvrqmie/python.rs @@ -8,7 +8,6 @@ use feos_core::parameter::{ }; use feos_core::python::parameter::PyIdentifier; use feos_core::*; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::{PyIOError, PyTypeError}; use pyo3::prelude::*; @@ -147,7 +146,12 @@ pub struct PySaftVRQMieParameters(pub Arc); impl_json_handling!(PySaftVRQMieRecord); impl_pure_record!(SaftVRQMieRecord, PySaftVRQMieRecord); impl_binary_record!(SaftVRQMieBinaryRecord, PySaftVRQMieBinaryRecord); -impl_parameter!(SaftVRQMieParameters, PySaftVRQMieParameters); +impl_parameter!( + SaftVRQMieParameters, + PySaftVRQMieParameters, + PySaftVRQMieRecord, + PySaftVRQMieBinaryRecord +); #[pymethods] impl PySaftVRQMieParameters { @@ -206,7 +210,7 @@ impl PySaftVRQMieParameters { ) -> PyResult<()> { self.0 .lammps_tables(temperature.into(), n, r_min.into(), r_max.into()) - .map_err(|e| PyIOError::new_err(e)) + .map_err(PyIOError::new_err) } fn _repr_markdown_(&self) -> String { diff --git a/src/uvtheory/parameters.rs b/src/uvtheory/parameters.rs index e6dbd1fec..901794469 100644 --- a/src/uvtheory/parameters.rs +++ b/src/uvtheory/parameters.rs @@ -111,7 +111,7 @@ pub struct UVParameters { pub sigma: Array1, pub epsilon_k: Array1, pub molarweight: Array1, - pub k_ij: Array2, + pub k_ij: Option>, pub rep_ij: Array2, pub att_ij: Array2, pub sigma_ij: Array2, @@ -119,7 +119,7 @@ pub struct UVParameters { pub cd_bh_pure: Vec>, pub cd_bh_binary: Array2>, pub pure_records: Vec>, - pub binary_records: Array2, + pub binary_records: Option>, } impl Parameter for UVParameters { @@ -128,7 +128,7 @@ impl Parameter for UVParameters { fn from_records( pure_records: Vec>, - binary_records: Array2, + binary_records: Option>, ) -> Self { let n = pure_records.len(); @@ -154,7 +154,7 @@ impl Parameter for UVParameters { let mut att_ij = Array2::zeros((n, n)); let mut sigma_ij = Array2::zeros((n, n)); let mut eps_k_ij = Array2::zeros((n, n)); - let k_ij = binary_records.map(|br| br.k_ij); + let k_ij = binary_records.as_ref().map(|br| br.map(|br| br.k_ij)); for i in 0..n { rep_ij[[i, i]] = rep[i]; @@ -168,7 +168,8 @@ impl Parameter for UVParameters { att_ij[[j, i]] = att_ij[[i, j]]; sigma_ij[[i, j]] = 0.5 * (sigma[i] + sigma[j]); sigma_ij[[j, i]] = sigma_ij[[i, j]]; - eps_k_ij[[i, j]] = (1.0 - k_ij[[i, j]]) * (epsilon_k[i] * epsilon_k[j]).sqrt(); + eps_k_ij[[i, j]] = (1.0 - k_ij.as_ref().map_or(0.0, |k_ij| k_ij[[i, j]])) + * (epsilon_k[i] * epsilon_k[j]).sqrt(); eps_k_ij[[j, i]] = eps_k_ij[[i, j]]; } } @@ -197,8 +198,8 @@ impl Parameter for UVParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { - (&self.pure_records, &self.binary_records) + fn records(&self) -> (&[PureRecord], Option<&Array2>) { + (&self.pure_records, self.binary_records.as_ref()) } } diff --git a/src/uvtheory/python.rs b/src/uvtheory/python.rs index 04a4425e0..406045782 100644 --- a/src/uvtheory/python.rs +++ b/src/uvtheory/python.rs @@ -5,7 +5,6 @@ use feos_core::parameter::{ }; use feos_core::python::parameter::*; use feos_core::*; -use ndarray::Array2; use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; @@ -97,8 +96,7 @@ impl PyUVParameters { PureRecord::new(identifier, 1.0, model_record) }) .collect(); - let binary = Array2::from_shape_fn((n, n), |(_, _)| UVBinaryRecord { k_ij: 0.0 }); - Self(Arc::new(UVParameters::from_records(pure_records, binary))) + Self(Arc::new(UVParameters::from_records(pure_records, None))) } /// Create UV Theory parameters for pure substance. @@ -131,7 +129,7 @@ impl PyUVParameters { } impl_pure_record!(UVRecord, PyUVRecord); -impl_parameter!(UVParameters, PyUVParameters); +impl_parameter!(UVParameters, PyUVParameters, PyUVRecord, PyUVBinaryRecord); #[pymodule] pub fn uvtheory(_py: Python<'_>, m: &PyModule) -> PyResult<()> { From 56281ad8dbfe154e16fe041206e4200ddec19271 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:27:02 +0200 Subject: [PATCH 7/7] Serialization and documentation of binary association records (#171) --- src/association/mod.rs | 12 ++++++++++++ src/pcsaft/parameters.rs | 18 ++++++++++++++---- src/pcsaft/python.rs | 28 +++------------------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/association/mod.rs b/src/association/mod.rs index 86b0ffeba..06da46f9f 100644 --- a/src/association/mod.rs +++ b/src/association/mod.rs @@ -93,13 +93,25 @@ impl fmt::Display for AssociationRecord { } } +/// Binary association parameters. #[derive(Serialize, Deserialize, Clone, Copy)] pub struct BinaryAssociationRecord { + /// Cross-association association volume parameter. + #[serde(skip_serializing_if = "Option::is_none")] pub kappa_ab: Option, + /// Cross-association energy parameter. + #[serde(skip_serializing_if = "Option::is_none")] pub epsilon_k_ab: Option, + /// Indices of sites that the record refers to. + #[serde(skip_serializing_if = "is_default_site_indices")] + #[serde(default)] pub site_indices: [usize; 2], } +fn is_default_site_indices([i, j]: &[usize; 2]) -> bool { + *i == 0 && *j == 0 +} + impl BinaryAssociationRecord { pub fn new( kappa_ab: Option, diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 2738b61ef..fd6a98ad5 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -6,6 +6,7 @@ use feos_core::parameter::{ }; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; +use num_traits::Zero; use quantity::si::{JOULE, KB, KELVIN}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -265,9 +266,15 @@ impl PcSaftRecord { } } +/// PC-SAFT binary interaction parameters. #[derive(Serialize, Deserialize, Clone, Default)] pub struct PcSaftBinaryRecord { + /// Binary dispersion interaction parameter + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] pub k_ij: f64, + /// Binary association parameters + #[serde(flatten)] association: Option, } @@ -313,16 +320,19 @@ impl> FromSegmentsBinary for PcSaftBinaryRecord { impl std::fmt::Display for PcSaftBinaryRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "PcSaftBinaryRecord(k_ij={}", self.k_ij)?; + let mut tokens = vec![]; + if !self.k_ij.is_zero() { + tokens.push(format!("k_ij={}", self.k_ij)); + } if let Some(association) = self.association { if let Some(kappa_ab) = association.kappa_ab { - write!(f, ", kappa_ab={}", kappa_ab)?; + tokens.push(format!("kappa_ab={}", kappa_ab)); } if let Some(epsilon_k_ab) = association.epsilon_k_ab { - write!(f, ", epsilon_k_ab={}", epsilon_k_ab)?; + tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); } } - write!(f, ")") + write!(f, "PcSaftBinaryRecord({})", tokens.join(", ")) } } diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 90caa218d..089f215d3 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -12,7 +12,7 @@ use pyo3::prelude::*; use std::convert::{TryFrom, TryInto}; use std::sync::Arc; -/// Create a set of PC-Saft parameters from records. +/// Create a new set of PC-SAFT pure component parameters. #[pyclass(name = "PcSaftRecord")] #[pyo3( text_signature = "(m, sigma, epsilon_k, mu=None, q=None, kappa_ab=None, epsilon_k_ab=None, na=None, nb=None, viscosity=None, diffusion=None, thermal_conductivity=None)" @@ -131,9 +131,6 @@ impl_pure_record!(PcSaftRecord, PyPcSaftRecord); impl_segment_record!(PcSaftRecord, PyPcSaftRecord); #[pyclass(name = "PcSaftBinaryRecord")] -#[pyo3( - text_signature = "(pure_records, binary_records=None, substances=None, search_option='Name')" -)] #[derive(Clone)] pub struct PyPcSaftBinaryRecord(PcSaftBinaryRecord); @@ -149,30 +146,11 @@ impl PyPcSaftBinaryRecord { } } +impl_json_handling!(PyPcSaftBinaryRecord); + impl_binary_record!(PcSaftBinaryRecord, PyPcSaftBinaryRecord); -/// Create a set of PC-SAFT parameters from records. -/// -/// Parameters -/// ---------- -/// pure_records : List[PureRecord] -/// pure substance records. -/// binary_records : List[BinaryRecord], optional -/// binary saft parameter records -/// substances : List[str], optional -/// The substances to use. Filters substances from `pure_records` according to -/// `search_option`. -/// When not provided, all entries of `pure_records` are used. -/// search_option : {'Name', 'Cas', 'Inchi', 'IupacName', 'Formula', 'Smiles'}, optional, defaults to 'Name'. -/// Identifier that is used to search substance. -/// -/// Returns -/// ------- -/// PcSaftParameters #[pyclass(name = "PcSaftParameters")] -#[pyo3( - text_signature = "(pure_records, binary_records=None, substances=None, search_option='Name')" -)] #[derive(Clone)] pub struct PyPcSaftParameters(pub Arc);