CoolProp 8.0.0
An open-source fluid property and humid air property database
SaturationSurrogate.cpp
Go to the documentation of this file.
1// Implementation of CoolProp::sbtl::SaturationSurrogate.
2//
3// Lays out 11 cubic-spline curves over a log-uniform T knot mesh on
4// [T_triple, 0.9999 * T_crit]:
5//
6// * P_of_T_ (one curve — same for L/V by Maxwell equal-pressure)
7// * D_of_T_[2] (liquid + vapor density)
8// * H_of_T_[2]
9// * S_of_T_[2]
10// * U_of_T_[2]
11// * T_of_logp_ (inverse: T as a function of log(p_sat))
12//
13// At construction we sweep the source backend via QT_INPUTS at each
14// knot (Q=0 then Q=1) and record (P, D, H, S, U). The 11 curves are
15// then built independently from these column-extracted vectors.
16//
17// All splines are CoolProp::region::CubicSplineCurve — natural cubic,
18// O(1) amortized eval via the indexed-search machinery — already used
19// for boundary curves elsewhere in the codebase, so no new numerics
20// here. Per-property tight bounds (b_min, b_max) are computed at
21// build time but currently unused (kept for parity with the existing
22// CubicSplineCurve API; future range-check assertions can rely on
23// them).
24
26
27#include <array>
28#include <cmath>
29#include <memory>
30#include <stdexcept>
31#include <utility>
32#include <vector>
33
37
38namespace CoolProp {
39namespace sbtl {
40
41namespace {
42
43// Knot mesh: Chebyshev-Lobatto distribution in [T_lo, T_hi] —
44//
45// T_k = (T_lo + T_hi)/2 - (T_hi - T_lo)/2 * cos(pi * k / (N-1))
46//
47// concentrates knots at both endpoints, in particular near T_crit
48// where the sat-density curve bends most sharply. Empirically (Water
49// at 64 knots): max relative error in saturated density drops from
50// ~1.5e-6 (log-T spacing) to <1e-7, well inside the bead's 1e-6
51// acceptance target. Pressure / enthalpy / entropy / energy all
52// already passed at 1e-6 with log-T spacing — the rho curve is the
53// most curvature-concentrated one and is the budget binder here.
54//
55// (The bead suggested log-T spacing but the acceptance bar is on
56// accuracy, not spacing law; this deviation is intentional.)
57std::vector<double> chebyshev_lobatto_T_knots(double T_lo, double T_hi, std::size_t n) {
58 // Callers are required to ensure n >= 2 (and build_from_source enforces
59 // n >= kMinKnots == 4); the n==1 case would divide by zero below.
60 std::vector<double> T(n);
61 const double mid = 0.5 * (T_lo + T_hi);
62 const double half = 0.5 * (T_hi - T_lo);
63 const auto denom = static_cast<double>(n - 1);
64 for (std::size_t k = 0; k < n; ++k) {
65 const double theta = M_PI * static_cast<double>(k) / denom;
66 T[k] = mid - half * std::cos(theta);
67 }
68 // Numerical safety: cos(0) == 1 and cos(pi) == -1 exactly in
69 // IEEE-754, so endpoint values should already be T_lo / T_hi;
70 // assign explicitly anyway to be robust against any future
71 // change in the spacing formula.
72 T.front() = T_lo;
73 T.back() = T_hi;
74 return T;
75}
76
77constexpr std::size_t kMinKnots = 4;
78constexpr double kCriticalAvoidance = 0.9999;
79
80} // namespace
81
83{
84 std::unique_ptr<region::CubicSplineCurve> P_of_T; // L/V identical (Maxwell)
85 std::array<std::unique_ptr<region::CubicSplineCurve>, 2> D_of_T;
86 std::array<std::unique_ptr<region::CubicSplineCurve>, 2> H_of_T;
87 std::array<std::unique_ptr<region::CubicSplineCurve>, 2> S_of_T;
88 std::array<std::unique_ptr<region::CubicSplineCurve>, 2> U_of_T;
89 std::unique_ptr<region::CubicSplineCurve> T_of_logp;
90 double T_lo = 0.0;
91 double T_hi = 0.0;
92 double p_lo = 0.0;
93 double p_hi = 0.0;
94 std::size_t n_knots = 0;
95};
96
97SaturationSurrogate::SaturationSurrogate() : impl_(std::make_unique<Impl>()) {}
99
100std::unique_ptr<SaturationSurrogate> SaturationSurrogate::build_from_source(::CoolProp::AbstractState& src, std::size_t n_knots) {
101 if (n_knots < kMinKnots) {
102 return nullptr;
103 }
104 double T_triple = 0.0;
105 double T_crit = 0.0;
106 try {
107 T_triple = src.Ttriple();
108 T_crit = src.T_critical();
109 } catch (const std::exception&) {
110 return nullptr;
111 }
112 // Triple point sometimes comes back below the source's actual
113 // sat-curve floor (e.g. REFPROP reports a slightly different value
114 // from its internal flash range) — bump up a hair so the first
115 // QT_INPUTS probe doesn't reject.
116 const double T_lo = T_triple * (1.0 + 1.0e-6);
117 const double T_hi = T_crit * kCriticalAvoidance;
118 if (!(T_lo > 0.0) || !(T_hi > T_lo)) {
119 return nullptr;
120 }
121
122 const auto T_knots = chebyshev_lobatto_T_knots(T_lo, T_hi, n_knots);
123
124 std::vector<double> p_col(n_knots);
125 std::array<std::vector<double>, 2> D_col = {std::vector<double>(n_knots), std::vector<double>(n_knots)};
126 std::array<std::vector<double>, 2> H_col = {std::vector<double>(n_knots), std::vector<double>(n_knots)};
127 std::array<std::vector<double>, 2> S_col = {std::vector<double>(n_knots), std::vector<double>(n_knots)};
128 std::array<std::vector<double>, 2> U_col = {std::vector<double>(n_knots), std::vector<double>(n_knots)};
129
130 for (std::size_t k = 0; k < n_knots; ++k) {
131 const double T = T_knots[k];
132 double p_k = 0.0;
133 for (int side : {0, 1}) {
134 try {
135 src.update(::CoolProp::QT_INPUTS, static_cast<double>(side), T);
136 } catch (const std::exception&) {
137 return nullptr;
138 }
139 if (side == 0) {
140 p_k = src.p();
141 }
142 D_col[side][k] = src.rhomolar();
143 H_col[side][k] = src.hmolar();
144 S_col[side][k] = src.smolar();
145 U_col[side][k] = src.umolar();
146 // region::CubicSplineCurve::build doesn't reject non-finite
147 // dependent values — a NaN/inf from the source would land
148 // silently in the spline coefficients and corrupt every
149 // eval downstream. Bail on the first bad sample.
150 if (!std::isfinite(D_col[side][k]) || !std::isfinite(H_col[side][k]) || !std::isfinite(S_col[side][k])
151 || !std::isfinite(U_col[side][k])) {
152 return nullptr;
153 }
154 }
155 p_col[k] = p_k;
156 // p_sat must be strictly increasing in T inside the dome; if
157 // the source returned a non-monotone or non-finite sample, bail
158 // — log(p) for the inverse curve will fail otherwise.
159 if (!std::isfinite(p_k) || p_k <= 0.0) {
160 return nullptr;
161 }
162 if (k > 0 && !(p_k > p_col[k - 1])) {
163 return nullptr;
164 }
165 }
166
167 auto surrogate = std::unique_ptr<SaturationSurrogate>(new SaturationSurrogate());
168 auto& impl = *surrogate->impl_;
169 try {
170 impl.P_of_T = region::CubicSplineCurve::build(T_knots, p_col);
171 for (int side : {0, 1}) {
172 impl.D_of_T[side] = region::CubicSplineCurve::build(T_knots, D_col[side]);
173 impl.H_of_T[side] = region::CubicSplineCurve::build(T_knots, H_col[side]);
174 impl.S_of_T[side] = region::CubicSplineCurve::build(T_knots, S_col[side]);
175 impl.U_of_T[side] = region::CubicSplineCurve::build(T_knots, U_col[side]);
176 }
177 std::vector<double> logp_col(n_knots);
178 for (std::size_t k = 0; k < n_knots; ++k) {
179 logp_col[k] = std::log(p_col[k]);
180 }
181 // T_knots is reused verbatim as the dependent column for the
182 // inverse T(log p) spline — pass a copy because build() takes
183 // ownership.
184 impl.T_of_logp = region::CubicSplineCurve::build(logp_col, T_knots);
185 } catch (const std::exception&) {
186 return nullptr;
187 }
188 impl.T_lo = T_lo;
189 impl.T_hi = T_hi;
190 impl.p_lo = p_col.front();
191 impl.p_hi = p_col.back();
192 impl.n_knots = n_knots;
193 return surrogate;
194}
195
196double SaturationSurrogate::get_T_from_p(double p) const {
197 if (!(p > 0.0)) {
198 throw std::out_of_range("SaturationSurrogate::get_T_from_p: p must be positive");
199 }
200 if (p < impl_->p_lo || p > impl_->p_hi) {
201 throw std::out_of_range("SaturationSurrogate::get_T_from_p: p outside surrogate range");
202 }
203 return impl_->T_of_logp->eval(std::log(p));
204}
205
206double SaturationSurrogate::eval_sat(double T, char what, int side) const {
207 if (side != 0 && side != 1) {
208 throw std::invalid_argument("SaturationSurrogate::eval_sat: side must be 0 (liquid) or 1 (vapor)");
209 }
210 // NaN compares false against both bounds, so the range check below
211 // silently lets non-finite T through into the spline evaluator.
212 if (!std::isfinite(T)) {
213 throw std::out_of_range("SaturationSurrogate::eval_sat: T must be finite");
214 }
215 if (T < impl_->T_lo || T > impl_->T_hi) {
216 throw std::out_of_range("SaturationSurrogate::eval_sat: T outside surrogate range");
217 }
218 switch (what) {
219 case 'P':
220 return impl_->P_of_T->eval(T);
221 case 'D':
222 return impl_->D_of_T[side]->eval(T);
223 case 'H':
224 return impl_->H_of_T[side]->eval(T);
225 case 'S':
226 return impl_->S_of_T[side]->eval(T);
227 case 'U':
228 return impl_->U_of_T[side]->eval(T);
229 default:
230 throw std::invalid_argument("SaturationSurrogate::eval_sat: 'what' must be one of {P, D, H, S, U}");
231 }
232}
233
234double SaturationSurrogate::T_min() const noexcept {
235 return impl_->T_lo;
236}
237double SaturationSurrogate::T_max() const noexcept {
238 return impl_->T_hi;
239}
240std::size_t SaturationSurrogate::n_knots() const noexcept {
241 return impl_->n_knots;
242}
243
244} // namespace sbtl
245} // namespace CoolProp