teqp 0.22.0
Loading...
Searching...
No Matches
critical_tracing.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <fstream>
4#include <optional>
5
6#include "nlohmann/json.hpp"
7
8#include <Eigen/Dense>
12#include "teqp/exceptions.hpp"
13
14// Imports from boost
15#include <boost/numeric/odeint/stepper/controlled_runge_kutta.hpp>
16#include <boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp>
17#include <boost/numeric/odeint/stepper/euler.hpp>
18
19namespace teqp {
20
21template<typename Model, typename Scalar = double, typename VecType = Eigen::ArrayXd>
23 /***
24 * \brief Simple wrapper to sort the eigenvalues(and associated eigenvectors) in increasing order
25 * \param H The matrix, in this case, the Hessian matrix of Psi w.r.t.the molar concentrations
26 * \param values The eigenvalues
27 * \returns vectors The eigenvectors, as columns
28 *
29 * See https://stackoverflow.com/a/56327853
30 *
31 * \note The input Matrix is symmetric, thus the SelfAdjointEigenSolver can be used, and returned eigenvalues
32 * will be real and sorted already with corresponding eigenvectors as columns
33 */
34 static auto sorted_eigen(const Eigen::MatrixXd& H) {
35 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(H);
36 return std::make_tuple(es.eigenvalues(), es.eigenvectors());
37 }
38
39 static auto eigen_problem(const AbstractModel& model, const Scalar T, const VecType& rhovec, const std::optional<VecType>& alignment_v0 = std::nullopt) {
40
41 EigenData ed;
42
43 auto N = rhovec.size();
44 Eigen::ArrayX<bool> mask = (rhovec != 0).eval();
45
46 // Build the Hessian for the residual part;
47#if defined(USE_AUTODIFF)
48 auto H = model.build_Psir_Hessian_autodiff(T, rhovec);
49#else
50 using id = IsochoricDerivatives<decltype(model)>;
51 auto H = id::build_Psir_Hessian_mcx(model, T, rhovec);
52#endif
53 // ... and add ideal-gas terms to H
54 for (auto i = 0; i < N; ++i) {
55 if (mask[i]) {
56 H(i, i) += model.R(rhovec/rhovec.sum()) * T / rhovec[i];
57 }
58 }
59
60 Eigen::Index nonzero_count = mask.count();
61 auto zero_count = N - nonzero_count;
62
63 if (zero_count == 0) {
64 // Not an infinitely dilute mixture, nothing special
65 std::tie(ed.eigenvalues, ed.eigenvectorscols) = sorted_eigen(H);
66
67 // Align with the eigenvector of the component with the smallest density, and make that one positive
68 Eigen::Index ind;
69 rhovec.minCoeff(&ind);
70 if (ed.eigenvectorscols.col(ind).minCoeff() < 0) {
71 ed.eigenvectorscols *= -1.0;
72 }
73 }
74 else if (zero_count == 1) {
75 // Extract Hessian matrix without entries where rho is exactly zero
76 std::vector<int> indicesToKeep;
77
78 int badindex = -1;
79 for (auto i = 0; i < N; ++i) {
80 if (mask[i]) {
81 indicesToKeep.push_back(i);
82 }
83 else {
84 badindex = i;
85 }
86 }
87 Eigen::MatrixXd Hprime = H(indicesToKeep, indicesToKeep);
88
89 auto [eigenvalues, eigenvectors] = sorted_eigen(Hprime);
90
91 // Inject values into the U^T and v0 vectors
92 //
93 // Make a padded matrix for U (with eigenvectors as rows)
94 Eigen::MatrixXd U = H; U.setZero();
95
96 // Fill in the associated elements corresponding to eigenvectors
97 for (auto i = 0; i < N - nonzero_count; ++i) {
98 U(i, indicesToKeep) = eigenvectors.col(i); // Put in the row, leaving a hole for the zero value
99 }
100
101 // The last row has a 1 in the column corresponding to the pure fluid entry
102 // We insist that there must be only one non-zero entry
103 U.row(U.rows() - 1)(badindex) = 1.0;
104
105 ed.eigenvalues = eigenvalues;
106 ed.eigenvectorscols = U.transpose();
107 }
108 else {
109 throw std::invalid_argument("More than one non-zero concentration value found; not currently supported");
110 }
111 if (alignment_v0 && alignment_v0.value().size() > 0 && ed.eigenvectorscols.col(0).matrix().dot(alignment_v0.value().matrix()) < 0) {
112 ed.eigenvectorscols.col(0) *= -1;
113 }
114
115 ed.v0 = ed.eigenvectorscols.col(0);
116 ed.v1 = ed.eigenvectorscols.col(1);
117 return ed;
118 }
119
120 struct psi1derivs {
121 Eigen::ArrayXd psir, psi0, tot;
123 };
124
125 static auto get_minimum_eigenvalue_Psi_Hessian(const AbstractModel& model, const Scalar T, const VecType& rhovec) {
126 return eigen_problem(model, T, rhovec).eigenvalues[0];
127 }
128
129 static auto get_derivs(const AbstractModel& model, const double T, const VecType& rhovec, const std::optional<VecType>& alignment_v0 = std::nullopt) {
130 auto molefrac = rhovec / rhovec.sum();
131 auto R = model.R(molefrac);
132
133 // Solve the complete eigenvalue problem
134 auto ei = eigen_problem(model, T, rhovec, alignment_v0);
135
136 // Ideal-gas contributions of psi0 w.r.t. sigma_1, in the same form as the residual part
137 Eigen::ArrayXd psi0_derivs(5); psi0_derivs.setZero();
138 psi0_derivs[0] = -1; // Placeholder, not needed
139 psi0_derivs[1] = -1; // Placeholder, not needed
140 for (auto i = 0; i < rhovec.size(); ++i) {
141 if (rhovec[i] != 0) {
142 psi0_derivs[2] += R * T * pow(ei.v0[i], 2) / rhovec[i]; // second derivative
143 psi0_derivs[3] += -R * T * pow(ei.v0[i], 3) / pow(rhovec[i], 2); // third derivative
144 psi0_derivs[4] += 2 * R * T * pow(ei.v0[i], 4) / pow(rhovec[i], 3); // fourth derivative
145 }
146 }
147
148#if defined(USE_AUTODIFF)
149 auto psir_derivs = model.get_Psir_sigma_derivs(T, rhovec, ei.v0);
150// // Calculate the first through fourth derivative of Psi^r w.r.t. sigma_1
151// ArrayXdual4th v0(ei.v0.size()); for (auto i = 0; i < ei.v0.size(); ++i) { v0[i] = ei.v0[i]; }
152// ArrayXdual4th rhovecad(rhovec.size()); for (auto i = 0; i < rhovec.size(); ++i) { rhovecad[i] = rhovec[i]; }
153// dual4th varsigma{ 0.0 };
154// auto wrapper = [&rhovecad, &v0, &T, &model](const auto& sigma_1) {
155// auto rhovecused = (rhovecad + sigma_1 * v0).eval();
156// auto rhotot = rhovecused.sum();
157// auto molefrac = (rhovecused / rhotot).eval();
158// return eval(model.alphar(T, rhotot, molefrac) * model.R(molefrac) * T * rhotot);
159// };
160// auto psir_derivs_ = derivatives(wrapper, wrt(varsigma), at(varsigma));
161// Eigen::ArrayXd psir_derivs; psir_derivs.resize(5);
162// for (auto i = 0; i < 5; ++i) { psir_derivs[i] = psir_derivs_[i]; }
163
164#else
165 using namespace mcx;
166 // Calculate the first through fourth derivative of Psi^r w.r.t. sigma_1
167 Eigen::Vector<MultiComplex<double>, Eigen::Dynamic> v0(ei.v0.size()); for (auto i = 0; i < ei.v0.size(); ++i) { v0[i] = ei.v0[i]; }
168 Eigen::Vector<MultiComplex<double>, Eigen::Dynamic> rhovecmcx(rhovec.size()); for (auto i = 0; i < rhovec.size(); ++i) { rhovecmcx[i] = rhovec[i]; }
169 using fcn_t = std::function<MultiComplex<double>(const MultiComplex<double>&)>;
170 fcn_t wrapper = [&rhovecmcx, &v0, &T, &model](const MultiComplex<double>& sigma_1) {
171 Eigen::Vector<MultiComplex<double>, Eigen::Dynamic> rhovecused = rhovecmcx + sigma_1 * v0;
172 auto rhotot = rhovecused.sum();
173 auto molefrac = (rhovecused / rhotot).eval();
174 return model.alphar(T, rhotot, molefrac) * model.R(molefrac) * T * rhotot;
175 };
176 auto psir_derivs_ = diff_mcx1(wrapper, 0.0, 4, true);
177 Eigen::ArrayXd psir_derivs; psir_derivs.resize(5);
178 for (auto i = 0; i < 5; ++i) { psir_derivs[i] = psir_derivs_[i]; }
179#endif
180
181 // As a sanity check, the minimum eigenvalue of the Hessian constructed based on the molar concentrations
182 // must match the second derivative of psi_tot w.r.t. sigma_1. This is not always satisfied for derivatives
183 // with Cauchy method
184 //if (abs(np.min(ei.eigvals) - psitot_derivs[2]) > 1e-3){
185 // print(np.min(ei.eigvals), psitot_derivs[2], rhovec)
186 //}
187
188 psi1derivs psi1;
189 psi1.psir = psir_derivs;
190 psi1.psi0 = psi0_derivs;
191 psi1.tot = psi0_derivs + psir_derivs;
192 psi1.ei = ei;
193 return psi1;
194 }
195
196 template <typename Iterable>
197 static bool all(const Iterable& foo) {
198 return std::all_of(std::begin(foo), std::end(foo), [](const auto x) { return x; });
199 }
200 template <typename Iterable>
201 static bool any(const Iterable& foo) {
202 return std::any_of(std::begin(foo), std::end(foo), [](const auto x) { return x; });
203 }
204
205 static auto get_drhovec_dT_crit(const AbstractModel& model, const Scalar& T, const VecType& rhovec) {
206
207 // The derivatives of total Psi w.r.t.sigma_1 (numerical for residual, analytic for ideal)
208 // Returns a tuple, with residual, ideal, total dicts with of number of derivatives, value of derivative
209 auto all_derivs = get_derivs(model, T, rhovec, std::nullopt);
210 auto derivs = all_derivs.tot;
211
212 // The temperature derivative of total Psi w.r.t.T from a centered finite difference in T
213 auto dT = 1e-7;
214 auto plusT = get_derivs(model, T + dT, rhovec, all_derivs.ei.v0).tot;
215 auto minusT = get_derivs(model, T - dT, rhovec, all_derivs.ei.v0).tot;
216 auto derivT = (plusT - minusT) / (2.0 * dT);
217
218 // Solve the eigenvalue problem for the given T & rho
219 auto ei = all_derivs.ei;
220
221 auto sigma2 = 2e-5 * rhovec.sum(); // This is the perturbation along the second eigenvector
222
223 auto rhovec_plus = (rhovec + ei.v1 * sigma2).eval();
224 auto rhovec_minus = (rhovec - ei.v1 * sigma2).eval();
225 std::string stepping_desc = "";
226 auto deriv_sigma2 = all_derivs.tot;
227 auto eval = [](const auto& ex) { return ex.eval(); };
228 if (all(eval(rhovec_minus > 0)) && all(eval(rhovec_plus > 0))) {
229 // Conventional centered derivative
230 auto plus_sigma2 = get_derivs(model, T, rhovec_plus, ei.v0);
231 auto minus_sigma2 = get_derivs(model, T, rhovec_minus, ei.v0);
232 deriv_sigma2 = (plus_sigma2.tot - minus_sigma2.tot) / (2.0 * sigma2);
233 stepping_desc = "conventional centered";
234 }
235 else if (all(eval(rhovec_plus > 0))) {
236 // Forward derivative in the direction of v1
237 auto plus_sigma2 = get_derivs(model, T, rhovec_plus, ei.v0);
238 auto rhovec_2plus = (rhovec + 2 * ei.v1 * sigma2).eval();
239 auto plus2_sigma2 = get_derivs(model, T, rhovec_2plus, ei.v0);
240 deriv_sigma2 = (-3 * derivs + 4 * plus_sigma2.tot - plus2_sigma2.tot) / (2.0 * sigma2);
241 stepping_desc = "forward";
242 }
243 else if (all(eval(rhovec_minus > 0))) {
244 // Negative derivative in the direction of v1
245 auto minus_sigma2 = get_derivs(model, T, rhovec_minus, ei.v0);
246 auto rhovec_2minus = (rhovec - 2 * ei.v1 * sigma2).eval();
247 auto minus2_sigma2 = get_derivs(model, T, rhovec_2minus, ei.v0);
248 deriv_sigma2 = (-3 * derivs + 4 * minus_sigma2.tot - minus2_sigma2.tot) / (-2.0 * sigma2);
249 stepping_desc = "backwards";
250 }
251 else {
252 throw std::invalid_argument("This is not possible I think.");
253 }
254
255 // The columns of b are from Eq. 31 and Eq. 33
256 Eigen::MatrixXd b(2, 2);
257 b << derivs[3], derivs[4], // row is d^3\Psi/d\sigma_1^3, d^4\Psi/d\sigma_1^4
258 deriv_sigma2[2], deriv_sigma2[3]; // row is d/d\sigma_2(d^3\Psi/d\sigma_1^3), d/d\sigma_2(d^3\Psi/d\sigma_1^3)
259
260 auto LHS = (ei.eigenvectorscols * b).transpose();
261 Eigen::MatrixXd RHS(2, 1); RHS << -derivT[2], -derivT[3];
262 Eigen::MatrixXd drhovec_dT = LHS.colPivHouseholderQr().solve(RHS);
263
264#if defined(DEBUG_get_drhovec_dT_crit)
265 std::cout << "LHS: " << LHS << std::endl;
266 std::cout << "RHS: " << RHS << std::endl;
267 std::cout << "b: " << b << std::endl;
268 std::cout << "stepping_desc: " << stepping_desc << std::endl;
269 std::cout << "deriv_sigma2: " << deriv_sigma2 << std::endl;
270 std::cout << "rhovec_plus: " << rhovec_plus << std::endl;
271 std::cout << "all_derivs.tot:" << all_derivs.tot << std::endl;
272 std::cout << "all_derivs.psir:" << all_derivs.psir << std::endl;
273 auto plus_sigma2 = get_derivs(model, T, rhovec_plus, ei.v0);
274 std::cout << "plus_sigma2.tot:" << plus_sigma2.tot << std::endl;
275 std::cout << "plus_sigma2.psir:" << plus_sigma2.psir << std::endl;
276 std::cout << "dot of v0: " << plus_sigma2.ei.v0 * ei.v0 << std::endl;
277#endif
278 return drhovec_dT;
279 }
280
281 static auto get_criticality_conditions(const AbstractModel& model, const Scalar T, const VecType& rhovec) {
282 auto derivs = get_derivs(model, T, rhovec, Eigen::ArrayXd());
283 return (Eigen::ArrayXd(2) << derivs.tot[2], derivs.tot[3]).finished();
284 }
285
291 static auto EigenVectorPath(const AbstractModel& model, const Scalar T, const VecType& rhovec, const VecType &u1, Scalar drho) {
292 VecType dir = u1/6.0;
293 VecType rhoshift = rhovec + drho*u1;
294 auto e1 = eigen_problem(model, T, rhoshift, u1);
295 VecType u1new = e1.v0;
296
297 dir += u1/3.0;
298 rhoshift = rhovec + drho*u1new;
299 auto e2 = eigen_problem(model, T, rhoshift, u1);
300 u1new = e2.v0;
301
302 dir += u1/3.0;
303 rhoshift = rhovec + 2*drho*u1new;
304 auto e3 = eigen_problem(model, T, rhoshift, u1);
305 u1new = e3.v0;
306
307 dir += u1new/6.0;
308 rhoshift = rhovec + 2*drho*dir;
309
310 return rhoshift;
311 }
312
316 static auto is_locally_stable(const AbstractModel& model, const Scalar T, const VecType& rhovec, const Scalar stability_rel_drho) {
317 // At the critical point
318 auto ezero = eigen_problem(model, T, rhovec);
319 Scalar lambda1 = ezero.eigenvalues(0);
320
321 Scalar drho = stability_rel_drho * rhovec.sum();
322 // Towards positive density shift
323 VecType rhovecplus = EigenVectorPath(model, T, rhovec, ezero.v0, drho);
324 auto eplus = eigen_problem(model, T, rhovecplus, ezero.v0);
325 // Towards negative density shift
326 VecType rhovecminus = EigenVectorPath(model, T, rhovec, ezero.v0, -drho);
327 auto eminus = eigen_problem(model, T, rhovecminus, ezero.v0);
328
329 bool unstable = (eplus.eigenvalues(0) < lambda1 || eminus.eigenvalues(0) < lambda1);
330 return !unstable;
331 }
332
334 static auto critical_polish_fixedmolefrac(const AbstractModel& model, const Scalar T, const VecType& rhovec, const Scalar z0) {
335 auto polish_x_resid = [&model, &z0](const auto& x) {
336 auto T = x[0];
337 Eigen::ArrayXd rhovec(2); rhovec << z0*x[1], (1-z0)*x[1];
338 //auto z0new = rhovec[0] / rhovec.sum();
339 auto derivs = get_derivs(model, T, rhovec, {});
340 // First two are residuals on critical point
341 return (Eigen::ArrayXd(2) << derivs.tot[2], derivs.tot[3]).finished();
342 };
343 Eigen::ArrayXd x0(2); x0 << T, rhovec[0]+rhovec[1];
344 auto r0 = polish_x_resid(x0);
345 auto x = NewtonRaphson(polish_x_resid, x0, 1e-10);
346 auto r = polish_x_resid(x);
347 Eigen::ArrayXd change = x0 - x;
348 if (!std::isfinite(x[0]) || !std::isfinite(x[1])) {
349 throw std::invalid_argument("Something not finite; aborting polishing");
350 }
351 Eigen::ArrayXd rhovecsoln(2); rhovecsoln << x[1]*z0, x[1] * (1 - z0);
352 return std::make_tuple(x[0], rhovecsoln);
353 }
354 static auto critical_polish_fixedrho(const AbstractModel& model, const Scalar T, const VecType& rhovec, const int i) {
355 Scalar rhoval = rhovec[i];
356 auto polish_x_resid = [&model, &i, &rhoval](const auto& x) {
357 auto T = x[0];
358 Eigen::ArrayXd rhovec(2); rhovec << x[1], x[2];
359 //auto z0new = rhovec[0] / rhovec.sum();
360 auto derivs = get_derivs(model, T, rhovec);
361 // First two are residuals on critical point, third is residual on the molar concentration to be held constant
362 return (Eigen::ArrayXd(3) << derivs.tot[2], derivs.tot[3], rhovec[i] - rhoval).finished();
363 };
364 Eigen::ArrayXd x0(3); x0 << T, rhovec[0], rhovec[1];
365 auto r0 = polish_x_resid(x0);
366 auto x = NewtonRaphson(polish_x_resid, x0, 1e-10);
367 auto r = polish_x_resid(x);
368 Eigen::ArrayXd change = x0 - x;
369 if (!std::isfinite(T) || !std::isfinite(x[1]) || !std::isfinite(x[2])) {
370 throw std::invalid_argument("Something not finite; aborting polishing");
371 }
372 Eigen::ArrayXd rho = x.tail(x.size() - 1);
373 return std::make_tuple(x[0], rho);
374 }
375 static auto critical_polish_fixedT(const AbstractModel& model, const Scalar T, const VecType& rhovec) {
376 auto polish_T_resid = [&model, &T](const auto& x) {
377 auto derivs = get_derivs(model, T, x);
378 return (Eigen::ArrayXd(2) << derivs.tot[2], derivs.tot[3]).finished();
379 };
380 Eigen::ArrayXd x0 = rhovec;
381 auto r0 = polish_T_resid(x0);
382 auto x = NewtonRaphson(polish_T_resid, x0, 1e-10);
383 auto r = polish_T_resid(x);
384 Eigen::ArrayXd change = x0 - x;
385 if (!std::isfinite(T) || !std::isfinite(x[1])) {
386 throw std::invalid_argument("Something not finite; aborting polishing");
387 }
388 return x;
389 }
390
391 static auto trace_critical_arclength_binary(const AbstractModel& model, const Scalar& T0, const VecType& rhovec0, const std::optional<std::string>& filename_ = std::nullopt, const std::optional<TCABOptions> &options_ = std::nullopt) -> nlohmann::json {
392 std::string filename = filename_.value_or("");
393 TCABOptions options = options_.value_or(TCABOptions{});
394
395 VecType last_drhodt;
396
397 // Typedefs for the types for odeint for simple Euler and RK45 integrators
398 using state_type = std::vector<double>;
399 using namespace boost::numeric::odeint;
400 euler<state_type> eul; // Class for simple Euler integration
401 typedef runge_kutta_cash_karp54< state_type > error_stepper_type;
402 typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type;
403
404 auto dot = [](const auto& v1, const auto& v2) { return (v1 * v2).sum(); };
405 auto norm = [](const auto& v) { return sqrt((v * v).sum()); };
406
407 auto JSONdata = nlohmann::json::array();
408 std::ofstream ofs = (filename.empty()) ? std::ofstream() : std::ofstream(filename);
409
410 double c = options.init_c;
411
412 // The function for the derivative in the form of odeint
413 // x is [T, rhovec]
414 auto xprime = [&](const state_type& x, state_type& dxdt, const double /* t */)
415 {
416 // Unpack the inputs
417 const double T = x[0];
418 const auto rhovec = Eigen::Map<const Eigen::ArrayXd>(&(x[0]) + 1, x.size() - 1);
419 if (options.terminate_negative_density && rhovec.minCoeff() < 0) {
420 throw std::invalid_argument("Density is negative");
421 }
422
423 auto drhodT = get_drhovec_dT_crit(model, T, rhovec).array().eval();
424 auto dTdt = 1.0 / norm(drhodT);
425 Eigen::ArrayXd drhodt = c * (drhodT * dTdt).eval();
426
427 dxdt[0] = c*dTdt;
428
429 auto drhodtview = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1);
430 drhodtview = drhodt; // Copy values into the view
431
432 if (last_drhodt.size() > 0) {
433 auto rhodot = dot(drhodt, last_drhodt);
434 if (rhodot < 0) {
435 Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]), dxdt.size()) *= -1;
436 }
437 }
438 };
439 auto get_dxdt = [&](const state_type& x) {
440 auto dxdt = state_type(x.size());
441 xprime(x, dxdt, -1);
442 return dxdt;
443 };
444 // Pull out the drhodt from dxdt, just a convenience function
445 auto extract_drhodt = [](const state_type& dxdt) -> Eigen::ArrayXd {
446 return Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1);
447 };
448 // Pull out the dTdt from dxdt, just a convenience function
449 auto extract_dTdt = [](const state_type& dxdt) -> double {
450 return dxdt[0];
451 };
452
453 // Define the tolerances
454 double abs_err = options.abs_err, rel_err = options.rel_err, a_x = 1.0, a_dxdt = 1.0;
455 controlled_stepper_type controlled_stepper(default_error_checker< double, range_algebra, default_operations >(abs_err, rel_err, a_x, a_dxdt));
456
457 double t = 0, dt = options.init_dt;
458
459 // Build the initial state array, with T followed by rhovec
460 std::vector<double> x0(rhovec0.size() + 1);
461 x0[0] = T0;
462 Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 1, x0.size() - 1) = rhovec0;
463
464 // Make variables T and rhovec references to the contents of x0 vector
465 // The views are mutable (danger!)
466 double& T = x0[0];
467 auto rhovec = Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 1, x0.size() - 1);
468
469 auto store_drhodt = [&](const state_type& x0) {
470 last_drhodt = extract_drhodt(get_dxdt(x0));
471 };
472
473 auto store_point = [&]() {
474
475 // Calculate some other parameters, for debugging, or scientific interest
476 auto rhotot = rhovec.sum();
477 double p = rhotot * model.R(rhovec / rhovec.sum()) * T + model.get_pr(T, rhovec);
478 auto conditions = get_criticality_conditions(model, T, rhovec);
479 double splus = model.get_splus(T, rhovec);
480 auto dxdt = x0;
481 xprime(x0, dxdt, -1.0);
482
483 // Store the data in a JSON structure
484 nlohmann::json point = {
485 {"t", t},
486 {"T / K", T},
487 {"rho0 / mol/m^3", static_cast<double>(rhovec[0])},
488 {"rho1 / mol/m^3", static_cast<double>(rhovec[1])},
489 {"c", c},
490 {"s^+", splus},
491 {"p / Pa", p},
492 {"dT/dt", dxdt[0]},
493 {"drho0/dt", dxdt[1]},
494 {"drho1/dt", dxdt[2]},
495 {"lambda1", conditions[0]},
496 {"dirderiv(lambda1)/dalpha", conditions[1]},
497 };
498 if (options.calc_stability) {
499 point["locally stable"] = is_locally_stable(model, T, rhovec, options.stability_rel_drho);
500 }
501 JSONdata.push_back(point);
502 };
503
504 // Line writer
505 auto write_line = [&]() {
506 std::stringstream out;
507 auto rhotot = rhovec.sum();
508 double z0 = rhovec[0] / rhotot;
509 auto conditions = get_criticality_conditions(model, T, rhovec);
510 out << z0 << "," << rhovec[0] << "," << rhovec[1] << "," << T << "," << rhotot * model.R(rhovec / rhovec.sum()) * T + model.get_pr(T, rhovec) << "," << c << "," << dt << "," << conditions(0) << "," << conditions(1) << std::endl;
511 std::string sout(out.str());
512 std::cout << sout;
513 if (ofs.is_open()) {
514 ofs << sout;
515 }
516 };
517
518 int counter_T_converged = 0, retry_count = 0;
519 ofs << "z0 / mole frac.,rho0 / mol/m^3,rho1 / mol/m^3,T / K,p / Pa,c,dt,condition(1),condition(2)" << std::endl;
520
521 // Determine the initial direction of integration
522 {
523 const auto step = (rhovec + extract_drhodt(get_dxdt(x0))*dt).eval();
524 Eigen::ArrayX<bool> negativestepvals = (step < 0).eval();
525 // Flip the sign if the first step would yield any negative concentrations
526 if (negativestepvals.any()) {
527 c *= -1;
528 }
529 }
530 //store_drhodt(x0);
531 if (!filename.empty()) {
532 write_line();
533 }
534
535 for (auto iter = 0; iter < options.max_step_count; ++iter) {
536
537 // Calculate the derivatives at the beginning of the step
538 auto dxdt_start_step = get_dxdt(x0);
539 auto x_start_step = x0;
540
541 if (iter == 0 && retry_count == 0) {
542 store_point(); }
543
544 if (options.integration_order == 5) {
545 auto res = controlled_step_result::fail;
546 try {
547 res = controlled_stepper.try_step(xprime, x0, t, dt);
548 }
549 catch (const std::exception &e) {
550 if (options.verbosity > 0) {
551 std::cout << e.what() << std::endl;
552 }
553 break;
554 }
555
556 if (res != controlled_step_result::success) {
557 // Try again, with a smaller step size
558 iter--;
559 retry_count++;
560 continue;
561 }
562 else {
563 retry_count = 0;
564 /*auto dxdt = x0;
565 xprime(x0, dxdt, -1.0);
566 auto drhodt = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1);
567 auto rhodotproduct = dot(drhodt, last_drhodt);
568 int rr = 0;*/
569 }
570 // Reduce step size if greater than the specified max step size
571 dt = std::min(dt, options.max_dt);
572 }
573 else if (options.integration_order == 1) {
574 try {
575 eul.do_step(xprime, x0, t, dt);
576 t += dt;
577 }
578 catch (const std::exception &e) {
579 std::cout << e.what() << std::endl;
580 break;
581 }
582 }
583 else {
584 throw std::invalid_argument("integration order is invalid:" + std::to_string(options.integration_order));
585 }
586
587 auto z0 = rhovec[0] / rhovec.sum();
588 if (z0 < 0 || z0 > 1) {
589 if (options.verbosity > 10) {
590 std::cout << "Termination because z0 of " + std::to_string(z0) + " is outside [0, 1]" << std::endl;
591 }
592 break;
593 }
594
595 // The polishers to be tried, in order, to polish the critical point
596 using PolisherType = std::function<std::tuple<double, VecType>(const AbstractModel&, const Scalar, const VecType&)>;
597 std::vector<PolisherType> polishers = {
598 [&](const AbstractModel& model, const Scalar T, const VecType& rhovec) {
599 auto [Tnew, rhovecnew] = critical_polish_fixedmolefrac(model, T, rhovec, z0);
600 return std::make_tuple(Tnew, rhovecnew);
601 },
602 [&](const AbstractModel& model, const Scalar T, const VecType& rhovec) {
603 auto rhovecnew = critical_polish_fixedT(model, T, rhovec);
604 return std::make_tuple(T, rhovecnew);
605 },
606 [&](const AbstractModel& model, const Scalar T, const VecType& rhovec) {
607 int i = 0;
608 auto [Tnew, rhovecnew] = critical_polish_fixedrho(model, T, rhovec, i);
609 return std::make_tuple(Tnew, rhovecnew);
610 },
611 [&](const AbstractModel& model, const Scalar T, const VecType& rhovec) {
612 int i = 1;
613 auto [Tnew, rhovecnew] = critical_polish_fixedrho(model, T, rhovec, i);
614 return std::make_tuple(Tnew, rhovecnew);
615 },
616 };
617
618 if (options.polish) {
619 bool polish_ok = false;
620 for (auto &polisher : polishers){
621 try {
622 auto [Tnew, rhovecnew] = polisher(model, T, rhovec);
623 if (std::abs(T - Tnew) > options.polish_reltol_T*T) {
624 throw IterationFailure("Polishing changed the temperature more than " + std::to_string(options.polish_reltol_T*100) + " %");
625 }
626 if (((rhovec-rhovecnew).cwiseAbs() > options.polish_reltol_rho*rhovec).any()){
627 throw IterationFailure("Polishing changed a molar concentration by more than "+std::to_string(options.polish_reltol_rho*100)+" %");
628 }
629 polish_ok = true;
630 T = Tnew;
631 rhovec = rhovecnew;
632 break;
633 }
634 catch (std::exception& e) {
635 if (options.verbosity > 10) {
636 std::cout << "Tracing problem: " << e.what() << std::endl;
637 std::cout << "Starting:: T:" << T << "; rhovec: " << rhovec << std::endl;
638 auto conditions = get_criticality_conditions(model, T, rhovec);
639 std::cout << "Starting crit. cond.: " << conditions << std::endl;
640 try {
641 auto [Tnew, rhovecnew] = polisher(model, T, rhovec);
642 std::cout << "Ending:: T:" << Tnew << "; rhovec: " << rhovecnew << std::endl;
643 } catch (...) {}
644 }
645 }
646 }
647 if (!polish_ok){
648 if (options.polish_exception_on_fail){
649 throw IterationFailure("Polishing was not successful");
650 }
651 else{
652 if (options.verbosity > 10){
653 std::cout << "Termination because polishing failed" << std::endl;
654 }
655 break;
656 }
657 }
658 }
659
660 // Store the derivative vector from the beginning of the step, before
661 // the actual step is taken. We don't want to use the values at the end
662 // because otherwise simple Euler will never consider the possible change in direction
663 //
664 // Also, we only do this after two completed steps because sometimes the infinite
665 // dilution derivatives seem to be not quite right. There is still a risk that the first
666 // step will try to turn around...
667 if (iter >= options.skip_dircheck_count) {
668 store_drhodt(x_start_step); }
669
670 auto actualstep = (Eigen::Map<Eigen::ArrayXd>(&(x0[0]), x0.size()) - Eigen::Map<Eigen::ArrayXd>(&(x_start_step[0]), x_start_step.size())).eval();
671
672 // Check if T has stopped changing
673 if (std::abs(actualstep[0]) < options.T_tol) {
674 counter_T_converged++;
675 }
676 else {
677 counter_T_converged = 0;
678 }
679
680 auto rhotot = rhovec.sum();
681 z0 = rhovec[0] / rhotot;
682 if (z0 < 0 || z0 > 1) {
683 if (options.verbosity > 10) {
684 std::cout << "Termination because z0 of " + std::to_string(z0) + " is outside [0, 1]" << std::endl;
685 }
686 break;
687 }
688
689 if (!filename.empty()) { write_line(); }
690 store_point();
691
692 if (counter_T_converged > options.small_T_count) {
693 if (options.verbosity > 10){
694 std::cout << "Termination because maximum number of small steps were taken" << std::endl;
695 }
696 break;
697 }
698 }
699 // If the last step crosses a zero concentration, see if it corresponds to a pure fluid
700 // and if so, iterate to find the pure fluid endpoint
701 if (options.pure_endpoint_polish) {
702 // Simple Euler step t
703 auto dxdt = get_dxdt(x0);
704 auto drhodt = extract_drhodt(dxdt);
705 const auto step = (rhovec + drhodt*dt).eval();
706 if ((step * rhovec > 0).any()) {
707 // Calculate step sizes to take concentrations to zero
708 auto step_sizes = ((-rhovec) / drhodt).eval();
709 Eigen::Index ipure;
710 rhovec.maxCoeff(&ipure);
711 // Find new step size based on the pure we are heading towards
712 auto new_step_size = step_sizes(ipure);
713 // Take an Euler step of the new size
714 const auto new_rhovec = (rhovec + drhodt*new_step_size).eval();
715 const double new_T = T + extract_dTdt(dxdt)*new_step_size;
716
717 // Solve for pure fluid critical point
718 //
719 // A bit trickier here because we need to hack the pure fluid model to put the pure fluid
720 // composition in an index other than the first if ipure != 0 because we don't want
721 // to require that a pure fluid model is also provided since zero mole fractions
722 // should be fine
723 nlohmann::json flags = { {"alternative_pure_index", ipure}, {"alternative_length", 2} };
724 auto [TT, rhorho] = solve_pure_critical(model, new_T, new_rhovec.sum(), flags);
725
726 // Replace the values in T and rhovec
727 T = TT;
728 rhovec[ipure] = rhorho;
729 rhovec[1 - ipure] = 0;
730
731 // And store the polished values
732 if (!filename.empty()) { write_line(); }
733 store_point();
734 }
735 }
736 //auto N = JSONdata.size();
737 return JSONdata;
738 }
739
747 static auto get_dp_dT_crit(const AbstractModel& model, const Scalar& T, const VecType& rhovec) {
748 auto dpdrhovec = model.get_dpdrhovec_constT(T, rhovec);
749 Scalar v = model.get_dpdT_constrhovec(T, rhovec) + (dpdrhovec.array()*get_drhovec_dT_crit(model, T, rhovec).array()).sum();
750 return v;
751 }
752
753#define CRIT_FUNCTIONS_TO_WRAP \
754 X(get_dp_dT_crit) \
755 X(trace_critical_arclength_binary) \
756 X(critical_polish_fixedmolefrac) \
757 X(get_drhovec_dT_crit) \
758 X(get_derivs) \
759 X(eigen_problem)
760
761#define X(f) template <typename TemplatedModel, typename ...Params, \
762typename = typename std::enable_if<not std::is_base_of<teqp::cppinterface::AbstractModel, TemplatedModel>::value>::type> \
763static auto f(const TemplatedModel& model, Params&&... params){ \
764 auto view = teqp::cppinterface::adapter::make_cview(model); \
765 const AbstractModel& am = *view.get(); \
766 return f(am, std::forward<Params>(params)...); \
767}
769#undef X
770#undef CRIT_FUNCTIONS_TO_WRAP
771
772//template <typename TemplatedModel, typename ...Params,
773//typename = typename std::enable_if<not std::is_base_of<teqp::cppinterface::AbstractModel, TemplatedModel>::value>::type>
774//static auto critical_polish_fixedmolefrac(const TemplatedModel& model, Params&&... params){
775// auto view = teqp::cppinterface::adapter::make_cview(model);
776// const AbstractModel& am = *view.get();
777// return critical_polish_fixedmolefrac(am, std::forward<Params>(params)...);
778//}
779
780}; // namespace Critical
781
782}; // namespace teqp
virtual Eigen::ArrayXd get_Psir_sigma_derivs(const double T, const EArrayd &rhovec, const EArrayd &v) const =0
virtual double get_dpdT_constrhovec(const double T, const EArrayd &rhovec) const =0
virtual EArrayd get_dpdrhovec_constT(const double T, const EArrayd &rhovec) const =0
virtual EMatrixd build_Psir_Hessian_autodiff(const double T, const EArrayd &rhovec) const =0
double R(const EArrayd &x) const
Definition teqpcpp.hpp:102
#define CRIT_FUNCTIONS_TO_WRAP
auto NewtonRaphson(Callable f, const Inputs &args, double tol)
auto solve_pure_critical(const Model &model, const Scalar T0, const Scalar rho0, const std::optional< nlohmann::json > &flags=std::nullopt)
ContainerType::value_type derivT(const FuncType &f, TType T, const ContainerType &rho)
Given a function, use complex step derivatives to calculate the derivative with respect to the first ...
Definition derivs.hpp:33
auto pow(const double &x, const double &e)
Definition types.hpp:195
static auto sorted_eigen(const Eigen::MatrixXd &H)
static auto get_derivs(const AbstractModel &model, const double T, const VecType &rhovec, const std::optional< VecType > &alignment_v0=std::nullopt)
static auto get_minimum_eigenvalue_Psi_Hessian(const AbstractModel &model, const Scalar T, const VecType &rhovec)
static auto get_criticality_conditions(const AbstractModel &model, const Scalar T, const VecType &rhovec)
static bool any(const Iterable &foo)
static auto trace_critical_arclength_binary(const AbstractModel &model, const Scalar &T0, const VecType &rhovec0, const std::optional< std::string > &filename_=std::nullopt, const std::optional< TCABOptions > &options_=std::nullopt) -> nlohmann::json
static auto is_locally_stable(const AbstractModel &model, const Scalar T, const VecType &rhovec, const Scalar stability_rel_drho)
The LocalStability function of Algorithm 2 from Deiters and Bell: https://dx.doi.org/10....
static auto critical_polish_fixedT(const AbstractModel &model, const Scalar T, const VecType &rhovec)
static auto critical_polish_fixedrho(const AbstractModel &model, const Scalar T, const VecType &rhovec, const int i)
static auto get_drhovec_dT_crit(const AbstractModel &model, const Scalar &T, const VecType &rhovec)
static auto eigen_problem(const AbstractModel &model, const Scalar T, const VecType &rhovec, const std::optional< VecType > &alignment_v0=std::nullopt)
static auto EigenVectorPath(const AbstractModel &model, const Scalar T, const VecType &rhovec, const VecType &u1, Scalar drho)
The EigenVectorPath function of Algorithm 2 from Deiters and Bell: https://dx.doi....
static auto get_dp_dT_crit(const AbstractModel &model, const Scalar &T, const VecType &rhovec)
Calculate dp/dT along the critical locus at given T, rhovec.
static bool all(const Iterable &foo)
static auto critical_polish_fixedmolefrac(const AbstractModel &model, const Scalar T, const VecType &rhovec, const Scalar z0)
Polish a critical point while keeping the overall composition constant and iterating for temperature ...
Eigen::MatrixXd eigenvectorscols
double T_tol
The tolerance on temperature to indicate that it is converged.
double polish_reltol_T
The maximum allowed change in temperature when polishing.
int max_step_count
Maximum number of steps allowed.
double stability_rel_drho
The relative size of the step (relative to the sum of the molar concentration vector) to be used when...
bool pure_endpoint_polish
If true, if the last step crossed into negative concentrations, try to interpolate to find the pure f...
bool polish_exception_on_fail
If true, when polishing fails, throw an exception, otherwise, terminate tracing.
double init_c
The c parameter which controls the initial search direction for the first step. Choices are 1 or -1.
int integration_order
The order of integration, either 1 for simple Euler or 5 for adaptive RK45.
bool polish
If true, polish the solution at every step.
int skip_dircheck_count
Only start checking the direction dot product after this many steps.
bool calc_stability
Calculate the local stability with the method of Deiters and Bell.
double polish_reltol_rho
The maximum allowed change in any molar concentration when polishing.
bool terminate_negative_density
Stop the tracing if the density is negative.
int verbosity
The greater the verbosity, the more output you will get, especially about polishing failures.
int small_T_count
How many small temperature steps indicates convergence.
double init_dt
The initial step size.