CoolProp 8.0.0
An open-source fluid property and humid air property database
CoolProp-Tests-HS.cpp
Go to the documentation of this file.
1// Binding regression test for the ACTIVE production H,S flash.
2//
3// This exercises the shipped flash directly via HmolarSmolar_INPUTS (no
4// prototype solver) and is the only HS test that runs in the default suite.
5// The exploratory prototype solvers and their [.]-hidden A/B characterization
6// tests live in CoolProp-Tests-HS-prototypes.cpp; the active production code is
7// in src/Backends/Helmholtz/FlashRoutines.cpp.
8
10#include "CoolProp/CoolProp.h"
12
13#if defined(ENABLE_CATCH)
14# include <catch2/catch_all.hpp>
15
16# include <cmath>
17# include <cstdio>
18# include <memory>
19# include <string>
20# include <vector>
21
22namespace {
23
24std::vector<double> linspace(double a, double b, std::size_t n) {
25 std::vector<double> v(n);
26 for (std::size_t i = 0; i < n; ++i) {
27 v[i] = (n == 1) ? a : a + (b - a) * static_cast<double>(i) / static_cast<double>(n - 1);
28 }
29 return v;
30}
31
32} // namespace
33
34// ---------------------------------------------------------------------------
35// PRODUCTION two-phase HS round-trip: exercises the new superancillary
36// two-phase pre-screen + EOS-exact HS_flash_twophase path in FlashRoutines.
37// CatchTestRunner "[HS_prod2ph]"
38// ---------------------------------------------------------------------------
39TEST_CASE("HS production two-phase round-trip", "[HS][HS_prod2ph]") {
40 const std::string fluid = GENERATE(as<std::string>{}, "Water", "Nitrogen", "R134a", "MM", "n-Pentane", "Methane");
41 CAPTURE(fluid);
42 auto ref = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
43 auto wrk = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
44 const double Tc = ref->T_critical(), Tmin = ref->Ttriple();
45 std::size_t total = 0, ok = 0;
46 for (double T : linspace(Tmin + 1.0, Tc - std::max(0.5, 1e-3 * Tc), 25)) {
47 for (double Q : {0.05, 0.25, 0.5, 0.75, 0.95}) {
48 try {
49 ref->update(CoolProp::QT_INPUTS, Q, T);
50 } catch (...) {
51 continue;
52 }
53 const double h = ref->hmolar(), s = ref->smolar();
54 if (!std::isfinite(h) || !std::isfinite(s)) continue;
55 ++total;
56 CAPTURE(T, Q, h, s);
57 try {
58 wrk->update(CoolProp::HmolarSmolar_INPUTS, h, s);
59 } catch (const std::exception& e) {
60 FAIL_CHECK("two-phase HS threw: " << e.what());
61 continue;
62 }
63 const bool good = std::abs(wrk->T() - T) / T < 1e-5 && std::abs(wrk->Q() - Q) < 1e-4;
64 if (good)
65 ++ok;
66 else {
67 CAPTURE(wrk->T(), wrk->Q());
68 CHECK(good);
69 }
70 }
71 }
72 std::printf("[HS_prod2ph] %s: %zu/%zu two-phase round-trips\n", fluid.c_str(), ok, total);
73 REQUIRE(total > 0); // grid actually exercised the flash
74 CHECK(ok == total); // every two-phase (h,s) round-tripped
75}
76
77// ---------------------------------------------------------------------------
78// PRODUCTION single-phase HS round-trip across the whole phase diagram (liquid,
79// vapor, supercritical). Each state is established with PT_INPUTS (always
80// single-phase), then (h,s) is round-tripped back to (T,rho) through the
81// production HmolarSmolar_INPUTS flash; the originating T and rho must return.
82// This is the single-phase complement to [HS_prod2ph]; together they cover
83// general inputs over the full phase diagram.
84// CatchTestRunner "[HS_prod1ph]"
85// ---------------------------------------------------------------------------
86TEST_CASE("HS production single-phase round-trip (phase-diagram sweep)", "[HS][HS_prod1ph]") {
87 const std::string fluid = GENERATE(as<std::string>{}, "Water", "Nitrogen", "CarbonDioxide", "R134a", "MM", "n-Pentane", "Methane", "Hydrogen");
88 CAPTURE(fluid);
89 auto ref = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
90 auto wrk = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
91
92 const double Tmin = ref->Tmin(), Tmax = ref->Tmax(), pmax = ref->pmax();
93 double plo;
94 try {
95 plo = std::max(ref->p_triple(), pmax * 1e-6);
96 } catch (...) {
97 plo = pmax * 1e-6;
98 }
99 std::size_t total = 0, ok = 0;
100 // (log p, linear T) grid: log-p spans the compressed-liquid / dense
101 // supercritical region down to dilute vapor; T spans triple to Tmax.
102 for (double T : linspace(Tmin + 0.5, Tmax, 20)) {
103 for (std::size_t j = 0; j < 20; ++j) {
104 const double p = plo * std::pow(pmax / plo, static_cast<double>(j) / 19.0);
105 try {
106 ref->update(CoolProp::PT_INPUTS, p, T); // always single-phase
107 } catch (...) {
108 continue; // unreachable (p, T) for this fluid
109 }
110 const double h = ref->hmolar(), s = ref->smolar(), rho = ref->rhomolar(), Tt = ref->T();
111 if (!std::isfinite(h) || !std::isfinite(s) || !std::isfinite(rho)) continue;
112 ++total;
113 CAPTURE(T, p, rho, h, s);
114 try {
115 wrk->update(CoolProp::HmolarSmolar_INPUTS, h, s);
116 } catch (const std::exception& e) {
117 FAIL_CHECK("single-phase HS threw: " << e.what());
118 continue;
119 }
120 const bool good = std::abs(wrk->T() - Tt) / Tt < 1e-5 && std::abs(wrk->rhomolar() - rho) / rho < 1e-5;
121 if (good)
122 ++ok;
123 else {
124 CAPTURE(wrk->T(), wrk->rhomolar());
125 CHECK(good);
126 }
127 }
128 }
129 std::printf("[HS_prod1ph] %s: %zu/%zu single-phase round-trips\n", fluid.c_str(), ok, total);
130 REQUIRE(total > 0); // grid actually exercised the flash
131 CHECK(ok == total); // every single-phase (h,s) round-tripped
132}
133
134// ---------------------------------------------------------------------------
135// Single-phase H,S round-trip for fluids WITHOUT a superancillary: the
136// pseudo-pure fluids (Air, the R-40x/50x blends, SES36) and any pure fluid
137// built without one. These cannot take the is_pure()-gated superancillary
138// happy path, so they exercise the dedicated superancillary-free fast path in
139// HS_flash (dome-free cascade legs: supercritical isentrope, ideal-gas
140// departure, melting-line anchor). Guards both correctness (round-trips to
141// the forward (T, rho)) and that the fast path does not throw where the legacy
142// blind scan succeeded. A looser tolerance than [HS_prod1ph] is used because
143// pseudo-pure property surfaces are ill-conditioned near the critical point
144// (the underlying mixture's temperature glide is collapsed); see the Air
145// critical-region harness for the same rationale.
146//
147// CatchTestRunner "[HS_pseudopure]"
148// ---------------------------------------------------------------------------
149TEST_CASE("HS single-phase round-trip (no-superancillary / pseudo-pure fluids)", "[HS][HS_pseudopure]") {
150 const std::string fluid = GENERATE(as<std::string>{}, "Air", "R407C", "R410A", "R404A", "R507A", "SES36");
151 CAPTURE(fluid);
152 auto ref = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
153 auto wrk = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", fluid));
154
155 const double Tmin = ref->Tmin(), Tmax = ref->Tmax(), pmax = ref->pmax();
156 double plo;
157 try {
158 plo = std::max(ref->p_triple(), pmax * 1e-6);
159 } catch (...) {
160 plo = pmax * 1e-6;
161 }
162 const double tol = 2e-4; // pseudo-pure critical-region ill-conditioning slack
163 std::size_t total = 0, ok = 0;
164 for (double T : linspace(Tmin + 0.5, Tmax, 20)) {
165 for (std::size_t j = 0; j < 20; ++j) {
166 const double p = plo * std::pow(pmax / plo, static_cast<double>(j) / 19.0);
167 try {
168 ref->update(CoolProp::PT_INPUTS, p, T); // throws inside the dome for a pseudo-pure -> skip
169 } catch (...) {
170 continue;
171 }
172 const double h = ref->hmolar(), s = ref->smolar(), rho = ref->rhomolar(), Tt = ref->T();
173 if (!std::isfinite(h) || !std::isfinite(s) || !std::isfinite(rho)) continue;
174 ++total;
175 CAPTURE(T, p, rho, h, s);
176 try {
177 wrk->update(CoolProp::HmolarSmolar_INPUTS, h, s);
178 } catch (const std::exception& e) {
179 FAIL_CHECK("single-phase HS threw: " << e.what());
180 continue;
181 }
182 const bool good = std::abs(wrk->T() - Tt) / Tt < tol && std::abs(wrk->rhomolar() - rho) / rho < tol;
183 if (good)
184 ++ok;
185 else {
186 CAPTURE(wrk->T(), wrk->rhomolar());
187 CHECK(good);
188 }
189 }
190 }
191 std::printf("[HS_pseudopure] %s: %zu/%zu single-phase round-trips\n", fluid.c_str(), ok, total);
192 REQUIRE(total > 0); // grid actually exercised the flash
193 CHECK(ok == total); // every single-phase (h,s) round-tripped
194}
195
196#endif // ENABLE_CATCH