teqp 0.22.0
Loading...
Searching...
No Matches
VLE.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <optional>
4#include "teqp/derivs.hpp"
5#include "teqp/exceptions.hpp"
10#include <Eigen/Dense>
11
12// Imports from boost for numerical integration
13#include <boost/numeric/odeint/stepper/controlled_runge_kutta.hpp>
14#include <boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp>
15#include <boost/numeric/odeint/stepper/euler.hpp>
16
17
18// Imports from Eigen unsupported for hybrj method
19#include <unsupported/Eigen/NonLinearOptimization>
20
21namespace teqp{
22
23 using namespace Eigen;
24
25 // Generic functor
26 template<typename _Scalar, int NX = Dynamic, int NY = Dynamic>
27 struct Functor
28 {
29 typedef _Scalar Scalar;
30 enum {
33 };
34 typedef Matrix<Scalar, InputsAtCompileTime, 1> InputType;
35 typedef Matrix<Scalar, ValuesAtCompileTime, 1> ValueType;
36 typedef Matrix<Scalar, ValuesAtCompileTime, InputsAtCompileTime> JacobianType;
37
38 const int m_inputs, m_values;
39
42
43 int inputs() const { return m_inputs; }
44 int values() const { return m_values; }
45
46 // you should define that in the subclass :
47 // void operator() (const InputType& x, ValueType* v, JacobianType* _j=0) const;
48 };
49
50// A convenience method to make linear system solving more concise with Eigen datatypes
51/***
52* All arguments are converted to matrices, the solve is done, and an array is returned
53*/
54template<class A, class B>
55auto linsolve(const A& a, const B& b) {
56 return a.matrix().colPivHouseholderQr().solve(b.matrix()).array().eval();
57}
58
59/***
60* \brief Do a vapor-liquid phase equilibrium problem for a mixture (binary only for now) with mole fractions specified in the liquid phase
61* \param model The model to operate on
62* \param T Temperature
63* \param rhovecL0 Initial values for liquid mole concentrations
64* \param rhovecV0 Initial values for vapor mole concentrations
65* \param xspec Specified mole fractions for all components
66* \param atol Absolute tolerance on function values
67* \param reltol Relative tolerance on function values
68* \param axtol Absolute tolerance on steps in independent variables
69* \param relxtol Relative tolerance on steps in independent variables
70* \param maxiter Maximum number of iterations permitted
71*
72* Note: if a mole fraction is zero in the provided vector, the molar concentrations in
73* this component will not be allowed to change (they will stay zero, avoiding the possibility that
74* they go to a negative value, which can cause trouble for some EOS)
75*/
76inline auto mix_VLE_Tx(const AbstractModel& model, double T, const Eigen::ArrayXd& rhovecL0, const Eigen::ArrayXd& rhovecV0, const Eigen::ArrayXd& xspec, double atol, double reltol, double axtol, double relxtol, int maxiter) {
77 using Scalar = double;
78
79 const Eigen::Index N = rhovecL0.size();
80 auto lengths = (Eigen::ArrayX<Eigen::Index>(3) << rhovecL0.size(), rhovecV0.size(), xspec.size()).finished();
81 if (lengths.minCoeff() != lengths.maxCoeff()){
82 throw InvalidArgument("lengths of rhovecs and xspec must be the same in mix_VLE_Tx");
83 }
84 Eigen::MatrixXd J(2 * N, 2 * N), r(2 * N, 1), x(2 * N, 1);
85 x.col(0).array().head(N) = rhovecL0;
86 x.col(0).array().tail(N) = rhovecV0;
87
88 Eigen::Map<Eigen::ArrayXd> rhovecL(&(x(0)), N);
89 Eigen::Map<Eigen::ArrayXd> rhovecV(&(x(0 + N)), N);
90 auto RT = model.get_R(xspec) * T;
91
93
94 for (int iter = 0; iter < maxiter; ++iter) {
95
96 auto [PsirL, PsirgradL, hessianL] = model.build_Psir_fgradHessian_autodiff(T, rhovecL);
97 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
98 auto rhoL = rhovecL.sum();
99 auto rhoV = rhovecV.sum();
100 Scalar pL = rhoL * RT - PsirL + (rhovecL.array() * PsirgradL.array()).sum(); // The (array*array).sum is a dot product
101 Scalar pV = rhoV * RT - PsirV + (rhovecV.array() * PsirgradV.array()).sum();
102 auto dpdrhovecL = RT + (hessianL * rhovecL.matrix()).array();
103 auto dpdrhovecV = RT + (hessianV * rhovecV.matrix()).array();
104
105 bool index0nonzero = rhovecL(0) > 0 && rhovecV(0) > 0;
106 bool index1nonzero = rhovecL(1) > 0 && rhovecV(1) > 0;
107
108 if (index0nonzero) {
109 r(0) = PsirgradL(0) + RT * log(rhovecL(0)) - (PsirgradV(0) + RT * log(rhovecV(0)));
110 } else {
111 r(0) = PsirgradL(0) - PsirgradV(0);
112 }
113 if (index1nonzero){
114 r(1) = PsirgradL(1) + RT * log(rhovecL(1)) - (PsirgradV(1) + RT * log(rhovecV(1)));
115 } else {
116 r(1) = PsirgradL(1) - PsirgradV(1);
117 }
118 r(2) = pL - pV;
119 r(3) = rhovecL(0) / rhovecL.sum() - xspec(0);
120
121 // Chemical potential contributions in Jacobian
122 J(0, 0) = hessianL(0, 0) + (index0nonzero ? RT / rhovecL(0) : 0);
123 J(0, 1) = hessianL(0, 1);
124 J(1, 0) = hessianL(1, 0); // symmetric, so same as above
125 J(1, 1) = hessianL(1, 1) + (index1nonzero ? RT / rhovecL(1) : 0);
126 J(0, 2) = -(hessianV(0, 0) + (index0nonzero ? RT / rhovecV(0) : 0));
127 J(0, 3) = -(hessianV(0, 1));
128 J(1, 2) = -(hessianV(1, 0)); // symmetric, so same as above
129 J(1, 3) = -(hessianV(1, 1) + (index1nonzero ? RT / rhovecV(1) : 0));
130 // Pressure contributions in Jacobian
131 J(2, 0) = dpdrhovecL(0);
132 J(2, 1) = dpdrhovecL(1);
133 J(2, 2) = -dpdrhovecV(0);
134 J(2, 3) = -dpdrhovecV(1);
135 // Mole fraction composition specification in Jacobian
136 J.row(3).array() = 0.0;
137 J(3, 0) = (rhoL - rhovecL(0)) / (rhoL * rhoL); // dxi/drhoj (j=i)
138 J(3, 1) = -rhovecL(0) / (rhoL * rhoL); // dxi/drhoj (j!=i)
139
140 // Solve for the step
141 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-r);
142
143 if ((!dx.isFinite()).all()) {
145 break;
146 }
147
148 // Constrain the step to yield only positive densities
149 if ((x.array() + dx.array() < 0).any()) {
150 // The step that would take all the concentrations to zero
151 Eigen::ArrayXd dxmax = -x;
152 // Most limiting variable is the smallest allowed
153 // before going negative
154 auto f = (dx / dxmax).minCoeff();
155 dx *= f / 2; // Only allow a step half the way to most constraining molar concentrations at most
156 }
157
158 // Don't allow changes to components with input zero mole fractions
159 for (auto i = 0; i < 2; ++i) {
160 if (xspec[i] == 0) {
161 dx[i] = 0;
162 dx[i+2] = 0;
163 }
164 }
165
166 x.array() += dx;
167
168 auto xtol_threshold = (axtol + relxtol * x.array().cwiseAbs()).eval();
169 if ((dx.array().cwiseAbs() < xtol_threshold).all()) {
171 break;
172 }
173
174 auto error_threshold = (atol + reltol * r.array().cwiseAbs()).eval();
175 if ((r.array().cwiseAbs() < error_threshold).all()) {
177 break;
178 }
179
180 // If the solution has stopped improving, stop. The change in x is equal to dx in infinite precision, but
181 // not when finite precision is involved, use the minimum non-denormal float as the determination of whether
182 // the values are done changing
183 if (((x.array() - dx.array()).cwiseAbs() < std::numeric_limits<Scalar>::min()).all()) {
185 break;
186 }
187 if (iter == maxiter - 1){
188 return_code = VLE_return_code::maxiter_met;
189 }
190 }
191 Eigen::ArrayXd rhovecLfinal = rhovecL, rhovecVfinal = rhovecV;
192 return std::make_tuple(return_code, rhovecLfinal, rhovecVfinal);
193}
194
195template<typename Model>
197{
198 const Model& model;
199 const double T, p;
200
201 hybrj_functor__mix_VLE_Tp(const Model& model, const double T, const double p) : Functor<double>(4, 4), model(model), T(T), p(p) {}
202
203 int operator()(const VectorXd& x, VectorXd& r)
204 {
205 const VectorXd::Index n = x.size() / 2;
206 Eigen::Map<const Eigen::ArrayXd> rhovecL(&(x(0)), n);
207 Eigen::Map<const Eigen::ArrayXd> rhovecV(&(x(0 + n)), n);
208 auto RT = model.get_R((rhovecL / rhovecL.sum()).eval()) * T;
209 auto [PsirL, PsirgradL, hessianL] = model.build_Psir_fgradHessian_autodiff(T, rhovecL);
210 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
211 auto rhoL = rhovecL.sum();
212 auto rhoV = rhovecV.sum();
213 Scalar pL = rhoL * RT - PsirL + (rhovecL.array() * PsirgradL.array()).sum(); // The (array*array).sum is a dot product
214 Scalar pV = rhoV * RT - PsirV + (rhovecV.array() * PsirgradV.array()).sum();
215
216 bool index0nonzero = rhovecL(0) > 0 && rhovecV(0) > 0;
217 bool index1nonzero = rhovecL(1) > 0 && rhovecV(1) > 0;
218
219 if (index0nonzero) {
220 r(0) = PsirgradL(0) + RT * log(rhovecL(0)) - (PsirgradV(0) + RT * log(rhovecV(0)));
221 }
222 else {
223 r(0) = PsirgradL(0) - PsirgradV(0);
224 }
225 if (index1nonzero) {
226 r(1) = PsirgradL(1) + RT * log(rhovecL(1)) - (PsirgradV(1) + RT * log(rhovecV(1)));
227 }
228 else {
229 r(1) = PsirgradL(1) - PsirgradV(1);
230 }
231 r(2) = (pV - p) / p;
232 r(3) = (pL - p) / p;
233 return 0;
234 }
235 int df(const VectorXd& x, MatrixXd& J)
236 {
237 const VectorXd::Index n = x.size() / 2;
238 Eigen::Map<const Eigen::ArrayXd> rhovecL(&(x(0)), n);
239 Eigen::Map<const Eigen::ArrayXd> rhovecV(&(x(0 + n)), n);
240 assert(J.rows() == 2*n);
241 assert(J.cols() == 2*n);
242
243 auto RT = model.get_R((rhovecL / rhovecL.sum()).eval()) * T;
244 auto [PsirL, PsirgradL, hessianL] = model.build_Psir_fgradHessian_autodiff(T, rhovecL);
245 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
246 auto dpdrhovecL = RT + (hessianL * rhovecL.matrix()).array();
247 auto dpdrhovecV = RT + (hessianV * rhovecV.matrix()).array();
248
249 bool index0nonzero = rhovecL(0) > 0 && rhovecV(0) > 0;
250 bool index1nonzero = rhovecL(1) > 0 && rhovecV(1) > 0;
251
252 // Chemical potential contributions in Jacobian
253 J(0, 0) = hessianL(0, 0) + (index0nonzero ? RT / rhovecL(0) : 0);
254 J(0, 1) = hessianL(0, 1);
255 J(1, 0) = hessianL(1, 0); // symmetric, so same as above
256 J(1, 1) = hessianL(1, 1) + (index1nonzero ? RT / rhovecL(1) : 0);
257 J(0, 2) = -(hessianV(0, 0) + (index0nonzero ? RT / rhovecV(0) : 0));
258 J(0, 3) = -(hessianV(0, 1));
259 J(1, 2) = -(hessianV(1, 0)); // symmetric, so same as above
260 J(1, 3) = -(hessianV(1, 1) + (index1nonzero ? RT / rhovecV(1) : 0));
261 // Pressure contributions in Jacobian
262 J(2, 0) = 0;
263 J(2, 1) = 0;
264 J(2, 2) = dpdrhovecV(0) / p;
265 J(2, 3) = dpdrhovecV(1) / p;
266 // Other pressure specification in Jacobian
267 J.row(3).array() = 0.0;
268 J(3, 0) = dpdrhovecL(0) / p;
269 J(3, 1) = dpdrhovecL(1) / p;
270 return 0;
271 }
272};
273
274/***
275* \brief Do a vapor-liquid phase equilibrium problem for a binary mixture with temperature and pressure specified
276*
277* The mole concentrations are solved for to give the right pressure
278*
279* \param model The model to operate on
280* \param T Temperature
281* \param pgiven Given pressure
282* \param rhovecL0 Initial values for liquid mole concentrations
283* \param rhovecV0 Initial values for vapor mole concentrations
284* \param flags Flags controlling the iteration and stopping conditions
285*/
286
287inline auto mix_VLE_Tp(const AbstractModel& model, double T, double pgiven, const Eigen::ArrayXd& rhovecL0, const Eigen::ArrayXd& rhovecV0, const std::optional<MixVLETpFlags>& flags_ = std::nullopt) {
288
289 auto flags = flags_.value_or(MixVLETpFlags{});
290
291 const Eigen::Index N = rhovecL0.size();
292 auto lengths = (Eigen::ArrayX<Eigen::Index>(2) << rhovecL0.size(), rhovecV0.size()).finished();
293 if (lengths.minCoeff() != lengths.maxCoeff()) {
294 throw InvalidArgument("lengths of rhovecs must be the same in mix_VLE_Tx");
295 }
296 Eigen::VectorXd x(2*N, 1);
297 x.col(0).array().head(N) = rhovecL0;
298 x.col(0).array().tail(N) = rhovecV0;
299
301 std::string message = "";
302
304 FunctorType functor(model, T, pgiven);
305 Eigen::VectorXd initial_r(2 * N); initial_r.setZero();
306 functor(x, initial_r);
307
308 bool success = false;
309 bool powell = false;
310
311 /*Eigen::MatrixXd J(2*N, 2*N), J2(2*N, 2*N);
312 Eigen::VectorXd dxvec = 1e-4*final_r.array();
313 for (auto i = 0; i < 4; ++i){
314 Eigen::VectorXd xplus = x, rplus(4);
315 xplus[i] += 1e-4*x[i];
316 functor(xplus, rplus);
317 J2.col(i) = (rplus.array() - final_r.array()) / (1e-4*x[i]);
318 }
319 functor.df(x, J);
320 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-final_r);*/
321
322 Eigen::Index niter = 0, nfev = 0;
323 Eigen::MatrixXd J(2 * N, 2 * N);
324 if (powell) {
325 HybridNonLinearSolver<FunctorType> solver(functor);
326
327 solver.diag.setConstant(2 * N, 1.);
328 solver.useExternalScaling = true;
329 auto info = solver.solve(x);
330
331 using e = Eigen::HybridNonLinearSolverSpace::Status;
332 success = (info == e::RelativeErrorTooSmall || info == e::TolTooSmall);
333 switch (info) {
334 case e::ImproperInputParameters:
336 case e::RelativeErrorTooSmall:
338 case e::TooManyFunctionEvaluation:
339 return_code = VLE_return_code::maxfev_met;
340 case e::TolTooSmall:
342 //NotMakingProgressJacobian = 4,
343 //NotMakingProgressIterations = 5,
344 default:
345 return_code = VLE_return_code::unset;
346 }
347 niter = solver.iter;
348 nfev = solver.nfev;
349 }
350 else {
351 for (auto iter = 0; iter < flags.maxiter; ++iter) {
352 Eigen::VectorXd rv(2 * N); rv.setZero();
353 functor(x, rv);
354 functor.df(x, J);
355 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-rv);
356 if ((x.array() + dx.array() < 0).any()) {
357 // The step that would take all the concentrations to zero
358 Eigen::ArrayXd dxmax = -x;
359 // Most limiting variable is the smallest allowed
360 // before going negative
361 auto f = (dx/dxmax).minCoeff();
362 dx *= f/2; // Only allow a step half the way to most constraining molar concentrations at most
363 }
364 x.array() += dx.array();
365 niter = iter;
366 nfev = iter;
367 }
368 }
369 Eigen::VectorXd final_r(2 * N); final_r.setZero();
370 functor(x, final_r);
371
372 Eigen::Map<const Eigen::ArrayXd> rhovecL(&(x(0)), N);
373 Eigen::Map<const Eigen::ArrayXd> rhovecV(&(x(0 + N)), N);
374
375 MixVLEReturn r;
376 r.return_code = return_code;
377 r.num_iter = static_cast<int>(niter);
378 r.num_fev = static_cast<int>(nfev);
379 r.r = final_r;
380 r.initial_r = initial_r;
381 r.success = success;
382 r.rhovecL = rhovecL;
383 r.rhovecV = rhovecV;
384 r.T = T;
385 return r;
386}
387
388/***
389* \brief Do vapor-liquid phase equilibrium problem at specified pressure and mole fractions in the bulk phase
390* \param model The model to operate on
391* \param p_spec Specified pressure
392* \param xmolar_spec Specified mole fractions for all components in the bulk phase
393* \param T0 Initial temperature
394* \param rhovecL0 Initial values for liquid mole concentrations
395* \param rhovecV0 Initial values for vapor mole concentrations
396
397* \param flags Additional flags
398*/
399inline auto mixture_VLE_px(const AbstractModel& model, double p_spec, const Eigen::ArrayXd& xmolar_spec, double T0, const Eigen::ArrayXd& rhovecL0, const Eigen::ArrayXd& rhovecV0, const std::optional<MixVLEpxFlags>& flags_ = std::nullopt) {
400 using Scalar = double;
401
402 auto flags = flags_.value_or(MixVLEpxFlags{});
403
404 const Eigen::Index N = rhovecL0.size();
405 auto lengths = (Eigen::ArrayX<Eigen::Index>(3) << rhovecL0.size(), rhovecV0.size(), xmolar_spec.size()).finished();
406 if (lengths.minCoeff() != lengths.maxCoeff()) {
407 throw InvalidArgument("lengths of rhovecs and xspec must be the same in mixture_VLE_px");
408 }
409 if ((rhovecV0 == 0).any()) {
410 throw InvalidArgument("Infinite dilution is not allowed for rhovecV0 in mixture_VLE_px");
411 }
412 if ((rhovecL0 == 0).any()) {
413 throw InvalidArgument("Infinite dilution is not allowed for rhovecL0 in mixture_VLE_px");
414 }
415 Eigen::MatrixXd J(2*N+1, 2*N+1); J.setZero();
416 Eigen::VectorXd r(2*N + 1), x(2*N + 1);
417 x(0) = T0;
418 x.segment(1, N) = rhovecL0;
419 x.tail(N) = rhovecV0;
420
421 Eigen::Map<Eigen::ArrayXd> rhovecL(&(x(1)), N);
422 Eigen::Map<Eigen::ArrayXd> rhovecV(&(x(1 + N)), N);
423
424 double T = T0;
425
427
428 for (int iter = 0; iter < flags.maxiter; ++iter) {
429
430 auto RL = model.get_R(xmolar_spec);
431 auto RLT = RL * T;
432 auto RVT = RLT; // Note: this should not be exactly the same if you use mole-fraction-weighted gas constants
433
434 // calculations from the EOS in the isochoric thermodynamics formalism
435 auto [PsirL, PsirgradL, hessianL] = model.build_Psir_fgradHessian_autodiff(T, rhovecL);
436 auto [PsirV, PsirgradV, hessianV] = model.build_Psir_fgradHessian_autodiff(T, rhovecV);
437 auto DELTAdmu_dT_res = (model.build_d2PsirdTdrhoi_autodiff(T, rhovecL.eval())
438 - model.build_d2PsirdTdrhoi_autodiff(T, rhovecV.eval())).eval();
439
440 auto make_diag = [](const Eigen::ArrayXd& v) -> Eigen::ArrayXXd {
441 Eigen::MatrixXd A = Eigen::MatrixXd::Identity(v.size(), v.size());
442 A.diagonal() = v;
443 return A;
444 };
445 auto HtotL = (hessianL.array() + make_diag(RLT/rhovecL)).eval();
446 auto HtotV = (hessianV.array() + make_diag(RVT/rhovecV)).eval();
447
448 auto rhoL = rhovecL.sum();
449 auto rhoV = rhovecV.sum();
450 Scalar pL = rhoL * RLT - PsirL + (rhovecL.array() * PsirgradL.array()).sum(); // The (array*array).sum is a dot product
451 Scalar pV = rhoV * RVT - PsirV + (rhovecV.array() * PsirgradV.array()).sum();
452 auto dpdrhovecL = RLT + (hessianL * rhovecL.matrix()).array();
453 auto dpdrhovecV = RVT + (hessianV * rhovecV.matrix()).array();
454
455 auto DELTA_dchempot_dT = (DELTAdmu_dT_res + RL*log(rhovecL/rhovecV)).eval();
456
457 // First N equations are equalities of chemical potentials in both phases
458 r.head(N) = PsirgradL + RLT*log(rhovecL) - (PsirgradV + RVT*log(rhovecV));
459 // Next two are pressures in each phase equaling the specification
460 r(N) = pL/p_spec - 1;
461 r(N+1) = pV/p_spec - 1;
462 // Remainder are N-1 mole fraction equalities in the liquid phase
463 r.tail(N-1) = (rhovecL/rhovecL.sum()).head(N-1) - xmolar_spec.head(N-1);
464 // So in total we have N + 2 + (N-1) = 2*N+1 equations and 2*N+1 independent variables
465
466 // Columns in Jacobian are: [T, rhovecL, rhovecV]
467 // ...
468 // N Chemical potential contributions in Jacobian (indices 0 to N-1)
469 J.block(0, 0, N, 1) = DELTA_dchempot_dT;
470 J.block(0, 1, N, N) = HtotL; // These are the concentration derivatives
471 J.block(0, N+1, N, N) = -HtotV; // These are the concentration derivatives
472 // Pressure contributions in Jacobian
473 J(N, 0) = model.get_dpdT_constrhovec(T, rhovecL)/p_spec;
474 J.block(N, 1, 1, N) = dpdrhovecL.transpose()/p_spec;
475 // No vapor concentration derivatives
476 J(N+1, 0) = model.get_dpdT_constrhovec(T, rhovecV)/p_spec;
477 // No liquid concentration derivatives
478 J.block(N+1, N+1, 1, N) = dpdrhovecV.transpose()/p_spec;
479 // Mole fraction contributions in Jacobian
480 // dxi/drhoj = (rho*Kronecker(i,j)-rho_i)/rho^2 since x_i = rho_i/rho
481 //
482 Eigen::ArrayXXd AA = rhovecL.matrix().reshaped(N, 1).replicate(1, N).array();
483 Eigen::MatrixXd M = ((rhoL * Eigen::MatrixXd::Identity(N, N).array() - AA) / (rhoL * rhoL));
484 J.block(N+2, 1, N-1, N) = M.block(0,0,N-1,N);
485
486 // Solve for the step
487 Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-r);
488
489 if ((!dx.isFinite()).all()) {
491 break;
492 }
493
494 T += dx(0);
495 x.tail(2*N).array() += dx.tail(2*N);
496
497 auto xtol_threshold = (flags.axtol + flags.relxtol * x.array().cwiseAbs()).eval();
498 if ((dx.array().cwiseAbs() < xtol_threshold).all()) {
500 break;
501 }
502
503 auto error_threshold = (flags.atol + flags.reltol * r.array().cwiseAbs()).eval();
504 if ((r.array().cwiseAbs() < error_threshold).all()) {
506 break;
507 }
508
509 // If the solution has stopped improving, stop. The change in x is equal to dx in infinite precision, but
510 // not when finite precision is involved, use the minimum non-denormal float as the determination of whether
511 // the values are done changing
512 if (((x.array() - dx.array()).cwiseAbs() < std::numeric_limits<Scalar>::min()).all()) {
514 break;
515 }
516 if (iter == flags.maxiter - 1) {
517 return_code = VLE_return_code::maxiter_met;
518 }
519 }
520 Eigen::ArrayXd rhovecLfinal = rhovecL, rhovecVfinal = rhovecV;
521 return std::make_tuple(return_code, T, rhovecLfinal, rhovecVfinal);
522}
523
524inline auto get_drhovecdp_Tsat(const AbstractModel& model, const double &T, const Eigen::ArrayXd& rhovecL, const Eigen::ArrayXd& rhovecV) {
525 //tic = timeit.default_timer();
526 using Scalar = double;
527 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hliq = model.build_Psi_Hessian_autodiff(T, rhovecL).eval();
528 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hvap = model.build_Psi_Hessian_autodiff(T, rhovecV).eval();
529 //Hvap[~np.isfinite(Hvap)] = 1e20;
530 //Hliq[~np.isfinite(Hliq)] = 1e20;
531
532 auto N = rhovecL.size();
533 Eigen::MatrixXd A = decltype(Hliq)::Zero(N, N);
534 auto b = decltype(Hliq)::Ones(N, 1);
535 decltype(Hliq) drhodp_liq, drhodp_vap;
536 assert(rhovecL.size() == rhovecV.size());
537 if ((rhovecL != 0).all() && (rhovecV != 0).all()) {
538 // Normal treatment for all concentrations not equal to zero
539 A(0, 0) = Hliq.row(0).dot(rhovecV.matrix());
540 A(0, 1) = Hliq.row(1).dot(rhovecV.matrix());
541 A(1, 0) = Hliq.row(0).dot(rhovecL.matrix());
542 A(1, 1) = Hliq.row(1).dot(rhovecL.matrix());
543
544 drhodp_liq = linsolve(A, b);
545 drhodp_vap = linsolve(Hvap, Hliq*drhodp_liq);
546 }
547 else{
548 // Special treatment for infinite dilution
549 auto murL = model.build_Psir_gradient_autodiff(T, rhovecL);
550 auto murV = model.build_Psir_gradient_autodiff(T, rhovecV);
551 auto RL = model.get_R(rhovecL / rhovecL.sum());
552 auto RV = model.get_R(rhovecV / rhovecV.sum());
553
554 // First, for the liquid part
555 for (auto i = 0; i < N; ++i) {
556 for (auto j = 0; j < N; ++j) {
557 if (rhovecL[j] == 0) {
558 // Analysis is special if j is the index that is a zero concentration.If you are multiplying by the vector
559 // of liquid concentrations, a different treatment than the case where you multiply by the vector
560 // of vapor concentrations is required
561 // ...
562 // Initial values
563 auto Aij = (Hliq.row(j).array().cwiseProduct(((i == 0) ? rhovecV : rhovecL).array().transpose())).eval(); // coefficient - wise product
564 // A throwaway boolean for clarity
565 bool is_liq = (i == 1);
566 // Apply correction to the j term (RT if liquid, RT*phi for vapor)
567 Aij[j] = (is_liq) ? RL*T : RL*T*exp(-(murV[j] - murL[j])/(RL*T));
568 // Fill in entry
569 A(i, j) = Aij.sum();
570 }
571 else{
572 // Normal
573 A(i, j) = Hliq.row(j).dot(((i==0) ? rhovecV : rhovecL).matrix());
574 }
575 }
576 }
577 drhodp_liq = linsolve(A, b);
578
579 // Then, for the vapor part, also requiring special treatment
580 // Left - multiplication of both sides of equation by diagonal matrix with liquid concentrations along diagonal, all others zero
581 auto diagrhovecL = rhovecL.matrix().asDiagonal();
582 auto PSIVstar = (diagrhovecL*Hvap).eval();
583 auto PSILstar = (diagrhovecL*Hliq).eval();
584 for (auto j = 0; j < N; ++j) {
585 if (rhovecL[j] == 0) {
586 PSILstar(j, j) = RL*T;
587 PSIVstar(j, j) = RV*T/exp(-(murV[j] - murL[j]) / (RV * T));
588 }
589 }
590 drhodp_vap = linsolve(PSIVstar, PSILstar*drhodp_liq);
591 }
592 return std::make_tuple(drhodp_liq, drhodp_vap);
593}
594
598inline auto get_drhovecdT_psat(const AbstractModel& model, const double &T, const Eigen::ArrayXd& rhovecL, const Eigen::ArrayXd& rhovecV) {
599 using Scalar = double;
600 if (rhovecL.size() != 2) { throw std::invalid_argument("Binary mixtures only"); }
601 assert(rhovecL.size() == rhovecV.size());
602
603 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hliq = model.build_Psi_Hessian_autodiff(T, rhovecL).eval();
604 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hvap = model.build_Psi_Hessian_autodiff(T, rhovecV).eval();
605
606 auto N = rhovecL.size();
607 Eigen::MatrixXd A = decltype(Hliq)::Zero(N, N);
608 Eigen::MatrixXd b = decltype(Hliq)::Ones(N, 1);
609 decltype(Hliq) drhovecdT_liq, drhovecdT_vap;
610 assert(rhovecL.size() == rhovecV.size());
611
612 if ((rhovecL != 0).all() && (rhovecV != 0).all()) {
613 // Normal treatment for all concentrations not equal to zero
614 A(0, 0) = Hliq.row(0).dot(rhovecV.matrix());
615 A(0, 1) = Hliq.row(1).dot(rhovecV.matrix());
616 A(1, 0) = Hliq.row(0).dot(rhovecL.matrix());
617 A(1, 1) = Hliq.row(1).dot(rhovecL.matrix());
618
619 auto DELTAdmu_dT = (model.get_dchempotdT_autodiff(T, rhovecV) - model.get_dchempotdT_autodiff(T, rhovecL)).eval();
620 b(0) = DELTAdmu_dT.matrix().dot(rhovecV.matrix()) - model.get_dpdT_constrhovec(T, rhovecV);
621 b(1) = -model.get_dpdT_constrhovec(T, rhovecL);
622 // Calculate the derivatives of the liquid phase
623 drhovecdT_liq = linsolve(A, b);
624 // Calculate the derivatives of the vapor phase
625 drhovecdT_vap = linsolve(Hvap, ((Hliq*drhovecdT_liq).array() - DELTAdmu_dT.array()).eval());
626 }
627 else{
628 // Special treatment for infinite dilution
629 auto murL = model.build_Psir_gradient_autodiff(T, rhovecL);
630 auto murV = model.build_Psir_gradient_autodiff(T, rhovecV);
631 auto RL = model.get_R(rhovecL / rhovecL.sum());
632 auto RV = model.get_R(rhovecV / rhovecV.sum());
633
634 // The dot product contains terms of the type:
635 // rho'_i (R ln(rho"_i /rho'_i) + d mu ^ r"_i/d T - d mu^r'_i/d T)
636
637 // Residual contribution to the difference in temperature derivative of chemical potential
638 // It should be fine to evaluate with zero densities:
639 auto DELTAdmu_dT_res = (model.build_d2PsirdTdrhoi_autodiff(T, rhovecV) - model.build_d2PsirdTdrhoi_autodiff(T, rhovecL)).eval();
640 // Now the ideal-gas part causes trouble, so multiply by the rhovec, once with liquid, another with vapor
641 // Start off with the assumption that the rhovec is all positive (fix elements later)
642 auto DELTAdmu_dT_rhoV_ideal = (rhovecV*(RV*log(rhovecV/rhovecL))).eval();
643 auto DELTAdmu_dT_ideal = (RV*log(rhovecV/rhovecL)).eval();
644 // Zero out contributions where a concentration is zero
645 for (auto i = 0; i < rhovecV.size(); ++i) {
646 if (rhovecV[i] == 0) {
647 DELTAdmu_dT_rhoV_ideal(i) = 0;
648 DELTAdmu_dT_ideal(i) = 0;
649 }
650 }
651 double DELTAdmu_dT_rhoV = rhovecV.matrix().dot(DELTAdmu_dT_res.matrix()) + DELTAdmu_dT_rhoV_ideal.sum();
652
653 b(0) = DELTAdmu_dT_rhoV - model.get_dpdT_constrhovec(T, rhovecV);
654 b(1) = -model.get_dpdT_constrhovec(T, rhovecL);
655
656 // First, for the liquid part
657 for (auto i = 0; i < N; ++i) {
658 // A throwaway boolean for clarity
659 bool is_liq = (i == 1);
660 for (auto j = 0; j < N; ++j) {
661 auto rhovec = (is_liq) ? rhovecL : rhovecV;
662 // Initial values
663 auto Aij = (Hliq.row(j).array().cwiseProduct(rhovec.array().transpose())).eval(); // coefficient - wise product
664 // Only rows in H that have a divergent entry in a column need to be adjusted
665 if (!(Hliq.row(j)).array().isFinite().all()) {
666 // A correction is needed in the entry in Aij corresponding to entry for zero concentration
667 if (rhovec[j] == 0) {
668 // Apply correction to the j term (RT if liquid, RT*phi for vapor)
669 Aij[j] = (is_liq) ? RL * T : RL * T * exp(-(murV[j] - murL[j]) / (RL * T));
670 }
671 }
672
673 // Fill in entry
674 A(i, j) = Aij.sum();
675 }
676 }
677 drhovecdT_liq = linsolve(A, b);
678
679 // Then, for the vapor part, also requiring special treatment
680 // Left-multiplication of both sides of equation by diagonal matrix with
681 // liquid concentrations along diagonal, all others zero
682 auto diagrhovecL = rhovecL.matrix().asDiagonal();
683 auto Hvapstar = (diagrhovecL*Hvap).eval();
684 auto Hliqstar = (diagrhovecL*Hliq).eval();
685 for (auto j = 0; j < N; ++j) {
686 if (rhovecL[j] == 0) {
687 Hliqstar(j, j) = RL*T;
688 Hvapstar(j, j) = RV*T/ exp((murL[j] - murV[j]) / (RV * T)); // Note not as given in Deiters
689 }
690 }
691 auto diagrhovecL_dot_DELTAdmu_dT = (diagrhovecL*(DELTAdmu_dT_res+DELTAdmu_dT_ideal).matrix()).array();
692 auto RHS = ((Hliqstar * drhovecdT_liq).array() - diagrhovecL_dot_DELTAdmu_dT).eval();
693 drhovecdT_vap = linsolve(Hvapstar.eval(), RHS);
694 }
695 return std::make_tuple(drhovecdT_liq, drhovecdT_vap);
696}
697
698/***
699* \brief Derivative of molar concentration vectors w.r.t. T along an isopleth of the phase envelope for binary mixtures
700*
701* The liquid phase will have its mole fractions held constant
702*
703* \f[
704* \left(\frac{d \vec\rho' }{d T}\right)_{x',\sigma} = \frac{\Delta s\dot \rho''-\Delta\beta_{\rho}}{(\Psi'\Delta\rho)\dot x')}x'
705* \f]
706*
707* See Eq 15 and 16 of Deiters and Bell, AICHEJ: https://doi.org/10.1002/aic.16730
708*
709* To keep the vapor mole fraction constant, just swap the input molar concentrations to this function, the first concentration
710* vector is always the one with fixed mole fractions
711*/
712inline auto get_drhovecdT_xsat(const AbstractModel& model, const double& T, const Eigen::ArrayXd& rhovecL, const Eigen::ArrayXd& rhovecV) {
713 using Scalar = double;
714 if (rhovecL.size() != 2) { throw std::invalid_argument("Binary mixtures only"); }
715 assert(rhovecL.size() == rhovecV.size());
716
717 Eigen::ArrayXd molefracL = rhovecL / rhovecL.sum();
718 Eigen::ArrayXd deltas = (model.get_dchempotdT_autodiff(T, rhovecV) - model.get_dchempotdT_autodiff(T, rhovecL)).eval();
719 Scalar deltabeta = (model.get_dpdT_constrhovec(T, rhovecV)- model.get_dpdT_constrhovec(T, rhovecL));
720 Eigen::ArrayXd deltarho = (rhovecV - rhovecL).eval();
721
722 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hliq = model.build_Psi_Hessian_autodiff(T, rhovecL).eval();
723 Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> Hvap = model.build_Psi_Hessian_autodiff(T, rhovecV).eval();
724
725 Eigen::MatrixXd drhodT_liq, drhodT_vap;
726 if ((rhovecL != 0).all() && (rhovecV != 0).all()) {
727 auto num = (deltas.matrix().dot(rhovecV.matrix()) - deltabeta); // numerator, a scalar
728 auto den = (Hliq*(deltarho.matrix())).dot(molefracL.matrix()); // denominator, a scalar
729 drhodT_liq = num/den*molefracL;
730 drhodT_vap = linsolve(Hvap, ((Hliq * drhodT_liq).array() - deltas.array()).eval());
731 }
732 else {
733 throw std::invalid_argument("Infinite dilution not yet supported");
734 }
735 return std::make_tuple(drhodT_liq, drhodT_vap);
736}
737
763template<typename Model = AbstractModel>
764auto get_dpsat_dTsat_isopleth(const Model& model, const double& T, const Eigen::ArrayXd& rhovecL, const Eigen::ArrayXd& rhovecV) {
765
766 // Derivative along phase envelope at constant composition (correct, tested)
767 auto [drhovecLdT_xsat, drhovecVdT_xsat] = get_drhovecdT_xsat(model, T, rhovecL, rhovecV);
768 // And the derivative of the total density
769 auto drhoLdT_sat = drhovecLdT_xsat.sum();
770
771 double rhoL = rhovecL.sum();
772 auto molefracL = rhovecL / rhoL;
773 auto RT = model.get_R(molefracL) * T;
774 auto derivs = model.get_Ar02n(T, rhoL, molefracL);
775 auto dpdrho = RT*(1 + 2 * derivs[1] + derivs[2]);
776 double dpdT = model.get_R(molefracL) * rhoL * (1 + derivs[1] - model.get_Ar11(T, rhoL, molefracL));
777 auto der = dpdT + dpdrho * drhoLdT_sat;
778 return der;
779
780 // How to do this derivative with isochoric formalism
781 //using iso = IsochoricDerivatives<Model, Scalar, VecType>;
782 //auto [PsirL, PsirgradL, hessianL] = iso::build_Psir_fgradHessian_autodiff(model, T, rhovecL);
783 //auto dpdrhovecL = (RT + (hessianL * rhovecL.matrix()).array()).eval();
784 //auto der = (dpdrhovecL * drhovecLdT_xsat.array()).sum() + dpdT;
785 //return der;
786}
787
788/***
789 * \brief Trace an isotherm with parametric tracing
790 * \ note If options.revision is 2, the data will be returned in the "data" field, otherwise the data will be returned as root array
791*/
792inline auto trace_VLE_isotherm_binary(const AbstractModel &model, double T, const Eigen::ArrayXd& rhovecL0, const Eigen::ArrayXd& rhovecV0, const std::optional<TVLEOptions>& options = std::nullopt)
793{
794 // Get the options, or the default values if not provided
795 TVLEOptions opt = options.value_or(TVLEOptions{});
796 auto N = rhovecL0.size();
797 if (N != 2) {
798 throw InvalidArgument("Size must be 2");
799 }
800 if (rhovecL0.size() != rhovecV0.size()) {
801 throw InvalidArgument("Both molar concentration arrays must be of the same size");
802 }
803
804 auto norm = [](const auto& v) { return (v * v).sum(); };
805
806 // Define datatypes and functions for tracing tools
807 auto JSONdata = nlohmann::json::array();
808
809 // Typedefs for the types
810 using namespace boost::numeric::odeint;
811 using state_type = std::vector<double>;
812
813 // Class for simple Euler integration
814 euler<state_type> eul;
815 typedef runge_kutta_cash_karp54< state_type > error_stepper_type;
816 typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type;
817
818 // Define the tolerances
819 double abs_err = opt.abs_err, rel_err = opt.rel_err, a_x = 1.0, a_dxdt = 1.0;
820 controlled_stepper_type controlled_stepper(default_error_checker< double, range_algebra, default_operations >(abs_err, rel_err, a_x, a_dxdt));
821
822 // Start off with the direction determined by c
823 double c = opt.init_c;
824
825 // Set up the initial state vector
826 state_type x0(2 * N), last_drhodt(2 * N), previous_drhodt(2 * N);
827 auto set_init_state = [&](state_type& X) {
828 auto rhovecL = Eigen::Map<Eigen::ArrayXd>(&(X[0]), N);
829 auto rhovecV = Eigen::Map<Eigen::ArrayXd>(&(X[0]) + N, N);
830 rhovecL = rhovecL0;
831 rhovecV = rhovecV0;
832 };
833 set_init_state(x0);
834
835 // The function to be integrated by odeint
836 auto xprime = [&](const state_type& X, state_type& Xprime, double /*t*/) {
837 // Memory maps into the state vector for inputs and their derivatives
838 // These are views, not copies!
839 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(X[0]), N);
840 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(X[0]) + N, N);
841 auto drhovecdtL = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]), N);
842 auto drhovecdtV = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]) + N, N);
843 // Get the derivatives with respect to pressure along the isotherm of the phase envelope
844 auto [drhovecdpL, drhovecdpV] = get_drhovecdp_Tsat(model, T, rhovecL, rhovecV);
845 // Get the derivative of p w.r.t. parameter
846 auto dpdt = 1.0/sqrt(norm(drhovecdpL.array()) + norm(drhovecdpV.array()));
847 // And finally the derivatives with respect to the tracing variable
848 drhovecdtL = c*drhovecdpL*dpdt;
849 drhovecdtV = c*drhovecdpV*dpdt;
850
851 if (previous_drhodt.empty()) {
852 return;
853 }
854
855 // Flip the step if it changes direction from the smooth continuation of previous steps
856 auto get_const_view = [&](const auto& v, Eigen::Index N) {
857 return Eigen::Map<const Eigen::ArrayXd>(&(v[0]), N);
858 };
859 if (get_const_view(Xprime, N).matrix().dot(get_const_view(previous_drhodt, N).matrix()) < 0) {
860 auto Xprimeview = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]), 2*N);
861 Xprimeview *= -1;
862 }
863 };
864
865 // Figure out which direction to trace initially
866 double t = 0, dt = opt.init_dt;
867 {
868 auto dxdt = x0;
869 xprime(x0, dxdt, -1.0);
870 const auto dXdt = Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]), dxdt.size());
871 const auto X = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), x0.size());
872
873 const Eigen::ArrayXd step = X + dXdt * dt;
874 Eigen::ArrayX<bool> negativestepvals = (step < 0).eval();
875 // Flip the sign if the first step would yield any negative concentrations
876 if (negativestepvals.any()) {
877 c *= -1;
878 }
879 }
880 std::string termination_reason;
881
882 // Then trace...
883 int retry_count = 0;
884 for (auto istep = 0; istep < opt.max_steps; ++istep) {
885
886 auto store_point = [&]() {
888 auto N = x0.size() / 2;
889 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), N);
890 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + N, N);
891 auto rhototL = rhovecL.sum(), rhototV = rhovecV.sum();
892 double pL = rhototL * model.get_R(rhovecL / rhovecL.sum())*T + model.get_pr(T, rhovecL);
893 double pV = rhototV * model.get_R(rhovecV / rhovecV.sum())*T + model.get_pr(T, rhovecV);
894
895 // Store the derivative
896 try {
897 xprime(x0, last_drhodt, -1.0);
898 }
899 catch (...) {
900 std::cout << "Something bad happened; couldn't calculate xprime in store_point" << std::endl;
901 }
902
903 // Store the data in a JSON structure
904 nlohmann::json point = {
905 {"t", t},
906 {"dt", dt},
907 {"T / K", T},
908 {"pL / Pa", pL},
909 {"pV / Pa", pV},
910 {"c", c},
911 {"rhoL / mol/m^3", rhovecL},
912 {"rhoV / mol/m^3", rhovecV},
913 {"xL_0 / mole frac.", rhovecL[0]/rhovecL.sum()},
914 {"xV_0 / mole frac.", rhovecV[0]/rhovecV.sum()},
915 {"drho/dt", last_drhodt}
916 };
917 if (opt.calc_criticality) {
918 point["crit. conditions L"] = model.get_criticality_conditions(T, rhovecL);
919 point["crit. conditions V"] = model.get_criticality_conditions(T, rhovecV);
920 }
921 JSONdata.push_back(point);
922 //std::cout << JSONdata.back().dump() << std::endl;
923 };
924 if (istep == 0 && retry_count == 0) {
925 store_point();
926 }
927
928 //double dtold = dt;
929 auto x0_previous = x0;
930
931 if (opt.integration_order == 5) {
932 controlled_step_result res = controlled_step_result::fail;
933 try {
934 res = controlled_stepper.try_step(xprime, x0, t, dt);
935 }
936 catch (...) {
937 break;
938 }
939
940 if (res != controlled_step_result::success) {
941 // Try again, with a smaller step size
942 istep--;
943 retry_count++;
944 continue;
945 }
946 else {
947 retry_count = 0;
948 }
949 // Reduce step size if greater than the specified max step size
950 dt = std::min(dt, opt.max_dt);
951 }
952 else if (opt.integration_order == 1) {
953 try {
954 eul.do_step(xprime, x0, t, dt);
955 t += dt;
956 }
957 catch (...) {
958 break;
959 }
960 }
961 else {
962 throw InvalidArgument("integration order is invalid:" + std::to_string(opt.integration_order));
963 }
964 auto stop_requested = [&]() {
966 auto N = x0.size() / 2;
967 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), N);
968 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]) + N, N);
969 auto x = rhovecL / rhovecL.sum();
970 auto y = rhovecV / rhovecV.sum();
971 double p = rhovecL.sum()*model.get_R(x)*T + model.get_pr(T, rhovecL);
972
973 // Check if the solution has gone mechanically unstable
974 if (opt.calc_criticality) {
975 auto condsL = model.get_criticality_conditions(T, rhovecL);
976 auto condsV = model.get_criticality_conditions(T, rhovecV);
977 if (condsL[0] < opt.crit_termination || condsV[0] < opt.crit_termination){
978 return true;
979 }
980 }
981 if (p > opt.p_termination){
982 return true;
983 }
984 if ((x < 0).any() || (x > 1).any() || (y < 0).any() || (y > 1).any() || (!rhovecL.isFinite()).any() || (!rhovecV.isFinite()).any()) {
985 return true;
986 }
987 else {
988 return false;
989 }
990 };
991 if (stop_requested()) {
992 break;
993 }
994 // Polish the solution
995 if (opt.polish) {
996 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), N).eval();
997 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[0 + N]), N).eval();
998 auto x = (Eigen::ArrayXd(2) << rhovecL(0) / rhovecL.sum(), rhovecL(1) / rhovecL.sum()).finished(); // Mole fractions in the liquid phase (to be kept constant)
999 auto [return_code, rhovecLnew, rhovecVnew] = model.mix_VLE_Tx(T, rhovecL, rhovecV, x, 1e-10, 1e-8, 1e-10, 1e-8, 10);
1000
1001 if (((rhovecL-rhovecLnew).cwiseAbs() > opt.polish_reltol_rho*rhovecL).any()){
1002 std::string msg;
1003 if (opt.polish_exception_on_fail){
1004 throw IterationFailure(msg);
1005 }
1006 else{
1007 if (opt.verbosity > 0){
1008 std::cout << msg << std::endl;
1009 }
1010 }
1011 }
1012 else{
1013 // If the step is accepted, copy into x again ...
1014 auto rhovecLview = Eigen::Map<Eigen::ArrayXd>(&(x0[0]), N);
1015 auto rhovecVview = Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + N, N);
1016 rhovecLview = rhovecLnew;
1017 rhovecVview = rhovecVnew;
1018 }
1019 }
1020
1021 std::swap(previous_drhodt, last_drhodt);
1022 store_point(); // last_drhodt is updated;
1023
1024 }
1025 if (opt.revision == 1){
1026 return JSONdata;
1027 }
1028 else if (opt.revision == 2){
1029 nlohmann::json meta{
1030 {"termination_reason", termination_reason}
1031 };
1032 return nlohmann::json{
1033 {"meta", meta},
1034 {"data", JSONdata}
1035 };
1036 }
1037 else
1038 {
1039 throw teqp::InvalidArgument("revision is not valid");
1040 }
1041}
1042
1043/***
1044* \brief Trace an isobar with parametric tracing
1045*/
1046template<typename Model = AbstractModel>
1047auto trace_VLE_isobar_binary(const Model& model, double p, double T0, const Eigen::ArrayXd& rhovecL0, const Eigen::ArrayXd& rhovecV0, const std::optional<PVLEOptions>& options = std::nullopt)
1048{
1049 // Get the options, or the default values if not provided
1050 PVLEOptions opt = options.value_or(PVLEOptions{});
1051 auto N = rhovecL0.size();
1052 if (N != 2) {
1053 throw InvalidArgument("Size must be 2");
1054 }
1055 if (rhovecL0.size() != rhovecV0.size()) {
1056 throw InvalidArgument("Both molar concentration arrays must be of the same size");
1057 }
1058
1059 auto norm = [](const auto& v) { return (v * v).sum(); };
1060
1061 // Define datatypes and functions for tracing tools
1062 auto JSONdata = nlohmann::json::array();
1063
1064 // Typedefs for the types
1065 using namespace boost::numeric::odeint;
1066 using state_type = std::vector<double>;
1067
1068 // Class for simple Euler integration
1069 euler<state_type> eul;
1070 typedef runge_kutta_cash_karp54< state_type > error_stepper_type;
1071 typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type;
1072
1073 // Define the tolerances
1074 double abs_err = opt.abs_err, rel_err = opt.rel_err, a_x = 1.0, a_dxdt = 1.0;
1075 controlled_stepper_type controlled_stepper(default_error_checker< double, range_algebra, default_operations >(abs_err, rel_err, a_x, a_dxdt));
1076
1077 // Start off with the direction determined by c
1078 double c = opt.init_c;
1079
1080 // Set up the initial state vector
1081 state_type x0(2*N+1), last_drhodt(2*N+1), previous_drhodt(2*N+1);
1082 auto set_init_state = [&](state_type& X) {
1083 X[0] = T0;
1084 auto rhovecL = Eigen::Map<Eigen::ArrayXd>(&(X[1]), N);
1085 auto rhovecV = Eigen::Map<Eigen::ArrayXd>(&(X[1]) + N, N);
1086 rhovecL = rhovecL0;
1087 rhovecV = rhovecV0;
1088 };
1089 set_init_state(x0);
1090
1091 // The function to be integrated by odeint
1092 auto xprime = [&](const state_type& X, state_type& Xprime, double /*t*/) {
1093 // Memory maps into the state vector for inputs and their derivatives
1094 // These are views, not copies!
1095 const double& T = X[0];
1096 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(X[1]), N);
1097 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(X[1]) + N, N);
1098 auto& dTdt = Xprime[0];
1099 auto drhovecdtL = Eigen::Map<Eigen::ArrayXd>(&(Xprime[1]), N);
1100 auto drhovecdtV = Eigen::Map<Eigen::ArrayXd>(&(Xprime[1]) + N, N);
1101 // Get the derivatives with respect to temperature along the isobar of the phase envelope
1102 auto [drhovecdTL, drhovecdTV] = get_drhovecdT_psat(model, T, rhovecL, rhovecV);
1103 // Get the derivative of T w.r.t. parameter
1104 dTdt = 1.0 / sqrt(norm(drhovecdTL.array()) + norm(drhovecdTV.array()));
1105 // And finally the derivatives with respect to the tracing variable
1106 drhovecdtL = c * drhovecdTL * dTdt;
1107 drhovecdtV = c * drhovecdTV * dTdt;
1108
1109 if (previous_drhodt.empty()) {
1110 return;
1111 }
1112
1113 // Flip the step if it changes direction from the smooth continuation of previous steps
1114 auto get_const_view = [&](const auto& v, Eigen::Index N) {
1115 return Eigen::Map<const Eigen::ArrayXd>(&(v[0]), N);
1116 };
1117 if (get_const_view(Xprime, N).matrix().dot(get_const_view(previous_drhodt, N).matrix()) < 0) {
1118 auto Xprimeview = Eigen::Map<Eigen::ArrayXd>(&(Xprime[0]), 2 * N);
1119 Xprimeview *= -1;
1120 }
1121 };
1122
1123 // Figure out which direction to trace initially
1124 double t = 0, dt = opt.init_dt;
1125 {
1126 auto dxdt = x0;
1127 xprime(x0, dxdt, -1.0);
1128 const auto dXdt = Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]), dxdt.size());
1129 const auto X = Eigen::Map<const Eigen::ArrayXd>(&(x0[0]), x0.size());
1130
1131 const Eigen::ArrayXd step = X + dXdt * dt;
1132 Eigen::ArrayX<bool> negativestepvals = (step < 0).eval();
1133 // Flip the sign if the first step would yield any negative concentrations
1134 if (negativestepvals.any()) {
1135 c *= -1;
1136 }
1137 }
1138 std::string termination_reason;
1139
1140 // Then trace...
1141 int retry_count = 0;
1142 for (auto istep = 0; istep < opt.max_steps; ++istep) {
1143
1144 auto store_point = [&]() {
1146 auto N = x0.size() / 2;
1147 double T = x0[0];
1148 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[1]), N);
1149 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[1]) + N, N);
1150 auto rhototL = rhovecL.sum(), rhototV = rhovecV.sum();
1151 double pL = rhototL * model.R(rhovecL / rhovecL.sum()) * T + model.get_pr(T, rhovecL);
1152 double pV = rhototV * model.R(rhovecV / rhovecV.sum()) * T + model.get_pr(T, rhovecV);
1153
1154 // Store the derivative
1155 try {
1156 xprime(x0, last_drhodt, -1.0);
1157 }
1158 catch (...) {
1159 std::cout << "Something bad happened; couldn't calculate xprime in store_point" << std::endl;
1160 }
1161
1162 // Store the data in a JSON structure
1163 nlohmann::json point = {
1164 {"t", t},
1165 {"dt", dt},
1166 {"T / K", T},
1167 {"pL / Pa", pL},
1168 {"pV / Pa", pV},
1169 {"c", c},
1170 {"rhoL / mol/m^3", rhovecL},
1171 {"rhoV / mol/m^3", rhovecV},
1172 {"xL_0 / mole frac.", rhovecL[0] / rhovecL.sum()},
1173 {"xV_0 / mole frac.", rhovecV[0] / rhovecV.sum()},
1174 {"drho/dt", last_drhodt}
1175 };
1176 if (opt.calc_criticality) {
1177 point["crit. conditions L"] = model.get_criticality_conditions(T, rhovecL);
1178 point["crit. conditions V"] = model.get_criticality_conditions(T, rhovecV);
1179 }
1180 JSONdata.push_back(point);
1181 //std::cout << JSONdata.back().dump() << std::endl;
1182 };
1183 if (istep == 0 && retry_count == 0) {
1184 store_point();
1185 }
1186
1187 //double dtold = dt;
1188 auto x0_previous = x0;
1189
1190 if (opt.integration_order == 5) {
1191 controlled_step_result res = controlled_step_result::fail;
1192 try {
1193 res = controlled_stepper.try_step(xprime, x0, t, dt);
1194 }
1195 catch (...) {
1196 break;
1197 }
1198
1199 if (res != controlled_step_result::success) {
1200 // Try again, with a smaller step size
1201 istep--;
1202 retry_count++;
1203 continue;
1204 }
1205 else {
1206 retry_count = 0;
1207 }
1208 // Reduce step size if greater than the specified max step size
1209 dt = std::min(dt, opt.max_dt);
1210 }
1211 else if (opt.integration_order == 1) {
1212 try {
1213 eul.do_step(xprime, x0, t, dt);
1214 t += dt;
1215 }
1216 catch (...) {
1217 break;
1218 }
1219 }
1220 else {
1221 throw InvalidArgument("integration order is invalid:" + std::to_string(opt.integration_order));
1222 }
1223 auto stop_requested = [&]() {
1225 auto N = (x0.size()-1) / 2;
1226 auto& T = x0[0];
1227 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[1]), N);
1228 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[1]) + N, N);
1229 auto x = rhovecL / rhovecL.sum();
1230 auto y = rhovecV / rhovecV.sum();
1231 // Check if the solution has gone mechanically unstable
1232 if (opt.calc_criticality) {
1233 auto condsL = model.get_criticality_conditions(T, rhovecL);
1234 auto condsV = model.get_criticality_conditions(T, rhovecV);
1235 if (condsL[0] < opt.crit_termination || condsV[0] < opt.crit_termination) {
1236 return true;
1237 }
1238 }
1239 if ((x < 0).any() || (x > 1).any() || (y < 0).any() || (y > 1).any() || (!rhovecL.isFinite()).any() || (!rhovecV.isFinite()).any()) {
1240 return true;
1241 }
1242 else {
1243 return false;
1244 }
1245 };
1246 if (stop_requested()) {
1247 break;
1248 }
1249 // Polish the solution
1250 if (opt.polish) {
1251 double T = x0[0];
1252 auto rhovecL = Eigen::Map<const Eigen::ArrayXd>(&(x0[1]), N).eval();
1253 auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(x0[1 + N]), N).eval();
1254 auto x = (Eigen::ArrayXd(2) << rhovecL(0) / rhovecL.sum(), rhovecL(1) / rhovecL.sum()).finished(); // Mole fractions in the liquid phase (to be kept constant)
1255 auto [return_code, Tnew, rhovecLnew, rhovecVnew] = model.mixture_VLE_px(p, x, T, rhovecL, rhovecV);
1256
1257 if (((rhovecL-rhovecLnew).cwiseAbs() > opt.polish_reltol_rho*rhovecL).any()){
1258 std::string msg;
1259 if (opt.polish_exception_on_fail){
1260 throw IterationFailure(msg);
1261 }
1262 else{
1263 if (opt.verbosity > 0){
1264 std::cout << msg << std::endl;
1265 }
1266 }
1267 }
1268 else{
1269 // If the step is accepted, copy into x again ...
1270 x0[0] = Tnew;
1271 auto rhovecLview = Eigen::Map<Eigen::ArrayXd>(&(x0[1]), N);
1272 auto rhovecVview = Eigen::Map<Eigen::ArrayXd>(&(x0[1]) + N, N);
1273 rhovecLview = rhovecLnew;
1274 rhovecVview = rhovecVnew;
1275 //std::cout << "[polish]: " << static_cast<int>(return_code) << ": " << rhovecLnew.sum() / rhovecL.sum() << " " << rhovecVnew.sum() / rhovecV.sum() << std::endl;
1276 }
1277 }
1278
1279 std::swap(previous_drhodt, last_drhodt);
1280 store_point(); // last_drhodt is updated;
1281
1282 }
1283 return JSONdata;
1284}
1285
1286#define VLE_FUNCTIONS_TO_WRAP \
1287 X(trace_VLE_isobar_binary) \
1288 X(trace_VLE_isotherm_binary) \
1289 X(get_dpsat_dTsat_isopleth) \
1290 X(get_drhovecdT_xsat) \
1291 X(get_drhovecdT_psat) \
1292 X(get_drhovecdp_Tsat) \
1293 X(trace_critical_arclength_binary) \
1294 X(mixture_VLE_px) \
1295 X(mix_VLE_Tp) \
1296 X(mix_VLE_Tx)
1297
1298#define X(f) template <typename TemplatedModel, typename ...Params, \
1299typename = typename std::enable_if<not std::is_base_of<teqp::cppinterface::AbstractModel, TemplatedModel>::value>::type> \
1300inline auto f(const TemplatedModel& model, Params&&... params){ \
1301 auto view = teqp::cppinterface::adapter::make_cview(model); \
1302 const AbstractModel& am = *view.get(); \
1303 return f(am, std::forward<Params>(params)...); \
1304}
1306#undef X
1307#undef VLE_FUNCTIONS_TO_WRAP
1308
1309//
1310//template <typename TemplatedModel, typename ...Params, typename = typename std::enable_if<not std::is_base_of<teqp::cppinterface::AbstractModel, TemplatedModel>::value>::type>
1311//auto get_drhovecdT_psat(const TemplatedModel& model, Params&&... params){
1312// return get_drhovecdT_psat(teqp::cppinterface::adapter::make_cview(model), std::forward<Params>(params)...);
1313//}
1314
1315}; /* namespace teqp*/
#define VLE_FUNCTIONS_TO_WRAP
Definition VLE.hpp:1286
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 get_dchempotdT_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
virtual std::tuple< VLE_return_code, EArrayd, EArrayd > mix_VLE_Tx(const double T, const REArrayd &rhovecL0, const REArrayd &rhovecV0, const REArrayd &xspec, const double atol, const double reltol, const double axtol, const double relxtol, const int maxiter) const
virtual EArrayd build_Psir_gradient_autodiff(const double T, const EArrayd &rhovec) const =0
virtual double get_pr(const double T, const EArrayd &rhovec) const =0
virtual std::tuple< double, Eigen::ArrayXd, Eigen::MatrixXd > build_Psir_fgradHessian_autodiff(const double T, const EArrayd &rhovec) const =0
#define X(f)
auto get_dpsat_dTsat_isopleth(const Model &model, const double &T, const Eigen::ArrayXd &rhovecL, const Eigen::ArrayXd &rhovecV)
Derivative of pressure w.r.t. temperature along the isopleth of a phase envelope (at constant composi...
Definition VLE.hpp:764
auto linsolve(const A &a, const B &b)
Definition VLE.hpp:55
auto trace_VLE_isotherm_binary(const AbstractModel &model, double T, const Eigen::ArrayXd &rhovecL0, const Eigen::ArrayXd &rhovecV0, const std::optional< TVLEOptions > &options=std::nullopt)
Definition VLE.hpp:792
auto mix_VLE_Tx(const AbstractModel &model, double T, const Eigen::ArrayXd &rhovecL0, const Eigen::ArrayXd &rhovecV0, const Eigen::ArrayXd &xspec, double atol, double reltol, double axtol, double relxtol, int maxiter)
Definition VLE.hpp:76
VLE_return_code
Definition VLE_types.hpp:40
auto get_drhovecdT_xsat(const AbstractModel &model, const double &T, const Eigen::ArrayXd &rhovecL, const Eigen::ArrayXd &rhovecV)
Definition VLE.hpp:712
auto mixture_VLE_px(const AbstractModel &model, double p_spec, const Eigen::ArrayXd &xmolar_spec, double T0, const Eigen::ArrayXd &rhovecL0, const Eigen::ArrayXd &rhovecV0, const std::optional< MixVLEpxFlags > &flags_=std::nullopt)
Definition VLE.hpp:399
auto mix_VLE_Tp(const AbstractModel &model, double T, double pgiven, const Eigen::ArrayXd &rhovecL0, const Eigen::ArrayXd &rhovecV0, const std::optional< MixVLETpFlags > &flags_=std::nullopt)
Definition VLE.hpp:287
auto get_drhovecdp_Tsat(const AbstractModel &model, const double &T, const Eigen::ArrayXd &rhovecL, const Eigen::ArrayXd &rhovecV)
Definition VLE.hpp:524
auto trace_VLE_isobar_binary(const Model &model, double p, double T0, const Eigen::ArrayXd &rhovecL0, const Eigen::ArrayXd &rhovecV0, const std::optional< PVLEOptions > &options=std::nullopt)
Definition VLE.hpp:1047
auto get_drhovecdT_psat(const AbstractModel &model, const double &T, const Eigen::ArrayXd &rhovecL, const Eigen::ArrayXd &rhovecV)
Definition VLE.hpp:598
int values() const
Definition VLE.hpp:44
@ ValuesAtCompileTime
Definition VLE.hpp:32
@ InputsAtCompileTime
Definition VLE.hpp:31
int inputs() const
Definition VLE.hpp:43
_Scalar Scalar
Definition VLE.hpp:29
Matrix< Scalar, InputsAtCompileTime, 1 > InputType
Definition VLE.hpp:34
const int m_inputs
Definition VLE.hpp:38
Matrix< Scalar, ValuesAtCompileTime, InputsAtCompileTime > JacobianType
Definition VLE.hpp:36
Functor(int inputs, int values)
Definition VLE.hpp:41
Matrix< Scalar, ValuesAtCompileTime, 1 > ValueType
Definition VLE.hpp:35
const int m_values
Definition VLE.hpp:38
VLE_return_code return_code
Definition VLE_types.hpp:46
Eigen::ArrayXd initial_r
Definition VLE_types.hpp:49
Eigen::ArrayXd rhovecV
Definition VLE_types.hpp:45
Eigen::ArrayXd rhovecL
Definition VLE_types.hpp:45
Eigen::ArrayXd r
Definition VLE_types.hpp:49
bool polish_exception_on_fail
Definition VLE_types.hpp:17
double crit_termination
Definition VLE_types.hpp:15
double polish_reltol_rho
Definition VLE_types.hpp:18
bool polish_exception_on_fail
Definition VLE_types.hpp:8
double polish_reltol_rho
Definition VLE_types.hpp:9
double crit_termination
Definition VLE_types.hpp:6
int df(const VectorXd &x, MatrixXd &J)
Definition VLE.hpp:235
int operator()(const VectorXd &x, VectorXd &r)
Definition VLE.hpp:203
hybrj_functor__mix_VLE_Tp(const Model &model, const double T, const double p)
Definition VLE.hpp:201