diff --git a/doc/source/models/CPA.ipynb b/doc/source/models/CPA.ipynb
index 47e2d936..62cd599c 100644
--- a/doc/source/models/CPA.ipynb
+++ b/doc/source/models/CPA.ipynb
@@ -7,8 +7,64 @@
"source": [
"# Cubic Plus Association (CPA)\n",
"\n",
- "The combination of a cubic EOS with association"
+ "The combination of a cubic EOS with association with the association term. The sum of the terms goes like:\n",
+ "$$\n",
+ "\\alpha^{\\rm r} = \\alpha^{\\rm r}_{\\rm cub} + \\alpha^{\\rm r}_{\\rm assoc}\n",
+ "$$\n",
+ "\n",
+ "## Cubic part\n",
+ "\n",
+ "The residual contribution to $\\alpha$ is expressed as the sum :\n",
+ "$$\n",
+ "\\alpha^{\\rm r}_{\\rm cub,rep} +\\alpha^{\\rm r}_{\\rm cub,att}\n",
+ "$$ where the cubic parts come from\n",
+ "\n",
+ "The repulsive part of the cubic EOS contribution:\n",
+ "$$\n",
+ "\\alpha^{\\rm r}_{\\rm cub,rep} = -\\ln(1 - b_{\\rm mix}\\rho) \n",
+ "$$\n",
+ "The attractive part of the cubic EOS contribution:\n",
+ "$$\n",
+ "\\alpha^{\\rm r}_{\\rm cub,att} = -\\frac{a_{\\rm mix}}{RT}\\dfrac{\\ln\\left(\\frac{\\Delta_1 b_{\\rm mix}\\rho + 1}{\\Delta_2b_{\\rm mix}\\rho + 1}\\right)}{b_{\\rm mix}\\cdot(\\Delta_1 - \\Delta_2)}\n",
+ "$$\n",
+ "with the coefficients depending on the cubic type:\n",
+ "\n",
+ "SRK: $\\Delta_1=1$, $\\Delta_2=0$\n",
+ "\n",
+ "PR: $\\Delta_1=1+\\sqrt{2}$, $\\Delta_2=1-\\sqrt{2}$\n",
+ "\n",
+ "The mixture models used for the $a_{\\rm mix}$ and $b_{\\rm mix}$ are the classical ones:\n",
+ "\n",
+ "$$\n",
+ "a_{\\rm mix} = \\sum_i\\sum_jx_ix_j(1-k_{ij})a_{ij}(T)\n",
+ "$$\n",
+ "with x the mole fraction, $k_{ij}$ a weighting parameter\n",
+ "$$\n",
+ "a_{ij}(T) = \\sqrt{a_ia_j}\n",
+ "$$\n",
+ "and\n",
+ "$$\n",
+ "a_{i}(T) = a_{0i}\\left[1+c_{1i}(1-\\sqrt{T/T_{{\\rm crit},i}})\\right]^2\n",
+ "$$\n",
+ "and for $b$:\n",
+ "$$\n",
+ "b_{\\rm mix} = \\sum_ix_ib_i\n",
+ "$$\n",
+ "so there are three cubic parameters per fluid that need to be obtained though fitting: $b_{i}$, $a_{0i}$, $c_{1i}$. The value of $a_{\\rm ij}$ depends on temperature while $b_{\\rm mix}$ does not.\n",
+ "\n",
+ "## Association part\n",
+ "\n",
+ "For the association, one must have a solid understanding of the association approach that is being applied. To this end, a short discussion of the general approach is required. \n",
+ "\n"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2180ef67",
+ "metadata": {},
+ "outputs": [],
+ "source": []
}
],
"metadata": {
@@ -27,7 +83,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.4"
+ "version": "3.10.13"
}
},
"nbformat": 4,
diff --git a/doc/source/models/Molecule.drawio.svg b/doc/source/models/Molecule.drawio.svg
new file mode 100644
index 00000000..c8d3a371
--- /dev/null
+++ b/doc/source/models/Molecule.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/include/teqp/models/CPA.hpp b/include/teqp/models/CPA.hpp
index 1ad88242..7bafb265 100644
--- a/include/teqp/models/CPA.hpp
+++ b/include/teqp/models/CPA.hpp
@@ -3,6 +3,9 @@
#include "nlohmann/json.hpp"
#include
#include "teqp/types.hpp"
+#include "teqp/exceptions.hpp"
+#include "teqp/models/association/association.hpp"
+#include "teqp/models/association/association_types.hpp"
namespace teqp {
@@ -11,29 +14,10 @@ namespace CPA {
template auto POW2(X x) { return x * x; };
template auto POW3(X x) { return x * POW2(x); };
-enum class association_classes {not_set, a1A, a2B, a3B, a4C, not_associating};
-
-inline auto get_association_classes(const std::string& s) {
- if (s == "1A") { return association_classes::a1A; }
- else if (s == "2B") { return association_classes::a2B; }
- else if (s == "2B") { return association_classes::a2B; }
- else if (s == "3B") { return association_classes::a3B; }
- else if (s == "4C") { return association_classes::a4C; }
- else {
- throw std::invalid_argument("bad association flag:" + s);
- }
-}
-
-enum class radial_dist { CS, KG, OT };
-
-inline auto get_radial_dist(const std::string& s) {
- if (s == "CS") { return radial_dist::CS; }
- else if (s == "KG") { return radial_dist::KG; }
- else if (s == "OT") { return radial_dist::OT; }
- else {
- throw std::invalid_argument("bad association flag:" + s);
- }
-}
+using association::radial_dist;
+using association::association_classes;
+using association::get_radial_dist;
+using association::get_association_classes;
/// Function that calculates the association binding strength between site A of molecule i and site B on molecule j
template
@@ -57,10 +41,10 @@ inline auto get_DeltaAB_pure(radial_dist dist, double epsABi, double betaABi, BT
g_vm_ref = 1.0 / (1.0 - 1.9 * eta);
break;
}
- case radial_dist::OT: {
- g_vm_ref = 1.0 / (1.0 - 0.475 * rhomolar * b_cubic);
- break;
- }
+// case radial_dist::OT: { // This is identical to KG
+// g_vm_ref = 1.0 / (1.0 - 0.475 * rhomolar * b_cubic);
+// break;
+// }
default: {
throw std::invalid_argument("Bad radial_dist");
}
@@ -136,13 +120,12 @@ inline auto get_cubic_flag(const std::string& s) {
class CPACubic {
private:
- std::valarray a0, bi, c1, Tc;
+ const std::valarray a0, bi, c1, Tc;
double delta_1, delta_2;
- std::valarray> k_ij;
- double R_gas;
-
+ const double R_gas;
+ const std::optional>> kmat;
public:
- CPACubic(cubic_flag flag, const std::valarray &a0, const std::valarray &bi, const std::valarray &c1, const std::valarray &Tc, double R_gas) : a0(a0), bi(bi), c1(c1), Tc(Tc), R_gas(R_gas) {
+ CPACubic(cubic_flag flag, const std::valarray &a0, const std::valarray &bi, const std::valarray &c1, const std::valarray &Tc, const double R_gas, const std::optional>> & kmat = std::nullopt) : a0(a0), bi(bi), c1(c1), Tc(Tc), R_gas(R_gas), kmat(kmat) {
switch (flag) {
case cubic_flag::PR:
{ delta_1 = 1 + sqrt(2); delta_2 = 1 - sqrt(2); break; }
@@ -151,8 +134,9 @@ class CPACubic {
default:
throw std::invalid_argument("Bad cubic flag");
}
- k_ij.resize(Tc.size()); for (auto i = 0U; i < k_ij.size(); ++i) { k_ij[i].resize(Tc.size()); }
};
+
+ std::size_t size() const {return a0.size(); }
template
auto R(const VecType& /*molefrac*/) const { return R_gas; }
@@ -171,7 +155,8 @@ class CPACubic {
auto ai = get_ai(T, i);
for (auto j = 0U; j < molefrac.size(); ++j) {
auto aj = get_ai(T, j);
- auto a_ij = (1.0 - k_ij[i][j]) * sqrt(ai * aj);
+ double kij = (kmat) ? kmat.value()[i][j] : 0.0;
+ auto a_ij = (1.0 - kij) * sqrt(ai * aj);
asummer += molefrac[i] * molefrac[j] * a_ij;
}
}
@@ -188,14 +173,14 @@ class CPACubic {
}
};
-template
+/** Implement the association approach of Huang & Radosz for pure fluids
+ */
class CPAAssociation {
private:
- const Cubic cubic;
const std::vector classes;
const radial_dist dist;
- const std::valarray epsABi, betaABi;
- const std::vector N_sites;
+ const std::valarray epsABi, betaABi, bi;
+ const std::vector N_sites;
const double R_gas;
auto get_N_sites(const std::vector &the_classes) {
@@ -216,13 +201,13 @@ class CPAAssociation {
}
public:
- CPAAssociation(const Cubic &&cubic, const std::vector& classes, const radial_dist dist, const std::valarray &epsABi, const std::valarray &betaABi, double R_gas)
- : cubic(cubic), classes(classes), dist(dist), epsABi(epsABi), betaABi(betaABi), N_sites(get_N_sites(classes)), R_gas(R_gas) {};
+ CPAAssociation(const std::vector& classes, const radial_dist dist, const std::valarray &epsABi, const std::valarray &betaABi, const std::valarray &bi, double R_gas)
+ : classes(classes), dist(dist), epsABi(epsABi), betaABi(betaABi), bi(bi), N_sites(get_N_sites(classes)), R_gas(R_gas) {};
template
auto alphar(const TType& T, const RhoType& rhomolar, const VecType& molefrac) const {
- // Calculate a and b of the mixture
- auto [a_cubic, b_cubic] = cubic.get_ab(T, molefrac);
+ // Calculate b of the mixture
+ auto b_cubic = (Eigen::Map(&bi[0], bi.size())*molefrac).sum();
// Calculate the fraction of sites not bonded with other active sites
auto RT = forceeval(R_gas * T); // R times T
@@ -259,6 +244,9 @@ class CPAEOS {
/// alphar = a/(R*T) where a and R are both molar quantities
template
auto alphar(const TType& T, const RhoType& rhomolar, const VecType& molefrac) const {
+ if (static_cast(molefrac.size()) != cubic.size()){
+ throw teqp::InvalidArgument("Mole fraction size is not correct; should be " + std::to_string(cubic.size()));
+ }
// Calculate the contribution to alphar from the conventional cubic EOS
auto alpha_r_cubic = cubic.alphar(T, rhomolar, molefrac);
@@ -270,12 +258,43 @@ class CPAEOS {
}
};
+struct AssociationVariantWrapper{
+ using vartype = std::variant;
+ const vartype holder;
+
+ AssociationVariantWrapper(const vartype& holder) : holder(holder) {};
+
+ template
+ auto alphar(const TType& T, const RhoType& rhomolar, const MoleFracsType& molefracs) const{
+ return std::visit([&](auto& h){ return h.alphar(T, rhomolar, molefracs); }, holder);
+ }
+};
+
/// A factory function to return an instantiated CPA instance given
/// the JSON representation of the model
inline auto CPAfactory(const nlohmann::json &j){
auto build_cubic = [](const auto& j) {
auto N = j["pures"].size();
std::valarray a0i(N), bi(N), c1(N), Tc(N);
+ std::vector> kmat;
+ if (j.contains("kmat")){
+ kmat = j.at("kmat");
+ std::string kmaterr = "The kmat is the wrong size. It should be square with dimension " + std::to_string(N);
+ if (kmat.size() != N){
+ throw teqp::InvalidArgument(kmaterr);
+ }
+ else{
+ for (auto& krow: kmat){
+ if(krow.size() != N){
+ throw teqp::InvalidArgument(kmaterr);
+ }
+ }
+ }
+ }
+ else{
+ kmat.resize(N); for (auto i = 0U; i < N; ++i){ kmat[i].resize(N); for (auto j = 0U; j < N; ++j){kmat[i][j] = 0.0;} }
+ }
+
std::size_t i = 0;
for (auto p : j["pures"]) {
a0i[i] = p["a0i / Pa m^6/mol^2"];
@@ -284,23 +303,79 @@ inline auto CPAfactory(const nlohmann::json &j){
Tc[i] = p["Tc / K"];
i++;
}
- return CPACubic(get_cubic_flag(j["cubic"]), a0i, bi, c1, Tc, j["R_gas / J/mol/K"]);
+ return CPACubic(get_cubic_flag(j["cubic"]), a0i, bi, c1, Tc, j["R_gas / J/mol/K"], kmat);
};
- auto build_assoc = [](const auto &&cubic, const auto& j) {
+
+ auto build_assoc_pure = [](const auto& j) -> AssociationVariantWrapper{
auto N = j["pures"].size();
- std::vector classes;
- radial_dist dist = get_radial_dist(j.at("radial_dist"));
- std::valarray epsABi(N), betaABi(N);
- std::size_t i = 0;
- for (auto p : j["pures"]) {
- epsABi[i] = p["epsABi / J/mol"];
- betaABi[i] = p["betaABi"];
- classes.push_back(get_association_classes(p["class"]));
- i++;
+ if (N == 1 && j.at("pures").contains("class") ){
+ // This is the backwards compatible approach
+ // with the old style of defining the association class {1,2B...}
+ std::vector classes;
+ radial_dist dist = get_radial_dist(j.at("radial_dist"));
+ std::valarray epsABi(N), betaABi(N), bi(N);
+ std::size_t i = 0;
+ for (auto p : j.at("pures")) {
+ epsABi[i] = p.at("epsABi / J/mol");
+ betaABi[i] = p.at("betaABi");
+ bi[i] = p.at("bi / m^3/mol");
+ classes.push_back(get_association_classes(p.at("class")));
+ i++;
+ }
+ return AssociationVariantWrapper{CPAAssociation(classes, dist, epsABi, betaABi, bi, j["R_gas / J/mol/K"])};
+ }
+ else{
+ // This branch uses the new code
+ Eigen::ArrayXd b_m3mol(N), beta(N), epsilon_Jmol(N);
+ association::AssociationOptions opt;
+ opt.radial_dist = get_radial_dist(j.at("radial_dist"));
+ if (j.contains("options")){
+ opt = j.at("options"); // Pulls in the options that are POD types
+ }
+
+ std::vector> molecule_sites;
+ std::size_t i = 0;
+ std::set unique_site_types;
+ for (auto p : j["pures"]) {
+ epsilon_Jmol[i] = p.at("epsABi / J/mol");
+ beta[i] = p.at("betaABi");
+ b_m3mol[i] = p.at("bi / m^3/mol");
+ molecule_sites.push_back(p.at("sites"));
+ for (auto & s : molecule_sites.back()){
+ unique_site_types.insert(s);
+ }
+ i++;
+ }
+ if (j.contains("options") && j.at("options").contains("interaction_partners")){
+ opt.interaction_partners = j.at("options").at("interaction_partners");
+ for (auto [k,partners] : opt.interaction_partners){
+ if (unique_site_types.count(k) == 0){
+ throw teqp::InvalidArgument("Site is invalid in interaction_partners: " + k);
+ }
+ for (auto& partner : partners){
+ if (unique_site_types.count(partner) == 0){
+ throw teqp::InvalidArgument("Partner " + partner + " is invalid for site " + k);
+ }
+ }
+ }
+ }
+ else{
+ // Every site type is assumed to interact with every other site type, except for itself
+ for (auto& site1 : unique_site_types){
+ std::vector partners;
+ for (auto& site2: unique_site_types){
+ if (site1 != site2){
+ partners.push_back(site2);
+ }
+ }
+ opt.interaction_partners[site1] = partners;
+ }
+ }
+
+ return AssociationVariantWrapper{association::Association(b_m3mol, beta, epsilon_Jmol, molecule_sites, opt)};
}
- return CPAAssociation(std::move(cubic), classes, dist, epsABi, betaABi, j["R_gas / J/mol/K"]);
};
- return CPAEOS(build_cubic(j), build_assoc(build_cubic(j), j));
+ return CPAEOS(build_cubic(j), build_assoc_pure( j));
}
}; /* namespace CPA */
diff --git a/include/teqp/models/association/association.hpp b/include/teqp/models/association/association.hpp
new file mode 100644
index 00000000..7872a59a
--- /dev/null
+++ b/include/teqp/models/association/association.hpp
@@ -0,0 +1,269 @@
+/**
+General routines for the calculation of association
+
+The implementation follows the approach of Langenbach for the index compression,
+
+ Many helpful hints from Andres Riedemann
+
+*/
+
+#pragma once
+
+#include