CoolProp 8.0.0
An open-source fluid property and humid air property database
superancillary.h
Go to the documentation of this file.
1/*
2# NIST Disclaimer of Copyright and Warranty
3
4This software was developed by employees of the National Institute of
5Standards and Technology (NIST), an agency of the Federal Government
6and is being made available as a public service. Pursuant to title 17
7United States Code Section 105, works of NIST employees are not
8subject to copyright protection in the United States. This software
9may be subject to foreign copyright. Permission in the United States
10and in foreign countries, to the extent that NIST may hold copyright,
11to use, copy, modify, create derivative works, and distribute this
12software and its documentation without fee is hereby granted on a
13non-exclusive basis, provided that this notice and disclaimer of
14warranty appears in all copies.
15
16THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND,
17EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED
18TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS,
19ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
20PURPOSE, AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE
21DOCUMENTATION WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE
22SOFTWARE WILL BE ERROR FREE. IN NO EVENT SHALL NIST BE LIABLE FOR ANY
23DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR
24CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR IN ANY WAY
25CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
26CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY
27PERSONS OR PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS
28SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF, OR USE OF, THE
29SOFTWARE OR SERVICES PROVIDED HEREUNDER.
30
31Subsequent edits by Ian Bell
32*/
33
34#pragma once
35
36#include <atomic>
37#include <cmath>
38#include <cstdio>
39#include <mutex>
40#include <set>
41#include <utility>
42#include <optional>
43#include <functional>
44#include <sstream>
45#include <iomanip>
46#include <Eigen/Dense>
47
48// NOTE: boost (the TOMS 748 rootfinder) is intentionally NOT included here. Its
49// only uses are routed through detail::toms748 (declared below, defined in
50// src/superancillary.cpp), so this installed public header needs only Eigen on
51// the include path -- not boost -- for downstream consumers (CoolProp-1tbe.14).
52
53namespace CoolProp {
54namespace superancillary {
55
56namespace detail {
57
65double toms748(const std::function<double(double)>& f, double a, double b, double fa, double fb, unsigned int bits, std::size_t max_iter);
66
67// From https://arxiv.org/pdf/1401.5766.pdf (Algorithm #3)
68template <typename Matrix>
69inline void balance_matrix(const Matrix& A, Matrix& Aprime, Matrix& D) {
70 const int p = 2;
71 double beta = 2; // Radix base (2?)
72 int iter = 0;
73 Aprime = A;
74 D = Matrix::Identity(A.rows(), A.cols());
75 bool converged = false;
76 do {
77 converged = true;
78 for (Eigen::Index i = 0; i < A.rows(); ++i) {
79 double c = Aprime.col(i).template lpNorm<p>();
80 double r = Aprime.row(i).template lpNorm<p>();
81 // A row/column norm that is zero or non-finite cannot be balanced by
82 // diagonal scaling, and worse, it spins the scaling loops below
83 // forever: e.g. r == inf makes (c < r/beta) perpetually true while
84 // c *= beta never catches up (inf/beta == inf), and the symmetric
85 // (c >= r*beta) loop traps once c overflows to inf. The companion
86 // matrix picks up an inf entry from a near-zero leading Chebyshev
87 // coefficient (last column is -coeffs.head(N)/(2*coeffs(N))); whether
88 // that division overflows is FP-rounding-dependent, so the hang is
89 // platform-specific. Leave such a row/column at unit scaling: the
90 // eigenvalue solve still runs and the caller falls through on the
91 // (degenerate) result instead of looping forever.
92 if (!std::isfinite(c) || !std::isfinite(r) || c == 0.0 || r == 0.0) {
93 continue;
94 }
95 double s = pow(c, p) + pow(r, p);
96 double f = 1;
97 while (c < r / beta) {
98 c *= beta;
99 r /= beta;
100 f *= beta;
101 }
102 while (c >= r * beta) {
103 c /= beta;
104 r *= beta;
105 f /= beta;
106 }
107 if (pow(c, p) + pow(r, p) < 0.95 * s) {
108 converged = false;
109 D(i, i) *= f;
110 Aprime.col(i) *= f;
111 Aprime.row(i) /= f;
112 }
113 }
114 iter++;
115 if (iter > 50) {
116 break;
117 }
118 } while (!converged);
119}
120
121inline void companion_matrix_transposed(const Eigen::ArrayXd& coeffs, Eigen::MatrixXd& A) {
122 auto N = coeffs.size() - 1; // degree
123 if (A.rows() != N) {
124 throw std::invalid_argument("A.rows() != N");
125 }
126 A.setZero();
127 // First col
128 A(1, 0) = 1;
129 // Last col
130 A.col(N - 1) = -coeffs.head(N) / (2.0 * coeffs(N));
131 A(N - 2, N - 1) += 0.5;
132 // All the other cols
133 for (int j = 1; j < N - 1; ++j) {
134 A(j - 1, j) = 0.5;
135 A(j + 1, j) = 0.5;
136 }
137}
138inline void companion_matrix_transposed(const std::vector<double>& coeffs, Eigen::MatrixXd& A) {
139 Eigen::ArrayXd coeffs_ = Eigen::Map<const Eigen::ArrayXd>(&coeffs[0], coeffs.size());
140 return companion_matrix_transposed(coeffs_, A);
141}
142
151inline auto get_LU_matrices(std::size_t N) {
152 Eigen::MatrixXd L(N + 1, N + 1);
153 Eigen::MatrixXd U(N + 1, N + 1);
154 for (std::size_t j = 0; j <= N; ++j) {
155 for (std::size_t k = j; k <= N; ++k) {
156 double p_j = (j == 0 || j == N) ? 2 : 1;
157 double p_k = (k == 0 || k == N) ? 2 : 1;
158 double cosjPikN = cos((j * EIGEN_PI * k) / N);
159 L(j, k) = 2.0 / (p_j * p_k * N) * cosjPikN;
160 // Exploit symmetry to fill in the symmetric elements in the matrix
161 L(k, j) = L(j, k);
162
163 U(j, k) = cosjPikN;
164 // Exploit symmetry to fill in the symmetric elements in the matrix
165 U(k, j) = U(j, k);
166 }
167 }
168 return std::make_tuple(L, U);
169}
170
171inline double M_element_norm(const std::vector<double>& x, Eigen::Index M) {
172 Eigen::Map<const Eigen::ArrayXd> X(&x[0], x.size());
173 return X.tail(M).matrix().norm() / X.head(M).matrix().norm();
174}
175
176inline double M_element_norm(const Eigen::ArrayXd& x, Eigen::Index M) {
177 return x.tail(M).matrix().norm() / x.head(M).matrix().norm();
178}
179
184template <typename Function, typename Container>
185inline auto dyadic_splitting(const std::size_t N, const Function& func, const double xmin, const double xmax, const int M = 3,
186 const double tol = 1e-12, const int max_refine_passes = 8,
187 const std::optional<std::function<void(int, const Container&)>>& callback = std::nullopt) -> Container {
188 using CE_t = std::decay_t<decltype(Container().front())>;
189 using ArrayType = std::decay_t<decltype(CE_t().coeff())>;
190
191 Eigen::MatrixXd Lmat, Umat;
192 std::tie(Lmat, Umat) = detail::get_LU_matrices(N);
193
194 auto builder = [&](double xmin, double xmax) -> CE_t {
195 auto get_nodes_realworld = [N, xmin, xmax]() -> Eigen::ArrayXd {
196 Eigen::ArrayXd nodes = (Eigen::ArrayXd::LinSpaced(N + 1, 0, static_cast<double>(N)).array() * EIGEN_PI / N).cos();
197 return ((xmax - xmin) * nodes + (xmax + xmin)) * 0.5;
198 };
199
200 Eigen::ArrayXd x = get_nodes_realworld();
201
202 // Apply the function to do the transformation
203 // of the functional values at the nodes
204 for (auto j = 0L; j < x.size(); ++j) {
205 x(j) = func(j, static_cast<long>(x.size()), x(j));
206 }
207
208 // And now rebuild the expansion by left-multiplying by the L matrix
209 Eigen::ArrayXd c = Lmat * x.matrix();
210 // std::cout << "c: " << c << std::endl;
211 // Check if any coefficients are invalid, stop if so
212 if (!c.allFinite()) {
213 throw std::invalid_argument("At least one coefficient is non-finite");
214 }
215 if constexpr (std::is_same_v<ArrayType, std::vector<double>>) {
216 return {xmin, xmax, std::vector<double>(&c[0], &c[0] + c.size())};
217 } else {
218 // Probably an Eigen::ArrayXd, just pass that into constructor
219 return {xmin, xmax, c};
220 }
221 };
222
223 // Start off with the full domain from xmin to xmax
224 Container expansions;
225 expansions.emplace_back(builder(xmin, xmax));
226
227 // Now enter into refinement passes
228 for (int refine_pass = 0; refine_pass < max_refine_passes; ++refine_pass) {
229 bool all_converged = true;
230 // Start at the right and move left because insertions will make the length increase
231 for (int iexpansion = static_cast<int>(expansions.size()) - 1; iexpansion >= 0; --iexpansion) {
232 auto& expan = expansions[iexpansion];
233 auto err = M_element_norm(expan.coeff(), M);
234 // std::cout << "err: " << err << std::endl;
235 if (err > tol) {
236 // Splitting is required, do a dyadic split
237 auto xmid = (expan.xmin() + expan.xmax()) / 2;
238 CE_t newleft{builder(expan.xmin(), xmid)};
239 CE_t newright{builder(xmid, expan.xmax())};
240
241 expansions.at(iexpansion) = std::move(newleft);
242 expansions.insert(expansions.begin() + iexpansion + 1, newright);
243 all_converged = false;
244 }
245 // std::cout << expansions.size() << std::endl;
246 }
247 if (callback) {
248 callback.value()(refine_pass, expansions);
249 }
250 if (all_converged) {
251 break;
252 }
253 }
254 return expansions;
255}
256
257} // namespace detail
258
269template <typename ArrayType>
271{
272 private:
273 double m_xmin,
274 m_xmax;
275 ArrayType m_coeff;
276 public:
278
280 ChebyshevExpansion(double xmin, double xmax, const ArrayType& coeff) : m_xmin(xmin), m_xmax(xmax), m_coeff(coeff) {};
281
286
288 const auto xmin() const {
289 return m_xmin;
290 }
291
293 const auto xmax() const {
294 return m_xmax;
295 }
296
298 const auto& coeff() const {
299 return m_coeff;
300 }
301
305 template <typename T>
306 auto eval(const T& x) const {
307 // Scale to (-1, 1)
308 T xscaled = (2.0 * x - (m_xmax + m_xmin)) / (m_xmax - m_xmin);
309 int Norder = static_cast<int>(m_coeff.size()) - 1;
310 T u_k = 0, u_kp1 = m_coeff[Norder], u_kp2 = 0;
311 for (int k = Norder - 1; k > 0; k--) { // k must be signed!
312 // Do the recurrent calculation
313 u_k = 2.0 * xscaled * u_kp1 - u_kp2 + m_coeff[k];
314 // Update the values
315 u_kp2 = u_kp1;
316 u_kp1 = u_k;
317 }
318 T retval = m_coeff[0] + xscaled * u_kp1
319 - u_kp2; // This seems inefficient but required to ensure that Eigen::Array are evaluated and an expression is not returned
320 return retval;
321 }
322
324 template <typename T>
325 auto eval_many(const T& x, T& y) const {
326 if (x.size() != y.size()) {
327 throw std::invalid_argument("x and y are not the same size");
328 }
329 for (auto i = 0; i < x.size(); ++i) {
330 y(i) = eval(x(i));
331 }
332 }
333
335 template <typename T>
336 auto eval_manyC(const T x[], T y[], std::size_t N) const {
337 for (std::size_t i = 0; i < N; ++i) {
338 y[i] = eval(x[i]);
339 }
340 }
341
343 template <typename T>
344 auto eval_Eigen(const T& x, T& y) const {
345 if (x.size() != y.size()) {
346 throw std::invalid_argument("x and y are not the same size");
347 }
348 y = eval(x);
349 }
350
352 Eigen::ArrayXd get_nodes_realworld() const {
353 Eigen::Index N = m_coeff.size() - 1;
354 Eigen::ArrayXd nodes = (Eigen::ArrayXd::LinSpaced(N + 1, 0, N).array() * EIGEN_PI / N).cos();
355 return ((m_xmax - m_xmin) * nodes + (m_xmax + m_xmin)) * 0.5;
356 }
357
371 auto solve_for_x_count(double y, double a, double b, unsigned int bits, std::size_t max_iter, double boundsytol) const {
372 std::size_t counter = 0;
373 auto f = [&](double x) {
374 counter++;
375 return eval(x) - y;
376 };
377 double fa = f(a);
378 if (std::abs(fa) < boundsytol) {
379 return std::make_tuple(a, std::size_t{1});
380 }
381 double fb = f(b);
382 if (std::abs(fb) < boundsytol) {
383 return std::make_tuple(b, std::size_t{2});
384 }
385 double root = detail::toms748(f, a, b, fa, fb, bits, max_iter);
386 return std::make_tuple(root, counter);
387 }
388
391 auto solve_for_x(double y, double a, double b, unsigned int bits, std::size_t max_iter, double boundsytol) const {
392 return std::get<0>(solve_for_x_count(y, a, b, bits, max_iter, boundsytol));
393 }
394
396 template <typename T, typename CountsT>
397 auto solve_for_x_many(const T& y, double a, double b, unsigned int bits, std::size_t max_iter, double boundsytol, T& x, CountsT& counts) const {
398 if (x.size() != y.size()) {
399 throw std::invalid_argument("x and y are not the same size");
400 }
401 for (auto i = 0; i < x.size(); ++i) {
402 std::tie(x(i), counts(i)) = solve_for_x_count(y(i), a, b, bits, max_iter, boundsytol);
403 }
404 }
405
407 template <typename T, typename CountsT>
408 auto solve_for_x_manyC(const T y[], std::size_t N, double a, double b, unsigned int bits, std::size_t max_iter, double boundsytol, T x[],
409 CountsT counts[]) const {
410 for (std::size_t i = 0; i < N; ++i) {
411 std::tie(x[i], counts[i]) = solve_for_x_count(y[i], a, b, bits, max_iter, boundsytol);
412 }
413 }
414
415 ArrayType do_derivs(std::size_t Nderiv) const {
416 // See Mason and Handscomb, p. 34, Eq. 2.52
417 // and example in https ://github.com/numpy/numpy/blob/master/numpy/polynomial/chebyshev.py#L868-L964
418 ArrayType c = m_coeff;
419 for (std::size_t deriv_counter = 0; deriv_counter < Nderiv; ++deriv_counter) {
420 std::size_t N = c.size() - 1,
421 Nd = N - 1;
422 ArrayType cd(N);
423 for (std::size_t r = 0; r <= Nd; ++r) {
424 cd[r] = 0;
425 for (std::size_t k = r + 1; k <= N; ++k) {
426 // Terms where k-r is odd have values, otherwise, they are zero
427 if ((k - r) % 2 == 1) {
428 cd[r] += 2 * k * c[k];
429 }
430 }
431 // The first term with r = 0 is divided by 2 (the single prime in Mason and Handscomb, p. 34, Eq. 2.52)
432 if (r == 0) {
433 cd[r] /= 2;
434 }
435 // Rescale the values if the range is not [-1,1]. Arrives from the derivative of d(xreal)/d(x_{-1,1})
436 cd[r] /= (m_xmax - m_xmin) / 2.0;
437 }
438 if (Nderiv == 1) {
439 return cd;
440 } else {
441 c = cd;
442 }
443 }
444 return c;
445 }
446};
447
448static_assert(std::is_move_assignable_v<ChebyshevExpansion<std::vector<double>>>);
449//static_assert(std::is_copy_assignable_v<ChebyshevExpansion<std::vector<double>>>);
450
453{
454 std::size_t idx;
455 double ymin,
460 bool contains_y(double y) const {
461 return y >= ymin && y <= ymax;
462 }
463};
464
467{
468 std::vector<MonotonicExpansionMatch> expansioninfo;
469 double xmin,
474 bool contains_y(double y) const {
475 return y >= ymin && y <= ymax;
476 }
477};
478
488template <typename ArrayType = Eigen::ArrayXd>
490{
491 private:
492 const double thresh_imag = 1e-15;
493 std::vector<ChebyshevExpansion<ArrayType>> m_expansions;
494 std::vector<double> m_x_at_extrema;
495 std::vector<IntervalMatch> m_monotonic_intervals;
496
497 Eigen::ArrayXd head(const Eigen::ArrayXd& c, Eigen::Index N) const {
498 return c.head(N);
499 }
500 std::vector<double> head(const std::vector<double>& c, Eigen::Index N) const {
501 return {c.begin(), c.begin() + N};
502 }
503
508 std::vector<double> determine_extrema(const std::decay_t<decltype(m_expansions)>& expansions, double thresh_im) const {
509 std::vector<double> x_at_extrema;
510 Eigen::MatrixXd companion_matrix, cprime, D;
511 for (auto& expan : expansions) {
512 // Get the coefficients of the derivative's expansion (for x in [-1, 1])
513 auto cd = expan.do_derivs(1);
514 // First, right-trim the coefficients that are equal to zero
515 int ilastnonzero = static_cast<int>(cd.size()) - 1;
516 for (int i = static_cast<int>(cd.size()) - 1; i >= 0; --i) {
517 if (std::abs(cd[i]) != 0) {
518 ilastnonzero = i;
519 break;
520 }
521 }
522 if (ilastnonzero != static_cast<int>(cd.size() - 1)) {
523 cd = head(cd, ilastnonzero);
524 }
525 // Then do eigenvalue rootfinding after balancing
526 // Define working buffers here to avoid allocations all over the place
527 if (companion_matrix.rows() != static_cast<int>(cd.size() - 1)) {
528 companion_matrix.resize(cd.size() - 1, cd.size() - 1);
529 companion_matrix.setZero();
530 D.resizeLike(companion_matrix);
531 D.setZero();
532 cprime.resizeLike(companion_matrix);
533 cprime.setZero();
534 }
535 detail::companion_matrix_transposed(cd, companion_matrix);
536 cprime = companion_matrix;
537
538 detail::balance_matrix(companion_matrix, cprime, D);
539 for (auto& root : companion_matrix.eigenvalues()) {
540 // re_n11 is in [-1,1], need to rescale back to real units
541 auto re_n11 = root.real(), im = root.imag();
542 if (std::abs(im) < thresh_im) {
543 if (re_n11 >= -1 && re_n11 <= 1) {
544 x_at_extrema.push_back(((expan.xmax() - expan.xmin()) * re_n11 + (expan.xmax() + expan.xmin())) / 2.0);
545 }
546 }
547 }
548 }
549 std::sort(x_at_extrema.begin(), x_at_extrema.end());
550 return x_at_extrema;
551 }
552
557 std::vector<IntervalMatch> build_monotonic_intervals(const std::vector<double>& x_at_extrema) const {
558 std::vector<IntervalMatch> intervals;
559
560 auto sort = [](double& x, double& y) {
561 if (x > y) {
562 std::swap(x, y);
563 }
564 };
565
566 if (false) { //x_at_extrema.empty()){
567 auto xmin = m_expansions.front().xmin();
568 auto xmax = m_expansions.back().xmax();
569 auto ymin = eval(xmin), ymax = eval(xmax);
570 sort(ymin, ymax);
572 mem.idx = 0;
573 mem.xmin = xmin;
574 mem.xmax = xmax;
575 mem.ymin = ymin;
576 mem.ymax = ymax;
577 IntervalMatch im;
578 im.expansioninfo = {mem};
579 im.xmin = mem.xmin;
580 im.xmax = mem.xmax;
581 im.ymin = mem.ymin;
582 im.ymax = mem.ymax;
583 intervals.push_back(im);
584 } else {
585 auto newx = x_at_extrema;
586 newx.insert(newx.begin(), m_expansions.front().xmin());
587 newx.insert(newx.end(), m_expansions.back().xmax());
588
590 auto interval_intersection = [](const auto& t1, const auto& t2) {
591 auto a = std::max(t1.xmin(), t2.xmin);
592 auto b = std::min(t1.xmax(), t2.xmax);
593 return std::make_tuple(a, b);
594 };
595
596 for (auto j = 0; j < static_cast<int>(newx.size() - 1); ++j) {
597 double xmin = newx[j], xmax = newx[j + 1];
598 IntervalMatch im;
599 // Loop over the expansions that contain one of the values of x
600 // that intersect the interval defined by [xmin, xmax]
601 for (auto i = 0UL; i < m_expansions.size(); ++i) {
602 struct A
603 {
604 double xmin, xmax;
605 };
606 auto [a, b] = interval_intersection(m_expansions[i], A{xmin, xmax});
607 if (a < b) {
608 const auto& e = m_expansions[i];
610 mem.idx = i;
611 mem.xmin = a;
612 mem.xmax = b;
613 double ymin = e.eval(a), ymax = e.eval(b);
614 sort(ymin, ymax);
615 mem.ymin = ymin;
616 mem.ymax = ymax;
617 im.expansioninfo.push_back(mem);
618 }
619 }
620 // These are the limits for the entire interval
621 im.xmin = xmin;
622 im.xmax = xmax;
623 double yminoverall = eval(xmin), ymaxoverall = eval(xmax);
624 sort(yminoverall, ymaxoverall);
625 im.ymin = yminoverall;
626 im.ymax = ymaxoverall;
627 intervals.push_back(im);
628 }
629 }
630 return intervals;
631 }
632 double m_xmin,
633 m_xmax;
634
635 public:
636 // Move constructor given a vector of expansions
638 : m_expansions(std::move(expansions)),
639 m_x_at_extrema(determine_extrema(m_expansions, thresh_imag)),
640 m_monotonic_intervals(build_monotonic_intervals(m_x_at_extrema)),
641 m_xmin(get_expansions().front().xmin()),
642 m_xmax(get_expansions().back().xmax()) {}
643
645 : m_expansions(other.m_expansions),
646 m_x_at_extrema(other.m_x_at_extrema),
647 m_monotonic_intervals(other.m_monotonic_intervals),
648 m_xmin(other.xmin()),
649 m_xmax(other.xmax()) {}
650
652 // ChebyshevApproximation1D& operator=(const ChebyshevApproximation1D& other) = default;
654 std::swap(m_expansions, other.m_expansions);
655 std::swap(m_x_at_extrema, other.m_x_at_extrema);
656 std::swap(m_monotonic_intervals, other.m_monotonic_intervals);
657 std::swap(m_xmin, other.m_xmin);
658 std::swap(m_xmax, other.m_xmax);
659 return *this;
660 }
661
663 const auto& get_expansions() const {
664 return m_expansions;
665 }
666
668 const auto& get_x_at_extrema() const {
669 return m_x_at_extrema;
670 }
671
673 const auto& get_monotonic_intervals() const {
674 return m_monotonic_intervals;
675 }
676
678 const auto xmin() const {
679 return m_xmin;
680 }
681
683 const auto xmax() const {
684 return m_xmax;
685 }
686
689 bool is_monotonic() const {
690 return m_monotonic_intervals.size() == 1;
691 }
692
697 auto get_index(double x) const {
698
699 // https://proquest.safaribooksonline.com/9780321637413
700 // https://web.stanford.edu/class/archive/cs/cs107/cs107.1202/lab1/
701 auto midpoint_Knuth = [](int x, int y) { return (x & y) + ((x ^ y) >> 1); };
702
703 int iL = 0U, iR = static_cast<int>(m_expansions.size()) - 1, iM;
704 while (iR - iL > 1) {
705 iM = midpoint_Knuth(iL, iR);
706 if (x >= m_expansions[iM].xmin()) {
707 iL = iM;
708 } else {
709 iR = iM;
710 }
711 }
712 return (x < m_expansions[iL].xmax()) ? iL : iR;
713 };
714
719 double eval(double x) const {
720 return m_expansions[get_index(x)].eval(x);
721 }
722
724 template <typename Container>
725 const auto eval_many(const Container& x, Container& y) const {
726 if (x.size() != y.size()) {
727 throw std::invalid_argument("x and y are not the same size");
728 }
729 for (auto i = 0U; i < x.size(); ++i) {
730 y(i) = eval(x(i));
731 }
732 }
733
735 template <typename Container>
736 const auto eval_manyC(const Container x[], Container y[], std::size_t N) const {
737 for (auto i = 0U; i < N; ++i) {
738 y[i] = eval(x[i]);
739 }
740 }
741
743 const std::vector<IntervalMatch> get_intervals_containing_y(double y) const {
744 std::vector<IntervalMatch> matches;
745 for (auto& interval : m_monotonic_intervals) {
746 if (y >= interval.ymin && y <= interval.ymax) {
747 matches.push_back(interval);
748 }
749 }
750 return matches;
751 }
752
755 const auto get_x_for_y(double y, unsigned int bits, std::size_t max_iter, double boundsftol) const {
756 std::vector<std::pair<double, int>> solns;
757 for (const auto& interval : m_monotonic_intervals) {
758 // Each interval is required to be monotonic internally, so if the value of
759 // y is within the y values at the endpoints it is a candidate
760 // for rootfinding, otherwise continue
761 if (interval.contains_y(y)) {
762 // Now look at the expansions that intersect the interval
763 for (const auto& ei : interval.expansioninfo) {
764 // Again, since the portions of the expansions are required
765 // to be monotonic, if it is contained then a solution must exist
766 if (ei.contains_y(y)) {
767 const ChebyshevExpansion<ArrayType>& e = m_expansions[ei.idx];
768 auto [xvalue, num_steps] = e.solve_for_x_count(y, ei.xmin, ei.xmax, bits, max_iter, boundsftol);
769 solns.emplace_back(xvalue, static_cast<int>(num_steps));
770 }
771 }
772 }
773 }
774 return solns;
775 }
776
778 template <typename YContainer, typename CountContainer>
779 const auto count_x_for_y_many(const YContainer& y, unsigned int bits, std::size_t max_iter, double boundsftol, CountContainer& x) const {
780 if (x.size() != y.size()) {
781 throw std::invalid_argument("x and y are not the same size");
782 }
783 for (auto i = 0U; i < x.size(); ++i) {
784 x(i) = get_x_for_y(y(i), bits, max_iter, boundsftol).size();
785 }
786 }
787
789 template <typename YContainer, typename CountContainer>
790 const auto count_x_for_y_manyC(const YContainer y[], size_t N, unsigned int bits, std::size_t max_iter, double boundsftol,
791 CountContainer x[]) const {
792 for (auto i = 0U; i < N; ++i) {
793 x[i] = get_x_for_y(y[i], bits, max_iter, boundsftol).size();
794 }
795 }
796};
797
798static_assert(std::is_copy_constructible_v<ChebyshevApproximation1D<std::vector<double>>>);
799static_assert(std::is_copy_assignable_v<ChebyshevApproximation1D<std::vector<double>>>);
800
802{
803 double T,
805 std::size_t counter;
806};
807
816template <typename ArrayType = Eigen::ArrayXd>
818{
819 public:
826 {
827 double T;
828 double p;
829 double rhoL;
830 double rhoV;
831 double p_SA_ratio;
834 };
835
841 {
842 std::vector<ChebyshevExpansion<ArrayType>> rhoL, rhoV, p;
843 double Tcrit_num = 0;
844 double rhocrit_num = 0;
845 std::vector<CheckPoint> check_points;
846 };
847
848 private:
851 m_rhoV,
852 m_p;
853
854 // These ones *may* be present
855 std::optional<ChebyshevApproximation1D<ArrayType>> m_hL,
856 m_hV,
857 m_sL,
858 m_sV,
859 m_uL,
860 m_uV,
861 m_invlnp;
862
863 double m_Tmin;
864 double m_Tcrit_num;
865 double m_rhocrit_num;
866 double m_pmin;
867 double m_pmax;
868 std::vector<CheckPoint> m_check_points;
869
874 mutable std::mutex m_lazy_build_mutex;
875
893 std::optional<std::pair<double, double>> m_caloric_alpha0_stamp;
894
901 mutable std::atomic<unsigned int> m_caloric_build_count{0};
902
909 explicit SuperAncillary(LoadedData&& d)
910 : m_rhoL(std::move(d.rhoL)),
911 m_rhoV(std::move(d.rhoV)),
912 m_p(std::move(d.p)),
913 m_Tmin(m_p.xmin()),
914 m_Tcrit_num(d.Tcrit_num),
915 m_rhocrit_num(d.rhocrit_num),
916 m_pmin(m_p.eval(m_p.xmin())),
917 m_pmax(m_p.eval(m_p.xmax())),
918 m_check_points(std::move(d.check_points)) {}
919
923 auto make_invlnp(Eigen::Index Ndegree) {
924
925 auto pmin = m_p.eval(m_p.xmin());
926 auto pmax = m_p.eval(m_p.xmax());
927 // auto N = m_p.get_expansions().front().coeff().size()-1;
928 const double EPSILON = std::numeric_limits<double>::epsilon();
929
930 auto func = [&](long i, long Npts, double lnp) -> double {
931 double p = exp(lnp);
932 auto solns = m_p.get_x_for_y(p, 64, 100U, 1e-8);
933
934 if (solns.size() != 1) {
935 if ((i == 0 || i == Npts - 1) && (p > pmin * (1 - EPSILON * 1000) && p < pmin * (1 + EPSILON * 1000))) {
936 return m_p.get_monotonic_intervals()[0].xmin;
937 }
938 if ((i == 0 || i == Npts - 1) && (p > pmax * (1 - EPSILON * 1000) && p < pmax * (1 + EPSILON * 1000))) {
939 return m_p.get_monotonic_intervals()[0].xmax;
940 }
941 std::stringstream ss;
942 ss << std::setprecision(20) << "Must have one solution for T(p), got " << solns.size() << " for " << p << " Pa; limits are [" << pmin
943 << +" Pa , " << pmax << " Pa]; i is " << i;
944 throw std::invalid_argument(ss.str());
945 }
946 auto [T, iters] = solns[0];
947 return T;
948 };
949
950 using CE_t = std::vector<ChebyshevExpansion<ArrayType>>;
951 return detail::dyadic_splitting<decltype(func), CE_t>(Ndegree, func, log(pmin), log(pmax), 3, 1e-12, 26);
952 }
953
954 // using PropertyPairs = properties::PropertyPairs; ///< A convenience alias to save some typing
955
956 public:
964 explicit SuperAncillary(const std::string& s);
965
971 const auto& get_approx1d(char k, short Q) const {
972 auto get_or_throw = [&](const auto& v) -> const auto& {
973 if (v) {
974 return v.value();
975 } else {
976 throw std::invalid_argument("unable to get the variable " + std::string(1, k) + ", make sure it has been added to superancillary");
977 }
978 };
979 switch (k) {
980 case 'P':
981 return m_p;
982 case 'D':
983 return (Q == 0) ? m_rhoL : m_rhoV;
984 case 'H':
985 return (Q == 0) ? get_or_throw(m_hL) : get_or_throw(m_hV);
986 case 'S':
987 return (Q == 0) ? get_or_throw(m_sL) : get_or_throw(m_sV);
988 case 'U':
989 return (Q == 0) ? get_or_throw(m_uL) : get_or_throw(m_uV);
990 default:
991 throw std::invalid_argument("Bad key of '" + std::string(1, k) + "'");
992 }
993 }
1001 std::lock_guard<std::mutex> lk(m_lazy_build_mutex);
1002 m_hL.reset();
1003 m_hV.reset();
1004 m_sL.reset();
1005 m_sV.reset();
1006 m_uL.reset();
1007 m_uV.reset();
1008 m_caloric_alpha0_stamp.reset();
1009 }
1010
1021 void ensure_HSU_under_lock(double caller_a1, double caller_a2, const std::function<double(double, double)>& h_callable,
1022 const std::function<double(double, double)>& s_callable, const std::function<double(double, double)>& u_callable) {
1023 // Always lock. Skipping the lock for a fast-path peek at
1024 // optional::has_value() is technically a data race per the C++
1025 // standard. An uncontended mutex acquire is ~20 ns — much less than a
1026 // single EOS-side h/s/u evaluation.
1027 std::lock_guard<std::mutex> lk(m_lazy_build_mutex);
1028 if (m_hL.has_value() && m_hV.has_value() && m_sL.has_value() && m_sV.has_value() && m_uL.has_value() && m_uV.has_value()) {
1029 return; // already built; stamp captured at first build, never invalidated
1030 }
1031 add_variable_locked('H', h_callable);
1032 add_variable_locked('S', s_callable);
1033 // U shares the H reference-frame stamp: u = h - p/rho and p/rho is
1034 // independent of the ideal-gas enthalpy/entropy offset, so the constant
1035 // caller->cache shift for u equals the one for h.
1036 add_variable_locked('U', u_callable);
1037 m_caloric_alpha0_stamp = std::make_pair(caller_a1, caller_a2);
1038 ++m_caloric_build_count; // for test instrumentation
1039 }
1040
1043 std::optional<std::pair<double, double>> get_caloric_alpha0_stamp() const {
1044 std::lock_guard<std::mutex> lk(m_lazy_build_mutex);
1045 return m_caloric_alpha0_stamp;
1046 }
1047
1051 unsigned int get_caloric_build_count() const {
1052 return m_caloric_build_count.load();
1053 }
1054
1059 bool has_variable(char k) const {
1060 switch (k) {
1061 case 'P':
1062 case 'D':
1063 return true;
1064 case 'H':
1065 return m_hL.has_value() && m_hV.has_value();
1066 case 'S':
1067 return m_sL.has_value() && m_sV.has_value();
1068 case 'U':
1069 return m_uL.has_value() && m_uV.has_value();
1070 default:
1071 return false;
1072 }
1073 }
1078 const auto& get_invlnp() {
1079 std::lock_guard<std::mutex> lk(m_lazy_build_mutex);
1080 if (!m_invlnp) {
1081 // Degree of expansion is the same as
1082 auto Ndegree = m_p.get_expansions()[0].coeff().size() - 1;
1083 m_invlnp = make_invlnp(Ndegree);
1084 }
1085 return m_invlnp;
1086 }
1088 const double get_pmin() const {
1089 return m_pmin;
1090 }
1092 const double get_pmax() const {
1093 return m_pmax;
1094 }
1096 const double get_Tmin() const {
1097 return m_Tmin;
1098 }
1100 const double get_Tcrit_num() const {
1101 return m_Tcrit_num;
1102 }
1104 const double get_rhocrit_num() const {
1105 return m_rhocrit_num;
1106 }
1108 const std::vector<CheckPoint>& get_check_points() const {
1109 return m_check_points;
1110 }
1111
1120 void add_variable(char var, const std::function<double(double, double)>& caller) {
1121 std::lock_guard<std::mutex> lk(m_lazy_build_mutex);
1122 add_variable_locked(var, caller);
1123 }
1124
1125 private:
1127 void add_variable_locked(char var, const std::function<double(double, double)>& caller) {
1128 Eigen::MatrixXd Lmat, Umat;
1129 std::tie(Lmat, Umat) = detail::get_LU_matrices(12);
1130
1131 // Helpers to bridge ArrayType (which may be Eigen::ArrayXd or
1132 // std::vector<double>, depending on the SuperAncillary template
1133 // instantiation) and the Eigen ops used in the inner loop.
1134 auto coeff_to_eigen = [](const ArrayType& a) -> Eigen::ArrayXd {
1135 if constexpr (std::is_same_v<ArrayType, std::vector<double>>) {
1136 return Eigen::Map<const Eigen::ArrayXd>(a.data(), static_cast<Eigen::Index>(a.size()));
1137 } else {
1138 return a;
1139 }
1140 };
1141 auto eigen_to_arraytype = [](const Eigen::ArrayXd& e) -> ArrayType {
1142 if constexpr (std::is_same_v<ArrayType, std::vector<double>>) {
1143 return std::vector<double>(e.data(), e.data() + e.size());
1144 } else {
1145 return e;
1146 }
1147 };
1148
1149 auto builder = [&](char var, auto& variantL, auto& variantV) {
1150 std::vector<ChebyshevExpansion<ArrayType>> newexpL, newexpV;
1151 const auto& expsL = get_approx1d('D', 0);
1152 const auto& expsV = get_approx1d('D', 1);
1153 if (expsL.get_expansions().size() != expsV.get_expansions().size()) {
1154 throw std::invalid_argument("L&V are not the same size");
1155 }
1156 for (auto i = 0U; i < expsL.get_expansions().size(); ++i) {
1157 const auto& expL = expsL.get_expansions()[i];
1158 const auto& expV = expsV.get_expansions()[i];
1159 const auto& T = expL.get_nodes_realworld();
1160 // Get the functional values at the Chebyshev nodes
1161 Eigen::ArrayXd funcL = Umat * coeff_to_eigen(expL.coeff()).matrix();
1162 Eigen::ArrayXd funcV = Umat * coeff_to_eigen(expV.coeff()).matrix();
1163 // Apply the function inplace to do the transformation
1164 // of the functional values at the nodes
1165 for (auto j = 0; j < funcL.size(); ++j) {
1166 funcL(j) = caller(T(j), funcL(j));
1167 funcV(j) = caller(T(j), funcV(j));
1168 }
1169 // And now rebuild the expansions by left-multiplying by the L matrix
1170 Eigen::ArrayXd newcoeffL = (Lmat * funcL.matrix()).array();
1171 Eigen::ArrayXd newcoeffV = (Lmat * funcV.matrix()).array();
1172 newexpL.emplace_back(expL.xmin(), expL.xmax(), eigen_to_arraytype(newcoeffL));
1173 newexpV.emplace_back(expV.xmin(), expV.xmax(), eigen_to_arraytype(newcoeffV));
1174 }
1175
1176 variantL.emplace(std::move(newexpL));
1177 variantV.emplace(std::move(newexpV));
1178 };
1179
1180 switch (var) {
1181 case 'H':
1182 builder(var, m_hL, m_hV);
1183 break;
1184 case 'S':
1185 builder(var, m_sL, m_sV);
1186 break;
1187 case 'U':
1188 builder(var, m_uL, m_uV);
1189 break;
1190 default:
1191 throw std::invalid_argument("nope");
1192 }
1193 }
1194
1195 public:
1201 double eval_sat(double T, char k, short Q) const {
1202 if (Q == 1 || Q == 0) {
1203 return get_approx1d(k, Q).eval(T);
1204 } else {
1205 throw std::invalid_argument("invalid_value for Q");
1206 }
1207 }
1208
1212 template <typename Container>
1213 void eval_sat_many(const Container& T, char k, short Q, Container& y) const {
1214 if (T.size() != y.size()) {
1215 throw std::invalid_argument("T and y are not the same size");
1216 }
1217 const auto& approx = get_approx1d(k, Q);
1218 for (auto i = 0; i < T.size(); ++i) {
1219 y(i) = approx.eval(T(i));
1220 }
1221 }
1222
1226 template <typename Container>
1227 void eval_sat_manyC(const Container T[], std::size_t N, char k, short Q, Container y[]) const {
1228 // if (T.size() != y.size()){ throw std::invalid_argument("T and y are not the same size"); }
1229 const auto& approx = get_approx1d(k, Q);
1230 for (std::size_t i = 0; i < N; ++i) {
1231 y[i] = approx.eval(T[i]);
1232 }
1233 }
1234
1244 auto solve_for_T(double propval, char k, bool Q, unsigned int bits = 64, unsigned int max_iter = 100, double boundsftol = 1e-13) const {
1245 return get_approx1d(k, Q).get_x_for_y(propval, bits, max_iter, boundsftol);
1246 }
1247
1254 auto get_vaporquality(double T, double propval, char k) const {
1255 if (k == 'D') {
1256 // Need to special-case here because v = q*v_V + (1-q)*v_V but it is NOT(!!!!) the case that rho = q*rho_V + (1-q)*rho_L
1257 double v_L = 1 / get_approx1d('D', 0).eval(T);
1258 double v_V = 1 / get_approx1d('D', 1).eval(T);
1259 return (1 / propval - v_L) / (v_V - v_L);
1260 } else {
1261 double L = get_approx1d(k, 0).eval(T);
1262 double V = get_approx1d(k, 1).eval(T);
1263 return (propval - L) / (V - L);
1264 }
1265 }
1266
1272 auto get_T_from_p(double p) {
1273 return get_invlnp().value().eval(log(p));
1274 }
1275
1283 auto get_yval(double T, double q, char k) const {
1284
1285 if (k == 'D') {
1286 // Need to special-case here because v = q*v_V + (1-q)*v_V but it is NOT(!!!!) the case that rho = q*rho_V + (1-q)*rho_L
1287 double v_L = 1 / get_approx1d('D', 0).eval(T);
1288 double v_V = 1 / get_approx1d('D', 1).eval(T);
1289 double v = q * v_V + (1 - q) * v_L;
1290 return 1 / v; // rho = 1/v
1291 } else {
1292 double L = get_approx1d(k, 0).eval(T);
1293 double V = get_approx1d(k, 1).eval(T);
1294 return q * V + (1 - q) * L;
1295 }
1296 }
1297
1299 template <typename Container>
1300 void get_yval_many(const Container& T, char k, const Container& q, Container& y) const {
1301 if (T.size() != y.size() || T.size() != q.size()) {
1302 throw std::invalid_argument("T, q, and y are not all the same size");
1303 }
1304
1305 const auto& L = get_approx1d(k, 0);
1306 const auto& V = get_approx1d(k, 1);
1307
1308 if (k == 'D') {
1309 for (auto i = 0; i < T.size(); ++i) {
1310 // Need to special-case here because v = q*v_V + (1-q)*v_V but it is NOT(!!!!) the case that rho = q*rho_V + (1-q)*rho_L
1311 double v_L = 1.0 / L.eval(T(i));
1312 double v_V = 1.0 / V.eval(T(i));
1313 double v = q(i) * v_V + (1 - q(i)) * v_L;
1314 y(i) = 1 / v;
1315 }
1316 } else {
1317 for (auto i = 0; i < T.size(); ++i) {
1318 y(i) = q(i) * V.eval(T(i)) + (1 - q(i)) * L.eval(T(i));
1319 }
1320 }
1321 }
1329 auto get_all_intersections(const char k, const double val, unsigned int bits, std::size_t max_iter, double boundsftol) const {
1330 const auto& L = get_approx1d(k, 0);
1331 const auto& V = get_approx1d(k, 1);
1332 auto TsatL = L.get_x_for_y(val, bits, max_iter, boundsftol);
1333 const auto TsatV = V.get_x_for_y(val, bits, max_iter, boundsftol);
1334 for (auto&& el : TsatV) {
1335 TsatL.push_back(el);
1336 }
1337 // TsatL.insert(TsatL.end(),
1338 // std::make_move_iterator(TsatV.begin() + TsatV.size()),
1339 // std::make_move_iterator(TsatV.end()));
1340 return TsatL;
1341 }
1342
1357 std::optional<SuperAncillaryTwoPhaseSolution> iterate_for_Tq_XY(double Tmin, double Tmax, char ch1, double val1, char ch2, double val2,
1358 unsigned int bits, std::size_t max_iter, double boundsftol) const {
1359
1360 std::size_t counter = 0;
1361 auto f = [&](double T_) {
1362 counter++;
1363 double q_fromv1 = get_vaporquality(T_, val1, ch1);
1364 double resid = get_yval(T_, q_fromv1, ch2) - val2;
1365 return resid;
1366 };
1367 double fTmin = f(Tmin), fTmax = f(Tmax);
1368
1369 // First we check if we are converged enough (TOMS748 does not stop based on function value)
1370 double T;
1371
1372 // A little lambda to make it easier to return
1373 // in different logical branches
1374 auto returner = [&]() {
1376 soln.T = T;
1377 soln.q = get_vaporquality(T, val1, ch1);
1378 soln.counter = counter;
1379 return soln;
1380 };
1381
1382 if (std::abs(fTmin) < boundsftol) {
1383 T = Tmin;
1384 return returner();
1385 }
1386 if (std::abs(fTmax) < boundsftol) {
1387 T = Tmax;
1388 return returner();
1389 }
1390 if (fTmin * fTmax > 0) {
1391 // No sign change, this means that the inputs are not within the two-phase region
1392 // and thus no iteration is needed
1393 return std::nullopt;
1394 }
1395 // Neither bound meets the convergence criterion, we need to iterate on temperature
1396 try {
1397 T = detail::toms748(f, Tmin, Tmax, fTmin, fTmax, bits, max_iter);
1398 return returner();
1399 } catch (...) {
1400 fprintf(stderr, "fTmin,fTmax: %g,%g\n", fTmin, fTmax);
1401 throw;
1402 }
1403 }
1404
1414 std::optional<SuperAncillaryTwoPhaseSolution> solve_for_Tq_DX(const double rho, const double propval, const char k, unsigned int bits,
1415 std::size_t max_iter, double boundsftol) const {
1416
1417 const auto& Lrho = get_approx1d('D', 0);
1418 const auto& Vrho = get_approx1d('D', 1);
1419 auto Tsat = get_all_intersections(k, propval, bits, max_iter, boundsftol);
1420 std::size_t rhosat_soln_count = Tsat.size();
1421
1422 std::tuple<double, double> Tsearchrange;
1423 if (rhosat_soln_count == 1) {
1424 // Search the complete range from Tmin to the intersection point where rhosat(T) = rho
1425 // obtained just above
1426 Tsearchrange = std::make_tuple(Lrho.xmin * 0.999, std::get<0>(Tsat[0]));
1427 } else if (rhosat_soln_count == 2) {
1428 double y1 = std::get<0>(Tsat[0]), y2 = std::get<0>(Tsat[1]);
1429 if (y2 < y1) {
1430 std::swap(y2, y2);
1431 } // Required to be sorted in increasing value
1432 Tsearchrange = std::make_tuple(y1, y2);
1433 } else {
1434 throw std::invalid_argument("cannot have number of solutions other than 1 or 2; got " + std::to_string(rhosat_soln_count) + " solutions");
1435 }
1436 auto [a, b] = Tsearchrange;
1437 return iterate_for_Tq_XY(a, b, 'D', rho, k, propval, bits, max_iter, boundsftol);
1438 }
1439
1441 template <typename Container>
1442 void solve_for_Tq_DX_many(const Container& rho, const Container& propval, const char k, unsigned int bits, std::size_t max_iter,
1443 double boundsftol, Container& T, Container& q, Container& count) {
1444 if (std::set<std::size_t>({rho.size(), propval.size(), T.size(), q.size(), count.size()}).size() != 1) {
1445 throw std::invalid_argument("rho, propval, T, q, count are not all the same size");
1446 }
1447 for (auto i = 0U; i < T.size(); ++i) {
1448 auto osoln = solve_for_Tq_DX(rho(i), propval(i), k, bits, max_iter, boundsftol);
1449 if (osoln) {
1450 const auto& soln = osoln.value();
1451 T(i) = soln.T;
1452 q(i) = soln.q;
1453 count(i) = soln.counter;
1454 } else {
1455 T(i) = -1;
1456 q(i) = -1;
1457 count(i) = -1;
1458 }
1459 }
1460 }
1461
1462 // /**
1463 // The high-level function used to carry out a solution. It handles all the different permutations of variables and delegates to lower-level functions to actually od the calculations
1464
1465 // \param pair The enumerated pair of thermodynamic variables being provided
1466 // \param val1 The first value
1467 // \param val2 The second value
1468 // */
1469 // auto flash(PropertyPairs pair, double val1, double val2) const -> std::optional<SuperAncillaryTwoPhaseSolution>{
1470 // double T, q;
1471 // std::size_t counter = 0;
1472 // double boundsftol = 1e-12;
1473
1474 // auto returner = [&](){
1475 // SuperAncillaryTwoPhaseSolution soln;
1476 // soln.T = T;
1477 // soln.q = q;
1478 // soln.counter = counter;
1479 // return soln;
1480 // };
1481
1482 // auto get_T_other = [&](){
1483 // switch (pair){
1484 // case PropertyPairs::DT: return std::make_tuple(val2, val1, 'D');
1485 // case PropertyPairs::HT: return std::make_tuple(val2, val1, 'H');
1486 // case PropertyPairs::ST: return std::make_tuple(val2, val1, 'S');
1487 // case PropertyPairs::TU: return std::make_tuple(val1, val2, 'U');
1488 // default:
1489 // throw std::invalid_argument("not valid");
1490 // };
1491 // };
1492 // auto get_p_other = [&](){
1493 // switch (pair){
1494 // case PropertyPairs::DP: return std::make_tuple(val2, val1, 'D');
1495 // case PropertyPairs::HP: return std::make_tuple(val2, val1, 'H');
1496 // case PropertyPairs::PS: return std::make_tuple(val1, val2, 'S');
1497 // case PropertyPairs::PU: return std::make_tuple(val1, val2, 'U');
1498 // default:
1499 // throw std::invalid_argument("not valid");
1500 // };
1501 // };
1502 // auto get_rho_other = [&](){
1503 // switch (pair){
1504 // case PropertyPairs::DH: return std::make_tuple(val1, val2, 'H');
1505 // case PropertyPairs::DS: return std::make_tuple(val1, val2, 'S');
1506 // case PropertyPairs::DU: return std::make_tuple(val1, val2, 'U');
1507 // default:
1508 // throw std::invalid_argument("not valid");
1509 // };
1510 // };
1511
1512 // // Given the arguments, unpack them into (xchar, xval, ychar, yval) where x is the
1513 // // variable of convenience that will be used to do the interval intersection and then
1514 // // the iteration will be in the other variable
1515 // auto parse_HSU = [&](){
1516 // switch (pair){
1517 // case PropertyPairs::HS: return std::make_tuple('S', val2, 'H', val1);
1518 // case PropertyPairs::SU: return std::make_tuple('S', val1, 'U', val2);
1519 // case PropertyPairs::HU: return std::make_tuple('H', val1, 'U', val2);
1520 // default:
1521 // throw std::invalid_argument("not valid");
1522 // };
1523 // };
1524
1525 // switch(pair){
1526 // // Case 0, PT is always single-phase, by definition
1527 // case PropertyPairs::PT: throw std::invalid_argument("PT inputs are trivial");
1528
1529 // // Case 1, T is a given variable
1530 // case PropertyPairs::DT:
1531 // case PropertyPairs::HT:
1532 // case PropertyPairs::ST:
1533 // case PropertyPairs::TU:
1534 // {
1535 // auto [Tval, other, otherchar] = get_T_other();
1536 // q = get_vaporquality(Tval, other, otherchar);
1537 // T = Tval;
1538 // if (0 <= q && q <= 1){
1539 // return returner();
1540 // }
1541 // break;
1542 // }
1543 // // Case 2, p is a given variable, p gets converted to T, and then q calculated
1544 // case PropertyPairs::DP:
1545 // case PropertyPairs::HP:
1546 // case PropertyPairs::PS:
1547 // case PropertyPairs::PU:
1548 // {
1549 // auto [p, other, otherchar] = get_p_other();
1550 // T = get_T_from_p(p);
1551 // q = get_vaporquality(T, other, otherchar); // Based on the T and the other variable
1552 // if (0 <= q && q <= 1){
1553 // return returner();
1554 // }
1555 // break;
1556 // }
1557 // // Case 3, rho is a given variable, special case that because it is relatively simple
1558 // // and only water and heavy water have more than one density solution along the saturation curve
1559 // case PropertyPairs::DH:
1560 // case PropertyPairs::DS:
1561 // case PropertyPairs::DU:
1562 // {
1563 // auto [rho, otherval, otherchar] = get_rho_other();
1564 // auto optsoln = solve_for_Tq_DX(rho, otherval, otherchar, 64, 100, boundsftol);
1565 // if (optsoln){
1566 // const auto& soln = optsoln.value();
1567 // T = soln.T;
1568 // q = soln.q;
1569 // counter = soln.counter;
1570 // return returner();
1571 // }
1572 // break;
1573 // }
1574 // // Case 4, handle all the cases where complicated interval checks are
1575 // // necessary
1576 // case PropertyPairs::HS:
1577 // case PropertyPairs::HU:
1578 // case PropertyPairs::SU:
1579 // {
1580 // auto [xchar, xval, ychar, yval] = parse_HSU();
1581 // auto Tsat = get_all_intersections(xchar, xval, 64, 100, boundsftol);
1582 // auto sort_predicate = [](const auto& x, const auto& y) {
1583 // return std::get<0>(x) < std::get<0>(y);
1584 // };
1585 // std::sort(Tsat.begin(), Tsat.end(), sort_predicate);
1586 // // std::cout << Tsat.size() << " T crossings for " << xval << std::endl;
1587 // // for (auto & el : Tsat){
1588 // // std::cout << std::get<0>(el) << std::endl;
1589 // // }
1590 // if (Tsat.size() == 1){
1591 // // A single crossing, in which the other temperature bound for iteration
1592 // // is assumed to be the minimum temperature of the superancillary
1593 // double Tmin = get_approx1d('D', 0).xmin*0.9999;
1594 // double Tmax = std::get<0>(Tsat[0]);
1595 // auto o = iterate_for_Tq_XY(Tmin, Tmax, xchar, xval, ychar, yval, 64, 100, boundsftol);
1596 // if (o){ return o.value(); }
1597 // }
1598 // else if (Tsat.size() % 2 == 0){ // Even number of crossings
1599 // // neighboring intersections should bound the solution, or if not, the point is single-phase
1600 // for (auto i = 0; i < Tsat.size(); i += 2){
1601 // auto o = iterate_for_Tq_XY(std::get<0>(Tsat[i]), std::get<0>(Tsat[i+1]), xchar, xval, ychar, yval, 64, 100, boundsftol);
1602 // if (o){ return o.value(); }
1603 // }
1604 // }
1605 // else{
1606 // throw std::invalid_argument("Don't know what to do about this number of T crossings ("+std::to_string(Tsat.size())+") yet");
1607 // }
1608 // break;
1609 // }
1610 // default:
1611 // throw std::invalid_argument("These inputs are not yet supported in flash");
1612 // }
1613 // return std::nullopt;
1614 // }
1615
1616 // /// A vectorized version of the flash function used in the Python interface for profiling
1617 // template <typename Container>
1618 // void flash_many(const PropertyPairs ppair, const Container& val1, const Container& val2, Container& T, Container& q, Container& count){
1619 // if (std::set<std::size_t>({val1.size(), val2.size(), T.size(), q.size(), count.size()}).size() != 1){
1620 // throw std::invalid_argument("val1, val2, T, q, count are not all the same size");
1621 // }
1622 // for (auto i = 0U; i < T.size(); ++i){
1623 // try{
1624 // auto osoln = flash(ppair, val1(i), val2(i));
1625 // if (osoln){
1626 // const auto& soln = osoln.value();
1627 // T(i) = soln.T;
1628 // q(i) = soln.q;
1629 // count(i) = soln.counter;
1630 // }
1631 // else{
1632 // T(i) = -1;
1633 // q(i) = -1;
1634 // count(i) = -1;
1635 // }
1636 // }
1637 // catch(std::exception&e){
1638 // // std::cout << e.what() << " for " << val1(i) << "," << val2(i) << std::endl;
1639 // T(i) = -2;
1640 // q(i) = -2;
1641 // count(i) = -2;
1642 // }
1643
1644 // }
1645 // }
1646};
1647
1648// SuperAncillary holds a std::mutex for lazy-build serialization (#2773 review C2),
1649// so it is intentionally non-copyable. In production it is held via std::shared_ptr
1650// from EquationOfState; ownership is shared, never duplicated.
1651static_assert(!std::is_copy_constructible_v<SuperAncillary<std::vector<double>>>);
1652static_assert(!std::is_copy_assignable_v<SuperAncillary<std::vector<double>>>);
1653
1654} // namespace superancillary
1655} // namespace CoolProp