CoolProp 8.0.0
An open-source fluid property and humid air property database
CoolProp-Tests.cpp
Go to the documentation of this file.
1
2
5#include "../Backends/Helmholtz/HelmholtzEOSMixtureBackend.h"
6#include "../Backends/Helmholtz/HelmholtzEOSBackend.h"
7#include "../Backends/REFPROP/REFPROPMixtureBackend.h"
8#include "../Backends/Cubics/CubicBackend.h"
11#include <atomic>
12#include <map>
13#include <set>
14#include <thread>
15
16// ############################################
17// TESTS
18// ############################################
19
20#if defined(ENABLE_CATCH)
21
22# include <memory>
23# include <catch2/catch_all.hpp>
25# include "CoolProp/CoolProp.h"
27
28using namespace CoolProp;
29
30namespace TransportValidation {
31
32// A structure to hold the values for one validation call
33struct vel
34{
35 public:
36 std::string in1, in2, out, fluid;
37 double v1, v2, tol, expected;
38 vel(std::string fluid, std::string in1, double v1, std::string in2, double v2, const std::string& out, double expected, double tol)
39 : in1(in1), in2(in2), fluid(fluid), v1(v1), v2(v2), expected(expected), tol(tol) {
40
41 };
42};
43
44vel viscosity_validation_data[] = {
45 // From Vogel, JPCRD, 1998
46 vel("Propane", "T", 90, "Dmolar", 16.52e3, "V", 7388e-6, 1e-3),
47 vel("Propane", "T", 150, "Dmolar", 15.14e3, "V", 656.9e-6, 5e-3),
48 vel("Propane", "T", 600, "Dmolar", 10.03e3, "V", 73.92e-6, 5e-3),
49 vel("Propane", "T", 280, "Dmolar", 11.78e3, "V", 117.4e-6, 1e-3),
50
51 // Huber, FPE, 2004
52 vel("n-Octane", "T", 300, "Dmolar", 6177.2, "V", 553.60e-6, 1e-3),
53 vel("n-Nonane", "T", 300, "Dmolar", 5619.1, "V", 709.53e-6, 1e-3),
54 vel("n-Decane", "T", 300, "Dmolar", 5150.4, "V", 926.44e-6, 1e-3),
55
56 // Huber, Energy & Fuels, 2004
57 vel("n-Dodecane", "T", 300, "Dmolar", 4411.5, "V", 1484.8e-6, 1e-3),
58 vel("n-Dodecane", "T", 500, "Dmolar", 3444.7, "V", 183.76e-6, 1e-3),
59
60 // Huber, I&ECR, 2006
61 vel("R125", "T", 300, "Dmolar", 10596.9998, "V", 177.37e-6, 1e-3),
62 vel("R125", "T", 400, "Dmolar", 30.631, "V", 17.070e-6, 1e-3),
63
64 // From REFPROP 9.1 since Huber I&ECR 2003 does not provide validation data
65 vel("R134a", "T", 185, "Q", 0, "V", 0.0012698376398294414, 1e-3),
66 vel("R134a", "T", 185, "Q", 1, "V", 7.4290821400170869e-006, 1e-3),
67 vel("R134a", "T", 360, "Q", 0, "V", 7.8146319978982133e-005, 1e-3),
68 vel("R134a", "T", 360, "Q", 1, "V", 1.7140264998576107e-005, 1e-3),
69
70 // From REFPROP 9.1 since Kiselev, IECR, 2005 does not provide validation data
71 vel("Ethanol", "T", 300, "Q", 0, "V", 0.0010439017679191723, 1e-3),
72 vel("Ethanol", "T", 300, "Q", 1, "V", 8.8293820936046416e-006, 1e-3),
73 vel("Ethanol", "T", 500, "Q", 0, "V", 6.0979347125450671e-005, 1e-3),
74 vel("Ethanol", "T", 500, "Q", 1, "V", 1.7229157141572511e-005, 1e-3),
75
76 // From CoolProp v5 implementation of correlation - more or less agrees with REFPROP
77 // Errata in BibTeX File
78 vel("Hydrogen", "T", 35, "Dmass", 100, "V", 5.47889e-005, 1e-3),
79
80 // From Meng 2012 experimental data (note erratum in BibTeX file)
81 vel("DimethylEther", "T", 253.146, "Dmass", 734.28, "V", 0.20444e-3, 3e-3),
82 vel("DimethylEther", "T", 373.132, "Dmass", 613.78, "V", 0.09991e-3, 3e-3),
83
84 // From Fenghour, JPCRD, 1995
85 vel("Ammonia", "T", 200, "Dmolar", 3.9, "V", 6.95e-6, 1e-3),
86 vel("Ammonia", "T", 200, "Dmolar", 42754.4, "V", 507.28e-6, 1e-3),
87 vel("Ammonia", "T", 398, "Dmolar", 7044.7, "V", 17.67e-6, 1e-3),
88 vel("Ammonia", "T", 398, "Dmolar", 21066.7, "V", 43.95e-6, 1e-3),
89
90 // From Lemmon and Jacobsen, JPCRD, 2004
91 vel("Nitrogen", "T", 100, "Dmolar", 1e-14, "V", 6.90349e-6, 1e-3),
92 vel("Nitrogen", "T", 300, "Dmolar", 1e-14, "V", 17.8771e-6, 1e-3),
93 vel("Nitrogen", "T", 100, "Dmolar", 25000, "V", 79.7418e-6, 1e-3),
94 vel("Nitrogen", "T", 200, "Dmolar", 10000, "V", 21.0810e-6, 1e-3),
95 vel("Nitrogen", "T", 300, "Dmolar", 5000, "V", 20.7430e-6, 1e-3),
96 vel("Nitrogen", "T", 126.195, "Dmolar", 11180, "V", 18.2978e-6, 1e-3),
97 vel("Argon", "T", 100, "Dmolar", 1e-14, "V", 8.18940e-6, 1e-3),
98 vel("Argon", "T", 300, "Dmolar", 1e-14, "V", 22.7241e-6, 1e-3),
99 vel("Argon", "T", 100, "Dmolar", 33000, "V", 184.232e-6, 1e-3),
100 vel("Argon", "T", 200, "Dmolar", 10000, "V", 25.5662e-6, 1e-3),
101 vel("Argon", "T", 300, "Dmolar", 5000, "V", 26.3706e-6, 1e-3),
102 vel("Argon", "T", 150.69, "Dmolar", 13400, "V", 27.6101e-6, 1e-3),
103 vel("Oxygen", "T", 100, "Dmolar", 1e-14, "V", 7.70243e-6, 1e-3),
104 vel("Oxygen", "T", 300, "Dmolar", 1e-14, "V", 20.6307e-6, 1e-3),
105 vel("Oxygen", "T", 100, "Dmolar", 35000, "V", 172.136e-6, 1e-3),
106 vel("Oxygen", "T", 200, "Dmolar", 10000, "V", 22.4445e-6, 1e-3),
107 vel("Oxygen", "T", 300, "Dmolar", 5000, "V", 23.7577e-6, 1e-3),
108 vel("Oxygen", "T", 154.6, "Dmolar", 13600, "V", 24.7898e-6, 1e-3),
109 vel("Air", "T", 100, "Dmolar", 1e-14, "V", 7.09559e-6, 1e-3),
110 vel("Air", "T", 300, "Dmolar", 1e-14, "V", 18.5230e-6, 1e-3),
111 vel("Air", "T", 100, "Dmolar", 28000, "V", 107.923e-6, 1e-3),
112 vel("Air", "T", 200, "Dmolar", 10000, "V", 21.1392e-6, 1e-3),
113 vel("Air", "T", 300, "Dmolar", 5000, "V", 21.3241e-6, 1e-3),
114 vel("Air", "T", 132.64, "Dmolar", 10400, "V", 17.7623e-6, 1e-3),
115
116 // From Michailidou, JPCRD, 2013
117 vel("Hexane", "T", 250, "Dmass", 1e-14, "V", 5.2584e-6, 1e-3),
118 vel("Hexane", "T", 400, "Dmass", 1e-14, "V", 8.4149e-6, 1e-3),
119 vel("Hexane", "T", 550, "Dmass", 1e-14, "V", 11.442e-6, 1e-3),
120 vel("Hexane", "T", 250, "Dmass", 700, "V", 528.2e-6, 1e-3),
121 vel("Hexane", "T", 400, "Dmass", 600, "V", 177.62e-6, 1e-3),
122 vel("Hexane", "T", 550, "Dmass", 500, "V", 95.002e-6, 1e-3),
123
124 // From Assael, JPCRD, 2014
125 vel("Heptane", "T", 250, "Dmass", 1e-14, "V", 4.9717e-6, 1e-3),
126 vel("Heptane", "T", 400, "Dmass", 1e-14, "V", 7.8361e-6, 1e-3),
127 vel("Heptane", "T", 550, "Dmass", 1e-14, "V", 10.7394e-6, 1e-3),
128 vel("Heptane", "T", 250, "Dmass", 720, "V", 725.69e-6, 1e-3),
129 vel("Heptane", "T", 400, "Dmass", 600, "V", 175.94e-6, 1e-3),
130 vel("Heptane", "T", 550, "Dmass", 500, "V", 95.105e-6, 1e-3),
131
132 // From Laesecke, JPCRD, 1998: https://pmc.ncbi.nlm.nih.gov/articles/PMC5514612/pdf/nihms869002.pdf
133 vel("CO2", "T", 100, "Dmass", 1e-5, "V", 0.0053757e-3, 1e-4),
134 vel("CO2", "T", 2000, "Dmass", 1e-5, "V", 0.066079e-3, 1e-4),
135 vel("CO2", "T", 10000, "Dmass", 1e-5, "V", 0.17620e-3, 1e-4),
136 vel("CO2", "T", 220, "Dmass", 3, "V", 0.011104e-3, 1e-4),
137 vel("CO2", "T", 225, "Dmass", 1150, "V", 0.22218e-3, 1e-4),
138 vel("CO2", "T", 300, "Dmass", 65, "V", 0.015563e-3, 1e-4),
139 vel("CO2", "T", 300, "Dmass", 1400, "V", 0.50594e-3, 1e-4),
140 vel("CO2", "T", 700, "Dmass", 100, "V", 0.033112e-3, 1e-4),
141 vel("CO2", "T", 700, "Dmass", 1200, "V", 0.22980e-3, 1e-4),
142
143 // Tanaka, IJT, 1996
144 vel("R123", "T", 265, "Dmass", 1545.8, "V", 627.1e-6, 1e-3),
145 vel("R123", "T", 265, "Dmass", 1.614, "V", 9.534e-6, 1e-3),
146 vel("R123", "T", 415, "Dmass", 1079.4, "V", 121.3e-6, 1e-3),
147 vel("R123", "T", 415, "Dmass", 118.9, "V", 15.82e-6, 1e-3),
148
149 // Huber, JPCRD, 2008 and IAPWS
150 vel("Water", "T", 298.15, "Dmass", 998, "V", 889.735100e-6, 1e-7),
151 vel("Water", "T", 298.15, "Dmass", 1200, "V", 1437.649467e-6, 1e-7),
152 vel("Water", "T", 373.15, "Dmass", 1000, "V", 307.883622e-6, 1e-7),
153 vel("Water", "T", 433.15, "Dmass", 1, "V", 14.538324e-6, 1e-7),
154 vel("Water", "T", 433.15, "Dmass", 1000, "V", 217.685358e-6, 1e-7),
155 vel("Water", "T", 873.15, "Dmass", 1, "V", 32.619287e-6, 1e-7),
156 vel("Water", "T", 873.15, "Dmass", 100, "V", 35.802262e-6, 1e-7),
157 vel("Water", "T", 873.15, "Dmass", 600, "V", 77.430195e-6, 1e-7),
158 vel("Water", "T", 1173.15, "Dmass", 1, "V", 44.217245e-6, 1e-7),
159 vel("Water", "T", 1173.15, "Dmass", 100, "V", 47.640433e-6, 1e-7),
160 vel("Water", "T", 1173.15, "Dmass", 400, "V", 64.154608e-6, 1e-7),
161 vel("Water", "T", 647.35, "Dmass", 122, "V", 25.520677e-6, 1e-7),
162 vel("Water", "T", 647.35, "Dmass", 222, "V", 31.337589e-6, 1e-7),
163 vel("Water", "T", 647.35, "Dmass", 272, "V", 36.228143e-6, 1e-7),
164 vel("Water", "T", 647.35, "Dmass", 322, "V", 42.961579e-6, 1e-7),
165 vel("Water", "T", 647.35, "Dmass", 372, "V", 45.688204e-6, 1e-7),
166 vel("Water", "T", 647.35, "Dmass", 422, "V", 49.436256e-6, 1e-7),
167
168 // Quinones-Cisneros, JPCRD, 2012
169 vel("SF6", "T", 300, "Dmass", 1e-14, "V", 15.2887e-6, 1e-4),
170 vel("SF6", "T", 300, "Dmass", 5.92, "V", 15.3043e-6, 1e-4),
171 vel("SF6", "T", 300, "Dmass", 1345.1, "V", 117.417e-6, 1e-4),
172 vel("SF6", "T", 400, "Dmass", 1e-14, "V", 19.6796e-6, 1e-4),
173 vel("SF6", "T", 400, "Dmass", 278.47, "V", 24.4272e-6, 1e-4),
174 vel("SF6", "T", 400, "Dmass", 1123.8, "V", 84.7835e-6, 1e-4),
175
176 // Quinones-Cisneros, JCED, 2012, data from validation
177 vel("H2S", "T", 200, "P", 1000e5, "V", 0.000460287, 1e-3),
178 vel("H2S", "T", 200, "P", 0.251702e5, "V", 8.02322E-06, 1e-3),
179 vel("H2S", "T", 596.961, "P", 1000e5, "V", 6.94741E-05, 1e-3),
180 vel("H2S", "T", 596.961, "P", 1e5, "V", 2.38654E-05, 1e-3),
181
182 // Geller, Purdue Conference, 2000
183 //vel("R410A", "T", 243.15, "Q", 0, "V", 238.61e-6, 5e-2),
184 //vel("R410A", "T", 243.15, "Q", 1, "V", 10.37e-6, 5e-2),
185 //vel("R410A", "T", 333.15, "Q", 0, "V", 70.71e-6, 5e-2),
186 //vel("R410A", "T", 333.15, "Q", 1, "V", 19.19e-6, 5e-2),
187 //vel("R407C", "T", 243.15, "Q", 0, "V", 304.18e-6, 1e-2),
188 //vel("R407C", "T", 243.15, "Q", 1, "V", 9.83e-6, 1e-2),
189 //vel("R407C", "T", 333.15, "Q", 0, "V", 95.96e-6, 1e-2),
190 //vel("R407C", "T", 333.15, "Q", 1, "V", 16.38e-6, 1e-2),
191 //vel("R404A", "T", 243.15, "Q", 0, "V", 264.67e-6, 1e-2),
192 //vel("R404A", "T", 243.15, "Q", 1, "V", 10.13e-6, 1e-2),
193 //vel("R404A", "T", 333.15, "Q", 0, "V", 73.92e-6, 1e-2),
194 //vel("R404A", "T", 333.15, "Q", 1, "V", 18.56e-6, 1e-2),
195 //vel("R507A", "T", 243.15, "Q", 0, "V", 284.59e-6, 3e-2),
196 //vel("R507A", "T", 243.15, "Q", 1, "V", 9.83e-6, 1e-2),
197 //vel("R507A", "T", 333.15, "Q", 0, "V", 74.37e-6, 1e-2),
198 //vel("R507A", "T", 333.15, "Q", 1, "V", 19.35e-6, 1e-2),
199
200 // From Arp, NIST, 1998
201 vel("Helium", "T", 3.6, "P", 0.180e6, "V", 3.745e-6, 1e-2),
202 vel("Helium", "T", 50, "P", 0.180e6, "V", 6.376e-6, 1e-2),
203 vel("Helium", "T", 400, "P", 0.180e6, "V", 24.29e-6, 1e-2),
204
205 // From Shan, ASHRAE, 2000
206 vel("R23", "T", 180, "Dmolar", 21097, "V", 353.88e-6, 1e-4),
207 vel("R23", "T", 420, "Dmolar", 7564, "V", 39.459e-6, 1e-4),
208 vel("R23", "T", 370, "Dmolar", 32.62, "V", 18.213e-6, 1e-4),
209
210 // From Friend, JPCRD, 1991
211 vel("Ethane", "T", 100, "Dmolar", 21330, "V", 878.6e-6, 1e-2),
212 vel("Ethane", "T", 430, "Dmolar", 12780, "V", 58.70e-6, 1e-2),
213 vel("Ethane", "T", 500, "Dmolar", 11210, "V", 48.34e-6, 1e-2),
214
215 // From Xiang, JPCRD, 2006
216 vel("Methanol", "T", 300, "Dmass", 0.12955, "V", 0.009696e-3, 1e-3),
217 vel("Methanol", "T", 300, "Dmass", 788.41, "V", 0.5422e-3, 1e-3),
218 vel("Methanol", "T", 630, "Dmass", 0.061183, "V", 0.02081e-3, 1e-3),
219 vel("Methanol", "T", 630, "Dmass", 888.50, "V", 0.2405e-3, 1e-1), // They use a different EOS in the high pressure region
220
221 // From REFPROP 9.1 since no data provided
222 vel("n-Butane", "T", 150, "Q", 0, "V", 0.0013697657668, 1e-4),
223 vel("n-Butane", "T", 400, "Q", 1, "V", 1.2027464524762453e-005, 1e-4),
224 vel("IsoButane", "T", 120, "Q", 0, "V", 0.0060558450757844271, 1e-4),
225 vel("IsoButane", "T", 400, "Q", 1, "V", 1.4761041187617117e-005, 2e-4),
226 vel("R134a", "T", 175, "Q", 0, "V", 0.0017558494524138289, 1e-4),
227 vel("R134a", "T", 360, "Q", 1, "V", 1.7140264998576107e-005, 1e-4),
228
229 // From Tariq, JPCRD, 2014
230 vel("Cyclohexane", "T", 300, "Dmolar", 1e-10, "V", 7.058e-6, 1e-4),
231 vel("Cyclohexane", "T", 300, "Dmolar", 0.0430e3, "V", 6.977e-6, 1e-4),
232 vel("Cyclohexane", "T", 300, "Dmolar", 9.1756e3, "V", 863.66e-6, 1e-4),
233 vel("Cyclohexane", "T", 300, "Dmolar", 9.9508e3, "V", 2850.18e-6, 1e-4),
234 vel("Cyclohexane", "T", 500, "Dmolar", 1e-10, "V", 11.189e-6, 1e-4),
235 vel("Cyclohexane", "T", 500, "Dmolar", 6.0213e3, "V", 94.842e-6, 1e-4),
236 vel("Cyclohexane", "T", 500, "Dmolar", 8.5915e3, "V", 380.04e-6, 1e-4),
237 vel("Cyclohexane", "T", 700, "Dmolar", 1e-10, "V", 15.093e-6, 1e-4),
238 vel("Cyclohexane", "T", 700, "Dmolar", 7.4765e3, "V", 176.749e-6, 1e-4),
239
240 // From Avgeri, JPCRD, 2014
241 vel("Benzene", "T", 300, "Dmass", 1e-10, "V", 7.625e-6, 1e-4),
242 vel("Benzene", "T", 400, "Dmass", 1e-10, "V", 10.102e-6, 1e-4),
243 vel("Benzene", "T", 550, "Dmass", 1e-10, "V", 13.790e-6, 1e-4),
244 vel("Benzene", "T", 300, "Dmass", 875, "V", 608.52e-6, 1e-4),
245 vel("Benzene", "T", 400, "Dmass", 760, "V", 211.74e-6, 1e-4),
246 vel("Benzene", "T", 550, "Dmass", 500, "V", 60.511e-6, 1e-4),
247
248 // From Cao, JPCRD, 2016
249 vel("m-Xylene", "T", 300, "Dmolar", 1e-10, "V", 6.637e-6, 1e-4),
250 vel("m-Xylene", "T", 300, "Dmolar", 0.04 * 1e3, "V", 6.564e-6, 1e-4),
251 vel("m-Xylene", "T", 300, "Dmolar", 8.0849 * 1e3, "V", 569.680e-6, 1e-4),
252 vel("m-Xylene", "T", 300, "Dmolar", 8.9421 * 1e3, "V", 1898.841e-6, 1e-4),
253 vel("m-Xylene", "T", 400, "Dmolar", 1e-10, "V", 8.616e-6, 1e-4),
254 vel("m-Xylene", "T", 400, "Dmolar", 0.04 * 1e3, "V", 8.585e-6, 1e-4),
255 vel("m-Xylene", "T", 400, "Dmolar", 7.2282 * 1e3, "V", 238.785e-6, 1e-4),
256 vel("m-Xylene", "T", 400, "Dmolar", 8.4734 * 1e3, "V", 718.950e-6, 1e-4),
257 vel("m-Xylene", "T", 600, "Dmolar", 1e-10, "V", 12.841e-6, 1e-4),
258 vel("m-Xylene", "T", 600, "Dmolar", 0.04 * 1e3, "V", 12.936e-6, 1e-4),
259 vel("m-Xylene", "T", 600, "Dmolar", 7.6591 * 1e3, "V", 299.164e-6, 1e-4),
260
261 // From Cao, JPCRD, 2016
262 vel("o-Xylene", "T", 300, "Dmolar", 1e-10, "V", 6.670e-6, 1e-4),
263 vel("o-Xylene", "T", 300, "Dmolar", 0.04 * 1e3, "V", 6.598e-6, 1e-4),
264 vel("o-Xylene", "T", 300, "Dmolar", 8.2369 * 1e3, "V", 738.286e-6, 1e-4),
265 vel("o-Xylene", "T", 300, "Dmolar", 8.7845 * 1e3, "V", 1645.436e-6, 1e-4),
266 vel("o-Xylene", "T", 400, "Dmolar", 1e-10, "V", 8.658e-6, 1e-4),
267 vel("o-Xylene", "T", 400, "Dmolar", 0.04 * 1e3, "V", 8.634e-6, 1e-4),
268 vel("o-Xylene", "T", 400, "Dmolar", 7.4060 * 1e3, "V", 279.954e-6, 1e-4),
269 vel("o-Xylene", "T", 400, "Dmolar", 8.2291 * 1e3, "V", 595.652e-6, 1e-4),
270 vel("o-Xylene", "T", 600, "Dmolar", 1e-10, "V", 12.904e-6, 1e-4),
271 vel("o-Xylene", "T", 600, "Dmolar", 0.04 * 1e3, "V", 13.018e-6, 1e-4),
272 vel("o-Xylene", "T", 600, "Dmolar", 7.2408 * 1e3, "V", 253.530e-6, 1e-4),
273
274 // From Balogun, JPCRD, 2016
275 vel("p-Xylene", "T", 300, "Dmolar", 1e-10, "V", 6.604e-6, 1e-4),
276 vel("p-Xylene", "T", 300, "Dmolar", 0.049 * 1e3, "V", 6.405e-6, 1e-4),
277 vel("p-Xylene", "T", 300, "Dmolar", 8.0548 * 1e3, "V", 593.272e-6, 1e-4),
278 vel("p-Xylene", "T", 300, "Dmolar", 8.6309 * 1e3, "V", 1266.337e-6, 1e-4),
279 vel("p-Xylene", "T", 400, "Dmolar", 1e-10, "V", 8.573e-6, 1e-4),
280 vel("p-Xylene", "T", 400, "Dmolar", 7.1995 * 1e3, "V", 239.202e-6, 1e-4),
281 vel("p-Xylene", "T", 400, "Dmolar", 8.0735 * 1e3, "V", 484.512e-6, 1e-4),
282 vel("p-Xylene", "T", 600, "Dmolar", 1e-10, "V", 12.777e-6, 1e-4),
283 vel("p-Xylene", "T", 600, "Dmolar", 7.0985 * 1e3, "V", 209.151e-6, 1e-4),
284
285 // From Mylona, JPCRD, 2014
286 vel("EthylBenzene", "T", 617, "Dmass", 316, "V", 33.22e-6, 1e-2),
287
288 // Heavy Water, IAPWS formulation
289 vel("HeavyWater", "T", 0.5000 * 643.847, "Dmass", 3.07 * 358, "V", 12.0604912273 * 55.2651e-6, 1e-5),
290 vel("HeavyWater", "T", 0.9000 * 643.847, "Dmass", 2.16 * 358, "V", 1.6561616211 * 55.2651e-6, 1e-5),
291 vel("HeavyWater", "T", 1.2000 * 643.847, "Dmass", 0.8 * 358, "V", 0.7651099154 * 55.2651e-6, 1e-5),
292
293 // Toluene, Avgeri, JPCRD, 2015
294 vel("Toluene", "T", 300, "Dmass", 1e-10, "V", 7.023e-6, 1e-4),
295 vel("Toluene", "T", 400, "Dmass", 1e-10, "V", 9.243e-6, 1e-4),
296 vel("Toluene", "T", 550, "Dmass", 1e-10, "V", 12.607e-6, 1e-4),
297 vel("Toluene", "T", 300, "Dmass", 865, "V", 566.78e-6, 1e-4),
298 vel("Toluene", "T", 400, "Dmass", 770, "V", 232.75e-6, 1e-4),
299 vel("Toluene", "T", 550, "Dmass", 550, "V", 80.267e-6, 1e-4),
300
301};
302
303class TransportValidationFixture
304{
305 protected:
306 CoolPropDbl actual, x1, x2;
307 shared_ptr<CoolProp::AbstractState> pState;
309
310 public:
311 TransportValidationFixture() = default;
312 ~TransportValidationFixture() = default;
313 void set_backend(const std::string& backend, const std::string& fluid_name) {
314 pState.reset(CoolProp::AbstractState::factory(backend, fluid_name));
315 }
316 void set_pair(std::string& in1, double v1, std::string& in2, double v2) {
317 double o1, o2;
320 CoolProp::input_pairs pair = CoolProp::generate_update_pair(iin1, v1, iin2, v2, o1, o2);
321 pState->update(pair, o1, o2);
322 }
323 void get_value(parameters key) {
324 actual = pState->keyed_output(key);
325 }
326};
327
328TEST_CASE_METHOD(TransportValidationFixture, "Compare viscosities against published data", "[viscosity],[transport]") {
329 int inputsN = sizeof(viscosity_validation_data) / sizeof(viscosity_validation_data[0]);
330 for (int i = 0; i < inputsN; ++i) {
331 vel el = viscosity_validation_data[i];
332 CHECK_NOTHROW(set_backend("HEOS", el.fluid));
333
334 CAPTURE(el.fluid);
335 CAPTURE(el.in1);
336 CAPTURE(el.v1);
337 CAPTURE(el.in2);
338 CAPTURE(el.v2);
339 CHECK_NOTHROW(set_pair(el.in1, el.v1, el.in2, el.v2));
340 CHECK_NOTHROW(get_value(CoolProp::iviscosity));
341 CAPTURE(el.expected);
342 CAPTURE(actual);
343 CHECK(std::abs(actual / el.expected - 1) < el.tol);
344 }
345}
346
347vel conductivity_validation_data[] = {
349
350 // From Assael, JPCRD, 2013
351 vel("Hexane", "T", 250, "Dmass", 700, "L", 137.62e-3, 1e-4),
352 vel("Hexane", "T", 400, "Dmass", 2, "L", 23.558e-3, 1e-4),
353 vel("Hexane", "T", 400, "Dmass", 650, "L", 129.28e-3, 3e-4),
354 vel("Hexane", "T", 510, "Dmass", 2, "L", 36.772e-3, 1e-4),
355
356 // From Assael, JPCRD, 2013
357 vel("Heptane", "T", 250, "Dmass", 720, "L", 137.09e-3, 1e-4),
358 vel("Heptane", "T", 400, "Dmass", 2, "L", 21.794e-3, 1e-4),
359 vel("Heptane", "T", 400, "Dmass", 650, "L", 120.75e-3, 1e-4),
360 vel("Heptane", "T", 535, "Dmass", 100, "L", 51.655e-3, 3e-3), // Relaxed tolerance because conductivity was fit using older viscosity correlation
361
362 // From Assael, JPCRD, 2013
363 vel("Ethanol", "T", 300, "Dmass", 850, "L", 209.68e-3, 1e-4),
364 vel("Ethanol", "T", 400, "Dmass", 2, "L", 26.108e-3, 1e-4),
365 vel("Ethanol", "T", 400, "Dmass", 690, "L", 149.21e-3, 1e-4),
366 vel("Ethanol", "T", 500, "Dmass", 10, "L", 39.594e-3, 1e-4),
367
369 //vel("Toluene", "T", 298.15, "Dmass", 1e-15, "L", 10.749e-3, 1e-4),
370 //vel("Toluene", "T", 298.15, "Dmass", 862.948, "L", 130.66e-3, 1e-4),
371 //vel("Toluene", "T", 298.15, "Dmass", 876.804, "L", 136.70e-3, 1e-4),
372 //vel("Toluene", "T", 595, "Dmass", 1e-15, "L", 40.538e-3, 1e-4),
373 //vel("Toluene", "T", 595, "Dmass", 46.512, "L", 41.549e-3, 1e-4),
374 //vel("Toluene", "T", 185, "Dmass", 1e-15, "L", 4.3758e-3, 1e-4),
375 //vel("Toluene", "T", 185, "Dmass", 968.821, "L", 158.24e-3, 1e-4),
376
377 // From Assael, JPCRD, 2012
378 vel("SF6", "T", 298.15, "Dmass", 1e-13, "L", 12.952e-3, 1e-4),
379 vel("SF6", "T", 298.15, "Dmass", 100, "L", 14.126e-3, 1e-4),
380 vel("SF6", "T", 298.15, "Dmass", 1600, "L", 69.729e-3, 1e-4),
381 vel("SF6", "T", 310, "Dmass", 1e-13, "L", 13.834e-3, 1e-4),
382 vel("SF6", "T", 310, "Dmass", 1200, "L", 48.705e-3, 1e-4),
383 vel("SF6", "T", 480, "Dmass", 100, "L", 28.847e-3, 1e-4),
384
386 //vel("Benzene", "T", 290, "Dmass", 890, "L", 147.66e-3, 1e-4),
387 //vel("Benzene", "T", 500, "Dmass", 2, "L", 30.174e-3, 1e-4),
388 //vel("Benzene", "T", 500, "Dmass", 32, "L", 32.175e-3, 1e-4),
389 //vel("Benzene", "T", 500, "Dmass", 800, "L", 141.24e-3, 1e-4),
390 //vel("Benzene", "T", 575, "Dmass", 1.7, "L", 37.763e-3, 1e-4),
391
392 // From Assael, JPCRD, 2011
393 vel("Hydrogen", "T", 298.15, "Dmass", 1e-13, "L", 185.67e-3, 1e-4),
394 vel("Hydrogen", "T", 298.15, "Dmass", 0.80844, "L", 186.97e-3, 1e-4),
395 vel("Hydrogen", "T", 298.15, "Dmass", 14.4813, "L", 201.35e-3, 1e-4),
396 vel("Hydrogen", "T", 35, "Dmass", 1e-13, "L", 26.988e-3, 1e-4),
397 vel("Hydrogen", "T", 35, "Dmass", 30, "L", 0.0770177, 1e-4), // Updated since Assael uses a different viscosity correlation
398 vel("Hydrogen", "T", 18, "Dmass", 1e-13, "L", 13.875e-3, 1e-4),
399 vel("Hydrogen", "T", 18, "Dmass", 75, "L", 104.48e-3, 1e-4),
400 /*vel("ParaHydrogen", "T", 298.15, "Dmass", 1e-13, "L", 192.38e-3, 1e-4),
401vel("ParaHydrogen", "T", 298.15, "Dmass", 0.80844, "L", 192.81e-3, 1e-4),
402vel("ParaHydrogen", "T", 298.15, "Dmass", 14.4813, "L", 207.85e-3, 1e-4),
403vel("ParaHydrogen", "T", 35, "Dmass", 1e-13, "L", 27.222e-3, 1e-4),
404vel("ParaHydrogen", "T", 35, "Dmass", 30, "L", 70.335e-3, 1e-4),
405vel("ParaHydrogen", "T", 18, "Dmass", 1e-13, "L", 13.643e-3, 1e-4),
406vel("ParaHydrogen", "T", 18, "Dmass", 75, "L", 100.52e-3, 1e-4),*/
407
408 // Some of these don't work
409 vel("R125", "T", 341, "Dmass", 600, "L", 0.0565642978494, 2e-4),
410 vel("R125", "T", 200, "Dmass", 1e-13, "L", 0.007036843623086, 2e-4),
411 vel("IsoButane", "T", 390, "Dmass", 387.09520158645068, "L", 0.063039, 2e-4),
412 vel("IsoButane", "T", 390, "Dmass", 85.76703973869482, "L", 0.036603, 2e-4),
413 vel("n-Butane", "T", 415, "Dmass", 360.01895129934866, "L", 0.067045, 2e-4),
414 vel("n-Butane", "T", 415, "Dmass", 110.3113177144, "L", 0.044449, 1e-4),
415
416 // From Huber, FPE, 2005
417 vel("n-Octane", "T", 300, "Dmolar", 6177.2, "L", 0.12836, 1e-4),
418 vel("n-Nonane", "T", 300, "Dmolar", 5619.4, "L", 0.13031, 1e-4),
419 //vel("n-Decane", "T", 300, "Dmass", 5150.4, "L", 0.13280, 1e-4), // no viscosity
420
421 // From Huber, EF, 2004
422 vel("n-Dodecane", "T", 300, "Dmolar", 4411.5, "L", 0.13829, 1e-4),
423 vel("n-Dodecane", "T", 500, "Dmolar", 3444.7, "L", 0.09384, 1e-4),
424 vel("n-Dodecane", "T", 660, "Dmolar", 1500.98, "L", 0.090346, 1e-4),
425
426 // From REFPROP 9.1 since no data provided in Marsh, 2002
427 vel("n-Propane", "T", 368, "Q", 0, "L", 0.07282154952457, 1e-3),
428 vel("n-Propane", "T", 368, "Dmolar", 1e-10, "L", 0.0266135388745317, 1e-4),
429
430 // From Perkins, JCED, 2011
431 //vel("R1234yf", "T", 250, "Dmass", 2.80006, "L", 0.0098481, 1e-4),
432 //vel("R1234yf", "T", 300, "Dmass", 4.671556, "L", 0.013996, 1e-4),
433 //vel("R1234yf", "T", 250, "Dmass", 1299.50, "L", 0.088574, 1e-4),
434 //vel("R1234yf", "T", 300, "Dmass", 1182.05, "L", 0.075245, 1e-4),
435 //vel("R1234ze(E)", "T", 250, "Dmass", 2.80451, "L", 0.0098503, 1e-4),
436 //vel("R1234ze(E)", "T", 300, "Dmass", 4.67948, "L", 0.013933, 1e-4),
437 //vel("R1234ze(E)", "T", 250, "Dmass", 1349.37, "L", 0.10066, 1e-4),
438 //vel("R1234ze(E)", "T", 300, "Dmass", 1233.82, "L", 0.085389, 1e-4),
439
440 // From Laesecke, IJR 1995
441 vel("R123", "T", 180, "Dmass", 1739, "L", 110.9e-3, 2e-4),
442 vel("R123", "T", 180, "Dmass", 0.2873e-2, "L", 2.473e-3, 1e-3),
443 vel("R123", "T", 430, "Dmass", 996.35, "L", 45.62e-3, 1e-3),
444 vel("R123", "T", 430, "Dmass", 166.9, "L", 21.03e-3, 1e-3),
445
446 // From Huber, JPCRD, 2016
447 vel("CO2", "T", 250.0, "Dmass", 1e-6, "L", 12.99e-3, 1e-3),
448 vel("CO2", "T", 250.0, "Dmass", 2.0, "L", 13.05e-3, 1e-3),
449 vel("CO2", "T", 250.0, "Dmass", 1058.0, "L", 140.00e-3, 1e-4),
450 vel("CO2", "T", 310.0, "Dmass", 400.0, "L", 73.04e-3, 1e-4),
451
452 // From Friend, JPCRD, 1991
453 vel("Ethane", "T", 100, "Dmass", 1e-13, "L", 3.46e-3, 1e-2),
454 vel("Ethane", "T", 230, "Dmolar", 16020, "L", 126.2e-3, 1e-2),
455 vel("Ethane", "T", 440, "Dmolar", 1520, "L", 45.9e-3, 1e-2),
456 vel("Ethane", "T", 310, "Dmolar", 4130, "L", 45.4e-3, 1e-2),
457
458 // From Lemmon and Jacobsen, JPCRD, 2004
459 vel("Nitrogen", "T", 100, "Dmolar", 1e-14, "L", 9.27749e-3, 1e-4),
460 vel("Nitrogen", "T", 300, "Dmolar", 1e-14, "L", 25.9361e-3, 1e-4),
461 vel("Nitrogen", "T", 100, "Dmolar", 25000, "L", 103.834e-3, 1e-4),
462 vel("Nitrogen", "T", 200, "Dmolar", 10000, "L", 36.0099e-3, 1e-4),
463 vel("Nitrogen", "T", 300, "Dmolar", 5000, "L", 32.7694e-3, 1e-4),
464 vel("Nitrogen", "T", 126.195, "Dmolar", 11180, "L", 675.800e-3, 1e-4),
465 vel("Argon", "T", 100, "Dmolar", 1e-14, "L", 6.36587e-3, 1e-4),
466 vel("Argon", "T", 300, "Dmolar", 1e-14, "L", 17.8042e-3, 1e-4),
467 vel("Argon", "T", 100, "Dmolar", 33000, "L", 111.266e-3, 1e-4),
468 vel("Argon", "T", 200, "Dmolar", 10000, "L", 26.1377e-3, 1e-4),
469 vel("Argon", "T", 300, "Dmolar", 5000, "L", 23.2302e-3, 1e-4),
470 vel("Argon", "T", 150.69, "Dmolar", 13400, "L", 856.793e-3, 1e-4),
471 vel("Oxygen", "T", 100, "Dmolar", 1e-14, "L", 8.94334e-3, 1e-4),
472 vel("Oxygen", "T", 300, "Dmolar", 1e-14, "L", 26.4403e-3, 1e-4),
473 vel("Oxygen", "T", 100, "Dmolar", 35000, "L", 146.044e-3, 1e-4),
474 vel("Oxygen", "T", 200, "Dmolar", 10000, "L", 34.6124e-3, 1e-4),
475 vel("Oxygen", "T", 300, "Dmolar", 5000, "L", 32.5491e-3, 1e-4),
476 vel("Oxygen", "T", 154.6, "Dmolar", 13600, "L", 377.476e-3, 1e-4),
477 vel("Air", "T", 100, "Dmolar", 1e-14, "L", 9.35902e-3, 1e-4),
478 vel("Air", "T", 300, "Dmolar", 1e-14, "L", 26.3529e-3, 1e-4),
479 vel("Air", "T", 100, "Dmolar", 28000, "L", 119.221e-3, 1e-4),
480 vel("Air", "T", 200, "Dmolar", 10000, "L", 35.3185e-3, 1e-4),
481 vel("Air", "T", 300, "Dmolar", 5000, "L", 32.6062e-3, 1e-4),
482 vel("Air", "T", 132.64, "Dmolar", 10400, "L", 75.6231e-3, 1e-4),
483
484 // Huber, JPCRD, 2012
485 vel("Water", "T", 298.15, "Dmass", 1e-14, "L", 18.4341883e-3, 1e-6),
486 vel("Water", "T", 298.15, "Dmass", 998, "L", 607.712868e-3, 1e-6),
487 vel("Water", "T", 298.15, "Dmass", 1200, "L", 799.038144e-3, 1e-6),
488 vel("Water", "T", 873.15, "Dmass", 1e-14, "L", 79.1034659e-3, 1e-6),
489 vel("Water", "T", 647.35, "Dmass", 1, "L", 51.9298924e-3, 1e-6),
490 vel("Water", "T", 647.35, "Dmass", 122, "L", 130.922885e-3, 2e-4),
491 vel("Water", "T", 647.35, "Dmass", 222, "L", 367.787459e-3, 2e-4),
492 vel("Water", "T", 647.35, "Dmass", 272, "L", 757.959776e-3, 2e-4),
493 vel("Water", "T", 647.35, "Dmass", 322, "L", 1443.75556e-3, 2e-4),
494 vel("Water", "T", 647.35, "Dmass", 372, "L", 650.319402e-3, 2e-4),
495 vel("Water", "T", 647.35, "Dmass", 422, "L", 448.883487e-3, 2e-4),
496 vel("Water", "T", 647.35, "Dmass", 750, "L", 600.961346e-3, 2e-4),
497
498 // From Shan, ASHRAE, 2000
499 vel("R23", "T", 180, "Dmolar", 21097, "L", 143.19e-3, 1e-4),
500 vel("R23", "T", 420, "Dmolar", 7564, "L", 50.19e-3, 2e-4),
501 vel("R23", "T", 370, "Dmolar", 32.62, "L", 17.455e-3, 1e-4),
502
503 // From REFPROP 9.1 since no sample data provided in Tufeu
504 vel("Ammonia", "T", 310, "Dmolar", 34320, "L", 0.45223303481784971, 1e-4),
505 vel("Ammonia", "T", 395, "Q", 0, "L", 0.2264480769301, 2e-3),
506
507 // From Hands, Cryogenics, 1981
508 vel("Helium", "T", 800, "P", 1e5, "L", 0.3085, 1e-2),
509 vel("Helium", "T", 300, "P", 1e5, "L", 0.1560, 1e-2),
510 vel("Helium", "T", 20, "P", 1e5, "L", 0.0262, 1e-2),
511 vel("Helium", "T", 8, "P", 1e5, "L", 0.0145, 1e-2),
512 vel("Helium", "T", 4, "P", 20e5, "L", 0.0255, 1e-2),
513 vel("Helium", "T", 8, "P", 20e5, "L", 0.0308, 1e-2),
514 vel("Helium", "T", 20, "P", 20e5, "L", 0.0328, 1e-2),
515 vel("Helium", "T", 4, "P", 100e5, "L", 0.0385, 3e-2),
516 vel("Helium", "T", 8, "P", 100e5, "L", 0.0566, 3e-2),
517 vel("Helium", "T", 20, "P", 100e5, "L", 0.0594, 1e-2),
518 vel("Helium", "T", 4, "P", 1e5, "L", 0.0186, 1e-2),
519 vel("Helium", "T", 4, "P", 2e5, "L", 0.0194, 1e-2),
520 vel("Helium", "T", 5.180, "P", 2.3e5, "L", 0.0195, 1e-1),
521 vel("Helium", "T", 5.2, "P", 2.3e5, "L", 0.0202, 1e-1),
522 vel("Helium", "T", 5.230, "P", 2.3e5, "L", 0.0181, 1e-1),
523 vel("Helium", "T", 5.260, "P", 2.3e5, "L", 0.0159, 1e-1),
524 vel("Helium", "T", 5.3, "P", 2.3e5, "L", 0.0149, 1e-1),
525
526 // Geller, IJT, 2001 - based on experimental data, no validation data provided
527 //vel("R404A", "T", 253.03, "P", 0.101e6, "L", 0.00991, 0.03),
528 //vel("R404A", "T", 334.38, "P", 2.176e6, "L", 19.93e-3, 0.03),
529 //vel("R407C", "T", 253.45, "P", 0.101e6, "L", 0.00970, 0.03),
530 //vel("R407C", "T", 314.39, "P", 0.458e6, "L", 14.87e-3, 0.03),
531 //vel("R410A", "T", 260.32, "P", 0.101e6, "L", 0.01043, 0.03),
532 //vel("R410A", "T", 332.09, "P", 3.690e6, "L", 22.76e-3, 0.03),
533 //vel("R507A", "T", 254.85, "P", 0.101e6, "L", 0.01007, 0.03),
534 //vel("R507A", "T", 333.18, "P", 2.644e6, "L", 21.31e-3, 0.03),
535
536 // From REFPROP 9.1 since no data provided
537 vel("R134a", "T", 240, "D", 1e-10, "L", 0.008698768, 1e-4),
538 vel("R134a", "T", 330, "D", 1e-10, "L", 0.015907606, 1e-4),
539 vel("R134a", "T", 330, "Q", 0, "L", 0.06746432253, 1e-4),
540 vel("R134a", "T", 240, "Q", 1, "L", 0.00873242359, 1e-4),
541
542 // Mylona, JPCRD, 2014 - dense check values taken from the implementation in REFPROP 10.0
543 vel("o-Xylene", "T", 635, "D", 270, "L", 0.10387803232507065, 5e-3),
544 vel("m-Xylene", "T", 616, "D", 220, "L", 0.10330950977360005, 5e-3),
545 vel("p-Xylene", "T", 620, "D", 287, "L", 0.09804128875928533, 5e-3),
546 vel("EthylBenzene", "T", 617, "D", 316, "L", 0.1479194493736235, 5e-2),
547 // dilute values
548 vel("o-Xylene", "T", 300, "D", 1e-12, "L", 13.68e-3, 1e-3),
549 vel("o-Xylene", "T", 600, "D", 1e-12, "L", 41.6e-3, 1e-3),
550 vel("m-Xylene", "T", 300, "D", 1e-12, "L", 9.45e-3, 1e-3),
551 vel("m-Xylene", "T", 600, "D", 1e-12, "L", 40.6e-3, 1e-3),
552 vel("p-Xylene", "T", 300, "D", 1e-12, "L", 10.57e-3, 1e-3),
553 vel("p-Xylene", "T", 600, "D", 1e-12, "L", 41.73e-3, 1e-3),
554 vel("EthylBenzene", "T", 300, "D", 1e-12, "L", 9.71e-3, 1e-3),
555 vel("EthylBenzene", "T", 600, "D", 1e-12, "L", 41.14e-3, 1e-3),
556
557 // Friend, JPCRD, 1989
558 vel("Methane", "T", 100, "D", 1e-12, "L", 9.83e-3, 1e-3),
559 vel("Methane", "T", 400, "D", 1e-12, "L", 49.96e-3, 1e-3),
560 vel("Methane", "T", 182, "Q", 0, "L", 82.5e-3, 5e-3),
561 vel("Methane", "T", 100, "Dmolar", 28.8e3, "L", 234e-3, 1e-2),
562
563 // Sykioti, JPCRD, 2013
564 vel("Methanol", "T", 300, "Dmass", 850, "L", 241.48e-3, 1e-2),
565 vel("Methanol", "T", 400, "Dmass", 2, "L", 25.803e-3, 1e-2),
566 vel("Methanol", "T", 400, "Dmass", 690, "L", 183.59e-3, 1e-2),
567 vel("Methanol", "T", 500, "Dmass", 10, "L", 40.495e-3, 1e-2),
568
569 // Heavy Water, IAPWS formulation
570 vel("HeavyWater", "T", 0.5000 * 643.847, "Dmass", 3.07 * 358, "V", 835.786416818 * 0.742128e-3, 1e-5),
571 vel("HeavyWater", "T", 0.9000 * 643.847, "Dmass", 2.16 * 358, "V", 627.777590127 * 0.742128e-3, 1e-5),
572 vel("HeavyWater", "T", 1.2000 * 643.847, "Dmass", 0.8 * 358, "V", 259.605241187 * 0.742128e-3, 1e-5),
573
574 // Vassiliou, JPCRD, 2015
575 vel("Cyclopentane", "T", 512, "Dmass", 1e-12, "L", 37.042e-3, 1e-5),
576 vel("Cyclopentane", "T", 512, "Dmass", 400, "L", 69.698e-3, 1e-1),
577 vel("Isopentane", "T", 460, "Dmass", 1e-12, "L", 35.883e-3, 1e-4),
578 vel("Isopentane", "T", 460, "Dmass", 329.914, "L", 59.649e-3, 1e-1),
579 vel("n-Pentane", "T", 460, "Dmass", 1e-12, "L", 34.048e-3, 1e-5),
580 vel("n-Pentane", "T", 460, "Dmass", 377.687, "L", 71.300e-3, 1e-1),
581};
582
583TEST_CASE_METHOD(TransportValidationFixture, "Compare thermal conductivities against published data", "[conductivity],[transport]") {
584 int inputsN = sizeof(conductivity_validation_data) / sizeof(conductivity_validation_data[0]);
585 for (int i = 0; i < inputsN; ++i) {
586 vel el = conductivity_validation_data[i];
587 CHECK_NOTHROW(set_backend("HEOS", el.fluid));
588 CAPTURE(el.fluid);
589 CAPTURE(el.in1);
590 CAPTURE(el.v1);
591 CAPTURE(el.in2);
592 CAPTURE(el.v2);
593 CHECK_NOTHROW(set_pair(el.in1, el.v1, el.in2, el.v2));
594 get_value(CoolProp::iconductivity);
595 CAPTURE(el.expected);
596 CAPTURE(actual);
597 CHECK(std::abs(actual / el.expected - 1) < el.tol);
598 }
599}
600
601}; /* namespace TransportValidation */
602
603static CoolProp::input_pairs inputs[] = {
605 //CoolProp::SmolarT_INPUTS,
606 //CoolProp::HmolarT_INPUTS,
607 //CoolProp::TUmolar_INPUTS,
608
609 // CoolProp::DmolarP_INPUTS,
610 // CoolProp::DmolarHmolar_INPUTS,
611 // CoolProp::DmolarSmolar_INPUTS,
612 // CoolProp::DmolarUmolar_INPUTS,
613 //
614 // CoolProp::HmolarP_INPUTS,
615 // CoolProp::PSmolar_INPUTS,
616 // CoolProp::PUmolar_INPUTS,
617 //
618 /*
619 CoolProp::HmolarSmolar_INPUTS,
620 CoolProp::HmolarUmolar_INPUTS,
621 CoolProp::SmolarUmolar_INPUTS
622 */
623};
624
625class ConsistencyFixture
626{
627 protected:
628 CoolPropDbl hmolar, pmolar, smolar, umolar, rhomolar, T, p, x1, x2;
629 shared_ptr<CoolProp::AbstractState> pState;
631
632 public:
633 ConsistencyFixture() = default;
634 ~ConsistencyFixture() = default;
635 void set_backend(const std::string& backend, const std::string& fluid_name) {
636 pState.reset(CoolProp::AbstractState::factory(backend, fluid_name));
637 }
638 void set_pair(CoolProp::input_pairs pair) {
639 this->pair = pair;
640 }
641 void set_TP(CoolPropDbl T, CoolPropDbl p) {
642 this->T = T;
643 this->p = p;
644 CoolProp::AbstractState& State = *pState;
645
646 // Start with T,P as inputs, cycle through all the other pairs that are supported
647 State.update(CoolProp::PT_INPUTS, p, T);
648
649 // Set the other state variables
650 rhomolar = State.rhomolar();
651 hmolar = State.hmolar();
652 smolar = State.smolar();
653 umolar = State.umolar();
654 }
655 void get_variables() {
656
657 switch (pair) {
660 x1 = hmolar;
661 x2 = T;
662 break;
664 x1 = smolar;
665 x2 = T;
666 break;
668 x1 = T;
669 x2 = umolar;
670 break;
672 x1 = rhomolar;
673 x2 = T;
674 break;
675
678 x1 = rhomolar;
679 x2 = hmolar;
680 break;
682 x1 = rhomolar;
683 x2 = smolar;
684 break;
686 x1 = rhomolar;
687 x2 = umolar;
688 break;
690 x1 = rhomolar;
691 x2 = p;
692 break;
693
696 x1 = hmolar;
697 x2 = p;
698 break;
700 x1 = p;
701 x2 = smolar;
702 break;
704 x1 = p;
705 x2 = umolar;
706 break;
707
709 x1 = hmolar;
710 x2 = smolar;
711 break;
713 x1 = smolar;
714 x2 = umolar;
715 break;
716
717 default:
718 throw CoolProp::ValueError();
719 }
720 }
721 void single_phase_consistency_check() {
722 CoolProp::AbstractState& State = *pState;
723 State.update(pair, x1, x2);
724
725 // Make sure we end up back at the same temperature and pressure we started out with
726 if (State.Q() < 1 && State.Q() > 0) throw CoolProp::ValueError(format("Q [%g] is between 0 and 1; two-phase solution", State.Q()));
727 if (std::abs(T - State.T()) > 1e-2) throw CoolProp::ValueError(format("Error on T [%Lg K] is greater than 1e-2", std::abs(State.T() - T)));
728 if (std::abs(p - State.p()) / p * 100 > 1e-2)
729 throw CoolProp::ValueError(format("Error on p [%Lg %%] is greater than 1e-2 %%", std::abs(p - State.p()) / p * 100));
730 }
731 void subcritical_pressure_liquid() {
732 // Subcritical pressure liquid
733 int inputsN = sizeof(inputs) / sizeof(inputs[0]);
734 // Geometric scaling (p *= 3) — no FP-counter-accumulation issue; ~3-5 iters.
735 for (double p = pState->p_triple() * 1.1; p < pState->p_critical(); p *= 3) { // NOLINT(cert-flp30-c)
736 double Ts = PropsSI("T", "P", p, "Q", 0, "Water");
737 double Tmelt = pState->melting_line(CoolProp::iT, CoolProp::iP, p);
738 // Test scaffold: T += 0.1 over a ~100 K range gives ~1e-14
739 // cumulative error which is far below the property tolerance
740 // any of the inner CHECKs use.
741 for (double T = Tmelt; T < Ts - 0.1; T += 0.1) { // NOLINT(cert-flp30-c)
742 CHECK_NOTHROW(set_TP(T, p));
743
744 for (int i = 0; i < inputsN; ++i) {
745 CoolProp::input_pairs pair = inputs[i];
746 std::string pair_desc = CoolProp::get_input_pair_short_desc(pair);
747 set_pair(pair);
748 CAPTURE(pair_desc);
749 CAPTURE(T);
750 CAPTURE(p);
751 get_variables();
752 CAPTURE(x1);
753 CAPTURE(x2);
754 CAPTURE(Ts);
755 CHECK_NOTHROW(single_phase_consistency_check());
756 double rhomolar_RP = PropsSI("Dmolar", "P", p, "T", T, "REFPROP::Water");
757 if (ValidNumber(rhomolar_RP)) {
758 CAPTURE(rhomolar_RP);
759 CAPTURE(rhomolar);
760 CHECK(std::abs((rhomolar_RP - rhomolar) / rhomolar) < 1e-3);
761 }
762 }
763 }
764 }
765 }
766};
767
768TEST_CASE_METHOD(ConsistencyFixture, "Test all input pairs for Water using all valid backends", "[consistency]") {
769 CHECK_NOTHROW(set_backend("HEOS", "Water"));
770 subcritical_pressure_liquid();
771
772 // int inputsN = sizeof(inputs)/sizeof(inputs[0]);
773 // for (double p = 600000; p < pState->pmax(); p *= 3)
774 // {
775 // for (double T = 220; T < pState->Tmax(); T += 1)
776 // {
777 // CHECK_NOTHROW(set_TP(T, p));
778 //
779 // for (int i = 0; i < inputsN; ++i)
780 // {
781 // CoolProp::input_pairs pair = inputs[i];
782 // std::string pair_desc = CoolProp::get_input_pair_short_desc(pair);
783 // set_pair(pair);
784 // CAPTURE(pair_desc);
785 // CAPTURE(T);
786 // CAPTURE(p);
787 // get_variables();
788 // CAPTURE(x1);
789 // CAPTURE(x2);
790 // CHECK_NOTHROW(single_phase_consistency_check());
791 // }
792 // }
793 // }
794}
795
796TEST_CASE("Test saturation properties for a few fluids", "[saturation],[slow]") {
797 SECTION("sat_p") {
798 std::vector<double> pv = linspace(Props1SI("CO2", "ptriple"), Props1SI("CO2", "pcrit") - 1e-6, 5);
799
800 SECTION("All pressures are ok")
801 for (double i : pv) {
802 CAPTURE(i);
803 double T = CoolProp::PropsSI("T", "P", i, "Q", 0, "CO2");
804 }
805 }
806}
807
808class HumidAirDewpointFixture
809{
810 public:
811 shared_ptr<CoolProp::AbstractState> AS;
812 std::vector<std::string> fluids;
813 std::vector<double> z;
814 void setup(double zH2O) {
815 double z_Air[4] = {0.7810, 0.2095, 0.0092, 0.0003}; // N2, O2, Ar, CO2
816 z.resize(5);
817 z[0] = zH2O;
818 for (int i = 0; i < 4; ++i) {
819 z[i + 1] = (1 - zH2O) * z_Air[i];
820 }
821 }
822 void run_p(double p) {
823 CAPTURE(p);
824 // Integer-indexed sweep (cert-flp30-c): zH2O = 0.999, 0.998, ...,
825 // 0.001 (999 samples), exactly preserving the original loop's
826 // exit condition without 1e-3 step accumulation.
827 constexpr std::size_t N_z = 999;
828 for (std::size_t i = 0; i < N_z; ++i) {
829 const double zH2O = 0.999 - 0.001 * i;
830 setup(zH2O);
831 AS->set_mole_fractions(z);
832 CAPTURE(zH2O);
833 CHECK_NOTHROW(AS->update(PQ_INPUTS, p, 1));
834 if (AS->T() < 273.15) {
835 break;
836 }
837 }
838 }
839 void run_checks() {
840 fluids = strsplit("Water&Nitrogen&Oxygen&Argon&CO2", '&');
841 AS.reset(AbstractState::factory("HEOS", fluids));
842 run_p(1e5);
843 run_p(1e6);
844 run_p(1e7);
845 }
846};
847//TEST_CASE_METHOD(HumidAirDewpointFixture, "Humid air dewpoint calculations", "[humid_air_dewpoint]") {
848// run_checks();
849//}
850
851TEST_CASE("HAPropsSI two-water-content inputs that uniquely determine dry-bulb temperature (issue #2670)", "[humid_air][2670]") {
852 // When one water-content input fixes psi_w independently of T and the other is
853 // relative humidity (which depends on T), the system has a unique solution for T.
854 // Note: HAPropsSI catches all exceptions internally and returns _HUGE on error,
855 // so we test ValidNumber() rather than CHECK_THROWS / CHECK_NOTHROW.
856 double p = 101325.0;
857 double T_dp = 283.15; // 10 °C dew-point
858 double R = 0.8; // 80 % relative humidity
859
860 SECTION("T_dp + R gives dry-bulb temperature") {
861 // This combination was broken in v6.3.0.
862 double T_drybulb = HumidAir::HAPropsSI("T", "D", T_dp, "R", R, "P", p);
863 CHECK(ValidNumber(T_drybulb));
864 CHECK(T_drybulb >= T_dp - 1e-6);
865
866 // Cross-check: round-trip T_dp == DewPoint(T_drybulb, R, P)
867 double T_dp_check = HumidAir::HAPropsSI("D", "T", T_drybulb, "R", R, "P", p);
868 CHECK(ValidNumber(T_dp_check));
869 CHECK(std::abs(T_dp_check - T_dp) < 1e-4);
870 }
871
872 SECTION("W + R gives dry-bulb temperature") {
873 // Derive W from the known T_dp + R state so we can verify consistency.
874 double W = HumidAir::HAPropsSI("W", "D", T_dp, "R", R, "P", p);
875 REQUIRE(ValidNumber(W));
876
877 double T_drybulb = HumidAir::HAPropsSI("T", "W", W, "R", R, "P", p);
878 CHECK(ValidNumber(T_drybulb));
879 CHECK(T_drybulb >= T_dp - 1e-6);
880
881 // Cross-check R round-trip
882 double R_check = HumidAir::HAPropsSI("R", "T", T_drybulb, "W", W, "P", p);
883 CHECK(ValidNumber(R_check));
884 CHECK(std::abs(R_check - R) < 1e-6);
885 }
886
887 SECTION("W + T_dp returns invalid (both fix psi_w, T is unconstrained)") {
888 double W = HumidAir::HAPropsSI("W", "D", T_dp, "R", R, "P", p);
889 REQUIRE(ValidNumber(W));
890 double result = HumidAir::HAPropsSI("T", "W", W, "D", T_dp, "P", p);
891 CHECK(!ValidNumber(result));
892 }
893}
894
895// ============================================================
896// Virial cache correctness: calc_all_virials (static helper in HumidAirProp.cpp,
897// invoked via fill_virial_cache) must produce HAPropsSI outputs consistent with
898// the reference EOS virial keyed_output values.
899//
900// The function is not accessible here directly (it is static in HumidAirProp.cpp),
901// so we test it end-to-end: compute HAPropsSI at conditions where the virial
902// correction is significant, and compare to values derived from individual
903// keyed_output calls assembled with the same mixing rule as the humid-air code.
904// ============================================================
905
906TEST_CASE("Humid-air virial-dependent properties are consistent with EOS virials", "[humid_air][virial_cache]") {
907 // Verify that the HAPropsSI fugacity coefficient ('f') and compressibility ('Z')
908 // are consistent with the individual B/C virial values from the EOS backends.
909 // These quantities go through fill_virial_cache → calc_all_virials.
910
911 const double P = 101325.0;
912 const double W = 0.01; // 10 g/kg — well within ideal-gas range for virials
913
914 SECTION("compressibility Z is close to 1 at atmospheric conditions") {
915 for (double T : {250.0, 273.15, 293.15, 333.15, 373.15}) {
916 CAPTURE(T);
917 double Z = HumidAir::HAPropsSI("Z", "T", T, "W", W, "P", P);
918 // At atmospheric pressure humid air deviates less than 0.2% from ideal
919 CHECK(Z == Catch::Approx(1.0).margin(2e-3));
920 }
921 }
922
923 SECTION("HAPropsSI virial-path results reproduce across cache invalidation") {
924 // Call at T1, T2, T1 again — the third must be bit-identical to the first.
925 double Z_T1_a = HumidAir::HAPropsSI("Z", "T", 293.15, "W", W, "P", P);
926 double Z_T2 = HumidAir::HAPropsSI("Z", "T", 333.15, "W", W, "P", P);
927 double Z_T1_b = HumidAir::HAPropsSI("Z", "T", 293.15, "W", W, "P", P);
928 (void)Z_T2;
929 CHECK(Z_T1_a == Z_T1_b);
930 }
931
932 SECTION("HAPropsSI enthalpy cache-invalidation reproduces") {
933 double H_T1_a = HumidAir::HAPropsSI("H", "T", 293.15, "W", W, "P", P);
934 double H_T2 = HumidAir::HAPropsSI("H", "T", 333.15, "W", W, "P", P);
935 double H_T1_b = HumidAir::HAPropsSI("H", "T", 293.15, "W", W, "P", P);
936 (void)H_T2;
937 CHECK(H_T1_a == H_T1_b);
938 CHECK(H_T1_a > 0.0);
939 CHECK(H_T2 > H_T1_a);
940 }
941}
942
943// ============================================================
944// Alpha0 cache correctness: calc_ideal_gas_alpha0 (via fill_alpha0_cache) must
945// produce enthalpy/entropy consistent with the direct update() path.
946//
947// calc_ideal_gas_alpha0 is a static helper in HumidAirProp.cpp, not accessible
948// here directly. We verify it end-to-end by comparing HAPropsSI('H'/'S') against
949// reference values computed via update() on the individual Air/Water backends, then
950// manually assembling the same h/s formula the humid-air code uses. Any mismatch
951// in alpha0 or da0_dtau propagates into h and s.
952// ============================================================
953
954TEST_CASE("Humid-air h and s are consistent with individual EOS alpha0", "[humid_air][alpha0_cache]") {
955 // Spot-check specific-enthalpy and specific-entropy of dry air (W=0) and
956 // pure water vapour (W→1, W=0.99) via HAPropsSI against direct backend calls.
957 // These quantities depend directly on the alpha0 cache (fill_alpha0_cache →
958 // calc_ideal_gas_alpha0), so any bug there surfaces here.
959
960 const double P = 101325.0;
961 const double Tvals[] = {213.15, 253.15, 293.15, 333.15, 373.15, 400.0};
962 const int NT = static_cast<int>(sizeof(Tvals) / sizeof(Tvals[0]));
963
964 SECTION("dry air enthalpy monotonically increases with T") {
965 // Simple sanity: h_dry_air(T2) > h_dry_air(T1) for T2 > T1.
966 double h_prev = HumidAir::HAPropsSI("H", "T", Tvals[0], "W", 0.0, "P", P);
967 for (int i = 1; i < NT; ++i) {
968 double h = HumidAir::HAPropsSI("H", "T", Tvals[i], "W", 0.0, "P", P);
969 CAPTURE(Tvals[i]);
970 CHECK(h > h_prev);
971 h_prev = h;
972 }
973 }
974
975 SECTION("dry air entropy monotonically increases with T") {
976 double s_prev = HumidAir::HAPropsSI("S", "T", Tvals[0], "W", 0.0, "P", P);
977 for (int i = 1; i < NT; ++i) {
978 double s = HumidAir::HAPropsSI("S", "T", Tvals[i], "W", 0.0, "P", P);
979 CAPTURE(Tvals[i]);
980 CHECK(s > s_prev);
981 s_prev = s;
982 }
983 }
984
985 SECTION("h round-trip: T recovered from H at W=0") {
986 // Given (T, W=0, P), compute H, then invert back to T via (H, W=0).
987 // Tests that the alpha0-derived enthalpy is internally consistent.
988 // Note: H+S alone cannot determine T (humidity ratio is unknown), so
989 // we keep W=0 fixed and invert H(T, W=0, P) → T.
990 for (double T : Tvals) {
991 CAPTURE(T);
992 double H = HumidAir::HAPropsSI("H", "T", T, "W", 0.0, "P", P);
993 REQUIRE(ValidNumber(H));
994 double T_back = HumidAir::HAPropsSI("T", "H", H, "W", 0.0, "P", P);
995 CHECK(T_back == Catch::Approx(T).epsilon(1e-6));
996 }
997 }
998}
999
1000// ============================================================
1001// Comprehensive Humid Air Validation Tests
1002// Based on ASHRAE RP-1485 scenarios from HAValidation.py.
1003//
1004// Organised by tag so each group can be run independently:
1005// [humid_air_validation] parent tag – runs everything below
1006// [ashrae_a61] A.6.1: saturated air, T=-60..0 °C, P=101.325 kPa
1007// [ashrae_a62] A.6.2: saturated air, T=0..90 °C, P=101.325 kPa
1008// [ashrae_a8] A.8: T=200 °C, W=0..1, P=101–10 000 kPa
1009// [ashrae_a9] A.9: T=320 °C, W=0..1, P=101–10 000 kPa
1010// [humid_air_physics] Physical constraints over a (T,R,P) grid
1011// [humid_air_roundtrip] Round-trip consistency
1012// [humid_air_aux] Auxiliary functions: f_factor, p_ws, beta_H, kT, vbar_ws
1013// ============================================================
1014
1015// -------------------------------------------------------
1016// Helpers shared across groups
1017// -------------------------------------------------------
1018namespace HumidAirTests {
1019// HAPropsSI wrapper that returns NaN rather than throwing
1020static double hap(const char* out, const char* k1, double v1, const char* k2, double v2, double p) {
1021 return HumidAir::HAPropsSI(out, k1, v1, k2, v2, "P", p);
1022}
1023} // namespace HumidAirTests
1024
1025// -------------------------------------------------------
1026// A.6.1 Saturated air at 101.325 kPa, T = -60 .. 0 °C
1027// -------------------------------------------------------
1028TEST_CASE("ASHRAE RP-1485 A.6.1: Saturated air properties, T=-60..0 C, P=101.325 kPa", "[humid_air_validation][ashrae_a61]") {
1029 // At R=1 every output must be a valid number; specific constraints below.
1030 // Known issue on unpatched master: T_wb returns inf at some temperatures –
1031 // recorded here as CHECK (not REQUIRE) so the suite continues to run.
1032 using namespace HumidAirTests;
1033 const double P = 101325.0;
1034
1035 // 13 evenly-spaced points from -60 to 0 °C (step = 5 °C)
1036 for (int i = 0; i <= 12; ++i) {
1037 const double T = (273.15 - 60.0) + i * 5.0;
1038 SECTION(std::string("T = ") + std::to_string(static_cast<int>(T - 273.15)) + " C") {
1039 const double W = hap("W", "T", T, "R", 1.0, P);
1040 const double h = hap("H", "T", T, "R", 1.0, P);
1041 const double v = hap("V", "T", T, "R", 1.0, P);
1042 const double s = hap("S", "T", T, "R", 1.0, P);
1043 const double Twb = hap("Twb", "T", T, "R", 1.0, P);
1044 const double Tdp = hap("D", "T", T, "R", 1.0, P);
1045
1046 // All outputs must be finite
1047 REQUIRE(ValidNumber(W));
1048 REQUIRE(ValidNumber(h));
1049 REQUIRE(ValidNumber(v));
1050 REQUIRE(ValidNumber(s));
1051
1052 // Physical bounds
1053 CHECK(W >= 0.0); // non-negative humidity ratio
1054 CHECK(v > 0.0); // positive specific volume
1055
1056 // At R=1: dew point and wet-bulb both equal dry-bulb temperature
1057 CHECK(ValidNumber(Tdp));
1058 CHECK(std::abs(Tdp - T) < 1e-3); // T_dp == T_db at saturation
1059 CHECK(ValidNumber(Twb));
1060 CHECK(std::abs(Twb - T) < 1e-3); // T_wb == T_db at saturation
1061 }
1062 }
1063}
1064
1065// -------------------------------------------------------
1066// A.6.2 Saturated air at 101.325 kPa, T = 0 .. 90 °C
1067// -------------------------------------------------------
1068TEST_CASE("ASHRAE RP-1485 A.6.2: Saturated air properties, T=0..90 C, P=101.325 kPa", "[humid_air_validation][ashrae_a62]") {
1069 using namespace HumidAirTests;
1070 const double P = 101325.0;
1071
1072 // 19 evenly-spaced points from 0 to 90 °C (step = 5 °C)
1073 for (int i = 0; i <= 18; ++i) {
1074 const double T = 273.15 + i * 5.0;
1075 SECTION(std::string("T = ") + std::to_string(static_cast<int>(T - 273.15)) + " C") {
1076 const double W = hap("W", "T", T, "R", 1.0, P);
1077 const double h = hap("H", "T", T, "R", 1.0, P);
1078 const double v = hap("V", "T", T, "R", 1.0, P);
1079 const double s = hap("S", "T", T, "R", 1.0, P);
1080 const double Twb = hap("Twb", "T", T, "R", 1.0, P);
1081 const double Tdp = hap("D", "T", T, "R", 1.0, P);
1082
1083 REQUIRE(ValidNumber(W));
1084 REQUIRE(ValidNumber(h));
1085 REQUIRE(ValidNumber(v));
1086 REQUIRE(ValidNumber(s));
1087
1088 CHECK(W > 0.0); // above 0 °C there is always some saturation humidity
1089 CHECK(v > 0.0);
1090 // Note: W can exceed 1 kg/kg at high T (e.g. ~1.42 at 90 °C, R=1) — no upper cap here
1091
1092 CHECK(ValidNumber(Tdp));
1093 CHECK(std::abs(Tdp - T) < 1e-3);
1094 CHECK(ValidNumber(Twb));
1095 CHECK(std::abs(Twb - T) < 1e-3); // T_wb == T_db at R=1
1096 }
1097 }
1098}
1099
1100// -------------------------------------------------------
1101// A.8 T = 200 °C (473.15 K), W = 0..1, multiple P
1102// -------------------------------------------------------
1103TEST_CASE("ASHRAE RP-1485 A.8: T=200 C, W=0..1 kg/kg, P=101 kPa..10 MPa", "[humid_air_validation][ashrae_a8]") {
1104 using namespace HumidAirTests;
1105 const double T = 200.0 + 273.15; // 473.15 K
1106
1107 // Pressure table and corresponding W ranges (limited at high P where T < T_sat)
1108 struct PressureCase
1109 {
1110 double p;
1111 std::vector<double> Wvals;
1112 };
1113 const PressureCase cases[] = {
1114 {101325.0, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1115 {1000e3, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1116 {2000e3, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1117 {5000e3, {0.0, 0.05, 0.1, 0.15, 0.20, 0.25, 0.30}}, // T < T_sat(5 MPa), W limited
1118 {10000e3, {0.0, 0.05, 0.1}}, // T < T_sat(10 MPa), W limited
1119 };
1120
1121 for (const auto& pc : cases) {
1122 for (double W : pc.Wvals) {
1123 SECTION("P=" + std::to_string(static_cast<int>(pc.p / 1000)) + " kPa W=" + std::to_string(W)) {
1124 const double h = hap("H", "T", T, "W", W, pc.p);
1125 const double v = hap("V", "T", T, "W", W, pc.p);
1126 const double s = hap("S", "T", T, "W", W, pc.p);
1127 const double R = hap("R", "T", T, "W", W, pc.p);
1128 const double Twb = hap("Twb", "T", T, "W", W, pc.p);
1129
1130 // All must be finite
1131 REQUIRE(ValidNumber(h));
1132 REQUIRE(ValidNumber(v));
1133 REQUIRE(ValidNumber(s));
1134 REQUIRE(ValidNumber(R));
1135
1136 // Physical bounds
1137 CHECK(v > 0.0);
1138 CHECK(R >= 0.0);
1139 CHECK(R <= 1.0 + 1e-9); // R ≤ 1 (allow tiny FP overshoot)
1140
1141 // Wet-bulb must be ≤ dry-bulb
1142 CHECK(ValidNumber(Twb));
1143 if (ValidNumber(Twb)) {
1144 CHECK(Twb <= T + 1e-6);
1145
1146 // Round-trip: from (T, W) → R, then from (T, R) → W_check
1147 if (ValidNumber(R) && R > 1e-10) {
1148 double W_check = hap("W", "T", T, "R", R, pc.p);
1149 CHECK(ValidNumber(W_check));
1150 if (ValidNumber(W_check)) {
1151 CHECK(std::abs(W_check - W) < W * 1e-6 + 1e-10);
1152 }
1153 }
1154 }
1155 }
1156 }
1157 }
1158}
1159
1160// -------------------------------------------------------
1161// A.9 T = 320 °C (593.15 K), W = 0..1, multiple P
1162// -------------------------------------------------------
1163TEST_CASE("ASHRAE RP-1485 A.9: T=320 C, W=0..1 kg/kg, P=101 kPa..10 MPa", "[humid_air_validation][ashrae_a9]") {
1164 using namespace HumidAirTests;
1165 const double T = 320.0 + 273.15; // 593.15 K
1166
1167 struct PressureCase
1168 {
1169 double p;
1170 std::vector<double> Wvals;
1171 };
1172 const PressureCase cases[] = {
1173 {101325.0, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1174 {1000e3, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1175 {2000e3, {0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1176 {5000e3, {0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1177 {10000e3, {0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}},
1178 };
1179
1180 for (const auto& pc : cases) {
1181 for (double W : pc.Wvals) {
1182 SECTION("P=" + std::to_string(static_cast<int>(pc.p / 1000)) + " kPa W=" + std::to_string(W)) {
1183 const double h = hap("H", "T", T, "W", W, pc.p);
1184 const double v = hap("V", "T", T, "W", W, pc.p);
1185 const double s = hap("S", "T", T, "W", W, pc.p);
1186 const double R = hap("R", "T", T, "W", W, pc.p);
1187 const double Twb = hap("Twb", "T", T, "W", W, pc.p);
1188
1189 REQUIRE(ValidNumber(h));
1190 REQUIRE(ValidNumber(v));
1191 REQUIRE(ValidNumber(s));
1192 REQUIRE(ValidNumber(R));
1193
1194 CHECK(v > 0.0);
1195 CHECK(R >= 0.0);
1196 CHECK(R <= 1.0 + 1e-9);
1197
1198 CHECK(ValidNumber(Twb));
1199 if (ValidNumber(Twb)) {
1200 CHECK(Twb <= T + 1e-6);
1201
1202 if (ValidNumber(R) && R > 1e-10) {
1203 double W_check = hap("W", "T", T, "R", R, pc.p);
1204 CHECK(ValidNumber(W_check));
1205 if (ValidNumber(W_check)) {
1206 CHECK(std::abs(W_check - W) < W * 1e-6 + 1e-10);
1207 }
1208 }
1209 }
1210 }
1211 }
1212 }
1213}
1214
1215// -------------------------------------------------------
1216// Physical constraints over a (T, R, P) grid
1217// -------------------------------------------------------
1218TEST_CASE("Humid air physical constraints: T_dp <= T_wb <= T_db, W >= 0, 0 <= R <= 1", "[humid_air_validation][humid_air_physics]") {
1219 using namespace HumidAirTests;
1220
1221 // A representative grid spanning sub-freezing, normal, and near-boiling conditions
1222 const double Tvals[] = {243.15, 263.15, 283.15, 293.15, 313.15, 333.15, 353.15};
1223 const double Rvals[] = {0.1, 0.3, 0.5, 0.7, 0.9};
1224 const double Pvals[] = {50000.0, 101325.0, 300000.0};
1225
1226 for (double T : Tvals) {
1227 for (double R : Rvals) {
1228 for (double p : Pvals) {
1229 SECTION("T=" + std::to_string(static_cast<int>(T - 273.15)) + " R=" + std::to_string(static_cast<int>(R * 100))
1230 + "% P=" + std::to_string(static_cast<int>(p))) {
1231 const double W = hap("W", "T", T, "R", R, p);
1232 const double Tdp = hap("D", "T", T, "R", R, p);
1233 const double Twb = hap("Twb", "T", T, "R", R, p);
1234 const double R2 = hap("R", "T", T, "W", W, p);
1235
1236 // Basic validity
1237 REQUIRE(ValidNumber(W));
1238 CHECK(W >= 0.0);
1239
1240 // R round-trip
1241 if (ValidNumber(R2)) {
1242 CHECK(std::abs(R2 - R) < 1e-6);
1243 }
1244
1245 // Temperature ordering: T_dp <= T_wb <= T_db
1246 if (ValidNumber(Tdp)) {
1247 CHECK(Tdp <= T + 1e-6);
1248 }
1249 if (ValidNumber(Twb)) {
1250 CHECK(Twb <= T + 1e-6);
1251 CHECK(Twb >= 100.0); // wet-bulb never below ~100 K
1252 if (ValidNumber(Tdp)) {
1253 CHECK(Tdp <= Twb + 1e-4); // dew point <= wet-bulb
1254 }
1255 }
1256 }
1257 }
1258 }
1259 }
1260}
1261
1262// -------------------------------------------------------
1263// Round-trip consistency: T+R → various outputs → back
1264// -------------------------------------------------------
1265TEST_CASE("Humid air round-trip consistency: outputs used as inputs recover the original state", "[humid_air_validation][humid_air_roundtrip]") {
1266 using namespace HumidAirTests;
1267
1268 // Representative conditions: (T [K], R [0-1], P [Pa])
1269 struct Cond
1270 {
1271 double T, R, p;
1272 };
1273 const Cond conds[] = {
1274 {253.15, 0.5, 101325.0}, // -20 °C, 50% RH
1275 {273.15, 0.8, 101325.0}, // 0 °C, 80% RH (ice/liquid boundary)
1276 {293.15, 0.3, 101325.0}, // 20 °C, 30% RH (typical indoor)
1277 {293.15, 0.9, 101325.0}, // 20 °C, 90% RH
1278 {313.15, 0.6, 101325.0}, // 40 °C, 60% RH
1279 {293.15, 0.5, 200000.0}, // 20 °C, 50% RH at 2 bar
1280 {293.15, 0.5, 50000.0}, // 20 °C, 50% RH at 0.5 bar
1281 };
1282
1283 for (const auto& c : conds) {
1284 SECTION("T=" + std::to_string(static_cast<int>(c.T - 273.15)) + " R=" + std::to_string(static_cast<int>(c.R * 100))
1285 + "% P=" + std::to_string(static_cast<int>(c.p))) {
1286 // Derive W from (T, R)
1287 const double W = hap("W", "T", c.T, "R", c.R, c.p);
1288 REQUIRE(ValidNumber(W));
1289
1290 // W → R round-trip
1291 {
1292 double R_check = hap("R", "T", c.T, "W", W, c.p);
1293 REQUIRE(ValidNumber(R_check));
1294 CHECK(std::abs(R_check - c.R) < 1e-6);
1295 }
1296
1297 // H round-trip: (T,R) → H → (H,R) → T
1298 {
1299 double H = hap("H", "T", c.T, "R", c.R, c.p);
1300 REQUIRE(ValidNumber(H));
1301 double T_check = hap("T", "H", H, "R", c.R, c.p);
1302 REQUIRE(ValidNumber(T_check));
1303 CHECK(std::abs(T_check - c.T) < 1e-3);
1304 }
1305
1306 // Dew-point round-trip: (T,R) → Tdp → (T,Tdp) → W_check ≈ W
1307 {
1308 double Tdp = hap("D", "T", c.T, "R", c.R, c.p);
1309 REQUIRE(ValidNumber(Tdp));
1310 double W_check = hap("W", "T", c.T, "D", Tdp, c.p);
1311 REQUIRE(ValidNumber(W_check));
1312 CHECK(std::abs(W_check - W) < W * 1e-4 + 1e-12);
1313 }
1314
1315 // Wet-bulb round-trip: (T,R) → Twb → (Twb,R) → T is NOT a valid round-trip
1316 // because (Twb, R) is overdetermined unless R=1.
1317 // Instead verify: compute T_wb and then verify T_wb(T_db, R) == T_wb.
1318 {
1319 double Twb = hap("Twb", "T", c.T, "R", c.R, c.p);
1320 CHECK(ValidNumber(Twb));
1321 if (ValidNumber(Twb)) {
1322 double Twb_check = hap("Twb", "T", c.T, "W", W, c.p);
1323 CHECK(ValidNumber(Twb_check));
1324 if (ValidNumber(Twb_check)) {
1325 CHECK(std::abs(Twb_check - Twb) < 1e-4);
1326 }
1327 }
1328 }
1329 }
1330 }
1331}
1332
1333// -------------------------------------------------------
1334// Auxiliary functions: f_factor, p_ws, beta_H, kT, vbar_ws
1335// -------------------------------------------------------
1336TEST_CASE("Humid air auxiliary functions: physical validity and monotonicity", "[humid_air_validation][humid_air_aux]") {
1337 // HAProps_Aux(name, T[K], p[Pa], W[kg/kg], units_buf)
1338 char units[64];
1339
1340 SECTION("Enhancement factor f >= 1.0 for all T and P") {
1341 // f = (actual vapour pressure) / (saturation vapour pressure).
1342 // The enhancement factor is always >= 1 due to dissolved air effects.
1343 const double Tvals[] = {213.15, 253.15, 273.15, 293.15, 313.15, 353.15, 423.15, 623.15};
1344 const double Pvals[] = {101325.0, 200000.0, 500000.0, 1000000.0, 10000000.0};
1345 for (double T : Tvals) {
1346 for (double p : Pvals) {
1347 double f = HumidAir::HAProps_Aux("f", T, p, 0.0, units);
1348 CAPTURE(T);
1349 CAPTURE(p);
1350 CHECK(ValidNumber(f));
1351 CHECK(f >= 1.0 - 1e-9); // should be ≥ 1.0
1352 }
1353 }
1354 }
1355
1356 SECTION("Enhancement factor increases with pressure at fixed T") {
1357 // At fixed T, f increases monotonically with p.
1358 const double T = 293.15;
1359 const double Pvals[] = {101325.0, 200000.0, 500000.0, 1000000.0, 5000000.0};
1360 double f_prev = 0.0;
1361 for (double p : Pvals) {
1362 double f = HumidAir::HAProps_Aux("f", T, p, 0.0, units);
1363 CAPTURE(p);
1364 CAPTURE(f);
1365 CHECK(f >= f_prev - 1e-9);
1366 f_prev = f;
1367 }
1368 }
1369
1370 SECTION("Saturation pressure p_ws is positive and increases with T") {
1371 const double Tvals[] = {213.15, 233.15, 253.15, 273.15, 293.15, 313.15, 333.15, 353.15};
1372 double p_ws_prev = 0.0;
1373 for (double T : Tvals) {
1374 double p_ws = HumidAir::HAProps_Aux("p_ws", T, 101325.0, 0.0, units);
1375 CAPTURE(T);
1376 CAPTURE(p_ws);
1377 CHECK(ValidNumber(p_ws));
1378 CHECK(p_ws > 0.0);
1379 CHECK(p_ws > p_ws_prev); // monotonically increasing with T
1380 p_ws_prev = p_ws;
1381 }
1382 }
1383
1384 SECTION("Henry constant is positive for liquid water; not finite below ice point") {
1385 // beta_H represents the dissolution of air in liquid water.
1386 // Only defined for liquid water (T >= 273.16 K); returns inf below ice point.
1387 const double T_ice = 263.15;
1388 const double T_liq1 = 283.15;
1389 const double T_liq2 = 313.15;
1390 double bH_ice = HumidAir::HAProps_Aux("beta_H", T_ice, 101325.0, 0.0, units);
1391 double bH_liq1 = HumidAir::HAProps_Aux("beta_H", T_liq1, 101325.0, 0.0, units);
1392 double bH_liq2 = HumidAir::HAProps_Aux("beta_H", T_liq2, 101325.0, 0.0, units);
1393 CHECK(!ValidNumber(bH_ice)); // undefined below ice point — returns inf
1394 CHECK(bH_liq1 > 0.0);
1395 CHECK(bH_liq2 > 0.0);
1396 }
1397
1398 SECTION("Isothermal compressibility kT is positive for liquid water") {
1399 const double Tvals[] = {283.15, 303.15, 323.15, 353.15};
1400 const double Pvals[] = {101325.0, 500000.0, 1000000.0};
1401 for (double T : Tvals) {
1402 for (double p : Pvals) {
1403 double kT = HumidAir::HAProps_Aux("kT", T, p, 0.0, units);
1404 CAPTURE(T);
1405 CAPTURE(p);
1406 CHECK(ValidNumber(kT));
1407 CHECK(kT > 0.0); // compressibility is positive for stable liquid
1408 }
1409 }
1410 }
1411
1412 SECTION("Saturated molar volume vbar_ws is physically sane") {
1413 // Molar volume of condensed water (liquid or ice) is ~1.8e-5 to ~2.0e-5
1414 // m^3/mol. Both branches must land in this range; in particular the ice
1415 // branch must not be off by the 1e6 factor of GH #2657. See also the
1416 // f_factor() computation in HumidAirProp.cpp which uses the correct
1417 // expression. Bound generously to allow for compression/expansion.
1418 const double Tvals[] = {213.15, 253.15, 273.15, 293.15, 333.15, 373.15};
1419 const double Pvals[] = {101325.0, 500000.0, 1000000.0};
1420 for (double T : Tvals) {
1421 for (double p : Pvals) {
1422 double v = HumidAir::HAProps_Aux("vbar_ws", T, p, 0.0, units);
1423 CAPTURE(T);
1424 CAPTURE(p);
1425 CHECK(ValidNumber(v));
1426 CHECK(v > 1.5e-5);
1427 CHECK(v < 2.5e-5);
1428 }
1429 }
1430 }
1431
1432 SECTION("vbar_ws is continuous across the ice/liquid boundary (GH #2657)") {
1433 // Just below (ice) and just above (liquid) the triple-point temperature
1434 // 273.16 K the molar volume must be nearly equal — ice is only ~9%
1435 // larger than liquid water, not 1e6x smaller as in GH #2657.
1436 const double p = 101325.0;
1437 double v_ice = HumidAir::HAProps_Aux("vbar_ws", 273.15, p, 0.0, units);
1438 double v_liq = HumidAir::HAProps_Aux("vbar_ws", 273.17, p, 0.0, units);
1439 CAPTURE(v_ice);
1440 CAPTURE(v_liq);
1441 CHECK(ValidNumber(v_ice));
1442 CHECK(ValidNumber(v_liq));
1443 CHECK(v_ice > 0.9 * v_liq);
1444 CHECK(v_ice < 1.2 * v_liq);
1445 }
1446
1447 SECTION("Virial coefficients Baa and Bww have expected signs") {
1448 // Second virial coefficient B: negative at moderate T (attractive interactions dominate)
1449 const double T = 293.15;
1450 double Baa = HumidAir::HAProps_Aux("Baa", T, 101325.0, 0.0, units);
1451 double Bww = HumidAir::HAProps_Aux("Bww", T, 101325.0, 0.0, units);
1452 CHECK(ValidNumber(Baa));
1453 CHECK(ValidNumber(Bww));
1454 CHECK(Baa < 0.0); // Baa < 0 for air at ambient conditions
1455 CHECK(Bww < 0.0); // Bww < 0 for water vapour at ambient conditions
1456 }
1457
1458 SECTION("Cross virial coefficient Baw") {
1459 const double T = 293.15;
1460 double Baw = HumidAir::HAProps_Aux("Baw", T, 101325.0, 0.0, units);
1461 CHECK(ValidNumber(Baw));
1462 CHECK(Baw < 0.0); // Baw is negative at typical atmospheric temperatures
1463 }
1464}
1465
1466TEST_CASE("Test consistency between Gernert models in CoolProp and Gernert models in REFPROP", "[Gernert]") {
1467 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
1468 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
1469
1470 std::string mixes[] = {"CO2[0.7]&Argon[0.3]", "CO2[0.7]&Water[0.3]", "CO2[0.7]&Nitrogen[0.3]"};
1471 for (const auto& mix : mixes) {
1472 const char* ykey = mix.c_str();
1473 std::ostringstream ss1;
1474 ss1 << mix;
1475 SECTION(ss1.str(), "") {
1476 double Tnbp_CP, Tnbp_RP, R_RP, R_CP, pchk_CP, pchk_RP;
1477 CHECK_NOTHROW(R_CP = PropsSI("gas_constant", "P", 101325, "Q", 1, "HEOS::" + mix));
1478 CAPTURE(R_CP);
1479 CHECK_NOTHROW(R_RP = PropsSI("gas_constant", "P", 101325, "Q", 1, "REFPROP::" + mix));
1480 CAPTURE(R_RP);
1481 CHECK_NOTHROW(Tnbp_CP = PropsSI("T", "P", 101325, "Q", 1, "HEOS::" + mix));
1482 CAPTURE(Tnbp_CP);
1483 CHECK_NOTHROW(pchk_CP = PropsSI("P", "T", Tnbp_CP, "Q", 1, "HEOS::" + mix));
1484 CAPTURE(pchk_CP);
1485 CHECK_NOTHROW(Tnbp_RP = PropsSI("T", "P", 101325, "Q", 1, "REFPROP::" + mix));
1486 CAPTURE(Tnbp_RP);
1487 CHECK_NOTHROW(pchk_RP = PropsSI("P", "T", Tnbp_RP, "Q", 1, "REFPROP::" + mix));
1488 CAPTURE(pchk_RP);
1489 double diff = std::abs(Tnbp_CP / Tnbp_RP - 1);
1490 CHECK(diff < 1e-2);
1491 }
1492 }
1493}
1494
1495TEST_CASE("Tests for solvers in P,T flash using Water", "[flash],[PT]") {
1496 SECTION("Check that T,P for saturated state yields error") {
1497 double Ts, ps, rho;
1498 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1499 CHECK(ValidNumber(Ts));
1500 CHECK_NOTHROW(ps = PropsSI("P", "T", Ts, "Q", 0, "Water"));
1501 CHECK(ValidNumber(ps));
1502 CAPTURE(Ts);
1503 CAPTURE(ps);
1504 CHECK_NOTHROW(rho = PropsSI("D", "T", Ts, "P", ps, "Water"));
1505 CAPTURE(rho);
1506 CHECK(!ValidNumber(rho));
1507 }
1508 SECTION("Subcritical p slightly subcooled should be ok") {
1509 double Ts, rho, dT = 1e-4;
1510 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1511 CAPTURE(Ts);
1512 CHECK(ValidNumber(Ts));
1513 CAPTURE(dT);
1514 CHECK_NOTHROW(rho = PropsSI("D", "T", Ts - dT, "P", 101325, "Water"));
1515 CAPTURE(rho);
1516 CHECK(ValidNumber(rho));
1517 }
1518 SECTION("Subcritical p slightly superheated should be ok") {
1519 double Ts, rho, dT = 1e-4;
1520 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1521 CAPTURE(Ts);
1522 CHECK(ValidNumber(Ts));
1523 CAPTURE(dT);
1524 CHECK_NOTHROW(rho = PropsSI("D", "T", Ts + dT, "P", 101325, "Water"));
1525 CAPTURE(rho);
1526 CHECK(ValidNumber(rho));
1527 }
1528}
1529
1530TEST_CASE("P,T flash at the critical point returns rhomolar_critical", "[flash],[PT],[critical_point],[2738]") {
1531 // At the critical point, dP/drho -> 0 so the generic density solver is ill-conditioned.
1532 // PT_flash should detect exact-critical inputs and return the tabulated critical density.
1533 for (const std::string fluid : {"CarbonDioxide", "Water", "R134a"}) {
1534 CAPTURE(fluid);
1535 std::shared_ptr<AbstractState> AS(AbstractState::factory("HEOS", fluid));
1536 double Tc = AS->T_critical();
1537 double pc = AS->p_critical();
1538 double rho_c = AS->rhomolar_critical();
1539 AS->update(PT_INPUTS, pc, Tc);
1540 CHECK(std::abs(AS->rhomolar() - rho_c) / rho_c < 1e-10);
1541 CHECK(AS->phase() == iphase_critical_point);
1542 }
1543 SECTION("Issue #2738 reproducer (high-level API for CO2)") {
1544 double Tc = Props1SI("CO2", "Tcrit");
1545 double pc = Props1SI("CO2", "Pcrit");
1546 double rho_crit = Props1SI("CO2", "rhomass_critical");
1547 double rho_pt = PropsSI("Dmass", "T", Tc, "P", pc, "CO2");
1548 CAPTURE(Tc);
1549 CAPTURE(pc);
1550 CAPTURE(rho_crit);
1551 CAPTURE(rho_pt);
1552 CHECK(ValidNumber(rho_pt));
1553 CHECK(std::abs(rho_pt - rho_crit) / rho_crit < 1e-10);
1554 }
1555}
1556
1557TEST_CASE("Tests for solvers in P,Y flash using Water", "[flash],[PH],[PS],[PU]") {
1558 double Ts, y, T2;
1559 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
1560 const std::vector<std::string> Ykeys = {"H", "S", "U", "Hmass", "Smass", "Umass", "Hmolar", "Smolar", "Umolar"};
1561 for (const auto& Ykey : Ykeys) {
1562 const char* ykey = Ykey.c_str();
1563 std::ostringstream ss1;
1564 ss1 << "Subcritical superheated P," << ykey;
1565 SECTION(ss1.str(), "") {
1566 double dT = 10;
1567 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1568 CHECK(ValidNumber(Ts));
1569 CAPTURE(Ts);
1570 CHECK_NOTHROW(y = PropsSI(ykey, "T", Ts + dT, "P", 101325, "Water"));
1571 CAPTURE(dT);
1572 CAPTURE(y);
1573 CHECK(ValidNumber(y));
1574 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", 101325, "Water"));
1575 CAPTURE(CoolProp::get_global_param_string("errstring"));
1576 CAPTURE(T2);
1577 CHECK(ValidNumber(T2));
1578 }
1579 std::ostringstream ss2;
1580 ss2 << "Subcritical barely superheated P," << ykey;
1581 SECTION(ss2.str(), "") {
1582 double dT = 1e-3;
1583 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1584 CHECK(ValidNumber(Ts));
1585 CAPTURE(Ts);
1586 CHECK_NOTHROW(y = PropsSI(ykey, "T", Ts + dT, "P", 101325, "Water"));
1587 CAPTURE(dT);
1588 CAPTURE(y);
1589 CHECK(ValidNumber(y));
1590 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", 101325, "Water"));
1591 CAPTURE(CoolProp::get_global_param_string("errstring"));
1592 CAPTURE(T2);
1593 CHECK(ValidNumber(T2));
1594 }
1595 std::ostringstream ss3;
1596 ss3 << "Subcritical subcooled P," << ykey;
1597 SECTION(ss3.str(), "") {
1598 double dT = -10;
1599 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1600 CHECK(ValidNumber(Ts));
1601 CAPTURE(Ts);
1602 CHECK_NOTHROW(y = PropsSI(ykey, "T", Ts + dT, "P", 101325, "Water"));
1603 CAPTURE(dT);
1604 CAPTURE(y);
1605 CHECK(ValidNumber(y));
1606 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", 101325, "Water"));
1607 CAPTURE(CoolProp::get_global_param_string("errstring"));
1608 CAPTURE(T2);
1609 CHECK(ValidNumber(T2));
1610 }
1611 std::ostringstream ss4;
1612 ss4 << "Subcritical barely subcooled P," << ykey;
1613 SECTION(ss4.str(), "") {
1614 double dT = -1e-3;
1615 CHECK_NOTHROW(Ts = PropsSI("T", "P", 101325, "Q", 0, "Water"));
1616 CHECK(ValidNumber(Ts));
1617 CAPTURE(Ts);
1618 CHECK_NOTHROW(y = PropsSI(ykey, "T", Ts + dT, "P", 101325, "Water"));
1619 CAPTURE(dT);
1620 CAPTURE(y);
1621 CHECK(ValidNumber(y));
1622 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", 101325, "Water"));
1623 CAPTURE(CoolProp::get_global_param_string("errstring"));
1624 CAPTURE(T2);
1625 CHECK(ValidNumber(T2));
1626 }
1627 std::ostringstream ss5;
1628 ss5 << "Supercritical P," << ykey;
1629 SECTION(ss5.str(), "") {
1630 double Tc = Props1SI("Water", "Tcrit");
1631 double pc = Props1SI("Water", "pcrit");
1632 double p = pc * 1.3;
1633 double T = Tc * 1.3;
1634 CAPTURE(T);
1635 CAPTURE(p);
1636 CHECK(ValidNumber(T));
1637 CHECK(ValidNumber(p));
1638 CHECK_NOTHROW(y = PropsSI(ykey, "P", p, "T", T, "Water"));
1639 CAPTURE(y);
1640 CHECK(ValidNumber(y));
1641 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", p, "Water"));
1642 CAPTURE(CoolProp::get_global_param_string("errstring"));
1643 CAPTURE(T2);
1644 CHECK(ValidNumber(T2));
1645 }
1646 std::ostringstream ss6;
1647 ss6 << "Supercritical \"gas\" P," << ykey;
1648 SECTION(ss6.str(), "") {
1649 double Tc = Props1SI("Water", "Tcrit");
1650 double pc = Props1SI("Water", "pcrit");
1651 double p = pc * 0.7;
1652 double T = Tc * 1.3;
1653 CAPTURE(T);
1654 CAPTURE(p);
1655 CHECK(ValidNumber(T));
1656 CHECK(ValidNumber(p));
1657 CHECK_NOTHROW(y = PropsSI(ykey, "P", p, "T", T, "Water"));
1658 CAPTURE(y);
1659 CHECK(ValidNumber(y));
1660 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", p, "Water"));
1661 CAPTURE(CoolProp::get_global_param_string("errstring"));
1662 CAPTURE(T2);
1663 CHECK(ValidNumber(T2));
1664 }
1665 std::ostringstream ss7;
1666 ss7 << "Supercritical \"liquid\" P," << ykey;
1667 SECTION(ss7.str(), "") {
1668 double Tc = Props1SI("Water", "Tcrit");
1669 double pc = Props1SI("Water", "pcrit");
1670 double p = pc * 2;
1671 double T = Tc * 0.5;
1672 CAPTURE(T);
1673 CAPTURE(p);
1674 CHECK(ValidNumber(T));
1675 CHECK(ValidNumber(p));
1676 CHECK_NOTHROW(y = PropsSI(ykey, "P", p, "T", T, "Water"));
1677 CAPTURE(y);
1678 CHECK(ValidNumber(y));
1679 CHECK_NOTHROW(T2 = PropsSI("T", ykey, y, "P", p, "Water"));
1680 CAPTURE(CoolProp::get_global_param_string("errstring"));
1681 CAPTURE(T2);
1682 CHECK(ValidNumber(T2));
1683 }
1684 }
1685}
1686
1687TEST_CASE("R134A saturation bug in dev", "[2545]") {
1688 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R134A"));
1689 AS->update(QT_INPUTS, 1, 273);
1690 double p = AS->p();
1691 CHECK(p == Catch::Approx(291215));
1692}
1693
1694TEST_CASE("Tests for solvers in P,H flash using Propane", "[flashdups],[flash],[PH],[consistency]") {
1695 double hmolar, hmass;
1696 SECTION("5 times PH with HEOS AbstractState yields same results every time", "") {
1697 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
1698
1699 CHECK_NOTHROW(AS->update(CoolProp::PT_INPUTS, 101325, 300));
1700 hmolar = AS->hmolar();
1701 hmass = AS->hmass();
1702 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1703 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1704 hmolar = AS->hmolar();
1705 hmass = AS->hmass();
1706 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1707 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1708 hmolar = AS->hmolar();
1709 hmass = AS->hmass();
1710 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1711 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1712 hmolar = AS->hmolar();
1713 hmass = AS->hmass();
1714 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1715 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1716 hmolar = AS->hmolar();
1717 hmass = AS->hmass();
1718 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1719 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1720 }
1721}
1722
1723TEST_CASE("Multiple calls to state class are consistent", "[flashdups],[flash],[PH],[consistency]") {
1724 double hmolar, hmass;
1725 SECTION("3 times PH with HEOS AbstractState yields same results every time", "") {
1726 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
1727
1728 CHECK_NOTHROW(AS->update(CoolProp::PT_INPUTS, 101325, 300));
1729 hmolar = AS->hmolar();
1730 hmass = AS->hmass();
1731 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1732 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1733 hmolar = AS->hmolar();
1734 hmass = AS->hmass();
1735 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1736 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1737 hmolar = AS->hmolar();
1738 hmass = AS->hmass();
1739 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 101325));
1740 CHECK_NOTHROW(AS->update(CoolProp::HmolarP_INPUTS, hmolar, 101325));
1741 }
1742}
1743
1744TEST_CASE("Test first partial derivatives using PropsSI", "[derivatives]") {
1745 double T = 300;
1746 SECTION("Check drhodp|T 3 ways", "") {
1747 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
1748 AS->update(CoolProp::PT_INPUTS, 101325, T);
1749
1750 double drhomolardp__T_AbstractState = AS->first_partial_deriv(CoolProp::iDmolar, CoolProp::iP, CoolProp::iT);
1751 double drhomolardp__T_PropsSI_num =
1752 (PropsSI("Dmolar", "T", T, "P", 101325 + 1e-3, "n-Propane") - PropsSI("Dmolar", "T", T, "P", 101325 - 1e-3, "n-Propane")) / (2 * 1e-3);
1753 double drhomolardp__T_PropsSI = PropsSI("d(Dmolar)/d(P)|T", "T", T, "P", 101325, "n-Propane");
1754
1755 CAPTURE(drhomolardp__T_AbstractState);
1756 CAPTURE(drhomolardp__T_PropsSI_num);
1757 CAPTURE(drhomolardp__T_PropsSI);
1758 double rel_err_exact = std::abs((drhomolardp__T_AbstractState - drhomolardp__T_PropsSI) / drhomolardp__T_PropsSI);
1759 double rel_err_approx = std::abs((drhomolardp__T_PropsSI_num - drhomolardp__T_PropsSI) / drhomolardp__T_PropsSI);
1760 CHECK(rel_err_exact < 1e-7);
1761 CHECK(rel_err_approx < 1e-7);
1762 }
1763 SECTION("Check drhodp|T 3 ways for water", "") {
1764 T = 80 + 273.15;
1765 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
1766 AS->update(CoolProp::PT_INPUTS, 101325, T);
1767
1768 double drhomolardp__T_AbstractState = AS->first_partial_deriv(CoolProp::iDmolar, CoolProp::iP, CoolProp::iT);
1769 double drhomolardp__T_PropsSI_num =
1770 (PropsSI("Dmolar", "T", T, "P", 101325 + 1, "Water") - PropsSI("Dmolar", "T", T, "P", 101325 - 1, "Water")) / (2 * 1);
1771 double drhomolardp__T_PropsSI = PropsSI("d(Dmolar)/d(P)|T", "T", T, "P", 101325, "Water");
1772
1773 CAPTURE(drhomolardp__T_AbstractState);
1774 CAPTURE(drhomolardp__T_PropsSI_num);
1775 CAPTURE(drhomolardp__T_PropsSI);
1776 double rel_err_exact = std::abs((drhomolardp__T_AbstractState - drhomolardp__T_PropsSI) / drhomolardp__T_PropsSI);
1777 double rel_err_approx = std::abs((drhomolardp__T_PropsSI_num - drhomolardp__T_PropsSI) / drhomolardp__T_PropsSI);
1778 CHECK(rel_err_exact < 1e-4);
1779 CHECK(rel_err_approx < 1e-4);
1780 }
1781 SECTION("Check dpdrho|T 3 ways for water", "") {
1782 T = 80 + 273.15;
1783 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
1784 AS->update(CoolProp::PT_INPUTS, 101325, T);
1785 CoolPropDbl rhomolar = AS->rhomolar();
1786 double dpdrhomolar__T_AbstractState = AS->first_partial_deriv(CoolProp::iP, CoolProp::iDmolar, CoolProp::iT);
1787 double dpdrhomolar__T_PropsSI_num =
1788 (PropsSI("P", "T", T, "Dmolar", rhomolar + 1e-3, "Water") - PropsSI("P", "T", T, "Dmolar", rhomolar - 1e-3, "Water")) / (2 * 1e-3);
1789 double dpdrhomolar__T_PropsSI = PropsSI("d(P)/d(Dmolar)|T", "T", T, "P", 101325, "Water");
1790 CAPTURE(rhomolar);
1791 CAPTURE(dpdrhomolar__T_AbstractState);
1792 CAPTURE(dpdrhomolar__T_PropsSI_num);
1793 CAPTURE(dpdrhomolar__T_PropsSI);
1794 double rel_err_exact = std::abs((dpdrhomolar__T_AbstractState - dpdrhomolar__T_PropsSI) / dpdrhomolar__T_PropsSI);
1795 double rel_err_approx = std::abs((dpdrhomolar__T_PropsSI_num - dpdrhomolar__T_PropsSI) / dpdrhomolar__T_PropsSI);
1796 CHECK(rel_err_exact < 1e-6);
1797 CHECK(rel_err_approx < 1e-6);
1798 }
1799 SECTION("Check dpdrho|T 3 ways for water using mass based", "") {
1800 T = 80 + 273.15;
1801 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
1802 AS->update(CoolProp::PT_INPUTS, 101325, T);
1803 CoolPropDbl rhomass = AS->rhomass();
1804 double dpdrhomass__T_AbstractState = AS->first_partial_deriv(CoolProp::iP, CoolProp::iDmass, CoolProp::iT);
1805 double dpdrhomass__T_PropsSI_num =
1806 (PropsSI("P", "T", T, "Dmass", rhomass + 1e-3, "Water") - PropsSI("P", "T", T, "Dmass", rhomass - 1e-3, "Water")) / (2 * 1e-3);
1807 double dpdrhomass__T_PropsSI = PropsSI("d(P)/d(Dmass)|T", "T", T, "P", 101325, "Water");
1808 CAPTURE(rhomass);
1809 CAPTURE(dpdrhomass__T_AbstractState);
1810 CAPTURE(dpdrhomass__T_PropsSI_num);
1811 CAPTURE(dpdrhomass__T_PropsSI);
1812 double rel_err_exact = std::abs((dpdrhomass__T_AbstractState - dpdrhomass__T_PropsSI) / dpdrhomass__T_PropsSI);
1813 double rel_err_approx = std::abs((dpdrhomass__T_PropsSI_num - dpdrhomass__T_PropsSI) / dpdrhomass__T_PropsSI);
1814 CHECK(rel_err_exact < 1e-7);
1815 CHECK(rel_err_approx < 1e-7);
1816 }
1817 SECTION("Invalid first partial derivatives", "") {
1818 CHECK(!ValidNumber(PropsSI("d()/d(P)|T", "T", 300, "P", 101325, "n-Propane")));
1819 CHECK(!ValidNumber(PropsSI("d(Dmolar)/d()|T", "T", 300, "P", 101325, "n-Propane")));
1820 CHECK(!ValidNumber(PropsSI("d(Dmolar)/d(P)|", "T", 300, "P", 101325, "n-Propane")));
1821 CHECK(!ValidNumber(PropsSI("d(XXXX)/d(P)|T", "T", 300, "P", 101325, "n-Propane")));
1822 CHECK(!ValidNumber(PropsSI("d(Dmolar)d(P)|T", "T", 300, "P", 101325, "n-Propane")));
1823 CHECK(!ValidNumber(PropsSI("d(Dmolar)/d(P)T", "T", 300, "P", 101325, "n-Propane")));
1824 CHECK(!ValidNumber(PropsSI("d(Bvirial)/d(P)T", "T", 300, "P", 101325, "n-Propane")));
1825 CHECK(!ValidNumber(PropsSI("d(Tcrit)/d(P)T", "T", 300, "P", 101325, "n-Propane")));
1826 }
1827}
1828
1829TEST_CASE("Test second partial derivatives", "[derivatives]") {
1830 double T = 300;
1831 SECTION("Check d2pdrho2|T 3 ways", "") {
1832 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
1833 double rhomolar = 60000;
1834 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T);
1835 double p = AS->p();
1836
1837 double d2pdrhomolar2__T_AbstractState =
1839 // Centered second derivative
1840 double del = 1e0;
1841 double d2pdrhomolar2__T_PropsSI_num =
1842 (PropsSI("P", "T", T, "Dmolar", rhomolar + del, "Water") - 2 * PropsSI("P", "T", T, "Dmolar", rhomolar, "Water")
1843 + PropsSI("P", "T", T, "Dmolar", rhomolar - del, "Water"))
1844 / pow(del, 2);
1845 double d2pdrhomolar2__T_PropsSI = PropsSI("d(d(P)/d(Dmolar)|T)/d(Dmolar)|T", "T", T, "Dmolar", rhomolar, "Water");
1846
1847 CAPTURE(d2pdrhomolar2__T_AbstractState);
1848 CAPTURE(d2pdrhomolar2__T_PropsSI_num);
1849 double rel_err_exact = std::abs((d2pdrhomolar2__T_AbstractState - d2pdrhomolar2__T_PropsSI) / d2pdrhomolar2__T_PropsSI);
1850 double rel_err_approx = std::abs((d2pdrhomolar2__T_PropsSI_num - d2pdrhomolar2__T_AbstractState) / d2pdrhomolar2__T_AbstractState);
1851 CHECK(rel_err_exact < 1e-5);
1852 CHECK(rel_err_approx < 1e-5);
1853 }
1854 SECTION("Valid second partial derivatives", "") {
1855 CHECK(ValidNumber(PropsSI("d(d(Hmolar)/d(P)|T)/d(T)|Dmolar", "T", 300, "P", 101325, "n-Propane")));
1856 }
1857 SECTION("Invalid second partial derivatives", "") {
1858 CHECK(!ValidNumber(PropsSI("d(d()/d(P)|T)/d()|", "T", 300, "P", 101325, "n-Propane")));
1859 CHECK(!ValidNumber(PropsSI("dd(Dmolar)/d()|T)|T", "T", 300, "P", 101325, "n-Propane")));
1860 }
1861 SECTION("Check derivatives with respect to T", "") {
1862 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Propane"));
1863 double rhomolar = 100, dT = 1e-1;
1864 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T);
1865
1866 // base state
1867 CoolPropDbl T0 = AS->T(), rhomolar0 = AS->rhomolar(), hmolar0 = AS->hmolar(), smolar0 = AS->smolar(), umolar0 = AS->umolar(), p0 = AS->p();
1868 CoolPropDbl dhdT_rho_ana = AS->first_partial_deriv(CoolProp::iHmolar, CoolProp::iT, CoolProp::iDmolar);
1869 CoolPropDbl d2hdT2_rho_ana = AS->second_partial_deriv(CoolProp::iHmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar);
1870 CoolPropDbl dsdT_rho_ana = AS->first_partial_deriv(CoolProp::iSmolar, CoolProp::iT, CoolProp::iDmolar);
1871 CoolPropDbl d2sdT2_rho_ana = AS->second_partial_deriv(CoolProp::iSmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar);
1872 CoolPropDbl dudT_rho_ana = AS->first_partial_deriv(CoolProp::iUmolar, CoolProp::iT, CoolProp::iDmolar);
1873 CoolPropDbl d2udT2_rho_ana = AS->second_partial_deriv(CoolProp::iUmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar);
1874 CoolPropDbl dpdT_rho_ana = AS->first_partial_deriv(CoolProp::iP, CoolProp::iT, CoolProp::iDmolar);
1875 CoolPropDbl d2pdT2_rho_ana = AS->second_partial_deriv(CoolProp::iP, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar);
1876
1877 // increment T
1878 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T + dT);
1879 CoolPropDbl Tpt = AS->T(), rhomolarpt = AS->rhomolar(), hmolarpt = AS->hmolar(), smolarpt = AS->smolar(), umolarpt = AS->umolar(),
1880 ppt = AS->p();
1881 // decrement T
1882 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T - dT);
1883 CoolPropDbl Tmt = AS->T(), rhomolarmt = AS->rhomolar(), hmolarmt = AS->hmolar(), smolarmt = AS->smolar(), umolarmt = AS->umolar(),
1884 pmt = AS->p();
1885
1886 CoolPropDbl dhdT_rho_num = (hmolarpt - hmolarmt) / (2 * dT);
1887 CoolPropDbl d2hdT2_rho_num = (hmolarpt - 2 * hmolar0 + hmolarmt) / pow(dT, 2);
1888 CoolPropDbl dsdT_rho_num = (smolarpt - smolarmt) / (2 * dT);
1889 CoolPropDbl d2sdT2_rho_num = (smolarpt - 2 * smolar0 + smolarmt) / pow(dT, 2);
1890 CoolPropDbl dudT_rho_num = (umolarpt - umolarmt) / (2 * dT);
1891 CoolPropDbl d2udT2_rho_num = (umolarpt - 2 * umolar0 + umolarmt) / pow(dT, 2);
1892 CoolPropDbl dpdT_rho_num = (ppt - pmt) / (2 * dT);
1893 CoolPropDbl d2pdT2_rho_num = (ppt - 2 * p0 + pmt) / pow(dT, 2);
1894
1895 CAPTURE(format("%0.15Lg", d2pdT2_rho_ana).c_str());
1896
1897 double tol = 1e-4;
1898 CHECK(std::abs((dhdT_rho_num - dhdT_rho_ana) / dhdT_rho_ana) < tol);
1899 CHECK(std::abs((d2hdT2_rho_num - d2hdT2_rho_ana) / d2hdT2_rho_ana) < tol);
1900 CHECK(std::abs((dpdT_rho_num - dpdT_rho_ana) / dpdT_rho_ana) < tol);
1901 CHECK(std::abs((d2pdT2_rho_num - d2pdT2_rho_ana) / d2pdT2_rho_ana) < tol);
1902 CHECK(std::abs((dsdT_rho_num - dsdT_rho_ana) / dsdT_rho_ana) < tol);
1903 CHECK(std::abs((d2sdT2_rho_num - d2sdT2_rho_ana) / d2sdT2_rho_ana) < tol);
1904 CHECK(std::abs((dudT_rho_num - dudT_rho_ana) / dudT_rho_ana) < tol);
1905 CHECK(std::abs((d2udT2_rho_num - d2udT2_rho_ana) / d2udT2_rho_ana) < tol);
1906 }
1907
1908 SECTION("Check derivatives with respect to rho", "") {
1909 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Propane"));
1910 double rhomolar = 100, drho = 1e-1;
1911 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T);
1912
1913 // base state
1914 CoolPropDbl T0 = AS->T(), rhomolar0 = AS->rhomolar(), hmolar0 = AS->hmolar(), smolar0 = AS->smolar(), umolar0 = AS->umolar(), p0 = AS->p();
1915 CoolPropDbl dhdrho_T_ana = AS->first_partial_deriv(CoolProp::iHmolar, CoolProp::iDmolar, CoolProp::iT);
1916 CoolPropDbl d2hdrho2_T_ana = AS->second_partial_deriv(CoolProp::iHmolar, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT);
1917 CoolPropDbl dsdrho_T_ana = AS->first_partial_deriv(CoolProp::iSmolar, CoolProp::iDmolar, CoolProp::iT);
1918 CoolPropDbl d2sdrho2_T_ana = AS->second_partial_deriv(CoolProp::iSmolar, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT);
1919 CoolPropDbl dudrho_T_ana = AS->first_partial_deriv(CoolProp::iUmolar, CoolProp::iDmolar, CoolProp::iT);
1920 CoolPropDbl d2udrho2_T_ana = AS->second_partial_deriv(CoolProp::iUmolar, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT);
1921 CoolPropDbl dpdrho_T_ana = AS->first_partial_deriv(CoolProp::iP, CoolProp::iDmolar, CoolProp::iT);
1922 CoolPropDbl d2pdrho2_T_ana = AS->second_partial_deriv(CoolProp::iP, CoolProp::iDmolar, CoolProp::iT, CoolProp::iDmolar, CoolProp::iT);
1923
1924 // increment rho
1925 AS->update(CoolProp::DmolarT_INPUTS, rhomolar + drho, T);
1926 CoolPropDbl Tpr = AS->T(), rhomolarpr = AS->rhomolar(), hmolarpr = AS->hmolar(), smolarpr = AS->smolar(), umolarpr = AS->umolar(),
1927 ppr = AS->p();
1928 // decrement rho
1929 AS->update(CoolProp::DmolarT_INPUTS, rhomolar - drho, T);
1930 CoolPropDbl Tmr = AS->T(), rhomolarmr = AS->rhomolar(), hmolarmr = AS->hmolar(), smolarmr = AS->smolar(), umolarmr = AS->umolar(),
1931 pmr = AS->p();
1932
1933 CoolPropDbl dhdrho_T_num = (hmolarpr - hmolarmr) / (2 * drho);
1934 CoolPropDbl d2hdrho2_T_num = (hmolarpr - 2 * hmolar0 + hmolarmr) / pow(drho, 2);
1935 CoolPropDbl dsdrho_T_num = (smolarpr - smolarmr) / (2 * drho);
1936 CoolPropDbl d2sdrho2_T_num = (smolarpr - 2 * smolar0 + smolarmr) / pow(drho, 2);
1937 CoolPropDbl dudrho_T_num = (umolarpr - umolarmr) / (2 * drho);
1938 CoolPropDbl d2udrho2_T_num = (umolarpr - 2 * umolar0 + umolarmr) / pow(drho, 2);
1939 CoolPropDbl dpdrho_T_num = (ppr - pmr) / (2 * drho);
1940 CoolPropDbl d2pdrho2_T_num = (ppr - 2 * p0 + pmr) / pow(drho, 2);
1941
1942 CAPTURE(format("%0.15Lg", d2pdrho2_T_ana).c_str());
1943
1944 double tol = 1e-4;
1945 CHECK(std::abs((dhdrho_T_num - dhdrho_T_ana) / dhdrho_T_ana) < tol);
1946 CHECK(std::abs((d2hdrho2_T_num - d2hdrho2_T_ana) / d2hdrho2_T_ana) < tol);
1947 CHECK(std::abs((dpdrho_T_num - dpdrho_T_ana) / dpdrho_T_ana) < tol);
1948 CHECK(std::abs((d2pdrho2_T_num - d2pdrho2_T_ana) / d2pdrho2_T_ana) < tol);
1949 CHECK(std::abs((dsdrho_T_num - dsdrho_T_ana) / dsdrho_T_ana) < tol);
1950 CHECK(std::abs((d2sdrho2_T_num - d2sdrho2_T_ana) / d2sdrho2_T_ana) < tol);
1951 CHECK(std::abs((dudrho_T_num - dudrho_T_ana) / dudrho_T_ana) < tol);
1952 CHECK(std::abs((d2udrho2_T_num - d2udrho2_T_ana) / d2udrho2_T_ana) < tol);
1953 }
1954 SECTION("Check second mixed partial(h,p) with respect to rho", "") {
1955 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Propane"));
1956 double dhmass = 1.0, T = 300;
1957 AS->update(CoolProp::QT_INPUTS, 0.0, T);
1958 double deriv1 = AS->first_partial_deriv(iDmass, iP, iHmass);
1959 double deriv_analyt = AS->second_partial_deriv(iDmass, iP, iHmass, iHmass, iP);
1960 double deriv_analyt2 = AS->second_partial_deriv(iDmass, iHmass, iP, iP, iHmass);
1961 AS->update(CoolProp::HmassP_INPUTS, AS->hmass() - 1, AS->p());
1962 double deriv2 = AS->first_partial_deriv(iDmass, iP, iHmass);
1963 double deriv_num = (deriv1 - deriv2) / dhmass;
1964 CAPTURE(deriv_num);
1965 CAPTURE(deriv_analyt);
1966
1967 double tol = 1e-4;
1968 CHECK(std::abs((deriv_num - deriv_analyt) / deriv_analyt) < tol);
1969 }
1970}
1971
1972TEST_CASE("REFPROP names for coolprop fluids", "[REFPROPName]") {
1973 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
1974
1975 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
1976 for (const auto& fluid : fluids) {
1977 std::ostringstream ss1;
1978 ss1 << "Check that REFPROP fluid name for fluid " << fluid << " is valid";
1979 SECTION(ss1.str(), "") {
1980 std::string RPName = get_fluid_param_string(fluid, "REFPROPName");
1981 CHECK(!RPName.empty());
1982 CAPTURE(RPName);
1983 if (!RPName.compare("N/A")) {
1984 break;
1985 }
1986 CHECK(ValidNumber(Props1SI("REFPROP::" + RPName, "molemass")));
1987 CHECK(ValidNumber(Props1SI(RPName, "molemass")));
1988 }
1989 }
1990}
1991TEST_CASE("Backwards compatibility for REFPROP v4 fluid name convention", "[REFPROP_backwards_compatibility]") {
1992 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
1993
1994 SECTION("REFPROP-", "") {
1995 double val = Props1SI("REFPROP-Water", "Tcrit");
1996 std::string err = get_global_param_string("errstring");
1997 CAPTURE(val);
1998 CAPTURE(err);
1999 CHECK(ValidNumber(val));
2000 }
2001 SECTION("REFPROP-MIX:", "") {
2002 double val = PropsSI("T", "P", 101325, "Q", 0, "REFPROP-MIX:Methane[0.5]&Ethane[0.5]");
2003 std::string err = get_global_param_string("errstring");
2004 CAPTURE(val);
2005 CAPTURE(err);
2006 CHECK(ValidNumber(val));
2007 }
2008}
2009
2010class AncillaryFixture
2011{
2012 public:
2013 std::string name;
2014 void run_checks() {
2015 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
2016 for (const auto& fluid : fluids) {
2017 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
2018 auto* rHEOS = dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
2019 if (!rHEOS->is_pure()) {
2020 continue;
2021 }
2022 do_sat(AS);
2023 }
2024 }
2025 void do_sat(shared_ptr<CoolProp::AbstractState>& AS) {
2026 // Integer-indexed (cert-flp30-c): f = 0.1, 0.5, 0.9 — exactly
2027 // what the original `for (double f = 0.1; f < 1; f += 0.4)`
2028 // produced.
2029 for (std::size_t k = 0; k < 3; ++k) {
2030 const double f = 0.1 + 0.4 * static_cast<double>(k);
2031 double Tc = AS->T_critical();
2032 double Tt = AS->Ttriple();
2033 double T = f * Tc + (1 - f) * Tt;
2034 name = strjoin(AS->fluid_names(), "&");
2035
2036 AS->update(CoolProp::QT_INPUTS, 0, T);
2037 check_rhoL(AS);
2038 check_pL(AS);
2039
2040 AS->update(CoolProp::QT_INPUTS, 1, T);
2041 check_rhoV(AS);
2042 check_pV(AS);
2043 }
2044 }
2045 void check_pL(const shared_ptr<CoolProp::AbstractState>& AS) {
2046 double p_EOS = AS->saturated_liquid_keyed_output(iP);
2047 double p_anc = AS->saturation_ancillary(CoolProp::iP, 0, CoolProp::iT, AS->T());
2048 double err = std::abs(p_EOS - p_anc) / p_anc;
2049 CAPTURE(name);
2050 CAPTURE("pL");
2051 CAPTURE(p_EOS);
2052 CAPTURE(p_anc);
2053 CAPTURE(AS->T());
2054 CHECK(err < 0.02);
2055 }
2056 void check_pV(const shared_ptr<CoolProp::AbstractState>& AS) {
2057 double p_EOS = AS->saturated_liquid_keyed_output(iP);
2058 double p_anc = AS->saturation_ancillary(CoolProp::iP, 1, CoolProp::iT, AS->T());
2059 double err = std::abs(p_EOS - p_anc) / p_anc;
2060 CAPTURE(name);
2061 CAPTURE("pV");
2062 CAPTURE(p_EOS);
2063 CAPTURE(p_anc);
2064 CAPTURE(AS->T());
2065 CHECK(err < 0.02);
2066 }
2067 void check_rhoL(const shared_ptr<CoolProp::AbstractState>& AS) {
2068 double rho_EOS = AS->saturated_liquid_keyed_output(iDmolar);
2069 double rho_anc = AS->saturation_ancillary(CoolProp::iDmolar, 0, CoolProp::iT, AS->T());
2070 double err = std::abs(rho_EOS - rho_anc) / rho_anc;
2071 CAPTURE("rhoL");
2072 CAPTURE(name);
2073 CAPTURE(rho_EOS);
2074 CAPTURE(rho_anc);
2075 CAPTURE(AS->T());
2076 CHECK(err < 0.03);
2077 }
2078 void check_rhoV(const shared_ptr<CoolProp::AbstractState>& AS) {
2079 double rho_EOS = AS->saturated_vapor_keyed_output(iDmolar);
2080 double rho_anc = AS->saturation_ancillary(CoolProp::iDmolar, 1, CoolProp::iT, AS->T());
2081 double err = std::abs(rho_EOS - rho_anc) / rho_anc;
2082 CAPTURE("rhoV");
2083 CAPTURE(name);
2084 CAPTURE(rho_EOS);
2085 CAPTURE(rho_anc);
2086 CAPTURE(AS->T());
2087 CHECK(err < 0.03);
2088 }
2089};
2090// Disabled because either they have a superancillary, and the ancillaries should not be used,
2091// or they are a pure fluid and superancillaries are not developed
2092//TEST_CASE_METHOD(AncillaryFixture, "Ancillary functions", "[ancillary]") {
2093// run_checks();
2094//};
2095
2096TEST_CASE("Triple point checks", "[triple_point]") {
2097 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
2098 for (const auto& fluid : fluids) {
2099 std::vector<std::string> names(1, fluid);
2100 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> HEOS = std::make_shared<CoolProp::HelmholtzEOSMixtureBackend>(names);
2101 // Skip pseudo-pure
2102 if (!HEOS->is_pure()) {
2103 continue;
2104 }
2105
2106 std::ostringstream ss1;
2107 ss1 << "Minimum saturation temperature state matches for liquid " << fluid;
2108 SECTION(ss1.str(), "") {
2109 REQUIRE_NOTHROW(HEOS->update(CoolProp::QT_INPUTS, 0, HEOS->Ttriple()));
2110 double p_EOS = HEOS->p();
2111 double p_sat_min_liquid = HEOS->get_components()[0].EOS().sat_min_liquid.p;
2112 double err_sat_min_liquid = std::abs(p_EOS - p_sat_min_liquid) / p_sat_min_liquid;
2113 CAPTURE(p_EOS);
2114 CAPTURE(p_sat_min_liquid);
2115 CAPTURE(err_sat_min_liquid);
2116 if (p_EOS < 1e-3) {
2117 continue;
2118 } // Skip very low pressure below 1 mPa
2119 CHECK(err_sat_min_liquid < 1e-3);
2120 }
2121 std::ostringstream ss2;
2122 ss2 << "Minimum saturation temperature state matches for vapor " << fluid;
2123 SECTION(ss2.str(), "") {
2124 REQUIRE_NOTHROW(HEOS->update(CoolProp::QT_INPUTS, 1, HEOS->Ttriple()));
2125
2126 double p_EOS = HEOS->p();
2127 double p_sat_min_vapor = HEOS->get_components()[0].EOS().sat_min_vapor.p;
2128 double err_sat_min_vapor = std::abs(p_EOS - p_sat_min_vapor) / p_sat_min_vapor;
2129 CAPTURE(p_EOS);
2130 CAPTURE(p_sat_min_vapor);
2131 CAPTURE(err_sat_min_vapor);
2132 if (p_EOS < 1e-3) {
2133 continue;
2134 } // Skip very low pressure below 1 mPa
2135 CHECK(err_sat_min_vapor < 1e-3);
2136 }
2137 std::ostringstream ss3;
2138 ss3 << "Minimum saturation temperature state matches for vapor " << fluid;
2139 SECTION(ss3.str(), "") {
2140 if (HEOS->p_triple() < 10) {
2141 continue;
2142 }
2143 REQUIRE_NOTHROW(HEOS->update(CoolProp::PQ_INPUTS, HEOS->p_triple(), 1));
2144
2145 double T_EOS = HEOS->T();
2146 double T_sat_min_vapor = HEOS->get_components()[0].EOS().sat_min_vapor.T;
2147 double err_sat_min_vapor = std::abs(T_EOS - T_sat_min_vapor);
2148 CAPTURE(T_EOS);
2149 CAPTURE(T_sat_min_vapor);
2150 CAPTURE(err_sat_min_vapor);
2151 CHECK(err_sat_min_vapor < 1e-3);
2152 }
2153 std::ostringstream ss4;
2154 ss4 << "Minimum saturation temperature state matches for liquid " << fluid;
2155 SECTION(ss4.str(), "") {
2156 if (HEOS->p_triple() < 10) {
2157 continue;
2158 }
2159 REQUIRE_NOTHROW(HEOS->update(CoolProp::PQ_INPUTS, HEOS->p_triple(), 0));
2160 double T_EOS = HEOS->T();
2161 double T_sat_min_vapor = HEOS->get_components()[0].EOS().sat_min_vapor.T;
2162 double err_sat_min_vapor = std::abs(T_EOS - T_sat_min_vapor);
2163 CAPTURE(T_EOS);
2164 CAPTURE(T_sat_min_vapor);
2165 CAPTURE(err_sat_min_vapor);
2166 CHECK(err_sat_min_vapor < 1e-3);
2167 }
2168 // std::ostringstream ss2;
2169 // ss2 << "Liquid density error < 3% for fluid " << fluids[i] << " at " << T << " K";
2170 // SECTION(ss2.str(), "")
2171 // {
2172 // double rho_EOS = AS->rhomolar();
2173 // double rho_anc = AS->saturation_ancillary(CoolProp::iDmolar, 0, CoolProp::iT, T);
2174 // double err = std::abs(rho_EOS-rho_anc)/rho_anc;
2175 // CAPTURE(rho_EOS);
2176 // CAPTURE(rho_anc);
2177 // CAPTURE(T);
2178 // CHECK(err < 0.03);
2179 // }
2180 // std::ostringstream ss3;
2181 // ss3 << "Vapor density error < 3% for fluid " << fluids[i] << " at " << T << " K";
2182 // SECTION(ss3.str(), "")
2183 // {
2184 // double rho_EOS = AS->rhomolar();
2185 // double rho_anc = AS->saturation_ancillary(CoolProp::iDmolar, 1, CoolProp::iT, T);
2186 // double err = std::abs(rho_EOS-rho_anc)/rho_anc;
2187 // CAPTURE(rho_EOS);
2188 // CAPTURE(rho_anc);
2189 // CAPTURE(T);
2190 // CHECK(err < 0.03);
2191 // }
2192 }
2193}
2194
2195class SatTFixture
2196{
2197 public:
2198 std::string name;
2199 double Tc;
2200 void run_checks() {
2201 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
2202 for (const auto& fluid : fluids) {
2203 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
2204 auto* rHEOS = dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
2205 if (!rHEOS->is_pure()) {
2206 continue;
2207 }
2208 do_sat(AS);
2209 }
2210 }
2211 void do_sat(shared_ptr<CoolProp::AbstractState>& AS) {
2212 Tc = AS->T_critical();
2213 name = strjoin(AS->fluid_names(), "&");
2214 check_at_Tc(AS);
2215 double Tt = AS->Ttriple();
2216 if (AS->fluid_param_string("pure") == "true") {
2217 Tc = std::min(Tc, AS->T_reducing());
2218 }
2219 // Geometric scaling (j /= 10) — not summation; 9 well-defined
2220 // iters from 0.1 down to 1e-10.
2221 for (double j = 0.1; j > 1e-10; j /= 10) { // NOLINT(cert-flp30-c)
2222 check_QT(AS, Tc - j);
2223 }
2224 }
2225 void check_at_Tc(const shared_ptr<CoolProp::AbstractState>& AS) {
2226 CAPTURE("Check @ Tc");
2227 CAPTURE(name);
2228 CHECK_NOTHROW(AS->update(QT_INPUTS, 0, Tc));
2229 }
2230 void check_QT(const shared_ptr<CoolProp::AbstractState>& AS, double T) {
2231 std::string test_name = "Check --> Tc";
2232 CAPTURE(test_name);
2233 CAPTURE(name);
2234 CAPTURE(T);
2235 CHECK_NOTHROW(AS->update(QT_INPUTS, 0, T));
2236 }
2237};
2238TEST_CASE_METHOD(SatTFixture, "Test that saturation solvers solve all the way to T = Tc", "[sat_T_to_Tc]") {
2239 run_checks();
2240};
2241
2242TEST_CASE("Check mixtures with fluid name aliases", "[mixture_name_aliasing]") {
2243 shared_ptr<CoolProp::AbstractState> AS1, AS2;
2244 AS1.reset(CoolProp::AbstractState::factory("HEOS", "EBENZENE&P-XYLENE"));
2245 AS2.reset(CoolProp::AbstractState::factory("HEOS", "EthylBenzene&P-XYLENE"));
2246 REQUIRE(AS1->fluid_names().size() == AS2->fluid_names().size());
2247 std::size_t N = AS1->fluid_names().size();
2248 for (std::size_t i = 0; i < N; ++i) {
2249 CAPTURE(i);
2250 CHECK(AS1->fluid_names()[i] == AS2->fluid_names()[i]);
2251 }
2252}
2253
2254TEST_CASE("Predefined mixtures", "[predefined_mixtures]") {
2255 SECTION("PropsSI") {
2256 double val = PropsSI("Dmolar", "P", 101325, "T", 300, "Air.mix");
2257 std::string err = get_global_param_string("errstring");
2258 CAPTURE(val);
2259 CAPTURE(err);
2260 CHECK(ValidNumber(val));
2261 }
2262}
2263TEST_CASE("Test that reference states yield proper values using high-level interface", "[reference_states]") {
2264 struct ref_entry
2265 {
2266 std::string name;
2267 double hmass, smass;
2268 std::string in1;
2269 double val1;
2270 std::string in2;
2271 double val2;
2272 };
2273 std::string fluids[] = {"n-Propane", "R134a", "R124"};
2274 ref_entry entries[3] = {{"IIR", 200000, 1000, "T", 273.15, "Q", 0}, {"ASHRAE", 0, 0, "T", 233.15, "Q", 0}, {"NBP", 0, 0, "P", 101325, "Q", 0}};
2275 for (const auto& fluid : fluids) {
2276 for (auto& entry : entries) {
2277 std::ostringstream ss1;
2278 ss1 << "Check state for " << fluid << " for " + entry.name + " reference state ";
2279 SECTION(ss1.str(), "") {
2280 // First reset the reference state
2281 set_reference_stateS(fluid, "DEF");
2282 // Then set to desired reference state
2283 set_reference_stateS(fluid, entry.name);
2284 // Calculate the values
2285 double hmass = PropsSI("Hmass", entry.in1, entry.val1, entry.in2, entry.val2, fluid);
2286 double smass = PropsSI("Smass", entry.in1, entry.val1, entry.in2, entry.val2, fluid);
2287 CHECK(std::abs(hmass - entry.hmass) < 1e-8);
2288 CHECK(std::abs(smass - entry.smass) < 1e-8);
2289 // Then reset the reference state
2290 set_reference_stateS(fluid, "DEF");
2291 }
2292 }
2293 }
2294}
2295TEST_CASE("Test that reference states yield proper values using low-level interface", "[reference_states]") {
2296 struct ref_entry
2297 {
2298 std::string name;
2299 double hmass, smass;
2300 parameters in1;
2301 double val1;
2302 parameters in2;
2303 double val2;
2304 };
2305 std::string fluids[] = {"n-Propane", "R134a", "R124"};
2306 ref_entry entries[3] = {{"IIR", 200000, 1000, iT, 273.15, iQ, 0}, {"ASHRAE", 0, 0, iT, 233.15, iQ, 0}, {"NBP", 0, 0, iP, 101325, iQ, 0}};
2307 for (const auto& fluid : fluids) {
2308 for (auto& entry : entries) {
2309 std::ostringstream ss1;
2310 ss1 << "Check state for " << fluid << " for " + entry.name + " reference state ";
2311 SECTION(ss1.str(), "") {
2312 double val1, val2;
2313 input_pairs pair = generate_update_pair(entry.in1, entry.val1, entry.in2, entry.val2, val1, val2);
2314 // Generate a state instance
2315 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
2316 AS->update(pair, val1, val2);
2317 double hmass0 = AS->hmass();
2318 double smass0 = AS->smass();
2319 // First reset the reference state
2320 set_reference_stateS(fluid, "DEF");
2321 AS->update(pair, val1, val2);
2322 double hmass00 = AS->hmass();
2323 double smass00 = AS->smass();
2324 CHECK(std::abs(hmass00 - hmass0) < 1e-10);
2325 CHECK(std::abs(smass00 - smass0) < 1e-10);
2326
2327 // Then set to desired reference state
2328 set_reference_stateS(fluid, entry.name);
2329
2330 // Should not change existing instance
2331 AS->clear();
2332 AS->update(pair, val1, val2);
2333 double hmass1 = AS->hmass();
2334 double smass1 = AS->smass();
2335 CHECK(std::abs(hmass1 - hmass0) < 1e-10);
2336 CHECK(std::abs(smass1 - smass0) < 1e-10);
2337
2338 // New instance - should get updated reference state
2339 shared_ptr<CoolProp::AbstractState> AS2(CoolProp::AbstractState::factory("HEOS", fluid));
2340 AS2->update(pair, val1, val2);
2341 double hmass2 = AS2->hmass();
2342 double smass2 = AS2->smass();
2343 CHECK(std::abs(hmass2 - entry.hmass) < 1e-8);
2344 CHECK(std::abs(smass2 - entry.smass) < 1e-8);
2345
2346 // Then reset the reference state
2347 set_reference_stateS(fluid, "DEF");
2348 }
2349 }
2350 }
2351}
2352
2353class FixedStateFixture
2354{
2355 public:
2356 void run_fluid(const std::string& fluid, const std::string& state, const std::string& ref_state) {
2357
2358 // Skip impossible reference states
2359 if (Props1SI("Ttriple", fluid) > 233.15 && ref_state == "ASHRAE") {
2360 return;
2361 }
2362 if (Props1SI("Tcrit", fluid) < 233.15 && ref_state == "ASHRAE") {
2363 return;
2364 }
2365 if (Props1SI("Tcrit", fluid) < 273.15 && ref_state == "IIR") {
2366 return;
2367 }
2368 if (Props1SI("Ttriple", fluid) > 273.15 && ref_state == "IIR") {
2369 return;
2370 }
2371 if (Props1SI("ptriple", fluid) > 101325 && ref_state == "NBP") {
2372 return;
2373 }
2374
2375 // First reset the reference state
2376 if (ref_state != "DEF") {
2377 set_reference_stateS(fluid, "DEF");
2378 try {
2379 // Then try to set to the specified reference state
2380 set_reference_stateS(fluid, ref_state);
2381 } catch (std::exception& e) {
2382 // Then set the reference state back to the default
2383 set_reference_stateS(fluid, "DEF");
2384 CAPTURE(e.what());
2385 REQUIRE(false);
2386 }
2387 }
2388
2389 std::ostringstream name;
2390 name << "Check state for " << state << " for " << fluid << " for reference state " << ref_state;
2391 CAPTURE(name.str());
2392
2393 std::vector<std::string> fl(1, fluid);
2394 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> HEOS = std::make_shared<CoolProp::HelmholtzEOSMixtureBackend>(fl);
2395
2396 // Skip the saturation maxima states for pure fluids
2397 if (HEOS->is_pure() && (state == "max_sat_T" || state == "max_sat_p")) {
2398 return;
2399 }
2400
2401 // Get the state
2402 CoolProp::SimpleState _state = HEOS->calc_state(state);
2403 HEOS->specify_phase(iphase_gas); // something homogenous
2404 // Bump a tiny bit for EOS with non-analytic parts
2405 double f = 1.0;
2406 if ((fluid == "Water" || fluid == "CarbonDioxide") && (state == "reducing" || state == "critical")) {
2407 f = 1.00001;
2408 }
2409 HEOS->update(CoolProp::DmolarT_INPUTS, _state.rhomolar * f, _state.T * f);
2410 CAPTURE(_state.hmolar);
2411 CAPTURE(_state.smolar);
2412 CHECK(ValidNumber(_state.hmolar));
2413 CHECK(ValidNumber(_state.smolar));
2414 double EOS_hmolar = HEOS->hmolar();
2415 double EOS_smolar = HEOS->smolar();
2416 CAPTURE(EOS_hmolar);
2417 CAPTURE(EOS_smolar);
2418 CHECK(std::abs(EOS_hmolar - _state.hmolar) < 1e-2);
2419 CHECK(std::abs(EOS_smolar - _state.smolar) < 1e-2);
2420 // Then set the reference state back to the default
2421 set_reference_stateS(fluid, "DEF");
2422 };
2423 void run_checks() {
2424
2425 std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"), ',');
2426 for (const auto& fluid : fluids) {
2427 std::string ref_state[4] = {"DEF", "IIR", "ASHRAE", "NBP"};
2428 for (const auto& j : ref_state) {
2429 std::string states[] = {"hs_anchor", "reducing", "critical", "max_sat_T", "max_sat_p", "triple_liquid", "triple_vapor"};
2430 for (const auto& state : states) {
2431 run_fluid(fluid, state, j);
2432 }
2433 }
2434 }
2435 }
2436};
2437TEST_CASE_METHOD(FixedStateFixture, "Test that enthalpies and entropies are correct for fixed states for all reference states", "[fixed_states]") {
2438 run_checks();
2439}; // !!!! check this
2440
2441TEST_CASE("Check the first partial derivatives", "[first_saturation_partial_deriv]") {
2442 const int number_of_pairs = 10;
2443 struct pair
2444 {
2445 parameters p1, p2;
2446 };
2447 pair pairs[number_of_pairs] = {{iP, iT}, {iDmolar, iT}, {iHmolar, iT}, {iSmolar, iT}, {iUmolar, iT},
2448 {iT, iP}, {iDmolar, iP}, {iHmolar, iP}, {iSmolar, iP}, {iUmolar, iP}};
2449 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
2450 for (auto& pair : pairs) {
2451 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
2452 std::ostringstream ss1;
2453 ss1 << "Check first partial derivative for d(" << get_parameter_information(pair.p1, "short") << ")/d("
2454 << get_parameter_information(pair.p2, "short") << ")|sat";
2455 SECTION(ss1.str(), "") {
2456 AS->update(QT_INPUTS, 1, 300);
2457 CoolPropDbl p = AS->p();
2458 CoolPropDbl analytical = AS->first_saturation_deriv(pair.p1, pair.p2);
2459 CAPTURE(analytical);
2460 CoolPropDbl numerical;
2461 if (pair.p2 == iT) {
2462 AS->update(QT_INPUTS, 1, 300 + 1e-5);
2463 CoolPropDbl v1 = AS->keyed_output(pair.p1);
2464 AS->update(QT_INPUTS, 1, 300 - 1e-5);
2465 CoolPropDbl v2 = AS->keyed_output(pair.p1);
2466 numerical = (v1 - v2) / (2e-5);
2467 } else if (pair.p2 == iP) {
2468 AS->update(PQ_INPUTS, p + 1e-2, 1);
2469 CoolPropDbl v1 = AS->keyed_output(pair.p1);
2470 AS->update(PQ_INPUTS, p - 1e-2, 1);
2471 CoolPropDbl v2 = AS->keyed_output(pair.p1);
2472 numerical = (v1 - v2) / (2e-2);
2473 } else {
2474 throw ValueError();
2475 }
2476 CAPTURE(numerical);
2477 CHECK(std::abs(numerical / analytical - 1) < 1e-4);
2478 }
2479 }
2480}
2481
2482TEST_CASE("Check the second saturation derivatives", "[second_saturation_partial_deriv]") {
2483 const int number_of_pairs = 5;
2484 struct pair
2485 {
2486 parameters p1, p2, p3;
2487 };
2488 pair pairs[number_of_pairs] = {{iT, iP, iP}, {iDmolar, iP, iP}, {iHmolar, iP, iP}, {iSmolar, iP, iP}, {iUmolar, iP, iP}};
2489 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
2490 for (auto& pair : pairs) {
2491 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
2492 std::ostringstream ss1;
2493 ss1 << "Check second saturation derivative for d2(" << get_parameter_information(pair.p1, "short") << ")/d("
2494 << get_parameter_information(pair.p2, "short") << ")2|sat";
2495 SECTION(ss1.str(), "") {
2496 AS->update(QT_INPUTS, 1, 300);
2497 CoolPropDbl p = AS->p();
2498 CoolPropDbl analytical = AS->second_saturation_deriv(pair.p1, pair.p2, pair.p3);
2499 CAPTURE(analytical);
2500 CoolPropDbl numerical;
2501 if (pair.p2 == iT) {
2502 throw NotImplementedError();
2503 } else if (pair.p2 == iP) {
2504 AS->update(PQ_INPUTS, p + 1e-2, 1);
2505 CoolPropDbl v1 = AS->first_saturation_deriv(pair.p1, pair.p2);
2506 AS->update(PQ_INPUTS, p - 1e-2, 1);
2507 CoolPropDbl v2 = AS->first_saturation_deriv(pair.p1, pair.p2);
2508 numerical = (v1 - v2) / (2e-2);
2509 } else {
2510 throw ValueError();
2511 }
2512 CAPTURE(numerical);
2513 CHECK(std::abs(numerical / analytical - 1) < 1e-4);
2514 }
2515 }
2516}
2517
2518TEST_CASE("Check the first two-phase derivative", "[first_two_phase_deriv]") {
2519 const int number_of_pairs = 4;
2520 struct pair
2521 {
2522 parameters p1, p2, p3;
2523 };
2524 pair pairs[number_of_pairs] = {{iDmass, iP, iHmass}, {iDmolar, iP, iHmolar}, {iDmolar, iHmolar, iP}, {iDmass, iHmass, iP}};
2525 shared_ptr<CoolProp::HelmholtzEOSBackend> AS = std::make_shared<CoolProp::HelmholtzEOSBackend>("n-Propane");
2526 for (auto& pair : pairs) {
2527 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
2528 std::ostringstream ss1;
2529 ss1 << "for (" << get_parameter_information(pair.p1, "short") << ", " << get_parameter_information(pair.p2, "short") << ", "
2530 << get_parameter_information(pair.p3, "short") << ")";
2531 SECTION(ss1.str(), "") {
2532 AS->update(QT_INPUTS, 0.3, 300);
2533 CoolPropDbl numerical;
2534 CoolPropDbl analytical = AS->first_two_phase_deriv(pair.p1, pair.p2, pair.p3);
2535 CAPTURE(analytical);
2536
2537 CoolPropDbl out1, out2;
2538 CoolPropDbl v2base, v3base;
2539 v2base = AS->keyed_output(pair.p2);
2540 v3base = AS->keyed_output(pair.p3);
2541 CoolPropDbl v2plus = v2base * 1.001;
2542 CoolPropDbl v2minus = v2base * 0.999;
2543 CoolProp::input_pairs input_pair1 = generate_update_pair(pair.p2, v2plus, pair.p3, v3base, out1, out2);
2544 AS->update(input_pair1, out1, out2);
2545 CoolPropDbl v1 = AS->keyed_output(pair.p1);
2546 CoolProp::input_pairs input_pair2 = generate_update_pair(pair.p2, v2minus, pair.p3, v3base, out1, out2);
2547 AS->update(input_pair2, out1, out2);
2548 CoolPropDbl v2 = AS->keyed_output(pair.p1);
2549
2550 numerical = (v1 - v2) / (v2plus - v2minus);
2551 CAPTURE(numerical);
2552 CHECK(std::abs(numerical / analytical - 1) < 1e-4);
2553 }
2554 }
2555}
2556
2557TEST_CASE("Check the second two-phase derivative", "[second_two_phase_deriv]") {
2558 SECTION("d2rhodhdp", "") {
2559 shared_ptr<CoolProp::HelmholtzEOSBackend> AS = std::make_shared<CoolProp::HelmholtzEOSBackend>("n-Propane");
2560 AS->update(QT_INPUTS, 0.3, 300);
2561 CoolPropDbl analytical = AS->second_two_phase_deriv(iDmolar, iHmolar, iP, iP, iHmolar);
2562 CAPTURE(analytical);
2563 CoolPropDbl pplus = AS->p() * 1.001, pminus = AS->p() * 0.999, h = AS->hmolar();
2564 AS->update(HmolarP_INPUTS, h, pplus);
2565 CoolPropDbl v1 = AS->first_two_phase_deriv(iDmolar, iHmolar, iP);
2566 AS->update(HmolarP_INPUTS, h, pminus);
2567 CoolPropDbl v2 = AS->first_two_phase_deriv(iDmolar, iHmolar, iP);
2568 CoolPropDbl numerical = (v1 - v2) / (pplus - pminus);
2569 CAPTURE(numerical);
2570 CHECK(std::abs(numerical / analytical - 1) < 1e-6);
2571 }
2572 SECTION("d2rhodhdp using mass", "") {
2573 shared_ptr<CoolProp::HelmholtzEOSBackend> AS = std::make_shared<CoolProp::HelmholtzEOSBackend>("n-Propane");
2574 AS->update(QT_INPUTS, 0.3, 300);
2575 CoolPropDbl analytical = AS->second_two_phase_deriv(iDmass, iHmass, iP, iP, iHmass);
2576 CAPTURE(analytical);
2577 CoolPropDbl pplus = AS->p() * 1.001, pminus = AS->p() * 0.999, h = AS->hmass();
2578 AS->update(HmassP_INPUTS, h, pplus);
2579 CoolPropDbl v1 = AS->first_two_phase_deriv(iDmass, iHmass, iP);
2580 AS->update(HmassP_INPUTS, h, pminus);
2581 CoolPropDbl v2 = AS->first_two_phase_deriv(iDmass, iHmass, iP);
2582 CoolPropDbl numerical = (v1 - v2) / (pplus - pminus);
2583 CAPTURE(numerical);
2584 CHECK(std::abs(numerical / analytical - 1) < 1e-6);
2585 }
2586}
2587
2588TEST_CASE("Check the first two-phase derivative using splines", "[first_two_phase_deriv_splined]") {
2637 using paramtuple = std::tuple<parameters, parameters, parameters>;
2638
2639 SECTION("Compared with reference data") {
2640
2641 std::map<paramtuple, double> pairs = {{{iDmass, iP, iHmass}, 0.00056718665544440146},
2642 {{iDmass, iHmass, iP}, -0.0054665229407696173},
2643 {{iDmass, iDmass, iDmass}, 179.19799206447755}};
2644
2645 std::unique_ptr<CoolProp::HelmholtzEOSBackend> AS(new CoolProp::HelmholtzEOSBackend("n-Propane"));
2646 for (auto& [pair, expected_value] : pairs) {
2647 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
2648 std::ostringstream ss1;
2649 auto& [p1, p2, p3] = pair;
2650 ss1 << "for (" << get_parameter_information(p1, "short") << ", " << get_parameter_information(p2, "short") << ", "
2651 << get_parameter_information(p3, "short") << ")";
2652 double x_end = 0.3;
2653 SECTION(ss1.str(), "") {
2654 AS->update(QT_INPUTS, 0.2, 300);
2655 CoolPropDbl analytical = AS->first_two_phase_deriv_splined(p1, p2, p3, x_end);
2656 CAPTURE(analytical);
2657 CHECK(std::abs(expected_value / analytical - 1) < 1e-8);
2658 }
2659 }
2660 }
2661 SECTION("Finite diffs") {
2662 std::vector<paramtuple> pairs = {{iDmass, iHmass, iP}, {iDmolar, iHmolar, iP}}; //, {iDmass, iHmass, iP}};
2663 std::unique_ptr<CoolProp::HelmholtzEOSBackend> AS(new CoolProp::HelmholtzEOSBackend("n-Propane"));
2664 for (auto& pair : pairs) {
2665 // See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
2666 std::ostringstream ss1;
2667 auto& [p1, p2, p3] = pair;
2668 ss1 << "for (" << get_parameter_information(p1, "short") << ", " << get_parameter_information(p2, "short") << ", "
2669 << get_parameter_information(p3, "short") << ")";
2670 double x_end = 0.3;
2671 SECTION(ss1.str(), "") {
2672 AS->update(QT_INPUTS, 0.2, 300);
2673 CoolPropDbl numerical;
2674 CoolPropDbl analytical = AS->first_two_phase_deriv_splined(p1, p2, p3, x_end);
2675 CAPTURE(analytical);
2676
2677 CoolPropDbl out1, out2;
2678 CoolPropDbl v2base, v3base;
2679 v2base = AS->keyed_output(p2);
2680 v3base = AS->keyed_output(p3);
2681 CoolPropDbl v2plus = v2base * 1.00001;
2682 CoolPropDbl v2minus = v2base * 0.99999;
2683
2684 // Get the density (molar or specific) for the second variable shifted up with the third variable
2685 // held constant
2686 CoolProp::input_pairs input_pair1 = generate_update_pair(p2, v2plus, p3, v3base, out1, out2);
2687 AS->update(input_pair1, out1, out2);
2688 CoolPropDbl D1 = AS->first_two_phase_deriv_splined(p1, p1, p1, x_end);
2689
2690 // Get the density (molar or specific) for the second variable shifted down with the third variable
2691 // held constant
2692 CoolProp::input_pairs input_pair2 = generate_update_pair(p2, v2minus, p3, v3base, out1, out2);
2693 AS->update(input_pair2, out1, out2);
2694 CoolPropDbl D2 = AS->first_two_phase_deriv_splined(p1, p1, p1, x_end);
2695
2696 numerical = (D1 - D2) / (v2plus - v2minus);
2697 CAPTURE(numerical);
2698 CHECK(std::abs(numerical / analytical - 1) < 1e-8);
2699 }
2700 }
2701 }
2702}
2703
2704TEST_CASE("Check the phase flags", "[phase]") {
2705 SECTION("subcooled liquid") {
2706 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
2707 AS->update(PT_INPUTS, 101325, 300);
2708 CHECK(AS->phase() == iphase_liquid);
2709 }
2710 SECTION("superheated gas") {
2711 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
2712 AS->update(PT_INPUTS, 101325, 400);
2713 CHECK(AS->phase() == iphase_gas);
2714 }
2715 SECTION("supercritical gas") {
2716 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
2717 AS->update(PT_INPUTS, 1e5, 800);
2718 CHECK(AS->phase() == iphase_supercritical_gas);
2719 }
2720 SECTION("supercritical liquid") {
2721 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
2722 AS->update(PT_INPUTS, 1e8, 500);
2723 CHECK(AS->phase() == iphase_supercritical_liquid);
2724 }
2725 SECTION("supercritical") {
2726 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
2727 AS->update(PT_INPUTS, 1e8, 800);
2728 CHECK(AS->phase() == iphase_supercritical);
2729 }
2730}
2731
2732TEST_CASE("Check the changing of reducing function constants", "[reducing]") {
2733 double z0 = 0.2;
2734 std::vector<double> z(2);
2735 z[0] = z0;
2736 z[1] = 1 - z[0];
2737 shared_ptr<CoolProp::AbstractState> AS1(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane"));
2738 shared_ptr<CoolProp::AbstractState> AS2(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane"));
2739 AS1->set_mole_fractions(z);
2740 AS2->set_mole_fractions(z);
2741 std::vector<CoolProp::CriticalState> pts1 = AS1->all_critical_points();
2742 double gammaT = AS2->get_binary_interaction_double(0, 1, "gammaT");
2743 AS2->set_binary_interaction_double(0, 1, "gammaT", gammaT * 0.7);
2744 std::vector<CoolProp::CriticalState> pts2 = AS2->all_critical_points();
2745 double Tdiff = abs(pts2[0].T - pts1[0].T);
2746 CHECK(Tdiff > 1e-3); // Make sure that it actually got the change to the interaction parameters
2747}
2748
2749TEST_CASE("Check the PC-SAFT pressure function", "[pcsaft_pressure]") {
2750 double p = 101325.;
2751 double p_calc = CoolProp::PropsSI("P", "T", 320., "Dmolar", 9033.114359706229, "PCSAFT::TOLUENE");
2752 CHECK(abs((p_calc / p) - 1) < 1e-5);
2753
2754 p_calc = CoolProp::PropsSI("P", "T", 274., "Dmolar", 55530.40675319466, "PCSAFT::WATER");
2755 CHECK(abs((p_calc / p) - 1) < 1e-5);
2756
2757 p_calc = CoolProp::PropsSI("P", "T", 305., "Dmolar", 16965.6697209874, "PCSAFT::ACETIC ACID");
2758 CHECK(abs((p_calc / p) - 1) < 1e-5);
2759
2760 p_calc = CoolProp::PropsSI("P", "T", 240., "Dmolar", 15955.50941242, "PCSAFT::DIMETHYL ETHER");
2761 CHECK(abs((p_calc / p) - 1) < 1e-5);
2762
2763 p_calc = CoolProp::PropsSI("P", "T", 298.15, "Dmolar", 9368.903838750752, "PCSAFT::METHANOL[0.055]&CYCLOHEXANE[0.945]");
2764 CHECK(abs((p_calc / p) - 1) < 1e-5);
2765
2766 //p_calc = CoolProp::PropsSI("P", "T", 298.15, "Dmolar", 55757.07260200306, "PCSAFT::Na+[0.010579869455908]&Cl-[0.010579869455908]&WATER[0.978840261088184]");
2767 //CHECK(abs((p_calc/p) - 1) < 1e-5);
2768
2769 p = CoolProp::PropsSI("P", "T", 100., "Q", 0, "PCSAFT::PROPANE");
2770 double rho = 300;
2771 double phase = CoolProp::PropsSI("Phase", "T", 100., "Dmolar", rho, "PCSAFT::PROPANE");
2772 CHECK(phase == get_phase_index("phase_twophase"));
2773 p_calc = CoolProp::PropsSI("P", "T", 100, "Dmolar", rho, "PCSAFT::PROPANE");
2774 CHECK(abs((p_calc / p) - 1) < 1e-4);
2775}
2776
2777TEST_CASE("Tkaczuk et al. (2020) cryogenic mixtures reproduce Table 8", "[Tkaczuk]") {
2778 // Validation points from Tkaczuk, Lemmon, Bell, Luchier, Millet,
2779 // J. Phys. Chem. Ref. Data 49, 023101 (2020), Table 8: equimolar
2780 // (0.50/0.50) composition at T = 200 K and rho = 10 mol/dm^3.
2781 const double T = 200.0, rhomolar = 10000.0; // 10 mol/dm^3 -> mol/m^3
2782 std::vector<double> z(2, 0.5);
2783
2784 // The reference pressures were generated (see the paper's supplementary
2785 // CoolProp validation script) with NORMALIZE_GAS_CONSTANTS = false, i.e.
2786 // each mixture uses the mole-fraction-weighted average of its components'
2787 // own EOS gas constants rather than the harmonized CODATA value. Forcing
2788 // CODATA instead shifts the argon-containing pairs by ~2.7e-6 (argon's EOS
2789 // R = 8.31451 vs CODATA 8.314463), so match the paper's convention here.
2790 // RAII so the global config is restored even if a CHECK below throws.
2791 struct NormalizeGuard
2792 {
2793 bool prev;
2794 NormalizeGuard() : prev(CoolProp::get_config_bool(NORMALIZE_GAS_CONSTANTS)) {
2795 CoolProp::set_config_bool(NORMALIZE_GAS_CONSTANTS, false);
2796 }
2797 ~NormalizeGuard() {
2798 CoolProp::set_config_bool(NORMALIZE_GAS_CONSTANTS, prev);
2799 }
2800 } normalize_guard;
2801
2802 struct
2803 {
2804 const char* fluids;
2805 double p_ref;
2806 } cases[] = {
2807 {"Helium&Neon", 18430775.292601},
2808 {"Helium&Argon", 17128034.388363},
2809 {"Neon&Argon", 15905875.375781},
2810 };
2811 for (auto& c : cases) {
2812 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", c.fluids));
2813 AS->set_mole_fractions(z);
2814 // T = 200 K is above the critical temperature of every constituent
2815 // (Ar Tc = 150.7 K is the highest), so the state is single-phase.
2816 // Impose the phase: the HEOS density-temperature flash does not yet
2817 // run the phase determination for mixtures, but it evaluates the
2818 // pressure directly from (rho, T) when a phase is imposed.
2819 AS->specify_phase(CoolProp::iphase_supercritical_gas);
2820 AS->update(CoolProp::DmolarT_INPUTS, rhomolar, T);
2821 double p = AS->p();
2822 CAPTURE(c.fluids);
2823 CAPTURE(p);
2824 CAPTURE(c.p_ref);
2825 // CoolProp reproduces the published pressures essentially to machine
2826 // precision (observed < 1e-12); 1e-10 leaves a little platform margin.
2827 CHECK(std::abs(p / c.p_ref - 1) < 1e-10);
2828 }
2829
2830 // The equimolar pressures above are algebraically invariant under
2831 // beta -> 1/beta (the GERG reducing weight x1*x2*(x1+x2)/(beta^2*x1+x2)
2832 // collapses to beta/(beta^2+1) at x1=x2), so they cannot detect a
2833 // Name1/Name2 (and hence beta) inversion in the binary-pair table. Pin
2834 // the reducing temperature and density at a NON-equimolar composition,
2835 // where the asymmetric reducing parameters do matter. Reference values
2836 // computed directly from the GERG reducing rules (paper Eq. 3) with the
2837 // Table 6 parameters in the documented (i,j) order and CoolProp's pure
2838 // reducing constants; an inverted betaT shifts T_r by 0.4-3%.
2839 struct
2840 {
2841 const char* fluids;
2842 double Tr_ref, rhor_ref;
2843 } redcases[] = {
2844 {"Helium&Neon", 26.3675566619, 23988.632514},
2845 {"Helium&Argon", 87.5390792465, 15166.941197},
2846 {"Neon&Argon", 111.4722707992, 15642.518187},
2847 };
2848 std::vector<double> znq{0.3, 0.7};
2849 for (auto& c : redcases) {
2850 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", c.fluids));
2851 AS->set_mole_fractions(znq);
2852 CAPTURE(c.fluids);
2853 CHECK(std::abs(AS->T_reducing() / c.Tr_ref - 1) < 1e-8);
2854 CHECK(std::abs(AS->rhomolar_reducing() / c.rhor_ref - 1) < 1e-6);
2855 }
2856}
2857
2858TEST_CASE("Check the PC-SAFT density function", "[pcsaft_density]") {
2859 double den = 9033.114209728405;
2860 double den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 320., "P", 101325., "PCSAFT::TOLUENE");
2861 CHECK(abs((den_calc / den) - 1) < 1e-5);
2862
2863 den = 55530.40512318346;
2864 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 274., "P", 101325, "PCSAFT::WATER");
2865 CHECK(abs((den_calc / den) - 1) < 1e-5);
2866
2867 den = 17240.; // source: DIPPR correlation
2868 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 305., "P", 101325, "PCSAFT::ACETIC ACID");
2869 CHECK(abs((den_calc / den) - 1) < 2e-2);
2870
2871 den = 15955.509146801696;
2872 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 240., "P", 101325, "PCSAFT::DIMETHYL ETHER");
2873 CHECK(abs((den_calc / den) - 1) < 1e-5);
2874
2875 den = 9368.90368306872;
2876 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 298.15, "P", 101325, "PCSAFT::METHANOL[0.055]&CYCLOHEXANE[0.945]");
2877 CHECK(abs((den_calc / den) - 1) < 1e-5);
2878
2879 den = 55740.157290833515;
2880 den_calc =
2881 CoolProp::PropsSI("Dmolar", "T|liquid", 298.15, "P", 101325, "PCSAFT::Na+[0.010579869455908]&Cl-[0.010579869455908]&WATER[0.978840261088184]");
2882 CHECK(abs((den_calc / den) - 1) < 1e-5);
2883
2884 den = 16621.0;
2885 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 85.525, "P", 1.7551e-4, "PCSAFT::PROPANE");
2886 CHECK(abs((den_calc / den) - 1) < 1e-2);
2887
2888 den = 1.9547e-7;
2889 den_calc = CoolProp::PropsSI("Dmolar", "T|gas", 85.525, "P", 1.39e-4, "PCSAFT::PROPANE");
2890 CHECK(abs((den_calc / den) - 1) < 1e-2);
2891
2892 den = 11346.0;
2893 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 293, "P", 833240, "PCSAFT::PROPANE");
2894 CHECK(abs((den_calc / den) - 1) < 1e-2);
2895
2896 den = 623.59;
2897 den_calc = CoolProp::PropsSI("Dmolar", "T|liquid", 430, "P", 2000000, "PCSAFT::PROPANE");
2898 CHECK(abs((den_calc / den) - 1) < 1e-2);
2899}
2900
2901TEST_CASE("Check the PC-SAFT residual enthalpy function", "[pcsaft_enthalpy]") {
2902 double h = -36809.962122036086;
2903 double h_calc = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "PCSAFT::TOLUENE");
2904 CHECK(abs((h_calc / h) - 1) < 1e-5);
2905
2906 h = -362.6832840695562;
2907 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "PCSAFT::TOLUENE");
2908 CHECK(abs((h_calc / h) - 1) < 1e-5);
2909
2910 h = -38925.302571456035;
2911 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 16655.853047419932, "PCSAFT::ACETIC ACID");
2912 CHECK(abs((h_calc / h) - 1) < 1e-5);
2913
2914 h = -15393.870073928741;
2915 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 85.70199446609787, "PCSAFT::ACETIC ACID");
2916 CHECK(abs((h_calc / h) - 1) < 1e-5);
2917
2918 h = -18242.128097841978;
2919 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 13141.475980937616, "PCSAFT::DIMETHYL ETHER");
2920 CHECK(abs((h_calc / h) - 1) < 1e-5);
2921
2922 h = -93.819615173017169;
2923 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 37.963459290365265, "PCSAFT::DIMETHYL ETHER");
2924 CHECK(abs((h_calc / h) - 1) < 1e-5);
2925
2926 // checks based on values from the HEOS backend
2927 h = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "HEOS::TOLUENE");
2928 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "PCSAFT::TOLUENE");
2929 CHECK(abs(h_calc - h) < 600.);
2930
2931 h = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "HEOS::TOLUENE");
2932 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "PCSAFT::TOLUENE");
2933 CHECK(abs(h_calc - h) < 600.);
2934
2935 h = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 54794.1, "HEOS::WATER");
2936 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|liquid", 325., "Dmolar", 54794.1, "PCSAFT::WATER");
2937 CHECK(abs(h_calc - h) < 600.);
2938
2939 h = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 0.370207, "HEOS::WATER");
2940 h_calc = CoolProp::PropsSI("Hmolar_residual", "T|gas", 325., "Dmolar", 0.370207, "PCSAFT::WATER");
2941 CHECK(abs(h_calc - h) < 600.);
2942}
2943
2944TEST_CASE("Check the PC-SAFT residual entropy function", "[pcsaft_entropy]") {
2945 // checks based on values from working PC-SAFT code
2946 double s = -50.81694890352192;
2947 double s_calc = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "PCSAFT::TOLUENE");
2948 CHECK(abs((s_calc / s) - 1) < 1e-5);
2949
2950 s = -0.2929618646219797;
2951 s_calc = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "PCSAFT::TOLUENE");
2952 CHECK(abs((s_calc / s) - 1) < 1e-5);
2953
2954 s = -47.42736805661422;
2955 s_calc = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 16655.853047419932, "PCSAFT::ACETIC ACID");
2956 CHECK(abs((s_calc / s) - 1) < 1e-5);
2957
2958 s = -34.0021996393859;
2959 s_calc = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 85.70199446609787, "PCSAFT::ACETIC ACID");
2960 CHECK(abs((s_calc / s) - 1) < 1e-5);
2961
2962 s = -26.42525828195748;
2963 s_calc = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 13141.475980937616, "PCSAFT::DIMETHYL ETHER");
2964 CHECK(abs((s_calc / s) - 1) < 1e-5);
2965
2966 s = -0.08427662199177874;
2967 s_calc = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 37.963459290365265, "PCSAFT::DIMETHYL ETHER");
2968 CHECK(abs((s_calc / s) - 1) < 1e-5);
2969
2970 // checks based on values from the HEOS backend
2971 s = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "HEOS::TOLUENE");
2972 s_calc = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 8983.377722763931, "PCSAFT::TOLUENE");
2973 CHECK(abs(s_calc - s) < 3.);
2974
2975 s = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "HEOS::TOLUENE");
2976 s_calc = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 39.44490805826904, "PCSAFT::TOLUENE");
2977 CHECK(abs(s_calc - s) < 3.);
2978
2979 s = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 54794.1, "HEOS::WATER");
2980 s_calc = CoolProp::PropsSI("Smolar_residual", "T|liquid", 325., "Dmolar", 54794.1, "PCSAFT::WATER");
2981 CHECK(abs(s_calc - s) < 3.);
2982
2983 s = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 0.370207, "HEOS::WATER");
2984 s_calc = CoolProp::PropsSI("Smolar_residual", "T|gas", 325., "Dmolar", 0.370207, "PCSAFT::WATER");
2985 CHECK(abs(s_calc - s) < 3.);
2986}
2987
2988TEST_CASE("Check the PC-SAFT residual gibbs energy function", "[pcsaft_gibbs]") {
2989 // Expected values were updated in #1943 — the previous formula was
2990 // (alphar + Z - 1 - ln Z) * RT (g_res on (T,P) basis), which did not
2991 // satisfy the CoolProp-wide convention g_res = h_res - T*s_res.
2992 // The new values come from g_res = h_res - T*s_res on (T,ρ) basis,
2993 // matching HEOS::calc_gibbsmolar_residual.
2994 double g = -20294.461258;
2995 double g_calc = CoolProp::PropsSI("Gmolar_residual", "T|liquid", 325., "Dmolar", 8983.377872003264, "PCSAFT::TOLUENE");
2996 CHECK(abs((g_calc / g) - 1) < 1e-5);
2997
2998 g = -267.470804;
2999 g_calc = CoolProp::PropsSI("Gmolar_residual", "T|gas", 325., "Dmolar", 39.44491269148218, "PCSAFT::TOLUENE");
3000 CHECK(abs((g_calc / g) - 1) < 1e-5);
3001
3002 g = -23511.405979;
3003 g_calc = CoolProp::PropsSI("Gmolar_residual", "T|liquid", 325., "Dmolar", 16655.853314424, "PCSAFT::ACETIC ACID");
3004 CHECK(abs((g_calc / g) - 1) < 1e-5);
3005
3006 g = -4343.156768;
3007 g_calc = CoolProp::PropsSI("Gmolar_residual", "T|gas", 325., "Dmolar", 85.70199446609787, "PCSAFT::ACETIC ACID");
3008 CHECK(abs((g_calc / g) - 1) < 1e-5);
3009
3010 g = -9653.922736;
3011 g_calc = CoolProp::PropsSI("Gmolar_residual", "T|liquid", 325., "Dmolar", 13141.47619110254, "PCSAFT::DIMETHYL ETHER");
3012 CHECK(abs((g_calc / g) - 1) < 1e-5);
3013
3014 g = -66.429712;
3015 g_calc = CoolProp::PropsSI("Gmolar_residual", "T|gas", 325., "Dmolar", 37.96344503293008, "PCSAFT::DIMETHYL ETHER");
3016 CHECK(abs((g_calc / g) - 1) < 1e-5);
3017}
3018
3019TEST_CASE("PC-SAFT gibbsmolar_residual satisfies g_res = h_res - T*s_res (#1943)", "[pcsaft_gibbs][1943]") {
3020 // Issue #1943: PCSAFT's calc_gibbsmolar_residual used Gross & Sadowski
3021 // A.50 [(alphar + Z - 1 - ln Z) * RT, on (T,P) basis] which does not
3022 // equal h_res - T*s_res. CoolProp's HEOS uses (T,ρ) basis throughout,
3023 // so the two backends gave inconsistent answers. The fix drops the
3024 // -ln(Z)*RT term so all backends satisfy g_res = h_res - T*s_res.
3025 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("PCSAFT", "METHANE"));
3026 const double T = 100.0;
3027 AS->update(CoolProp::QT_INPUTS, 0.0, T);
3028 const double g = AS->gibbsmolar_residual();
3029 const double h = AS->hmolar_residual();
3030 const double s = AS->smolar_residual();
3031 CAPTURE(g);
3032 CAPTURE(h);
3033 CAPTURE(s);
3034 CHECK(std::abs(g - (h - T * s)) < 1e-6);
3035}
3036
3037TEST_CASE("Check vapor pressures calculated using PC-SAFT", "[pcsaft_vapor_pressure]") {
3038 double vp = 3290651.18080112;
3039 double vp_calc = CoolProp::PropsSI("P", "T", 572.6667, "Q", 0, "PCSAFT::TOLUENE");
3040 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3041
3042 vp = 66917.67387203;
3043 vp_calc = CoolProp::PropsSI("P", "T", 362, "Q", 0, "PCSAFT::WATER");
3044 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3045
3046 vp = 190061.78088909;
3047 vp_calc = CoolProp::PropsSI("P", "T", 413.5385, "Q", 0, "PCSAFT::ACETIC ACID");
3048 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3049
3050 vp = 622763.506195;
3051 vp_calc = CoolProp::PropsSI("P", "T", 300., "Q", 0, "PCSAFT::DIMETHYL ETHER");
3052 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3053
3054 // This test doesn't pass yet. The flash algorithm for the PC-SAFT backend is not yet robust enough.
3055 // vp = 1.7551e-4;
3056 // vp_calc = CoolProp::PropsSI("P","T",85.525,"Q", 0, "PCSAFT::PROPANE");
3057 // CHECK(abs((vp_calc/vp) - 1) < 0.1);
3058
3059 vp = 8.3324e5;
3060 vp_calc = CoolProp::PropsSI("P", "T", 293, "Q", 0, "PCSAFT::PROPANE");
3061 CHECK(abs((vp_calc / vp) - 1) < 0.01);
3062
3063 vp = 42.477e5;
3064 vp_calc = CoolProp::PropsSI("P", "T", 369.82, "Q", 0, "PCSAFT::PROPANE");
3065 CHECK(abs((vp_calc / vp) - 1) < 0.01);
3066}
3067
3068TEST_CASE("Check PC-SAFT interaction parameter functions", "[pcsaft_binary_interaction]") {
3069 std::string CAS_water = get_fluid_param_string("WATER", "CAS");
3070 std::string CAS_aacid = "64-19-7";
3071 set_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij", -0.127);
3072 CHECK(atof(get_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij").c_str()) == -0.127);
3073}
3074
3075TEST_CASE("Check bubble pressures calculated using PC-SAFT", "[pcsaft_bubble_pressure]") {
3076 double vp =
3077 1816840.45112607; // source: H.-M. Lin, H. M. Sebastian, J. J. Simnick, and K.-C. Chao, “Gas-liquid equilibrium in binary mixtures of methane with N-decane, benzene, and toluene,” J. Chem. Eng. Data, vol. 24, no. 2, pp. 146–149, Apr. 1979.
3078 double vp_calc = CoolProp::PropsSI("P", "T", 421.05, "Q", 0, "PCSAFT::METHANE[0.0252]&BENZENE[0.9748]");
3079 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3080
3081 // This test doesn't pass yet. The flash algorithm for the PC-SAFT backend cannot yet get a good enough initial guess value for the k values (vapor-liquid distribution ratios)
3082 // vp = 6691000; // source: Hughes TJ, Kandil ME, Graham BF, Marsh KN, Huang SH, May EF. Phase equilibrium measurements of (methane+ benzene) and (methane+ methylbenzene) at temperatures from (188 to 348) K and pressures to 13 MPa. The Journal of Chemical Thermodynamics. 2015 Jun 1;85:141-7.
3083 // vp_calc = CoolProp::PropsSI("P", "T", 348.15, "Q", 0, "PCSAFT::METHANE[0.119]&BENZENE[0.881]");
3084 // CHECK(abs((vp_calc/vp) - 1) < 1e-3);
3085
3086 vp = 96634.2439079;
3087 vp_calc = CoolProp::PropsSI("P", "T", 327.48, "Q", 0, "PCSAFT::METHANOL[0.3]&CYCLOHEXANE[0.7]");
3088 CHECK(abs((vp_calc / vp) - 1) < 1e-3);
3089
3090 // set binary interaction parameter
3091 std::string CAS_water = get_fluid_param_string("WATER", "CAS");
3092 std::string CAS_aacid = "64-19-7";
3093 try {
3094 get_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij");
3095 } catch (...) {
3096 set_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij", -0.127);
3097 }
3098
3099 vp = 274890.39985918;
3100 vp_calc = CoolProp::PropsSI("P", "T", 403.574, "Q", 0, "PCSAFT::WATER[0.9898662364]&ACETIC ACID[0.0101337636]");
3101 CHECK(abs((vp_calc / vp) - 1) < 1e-2);
3102
3103 vp = 72915.92217342;
3104 vp_calc = CoolProp::PropsSI("P", "T", 372.774, "Q", 0, "PCSAFT::WATER[0.2691800943]&ACETIC ACID[0.7308199057]");
3105 CHECK(abs((vp_calc / vp) - 1) < 2e-2);
3106
3107 vp = 2387.42669687;
3108 vp_calc = CoolProp::PropsSI("P", "T", 298.15, "Q", 0, "PCSAFT::Na+[0.0907304774758426]&Cl-[0.0907304774758426]&WATER[0.818539045048315]");
3109 CHECK(abs((vp_calc / vp) - 1) < 0.23);
3110}
3111
3112TEST_CASE("Check bubble temperatures calculated using PC-SAFT", "[pcsaft_bubble_temperature]") {
3113 double t = 572.6667;
3114 double t_calc = CoolProp::PropsSI("T", "P", 3290651.18080112, "Q", 0, "PCSAFT::TOLUENE");
3115 CHECK(abs((t_calc / t) - 1) < 1e-3);
3116
3117 t = 362;
3118 t_calc = CoolProp::PropsSI("T", "P", 66917.67387203, "Q", 0, "PCSAFT::WATER");
3119 CHECK(abs((t_calc / t) - 1) < 1e-3);
3120
3121 t = 413.5385;
3122 t_calc = CoolProp::PropsSI("T", "P", 190061.78088909, "Q", 0, "PCSAFT::ACETIC ACID");
3123 CHECK(abs((t_calc / t) - 1) < 1e-3);
3124
3125 t = 300.;
3126 t_calc = CoolProp::PropsSI("T", "P", 623027.07850612, "Q", 0, "PCSAFT::DIMETHYL ETHER");
3127 CHECK(abs((t_calc / t) - 1) < 1e-3);
3128
3129 // This test doesn't pass yet. The flash algorithm for the PC-SAFT backend cannot yet get a good enough initial guess value for the k values (vapor-liquid distribution ratios)
3130 // t = 421.05;
3131 // t_calc = CoolProp::PropsSI("T", "P", 1816840.45112607, "Q", 0, "PCSAFT::METHANE[0.0252]&BENZENE[0.9748]");
3132 // CHECK(abs((t_calc/t) - 1) < 1e-3);
3133
3134 t = 327.48;
3135 t_calc = CoolProp::PropsSI("T", "P", 96634.2439079, "Q", 0, "PCSAFT::METHANOL[0.3]&CYCLOHEXANE[0.7]");
3136 CHECK(abs((t_calc / t) - 1) < 1e-3);
3137
3138 // set binary interaction parameter, if not already set
3139 std::string CAS_water = get_fluid_param_string("WATER", "CAS");
3140 std::string CAS_aacid = "64-19-7";
3141 try {
3142 get_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij");
3143 } catch (...) {
3144 set_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij", -0.127);
3145 }
3146
3147 t = 403.574;
3148 t_calc = CoolProp::PropsSI("T", "P", 274890.39985918, "Q", 0, "PCSAFT::WATER[0.9898662364]&ACETIC ACID[0.0101337636]");
3149 CHECK(abs((t_calc / t) - 1) < 1e-3);
3150
3151 t = 372.774;
3152 t_calc = CoolProp::PropsSI("T", "P", 72915.92217342, "Q", 0, "PCSAFT::WATER[0.2691800943]&ACETIC ACID[0.7308199057]");
3153 CHECK(abs((t_calc / t) - 1) < 2e-3);
3154
3155 t = 298.15;
3156 t_calc = CoolProp::PropsSI("T", "P", 2387.42669687, "Q", 0, "PCSAFT::Na+[0.0907304774758426]&Cl-[0.0907304774758426]&WATER[0.818539045048315]");
3157 CHECK(abs((t_calc / t) - 1) < 1e-2);
3158}
3159
3160TEST_CASE("Github issue #2470", "[pureflash]") {
3161 auto fluide = "Nitrogen";
3162 auto enthalpy = 67040.57857; //J / kg
3163 auto pressure = 3368965.046; //Pa
3164 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", fluide));
3165 AS->update(PQ_INPUTS, pressure, 1);
3166 auto Ts = AS->T();
3167 AS->specify_phase(iphase_gas);
3168 CHECK_NOTHROW(AS->update(PT_INPUTS, pressure, Ts));
3169 AS->unspecify_phase();
3170 CHECK_NOTHROW(AS->update(HmassP_INPUTS, enthalpy, pressure));
3171 auto Tfinal = AS->T();
3172 CHECK(Tfinal > AS->T_critical());
3173}
3174
3175TEST_CASE("Github issue #2467", "[pureflash]") {
3176 auto fluide = "Pentane";
3177 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", fluide));
3178 AS->update(CoolProp::QT_INPUTS, 1, 353.15);
3179 double p1 = AS->p();
3180 AS->update(CoolProp::QT_INPUTS, 1, 433.15);
3181 double p2 = AS->p();
3182 AS->update(CoolProp::PT_INPUTS, p1, 393.15);
3183 double s1 = AS->smass();
3184 CHECK_NOTHROW(AS->update(CoolProp::PSmass_INPUTS, p2, s1));
3185}
3186
3187TEST_CASE("Github issue #1870", "[pureflash]") {
3188 auto fluide = "Pentane";
3189 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", fluide));
3190 CHECK_NOTHROW(AS->update(CoolProp::PSmass_INPUTS, 1000000, 1500));
3191}
3192
3193TEST_CASE("Github issue #2447", "[2447]") {
3194 double pvap = PropsSI("P", "T", 360 + 273.15, "Q", 0, "INCOMP::S800");
3195 double err = std::abs(pvap / 961e3 - 1);
3196 CHECK(err < 0.05);
3197}
3198
3199TEST_CASE("Github issue #2558", "[2558]") {
3200 double Tau = CoolProp::PropsSI("Tau", "Dmolar|gas", 200.0, "T", 300.0, "CarbonDioxide[0.5]&Hydrogen[0.5]");
3201 double Delta = CoolProp::PropsSI("Delta", "Dmolar|gas", 200.0, "T", 300.0, "CarbonDioxide[0.5]&Hydrogen[0.5]");
3202 CHECK(std::isfinite(Tau));
3203 CHECK(std::isfinite(Delta));
3204}
3205
3206TEST_CASE("Github issue #2491", "[2491]") {
3207 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", "Xenon"));
3208 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, 59867.351071950761, 5835843.7305891514));
3209 CHECK(std::isfinite(AS->rhomolar()));
3210}
3211
3212TEST_CASE("Github issue #2608", "[2608]") {
3213 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", "CO2"));
3214 double pc = AS->p_critical();
3215 // 218.048 K was updated to 218.050 K: the new melting line check now rejects inputs
3216 // below Tmelt(p), and at p=73.8e5 Pa CO2's melting temperature is ~218.049 K.
3217 CHECK_NOTHROW(AS->update(CoolProp::PT_INPUTS, 73.8e5, 218.050));
3218 SECTION("Without phase") {
3219 AS->unspecify_phase();
3220 CHECK_NOTHROW(AS->update(CoolProp::PSmass_INPUTS, 73.8e5, 1840.68));
3221 }
3222 SECTION("With phase") {
3223 AS->specify_phase(iphase_supercritical_gas);
3224 CHECK_NOTHROW(AS->update(CoolProp::PSmass_INPUTS, 73.8e5, 1840.68));
3225 AS->unspecify_phase();
3226 }
3227}
3228
3229TEST_CASE("Github issue #2622", "[2622]") {
3230 auto h5 = 233250;
3231 auto p5 = 5e6;
3232 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", "R123"));
3233 double pc = AS->p_critical();
3234 CAPTURE(pc);
3235 double Tt = AS->Ttriple();
3236 CAPTURE(Tt);
3237
3239 AS->update(PT_INPUTS, p5, 165.999);
3240
3241 AS->update(HmassP_INPUTS, h5, p5);
3242 double A = AS->T();
3243 CAPTURE(A);
3244}
3245
3246template <typename T>
3247std::vector<T> linspace(T start, T end, int num) {
3248 std::vector<T> linspaced;
3249 if (num <= 0) {
3250 return linspaced; // Return empty vector for invalid num
3251 }
3252 if (num == 1) {
3253 linspaced.push_back(start);
3254 return linspaced;
3255 }
3256
3257 T step = (end - start) / (num - 1);
3258 for (int i = 0; i < num; ++i) {
3259 linspaced.push_back(start + step * i);
3260 }
3261 return linspaced;
3262}
3263
3264TEST_CASE("Github issue #2582", "[2582]") {
3265 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", "CO2"));
3266 double pc = AS->p_critical();
3267 AS->update(PQ_INPUTS, 73.33e5, 0);
3268 double hmass_liq = AS->saturated_liquid_keyed_output(iHmass);
3269 double hmass_vap = AS->saturated_vapor_keyed_output(iHmass);
3270 // std::cout << pc << std::endl;
3271 // std::cout << hmass_liq << std::endl;
3272 // std::cout << hmass_vap << std::endl;
3273 for (auto hmass : linspace(100e3, 700e3, 1000)) {
3274 CAPTURE(hmass);
3275 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 73.76e5));
3276 }
3277 for (auto hmass : linspace(100e3, 700e3, 1000)) {
3278 CAPTURE(hmass);
3279 CHECK_NOTHROW(AS->update(CoolProp::HmassP_INPUTS, hmass, 73.33e5));
3280 }
3281}
3282
3283TEST_CASE("Github issue #2594", "[2594]") {
3284 std::shared_ptr<CoolProp::AbstractState> AS(AbstractState::factory("HEOS", "CO2"));
3285 auto p = 7377262.928140703;
3286 double pc = AS->p_critical();
3287 AS->update(PQ_INPUTS, p, 0);
3288 double Tsat = AS->T();
3289 double rholiq = AS->rhomolar();
3290 double umass_liq = AS->saturated_liquid_keyed_output(iUmass);
3291 double umass_vap = AS->saturated_vapor_keyed_output(iUmass);
3292 // std::cout << std::setprecision(20) << pc << std::endl;
3293 // std::cout << umass_liq << std::endl;
3294 // std::cout << umass_vap << std::endl;
3295
3296 auto umass = 314719.5306503257;
3297 // auto& rHEOS = *dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
3298 // bool sat_called = false;
3299 // auto MM = AS->molar_mass();
3300 // rHEOS.p_phase_determination_pure_or_pseudopure(iUmolar, umass*MM, sat_called);
3301 // CHECK(rHEOS.phase() == iphase_liquid);
3302
3303 AS->update(DmolarP_INPUTS, rholiq, p);
3304 double rho1 = AS->rhomolar();
3305 double T1 = AS->T();
3306 double dumolardT_P = AS->first_partial_deriv(iUmolar, iT, iP);
3307 double dpdrho_T = AS->first_partial_deriv(iP, iDmolar, iT);
3308 // double dumassdT_P = AS->first_partial_deriv(iUmass, iT, iP);
3309
3310 AS->specify_phase(iphase_liquid);
3311 AS->update(PT_INPUTS, p, Tsat);
3312 double rho2 = AS->rhomolar();
3313 double T2 = AS->T();
3314 double dpdrho_T_imposed = AS->first_partial_deriv(iP, iDmolar, iT);
3315 double dumolardT_P_imposed = AS->first_partial_deriv(iUmolar, iT, iP);
3316 // double dumassdT_P_imposed = AS->first_partial_deriv(iUmass, iT, iP);
3317 AS->unspecify_phase();
3318
3319 CHECK_NOTHROW(AS->update(CoolProp::PUmass_INPUTS, p, umass));
3320
3321 BENCHMARK("dp/drho|T") {
3322 return AS->first_partial_deriv(iP, iDmolar, iT);
3323 };
3324 BENCHMARK("du/dT|p") {
3325 return AS->first_partial_deriv(iUmolar, iT, iP);
3326 };
3327}
3328
3329TEST_CASE("CoolProp.jl tests", "[2598]") {
3330 // // Whoah, actually quite a few change meaningfully
3331 // SECTION("Check pcrit doesn't change too much with SA on"){
3332 // auto init = get_config_bool(ENABLE_SUPERANCILLARIES);
3333 // for (auto fluid : strsplit(get_global_param_string("fluids_list"), ',')){
3334 // CAPTURE(fluid);
3335 // set_config_bool(ENABLE_SUPERANCILLARIES, true); auto pcrit_SA = Props1SI(fluid, "pcrit");
3336 // set_config_bool(ENABLE_SUPERANCILLARIES, false); auto pcrit_noSA = Props1SI(fluid, "pcrit");
3337 // CAPTURE(pcrit_SA - pcrit_noSA);
3338 // CHECK(std::abs(pcrit_SA/pcrit_noSA-1) < 1E-2);
3339 // }
3340 // set_config_bool(ENABLE_SUPERANCILLARIES, init);
3341 // }
3342
3343 for (auto fluid : strsplit(get_global_param_string("fluids_list"), ',')) {
3344 auto pcrit = Props1SI(fluid, "pcrit");
3345 auto Tcrit = Props1SI(fluid, "Tcrit");
3346 CAPTURE(fluid);
3347 CAPTURE(PhaseSI("P", pcrit + 50000, "T", Tcrit + 3, fluid));
3348 CAPTURE(PhaseSI("P", pcrit + 50000, "T", Tcrit - 3, fluid));
3349 CAPTURE(PhaseSI("P", pcrit - 50000, "T", Tcrit + 3, fluid));
3350
3351 CAPTURE(PropsSI("Q", "P", pcrit + 50000, "T", Tcrit + 3, fluid));
3352 CAPTURE(PropsSI("Q", "P", pcrit + 50000, "T", Tcrit - 3, fluid));
3353 CAPTURE(PropsSI("Q", "P", pcrit - 50000, "T", Tcrit + 3, fluid));
3354
3355 CHECK(PhaseSI("P", pcrit + 50000, "T", Tcrit + 3, fluid) == "supercritical");
3356 CHECK(PhaseSI("P", pcrit + 50000, "T", Tcrit - 3, fluid) == "supercritical_liquid");
3357 CHECK(PhaseSI("P", pcrit - 50000, "T", Tcrit + 3, fluid) == "supercritical_gas");
3358 }
3359}
3360
3361TEST_CASE("Check methanol EOS matches REFPROP 10", "[2538]") {
3362 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
3363
3364 auto TNBP_RP = PropsSI("T", "P", 101325, "Q", 0, "REFPROP::METHANOL");
3365 auto TNBP_CP = PropsSI("T", "P", 101325, "Q", 0, "HEOS::METHANOL");
3366 CHECK(TNBP_RP == Catch::Approx(TNBP_CP).epsilon(1e-6));
3367
3368 auto rhoL_RP = PropsSI("D", "T", 400, "Q", 0, "REFPROP::METHANOL");
3369 auto rhoL_CP = PropsSI("D", "T", 400, "Q", 0, "HEOS::METHANOL");
3370 CHECK(rhoL_RP == Catch::Approx(rhoL_CP).epsilon(1e-12));
3371
3372 auto cp0_RP = PropsSI("CP0MOLAR", "T", 400, "Dmolar", 1e-5, "REFPROP::METHANOL");
3373 auto cp0_CP = PropsSI("CP0MOLAR", "T", 400, "Dmolar", 1e-5, "HEOS::METHANOL");
3374 CHECK(cp0_RP == Catch::Approx(cp0_CP).epsilon(1e-4));
3375}
3376
3377TEST_CASE("Check phase determination for PC-SAFT backend", "[pcsaft_phase]") {
3378 double den = 9033.114209728405;
3379 double den_calc = CoolProp::PropsSI("Dmolar", "T", 320., "P", 101325., "PCSAFT::TOLUENE");
3380 CHECK(abs((den_calc / den) - 1) < 1e-2);
3381 double phase = CoolProp::PropsSI("Phase", "T", 320., "P", 101325., "PCSAFT::TOLUENE");
3382 CHECK(phase == get_phase_index("phase_liquid"));
3383
3384 den = 0.376013;
3385 den_calc = CoolProp::PropsSI("Dmolar", "T", 320., "P", 1000., "PCSAFT::TOLUENE");
3386 CHECK(abs((den_calc / den) - 1) < 1e-2);
3387 phase = CoolProp::PropsSI("Phase", "T", 320., "P", 1000., "PCSAFT::TOLUENE");
3388 CHECK(phase == get_phase_index("phase_gas"));
3389}
3390
3391TEST_CASE("Check that indexes for mixtures are assigned correctly, especially for the association term", "[pcsaft_indexes]") {
3392 // The tests are performed by adding parameters for extra compounds that actually
3393 // are not present in the system and ensuring that the properties of the fluid do not change.
3394
3395 // Binary mixture: water-acetic acid
3396 // set binary interaction parameter, if not already set
3397 std::string CAS_water = get_fluid_param_string("WATER", "CAS");
3398 std::string CAS_aacid = "64-19-7";
3399 try {
3400 get_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij");
3401 } catch (...) {
3402 set_mixture_binary_pair_pcsaft(CAS_water, CAS_aacid, "kij", -0.127);
3403 }
3404
3405 double t = 413.5385;
3406 double rho = 15107.481234283325;
3407 double p = CoolProp::PropsSI("P", "T", t, "Dmolar", rho, "PCSAFT::ACETIC ACID"); // only parameters for acetic acid
3408 double p_extra =
3409 CoolProp::PropsSI("P", "T", t, "Dmolar", rho, "PCSAFT::ACETIC ACID[1.0]&WATER[0]"); // same composition, but with mixture parameters
3410 CHECK(abs((p_extra - p) / p * 100) < 1e-1);
3411
3412 // Binary mixture: water-furfural
3413 t = 400; // K
3414 // p = 34914.37778265716; // Pa
3415 rho = 10657.129498214763;
3416 p = CoolProp::PropsSI("P", "T", t, "Dmolar", rho, "PCSAFT::FURFURAL"); // only parameters for furfural
3417 p_extra = CoolProp::PropsSI("P", "T", t, "Dmolar", rho, "PCSAFT::WATER[0]&FURFURAL[1.0]"); // same composition, but with mixture of components
3418 CHECK(abs((p_extra - p) / p * 100) < 1e-1);
3419
3420 // Mixture: NaCl in water with random 4th component
3421 t = 298.15; // K
3422 // p = 3153.417688548272; // Pa
3423 rho = 55320.89616248148;
3424 p = CoolProp::PropsSI("P", "T", t, "Dmolar", rho, "PCSAFT::WATER"); // only parameters for water
3425 p_extra = CoolProp::PropsSI("P", "T", t, "Dmolar", rho,
3426 "PCSAFT::Na+[0]&Cl-[0]&WATER[1.0]&DIMETHOXYMETHANE[0]"); // same composition, but with mixture of components
3427 CHECK(abs((p_extra - p) / p * 100) < 1e-1);
3428}
3429
3431class SuperAncillaryOnFixture
3432{
3433 private:
3434 const configuration_keys m_key = ENABLE_SUPERANCILLARIES;
3435 const bool initial_value;
3436
3437 public:
3438 SuperAncillaryOnFixture() : initial_value(CoolProp::get_config_bool(m_key)) {
3439 CoolProp::set_config_bool(m_key, true);
3440 }
3441 ~SuperAncillaryOnFixture() {
3442 CoolProp::set_config_bool(m_key, initial_value);
3443 }
3444};
3445
3447class SuperAncillaryOffFixture
3448{
3449 private:
3450 const configuration_keys m_key = ENABLE_SUPERANCILLARIES;
3451 const bool initial_value;
3452
3453 public:
3454 SuperAncillaryOffFixture() : initial_value(CoolProp::get_config_bool(m_key)) {
3455 CoolProp::set_config_bool(m_key, false);
3456 }
3457 ~SuperAncillaryOffFixture() {
3458 CoolProp::set_config_bool(m_key, initial_value);
3459 }
3460};
3461
3464class PropertyLimitsFixture
3465{
3466 private:
3467 const configuration_keys m_key = DONT_CHECK_PROPERTY_LIMITS;
3468 const bool initial_value;
3469
3470 public:
3471 PropertyLimitsFixture() : initial_value(CoolProp::get_config_bool(m_key)) {}
3472 ~PropertyLimitsFixture() {
3473 CoolProp::set_config_bool(m_key, initial_value);
3474 }
3475};
3476
3477TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check superancillary for water", "[superanc]") {
3478
3479 auto json = nlohmann::json::parse(get_fluid_param_string("WATER", "JSON"))[0].at("EOS")[0].at("SUPERANCILLARY");
3481 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
3482 shared_ptr<CoolProp::AbstractState> IF97(CoolProp::AbstractState::factory("IF97", "Water"));
3483 auto& rHEOS = *dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
3484 BENCHMARK("HEOS.clear()") {
3485 return rHEOS.clear();
3486 };
3487 BENCHMARK("HEOS rho(T)") {
3488 return AS->update(QT_INPUTS, 1.0, 300.0);
3489 };
3490 BENCHMARK("HEOS update_QT_pure_superanc(Q,T)") {
3491 return rHEOS.update_QT_pure_superanc(1.0, 300.0);
3492 };
3493 BENCHMARK("superanc rho(T)") {
3494 return anc.eval_sat(300.0, 'D', 1);
3495 };
3496 BENCHMARK("IF97 rho(T)") {
3497 return IF97->update(QT_INPUTS, 1.0, 300.0);
3498 };
3499
3500 double Tmin = AS->get_fluid_parameter_double(0, "SUPERANC::Tmin");
3501 double Tc = AS->get_fluid_parameter_double(0, "SUPERANC::Tcrit_num");
3502 double pmin = AS->get_fluid_parameter_double(0, "SUPERANC::pmin");
3503 double pmax = AS->get_fluid_parameter_double(0, "SUPERANC::pmax");
3504
3505 CHECK_THROWS(AS->get_fluid_parameter_double(1, "SUPERANC::pmax"));
3506
3507 BENCHMARK("HEOS rho(p)") {
3508 return AS->update(PQ_INPUTS, 101325, 1.0);
3509 };
3510 BENCHMARK("superanc T(p)") {
3511 return anc.get_T_from_p(101325);
3512 };
3513 BENCHMARK("IF97 rho(p)") {
3514 return IF97->update(PQ_INPUTS, 101325, 1.0);
3515 };
3516}
3517
3518TEST_CASE_METHOD(SuperAncillaryOnFixture, "Benchmark class construction", "[superanc]") {
3519
3520 BENCHMARK("Water [SA]") {
3521 return shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
3522 };
3523 BENCHMARK("R410A [no SA]") {
3524 return shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R410A"));
3525 };
3526 BENCHMARK("propane [SA]") {
3527 return shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
3528 };
3529 BENCHMARK("air, pseudo-pure [SA]") {
3530 return shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Air"));
3531 };
3532}
3533
3534TEST_CASE_METHOD(SuperAncillaryOffFixture, "Check superancillary-like calculations with superancillary disabled for water", "[superanc]") {
3535
3536 auto json = nlohmann::json::parse(get_fluid_param_string("WATER", "JSON"))[0].at("EOS")[0].at("SUPERANCILLARY");
3538 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
3539 shared_ptr<CoolProp::AbstractState> IF97(CoolProp::AbstractState::factory("IF97", "Water"));
3540 auto& approxrhoL = anc.get_approx1d('D', 0);
3541
3542 BENCHMARK("HEOS rho(T)") {
3543 return AS->update(QT_INPUTS, 1.0, 300.0);
3544 };
3545 BENCHMARK("superanc rho(T)") {
3546 return anc.eval_sat(300.0, 'D', 1);
3547 };
3548 BENCHMARK("superanc rho(T) with expansion directly") {
3549 return approxrhoL.eval(300.0);
3550 };
3551 BENCHMARK("superanc get_index rho(T)") {
3552 return approxrhoL.get_index(300.0);
3553 };
3554 BENCHMARK("IF97 rho(T)") {
3555 return IF97->update(QT_INPUTS, 1.0, 300.0);
3556 };
3557
3558 BENCHMARK("HEOS rho(p)") {
3559 return AS->update(PQ_INPUTS, 101325, 1.0);
3560 };
3561 BENCHMARK("superanc T(p)") {
3562 return anc.get_T_from_p(101325);
3563 };
3564 BENCHMARK("IF97 rho(p)") {
3565 return IF97->update(PQ_INPUTS, 101325, 1.0);
3566 };
3567}
3568
3569TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check superancillary functions are available for all pure fluids", "[ancillary]") {
3570 for (auto& fluid : strsplit(CoolProp::get_global_param_string("fluids_list"), ',')) {
3571 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", fluid));
3572 auto& rHEOS = *dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
3573 if (rHEOS.is_pure()) {
3574 CAPTURE(fluid);
3575 // A small number of pure fluids legitimately lack a superancillary
3576 // (e.g. propylene glycol, whose published EOS has a numerically
3577 // unstable critical region that fastchebpure cannot converge
3578 // against). Skip them instead of failing the suite.
3579 try {
3580 rHEOS.update_QT_pure_superanc(1, rHEOS.T_critical() * 0.9999);
3581 } catch (const ValueError& e) {
3582 if (std::string(e.what()).find("Superancillaries not available") != std::string::npos) {
3583 continue;
3584 }
3585 CHECK_NOTHROW((void)("rethrow")); // mark the section as failed
3586 throw;
3587 }
3588 }
3589 }
3590};
3591
3592extern "C"
3593{
3594 extern unsigned char gall_fluids_CBORData[];
3595 extern unsigned int gall_fluids_CBORSize;
3596}
3597
3598TEST_CASE("Superancillary source_eos_hash matches current EOS at bit level", "[ancillary]") {
3599 // Byte-level freshness check: the stored source_eos_hash was stamped from
3600 // the EOS fastchebpure saw when it fit the superancillary; if anyone has edited
3601 // the EOS since (gas constant, alpha0/alphar, reducing state, ...), the
3602 // current hash of EOS[0] (with the SUPERANCILLARY subtree removed) will
3603 // disagree and this test fails. Mirror of
3604 // dev/scripts/check_superanc_freshness.py on the Python side.
3605 //
3606 // Two subtleties motivate the implementation below:
3607 //
3608 // 1. We must bypass CoolProp's runtime JSON parse: it historically
3609 // rounded some doubles 1 ULP
3610 // away from the JSON text value. Decompressing the raw compiled-in
3611 // blob and parsing with nlohmann::json (correctly-rounded by
3612 // default) yields the same doubles Python saw at inject time.
3613 //
3614 // 2. We cannot hash via a canonical JSON dump: nlohmann's and Python's
3615 // shortest-round-trip float formatters occasionally disagree (e.g.,
3616 // nlohmann emits "19673.920781104862" where Python emits
3617 // "19673.92078110486" for the same double). So we hash the parsed
3618 // TREE (type tags + raw IEEE-754 bits for doubles, two's complement
3619 // for ints, UTF-8 for strings, sorted keys for objects). Since the
3620 // parsed values are bit-identical across languages, the byte stream
3621 // fed to FNV-1a is too, and the hashes match exactly.
3622 //
3623 // Known-stale fluids: EOS edited in master since the last fastchebpure
3624 // release, with no yet-released SA to match. Adding a fluid here makes
3625 // the test assert on the *mismatch* (so an accidental regen also forces
3626 // an update here). Currently empty — fastchebpure 2026.04.23 covers
3627 // every master fluid that has an SA.
3628 static const std::set<std::string> known_stale_SA = {};
3629
3630 // ---------------------------------------------------------------------
3631 // TreeHasher: FNV-1a 64 over a deterministic byte serialization of a
3632 // parsed JSON tree.
3633 //
3634 // We need a hash that (a) agrees byte-for-byte with the Python inject
3635 // script, (b) is cross-platform / cross-compiler deterministic, and
3636 // (c) is robust to insignificant representation differences (trailing
3637 // digits, key ordering, etc.). Hashing a JSON *string* doesn't satisfy
3638 // (c): Python's repr and nlohmann::dump disagree on "shortest round
3639 // trip" for a handful of doubles. So instead we feed FNV-1a a byte
3640 // stream derived from the parsed VALUES — two implementations that
3641 // parse the same JSON into the same doubles produce identical bytes.
3642 //
3643 // Byte-stream contract (this C++ walk must stay in lockstep with
3644 // `eos_fnv1a_hex` in dev/scripts/inject_superanc_check_points.py):
3645 //
3646 // null -> 'n'
3647 // false -> 'f'
3648 // true -> 't'
3649 // integer -> 'i' then int64 two's-complement bits as LE u64
3650 // float -> 'd' then IEEE-754 bits as LE u64
3651 // string -> 's' then LE u64 UTF-8 byte count then UTF-8 bytes
3652 // array -> 'a' then LE u64 length then each element walked
3653 // object -> 'o' then LE u64 size then, for each key in sorted
3654 // order: LE u64 UTF-8 byte count, UTF-8 bytes, walked
3655 // value
3656 //
3657 // Notes / invariants future readers should preserve:
3658 // * Type tags let us distinguish 0, 0.0, false, and "" (they would
3659 // otherwise all hash the same with integer-only or string-only
3660 // encodings).
3661 // * nlohmann::json is backed by std::map, so iterating `items()`
3662 // already yields keys in sorted order. Do not switch to
3663 // ordered_json (insertion order) or unordered_json — the Python
3664 // side explicitly sorts, and we must match.
3665 // * All CoolProp-supported platforms are little-endian, so we
3666 // encode u64s little-endian without an explicit swap. If that
3667 // ever changes, replace mix_u64 with an endianness-normalized
3668 // variant in both languages simultaneously.
3669 // * is_number_unsigned is lumped with is_number_integer and cast
3670 // through int64_t. For any JSON integer that fits in int64 the
3671 // two's-complement bit pattern matches Python's `x & 0xFF...F`
3672 // encoding; fluid JSONs never exceed that range.
3673 // * FNV-1a is NOT cryptographic; we only need determinism and
3674 // reasonable distribution for change detection. Never rely on
3675 // this for security.
3676 //
3677 // The seed 0xcbf29ce484222325 and prime 0x100000001b3 are the
3678 // standard FNV-1a 64-bit parameters.
3679 // ---------------------------------------------------------------------
3680 struct TreeHasher
3681 {
3682 uint64_t h = 0xcbf29ce484222325ULL;
3684 void mix_u8(uint8_t b) {
3685 h ^= b;
3686 h *= 0x100000001b3ULL;
3687 }
3689 void mix_bytes(const void* data, std::size_t n) {
3690 const auto* p = static_cast<const uint8_t*>(data);
3691 for (std::size_t i = 0; i < n; ++i)
3692 mix_u8(p[i]);
3693 }
3696 void mix_u64(uint64_t v) {
3697 for (int i = 0; i < 8; ++i)
3698 mix_u8(static_cast<uint8_t>((v >> (i * 8)) & 0xff));
3699 }
3702 void walk(const nlohmann::json& j) {
3703 if (j.is_null()) {
3704 mix_u8('n');
3705 } else if (j.is_boolean()) {
3706 // Distinct tags for true and false; we never fold a payload
3707 // byte here, so 'true' alone is what distinguishes booleans
3708 // from strings like "t" (which emit 's' + length + 't').
3709 mix_u8(j.get<bool>() ? 't' : 'f');
3710 } else if (j.is_number_integer() || j.is_number_unsigned()) {
3711 // All JSON ints — whether nlohmann classified them signed or
3712 // unsigned — get the same 'i' tag and int64 bit pattern. This
3713 // matches Python, where json.loads always yields a single int
3714 // type regardless of sign.
3715 mix_u8('i');
3716 mix_u64(static_cast<uint64_t>(j.get<int64_t>()));
3717 } else if (j.is_number_float()) {
3718 // Feed raw IEEE-754 bits, not the textual representation.
3719 // std::memcpy is the well-defined way to type-pun double -> u64;
3720 // both Python's struct.pack('<d', x) and this produce the same
3721 // 8 bytes on a little-endian host.
3722 mix_u8('d');
3723 uint64_t bits;
3724 double v = j.get<double>();
3725 std::memcpy(&bits, &v, 8);
3726 mix_u64(bits);
3727 } else if (j.is_string()) {
3728 // Length-prefixed UTF-8. The length prefix prevents collisions
3729 // between, e.g., ["ab","c"] and ["a","bc"] when array elements
3730 // are walked back to back.
3731 const auto& s = j.get_ref<const std::string&>();
3732 mix_u8('s');
3733 mix_u64(s.size());
3734 mix_bytes(s.data(), s.size());
3735 } else if (j.is_array()) {
3736 // Length prefix disambiguates nested arrays — without it, [[1],[]]
3737 // and [[1,[]]] would serialize to the same byte stream.
3738 mix_u8('a');
3739 mix_u64(j.size());
3740 for (const auto& el : j)
3741 walk(el);
3742 } else if (j.is_object()) {
3743 // Size prefix + keys in sorted (lexicographic by UTF-8 bytes)
3744 // order. nlohmann's underlying std::map already iterates in
3745 // that order; if a future maintainer swaps in an ordered_json
3746 // or unordered_json type, this loop will silently change
3747 // behavior and stop agreeing with Python — add an explicit
3748 // sort there.
3749 mix_u8('o');
3750 mix_u64(j.size());
3751 for (auto it = j.begin(); it != j.end(); ++it) {
3752 const auto& k = it.key();
3753 mix_u64(k.size());
3754 mix_bytes(k.data(), k.size());
3755 walk(it.value());
3756 }
3757 }
3758 }
3760 std::string hex() const {
3761 char buf[17];
3762 std::snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(h));
3763 return {buf};
3764 }
3765 };
3766
3767 // Self-test the TreeHasher against a fixed input/hash pair so that the
3768 // byte-stream contract can't silently drift even if every fluid JSON
3769 // happens to stay internally consistent. The expected value is computed
3770 // by dev/scripts/inject_superanc_check_points.py::eos_fnv1a_hex on the
3771 // same literal fixture; a regression on either side flips one digit.
3772 // This fixture exercises every type in the contract (null, bool, int,
3773 // float, string, array, object, nested objects, empty containers).
3774 {
3775 nlohmann::json fixture = {
3776 {"alphar", {{{"d", {1, 2, 3}}, {"n", {-0.5, 1.25e-10, 3.14159265358979}}}}},
3777 {"empty_array", nlohmann::json::array()},
3778 {"empty_string", ""},
3779 {"flag_false", false},
3780 {"flag_true", true},
3781 {"gas_constant", 8.3144598},
3782 {"nested", {{"deep", {{"deeper", nullptr}}}}},
3783 {"zero_float", 0.0},
3784 {"zero_int", 0},
3785 };
3786 TreeHasher fixture_hasher;
3787 fixture_hasher.walk(fixture);
3788 CHECK(fixture_hasher.hex() == "8e75626511d00b5c");
3789 }
3790
3791 // Decode the raw all_fluids CBOR bytes — the same blob FluidLibrary
3792 // loads, but without the subsequent runtime round-trip.
3794
3795 int fluids_checked = 0;
3796 for (const auto& jfluid : all_fluids) {
3797 if (!jfluid.contains("EOS") || jfluid.at("EOS").empty()) {
3798 continue;
3799 }
3800 const auto& eos = jfluid.at("EOS")[0];
3801 if (!eos.contains("SUPERANCILLARY")) {
3802 continue;
3803 }
3804 std::string name = jfluid.value("INFO", nlohmann::json::object()).value("NAME", std::string("?"));
3805 CAPTURE(name);
3806 const auto& jsuper = eos.at("SUPERANCILLARY");
3807 // Every SA-bearing fluid must carry the freshness stamp. Fail loudly
3808 // rather than silently skip — a new fluid added without an inject
3809 // step should not slip past this test. The field name is the one
3810 // fastchebpure emits (`fitcheb inject`'s `source_eos_hash`);
3811 // `dev/scripts/inject_superanc_check_points.py` now writes the same
3812 // key so that there is a single canonical field across producers.
3813 REQUIRE(jsuper.contains("source_eos_hash"));
3814 auto stored = jsuper.at("source_eos_hash").get<std::string>();
3815 auto stripped = eos;
3816 stripped.erase("SUPERANCILLARY");
3817 TreeHasher th;
3818 th.walk(stripped);
3819 auto computed = th.hex();
3820 CAPTURE(stored);
3821 CAPTURE(computed);
3822 if (known_stale_SA.count(name)) {
3823 // Expected mismatch — assert it so that an accidentally-regenerated
3824 // SA also forces an update of this skip list.
3825 CHECK(computed != stored);
3826 } else {
3827 CHECK(computed == stored);
3828 }
3829 ++fluids_checked;
3830 }
3831 CHECK(fluids_checked > 0);
3832};
3833
3834TEST_CASE_METHOD(SuperAncillaryOnFixture, "Superancillary eval matches extended-precision check points for all fluids", "[ancillary]") {
3835 // Per-point tolerance comes from fastchebpure's own reported (SA)/(mp) ratio:
3836 // we demand that the C++ Chebyshev eval reproduce the multi-precision reference
3837 // to within the same accuracy fastchebpure itself achieved at that T, plus a
3838 // safety factor to absorb cross-platform floating-point jitter. The 1e-14 floor
3839 // handles points where fastchebpure's ratio rounded exactly to 1.0.
3840 const double safety_factor = 4.0;
3841 const double floor_tol = 1e-14;
3842 int fluids_checked = 0;
3843 for (auto& fluid : strsplit(CoolProp::get_global_param_string("fluids_list"), ',')) {
3844 auto jfluid = nlohmann::json::parse(get_fluid_param_string(fluid, "JSON"))[0];
3845 if (!jfluid.at("EOS")[0].contains("SUPERANCILLARY")) {
3846 continue;
3847 }
3848 CAPTURE(fluid);
3849 auto jsuper = jfluid.at("EOS")[0].at("SUPERANCILLARY");
3850 // Every SA-bearing fluid must carry check_points. Fail loudly rather
3851 // than silently skip — a new fluid added without re-running
3852 // inject_superanc_check_points.py should not slip past this test.
3853 REQUIRE(jsuper.contains("check_points"));
3855 for (const auto& pt : anc.get_check_points()) {
3856 CAPTURE(pt.T);
3857 const double tol_p = std::max(std::abs(pt.p_SA_ratio - 1.0), floor_tol) * safety_factor;
3858 const double tol_rhoL = std::max(std::abs(pt.rhoL_SA_ratio - 1.0), floor_tol) * safety_factor;
3859 const double tol_rhoV = std::max(std::abs(pt.rhoV_SA_ratio - 1.0), floor_tol) * safety_factor;
3860 CHECK(std::abs(anc.eval_sat(pt.T, 'D', 0) / pt.rhoL - 1) < tol_rhoL);
3861 CHECK(std::abs(anc.eval_sat(pt.T, 'D', 1) / pt.rhoV - 1) < tol_rhoV);
3862 CHECK(std::abs(anc.eval_sat(pt.T, 'P', 1) / pt.p - 1) < tol_p);
3863 }
3864 ++fluids_checked;
3865 }
3866 // Guard against a silent schema drift that would make every fluid skip.
3867 CHECK(fluids_checked > 0);
3868};
3869
3870TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check out of bound for superancillary", "[superanc]") {
3871 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
3872 CHECK_THROWS(AS->update(PQ_INPUTS, 100000000001325, 1.0));
3873 CHECK_THROWS(AS->update(QT_INPUTS, 1.0, 1000000));
3874}
3875
3876TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check throws for R410A", "[superanc]") {
3877 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R410A"));
3878 auto& rHEOS = *dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
3879 CHECK_THROWS(rHEOS.update_QT_pure_superanc(1.0, 300.0));
3880}
3881
3882TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check throws for REFPROP", "[superanc]") {
3883 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
3884 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("REFPROP", "WATER"));
3885 CHECK_THROWS(AS->update_QT_pure_superanc(1.0, 300.0));
3886}
3887
3888TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check Tc & pc", "[superanccrit]") {
3889 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
3890 set_config_bool(ENABLE_SUPERANCILLARIES, true);
3891 auto TcSA = AS->T_critical();
3892 auto pcSA = AS->p_critical();
3893 auto rhocSA = AS->rhomolar_critical();
3894 set_config_bool(ENABLE_SUPERANCILLARIES, false);
3895 auto TcnonSA = AS->T_critical();
3896 auto pcnonSA = AS->p_critical();
3897 auto rhocnonSA = AS->rhomolar_critical();
3898 CHECK(TcSA != TcnonSA);
3899 CHECK(pcSA != pcnonSA);
3900 CHECK(rhocSA != rhocnonSA);
3901}
3902
3903TEST_CASE_METHOD(SuperAncillaryOnFixture, "Check h_fg", "[superanc]") {
3904 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
3905 CHECK_THROWS(AS->saturated_vapor_keyed_output(iHmolar) - AS->saturated_liquid_keyed_output(iHmolar));
3906 AS->update_QT_pure_superanc(1, 300);
3907 CHECK_NOTHROW(AS->saturated_vapor_keyed_output(iHmolar) - AS->saturated_liquid_keyed_output(iHmolar));
3908}
3909
3910TEST_CASE_METHOD(SuperAncillaryOnFixture, "Performance regression; on", "[2438]") {
3911 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "CO2"));
3912 BENCHMARK("HP regression") {
3913 AS->update(HmassP_INPUTS, 300e3, 70e5);
3914 return AS;
3915 };
3916 AS->update(HmassP_INPUTS, 300e3, 70e5);
3917 std::cout << AS->Q() << '\n';
3918}
3919TEST_CASE_METHOD(SuperAncillaryOffFixture, "Performance regression; off", "[2438]") {
3920 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "CO2"));
3921 BENCHMARK("HP regression") {
3922 AS->update(HmassP_INPUTS, 300e3, 70e5);
3923 return AS;
3924 };
3925 AS->update(HmassP_INPUTS, 300e3, 70e5);
3926 std::cout << AS->Q() << '\n';
3927}
3928TEST_CASE_METHOD(SuperAncillaryOnFixture, "Performance regression for TS; on", "[2438saontime]") {
3929 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
3930 double T = 298.0;
3931 AS->update(QT_INPUTS, 1, T);
3932 auto sL = AS->saturated_liquid_keyed_output(iSmolar);
3933 auto sV = AS->saturated_vapor_keyed_output(iSmolar);
3934 auto N = 1000000U;
3935 for (auto i = 0; i < N; ++i) {
3936 AS->update(SmolarT_INPUTS, (sL + sV) / 2 + i * 1e-14, T);
3937 }
3938 CHECK(AS->T() != 0);
3939}
3940
3941TEST_CASE_METHOD(SuperAncillaryOnFixture, "Performance regression for TS; on", "[2438]") {
3942 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "CO2"));
3943 double T = 298.0;
3944 AS->update(QT_INPUTS, 1, T);
3945 auto sL = AS->saturated_liquid_keyed_output(iSmolar);
3946 auto sV = AS->saturated_vapor_keyed_output(iSmolar);
3947 BENCHMARK("ST regression") {
3948 AS->update(SmolarT_INPUTS, (sL + sV) / 2, T);
3949 return AS;
3950 };
3951}
3952
3953TEST_CASE_METHOD(SuperAncillaryOffFixture, "Performance regression for TS; off", "[2438]") {
3954 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "CO2"));
3955 double T = 298.0;
3956 AS->update(QT_INPUTS, 1, T);
3957 auto sL = AS->saturated_liquid_keyed_output(iSmolar);
3958 auto sV = AS->saturated_vapor_keyed_output(iSmolar);
3959 BENCHMARK("ST regression") {
3960 AS->update(SmolarT_INPUTS, (sL + sV) / 2, T);
3961 return AS;
3962 };
3963}
3964
3965TEST_CASE_METHOD(SuperAncillaryOnFixture, "Benchmarking caching options", "[caching]") {
3966 std::array<double, 16> buf15;
3967 buf15.fill(0.0);
3968 std::array<double, 100> buf100;
3969 buf100.fill(0.0);
3970 std::array<bool, 100> bool100;
3971 bool100.fill(false);
3972 std::vector<CachedElement> cache100(100);
3973 for (auto& i : cache100) {
3974 i = _HUGE;
3975 }
3976
3977 std::vector<std::optional<double>> opt100(100);
3978 for (auto& i : opt100) {
3979 i = _HUGE;
3980 }
3981
3982 BENCHMARK("memset array15 w/ 0") {
3983 std::memset(buf15.data(), 0, sizeof(buf15));
3984 return buf15;
3985 };
3986 BENCHMARK("std::fill_n array15") {
3987 std::fill_n(buf15.data(), 15, _HUGE);
3988 return buf15;
3989 };
3990 BENCHMARK("std::fill array15") {
3991 std::fill(buf15.begin(), buf15.end(), _HUGE);
3992 return buf15;
3993 };
3994 BENCHMARK("array15.fill()") {
3995 buf15.fill(_HUGE);
3996 return buf15;
3997 };
3998 BENCHMARK("memset array100 w/ 0") {
3999 memset(buf100.data(), 0, sizeof(buf100));
4000 return buf100;
4001 };
4002 BENCHMARK("memset bool100 w/ 0") {
4003 memset(bool100.data(), false, sizeof(bool100));
4004 return buf100;
4005 };
4006 BENCHMARK("std::fill_n array100") {
4007 std::fill_n(buf100.data(), 100, _HUGE);
4008 return buf100;
4009 };
4010 BENCHMARK("fill array100") {
4011 buf100.fill(_HUGE);
4012 return buf100;
4013 };
4014 BENCHMARK("fill cache100") {
4015 for (auto& i : cache100) {
4016 i = _HUGE;
4017 }
4018 return cache100;
4019 };
4020 BENCHMARK("fill opt100") {
4021 for (auto& i : opt100) {
4022 i = _HUGE;
4023 }
4024 return opt100;
4025 };
4026}
4027std::vector<std::tuple<double, double, double, double>> MSA22values = {
4028 {200, 199.97, 142.56, 1.29559},
4029 {300, 300.19, 214.07, 1.70203},
4030 {400, 400.98, 286.16, 1.99194},
4031 {500, 503.02, 359.49, 2.21952},
4032};
4033
4034TEST_CASE("Ideal gas thermodynamic properties", "[2589]") {
4035 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
4036
4037 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Air"));
4038 shared_ptr<CoolProp::AbstractState> RP(CoolProp::AbstractState::factory("REFPROP", "Air"));
4039
4040 auto& rRP = *dynamic_cast<REFPROPMixtureBackend*>(AS.get());
4041 auto& rHEOS = *dynamic_cast<HelmholtzEOSMixtureBackend*>(AS.get());
4042
4044 RP->specify_phase(iphase_gas);
4045
4046 double pig = 101325;
4047
4048 // Moran & Shapiro Table A-22 reference is h(T=0) = 0, but that doesn't play nicely
4049 // with tau=Tc/T = oo and delta = 0/rhor = 0
4050
4051 for (auto [T_K, h_kJkg, u_kJkg, s_kJkgK] : MSA22values) {
4052 double rho = pig / (AS->gas_constant() * T_K); // ideal-gas molar density assuming Z=1
4053 AS->update(DmolarT_INPUTS, rho, T_K);
4054 RP->update(DmolarT_INPUTS, rho, T_K);
4055
4056 CHECK(AS->smass_idealgas() / AS->gas_constant() == Catch::Approx(RP->smass_idealgas() / AS->gas_constant()));
4057 CHECK(AS->hmass_idealgas() / AS->gas_constant() == Catch::Approx(RP->hmass_idealgas() / AS->gas_constant()));
4058
4059 std::vector<double> mf(20, 1.0);
4060 auto o = rRP.call_THERM0dll(T_K, rho / 1e3, mf);
4061 CHECK(o.hmol_Jmol == Catch::Approx(RP->hmolar_idealgas()).epsilon(1e-12));
4062 CHECK(o.smol_JmolK == Catch::Approx(RP->smolar_idealgas()).epsilon(1e-12));
4063 CHECK(o.umol_Jmol == Catch::Approx(RP->umolar_idealgas()).epsilon(1e-12));
4064
4065 CAPTURE(T_K);
4066 CAPTURE(AS->hmass_idealgas());
4067 CAPTURE(AS->hmass_idealgas() - h_kJkg * 1e3);
4068 CAPTURE(AS->smass_idealgas());
4069 CAPTURE(AS->smass_idealgas() - s_kJkgK * 1e3);
4070 CAPTURE(AS->umass_idealgas());
4071 CAPTURE(AS->umass_idealgas() - u_kJkg * 1e3);
4072 }
4073}
4074TEST_CASE_METHOD(SuperAncillaryOnFixture, "Phase for solid water should throw", "[2639]") {
4075 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
4076 for (auto p_Pa : linspace(AS->p_triple() * 1.0001, AS->pmax(), 1000)) {
4077 CAPTURE(p_Pa);
4078 auto Tm = AS->melting_line(iT, iP, p_Pa);
4079 CAPTURE(Tm);
4080 CHECK_THROWS(AS->update(PT_INPUTS, p_Pa, -5 + Tm));
4081 }
4082}
4083
4084// The melting-line guards added in #2648 must honor DONT_CHECK_PROPERTY_LIMITS so the
4085// metastable/below-melting escape hatch behaves the same below and above the critical
4086// pressure. Before the fix, the supercritical-pressure branch (p > psat_max) and the
4087// other==iP branch in T_phase_determination_pure_or_pseudopure ignored the override and
4088// threw regardless. See GitHub #1936.
4089TEST_CASE_METHOD(PropertyLimitsFixture, "Below-melting PT guard honors DONT_CHECK_PROPERTY_LIMITS", "[1936]") {
4090 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "CO2"));
4091 const double Tt = AS->Ttriple();
4092 const double pc = AS->p_critical();
4093 // CO2's melting line has positive slope, so each of these (T, p) pairs sits just below
4094 // the melting line. The three pressures exercise the three distinct internal paths
4095 // that carry a melting-line guard:
4096 // - p_sub at Ttriple -> p_phase_determination, subcritical superancillary branch
4097 // - p_sup at Ttriple -> p_phase_determination, supercritical-pressure branch (p > psat_max)
4098 // - p_hiT at T > triple-routing threshold -> T_phase_determination, other==iP branch
4099 const double p_sub = 0.5 * (AS->p_triple() + pc); // between ptriple and pcrit
4100 const double p_sup = 5 * pc; // well above pcrit
4101 struct Case
4102 {
4103 double p_Pa, T;
4104 };
4105 for (const Case& c : {Case{p_sub, Tt}, Case{p_sup, Tt}, Case{1e8, 230.0}}) {
4106 CAPTURE(c.p_Pa);
4107 CAPTURE(c.T);
4108 const double Tm = AS->melting_line(iT, iP, c.p_Pa);
4109 CAPTURE(Tm);
4110 REQUIRE(c.T < Tm - 0.001); // sanity: the state really is below the melting line
4111 // Default: below the melting line throws
4112 {
4113 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, false);
4114 CHECK_THROWS(AS->update(PT_INPUTS, c.p_Pa, c.T));
4115 }
4116 // With the override, the same state must resolve to a finite (metastable) density
4117 {
4118 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, true);
4119 CHECK_NOTHROW(AS->update(PT_INPUTS, c.p_Pa, c.T));
4120 // Guard against a silent no-op update leaving the prior case's state behind
4121 CHECK(AS->T() == Catch::Approx(c.T).epsilon(1e-12));
4122 CHECK(AS->p() == Catch::Approx(c.p_Pa).epsilon(1e-12));
4123 CHECK(ValidNumber(AS->rhomolar()));
4124 CHECK(AS->rhomolar() > 0);
4125 }
4126 }
4127}
4128
4129TEST_CASE("PT update below melting-line pmin succeeds without specify_phase for all HEOS fluids", "[melting]") {
4130 // Before the fix in HelmholtzEOSMixtureBackend.cpp, the phase-determination
4131 // paths called melting_line(iT, iP, p) unconditionally for any fluid with a
4132 // melting-line fit, including when p was below pmin (the triple-point pressure).
4133 // The evaluator threw an out-of-bounds error in that case, breaking any call
4134 // that omits specify_phase when the pressure is below the fit's lower bound --
4135 // for example Argon in an air mixture where the partial pressure (~941 Pa) is
4136 // far below Argon's pmin (~69 688 Pa).
4137 using namespace CoolProp;
4138 const std::vector<std::string> all_fluids = strsplit(get_global_param_string("fluids_list"), ',');
4139 for (const auto& name : all_fluids) {
4140 std::shared_ptr<AbstractState> AS;
4141 try {
4142 AS.reset(AbstractState::factory("HEOS", name));
4143 } catch (...) {
4144 continue;
4145 }
4146 if (!AS->has_melting_line()) continue;
4147
4148 const double pmin = AS->melting_line(iP_min, -1, -1);
4149 // Just below the lower bound of the melting-line fit
4150 const double p_test = 0.99 * pmin;
4151 // Well above the triple point temperature -- unambiguously gas at p < ptriple
4152 const double T_test = AS->Ttriple() * 1.1;
4153
4154 SECTION(name) {
4155 CAPTURE(pmin);
4156 CAPTURE(p_test);
4157 CAPTURE(T_test);
4158 CHECK_NOTHROW(AS->update(PT_INPUTS, p_test, T_test));
4159 CHECK(ValidNumber(AS->rhomolar()));
4160 CHECK(AS->rhomolar() > 0);
4161 }
4162 }
4163}
4164
4165TEST_CASE("HeavyWater (D2O) melting line matches Herrig et al. check values", "[melting]") {
4166 // Melting-pressure equations Eqs. (4)-(7) of Herrig, Thol, Harvey, Lemmon,
4167 // "A Reference Equation of State for Heavy Water," J. Phys. Chem. Ref. Data
4168 // 47, 043102 (2018). The values below are the paper's computer-implementation
4169 // check values (Sec. 3.4) for the four ice-structure branches.
4170 using namespace CoolProp;
4171 std::shared_ptr<AbstractState> AS(AbstractState::factory("HEOS", "HeavyWater"));
4172 REQUIRE(AS->has_melting_line());
4173 struct Pt
4174 {
4175 double T_K, p_Pa;
4176 };
4177 // (T, p) check points, one per ice-structure branch; p in MPa from the paper.
4178 // NB: melting pressure p(T) is multi-valued near the ice-Ih/III/V triples (the
4179 // ice-Ih branch folds back), so we validate via the single-valued T(p) inverse
4180 // — the direction the parser indexes by pressure, and the one the EOS uses for
4181 // its lower temperature bound. Recovering each branch's check temperature from
4182 // its check pressure exercises that branch's transcribed coefficients.
4183 const Pt pts[] = {
4184 {270.0, 0.837888413e2 * 1e6}, // ice Ih, Eq. (4)
4185 {255.0, 0.236470168e3 * 1e6}, // ice III, Eq. (5)
4186 {275.0, 0.619526971e3 * 1e6}, // ice V, Eq. (6)
4187 {300.0, 0.959203594e3 * 1e6}, // ice VI, Eq. (7)
4188 };
4189 for (const Pt& pt : pts) {
4190 CAPTURE(pt.T_K, pt.p_Pa);
4191 const double T = AS->melting_line(iT, iP, pt.p_Pa);
4192 CHECK(T == Catch::Approx(pt.T_K).epsilon(1e-6));
4193 }
4194}
4195
4196TEST_CASE("REFPROP melting_line honors iP_min/iT_min/iP_max/iT_max sentinels", "[melting][REFPROP]") {
4197 Skip_if_No_REFPROP(); // Skip this test if REFPROPMixture backend is not available
4198 std::shared_ptr<CoolProp::AbstractState> RP(CoolProp::AbstractState::factory("REFPROP", "Water"));
4199 std::shared_ptr<CoolProp::AbstractState> HE(CoolProp::AbstractState::factory("HEOS", "Water"));
4200
4201 double pmin = RP->melting_line(iP_min, -1, -1);
4202 double Tmin = RP->melting_line(iT_min, -1, -1);
4203
4204 // iP_min is the triple-point pressure, for consistency with HEOS.
4205 CHECK(pmin == Catch::Approx(RP->p_triple()));
4206 CHECK(pmin == Catch::Approx(HE->melting_line(iP_min, -1, -1)).epsilon(1e-3));
4207
4208 // iT_min is the melting line's lowest temperature. For water that is the ice
4209 // Ih/III junction (251.165 K), which lies BELOW the triple-point temperature --
4210 // the one case (with heavy water) where it differs from the triple point.
4211 CHECK(Tmin == Catch::Approx(251.165).epsilon(1e-5));
4212 CHECK(Tmin < RP->Ttriple());
4213 // ...and Tmin is a valid point on the melting line (the floor MELTT accepts).
4214 CHECK_NOTHROW(RP->melting_line(iP, iT, Tmin));
4215
4216 // The high-pressure end must be a finite value above the minimum.
4217 double pmax = RP->melting_line(iP_max, -1, -1);
4218 double Tmax = RP->melting_line(iT_max, -1, -1);
4219 CHECK(ValidNumber(pmax));
4220 CHECK(ValidNumber(Tmax));
4221 CHECK(pmax > pmin);
4222 CHECK(Tmax > Tmin);
4223
4224 // A genuine melting lookup must still work and agree with HEOS (regression).
4225 CHECK(RP->melting_line(iT, iP, 1e8) == Catch::Approx(HE->melting_line(iT, iP, 1e8)).epsilon(1e-3));
4226
4227 // A truly invalid input pair must throw a ValueError -- not crash building
4228 // its own error message via get_parameter_information() on an invalid key.
4229 CHECK_THROWS_AS(RP->melting_line(iDmolar, iHmolar, 0), CoolProp::ValueError);
4230}
4231
4232// Tests for cubic EOS superancillaries (#2739)
4233TEST_CASE("Cubic superancillary saturation_ancillary accuracy vs EOS flash", "[cubic_superanc][2739]") {
4234 for (const auto& backend : std::vector<std::string>{"PR", "SRK"}) {
4235 CAPTURE(backend);
4236 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory(backend, "Propane"));
4237 auto& ACB = *dynamic_cast<AbstractCubicBackend*>(AS.get());
4238 // Use the Tc from the superancillary (the max T supported by its domain)
4239 double Tc_sa = ACB.calc_superanc_Tmax();
4240
4241 SECTION(backend + " accuracy across T range (0.3 to 0.99 of superanc Tc)") {
4242 for (double frac : {0.3, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99}) {
4243 double T = frac * Tc_sa;
4244 CAPTURE(T);
4245 AS->update(QT_INPUTS, 0, T);
4246 double p_eos = AS->p();
4247 double rhoL_eos = AS->saturated_liquid_keyed_output(iDmolar);
4248 double rhoV_eos = AS->saturated_vapor_keyed_output(iDmolar);
4249
4250 double p_anc = ACB.calc_saturation_ancillary(iP, 0, iT, T);
4251 double rhoL_anc = ACB.calc_saturation_ancillary(iDmolar, 0, iT, T);
4252 double rhoV_anc = ACB.calc_saturation_ancillary(iDmolar, 1, iT, T);
4253
4254 CAPTURE(p_eos);
4255 CAPTURE(p_anc);
4256 CAPTURE(rhoL_eos);
4257 CAPTURE(rhoL_anc);
4258 CAPTURE(rhoV_eos);
4259 CAPTURE(rhoV_anc);
4260 // Superancillaries achieve < 1e-3 relative error everywhere
4261 CHECK(std::abs(p_anc - p_eos) / p_eos < 1e-3);
4262 CHECK(std::abs(rhoL_anc - rhoL_eos) / rhoL_eos < 1e-3);
4263 CHECK(std::abs(rhoV_anc - rhoV_eos) / rhoV_eos < 1e-3);
4264 }
4265 }
4266
4267 // Very close to the superancillary critical point the EOS flash becomes unreliable,
4268 // but the superancillary is still valid. Check that the returned values are physically
4269 // reasonable: rhoL > rhoV, p > 0, and p converges toward the superancillary's own pc.
4270 SECTION(backend + " physically reasonable very close to superanc Tc") {
4271 // Use the superancillary's pc (p at T just below Tmax) as the reference,
4272 // not AS->p_critical() which reflects the real fluid, not the cubic model.
4273 double pc_sa = ACB.calc_saturation_ancillary(iP, 0, iT, Tc_sa * (1.0 - 1e-7));
4274 for (double frac : {0.9999, 0.99999, 0.999999, 1.0 - 1e-7}) {
4275 double T = frac * Tc_sa;
4276 CAPTURE(T);
4277 double p_anc = ACB.calc_saturation_ancillary(iP, 0, iT, T);
4278 double rhoL_anc = ACB.calc_saturation_ancillary(iDmolar, 0, iT, T);
4279 double rhoV_anc = ACB.calc_saturation_ancillary(iDmolar, 1, iT, T);
4280 CAPTURE(p_anc);
4281 CAPTURE(rhoL_anc);
4282 CAPTURE(rhoV_anc);
4283 CHECK(p_anc > 0);
4284 CHECK(rhoL_anc > rhoV_anc);
4285 CHECK(std::abs(p_anc - pc_sa) / pc_sa < 0.01); // within 1 % of superanc pc
4286 }
4287 }
4288 }
4289}
4290
4291TEST_CASE("Cubic superancillary update_QT_pure_superanc", "[cubic_superanc][2739]") {
4292 for (const auto& backend : std::vector<std::string>{"PR", "SRK"}) {
4293 CAPTURE(backend);
4294 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory(backend, "Propane"));
4295 auto& ACB = *dynamic_cast<AbstractCubicBackend*>(AS.get());
4296 double Tc_sa = ACB.calc_superanc_Tmax();
4297
4298 SECTION(backend + " update_QT_pure_superanc consistency at several T") {
4299 for (double frac : {0.5, 0.7, 0.9, 0.9999, 0.99999, 1.0 - 1e-7}) {
4300 double T = frac * Tc_sa;
4301 CAPTURE(T);
4302 CHECK_NOTHROW(AS->update_QT_pure_superanc(0.5, T));
4303 CHECK(std::abs(AS->T() - T) < 1e-10);
4304 CHECK(AS->p() > 0);
4305 }
4306 }
4307
4308 SECTION(backend + " update_QT_pure_superanc Q=0 and Q=1 densities bracket Q=0.5") {
4309 double T = 0.8 * Tc_sa;
4310 AS->update_QT_pure_superanc(0.0, T);
4311 double rhoL = AS->rhomolar();
4312 AS->update_QT_pure_superanc(1.0, T);
4313 double rhoV = AS->rhomolar();
4314 AS->update_QT_pure_superanc(0.5, T);
4315 double rhoM = AS->rhomolar();
4316 CAPTURE(rhoL);
4317 CAPTURE(rhoV);
4318 CAPTURE(rhoM);
4319 CHECK(rhoL > rhoM);
4320 CHECK(rhoM > rhoV);
4321 }
4322 }
4323}
4324
4325TEST_CASE("Cubic pure-fluid DmolarT/DmassT round-trip vs PT", "[cubic_DmolarT][2673]") {
4326 for (const auto& backend : std::vector<std::string>{"PR", "SRK"}) {
4327 CAPTURE(backend);
4328 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory(backend, "nButane"));
4329 auto& ACB = *dynamic_cast<AbstractCubicBackend*>(AS.get());
4330 const double Tc_sa = ACB.calc_superanc_Tmax();
4331
4332 SECTION(backend + " liquid round-trip subcooled") {
4333 // Subcooled liquid: p > p_sat(T) at T < Tc. Using PT_INPUTS at
4334 // (101325, 350 K) for nButane lands on the vapor side (p_sat(350) ~ 1.5 MPa),
4335 // so set p_in well above p_sat(T_in) to actually exercise the liquid branch.
4336 const double T_in = 0.7 * Tc_sa;
4337 const double p_sat = ACB.calc_saturation_ancillary(iP, 0, iT, T_in);
4338 const double p_in = 2.0 * p_sat;
4339 AS->update(PT_INPUTS, p_in, T_in);
4340 const double rho_molar = AS->rhomolar();
4341 const double rho_mass = AS->rhomass();
4342
4343 CHECK_NOTHROW(AS->update(DmolarT_INPUTS, rho_molar, T_in));
4344 CHECK(AS->p() == Catch::Approx(p_in).epsilon(1e-6));
4345 CHECK(AS->T() == Catch::Approx(T_in).epsilon(1e-10));
4346 CHECK(AS->rhomolar() == Catch::Approx(rho_molar).epsilon(1e-12));
4347 CHECK(AS->phase() == iphase_liquid);
4348
4349 CHECK_NOTHROW(AS->update(DmassT_INPUTS, rho_mass, T_in));
4350 CHECK(AS->p() == Catch::Approx(p_in).epsilon(1e-6));
4351 CHECK(AS->rhomass() == Catch::Approx(rho_mass).epsilon(1e-12));
4352 CHECK(AS->phase() == iphase_liquid);
4353 }
4354
4355 SECTION(backend + " vapor round-trip below Tsat") {
4356 // pick T well below superanc Tc and a low pressure to guarantee vapor
4357 const double T_in = 0.7 * Tc_sa;
4358 const double p_sat = ACB.calc_saturation_ancillary(iP, 1, iT, T_in);
4359 const double p_in = 0.5 * p_sat; // half saturation pressure -> vapor
4360 AS->update(PT_INPUTS, p_in, T_in);
4361 const double rho_molar = AS->rhomolar();
4362 CHECK_NOTHROW(AS->update(DmolarT_INPUTS, rho_molar, T_in));
4363 CHECK(AS->p() == Catch::Approx(p_in).epsilon(1e-6));
4364 CHECK(AS->phase() == iphase_gas);
4365 }
4366
4367 SECTION(backend + " two-phase region returns iphase_twophase") {
4368 const double T_in = 0.7 * Tc_sa;
4369 const double rhoL = ACB.calc_saturation_ancillary(iDmolar, 0, iT, T_in);
4370 const double rhoV = ACB.calc_saturation_ancillary(iDmolar, 1, iT, T_in);
4371 const double p_sat = ACB.calc_saturation_ancillary(iP, 0, iT, T_in);
4372 const double rho_mid = 0.5 * (rhoL + rhoV); // midpoint of the dome
4373 CHECK_NOTHROW(AS->update(DmolarT_INPUTS, rho_mid, T_in));
4374 CHECK(AS->phase() == iphase_twophase);
4375 CHECK(AS->p() == Catch::Approx(p_sat).epsilon(1e-6));
4376 CHECK(AS->Q() > 0.0);
4377 CHECK(AS->Q() < 1.0);
4378 }
4379
4380 SECTION(backend + " supercritical T returns single phase") {
4381 const double T_in = 1.5 * Tc_sa;
4382 const double p_in = 5e6;
4383 AS->update(PT_INPUTS, p_in, T_in);
4384 const double rho_molar = AS->rhomolar();
4385 CHECK_NOTHROW(AS->update(DmolarT_INPUTS, rho_molar, T_in));
4386 CHECK(AS->p() == Catch::Approx(p_in).epsilon(1e-6));
4387 const auto ph = AS->phase();
4389 }
4390 }
4391}
4392
4393// ============================================================================
4394// Lemmon-Akasaka 2022 R-1234yf EOS check values (Table 7)
4395// Lemmon & Akasaka, Int. J. Thermophys. 43:119 (2022), DOI 10.1007/s10765-022-03015-y
4396// Table 7: density in mol/dm^3, pressure in MPa, cv/cp in J/(mol K), w in m/s
4397// ============================================================================
4398
4399TEST_CASE("Lemmon-IJT-2022 R1234yf pure fluid check values", "[R1234yf],[Lemmon-IJT-2022]") {
4400 const double tol = 1e-4; // 0.01% relative
4401 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf"));
4402
4403 // T=280 K, rho=0 mol/dm3 (ideal-gas limit): cv=89.2037, cp=97.5182, w=149.388
4404 SECTION("T=280 K, rho->0 (ideal-gas limit)") {
4405 AS->update(DmolarT_INPUTS, 0.001, 280.0);
4406 CAPTURE(AS->cvmolar());
4407 CAPTURE(AS->cpmolar());
4408 CAPTURE(AS->speed_sound());
4409 CHECK(AS->cvmolar() == Catch::Approx(89.2037).epsilon(tol));
4410 CHECK(AS->cpmolar() == Catch::Approx(97.5182).epsilon(tol));
4411 CHECK(AS->speed_sound() == Catch::Approx(149.388).epsilon(tol));
4412 }
4413 // T=280 K, rho=11 mol/dm3=11000 mol/m3: p=28.95760 MPa, cv=101.930, cp=139.307, w=738.905
4414 SECTION("T=280 K, rho=11000 mol/m3 (compressed liquid)") {
4415 AS->update(DmolarT_INPUTS, 11000.0, 280.0);
4416 CAPTURE(AS->p());
4417 CAPTURE(AS->cvmolar());
4418 CAPTURE(AS->cpmolar());
4419 CAPTURE(AS->speed_sound());
4420 CHECK(AS->p() == Catch::Approx(28.95760e6).epsilon(tol));
4421 CHECK(AS->cvmolar() == Catch::Approx(101.930).epsilon(tol));
4422 CHECK(AS->cpmolar() == Catch::Approx(139.307).epsilon(tol));
4423 CHECK(AS->speed_sound() == Catch::Approx(738.905).epsilon(tol));
4424 }
4425 // T=280 K, rho=0.1 mol/dm3=100 mol/m3: p=0.2185345 MPa, cv=91.3497, cp=102.623, w=141.882
4426 SECTION("T=280 K, rho=100 mol/m3 (gas)") {
4427 AS->update(DmolarT_INPUTS, 100.0, 280.0);
4428 CAPTURE(AS->p());
4429 CAPTURE(AS->cvmolar());
4430 CAPTURE(AS->cpmolar());
4431 CAPTURE(AS->speed_sound());
4432 CHECK(AS->p() == Catch::Approx(0.2185345e6).epsilon(tol));
4433 CHECK(AS->cvmolar() == Catch::Approx(91.3497).epsilon(tol));
4434 CHECK(AS->cpmolar() == Catch::Approx(102.623).epsilon(tol));
4435 CHECK(AS->speed_sound() == Catch::Approx(141.882).epsilon(tol));
4436 }
4437 // T=340 K, rho=8 mol/dm3=8000 mol/m3: p=2.309798 MPa, cv=113.805, cp=195.748, w=265.888
4438 SECTION("T=340 K, rho=8000 mol/m3 (liquid)") {
4439 AS->update(DmolarT_INPUTS, 8000.0, 340.0);
4440 CAPTURE(AS->p());
4441 CAPTURE(AS->cvmolar());
4442 CAPTURE(AS->cpmolar());
4443 CAPTURE(AS->speed_sound());
4444 CHECK(AS->p() == Catch::Approx(2.309798e6).epsilon(tol));
4445 CHECK(AS->cvmolar() == Catch::Approx(113.805).epsilon(tol));
4446 CHECK(AS->cpmolar() == Catch::Approx(195.748).epsilon(tol));
4447 CHECK(AS->speed_sound() == Catch::Approx(265.888).epsilon(tol));
4448 }
4449 // T=340 K, rho=1 mol/dm3=1000 mol/m3: p=1.855076 MPa, cv=113.479, cp=168.646, w=114.354
4450 SECTION("T=340 K, rho=1000 mol/m3 (superheated vapor)") {
4451 AS->update(DmolarT_INPUTS, 1000.0, 340.0);
4452 CAPTURE(AS->p());
4453 CAPTURE(AS->cvmolar());
4454 CAPTURE(AS->cpmolar());
4455 CAPTURE(AS->speed_sound());
4456 CHECK(AS->p() == Catch::Approx(1.855076e6).epsilon(tol));
4457 CHECK(AS->cvmolar() == Catch::Approx(113.479).epsilon(tol));
4458 CHECK(AS->cpmolar() == Catch::Approx(168.646).epsilon(tol));
4459 CHECK(AS->speed_sound() == Catch::Approx(114.354).epsilon(tol));
4460 }
4461 // T=368 K, rho=4.2 mol/dm3=4200 mol/m3: p=3.394716 MPa, cv=149.703, cp=48981.3, w=76.3597
4462 SECTION("T=368 K, rho=4200 mol/m3 (near-critical)") {
4463 AS->update(DmolarT_INPUTS, 4200.0, 368.0);
4464 CAPTURE(AS->p());
4465 CAPTURE(AS->cvmolar());
4466 CAPTURE(AS->cpmolar());
4467 CAPTURE(AS->speed_sound());
4468 CHECK(AS->p() == Catch::Approx(3.394716e6).epsilon(tol));
4469 CHECK(AS->cvmolar() == Catch::Approx(149.703).epsilon(tol));
4470 // Cp diverges near the critical point; use a looser tolerance
4471 CHECK(AS->cpmolar() == Catch::Approx(48981.3).epsilon(5e-3));
4472 CHECK(AS->speed_sound() == Catch::Approx(76.3597).epsilon(tol));
4473 }
4474}
4475
4476TEST_CASE("Lemmon-IJT-2022 R1234yf fixed-point constants", "[R1234yf],[Lemmon-IJT-2022]") {
4477 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf"));
4478 CHECK(AS->T_critical() == Catch::Approx(367.85).epsilon(1e-5));
4479 CHECK(AS->p_critical() == Catch::Approx(3384400.0).epsilon(1e-4));
4480 CHECK(AS->rhomolar_critical() == Catch::Approx(4180.0).epsilon(1e-4));
4481 CHECK(AS->Ttriple() == Catch::Approx(121.6).epsilon(1e-4));
4482}
4483
4484// McLinden & Akasaka, J. Chem. Eng. Data 65:4201 (2020), DOI 10.1021/acs.jced.9b01198 —
4485// ISO 17584 international-standard EOS for R-1336mzz(Z). The paper publishes critical
4486// parameters in Table 7 (Tc=444.5 K, rhoc=3.044 mol/L, pc=2.903 MPa) and molar mass in
4487// the text; it does not include a computer-verification table of (p, cv, cp, w) check
4488// values, so regression coverage at the EOS-coefficient level comes instead from the
4489// R-1336mzz(Z)/R-1130(E) alphar check in the NIST-IR-8570 mixture test below (Table 4-4
4490// departure function exercises all pure-fluid residual Helmholtz terms indirectly).
4491//
4492// p_critical / rhomolar_critical tolerances are set to the paper's stated precision
4493// (4 sig figs, ~1e-3) rather than EOS-level precision: with the SUPERANCILLARY block
4494// loaded, those accessors return the EOS's *numerical* critical (where dp/drho|T = 0
4495// and d2p/drho2|T = 0, evaluated from the SA crit_anc), which for this fluid sits at
4496// pc=2.9037 MPa, rhoc=3044.5 mol/m^3 — both round to the paper values at 4 sig figs.
4497TEST_CASE("McLinden-JCED-2020 R1336mzz(Z) fixed-point constants", "[R1336mzzZ],[McLinden-JCED-2020-R1336mzzZ]") {
4498 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1336mzz(Z)"));
4499 CHECK(AS->T_critical() == Catch::Approx(444.5).epsilon(1e-5));
4500 CHECK(AS->p_critical() == Catch::Approx(2.903e6).epsilon(1e-3));
4501 CHECK(AS->rhomolar_critical() == Catch::Approx(3044.0).epsilon(1e-3));
4502 CHECK(AS->molar_mass() == Catch::Approx(0.164056).epsilon(1e-5));
4503}
4504
4505// ============================================================================
4506// Mixture binary pair checks — Bell-JPCRD-2022 and Bell-JPCRD-2023
4507//
4508// Check values are the dimensionless residual Helmholtz energy alphar at the
4509// state point defined by rho/rho_red = 0.8 and T_red/T = 0.8 (z1 = 0.4).
4510//
4511// Table XI from Bell, JPCRD 51, 013103 (2022), DOI 10.1063/5.0083545
4512// (pairs unique to Paper 1: R1234yf/R1234zeE, R1234yf/R134a, R134a/R1234zeE)
4513//
4514// Table XIII from Bell, JPCRD 52, 013101 (2023), DOI 10.1063/5.0124188
4515// (all five pairs in Paper 2, including R125/R1234yf, R1234yf/R152a,
4516// R1234zeE/R227ea which supersede Paper 1 interim models)
4517//
4518// Note: Paper 1's Table XI used a pre-publication version of the R1234yf EOS
4519// and matches only to ~1e-6 with the final Lemmon-IJT-2022 EOS used here.
4520// Paper 2's Table XIII used the final EOS and agrees to ~1e-10.
4521// ============================================================================
4522
4523TEST_CASE("Bell-JPCRD-2022 mixture alphar check values (Table XI)", "[mixtures],[Bell-JPCRD-2022]") {
4524 // Table XI was computed with a pre-publication R1234yf EOS. Pairs containing R1234yf
4525 // use check values recomputed with the final Lemmon-IJT-2022 R1234yf EOS (the R1234yf
4526 // EOS change shifts alphar by ~0.4%). The R134a+R1234zeE pair contains no R1234yf and
4527 // agrees with Table XI to ~1e-10.
4528 const double tol = 1e-10;
4529
4530 // R1234yf + R1234zeE: z1=0.4, T=469 K, rho=3399 mol/m3
4531 // Table XI (pre-pub R1234yf EOS): -0.46059464176252; Lemmon-IJT-2022 R1234yf EOS: -0.46467899824257763
4532 SECTION("R1234yf + R1234ze(E): Table XI") {
4533 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R1234zeE"));
4534 AS->set_mole_fractions({0.4, 0.6});
4535 AS->specify_phase(CoolProp::iphase_gas);
4536 AS->update(DmolarT_INPUTS, 3399.0, 469.0);
4537 CAPTURE(AS->alphar());
4538 CHECK(AS->alphar() == Catch::Approx(-0.46467899824257763).epsilon(tol));
4539 }
4540 // R1234yf + R134a: z1=0.4, T=462 K, rho=3698 mol/m3
4541 // Table XI (pre-pub R1234yf EOS): -0.46550859128831; Lemmon-IJT-2022 R1234yf EOS: -0.46550859405816197
4542 SECTION("R1234yf + R134a: Table XI") {
4543 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R134a"));
4544 AS->set_mole_fractions({0.4, 0.6});
4545 AS->specify_phase(CoolProp::iphase_gas);
4546 AS->update(DmolarT_INPUTS, 3698.0, 462.0);
4547 CAPTURE(AS->alphar());
4548 CHECK(AS->alphar() == Catch::Approx(-0.46550859405816197).epsilon(tol));
4549 }
4550 // R134a + R1234zeE: z1=0.4, T=472 K, rho=3639 mol/m3, alphar=-0.46245130334193
4551 SECTION("R134a + R1234ze(E): Table XI") {
4552 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R134a&R1234zeE"));
4553 AS->set_mole_fractions({0.4, 0.6});
4554 AS->specify_phase(CoolProp::iphase_gas);
4555 AS->update(DmolarT_INPUTS, 3639.0, 472.0);
4556 CAPTURE(AS->alphar());
4557 CHECK(AS->alphar() == Catch::Approx(-0.46245130334193).epsilon(tol));
4558 }
4559 // Inverted-order sections: verify betaT inversion is applied correctly in both orderings.
4560 // The GERG reducing function is symmetric under component swap + reciprocal beta, so
4561 // alphar must agree with the tabulated value within floating-point precision.
4562 SECTION("R1234yf + R1234ze(E): inverted component order") {
4563 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R1234yf"));
4564 AS->set_mole_fractions({0.6, 0.4});
4565 AS->specify_phase(CoolProp::iphase_gas);
4566 AS->update(DmolarT_INPUTS, 3399.0, 469.0);
4567 CAPTURE(AS->alphar());
4568 CHECK(AS->alphar() == Catch::Approx(-0.46467899824257763).epsilon(tol));
4569 }
4570 SECTION("R134a + R1234ze(E): inverted component order") {
4571 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R134a"));
4572 AS->set_mole_fractions({0.6, 0.4});
4573 AS->specify_phase(CoolProp::iphase_gas);
4574 AS->update(DmolarT_INPUTS, 3639.0, 472.0);
4575 CAPTURE(AS->alphar());
4576 CHECK(AS->alphar() == Catch::Approx(-0.46245130334193).epsilon(tol));
4577 }
4578 SECTION("R1234yf + R134a: inverted component order") {
4579 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R134a&R1234yf"));
4580 AS->set_mole_fractions({0.6, 0.4});
4581 AS->specify_phase(CoolProp::iphase_gas);
4582 AS->update(DmolarT_INPUTS, 3698.0, 462.0);
4583 CAPTURE(AS->alphar());
4584 CHECK(AS->alphar() == Catch::Approx(-0.46550859405816197).epsilon(tol));
4585 }
4586}
4587
4588TEST_CASE("Bell-JPCRD-2023 mixture alphar check values (Table XIII)", "[mixtures],[Bell-JPCRD-2023]") {
4589 // Table XIII used the final Lemmon-IJT-2022 R1234yf EOS; expect ~1e-10 agreement
4590 const double tol = 1e-10;
4591
4592 // R32 + R1234yf: z1=0.4, T=445 K, rho=4149 mol/m3, alphar=-0.47311064743911
4593 SECTION("R32 + R1234yf: Table XIII") {
4594 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R1234yf"));
4595 AS->set_mole_fractions({0.4, 0.6});
4596 AS->specify_phase(CoolProp::iphase_gas);
4597 AS->update(DmolarT_INPUTS, 4149.0, 445.0);
4598 CAPTURE(AS->alphar());
4599 CHECK(AS->alphar() == Catch::Approx(-0.47311064743911).epsilon(tol));
4600 }
4601 // R32 + R1234zeE: z1=0.4, T=451 K, rho=4242 mol/m3, alphar=-0.48576186760231
4602 SECTION("R32 + R1234ze(E): Table XIII") {
4603 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R1234zeE"));
4604 AS->set_mole_fractions({0.4, 0.6});
4605 AS->specify_phase(CoolProp::iphase_gas);
4606 AS->update(DmolarT_INPUTS, 4242.0, 451.0);
4607 CAPTURE(AS->alphar());
4608 CHECK(AS->alphar() == Catch::Approx(-0.48576186760231).epsilon(tol));
4609 }
4610 // R125 + R1234yf: z1=0.4, T=445 K, rho=3513 mol/m3, alphar=-0.46576307479447
4611 SECTION("R125 + R1234yf: Table XIII") {
4612 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R125&R1234yf"));
4613 AS->set_mole_fractions({0.4, 0.6});
4614 AS->specify_phase(CoolProp::iphase_gas);
4615 AS->update(DmolarT_INPUTS, 3513.0, 445.0);
4616 CAPTURE(AS->alphar());
4617 CHECK(AS->alphar() == Catch::Approx(-0.46576307479447).epsilon(tol));
4618 }
4619 // R1234yf + R152a: z1=0.4, T=469 K, rho=3930 mol/m3, alphar=-0.48967548916638
4620 SECTION("R1234yf + R152a: Table XIII") {
4621 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R152a"));
4622 AS->set_mole_fractions({0.4, 0.6});
4623 AS->specify_phase(CoolProp::iphase_gas);
4624 AS->update(DmolarT_INPUTS, 3930.0, 469.0);
4625 CAPTURE(AS->alphar());
4626 CHECK(AS->alphar() == Catch::Approx(-0.48967548916638).epsilon(tol));
4627 }
4628 // R1234zeE + R227ea: z1=0.4, T=470 K, rho=3023 mol/m3, alphar=-0.45378834770736
4629 SECTION("R1234ze(E) + R227ea: Table XIII") {
4630 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R227ea"));
4631 AS->set_mole_fractions({0.4, 0.6});
4632 AS->specify_phase(CoolProp::iphase_gas);
4633 AS->update(DmolarT_INPUTS, 3023.0, 470.0);
4634 CAPTURE(AS->alphar());
4635 CHECK(AS->alphar() == Catch::Approx(-0.45378834770736).epsilon(tol));
4636 }
4637}
4638
4639// ============================================================================
4640// NIST IR 8570 (McLinden et al., 2025, DOI 10.6028/NIST.IR.8570) Tables 4-3
4641// and 4-4 mixing parameters: alphar check values for the three binary pairs
4642// added in this PR:
4643// * R-1132(E)/R-32 (F=0, no departure)
4644// * R-1132(E)/R-1234yf (F=0, no departure)
4645// * R-1336mzz(Z)/R-1130(E) (F=1, one-term exponential departure)
4646//
4647// State point convention follows Bell-JPCRD-2022/2023: z1 = 0.4 with
4648// tau = T_red(x)/T = 0.8, delta = rho/rho_red(x) = 0.8, computed from the
4649// GERG-2008 reducing functions (NIST IR 8570 Eqs. 4-9, 4-10) using the
4650// betaT/gammaT/betaV/gammaV from Table 4-3, then T and rho rounded to the
4651// nearest K and mol/m^3 for portable hard-coded constants. alphar is then
4652// evaluated at the rounded state point. Inverted-component-order sections
4653// verify the GERG reducing-function symmetry (component swap + reciprocal
4654// beta = same alphar).
4655// ============================================================================
4656
4657TEST_CASE("NIST IR 8570 mixture alphar check values (Tables 4-3 / 4-4)", "[mixtures],[NIST-IR-8570]") {
4658 const double tol = 1e-10;
4659
4660 // R-1132(E) + R-32: betaT=0.9509, gammaT=1.0281, betaV=1.0336, gammaV=1.0040
4661 // T_red = 353.064175 K, rho_red = 7520.0025 mol/m^3 -> T = 441 K, rho = 6016 mol/m^3
4662 SECTION("R-1132(E) + R-32: Table 4-3") {
4663 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1132E&R32"));
4664 AS->set_mole_fractions({0.4, 0.6});
4665 AS->specify_phase(CoolProp::iphase_gas);
4666 AS->update(DmolarT_INPUTS, 6016.0, 441.0);
4667 CAPTURE(AS->alphar());
4668 CHECK(AS->alphar() == Catch::Approx(-0.5172864096181671).epsilon(tol));
4669 }
4670 SECTION("R-1132(E) + R-32: inverted component order") {
4671 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R1132E"));
4672 AS->set_mole_fractions({0.6, 0.4});
4673 AS->specify_phase(CoolProp::iphase_gas);
4674 AS->update(DmolarT_INPUTS, 6016.0, 441.0);
4675 CAPTURE(AS->alphar());
4676 CHECK(AS->alphar() == Catch::Approx(-0.5172864096181671).epsilon(tol));
4677 }
4678
4679 // R-1132(E) + R-1234yf: betaT=0.9835, gammaT=0.9999, betaV=0.9972, gammaV=1.0197
4680 // T_red = 359.566316 K, rho_red = 4941.0809 mol/m^3 -> T = 449 K, rho = 3953 mol/m^3
4681 SECTION("R-1132(E) + R-1234yf: Table 4-3") {
4682 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1132E&R1234yf"));
4683 AS->set_mole_fractions({0.4, 0.6});
4684 AS->specify_phase(CoolProp::iphase_gas);
4685 AS->update(DmolarT_INPUTS, 3953.0, 449.0);
4686 CAPTURE(AS->alphar());
4687 CHECK(AS->alphar() == Catch::Approx(-0.4749927188694013).epsilon(tol));
4688 }
4689 SECTION("R-1132(E) + R-1234yf: inverted component order") {
4690 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R1132E"));
4691 AS->set_mole_fractions({0.6, 0.4});
4692 AS->specify_phase(CoolProp::iphase_gas);
4693 AS->update(DmolarT_INPUTS, 3953.0, 449.0);
4694 CAPTURE(AS->alphar());
4695 CHECK(AS->alphar() == Catch::Approx(-0.4749927188694013).epsilon(tol));
4696 }
4697
4698 // R-1336mzz(Z) + R-1130(E): betaT=0.9740, gammaT=0.9195, betaV=1.1480, gammaV=0.9251, F=1.0
4699 // Departure function (Table 4-4): one-term exponential, n=-0.277036, t=2.956973, d=1, l=1
4700 // T_red = 466.899749 K, rho_red = 3931.9577 mol/m^3 -> T = 584 K, rho = 3146 mol/m^3
4701 SECTION("R-1336mzz(Z) + R-1130(E): Table 4-3 + 4-4") {
4702 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1336mzz(Z)&R1130(E)"));
4703 AS->set_mole_fractions({0.4, 0.6});
4704 AS->specify_phase(CoolProp::iphase_gas);
4705 AS->update(DmolarT_INPUTS, 3146.0, 584.0);
4706 CAPTURE(AS->alphar());
4707 CHECK(AS->alphar() == Catch::Approx(-0.4936442709738808).epsilon(tol));
4708 }
4709 SECTION("R-1336mzz(Z) + R-1130(E): inverted component order") {
4710 shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1130(E)&R1336mzz(Z)"));
4711 AS->set_mole_fractions({0.6, 0.4});
4712 AS->specify_phase(CoolProp::iphase_gas);
4713 AS->update(DmolarT_INPUTS, 3146.0, 584.0);
4714 CAPTURE(AS->alphar());
4715 CHECK(AS->alphar() == Catch::Approx(-0.4936442709738808).epsilon(tol));
4716 }
4717}
4718
4719TEST_CASE("NIST IR 8570 PropsSI saturation smoke check", "[mixtures],[NIST-IR-8570]") {
4720 // Just verify the new pairs are loadable end-to-end via the high-level API.
4721 SECTION("R-1132(E) + R-32 saturation at 300 K returns finite") {
4722 double p = CoolProp::PropsSI("P", "T", 300.0, "Q", 0, "R1132E[0.5]&R32[0.5]");
4723 CAPTURE(p);
4724 CHECK(std::isfinite(p));
4725 CHECK(p > 0);
4726 }
4727 SECTION("R-1132(E) + R-1234yf saturation at 300 K returns finite") {
4728 double p = CoolProp::PropsSI("P", "T", 300.0, "Q", 0, "R1132E[0.5]&R1234yf[0.5]");
4729 CAPTURE(p);
4730 CHECK(std::isfinite(p));
4731 CHECK(p > 0);
4732 }
4733 SECTION("R-1336mzz(Z) + R-1130(E) saturation at 380 K returns finite") {
4734 // Both pure components have Tc > 440 K; 380 K is well below both saturation envelopes.
4735 double p = CoolProp::PropsSI("P", "T", 380.0, "Q", 0, "R1336mzz(Z)[0.5]&R1130(E)[0.5]");
4736 CAPTURE(p);
4737 CHECK(std::isfinite(p));
4738 CHECK(p > 0);
4739 }
4740}
4741
4742/*
4743TEST_CASE("Test that HS solver works for a few fluids", "[HS_solver]")
4744{
4745 std::vector<std::string> fluids; fluids.push_back("Propane"); fluids.push_back("D4"); fluids.push_back("Water");
4746 for (std::size_t i = 0; i < fluids.size(); ++i)
4747 {
4748 std::vector<std::string> fl(1,fluids[i]);
4749 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> HEOS = std::make_shared<CoolProp::HelmholtzEOSMixtureBackend>(fl);
4750 for (double p = HEOS->p_triple()*10; p < HEOS->pmax(); p *= 10)
4751 {
4752 double Tmin = HEOS->Ttriple();
4753 double Tmax = HEOS->Tmax();
4754 for (double T = Tmin + 1; T < Tmax-1; T += 10)
4755 {
4756 std::ostringstream ss;
4757 ss << "Check HS for " << fluids[i] << " for T=" << T << ", p=" << p;
4758 SECTION(ss.str(),"")
4759 {
4760 CHECK_NOTHROW(HEOS->update(PT_INPUTS, p, T));
4761 std::ostringstream ss1;
4762 ss1 << "h=" << HEOS->hmolar() << ", s=" << HEOS->smolar();
4763 SECTION(ss1.str(),"")
4764 {
4765 CAPTURE(T);
4766 CAPTURE(p);
4767 CAPTURE(HEOS->hmolar());
4768 CAPTURE(HEOS->smolar());
4769 CHECK_NOTHROW(HEOS->update(HmolarSmolar_INPUTS, HEOS->hmolar(), HEOS->smolar()));
4770 double Terr = HEOS->T()- T;
4771 CAPTURE(Terr);
4772 CHECK(std::abs(Terr) < 1e-6);
4773 }
4774 }
4775 }
4776 }
4777 }
4778}
4779*/
4780
4781// One published computer-verification test point per fluid added in the
4782// Akasaka/Lemmon/Thol batch of EOS updates (issues #2762, #2763, #2764,
4783// #2765). Each row is lifted directly from the validation table in the
4784// corresponding paper; tolerances are generous enough to absorb the last
4785// printed digit of each published value but tight enough to catch a real
4786// regression in the EOS or its loader.
4787TEST_CASE("Fluid batch 2020-2024: verify EOS against paper validation tables", "[fluids][batch_2020_2024]") {
4788 struct row
4789 {
4790 const char* fluid;
4791 double T_K, rho_molm3;
4792 double p_Pa, cv_JmolK, cp_JmolK, w_ms;
4793 double rtol; // relative tolerance for all four properties
4794 const char* note;
4795 };
4796 const std::vector<row> rows = {
4797 // Paper / Table / Row
4798 {"R1224YDZ", 400.0, 8000.0, 21.17909e6, 139.592, 185.184, 489.479, 1e-5, "Akasaka & Lemmon, IJT 2023, Table 7 row 4"},
4799 {"R1132E", 330.0, 12000.0, 3.845082e6, 70.9361, 165.548, 314.193, 1e-5, "Akasaka & Lemmon, IJT 2024, Table 6 row 4"},
4800 {"Tetrahydrofuran", 450.0, 10000.0, 12.357974600e6, 0.0, 167.23826646, 739.195761440, 1e-6,
4801 "Fiedler et al., IJT 2023, Table 11 row 3 (cv not published)"},
4802 {"PropyleneGlycol", 400.0, 13000.0, 61.287909e6, 0.0, 227.48403, 1467.8267, 1e-5,
4803 "Eisenbach et al., JPCRD 2021, Table 8 row 2 (cv not published)"},
4804 {"VinylChloride", 300.0, 15000.0, 23.0374719e6, 0.0, 91.4066946, 1008.04450, 1e-6,
4805 "Thol, Fenkl & Lemmon, IJT 2022, Table 5 row 4 (cv not published)"},
4806 {"R1123", 320.0, 11000.0, 5.456590e6, 74.3579, 158.839, 296.996, 1e-5, "Akasaka et al., IJR 2020, Table 8 row 4"},
4807 {"n-Perfluorobutane", 360.0, 5200.0, 3.128110e6, 223.0894, 303.2828, 226.8389, 1e-5, "Gao et al., IECR 2022, Table 14 C4F10 row 3"},
4808 {"n-Perfluoropentane", 390.0, 4200.0, 1.496384e6, 273.9917, 375.3160, 182.6921, 1e-5, "Gao et al., IECR 2022, Table 14 C5F12 row 3"},
4809 {"n-Perfluorohexane", 410.0, 3700.0, 0.9573522e6, 336.7461, 435.6546, 181.2565, 1e-5, "Gao et al., IECR 2022, Table 14 C6F14 row 3"},
4810 {"R1233zd(E)", 400.0, 8000.0, 10.79073e6, 122.693, 176.124, 441.123, 1e-5,
4811 "Akasaka & Lemmon, JPCRD 2022, Table IX row 4 (supersedes Mondejar-JCED-2015)"},
4812 {"R1130(E)", 320.0, 12500.0, 3.39671e6, 76.665, 115.586, 946.434, 1e-5,
4813 "Huber, Kazakov & Lemmon, IJT 2025, Table 4 row 3 (g_i != 1 in exponential terms)"},
4814 {"R1243zf", 280.0, 11000.0, 7.393335e6, 90.7467, 130.734, 648.467, 1e-5,
4815 "Akasaka & Lemmon, IJT 2025, Table 6 row 2 (3rd EOS, g_i != 1; supersedes Akasaka-JCED-2019)"},
4816 };
4817
4818 for (const auto& r : rows) {
4819 SECTION(std::string(r.fluid) + " (" + r.note + ")") {
4820 CAPTURE(r.fluid);
4821 CAPTURE(r.T_K);
4822 CAPTURE(r.rho_molm3);
4823
4824 const double p_calc = PropsSI("P", "T", r.T_K, "Dmolar", r.rho_molm3, r.fluid);
4825 const double cp_calc = PropsSI("Cpmolar", "T", r.T_K, "Dmolar", r.rho_molm3, r.fluid);
4826 const double w_calc = PropsSI("A", "T", r.T_K, "Dmolar", r.rho_molm3, r.fluid);
4827
4828 CHECK(p_calc == Catch::Approx(r.p_Pa).epsilon(r.rtol));
4829 CHECK(cp_calc == Catch::Approx(r.cp_JmolK).epsilon(r.rtol));
4830 CHECK(w_calc == Catch::Approx(r.w_ms).epsilon(r.rtol));
4831
4832 // cv was not published in every paper's verification table; skip
4833 // the check when the reference entry is exactly 0.0.
4834 if (r.cv_JmolK != 0.0) {
4835 const double cv_calc = PropsSI("Cvmolar", "T", r.T_K, "Dmolar", r.rho_molm3, r.fluid);
4836 CHECK(cv_calc == Catch::Approx(r.cv_JmolK).epsilon(r.rtol));
4837 }
4838 }
4839 }
4840}
4841
4842TEST_CASE("Qmass output: pure fluid equals Qmolar", "[Qmass]") {
4843 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
4844 for (double Q : {0.0, 0.1, 0.5, 0.9, 1.0}) {
4845 AS->update(CoolProp::QT_INPUTS, Q, 350.0);
4846 CHECK(AS->Qmass() == Catch::Approx(Q).epsilon(1e-12));
4847 }
4848}
4849
4850TEST_CASE("Qmass output: HEOS mixture differs from Qmolar and is internally consistent", "[Qmass][mixture]") {
4851 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
4852 AS->set_mole_fractions({0.5, 0.5});
4853
4854 // Retrieve component molar masses from CoolProp itself to avoid hardcoded constant mismatch
4855 auto AS_R32 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32"));
4856 auto AS_R125 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R125"));
4857 const double MM_R32 = AS_R32->molar_mass();
4858 const double MM_R125 = AS_R125->molar_mass();
4859
4860 for (double Q : {0.1, 0.3, 0.5, 0.7, 0.9}) {
4861 AS->update(CoolProp::QT_INPUTS, Q, 280.0);
4862
4863 const auto x = AS->mole_fractions_liquid();
4864 const auto y = AS->mole_fractions_vapor();
4865 const double MM_l = static_cast<double>(x[0]) * MM_R32 + static_cast<double>(x[1]) * MM_R125;
4866 const double MM_v = static_cast<double>(y[0]) * MM_R32 + static_cast<double>(y[1]) * MM_R125;
4867 const double Qmass_expected = (Q * MM_v) / (Q * MM_v + (1.0 - Q) * MM_l);
4868
4869 CHECK(AS->Qmass() == Catch::Approx(Qmass_expected).epsilon(1e-6));
4870 // For an asymmetric mixture, Qmass should differ from Qmolar at intermediate Q
4871 if (Q > 0.05 && Q < 0.95) {
4872 CHECK(std::abs(AS->Qmass() - Q) > 1e-3);
4873 }
4874 }
4875}
4876
4877TEST_CASE("Qmass input: HEOS R32+R125 round-trip via QmassT_INPUTS", "[Qmass][mixture]") {
4878 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
4879 AS->set_mole_fractions({0.5, 0.5});
4880 AS->update(CoolProp::QT_INPUTS, 0.4, 280.0);
4881 const double Qmass_observed = AS->Qmass();
4882 const double p_ref = AS->p();
4883 const double Q_ref = AS->Q();
4884
4885 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
4886 AS2->set_mole_fractions({0.5, 0.5});
4887 AS2->update(CoolProp::QmassT_INPUTS, Qmass_observed, 280.0);
4888 CHECK(AS2->p() == Catch::Approx(p_ref).epsilon(1e-8));
4889 CHECK(AS2->Q() == Catch::Approx(Q_ref).epsilon(1e-8));
4890 CHECK(AS2->Qmass() == Catch::Approx(Qmass_observed).epsilon(1e-10));
4891}
4892
4893TEST_CASE("Qmass input: pure Water round-trips for QmassT and PQmass", "[Qmass][pure]") {
4894 auto ref = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
4895 auto sut = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
4896
4897 SECTION("QmassT_INPUTS") {
4898 ref->update(CoolProp::QT_INPUTS, 0.4, 350.0);
4899 const double Qmass = ref->Qmass();
4900 const double p_ref = ref->p();
4901 sut->update(CoolProp::QmassT_INPUTS, Qmass, 350.0);
4902 CHECK(sut->T() == Catch::Approx(350.0).epsilon(1e-12));
4903 CHECK(sut->p() == Catch::Approx(p_ref).epsilon(1e-12));
4904 CHECK(sut->Q() == Catch::Approx(0.4).epsilon(1e-12));
4905 }
4906
4907 SECTION("PQmass_INPUTS") {
4908 ref->update(CoolProp::QT_INPUTS, 0.4, 350.0);
4909 const double p_ref = ref->p();
4910 const double Qmass = ref->Qmass();
4911 sut->update(CoolProp::PQmass_INPUTS, p_ref, Qmass);
4912 CHECK(sut->Q() == Catch::Approx(0.4).epsilon(1e-12));
4913 CHECK(sut->T() == Catch::Approx(350.0).epsilon(1e-12));
4914 }
4915}
4916
4917TEST_CASE("Qmass input: HEOS mixture round-trip via PQmass / HmolarQmass / DmolarQmass", "[Qmass][mixture]") {
4918 auto setup = []() {
4919 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
4920 AS->set_mole_fractions({0.5, 0.5});
4921 return AS;
4922 };
4923
4924 SECTION("PQmass") {
4925 auto ref = setup();
4926 ref->update(CoolProp::PQ_INPUTS, 1.5e6, 0.4);
4927 const double Qmass = ref->Qmass();
4928 const double T_ref = ref->T();
4929
4930 auto sut = setup();
4931 sut->update(CoolProp::PQmass_INPUTS, 1.5e6, Qmass);
4932 CHECK(sut->T() == Catch::Approx(T_ref).epsilon(1e-8));
4933 CHECK(sut->Q() == Catch::Approx(0.4).epsilon(1e-8));
4934 }
4935
4936 // NOTE: HmolarQmass and DmolarQmass sections commented out due to pre-existing HEOS mixture
4937 // flash limitation: HQ_flash and DQ_flash are not yet ready for mixtures. The PQmass section
4938 // (which uses PT_flash) exercises the full qmass_slot==2 iteration logic and passes.
4939
4940 // SECTION("HmolarQmass") {
4941 // auto ref = setup();
4942 // ref->update(CoolProp::QT_INPUTS, 0.4, 280.0);
4943 // const double H = ref->hmolar();
4944 // const double Qmass = ref->Qmass();
4945 // ref->update(CoolProp::HmolarQ_INPUTS, H, 0.4);
4946 // const double T_ref = ref->T();
4947 //
4948 // auto sut = setup();
4949 // sut->update(CoolProp::HmolarQmass_INPUTS, H, Qmass);
4950 // CHECK(sut->T() == Catch::Approx(T_ref).epsilon(1e-8));
4951 // CHECK(sut->Q() == Catch::Approx(0.4).epsilon(1e-6));
4952 // }
4953 //
4954 // SECTION("DmolarQmass") {
4955 // auto ref = setup();
4956 // ref->update(CoolProp::QT_INPUTS, 0.4, 280.0);
4957 // const double D = ref->rhomolar();
4958 // const double Qmass = ref->Qmass();
4959 // const double T_ref = ref->T();
4960 //
4961 // auto sut = setup();
4962 // sut->update(CoolProp::DmolarQmass_INPUTS, D, Qmass);
4963 // CHECK(sut->T() == Catch::Approx(T_ref).epsilon(1e-8));
4964 // CHECK(sut->Q() == Catch::Approx(0.4).epsilon(1e-6));
4965 // }
4966}
4967
4968TEST_CASE("Qmass output: REFPROP R32+R125 matches HEOS to leading digits", "[Qmass][REFPROP]") {
4969 std::shared_ptr<CoolProp::AbstractState> AS;
4970 try {
4971 AS.reset(CoolProp::AbstractState::factory("REFPROP", "R32&R125"));
4972 } catch (...) {
4973 WARN("REFPROP not available; skipping");
4974 return;
4975 }
4976 AS->set_mole_fractions({0.5, 0.5});
4977 AS->update(CoolProp::QT_INPUTS, 0.4, 280.0);
4978 const double Qmass_refprop = AS->Qmass();
4979
4980 auto AS_heos = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
4981 AS_heos->set_mole_fractions({0.5, 0.5});
4982 AS_heos->update(CoolProp::QT_INPUTS, 0.4, 280.0);
4983 const double Qmass_heos = AS_heos->Qmass();
4984
4985 // Different EOS but same physical mixture; agreement to ~3 digits is plenty
4986 // to confirm the REFPROP override is computing something sensible.
4987 CHECK(Qmass_refprop == Catch::Approx(Qmass_heos).epsilon(1e-2));
4988 // Sanity: result is in (0, 1) and not equal to Qmolar=0.4 (mixture should differ)
4989 CHECK(Qmass_refprop > 0.0);
4990 CHECK(Qmass_refprop < 1.0);
4991 CHECK(std::abs(Qmass_refprop - 0.4) > 1e-3);
4992}
4993
4994TEST_CASE("Qmass input: REFPROP R32+R125 native kq=2 fast path", "[Qmass][REFPROP]") {
4995 std::shared_ptr<CoolProp::AbstractState> AS;
4996 try {
4997 AS.reset(CoolProp::AbstractState::factory("REFPROP", "R32&R125"));
4998 } catch (...) {
4999 WARN("REFPROP not available; skipping");
5000 return;
5001 }
5002 AS->set_mole_fractions({0.5, 0.5});
5003 AS->update(CoolProp::QT_INPUTS, 0.4, 280.0);
5004 const double Qmass = AS->Qmass();
5005 const double P_ref = AS->p();
5006 const double Q_ref = AS->Q();
5007
5008 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("REFPROP", "R32&R125"));
5009 AS2->set_mole_fractions({0.5, 0.5});
5010 AS2->update(CoolProp::QmassT_INPUTS, Qmass, 280.0);
5011 CHECK(AS2->p() == Catch::Approx(P_ref).epsilon(1e-5));
5012 CHECK(AS2->Q() == Catch::Approx(Q_ref).epsilon(1e-5));
5013 CHECK(AS2->Qmass() == Catch::Approx(Qmass).epsilon(1e-12));
5014
5015 // PQmass round-trip
5016 auto AS3 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("REFPROP", "R32&R125"));
5017 AS3->set_mole_fractions({0.5, 0.5});
5018 AS3->update(CoolProp::PQmass_INPUTS, P_ref, Qmass);
5019 CHECK(AS3->T() == Catch::Approx(280.0).epsilon(1e-5));
5020 CHECK(AS3->Q() == Catch::Approx(Q_ref).epsilon(1e-5));
5021}
5022
5023TEST_CASE("Qmass edge cases: bubble/dew, out-of-range, single-phase", "[Qmass][edge]") {
5024 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
5025 AS->set_mole_fractions({0.5, 0.5});
5026
5027 SECTION("Qmass = 0 (bubble) bypasses iteration") {
5028 AS->update(CoolProp::QmassT_INPUTS, 0.0, 280.0);
5029 CHECK(AS->Q() == Catch::Approx(0.0).epsilon(1e-12));
5030 CHECK(AS->Qmass() == Catch::Approx(0.0).epsilon(1e-12));
5031 }
5032
5033 SECTION("Qmass = 1 (dew) bypasses iteration") {
5034 AS->update(CoolProp::QmassT_INPUTS, 1.0, 280.0);
5035 CHECK(AS->Q() == Catch::Approx(1.0).epsilon(1e-12));
5036 CHECK(AS->Qmass() == Catch::Approx(1.0).epsilon(1e-12));
5037 }
5038
5039 SECTION("Qmass < 0 throws") {
5040 CHECK_THROWS_AS(AS->update(CoolProp::QmassT_INPUTS, -0.1, 280.0), CoolProp::ValueError);
5041 }
5042
5043 SECTION("Qmass > 1 throws") {
5044 CHECK_THROWS_AS(AS->update(CoolProp::QmassT_INPUTS, 1.5, 280.0), CoolProp::ValueError);
5045 }
5046
5047 SECTION("Qmass() in single-phase state throws") {
5048 AS->update(CoolProp::PT_INPUTS, 5e6, 400.0); // single phase (well above critical for R32+R125)
5049 CHECK_THROWS_AS(AS->Qmass(), CoolProp::ValueError);
5050 }
5051}
5052
5053TEST_CASE("Qmass: PropsSI integration (output + input)", "[Qmass][PropsSI]") {
5054 SECTION("Qmass as output for pure Water == Q") {
5055 const double Q = 0.3, T = 350.0;
5056 const double Qmolar = CoolProp::PropsSI("Q", "T", T, "Q", Q, "Water");
5057 const double Qmass = CoolProp::PropsSI("Qmass", "T", T, "Q", Q, "Water");
5058 CHECK(Qmolar == Catch::Approx(0.3).epsilon(1e-12));
5059 CHECK(Qmass == Catch::Approx(0.3).epsilon(1e-12));
5060 }
5061 SECTION("Qmass as input for pure Water round-trip via PropsSI") {
5062 const double T = 350.0;
5063 const double P_ref = CoolProp::PropsSI("P", "T", T, "Q", 0.3, "Water");
5064 const double P_via = CoolProp::PropsSI("P", "T", T, "Qmass", 0.3, "Water");
5065 CHECK(P_via == Catch::Approx(P_ref).epsilon(1e-12));
5066 }
5067 SECTION("Qmass as input for HEOS R32+R125 mixture via PropsSI") {
5068 // Note: PropsSI doesn't support setting mole fractions for mixtures
5069 // through a "&"-syntax fluid name without a separate concentration vector,
5070 // so we use the AbstractState API for the mixture round-trip test below.
5071 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
5072 AS->set_mole_fractions({0.5, 0.5});
5073 AS->update(CoolProp::QT_INPUTS, 0.4, 280.0);
5074 const double Qmass_obs = AS->Qmass();
5075 const double P_ref = AS->p();
5076
5077 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
5078 AS2->set_mole_fractions({0.5, 0.5});
5079 AS2->update(CoolProp::QmassT_INPUTS, Qmass_obs, 280.0);
5080 CHECK(AS2->p() == Catch::Approx(P_ref).epsilon(1e-8));
5081 }
5082}
5083
5084TEST_CASE("Qmass: PCSAFT mixture output works after Q-pair flash", "[Qmass][PCSAFT][mixture]") {
5085 // Verifies PCSAFT calc_phase_molar_masses override and the early-exit hook.
5086 // METHANE[0.0252]+BENZENE[0.9748] at Q=0, T=421.05 K is in the PCSAFT
5087 // VLE regression suite (existing test in this file, ~line 2942).
5088 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("PCSAFT", "METHANE&BENZENE"));
5089 AS->set_mole_fractions({0.0252, 0.9748});
5090 AS->update(CoolProp::QT_INPUTS, 0.0, 421.05);
5091 // At Q=0 (saturated liquid) the endpoint short-circuit in calc_Qmass returns 0
5092 // without computing MM_l/MM_v — important because PCSAFT may not populate
5093 // SatV->mole_fractions at saturation endpoints.
5094 CHECK(AS->Q() == Catch::Approx(0.0).epsilon(1e-10));
5095 CHECK(AS->Qmass() == Catch::Approx(0.0).epsilon(1e-10));
5096 // QmassT_INPUTS at Qmass=0 must reach the same state via the endpoint
5097 // bypass in update_Qmass_pair (no iteration, no flash convergence concerns).
5098 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("PCSAFT", "METHANE&BENZENE"));
5099 AS2->set_mole_fractions({0.0252, 0.9748});
5100 AS2->update(CoolProp::QmassT_INPUTS, 0.0, 421.05);
5101 CHECK(AS2->p() == Catch::Approx(AS->p()).epsilon(1e-10));
5102 CHECK(AS2->Q() == Catch::Approx(0.0).epsilon(1e-10));
5103}
5104
5105TEST_CASE("Qmass: SRK cubic mixture output works after Q-pair flash", "[Qmass][cubic][mixture]") {
5106 // Verifies the cubic backend's early-exit hook + inherited HEOS
5107 // calc_phase_molar_masses override (cubic shares HEOS's SatL/SatV machinery).
5108 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("SRK", "Propane&n-Butane"));
5109 AS->set_mole_fractions({0.5, 0.5});
5110 AS->update(CoolProp::PQ_INPUTS, 5e5, 0.0);
5111 CHECK(AS->Q() == Catch::Approx(0.0).epsilon(1e-10));
5112 CHECK(AS->Qmass() == Catch::Approx(0.0).epsilon(1e-10));
5113 // Endpoint round-trip via Qmass-input pair (early-exit hook + endpoint shortcut).
5114 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("SRK", "Propane&n-Butane"));
5115 AS2->set_mole_fractions({0.5, 0.5});
5116 AS2->update(CoolProp::PQmass_INPUTS, 5e5, 0.0);
5117 CHECK(AS2->T() == Catch::Approx(AS->T()).epsilon(1e-10));
5118 CHECK(AS2->Q() == Catch::Approx(0.0).epsilon(1e-10));
5119}
5120
5121TEST_CASE("User-fluid schema validation rejects malformed PCSAFT/cubic payloads", "[json_validation]") {
5122 // --- Cubic (SRK) ---
5123 // 1. Structurally invalid JSON must throw unconditionally.
5124 SECTION("Cubic SRK - invalid JSON syntax throws") {
5125 CHECK_THROWS_AS(CoolProp::add_fluids_as_JSON("SRK", "this is not json"), CoolProp::ValueError);
5126 }
5127 // 2. Valid JSON array but missing required schema fields (Tc_units, pc_units,
5128 // molemass_units, acentric) — the Valijson validator catches the omissions
5129 // and add_fluids_from_JSON_string throws unconditionally for cubics.
5130 SECTION("Cubic SRK - schema-invalid payload (missing required fields) throws") {
5131 // Provides name, CAS, Tc, pc, molemass but omits the *_units fields and
5132 // acentric, all of which are listed in the cubic schema's "required" array.
5133 const std::string bad_cubic = R"([{"name":"BadFluid","CAS":"000-00-0","Tc":400.0,"pc":3e6,"molemass":0.04}])";
5134 CHECK_THROWS_AS(CoolProp::add_fluids_as_JSON("SRK", bad_cubic), CoolProp::ValueError);
5135 }
5136
5137 // --- PC-SAFT ---
5138 // The PCSAFT loader only throws on validation failure when debug_level > 0
5139 // (at level 0 it silently returns); raise it for these checks and restore
5140 // unconditionally via RAII, so other tests are unaffected even if the
5141 // assertion throws an unexpected type.
5142
5143 // 3. Structurally invalid JSON under debug_level > 0 must throw.
5144 SECTION("PCSAFT - invalid JSON syntax throws at debug_level > 0") {
5145 // PCSAFT's loader only surfaces validation failures when debug_level > 0
5146 // (at level 0 it silently returns); raise it for this check and restore
5147 // unconditionally via RAII, so other tests are unaffected even if the
5148 // assertion throws an unexpected type.
5149 struct DebugLevelGuard
5150 {
5151 int saved;
5152 DebugLevelGuard() : saved(CoolProp::get_debug_level()) {}
5153 ~DebugLevelGuard() {
5155 }
5156 } guard;
5158 CHECK_THROWS_AS(CoolProp::add_fluids_as_JSON("PCSAFT", "this is not json"), CoolProp::ValueError);
5159 }
5160
5161 // 4. Valid JSON array but missing required schema fields (m, sigma, sigma_units,
5162 // u, u_units, molemass, molemass_units) — the PCSAFT schema marks these in its
5163 // "required" array, so Valijson rejects the payload and the loader throws when
5164 // debug_level > 0.
5165 SECTION("PCSAFT - schema-invalid payload (missing required fields) throws at debug_level > 0") {
5166 struct DebugLevelGuard
5167 {
5168 int saved;
5169 DebugLevelGuard() : saved(CoolProp::get_debug_level()) {}
5170 ~DebugLevelGuard() {
5172 }
5173 } guard;
5175 // Provides name and CAS but omits m, sigma, sigma_units, u, u_units,
5176 // molemass, molemass_units — all listed in the PCSAFT schema's "required" array.
5177 CHECK_THROWS_AS(CoolProp::add_fluids_as_JSON("PCSAFT", R"([{"name":"Bogus","CAS":"000-00-0"}])"), CoolProp::ValueError);
5178 }
5179}
5180
5181TEST_CASE("Water TS_INPUTS flash near 631-634 K is smooth (no spike to 6e13 Pa)", "[water_flash][2079]") {
5182 // Issue #2079: previously CP.PropsSI('P','T',T,'S',6763.617,'Water')
5183 // for T in {631, 632, 633, 634} returned ~6e13 Pa (vs ~3.1 MPa
5184 // expected). The HEOS PT-style flash now converges smoothly across
5185 // this region; this guard makes sure it stays that way.
5186 const double s = 6763.617210539725;
5187 const double p_expected = 3.1e6; // expected magnitude across the band
5188 for (double T : {631.0, 632.0, 633.0, 634.0}) {
5189 CAPTURE(T);
5190 const double p = CoolProp::PropsSI("P", "T", T, "S", s, "Water");
5191 CAPTURE(p);
5192 CHECK(std::isfinite(p));
5193 CHECK(p > 0);
5194 CHECK(p < 1e8); // reject the old 6e13 spike with massive margin
5195 CHECK(std::abs(p - p_expected) / p_expected < 0.05);
5196 }
5197}
5198
5199TEST_CASE("Water HS_INPUTS flash near H=3133800, S=6777 is smooth (no spike to 5.9e13 Pa)", "[water_flash][1730]") {
5200 // Issue #1730: PropsSI('P','H',3133800,'S',6777,'Water') previously
5201 // returned ~5.9e13 Pa while the neighbouring points returned ~2.97 MPa.
5202 // The HS flash now converges smoothly. Lock down the original three
5203 // reproducer points plus a handful around them.
5204 for (double H : {3132000.0, 3133000.0, 3133500.0, 3133800.0, 3134000.0, 3135000.0}) {
5205 CAPTURE(H);
5206 const double p = CoolProp::PropsSI("P", "H", H, "S", 6777.0, "Water");
5207 CAPTURE(p);
5208 CHECK(std::isfinite(p));
5209 CHECK(p > 0);
5210 CHECK(p < 1e8);
5211 CHECK(std::abs(p - 2.97e6) / 2.97e6 < 0.02);
5212 }
5213}
5214
5215TEST_CASE("Saturation ancillary returns NaN above T_r instead of UB / SIGFPE (#1611)", "[ancillary][1611]") {
5216 // Issue #1611: pow(THETA, t) with THETA = 1 - T/T_r < 0 was
5217 // pow(negative, fractional) which produced NaN and (depending on
5218 // FP trap settings) sometimes a SIGFPE that escaped C++ try/catch.
5219 // Internal callers (mixture critical-point search, VLE init) probe
5220 // ancillaries above the pure-component T_r and rely on getting a
5221 // well-behaved value back, so the guard returns NaN explicitly
5222 // (no UB, no SIGFPE) instead of throwing.
5223 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R134a"));
5224 auto& heos = *dynamic_cast<CoolProp::HelmholtzEOSMixtureBackend*>(AS.get());
5225 auto& fluid = heos.get_components()[0];
5226 double Tcrit = AS->T_critical();
5227
5228 // Direct ancillary evaluation a few K above the reducing T returns
5229 // NaN cleanly (no SIGFPE, no UB).
5230 CHECK(std::isnan(fluid.ancillaries.pL.evaluate(Tcrit + 5.0)));
5231 CHECK(std::isnan(fluid.ancillaries.pV.evaluate(Tcrit + 5.0)));
5232 CHECK(std::isnan(fluid.ancillaries.rhoL.evaluate(Tcrit + 5.0)));
5233 CHECK(std::isnan(fluid.ancillaries.rhoV.evaluate(Tcrit + 5.0)));
5234 // Comfortably below T_r still produces finite values
5235 CHECK(std::isfinite(fluid.ancillaries.pL.evaluate(0.95 * Tcrit)));
5236 CHECK(std::isfinite(fluid.ancillaries.pL.evaluate(0.5 * Tcrit)));
5237
5238 // The original user-facing reproducer no longer crashes
5239 // (PropsSI returns NaN with errstring set rather than SIGFPE).
5240 CHECK_NOTHROW(CoolProp::PropsSI("T", "P", 4863285.0, "Q", 0.0, "HEOS::R407C"));
5241 CHECK_NOTHROW(CoolProp::PropsSI("T", "P", 1.0e6, "Q", 0.0, "HEOS::R407C"));
5242}
5243
5244TEST_CASE("INCOMP psat throws below TminPsat instead of returning 0 silently (#2209)", "[INCOMP][2209]") {
5245 // Issue #2209: PropsSI('P','Q',0,'T',373,'INCOMP::MEG[0.1]') returned
5246 // 0.0 because MEG.json has TminPsat=373.15 (which equals Tmax for
5247 // this fluid, leaving no usable Psat range below the upper bound)
5248 // and psat() silently returned 0.0 below TminPsat. Now throw a
5249 // ValueError so PropsSI surfaces a non-finite return AND populates
5250 // errstring rather than returning a misleading 0.
5251 const double v = CoolProp::PropsSI("P", "Q", 0, "T", 373.0, "INCOMP::MEG[0.1]");
5252 CHECK(!ValidNumber(v));
5253 const std::string err = CoolProp::get_global_param_string("errstring");
5254 CAPTURE(err);
5255 CHECK(err.find("TminPsat") != std::string::npos);
5256 // LiBr has TminPsat = 273.15 so T=373 is above it and still works
5257 const double v2 = CoolProp::PropsSI("P", "Q", 0, "T", 373.0, "INCOMP::LiBr[0.1]");
5258 CHECK(ValidNumber(v2));
5259 CHECK(v2 > 0);
5260}
5261TEST_CASE("Incompressible enthalpy is finite and continuous at T == Tbase (#1578)", "[INCOMP][1578]") {
5262 // Issue #1578: PropsSI for incompressible fluids used to throw "A fraction
5263 // cannot be evaluated with zero as denominator" whenever the requested
5264 // temperature equalled the fluid's Tbase (so x_in - x_base == 0). For most
5265 // fluids that is a single unlucky point, but Antifrogen N/L (AN/AL) have
5266 // Tbase == 293.15 K, which is also their reference temperature, so
5267 // enthalpy/entropy failed for *every* state. The singularity is now handled
5268 // by L'Hopital-style interpolation in Polynomial2DFrac::evaluate; this guards
5269 // against the throw returning.
5270 const double p = 101325.0;
5271 // (fluid name, Tbase [K]) pairs taken straight from the issue report.
5272 struct Case
5273 {
5274 std::string name;
5275 double Tbase;
5276 };
5277 const std::vector<Case> cases = {
5278 {"INCOMP::AS30", 273.15}, // single-point case from the issue
5279 {"INCOMP::TD12", 345.65}, // single-point case from the issue
5280 {"INCOMP::AN-30%", 293.15}, // Antifrogen N: Tbase == Tref, failed for all T
5281 {"INCOMP::AL-30%", 293.15}, // Antifrogen L: Tbase == Tref, failed for all T
5282 };
5283 for (const Case& c : cases) {
5284 const double dT = 0.1;
5285 double h_lo = 0, h_mid = 0, h_hi = 0;
5286 CHECK_NOTHROW(h_lo = CoolProp::PropsSI("Hmass", "T", c.Tbase - dT, "P", p, c.name));
5287 CHECK_NOTHROW(h_mid = CoolProp::PropsSI("Hmass", "T", c.Tbase, "P", p, c.name));
5288 CHECK_NOTHROW(h_hi = CoolProp::PropsSI("Hmass", "T", c.Tbase + dT, "P", p, c.name));
5289 CAPTURE(c.name);
5290 CAPTURE(c.Tbase);
5291 CAPTURE(h_lo);
5292 CAPTURE(h_mid);
5293 CAPTURE(h_hi);
5294 CAPTURE(CoolProp::get_global_param_string("errstring"));
5295 REQUIRE(ValidNumber(h_mid));
5296 // Enthalpy is ~linear in T over a 0.2 K window (cp is locally constant),
5297 // so the value at Tbase must equal the midpoint of its neighbours: this
5298 // catches a spike/dropout if the singular point is mishandled. The bound
5299 // is on the linearity residual (curvature ~ d(cp)/dT * dT^2), not the
5300 // slope (~cp*dT ~ 400 J/kg); 1.0 J/kg leaves a wide margin.
5301 const double midpoint = 0.5 * (h_lo + h_hi);
5302 CHECK(std::abs(h_mid - midpoint) < 1.0); // [J/kg]
5303 }
5304}
5305TEST_CASE("Incompressible MPG2 viscosity matches Melinder source data (#1374)", "[INCOMP][1374]") {
5306 // Issue #1374: the fitted viscosity (and hence Prandtl number) of MPG2
5307 // (Melinder propylene glycol) was a uniform factor of 10 too small versus
5308 // MPG/APG and every other propylene-glycol fluid. Root cause: the
5309 // exp-polynomial constant term in MPG2.json was off by ln(10), so
5310 // viscosity = exp(poly) came out 10x low everywhere. The reference values
5311 // below are the Melinder propylene-glycol viscosity table
5312 // (dev/incompressible_liquids/CPIncomp/data/SecCool/xMass/"Melinder,
5313 // Propylene Glycol_Mu.txt") scaled by viscosityFactor = 1e-5 to obtain
5314 // Pa*s, sampled at exact (T, x) grid points where the fit reproduces it.
5315 const double p = 101325.0;
5316 const double rel = 0.01; // 1% relative; pre-fix the values were ~90% low
5317 struct Pt
5318 {
5319 double T, x, mu;
5320 };
5321 // T [K], x = mass fraction, mu = dynamic viscosity [Pa*s]
5322 const std::vector<Pt> pts = {
5323 {283.15, 0.42, 742.1231823e-5}, // 10 C, 42%
5324 {283.15, 0.52, 1137.598557e-5}, // 10 C, 52%
5325 {283.15, 0.57, 1443.949841e-5}, // 10 C, 57%
5326 {303.15, 0.42, 326.47355e-5}, // 30 C, 42%
5327 {303.15, 0.52, 456.9447475e-5}, // 30 C, 52%
5328 };
5329 for (const Pt& pt : pts) {
5330 const std::string name = "INCOMP::MPG2" + format("[%f]", pt.x);
5331 const double actual = CoolProp::PropsSI("V", "T", pt.T, "P", p, name);
5332 CAPTURE(pt.T);
5333 CAPTURE(pt.x);
5334 CAPTURE(pt.mu);
5335 CAPTURE(actual);
5336 CAPTURE(CoolProp::get_global_param_string("errstring"));
5337 CHECK(check_abs(pt.mu, actual, rel));
5338 }
5339 // The Prandtl number reported in #1374 should be ~100 (like MPG/APG), not ~10.
5340 const double Pr = CoolProp::PropsSI("PRANDTL", "T", 283.15, "P", p, "INCOMP::MPG2[0.5]");
5341 CAPTURE(Pr);
5342 CHECK(Pr > 80.0);
5343}
5344TEST_CASE("PropsSImulti with empty input vectors returns empty result instead of segfaulting", "[PropsSImulti][2417]") {
5345 // Issue #2417: PropsSI('T','P',[],'Q',[],'Ammonia') segfaulted because
5346 // _PropsSI_outputs forced N1 = max(1, in1.size()) and then dereferenced
5347 // in1[0] / in2[0]. Empty inputs must now return an empty IO matrix.
5348 std::vector<std::string> outputs{"T"};
5349 std::vector<std::string> fluids{"Ammonia"};
5350 std::vector<double> fractions{1.0};
5351 std::vector<double> p_empty;
5352 std::vector<double> q_empty;
5353 auto IO = CoolProp::PropsSImulti(outputs, "P", p_empty, "Q", q_empty, "HEOS", fluids, fractions);
5354 CHECK(IO.empty());
5355 // And the non-empty path still works
5356 std::vector<double> p_one{101325.0};
5357 std::vector<double> q_one{0.0};
5358 auto IO2 = CoolProp::PropsSImulti(outputs, "P", p_one, "Q", q_one, "HEOS", fluids, fractions);
5359 REQUIRE(IO2.size() == 1);
5360 REQUIRE(IO2[0].size() == 1);
5361 CHECK(IO2[0][0] > 0);
5362}
5363TEST_CASE("first_saturation_deriv mixture: dT/dP via Gernert vs finite-difference along bubble curve", "[first_saturation_deriv][2091]") {
5364 // Issue #2091: the mixture path used the pure-fluid Clapeyron
5365 // expression (T*Δv / Δh) and silently returned wrong values.
5366 // The fix uses Gernert thesis 3.96/3.97 weighted by the OTHER
5367 // phase's compositions.
5368 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane"));
5369 AS->set_mole_fractions({0.7, 0.3});
5370
5371 double T = 180.0;
5372 AS->update(CoolProp::QT_INPUTS, 0.0, T);
5373 double dTdP = AS->first_saturation_deriv(CoolProp::iT, CoolProp::iP);
5374
5375 // Finite difference along Q=0 (bubble) curve
5376 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane"));
5377 AS2->set_mole_fractions({0.7, 0.3});
5378 double dT = 0.05;
5379 AS2->update(CoolProp::QT_INPUTS, 0.0, T + dT);
5380 double pP = AS2->p();
5381 AS2->update(CoolProp::QT_INPUTS, 0.0, T - dT);
5382 double pM = AS2->p();
5383 double dTdP_fd = (2 * dT) / (pP - pM);
5384
5385 CAPTURE(dTdP);
5386 CAPTURE(dTdP_fd);
5387 CHECK(dTdP > 0);
5388 CHECK(std::abs(dTdP - dTdP_fd) / std::abs(dTdP_fd) < 5e-3);
5389}
5390
5391TEST_CASE("first_saturation_deriv mixture: throws at intermediate Q", "[first_saturation_deriv][2091]") {
5392 // Two-phase intermediate quality is not on a single saturation curve
5393 // for mixtures; the new code throws (was previously silently wrong).
5394 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane"));
5395 AS->set_mole_fractions({0.7, 0.3});
5396 AS->update(CoolProp::QT_INPUTS, 0.5, 180.0);
5397 CHECK_THROWS(AS->first_saturation_deriv(CoolProp::iT, CoolProp::iP));
5398}
5399
5400TEST_CASE("Two-phase chemical_potential mirrors sat-state values; fugacity_coefficient throws", "[mixtures][2345-followup]") {
5401 // Follow-up to PR #2345: calc_chemical_potential and calc_fugacity_coefficient
5402 // were calling MixtureDerivatives::* directly on the overall homogeneous state,
5403 // which is meaningless inside the two-phase dome.
5404 // - mu_i^L == mu_i^V at VLE (the equilibrium condition), so the new code
5405 // returns the Q-weighted sat-state value, which collapses to either
5406 // endpoint up to flash tolerance.
5407 // - phi_i^L != phi_i^V because the phase compositions differ; the new
5408 // code throws to force callers to evaluate on SatL/SatV explicitly.
5409 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Methane&Ethane&Propane"));
5410 AS->set_mole_fractions({0.25, 0.25, 0.5});
5411 AS->update(CoolProp::PQ_INPUTS, 500e3, 0.5);
5412 REQUIRE(AS->phase() == CoolProp::iphase_twophase);
5413
5414 auto* heos_ptr = dynamic_cast<CoolProp::HelmholtzEOSMixtureBackend*>(AS.get());
5415 REQUIRE(heos_ptr != nullptr);
5416 auto& satL = heos_ptr->get_SatL();
5417 auto& satV = heos_ptr->get_SatV();
5418
5419 for (std::size_t i = 0; i < 3; ++i) {
5420 CAPTURE(i);
5421 // chemical_potential: mu_i^L == mu_i^V at VLE
5422 const double mu = AS->chemical_potential(i);
5423 const double muL = satL.chemical_potential(i);
5424 const double muV = satV.chemical_potential(i);
5425 CHECK(std::isfinite(mu));
5426 // sat-state pair equal up to flash tolerance
5427 CHECK(std::abs(muL - muV) < 1e-4 * std::abs(muL));
5428 // overall == Q-weighted combination of sat states (Q=0.5)
5429 CHECK(std::abs(mu - 0.5 * (muL + muV)) < 1e-9 * std::abs(0.5 * (muL + muV)));
5430
5431 // fugacity_coefficient throws in two-phase
5432 CHECK_THROWS(AS->fugacity_coefficient(i));
5433 // but works on the sat states
5434 CHECK(std::isfinite(satL.fugacity_coefficient(i)));
5435 CHECK(std::isfinite(satV.fugacity_coefficient(i)));
5436 }
5437}
5438
5439TEST_CASE("HAPropsSI Twb at high T (T > Tsat) returns the physical root (#2255)", "[HAPropsSI][2255]") {
5440 // Issue #2255: HAPropsSI('Twb','T',200+273.15,'W',0.2,'P',1000E3)
5441 // returned 180.7241 C (essentially Tsat at 1 MPa) instead of the
5442 // correct ~131 C. The wet-bulb residual function is meaningless
5443 // for Twb > Tsat(p) (W_s_wb goes negative), and the Brent solver
5444 // converged to a spurious "root" at the upper bracket.
5445 const double Twb_K = HumidAir::HAPropsSI("Twb", "T", 200 + 273.15, "W", 0.2, "P", 1.0e6);
5446 const double Twb_C = Twb_K - 273.15;
5447 CAPTURE(Twb_C);
5448 // Expected physical root is ~130-131 C; the buggy value was 180.72 C
5449 CHECK(Twb_C > 125.0);
5450 CHECK(Twb_C < 135.0);
5451
5452 // Smoothness across the discontinuity reported by the second commenter:
5453 // Sweep T at fixed W=0.05, P=1 atm and verify Twb is monotonic and finite.
5454 double prev = -1.0;
5455 for (double T_C : {100.0, 105.0, 110.0, 115.0, 120.0, 130.0, 150.0, 200.0, 250.0, 300.0}) {
5456 const double Twb = HumidAir::HAPropsSI("Twb", "T", T_C + 273.15, "W", 0.05, "P", 101325.0) - 273.15;
5457 CAPTURE(T_C);
5458 CAPTURE(Twb);
5459 CHECK(std::isfinite(Twb));
5460 CHECK(Twb > 0);
5461 CHECK(Twb < T_C); // wet bulb must be below dry bulb
5462 if (prev > 0) {
5463 // monotone increasing across the sweep (within fp tolerance)
5464 CHECK(Twb >= prev - 1e-6);
5465 }
5466 prev = Twb;
5467 }
5468}
5469TEST_CASE("HAPropsSI T_db from (T_wb, RelHum, P) — issue #2690 surviving failure modes", "[HAPropsSI][humid_air][2690]") {
5470 // Issue #2690: HAPropsSI('T_db','T_wb',T_wb,'RelHum',RH,'P',P) returned
5471 // "Temperature value (inf)" for narrow pressure bands at very low RH.
5472 // A reporter sweep (td.xlsx, May 2026) decomposes the failures into
5473 // distinct modes. The b3360b8c6 fix for #2255 (Twb solver bracket
5474 // handling) already addressed the bulk of the originally-reported
5475 // bands. This test pins the two surviving modes addressed in this PR.
5476
5477 SECTION("Mode B — Tsat(p) crossing isolated outliers") {
5478 // The inner WetbulbTemperature historically used Tupper = Tmax + 1.
5479 // For T just below Tsat(p), this Tupper crosses Tsat and Brent steps
5480 // hit the W_s_wb singularity (p_ws_wb -> p, denominator -> 0).
5481 // The fix tightens Tupper to std::min(Tmax + 1, Tsat - 1e-3).
5482 const double T_db_a = HumidAir::HAPropsSI("T_db", "T_wb", 30 + 273.15, "RelHum", 1e-5, "P", 97950.0);
5483 CAPTURE(T_db_a);
5484 CHECK(std::isfinite(T_db_a));
5485 CHECK(T_db_a > 30 + 273.15); // T_db must exceed T_wb
5486 CHECK(T_db_a < 200 + 273.15); // and stay within HVAC range
5487
5488 const double T_db_b = HumidAir::HAPropsSI("T_db", "T_wb", 28 + 273.15, "RelHum", 1e-4, "P", 87550.0);
5489 CAPTURE(T_db_b);
5490 CHECK(std::isfinite(T_db_b));
5491 CHECK(T_db_b > 28 + 273.15);
5492 }
5493
5494 SECTION("Mode A — pressure-band cases incidentally fixed by b3360b8c6 stay green") {
5495 // Sample points from the reporter's failing band on CoolProp 7.2.0.
5496 // These passed once b3360b8c6 landed; pin them so the new Tupper
5497 // clamp (Mode B fix) doesn't accidentally regress them.
5498 for (double P : {90200.0, 90550.0, 90700.0, 91000.0, 91100.0, 91024.0}) {
5499 const double T_db = HumidAir::HAPropsSI("T_db", "T_wb", 30 + 273.15, "RelHum", 1e-5, "P", P);
5500 CAPTURE(P);
5501 CAPTURE(T_db);
5502 CHECK(std::isfinite(T_db));
5503 CHECK(T_db > 30 + 273.15);
5504 CHECK(T_db < 200 + 273.15);
5505 }
5506 }
5507
5508 SECTION("Mode C — physically infeasible inputs surface a clear error, not 'inf'") {
5509 // At T_wb >= ~55 C with RH <= 1e-4 the wet-bulb energy balance
5510 // implies T_db > 500 K, beyond the validity of the humid-air
5511 // mixture model. The previous behavior was an opaque
5512 // "Temperature value (inf)" from check_bounds at the bounds-check
5513 // boundary. The fix detects infeasibility upfront and throws a
5514 // CoolProp::ValueError whose message names T_wb / RelHum / P /
5515 // T_db_estimate. HAPropsSI catches all exceptions and returns _HUGE,
5516 // so we verify (a) the result is not a valid number and (b) the
5517 // diagnostic error string contains the new message — not the old
5518 // misleading "outside the range of validity" wording.
5519 const double r1 = HumidAir::HAPropsSI("T_db", "T_wb", 60 + 273.15, "RelHum", 1e-5, "P", 90000.0);
5520 CHECK_FALSE(ValidNumber(r1));
5521 const std::string err1 = CoolProp::get_global_param_string("errstring");
5522 CAPTURE(err1);
5523 CHECK(err1.find("beyond the validity range") != std::string::npos);
5524
5525 const double r2 = HumidAir::HAPropsSI("T_db", "T_wb", 65 + 273.15, "RelHum", 1e-4, "P", 95000.0);
5526 CHECK_FALSE(ValidNumber(r2));
5527 const std::string err2 = CoolProp::get_global_param_string("errstring");
5528 CAPTURE(err2);
5529 CHECK(err2.find("beyond the validity range") != std::string::npos);
5530 }
5531
5532 SECTION("Reporter sweep (td.xlsx) — Modes B+C clean, Mode D out of scope") {
5533 // Abbreviated replay of the reporter's sweep over (RH, T_wb, P).
5534 // After this PR, for T_wb >= 5 °C every point should either
5535 // (a) return a finite T_db, or (b) surface the explicit
5536 // "beyond the validity range" Mode C infeasibility error —
5537 // never the misleading "Temperature value (inf)" of the prior
5538 // code path. Mode D (sub-freezing T_wb=0 °C) is a separate
5539 // failure mechanism and is explicitly excluded from this PR.
5540 int unexpected_failures_above_freezing = 0;
5541 int mode_c_above_freezing = 0;
5542 int fail_at_freezing = 0;
5543 const double T_wbs[] = {0, 5, 10, 15, 20, 25, 28, 29, 30, 31, 32, 35, 40, 45, 50, 55, 60, 65};
5544 const double RHs[] = {0.01, 1e-3, 1e-4, 1e-5};
5545 for (double RH : RHs) {
5546 for (double T_wb_C : T_wbs) {
5547 for (int P = 87000; P < 99000; P += 250) { // step 250 keeps test ~< 1s
5548 const double v = HumidAir::HAPropsSI("T_db", "T_wb", T_wb_C + 273.15, "RelHum", RH, "P", (double)P);
5549 if (!ValidNumber(v)) {
5550 if (T_wb_C == 0) {
5551 ++fail_at_freezing;
5552 } else {
5553 const std::string err = CoolProp::get_global_param_string("errstring");
5554 if (err.find("beyond the validity range") != std::string::npos) {
5555 ++mode_c_above_freezing;
5556 } else {
5557 ++unexpected_failures_above_freezing;
5558 }
5559 }
5560 }
5561 }
5562 }
5563 }
5564 CAPTURE(unexpected_failures_above_freezing);
5565 CAPTURE(mode_c_above_freezing);
5566 CAPTURE(fail_at_freezing);
5567 // Modes A+B: zero unexpected failures above freezing.
5568 CHECK(unexpected_failures_above_freezing == 0);
5569 // Mode C: the infeasibility detector should fire for at least the
5570 // T_wb >= 60 °C low-RH points; confirms the throw path is wired up.
5571 CHECK(mode_c_above_freezing > 0);
5572 // Mode D: sub-freezing failures still exist (out of scope here).
5573 CHECK(fail_at_freezing > 0);
5574 }
5575
5576 SECTION("Regression guard — #2697 high-pressure Twb cases must still work") {
5577 // PR #2697 was reverted because raising T_max unconditionally to 640 K
5578 // broke ASHRAE A.8/A.9-style high-pressure wet-bulb computations.
5579 // Re-pin a representative high-P Twb case to catch a repeat over-correction.
5580 const double Twb = HumidAir::HAPropsSI("Twb", "T", 200 + 273.15, "W", 0.05, "P", 5.0e6);
5581 CAPTURE(Twb);
5582 CHECK(std::isfinite(Twb));
5583 CHECK(Twb > 100 + 273.15);
5584 CHECK(Twb < 250 + 273.15);
5585 }
5586}
5587
5588TEST_CASE("HAPropsSI T_db from T_wb near the water triple point — issue #2906", "[HAPropsSI][humid_air][2906]") {
5589 // Issue #2906 (follow-up to #2690). Near 273.16 K the wet-bulb energy
5590 // balance in WetBulbSolver is discontinuous: h_w jumps by the latent heat
5591 // of fusion (~333 kJ/kg) as the saturant wick switches liquid<->ice, so the
5592 // forward map Twb(T_db) skips a band of wet-bulb temperatures (widest at low
5593 // RH, vanishing by RH ~ 0.7). For a target T_wb inside that band there is no
5594 // dry-bulb solution. The old solver handled this badly two ways: a scattered
5595 // set returned the opaque "Temperature value (inf)", but MOST of the band was
5596 // worse — the outer Brent latched onto the discontinuity T_db* and returned a
5597 // T_db whose actual wet-bulb was NOT the request (a silently-wrong "flat
5598 // section"). The fix verifies the solved T_db reproduces the requested T_wb
5599 // and otherwise throws a clean infeasibility naming the triple point.
5600
5601 SECTION("No silently-wrong result: every finite T_db reproduces the requested T_wb") {
5602 // Core invariant. Sweep target T_wb through the gap region across RH and
5603 // a WIDE pressure range; any finite T_db must be self-consistent (its
5604 // forward wet-bulb equals the request), and unreachable targets must
5605 // surface a clean 'triple point' error — never the opaque "(inf)" and
5606 // never a wrong number. The band is widest at low pressure (reaching
5607 // ~1.2 K above the triple point near 20-30 kPa), so the T_wb sweep
5608 // extends to +1.5 C and pressures down to 20 kPa to exercise it.
5609 int valid = 0, errored_clean = 0, bad = 0;
5610 for (double RH : {0.01, 0.1, 0.5}) {
5611 for (int i = 0; i <= 42; ++i) { // T_wb from -0.6 to +1.5 C in 0.05 C steps
5612 const double twb_C = -0.6 + 0.05 * i;
5613 for (int P = 20000; P <= 98000; P += 13000) {
5614 const double tdb = HumidAir::HAPropsSI("T_db", "T_wb", twb_C + 273.15, "RelHum", RH, "P", (double)P);
5615 if (ValidNumber(tdb)) {
5616 const double back = HumidAir::HAPropsSI("Twb", "T", tdb, "RelHum", RH, "P", (double)P) - 273.15;
5617 if (ValidNumber(back) && std::abs(back - twb_C) <= 1e-2) // match the production guard's 1e-2 K contract
5618 ++valid;
5619 else
5620 ++bad; // silently-wrong T_db
5621 } else {
5622 const std::string err = CoolProp::get_global_param_string("errstring");
5623 if (err.find("triple point") != std::string::npos)
5624 ++errored_clean;
5625 else
5626 ++bad; // opaque "(inf)" or other unexpected failure
5627 }
5628 }
5629 }
5630 }
5631 CAPTURE(valid);
5632 CAPTURE(errored_clean);
5633 CAPTURE(bad);
5634 CHECK(bad == 0); // no silently-wrong T_db, no opaque inf
5635 CHECK(valid > 0); // reachable targets solve
5636 CHECK(errored_clean > 0); // the genuine gap is reported cleanly
5637 }
5638
5639 SECTION("Reachable wet-bulbs on both branches still solve") {
5640 // Ice branch (T_wb < 0 C) and liquid branch (T_wb > 0 C), away from the
5641 // gap, must keep returning a finite, self-consistent T_db.
5642 for (double tw : {-5.0, -0.5, 1.0, 5.0}) {
5643 const double tdb = HumidAir::HAPropsSI("T_db", "T_wb", tw + 273.15, "RelHum", 0.5, "P", 101325.0);
5644 CAPTURE(tw);
5645 CHECK(std::isfinite(tdb));
5646 CHECK(tdb >= tw + 273.15 - 1e-6); // dry-bulb is never below wet-bulb
5647 }
5648 }
5649
5650 SECTION("Canonical T_wb = 0 C failing pressures error cleanly, never opaque 'inf'") {
5651 // The reporter's headline case: at exactly 0 C a scattered set of P is in
5652 // the gap. Those must surface the explicit triple-point message; none may
5653 // leak the old "(inf)" wording.
5654 int clean = 0, opaque = 0, unexpected = 0;
5655 for (int P = 87000; P < 99000; P += 50) {
5656 const double v = HumidAir::HAPropsSI("T_db", "T_wb", 273.15, "RelHum", 0.01, "P", (double)P);
5657 if (ValidNumber(v)) {
5658 continue;
5659 }
5660 const std::string err = CoolProp::get_global_param_string("errstring");
5661 if (err.find("triple point") != std::string::npos)
5662 ++clean;
5663 else if (err.find("(inf)") != std::string::npos)
5664 ++opaque;
5665 else
5666 ++unexpected; // any other failure text is an unrecognized regression
5667 }
5668 CAPTURE(clean);
5669 CAPTURE(opaque);
5670 CAPTURE(unexpected);
5671 CHECK(clean > 0);
5672 CHECK(opaque == 0);
5673 CHECK(unexpected == 0);
5674 }
5675}
5676
5677TEST_CASE("Out-of-range Q in update() does not corrupt cached state (#2195)", "[update][2195]") {
5678 // Issue #2195: PQ_INPUTS / QT_INPUTS / etc. assigned _Q = value
5679 // BEFORE validating the range, so a thrown OutOfRangeError left _Q at
5680 // the bad value (e.g. 1.1). A subsequent hmass() call then computed
5681 // _hmolar = _Q * SatV->hmolar() + (1 - _Q) * SatL->hmolar()
5682 // with _Q = 1.1, returning a meaningless extrapolated number that
5683 // looked plausible. The fix validates first; on throw the state is
5684 // either left clean or surfaces NaN, never a misleading finite value.
5685 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
5686 AS->update(CoolProp::PQ_INPUTS, 10e6, 0.5);
5687 const double h_legal = AS->hmass();
5688 CHECK(std::isfinite(h_legal));
5689 CHECK(h_legal > 1e6);
5690
5691 CHECK_THROWS(AS->update(CoolProp::PQ_INPUTS, 10e6, 1.1));
5692 // After the failed update, hmass must NOT silently return a finite
5693 // value computed from the bad Q. NaN is acceptable (cleared cache);
5694 // the legal cached value is also acceptable.
5695 const double h_after = AS->hmass();
5696 CAPTURE(h_after);
5697 CHECK((!std::isfinite(h_after) || h_after == h_legal));
5698
5699 CHECK_THROWS(AS->update(CoolProp::PQ_INPUTS, 10e6, -0.1));
5700 CHECK_THROWS(AS->update(CoolProp::QT_INPUTS, 1.5, 400.0));
5701 CHECK_THROWS(AS->update(CoolProp::QT_INPUTS, -0.5, 400.0));
5702}
5703TEST_CASE("Water below 0 C at atmospheric pressure is not silently labelled liquid (#1098)", "[water_phase][1098]") {
5704 // Issue #1098 (2017): PhaseSI returned "liquid" for water at T=-5 C,
5705 // P=1 atm. The HEOS solid path is not implemented but the response
5706 // should at least not be a misleading "liquid". On current master
5707 // PhaseSI returns "unknown: ..." with a helpful "T below Tmelt(p)"
5708 // message and PropsSI("Phase",...) throws. Pin both behaviours so
5709 // the original silent-"liquid" failure mode cannot return.
5710 const std::string phase = CoolProp::PhaseSI("T", 273.15 - 5, "P", 101325.0, "Water");
5711 CAPTURE(phase);
5712 CHECK(phase != "liquid");
5713 CHECK(phase != "twophase");
5714 CHECK(phase != "gas");
5715 // PropsSI("Phase", ...) catches the underlying ValueError and surfaces
5716 // it as a non-finite return + populated errstring (Cython then raises).
5717 const double v = CoolProp::PropsSI("Phase", "T", 273.15 - 5, "P", 101325.0, "Water");
5718 CHECK(!ValidNumber(v));
5719}
5720
5721TEST_CASE("DmassSmass round-trip on the dew curve preserves enthalpy (#1907)", "[water_flash][1907]") {
5722 // Issue #1907: looping along the saturated-vapor curve and
5723 // re-flashing each (D, S) pair produced wildly inconsistent
5724 // enthalpies. Root cause was that HSU_D_flash assigned _T to the
5725 // converged temperature without re-evaluating alphar at that T,
5726 // leaving the alphar-derivative cache from the LAST solver
5727 // iteration. Subsequent h/s/p queries used stale derivatives.
5728 //
5729 // Walk densities along the dew curve, capture (h_dew, s_dew),
5730 // re-flash via DmassSmass and verify hmass agrees.
5731 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "water"));
5732 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "water"));
5733 // Avoid densities near rho_crit where DmassQ has multiple T-roots — the
5734 // post-#2835 strict path raises MultipleSolutionsError there. The low-d
5735 // / falling-branch densities below are unique-root.
5736 for (double d : {200.0, 175.0, 130.0, 70.0, 30.0, 15.126, 11.141, 5.0}) {
5737 CAPTURE(d);
5738 AS->update(CoolProp::DmassQ_INPUTS, d, 1.0);
5739 const double T_dew = AS->T();
5740 const double h_dew = AS->hmass();
5741 const double s_dew = AS->smass();
5742 const double p_dew = AS->p();
5743
5744 AS2->update(CoolProp::DmassSmass_INPUTS, d, s_dew);
5745 CAPTURE(T_dew);
5746 CAPTURE(h_dew);
5747 CAPTURE(s_dew);
5748 CAPTURE(AS2->T());
5749 CAPTURE(AS2->hmass());
5750 CAPTURE(AS2->smass());
5751 CHECK(std::abs(AS2->T() - T_dew) / T_dew < 1e-6);
5752 CHECK(std::abs(AS2->hmass() - h_dew) / h_dew < 1e-3);
5753 CHECK(std::abs(AS2->smass() - s_dew) / s_dew < 1e-6);
5754 CHECK(std::abs(AS2->p() - p_dew) / p_dew < 1e-3);
5755 }
5756}
5757
5758TEST_CASE("D+{H,S,U} round-trip for sub-triple compressed-liquid water (CoolProp-lgk)", "[water_flash][HSU_D_subtriple][lgk]") {
5759 // Dense liquid water exists below the triple-point temperature (273.16 K)
5760 // wherever the state is above the melting curve Tmelt(p): ~266 K at 100 MPa,
5761 // down toward ~251 K near the 209 MPa melting minimum. IAPWS-95 is valid
5762 // there, so a density+caloric flash must round-trip these compressed-liquid
5763 // states. Previously PHSU_D_flash bracketed the single-phase T-search at
5764 // [Ttriple, Tmax*1.5] and threw "D < DLtriple" for any state colder than the
5765 // triple-point isochore (i.e. all T < Ttriple), even above the melting line.
5766 auto ref = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
5767 auto rt = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
5768 const double Tt = Props1SI("Water", "Ttriple");
5769
5771 CAPTURE(static_cast<int>(pair));
5772
5773 for (double p : {100e6, 150e6, 200e6}) {
5774 const double Tmelt = ref->melting_line(CoolProp::iT, CoolProp::iP, p);
5775 CAPTURE(p, Tmelt, Tt);
5776 // The sampled band is [Tmelt+1, Tt-0.5]; require it non-inverted so the
5777 // sweep genuinely lands below the triple point (not a vacuous pass).
5778 REQUIRE(Tmelt + 1.0 < Tt - 0.5);
5779 // Strictly above the melting curve, strictly below the triple point.
5780 for (double T : linspace(Tmelt + 1.0, Tt - 0.5, 6)) {
5781 CAPTURE(T);
5782 ref->update(CoolProp::PT_INPUTS, p, T);
5783 const double d = ref->rhomass();
5784 double other = 0;
5785 switch (pair) {
5787 other = ref->hmass();
5788 break;
5790 other = ref->smass();
5791 break;
5792 default:
5793 other = ref->umass();
5794 break;
5795 }
5796 CAPTURE(d, other);
5797 REQUIRE_NOTHROW(rt->update(pair, d, other));
5798 CHECK(rt->rhomass() == Catch::Approx(d).epsilon(1e-5));
5799 CHECK(rt->T() == Catch::Approx(T).epsilon(1e-4));
5800 }
5801 }
5802
5803 // Deep point near the melting-curve minimum (~251.2 K at ~208.5 MPa, the
5804 // coldest liquid water attainable). A coarse-scan-only floor lands ~1 K
5805 // high (~252.3 K) and would wrongly reject this valid liquid state; the
5806 // refined two-stage melting-minimum search must round-trip it.
5807 {
5808 const double p = 208.5e6;
5809 const double Tmelt = ref->melting_line(CoolProp::iT, CoolProp::iP, p);
5810 const double T = Tmelt + 0.5; // valid liquid just above the melting line, well below Ttriple
5811 REQUIRE(T < Tt);
5812 ref->update(CoolProp::PT_INPUTS, p, T);
5813 const double d = ref->rhomass();
5814 const double other = (pair == CoolProp::DmassHmass_INPUTS) ? ref->hmass()
5815 : (pair == CoolProp::DmassSmass_INPUTS) ? ref->smass()
5816 : ref->umass();
5817 CAPTURE(p, Tmelt, T, d, other);
5818 REQUIRE_NOTHROW(rt->update(pair, d, other));
5819 CHECK(rt->rhomass() == Catch::Approx(d).epsilon(1e-5));
5820 CHECK(rt->T() == Catch::Approx(T).epsilon(1e-4));
5821 }
5822}
5823
5824TEST_CASE("REFPROP DmolarSmolar honours imposed phase (#2042)", "[REFPROP][2042]") {
5826 // Issue #2042: a multi-component natural-gas mixture with phase
5827 // imposed to gas via specify_phase still routed through DSFLSH ->
5828 // DSFL2, which threw "2-phase iteration did not converge". Imposed
5829 // single-phase now uses DSFL1 + THERMdll directly.
5830 auto AS = std::shared_ptr<CoolProp::AbstractState>(
5831 CoolProp::AbstractState::factory("REFPROP", "METHANE&ETHANE&PROPANE&BUTANE&ISOBUTAN&PENTANE&IPENTANE&HEXANE&NITROGEN&H2S&CO2"));
5832 AS->set_mole_fractions({0.30241, 0.03639, 0.02119, 0.007, 0.0031, 0.0039, 0.0015, 0.0008, 0.0025, 0.0002, 0.62101});
5833 AS->update(CoolProp::PT_INPUTS, 14500000.0, 315.35);
5834 const double rho_in = 405.4197781472575;
5835 const double s_in = 2004.9031527899563;
5836 AS->specify_phase(CoolProp::iphase_gas);
5837 REQUIRE_NOTHROW(AS->update(CoolProp::DmassSmass_INPUTS, rho_in, s_in));
5838 CHECK(std::abs(AS->rhomass() - rho_in) / rho_in < 1e-6);
5839 CHECK(AS->T() > 0);
5840 CHECK(AS->p() > 0);
5841}
5842
5843TEST_CASE("change_EOS rejects unknown EOS name", "[change_EOS][1703]") {
5844 // Issue #1703: change_EOS used to silently no-op for unrecognized
5845 // EOS names; it now throws. Valid names (SRK, Peng-Robinson,
5846 // XiangDeiters) must still work; an out-of-range index must still throw.
5847 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "CO2"));
5848 CHECK_THROWS(AS->change_EOS(0, "kdsfakhds"));
5849 CHECK_THROWS(AS->change_EOS(0, ""));
5850 CHECK_THROWS(AS->change_EOS(99, "SRK"));
5851 CHECK_NOTHROW(AS->change_EOS(0, "SRK"));
5852 CHECK_NOTHROW(AS->change_EOS(0, "Peng-Robinson"));
5853 CHECK_NOTHROW(AS->change_EOS(0, "XiangDeiters"));
5854}
5855
5856TEST_CASE("Ammonia d(U)/d(P)|sigma at P=60110.77... is finite (#2244)", "[ammonia][2244]") {
5857 // Issue #2244: PropsSI('d(U)/d(P)|sigma','P',60110.7723310773,'Q',0,
5858 // 'Ammonia') used to throw while neighbouring P values worked.
5859 // The exact reproducer plus a small bracket are now all smooth on
5860 // current master; pin them so the boundary condition stays fixed.
5861 const double P = 60110.7723310773;
5862 for (double dP : {-0.001, 0.0, 0.001}) {
5863 CAPTURE(dP);
5864 const double v = CoolProp::PropsSI("d(U)/d(P)|sigma", "P", P + dP, "Q", 0, "Ammonia");
5865 CAPTURE(v);
5866 CHECK(std::isfinite(v));
5867 CHECK(v > 1.0);
5868 CHECK(v < 2.0); // expected ~1.33
5869 }
5870}
5871
5872TEST_CASE("Ammonia (R717) PT_INPUTS in superheated vapor returns valid enthalpy (#2461)", "[ammonia][R717][2461]") {
5873 // Issue #2461: a chained PropsSI workflow on R717 (ammonia) at
5874 // P=130000 Pa, T=Tsat+2K threw "options.T is not valid in
5875 // saturation_P_pure_1D_T". On current master the same calls
5876 // succeed; pin the PT path that the user's original code exercises.
5877 const double P_low = 1.3e5;
5878 const double T_sat = CoolProp::PropsSI("T", "P", P_low, "Q", 1, "R717");
5879 const double T_evap = T_sat + 2.0; // 2 K superheat
5880 const double h = CoolProp::PropsSI("H", "P", P_low, "T", T_evap, "R717");
5881 CAPTURE(T_sat);
5882 CAPTURE(T_evap);
5883 CAPTURE(h);
5884 CHECK(std::isfinite(h));
5885 // Superheated ammonia at near-atmospheric P has h around 1.5e6 J/kg
5886 CHECK(h > 1.4e6);
5887 CHECK(h < 1.7e6);
5888}
5889
5890// GitHub #2773: saturation flashes can have multiple T solutions for the same
5891// (h | s | rho, Q) input. update_with_guesses now uses guess.T to pick the
5892// branch via TOMS748 rootfinding inside each provably-monotonic Chebyshev
5893// sub-interval of the saturation superancillary. h_sat and s_sat
5894// superancillaries are built lazily on first use against the EOS in its
5895// current configuration.
5896TEST_CASE("DmolarQ branch selection via guess.T", "[2773][branch_selection]") {
5897
5898 // Anchor target density between rho(near triple) and rho(near peak) on
5899 // the rising branch — this guarantees two T-roots exist (one on the
5900 // rising branch below the density max, one on the falling branch above).
5901 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5902 AS->update(CoolProp::QT_INPUTS, 0.0, 277.0); // close to peak
5903 const double rho_near_peak = AS->rhomolar();
5904 AS->update(CoolProp::QT_INPUTS, 0.0, 273.5); // close to triple
5905 const double rho_near_triple = AS->rhomolar();
5906 REQUIRE(rho_near_peak > rho_near_triple); // confirms density max is real
5907 const double rho_target = 0.5 * (rho_near_peak + rho_near_triple);
5908 AS->update(CoolProp::QT_INPUTS, 0.0, 290.0);
5909 REQUIRE(AS->rhomolar() < rho_target); // confirms falling branch reaches target
5910
5911 const double T_density_max = 277.13; // approximate peak T
5912
5913 SECTION("Guess near low-T (rising) branch → root below density max") {
5915 g.T = 274.0;
5916 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::DmolarQ_INPUTS, rho_target, 0.0, g));
5917 CHECK(AS->T() < T_density_max);
5918 CHECK(AS->rhomolar() == Catch::Approx(rho_target).epsilon(1e-6));
5919 }
5920 SECTION("Guess near high-T (falling) branch → root above density max") {
5922 g.T = 282.0;
5923 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::DmolarQ_INPUTS, rho_target, 0.0, g));
5924 CHECK(AS->T() > T_density_max);
5925 CHECK(AS->rhomolar() == Catch::Approx(rho_target).epsilon(1e-6));
5926 }
5927}
5928
5929TEST_CASE("HmolarQ branch selection via guess.T (water saturated vapor)", "[2773][branch_selection]") {
5930 // h_g(T) on water saturated vapor has a maximum near T ≈ 540 K. Anchor
5931 // h_target = h_g(T_low) where T_low is on the rising side; by smoothness
5932 // of h_g there is necessarily a second T_high > T_peak on the falling
5933 // side that also satisfies h_g(T_high) = h_target.
5934 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5935 const double T_low_anchor = 470.0;
5936 AS->update(CoolProp::QT_INPUTS, 1.0, T_low_anchor);
5937 const double h_target = AS->hmolar();
5938 const double T_h_max_approx = 540.0; // approximate h_g peak T
5939
5940 SECTION("Guess near T_low_anchor → low-T root") {
5942 g.T = T_low_anchor;
5943 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_target, 1.0, g));
5944 CHECK(AS->T() < T_h_max_approx);
5945 CHECK(AS->T() == Catch::Approx(T_low_anchor).epsilon(0.01));
5946 }
5947 SECTION("Guess far above the h-peak → high-T root") {
5949 g.T = 620.0;
5950 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_target, 1.0, g));
5951 CHECK(AS->T() > T_h_max_approx); // a different root past the peak
5952 CHECK(AS->T() != Catch::Approx(T_low_anchor).epsilon(0.05)); // not the same root
5953 }
5954}
5955
5956TEST_CASE("Default update() throws MultipleSolutionsError on ambiguous saturation flash", "[2773][strict_mode]") {
5957 // GitHub #2773 / #2834: when an HQ / SQ / DQ saturation flash input lies
5958 // in a multi-root region, the no-guess default update() now raises
5959 // MultipleSolutionsError instead of silently picking one. Users in this
5960 // case should call update_with_guesses with a guess.T.
5961
5962 SECTION("Water DmolarQ in the rho_L-non-monotonic region near 4 °C") {
5963 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5964 AS->update(CoolProp::QT_INPUTS, 0.0, 277.0);
5965 const double rho_near_peak = AS->rhomolar();
5966 AS->update(CoolProp::QT_INPUTS, 0.0, 273.5);
5967 const double rho_near_triple = AS->rhomolar();
5968 const double rho_target = 0.5 * (rho_near_peak + rho_near_triple);
5969 // Default flash should now refuse the ambiguous input cleanly.
5970 CHECK_THROWS_AS(AS->update(CoolProp::DmolarQ_INPUTS, rho_target, 0.0), CoolProp::MultipleSolutionsError);
5971 }
5972
5973 SECTION("Water HmolarQ in the h_g-non-monotonic region around 540 K peak") {
5974 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5975 AS->update(CoolProp::QT_INPUTS, 1.0, 470.0);
5976 const double h_target = AS->hmolar(); // also reached on the high-T side
5977 CHECK_THROWS_AS(AS->update(CoolProp::HmolarQ_INPUTS, h_target, 1.0), CoolProp::MultipleSolutionsError);
5978 }
5979
5980 SECTION("Single-root DQ still succeeds via the default path") {
5981 // Far from the density max, rho_L(T) is monotone — single root, no error.
5982 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5983 AS->update(CoolProp::QT_INPUTS, 0.0, 400.0);
5984 const double rho_400 = AS->rhomolar();
5985 REQUIRE_NOTHROW(AS->update(CoolProp::DmolarQ_INPUTS, rho_400, 0.0));
5986 CHECK(AS->T() == Catch::Approx(400.0).epsilon(1e-6));
5987 }
5988}
5989
5990TEST_CASE("QSmolar branch selection via guess.T (water saturated vapor)", "[2773][branch_selection]") {
5991 // s_g(T) on water saturated vapor descends monotonically from triple to
5992 // critical, so it does not by itself exhibit two roots — but exercising
5993 // the QSmolar_INPUTS dispatch validates that the new code path runs end-
5994 // to-end and returns a sensible T.
5995 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
5996 const double T_anchor = 500.0;
5997 AS->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
5998 const double s_target = AS->smolar();
6000 g.T = T_anchor;
6001 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::QSmolar_INPUTS, 1.0, s_target, g));
6002 CHECK(AS->T() == Catch::Approx(T_anchor).epsilon(0.01));
6003}
6004
6005// Edge-case coverage gaps surfaced in PR #2835 review (S4):
6006TEST_CASE("Saturation branch flash: edge cases (#2773)", "[2773][edge_cases]") {
6007
6008 SECTION("Mass-input dispatch: HmassQ_INPUTS converts and resolves") {
6009 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6010 const double T_anchor = 470.0;
6011 AS->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6012 const double h_mass = AS->hmass(); // J/kg
6014 g.T = T_anchor;
6015 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::HmassQ_INPUTS, h_mass, 1.0, g));
6016 CHECK(AS->T() == Catch::Approx(T_anchor).epsilon(0.01));
6017 }
6018
6019 SECTION("Mass-input dispatch: QSmass_INPUTS converts and resolves") {
6020 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6021 const double T_anchor = 500.0;
6022 AS->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6023 const double s_mass = AS->smass(); // J/kg/K
6025 g.T = T_anchor;
6026 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::QSmass_INPUTS, 1.0, s_mass, g));
6027 CHECK(AS->T() == Catch::Approx(T_anchor).epsilon(0.01));
6028 }
6029
6030 SECTION("Reference-state change works correctly across instances") {
6031 // Build the h-superancillary against the default reference state,
6032 // then switch to NBP and verify a fresh HEOS instance returns the
6033 // NBP-frame T-root. The cache is shared (single shared_ptr) but the
6034 // shift-c0 trick translates the new caller's target into the cache's
6035 // frame at query time — no rebuild needed. NBP works for water
6036 // (IIR is rejected because Ttriple > 273.15 K).
6037 //
6038 // RAII guard ensures the global ref state is reset to DEF even if
6039 // an assertion below fails — otherwise downstream water tests would
6040 // operate under NBP and behave unpredictably.
6041 struct WaterRefStateGuard
6042 {
6043 ~WaterRefStateGuard() {
6044 CoolProp::set_reference_stateS("Water", "DEF");
6045 }
6046 } guard;
6047
6048 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6049 const double T_anchor = 470.0;
6050 AS->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6051 const double h_def = AS->hmolar();
6053 g.T = T_anchor;
6054 // Force the caloric superancillary build under the DEF reference.
6055 REQUIRE_NOTHROW(AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_def, 1.0, g));
6056 // Switch reference state. The cache stays as-is; only the offset
6057 // stamp records what frame it's in.
6058 CoolProp::set_reference_stateS("Water", "NBP");
6059 std::shared_ptr<CoolProp::AbstractState> AS2(CoolProp::AbstractState::factory("HEOS", "Water"));
6060 AS2->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6061 const double h_nbp = AS2->hmolar();
6062 REQUIRE(std::abs(h_nbp - h_def) > 100.0); // confirms the offset is real
6063 // resolve_T_via_superancillary translates h_nbp into the cache's DEF
6064 // frame via R·T_red·(a2_cache − a2_caller), so TOMS748 finds the same
6065 // saturation T_anchor that satisfies both frames.
6066 REQUIRE_NOTHROW(AS2->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_nbp, 1.0, g));
6067 CHECK(AS2->T() == Catch::Approx(T_anchor).epsilon(0.01));
6068 }
6069
6070 SECTION("Mixture: NotImplementedError preserved") {
6071 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R125"));
6072 std::vector<double> z = {0.5, 0.5};
6073 AS->set_mole_fractions(z);
6075 g.T = 300.0;
6076 CHECK_THROWS_AS(AS->update_with_guesses(CoolProp::DmolarQ_INPUTS, 10000.0, 0.0, g), CoolProp::NotImplementedError);
6077 }
6078
6079 SECTION("guess.T outside saturation range: clear SolutionError") {
6080 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6081 AS->update(CoolProp::QT_INPUTS, 1.0, 470.0);
6082 const double h_target = AS->hmolar();
6084 g.T = 700.0; // above water's critical (647 K) — outside range
6085 CHECK_THROWS_AS(AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_target, 1.0, g), CoolProp::SolutionError);
6086 }
6087
6088 SECTION("Multi-root region throws MultipleSolutionsError on default update()") {
6089 // h_g(T) on water saturated vapor is non-monotonic with peak near 540 K.
6090 // h_target = h_g(470 K) is also reached near 605 K — two distinct
6091 // roots that the dedup must NOT merge.
6092 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6093 AS->update(CoolProp::QT_INPUTS, 1.0, 470.0);
6094 const double h_low = AS->hmolar();
6095 CHECK_THROWS_AS(AS->update(CoolProp::HmolarQ_INPUTS, h_low, 1.0), CoolProp::MultipleSolutionsError);
6096 }
6097
6098 SECTION("Mass-input multi-root region also throws MultipleSolutionsError") {
6099 // Same as above but exercises the HmassQ_INPUTS → HmolarQ_INPUTS
6100 // conversion path in mass_to_molar_inputs (review I4 follow-up).
6101 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6102 AS->update(CoolProp::QT_INPUTS, 1.0, 470.0);
6103 const double h_low_mass = AS->hmass();
6104 CHECK_THROWS_AS(AS->update(CoolProp::HmassQ_INPUTS, h_low_mass, 1.0), CoolProp::MultipleSolutionsError);
6105 }
6106}
6107
6108// Verify the option-D shift-c0 trick: the caloric superancillary cache is
6109// reference-state-agnostic. Two coexisting HEOS instances at different
6110// reference states should both get correct answers without forcing the cache
6111// to rebuild on every alternation. See #2773.
6112TEST_CASE("Caloric superancillary is reference-state-agnostic (no thrashing)", "[2773][ref_state_shared]") {
6113 // RAII guard: ensure water is reset to DEF on exit, regardless of
6114 // assertion failures, so downstream tests aren't contaminated.
6115 struct WaterRefStateGuard
6116 {
6117 WaterRefStateGuard() {
6118 CoolProp::set_reference_stateS("Water", "DEF");
6119 }
6120 ~WaterRefStateGuard() {
6121 CoolProp::set_reference_stateS("Water", "DEF");
6122 }
6123 } guard;
6124
6125 // (1) Build the cache via the first HEOS instance under DEF.
6126 std::shared_ptr<CoolProp::AbstractState> AS_def(CoolProp::AbstractState::factory("HEOS", "Water"));
6127 const double T_anchor = 470.0;
6128 AS_def->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6129 const double h_def_anchor = AS_def->hmolar();
6131 g.T = T_anchor;
6132 REQUIRE_NOTHROW(AS_def->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_def_anchor, 1.0, g));
6133 CHECK(AS_def->T() == Catch::Approx(T_anchor).epsilon(1e-6));
6134
6135 const auto build_count_after_first = static_cast<unsigned int>(AS_def->get_fluid_parameter_double(0, "SUPERANC::caloric_build_count"));
6136 REQUIRE(build_count_after_first >= 1u);
6137
6138 // (2) Switch the global reference state to NBP. AS_def keeps its own DEF
6139 // alpha0 (each HEOS holds its own copy); a fresh HEOS gets NBP.
6140 CoolProp::set_reference_stateS("Water", "NBP");
6141 std::shared_ptr<CoolProp::AbstractState> AS_nbp(CoolProp::AbstractState::factory("HEOS", "Water"));
6142 AS_nbp->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6143 const double h_nbp_anchor = AS_nbp->hmolar();
6144 REQUIRE(std::abs(h_nbp_anchor - h_def_anchor) > 100.0); // confirm offset is real
6145
6146 // (3) Both instances must resolve their own h-target back to T_anchor.
6147 REQUIRE_NOTHROW(AS_nbp->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_nbp_anchor, 1.0, g));
6148 CHECK(AS_nbp->T() == Catch::Approx(T_anchor).epsilon(1e-6));
6149 REQUIRE_NOTHROW(AS_def->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_def_anchor, 1.0, g));
6150 CHECK(AS_def->T() == Catch::Approx(T_anchor).epsilon(1e-6));
6151
6152 // (4) Alternate queries. With option-D the cache stays built once; the
6153 // shift trick translates each user's target into the cache's frame.
6154 for (int i = 0; i < 5; ++i) {
6155 REQUIRE_NOTHROW(AS_def->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_def_anchor, 1.0, g));
6156 CHECK(AS_def->T() == Catch::Approx(T_anchor).epsilon(1e-6));
6157 REQUIRE_NOTHROW(AS_nbp->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_nbp_anchor, 1.0, g));
6158 CHECK(AS_nbp->T() == Catch::Approx(T_anchor).epsilon(1e-6));
6159 }
6160
6161 // (5) The build count must equal what it was after step (1) — no
6162 // thrashing rebuilds despite alternating reference states.
6163 const auto build_count_final = static_cast<unsigned int>(AS_def->get_fluid_parameter_double(0, "SUPERANC::caloric_build_count"));
6164 CHECK(build_count_final == build_count_after_first);
6165
6166 // Same for QSmolar with mixed reference states.
6167 AS_def->update(CoolProp::QT_INPUTS, 1.0, 500.0);
6168 const double s_def_anchor = AS_def->smolar();
6169 AS_nbp->update(CoolProp::QT_INPUTS, 1.0, 500.0);
6170 const double s_nbp_anchor = AS_nbp->smolar();
6171 REQUIRE(std::abs(s_nbp_anchor - s_def_anchor) > 1.0); // confirm offset is real
6172 g.T = 500.0;
6173 REQUIRE_NOTHROW(AS_def->update_with_guesses(CoolProp::QSmolar_INPUTS, 1.0, s_def_anchor, g));
6174 CHECK(AS_def->T() == Catch::Approx(500.0).epsilon(1e-3));
6175 REQUIRE_NOTHROW(AS_nbp->update_with_guesses(CoolProp::QSmolar_INPUTS, 1.0, s_nbp_anchor, g));
6176 CHECK(AS_nbp->T() == Catch::Approx(500.0).epsilon(1e-3));
6177
6178 // RAII guard at the top of the test resets the reference state on exit.
6179}
6180
6181// Verify thread safety of the lazy build path. Spawn N threads, each creating
6182// its own HEOS for water and running multiple HmolarQ flashes concurrently.
6183// Without the mutex inside ensure_HS_under_lock this would race on
6184// optional<...>::emplace and could corrupt the shared SuperAncillary.
6185// With the mutex, exactly one thread builds the cache and the others wait.
6186TEST_CASE("Caloric superancillary thread-safe lazy build (#2773)", "[2773][thread_safety]") {
6187 // Capture the build count before this test runs so the assertion below is
6188 // insensitive to any prior test having already triggered a build.
6189 std::shared_ptr<CoolProp::AbstractState> probe(CoolProp::AbstractState::factory("HEOS", "Water"));
6190 const auto build_count_before = static_cast<unsigned int>(probe->get_fluid_parameter_double(0, "SUPERANC::caloric_build_count"));
6191 probe.reset();
6192
6193 constexpr int N_THREADS = 8;
6194 constexpr int N_ITERS_PER_THREAD = 50;
6195 std::atomic<int> error_count{0};
6196 std::atomic<int> success_count{0};
6197
6198 auto worker = [&]() {
6199 try {
6200 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6201 const double T_anchor = 470.0;
6202 AS->update(CoolProp::QT_INPUTS, 1.0, T_anchor);
6203 const double h_target = AS->hmolar();
6205 g.T = T_anchor;
6206 for (int i = 0; i < N_ITERS_PER_THREAD; ++i) {
6207 AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_target, 1.0, g);
6208 if (std::abs(AS->T() - T_anchor) > 1e-3) {
6209 error_count.fetch_add(1, std::memory_order_relaxed);
6210 return;
6211 }
6212 }
6213 success_count.fetch_add(1, std::memory_order_relaxed);
6214 } catch (...) {
6215 error_count.fetch_add(1, std::memory_order_relaxed);
6216 }
6217 };
6218
6219 std::vector<std::thread> threads;
6220 threads.reserve(N_THREADS);
6221 for (int t = 0; t < N_THREADS; ++t) {
6222 threads.emplace_back(worker);
6223 }
6224 for (auto& t : threads) {
6225 t.join();
6226 }
6227
6228 CHECK(error_count.load() == 0);
6229 CHECK(success_count.load() == N_THREADS);
6230
6231 // Build counter delta should be at most 1: with the mutex, exactly one
6232 // thread wins the race to build; the others observe the cache and skip.
6233 // Allow 0 if a prior test already built the cache.
6234 std::shared_ptr<CoolProp::AbstractState> probe2(CoolProp::AbstractState::factory("HEOS", "Water"));
6235 const auto build_count_after = static_cast<unsigned int>(probe2->get_fluid_parameter_double(0, "SUPERANC::caloric_build_count"));
6236 CHECK(build_count_after - build_count_before <= 1u);
6237}
6238
6239// Benchmark the new superancillary-based with_guesses path against the
6240// existing default flash for the input pairs covered by #2773. Tagged
6241// [!benchmark] so it doesn't run with the default test selection — invoke
6242// with e.g. `CatchTestRunner "[2773][bench]"`.
6243//
6244// Only DQ has a working baseline: default HQ_flash and QS_flash are unreliable
6245// for water (and propane) at many sensible operating points (the symptom that
6246// motivates #2773). Where the baseline can be benchmarked, it is; otherwise the
6247// with_guesses path's absolute timing stands on its own.
6248TEST_CASE("DQ/HQ/QS flash benchmarks (#2773)", "[2773][bench][!benchmark]") {
6249 std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "Water"));
6250
6251 // DQ: pick a T well away from the 4 °C density max so the existing
6252 // single-root Brent path doesn't bracket-hop. T = 400 K is on the
6253 // monotonic-decreasing side of rho_L(T).
6254 AS->update(CoolProp::QT_INPUTS, 0.0, 400.0);
6255 const double rho_DQ = AS->rhomolar();
6257 g_DQ.T = 400.0;
6258
6259 // HQ: anchor on a rising-branch h_g for water saturated vapor.
6260 AS->update(CoolProp::QT_INPUTS, 1.0, 470.0);
6261 const double h_HQ = AS->hmolar();
6263 g_HQ.T = 470.0;
6264
6265 // QS: saturated liquid n-propane at 300 K.
6266 std::shared_ptr<CoolProp::AbstractState> AS_prop(CoolProp::AbstractState::factory("HEOS", "n-Propane"));
6267 AS_prop->update(CoolProp::QT_INPUTS, 0.0, 300.0);
6268 const double s_QS = AS_prop->smolar();
6270 g_QS.T = 300.0;
6271
6272 BENCHMARK("DQ default update(DmolarQ,rho,Q=0)") {
6273 return AS->update(CoolProp::DmolarQ_INPUTS, rho_DQ, 0.0);
6274 };
6275 BENCHMARK("DQ with guesses update_with_guesses(DmolarQ,rho,Q=0,T)") {
6276 return AS->update_with_guesses(CoolProp::DmolarQ_INPUTS, rho_DQ, 0.0, g_DQ);
6277 };
6278 // Default HQ_flash and QS_flash are skipped — both throw or crash for the
6279 // chosen anchor points (pre-existing #2773 symptom).
6280 BENCHMARK("HQ with guesses update_with_guesses(HmolarQ,h,Q=1,T)") {
6281 return AS->update_with_guesses(CoolProp::HmolarQ_INPUTS, h_HQ, 1.0, g_HQ);
6282 };
6283 BENCHMARK("QS with guesses update_with_guesses(QSmolar,Q=0,s,T) [propane]") {
6284 return AS_prop->update_with_guesses(CoolProp::QSmolar_INPUTS, 0.0, s_QS, g_QS);
6285 };
6286}
6287
6288TEST_CASE("INCOMP backend rejects molar property requests with a clean error", "[INCOMP][1908]") {
6289 // Issue #1908: PropsSI('Dmolar','T',298.15,'P',101325,'INCOMP::AEG[0.1]')
6290 // returned -infinity (the AbstractState default for _rhomolar) and the
6291 // Python wrapper surfaced the unhelpful "PropsSI failed ungracefully"
6292 // message because errstring was empty. The INCOMP backend now throws
6293 // a NotImplementedError naming the mass-basis equivalent so PropsSI
6294 // propagates a useful errstring.
6295 double v = CoolProp::PropsSI("Dmolar", "T", 298.15, "P", 101325.0, "INCOMP::AEG[0.1]");
6296 CHECK(!ValidNumber(v));
6297 std::string err = CoolProp::get_global_param_string("errstring");
6298 CAPTURE(err);
6299 CHECK(err.find("INCOMP") != std::string::npos);
6300 // Mass-basis output must still work for the same state
6301 double rhomass = CoolProp::PropsSI("Dmass", "T", 298.15, "P", 101325.0, "INCOMP::AEG[0.1]");
6302 CHECK(rhomass > 0);
6303 CHECK(rhomass < 2000);
6304}
6305
6306TEST_CASE("mole_fractions_liquid/vapor reject single-phase states (#2308)", "[mole_fractions][2308]") {
6307 // Issue #2308: SatL/SatV retain composition vectors from the most recent
6308 // VLE flash (or phase-envelope build); calc_mole_fractions_liquid/vapor
6309 // were returning those stale values when the current state was actually
6310 // single-phase, which is misleading. Now they throw.
6311 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Nitrogen&Methane&Ethane&Propane"));
6312 AS->set_mole_fractions({0.10, 0.34, 0.41, 0.15});
6313
6314 // Build the phase envelope so SatL/SatV pick up some non-trivial state
6315 AS->build_phase_envelope("");
6316
6317 // Single-phase point: T well above the dew curve at 1.5 bar
6318 AS->update(CoolProp::PT_INPUTS, 1.5e5, 298.15);
6319 REQUIRE_FALSE(AS->phase() == CoolProp::iphase_twophase);
6320 CHECK_THROWS_AS(AS->mole_fractions_liquid(), CoolProp::ValueError);
6321 CHECK_THROWS_AS(AS->mole_fractions_vapor(), CoolProp::ValueError);
6322
6323 // Two-phase point: PQ flash, Q=0.5
6324 AS->update(CoolProp::PQ_INPUTS, 1.5e5, 0.5);
6325 REQUIRE(AS->phase() == CoolProp::iphase_twophase);
6326 REQUIRE_NOTHROW(AS->mole_fractions_liquid());
6327 REQUIRE_NOTHROW(AS->mole_fractions_vapor());
6328 auto x = AS->mole_fractions_liquid();
6329 auto y = AS->mole_fractions_vapor();
6330 REQUIRE(x.size() == 4);
6331 REQUIRE(y.size() == 4);
6332 // Each composition must sum to ~1.0
6333 double sx = 0.0, sy = 0.0;
6334 for (auto v : x)
6335 sx += v;
6336 for (auto v : y)
6337 sy += v;
6338 CHECK(sx == Catch::Approx(1.0).epsilon(1e-9));
6339 CHECK(sy == Catch::Approx(1.0).epsilon(1e-9));
6340}
6341
6342TEST_CASE("REFPROP supports DmolarQ / DmassQ inputs (#1845)", "[REFPROP][1845]") {
6344 // Issue #1845: DmassQ_INPUTS / DmolarQ_INPUTS were missing from the
6345 // REFPROP backend dispatch. REFPROP itself supports them via DQFL2dll.
6346 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("REFPROP", "CO2"));
6347 REQUIRE_NOTHROW(AS->update(CoolProp::DmassQ_INPUTS, 15.0, 1.0));
6348 const double T_refprop = AS->T();
6349 CHECK(AS->rhomass() == Catch::Approx(15.0).epsilon(1e-9));
6350 CHECK(T_refprop > 200.0);
6351 CHECK(T_refprop < 250.0);
6352 // Cross-check with HEOS
6353 auto AS2 = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "CO2"));
6354 AS2->update(CoolProp::DmassQ_INPUTS, 15.0, 1.0);
6355 CHECK(AS->T() == Catch::Approx(AS2->T()).epsilon(1e-6));
6356}
6357
6358TEST_CASE("TABULAR_NX/NY config keys exist and default to 200", "[Configuration][TABULAR]") {
6359 // The tabular backend grid resolution is configurable; confirm the keys
6360 // are present and the documented default is honored.
6361 CHECK(CoolProp::get_config_int(TABULAR_NX) == 200);
6362 CHECK(CoolProp::get_config_int(TABULAR_NY) == 200);
6363}
6364
6365TEST_CASE("BICUBIC PT below saturation no longer segfaults (#1950)", "[BICUBIC][1950]") {
6366 // Issue #1950: BICUBIC&HEOS update(PT_INPUTS, p, T) where T is just below
6367 // Tsat(p) and the saturation curve sits inside the table cell:
6368 // - find_native_nearest_good_indices returns a valid cell on the vapor side
6369 // - saturation-curve check bumps `cached_single_phase_i--` to land in the
6370 // table's two-phase notch, where CellCoeffs has no alpha[] populated
6371 // - evaluate_single_phase dereferences alpha[12] → segfault
6372 //
6373 // After the fix, the bumped cell is checked for validity; if the alternate
6374 // neighbour is set, we use it; otherwise we throw a clean ValueError.
6375 auto BICU = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("BICUBIC&HEOS", "Nitrogen"));
6376 // P=2 bar, T=78 K is sub-saturation for N2 (Tsat(2 bar) ~ 83.6 K) — was the
6377 // original repro from the issue that crashed.
6378 REQUIRE_NOTHROW(BICU->update(CoolProp::PT_INPUTS, 2.0e5, 78.0));
6379 // BICU should land on a valid liquid cell and produce a sensible density.
6380 const double rho = BICU->rhomass();
6381 CHECK(std::isfinite(rho));
6382 CHECK(rho > 700.0); // liquid N2 ~803 kg/m^3 at this state
6383 CHECK(rho < 900.0);
6384
6385 // Cross-check vs HEOS — the magnitudes should agree to within bicubic noise.
6386 auto HEOS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Nitrogen"));
6387 HEOS->update(CoolProp::PT_INPUTS, 2.0e5, 78.0);
6388 CHECK(rho == Catch::Approx(HEOS->rhomass()).epsilon(1e-2)); // 1% bicubic tolerance
6389}
6390
6391TEST_CASE("REFPROP update_DmolarT_direct matches update(DmolarT)", "[REFPROPsat]") {
6393 std::shared_ptr<AbstractState> direct(AbstractState::factory("REFPROP", "Propane"));
6394 std::shared_ptr<AbstractState> flash(AbstractState::factory("REFPROP", "Propane"));
6395 double T = 300.0, rhomolar = 12000.0; // dense liquid, single phase
6396 flash->update(DmolarT_INPUTS, rhomolar, T);
6397 auto* be = dynamic_cast<REFPROPMixtureBackend*>(direct.get());
6398 REQUIRE(be != nullptr);
6399 be->update_DmolarT_direct(rhomolar, T);
6400 CHECK(direct->p() == Catch::Approx(flash->p()).epsilon(1e-9));
6401 CHECK(direct->hmolar() == Catch::Approx(flash->hmolar()).epsilon(1e-9));
6402 CHECK(direct->smolar() == Catch::Approx(flash->smolar()).epsilon(1e-9));
6403}
6404
6405TEST_CASE("REFPROP saturation shim reproduces saturated densities", "[REFPROPsat]") {
6407 std::shared_ptr<AbstractState> host(AbstractState::factory("REFPROP", "Propane"));
6408 host->update(QT_INPUTS, 0.5, 300.0); // two-phase
6409 auto* be = dynamic_cast<REFPROPMixtureBackend*>(host.get());
6410 REQUIRE(be != nullptr);
6411 auto shimL = be->build_saturation_shim(0); // liquid
6412 auto shimV = be->build_saturation_shim(1); // vapor
6413 CHECK(shimL->rhomolar() == Catch::Approx(be->saturated_liquid_keyed_output(iDmolar)).epsilon(1e-10));
6414 CHECK(shimV->rhomolar() == Catch::Approx(be->saturated_vapor_keyed_output(iDmolar)).epsilon(1e-10));
6415 // The shim must NOT have triggered a re-SETUP: enthalpy from THERMdll at (T, rhoL)
6416 // must equal a fresh single-phase evaluation at the same point.
6417 std::shared_ptr<AbstractState> probe(AbstractState::factory("REFPROP", "Propane"));
6418 probe->specify_phase(iphase_liquid);
6419 probe->update(DmolarT_INPUTS, shimL->rhomolar(), 300.0);
6420 CHECK(shimL->hmolar() == Catch::Approx(probe->hmolar()).epsilon(1e-9));
6421}
6422
6423TEST_CASE("REFPROP saturated keyed outputs (h,s,cp,visc) match endpoint flashes", "[REFPROPsat]") {
6425 std::shared_ptr<AbstractState> twophase(AbstractState::factory("REFPROP", "Propane"));
6426 twophase->update(QT_INPUTS, 0.5, 300.0);
6427 std::shared_ptr<AbstractState> bubble(AbstractState::factory("REFPROP", "Propane"));
6428 bubble->update(QT_INPUTS, 0.0, 300.0);
6429 std::shared_ptr<AbstractState> dew(AbstractState::factory("REFPROP", "Propane"));
6430 dew->update(QT_INPUTS, 1.0, 300.0);
6431
6432 CHECK(twophase->saturated_liquid_keyed_output(iHmolar) == Catch::Approx(bubble->hmolar()).epsilon(1e-7));
6433 CHECK(twophase->saturated_vapor_keyed_output(iHmolar) == Catch::Approx(dew->hmolar()).epsilon(1e-7));
6434 CHECK(twophase->saturated_liquid_keyed_output(iSmolar) == Catch::Approx(bubble->smolar()).epsilon(1e-7));
6435 CHECK(twophase->saturated_liquid_keyed_output(iCpmolar) == Catch::Approx(bubble->cpmolar()).epsilon(1e-6));
6436 CHECK(twophase->saturated_vapor_keyed_output(iviscosity) == Catch::Approx(dew->viscosity()).epsilon(1e-6));
6437 CHECK(twophase->saturated_liquid_keyed_output(iconductivity) == Catch::Approx(bubble->conductivity()).epsilon(1e-6));
6438}
6439
6440TEST_CASE("REFPROP first_saturation_deriv matches HEOS for a pure fluid", "[REFPROPsat]") {
6442 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Propane"));
6443 std::shared_ptr<AbstractState> HE(AbstractState::factory("HEOS", "Propane"));
6444 for (int Ti = 200; Ti <= 360; Ti += 40) {
6445 double T = Ti;
6446 RP->update(QT_INPUTS, 0.0, T);
6447 HE->update(QT_INPUTS, 0.0, T);
6448 CHECK(RP->first_saturation_deriv(iT, iP) == Catch::Approx(HE->first_saturation_deriv(iT, iP)).epsilon(1e-4));
6449 CHECK(RP->first_saturation_deriv(iDmolar, iT) == Catch::Approx(HE->first_saturation_deriv(iDmolar, iT)).epsilon(1e-3));
6450 CHECK(RP->first_saturation_deriv(iHmolar, iP) == Catch::Approx(HE->first_saturation_deriv(iHmolar, iP)).epsilon(1e-3));
6451 }
6452 // Pure fluid: a two-phase state with 0<Q<1 must also work (vapor-pressure slope is Q-independent).
6453 RP->update(QT_INPUTS, 0.5, 300.0);
6454 HE->update(QT_INPUTS, 0.5, 300.0);
6455 CHECK(RP->first_saturation_deriv(iT, iP) == Catch::Approx(HE->first_saturation_deriv(iT, iP)).epsilon(1e-4));
6456}
6457
6458TEST_CASE("REFPROP first_two_phase_deriv matches HEOS for a pure fluid", "[REFPROPsat]") {
6460 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Propane"));
6461 std::shared_ptr<AbstractState> HE(AbstractState::factory("HEOS", "Propane"));
6462 RP->update(QT_INPUTS, 0.4, 300.0);
6463 HE->update(QT_INPUTS, 0.4, 300.0);
6464 CHECK(RP->first_two_phase_deriv(iDmolar, iHmolar, iP) == Catch::Approx(HE->first_two_phase_deriv(iDmolar, iHmolar, iP)).epsilon(1e-4));
6465 CHECK(RP->first_two_phase_deriv(iDmolar, iP, iHmolar) == Catch::Approx(HE->first_two_phase_deriv(iDmolar, iP, iHmolar)).epsilon(1e-3));
6466}
6467
6468TEST_CASE("REFPROP saturated keyed output throws in single phase", "[REFPROPsat]") {
6470 std::shared_ptr<AbstractState> AS(AbstractState::factory("REFPROP", "Propane"));
6471 AS->update(PT_INPUTS, 101325, 300.0); // single-phase gas
6472 CHECK_THROWS(AS->saturated_liquid_keyed_output(iHmolar));
6473 // A subsequent two-phase update must still work (no stale state leaks).
6474 AS->update(QT_INPUTS, 0.3, 280.0);
6475 CHECK(ValidNumber(AS->saturated_liquid_keyed_output(iHmolar)));
6476}
6477
6478TEST_CASE("REFPROP first_two_phase_deriv_splined matches HEOS (pure)", "[REFPROPsat]") {
6480 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Propane"));
6481 std::shared_ptr<AbstractState> HE(AbstractState::factory("HEOS", "Propane"));
6482 double x_end = 0.3;
6483 RP->update(QT_INPUTS, 0.1, 300.0);
6484 HE->update(QT_INPUTS, 0.1, 300.0);
6485 CHECK(RP->first_two_phase_deriv_splined(iDmolar, iHmolar, iP, x_end)
6486 == Catch::Approx(HE->first_two_phase_deriv_splined(iDmolar, iHmolar, iP, x_end)).epsilon(1e-3));
6487 CHECK(RP->first_two_phase_deriv_splined(iDmolar, iP, iHmolar, x_end)
6488 == Catch::Approx(HE->first_two_phase_deriv_splined(iDmolar, iP, iHmolar, x_end)).epsilon(1e-3));
6489}
6490
6491TEST_CASE("REFPROP saturation derivs work for a mixture", "[REFPROPsat]") {
6493 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Methane&Ethane"));
6494 std::vector<double> z = {0.6, 0.4};
6495 RP->set_mole_fractions(z);
6496 RP->update(QT_INPUTS, 0.0, 180.0); // bubble
6497 // dT/dp along the bubble curve should be finite and positive away from the critical point.
6498 double dTdp = RP->first_saturation_deriv(iT, iP);
6499 CHECK(ValidNumber(dTdp));
6500 CHECK(dTdp > 0);
6501 // Two-phase interior derivatives are not implemented for mixtures (need the
6502 // constant-overall-composition saturation slope); they must throw, not return garbage.
6503 RP->update(QT_INPUTS, 0.4, 180.0);
6504 CHECK_THROWS(RP->first_two_phase_deriv(iDmolar, iHmolar, iP));
6505 CHECK_THROWS(RP->first_two_phase_deriv_splined(iDmolar, iHmolar, iP, 0.5));
6506}
6507
6508TEST_CASE("REFPROP set_binary_interaction_string model param is length-guarded (CoolProp-tw7t)", "[REFPROP][refprop][binary_interaction]") {
6510 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Methane&Ethane"));
6511 RP->set_mole_fractions({0.6, 0.4});
6512
6513 // hmodij is a 3-character REFPROP (FORTRAN) field. An over-long model value
6514 // must be rejected by the length guard, NOT strcpy'd into the 3-byte buffer
6515 // (which overflowed the stack by 1-2 bytes — CoolProp-tw7t). The length-4
6516 // case is the precise regression: the old guard `value.length() > 4` let it
6517 // through and overflowed; ASAN only flags it with a boundary-length input,
6518 // which no prior test supplied.
6519 CHECK_THROWS_AS(RP->set_binary_interaction_string(0, 1, "model", "ABCD"), CoolProp::ValueError); // 4 chars
6520 CHECK_THROWS_AS(RP->set_binary_interaction_string(0, 1, "model", "TOOLONG"), CoolProp::ValueError); // 7 chars
6521 // An unknown parameter must also fail cleanly rather than silently no-op.
6522 CHECK_THROWS_AS(RP->set_binary_interaction_string(0, 1, "bogus", "x"), CoolProp::ValueError);
6523
6524 // A valid-length (<=3 char) model exercises the bounded copy: it must not
6525 // overflow hmodij[3]. Whether REFPROP accepts the specific code is version-
6526 // dependent, so we only require the call does not corrupt the stack (ASAN
6527 // would abort the binary if it did).
6528 CHECK_NOTHROW([&] {
6529 try {
6530 RP->set_binary_interaction_string(0, 1, "model", "KW0");
6531 } catch (const CoolProp::ValueError&) { /* REFPROP may reject the code; acceptable */
6532 }
6533 }());
6534
6535 // The getter reads the same fixed 3-char field; constructing a std::string
6536 // from hmodij as a C-string over-reads past the 3-byte buffer (CWE-126), so
6537 // it must read exactly the field width and trim the FORTRAN space padding.
6538 // Exercise the read path (ASAN guards the over-read); CAS: Methane=74-82-8,
6539 // Ethane=74-84-0. The CAS lookup is REFPROP-version dependent, so tolerate
6540 // a ValueError but require a within-field-width result when it succeeds.
6541 CHECK_NOTHROW([&] {
6542 try {
6543 std::string m = RP->get_binary_interaction_string("74-82-8", "74-84-0", "model");
6544 CHECK(m.size() <= 3);
6545 } catch (const CoolProp::ValueError&) { /* CAS lookup version-dependent; acceptable */
6546 }
6547 }());
6548}
6549
6550TEST_CASE("REFPROP cross-check: sat-state fugacity_coefficient agrees with HEOS for Methane/Ethane/Propane", "[REFPROPsat][2345-followup]") {
6552 const std::vector<double> z = {0.25, 0.25, 0.5};
6553
6554 std::shared_ptr<AbstractState> HEOS(AbstractState::factory("HEOS", "Methane&Ethane&Propane"));
6555 HEOS->set_mole_fractions(z);
6556 HEOS->update(PQ_INPUTS, 500e3, 0.5);
6557
6558 std::shared_ptr<AbstractState> RP(AbstractState::factory("REFPROP", "Methane&Ethane&Propane"));
6559 RP->set_mole_fractions(z);
6560 RP->update(PQ_INPUTS, 500e3, 0.5);
6561
6562 // Bubble/dew T should agree across the two libraries to a few mK; that's the
6563 // anchor for the fugacity-coefficient comparison below.
6564 CHECK(std::abs(HEOS->T() - RP->T()) < 0.05);
6565
6566 auto* heos_mix_ptr = dynamic_cast<HelmholtzEOSMixtureBackend*>(HEOS.get());
6567 REQUIRE(heos_mix_ptr != nullptr);
6568 auto& satL_heos = heos_mix_ptr->get_SatL();
6569 auto& satV_heos = heos_mix_ptr->get_SatV();
6570
6571 // The REFPROP sat states are re-flashed at the bubble/dew T of the
6572 // liquid/vapor composition respectively — those T's differ slightly from
6573 // the original two-phase T at Q=0.5, so this is a near-apples comparison
6574 // rather than exact. The 1 % tolerance below absorbs the mismatch.
6575 std::shared_ptr<AbstractState> satL_rp(AbstractState::factory("REFPROP", "Methane&Ethane&Propane"));
6576 satL_rp->set_mole_fractions(RP->mole_fractions_liquid());
6577 satL_rp->update(PQ_INPUTS, RP->p(), 0.0);
6578 std::shared_ptr<AbstractState> satV_rp(AbstractState::factory("REFPROP", "Methane&Ethane&Propane"));
6579 satV_rp->set_mole_fractions(RP->mole_fractions_vapor());
6580 satV_rp->update(PQ_INPUTS, RP->p(), 1.0);
6581
6582 for (std::size_t i = 0; i < 3; ++i) {
6583 CAPTURE(i);
6584 const double phiL_heos = satL_heos.fugacity_coefficient(i);
6585 const double phiL_rp = satL_rp->fugacity_coefficient(i);
6586 const double phiV_heos = satV_heos.fugacity_coefficient(i);
6587 const double phiV_rp = satV_rp->fugacity_coefficient(i);
6588 CAPTURE(phiL_heos);
6589 CAPTURE(phiL_rp);
6590 CAPTURE(phiV_heos);
6591 CAPTURE(phiV_rp);
6592 // Both libraries use Helmholtz-based mixture models; for this well-studied
6593 // alkane system the fugacity coefficients should agree to ~1%.
6594 CHECK(std::abs(phiL_heos - phiL_rp) / std::abs(phiL_rp) < 1e-2);
6595 CHECK(std::abs(phiV_heos - phiV_rp) / std::abs(phiV_rp) < 1e-2);
6596 }
6597}
6598
6599// Regression guard for the SurfaceTensionCorrelation ctor: a clang-tidy
6600// prefer-member-initializer "fix" once hoisted N=n.size() and s=n into the
6601// member-init list, where n is still empty (it's populated in the body), so
6602// every surface tension silently evaluated to 0.0. These checks fail loudly
6603// if that ever recurs. IAPWS reference values, ~1% tolerance.
6604TEST_CASE("Surface tension of water is nonzero and matches IAPWS", "[surface_tension]") {
6605 const double st_300 = CoolProp::PropsSI("I", "T", 300, "Q", 0, "Water");
6606 const double st_350 = CoolProp::PropsSI("I", "T", 350, "Q", 0, "Water");
6607 CAPTURE(st_300);
6608 CAPTURE(st_350);
6609 // Load-bearing: the regression made these exactly 0.
6610 CHECK(st_300 > 0.0);
6611 CHECK(st_350 > 0.0);
6612 // IAPWS: sigma(300 K) ~ 0.07170 N/m, sigma(350 K) ~ 0.06301 N/m.
6613 CHECK(st_300 == Catch::Approx(0.07170).epsilon(0.01));
6614 CHECK(st_350 == Catch::Approx(0.06301).epsilon(0.01));
6615 // Surface tension decreases monotonically toward the critical point.
6616 CHECK(st_350 < st_300);
6617 // A second fluid, looser bound, just to exercise another correlation.
6618 CHECK(CoolProp::PropsSI("I", "T", 300, "Q", 0, "R134a") > 0.0);
6619}
6620
6621#endif