15#if defined(ENABLE_CATCH)
16# include <catch2/catch_all.hpp>
20#include "boost/math/tools/toms748_solve.hpp"
29 std::size_t iclosest = 0;
35 throw ValueError(
"Phase envelope is_inside returned false without populating closest_state");
59 std::size_t N = env.K.size();
63 for (std::size_t k = 0; k < N; ++k) {
64 o.
x[k] = env.x[k][iclosest];
65 o.
y[k] = env.y[k][iclosest];
80 }
else if (o.
beta > 1.0 - 1e-10) {
104 auto solve_single_phase = [&]() {
117 bool gas_ok = rho_srk_gas > 0 &&
ValidNumber(rho_srk_gas);
118 bool liq_ok = rho_srk_liq > 0 &&
ValidNumber(rho_srk_liq);
120 if (gas_ok && liq_ok) {
122 double rho_gas = -1, rho_liq = -1;
135 if (rho_gas > 0 && rho_liq > 0) {
138 rho = (G_liq <= G_gas) ? rho_liq : rho_gas;
139 }
else if (rho_gas > 0 || rho_liq > 0) {
140 rho = (rho_gas > 0) ? rho_gas : rho_liq;
142 throw ValueError(
"Unable to obtain either HEOS density root in PT_flash_mixtures");
170 bool do_twophase = !stability_tester.
is_stable();
173 bool wilson_seeded =
false;
191 wilson_seeded =
true;
197 wilson_seeded =
false;
203 if (!wilson_seeded) {
229 for (std::size_t i = 0; i < o.
z.size(); ++i)
230 spread = std::max(spread, std::abs(o.
x[i] - o.
y[i]));
231 HEOS.
SatL->set_mole_fractions(o.
x);
233 HEOS.
SatV->set_mole_fractions(o.
y);
236 for (std::size_t i = 0; i < o.
z.size(); ++i) {
237 if (o.
x[i] < 1e-12 || o.
y[i] < 1e-12)
continue;
238 CoolPropDbl lnfL = std::log(o.
x[i]) + std::log(HEOS.
SatL->fugacity_coefficient(i));
239 CoolPropDbl lnfV = std::log(o.
y[i]) + std::log(HEOS.
SatV->fugacity_coefficient(i));
240 fug_resid = std::max(fug_resid, std::abs(lnfV - lnfL));
242 ok = (spread >= 1e-6) &&
ValidNumber(fug_resid) && (fug_resid <= 1e-7);
246 if (!ok) do_twophase =
false;
270 if (o.
beta < 1e-10) {
274 }
else if (o.
beta > 1.0 - 1e-10) {
286 solve_single_phase();
321 bool saturation_called =
false;
329 throw ValueError(
"twophase not implemented yet");
386 double omega = NAN, R = NAN, kappa = NAN, a = NAN, b = NAN, A = NAN, B = NAN, C = NAN, Tc = NAN, pc = NAN, V = 1 / rhomolar;
392 kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega * omega;
393 a = 0.457235 * R * R * Tc * Tc / pc;
394 b = 0.077796 * R * Tc / pc;
395 double den = V * V + 2 * b * V - b * b;
398 A = R * Tc / (V - b) - a * kappa * kappa / (den);
399 B = +2 * a * kappa * (1 + kappa) / (den);
400 C = -a * (1 + 2 * kappa + kappa * kappa) / (den)-p;
404 double sqrt_Tr1 = (-B + sqrt(B * B - 4 * A * C)) / (2 * A);
406 return sqrt_Tr1 * sqrt_Tr1 * Tc;
417 bool saturation_called =
false;
424 if (saturation_called) {
438 if (!std::isfinite(T0)) {
439 throw ValueError(
"Starting value of T0 is not valid in DP_flash");
445 Halley(resid, T0, 1e-10, 100);
465 flo = resid.
call(Tlo);
466 fhi = resid.
call(Thi);
473 boost::math::uintmax_t max_iter = 100;
474 auto f = [&resid](
const double T) {
return resid.
call(
T); };
475 auto [l, r] = toms748_solve(f,
static_cast<double>(Tlo),
static_cast<double>(Thi),
static_cast<double>(flo),
476 static_cast<double>(fhi), boost::math::tools::eps_tolerance<double>(30), max_iter);
478 resid.
call(0.5 * (l + r));
482 throw ValueError(
format(
"DP_flash TOMS748 fallback failed for rho=%Lg mol/m^3, p=%Lg Pa",
486 throw ValueError(
format(
"DP_flash could not bracket T for rho=%Lg mol/m^3, p=%Lg Pa (Halley diverged)",
530static constexpr double Q_BOUNDARY_TOL = 1e-10;
534static constexpr double T_ROOT_DEDUP_TOL = 1e-6;
544 const auto& core = alpha0.EnthalpyEntropyOffsetCore;
545 const auto& user = alpha0.EnthalpyEntropyOffset;
546 const double prefactor = alpha0.get_prefactor();
547 const double a1_sum =
548 (core.is_enabled() ?
static_cast<double>(core.get_a1()) : 0.0) + (user.is_enabled() ?
static_cast<double>(user.get_a1()) : 0.0);
549 const double a2_sum =
550 (core.is_enabled() ?
static_cast<double>(core.get_a2()) : 0.0) + (user.is_enabled() ?
static_cast<double>(user.get_a2()) : 0.0);
551 return {prefactor * a1_sum, prefactor * a2_sum};
559 if (!HEOS.
is_pure())
return false;
561 if (!p)
return false;
562 const double Q = HEOS.
_Q;
563 return std::abs(Q) < Q_BOUNDARY_TOL || std::abs(Q - 1) < Q_BOUNDARY_TOL;
572 const double Q = HEOS.
_Q;
590 double Tmin = HEOS.
Tmin() + 0.1;
591 const double eps = 1e-12;
594 format(
"DQ inputs are not defined for density (%g) above critical density (%g) and Q>0", rhomolar, HEOS.
rhomolar_critical()).c_str());
608 if (std::abs(HEOS.
Q() - 1) > 1e-10) {
609 throw ValueError(
format(
"non-unity quality not currently allowed for HQ_flash"));
612 const double hmolar = HEOS.
_hmolar;
649 const double smolar = HEOS.
_smolar;
660 if (std::abs(HEOS.
Q()) < 1e-10) {
668 }
else if (std::abs(HEOS.
Q() - 1) < 1e-10) {
677 throw ValueError(
format(
"non-zero or 1 quality not currently allowed for QS_flash"));
684static char param_to_superanc_key(
parameters key) {
695 throw ValueError(
format(
"Unsupported parameters key for saturation superancillary: %d",
static_cast<int>(key)));
713 std::optional<double> guess_T,
const char* fn_name) {
714 if (guess_T.has_value() && (!
ValidNumber(*guess_T) || *guess_T <= 0)) {
727 const double Q = HEOS.
_Q;
729 if (std::abs(Q) < Q_BOUNDARY_TOL) {
731 }
else if (std::abs(Q - 1) < Q_BOUNDARY_TOL) {
734 throw ValueError(
format(
"%s currently requires Q=0 or Q=1; got Q=%g", fn_name, Q));
736 const char k = param_to_superanc_key(key);
737 if (k ==
'H' || k ==
'S' || k ==
'U') {
740 auto& superanc = *superanc_ptr;
741 if (!superanc.has_variable(k)) {
744 const auto& approx = superanc.get_approx1d(k, Q_key);
745 const char* phase_name = (Q_key == 0) ?
"liquid" :
"vapor";
755 double target_in_cache_frame = target_value;
756 if (k ==
'H' || k ==
'S' || k ==
'U') {
757 const auto stamp = superanc.get_caloric_alpha0_stamp();
758 if (stamp.has_value()) {
760 const double cache_a1 = stamp->first;
761 const double cache_a2 = stamp->second;
762 if (k ==
'H' || k ==
'U') {
767 target_in_cache_frame = target_value + R * T_red * (cache_a2 - caller_a2);
771 target_in_cache_frame = target_value - R * (cache_a1 - caller_a1);
776 if (guess_T.has_value()) {
782 const double gT = *guess_T;
783 const auto& intervals = approx.get_monotonic_intervals();
784 const auto& expansions = approx.get_expansions();
785 const auto* chosen = [&]() ->
const auto* {
786 const auto* best_match =
static_cast<decltype(intervals.data())
>(
nullptr);
787 double best_midpoint_dist = std::numeric_limits<double>::infinity();
788 for (
const auto& iv : intervals) {
789 if (!iv.contains_y(target_in_cache_frame))
continue;
790 if (gT >= iv.xmin && gT <= iv.xmax) {
791 const double mid_dist = std::abs(0.5 * (iv.xmin + iv.xmax) - gT);
792 if (mid_dist < best_midpoint_dist) {
793 best_midpoint_dist = mid_dist;
800 if (chosen !=
nullptr) {
801 for (
const auto& ei : chosen->expansioninfo) {
802 if (ei.contains_y(target_in_cache_frame)) {
803 const auto& e = expansions[ei.idx];
804 auto [xvalue, num_steps] = e.solve_for_x_count(target_in_cache_frame, ei.xmin, ei.xmax, 64, 100U, 1e-10);
810 throw SolutionError(
format(
"%s: no T-root on saturated %s in the monotonic sub-interval containing guess.T=%g K for %c=%g; "
811 "superancillary range [%g, %g] K",
812 fn_name, phase_name, gT, k, target_value, approx.xmin(), approx.xmax()));
817 auto solns = approx.get_x_for_y(target_in_cache_frame, 64, 100U, 1e-10);
819 throw OutOfRangeError(
format(
"%s: no T-root on saturated %s for %c=%g; superancillary range [%g, %g] K", fn_name, phase_name, k, target_value,
820 approx.xmin(), approx.xmax()));
822 std::sort(solns.begin(), solns.end(), [](
const auto& a,
const auto& b) { return a.first < b.first; });
823 std::vector<double> Ts;
824 Ts.reserve(solns.size());
825 for (
const auto& s : solns) {
826 if (Ts.empty() || std::abs(s.first - Ts.back()) > T_ROOT_DEDUP_TOL) {
827 Ts.push_back(s.first);
832 for (std::size_t i = 0; i < Ts.size(); ++i) {
833 Ts_str +=
format(
"%g K", Ts[i]);
834 if (i + 1 < Ts.size()) Ts_str +=
", ";
837 format(
"%s: %c=%g on saturated %s has %zu T-roots (%s); use update_with_guesses with guess.T to pick a branch (see GitHub #2773)", fn_name,
838 k, target_value, phase_name, Ts.size(), Ts_str.c_str()));
866 const double hmolar = HEOS.
_hmolar;
878 const double smolar = HEOS.
_smolar;
896 auto& superanc = *superanc_ptr;
901 format(
"Temperature to QT_flash [%0.8Lg K] may not be above the numerical critical point of %0.15Lg K",
T, Tcrit_num));
903 auto rhoL = superanc.eval_sat(
T,
'D', 0);
904 auto rhoV = superanc.eval_sat(
T,
'D', 1);
905 auto p = superanc.eval_sat(
T,
'P', 1);
906 HEOS.
SatL->update_TDmolarP_unchecked(
T, rhoL, p);
907 HEOS.
SatV->update_TDmolarP_unchecked(
T, rhoV, p);
909 HEOS.
_rhomolar = 1 / (Q / rhoV + (1 - Q) / rhoL);
920 CoolPropDbl Tmin_satL = NAN, Tmin_satV = NAN, Tmin_sat = NAN;
922 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
927 if ((
get_config_bool(CRITICAL_WITHIN_1UK) && std::abs(
T - Tmax_sat) < 1e-6) || std::abs(
T - Tmax_sat) < 1e-12) {
932 HEOS.
_p = 0.5 * HEOS.
SatV->p() + 0.5 * HEOS.
SatL->p();
934 throw ValueError(
format(
"Temperature to QT_flash [%0.8Lg K] must be in range [%0.8Lg K, %0.8Lg K]",
T, Tmin_sat - 0.1, Tmax_sat));
936 double rhoL = _HUGE, rhoV = _HUGE;
941 HEOS.
_p = 0.5 * HEOS.
SatV->p() + 0.5 * HEOS.
SatL->p();
943 }
else if (!(HEOS.
components[0].EOS().pseudo_pure)) {
950 HEOS.
_p = 0.5 * HEOS.
SatV->p() + 0.5 * HEOS.
SatL->p();
954 CoolPropDbl rhoLanc = _HUGE, rhoVanc = _HUGE, rhoLsat = _HUGE, rhoVsat = _HUGE;
957 rhoLanc = HEOS.
components[0].ancillaries.rhoL.evaluate(HEOS.
_T);
958 HEOS.
SatL->update_TP_guessrho(HEOS.
_T, HEOS.
_p, rhoLanc);
962 rhoVanc = HEOS.
components[0].ancillaries.rhoV.evaluate(HEOS.
_T);
963 HEOS.
SatV->update_TP_guessrho(HEOS.
_T, HEOS.
_p, rhoVanc);
966 throw CoolProp::ValueError(
format(
"For pseudo-pure fluid, quality must be equal to 0 or 1. Two-phase quality is not defined"));
984 bool solved_with_envelope =
false;
993 solved_with_envelope =
true;
997 solved_with_envelope =
false;
1000 if (!solved_with_envelope) {
1036 NR.
call(HEOS, IO.
x, IO.
y, IO);
1039 NR.
call(HEOS, IO.
y, IO.
x, IO);
1047 HEOS.
_p = HEOS.
SatV->p();
1049 HEOS.
_T = HEOS.
SatL->T();
1055 if (CAS ==
"7440-59-7")
1062 }
else if (CAS ==
"7440-01-9")
1069 }
else if (CAS ==
"7440-37-1")
1076 }
else if (CAS ==
"7439-90-9")
1083 }
else if (CAS ==
"7440-63-3")
1090 }
else if (CAS ==
"1333-74-0")
1097 }
else if (CAS ==
"7727-37-9")
1104 }
else if (CAS ==
"7782-44-7")
1111 }
else if (CAS ==
"630-08-0")
1118 }
else if (CAS ==
"124-38-9")
1125 }
else if (CAS ==
"7783-06-4")
1132 }
else if (CAS ==
"74-82-8")
1139 }
else if (CAS ==
"74-84-0")
1146 }
else if (CAS ==
"2551-62-4")
1154 throw ValueError(
"Bad component in Henry's law constants");
1163 auto& superanc = *superanc_ptr;
1165 if (HEOS.
_p > pmax_num) {
1167 format(
"Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS.
_p, pmax_num));
1169 auto T = superanc.get_T_from_p(HEOS.
_p);
1170 auto rhoL = superanc.eval_sat(
T,
'D', 0);
1171 auto rhoV = superanc.eval_sat(
T,
'D', 1);
1173 HEOS.
SatL->update_TDmolarP_unchecked(
T, rhoL, p);
1174 HEOS.
SatV->update_TDmolarP_unchecked(
T, rhoV, p);
1192 HEOS.
SatL->update_TP_guessrho(HEOS.
_TLanc, HEOS.
_p, rhoL);
1193 HEOS.
SatV->update_TP_guessrho(HEOS.
_TVanc, HEOS.
_p, rhoV);
1197 HEOS.
_p = HEOS.
_Q * HEOS.
SatV->p() + (1 - HEOS.
_Q) * HEOS.
SatL->p();
1198 HEOS.
_T = HEOS.
_Q * HEOS.
SatV->T() + (1 - HEOS.
_Q) * HEOS.
SatL->T();
1205 CoolPropDbl pmin_satL = NAN, pmin_satV = NAN, pmin_sat = NAN;
1207 pmin_sat = std::max(pmin_satL, pmin_satV);
1222 throw ValueError(
format(
"Pressure to PQ_flash [%6g Pa] must be in range [%8Lg Pa, %8Lg Pa]", HEOS.
_p, pmin_sat, pmax_sat));
1236 double increment = 0.4;
1241 for (
double omega = 1.0; omega > 0; omega -= increment) {
1243 options.
omega = omega;
1251 if (omega < 1.1 * increment) {
1264 HEOS.
_p = HEOS.
_Q * HEOS.
SatV->p() + (1 - HEOS.
_Q) * HEOS.
SatL->p();
1266 HEOS.
_T = HEOS.
SatL->T();
1272 bool solved_with_envelope =
false;
1279 solved_with_envelope =
true;
1283 solved_with_envelope =
false;
1286 if (!solved_with_envelope) {
1299 std::vector<CoolPropDbl> K = HEOS.
K;
1301 if (
get_config_bool(HENRYS_LAW_TO_GENERATE_VLE_GUESSES) && std::abs(HEOS.
_Q - 1) < 1e-10) {
1302 const std::vector<CoolPropFluid>& components = HEOS.
get_components();
1303 std::size_t iWater = 0;
1304 double p1star =
PropsSI(
"P",
"T", Tguess,
"Q", 1,
"Water");
1306 std::vector<CoolPropDbl> x(y.size());
1307 for (std::size_t i = 0; i < components.size(); ++i) {
1308 if (components[i].CAS ==
"7732-18-5") {
1312 double A = NAN, B = NAN, C = NAN, Tmin = NAN, Tmax = NAN;
1314 double T_R = Tguess / 647.096, tau = 1 - T_R;
1315 double k_H = p1star * exp(A / T_R + B * pow(tau, 0.355) / T_R + C * pow(T_R, -0.41) * exp(tau));
1316 x[i] = y[i] * HEOS.
_p / k_H;
1323 for (std::size_t i = 0; i < y.size(); ++i) {
1329 K[iWater] = y[iWater] / x[iWater];
1354 NR.
call(HEOS, IO.
x, IO.
y, IO);
1357 NR.
call(HEOS, IO.
y, IO.
x, IO);
1363 HEOS.
_p = HEOS.
SatV->p();
1365 HEOS.
_T = HEOS.
SatL->T();
1374 IO.
x = std::vector<CoolPropDbl>(guess.
x.begin(), guess.
x.end());
1375 IO.
y = std::vector<CoolPropDbl>(guess.
y.begin(), guess.
y.end());
1381 if (std::abs(HEOS.
Q()) < 1e-10) {
1383 NR.
call(HEOS, IO.
x, IO.
y, IO);
1384 }
else if (std::abs(HEOS.
Q() - 1) < 1e-10) {
1386 NR.
call(HEOS, IO.
y, IO.
x, IO);
1401 IO.
x = std::vector<CoolPropDbl>(guess.
x.begin(), guess.
x.end());
1402 IO.
y = std::vector<CoolPropDbl>(guess.
y.begin(), guess.
y.end());
1413 if (std::abs(HEOS.
Q()) < 1e-10) {
1415 NR.
call(HEOS, IO.
x, IO.
y, IO);
1416 }
else if (std::abs(HEOS.
Q() - 1) < 1e-10) {
1418 NR.
call(HEOS, IO.
y, IO.
x, IO);
1457 std::vector<std::pair<std::size_t, std::size_t>> intersections =
1466 if (env.T.size() < 4) {
1467 throw ValueError(
format(
"Phase envelope has too few points (%d) for envelope-guided flash",
static_cast<int>(env.T.size())));
1470 enum quality_options
1476 quality_options quality;
1478 quality = SATURATED_LIQUID;
1480 quality = SATURATED_VAPOR;
1481 }
else if (HEOS.
_Q > 0 && HEOS.
_Q < 1) {
1482 quality = TWO_PHASE;
1484 throw ValueError(
"Quality is not within 0 and 1");
1487 if (quality == SATURATED_LIQUID || quality == SATURATED_VAPOR) {
1492 std::vector<std::size_t> solutions;
1493 for (
const auto& intersection : intersections) {
1494 if (std::abs(env.Q[intersection.first] - HEOS.
_Q) < 10 *
DBL_EPSILON
1495 && std::abs(env.Q[intersection.second] - HEOS.
_Q) < 10 *
DBL_EPSILON) {
1496 solutions.push_back(intersection.first);
1500 if (solutions.size() == 1) {
1502 std::size_t& imax = solutions[0];
1508 imax = std::min(imax, env.T.size() - 3);
1509 imax = std::max(imax, std::size_t(1));
1520 }
else if (other ==
iT) {
1531 if (quality == SATURATED_VAPOR) {
1534 IO.
x.resize(IO.
y.size());
1535 for (std::size_t i = 0; i < IO.
x.size() - 1; ++i)
1539 IO.
x[IO.
x.size() - 1] = 1 - std::accumulate(IO.
x.begin(), IO.
x.end() - 1, 0.0);
1540 NR.
call(HEOS, IO.
y, IO.
x, IO);
1544 IO.
y.resize(IO.
x.size());
1547 for (std::size_t i = 0; i < IO.
y.size() - 1; ++i)
1553 IO.
y[IO.
y.size() - 1] = 1 - std::accumulate(IO.
y.begin(), IO.
y.end() - 1, 0.0);
1554 NR.
call(HEOS, IO.
x, IO.
y, IO);
1556 }
else if (solutions.size() == 0) {
1557 throw ValueError(
"No solution was found in PQ_flash");
1559 throw ValueError(
"More than 1 solution was found in PQ_flash");
1567 std::vector<std::size_t> liquid_solutions, vapor_solutions;
1568 for (
const auto& intersection : intersections) {
1569 if (std::abs(env.Q[intersection.first] - 0) < 10 *
DBL_EPSILON && std::abs(env.Q[intersection.second] - 0) < 10 *
DBL_EPSILON) {
1570 liquid_solutions.push_back(intersection.first);
1572 if (std::abs(env.Q[intersection.first] - 1) < 10 *
DBL_EPSILON && std::abs(env.Q[intersection.second] - 1) < 10 *
DBL_EPSILON) {
1573 vapor_solutions.push_back(intersection.first);
1577 if (liquid_solutions.size() != 1 || vapor_solutions.size() != 1) {
1578 throw ValueError(
format(
"Number liquid solutions [%d] or vapor solutions [%d] != 1", liquid_solutions.size(), vapor_solutions.size()));
1580 std::size_t iliq = liquid_solutions[0], ivap = vapor_solutions[0];
1587 const std::size_t Nenv = env.T.size();
1588 ivap = std::min(ivap, Nenv - 3);
1589 iliq = std::min(iliq, Nenv - 3);
1590 ivap = std::max(ivap, std::size_t(1));
1591 iliq = std::max(iliq, std::size_t(1));
1597 CoolPropDbl rhomolar_vap_sat_vap = NAN, T_sat_vap = NAN, rhomolar_liq_sat_vap = NAN, rhomolar_liq_sat_liq = NAN, T_sat_liq = NAN,
1598 rhomolar_vap_sat_liq = NAN, p_sat_liq = NAN, p_sat_vap = NAN;
1607 rhomolar_vap_sat_vap =
CubicInterp(env.p, env.rhomolar_vap, ivap - 1, ivap, ivap + 1, ivap + 2,
static_cast<CoolPropDbl>(IO.
p));
1608 T_sat_vap =
CubicInterp(env.rhomolar_vap, env.T, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1609 rhomolar_liq_sat_vap =
CubicInterp(env.rhomolar_vap, env.rhomolar_liq, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1612 rhomolar_liq_sat_liq =
CubicInterp(env.p, env.rhomolar_vap, iliq - 1, iliq, iliq + 1, iliq + 2,
static_cast<CoolPropDbl>(IO.
p));
1613 T_sat_liq =
CubicInterp(env.rhomolar_vap, env.T, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1614 rhomolar_vap_sat_liq =
CubicInterp(env.rhomolar_vap, env.rhomolar_liq, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1615 }
else if (other ==
iT) {
1622 rhomolar_vap_sat_vap =
CubicInterp(env.T, env.rhomolar_vap, ivap - 1, ivap, ivap + 1, ivap + 2,
static_cast<CoolPropDbl>(IO.
T));
1623 p_sat_vap =
CubicInterp(env.rhomolar_vap, env.p, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1624 rhomolar_liq_sat_vap =
CubicInterp(env.rhomolar_vap, env.rhomolar_liq, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1627 rhomolar_liq_sat_liq =
CubicInterp(env.T, env.rhomolar_vap, iliq - 1, iliq, iliq + 1, iliq + 2,
static_cast<CoolPropDbl>(IO.
T));
1628 p_sat_liq =
CubicInterp(env.rhomolar_vap, env.p, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1629 rhomolar_vap_sat_liq =
CubicInterp(env.rhomolar_vap, env.rhomolar_liq, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1637 IO.
T = IO.
beta * T_sat_vap + (1 - IO.
beta) * T_sat_liq;
1638 IO.
p = IO.
beta * p_sat_vap + (1 - IO.
beta) * p_sat_liq;
1641 IO.
x.resize(IO.
z.size());
1642 IO.
y.resize(IO.
z.size());
1644 for (std::size_t i = 0; i < IO.
x.size() - 1; ++i)
1646 CoolPropDbl x_sat_vap =
CubicInterp(env.rhomolar_vap, env.x[i], ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1647 CoolPropDbl y_sat_vap =
CubicInterp(env.rhomolar_vap, env.y[i], ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1649 CoolPropDbl x_sat_liq =
CubicInterp(env.rhomolar_vap, env.y[i], iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1650 CoolPropDbl y_sat_liq =
CubicInterp(env.rhomolar_vap, env.x[i], iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1652 IO.
x[i] = IO.
beta * x_sat_vap + (1 - IO.
beta) * x_sat_liq;
1653 IO.
y[i] = IO.
beta * y_sat_vap + (1 - IO.
beta) * y_sat_liq;
1655 IO.
x[IO.
x.size() - 1] = 1 - std::accumulate(IO.
x.begin(), IO.
x.end() - 1, 0.0);
1656 IO.
y[IO.
y.size() - 1] = 1 - std::accumulate(IO.
y.begin(), IO.
y.end() - 1, 0.0);
1682 : HEOS(HEOS), rhomolar_spec(rhomolar_spec), other(other), value(value), Qd(_HUGE) {
1685 double call(
double T)
override {
1689 Qd = (1 / rhomolar_spec - 1 / SatL.
rhomolar()) / (1 / SatV.rhomolar() - 1 / SatL.
rhomolar());
1695 } resid(HEOS, rhomolar_spec, other, value);
1701 CoolPropDbl Tmin_satL = NAN, Tmin_satV = NAN, Tmin_sat = NAN;
1703 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
1745 rhomolar_spec(rhomolar_spec),
1750 value_cache(value_cache),
1752 double call(
double T)
override {
1753 const double rhoL = superanc.
eval_sat(
T,
'D', 0);
1754 const double rhoV = superanc.
eval_sat(
T,
'D', 1);
1756 Qd = (1 / rhomolar_spec - 1 / rhoL) / (1 / rhoV - 1 / rhoL);
1762 const double yL = superanc.
eval_sat(
T, ca_key, 0);
1763 const double yV = superanc.
eval_sat(
T, ca_key, 1);
1764 Qo = (value_cache - yL) / (yV - yL);
1767 const double p = superanc.
eval_sat(
T,
'P', 1);
1768 HEOS.
SatL->update_TDmolarP_unchecked(
T, rhoL, p);
1769 HEOS.
SatV->update_TDmolarP_unchecked(
T, rhoV, p);
1770 const double yL = HEOS.
SatL->keyed_output(other);
1771 const double yV = HEOS.
SatV->keyed_output(other);
1772 Qo = (value - yL) / (yV - yL);
1774 const double resid = Qo - Qd;
1775 if (!std::isfinite(resid)) {
1776 throw ValueError(
format(
"HSU_D superancillary resid not finite @ T=%g K; Qo=%g; Qd=%g",
T, Qo, Qd));
1792 : HEOS(HEOS), rhomolar(rhomolar), value(value), other(other), Tmin(Tmin), Tmax(Tmax) {
1812 double call(
double T)
override {
1817 return (eos - value) / value;
1823 double deriv(
double T)
override {
1829 double second_deriv(
double T)
override {
1835 bool input_not_in_range(
double T)
override {
1836 return (T < Tmin || T > Tmax);
1853 static const bool superanc_hsu_d_disabled = (std::getenv(
"COOLPROP_DISABLE_SUPERANC_HSU_D") !=
nullptr);
1856 std::shared_ptr<EquationOfState::SuperAncillary_t> sa_ptr = HEOS.
get_superanc();
1866 auto restore_inputs = [&]() {
1870 }
else if (other ==
iSmolar) {
1887 const char ca_key = (other ==
iHmolar) ?
'H' : ((other ==
iSmolar) ?
'S' :
'U');
1888 bool use_ca =
false;
1894 if (stamp.has_value()) {
1897 if (ca_key ==
'S') {
1898 delta = -R * (stamp->first - caller_a1);
1901 delta = R * T_red * (stamp->second - caller_a2);
1904 value_cache = value + delta;
1908 const double Tmin_sa = superanc.
get_Tmin();
1909 const double Tmax_1phase = HEOS.
Tmax() * 1.5;
1910 const double tol = 1e-12;
1912 auto eps44 = boost::math::tools::eps_tolerance<double>(44);
1917 H._p =
H.calc_pressure_nocache(
H.T(),
H.rhomolar());
1918 H.unspecify_phase();
1919 H.recalculate_singlephase_phase();
1927 auto committed_ok = [&]() ->
bool {
1928 const double dout = HEOS.
rhomolar();
1929 if (!std::isfinite(dout) || std::abs(dout / rho_in - 1) > 1e-7)
return false;
1931 return std::isfinite(xout) && std::abs(xout - value) <= 1e-6 * std::abs(value) + 1e-3;
1936 auto inside_dome = [&](
double T) ->
bool {
1938 const double rhoV = superanc.
eval_sat(
T,
'D', 1);
1939 const double rhoL = superanc.
eval_sat(
T,
'D', 0);
1944 auto solve_1phase = [&](
double Ta,
double Tb) ->
bool {
1946 solver_resid resid(&HEOS, HEOS.
_rhomolar, value, other, Ta, Tb);
1947 const double fa = resid.call(Ta), fb = resid.call(Tb);
1949 if (std::abs(fa) < tol) {
1951 }
else if (std::abs(fb) < tol) {
1953 }
else if (fa * fb < 0) {
1954 boost::math::uintmax_t max_iter = 100;
1955 auto f = [&resid](
double T) {
return resid.call(
T); };
1956 auto [l, r] = toms748_solve(f, Ta, Tb, fa, fb, eps44, max_iter);
1957 Tconv = 0.5 * (l + r);
1966 if (inside_dome(Tconv))
return false;
1968 finalize_1phase(HEOS);
1970 }
catch (
const std::exception&) {
1976 auto solve_2phase = [&](
double Ta,
double Tb) ->
bool {
1978 solver_resid_2phase resid(HEOS, HEOS.
_rhomolar, other, value, superanc, use_ca, ca_key, value_cache);
1979 const double fa = resid.call(Ta), fb = resid.call(Tb);
1981 if (std::abs(fa) < tol) {
1983 }
else if (std::abs(fb) < tol) {
1985 }
else if (fa * fb < 0) {
1986 boost::math::uintmax_t max_iter = 100;
1987 auto f = [&resid](
double T) {
return resid.call(
T); };
1988 auto [l, r] = toms748_solve(f, Ta, Tb, fa, fb, eps44, max_iter);
1989 Tsol = 0.5 * (l + r);
2005 solver_resid_2phase resid_eos(HEOS, HEOS.
_rhomolar, other, value, superanc,
false, ca_key, value_cache);
2006 double t0 = Tsol, r0 = resid_eos.call(t0);
2012 double t1 = Tsol * (1.0 + 1e-7);
2013 if (t1 <= Ta || t1 >= Tb) {
2014 t1 = 0.5 * (Tsol + ((Tsol - Ta) > (Tb - Tsol) ? Ta : Tb));
2016 double r1 = resid_eos.call(t1);
2017 for (
int it = 0; it < 4; ++it) {
2018 if (!std::isfinite(r1) || std::abs(r1) < tol || std::abs(t1 - t0) <= 1e-13 * t1)
break;
2022 const double tn = t1 - r1 * (t1 - t0) / (r1 - r0);
2023 if (!std::isfinite(tn))
break;
2027 r1 = resid_eos.call(t1);
2030 resid_eos.call(Tsol);
2031 Qd_final = resid_eos.Qd;
2034 Qd_final = resid.Qd;
2043 constexpr double Qeps = 1e-8;
2044 if (Qd_final < -Qeps || Qd_final > 1.0 + Qeps)
return false;
2047 }
catch (
const std::exception&) {
2065 std::sort(Tsats.begin(), Tsats.end());
2070 const double Tcrit_2phase = Tcrit - std::max(1e-6, 1e-9 * Tcrit);
2072 std::vector<double> edges{Tmin_sa};
2073 for (
const auto& pr : Tsats) {
2074 if (pr.first > Tmin_sa && pr.first < Tcrit) edges.push_back(pr.first);
2076 edges.push_back(Tcrit);
2077 edges.push_back(Tmax_1phase);
2079 for (std::size_t i = 0; i + 1 < edges.size(); ++i) {
2080 const double a = edges[i], b = edges[i + 1];
2081 if (b - a < 1e-10)
continue;
2082 const double mid = 0.5 * (a + b);
2090 if (mid < Tcrit && inside_dome(mid)) {
2091 const double ub = std::min(b, Tcrit_2phase);
2092 solved = (ub > a) ? solve_2phase(a, ub) : solve_1phase(a, b);
2094 solved = solve_1phase(a, b);
2097 if (committed_ok())
return;
2103 throw ValueError(
"HSU_D superancillary: no candidate interval reproduced the inputs");
2104 }
catch (
const std::exception& e) {
2106 std::cout <<
"HSU_D_flash superancillary path failed (" << e.what() <<
"); using legacy path\n";
2131 bool fast_ok =
false;
2134 const double Tb = HEOS.
Tmax() * 1.5;
2146 solver_resid resid(&HEOS, rho_in, value_in, other, Tc_, Tb);
2147 const double fa = resid.call(Tc_), fb = resid.call(Tb);
2149 boost::math::uintmax_t max_iter = 100;
2150 auto f = [&resid](
double T) {
return resid.call(
T); };
2151 auto [l, r] = toms748_solve(f, Tc_, Tb, fa, fb, boost::math::tools::eps_tolerance<double>(44), max_iter);
2156 if (
ValidNumber(xout) && std::abs(xout - value_in) <= 1e-6 * std::abs(value_in) + 1e-3) {
2177 }
else if (other ==
iSmolar) {
2185 shared_ptr<HelmholtzEOSMixtureBackend> Sat;
2190 CoolPropDbl yL = NAN, yV = NAN, value = NAN, y_solid = NAN;
2219 y_solid = (yV - yL) / (1 / rhoVtriple - 1 / rhoLtriple) * (1 / HEOS.
_rhomolar - 1 / rhoLtriple) + yL;
2221 if (value < y_solid) {
2230 for (
int i_try = 0; i_try < 7; i_try++) {
2245 optionsD.
omega /= 2;
2254 if (value > Sat->keyed_output(other)) {
2255 solver_resid resid(&HEOS, HEOS.
_rhomolar, value, other, Sat->keyed_output(
iT), HEOS.
Tmax() * 1.5);
2258 T_converged =
Halley(resid, 0.5 * (Sat->keyed_output(
iT) + HEOS.
Tmax() * 1.5), 1e-10, 100);
2288 HEOS.
_Q = (1 / HEOS.
_rhomolar - 1 / HEOS.
SatL->rhomolar()) / (1 / HEOS.
SatV->rhomolar() - 1 / HEOS.
SatL->rhomolar());
2289 HEOS.
_T = HEOS.
SatL->T();
2325 solver_resid resid(&HEOS, HEOS.
_rhomolar, value, other, TVtriple, HEOS.
Tmax() * 1.5);
2371 solver_resid resid(&HEOS, HEOS.
_rhomolar, value, other, TLtriple, HEOS.
Tmax() * 1.5);
2417 const int Ncoarse = 100;
2419 for (
int i = 0; i <= Ncoarse; ++i) {
2431 const int Nfine = 200;
2432 for (
int i = 0; i <= Nfine; ++i) {
2468 const CoolPropDbl floor_tol = 1e-8 * std::abs(y_lo) + 1e-10;
2469 if (!
ValidNumber(y_lo) || value < y_lo - floor_tol) {
2472 throw ValueError(
format(
"D+X below melting line [other=%d]: %g < %g at Tmelt_min %g K", other, value, y_lo, T_lo));
2474 solver_resid resid(&HEOS, HEOS.
_rhomolar, value, other, T_lo, TLtriple);
2516 throw ValueError(
"Invalid input for HSU_D_flash mixture");
2519 bool solved =
false;
2529 auto nocache_resid = [&](
double T) ->
double {
2543 auto is_mechanically_stable = [&](
double T) ->
bool {
2544 double eps = std::max(
static_cast<double>(rho_target) * 1e-6, 1e-10);
2547 return (P_hi - P_lo) > 0;
2551 std::uintmax_t max_iter = 100;
2552 auto bracket = boost::math::tools::toms748_solve(nocache_resid,
static_cast<double>(Tmin),
static_cast<double>(Tmax),
2553 boost::math::tools::eps_tolerance<double>(40), max_iter);
2554 double T_cand = (bracket.first + bracket.second) / 2.0;
2558 if (P_eos > 0 && is_mechanically_stable(T_cand)) {
2563 stab.
set_TP(T_cand, P_eos);
2585 if (Pmin_bound < 100) Pmin_bound = 100;
2591 auto solve_for_caloric_at_T = [&](
double T) ->
double {
2592 auto rho_resid = [&](
double P) ->
double {
2598 double logPmin = std::log10(
static_cast<double>(Pmin_bound));
2599 double logPmax = std::log10(
static_cast<double>(Pmax_bound));
2601 int nsteps =
static_cast<int>((logPmax - logPmin) / dlogP) + 1;
2602 double P_lo = -1, P_hi = -1;
2603 double f_prev = 0, P_prev = 0;
2604 bool have_prev =
false;
2606 for (
int i = 0; i <= nsteps; i++) {
2607 double logP = logPmin + i * dlogP;
2608 if (logP > logPmax) logP = logPmax;
2609 double P = std::pow(10.0, logP);
2617 if (have_prev && f_prev * f < 0) {
2627 if (P_lo < 0 || P_hi < 0)
return std::numeric_limits<double>::quiet_NaN();
2629 std::uintmax_t max_iter = 100;
2630 auto bracket = boost::math::tools::toms748_solve(rho_resid, P_lo, P_hi, boost::math::tools::eps_tolerance<double>(40), max_iter);
2631 double P_sol = (bracket.first + bracket.second) / 2.0;
2638 double logTmin = std::log10(
static_cast<double>(Tmin));
2639 double logTmax = std::log10(
static_cast<double>(Tmax));
2641 int nsteps_T =
static_cast<int>((logTmax - logTmin) / dlogT) + 1;
2643 double T_lo = -1, T_hi = -1;
2644 double f_prev_T = 0, T_prev = 0;
2645 bool have_prev_T =
false;
2647 for (
int i = 0; i <= nsteps_T; i++) {
2648 double logT = logTmin + i * dlogT;
2649 if (logT > logTmax) logT = logTmax;
2650 double T = std::pow(10.0, logT);
2653 X = solve_for_caloric_at_T(
T);
2655 have_prev_T =
false;
2658 if (std::isnan(
X)) {
2659 have_prev_T =
false;
2662 double f =
X - value;
2663 if (have_prev_T && f_prev_T * f < 0) {
2673 if (T_lo > 0 && T_hi > 0) {
2674 auto outer_resid = [&](
double T) ->
double {
2675 double X = solve_for_caloric_at_T(
T);
2676 if (std::isnan(
X))
throw ValueError(
"inner P-sweep failed during T-sweep");
2679 std::uintmax_t max_iter = 100;
2680 auto bracket = boost::math::tools::toms748_solve(outer_resid, T_lo, T_hi, boost::math::tools::eps_tolerance<double>(40), max_iter);
2681 double T_sol = (bracket.first + bracket.second) / 2.0;
2682 solve_for_caloric_at_T(T_sol);
2685 throw ValueError(
format(
"HSU_D_flash for mixture: no T bracket found in [%Lg, %Lg] "
2686 "for target %s=%Lg at rho=%Lg",
2697 const auto resid_cal =
static_cast<double>(HEOS.
keyed_output(other) - value);
2699 const double scale_cal = std::abs(
static_cast<double>(value)) + 1.0;
2700 const double scale_rho = std::abs(
static_cast<double>(rho_target)) + 1.0;
2702 || std::abs(resid_rho) > 1e-6 * scale_rho) {
2703 throw ValueError(
format(
"HSU_D_flash for mixture did not converge to the specification: caloric residual %g "
2704 "(target %s=%g), density residual %g (target %g) -- the (T,p) flash is misclassifying the phase",
2706 static_cast<double>(rho_target)));
2713 double A[2][2], B[2][2];
2729 throw ValueError(
"other is invalid in HSU_P_flash_singlephase_Newton");
2734 bool failed =
false;
2735 CoolPropDbl omega = 1.0, f2 = NAN, df2_dtau = NAN, df2_ddelta = NAN;
2737 while (worst_error > 1e-6 && failed ==
false) {
2753 CoolPropDbl f1 = delta / tau * (1 + delta * dar_ddelta) - p / (rhoc * R * Tc);
2754 CoolPropDbl df1_dtau = (1 + delta * dar_ddelta) * (-delta / tau / tau) + delta / tau * (delta * d2ar_ddelta_dtau);
2755 CoolPropDbl df1_ddelta = (1.0 / tau) * (1 + 2.0 * delta * dar_ddelta + delta * delta * d2ar_ddelta2);
2758 f2 = (1 + delta * dar_ddelta) + tau * (da0_dtau + dar_dtau) - tau * y / (R * Tc);
2759 df2_dtau = delta * d2ar_ddelta_dtau + da0_dtau + dar_dtau + tau * (d2a0_dtau2 + d2ar_dtau2) - y / (R * Tc);
2760 df2_ddelta = (dar_ddelta + delta * d2ar_ddelta2) + tau * (d2a0_ddelta_dtau + d2ar_ddelta_dtau);
2764 f2 = tau * (da0_dtau + dar_dtau) - ar - a0 - y / R;
2765 df2_dtau = tau * (d2a0_dtau2 + d2ar_dtau2) + (da0_dtau + dar_dtau) - dar_dtau - da0_dtau;
2766 df2_ddelta = tau * (d2a0_ddelta_dtau + d2ar_ddelta_dtau) - dar_ddelta - da0_ddelta;
2770 throw ValueError(
"other variable in HSU_P_flash_singlephase_Newton is invalid");
2775 A[0][1] = df1_ddelta;
2777 A[1][1] = df2_ddelta;
2782 tau -= omega * (B[0][0] * f1 + B[0][1] * f2);
2783 delta -= omega * (B[1][0] * f1 + B[1][1] * f2);
2785 if (std::abs(f1) > std::abs(f2))
2786 worst_error = std::abs(f1);
2788 worst_error = std::abs(f2);
2791 throw SolutionError(
format(
"Invalid values for inputs p=%g y=%g for fluid %s in HSU_P_flash_singlephase", p, y, _HEOS.
name().c_str()));
2796 throw SolutionError(
format(
"HSU_P_flash_singlephase did not converge with inputs p=%g h=%g for fluid %s", p, y, _HEOS.
name().c_str()));
2805 throw ValueError(
"value for p in HSU_P_flash_singlephase_Brent is invalid");
2808 throw ValueError(
"value for other in HSU_P_flash_singlephase_Brent is invalid");
2836 static const bool pxflash_direct_eos = []() {
2837 const char* env = std::getenv(
"PXFLASH_DIRECT_EOS");
2838 if (env ==
nullptr)
return true;
2839 return std::string(env) !=
"0";
2841 static const bool pxflash_inner_newton = (std::getenv(
"PXFLASH_INNER_NEWTON") !=
nullptr);
2842 const bool direct_path_enabled = pxflash_direct_eos && HEOS.
is_pure();
2851 CoolPropDbl eos0, eos1, rhomolar, rhomolar0, rhomolar1;
2853 bool direct_path_enabled;
2859 bool direct_path_enabled)
2872 direct_path_enabled(direct_path_enabled),
2896 static double direct_density_newton(
HelmholtzEOSMixtureBackend&
H,
const std::vector<CoolPropDbl>& x,
double Tr,
double rhor,
double R,
2899 const double tau = Tr /
T;
2900 double rho = rho_initial;
2901 for (
int it = 0; it < 15; ++it) {
2902 const double delta = rho / rhor;
2903 ar_out =
H.residual_helmholtz->all(
H, x, tau, delta,
false);
2904 const double alphar_d = ar_out.dalphar_ddelta;
2905 const double alphar_dd = ar_out.d2alphar_ddelta2;
2906 const double p = rho * R *
T * (1.0 + delta * alphar_d);
2907 const double f = p - p_target;
2908 const double dpdrho = R *
T * (1.0 + 2.0 * delta * alphar_d + delta * delta * alphar_dd);
2909 if (dpdrho <= 0.0) {
2913 throw ValueError(
"direct-EOS Newton: dpdrho <= 0 (unstable region)");
2916 if (std::abs(f) < 1e-12 * std::abs(p_target)) {
2917 a0_out =
H.calc_all_alpha0_derivs_nocache(x, tau, delta, Tr, rhor);
2921 if (use_plain_newton) {
2922 if (!std::isfinite(dpdrho) || std::abs(dpdrho) < 1e-300) {
2923 throw ValueError(
"direct-EOS density Newton: singular dpdrho");
2927 const double alphar_ddd = ar_out.d3alphar_ddelta3;
2928 const double alphar_dddd = ar_out.d4alphar_ddelta4;
2929 const double d2pdrho2 = (R *
T / rhor) * (2.0 * alphar_d + 4.0 * delta * alphar_dd + delta * delta * alphar_ddd);
2930 const double d3pdrho3 = (R *
T / (rhor * rhor)) * (6.0 * alphar_dd + 6.0 * delta * alphar_ddd + delta * delta * alphar_dddd);
2932 const double num = f * (dpdrho * dpdrho - f * d2pdrho2 / 2.0);
2933 const double den = dpdrho * dpdrho * dpdrho - f * dpdrho * d2pdrho2 + d3pdrho3 * f * f / 6.0;
2934 dx = (!std::isfinite(den) || std::abs(den) < 1e-300) ? (-f / dpdrho) : (-num / den);
2936 if (!std::isfinite(dx))
throw ValueError(
"direct-EOS density Newton: non-finite step");
2938 while (rho + scale * dx <= 0.0 && scale > 1e-6)
2940 const double rho_new = rho + scale * dx;
2941 if (!std::isfinite(rho_new) || rho_new <= 0.0)
throw ValueError(
"direct-EOS density Newton: non-finite rho");
2948 if (std::abs(scale * dx) / rho_new < 1e-12) {
2950 const double delta_new = rho / rhor;
2951 ar_out =
H.residual_helmholtz->all(
H, x, tau, delta_new,
false);
2952 a0_out =
H.calc_all_alpha0_derivs_nocache(x, tau, delta_new, Tr, rhor);
2957 throw ValueError(
"direct-EOS density Newton: max iters");
2965 static double direct_property_eval(
HelmholtzEOSMixtureBackend&
H,
const std::vector<CoolPropDbl>& x,
double Tr,
double rhor,
double R,
2967 const double tau = Tr /
T;
2968 const double delta = rho / rhor;
2969 const double atau_total = a0.dalphar_dtau + ar.dalphar_dtau;
2970 const double a_total = a0.alphar + ar.alphar;
2973 return R *
T * (1.0 + tau * atau_total + delta * ar.dalphar_ddelta);
2975 return R * (tau * atau_total - a_total);
2977 return R *
T * tau * atau_total;
2979 throw ValueError(
"direct-EOS property: unsupported `other` key");
2982 double call(
double T)
override {
3009 const bool force_robust_density = (p > p_crit);
3010 const bool want_warm = (iter >= 2 && std::abs(rhomolar1 / rhomolar0 - 1) <= 0.05);
3011 if (direct_path_enabled && want_warm && !force_robust_density) {
3021 const double rho_solved =
3023 HEOS->
gas_constant(),
T, p, rhomolar, pxflash_inner_newton, ar, a0);
3034 rhomolar = rho_solved;
3036 if (verbosity > 0 && iter == 0) {
3037 std::cout <<
format(
"T: %0.15g rho: %0.15g eos: %0.15g",
T, rhomolar, eos);
3042 rhomolar0 = rhomolar;
3043 }
else if (iter == 1) {
3045 rhomolar1 = rhomolar;
3049 rhomolar0 = rhomolar1;
3050 rhomolar1 = rhomolar;
3060 if (iter < 2 || std::abs(rhomolar1 / rhomolar0 - 1) > 0.05 || force_robust_density) {
3075 if (verbosity > 0 && iter == 0) {
3076 std::cout <<
format(
"T: %0.15g rho: %0.15g eos: %0.15g",
T, rhomolar, eos);
3085 rhomolar0 = rhomolar;
3086 }
else if (iter == 1) {
3088 rhomolar1 = rhomolar;
3092 rhomolar0 = rhomolar1;
3093 rhomolar1 = rhomolar;
3099 double deriv(
double T)
override {
3102 double second_deriv(
double T)
override {
3105 bool input_not_in_range(
double x)
override {
3106 return (x < Tmin || x > Tmax);
3109 solver_resid resid(&HEOS, HEOS.
_p, value, other, Tmin, Tmax, direct_path_enabled);
3118 std::vector<std::vector<double>> J = {{-1.0, -1.0}, {-1.0, -1.0}};
3121 : HEOS(HEOS), p(p), value(value), other(other), iter(0) {}
3122 std::vector<double> call(
const std::vector<double>& x)
override {
3123 double T = x[0], rhomolar = x[1];
3130 return {(HEOS->
p() - p) / p, HEOS->
keyed_output(other) - value};
3132 std::vector<std::vector<double>> Jacobian(
const std::vector<double>& )
override {
3137 resid_2D solver_resid2d(&HEOS, HEOS.
_p, value, other, Tmin, Tmax);
3140 double resid_Tmin = resid.call(Tmin);
3141 double rhomolar_Tmin = HEOS.
rhomolar();
3143 double resid_Tmax = resid.call(Tmax);
3144 double rhomolar_Tmax = HEOS.
rhomolar();
3147 bool use_min = std::abs(resid_Tmin) < std::abs(resid_Tmax);
3148 double Tstart = use_min ? Tmin : Tmax;
3149 double rhomolarstart = use_min ? rhomolar_Tmin : rhomolar_Tmax;
3153 resid.verbosity = 1;
3155 if (resid_Tmin * resid_Tmax < 0) {
3163 boost::math::uintmax_t max_iter = 100;
3165 auto f = [&resid](
const double T) {
return resid.call(
T); };
3171 auto [l, r] = toms748_solve(f, Tmin, Tmax, resid_Tmin, resid_Tmax, boost::math::tools::eps_tolerance<double>(30), max_iter);
3177 resid.call(0.5 * (l + r));
3187 Halley(resid, Tstart, 1e-12, 100);
3189 throw ValueError(
"Halley's method was unable to find a solution in HSU_P_flash_singlephase_Brent");
3207 const bool in_near_critical = (p_target > 0.95 * p_critical_ && p_target <= p_critical_);
3208 const bool in_supercritical = (p_target > p_critical_);
3209 if (!in_near_critical && !in_supercritical) {
3213 std::cout << resid.errstring <<
'\n';
3215 if (in_supercritical) {
3224 const double Tnarrow = T_crit * 1.05;
3225 bool narrow_toms748_ok =
false;
3226 if (Tnarrow < Tmax) {
3228 double resid_Tnarrow = 0.0;
3229 bool have_narrow_bracket =
false;
3231 resid_Tnarrow = resid.call(Tnarrow);
3232 have_narrow_bracket = (resid_Tnarrow * resid_Tmin < 0);
3233 }
catch (
const std::exception& e) {
3235 std::cout <<
"narrow TOMS748 probe threw: " << e.what() <<
'\n';
3239 if (have_narrow_bracket) {
3241 boost::math::uintmax_t max_iter_narrow = 100;
3242 auto fn = [&resid](
const double T) {
return resid.call(
T); };
3244 auto [nl, nr] = toms748_solve(fn, Tmin, Tnarrow, resid_Tmin, resid_Tnarrow, boost::math::tools::eps_tolerance<double>(30),
3249 const double T_conv = 0.5 * (nl + nr);
3255 narrow_toms748_ok =
true;
3257 }
catch (
const std::exception& e) {
3262 std::cout <<
"narrow TOMS748 fallback threw: " << e.what() <<
'\n';
3267 if (!narrow_toms748_ok) {
3269 std::vector<double> x0 = {Tmin, rhomolar_Tmin};
3276 const double T_sol =
static_cast<CoolPropDbl>(solver_resid2d.HEOS->T());
3277 const double p_resid_rel = std::abs(HEOS.
p() - p_target) / p_target;
3278 const double other_scale = std::abs(value) > 1.0 ? std::abs(value) : 1.0;
3279 const double other_resid_rel = std::abs(HEOS.
keyed_output(other) - value) / other_scale;
3280 if (!
is_in_closed_range(Tmin, Tmax, T_sol) || p_resid_rel > 1e-6 || other_resid_rel > 1e-6) {
3281 throw ValueError(
"2D Newton method was unable to find a solution in HSU_P_flash_singlephase_Brent");
3288 std::vector<double> x0 = {Tstart, rhomolarstart};
3290 const double T_sol =
static_cast<CoolPropDbl>(solver_resid2d.HEOS->T());
3291 const double p_resid_rel = std::abs(HEOS.
p() - p_target) / p_target;
3292 const double other_scale = std::abs(value) > 1.0 ? std::abs(value) : 1.0;
3293 const double other_resid_rel = std::abs(HEOS.
keyed_output(other) - value) / other_scale;
3294 if (!
is_in_closed_range(Tmin, Tmax, T_sol) || solver_resid2d.HEOS->phase() != phase || p_resid_rel > 1e-6 || other_resid_rel > 1e-6) {
3295 throw ValueError(
"2D Newton method was unable to find a solution in HSU_P_flash_singlephase_Brent");
3303 std::cout << resid.errstring <<
'\n';
3313 if (eos1 > eos0 && value > eos1) {
3315 format(
"HSU_P_flash_singlephase_Brent could not find a solution because %s [%Lg %s] is above the maximum value of %0.12Lg %s",
3316 name.c_str(), value, units.c_str(), eos1, units.c_str()));
3318 if (eos1 > eos0 && value < eos0) {
3320 format(
"HSU_P_flash_singlephase_Brent could not find a solution because %s [%Lg %s] is below the minimum value of %0.12Lg %s",
3321 name.c_str(), value, units.c_str(), eos0, units.c_str()));
3330 bool saturation_called =
false;
3358 Tmax = 1.5 * HEOS.
Tmax();
3366 auto& superanc = *superanc_ptr;
3368 if (HEOS.
_p > pmax_num) {
3370 format(
"Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS.
_p,
3373 Tmin = superanc.get_T_from_p(HEOS.
_p);
3377 if (saturation_called) {
3378 Tmin = HEOS.
SatV->T();
3391 Tmin = HEOS.
Tmin() - 1e-3;
3397 auto& superanc = *superanc_ptr;
3399 if (HEOS.
_p > pmax_num) {
3401 "Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS.
_p, pmax_num));
3403 Tmax = superanc.get_T_from_p(HEOS.
_p);
3408 if (saturation_called) {
3409 Tmax = HEOS.
SatL->T();
3419 Tmax = 1.5 * HEOS.
Tmax();
3424 Tmin = HEOS.
Tmin() - 1e-3;
3434 }
catch (std::exception& e) {
3435 throw ValueError(
format(
"unable to solve 1phase PY flash with Tmin=%Lg, Tmax=%Lg due to error: %s", Tmin, Tmax, e.what()));
3459 T_bubble = HEOS.
T();
3464 pq_ok = std::isfinite(
static_cast<double>(T_bubble)) && std::isfinite(
static_cast<double>(T_dew))
3465 && std::isfinite(
static_cast<double>(val_bubble)) && std::isfinite(
static_cast<double>(val_dew));
3472 double tol_sat = 1e-6 * (std::abs(
static_cast<double>(value)) + 1.0);
3473 if (std::abs(
static_cast<double>(value - val_bubble)) < tol_sat) {
3478 if (std::abs(
static_cast<double>(value - val_dew)) < tol_sat) {
3485 CoolPropDbl val_lo = (val_bubble < val_dew) ? val_bubble : val_dew;
3486 CoolPropDbl val_hi = (val_bubble < val_dew) ? val_dew : val_bubble;
3487 CoolPropDbl T_at_lo = (val_bubble < val_dew) ? T_bubble : T_dew;
3488 CoolPropDbl T_at_hi = (val_bubble < val_dew) ? T_dew : T_bubble;
3490 if (value < val_lo) {
3493 }
else if (value > val_hi) {
3505 auto resid = [&](
double T) ->
double {
3514 double resid_lo = _HUGE, resid_hi = _HUGE;
3515 bool lo_ok =
false, hi_ok =
false;
3517 resid_lo = resid(
static_cast<double>(Tmin));
3518 lo_ok = std::isfinite(resid_lo);
3522 resid_hi = resid(
static_cast<double>(Tmax));
3523 hi_ok = std::isfinite(resid_hi);
3530 if (!lo_ok && hi_ok) {
3531 double a =
static_cast<double>(Tmin), b =
static_cast<double>(Tmax);
3532 for (
int i = 0; i < 50 && (b - a) > 0.01; ++i) {
3533 double mid = 0.5 * (a + b);
3535 resid_lo = resid(mid);
3536 if (std::isfinite(resid_lo)) {
3547 }
else if (!hi_ok && lo_ok) {
3548 double a =
static_cast<double>(Tmin), b =
static_cast<double>(Tmax);
3549 for (
int i = 0; i < 50 && (b - a) > 0.01; ++i) {
3550 double mid = 0.5 * (a + b);
3552 resid_hi = resid(mid);
3553 if (std::isfinite(resid_hi)) {
3566 if (lo_ok && hi_ok && resid_lo * resid_hi < 0) {
3569 std::uintmax_t max_iter = 100;
3570 auto bracket = boost::math::tools::toms748_solve(resid,
static_cast<double>(Tmin),
static_cast<double>(Tmax), resid_lo, resid_hi,
3571 boost::math::tools::eps_tolerance<double>(40), max_iter);
3572 double T_sol = (bracket.first + bracket.second) / 2.0;
3576 }
catch (std::exception& e) {
3577 throw ValueError(
format(
"HSU_P_flash for mixture failed with Tmin=%Lg, Tmax=%Lg, p=%Lg: %s", Tmin, Tmax, p, e.what()));
3579 }
else if (lo_ok || hi_ok) {
3581 double T_best = (!hi_ok || (lo_ok && std::abs(resid_lo) <= std::abs(resid_hi))) ?
static_cast<double>(Tmin) :
static_cast<double>(Tmax);
3584 }
catch (std::exception& e) {
3585 throw ValueError(
format(
"HSU_P_flash for mixture: endpoints do not bracket (resid_lo=%g, resid_hi=%g) "
3586 "and fallback at T=%g failed: %s",
3587 resid_lo, resid_hi, T_best, e.what()));
3590 throw ValueError(
format(
"HSU_P_flash for mixture: neither endpoint evaluable (Tmin=%Lg, Tmax=%Lg, p=%Lg)", Tmin, Tmax, p));
3600 const auto resid_final =
static_cast<double>(HEOS.
keyed_output(other) - value);
3601 const double resid_scale = std::abs(
static_cast<double>(value)) + 1.0;
3602 if (!
ValidNumber(resid_final) || std::abs(resid_final) > 1e-6 * resid_scale) {
3603 throw ValueError(
format(
"HSU_P_flash for mixture did not converge to the specification: residual %g (target %g) "
3604 "at T=%g K, p=%g Pa -- the (T,p) flash is misclassifying the phase for this mixture",
3605 resid_final,
static_cast<double>(value),
static_cast<double>(HEOS.
T()),
static_cast<double>(p)));
3619 : HEOS(HEOS),
T(
T), value(value), other(other) {}
3620 double call(
double rhomolar)
override {
3625 double deriv(
double rhomolar)
override {
3628 double second_deriv(
double rhomolar)
override {
3632 solver_resid resid(&HEOS,
T, value, other);
3637 if (HEOS.
_T > T_critical_) {
3666 Brent(resid, rhoc, rhomin, LDBL_EPSILON, 1e-9, 100);
3667 }
else if (y < yc) {
3685 if (step_count > 30) {
3686 throw ValueError(
format(
"Even by increasing rhoc, not able to bound input; input %Lg is not in range %Lg,%Lg", y, yc, ymin));
3690 Brent(resid, rhomin, rhoc, LDBL_EPSILON, 1e-9, 100);
3692 throw ValueError(
format(
"input %Lg is not in range %Lg,%Lg,%Lg", y, yc, ymin));
3716 rhoL =
static_cast<double>(HEOS.
_rhoLanc);
3725 rhoL = superanc_ptr->eval_sat(HEOS.
_T,
'D', 0);
3732 rhoL = HEOS.
components[0].ancillaries.rhoL.evaluate(HEOS.
_T);
3758 CoolPropDbl rhomolar_guess = (rhomelt - rhoL) / (ymelt - yL) * (y - yL) + rhoL;
3761 Halley(resid, rhomolar_guess, 1e-8, 100);
3763 Secant(resid, rhomolar_guess, 0.0001 * rhomolar_guess, 1e-12, 100);
3772 rhoV =
static_cast<double>(HEOS.
_rhoVanc);
3778 rhoV = superanc_ptr->eval_sat(HEOS.
_T,
'D', 1);
3785 rhoV = HEOS.
components[0].ancillaries.rhoV.evaluate(HEOS.
_T);
3789 Halley(resid, 0.5 * (rhomin + rhoV), 1e-8, 100);
3792 Brent(resid, rhomin, rhoV, LDBL_EPSILON, 1e-12, 100);
3798 throw ValueError(
format(
"phase to solver_for_rho_given_T_oneof_HSU is invalid"));
3809 if (HEOS.
_T < T_critical_)
3844 Q = (1 / HEOS.
rhomolar() - 1 / HEOS1.
SatL->rhomolar()) / (1 / HEOS1.
SatV->rhomolar() - 1 / HEOS1.
SatL->rhomolar());
3847 Q = (HEOS.
smolar() - HEOS1.
SatL->smolar()) / (HEOS1.
SatV->smolar() - HEOS1.
SatL->smolar());
3850 Q = (HEOS.
hmolar() - HEOS1.
SatL->hmolar()) / (HEOS1.
SatV->hmolar() - HEOS1.
SatL->hmolar());
3853 Q = (HEOS.
umolar() - HEOS1.
SatL->umolar()) / (HEOS1.
SatV->umolar() - HEOS1.
SatL->umolar());
3865 HEOS.
_p = HEOS.
_Q * HEOS1.
SatV->p() + (1 - HEOS.
_Q) * HEOS1.
SatL->p();
3871 throw ValueError(
format(
"Temperature specified is not the imposed phase region."));
3872 }
else if (HEOS.
_T > T_critical_ && HEOS.
_T > HEOS.
components[0].EOS().Ttriple) {
3929 bool solved =
false;
3941 stability_tester.
set_TP(
T, P_eos);
3963 auto is_mechanically_stable = [&](
double rho) ->
bool {
3964 double eps_rho = std::max(rho * 1e-6, 1e-10);
3967 return (P_hi - P_lo) > 0;
3970 auto rho_resid = [&](
double rho) ->
double {
3984 double rho_gas = -1;
3986 std::uintmax_t max_iter = 100;
3987 auto bracket = boost::math::tools::toms748_solve(rho_resid,
static_cast<double>(rho_min),
static_cast<double>(rho_reducing),
3988 boost::math::tools::eps_tolerance<double>(40), max_iter);
3989 double rho_cand = (bracket.first + bracket.second) / 2.0;
3997 double rho_liq = -1;
3999 std::uintmax_t max_iter = 100;
4000 auto bracket = boost::math::tools::toms748_solve(rho_resid,
static_cast<double>(rho_reducing),
static_cast<double>(rho_max),
4001 boost::math::tools::eps_tolerance<double>(40), max_iter);
4002 double rho_cand = (bracket.first + bracket.second) / 2.0;
4011 double rho_neg = -1, rho_pos = -1;
4012 double rho_prev = rho_max;
4014 for (
double rho_scan = rho_max * 0.95; rho_scan > rho_reducing; rho_scan *= 0.95) {
4016 if (P_prev > 0 && P_scan < 0) {
4021 rho_prev = rho_scan;
4024 if (rho_neg > 0 && rho_pos > 0) {
4025 for (
int i = 0; i < 30; i++) {
4026 double rho_mid = (rho_neg + rho_pos) / 2.0;
4034 std::uintmax_t max_iter = 100;
4035 auto bracket = boost::math::tools::toms748_solve(rho_resid, rho_pos,
static_cast<double>(rho_max),
4036 boost::math::tools::eps_tolerance<double>(40), max_iter);
4037 double rho_cand = (bracket.first + bracket.second) / 2.0;
4038 if (is_mechanically_stable(rho_cand)) {
4049 double rho_cand = -1;
4050 if (rho_gas > 0 && rho_liq > 0) {
4053 rho_cand = (G_liq <= G_gas) ? rho_liq : rho_gas;
4054 }
else if (rho_gas > 0) {
4056 }
else if (rho_liq > 0) {
4068 stability_tester.
set_TP(
T, P_eos);
4095 if (Pmin_bound < 100) Pmin_bound = 100;
4097 auto p_resid = [&](
double P) ->
double {
4104 double logPmin = std::log10(
static_cast<double>(Pmin_bound));
4105 double logPmax = std::log10(
static_cast<double>(Pmax_bound));
4107 int nsteps =
static_cast<int>((logPmax - logPmin) / dlogP) + 1;
4109 double P_lo = -1, P_hi = -1;
4112 bool have_prev =
false;
4114 for (
int i = 0; i <= nsteps; i++) {
4115 double logP = logPmin + i * dlogP;
4116 if (logP > logPmax) logP = logPmax;
4117 double P = std::pow(10.0, logP);
4125 if (have_prev && f_prev * f < 0) {
4136 if (P_lo > 0 && P_hi > 0) {
4138 std::uintmax_t max_iter = 100;
4140 boost::math::tools::toms748_solve(p_resid, P_lo, P_hi, boost::math::tools::eps_tolerance<double>(40), max_iter);
4141 double P_sol = (bracket.first + bracket.second) / 2.0;
4143 }
catch (std::exception& e) {
4144 throw ValueError(
format(
"DHSU_T_flash P-sweep TOMS748 for mixture failed: T=%Lg, target=%s, value=%Lg, "
4145 "bracket=[%g, %g]: %s",
4149 throw ValueError(
format(
"DHSU_T_flash P-sweep for mixture: no bracket found scanning P in [%Lg, %Lg] at T=%Lg "
4150 "for target %s=%Lg",
4162 const auto resid_dhsu =
static_cast<double>(HEOS.
keyed_output(other) - value);
4163 const double scale_dhsu = std::abs(
static_cast<double>(value)) + 1.0;
4164 if (!
ValidNumber(resid_dhsu) || std::abs(resid_dhsu) > 1e-6 * scale_dhsu) {
4165 throw ValueError(
format(
"DHSU_T_flash for mixture did not converge to the specification: residual %g "
4166 "(target %s=%g) at T=%g K -- the (T,p) flash is misclassifying the phase for this mixture",
4168 static_cast<double>(HEOS.
T())));
4214 : HEOS(HEOS), hmolar(hmolar_spec), smolar(smolar_spec), Qs(_HUGE) {};
4215 double call(
double T)
override {
4219 Qs = (smolar - SatL.
smolar()) / (SatV.smolar() - SatL.
smolar());
4225 } resid(HEOS, hmolar_spec, smolar_spec);
4231 CoolPropDbl Tmin_satL = NAN, Tmin_satV = NAN, Tmin_sat = NAN;
4233 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
4242 double resid = 9e30, resid_old = 9e30;
4248 r(0) = HEOS.
hmolar() - hmolar_spec;
4249 r(1) = HEOS.
smolar() - smolar_spec;
4255 Eigen::Vector2d v = J.colPivHouseholderQr().solve(-r);
4256 bool good_solution =
false;
4257 double tau0 = HEOS.
tau(), delta0 = HEOS.
delta();
4263 for (
double frac = 1.0; frac > 0.001; frac /= 2) {
4266 double tau_new = tau0 + options.
omega * frac * v(0);
4267 double delta_new = delta0 + options.
omega * frac * v(1);
4268 double T_new = reducing.
T / tau_new;
4269 double rhomolar_new = delta_new * reducing.
rhomolar;
4273 if (resid > resid_old) {
4274 throw ValueError(
format(
"residual not decreasing; frac: %g, resid: %g, resid_old: %g", frac, resid, resid_old));
4276 good_solution =
true;
4283 if (!good_solution) {
4288 throw ValueError(
format(
"HS_flash_singlephase took too many iterations; residual is %g; prior was %g", resid, resid_old));
4290 }
while (std::abs(resid) > 1e-9);
4294 double logp = ((double)rand() / (double)RAND_MAX) * (log(HEOS.
pmax()) - log(HEOS.
p_triple())) + log(HEOS.
p_triple());
4295 T = ((double)rand() / (double)RAND_MAX) * (HEOS.
Tmax() - HEOS.
Ttriple()) + HEOS.
Ttriple();
4319 HelmholtzEOSMixtureBackend&
H;
4320 explicit HSGasGuard(HelmholtzEOSMixtureBackend&
H) :
H(
H) {
4326 H.unspecify_phase();
4328 HSGasGuard(
const HSGasGuard&) =
delete;
4329 HSGasGuard& operator=(
const HSGasGuard&) =
delete;
4330 HSGasGuard(HSGasGuard&&) =
delete;
4331 HSGasGuard& operator=(HSGasGuard&&) =
delete;
4338bool hs_corrector(HelmholtzEOSMixtureBackend&
H,
double T0,
double rho0,
double h_t,
double s_t,
double& T_out,
double& rho_out,
4339 double Tlo_override = -1.0) {
4341 const double hscale = Rgas * Tsc, sscale = Rgas;
4342 const double Tlo = (Tlo_override > 0.0) ? Tlo_override :
H.
Tmin() * (1.0 - 2e-2);
4343 const double Thi = 1.5 *
H.
Tmax();
4347 for (
int N = 1; N <= 128; N *= 2) {
4348 double T = T0, w = std::log(rho0);
4349 bool failed =
false;
4350 for (
int k = 1; k <= N && !failed; ++k) {
4351 const double lam =
static_cast<double>(k) / N;
4352 const double ht = h0 + lam * (h_t - h0), st = s0 + lam * (s_t - s0);
4354 double best_norm = 1e300, best_T =
T, best_w = w;
4355 for (
int iter = 0; iter < 40; ++iter) {
4356 const double rho = std::exp(w);
4359 const double norm = std::abs(rh) / hscale + std::abs(rs) / sscale;
4360 if (norm < best_norm) {
4371 const double a11 = hT, a12 = rho * hr, a21 = sT, a22 = rho * sr;
4372 const double det = a11 * a22 - a12 * a21;
4373 if (!std::isfinite(det) || std::abs(det) < 1e-300)
break;
4374 const double dT = -(a22 * rh - a12 * rs) / det, dw = -(-a21 * rh + a11 * rs) / det;
4376 while ((
T + f * dT < Tlo || T + f * dT > Thi || std::abs(f * dw) > 2.0) && f > 1e-6)
4378 for (
int g = 0; g < 8 && f > 1e-3; ++g) {
4379 eval(
T + f * dT, std::exp(w + f * dw));
4386 if (!conv && best_norm < 1e-8) {
4391 if (!conv) failed =
true;
4395 rho_out = std::exp(w);
4403double hs_s_to_cache(HelmholtzEOSMixtureBackend&
H, HS_SA_t& sa,
double s_t) {
4404 const auto stamp = sa.get_caloric_alpha0_stamp();
4405 if (!stamp.has_value())
return s_t;
4410double hs_h_to_cache(HelmholtzEOSMixtureBackend&
H, HS_SA_t& sa,
double h_t) {
4411 const auto stamp = sa.get_caloric_alpha0_stamp();
4412 if (!stamp.has_value())
return h_t;
4419bool hs_leg_saturation(HelmholtzEOSMixtureBackend&
H, HS_SA_t& sa,
double h_t,
double s_t,
double& T_out,
double& rho_out) {
4420 HSGasGuard guard(
H);
4421 const double Tc = sa.get_Tcrit_num(), Tmin = sa.get_Tmin();
4422 const double Tcap = Tc - std::max(1e-4, 1e-6 * Tc);
4423 const double a = Tmin + 1e-3, b = Tcap;
4425 const double s_cache = hs_s_to_cache(
H, sa, s_t);
4431 const bool use_ca_h = sa.has_variable(
'H');
4432 const double h_cache = use_ca_h ? hs_h_to_cache(
H, sa, h_t) : h_t;
4433 double T0 = 0, rho0 = 0, best_hgap = 1e300;
4434 bool found =
false, near_critical =
false;
4435 auto consider = [&](
double Tcand,
short Q) {
4436 const double rho = sa.eval_sat(Tcand,
'D', Q);
4439 hgap = std::abs(sa.eval_sat(Tcand,
'H', Q) - h_cache);
4442 hgap = std::abs(
H.
hmolar() - h_t);
4444 if (std::isfinite(hgap) && hgap < best_hgap) {
4451 for (
const auto& pr : sa.get_all_intersections(
'S', s_cache, 48, 100, 1e-12)) {
4452 const double Tcand = pr.first;
4453 if (Tcand >= b) near_critical =
true;
4454 if (Tcand <= a || Tcand >= b)
continue;
4455 const short Qroot = (std::abs(sa.eval_sat(Tcand,
'S', 0) - s_cache) <= std::abs(sa.eval_sat(Tcand,
'S', 1) - s_cache)) ? 0 : 1;
4456 consider(Tcand, Qroot);
4459 if (near_critical)
return false;
4460 const double s_crit = [&] {
4465 rho0 = sa.get_rhocrit_num() * 1e-4;
4466 auto hres = [&](
double T) {
4470 double Ta = Tmin + 1e-3, Tb = 1.5 *
H.
Tmax(), fa = hres(Ta), fb = hres(Tb);
4472 boost::math::uintmax_t it = 60;
4473 auto [l, r] = toms748_solve(hres, Ta, Tb, fa, fb, boost::math::tools::eps_tolerance<double>(30), it);
4476 T0 = (std::abs(fa) <= std::abs(fb)) ? Ta : Tb;
4480 rho0 = sa.eval_sat(a,
'D', 0);
4483 return hs_corrector(
H, T0, rho0, h_t, s_t, T_out, rho_out);
4488bool hs_leg_isentrope(HelmholtzEOSMixtureBackend&
H,
double h_t,
double s_t,
double& T_out,
double& rho_out) {
4489 HSGasGuard guard(
H);
4491 auto sres = [&](
double rho) {
4495 const double ra = 1e-6 * rhoc, ga = sres(ra);
4496 double rb = 6.0 * rhoc, gb = sres(rb);
4497 for (
int e = 0; e < 12 && ga * gb > 0; ++e) {
4498 const double rb_next = rb * 2.0;
4501 gb_next = sres(rb_next);
4505 if (!std::isfinite(gb_next))
break;
4511 rho0 = (std::abs(ga) <= std::abs(gb)) ? ra : rb;
4513 boost::math::uintmax_t it = 80;
4514 auto [l, r] = toms748_solve(sres, ra, rb, ga, gb, boost::math::tools::eps_tolerance<double>(40), it);
4515 rho0 = 0.5 * (l + r);
4517 return hs_corrector(
H, Ta, rho0, h_t, s_t, T_out, rho_out);
4522bool hs_leg_departure(HelmholtzEOSMixtureBackend&
H,
double h_t,
double s_t,
double& T_out,
double& rho_out) {
4523 HSGasGuard guard(
H);
4526 const double Tr = red.
T, rhor = red.rhomolar;
4528 const double Tmin =
H.
Tmin(), Tmax =
H.
Tmax();
4535 double h, s, prho, hT, hr, sT, sr;
4538 auto lprops = [&](
double T,
double rho,
double lam) -> LP {
4539 const double tau = Tr /
T, delta = rho / rhor;
4545 const double a0 = a0d.alphar, a0t = a0d.dalphar_dtau, a0tt = a0d.d2alphar_dtau2;
4546 const double ar = ard_all.alphar, art = ard_all.dalphar_dtau, ard = ard_all.dalphar_ddelta;
4547 const double artt = ard_all.d2alphar_dtau2, ardd = ard_all.d2alphar_ddelta2, artd = ard_all.d2alphar_ddelta_dtau;
4548 const double at = a0t + lam * art, att = a0tt + lam * artt;
4549 const double Hh = 1.0 + tau * at + delta * lam * ard;
4552 L.s = Rg * (tau * at - (a0 + lam * ar));
4553 L.prho = Rg *
T * (1.0 + 2.0 * delta * lam * ard + delta * delta * lam * ardd);
4554 const double dHdtau = at + tau * att + delta * lam * artd;
4555 const double dHddelta = lam * ard + tau * lam * artd + delta * lam * ardd;
4556 L.hT = Rg * (Hh - tau * dHdtau);
4557 L.hr = Rg *
T * dHddelta / rhor;
4558 L.sT = -Rg * tau * tau * att /
T;
4559 L.sr = Rg * (tau * lam * artd - 1.0 / delta - lam * ard) / rhor;
4564 if (!std::isfinite(L.h) || !std::isfinite(L.s) || !std::isfinite(L.prho) || !std::isfinite(L.hT) || !std::isfinite(L.hr)
4565 || !std::isfinite(L.sT) || !std::isfinite(L.sr)) {
4566 throw ValueError(
"hs_leg_departure: non-finite lambda-model property/derivative");
4573 double T0 = 0, rho0 = 0;
4575 auto hig = [&](
double T) {
return lprops(
T, rhor, 0.0).h - h_t; };
4576 double Ta = 0.3 * Tmin, Tb = 3.0 * Tmax, fa = hig(Ta), fb = hig(Tb);
4577 if (fa * fb > 0)
return false;
4578 boost::math::uintmax_t it = 80;
4579 auto [l, r] = toms748_solve(hig, Ta, Tb, fa, fb, boost::math::tools::eps_tolerance<double>(40), it);
4581 auto sig = [&](
double rho) {
return lprops(T0, rho, 0.0).s - s_t; };
4582 double ra = 1e-8 * rhor, rb = 50.0 * rhor, ga = sig(ra), gb = sig(rb);
4584 rho0 = (std::abs(ga) <= std::abs(gb)) ? ra : rb;
4586 boost::math::uintmax_t it2 = 80;
4587 auto [l2, r2] = toms748_solve(sig, ra, rb, ga, gb, boost::math::tools::eps_tolerance<double>(40), it2);
4588 rho0 = 0.5 * (l2 + r2);
4594 const double hscale = Rg * Tr, sscale = Rg;
4595 for (
int N = 1; N <= 256; N *= 2) {
4596 double T = T0, w = std::log(rho0);
4597 bool failed =
false;
4598 for (
int k = 1; k <= N && !failed; ++k) {
4599 const double lam =
static_cast<double>(k) / N;
4601 double best_norm = 1e300, best_T =
T, best_w = w;
4602 for (
int iter = 0; iter < 40; ++iter) {
4603 const double rho = std::exp(w);
4604 LP L = lprops(
T, rho, lam);
4605 const double rh = L.h - h_t, rs = L.s - s_t;
4606 const double norm = std::abs(rh) / hscale + std::abs(rs) / sscale;
4607 if (norm < best_norm) {
4618 const double a11 = L.hT, a12 = rho * L.hr, a21 = L.sT, a22 = rho * L.sr;
4619 const double det = a11 * a22 - a12 * a21;
4620 if (!std::isfinite(det) || std::abs(det) < 1e-300)
break;
4621 const double dT = -(a22 * rh - a12 * rs) / det, dw = -(-a21 * rh + a11 * rs) / det;
4625 while ((
T + f * dT <= 0 || T + f * dT > 3.0 * Tmax || std::abs(f * dw) > 2.0) && f > 1e-6)
4627 for (
int g = 0; g < 8 && f > 1e-3; ++g) {
4628 if (lprops(
T + f * dT, std::exp(w + f * dw), lam).prho > 0)
break;
4634 if (!conv && best_norm < 1e-8) {
4639 if (!conv) failed =
true;
4643 rho_out = std::exp(w);
4656bool hs_accept(HelmholtzEOSMixtureBackend&
H,
double T,
double rho,
double h_t,
double s_t,
double Tmin_override = -1.0) {
4657 if (!std::isfinite(
T) || !std::isfinite(rho) || rho <= 0)
return false;
4659 const double Tmin_eff = (Tmin_override > 0) ? Tmin_override :
H.
Tmin();
4660 if (
T < Tmin_eff * (1 - 1e-6) ||
T >
H.
Tmax() * (1 + 1e-6))
return false;
4661 HSGasGuard guard(
H);
4663 if (std::abs(
H.
hmolar() - h_t) > 1e-6 * Rg * Tc || std::abs(
H.
smolar() - s_t) > 1e-6 * Rg)
return false;
4670 if (!std::isfinite(cv) || cv <= 0)
return false;
4677bool hs_inside_dome(HS_SA_t& sa,
double T,
double rho) {
4678 if (
T >= sa.get_Tcrit_num())
return false;
4679 return rho > sa.eval_sat(
T,
'D', 1) && rho < sa.eval_sat(
T,
'D', 0);
4684double meltcal_s_to_cache(HelmholtzEOSMixtureBackend&
H,
const MeltingCaloric& mc,
double s_t) {
4685 const auto stamp = mc.stamp();
4686 if (!stamp.has_value())
return s_t;
4690double meltcal_h_to_cache(HelmholtzEOSMixtureBackend&
H,
const MeltingCaloric& mc,
double h_t) {
4691 const auto stamp = mc.stamp();
4692 if (!stamp.has_value())
return h_t;
4698bool hs_leg_melting(HelmholtzEOSMixtureBackend&
H,
const MeltingCaloric& mc,
double h_t,
double s_t,
double& T_out,
double& rho_out) {
4699 HSGasGuard guard(
H);
4700 const double s_cache = meltcal_s_to_cache(
H, mc, s_t);
4701 const double h_cache = meltcal_h_to_cache(
H, mc, h_t);
4702 double T0 = 0, rho0 = 0;
4703 if (!mc.seed_for_hs(s_cache, h_cache, T0, rho0))
return false;
4707 const double cTmin = mc.curve_Tmin();
4708 if (std::isfinite(cTmin) && cTmin > 0) Tlo = cTmin * (1.0 - 1e-3);
4709 return hs_corrector(
H, T0, rho0, h_t, s_t, T_out, rho_out, Tlo);
4716bool hs_cascade(HelmholtzEOSMixtureBackend&
H, HS_SA_t* sa,
double h_t,
double s_t,
double& T_out,
double& rho_out) {
4717 auto good = [&](
double T,
double rho) {
return hs_accept(
H,
T, rho, h_t, s_t) && !(sa !=
nullptr && hs_inside_dome(*sa,
T, rho)); };
4720 auto run = [&](
auto&& leg) ->
bool {
4722 double T = 0, rho = 0;
4723 if (leg(
T, rho) && good(
T, rho)) {
4732 if (sa !=
nullptr && run([&](
double&
T,
double& rho) {
return hs_leg_saturation(
H, *sa, h_t, s_t,
T, rho); }))
return true;
4733 if (run([&](
double&
T,
double& rho) {
return hs_leg_isentrope(
H, h_t, s_t,
T, rho); }))
return true;
4734 if (run([&](
double&
T,
double& rho) {
return hs_leg_departure(
H, h_t, s_t,
T, rho); }))
return true;
4743 static const bool meltcal_disabled = (std::getenv(
"COOLPROP_DISABLE_MELTING_CALORIC_HS") !=
nullptr);
4744 if (!meltcal_disabled &&
get_config_bool(ENABLE_MELTING_CALORIC_HS)) {
4746 const double melt_Tmin = mc->curve_Tmin();
4747 auto good_melt = [&](
double T,
double rho) {
4748 return hs_accept(
H,
T, rho, h_t, s_t, melt_Tmin) && !(sa !=
nullptr && hs_inside_dome(*sa,
T, rho));
4750 auto run_melt = [&](
auto&& leg) ->
bool {
4752 double T = 0, rho = 0;
4753 if (leg(
T, rho) && good_melt(
T, rho)) {
4762 if (run_melt([&](
double&
T,
double& rho) {
return hs_leg_melting(
H, *mc, h_t, s_t,
T, rho); }))
return true;
4776bool hs_two_phase_likely(HelmholtzEOSMixtureBackend&
H, HS_SA_t& sa,
double h_t,
double s_t) {
4778 if (!sa.has_variable(
'H') || !sa.has_variable(
'S'))
return false;
4779 const double hc = hs_h_to_cache(
H, sa, h_t), sc = hs_s_to_cache(
H, sa, s_t);
4780 const double Tmin = sa.get_Tmin(), Tc = sa.get_Tcrit_num();
4781 auto Qh_minus_Qs = [&](
double T) ->
double {
4782 const double sL = sa.eval_sat(
T,
'S', 0), sV = sa.eval_sat(
T,
'S', 1);
4783 const double hL = sa.eval_sat(
T,
'H', 0), hV = sa.eval_sat(
T,
'H', 1);
4784 return (hc - hL) / (hV - hL) - (sc - sL) / (sV - sL);
4790 const double Tlo = Tmin + 1e-3, Thi = Tc - std::max(0.5, 1e-3 * Tc);
4791 double Tprev = Tlo, fprev = Qh_minus_Qs(Tlo);
4792 for (
int i = 1; i <= M; ++i) {
4793 const double T = Tlo + (Thi - Tlo) * i / M;
4794 const double f = Qh_minus_Qs(
T);
4795 if (std::isfinite(fprev) && std::isfinite(f) && fprev * f <= 0) {
4796 boost::math::uintmax_t it = 60;
4797 auto [l, r] = toms748_solve(Qh_minus_Qs, Tprev,
T, fprev, f, boost::math::tools::eps_tolerance<double>(40), it);
4798 const double Tsol = 0.5 * (l + r);
4799 const double sL = sa.eval_sat(Tsol,
'S', 0), sV = sa.eval_sat(Tsol,
'S', 1);
4800 const double Q = (sc - sL) / (sV - sL);
4801 if (Q > 1e-6 && Q < 1.0 - 1e-6)
return true;
4812 double Tlo_override) {
4813 HSGasGuard guard(
H);
4814 return hs_corrector(
H, T0, rho0, h_t, s_t, T_out, rho_out, Tlo_override);
4825 static const bool hs_disabled = (std::getenv(
"COOLPROP_DISABLE_SUPERANC_HS") !=
nullptr);
4827 std::shared_ptr<EquationOfState::SuperAncillary_t> sa_ptr = HEOS.
get_superanc();
4830 auto restore_inputs = [&]() {
4835 auto reproduces = [&]() ->
bool {
4837 return std::isfinite(hh) && std::isfinite(ss) && std::abs(hh - h_t) <= 1e-6 * std::abs(h_t) + 1e-3
4838 && std::abs(ss - s_t) <= 1e-6 * std::abs(s_t) + 1e-5;
4842 bool tried_2ph =
false;
4843 auto try_twophase = [&]() ->
bool {
4847 return reproduces();
4852 if (hs_two_phase_likely(HEOS, sa, h_t, s_t)) {
4853 if (try_twophase())
return;
4860 double T = 0, rho = 0;
4861 if (hs_cascade(HEOS, sa_ptr.get(), h_t, s_t,
T, rho)) {
4867 if (reproduces())
return;
4873 if (try_twophase())
return;
4881 }
catch (
const std::exception& e) {
4883 std::cout <<
"HS_flash superancillary path failed (" << e.what() <<
"); using legacy path\n";
4901 static const bool hs_disabled_na = (std::getenv(
"COOLPROP_DISABLE_SUPERANC_HS") !=
nullptr);
4911 bool superanc_available =
false;
4922 double T = 0, rho = 0;
4923 if (hs_cascade(HEOS,
nullptr, h_t, s_t,
T, rho)) {
4930 ok = std::isfinite(hh) && std::isfinite(ss) && std::abs(hh - h_t) <= 1e-6 * std::abs(h_t) + 1e-3
4931 && std::abs(ss - s_t) <= 1e-6 * std::abs(s_t) + 1e-5;
4955 : HEOS(HEOS), hmolar(hmolar_spec), smolar(smolar_spec) {};
4956 double call(
double T)
override {
4958 double r = HEOS.
hmolar() - hmolar;
4961 } resid(HEOS, hmolar, smolar);
4963 bool good_Tmin =
false;
4968 rmin = resid.call(Tmin);
4973 if (Tmin > HEOS.
Tmax()) {
4976 }
while (!good_Tmin);
4979 bool good_Tmax =
false;
4980 double Tmax = HEOS.
Tmax() * 1.01;
4984 rmax = resid.call(Tmax);
4992 }
while (!good_Tmax);
4993 if (rmin * rmax > 0 && std::abs(rmax) < std::abs(rmin)) {
5003 bool hs_solved =
false;
5005 const int Nscan = 50;
5006 double T_prev = Tmin, f_prev = rmin;
5008 for (
int i = 1; i <= Nscan; ++i) {
5009 const double Tt = Tmin + (Tmax - Tmin) *
static_cast<double>(i) / Nscan;
5012 ft = resid.call(Tt);
5023 if (i == i_prev + 1 && f_prev * ft <= 0) {
5025 boost::math::uintmax_t max_iter = 100;
5026 auto f = [&resid](
const double T) {
return resid.call(
T); };
5027 auto [l, r] = toms748_solve(f, T_prev, Tt, f_prev, ft, boost::math::tools::eps_tolerance<double>(30), max_iter);
5028 resid.call(0.5 * (l + r));
5046#if defined(ENABLE_CATCH)
5048TEST_CASE(
"PD with T very large should yield error",
"[PDflash]") {
5049 shared_ptr<HelmholtzEOSBackend> HEOS = std::make_shared<HelmholtzEOSBackend>(
"R134a");
5050 double Tc = HEOS->T_critical();
5052 CHECK_THROWS(HEOS->update(
DmassP_INPUTS, 2, 5 * HEOS->p()));
5055TEST_CASE(
"Stability testing",
"[stability]") {
5056 shared_ptr<HelmholtzEOSMixtureBackend> HEOS =
5057 std::make_shared<HelmholtzEOSMixtureBackend>(
strsplit(
"n-Propane&n-Butane&n-Pentane&n-Hexane",
'&'));
5058 std::vector<double> z(4);
5063 HEOS->set_mole_fractions(z);
5066 double TL = HEOS->T();
5069 double TV = HEOS->T();
5071 SECTION(
"Liquid (feed is stable)") {
5072 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
5074 for (
double T = TL - 1;
T >= 100;
T -= 1) {
5075 stability_tester.set_TP(
T, 101325);
5077 CHECK_NOTHROW(stability_tester.is_stable());
5080 SECTION(
"Vapor (feed is stable)") {
5081 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
5083 for (
double T = TV + 1;
T <= 500;
T += 1) {
5084 stability_tester.set_TP(
T, 101325);
5086 CHECK_NOTHROW(stability_tester.is_stable());
5089 SECTION(
"Two-phase (feed is unstable)") {
5090 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
5091 stability_tester.set_TP((TV + TL) / 2.0, 101325);
5092 CHECK(stability_tester.is_stable() ==
false);
5096TEST_CASE(
"Test critical points for methane + H2S",
"[critical_points]") {
5097 shared_ptr<HelmholtzEOSMixtureBackend> HEOS = std::make_shared<HelmholtzEOSMixtureBackend>(
strsplit(
"Methane&H2S",
'&'));
5099 double zz[] = {0.998, 0.97, 0.9475, 0.94, 0.93, 0.86, 0.85, 0.84, 0.75, 0.53, 0.52, 0.51, 0.49, 0.36, 0.24, 0.229, 0.09};
5100 int Npts[] = {2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 1, 1, 1, 1};
5101 int imax =
sizeof(zz) /
sizeof(
double);
5103 for (
int i = 0; i < imax; ++i) {
5105 std::vector<double> z(2);
5108 HEOS->set_mole_fractions(z);
5110 std::vector<CriticalState> pts = HEOS->all_critical_points();
5111 CHECK(pts.size() == Npts[i]);
5115TEST_CASE(
"Test critical points for nitrogen + ethane with HEOS",
"[critical_points]") {
5116 shared_ptr<HelmholtzEOSMixtureBackend> HEOS = std::make_shared<HelmholtzEOSMixtureBackend>(
strsplit(
"Nitrogen&Ethane",
'&'));
5117 std::vector<double> zz =
linspace(0.001, 0.999, 21);
5118 int failure_count = 0;
5119 for (
double z0 : zz) {
5120 std::vector<double> z(2);
5123 HEOS->set_mole_fractions(z);
5125 std::vector<CriticalState> pts;
5127 pts = HEOS->all_critical_points();
5128 }
catch (std::exception& e) {
5134 CHECK(failure_count < 10);
5137TEST_CASE(
"Test critical points for nitrogen + ethane with PR",
"[critical_points]") {
5138 shared_ptr<PengRobinsonBackend> HEOS = std::make_shared<PengRobinsonBackend>(
strsplit(
"Nitrogen&Ethane",
'&'));
5139 HEOS->set_binary_interaction_double(0, 1,
"kij", 0.0407);
5140 std::vector<double> zz =
linspace(0.001, 0.999, 21);
5141 for (
double z0 : zz) {
5142 std::vector<double> z(2);
5145 HEOS->set_mole_fractions(z);
5147 std::vector<CriticalState> pts;
5148 CHECK_NOTHROW(pts = HEOS->all_critical_points());