CoolProp 8.0.0
An open-source fluid property and humid air property database
Ancillaries.cpp
Go to the documentation of this file.
2
3#include <cmath>
4#include <limits>
5#include "CoolProp/CoolProp.h"
9
10#if defined(ENABLE_CATCH)
11
12# include <memory>
13using std::shared_ptr;
14# include <catch2/catch_all.hpp>
15
16#endif
17
18namespace CoolProp {
19
20// The anonymous union (max_abs_error vs. {using_tau_r, reducing_value, T_r, N})
21// is intentionally written one active member per branch; the `type` discriminant
22// gates every read, so the inactive members are never accessed. clang-tidy's
23// union-access / member-init guidance does not model discriminated unions, so it
24// is suppressed across this constructor.
25// NOLINTBEGIN(cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-union-access)
26SaturationAncillaryFunction::SaturationAncillaryFunction(const Values& v) : type(v.type), Tmin(v.Tmin), Tmax(v.Tmax) {
27 if (type == TYPE_RATIONAL_POLYNOMIAL) {
28 num_coeffs = v.num_coeffs;
29 den_coeffs = v.den_coeffs;
31 } else {
32 n = v.n;
33 t = v.t;
34 N = n.size();
35 s = n;
38 T_r = v.T_r;
39 }
40}
41// NOLINTEND(cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-union-access)
42
44 if (type == TYPE_NOT_SET) {
45 throw ValueError(format("type not set"));
46 } else if (type == TYPE_RATIONAL_POLYNOMIAL) {
47 Polynomial2D poly;
48 return poly.evaluate(num_coeffs, T) / poly.evaluate(den_coeffs, T);
49 } else {
50 double THETA = 1 - T / T_r;
51 // Saturation ancillaries are only defined at or below the reducing
52 // temperature. For T > T_r, pow(THETA, t) with non-integer t was
53 // pow(negative, fractional), producing NaN and (depending on FP
54 // trap settings) sometimes a SIGFPE that escaped C++ try/catch
55 // (#1611). Many internal callers (mixture critical-point search,
56 // VLE initialisers, etc.) legitimately probe ancillaries at T
57 // ranges where the pure component is supercritical and rely on
58 // the result being well-behaved-but-invalid (NaN) rather than an
59 // exception. Return NaN explicitly so the caller's existing
60 // sanity check / fallback runs without a SIGFPE.
61 if (THETA < 0) {
62 return std::numeric_limits<double>::quiet_NaN();
63 }
64
65 for (std::size_t i = 0; i < N; ++i) {
66 s[i] = n[i] * pow(THETA, t[i]);
67 }
68 double summer = std::accumulate(s.begin(), s.end(), 0.0);
69
70 if (type == TYPE_NOT_EXPONENTIAL) {
71 return reducing_value * (1 + summer);
72 } else {
73 double tau_r_value = NAN;
74 if (using_tau_r)
75 tau_r_value = T_r / T;
76 else
77 tau_r_value = 1.0;
78 return reducing_value * exp(tau_r_value * summer);
79 }
80 }
81}
82double SaturationAncillaryFunction::invert(double value, double min_bound, double max_bound) {
83 // Invert the ancillary curve to get the temperature as a function of the output variable
84 // Define the residual to be driven to zero
85 class solver_resid : public FuncWrapper1D
86 {
87 public:
89 CoolPropDbl value;
90
91 solver_resid(SaturationAncillaryFunction* anc, CoolPropDbl value) : anc(anc), value(value) {}
92
93 double call(double T) override {
94 CoolPropDbl current_value = anc->evaluate(T);
95 return current_value - value;
96 }
97 };
98 solver_resid resid(this, value);
99 if (min_bound < 0) {
100 min_bound = Tmin - 0.01;
101 }
102 if (max_bound < 0) {
103 max_bound = Tmax;
104 }
105
106 try {
107 // Safe to expand the domain a little bit to lower temperature, absolutely cannot exceed Tmax
108 // because then you get (negative number)^(double) which is undefined.
109 return Brent(resid, min_bound, max_bound, DBL_EPSILON, 1e-10, 100);
110 } catch (...) {
111 return ExtrapolatingSecant(resid, max_bound, -0.01, 1e-12, 100);
112 }
113}
114
117
118 // Fill in the min and max pressures for each part
119 for (auto& part : simon.parts) {
120 part.p_min = part.p_0 + part.a * (pow(part.T_min / part.T_0, part.c) - 1);
121 part.p_max = part.p_0 + part.a * (pow(part.T_max / part.T_0, part.c) - 1);
122 }
123 pmin = simon.parts.front().p_min;
124 pmax = simon.parts.back().p_max;
125 Tmin = simon.parts.front().T_min;
126 Tmax = simon.parts.back().T_max;
128 // Fill in the min and max pressures for each part
129 for (auto& part : polynomial_in_Tr.parts) {
130 part.p_min = part.evaluate(part.T_min);
131 part.p_max = part.evaluate(part.T_max);
132 }
133 Tmin = polynomial_in_Tr.parts.front().T_min;
134 pmin = polynomial_in_Tr.parts.front().p_min;
135 Tmax = polynomial_in_Tr.parts.back().T_max;
136 pmax = polynomial_in_Tr.parts.back().p_max;
138 // Fill in the min and max pressures for each part
139 for (auto& part : polynomial_in_Theta.parts) {
140 part.p_min = part.evaluate(part.T_min);
141 part.p_max = part.evaluate(part.T_max);
142 }
143 Tmin = polynomial_in_Theta.parts.front().T_min;
144 pmin = polynomial_in_Theta.parts.front().p_min;
145 Tmax = polynomial_in_Theta.parts.back().T_max;
146 pmax = polynomial_in_Theta.parts.back().p_max;
147 } else {
148 throw ValueError("only Simon supported now");
149 }
150}
151
153 if (type == MELTING_LINE_NOT_SET) {
154 throw ValueError("Melting line curve not set");
155 }
156 if (OF == iP_max) {
157 return pmax;
158 } else if (OF == iP_min) {
159 return pmin;
160 } else if (OF == iT_max) {
161 return Tmax;
162 } else if (OF == iT_min) {
163 return Tmin;
164 } else if (OF == iP && GIVEN == iT) {
165 CoolPropDbl T = value;
167 // Need to find the right segment
168 for (auto& part : simon.parts) {
169 if (is_in_closed_range(part.T_min, part.T_max, T)) {
170 return part.p_0 + part.a * (pow(T / part.T_0, part.c) - 1);
171 }
172 }
173 throw ValueError("unable to calculate melting line (p,T) for Simon curve");
175 // Need to find the right segment
176 for (auto& part : polynomial_in_Tr.parts) {
177 if (is_in_closed_range(part.T_min, part.T_max, T)) {
178 return part.evaluate(T);
179 }
180 }
181 throw ValueError("unable to calculate melting line (p,T) for polynomial_in_Tr curve");
183 // Need to find the right segment
184 for (auto& part : polynomial_in_Theta.parts) {
185 if (is_in_closed_range(part.T_min, part.T_max, T)) {
186 return part.evaluate(T);
187 }
188 }
189 throw ValueError("unable to calculate melting line (p,T) for polynomial_in_Theta curve");
190 } else {
191 throw ValueError(format("Invalid melting line type [%d]", type));
192 }
193 } else {
195 // Need to find the right segment
196 for (auto& part : simon.parts) {
197 // p = part.p_0 + part.a*(pow(T/part.T_0,part.c)-1);
198 CoolPropDbl T = pow((value - part.p_0) / part.a + 1, 1 / part.c) * part.T_0;
199 if (get_config_bool(DONT_CHECK_PROPERTY_LIMITS) || (T >= part.T_0 && T <= part.T_max)) {
200 return T;
201 }
202 }
203 throw ValueError(format("unable to calculate melting line T(p) for Simon curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
205 class solver_resid : public FuncWrapper1D
206 {
207 public:
209 CoolPropDbl given_p;
210 solver_resid(MeltingLinePiecewisePolynomialInTrSegment* part, CoolPropDbl p) : part(part), given_p(p) {};
211 double call(double T) override {
212
213 CoolPropDbl calc_p = part->evaluate(T);
214
215 // Difference between the two is to be driven to zero
216 return given_p - calc_p;
217 };
218 };
219
220 // Need to find the right segment
221 for (auto& part : polynomial_in_Tr.parts) {
222 if (is_in_closed_range(part.p_min, part.p_max, value)) {
223 solver_resid resid(&part, value);
224 double T = Brent(resid, part.T_min, part.T_max, DBL_EPSILON, 1e-12, 100);
225 return T;
226 }
227 }
228 throw ValueError(
229 format("unable to calculate melting line T(p) for polynomial_in_Theta curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
231
232 class solver_resid : public FuncWrapper1D
233 {
234 public:
236 CoolPropDbl given_p;
237 solver_resid(MeltingLinePiecewisePolynomialInThetaSegment* part, CoolPropDbl p) : part(part), given_p(p) {};
238 double call(double T) override {
239
240 CoolPropDbl calc_p = part->evaluate(T);
241
242 // Difference between the two is to be driven to zero
243 return given_p - calc_p;
244 };
245 };
246
247 // Need to find the right segment
248 for (auto& part : polynomial_in_Theta.parts) {
249 if (is_in_closed_range(part.p_min, part.p_max, value)) {
250 solver_resid resid(&part, value);
251 double T = Brent(resid, part.T_min, part.T_max, DBL_EPSILON, 1e-12, 100);
252 return T;
253 }
254 }
255
256 throw ValueError(
257 format("unable to calculate melting line T(p) for polynomial_in_Theta curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
258 } else {
259 throw ValueError(format("Invalid melting line type T(p) [%d]", type));
260 }
261 }
262}
263
264std::vector<std::pair<CoolPropDbl, CoolPropDbl>> MeltingLineVariables::get_parts_pranges() const {
265 std::vector<std::pair<CoolPropDbl, CoolPropDbl>> out;
267 for (const auto& p : simon.parts)
268 out.emplace_back(p.p_min, p.p_max);
270 for (const auto& p : polynomial_in_Tr.parts)
271 out.emplace_back(p.p_min, p.p_max);
273 for (const auto& p : polynomial_in_Theta.parts)
274 out.emplace_back(p.p_min, p.p_max);
275 }
276 return out;
277}
278
279}; /* namespace CoolProp */
280
281#if defined(ENABLE_CATCH)
282TEST_CASE("Water melting line", "[melting]") {
283 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "water"));
285 SECTION("Ice Ih-liquid") {
286 double actual = AS->melting_line(iT, iP, 138.268e6);
287 double expected = 260.0;
288 CAPTURE(actual);
289 CAPTURE(expected);
290 CHECK(std::abs(actual - expected) < 0.01);
291 }
292 SECTION("Ice III-liquid") {
293 double actual = AS->melting_line(iT, iP, 268.685e6);
294 double expected = 254;
295 CAPTURE(actual);
296 CAPTURE(expected);
297 CHECK(std::abs(actual - expected) < 0.01);
298 }
299 SECTION("Ice V-liquid") {
300 double actual = AS->melting_line(iT, iP, 479.640e6);
301 double expected = 265;
302 CAPTURE(actual);
303 CAPTURE(expected);
304 CHECK(std::abs(actual - expected) < 0.01);
305 }
306 SECTION("Ice VI-liquid") {
307 double actual = AS->melting_line(iT, iP, 1356.76e6);
308 double expected = 320;
309 CAPTURE(actual);
310 CAPTURE(expected);
311 CHECK(std::abs(actual - expected) < 1);
312 }
313}
314
315TEST_CASE("Tests for values from melting lines", "[melting]") {
317 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
318 for (const auto& fluid : fluids) {
319 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
320
321 // Skip fluids without a melting line. Also skip Water and HeavyWater:
322 // their melting curves are anomalous (the ice-Ih branch folds back BELOW
323 // the triple temperature), which violates this generic test's
324 // actual_T > Tmin / actual_T < Tmax bounds. Both have dedicated
325 // check-value tests instead (see the "[melting]" cases in CoolProp-Tests).
326 if (!AS->has_melting_line() || !fluid.compare("Water") || !fluid.compare("HeavyWater")) {
327 continue;
328 }
329
330 double pmax = AS->melting_line(CoolProp::iP_max, iT, 0);
331 double pmin = AS->melting_line(CoolProp::iP_min, iT, 0);
332 double Tmax = AS->melting_line(CoolProp::iT_max, iT, 0);
333 double Tmin = AS->melting_line(CoolProp::iT_min, iT, 0);
334
335 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
336 std::ostringstream ss0;
337 ss0 << "Check melting line limits for fluid " << fluid;
338 SECTION(ss0.str(), "") {
339 CAPTURE(Tmin);
340 CAPTURE(Tmax);
341 CAPTURE(pmin);
342 CAPTURE(pmax);
343 CHECK(Tmax > Tmin);
344 CHECK(pmax > pmin);
345 CHECK(pmin > 0);
346 }
347 // Integer-indexed grid (cert-flp30-c): the original
348 // for (p = 0.1*range + pmin; p < pmax; p += 0.2*range)
349 // intends 5 samples at p_i = pmin + (0.1 + 0.2*i)*range for
350 // i in {0..4}; preserve that count exactly.
351 const double p_range = pmax - pmin;
352 for (int p_i = 0; p_i < 5; ++p_i) {
353 const double p = pmin + (0.1 + 0.2 * p_i) * p_range;
354 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
355 std::ostringstream ss1;
356 ss1 << "Melting line for " << fluid << " at p=" << p;
357 SECTION(ss1.str(), "") {
358 double actual_T = AS->melting_line(iT, iP, p);
359 CAPTURE(Tmin);
360 CAPTURE(Tmax);
361 CAPTURE(actual_T);
362 CHECK(actual_T > Tmin);
363 CHECK(actual_T < Tmax);
364 }
365 }
366 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
367 std::ostringstream ss2;
368 ss2 << "Ensure melting line valid for " << fluid << " @ EOS pmax";
369 SECTION(ss2.str(), "") {
370 double actual_T = -_HUGE;
371 double EOS_pmax = AS->pmax();
372 double T_pmax_required = -1;
373 try {
374 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, true);
375 T_pmax_required = AS->melting_line(iT, iP, EOS_pmax);
376 } catch (...) { // NOLINT(bugprone-empty-catch)
377 // Best-effort probe: T_pmax_required stays at its -1
378 // sentinel and shows up in the CAPTURE below. The real
379 // assertion is CHECK_NOTHROW on the next melting_line
380 // call.
381 }
382 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, false);
383 CAPTURE(T_pmax_required);
384 CAPTURE(EOS_pmax);
385 CHECK_NOTHROW(actual_T = AS->melting_line(iT, iP, EOS_pmax));
386 CAPTURE(actual_T);
387 }
388 }
389}
390
391TEST_CASE("Test that hs_anchor enthalpy/entropy agrees with EOS", "[ancillaries]") {
392 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
393 for (const auto& fluid : fluids) {
394 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
395
396 CoolProp::SimpleState hs_anchor = AS->get_state("hs_anchor");
397
398 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
399 std::ostringstream ss1;
400 ss1 << "Check hs_anchor for " << fluid;
401 SECTION(ss1.str(), "") {
402 INFO("The enthalpy and entropy are hardcoded in the fluid JSON files. They MUST agree with the values calculated by the EOS");
403 AS->update(CoolProp::DmolarT_INPUTS, hs_anchor.rhomolar, hs_anchor.T);
404 double EOS_hmolar = AS->hmolar();
405 double EOS_smolar = AS->smolar();
406 CAPTURE(hs_anchor.hmolar);
407 CAPTURE(hs_anchor.smolar);
408 CAPTURE(EOS_hmolar);
409 CAPTURE(EOS_smolar);
410 CHECK(std::abs(EOS_hmolar - hs_anchor.hmolar) < 1e-3);
411 CHECK(std::abs(EOS_smolar - hs_anchor.smolar) < 1e-3);
412 }
413 }
414}
415
416TEST_CASE("Surface tension", "[surface_tension]") {
417 SECTION("from PropsSI") {
418 CHECK(ValidNumber(CoolProp::PropsSI("surface_tension", "T", 300, "Q", 0, "Water")));
419 }
420 SECTION("from saturation_ancillary") {
421 CHECK(ValidNumber(CoolProp::saturation_ancillary("Water", "surface_tension", 0, "T", 300)));
422 }
423 SECTION("from AbstractState") {
424 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
425 AS->update(CoolProp::QT_INPUTS, 0, 300);
426 CHECK_NOTHROW(AS->surface_tension());
427 }
428}
429#endif