teqp 0.22.0
Loading...
Searching...
No Matches
VLLE.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "teqp/derivs.hpp"
4#include "teqp/exceptions.hpp"
7
8// Imports from boost
9#include <boost/numeric/odeint/stepper/controlled_runge_kutta.hpp>
10#include <boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp>
11
12namespace teqp {
13namespace VLLE {
14
15 using namespace teqp::cppinterface;
16 /***
17 * \brief Do a vapor-liquid-liquid phase equilibrium problem for a mixture (binary only for now)
18 * \param model The model to operate on
19 * \param T Temperature
20 * \param rhovecVinit Initial values for vapor mole concentrations
21 * \param rhovecL1init Initial values for liquid #1 mole concentrations
22 * \param rhovecL2init Initial values for liquid #2 mole concentrations
23 * \param atol Absolute tolerance on function values
24 * \param reltol Relative tolerance on function values
25 * \param axtol Absolute tolerance on steps in independent variables
26 * \param relxtol Relative tolerance on steps in independent variables
27 * \param maxiter Maximum number of iterations permitted
28 */
29
30 inline auto mix_VLLE_T(const AbstractModel& model, double T, const EArrayd& rhovecVinit, const EArrayd& rhovecL1init, const EArrayd& rhovecL2init, double atol, double reltol, double axtol, double relxtol, int maxiter) {
31
32 const Eigen::Index N = rhovecVinit.size();
33 Eigen::MatrixXd J(3 * N, 3 * N); J.setZero();
34 Eigen::VectorXd r(3 * N), x(3 * N);
35
36 x.head(N) = rhovecVinit;
37 x.segment(N, N) = rhovecL1init;
38 x.tail(N) = rhovecL2init;
39
40 Eigen::Map<Eigen::ArrayXd> rhovecV (&(x(0)), N);
41 Eigen::Map<Eigen::ArrayXd> rhovecL1(&(x(0+N)), N);
42 Eigen::Map<Eigen::ArrayXd> rhovecL2(&(x(0+2*N)), N);
43
45
46 for (int iter = 0; iter < maxiter; ++iter) {
47
48 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
49 auto [PsirL1, PsirgradL1, hessianL1] = model.build_Psir_fgradHessian_autodiff(T, rhovecL1);
50 auto [PsirL2, PsirgradL2, hessianL2] = model.build_Psir_fgradHessian_autodiff(T, rhovecL2);
51
52 auto HtotV = model.build_Psi_Hessian_autodiff(T, rhovecV);
53 auto HtotL1 = model.build_Psi_Hessian_autodiff(T, rhovecL1);
54 auto HtotL2 = model.build_Psi_Hessian_autodiff(T, rhovecL2);
55
56 auto zV = rhovecV/rhovecV.sum(), zL1 = rhovecL1 / rhovecL1.sum(), zL2 = rhovecL2 / rhovecL2.sum();
57 double RTL1 = model.get_R(zL1)*T, RTL2 = model.get_R(zL2)*T, RTV = model.get_R(zV)*T;
58
59 auto rhoL1 = rhovecL1.sum();
60 auto rhoL2 = rhovecL2.sum();
61 auto rhoV = rhovecV.sum();
62 double pL1 = rhoL1 * RTL1 - PsirL1 + (rhovecL1.array() * PsirgradL1.array()).sum(); // The (array*array).sum is a dot product
63 double pL2 = rhoL2 * RTL2 - PsirL2 + (rhovecL2.array() * PsirgradL2.array()).sum(); // The (array*array).sum is a dot product
64 double pV = rhoV * RTV - PsirV + (rhovecV.array() * PsirgradV.array()).sum();
65 auto dpdrhovecL1 = RTL1 + (hessianL1 * rhovecL1.matrix()).array();
66 auto dpdrhovecL2 = RTL2 + (hessianL2 * rhovecL2.matrix()).array();
67 auto dpdrhovecV = RTV + (hessianV * rhovecV.matrix()).array();
68
69 // 2N rows are equality of chemical equilibria
70 r.head(N) = PsirgradV + RTV*log(rhovecV) - (PsirgradL1 + RTL1*log(rhovecL1));
71 r.segment(N,N) = PsirgradL1 + RTL1 * log(rhovecL1) - (PsirgradL2 + RTL2 * log(rhovecL2));
72 // Followed by N pressure equilibria
73 r(2*N) = pV - pL1;
74 r(2*N+1) = pL1 - pL2;
75
76 // Chemical potential contributions in Jacobian
77 J.block(0,0,N,N) = HtotV;
78 J.block(0,N,N,N) = -HtotL1;
79 //J.block(0,2*N,N,N) = 0; (following the pattern, to make clear the structure)
80 //J.block(N,0,N,N) = 0; (following the pattern, to make clear the structure)
81 J.block(N, N, N, N) = HtotL1;
82 J.block(N, 2 * N, N, N) = -HtotL2;
83 // Pressure contributions in Jacobian
84 J.block(2 * N, 0, 1, N) = dpdrhovecV.transpose();
85 J.block(2 * N, N, 1, N) = -dpdrhovecL1.transpose();
86 J.block(2 * N + 1, N, 1, N) = dpdrhovecL1.transpose();
87 J.block(2 * N + 1, 2 * N, 1, N) = -dpdrhovecL2.transpose();
88
89 // Solve for the step
90 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-r);
91 x.array() += dx;
92
93 auto xtol_threshold = (axtol + relxtol * x.array().cwiseAbs()).eval();
94 if ((dx.array() < xtol_threshold).all()) {
96 break;
97 }
98
99 auto error_threshold = (atol + reltol * r.array().cwiseAbs()).eval();
100 if ((r.array().cwiseAbs() < error_threshold).all()) {
102 break;
103 }
104
105 // If the solution has stopped improving, stop. The change in x is equal to dx in infinite precision, but
106 // not when finite precision is involved, use the minimum non-denormal float as the determination of whether
107 // the values are done changing
108 if (((x.array() - dx.array()).cwiseAbs() < std::numeric_limits<double>::min()).all()) {
110 break;
111 }
112 if (iter == maxiter - 1) {
113 return_code = VLLE_return_code::maxiter_met;
114 }
115 }
116 Eigen::ArrayXd rhovecVfinal = rhovecV, rhovecL1final = rhovecL1, rhovecL2final = rhovecL2;
117 return std::make_tuple(return_code, rhovecVfinal, rhovecL1final, rhovecL2final);
118 }
119
120 /***
121 * \brief Do a vapor-liquid-liquid phase equilibrium problem for a mixture (binary only for now)
122 * \param model The model to operate on
123 * \param T Temperature
124 * \param rhovecVinit Initial values for vapor mole concentrations
125 * \param rhovecL1init Initial values for liquid #1 mole concentrations
126 * \param rhovecL2init Initial values for liquid #2 mole concentrations
127 * \param atol Absolute tolerance on function values
128 * \param reltol Relative tolerance on function values
129 * \param axtol Absolute tolerance on steps in independent variables
130 * \param relxtol Relative tolerance on steps in independent variables
131 * \param maxiter Maximum number of iterations permitted
132 */
133
134 inline auto mix_VLLE_p(const AbstractModel& model, double p, double Tinit, const EArrayd& rhovecVinit, const EArrayd& rhovecL1init, const EArrayd& rhovecL2init, double atol, double reltol, double axtol, double relxtol, int maxiter) {
135
136 const Eigen::Index N = rhovecVinit.size();
137 Eigen::MatrixXd J(3*N+1, 3*N+1); J.setZero();
138 Eigen::VectorXd r(3*N+1), x(3*N+1);
139
140 x.head(N) = rhovecVinit;
141 x.segment(N, N) = rhovecL1init;
142 x.segment(2*N, N) = rhovecL2init;
143 x(x.size()-1) = Tinit;
144
145 Eigen::Map<Eigen::ArrayXd> rhovecV (&(x(0)), N);
146 Eigen::Map<Eigen::ArrayXd> rhovecL1(&(x(0+N)), N);
147 Eigen::Map<Eigen::ArrayXd> rhovecL2(&(x(0+2*N)), N);
148
150
151 double T = -1;
152 for (int iter = 0; iter < maxiter; ++iter) {
153 T = x(x.size()-1);
154
155 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
156 auto [PsirL1, PsirgradL1, hessianL1] = model.build_Psir_fgradHessian_autodiff(T, rhovecL1);
157 auto [PsirL2, PsirgradL2, hessianL2] = model.build_Psir_fgradHessian_autodiff(T, rhovecL2);
158
159 auto HtotV = model.build_Psi_Hessian_autodiff(T, rhovecV);
160 auto HtotL1 = model.build_Psi_Hessian_autodiff(T, rhovecL1);
161 auto HtotL2 = model.build_Psi_Hessian_autodiff(T, rhovecL2);
162
163 auto zV = rhovecV/rhovecV.sum(), zL1 = rhovecL1 / rhovecL1.sum(), zL2 = rhovecL2 / rhovecL2.sum();
164 double RL1 = model.get_R(zL1), RL2 = model.get_R(zL2), RV = model.get_R(zV);
165 double RTL1 = RL1*T, RTL2 = RL2*T, RTV = RV*T;
166
167 auto rhoL1 = rhovecL1.sum();
168 auto rhoL2 = rhovecL2.sum();
169 auto rhoV = rhovecV.sum();
170 double pL1 = rhoL1 * RTL1 - PsirL1 + (rhovecL1.array() * PsirgradL1.array()).sum(); // The (array*array).sum is a dot product
171 double pL2 = rhoL2 * RTL2 - PsirL2 + (rhovecL2.array() * PsirgradL2.array()).sum(); // The (array*array).sum is a dot product
172 double pV = rhoV * RTV - PsirV + (rhovecV.array() * PsirgradV.array()).sum();
173 auto dpdrhovecL1 = RTL1 + (hessianL1 * rhovecL1.matrix()).array();
174 auto dpdrhovecL2 = RTL2 + (hessianL2 * rhovecL2.matrix()).array();
175 auto dpdrhovecV = RTV + (hessianV * rhovecV.matrix()).array();
176
177 auto DELTAVL1dmu_dT_res = (model.build_d2PsirdTdrhoi_autodiff(T, rhovecV.eval())
178 - model.build_d2PsirdTdrhoi_autodiff(T, rhovecL1.eval())).eval();
179 auto DELTAL1L2dmu_dT_res = (model.build_d2PsirdTdrhoi_autodiff(T, rhovecL1.eval())
180 - model.build_d2PsirdTdrhoi_autodiff(T, rhovecL2.eval())).eval();
181 auto DELTAVL1_dchempot_dT = (DELTAVL1dmu_dT_res + RV*log(rhovecV) - RL1*log(rhovecL1)).eval();
182 auto DELTAL1L2_dchempot_dT = (DELTAL1L2dmu_dT_res + RL1*log(rhovecL1) - RL2*log(rhovecL2)).eval();
183
184 // 2N rows are equality of chemical equilibria
185 r.head(N) = PsirgradV + RTV*log(rhovecV) - (PsirgradL1 + RTL1*log(rhovecL1));
186 r.segment(N,N) = PsirgradL1 + RTL1 * log(rhovecL1) - (PsirgradL2 + RTL2 * log(rhovecL2));
187 // Followed by 2 pressure equilibria for the phases
188 r(2*N) = pV - pL1;
189 r(2*N+1) = pL1 - pL2;
190 // And finally, the pressure must equal the specified one
191 r(2*N+2) = pV - p;
192
193
194 // Chemical potential contributions in Jacobian
195 J.block(0,0,N,N) = HtotV;
196 J.block(0,N,N,N) = -HtotL1;
197 //J.block(0,2*N,N,N) = 0; // For L2, following the pattern, to make clear the structure
198 J.block(0,2*N+2, N, 1) = DELTAVL1_dchempot_dT;
199
200 //J.block(N,0,N,N) = 0; // For V, following the pattern, to make clear the structure)
201 J.block(N,N, N, N) = HtotL1;
202 J.block(N,2* N, N, N) = -HtotL2;
203 J.block(N,2*N+2, N, 1) = DELTAL1L2_dchempot_dT;
204 // So far, 2*N constraints...
205
206 // Pressure contributions in Jacobian
207 J.block(2*N, 0, 1, N) = dpdrhovecV.transpose();
208 J.block(2*N, N, 1, N) = -dpdrhovecL1.transpose();
209 J(2*N, 2*N+2) = model.get_dpdT_constrhovec(T, rhovecV) - model.get_dpdT_constrhovec(T, rhovecL1);
210 J.block(2 * N + 1, N, 1, N) = dpdrhovecL1.transpose();
211 J.block(2 * N + 1, 2 * N, 1, N) = -dpdrhovecL2.transpose();
212 J(2*N+1, 2*N+2) = model.get_dpdT_constrhovec(T, rhovecL1) - model.get_dpdT_constrhovec(T, rhovecL2);
213
214 J.block(2*N+2, 0, 1, N) = dpdrhovecV.transpose();
215 J(2*N+2, 2*N+2) = model.get_dpdT_constrhovec(T, rhovecV);
216 // Takes us to 2*N + 3 constraints, or 3*N+1 for N=2
217
218 // Solve for the step
219 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-r);
220 x.array() += dx;
221 T = x(x.size()-1);
222
223 auto xtol_threshold = (axtol + relxtol * x.array().cwiseAbs()).eval();
224 if ((dx.array() < xtol_threshold).all()) {
226 break;
227 }
228
229 auto error_threshold = (atol + reltol * r.array().cwiseAbs()).eval();
230 if ((r.array().cwiseAbs() < error_threshold).all()) {
232 break;
233 }
234
235 // If the solution has stopped improving, stop. The change in x is equal to dx in infinite precision, but
236 // not when finite precision is involved, use the minimum non-denormal float as the determination of whether
237 // the values are done changing
238 if (((x.array() - dx.array()).cwiseAbs() < std::numeric_limits<double>::min()).all()) {
240 break;
241 }
242 if (iter == maxiter - 1) {
243 return_code = VLLE_return_code::maxiter_met;
244 }
245 }
246 double Tfinal = T;
247 Eigen::ArrayXd rhovecVfinal = rhovecV, rhovecL1final = rhovecL1, rhovecL2final = rhovecL2;
248 return std::make_tuple(return_code, Tfinal, rhovecVfinal, rhovecL1final, rhovecL2final);
249 }
250
254 template<typename Iterable>
255 inline auto get_self_intersections(Iterable& x, Iterable& y) {
256 Eigen::Array22d A;
257 std::vector<SelfIntersectionSolution> solns;
258 for (auto j = 0U; j < x.size() - 1; ++j) {
259 auto p0 = (Eigen::Array2d() << x[j], y[j]).finished();
260 auto p1 = (Eigen::Array2d() << x[j + 1], y[j + 1]).finished();
261 A.col(0) = p1 - p0;
262 for (auto k = j + 1; k < x.size() - 1; ++k) {
263 auto q0 = (Eigen::Array2d() << x[k], y[k]).finished();
264 auto q1 = (Eigen::Array2d() << x[k + 1], y[k + 1]).finished();
265 A.col(1) = q0 - q1;
266 Eigen::Array2d params = A.matrix().colPivHouseholderQr().solve((q0 - p0).matrix());
267 if ((params > 0).binaryExpr((params < 1), [](auto x, auto y) {return x & y; }).all()) { // Both of the params are in (0,1)
268 auto soln = p0 + params[0] * (p1 - p0);
269 solns.emplace_back(SelfIntersectionSolution{ static_cast<std::size_t>(j), static_cast<std::size_t>(k), params[0], params[1], soln[0], soln[1] });
270 }
271 }
272 }
273 return solns;
274 }
275
276 template<typename Iterable>
277 inline auto get_cross_intersections(Iterable& x1, Iterable& y1, Iterable& x2, Iterable& y2) {
278 Eigen::Array22d A;
279 std::vector<SelfIntersectionSolution> solns;
280 for (auto j = 0U; j < x1.size() - 1; ++j) {
281 auto p0 = (Eigen::Array2d() << x1[j], y1[j]).finished();
282 auto p1 = (Eigen::Array2d() << x1[j + 1], y1[j + 1]).finished();
283 A.col(0) = p1 - p0;
284 for (auto k = 0U; k < x2.size() - 1; ++k) {
285 auto q0 = (Eigen::Array2d() << x2[k], y2[k]).finished();
286 auto q1 = (Eigen::Array2d() << x2[k + 1], y2[k + 1]).finished();
287 A.col(1) = q0 - q1;
288 Eigen::Array2d params = A.matrix().colPivHouseholderQr().solve((q0 - p0).matrix());
289 if ((params > 0).binaryExpr((params < 1), [](auto x, auto y) {return x & y; }).all()) { // Both of the params are in (0,1)
290 auto soln = p0 + params[0] * (p1 - p0);
291 solns.emplace_back(SelfIntersectionSolution{ static_cast<std::size_t>(j), static_cast<std::size_t>(k), params[0], params[1], soln[0], soln[1] });
292 }
293 }
294 }
295 return solns;
296 }
297
298
299
300 inline auto find_VLLE_gen_binary(const AbstractModel& model, const std::vector<nlohmann::json>& traces, const std::string& key, const std::optional<VLLEFinderOptions> options = std::nullopt) {
301 std::vector<double> x, y;
302 auto opt = options.value_or(VLLEFinderOptions{});
303
304 Eigen::ArrayXd rhoL1(2), rhoL2(2), rhoV(2);
305 std::string xkey = (key == "T") ? "T / K" : "pL / Pa";
306 std::string ykey = (key == "T") ? "pL / Pa" : "T / K";
307
308 // A convenience function to weight the values
309 auto avg_values = [](const nlohmann::json&j1, const nlohmann::json &j2, const std::string& key, const double w) -> Eigen::ArrayXd{
310 auto v1 = j1.at(key).template get<std::valarray<double>>();
311 auto v2 = j2.at(key).template get<std::valarray<double>>();
312 std::valarray<double> vs = v1*w + v2*(1 - w);
313 return Eigen::Map<Eigen::ArrayXd>(&(vs[0]), vs.size());
314 };
315
316 if (traces.empty()) {
317 throw InvalidArgument("The traces variable is empty");
318 }
319 else if (traces.size() == 1) {
320 // Build the arrays of values to find the self-intersection
321 for (auto& el : traces[0]) {
322 auto rhoV_ = el.at("rhoV / mol/m^3").get<std::valarray<double>>();
323 auto y_ = el.at(ykey).get<double>();
324 x.push_back(rhoV_[0] / rhoV_.sum()); // Mole fractions in the vapor phase
325 y.push_back(y_);
326 }
327 auto intersections = get_self_intersections(x, y);
328 //auto& trace = traces[0];
329
330 auto process_intersection = [&](auto& trace, auto& i) {
331 rhoL1 = avg_values(trace[i.j], trace[i.j + 1], "rhoL / mol/m^3", i.s);
332 rhoL2 = avg_values(trace[i.k], trace[i.k + 1], "rhoL / mol/m^3", i.t);
333 rhoV = avg_values(trace[i.j], trace[i.j + 1], "rhoV / mol/m^3", i.s);
334
335 if (key == "T"){
336 double T = trace[0].at("T / K"); // All at same temperature
337
338 // Polish the solution
339 auto [code, rhoVfinal, rhoL1final, rhoL2final] = mix_VLLE_T(model, T, rhoV, rhoL1, rhoL2, 1e-10, 1e-10, 1e-10, 1e-10, opt.max_steps);
340
341 return nlohmann::json{
342 {"variables", "rhoV, rhoL1, rhoL2"},
343 {"approximate", {rhoV, rhoL1, rhoL2} },
344 {"polished", {rhoVfinal, rhoL1final, rhoL2final} },
345 {"polisher_return_code", static_cast<int>(code)}
346 };
347 }
348 else if (key == "P"){
349 double p = trace[0].at("pL / Pa"); // all at same pressure
350
351 // Polish the solution
352 auto [code, Tfinal, rhoVfinal, rhoL1final, rhoL2final] = mix_VLLE_p(model, p, i.y, rhoV, rhoL1, rhoL2, 1e-10, 1e-10, 1e-10, 1e-10, opt.max_steps);
353
354 return nlohmann::json{
355 {"variables", "rhoV, rhoL1, rhoL2, T"},
356 {"approximate", {rhoV, rhoL1, rhoL2, i.y} },
357 {"polished", {rhoVfinal, rhoL1final, rhoL2final, Tfinal} },
358 {"polisher_return_code", static_cast<int>(code)}
359 };
360 }
361 else{
362 throw teqp::InvalidArgument("Bad key");
363 }
364 };
365 std::vector<nlohmann::json> solutions;
366
367 for (auto& intersection : intersections) {
368 try {
369 auto soln = process_intersection(traces[0], intersection);
370 auto rhovecL1 = soln.at("polished")[1].template get<std::valarray<double>>();
371 auto rhovecL2 = soln.at("polished")[2].template get<std::valarray<double>>();
372 auto rhodiff = 100*(rhovecL1.sum() / rhovecL2.sum() - 1);
373 if (std::abs(rhodiff) > opt.rho_trivial_threshold) {
374 // Only keep non-trivial solutions
375 solutions.push_back(soln);
376 }
377 }
378 catch(...) {
379 // Additional sanity checking goes here...
380 ;
381 }
382 }
383 return solutions;
384 }
385 else if (traces.size() == 2) {
386
387 std::vector<double> x1, y1, x2, y2;
388 // Build the arrays of values to find the cross-intersection
389 for (auto& el : traces[0]) {
390 auto rhoV_ = el.at("rhoV / mol/m^3").get<std::valarray<double>>();
391 auto y_ = el.at(ykey).get<double>();
392 x1.push_back(rhoV_[0] / rhoV_.sum()); // Mole fractions in the vapor phase
393 y1.push_back(y_);
394 }
395 for (auto& el : traces[1]) {
396 auto rhoV_ = el.at("rhoV / mol/m^3").get<std::valarray<double>>();
397 auto y_ = el.at(ykey).get<double>();
398 x2.push_back(rhoV_[0] / rhoV_.sum()); // Mole fractions in the vapor phase
399 y2.push_back(y_);
400 }
401 auto intersections = get_cross_intersections(x1, y1, x2, y2);
402
403 auto process_intersection = [&](auto& i) {
404 rhoL1 = avg_values(traces[0][i.j], traces[0][i.j + 1], "rhoL / mol/m^3", i.s);
405 rhoL2 = avg_values(traces[1][i.k], traces[1][i.k + 1], "rhoL / mol/m^3", i.t);
406 rhoV = avg_values(traces[0][i.j], traces[0][i.j + 1], "rhoV / mol/m^3", i.s);
407
408 if (key == "T"){
409 double T = traces[0][0].at(xkey);
410
411 // Polish the solution
412 auto [code, rhoVfinal, rhoL1final, rhoL2final] = mix_VLLE_T(model, T, rhoV, rhoL1, rhoL2, 1e-10, 1e-10, 1e-10, 1e-10, opt.max_steps);
413
414 return nlohmann::json{
415 {"variables", "rhoV, rhoL1, rhoL2"},
416 {"approximate", {rhoV, rhoL1, rhoL2} },
417 {"polished", {rhoVfinal, rhoL1final, rhoL2final} },
418 {"polisher_return_code", static_cast<int>(code)}
419 };
420 }
421 else if (key == "P"){
422 double p = traces[0][0].at(xkey);
423
424 // Polish the solution
425 auto [code, Tfinal, rhoVfinal, rhoL1final, rhoL2final] = mix_VLLE_p(model, p, i.y, rhoV, rhoL1, rhoL2, 1e-10, 1e-10, 1e-10, 1e-10, opt.max_steps);
426
427 return nlohmann::json{
428 {"variables", "rhoV, rhoL1, rhoL2, T"},
429 {"approximate", {rhoV, rhoL1, rhoL2, i.y} },
430 {"polished", {rhoVfinal, rhoL1final, rhoL2final, Tfinal} },
431 {"polisher_return_code", static_cast<int>(code)}
432 };
433 }
434 else{
435 throw teqp::InvalidArgument("Bad key");
436 }
437 };
438 std::vector<nlohmann::json> solutions;
439
440 for (auto& intersection : intersections) {
441 try {
442 auto soln = process_intersection(intersection);
443 auto rhovecL1 = soln.at("polished")[1].template get<std::valarray<double>>();
444 auto rhovecL2 = soln.at("polished")[2].template get<std::valarray<double>>();
445 auto rhodiff = 100*(rhovecL1.sum() / rhovecL2.sum() - 1);
446 if (std::abs(rhodiff) > opt.rho_trivial_threshold) {
447 // Only keep non-trivial solutions
448 solutions.push_back(soln);
449 }
450 }
451 catch(...) {
452 // Additional sanity checking goes here...
453 ;
454 }
455 }
456 return solutions;
457 }
458 else {
459 throw InvalidArgument("No cross intersection between traces implemented yet");
460 }
461 }
462
468 inline auto find_VLLE_T_binary(const AbstractModel& model, const std::vector<nlohmann::json>& traces, const std::optional<VLLEFinderOptions>& options = std::nullopt){
469 return find_VLLE_gen_binary(model, traces, "T", options);
470 }
471
477 inline auto find_VLLE_p_binary(const AbstractModel& model, const std::vector<nlohmann::json>& traces, const std::optional<VLLEFinderOptions>& options = std::nullopt){
478 return find_VLLE_gen_binary(model, traces, "P", options);
479 }
480
481 inline auto get_drhovecdT_VLLE_binary(const AbstractModel& model, double T, const EArrayd &rhovecV, const EArrayd& rhovecL1, const EArrayd& rhovecL2){
482
483 auto dot = [](const EArrayd&a, const EArrayd &b){ return a.cwiseProduct(b).sum(); };
484
485 Eigen::MatrixXd LHS(2, 2);
486 Eigen::MatrixXd RHS(2, 1);
487 Eigen::MatrixXd PSIV = model.build_Psi_Hessian_autodiff(T, rhovecV);
488 Eigen::MatrixXd PSIL1 = model.build_Psi_Hessian_autodiff(T, rhovecL1);
489 Eigen::MatrixXd PSIL2 = model.build_Psi_Hessian_autodiff(T, rhovecL2);
490 double dpdTV = model.get_dpdT_constrhovec(T, rhovecV);
491 double dpdTL1 = model.get_dpdT_constrhovec(T, rhovecL1);
492 double dpdTL2 = model.get_dpdT_constrhovec(T, rhovecL2);
493
494 // here mu is not the entire chemical potential, rather it is just the residual part and
495 // the density-dependent part from the ideal-gas
496 double RV = model.R(rhovecV/rhovecV.sum());
497 double RL1 = model.R(rhovecL1/rhovecL1.sum());
498 double RL2 = model.R(rhovecL2/rhovecL2.sum());
499 EArrayd dmudTV = model.build_d2PsirdTdrhoi_autodiff(T, rhovecV) + RV*log(rhovecV);
500 EArrayd dmudTL1 = model.build_d2PsirdTdrhoi_autodiff(T, rhovecL1) + RL1*log(rhovecL1);
501 EArrayd dmudTL2 = model.build_d2PsirdTdrhoi_autodiff(T, rhovecL2) + RL2*log(rhovecL2);
502
503 LHS.row(0) = PSIV*(rhovecL1-rhovecV).matrix();
504 LHS.row(1) = PSIV*(rhovecL2-rhovecV).matrix();
505 RHS(0) = dot(dmudTL1-dmudTV, rhovecL1) - (dpdTL1-dpdTV);
506 RHS(1) = dot(dmudTL2-dmudTV, rhovecL2) - (dpdTL2-dpdTV);
507
508 Eigen::ArrayXd drhovecVdT = LHS.colPivHouseholderQr().solve(RHS);
509 Eigen::VectorXd AV = PSIV*drhovecVdT.matrix();
510 Eigen::ArrayXd drhovecL1dT = PSIL1.colPivHouseholderQr().solve((AV.array() - (dmudTL1-dmudTV)).matrix());
511 Eigen::ArrayXd drhovecL2dT = PSIL2.colPivHouseholderQr().solve((AV.array() - (dmudTL2-dmudTV)).matrix());
512
513 return std::make_tuple(drhovecVdT, drhovecL1dT, drhovecL2dT);
514 };
515
519 inline auto trace_VLLE_binary(const teqp::VLLE::AbstractModel& model, const double Tinit, const EArrayd& rhovecVinit, const EArrayd& rhovecL1init, const EArrayd& rhovecL2init, const std::optional<VLLETracerOptions>& options_ = std::nullopt){
520 auto options = options_.value_or(VLLETracerOptions());
521
522 // Typedefs for the types for odeint for simple Euler and RK45 integrators
523 using state_type = std::vector<double>;
524 using namespace boost::numeric::odeint;
525
526 typedef runge_kutta_cash_karp54< state_type > error_stepper_type;
527 typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type;
528
529 auto xprime = [&](const state_type& x, state_type& dxdt, const double T)
530 {
531 // Unpack the inputs
532 const auto rhovecV_ = Eigen::Map<const Eigen::ArrayXd>(&(x[0]), 2),
533 rhovecL1_ = Eigen::Map<const Eigen::ArrayXd>(&(x[0]) + 2, 2),
534 rhovecL2_ = Eigen::Map<const Eigen::ArrayXd>(&(x[0]) + 4, 2);
535
536 auto [drhovecVdT, drhovecL1dT, drhovecL2dT] = VLLE::get_drhovecdT_VLLE_binary(model, T, rhovecV_, rhovecL1_, rhovecL2_);
537 Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]), 2) = drhovecVdT;
538 Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 2, 2) = drhovecL1dT;
539 Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 4, 2) = drhovecL2dT;
540 };
541
542 // Define the tolerances
543 double abs_err = options.abs_err, rel_err = options.rel_err, a_x = 1.0, a_dxdt = 1.0;
544 controlled_stepper_type controlled_stepper(default_error_checker< double, range_algebra, default_operations >(abs_err, rel_err, a_x, a_dxdt));
545
546 // Copy variables into the stepping array
547 double T = Tinit, dT = options.init_dT;
548 state_type x0(3*2);
549 Eigen::Map<Eigen::ArrayXd>(&(x0[0]), 2) = rhovecVinit;
550 Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 2, 2) = rhovecL1init;
551 Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 4, 2) = rhovecL2init;
552
553 nlohmann::json data_collector = nlohmann::json::array();
554 for (auto iter = 0; iter < options.max_step_count; ++iter) {
555 int retry_count = 0;
556
557 auto res = controlled_step_result::fail;
558 try {
559 res = controlled_stepper.try_step(xprime, x0, T, dT);
560 }
561 catch (const std::exception &e) {
562 if (options.verbosity > 0) {
563 std::cout << e.what() << std::endl;
564 }
565 break;
566 }
567
568 if (res != controlled_step_result::success) {
569 // Try again, with a smaller step size
570 iter--;
571 retry_count++;
572 continue;
573 }
574 else {
575 retry_count = 0;
576 }
577 // Reduce step size if greater than the specified max step size
578 dT = std::min(dT, options.max_dT);
579
580 const auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), 2),
581 rhovecL1 = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + 2, 2),
582 rhovecL2 = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + 4, 2);
583
584 // Polish if requested
585 if (options.polish){
586 auto [code, rhovecVnew, rhovecL1new, rhovecL2new] = teqp::VLLE::mix_VLLE_T(model, T, rhovecV, rhovecL1, rhovecL2, 1e-10, 1e-10, 1e-10, 1e-10, options.max_polish_steps);
587 Eigen::Map<Eigen::ArrayXd>(&(x0[0]), 2) = rhovecVnew;
588 Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 2, 2) = rhovecL1new;
589 Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 4, 2) = rhovecL2new;
590 }
591
592 auto critV = model.get_criticality_conditions(T, rhovecV);
593 auto critL1 = model.get_criticality_conditions(T, rhovecL1);
594 auto critL2 = model.get_criticality_conditions(T, rhovecL2);
595 double rhomolarV = rhovecV.sum();
596 auto molefracV = rhovecV/rhomolarV;
597 auto pV = rhomolarV*model.get_R(molefracV)*T*(1.0 + model.get_Ar01(T, rhomolarV, molefracV));
598 if (!std::isfinite(pV)){
599 if (options.verbosity > 0) {
600 std::cout << "Calculated pressure is not finite" << std::endl;
601 }
602 break;
603 }
604 if(options.init_dT > 0 && T > options.T_limit){
605 if (options.verbosity > 0) {
606 std::cout << "Exceeded maximum temperature of " << options.T_limit << " K" << std::endl;
607 }
608 break;
609 }
610 if(options.init_dT < 0 && T < options.T_limit){
611 if (options.verbosity > 0) {
612 std::cout << "Exceeded minimum temperature of " << options.T_limit << " K" << std::endl;
613 }
614 break;
615 }
616
617 if (options.verbosity > 100){
618 std::cout << "[T,x0L1,x0L2,x0V]: " << T << "," << rhovecL1[0]/rhovecL1.sum() << "," << rhovecL2[0]/rhovecL2.sum() << "," << rhovecV[0]/rhovecV.sum() << std::endl;
619 std::cout << "[crits]: " << critV << "," << critL1 << "," << critL2 << std::endl;
620 }
621
622 if (options.terminate_composition){
623 auto c0 = (Eigen::ArrayXd(3) << rhovecL1[0]/rhovecL1.sum(), rhovecL2[0]/rhovecL2.sum(), rhovecV[0]/rhovecV.sum()).finished();
624 auto diffs = (Eigen::ArrayXd(3) << c0[0]-c0[1], c0[0]-c0[2], c0[1]-c0[2]).finished();
625 if ((diffs.cwiseAbs() < options.terminate_composition_tol).any()){
626 break;
627 }
628 }
629 if (retry_count > options.max_step_retries){
630 if (options.verbosity > 0) {
631 std::cout << "Max retries of step exceeded." << std::endl;
632 }
633 break;
634 }
635
636 nlohmann::json entry{
637 {"T / K", T},
638 {"rhoL1 / mol/m^3", rhovecL1},
639 {"rhoL2 / mol/m^3", rhovecL2},
640 {"rhoV / mol/m^3", rhovecV},
641 {"critV", critV},
642 {"critL1", critL1},
643 {"critL2", critL2},
644 {"pV / Pa", pV}
645 };
646 data_collector.push_back(entry);
647 }
648 return data_collector;
649 }
650
651}
652}
virtual double get_Ar01(const double T, const double rho, const REArrayd &molefrac) const =0
virtual double get_dpdT_constrhovec(const double T, const EArrayd &rhovec) const =0
virtual double get_R(const EArrayd &) const =0
virtual EMatrixd build_Psi_Hessian_autodiff(const double T, const EArrayd &rhovec) const =0
virtual EArrayd build_d2PsirdTdrhoi_autodiff(const double T, const EArrayd &rhovec) const =0
virtual EArray2 get_criticality_conditions(const double T, const REArrayd &rhovec) const
double R(const EArrayd &x) const
Definition teqpcpp.hpp:102
virtual std::tuple< double, Eigen::ArrayXd, Eigen::MatrixXd > build_Psir_fgradHessian_autodiff(const double T, const EArrayd &rhovec) const =0
auto get_cross_intersections(Iterable &x1, Iterable &y1, Iterable &x2, Iterable &y2)
Definition VLLE.hpp:277
auto find_VLLE_p_binary(const AbstractModel &model, const std::vector< nlohmann::json > &traces, const std::optional< VLLEFinderOptions > &options=std::nullopt)
Given an isobaric VLE trace(s) for a binary mixture, obtain the VLLE solution.
Definition VLLE.hpp:477
auto find_VLLE_T_binary(const AbstractModel &model, const std::vector< nlohmann::json > &traces, const std::optional< VLLEFinderOptions > &options=std::nullopt)
Given an isothermal VLE trace(s) for a binary mixture, obtain the VLLE solution.
Definition VLLE.hpp:468
auto mix_VLLE_p(const AbstractModel &model, double p, double Tinit, const EArrayd &rhovecVinit, const EArrayd &rhovecL1init, const EArrayd &rhovecL2init, double atol, double reltol, double axtol, double relxtol, int maxiter)
Definition VLLE.hpp:134
auto find_VLLE_gen_binary(const AbstractModel &model, const std::vector< nlohmann::json > &traces, const std::string &key, const std::optional< VLLEFinderOptions > options=std::nullopt)
Definition VLLE.hpp:300
auto get_drhovecdT_VLLE_binary(const AbstractModel &model, double T, const EArrayd &rhovecV, const EArrayd &rhovecL1, const EArrayd &rhovecL2)
Definition VLLE.hpp:481
auto trace_VLLE_binary(const teqp::VLLE::AbstractModel &model, const double Tinit, const EArrayd &rhovecVinit, const EArrayd &rhovecL1init, const EArrayd &rhovecL2init, const std::optional< VLLETracerOptions > &options_=std::nullopt)
Given an initial VLLE solution, trace the VLLE curve. We know the VLLE curve is a function of only on...
Definition VLLE.hpp:519
auto get_self_intersections(Iterable &x, Iterable &y)
Definition VLLE.hpp:255
auto mix_VLLE_T(const AbstractModel &model, double T, const EArrayd &rhovecVinit, const EArrayd &rhovecL1init, const EArrayd &rhovecL2init, double atol, double reltol, double axtol, double relxtol, int maxiter)
Definition VLLE.hpp:30
Eigen::ArrayX< double > EArrayd
Definition teqpcpp.hpp:16