CoolProp 8.0.0
An open-source fluid property and humid air property database
MeltingCaloric.cpp
Go to the documentation of this file.
2// NOTE: match the include paths/style that FlashRoutines.cpp (same directory) uses
3// for these two headers:
6#include "FlashRoutines.h"
7#include <algorithm>
8#include <cmath>
9#include <functional>
10#include <map>
11#include <mutex>
12#include <stdexcept>
13#include <string>
14#include <Eigen/Dense>
15
16namespace CoolProp {
17
19 if (!H.has_melting_line()) {
20 throw ValueError("MeltingCaloric::sample: fluid has no melting line");
21 }
22 if (n < 8) n = 8;
23 const double p_lo = H.calc_melting_line(iP_min, iT, 0.0);
24 const double p_hi = H.calc_melting_line(iP_max, iT, 0.0);
25 const double lo = std::log(p_lo), hi = std::log(p_hi);
26
27 m_lnp.clear();
28 m_T.clear();
29 m_rho.clear();
30 m_h.clear();
31 m_s.clear();
32 m_lnp.reserve(n);
33 m_T.reserve(n);
34 m_rho.reserve(n);
35 m_h.reserve(n);
36 m_s.reserve(n);
37 for (std::size_t i = 0; i < n; ++i) {
38 const double lnp = lo + (hi - lo) * static_cast<double>(i) / static_cast<double>(n - 1);
39 const double p = std::exp(lnp);
40 double Tm;
41 try {
42 Tm = H.calc_melting_line(iT, iP, p);
43 } catch (...) {
44 continue;
45 }
46 try {
47 H.update(PT_INPUTS, p, Tm);
48 } catch (...) {
49 continue;
50 }
51 const double rho = H.rhomolar(), h = H.hmolar(), s = H.smolar();
52 if (!std::isfinite(rho) || !std::isfinite(h) || !std::isfinite(s)) continue;
53 m_lnp.push_back(lnp);
54 m_T.push_back(Tm);
55 m_rho.push_back(rho);
56 m_h.push_back(h);
57 m_s.push_back(s);
58 }
59}
60
61} // namespace CoolProp
62
63namespace {
64using CE_t = std::vector<CoolProp::superancillary::ChebyshevExpansion<Eigen::ArrayXd>>;
65
66void fit_part(CoolProp::HelmholtzEOSMixtureBackend& H, double ln_lo, double ln_hi,
67 const std::function<double(CoolProp::HelmholtzEOSMixtureBackend&)>& pick, CE_t& out) {
68 using namespace CoolProp;
69 // Shrink interval by a tiny factor to avoid landing exactly on segment
70 // boundaries where floating-point round-trip (double -> long double p_min)
71 // can cause is_in_closed_range() to fail.
72 const double span = ln_hi - ln_lo;
73 const double eps_rel = 1e-9;
74 const double adj_lo = ln_lo + eps_rel * span;
75 const double adj_hi = ln_hi - eps_rel * span;
76 auto func = [&](long /*i*/, long /*Npts*/, double lnp) -> double {
77 const double p = std::exp(lnp);
78 const double Tm = H.calc_melting_line(iT, iP, p);
79 H.update(PT_INPUTS, p, Tm);
80 return pick(H);
81 };
82 auto exps = superancillary::detail::dyadic_splitting<decltype(func), CE_t>(8, func, adj_lo, adj_hi, 3, 1e-10, 2);
83 for (auto& e : exps)
84 out.push_back(std::move(e));
85}
86
88bool can_eval_all(CoolProp::HelmholtzEOSMixtureBackend& H, double lnp) {
89 try {
90 const double p = std::exp(lnp);
91 const double Tm = H.calc_melting_line(CoolProp::iT, CoolProp::iP, p);
92 H.update(CoolProp::PT_INPUTS, p, Tm);
93 return std::isfinite(H.T()) && std::isfinite(H.rhomolar()) && std::isfinite(H.hmolar()) && std::isfinite(H.smolar());
94 } catch (...) {
95 return false;
96 }
97}
98
100std::vector<std::pair<double, double>> filter_valid_parts(CoolProp::HelmholtzEOSMixtureBackend& H,
101 const std::vector<std::pair<double, double>>& pranges) {
102 std::vector<std::pair<double, double>> valid;
103 for (const auto& pr : pranges) {
104 const double ln_lo = std::log(pr.first), ln_hi = std::log(pr.second);
105 if (!(ln_hi > ln_lo)) continue;
106 const double eps = 1e-9 * (ln_hi - ln_lo);
107 if (can_eval_all(H, ln_lo + eps) && can_eval_all(H, ln_hi - eps)) {
108 valid.push_back(pr);
109 }
110 }
111 return valid;
112}
113
115 fit_all_parts(CoolProp::HelmholtzEOSMixtureBackend& H, const std::vector<std::pair<double, double>>& pranges,
116 const std::function<double(CoolProp::HelmholtzEOSMixtureBackend&)>& pick) {
117 using namespace CoolProp;
118 CE_t exps;
119 for (const auto& pr : pranges) {
120 const double ln_lo = std::log(pr.first), ln_hi = std::log(pr.second);
121 if (!(ln_hi > ln_lo)) continue;
122 fit_part(H, ln_lo, ln_hi, pick, exps);
123 }
124 std::sort(exps.begin(), exps.end(), [](const auto& a, const auto& b) { return a.xmin() < b.xmin(); });
125 return {std::move(exps)};
126}
130double scan_bisect_h(const CoolProp::superancillary::ChebyshevApproximation1D<Eigen::ArrayXd>& approx, double h_cache) {
131 const double lo = approx.xmin(), hi = approx.xmax();
132 constexpr int Nscan = 200;
133 double best_lnp = lo, best_gap = 1e300;
134 double prev_lnp = lo, prev_h = approx.eval(lo) - h_cache;
135 for (int k = 1; k <= Nscan; ++k) {
136 const double cur_lnp = lo + (hi - lo) * k / Nscan;
137 const double cur_h = approx.eval(cur_lnp) - h_cache;
138 if (prev_h * cur_h <= 0.0) {
139 double a = prev_lnp, b = cur_lnp, fa = prev_h;
140 for (int it = 0; it < 60; ++it) {
141 const double m = 0.5 * (a + b);
142 const double fm = approx.eval(m) - h_cache;
143 if (fa * fm <= 0.0) {
144 b = m;
145 } else {
146 a = m;
147 fa = fm;
148 }
149 if ((b - a) < 1e-14 * (std::abs(a) + std::abs(b) + 1e-30)) break;
150 }
151 const double root_lnp = 0.5 * (a + b);
152 const double gap = std::abs(approx.eval(root_lnp) - h_cache);
153 if (std::isfinite(gap) && gap < best_gap) {
154 best_gap = gap;
155 best_lnp = root_lnp;
156 }
157 }
158 prev_lnp = cur_lnp;
159 prev_h = cur_h;
160 }
161 return best_lnp;
162}
163} // namespace
164
165namespace CoolProp {
166bool MeltingCaloric::seed_for_hs(double s_cache, double h_cache, double& T0, double& rho0) const {
167 if (!m_built || !m_s_approx || !m_h_approx || !m_T_approx || !m_rho_approx) return false;
168 // Enthalpy is monotonic in ln(p) along the melting curve (entropy and T are
169 // NOT -- they fold back at the ice-Ih/III junction), so intersect on h: this
170 // yields a single, well-conditioned root via the Chebyshev monotone-interval
171 // inverter. Entropy is used only to disambiguate in the (not expected) event
172 // of multiple roots.
173 std::vector<double> cand_lnp;
174 for (const auto& pr : m_h_approx->get_x_for_y(h_cache, 48, 100, 1e-12)) {
175 const double lnp = pr.first;
176 if (lnp >= m_h_approx->xmin() && lnp <= m_h_approx->xmax()) cand_lnp.push_back(lnp);
177 }
178 if (cand_lnp.empty()) {
179 // Fallback: bracket-scan h(lnp) - h_cache for a sign change (robust if the
180 // assembled-expansion metadata ever misbehaves or h is out of range).
181 const double fb_lnp = scan_bisect_h(*m_h_approx, h_cache);
182 const double fb_gap = std::abs(m_h_approx->eval(fb_lnp) - h_cache);
183 // Only accept the fallback if it actually found a near-zero residual
184 if (std::isfinite(fb_gap) && fb_gap < std::abs(h_cache) * 0.01 + 1.0) {
185 cand_lnp.push_back(fb_lnp);
186 }
187 }
188 if (cand_lnp.empty()) return false;
189 // Disambiguate by entropy closeness (usually exactly one candidate).
190 double best_lnp = cand_lnp.front(), best_gap = 1e300;
191 bool found = false;
192 for (double lnp : cand_lnp) {
193 const double sgap = std::abs(m_s_approx->eval(lnp) - s_cache);
194 if (std::isfinite(sgap) && sgap < best_gap) {
195 best_gap = sgap;
196 best_lnp = lnp;
197 found = true;
198 }
199 }
200 if (!found) return false;
201 T0 = m_T_approx->eval(best_lnp);
202 rho0 = m_rho_approx->eval(best_lnp);
203 return std::isfinite(T0) && std::isfinite(rho0) && rho0 > 0;
204}
205
207 if (m_built) return;
208 if (!H.has_melting_line()) return;
209 auto pranges = H.get_melting_line_part_pranges();
210 const double p_lo_eos = H.calc_melting_line(iP_min, iT, 0.0);
211 const double p_hi_eos = H.calc_melting_line(iP_max, iT, 0.0);
212 for (auto& pr : pranges) {
213 pr.first = std::max(pr.first, p_lo_eos);
214 pr.second = std::min(pr.second, p_hi_eos);
215 }
216 pranges.erase(std::remove_if(pranges.begin(), pranges.end(), [](const std::pair<double, double>& pr) { return !(pr.second > pr.first); }),
217 pranges.end());
218 if (pranges.empty()) return;
219
220 // Filter to only parts where the EOS can evaluate all caloric properties at both endpoints.
221 // Some melting-curve parts extend to pressures beyond the EOS validity domain.
222 pranges = filter_valid_parts(H, pranges);
223 if (pranges.empty()) return;
224
225 m_T_approx.emplace(fit_all_parts(H, pranges, [](HelmholtzEOSMixtureBackend& S) { return S.T(); }));
226 m_rho_approx.emplace(fit_all_parts(H, pranges, [](HelmholtzEOSMixtureBackend& S) { return S.rhomolar(); }));
227 m_h_approx.emplace(fit_all_parts(H, pranges, [](HelmholtzEOSMixtureBackend& S) { return S.hmolar(); }));
228 m_s_approx.emplace(fit_all_parts(H, pranges, [](HelmholtzEOSMixtureBackend& S) { return S.smolar(); }));
230 // Melting-curve minimum temperature (water folds to ~251 K), used as the
231 // corrector floor for the cold-liquid leg.
232 if (m_T_approx) {
233 const double lo = m_T_approx->xmin(), hi = m_T_approx->xmax();
234 double tmin = 1e300;
235 const int Nscan = 257;
236 for (int k = 0; k <= Nscan; ++k) {
237 const double lnp = lo + (hi - lo) * k / Nscan;
238 const double T = m_T_approx->eval(lnp);
239 if (std::isfinite(T) && T < tmin) tmin = T;
240 }
241 if (std::isfinite(tmin) && tmin < 1e299) m_curve_Tmin = tmin;
242 // else leave m_curve_Tmin == 0.0 (conservative sentinel: callers fall back to H.Tmin())
243 }
244 m_built = true;
245}
246
247std::shared_ptr<MeltingCaloric> get_melting_caloric_cached(HelmholtzEOSMixtureBackend& H) {
248 if (!H.is_pure() || !H.has_melting_line()) return nullptr;
249 static std::map<std::string, std::shared_ptr<MeltingCaloric>> cache;
250 static std::mutex mtx;
251 const std::string key = H.fluid_names()[0]; // pure fluid: single name
252 std::scoped_lock lk(mtx);
253 auto it = cache.find(key);
254 if (it != cache.end()) return it->second;
255 auto mc = std::make_shared<MeltingCaloric>();
256 try {
257 mc->build(H);
258 } catch (...) {
259 cache[key] = nullptr; // remember the failure; don't retry every call
260 return nullptr;
261 }
262 if (!mc->built()) mc = nullptr;
263 cache[key] = mc;
264 return mc;
265}
266} // namespace CoolProp