From a0fcb6f26c7630973af2b2acca1254ff28970ed8 Mon Sep 17 00:00:00 2001 From: Ian Bell <ian.bell@nist.gov> Date: Fri, 28 Apr 2023 16:50:04 -0400 Subject: [PATCH] Implementation of Gubbins and Twu + Luckas Appears to be working, additional testing required. Generalizing to handle G&V and G&T correlation integrals remains --- include/teqp/models/pcsaft.hpp | 258 +--------- .../models/saft/correlation_integrals.hpp | 18 +- include/teqp/models/saft/polar_terms.hpp | 447 ++++++++++++++++++ include/teqp/models/saftvrmie.hpp | 15 +- src/tests/catch_test_SAFTpolar.cxx | 43 +- 5 files changed, 511 insertions(+), 270 deletions(-) create mode 100644 include/teqp/models/saft/polar_terms.hpp diff --git a/include/teqp/models/pcsaft.hpp b/include/teqp/models/pcsaft.hpp index cec7537..324c134 100644 --- a/include/teqp/models/pcsaft.hpp +++ b/include/teqp/models/pcsaft.hpp @@ -10,6 +10,7 @@ #include "teqp/types.hpp" #include "teqp/exceptions.hpp" #include "teqp/constants.hpp" +#include "teqp/models/saft/polar_terms.hpp" #include <optional> namespace teqp { @@ -146,74 +147,6 @@ auto get_I2(const Eta& eta, MbarType mbar) { return std::make_tuple(forceeval(summer_I2), forceeval(summer_etadI2deta)); } -/// Eq. 10 from Gross and Vrabec -template <typename Eta, typename MType, typename TType> -auto get_JDD_2ij(const Eta& eta, const MType& mij, const TType& Tstarij) { - static Eigen::ArrayXd a_0 = (Eigen::ArrayXd(5) << 0.3043504, -0.1358588, 1.4493329, 0.3556977, -2.0653308).finished(); - static Eigen::ArrayXd a_1 = (Eigen::ArrayXd(5) << 0.9534641, -1.8396383, 2.0131180, -7.3724958, 8.2374135).finished(); - static Eigen::ArrayXd a_2 = (Eigen::ArrayXd(5) << -1.1610080, 4.5258607, 0.9751222, -12.281038, 5.9397575).finished(); - - static Eigen::ArrayXd b_0 = (Eigen::ArrayXd(5) << 0.2187939, -1.1896431, 1.1626889, 0, 0).finished(); - static Eigen::ArrayXd b_1 = (Eigen::ArrayXd(5) << -0.5873164, 1.2489132, -0.5085280, 0, 0).finished(); - static Eigen::ArrayXd b_2 = (Eigen::ArrayXd(5) << 3.4869576, -14.915974, 15.372022, 0, 0).finished(); - - std::common_type_t<Eta, MType> summer = 0.0; - for (auto n = 0; n < 5; ++n){ - auto anij = a_0[n] + (mij-1)/mij*a_1[n] + (mij-1)/mij*(mij-2)/mij*a_2[n]; // Eq. 12 - auto bnij = b_0[n] + (mij-1)/mij*b_1[n] + (mij-1)/mij*(mij-2)/mij*b_2[n]; // Eq. 13 - summer += (anij + bnij/Tstarij)*pow(eta, n); - } - return forceeval(summer); -} - -/// Eq. 11 from Gross and Vrabec -template <typename Eta, typename MType> -auto get_JDD_3ijk(const Eta& eta, const MType& mijk) { - static Eigen::ArrayXd c_0 = (Eigen::ArrayXd(5) << -0.0646774, 0.1975882, -0.8087562, 0.6902849, 0.0).finished(); - static Eigen::ArrayXd c_1 = (Eigen::ArrayXd(5) << -0.9520876, 2.9924258, -2.3802636, -0.2701261, 0.0).finished(); - static Eigen::ArrayXd c_2 = (Eigen::ArrayXd(5) << -0.6260979, 1.2924686, 1.6542783, -3.4396744, 0.0).finished(); - std::common_type_t<Eta, MType> summer = 0.0; - for (auto n = 0; n < 5; ++n){ - auto cnijk = c_0[n] + (mijk-1)/mijk*c_1[n] + (mijk-1)/mijk*(mijk-2)/mijk*c_2[n]; // Eq. 14 - summer += cnijk*pow(eta, n); - } - return forceeval(summer); -} - -/// Eq. 12 from Gross and Vrabec, AICHEJ -template <typename Eta, typename MType, typename TType> -auto get_JQQ_2ij(const Eta& eta, const MType& mij, const TType& Tstarij) { - static Eigen::ArrayXd a_0 = (Eigen::ArrayXd(5) << 1.2378308, 2.4355031, 1.6330905, -1.6118152, 6.9771185).finished(); - static Eigen::ArrayXd a_1 = (Eigen::ArrayXd(5) << 1.2854109, -11.465615, 22.086893, 7.4691383, -17.197772).finished(); - static Eigen::ArrayXd a_2 = (Eigen::ArrayXd(5) << 1.7942954, 0.7695103, 7.2647923, 94.486699, -77.148458).finished(); - - static Eigen::ArrayXd b_0 = (Eigen::ArrayXd(5) << 0.4542718, -4.5016264, 3.5858868, 0.0, 0.0).finished(); - static Eigen::ArrayXd b_1 = (Eigen::ArrayXd(5) << -0.8137340, 10.064030, -10.876631, 0.0, 0.0).finished(); - static Eigen::ArrayXd b_2 = (Eigen::ArrayXd(5) << 6.8682675, -5.1732238, -17.240207, 0.0, 0.0).finished(); - - std::common_type_t<Eta, MType> summer = 0.0; - for (auto n = 0; n < 5; ++n){ - auto anij = a_0[n] + (mij-1)/mij*a_1[n] + (mij-1)/mij*(mij-2)/mij*a_2[n]; // Eq. 12 - auto bnij = b_0[n] + (mij-1)/mij*b_1[n] + (mij-1)/mij*(mij-2)/mij*b_2[n]; // Eq. 13 - summer += (anij + bnij/Tstarij)*pow(eta, n); - } - return forceeval(summer); -} - -/// Eq. 13 from Gross and Vrabec, AICHEJ -template <typename Eta, typename MType> -auto get_JQQ_3ijk(const Eta& eta, const MType& mijk) { - static Eigen::ArrayXd c_0 = (Eigen::ArrayXd(5) << 0.5000437, 6.5318692, -16.014780, 14.425970, 0.0).finished(); - static Eigen::ArrayXd c_1 = (Eigen::ArrayXd(5) << 2.0002094, -6.7838658, 20.383246, -10.895984, 0.0).finished(); - static Eigen::ArrayXd c_2 = (Eigen::ArrayXd(5) << 3.1358271, 7.2475888, 3.0759478, 0.0, 0.0).finished(); - std::common_type_t<Eta, MType> summer = 0.0; - for (auto n = 0; n < 5; ++n){ - auto cnijk = c_0[n] + (mijk-1)/mijk*c_1[n] + (mijk-1)/mijk*(mijk-2)/mijk*c_2[n]; // Eq. 14 - summer += cnijk*pow(eta, n); - } - return forceeval(summer); -} - /** Sum up three array-like objects that can each have different container types and value types */ @@ -337,197 +270,16 @@ public: } }; -/*** - * \brief The dipolar contribution given in Gross and Vrabec - */ -class PCSAFTDipolarContribution { -private: - const Eigen::ArrayXd m, sigma_Angstrom, epsilon_over_k, mustar2, nmu; - template<typename A> auto POW2(const A& x) const { return forceeval(x*x); } - template<typename A> auto POW3(const A& x) const { return forceeval(POW2(x)*x); } -public: - PCSAFTDipolarContribution(const Eigen::ArrayX<double> &m, const Eigen::ArrayX<double> &sigma_Angstrom, const Eigen::ArrayX<double> &epsilon_over_k, const Eigen::ArrayX<double> &mustar2, const Eigen::ArrayX<double> &nmu) : m(m), sigma_Angstrom(sigma_Angstrom), epsilon_over_k(epsilon_over_k), mustar2(mustar2), nmu(nmu) { - // Check lengths match - if (m.size() != mustar2.size()){ - throw teqp::InvalidArgument("bad size of mustar2"); - } - if (m.size() != nmu.size()){ - throw teqp::InvalidArgument("bad size of n"); - } - } - - /// Eq. 8 from Gross and Vrabec - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto get_alpha2DD(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ - const auto& x = mole_fractions; // concision - const auto& sigma = sigma_Angstrom; // concision - const auto N = mole_fractions.size(); - std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; - for (auto i = 0; i < N; ++i){ - for (auto j = 0; j < N; ++j){ - auto ninj = nmu[i]*nmu[j]; - if (ninj > 0){ - // Lorentz-Berthelot mixing rules - auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); - auto sigmaij = (sigma[i]+sigma[j])/2; - - auto Tstarij = T/epskij; - auto mij = std::max(sqrt(m[i]*m[j]), 2); - summer += x[i]*x[j]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*POW3(sigma[i]*sigma[j]/sigmaij)*ninj*mustar2[i]*mustar2[j]*get_JDD_2ij(eta, mij, Tstarij); - } - } - } - return forceeval(-static_cast<double>(EIGEN_PI)*rhoN_A3*summer); - } - - /// Eq. 9 from Gross and Vrabec - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto get_alpha3DD(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ - const auto& x = mole_fractions; // concision - const auto& sigma = sigma_Angstrom; // concision - const auto N = mole_fractions.size(); - std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; - for (auto i = 0; i < N; ++i){ - for (auto j = 0; j < N; ++j){ - for (auto k = 0; k < N; ++k){ - auto ninjnk = nmu[i]*nmu[j]*nmu[k]; - if (ninjnk > 0){ - // Lorentz-Berthelot mixing rules for sigma - auto sigmaij = (sigma[i]+sigma[j])/2; - auto sigmaik = (sigma[i]+sigma[k])/2; - auto sigmajk = (sigma[j]+sigma[k])/2; - - auto mijk = std::max(pow(m[i]*m[j]*m[k], 1.0/3.0), 2.0); - summer += x[i]*x[j]*x[k]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*epsilon_over_k[k]/T*POW3(sigma[i]*sigma[j]*sigma[k])/(sigmaij*sigmaik*sigmajk)*ninjnk*mustar2[i]*mustar2[j]*mustar2[k]*get_JDD_3ijk(eta, mijk); - } - } - } - } - return forceeval(-4.0*POW2(static_cast<double>(EIGEN_PI))/3.0*POW2(rhoN_A3)*summer); - } - - /*** - * \brief Get the dipolar contribution to \f$ \alpha = A/(NkT) \f$ - */ - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto eval(const TTYPE& T, const RhoType& rho_A3, const EtaType& eta, const VecType& mole_fractions) const { - auto alpha2 = get_alpha2DD(T, rho_A3, eta, mole_fractions); - auto alpha3 = get_alpha3DD(T, rho_A3, eta, mole_fractions); - auto alpha = forceeval(alpha2/(1.0-alpha3/alpha2)); - - using alpha2_t = decltype(alpha2); - using alpha3_t = decltype(alpha3); - using alpha_t = decltype(alpha); - struct PCSAFTDipolarContributionTerms{ - alpha2_t alpha2; - alpha3_t alpha3; - alpha_t alpha; - }; - return PCSAFTDipolarContributionTerms{alpha2, alpha3, alpha}; - } -}; - -/*** - * \brief The quadrupolar contribution from Gross and Vrabec - * - */ -class PCSAFTQuadrupolarContribution { -private: - const Eigen::ArrayXd m, sigma_Angstrom, epsilon_over_k, Qstar2, nQ; - template<typename A> auto POW2(const A& x) const { return forceeval(x*x); } - template<typename A> auto POW3(const A& x) const { return forceeval(POW2(x)*x); } - template<typename A> auto POW5(const A& x) const { return forceeval(POW2(x)*POW3(x)); } - template<typename A> auto POW7(const A& x) const { return forceeval(POW2(x)*POW5(x)); } -public: - PCSAFTQuadrupolarContribution(const Eigen::ArrayX<double> &m, const Eigen::ArrayX<double> &sigma_Angstrom, const Eigen::ArrayX<double> &epsilon_over_k, const Eigen::ArrayX<double> &Qstar2, const Eigen::ArrayX<double> &nQ) : m(m), sigma_Angstrom(sigma_Angstrom), epsilon_over_k(epsilon_over_k), Qstar2(Qstar2), nQ(nQ) { - // Check lengths match - if (m.size() != Qstar2.size()){ - throw teqp::InvalidArgument("bad size of mustar2"); - } - if (m.size() != nQ.size()){ - throw teqp::InvalidArgument("bad size of n"); - } - } - PCSAFTQuadrupolarContribution& operator=( const PCSAFTQuadrupolarContribution& ) = delete; // non copyable - - /// Eq. 9 from Gross and Vrabec - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto get_alpha2QQ(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ - const auto& x = mole_fractions; // concision - const auto& sigma = sigma_Angstrom; // concision - const auto N = mole_fractions.size(); - std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; - for (auto i = 0; i < N; ++i){ - for (auto j = 0; j < N; ++j){ - auto ninj = nQ[i]*nQ[j]; - if (ninj > 0){ - // Lorentz-Berthelot mixing rules - auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); - auto sigmaij = (sigma[i]+sigma[j])/2; - - auto Tstarij = T/epskij; - auto mij = sqrt(m[i]*m[j]); - summer += x[i]*x[j]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*POW5(sigma[i]*sigma[j])/POW7(sigmaij)*ninj*Qstar2[i]*Qstar2[j]*get_JQQ_2ij(eta, mij, Tstarij); - } - } - } - return forceeval(-static_cast<double>(EIGEN_PI)*POW2(3.0/4.0)*rhoN_A3*summer); - } - - /// Eq. 1 from Gross and Vrabec - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto get_alpha3QQ(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ - const auto& x = mole_fractions; // concision - const auto& sigma = sigma_Angstrom; // concision - const auto N = mole_fractions.size(); - std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; - for (auto i = 0; i < N; ++i){ - for (auto j = 0; j < N; ++j){ - for (auto k = 0; k < N; ++k){ - auto ninjnk = nQ[i]*nQ[j]*nQ[k]; - if (ninjnk > 0){ - // Lorentz-Berthelot mixing rules for sigma - auto sigmaij = (sigma[i]+sigma[j])/2; - auto sigmaik = (sigma[i]+sigma[k])/2; - auto sigmajk = (sigma[j]+sigma[k])/2; - - auto mijk = pow(m[i]*m[j]*m[k], 1.0/3.0); - summer += x[i]*x[j]*x[k]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*epsilon_over_k[k]/T*POW5(sigma[i]*sigma[j]*sigma[k])/POW3(sigmaij*sigmaik*sigmajk)*ninjnk*Qstar2[i]*Qstar2[j]*Qstar2[k]*get_JDD_3ijk(eta, mijk); - } - } - } - } - return forceeval(-4.0*POW2(static_cast<double>(EIGEN_PI))/3.0*POW3(3.0/4.0)*POW2(rhoN_A3)*summer); - } - - /*** - * \brief Get the quadrupolar contribution to \f$ \alpha = A/(NkT) \f$ - */ - template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> - auto eval(const TTYPE& T, const RhoType& rho_A3, const EtaType& eta, const VecType& mole_fractions) const { - auto alpha2 = get_alpha2QQ(T, rho_A3, eta, mole_fractions); - auto alpha3 = get_alpha3QQ(T, rho_A3, eta, mole_fractions); - auto alpha = forceeval(alpha2/(1.0-alpha3/alpha2)); - - using alpha2_t = decltype(alpha2); - using alpha3_t = decltype(alpha3); - using alpha_t = decltype(alpha); - struct PCSAFTQuadrupolarContributionTerms{ - alpha2_t alpha2; - alpha3_t alpha3; - alpha_t alpha; - }; - return PCSAFTQuadrupolarContributionTerms{alpha2, alpha3, alpha}; - } -}; - /** A class used to evaluate mixtures using PC-SAFT model This is the classical Gross and Sadowski model from 2001: https://doi.org/10.1021/ie0003887 -with the error fixed as noted in a comment: https://doi.org/10.1021/acs.iecr.9b01515 +with the errors fixed as noted in a comment: https://doi.org/10.1021/acs.iecr.9b01515 */ class PCSAFTMixture { +public: + using PCSAFTDipolarContribution = SAFTpolar::DipolarContributionGrossVrabec; + using PCSAFTQuadrupolarContribution = SAFTpolar::QuadrupolarContributionGrossVrabec; protected: Eigen::ArrayX<double> m, ///< number of segments mminus1, ///< m-1 diff --git a/include/teqp/models/saft/correlation_integrals.hpp b/include/teqp/models/saft/correlation_integrals.hpp index d2fa92c..6529a1c 100644 --- a/include/teqp/models/saft/correlation_integrals.hpp +++ b/include/teqp/models/saft/correlation_integrals.hpp @@ -21,14 +21,14 @@ static const std::map<int, std::array<double, 12>> Luckas_J_coeffs = { {15, {-2.14335965e-01, 2.24240261e-02, 2.68094773e-01, 8.11899188e00, 2.15506735e-01, -8.11465705e00, -1.88310645e01, -4.18309476e-01, 1.88679367e01, 1.02033085e01, 2.37674032e-01, -1.00120648e01}}, }; -template<int n> + class LuckasJIntegral{ public: - + const int n; const std::array<double, 12> a; double a00,a01,a02,a10,a11,a12,a20,a21,a22,a30,a31,a32; - LuckasJIntegral() : a(Luckas_J_coeffs.at(n)){ + LuckasJIntegral(int n) : n(n), a(Luckas_J_coeffs.at(n)){ a00 = a[0]; a01 = a[1]; a02 = a[2]; a10 = a[3]; a11 = a[4]; a12 = a[5]; a20 = a[6]; a21 = a[7]; a22 = a[8]; @@ -36,13 +36,15 @@ public: } template<typename TType, typename RhoType> - auto get_J(const TType& Tstar, const RhoType& rhostar){ + auto get_J(const TType& Tstar, const RhoType& rhostar) const{ auto Z_1 = 0.3 + 0.05*n; auto Z_2 = 1.0/n; auto A_0 = a00 + a10*rhostar + a20*rhostar*rhostar + a30*rhostar*rhostar*rhostar; auto A_1 = a01 + a11*rhostar + a21*rhostar*rhostar + a31*rhostar*rhostar*rhostar; auto A_2 = a02 + a12*rhostar + a22*rhostar*rhostar + a32*rhostar*rhostar*rhostar; - std::common_type_t<TType, RhoType> out = (A_0 + A_1*pow(Tstar, Z_1) + A_2*pow(Tstar, Z_2))*exp(1.0/(Tstar + 4.0/pow(std::abs(log(rhostar/sqrt(2.0))), 3.0))); + // |x| = sqrt(x^2), the latter is well-suited to differentiation + auto differentiable_abs = [](const auto& x){ return sqrt(x*x); }; + std::common_type_t<TType, RhoType> out = (A_0 + A_1*pow(Tstar, Z_1) + A_2*pow(Tstar, Z_2))*exp(1.0/(Tstar + 4.0/pow(differentiable_abs(log(rhostar/sqrt(2.0))), 3.0))); return out; } }; @@ -54,14 +56,14 @@ static const std::map<std::tuple<int, int>, std::array<double, 16>> Luckas_K_coe {{444,555},{ 1.07862851e-03, 2.88647260e-04, 2.07369614e-03, 1.79600129e-04, 7.57084957e-02, -2.28761197e-03, -7.33670248e-02, 3.12736172e-02, 1.73518964e-01, 4.54174801e-03, -5.14488122e-01, 2.85483011e-01, 1.71670939e-01, -1.07503182e-03, -5.51880129e-01, 4.16887229e-01}} }; -template<int n1, int n2> class LuckasKIntegral{ public: + const int n1, n2; const std::array<double, 16> a; double a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33; - LuckasKIntegral() : a(Luckas_K_coeffs.at({n1, n2})){ + LuckasKIntegral(const int n1, const int n2) : n1(n1), n2(n2), a(Luckas_K_coeffs.at({n1, n2})){ a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; @@ -69,7 +71,7 @@ public: } template<typename TType, typename RhoType> - auto get_K(const TType& Tstar, const RhoType& rhostar){ + auto get_K(const TType& Tstar, const RhoType& rhostar) const{ auto Z_1 = 2.0; auto Z_2 = 3.0; auto Z_3 = 4.0; diff --git a/include/teqp/models/saft/polar_terms.hpp b/include/teqp/models/saft/polar_terms.hpp new file mode 100644 index 0000000..173a2ce --- /dev/null +++ b/include/teqp/models/saft/polar_terms.hpp @@ -0,0 +1,447 @@ +#pragma once + +/** + This header contains methods that pertain to polar contributions to SAFT models + + Initially the contribution of Gross and Vrabec were implemented for PC-SAFT, but they can be used with other + non-polar base models as well, so this header collects all the things in one place + */ + +#include "teqp/types.hpp" +#include "teqp/exceptions.hpp" +#include "correlation_integrals.hpp" +#include <Eigen/Dense> + +namespace teqp{ + +namespace SAFTpolar{ + +/// Eq. 10 from Gross and Vrabec +template <typename Eta, typename MType, typename TType> +auto get_JDD_2ij(const Eta& eta, const MType& mij, const TType& Tstarij) { + static Eigen::ArrayXd a_0 = (Eigen::ArrayXd(5) << 0.3043504, -0.1358588, 1.4493329, 0.3556977, -2.0653308).finished(); + static Eigen::ArrayXd a_1 = (Eigen::ArrayXd(5) << 0.9534641, -1.8396383, 2.0131180, -7.3724958, 8.2374135).finished(); + static Eigen::ArrayXd a_2 = (Eigen::ArrayXd(5) << -1.1610080, 4.5258607, 0.9751222, -12.281038, 5.9397575).finished(); + + static Eigen::ArrayXd b_0 = (Eigen::ArrayXd(5) << 0.2187939, -1.1896431, 1.1626889, 0, 0).finished(); + static Eigen::ArrayXd b_1 = (Eigen::ArrayXd(5) << -0.5873164, 1.2489132, -0.5085280, 0, 0).finished(); + static Eigen::ArrayXd b_2 = (Eigen::ArrayXd(5) << 3.4869576, -14.915974, 15.372022, 0, 0).finished(); + + std::common_type_t<Eta, MType> summer = 0.0; + for (auto n = 0; n < 5; ++n){ + auto anij = a_0[n] + (mij-1)/mij*a_1[n] + (mij-1)/mij*(mij-2)/mij*a_2[n]; // Eq. 12 + auto bnij = b_0[n] + (mij-1)/mij*b_1[n] + (mij-1)/mij*(mij-2)/mij*b_2[n]; // Eq. 13 + summer += (anij + bnij/Tstarij)*pow(eta, n); + } + return forceeval(summer); +} + +/// Eq. 11 from Gross and Vrabec +template <typename Eta, typename MType> +auto get_JDD_3ijk(const Eta& eta, const MType& mijk) { + static Eigen::ArrayXd c_0 = (Eigen::ArrayXd(5) << -0.0646774, 0.1975882, -0.8087562, 0.6902849, 0.0).finished(); + static Eigen::ArrayXd c_1 = (Eigen::ArrayXd(5) << -0.9520876, 2.9924258, -2.3802636, -0.2701261, 0.0).finished(); + static Eigen::ArrayXd c_2 = (Eigen::ArrayXd(5) << -0.6260979, 1.2924686, 1.6542783, -3.4396744, 0.0).finished(); + std::common_type_t<Eta, MType> summer = 0.0; + for (auto n = 0; n < 5; ++n){ + auto cnijk = c_0[n] + (mijk-1)/mijk*c_1[n] + (mijk-1)/mijk*(mijk-2)/mijk*c_2[n]; // Eq. 14 + summer += cnijk*pow(eta, n); + } + return forceeval(summer); +} + +/// Eq. 12 from Gross and Vrabec, AICHEJ +template <typename Eta, typename MType, typename TType> +auto get_JQQ_2ij(const Eta& eta, const MType& mij, const TType& Tstarij) { + static Eigen::ArrayXd a_0 = (Eigen::ArrayXd(5) << 1.2378308, 2.4355031, 1.6330905, -1.6118152, 6.9771185).finished(); + static Eigen::ArrayXd a_1 = (Eigen::ArrayXd(5) << 1.2854109, -11.465615, 22.086893, 7.4691383, -17.197772).finished(); + static Eigen::ArrayXd a_2 = (Eigen::ArrayXd(5) << 1.7942954, 0.7695103, 7.2647923, 94.486699, -77.148458).finished(); + + static Eigen::ArrayXd b_0 = (Eigen::ArrayXd(5) << 0.4542718, -4.5016264, 3.5858868, 0.0, 0.0).finished(); + static Eigen::ArrayXd b_1 = (Eigen::ArrayXd(5) << -0.8137340, 10.064030, -10.876631, 0.0, 0.0).finished(); + static Eigen::ArrayXd b_2 = (Eigen::ArrayXd(5) << 6.8682675, -5.1732238, -17.240207, 0.0, 0.0).finished(); + + std::common_type_t<Eta, MType> summer = 0.0; + for (auto n = 0; n < 5; ++n){ + auto anij = a_0[n] + (mij-1)/mij*a_1[n] + (mij-1)/mij*(mij-2)/mij*a_2[n]; // Eq. 12 + auto bnij = b_0[n] + (mij-1)/mij*b_1[n] + (mij-1)/mij*(mij-2)/mij*b_2[n]; // Eq. 13 + summer += (anij + bnij/Tstarij)*pow(eta, n); + } + return forceeval(summer); +} + +/// Eq. 13 from Gross and Vrabec, AICHEJ +template <typename Eta, typename MType> +auto get_JQQ_3ijk(const Eta& eta, const MType& mijk) { + static Eigen::ArrayXd c_0 = (Eigen::ArrayXd(5) << 0.5000437, 6.5318692, -16.014780, 14.425970, 0.0).finished(); + static Eigen::ArrayXd c_1 = (Eigen::ArrayXd(5) << 2.0002094, -6.7838658, 20.383246, -10.895984, 0.0).finished(); + static Eigen::ArrayXd c_2 = (Eigen::ArrayXd(5) << 3.1358271, 7.2475888, 3.0759478, 0.0, 0.0).finished(); + std::common_type_t<Eta, MType> summer = 0.0; + for (auto n = 0; n < 5; ++n){ + auto cnijk = c_0[n] + (mijk-1)/mijk*c_1[n] + (mijk-1)/mijk*(mijk-2)/mijk*c_2[n]; // Eq. 14 + summer += cnijk*pow(eta, n); + } + return forceeval(summer); +} + +/*** + * \brief The dipolar contribution given in Gross and Vrabec + */ +class DipolarContributionGrossVrabec { +private: + const Eigen::ArrayXd m, sigma_Angstrom, epsilon_over_k, mustar2, nmu; + template<typename A> auto POW2(const A& x) const { return forceeval(x*x); } + template<typename A> auto POW3(const A& x) const { return forceeval(POW2(x)*x); } +public: + DipolarContributionGrossVrabec(const Eigen::ArrayX<double> &m, const Eigen::ArrayX<double> &sigma_Angstrom, const Eigen::ArrayX<double> &epsilon_over_k, const Eigen::ArrayX<double> &mustar2, const Eigen::ArrayX<double> &nmu) : m(m), sigma_Angstrom(sigma_Angstrom), epsilon_over_k(epsilon_over_k), mustar2(mustar2), nmu(nmu) { + // Check lengths match + if (m.size() != mustar2.size()){ + throw teqp::InvalidArgument("bad size of mustar2"); + } + if (m.size() != nmu.size()){ + throw teqp::InvalidArgument("bad size of n"); + } + } + + /// Eq. 8 from Gross and Vrabec + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto get_alpha2DD(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_Angstrom; // concision + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + auto ninj = nmu[i]*nmu[j]; + if (ninj > 0){ + // Lorentz-Berthelot mixing rules + auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); + auto sigmaij = (sigma[i]+sigma[j])/2; + + auto Tstarij = T/epskij; + auto mij = std::max(sqrt(m[i]*m[j]), 2.0); + summer += x[i]*x[j]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*POW3(sigma[i]*sigma[j]/sigmaij)*ninj*mustar2[i]*mustar2[j]*get_JDD_2ij(eta, mij, Tstarij); + } + } + } + return forceeval(-static_cast<double>(EIGEN_PI)*rhoN_A3*summer); + } + + /// Eq. 9 from Gross and Vrabec + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto get_alpha3DD(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_Angstrom; // concision + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + for (auto k = 0; k < N; ++k){ + auto ninjnk = nmu[i]*nmu[j]*nmu[k]; + if (ninjnk > 0){ + // Lorentz-Berthelot mixing rules for sigma + auto sigmaij = (sigma[i]+sigma[j])/2; + auto sigmaik = (sigma[i]+sigma[k])/2; + auto sigmajk = (sigma[j]+sigma[k])/2; + + auto mijk = std::max(pow(m[i]*m[j]*m[k], 1.0/3.0), 2.0); + summer += x[i]*x[j]*x[k]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*epsilon_over_k[k]/T*POW3(sigma[i]*sigma[j]*sigma[k])/(sigmaij*sigmaik*sigmajk)*ninjnk*mustar2[i]*mustar2[j]*mustar2[k]*get_JDD_3ijk(eta, mijk); + } + } + } + } + return forceeval(-4.0*POW2(static_cast<double>(EIGEN_PI))/3.0*POW2(rhoN_A3)*summer); + } + + /*** + * \brief Get the dipolar contribution to \f$ \alpha = A/(NkT) \f$ + */ + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto eval(const TTYPE& T, const RhoType& rho_A3, const EtaType& eta, const VecType& mole_fractions) const { + auto alpha2 = get_alpha2DD(T, rho_A3, eta, mole_fractions); + auto alpha3 = get_alpha3DD(T, rho_A3, eta, mole_fractions); + auto alpha = forceeval(alpha2/(1.0-alpha3/alpha2)); + + using alpha2_t = decltype(alpha2); + using alpha3_t = decltype(alpha3); + using alpha_t = decltype(alpha); + struct DipolarContributionTerms{ + alpha2_t alpha2; + alpha3_t alpha3; + alpha_t alpha; + }; + return DipolarContributionTerms{alpha2, alpha3, alpha}; + } +}; + +/*** + * \brief The quadrupolar contribution from Gross and Vrabec + * + */ +class QuadrupolarContributionGrossVrabec { +private: + const Eigen::ArrayXd m, sigma_Angstrom, epsilon_over_k, Qstar2, nQ; + template<typename A> auto POW2(const A& x) const { return forceeval(x*x); } + template<typename A> auto POW3(const A& x) const { return forceeval(POW2(x)*x); } + template<typename A> auto POW5(const A& x) const { return forceeval(POW2(x)*POW3(x)); } + template<typename A> auto POW7(const A& x) const { return forceeval(POW2(x)*POW5(x)); } +public: + QuadrupolarContributionGrossVrabec(const Eigen::ArrayX<double> &m, const Eigen::ArrayX<double> &sigma_Angstrom, const Eigen::ArrayX<double> &epsilon_over_k, const Eigen::ArrayX<double> &Qstar2, const Eigen::ArrayX<double> &nQ) : m(m), sigma_Angstrom(sigma_Angstrom), epsilon_over_k(epsilon_over_k), Qstar2(Qstar2), nQ(nQ) { + // Check lengths match + if (m.size() != Qstar2.size()){ + throw teqp::InvalidArgument("bad size of mustar2"); + } + if (m.size() != nQ.size()){ + throw teqp::InvalidArgument("bad size of n"); + } + } + QuadrupolarContributionGrossVrabec& operator=( const QuadrupolarContributionGrossVrabec& ) = delete; // non copyable + + /// Eq. 9 from Gross and Vrabec + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto get_alpha2QQ(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_Angstrom; // concision + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + auto ninj = nQ[i]*nQ[j]; + if (ninj > 0){ + // Lorentz-Berthelot mixing rules + auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); + auto sigmaij = (sigma[i]+sigma[j])/2; + + auto Tstarij = T/epskij; + auto mij = sqrt(m[i]*m[j]); + summer += x[i]*x[j]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*POW5(sigma[i]*sigma[j])/POW7(sigmaij)*ninj*Qstar2[i]*Qstar2[j]*get_JQQ_2ij(eta, mij, Tstarij); + } + } + } + return forceeval(-static_cast<double>(EIGEN_PI)*POW2(3.0/4.0)*rhoN_A3*summer); + } + + /// Eq. 1 from Gross and Vrabec + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto get_alpha3QQ(const TTYPE& T, const RhoType& rhoN_A3, const EtaType& eta, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_Angstrom; // concision + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer = 0.0; + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + for (auto k = 0; k < N; ++k){ + auto ninjnk = nQ[i]*nQ[j]*nQ[k]; + if (ninjnk > 0){ + // Lorentz-Berthelot mixing rules for sigma + auto sigmaij = (sigma[i]+sigma[j])/2; + auto sigmaik = (sigma[i]+sigma[k])/2; + auto sigmajk = (sigma[j]+sigma[k])/2; + + auto mijk = pow(m[i]*m[j]*m[k], 1.0/3.0); + summer += x[i]*x[j]*x[k]*epsilon_over_k[i]/T*epsilon_over_k[j]/T*epsilon_over_k[k]/T*POW5(sigma[i]*sigma[j]*sigma[k])/POW3(sigmaij*sigmaik*sigmajk)*ninjnk*Qstar2[i]*Qstar2[j]*Qstar2[k]*get_JDD_3ijk(eta, mijk); + } + } + } + } + return forceeval(-4.0*POW2(static_cast<double>(EIGEN_PI))/3.0*POW3(3.0/4.0)*POW2(rhoN_A3)*summer); + } + + /*** + * \brief Get the quadrupolar contribution to \f$ \alpha = A/(NkT) \f$ + */ + template<typename TTYPE, typename RhoType, typename EtaType, typename VecType> + auto eval(const TTYPE& T, const RhoType& rho_A3, const EtaType& eta, const VecType& mole_fractions) const { + auto alpha2 = get_alpha2QQ(T, rho_A3, eta, mole_fractions); + auto alpha3 = get_alpha3QQ(T, rho_A3, eta, mole_fractions); + auto alpha = forceeval(alpha2/(1.0-alpha3/alpha2)); + + using alpha2_t = decltype(alpha2); + using alpha3_t = decltype(alpha3); + using alpha_t = decltype(alpha); + struct QuadrupolarContributionTerms{ + alpha2_t alpha2; + alpha3_t alpha3; + alpha_t alpha; + }; + return QuadrupolarContributionTerms{alpha2, alpha3, alpha}; + } +}; + +/** + \tparam JIntegral A type that can be indexed with a single integer n to give the J^{(n)} integral + \tparam KIntegral A type that can be indexed with a two integers a and b to give the K(a,b) integral + + The flexibility was added to include J and K integrals from either Luckas et al. or Gubbins and Twu (or any others following the interface) + */ +template<class JIntegral, class KIntegral> +class MultipolarContributionGubbinsTwu { +private: + const Eigen::ArrayXd sigma_m, epsilon_over_k, mubar2, Qbar2; + template<typename A> auto POW2(const A& x) const { return forceeval(x*x); } + template<typename A> auto POW3(const A& x) const { return forceeval(POW2(x)*x); } + template<typename A> auto POW4(const A& x) const { return forceeval(POW2(x)*POW2(x)); } + template<typename A> auto POW5(const A& x) const { return forceeval(POW2(x)*POW3(x)); } + template<typename A> auto POW7(const A& x) const { return forceeval(POW2(x)*POW5(x)); } + template<typename A> auto POW8(const A& x) const { return forceeval(POW4(x)*POW4(x)); } + template<typename A> auto POW10(const A& x) const { return forceeval(POW2(x)*POW8(x)); } + template<typename A> auto POW12(const A& x) const { return forceeval(POW4(x)*POW8(x)); } + + const JIntegral J6{6}; + const JIntegral J8{8}; + const JIntegral J10{10}; + const JIntegral J11{11}; + const JIntegral J13{13}; + const JIntegral J15{15}; + const KIntegral K222_333{222, 333}; + const KIntegral K233_344{233, 344}; + const KIntegral K334_445{334, 445}; + const KIntegral K444_555{444, 555}; + const double epsilon_0 = 8.8541878128e-12; // https://en.wikipedia.org/wiki/Vacuum_permittivity, in F/m, or C^2⋅N^−1⋅m^−2 + const double PI_ = static_cast<double>(EIGEN_PI); + +public: + MultipolarContributionGubbinsTwu(const Eigen::ArrayX<double> &sigma_m, const Eigen::ArrayX<double> &epsilon_over_k, const Eigen::ArrayX<double> &mubar2, const Eigen::ArrayX<double> &Qbar2) : sigma_m(sigma_m), epsilon_over_k(epsilon_over_k), mubar2(mubar2), Qbar2(Qbar2) { + // Check lengths match + if (sigma_m.size() != mubar2.size()){ + throw teqp::InvalidArgument("bad size of mubar2"); + } + if (sigma_m.size() != Qbar2.size()){ + throw teqp::InvalidArgument("bad size of Qbar2"); + } + } + MultipolarContributionGubbinsTwu& operator=( const MultipolarContributionGubbinsTwu& ) = delete; // non copyable + + template<typename TTYPE, typename RhoType, typename VecType> + auto get_alpha2(const TTYPE& T, const RhoType& rhoN, const RhoType& rhostar, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_m; // concision + + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summer112 = 0.0, summer123 = 0.0, summer224 = 0.0; + + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + // Lorentz-Berthelot mixing rules + auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); + auto sigmaij = (sigma[i]+sigma[j])/2; + + auto Tstari = T/epsilon_over_k[i], Tstarj = T/epsilon_over_k[j]; + auto leading = x[i]*x[j]/(Tstari*Tstarj); // common for all alpha_2 terms + auto Tstarij = forceeval(T/epskij); + + summer112 += leading*POW3(sigma[i]*sigma[j])/POW3(sigmaij)*mubar2[i]*mubar2[j]*J6.get_J(Tstarij, rhostar); + summer123 += leading*POW3(sigma[i])*POW5(sigma[j])/POW5(sigmaij)*mubar2[i]*Qbar2[j]*J8.get_J(Tstarij, rhostar); + summer224 += leading*POW5(sigma[i]*sigma[j])/POW7(sigmaij)*Qbar2[i]*Qbar2[j]*J10.get_J(Tstarij, rhostar); + } + } + + auto alpha2_112 = -2.0*PI_*rhoN*POW2(4*PI_*epsilon_0)/3.0*summer112; + auto alpha2_123 = -PI_*rhoN*POW2(4*PI_*epsilon_0)/3.0*summer123; + auto alpha2_224 = -14.0*PI_*rhoN*POW2(4*PI_*epsilon_0)/5.0*summer224; + + return forceeval(alpha2_112 + 2.0*alpha2_123 + alpha2_224); + } + + template<typename TTYPE, typename RhoType, typename VecType> + auto get_alpha3(const TTYPE& T, const RhoType& rhoN, const RhoType& rhostar, const VecType& mole_fractions) const{ + const auto& x = mole_fractions; // concision + const auto& sigma = sigma_m; // concision + const auto N = mole_fractions.size(); + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summerA_112_112_224 = 0.0, summerA_112_123_213 = 0.0, summerA_123_123_224 = 0.0, summerA_224_224_224 = 0.0; + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> summerB_112_112_112 = 0.0, summerB_112_123_123 = 0.0, summerB_123_123_224 = 0.0, summerB_224_224_224 = 0.0; + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + + // Lorentz-Berthelot mixing rules + auto epskij = sqrt(epsilon_over_k[i]*epsilon_over_k[j]); + auto sigmaij = (sigma[i]+sigma[j])/2; + + auto Tstari = T/epsilon_over_k[i], Tstarj = T/epsilon_over_k[j]; + auto Tstarij = forceeval(T/epskij); + + auto leading = x[i]*x[j]/pow(Tstari*Tstarj, 3.0/2.0); // common for all alpha_3A terms + + summerA_112_112_224 += leading*pow(sigma[i]*sigma[j], 11.0/2.0)/POW8(sigmaij)*mubar2[i]*mubar2[j]*sqrt(Qbar2[i]*Qbar2[j])*J11.get_J(Tstarij, rhostar); + summerA_112_123_213 += leading*pow(sigma[i]*sigma[j], 11.0/2.0)/POW8(sigmaij)*mubar2[i]*mubar2[j]*sqrt(Qbar2[i]*Qbar2[j])*J11.get_J(Tstarij, rhostar); + summerA_123_123_224 += leading*pow(sigma[i], 11.0/2.0)*pow(sigma[j], 15.0/2.0)/POW10(sigmaij)*mubar2[i]*sqrt(Qbar2[i])*pow(Qbar2[j], 3.0/2.0)*J13.get_J(Tstarij, rhostar); + summerA_224_224_224 += leading*pow(sigma[i]*sigma[j], 15.0/2.0)/POW12(sigmaij)*Qbar2[i]*Qbar2[j]*J15.get_J(Tstarij, rhostar); + + for (auto k = 0; k < N; ++k){ + auto Tstark = T/epsilon_over_k[k]; + auto epskik = sqrt(epsilon_over_k[i]*epsilon_over_k[k]); + auto epskjk = sqrt(epsilon_over_k[j]*epsilon_over_k[k]); + auto Tstarik = T/epskik; + auto Tstarjk = T/epskjk; + + // Lorentz-Berthelot mixing rules for sigma + auto sigmaij = (sigma[i]+sigma[j])/2; + auto sigmaik = (sigma[i]+sigma[k])/2; + auto sigmajk = (sigma[j]+sigma[k])/2; + auto leadingijk = x[i]*x[j]*x[k]/(Tstari*Tstarj*Tstark); + + auto K222333 = pow(K222_333.get_K(Tstarij, rhostar)*K222_333.get_K(Tstarik, rhostar)*K222_333.get_K(Tstarjk, rhostar), 1.0/3.0); + summerB_112_112_112 += leadingijk*POW3(sigma[i]*sigma[j]*sigma[k])/(sigmaij*sigmaik*sigmajk)*mubar2[i]*mubar2[j]*mubar2[k]*K222333; + auto K233344 = pow(K233_344.get_K(Tstarij, rhostar)*K233_344.get_K(Tstarik, rhostar)*K233_344.get_K(Tstarjk, rhostar), 1.0/3.0); + summerB_112_123_123 += leadingijk*POW3(sigma[i]*sigma[j])*POW5(sigma[k])/(sigmaij*POW2(sigmaik*sigmajk))*mubar2[i]*mubar2[j]*Qbar2[k]*K233344; + auto K334445 = pow(K334_445.get_K(Tstarij, rhostar)*K334_445.get_K(Tstarik, rhostar)*K334_445.get_K(Tstarjk, rhostar), 1.0/3.0); + summerB_123_123_224 += leadingijk*POW3(sigma[i])*POW5(sigma[j]*sigma[k])/(POW2(sigmaij*sigmaik)*POW3(sigmajk))*mubar2[i]*Qbar2[j]*Qbar2[k]*K334445; + auto K444555 = pow(K444_555.get_K(Tstarij, rhostar)*K444_555.get_K(Tstarik, rhostar)*K444_555.get_K(Tstarjk, rhostar), 1.0/3.0); + summerB_224_224_224 += leadingijk*POW5(sigma[i]*sigma[j]*sigma[k])/(POW3(sigmaij*sigmaik*sigmajk))*Qbar2[i]*Qbar2[j]*Qbar2[k]*K444555; + } + } + } + auto alpha3A_112_112_224 = 8.0*PI_*rhoN*POW3(4*PI_*epsilon_0)/25.0*summerA_112_112_224; + auto alpha3A_112_123_213 = 8.0*PI_*rhoN*POW3(4*PI_*epsilon_0)/75.0*summerA_112_123_213; + auto alpha3A_123_123_224 = 8.0*PI_*rhoN*POW3(4*PI_*epsilon_0)/35.0*summerA_123_123_224; + auto alpha3A_224_224_224 = 144.0*PI_*rhoN*POW3(4*PI_*epsilon_0)/245.0*summerA_224_224_224; + + auto alpha3A = 3.0*alpha3A_112_112_224 + 6.0*alpha3A_112_123_213 + 6.0*alpha3A_123_123_224 + alpha3A_224_224_224; + + auto alpha3B_112_112_112 = 32.0*POW3(PI_)*POW2(rhoN)*POW3(4*PI_*epsilon_0)/135.0*sqrt(14*PI_/5.0)*summerB_112_112_112; + auto alpha3B_112_123_123 = 64.0*POW3(PI_)*POW2(rhoN)*POW3(4*PI_*epsilon_0)/315.0*sqrt(3.0*PI_)*summerB_112_123_123; + auto alpha3B_123_123_224 = -32.0*POW3(PI_)*POW2(rhoN)*POW3(4*PI_*epsilon_0)/45.0*sqrt(22.0*PI_/63.0)*summerB_123_123_224; + auto alpha3B_224_224_224 = 32.0*POW3(PI_)*POW2(rhoN)*POW3(4*PI_*epsilon_0)/2025.0*sqrt(2002.0*PI_)*summerB_224_224_224; + + auto alpha3B = alpha3B_112_112_112 + 3.0*alpha3B_112_123_123 + 3.0*alpha3B_123_123_224 + alpha3B_224_224_224; + + return forceeval(alpha3A + alpha3B); + } + + /*** + * \brief Get the contribution to \f$ \alpha = A/(NkT) \f$ + */ + template<typename TTYPE, typename RhoType, typename VecType> + auto eval(const TTYPE& T, const RhoType& rhoN, const VecType& mole_fractions) const { + + // Calculate the effective reduced diameter (cubed) to be used for evaluation + // Eq. 24 from Gubbins + std::common_type_t<TTYPE, RhoType, decltype(mole_fractions[0])> sigma_x3 = 0.0; + auto N = mole_fractions.size(); + for (auto i = 0; i < N; ++i){ + for (auto j = 0; j < N; ++j){ + auto sigmaij = (sigma_m[i] + sigma_m[j])/2; + sigma_x3 += mole_fractions[i]*mole_fractions[j]*POW3(sigmaij); + } + } + auto rhostar = rhoN*sigma_x3; + + auto alpha2 = get_alpha2(T, rhoN, rhostar, mole_fractions); + auto alpha3 = get_alpha3(T, rhoN, rhostar, mole_fractions); + auto alpha = forceeval(alpha2/(1.0-alpha3/alpha2)); + + using alpha2_t = decltype(alpha2); + using alpha3_t = decltype(alpha3); + using alpha_t = decltype(alpha); + struct Terms{ + alpha2_t alpha2; + alpha3_t alpha3; + alpha_t alpha; + }; + return Terms{alpha2, alpha3, alpha}; + } +}; +using MCGTL = MultipolarContributionGubbinsTwu<LuckasJIntegral, LuckasKIntegral>; + +} +} diff --git a/include/teqp/models/saftvrmie.hpp b/include/teqp/models/saftvrmie.hpp index b1f4b26..80b844b 100644 --- a/include/teqp/models/saftvrmie.hpp +++ b/include/teqp/models/saftvrmie.hpp @@ -704,12 +704,15 @@ struct SAFTVRMieChainContributionTerms{ \brief A class used to evaluate mixtures using the SAFT-VR-Mie model */ class SAFTVRMieMixture { +public: + using PCSAFTDipolarContribution = SAFTpolar::DipolarContributionGrossVrabec; + using PCSAFTQuadrupolarContribution = SAFTpolar::QuadrupolarContributionGrossVrabec; private: std::vector<std::string> names; const SAFTVRMieChainContributionTerms terms; - std::optional<PCSAFT::PCSAFTDipolarContribution> dipolar; // Can be present or not - std::optional<PCSAFT::PCSAFTQuadrupolarContribution> quadrupolar; // Can be present or not + std::optional<PCSAFTDipolarContribution> dipolar; // Can be present or not + std::optional<PCSAFTQuadrupolarContribution> quadrupolar; // Can be present or not void check_kmat(const Eigen::ArrayXXd& kmat, std::size_t N) { if (kmat.size() == 0){ @@ -750,7 +753,7 @@ private: } } - auto build_dipolar(const std::vector<SAFTVRMieCoeffs> &coeffs) -> std::optional<PCSAFT::PCSAFTDipolarContribution>{ + auto build_dipolar(const std::vector<SAFTVRMieCoeffs> &coeffs) -> std::optional<PCSAFTDipolarContribution>{ Eigen::ArrayXd mustar2(coeffs.size()), nmu(coeffs.size()); auto i = 0; for (const auto &coeff : coeffs) { @@ -762,9 +765,9 @@ private: return std::nullopt; // No dipolar contribution is present } // The dispersive and hard chain initialization has already happened at this point - return PCSAFT::PCSAFTDipolarContribution(terms.m, terms.sigma_A, terms.epsilon_over_k, mustar2, nmu); + return PCSAFTDipolarContribution(terms.m, terms.sigma_A, terms.epsilon_over_k, mustar2, nmu); } - auto build_quadrupolar(const std::vector<SAFTVRMieCoeffs> &coeffs) -> std::optional<PCSAFT::PCSAFTQuadrupolarContribution>{ + auto build_quadrupolar(const std::vector<SAFTVRMieCoeffs> &coeffs) -> std::optional<PCSAFTQuadrupolarContribution>{ // The dispersive and hard chain initialization has already happened at this point Eigen::ArrayXd Qstar2(coeffs.size()), nQ(coeffs.size()); auto i = 0; @@ -776,7 +779,7 @@ private: if ((Qstar2*nQ).cwiseAbs().sum() == 0){ return std::nullopt; // No quadrupolar contribution is present } - return PCSAFT::PCSAFTQuadrupolarContribution(terms.m, terms.sigma_A, terms.epsilon_over_k, Qstar2, nQ); + return PCSAFTQuadrupolarContribution(terms.m, terms.sigma_A, terms.epsilon_over_k, Qstar2, nQ); } public: SAFTVRMieMixture(const std::vector<std::string> &names, const std::optional<Eigen::ArrayXXd>& kmat = std::nullopt) : SAFTVRMieMixture(get_coeffs_from_names(names), kmat){}; diff --git a/src/tests/catch_test_SAFTpolar.cxx b/src/tests/catch_test_SAFTpolar.cxx index 966012a..9c91010 100644 --- a/src/tests/catch_test_SAFTpolar.cxx +++ b/src/tests/catch_test_SAFTpolar.cxx @@ -4,20 +4,57 @@ using Catch::Approx; #include "teqp/models/saft/correlation_integrals.hpp" +#include "teqp/models/saft/polar_terms.hpp" +#include "teqp/types.hpp" +using namespace teqp; using namespace teqp::SAFTpolar; TEST_CASE("Evaluation of J^{(n)}", "[LuckasJn]") { - LuckasJIntegral<12> J12; + LuckasJIntegral J12{12}; auto Jval = J12.get_J(3.0, 1.0); } TEST_CASE("Evaluation of K(xxx, yyy)", "[LuckasKnn]") { - auto Kval23 = LuckasKIntegral<222, 333>().get_K(1.0, 0.9); + auto Kval23 = LuckasKIntegral(222, 333).get_K(1.0, 0.9); CHECK(Kval23 == Approx(0.03332).margin(0.02)); - auto Kval45 = LuckasKIntegral<444, 555>().get_K(1.0, 0.9); + auto Kval45 = LuckasKIntegral(444, 555).get_K(1.0, 0.9); CHECK(Kval45 == Approx(0.01541).margin(0.02)); } + +TEST_CASE("Evaluation of Gubbins and Twu", "[GTLPolar]") +{ + auto sigma_m = (Eigen::ArrayXd(2) << 3e-10, 3.1e-10).finished(); + auto epsilon_over_k= (Eigen::ArrayXd(2) << 200, 300).finished(); + auto mubar2 = (Eigen::ArrayXd(2) << 0.0, 0.5).finished(); + auto Qbar2 = (Eigen::ArrayXd(2) << 0.5, 0).finished(); + MCGTL GTL{sigma_m, epsilon_over_k, mubar2, Qbar2}; + auto z = (Eigen::ArrayXd(2) << 0.1, 0.9).finished(); + auto rhoN = std::complex<double>(300, 1e-100); + GTL.eval(300.0, rhoN, z); +} + +// This test is used to make sure that replacing std::abs with a more flexible function +// that can handle differentation types like std::complex<double> is still ok + +TEST_CASE("Check derivative of |x|", "[diffabs]") +{ + double h = 1e-100; + auto safe_abs1 = [](const auto&x) { return sqrt(x*x); }; + SECTION("|x|"){ + CHECK(safe_abs1(3.1) == 3.1); + CHECK(safe_abs1(-3.1) == 3.1); + } + SECTION("sqrt(x^2)"){ + CHECK(safe_abs1(std::complex<double>(3.1, h)).imag()/h == 1); + CHECK(safe_abs1(std::complex<double>(-3.1, h)).imag()/h == -1); + } + auto safe_abs2 = [](const auto&x) { return (getbaseval(x) < 0) ? -x : x; }; + SECTION("check base"){ + CHECK(safe_abs2(std::complex<double>(3.1, h)).imag()/h == 1); + CHECK(safe_abs2(std::complex<double>(-3.1, h)).imag()/h == -1); + } +} -- GitLab