teqp 0.19.1
Loading...
Searching...
No Matches
association.hpp
Go to the documentation of this file.
1
10#pragma once
11
12#include <map>
13#include <set>
14
15#include "teqp/constants.hpp"
16#include "teqp/types.hpp"
17
18#include <Eigen/Dense>
19
21
22namespace teqp{
23namespace association{
24
26 std::map<std::string, std::vector<std::string>> interaction_partners;
27 std::vector<std::string> site_order;
29 double alpha = 0.5;
30 double rtol = 1e-12, atol = 1e-12;
31 int max_iters = 100;
32};
33inline void from_json(const nlohmann::json& j, AssociationOptions& o) {
34 if (j.contains("alpha")){ j.at("alpha").get_to(o.alpha); }
35 if (j.contains("rtol")){ j.at("rtol").get_to(o.rtol); }
36 if (j.contains("atol")){ j.at("atol").get_to(o.atol); }
37 if (j.contains("max_iters")){ j.at("max_iters").get_to(o.max_iters); }
38}
39
40/***
41 A general class for calculating association fractions and association energy for mixtures
42
43 A mixture is formed of multiple components, each component has an index i.
44 For each component, it may multiple unique sites, and each unique site has a multiplicity associated with it.
45
46 For instance, for 4C water, there are two "e" sites and two "H" sites in the nomenclature of Clapeyron.
47 Thus the "e" site has multiplicity of 2 and the "H" site has multiplicity of 2
48 */
50public:
51 using CompSite = std::tuple<std::size_t, std::string>;
52 using CompCIndex = std::tuple<std::size_t, std::size_t>;
54 std::map<std::size_t, CompSite> to_CompSite;
55 std::map<CompSite, std::size_t> to_siteid;
56 std::map<CompCIndex, std::size_t> CompCIndex_to_siteid;
57 Eigen::ArrayXi counts;
58 Eigen::ArrayXi N_sites;
59 Eigen::ArrayXi N_unique_sites;
60 Eigen::ArrayXi comp_index;
61 std::vector<std::vector<std::string>> molecule_sites;
62 };
63private:
64 IndexMapper make_mapper(const std::vector<std::vector<std::string>>& molecule_sites, const AssociationOptions& options) const {
65 IndexMapper ind;
66 ind.counts.resize(1000);
67 ind.comp_index.resize(1000);
68 ind.N_sites.resize(molecule_sites.size());
69 ind.N_unique_sites.resize(molecule_sites.size());
70 ind.molecule_sites = molecule_sites;
71
72 // Build the maps from siteid->(component index, site name) and (component index, site name)->siteid
73 std::size_t icomp = 0;
74 std::size_t siteid = 0;
75 for (auto& molecule: molecule_sites){
77 std::map<std::string, int> site_counts;
78 for (auto& site: molecule){
79 ++site_counts[site];
80 }
81 auto unique_sites_on_molecule = std::set(molecule.begin(), molecule.end());
82 if (!options.site_order.empty()){
83 // TODO: enforce sites to appear in the order matching the specification
84 // TODO: this would be required to for instance check the D matrix of Langenbach and Enders
85 }
86 std::size_t Cindex = 0;
87 for (auto& site: unique_sites_on_molecule){
88 CompSite cs{icomp, site};
89 ind.to_CompSite[siteid] = cs;
90 ind.to_siteid[cs] = siteid;
91 ind.CompCIndex_to_siteid[CompCIndex{icomp, Cindex}] = siteid;
92 ind.counts[siteid] = site_counts[site];
93 ind.comp_index[siteid] = static_cast<int>(icomp);
94 Cindex++;
95 siteid++;
96 }
97 ind.N_sites[icomp] = static_cast<int>(molecule.size());
98 ind.N_unique_sites[icomp] = static_cast<int>(unique_sites_on_molecule.size());
99 icomp++;
100 }
101
102 ind.counts.conservativeResize(siteid);
103 ind.comp_index.conservativeResize(siteid);
104 return ind;
105 }
106
107 /***
108 Construct the counting matrix \f$ D_{IJ} \f$ as given by Langenbach and Enders
109 */
110 auto make_D(const IndexMapper& ind, const AssociationOptions& options ) const{
111
112 auto get_DIJ = [&ind, &options](std::size_t I, std::size_t J) -> int {
117 auto [ph1, typei] = ind.to_CompSite.at(I);
118 auto [ph2, typej] = ind.to_CompSite.at(J);
119 auto contains = [](auto& container, const auto& val){ return std::find(container.begin(), container.end(), val) != container.end(); };
121 if (options.interaction_partners.empty() || (contains(options.interaction_partners.at(typei), typej))){
122 return ind.counts[J];
123 }
124 return 0;
125 };
126
127 int Ngroups = static_cast<int>(ind.to_siteid.size());
128 Eigen::ArrayXXi D(Ngroups, Ngroups);
129 // I and J are the numerical indices of the sites
130 for (int I = 0; I < Ngroups; ++I){
131 for (int J = 0; J < Ngroups; ++J){
132 D(I, J) = get_DIJ(I, J);
133 }
134 }
135 return D;
136 }
137
138public:
139 const Eigen::ArrayXd b_m3mol,
144 const Eigen::ArrayXXi D;
146
147 Association(const Eigen::ArrayXd& b_m3mol, const Eigen::ArrayXd& beta, const Eigen::ArrayXd& epsilon_Jmol, const std::vector<std::vector<std::string>>& molecule_sites, const AssociationOptions& options) : b_m3mol(b_m3mol), beta(beta), epsilon_Jmol(epsilon_Jmol), options(options), mapper(make_mapper(molecule_sites, options)), D(make_D(mapper, options)), m_radial_dist(options.radial_dist){
148 }
149
156 template<typename TType, typename RhoType, typename MoleFracsType>
157 auto get_Delta(const TType& T, const RhoType& rhomolar, const MoleFracsType& molefracs) const {
158
159 using resulttype = std::common_type_t<decltype(T), decltype(rhomolar), decltype(molefracs[0])>; // Type promotion, without the const-ness
160 using Mat = Eigen::Array<resulttype, Eigen::Dynamic, Eigen::Dynamic>;
161 auto Ngroups = mapper.to_CompSite.size();
162 auto bmix = (molefracs*b_m3mol).sum();
163 auto eta = bmix*rhomolar/4.0;
164
165 decltype(forceeval(eta)) g;
166 switch(m_radial_dist){
167 case radial_dist::CS:
168 g = (2.0-eta)/(2.0*(1.0-eta)*(1.0-eta)*(1.0-eta)); break;
169 case radial_dist::KG:
170 g = 1.0 / (1.0 - 1.9*eta); break;
171 default:
172 throw std::invalid_argument("Bad radial distribution");
173 }
174
175 Mat Delta = Mat::Zero(Ngroups, Ngroups);
176 for (auto I = 0U; I < Ngroups; ++I){
177 auto i = std::get<0>(mapper.to_CompSite.at(I));
178 for (auto J = 0U; J < Ngroups; ++J){
179 auto j = std::get<0>(mapper.to_CompSite.at(J));
180
181 using namespace teqp::constants;
182 // The CR1 rule is used to calculate the cross contributions
183 if (true){ // Is CR1 // TODO: also allow the other combining rule
184 auto b_ij = (b_m3mol[i] + b_m3mol[j])/2.0;
185 auto epsilon_ij_Jmol = (epsilon_Jmol[i] + epsilon_Jmol[j])/2.0;
186 auto beta_ij = sqrt(beta[i]*beta[j]);
187 Delta(I, J) = g*b_ij*beta_ij*(exp(epsilon_ij_Jmol/(R_CODATA2017*T))-1.0)/N_A;
188 }
189 }
190 }
191 return Delta;
192 }
193
194 template<typename TType, typename RhoType, typename MoleFracsType, typename XType>
195 auto successive_substitution(const TType& T, const RhoType& rhomolar, const MoleFracsType& molefracs, const XType& X_init) const {
196
197 using resulttype = std::common_type_t<decltype(T), decltype(rhomolar), decltype(molefracs[0])>; // Type promotion, without the const-ness
198 using Mat = Eigen::Array<resulttype, Eigen::Dynamic, Eigen::Dynamic>;
199
200 const Mat Delta = get_Delta(T, rhomolar, molefracs);
201// const Mat DD = Delta.array()*D.cast<resulttype>().array(); // coefficient-wise product, with D upcast from int to type of Delta matrix
202
203 auto Ngroups = mapper.to_CompSite.size();
204 Eigen::RowVectorX<typename MoleFracsType::Scalar> xj(Ngroups); // Mole fractions of the component containing each siteid
205 for (auto I = 0U; I< Ngroups; ++I){
206 xj(I) = molefracs(std::get<0>(mapper.to_CompSite.at(I)));
207 }
208
209 using rDDXtype = std::decay_t<std::common_type_t<typename decltype(Delta)::Scalar, decltype(rhomolar), decltype(molefracs[0])>>; // Type promotion, without the const-ness
210 Eigen::MatrixX<rDDXtype> rDDX = rhomolar*N_A*(Delta.array()*D.cast<resulttype>().array()).matrix();
211 for (auto j = 0; j < rDDX.rows(); ++j){
212 rDDX.row(j).array() = rDDX.row(j).array()*xj.array();
213 }
214// rDDX.rowwise() *= xj;
215
216 Eigen::ArrayX<std::decay_t<rDDXtype>> X = X_init, Xnew;
217
218 for (auto counter = 0; counter < options.max_iters; ++counter){
219 // calculate the new array of non-bonded site fractions X
220 Xnew = options.alpha*X + (1.0-options.alpha)/(1.0+(rDDX*X.matrix()).array());
221 // These unaryExpr extract the numerical value from an Eigen array of generic type, allowing for comparison.
222 // Otherwise for instance it is imposible to compare two complex numbers (if you are using complex step derivatives)
223 auto diff = (Xnew-X).eval().cwiseAbs().unaryExpr([](const auto&x){return getbaseval(x); }).eval();
224 auto tol = (options.rtol*Xnew + options.atol).unaryExpr([](const auto&x){return getbaseval(x); }).eval();
225 if ((diff < tol).all()){
226 break;
227 }
228 X = Xnew;
229 }
230 return X;
231 }
232
236 template<typename TType, typename RhoType, typename MoleFracsType>
237 auto alphar(const TType& T, const RhoType& rhomolar, const MoleFracsType& molefracs) const {
238
239 // Do the sucessive substitution to obtain the non-bonded fractions for each unique site
240 Eigen::ArrayXd X_init = Eigen::ArrayXd::Ones(mapper.to_siteid.size());
241 auto X_A = successive_substitution(T, rhomolar, molefracs, X_init);
242
243 // Calculate the contribution alpha based on the "naive" summation like in Clapeyron
244 using resulttype = std::common_type_t<decltype(T), decltype(rhomolar), decltype(molefracs[0])>; // Type promotion, without the const-ness
245 resulttype alpha_r_asso = 0.0;
246 for (auto icomp = 0; icomp < molefracs.size(); ++icomp){ // loop over all components
247 resulttype summer = 0.0;
248 for (auto jsite = 0; jsite < mapper.N_unique_sites[icomp]; ++jsite){
249 // Get the siteid given the (component index, site index on the component)
250 const auto I = mapper.CompCIndex_to_siteid.at({icomp, jsite});
251
252 // each contribution (per unique site on a molecule) is multiplied by its multiplicity within the molecule
253 summer += (log(X_A(I))- X_A(I)/2.0 + 0.5)*static_cast<double>(mapper.counts(I));
254 }
255 alpha_r_asso += molefracs[icomp]*summer;
256 }
257 return alpha_r_asso;
258
259// // And do the summation to calculate the contribution to alpha=a/(RT)
260// using V = std::common_type_t<double, decltype(X_A[0]), int>;
261// auto xj = molefracs(mapper.comp_index); // mole fractions of the components attached at each site
262// auto alpha_r_asso = (xj.template cast<V>()*(log(X_A) - X_A/2.0 + 0.5).template cast<V>()*mapper.counts.cast<double>()).sum();
263// return forceeval(alpha_r_asso);
264 }
265
269 nlohmann::json get_assoc_calcs(double T, double rhomolar, const Eigen::ArrayXd& mole_fractions) const{
270
271 using Mat = Eigen::Array<double, Eigen::Dynamic, Eigen::Dynamic>;
272 const Mat Delta = get_Delta(T, rhomolar, mole_fractions);
273 Eigen::ArrayXd XAinit = 0.0*mole_fractions + 1.0;
274 auto XA = successive_substitution(T, rhomolar, mole_fractions, XAinit);
275
276 auto fromArrayXd = [](const Eigen::ArrayXd &x){std::valarray<double>n(x.size()); for (auto i = 0U; i < n.size(); ++i){ n[i] = x[i];} return n;};
277 auto fromArrayXXd = [](const Eigen::ArrayXXd &x){
278 std::size_t N = x.rows();
279 std::vector<std::vector<double>> n; n.resize(N);
280 for (auto i = 0U; i < N; ++i){
281 n[i].resize(N);
282 for (auto j = 0U; j < N; ++j){
283 n[i][j] = x(i,j);
284 }
285 }
286 return n;
287 };
288 auto fromArrayXXi = [](const Eigen::ArrayXXi &x){
289 std::size_t N = x.rows();
290 std::vector<std::vector<int>> n; n.resize(N);
291 for (auto i = 0U; i < N; ++i){
292 n[i].resize(N);
293 for (auto j = 0U; j < N; ++j){
294 n[i][j] = x(i,j);
295 }
296 }
297 return n;
298 };
299 return {
300 {"to_CompSite", mapper.to_CompSite},
301 {"to_siteid", mapper.to_siteid},
302 {"counts", mapper.counts},
303 {"D", fromArrayXXi(D.array())},
304 {"Delta", fromArrayXXd(Delta.array())},
305 {"X_A", fromArrayXd(XA.array())},
306 {"note", "X_A is the fraction of non-bonded sites for each siteid"}
307 };
308 }
309
310};
311
312}
313}
const Eigen::ArrayXd epsilon_Jmol
The association energy of each molecule, in J/mol.
nlohmann::json get_assoc_calcs(double T, double rhomolar, const Eigen::ArrayXd &mole_fractions) const
Get things from the association calculations for debug purposes.
std::tuple< std::size_t, std::size_t > CompCIndex
auto get_Delta(const TType &T, const RhoType &rhomolar, const MoleFracsType &molefracs) const
const Eigen::ArrayXd beta
The volume factor, dimensionless.
std::tuple< std::size_t, std::string > CompSite
Association(const Eigen::ArrayXd &b_m3mol, const Eigen::ArrayXd &beta, const Eigen::ArrayXd &epsilon_Jmol, const std::vector< std::vector< std::string > > &molecule_sites, const AssociationOptions &options)
auto successive_substitution(const TType &T, const RhoType &rhomolar, const MoleFracsType &molefracs, const XType &X_init) const
const AssociationOptions options
auto alphar(const TType &T, const RhoType &rhomolar, const MoleFracsType &molefracs) const
Calculate the contribution , where the Helmholtz energy is on a molar basis, making dimensionless.
const Eigen::ArrayXd b_m3mol
The covolume b, in m^3/mol.
#define X(f)
void from_json(const nlohmann::json &j, AssociationOptions &o)
auto getbaseval(const T &expr)
Definition types.hpp:84
auto forceeval(T &&expr)
Definition types.hpp:46
std::map< std::string, std::vector< std::string > > interaction_partners
std::vector< std::string > site_order
association::radial_dist radial_dist
Eigen::ArrayXi N_unique_sites
How many unique types of sites are on each molecule.
Eigen::ArrayXi comp_index
The pure component indices associated with each siteid.
std::map< CompSite, std::size_t > to_siteid
Eigen::ArrayXi N_sites
How many total sites are on each molecule.
std::map< CompCIndex, std::size_t > CompCIndex_to_siteid
std::map< std::size_t, CompSite > to_CompSite
std::vector< std::vector< std::string > > molecule_sites
Eigen::ArrayXi counts
An array of counts of each siteid for the entire mixture.