9#include "boost/math/tools/toms748_solve.hpp"
29std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t> try_get_superanc(
::CoolProp::AbstractState& src,
bool need_caloric) {
31 if (helmholtz ==
nullptr)
return nullptr;
32 std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t> sa;
39 if (!sa)
return nullptr;
42 helmholtz->ensure_caloric_superancillaries();
57std::unique_ptr<region::CubicSplineCurve> spline_through_log_p_samples(
double p_min,
double p_max, std::size_t n_knots,
58 const std::function<
double(
double)>& f) {
62 if (!(p_min > 0.0) || !(p_max > p_min) || n_knots < 2) {
63 throw std::invalid_argument(
"SatBoundaryFactory: invalid p range or n_knots (need 0 < p_min < p_max and n_knots >= 2)");
65 std::vector<double> p_knots(n_knots);
66 std::vector<double> y(n_knots);
67 const double log_p_min = std::log(p_min);
68 const double log_p_max = std::log(p_max);
69 for (std::size_t k = 0; k < n_knots; ++k) {
70 const double log_p = log_p_min +
static_cast<double>(k) * (log_p_max - log_p_min) /
static_cast<double>(n_knots - 1);
71 p_knots[k] = std::exp(log_p);
81 if (
auto sa = try_get_superanc(heos,
true)) {
91 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
92 heos.update(::CoolProp::PQ_INPUTS, p, 0.0);
99 if (
auto sa = try_get_superanc(heos,
true)) {
106 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
107 heos.update(::CoolProp::PQ_INPUTS, p, 1.0);
114 if (
auto sa = try_get_superanc(heos,
true)) {
124 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
125 heos.update(::CoolProp::PQ_INPUTS, p, 0.0);
132 if (
auto sa = try_get_superanc(heos,
true)) {
139 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
140 heos.update(::CoolProp::PQ_INPUTS, p, 1.0);
150std::unique_ptr<region::CubicSplineCurve> spline_through_linear_T_samples(
double T_min,
double T_max, std::size_t n_knots,
151 const std::function<
double(
double)>& f) {
152 if (!(T_min > 0.0) || !(T_max > T_min) || n_knots < 2) {
153 throw std::invalid_argument(
"SatBoundaryFactory: invalid T range or n_knots (need 0 < T_min < T_max and n_knots >= 2)");
155 std::vector<double> T_knots(n_knots);
156 std::vector<double> y(n_knots);
157 const double dT = (T_max - T_min) /
static_cast<double>(n_knots - 1);
158 for (std::size_t k = 0; k < n_knots; ++k) {
159 T_knots[k] = T_min +
static_cast<double>(k) * dT;
160 y[k] = f(T_knots[k]);
168 if (
auto sa = try_get_superanc(heos,
false)) {
178 return spline_through_linear_T_samples(T_min, T_max, opts.
n_knots, [&](
double T) {
179 heos.update(::CoolProp::QT_INPUTS, 0.0, T);
180 return heos.rhomass();
186 if (
auto sa = try_get_superanc(heos,
false)) {
193 return spline_through_linear_T_samples(T_min, T_max, opts.
n_knots, [&](
double T) {
194 heos.update(::CoolProp::QT_INPUTS, 1.0, T);
195 return heos.rhomass();
205 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
206 heos.update(::CoolProp::PQ_INPUTS, p, 0.0);
217 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&, walk_steps](
double p) {
218 for (std::size_t k = 0; k < walk_steps; ++k) {
219 const double T_try = T_min + 0.5 * static_cast<double>(k);
221 heos.update(::CoolProp::PT_INPUTS, p, T_try);
232 throw std::runtime_error(
"build_h_isotherm_floor: floor unreachable within " + std::to_string(walk_steps / 2) +
" K of T_min");
238 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
239 heos.update(::CoolProp::PT_INPUTS, p, T_max);
250 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&, walk_steps](
double p) {
251 for (std::size_t k = 0; k < walk_steps; ++k) {
252 const double T_try = T_min + 0.5 * static_cast<double>(k);
254 heos.update(::CoolProp::PT_INPUTS, p, T_try);
260 throw std::runtime_error(
"build_s_isotherm_floor: floor unreachable within " + std::to_string(walk_steps / 2) +
" K of T_min");
266 return spline_through_log_p_samples(p_min, p_max, opts.
n_knots, [&](
double p) {
267 heos.update(::CoolProp::PT_INPUTS, p, T_max);
285 if (!std::isfinite(p_triple) || p_triple <= 0.0) {
289 if (!std::isfinite(p_triple) || p_triple <= 0.0) {
290 throw ValueError(
"SVDSBTL: unable to resolve a finite, positive triple-point pressure for the subcritical floor.");
292 double p_min = p_triple;
293 if (p_min_override) {
300 if (!std::isfinite(*p_min_override) || *p_min_override <= 0.0) {
301 throw ValueError(
format(
"SVDSBTL: pmin (%g Pa) must be a finite, positive pressure.", *p_min_override));
303 if (*p_min_override < p_triple) {
304 throw ValueError(
format(
"SVDSBTL: pmin (%g Pa) is below the triple-point pressure (%g Pa); the subcritical "
305 "surfaces have no saturation boundary below the triple line.",
306 *p_min_override, p_triple));
308 p_min = *p_min_override;
310 return {p_min, 0.999 * p_crit};
315 const double p_max_eos = heos.
pmax();
316 return {1.001 * p_crit, 0.99 * p_max_eos};
320 if (!(T_max > T_min)) {
326 if (
auto sa = try_get_superanc(heos,
false)) {
328 const auto& approx = sa->get_approx1d(
'D', 0);
329 std::vector<double> hits;
330 for (
double T : approx.get_x_at_extrema()) {
331 if (
T > T_min &&
T < T_max) {
335 std::sort(hits.begin(), hits.end());
345 constexpr std::size_t kProbes = 64;
346 std::vector<double> T_grid(kProbes);
347 std::vector<double> rhoL(kProbes);
348 const double dT_grid = (T_max - T_min) /
static_cast<double>(kProbes - 1);
349 for (std::size_t k = 0; k < kProbes; ++k) {
350 T_grid[k] = T_min +
static_cast<double>(k) * dT_grid;
355 rhoL[k] = std::nan(
"");
361 auto slope_sign = [&](std::size_t i) ->
int {
362 if (i == 0 || i + 1 >= kProbes)
return 0;
363 const double rl = rhoL[i - 1];
364 const double rr = rhoL[i + 1];
365 if (!std::isfinite(rl) || !std::isfinite(rr))
return 0;
366 const double dr = rr - rl;
367 return (dr > 0.0) ? 1 : (dr < 0.0 ? -1 : 0);
369 std::vector<double> hits;
370 for (std::size_t i = 1; i + 1 < kProbes; ++i) {
371 const int s_lo = slope_sign(i);
372 const int s_hi = slope_sign(i + 1);
373 if (s_lo == 0 || s_hi == 0 || s_lo == s_hi)
continue;
376 auto residual = [&](
double T) ->
double {
380 const double h = 0.5 * dT_grid;
382 const double rl = heos.
rhomass();
384 const double rr = heos.
rhomass();
387 boost::math::tools::eps_tolerance<double> tol(48);
388 std::uintmax_t max_iter = 64;
389 auto root = boost::math::tools::toms748_solve(residual, T_grid[i], T_grid[i + 1], tol, max_iter);
390 hits.push_back(0.5 * (root.first + root.second));
397 std::sort(hits.begin(), hits.end());
401 hits.erase(std::unique(hits.begin(), hits.end(),
402 [](
double x,
double y) {
403 const double tol = 1e-8 * std::max({1.0, std::abs(x), std::abs(y)});
404 return std::abs(x - y) <= tol;