CoolProp 8.0.0
An open-source fluid property and humid air property database
CoolProp-Tests-CubicU.cpp
Go to the documentation of this file.
1// Catch2 benchmarks + bit-exactness guard for the cubic-EOS forward eval u(T, rho).
2//
3// Motivation: when designing UV tabular inputs for SVDSBTL we want to know whether a
4// tabular forward eval is actually faster than calling a cubic EOS (SRK/PR) directly, so
5// we time u(T, rho) for the cubics against the full Helmholtz reference (HEOS). This file
6// replaces the old standalone dev/bench_cubic_u.cpp executable: the timing lives in a
7// [!benchmark] opt-in test (run with `CatchTestRunner "[cubic][!benchmark]"`) and the
8// bit-exactness check is a normal [cubic] test that runs in CI.
9
10#if defined(ENABLE_CATCH)
11
12# include <catch2/catch_all.hpp>
13
16# include "../Backends/Cubics/CubicBackend.h"
17# include "../Backends/Cubics/GeneralizedCubic.h"
18
19# include <cstdint>
20# include <cstring>
21# include <memory>
22# include <string>
23# include <vector>
24
25using namespace CoolProp;
26
27namespace {
28
29// Reinterpret a double's storage as a uint64 so two values can be compared bit-for-bit.
30// The shared-intermediate optimization claims a *term-for-term identical* assembly, not
31// merely ~equal results, so the guard below must be exact (and memcpy avoids the
32// suspicious-memory-comparison pitfall of memcmp on floating-point).
33std::uint64_t bits_of(double d) {
34 std::uint64_t u = 0;
35 std::memcpy(&u, &d, sizeof(u));
36 return u;
37}
38
39} // namespace
40
41// Opt-in via [!benchmark] (mirrors the [SVDComponents][!benchmark] timing tests). Each cubic
42// is timed twice: default (lets update_DmolarT do superancillary phase determination) and with
43// the phase imposed (sends update_DmolarT down the direct-pressure branch, skipping that work).
44// update(DmolarT) clears the property cache every call, so umolar() genuinely recomputes each
45// iteration -- this is the true forward-eval cost, not a cache hit.
46TEST_CASE("Cubic u(T,rho) forward-eval timing", "[cubic][!benchmark]") {
47 const std::string fluid = "Methane";
48 const double T = 300.0; // K
49 const double rho_molar = 5000.0; // mol/m^3
50
51 struct Case
52 {
53 const char* backend;
54 bool impose_phase;
55 };
56 for (auto c : {Case{"SRK", false}, Case{"SRK", true}, Case{"PR", false}, Case{"PR", true}, Case{"HEOS", false}}) {
57 auto AS = std::shared_ptr<AbstractState>(AbstractState::factory(c.backend, fluid));
58 if (c.impose_phase) {
59 AS->specify_phase(iphase_gas);
60 }
61 AS->update(DmolarT_INPUTS, rho_molar, T); // warm-up: pay one-time lazy-init costs
62 const std::string label = std::string(c.backend) + (c.impose_phase ? " (phase)" : "");
63 BENCHMARK(std::string("u(T,rho): ") + label) {
64 AS->update(DmolarT_INPUTS, rho_molar, T);
65 return AS->umolar();
66 };
67 }
68
69 // Floor cost: just the single residual derivative umolar actually needs --
70 // alphar(tau, delta, z, itau=1, idelta=0) called directly on the cubic object.
71 for (const char* backend : {"SRK", "PR"}) {
72 auto AS = std::shared_ptr<AbstractState>(AbstractState::factory(backend, fluid));
73 auto* cb = dynamic_cast<AbstractCubicBackend*>(AS.get());
74 REQUIRE(cb != nullptr);
75 auto& cubic = cb->get_cubic();
76 const std::vector<double> z = {1.0};
77 const double tau = cubic->get_Tr() / T;
78 const double delta = rho_molar / cubic->get_rhor();
79 BENCHMARK(std::string("raw alphar itau=1: ") + backend) {
80 return cubic->alphar(tau, delta, z, 1, 0);
81 };
82 }
83}
84
85// Correctness guard (runs in CI): the shared-intermediate assembly in the *production*
86// CubicResidualHelmholtz::all() must be bit-for-bit identical to 15 independent alphar() calls
87// (alphar() itself is unchanged, so it is the reference). This drives the real all() -- including
88// its 15 hand-unrolled derivative assignments -- so a transcription typo in any one line is caught,
89// not just the algebraic identity it relies on. The 15 (itau, idelta) derivatives are exactly the
90// pairs with itau + idelta <= 4.
91TEST_CASE("Cubic shared-intermediate all() is bit-exact vs 15 alphar() calls", "[cubic]") {
92 for (const char* backend : {"SRK", "PR"}) {
93 auto AS = std::shared_ptr<AbstractState>(AbstractState::factory(backend, "Methane"));
94 auto* cb = dynamic_cast<AbstractCubicBackend*>(AS.get());
95 REQUIRE(cb != nullptr);
96 auto& cubic = cb->get_cubic();
98 const std::vector<CoolPropDbl> z = {1.0};
99 for (int it_tau = 0; it_tau <= 10; ++it_tau) {
100 const double tau = 0.7 + 0.13 * it_tau;
101 for (int it_del = 0; it_del <= 10; ++it_del) {
102 const double delta = 0.2 + 0.27 * it_del;
103 // Production assembly under test (the optimized 15-derivative path).
104 HelmholtzDerivatives assembled = rh.all(*cb, z, tau, delta, /*cache_values=*/false);
105 for (std::size_t it = 0; it <= 4; ++it) {
106 for (std::size_t id = 0; id + it <= 4; ++id) {
107 const double direct = cubic->alphar(tau, delta, z, it, id);
108 CHECK(bits_of(assembled.get(it, id)) == bits_of(direct));
109 }
110 }
111 }
112 }
113 }
114}
115
116#endif