36#if defined(ENABLE_CATCH)
37# include <catch2/catch_all.hpp>
54std::vector<double> distinct_values(
const std::vector<double>& xs,
double rtol = 1e-12) {
55 std::vector<double> out;
56 out.reserve(xs.size());
59 for (
double u : out) {
60 const double scale = std::max(std::abs(u), 1.0);
61 if (std::abs(u - v) <= rtol * scale) {
66 if (!seen) out.push_back(v);
77 std::size_t N_l_used = 0;
78 std::size_t N_m_used = 0;
79 std::size_t unique_l = 0;
80 std::size_t unique_m = 0;
81 std::vector<double> ls;
82 std::vector<double> ms;
83 bool delta_li_in_u =
false;
84 bool tau_mi_in_u =
false;
91TermStats inspect_fluid(
const std::string& name) {
93 std::shared_ptr<CoolProp::AbstractState> AS;
100 if (be ==
nullptr)
return s;
101 auto& comps = be->get_components();
102 if (comps.empty())
return s;
103 auto& gen = comps[0].EOS().alphar.GenExp;
104 s.delta_li_in_u = gen.delta_li_in_u;
105 s.tau_mi_in_u = gen.tau_mi_in_u;
106 s.N = gen.elements.size();
108 std::vector<double> active_l, active_m;
109 active_l.reserve(s.N);
110 active_m.reserve(s.N);
111 for (std::size_t i = 0; i < s.N; ++i) {
112 const auto& el = gen.elements[i];
115 const bool l_active = s.delta_li_in_u && std::isfinite(
static_cast<double>(el.l_double)) &&
static_cast<double>(el.l_double) > 0.0
116 && std::abs(
static_cast<double>(el.c)) >
DBL_EPSILON;
118 const bool m_active = s.tau_mi_in_u && std::abs(
static_cast<double>(el.m_double)) > 0.0;
119 if (l_active) active_l.push_back(
static_cast<double>(el.l_double));
120 if (m_active) active_m.push_back(
static_cast<double>(el.m_double));
122 s.N_l_used = active_l.size();
123 s.N_m_used = active_m.size();
126 s.unique_l = distinct_values(active_l).size();
127 s.unique_m = distinct_values(active_m).size();
136double path_A(
const std::vector<double>& ls,
const std::vector<double>& ms,
double log_delta,
double log_tau) {
138 const std::size_t Nl = ls.size(), Nm = ms.size();
139 for (std::size_t i = 0; i < Nl; ++i)
140 acc += std::exp(ls[i] * log_delta);
141 for (std::size_t i = 0; i < Nm; ++i)
142 acc += std::exp(ms[i] * log_tau);
149double path_B(
const std::vector<double>& ls,
const std::vector<double>& ms,
const std::vector<double>& uniq_l,
const std::vector<double>& uniq_m,
150 double log_delta,
double log_tau) {
152 double cache_l[64], cache_m[64];
153 const std::size_t Ul = std::min<std::size_t>(uniq_l.size(), 64), Um = std::min<std::size_t>(uniq_m.size(), 64);
154 for (std::size_t k = 0; k < Ul; ++k)
155 cache_l[k] = std::exp(uniq_l[k] * log_delta);
156 for (std::size_t k = 0; k < Um; ++k)
157 cache_m[k] = std::exp(uniq_m[k] * log_tau);
160 const std::size_t Nl = ls.size(), Nm = ms.size();
161 for (std::size_t i = 0; i < Nl; ++i) {
162 const double li = ls[i];
163 for (std::size_t k = 0; k < Ul; ++k) {
164 if (uniq_l[k] == li) {
170 for (std::size_t i = 0; i < Nm; ++i) {
171 const double mi = ms[i];
172 for (std::size_t k = 0; k < Um; ++k) {
173 if (uniq_m[k] == mi) {
184 double mean_ns = 0.0;
185 double stddev_ns = 0.0;
190TrialStats bench(F&& fn, std::size_t reps, std::size_t n_trials) {
191 std::vector<double> ns_per_iter;
192 ns_per_iter.reserve(n_trials);
193 volatile double sink = 0.0;
194 for (std::size_t t = 0; t < n_trials; ++t) {
195 const auto t0 = std::chrono::steady_clock::now();
196 for (std::size_t r = 0; r < reps; ++r) {
199 const auto t1 = std::chrono::steady_clock::now();
200 const double dt_ns = std::chrono::duration<double, std::nano>(t1 - t0).count();
201 ns_per_iter.push_back(dt_ns /
static_cast<double>(reps));
205 for (
double x : ns_per_iter)
207 const double mean = sum /
static_cast<double>(n_trials);
209 for (
double x : ns_per_iter)
210 sq += (x - mean) * (x - mean);
211 const double stddev = (n_trials > 1) ? std::sqrt(sq /
static_cast<double>(n_trials - 1)) : 0.0;
212 return TrialStats{mean, stddev};
218bool eval_state(
const std::string& name,
double T,
double p,
double& log_tau,
double& log_delta) {
219 std::shared_ptr<CoolProp::AbstractState> AS;
226 const double tau = AS->tau(), delta = AS->delta();
227 if (!(tau > 0) || !(delta > 0))
return false;
228 log_tau = std::log(tau);
229 log_delta = std::log(delta);
235TEST_CASE(
"Tau-mi sweep: which fluids actually populate tau_mi_in_u with non-zero m_double",
"[term_cache_tau_sweep][.]") {
238 std::vector<std::string> all_fluids;
239 std::stringstream ss(fluids_csv);
241 while (std::getline(ss, item,
','))
242 all_fluids.push_back(item);
243 std::printf(
"\n=== tau_mi sweep: %zu fluids ===\n", all_fluids.size());
244 std::printf(
"%-30s %5s %8s %10s %8s %10s\n",
"fluid",
"N",
"N_l_used",
"unique_l",
"N_m_used",
"unique_m");
245 std::size_t with_m = 0;
246 for (
const auto& name : all_fluids) {
247 const TermStats s = inspect_fluid(name);
249 if (s.N_m_used == 0)
continue;
251 std::printf(
"%-30s %5zu %8zu %10zu %8zu %10zu\n", name.c_str(), s.N, s.N_l_used, s.unique_l, s.N_m_used, s.unique_m);
253 std::printf(
"--- %zu / %zu fluids have N_m_used > 0 ---\n", with_m, all_fluids.size());
256TEST_CASE(
"Term-cache feasibility profile for ResidualHelmholtzGeneralizedExponential::all",
"[term_cache_profile][.]") {
258 const std::vector<std::string> fluids = {
"Water",
"CarbonDioxide",
"n-Propane",
"R134a",
"Nitrogen",
"Methane",
259 "MM",
"Ammonia",
"Hydrogen",
"R32",
"R143a"};
261 std::printf(
"\n=== Part 1: residual-term-table structure (per fluid) ===\n");
262 std::printf(
"%-15s %5s %8s %10s %8s %10s %12s\n",
"fluid",
"N",
"N_l_used",
"unique_l",
"N_m_used",
"unique_m",
"savings_pow");
263 std::printf(
"%-15s %5s %8s %10s %8s %10s %12s\n",
"-----",
"-",
"--------",
"--------",
"--------",
"--------",
"-----------");
264 std::vector<std::string> skipped;
265 for (
const auto& name : fluids) {
266 const TermStats s = inspect_fluid(name);
268 skipped.push_back(name);
271 const std::size_t denom = s.N_l_used + s.N_m_used;
272 const double savings = (denom > 0) ?
static_cast<double>(denom - s.unique_l - s.unique_m) /
static_cast<double>(denom) : 0.0;
273 std::printf(
"%-15s %5zu %8zu %10zu %8zu %10zu %12.3f\n", name.c_str(), s.N, s.N_l_used, s.unique_l, s.N_m_used, s.unique_m, savings);
276 REQUIRE(s.unique_l <= s.N_l_used);
277 REQUIRE(s.unique_m <= s.N_m_used);
278 REQUIRE(savings >= 0.0);
279 REQUIRE(savings <= 1.0);
281 if (!skipped.empty()) {
282 std::printf(
"(skipped: ");
283 for (std::size_t i = 0; i < skipped.size(); ++i)
284 std::printf(
"%s%s", skipped[i].c_str(), i + 1 == skipped.size() ?
"" :
", ");
296 const std::vector<BenchFluid> benches = {
297 {
"Water", 400.0, 1.0e7},
298 {
"R134a", 350.0, 5.0e5},
299 {
"n-Propane", 300.0, 2.0e5}
301 const std::size_t reps = 1'000'000;
302 const std::size_t n_trials = 5;
304 std::printf(
"\n=== Part 2: microbench Path A (per-term exp) vs Path B (precompute+lookup) ===\n");
305 std::printf(
"(%zu reps x %zu trials, ns per loop iteration)\n", reps, n_trials);
306 std::printf(
"%-12s %5s %10s %10s %14s %14s %8s %14s\n",
"fluid",
"N",
"uniq_l",
"uniq_m",
"A mean (ns)",
"B mean (ns)",
"A/B",
"Delta x 25 ns");
307 std::printf(
"%-12s %5s %10s %10s %14s %14s %8s %14s\n",
"-----",
"-",
"------",
"------",
"-----------",
"-----------",
"---",
"-------------");
309 for (
const auto& b : benches) {
310 TermStats s = inspect_fluid(b.name);
311 if (!s.ok || (s.N_l_used == 0 && s.N_m_used == 0)) {
312 std::printf(
"%-12s [skipped — introspection failed or no l/m terms]\n", b.name.c_str());
315 double log_tau = 0.0, log_delta = 0.0;
316 if (!eval_state(b.name, b.T, b.p, log_tau, log_delta)) {
317 std::printf(
"%-12s [skipped — state (%g K, %g Pa) unreachable]\n", b.name.c_str(), b.T, b.p);
320 const std::vector<double> uniq_l = distinct_values(s.ls);
321 const std::vector<double> uniq_m = distinct_values(s.ms);
324 (void)path_A(s.ls, s.ms, log_delta, log_tau);
325 (void)path_B(s.ls, s.ms, uniq_l, uniq_m, log_delta, log_tau);
327 const TrialStats A = bench([&] {
return path_A(s.ls, s.ms, log_delta, log_tau); }, reps, n_trials);
328 const TrialStats B = bench([&] {
return path_B(s.ls, s.ms, uniq_l, uniq_m, log_delta, log_tau); }, reps, n_trials);
330 const double ratio = (B.mean_ns > 0) ? A.mean_ns / B.mean_ns : 0.0;
335 const double per_solve_ns = (A.mean_ns - B.mean_ns) * 25.0;
337 std::printf(
"%-12s %5zu %10zu %10zu %8.1f +-%3.1f %8.1f +-%3.1f %8.2f %14.1f\n", b.name.c_str(), s.N, uniq_l.size(), uniq_m.size(), A.mean_ns,
338 A.stddev_ns, B.mean_ns, B.stddev_ns, ratio, per_solve_ns);
341 std::printf(
"\nNote: Path A mimics 'exp(l*log_delta) + exp(m*log_tau) per term'.\n");
342 std::printf(
" Path B precomputes one exp per UNIQUE exponent + linear-search lookup per term.\n");
343 std::printf(
" If A/B >> 1 with low unique counts, optimization (E) is worth implementing.\n");