31#if defined(ENABLE_CATCH)
32# include <catch2/catch_all.hpp>
46std::size_t env_or(
const char* name, std::size_t def) {
47 const char* v = std::getenv(name);
48 if (v ==
nullptr)
return def;
50 const long n = std::stol(v);
51 if (n > 1)
return static_cast<std::size_t
>(n);
58std::vector<double>
linspace(
double a,
double b, std::size_t n) {
59 std::vector<double> v(n);
60 for (std::size_t i = 0; i < n; ++i) {
61 v[i] = (n == 1) ? a : a + (b - a) *
static_cast<double>(i) /
static_cast<double>(n - 1);
66std::vector<double>
logspace(
double a,
double b, std::size_t n) {
67 std::vector<double> v(n);
68 for (std::size_t i = 0; i < n; ++i) {
69 v[i] = (n == 1) ? a : a * std::pow(b / a,
static_cast<double>(i) /
static_cast<double>(n - 1));
105 INFO(
"pair=" << pair_label(pr) <<
" p=" << p <<
" value=" << value <<
" T_expect=" << T_expect <<
" rho_expect=" << rho_expect);
112 }
catch (
const std::exception& e) {
113 FAIL_CHECK(
"update threw: " << e.what());
116 FAIL_CHECK(
"update threw non-std exception");
119 const double T_out = wrk.
T(), rho_out = wrk.
rhomolar();
120 CHECK(std::isfinite(T_out));
121 CHECK(std::isfinite(rho_out));
122 CHECK(T_out == Catch::Approx(T_expect).epsilon(1e-5));
123 CHECK(rho_out == Catch::Approx(rho_expect).epsilon(1e-5));
138void run_named_case(
const NamedCase& c) {
139 CAPTURE(c.fluid, c.note, c.T_K, c.p_Pa);
144 }
catch (
const std::exception& e) {
145 FAIL_CHECK(
"PT reference update threw for " << c.fluid <<
": " << e.what());
148 const double T_ref = ref->T(), rho_ref = ref->rhomolar();
149 const double h = ref->hmolar(), s = ref->smolar(), u = ref->umolar();
150 if (!std::isfinite(rho_ref) || !std::isfinite(h) || !std::isfinite(s) || !std::isfinite(u)) {
151 FAIL_CHECK(
"non-finite reference state for " << c.fluid);
186TEST_CASE(
"PXcdj named regressions",
"[PXcdj]") {
187 static const NamedCase kCases[] = {
189 {
"Water", 600.0, 1.0e5,
"gas"},
190 {
"Water", 320.0, 5.0e6,
"liquid"},
191 {
"Water", 700.0, 30.0e6,
"supercrit"},
193 {
"CarbonDioxide", 350.0, 1.0e6,
"gas"},
194 {
"CarbonDioxide", 320.0, 10.0e6,
"supercrit"},
195 {
"CarbonDioxide", 250.0, 5.0e6,
"liquid"},
197 {
"R134a", 250.0, 3.0e5,
"liquid"},
198 {
"R134a", 350.0, 2.0e5,
"gas"},
200 {
"n-Propane", 350.0, 1.0e6,
"gas"},
201 {
"n-Propane", 250.0, 5.0e6,
"liquid"},
203 {
"Nitrogen", 300.0, 1.0e5,
"gas"},
204 {
"Nitrogen", 200.0, 10.0e6,
"supercrit"},
206 {
"MM", 450.0, 1.0e5,
"gas"},
207 {
"MM", 350.0, 5.0e6,
"liquid"},
209 {
"Nitrogen", 121.0, 3.7e6,
"supercritical-cold wrong-basin trap"},
210 {
"R134a", 250.0, 3.0e5,
"spinodal step"},
211 {
"Water", 277.0, 5.0e6,
"density anomaly"},
213 for (
const auto& c : kCases) {
214 DYNAMIC_SECTION(c.fluid <<
" " << c.note <<
" T=" << c.T_K <<
"K p=" << c.p_Pa <<
"Pa") {
234TEST_CASE(
"PXcdj broad sweep",
"[PXcdj_sweep][.]") {
235 const std::size_t NT = env_or(
"PXCDJ_NT", 40);
236 const std::size_t NP = env_or(
"PXCDJ_NP", 40);
237 const std::size_t reps = env_or(
"PXCDJ_REPS", 3);
238 const char* csv_env = std::getenv(
"PXCDJ_CSV");
239 const std::string csv_path = (csv_env !=
nullptr) ? csv_env :
"pxcdj_sweep.csv";
241 std::ofstream out(csv_path);
242 if (!out.is_open()) {
243 FAIL(
"Failed to open PXcdj sweep CSV: " << csv_path);
245 out <<
"fluid,pair,T_K,p_Pa,rho_molm3,value,T_solved,rho_solved,success,microseconds\n";
248 const std::vector<std::string> fluids =
strsplit(fluids_csv,
',');
252 std::size_t fluids_attempted = 0, fluids_completed = 0;
253 std::size_t grand_total = 0, grand_ok = 0;
255 for (
const auto& fluid : fluids) {
256 if (fluid.empty())
continue;
259 std::shared_ptr<CoolProp::AbstractState> ref, wrk;
263 }
catch (
const std::exception& e) {
264 std::printf(
"[PXcdj_sweep] %s: factory threw — skipped (%s)\n", fluid.c_str(), e.what());
267 std::printf(
"[PXcdj_sweep] %s: factory threw non-std — skipped\n", fluid.c_str());
271 double Tmin = 0, Tmax = 0, pmax = 0, ptriple = 0;
276 }
catch (
const std::exception& e) {
277 std::printf(
"[PXcdj_sweep] %s: limits query threw — skipped (%s)\n", fluid.c_str(), e.what());
281 ptriple = ref->p_triple();
285 const double plo = std::max(std::max(ptriple, pmax * 1e-8), 1.0);
286 if (!(Tmax > Tmin) || !(pmax > plo)) {
287 std::printf(
"[PXcdj_sweep] %s: degenerate (T,p) bounds — skipped\n", fluid.c_str());
291 const auto T_vec =
linspace(Tmin + 0.1, Tmax, NT);
292 const auto p_vec =
logspace(plo, pmax, NP);
295 std::size_t pair_total[3] = {0, 0, 0};
296 std::size_t pair_ok[3] = {0, 0, 0};
297 std::size_t fluid_total = 0, fluid_ok = 0;
299 for (
double T : T_vec) {
300 for (
double p : p_vec) {
308 const double rho_ref = ref->rhomolar();
309 if (!std::isfinite(rho_ref) || rho_ref <= 0)
continue;
311 for (std::size_t pi = 0; pi < 3; ++pi) {
313 double value = _HUGE;
315 value = caloric_molar(*ref, pr);
319 if (!std::isfinite(value))
continue;
325 double best_us = 1e300;
327 double T_out = std::nan(
""), rho_out = std::nan(
"");
328 for (std::size_t r = 0; r < reps; ++r) {
329 const auto t0 = std::chrono::steady_clock::now();
332 wrk->update(pr, value, p);
334 wrk->update(pr, p, value);
339 const double us = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - t0).count();
340 if (us < best_us) best_us = us;
344 bool point_ok =
false;
348 rho_out = wrk->rhomolar();
350 T_out = std::nan(
"");
351 rho_out = std::nan(
"");
353 if (std::isfinite(T_out) && std::isfinite(rho_out) && std::abs(T_out -
T) /
T < 1e-5
354 && std::abs(rho_out - rho_ref) / rho_ref < 1e-5) {
366 out << fluid <<
',' << pair_label(pr) <<
',' <<
T <<
',' << p <<
',' << rho_ref <<
',' << value <<
',' << T_out <<
',' << rho_out
367 <<
',' << (point_ok ? 1 : 0) <<
',' << best_us <<
'\n';
373 std::printf(
"[PXcdj_sweep] %s: %zu/%zu round-trips (PH:%zu/%zu PS:%zu/%zu PU:%zu/%zu)\n", fluid.c_str(), fluid_ok, fluid_total, pair_ok[0],
374 pair_total[0], pair_ok[1], pair_total[1], pair_ok[2], pair_total[2]);
378 std::printf(
"[PXcdj_sweep] wrote %zu rows to %s\n", grand_total, csv_path.c_str());
379 std::printf(
"[PXcdj_sweep] fluids: %zu attempted, %zu completed; round-trips: %zu/%zu (%.2f%%)\n", fluids_attempted, fluids_completed, grand_ok,
380 grand_total, (grand_total > 0) ? 100.0 * grand_ok / grand_total : 0.0);