8#ifndef _CRT_SECURE_NO_WARNINGS
9#define _CRT_SECURE_NO_WARNINGS
14#include "boost/math/tools/toms748_solve.hpp"
25#if !defined(NO_TABULAR_BACKENDS)
39 std::map<backend_families, shared_ptr<AbstractStateGenerator>>
backends;
46 std::map<
backend_families, shared_ptr<AbstractStateGenerator>>::const_iterator& generator,
47 std::map<
backend_families, shared_ptr<AbstractStateGenerator>>::const_iterator& end) {
71 std::size_t lo = options_json.find_first_not_of(
" \t\r\n");
72 if (lo == std::string::npos) {
75 std::size_t hi = options_json.find_last_not_of(
" \t\r\n");
76 const std::string trimmed = options_json.substr(lo, hi - lo + 1);
77 if (trimmed ==
"{}") {
80 throw NotImplementedError(
"This backend does not accept factory-string options. Drop the '?<...>' suffix or opt the backend in (see "
81 "docs/superpowers/specs/2026-05-16-backend-options-string-design.md).");
88 if (fluid_names.size() == 1) {
89 const std::string& str = fluid_names[0];
90 if ((
upper(str) ==
"WATER") || (
upper(str) ==
"H2O")) {
93 throw ValueError(
format(
"The IF97 backend returns Water props only; fluid name [%s] not allowed", fluid_names[0].c_str()));
96 throw ValueError(
format(
"The IF97 backend does not support mixtures, only Water"));
131 if (fluid_names.size() != 1) {
132 throw ValueError(
format(
"For INCOMP backend, name vector must be one element long"));
184 std::string clean_backend = parsed.clean_string;
185 std::string options_json = parsed.options_json;
186 std::vector<std::string> clean_fluid_names =
fluid_names;
196 std::size_t fluid_options_index = clean_fluid_names.size();
197 for (std::size_t i = 0; i < clean_fluid_names.size(); ++i) {
198 if (clean_fluid_names[i].find(
'?') == std::string::npos) {
201 if (fluid_options_index != clean_fluid_names.size()) {
202 throw ValueError(
"factory: '?<options>' supplied in multiple fluid components; encode it once.");
204 fluid_options_index = i;
206 if (fluid_options_index != clean_fluid_names.size()) {
208 if (!parsed_fluid.options_json.empty() && !options_json.empty()) {
209 throw ValueError(
"factory: '?<options>' supplied on both the backend and fluid sides; pick one side.");
215 clean_fluid_names[fluid_options_index] = parsed_fluid.clean_string;
216 if (!parsed_fluid.options_json.empty()) {
217 options_json = parsed_fluid.options_json;
225 std::map<backend_families, shared_ptr<AbstractStateGenerator>>::const_iterator gen, end;
234 return gen->second->get_AbstractState(clean_fluid_names, options_json);
236#if !defined(NO_TABULAR_BACKENDS)
241 if (!options_json.empty()) {
244 const shared_ptr<AbstractState> AS(
factory(f2, clean_fluid_names));
247 if (!options_json.empty()) {
250 const shared_ptr<AbstractState> AS(
factory(f2, clean_fluid_names));
258 throw ValueError(
format(R
"(SVDSBTL requires an explicit source backend, e.g. factory("SVDSBTL&HEOS", "%s"))",
259 clean_fluid_names.empty() ? "<fluid>" : clean_fluid_names[0].c_str()));
261 if (clean_fluid_names.size() != 1) {
262 throw ValueError(
"SVDSBTL backend is pure-fluid only; expected exactly one fluid name");
267 return new SVDSBTLBackend(clean_fluid_names[0], f2, options_json);
270 else if (clean_backend ==
"?" || clean_backend.empty()) {
276 const std::size_t idel = clean_fluid_names[0].find(
"::");
277 const std::string suffix = options_json.empty() ? std::string{} : (
"?" + options_json);
278 if (idel == std::string::npos) {
280 return factory(
"HEOS" + suffix, clean_fluid_names);
282 std::string inner_backend = clean_fluid_names[0].substr(0, idel) + suffix;
283 std::vector<std::string> inner_fluid = {clean_fluid_names[0].substr(idel + 2)};
284 return factory(inner_backend, inner_fluid);
287 throw ValueError(
format(
"Invalid backend name [%s] to factory function", clean_backend.c_str()));
324 switch (input_pair) {
354 switch (input_pair) {
378 switch (input_pair) {
734 return -3.0 * (Ar01 - Ar11) / Ar20;
824 throw ValueError(
"Qmass requires a two-phase state (0 <= Q <= 1)");
840 throw NotImplementedError(
"calc_phase_molar_masses must be overridden by mixture backends");
877 throw ValueError(
"update_Qmass_pair called with non-Qmass pair");
879 const double Qmass_target = (m.qmass_slot == 1) ? v1 : v2;
880 const double partner = (m.qmass_slot == 1) ? v2 : v1;
882 if (Qmass_target < 0 || Qmass_target > 1) {
886 auto run_molar = [&](
double Qmolar) {
887 if (m.qmass_slot == 1)
888 update(m.molar, Qmolar, partner);
890 update(m.molar, partner, Qmolar);
894 if (Qmass_target == 0.0 || Qmass_target == 1.0) {
895 run_molar(Qmass_target);
900 auto residual = [&](
double Qmolar) ->
double {
908 constexpr double eps = 1e-12;
909 constexpr int bits = 48;
910 boost::math::uintmax_t max_iter = 50;
911 const double a = eps;
912 const double b = 1.0 - eps;
913 const double fa = residual(a);
914 const double fb = residual(b);
916 throw SolutionError(
format(
"update_Qmass_pair: cannot bracket Qmolar for Qmass=%g (fa=%g, fb=%g)", Qmass_target, fa, fb));
918 const auto [lo, hi] = boost::math::tools::toms748_solve(residual, a, b, fa, fb, boost::math::tools::eps_tolerance<double>(bits), max_iter);
919 const double Qmolar_solution = 0.5 * (lo + hi);
921 throw SolutionError(
format(
"update_Qmass_pair: did not converge for Qmass=%g", Qmass_target));
924 run_molar(Qmolar_solution);
1049 const double dTau_dT = 1 / dT_dtau;
1053 const double dDelta_drho = 1 / rhor;
1087 dT = R /
T * pow(tau, 2)
1096 dT -= ((R / rho * delta
1104 drho = R / rho * delta
1130 const double dbb_dTau =
1133 dT = 1.0 / 2.0 / w /
T
1137 - (2 * aa / bb * daa_dTau - pow(aa / bb, 2) * dbb_dTau)));
1139 const double daa_dDelta =
1145 - (2 * aa / bb * daa_dDelta - pow(aa / bb, 2) * dbb_dDelta));
1168 dT2 = 2 * Tr / pow(
T, 3);
1191 dT2 = R /
T * pow(tau, 2)
1195 drho_dT = R / rho * delta
1240 CoolPropDbl dOf_dT = NAN, dOf_drho = NAN, dWrt_dT = NAN, dWrt_drho = NAN, dConstant_dT = NAN, dConstant_drho = NAN;
1244 get_dT_drho(*
this, Constant, dConstant_dT, dConstant_drho);
1246 return (dOf_dT * dConstant_drho - dOf_drho * dConstant_dT) / (dWrt_dT * dConstant_drho - dWrt_drho * dConstant_dT);
1249 CoolPropDbl dOf1_dT = NAN, dOf1_drho = NAN, dWrt1_dT = NAN, dWrt1_drho = NAN, dConstant1_dT = NAN, dConstant1_drho = NAN, d2Of1_dT2 = NAN,
1250 d2Of1_drhodT = NAN, d2Of1_drho2 = NAN, d2Wrt1_dT2 = NAN, d2Wrt1_drhodT = NAN, d2Wrt1_drho2 = NAN, d2Constant1_dT2 = NAN,
1251 d2Constant1_drhodT = NAN, d2Constant1_drho2 = NAN, dWrt2_dT = NAN, dWrt2_drho = NAN, dConstant2_dT = NAN, dConstant2_drho = NAN,
1252 N = NAN, D = NAN, dNdrho__T = NAN, dDdrho__T = NAN, dNdT__rho = NAN, dDdT__rho = NAN, dderiv1_drho = NAN, dderiv1_dT = NAN,
1258 get_dT_drho(*
this, Constant1, dConstant1_dT, dConstant1_drho);
1265 get_dT_drho(*
this, Constant2, dConstant2_dT, dConstant2_drho);
1268 N = dOf1_dT * dConstant1_drho - dOf1_drho * dConstant1_dT;
1269 D = dWrt1_dT * dConstant1_drho - dWrt1_drho * dConstant1_dT;
1273 dNdrho__T = dOf1_dT * d2Constant1_drho2 + d2Of1_drhodT * dConstant1_drho - dOf1_drho * d2Constant1_drhodT - d2Of1_drho2 * dConstant1_dT;
1274 dDdrho__T = dWrt1_dT * d2Constant1_drho2 + d2Wrt1_drhodT * dConstant1_drho - dWrt1_drho * d2Constant1_drhodT - d2Wrt1_drho2 * dConstant1_dT;
1278 dNdT__rho = dOf1_dT * d2Constant1_drhodT + d2Of1_dT2 * dConstant1_drho - dOf1_drho * d2Constant1_dT2 - d2Of1_drhodT * dConstant1_dT;
1279 dDdT__rho = dWrt1_dT * d2Constant1_drhodT + d2Wrt1_dT2 * dConstant1_drho - dWrt1_drho * d2Constant1_dT2 - d2Wrt1_drhodT * dConstant1_dT;
1282 dderiv1_drho = (D * dNdrho__T - N * dDdrho__T) / pow(D, 2);
1285 dderiv1_dT = (D * dNdT__rho - N * dDdT__rho) / pow(D, 2);
1288 second = (dderiv1_dT * dConstant2_drho - dderiv1_drho * dConstant2_dT) / (dWrt2_dT * dConstant2_drho - dWrt2_drho * dConstant2_dT);
1306#include <catch2/catch_all.hpp>
1308TEST_CASE(
"Check AbstractState",
"[AbstractState]") {
1309 SECTION(
"bad backend") {
1312 SECTION(
"good backend - bad fluid") {
1315 SECTION(
"good backend - helmholtz") {
1318 SECTION(
"good backend - incomp") {
1325 SECTION(
"good backend - REFPROP") {
1329 }
catch (
const std::exception& e) {
1330 WARN(std::string(
"REFPROP backend unavailable. All tests requiring REFPROP will be skipped."));
1335TEST_CASE(
"Check derivatives in first_partial_deriv",
"[derivs_in_first_partial_deriv]") {
1342 double dT = 1e-3, drho = 1;
1350 CoolPropDbl dP_dT_num = (WaterplusT->p() - WaterminusT->p()) / (2 * dT);
1351 CoolPropDbl dP_drho_num = (Waterplusrho->p() - Waterminusrho->p()) / (2 * drho);
1353 CoolPropDbl dHmolar_dT_num = (WaterplusT->hmolar() - WaterminusT->hmolar()) / (2 * dT);
1354 CoolPropDbl dHmolar_drho_num = (Waterplusrho->hmolar() - Waterminusrho->hmolar()) / (2 * drho);
1355 CoolPropDbl dHmass_dT_num = (WaterplusT->hmass() - WaterminusT->hmass()) / (2 * dT);
1356 CoolPropDbl dHmass_drho_num = (Waterplusrho->hmass() - Waterminusrho->hmass()) / (2 * drho);
1358 CoolPropDbl dSmolar_dT_num = (WaterplusT->smolar() - WaterminusT->smolar()) / (2 * dT);
1359 CoolPropDbl dSmolar_drho_num = (Waterplusrho->smolar() - Waterminusrho->smolar()) / (2 * drho);
1360 CoolPropDbl dSmass_dT_num = (WaterplusT->smass() - WaterminusT->smass()) / (2 * dT);
1361 CoolPropDbl dSmass_drho_num = (Waterplusrho->smass() - Waterminusrho->smass()) / (2 * drho);
1363 CoolPropDbl dUmolar_dT_num = (WaterplusT->umolar() - WaterminusT->umolar()) / (2 * dT);
1364 CoolPropDbl dUmolar_drho_num = (Waterplusrho->umolar() - Waterminusrho->umolar()) / (2 * drho);
1365 CoolPropDbl dUmass_dT_num = (WaterplusT->umass() - WaterminusT->umass()) / (2 * dT);
1366 CoolPropDbl dUmass_drho_num = (Waterplusrho->umass() - Waterminusrho->umass()) / (2 * drho);
1368 CoolPropDbl dGmolar_dT_num = (WaterplusT->gibbsmolar() - WaterminusT->gibbsmolar()) / (2 * dT);
1369 CoolPropDbl dGmolar_drho_num = (Waterplusrho->gibbsmolar() - Waterminusrho->gibbsmolar()) / (2 * drho);
1370 CoolPropDbl dGmass_dT_num = (WaterplusT->gibbsmass() - WaterminusT->gibbsmass()) / (2 * dT);
1371 CoolPropDbl dGmass_drho_num = (Waterplusrho->gibbsmass() - Waterminusrho->gibbsmass()) / (2 * drho);
1373 CoolPropDbl dCvmolar_dT_num = (WaterplusT->cvmolar() - WaterminusT->cvmolar()) / (2 * dT);
1374 CoolPropDbl dCvmolar_drho_num = (Waterplusrho->cvmolar() - Waterminusrho->cvmolar()) / (2 * drho);
1375 CoolPropDbl dCvmass_dT_num = (WaterplusT->cvmass() - WaterminusT->cvmass()) / (2 * dT);
1376 CoolPropDbl dCvmass_drho_num = (Waterplusrho->cvmass() - Waterminusrho->cvmass()) / (2 * drho);
1378 CoolPropDbl dCpmolar_dT_num = (WaterplusT->cpmolar() - WaterminusT->cpmolar()) / (2 * dT);
1379 CoolPropDbl dCpmolar_drho_num = (Waterplusrho->cpmolar() - Waterminusrho->cpmolar()) / (2 * drho);
1380 CoolPropDbl dCpmass_dT_num = (WaterplusT->cpmass() - WaterminusT->cpmass()) / (2 * dT);
1381 CoolPropDbl dCpmass_drho_num = (Waterplusrho->cpmass() - Waterminusrho->cpmass()) / (2 * drho);
1383 CoolPropDbl dspeed_sound_dT_num = (WaterplusT->speed_sound() - WaterminusT->speed_sound()) / (2 * dT);
1384 CoolPropDbl dspeed_sound_drho_num = (Waterplusrho->speed_sound() - Waterminusrho->speed_sound()) / (2 * drho);
1390 CoolPropDbl dHmolar_dT_analyt, dHmolar_drho_analyt;
1395 CoolPropDbl dSmolar_dT_analyt, dSmolar_drho_analyt;
1400 CoolPropDbl dUmolar_dT_analyt, dUmolar_drho_analyt;
1405 CoolPropDbl dGmolar_dT_analyt, dGmolar_drho_analyt;
1410 CoolPropDbl dCvmolar_dT_analyt, dCvmolar_drho_analyt;
1412 CoolPropDbl dCvmass_dT_analyt, dCvmass_drho_analyt;
1415 CoolPropDbl dCpmolar_dT_analyt, dCpmolar_drho_analyt;
1417 CoolPropDbl dCpmass_dT_analyt, dCpmass_drho_analyt;
1420 CoolPropDbl dspeed_sound_dT_analyt, dspeed_sound_drho_analyt;
1425 CHECK(std::abs(dP_dT_analyt / dP_dT_num - 1) < eps);
1426 CHECK(std::abs(dP_drho_analyt / dP_drho_num - 1) < eps);
1428 CHECK(std::abs(dHmolar_dT_analyt / dHmolar_dT_num - 1) < eps);
1429 CHECK(std::abs(dHmolar_drho_analyt / dHmolar_drho_num - 1) < eps);
1430 CHECK(std::abs(dHmass_dT_analyt / dHmass_dT_num - 1) < eps);
1431 CHECK(std::abs(dHmass_drho_analyt / dHmass_drho_num - 1) < eps);
1433 CHECK(std::abs(dSmolar_dT_analyt / dSmolar_dT_num - 1) < eps);
1434 CHECK(std::abs(dSmolar_drho_analyt / dSmolar_drho_num - 1) < eps);
1435 CHECK(std::abs(dSmass_dT_analyt / dSmass_dT_num - 1) < eps);
1436 CHECK(std::abs(dSmass_drho_analyt / dSmass_drho_num - 1) < eps);
1438 CHECK(std::abs(dUmolar_dT_analyt / dUmolar_dT_num - 1) < eps);
1439 CHECK(std::abs(dUmolar_drho_analyt / dUmolar_drho_num - 1) < eps);
1440 CHECK(std::abs(dUmass_dT_analyt / dUmass_dT_num - 1) < eps);
1441 CHECK(std::abs(dUmass_drho_analyt / dUmass_drho_num - 1) < eps);
1443 CHECK(std::abs(dGmolar_dT_analyt / dGmolar_dT_num - 1) < eps);
1444 CHECK(std::abs(dGmolar_drho_analyt / dGmolar_drho_num - 1) < eps);
1445 CHECK(std::abs(dGmass_dT_analyt / dGmass_dT_num - 1) < eps);
1446 CHECK(std::abs(dGmass_drho_analyt / dGmass_drho_num - 1) < eps);
1448 CHECK(std::abs(dCvmolar_dT_analyt / dCvmolar_dT_num - 1) < eps);
1449 CHECK(std::abs(dCvmolar_drho_analyt / dCvmolar_drho_num - 1) < eps);
1450 CHECK(std::abs(dCvmass_dT_analyt / dCvmass_dT_num - 1) < eps);
1451 CHECK(std::abs(dCvmass_drho_analyt / dCvmass_drho_num - 1) < eps);
1453 CHECK(std::abs(dCpmolar_dT_analyt / dCpmolar_dT_num - 1) < eps);
1454 CHECK(std::abs(dCpmolar_drho_analyt / dCpmolar_drho_num - 1) < eps);
1455 CHECK(std::abs(dCpmass_dT_analyt / dCpmass_dT_num - 1) < eps);
1456 CHECK(std::abs(dCpmass_drho_analyt / dCpmass_drho_num - 1) < eps);
1458 CHECK(std::abs(dspeed_sound_dT_analyt / dspeed_sound_dT_num - 1) < eps);
1459 CHECK(std::abs(dspeed_sound_drho_analyt / dspeed_sound_drho_num - 1) < eps);