CoolProp 8.0.0
An open-source fluid property and humid air property database
HumidAirProp.cpp
Go to the documentation of this file.
1#if defined(_MSC_VER)
2# ifndef _CRT_SECURE_NO_WARNINGS
3# define _CRT_SECURE_NO_WARNINGS
4# endif
5#endif
6
7#include <atomic>
8#include <cmath>
9#include <memory>
10using std::shared_ptr;
11
16#include "CoolProp/fluids/Ice.h"
17#include "CoolProp/CoolProp.h"
18#include "CoolProp/Exceptions.h"
20
21#include <algorithm> // std::next_permutation
22#include <cstdio>
23#include <cstring>
24#include <iostream>
25#include <list>
26#include <IF97.h>
27
29std::size_t strcmp(const std::string& s, const std::string& e) {
30 return s.compare(e);
31}
32std::size_t strcmp(const std::string& s, const char* e) { // To avoid unnecessary constructors
33 return s.compare(e);
34}
35std::size_t strcmp(const char* e, const std::string& s) {
36 return -s.compare(e);
37}
38
39// This is a lazy stub function to avoid recoding all the strcpy calls below
40void strcpy(std::string& s, const std::string& e) {
41 s = e;
42}
43
44// Per-thread Water/Air backends. The HelmholtzEOSBackend instances are mutated
45// on every HAPropsSI call (update(), specify_phase(), clear()), so a single
46// shared instance can't safely back concurrent callers. C++11 thread_local
47// gives each thread its own backend; first-call construction goes through the
48// FluidLibrary singleton, which is itself protected by std::call_once.
49// `static` makes the linkage internal — these are file-private despite living
50// at namespace scope above the HumidAir namespace block.
51static thread_local shared_ptr<CoolProp::HelmholtzEOSBackend> Water;
52static thread_local shared_ptr<CoolProp::HelmholtzEOSBackend> Air;
53static thread_local shared_ptr<CoolProp::AbstractState> WaterIF97;
54
55namespace HumidAir {
57{
84};
85
86#if !defined(NO_FMTLIB) && FMT_VERSION >= 90000
87int format_as(givens given) {
88 return fmt::underlying(given);
89}
90#endif
91
92void _HAPropsSI_inputs(double p, const std::vector<givens>& input_keys, const std::vector<double>& input_vals, double& T, double& psi_w);
93double _HAPropsSI_outputs(givens OuputType, double p, double T, double psi_w);
94double MoleFractionWater(double, double, int, double);
95
97 // Each thread lazily constructs its own Water/Air/WaterIF97 backends on
98 // first use. No synchronization required: Water/Air/WaterIF97 are
99 // thread_local, so the null check and reset() touch only this thread's
100 // shared_ptrs. The construction calls below funnel into the global
101 // FluidLibrary singleton, which is itself made thread-safe by call_once
102 // (gh-2787, gh-2800).
103 if (!Water) {
104 Water = std::make_shared<CoolProp::HelmholtzEOSBackend>("Water");
105 WaterIF97.reset(CoolProp::AbstractState::factory("IF97", "Water"));
106 Air = std::make_shared<CoolProp::HelmholtzEOSBackend>("Air");
107 }
108};
109
110static double epsilon = 0.621945, R_bar = 8.314472;
111// Correlation toggles are global flags settable from any thread; std::atomic<int>
112// gives torn-write-free reads in the hot path. The reads use implicit
113// conversion (seq_cst), the writes use plain assignment.
114static std::atomic<int> FlagUseVirialCorrelations{0};
115static std::atomic<int> FlagUseIsothermCompressCorrelation{0};
116static std::atomic<int> FlagUseIdealGasEnthalpyCorrelations{0};
117double f_factor(double T, double p);
118
119// A central place to check bounds, should be used much more frequently
120static inline bool check_bounds(const givens prop, const double& value, double& min_val, double& max_val) {
121 // If limit checking is disabled, just accept the inputs, return true
122 if (CoolProp::get_config_bool(DONT_CHECK_PROPERTY_LIMITS)) {
123 return true;
124 }
125 if (!ValidNumber(value)) return false;
126
127 switch (prop) {
128 case GIVEN_P:
129 min_val = 0.00001e6;
130 max_val = 10e6;
131 break;
132 case GIVEN_T:
133 case GIVEN_TDP:
134 case GIVEN_TWB:
135 min_val = -143.15 + 273.15;
136 max_val = 350 + 273.15;
137 break;
138 case GIVEN_HUMRAT:
139 min_val = 0.0;
140 max_val = 10.0;
141 break;
142 case GIVEN_PSIW:
143 min_val = 0.0;
144 max_val = 0.94145;
145 break;
146 case GIVEN_RH:
147 min_val = 0.0;
148 max_val = 1.0;
149 break;
150 default:
151 min_val = -_HUGE;
152 max_val = _HUGE;
153 break;
154 }
155 bool ret = !((value < min_val) || (value > max_val));
156 return ret;
157}
158
159// A couple of convenience functions that are needed quite a lot
160static double MM_Air() {
162 return Air->keyed_output(CoolProp::imolar_mass);
163}
164static double MM_Water() {
166 return Water->keyed_output(CoolProp::imolar_mass);
167}
168// Two separate per-temperature caches, both keyed by T and filled via direct EOS calls
169// (no update()), so they never disturb the backends' cached state.
170// Keeping them separate means the virial cache is skipped when FlagUseVirialCorrelations=1.
171// thread_local: each thread has its own cache, mirroring the per-thread Water/Air
172// backends. The check-then-fill pattern (T == cache.T) is therefore race-free.
173
174// Residual virial coefficients and T-derivatives
175static thread_local struct
176{
177 double T;
178 double B_a, dBdT_a, C_a, dCdT_a;
179 double B_w, dBdT_w, C_w, dCdT_w;
180} s_virial_cache = {-1.0, 0, 0, 0, 0, 0, 0, 0, 0};
181
189static void calc_all_virials(CoolProp::HelmholtzEOSMixtureBackend* fluid, double T, double& B, double& dBdT, double& C, double& dCdT) {
191 double tau = red.T / T;
192 double dtau_dT = -red.T / (T * T);
193 CoolProp::HelmholtzDerivatives derivs = fluid->residual_helmholtz->all(*fluid, fluid->get_mole_fractions(), tau, 1e-12, false);
194 B = derivs.get(0, 1) / red.rhomolar;
195 dBdT = derivs.get(1, 1) / red.rhomolar * dtau_dT;
196 C = derivs.get(0, 2) / (red.rhomolar * red.rhomolar);
197 dCdT = derivs.get(1, 2) / (red.rhomolar * red.rhomolar) * dtau_dT;
198}
199
200static void fill_virial_cache(double T) {
201 if (T == s_virial_cache.T) return;
203 calc_all_virials(Air.get(), T, s_virial_cache.B_a, s_virial_cache.dBdT_a, s_virial_cache.C_a, s_virial_cache.dCdT_a);
204 calc_all_virials(Water.get(), T, s_virial_cache.B_w, s_virial_cache.dBdT_w, s_virial_cache.C_w, s_virial_cache.dCdT_w);
205 s_virial_cache.T = T;
206}
207
208// Ideal-gas alpha0 at delta=1 (f(tau)) and dalpha0/dtau — used by enthalpy/entropy functions
209// thread_local: see s_virial_cache.
210static thread_local struct
211{
212 double T;
215} s_alpha0_cache = {-1.0, 0, 0, 0, 0};
216
226static void calc_ideal_gas_alpha0(CoolProp::HelmholtzEOSMixtureBackend* fluid, double T, double& a0, double& da0_dtau) {
227 const auto& comps = fluid->get_components();
228 if (comps.size() != 1) {
229 throw CoolProp::ValueError("calc_ideal_gas_alpha0 only supports single-component fluids");
230 }
232 double tau = red.T / T;
233 CoolProp::EquationOfState& E = fluid->get_components()[0].EOS();
234 // set_Tred is required so that GERG-2004-style sinh/cosh terms use the correct Tr.
235 E.alpha0.set_Tred(red.T);
236 CoolProp::HelmholtzDerivatives derivs = E.alpha0.all(tau, 1.0, false);
237 a0 = derivs.alphar;
238 da0_dtau = derivs.dalphar_dtau;
239}
240
241static void fill_alpha0_cache(double T) {
242 if (T == s_alpha0_cache.T) return;
244 calc_ideal_gas_alpha0(Air.get(), T, s_alpha0_cache.a0_a, s_alpha0_cache.da0_dtau_a);
245 calc_ideal_gas_alpha0(Water.get(), T, s_alpha0_cache.a0_w, s_alpha0_cache.da0_dtau_w);
246 s_alpha0_cache.T = T;
247}
248
249// Reference-state constant offsets: these are determined by a single update() at
250// Tref=473.15 K for each function and never change thereafter. Computed lazily
251// on first use so that the Air/Water singletons are already initialised.
252// thread_local: ensure_ref_offsets() calls Water->update()/Air->update(), and the
253// Water/Air backends are themselves thread_local — so each thread fills its own
254// s_ref against its own backend, with no cross-thread races.
255static thread_local struct
256{
257 bool ready;
258 double T_red_w, rho_red_w; // Water reducing temperature [K] and molar density [mol/m³]
259 double rho_red_a; // Air reducing molar density [mol/m³]
260 double hoffset_w; // Water enthalpy reference offset [J/mol]
261 double hoffset_a; // Air enthalpy reference offset [J/mol]
262 double soffset_w; // Water entropy reference offset [J/(mol·K)]
263 double soffset_a; // Air entropy reference offset [J/(mol·K)]
264 double ln_delta_air_s; // ln((1/vmolar_a0) / rho_red_a) — constant ln(delta) in Air entropy
265} s_ref = {false, 0, 0, 0, 0, 0, 0, 0, 0};
266
267static void ensure_ref_offsets() {
268 if (s_ref.ready) return;
270 const double R_bar_global = 8.314472; // used in Water enthalpy (matches global R_bar)
271 const double R_bar_ws = 8.314371; // used in Water entropy (local R_bar in original)
272 const double R_bar_air = 8.314510; // R_bar_Lemmon used in Air functions
273
274 // Fluid reducing constants (independent of update() state)
275 s_ref.T_red_w = Water->keyed_output(CoolProp::iT_reducing);
276 s_ref.rho_red_w = Water->keyed_output(CoolProp::irhomolar_reducing);
277 s_ref.rho_red_a = Air->keyed_output(CoolProp::irhomolar_reducing);
278
279 // Water enthalpy offset (R_bar_global = 8.314472, ASHRAE RP-1485 §3)
280 {
281 const double Tref = 473.15, vmolarref = 0.038837428192186184, href = 51885.582451893446;
282 Water->specify_phase(CoolProp::iphase_gas);
283 Water->update(CoolProp::DmolarT_INPUTS, 1.0 / vmolarref, Tref);
284 Water->unspecify_phase();
285 double tauref = s_ref.T_red_w / Tref;
286 double href_EOS = R_bar_global * Tref * (1.0 + tauref * Water->keyed_output(CoolProp::idalpha0_dtau_constdelta));
287 s_ref.hoffset_w = href - href_EOS;
288 }
289
290 // Water entropy offset (R_bar_ws = 8.314371, IAPWS-95)
291 {
292 const double Tref = 473.15, pref = 101325, sref = 141.18297895840303;
293 Water->specify_phase(CoolProp::iphase_gas);
294 Water->update(CoolProp::DmolarT_INPUTS, pref / (R_bar_ws * Tref), Tref);
295 Water->unspecify_phase();
296 double tauref = s_ref.T_red_w / Tref;
297 double sref_EOS = R_bar_ws * (tauref * Water->keyed_output(CoolProp::idalpha0_dtau_constdelta) - Water->keyed_output(CoolProp::ialpha0));
298 s_ref.soffset_w = sref - sref_EOS;
299 }
300
301 // Air enthalpy offset (R_bar_air = 8.314510, Lemmon 2000)
302 {
303 const double Tref = 473.15, vmolarref = 0.038837428192186184, href = 13782.240592933371;
304 Air->specify_phase(CoolProp::iphase_gas);
305 Air->update(CoolProp::DmolarT_INPUTS, 1.0 / vmolarref, Tref);
306 Air->unspecify_phase();
307 double tauref = 132.6312 / Tref;
308 double href_EOS = R_bar_air * Tref * (1.0 + tauref * Air->keyed_output(CoolProp::idalpha0_dtau_constdelta));
309 s_ref.hoffset_a = href - href_EOS;
310 }
311
312 // Air entropy offset and constant ln(delta) at the fixed reference density (1/vmolar_a0)
313 {
314 const double T0 = 273.15, p0 = 101325;
315 const double Tref = 473.15, vmolarref = 0.038837605637863169, sref = 212.22365283759311;
316 double vmolar_a_0 = R_bar_air * T0 / p0;
317 Air->specify_phase(CoolProp::iphase_gas);
318 Air->update(CoolProp::DmolarT_INPUTS, 1.0 / vmolar_a_0, Tref);
319 Air->unspecify_phase();
320 double tauref = 132.6312 / Tref;
321 double sref_EOS = R_bar_air * (tauref * Air->keyed_output(CoolProp::idalpha0_dtau_constdelta) - Air->keyed_output(CoolProp::ialpha0))
322 + R_bar_air * log(vmolarref / vmolar_a_0);
323 s_ref.soffset_a = sref - sref_EOS;
324 // ln(delta) at the actual-state density (1/vmolar_a0) used in IdealGasMolarEntropy_Air
325 s_ref.ln_delta_air_s = log(1.0 / (vmolar_a_0 * s_ref.rho_red_a));
326 }
327
328 s_ref.ready = true;
329}
330
331static double B_Air(double T) {
332 fill_virial_cache(T);
333 return s_virial_cache.B_a;
334}
335static double dBdT_Air(double T) {
336 fill_virial_cache(T);
337 return s_virial_cache.dBdT_a;
338}
339static double B_Water(double T) {
340 fill_virial_cache(T);
341 return s_virial_cache.B_w;
342}
343static double dBdT_Water(double T) {
344 fill_virial_cache(T);
345 return s_virial_cache.dBdT_w;
346}
347static double C_Air(double T) {
348 fill_virial_cache(T);
349 return s_virial_cache.C_a;
350}
351static double dCdT_Air(double T) {
352 fill_virial_cache(T);
353 return s_virial_cache.dCdT_a;
354}
355static double C_Water(double T) {
356 fill_virial_cache(T);
357 return s_virial_cache.C_w;
358}
359static double dCdT_Water(double T) {
360 fill_virial_cache(T);
361 return s_virial_cache.dCdT_w;
362}
363void UseVirialCorrelations(int flag) {
364 if (flag == 0 || flag == 1) {
365 FlagUseVirialCorrelations = flag;
366 } else {
367 std::cout << "UseVirialCorrelations takes an integer, either 0 (no) or 1 (yes)\n";
368 }
369}
371 if (flag == 0 || flag == 1) {
372 FlagUseIsothermCompressCorrelation = flag;
373 } else {
374 std::cout << "UseIsothermCompressCorrelation takes an integer, either 0 (no) or 1 (yes)\n";
375 }
376}
378 if (flag == 0 || flag == 1) {
379 FlagUseIdealGasEnthalpyCorrelations = flag;
380 } else {
381 std::cout << "UseIdealGasEnthalpyCorrelations takes an integer, either 0 (no) or 1 (yes)\n";
382 }
383}
384static double Brent_HAProps_W(givens OutputKey, double p, givens In1Name, double Input1, double TargetVal, double W_min, double W_max) {
385 // Iterating for W,
386 double W = NAN;
387 class BrentSolverResids : public CoolProp::FuncWrapper1D
388 {
389 private:
390 givens OutputKey;
391 double p;
392 givens In1Key;
393 double Input1, TargetVal;
394 std::vector<givens> input_keys;
395 std::vector<double> input_vals;
396
397 public:
398 BrentSolverResids(givens OutputKey, double p, givens In1Key, double Input1, double TargetVal)
399 : OutputKey(OutputKey), p(p), In1Key(In1Key), Input1(Input1), TargetVal(TargetVal) {
400 input_keys.resize(2);
401 input_keys[0] = In1Key;
402 input_keys[1] = GIVEN_HUMRAT;
403 input_vals.resize(2);
404 input_vals[0] = Input1;
405 };
406
407 double call(double W) override {
408 input_vals[1] = W;
409 double T = _HUGE, psi_w = _HUGE;
410 _HAPropsSI_inputs(p, input_keys, input_vals, T, psi_w);
411 if (CoolProp::get_debug_level() > 0) {
412 std::cout << format("T: %g K, psi_w %g\n", T, psi_w);
413 }
414 return _HAPropsSI_outputs(OutputKey, p, T, psi_w) - TargetVal;
415 }
416 };
417
418 BrentSolverResids BSR = BrentSolverResids(OutputKey, p, In1Name, Input1, TargetVal);
419
420 // Now we need to check the bounds and make sure that they are ok (don't yield invalid output)
421 // and actually bound the solution
422 double r_min = BSR.call(W_min);
423 bool W_min_valid = ValidNumber(r_min);
424 double r_max = BSR.call(W_max);
425 bool W_max_valid = ValidNumber(r_max);
426 if (!W_min_valid && !W_max_valid) {
427 throw CoolProp::ValueError(format("Both W_min [%g] and W_max [%g] yield invalid output values in Brent_HAProps_W", W_min, W_max).c_str());
428 } else if (W_min_valid && !W_max_valid) {
429 while (!W_max_valid) {
430 // Reduce W_max until it works
431 W_max = 0.95 * W_max + 0.05 * W_min;
432 r_max = BSR.call(W_max);
433 W_max_valid = ValidNumber(r_max);
434 }
435 } else if (!W_min_valid && W_max_valid) {
436 while (!W_min_valid) {
437 // Increase W_min until it works
438 W_min = 0.95 * W_min + 0.05 * W_max;
439 r_min = BSR.call(W_min);
440 W_min_valid = ValidNumber(r_min);
441 }
442 }
443 // We will do a secant call if the values at W_min and W_max have the same sign
444 if (r_min * r_max > 0) {
445 if (std::abs(r_min) < std::abs(r_max)) {
446 W = CoolProp::Secant(BSR, W_min, 0.01 * W_min, 1e-7, 50);
447 } else {
448 W = CoolProp::Secant(BSR, W_max, -0.01 * W_max, 1e-7, 50);
449 }
450 } else {
451 W = CoolProp::Brent(BSR, W_min, W_max, 1e-7, 1e-7, 50);
452 }
453 // Validate that the returned humidity ratio actually reproduces the requested output. As in
454 // Brent_HAProps_T, the same-sign (unbracketed) branch extrapolates with an unbounded secant
455 // that returns its last iterate even on non-convergence, silently producing a wrong W for an
456 // out-of-range target instead of signalling that the input is impossible (bd CoolProp-eaok).
457 double r_check = ValidNumber(W) ? BSR.call(W) : _HUGE;
458 if (!ValidNumber(r_check) || std::abs(r_check) > 1e-4 * std::abs(TargetVal) + 1e-6) {
460 format("Brent_HAProps_W: no humidity ratio in [%g, %g] yields the requested output [%g] (closest residual %g); the input is out of range",
461 W_min, W_max, TargetVal, r_check)
462 .c_str());
463 }
464 return W;
465}
466static double Brent_HAProps_T(givens OutputKey, double p, givens In1Name, double Input1, double TargetVal, double T_min, double T_max) {
467 double T = NAN;
468 class BrentSolverResids : public CoolProp::FuncWrapper1D
469 {
470 private:
471 givens OutputKey;
472 double p;
473 givens In1Key;
474 double Input1, TargetVal;
475 std::vector<givens> input_keys;
476 std::vector<double> input_vals;
477
478 public:
479 BrentSolverResids(givens OutputKey, double p, givens In1Key, double Input1, double TargetVal)
480 : OutputKey(OutputKey), p(p), In1Key(In1Key), Input1(Input1), TargetVal(TargetVal) {
481 input_keys.resize(2);
482 input_keys[0] = In1Key;
483 input_keys[1] = GIVEN_T;
484 input_vals.resize(2);
485 input_vals[0] = Input1;
486 };
487
488 double call(double T_drybulb) override {
489 double psi_w = NAN;
490 psi_w = MoleFractionWater(T_drybulb, p, input_keys[0], input_vals[0]);
491 double val = _HAPropsSI_outputs(OutputKey, p, T_drybulb, psi_w);
492 return val - TargetVal;
493 }
494 };
495
496 BrentSolverResids BSR = BrentSolverResids(OutputKey, p, In1Name, Input1, TargetVal);
497
498 // Now we need to check the bounds and make sure that they are ok (don't yield invalid output)
499 // and actually bound the solution
500 double r_min = BSR.call(T_min);
501 bool T_min_valid = ValidNumber(r_min);
502 double r_max = BSR.call(T_max);
503 bool T_max_valid = ValidNumber(r_max);
504 if (!T_min_valid && !T_max_valid) {
505 throw CoolProp::ValueError(format("Both T_min [%g] and T_max [%g] yield invalid output values in Brent_HAProps_T", T_min, T_max).c_str());
506 } else if (T_min_valid && !T_max_valid) {
507 while (!T_max_valid) {
508 // Reduce T_max until it works
509 T_max = 0.95 * T_max + 0.05 * T_min;
510 r_max = BSR.call(T_max);
511 T_max_valid = ValidNumber(r_max);
512 }
513 } else if (!T_min_valid && T_max_valid) {
514 while (!T_min_valid) {
515 // Increase T_min until it works
516 T_min = 0.95 * T_min + 0.05 * T_max;
517 r_min = BSR.call(T_min);
518 T_min_valid = ValidNumber(r_min);
519 }
520 }
521 // We will do a secant call if the values at T_min and T_max have the same sign
522 if (r_min * r_max > 0) {
523 if (std::abs(r_min) < std::abs(r_max)) {
524 T = CoolProp::Secant(BSR, T_min, 0.01 * T_min, 1e-7, 50);
525 } else {
526 T = CoolProp::Secant(BSR, T_max, -0.01 * T_max, 1e-7, 50);
527 }
528 } else {
529 double mach_eps = 1e-15, tol = 1e-10;
530 T = CoolProp::Brent(BSR, T_min, T_max, mach_eps, tol, 50);
531 }
532 // Validate that the returned temperature actually reproduces the requested output. When the
533 // endpoints do not bracket the root (same-sign residuals, i.e. the target is out of the
534 // achievable range), the secant above extrapolates and CoolProp::Secant returns its last
535 // iterate even when it never converged -- which silently yields a plausible but wrong T
536 // instead of signalling that the input is impossible (bd CoolProp-eaok).
537 double r_check = ValidNumber(T) ? BSR.call(T) : _HUGE;
538 if (!ValidNumber(r_check) || std::abs(r_check) > 1e-4 * std::abs(TargetVal) + 1e-6) {
540 format("Brent_HAProps_T: no temperature in [%g, %g] K yields the requested output [%g] (closest residual %g); the input is out of range",
541 T_min, T_max, TargetVal, r_check)
542 .c_str());
543 }
544 return T;
545}
546static double Secant_Tdb_at_saturated_W(double psi_w, double p, double T_guess) {
547 double T = NAN;
548 class BrentSolverResids : public CoolProp::FuncWrapper1D
549 {
550 private:
551 double pp_water, psi_w, p;
552
553 public:
554 BrentSolverResids(double psi_w, double p)
555 : pp_water(psi_w * p), psi_w(psi_w), p(p) {
556
557 };
558 ~BrentSolverResids() = default;
559
560 double call(double T) override {
561 double p_ws = NAN;
562 if (T >= 273.16) {
563 // Saturation pressure [Pa] using IF97 formulation
564 p_ws = IF97::psat97(T);
565 } else {
566 // Sublimation pressure [Pa]
567 p_ws = psub_Ice(T);
568 }
569 double f = f_factor(T, p);
570 double pp_water_calc = f * p_ws;
571 double psi_w_calc = pp_water_calc / p;
572 return (psi_w_calc - psi_w) / psi_w;
573 }
574 };
575
576 BrentSolverResids Resids(psi_w, p);
577
578 try {
579 T = CoolProp::Secant(Resids, T_guess, 0.1, 1e-7, 100);
580 if (!ValidNumber(T)) {
581 throw CoolProp::ValueError("Intermediate value for Tdb is invalid");
582 }
583 } catch (std::exception& /* e */) {
584 T = CoolProp::Brent(Resids, 100, 640, 1e-15, 1e-10, 100);
585 }
586
587 return T;
588}
589
590//static double Brent_Tdb_at_saturated_W(double psi_w, double p, double T_min, double T_max)
591//{
592// double T;
593// class BrentSolverResids : public CoolProp::FuncWrapper1D
594// {
595// private:
596// double pp_water, psi_w, p;
597// public:
598// BrentSolverResids(double psi_w, double p) : psi_w(psi_w), p(p) { pp_water = psi_w*p; };
599// ~BrentSolverResids(){};
600//
601// double call(double T){
602// double p_ws;
603// if (T>=273.16){
604// // Saturation pressure [Pa] using IF97 formulation
605// p_ws= IF97::psat97(T);
606// }
607// else{
608// // Sublimation pressure [Pa]
609// p_ws=psub_Ice(T);
610// }
611// double f = f_factor(T, p);
612// double pp_water_calc = f*p_ws;
613// double psi_w_calc = pp_water_calc/p;
614// return (psi_w_calc - psi_w)/psi_w;
615// }
616// };
617//
618// BrentSolverResids Resids(psi_w, p);
619//
620// T = CoolProp::Brent(Resids, 150, 350, 1e-16, 1e-7, 100);
621//
622// return T;
623//}
624
625/*
626static double Secant_HAProps_T(const std::string &OutputName, const std::string &Input1Name, double Input1, const std::string &Input2Name, double Input2, double TargetVal, double T_guess)
627{
628 // Use a secant solve in order to yield a target output value for HAProps by altering T
629 double x1=0,x2=0,x3=0,y1=0,y2=0,eps=5e-7,f=999,T=300,change;
630 int iter=1;
631 std::string sT = "T";
632
633 while ((iter<=3 || (std::abs(f)>eps && std::abs(change)>1e-10)) && iter<100)
634 {
635 if (iter==1){x1=T_guess; T=x1;}
636 if (iter==2){x2=T_guess+0.001; T=x2;}
637 if (iter>2) {T=x2;}
638 f=HAPropsSI(OutputName,sT,T,Input1Name,Input1,Input2Name,Input2)-TargetVal;
639 if (iter==1){y1=f;}
640 if (iter>1)
641 {
642 y2=f;
643 x3=x2-y2/(y2-y1)*(x2-x1);
644 change = y2/(y2-y1)*(x2-x1);
645 y1=y2; x1=x2; x2=x3;
646 }
647 iter=iter+1;
648 }
649 return T;
650}
651*/
652
653// Mixed virial components
654static double _B_aw(double T) {
656 // Returns value in m^3/mol
657 double a[] = {0, 0.665687e2, -0.238834e3, -0.176755e3};
658 double b[] = {0, -0.237, -1.048, -3.183};
659 double rhobarstar = 1000, Tstar = 100;
660 return 1 / rhobarstar * (a[1] * pow(T / Tstar, b[1]) + a[2] * pow(T / Tstar, b[2]) + a[3] * pow(T / Tstar, b[3]))
661 / 1000; // Correlation has units of dm^3/mol, to convert to m^3/mol, divide by 1000
662}
663
664static double _dB_aw_dT(double T) {
666 // Returns value in m^3/mol
667 double a[] = {0, 0.665687e2, -0.238834e3, -0.176755e3};
668 double b[] = {0, -0.237, -1.048, -3.183};
669 double rhobarstar = 1000, Tstar = 100;
670 return 1 / rhobarstar / Tstar
671 * (a[1] * b[1] * pow(T / Tstar, b[1] - 1) + a[2] * b[2] * pow(T / Tstar, b[2] - 1) + a[3] * b[3] * pow(T / Tstar, b[3] - 1))
672 / 1000; // Correlation has units of dm^3/mol/K, to convert to m^3/mol/K, divide by 1000
673}
674
675static double _C_aaw(double T) {
677 // Function return has units of m^6/mol^2
678 double c[] = {0, 0.482737e3, 0.105678e6, -0.656394e8, 0.294442e11, -0.319317e13};
679 double rhobarstar = 1000, Tstar = 1, summer = 0;
680 int i = 0;
681 for (i = 1; i <= 5; i++) {
682 summer += c[i] * pow(T / Tstar, 1 - i);
683 }
684 return 1.0 / rhobarstar / rhobarstar * summer / 1e6; // Correlation has units of dm^6/mol^2, to convert to m^6/mol^2 divide by 1e6
685}
686
687static double _dC_aaw_dT(double T) {
689 // Function return in units of m^6/mol^2/K
690 double c[] = {0, 0.482737e3, 0.105678e6, -0.656394e8, 0.294442e11, -0.319317e13};
691 double rhobarstar = 1000, Tstar = 1, summer = 0;
692 int i = 0;
693 for (i = 2; i <= 5; i++) {
694 summer += c[i] * (1 - i) * pow(T / Tstar, -i);
695 }
696 return 1.0 / rhobarstar / rhobarstar / Tstar * summer / 1e6; // Correlation has units of dm^6/mol^2/K, to convert to m^6/mol^2/K divide by 1e6
697}
698
699static double _C_aww(double T) {
701 // Function return has units of m^6/mol^2
702 double d[] = {0, -0.1072887e2, 0.347804e4, -0.383383e6, 0.334060e8};
703 double rhobarstar = 1, Tstar = 1, summer = 0;
704 int i = 0;
705 for (i = 1; i <= 4; i++) {
706 summer += d[i] * pow(T / Tstar, 1 - i);
707 }
708 return -1.0 / rhobarstar / rhobarstar * exp(summer) / 1e6; // Correlation has units of dm^6/mol^2, to convert to m^6/mol^2 divide by 1e6
709}
710
711static double _dC_aww_dT(double T) {
713 // Function return in units of m^6/mol^2/K
714 double d[] = {0, -0.1072887e2, 0.347804e4, -0.383383e6, 0.334060e8};
715 double rhobarstar = 1, Tstar = 1, summer1 = 0, summer2 = 0;
716 int i = 0;
717 for (i = 1; i <= 4; i++) {
718 summer1 += d[i] * pow(T / Tstar, 1 - i);
719 }
720 for (i = 2; i <= 4; i++) {
721 summer2 += d[i] * (1 - i) * pow(T / Tstar, -i);
722 }
723 return -1.0 / rhobarstar / rhobarstar / Tstar * exp(summer1) * summer2
724 / 1e6; // Correlation has units of dm^6/mol^2/K, to convert to m^6/mol^2/K divide by 1e6
725}
726
727static double B_m(double T, double psi_w) {
728 // Bm has units of m^3/mol
729 double B_aa = NAN, B_ww = NAN, B_aw = NAN;
730 if (FlagUseVirialCorrelations == 1) {
731 B_aa = -0.000721183853646 + 1.142682674467e-05 * T - 8.838228412173e-08 * pow(T, 2) + 4.104150642775e-10 * pow(T, 3)
732 - 1.192780880645e-12 * pow(T, 4) + 2.134201312070e-15 * pow(T, 5) - 2.157430412913e-18 * pow(T, 6) + 9.453830907795e-22 * pow(T, 7);
733 B_ww = -10.8963128394 + 2.439761625859e-01 * T - 2.353884845100e-03 * pow(T, 2) + 1.265864734412e-05 * pow(T, 3)
734 - 4.092175700300e-08 * pow(T, 4) + 7.943925411344e-11 * pow(T, 5) - 8.567808759123e-14 * pow(T, 6) + 3.958203548563e-17 * pow(T, 7);
735 } else {
736 B_aa = B_Air(T); // [m^3/mol]
737 B_ww = B_Water(T); // [m^3/mol]
738 }
739
740 B_aw = _B_aw(T); // [m^3/mol]
741 return pow(1 - psi_w, 2) * B_aa + 2 * (1 - psi_w) * psi_w * B_aw + psi_w * psi_w * B_ww;
742}
743
744static double dB_m_dT(double T, double psi_w) {
745 //dBm_dT has units of m^3/mol/K
746 double dB_dT_aa = NAN, dB_dT_ww = NAN, dB_dT_aw = NAN;
747 if (FlagUseVirialCorrelations) {
748 dB_dT_aa = 1.65159324353e-05 - 3.026130954749e-07 * T + 2.558323847166e-09 * pow(T, 2) - 1.250695660784e-11 * pow(T, 3)
749 + 3.759401946106e-14 * pow(T, 4) - 6.889086380822e-17 * pow(T, 5) + 7.089457032972e-20 * pow(T, 6)
750 - 3.149942145971e-23 * pow(T, 7);
751 dB_dT_ww = 0.65615868848 - 1.487953162679e-02 * T + 1.450134660689e-04 * pow(T, 2) - 7.863187630094e-07 * pow(T, 3)
752 + 2.559556607010e-09 * pow(T, 4) - 4.997942221914e-12 * pow(T, 5) + 5.417678681513e-15 * pow(T, 6)
753 - 2.513856275241e-18 * pow(T, 7);
754 } else {
755 dB_dT_aa = dBdT_Air(T); // [m^3/mol]
756 dB_dT_ww = dBdT_Water(T); // [m^3/mol]
757 }
758 dB_dT_aw = _dB_aw_dT(T); // [m^3/mol]
759 return pow(1 - psi_w, 2) * dB_dT_aa + 2 * (1 - psi_w) * psi_w * dB_dT_aw + psi_w * psi_w * dB_dT_ww;
760}
761
762static double C_m(double T, double psi_w) {
763 // Cm has units of m^6/mol^2
764 double C_aaa = NAN, C_www = NAN, C_aww = NAN, C_aaw = NAN;
765 if (FlagUseVirialCorrelations) {
766 C_aaa = 1.29192158975e-08 - 1.776054020409e-10 * T + 1.359641176409e-12 * pow(T, 2) - 6.234878717893e-15 * pow(T, 3)
767 + 1.791668730770e-17 * pow(T, 4) - 3.175283581294e-20 * pow(T, 5) + 3.184306136120e-23 * pow(T, 6) - 1.386043640106e-26 * pow(T, 7);
768 C_www = -0.580595811134 + 1.365952762696e-02 * T - 1.375986293288e-04 * pow(T, 2) + 7.687692259692e-07 * pow(T, 3)
769 - 2.571440816920e-09 * pow(T, 4) + 5.147432221082e-12 * pow(T, 5) - 5.708156494894e-15 * pow(T, 6) + 2.704605721778e-18 * pow(T, 7);
770 } else {
771 C_aaa = C_Air(T); //[m^6/mol^2]
772 C_www = C_Water(T); //[m^6/mol^2]
773 }
774 C_aaw = _C_aaw(T); //[m^6/mol^2]
775 C_aww = _C_aww(T); //[m^6/mol^2]
776 return pow(1 - psi_w, 3) * C_aaa + 3 * pow(1 - psi_w, 2) * psi_w * C_aaw + 3 * (1 - psi_w) * psi_w * psi_w * C_aww + pow(psi_w, 3) * C_www;
777}
778
779static double dC_m_dT(double T, double psi_w) {
780 // dCm_dT has units of m^6/mol^2/K
781
782 double dC_dT_aaa = NAN, dC_dT_www = NAN, dC_dT_aww = NAN, dC_dT_aaw = NAN;
783 // NDG for fluid EOS for virial terms
784 if (FlagUseVirialCorrelations) {
785 dC_dT_aaa = -2.46582342273e-10 + 4.425401935447e-12 * T - 3.669987371644e-14 * pow(T, 2) + 1.765891183964e-16 * pow(T, 3)
786 - 5.240097805744e-19 * pow(T, 4) + 9.502177003614e-22 * pow(T, 5) - 9.694252610339e-25 * pow(T, 6)
787 + 4.276261986741e-28 * pow(T, 7);
788 dC_dT_www = 0.0984601196142 - 2.356713397262e-03 * T + 2.409113323685e-05 * pow(T, 2) - 1.363083778715e-07 * pow(T, 3)
789 + 4.609623799524e-10 * pow(T, 4) - 9.316416405390e-13 * pow(T, 5) + 1.041909136255e-15 * pow(T, 6)
790 - 4.973918480607e-19 * pow(T, 7);
791 } else {
792 dC_dT_aaa = dCdT_Air(T); // [m^6/mol^2]
793 dC_dT_www = dCdT_Water(T); // [m^6/mol^2]
794 }
795 dC_dT_aaw = _dC_aaw_dT(T); // [m^6/mol^2]
796 dC_dT_aww = _dC_aww_dT(T); // [m^6/mol^2]
797 return pow(1 - psi_w, 3) * dC_dT_aaa + 3 * pow(1 - psi_w, 2) * psi_w * dC_dT_aaw + 3 * (1 - psi_w) * psi_w * psi_w * dC_dT_aww
798 + pow(psi_w, 3) * dC_dT_www;
799}
800double HumidityRatio(double psi_w) {
801 return psi_w * epsilon / (1 - psi_w);
802}
803
804static double HenryConstant(double T) {
805 // Result has units of 1/Pa
806 double p_ws = NAN, beta_N2 = NAN, beta_O2 = NAN, beta_Ar = NAN, beta_a = NAN, tau = NAN, Tr = NAN, Tc = 647.096;
807 Tr = T / Tc;
808 tau = 1 - Tr;
809 p_ws = IF97::psat97(T); //[Pa]
810 beta_N2 = p_ws * exp(-9.67578 / Tr + 4.72162 * pow(tau, 0.355) / Tr + 11.70585 * pow(Tr, -0.41) * exp(tau));
811 beta_O2 = p_ws * exp(-9.44833 / Tr + 4.43822 * pow(tau, 0.355) / Tr + 11.42005 * pow(Tr, -0.41) * exp(tau));
812 beta_Ar = p_ws * exp(-8.40954 / Tr + 4.29587 * pow(tau, 0.355) / Tr + 10.52779 * pow(Tr, -0.41) * exp(tau));
813 beta_a = 1 / (0.7812 / beta_N2 + 0.2095 / beta_O2 + 0.0093 / beta_Ar);
814 return 1 / (1.01325 * beta_a);
815}
816double isothermal_compressibility(double T, double p) {
817 double k_T = NAN;
818
819 if (T > 273.16) {
820 if (FlagUseIsothermCompressCorrelation) {
821 k_T = 1.6261876614E-22 * pow(T, 6) - 3.3016385196E-19 * pow(T, 5) + 2.7978984577E-16 * pow(T, 4) - 1.2672392901E-13 * pow(T, 3)
822 + 3.2382864853E-11 * pow(T, 2) - 4.4318979503E-09 * T + 2.5455947289E-07;
823 } else {
824 // IF97Backend::set_phase rejects PT_INPUTS whenever |p - psat(T)|
825 // is within IF97's 3.3e-5 RMS saturation-line tolerance — the
826 // pure-water phase is ambiguous there. Pre-check that same
827 // condition and route the near-saturation case through the
828 // polynomial correlation instead; for f_factor purposes the
829 // precise k_T at saturation does not matter much, and a
830 // single-valued surrogate keeps the outer (T_wb,RelHum) solver
831 // from blowing up when its T_db iterate brushes Tsat(p). (#2690)
832 constexpr double if97_sat_tol = 3.3e-5;
833 const double p_ws_T = IF97::psat97(T);
834 if (std::abs(p - p_ws_T) <= p_ws_T * if97_sat_tol) {
835 k_T = 1.6261876614E-22 * pow(T, 6) - 3.3016385196E-19 * pow(T, 5) + 2.7978984577E-16 * pow(T, 4) - 1.2672392901E-13 * pow(T, 3)
836 + 3.2382864853E-11 * pow(T, 2) - 4.4318979503E-09 * T + 2.5455947289E-07;
837 } else {
838 WaterIF97->update(CoolProp::PT_INPUTS, p, T);
839 Water->update(CoolProp::DmassT_INPUTS, WaterIF97->rhomass(), T);
840 k_T = Water->keyed_output(CoolProp::iisothermal_compressibility);
841 }
842 }
843 } else {
844 k_T = IsothermCompress_Ice(T, p); //[1/Pa]
845 }
846 return k_T;
847}
848double f_factor(double T, double p) {
849 double f = 0, Rbar = 8.314371, eps = 1e-8;
850 double x1 = 0, x2 = 0, x3 = NAN, y1 = 0, y2 = NAN, change = _HUGE;
851 int iter = 1;
852 double p_ws = NAN, B_aa = NAN, B_aw = NAN, B_ww = NAN, C_aaa = NAN, C_aaw = NAN, C_aww = NAN, C_www = NAN, line1 = NAN, line2 = NAN, line3 = NAN,
853 line4 = NAN, line5 = NAN, line6 = NAN, line7 = NAN, line8 = NAN, k_T = NAN, beta_H = NAN, LHS = NAN, RHS = NAN, psi_ws = NAN,
854 vbar_ws = NAN;
855
856 // Saturation pressure [Pa]
857 if (T > 273.16) {
858 // It is liquid water
859 Water->update(CoolProp::QT_INPUTS, 0, T);
860 p_ws = Water->p();
861 vbar_ws = 1.0 / Water->keyed_output(CoolProp::iDmolar); //[m^3/mol]
862 beta_H = HenryConstant(T); //[1/Pa]
863 } else {
864 // It is ice
865 p_ws = psub_Ice(T); // [Pa]
866 beta_H = 0;
867 vbar_ws = dg_dp_Ice(T, p) * MM_Water(); //[m^3/mol]
868 }
869
870 k_T = isothermal_compressibility(T, p); //[1/Pa]
871
872 // Hermann: In the iteration process of the enhancement factor in Eq. (3.25), k_T is set to zero for pw,s (T) > p.
873 if (p_ws > p) {
874 k_T = 0;
875 beta_H = 0;
876 }
877
878 // NDG for fluid EOS for virial terms
879 if (FlagUseVirialCorrelations) {
880 B_aa = -0.000721183853646 + 1.142682674467e-05 * T - 8.838228412173e-08 * pow(T, 2) + 4.104150642775e-10 * pow(T, 3)
881 - 1.192780880645e-12 * pow(T, 4) + 2.134201312070e-15 * pow(T, 5) - 2.157430412913e-18 * pow(T, 6) + 9.453830907795e-22 * pow(T, 7);
882 B_ww = -10.8963128394 + 2.439761625859e-01 * T - 2.353884845100e-03 * pow(T, 2) + 1.265864734412e-05 * pow(T, 3)
883 - 4.092175700300e-08 * pow(T, 4) + 7.943925411344e-11 * pow(T, 5) - 8.567808759123e-14 * pow(T, 6) + 3.958203548563e-17 * pow(T, 7);
884 C_aaa = 1.29192158975e-08 - 1.776054020409e-10 * T + 1.359641176409e-12 * pow(T, 2) - 6.234878717893e-15 * pow(T, 3)
885 + 1.791668730770e-17 * pow(T, 4) - 3.175283581294e-20 * pow(T, 5) + 3.184306136120e-23 * pow(T, 6) - 1.386043640106e-26 * pow(T, 7);
886 C_www = -0.580595811134 + 1.365952762696e-02 * T - 1.375986293288e-04 * pow(T, 2) + 7.687692259692e-07 * pow(T, 3)
887 - 2.571440816920e-09 * pow(T, 4) + 5.147432221082e-12 * pow(T, 5) - 5.708156494894e-15 * pow(T, 6) + 2.704605721778e-18 * pow(T, 7);
888 } else {
889 B_aa = B_Air(T); // [m^3/mol]
890 C_aaa = C_Air(T); // [m^6/mol^2]
891 B_ww = B_Water(T); // [m^3/mol]
892 C_www = C_Water(T); // [m^6/mol^2]
893 }
894 B_aw = _B_aw(T); //[m^3/mol]
895 C_aaw = _C_aaw(T); //[m^6/mol^2]
896 C_aww = _C_aww(T); //[m^6/mol^2]
897
898 // Use a little secant loop to find f iteratively
899 // Start out with a guess value of 1 for f
900 while ((iter <= 3 || change > eps) && iter < 100) {
901 if (iter == 1) {
902 x1 = 1.00;
903 f = x1;
904 }
905 if (iter == 2) {
906 x2 = 1.00 + 0.000001;
907 f = x2;
908 }
909 if (iter > 2) {
910 f = x2;
911 }
912
913 // Left-hand-side of Equation 3.25
914 LHS = log(f);
915 // Eqn 3.24
916 psi_ws = f * p_ws / p;
917
918 // All the terms forming the RHS of Eqn 3.25
919 line1 = ((1 + k_T * p_ws) * (p - p_ws) - k_T * (p * p - p_ws * p_ws) / 2.0) / (Rbar * T) * vbar_ws + log(1 - beta_H * (1 - psi_ws) * p);
920 line2 = pow(1 - psi_ws, 2) * p / (Rbar * T) * B_aa - 2 * pow(1 - psi_ws, 2) * p / (Rbar * T) * B_aw
921 - (p - p_ws - pow(1 - psi_ws, 2) * p) / (Rbar * T) * B_ww;
922 line3 = pow(1 - psi_ws, 3) * p * p / pow(Rbar * T, 2) * C_aaa
923 + (3 * pow(1 - psi_ws, 2) * (1 - 2 * (1 - psi_ws)) * p * p) / (2 * pow(Rbar * T, 2)) * C_aaw;
924 line4 = -3 * pow(1 - psi_ws, 2) * psi_ws * p * p / pow(Rbar * T, 2) * C_aww
925 - ((3 - 2 * psi_ws) * psi_ws * psi_ws * p * p - p_ws * p_ws) / (2 * pow(Rbar * T, 2)) * C_www;
926 line5 = -(pow(1 - psi_ws, 2) * (-2 + 3 * psi_ws) * psi_ws * p * p) / pow(Rbar * T, 2) * B_aa * B_ww;
927 line6 = -(2 * pow(1 - psi_ws, 3) * (-1 + 3 * psi_ws) * p * p) / pow(Rbar * T, 2) * B_aa * B_aw;
928 line7 = (6 * pow(1 - psi_ws, 2) * psi_ws * psi_ws * p * p) / pow(Rbar * T, 2) * B_ww * B_aw
929 - (3 * pow(1 - psi_ws, 4) * p * p) / (2 * pow(Rbar * T, 2)) * B_aa * B_aa;
930 line8 = -(2 * pow(1 - psi_ws, 2) * psi_ws * (-2 + 3 * psi_ws) * p * p) / pow(Rbar * T, 2) * B_aw * B_aw
931 - (p_ws * p_ws - (4 - 3 * psi_ws) * pow(psi_ws, 3) * p * p) / (2 * pow(Rbar * T, 2)) * B_ww * B_ww;
932 RHS = line1 + line2 + line3 + line4 + line5 + line6 + line7 + line8;
933
934 if (iter == 1) {
935 y1 = LHS - RHS;
936 }
937 if (iter > 1) {
938 y2 = LHS - RHS;
939 x3 = x2 - y2 / (y2 - y1) * (x2 - x1);
940 change = std::abs(y2 / (y2 - y1) * (x2 - x1));
941 y1 = y2;
942 x1 = x2;
943 x2 = x3;
944 }
945 iter = iter + 1;
946 }
947 if (f >= 1.0)
948 return f;
949 else
950 return 1.0;
951}
952void HAHelp() {
953 std::cout << "Sorry, Need to update!";
954}
955int returnHumAirCode(const char* Code) {
956 if (!strcmp(Code, "GIVEN_TDP"))
957 return GIVEN_TDP;
958 else if (!strcmp(Code, "GIVEN_HUMRAT"))
959 return GIVEN_HUMRAT;
960 else if (!strcmp(Code, "GIVEN_TWB"))
961 return GIVEN_TWB;
962 else if (!strcmp(Code, "GIVEN_RH"))
963 return GIVEN_RH;
964 else if (!strcmp(Code, "GIVEN_ENTHALPY"))
965 return GIVEN_ENTHALPY;
966 else {
967 std::cerr << format("Code to returnHumAirCode in HumAir.c [%s] not understood", Code);
968 return -1;
969 }
970}
971double Viscosity(double T, double p, double psi_w) {
972 /*
973 Using the method of:
974
975 P.T. Tsilingiris, 2009, Thermophysical and transport properties of humid air at temperature range between 0 and 100 oC, Energy Conversion and Management, 49, 1098-1010
976
977 but using the detailed measurements for pure fluid from IAPWS formulations
978 */
979 double mu_a = NAN, mu_w = NAN, Phi_av = NAN, Phi_va = NAN, Ma = NAN, Mw = NAN;
980 Mw = MM_Water();
981 Ma = MM_Air();
982 // Viscosity of dry air at dry-bulb temp and total pressure
983 Air->update(CoolProp::PT_INPUTS, p, T);
984 mu_a = Air->keyed_output(CoolProp::iviscosity);
985 // Saturated water vapor of pure water at total pressure
986 Water->update(CoolProp::PQ_INPUTS, p, 1);
987 mu_w = Water->keyed_output(CoolProp::iviscosity);
988 Phi_av = sqrt(2.0) / 4.0 * pow(1 + Ma / Mw, -0.5) * pow(1 + sqrt(mu_a / mu_w) * pow(Mw / Ma, 0.25), 2); //[-]
989 Phi_va = sqrt(2.0) / 4.0 * pow(1 + Mw / Ma, -0.5) * pow(1 + sqrt(mu_w / mu_a) * pow(Ma / Mw, 0.25), 2); //[-]
990 return (1 - psi_w) * mu_a / ((1 - psi_w) + psi_w * Phi_av) + psi_w * mu_w / (psi_w + (1 - psi_w) * Phi_va);
991}
992double Conductivity(double T, double p, double psi_w) {
993 /*
994 Using the method of:
995
996 P.T. Tsilingiris, 2009, Thermophysical and transport properties of humid air at temperature range between 0 and 100 oC, Energy Conversion and Management, 49, 1098-1010
997
998 but using the detailed measurements for pure fluid from IAPWS formulations
999 */
1000 double mu_a = NAN, mu_w = NAN, k_a = NAN, k_w = NAN, Phi_av = NAN, Phi_va = NAN, Ma = NAN, Mw = NAN;
1001 Mw = MM_Water();
1002 Ma = MM_Air();
1003
1004 // Viscosity of dry air at dry-bulb temp and total pressure
1005 Air->update(CoolProp::PT_INPUTS, p, T);
1006 mu_a = Air->keyed_output(CoolProp::iviscosity);
1007 k_a = Air->keyed_output(CoolProp::iconductivity);
1008 // Conductivity of saturated pure water at total pressure
1009 Water->update(CoolProp::PQ_INPUTS, p, 1);
1010 mu_w = Water->keyed_output(CoolProp::iviscosity);
1011 k_w = Water->keyed_output(CoolProp::iconductivity);
1012 Phi_av = sqrt(2.0) / 4.0 * pow(1 + Ma / Mw, -0.5) * pow(1 + sqrt(mu_a / mu_w) * pow(Mw / Ma, 0.25), 2); //[-]
1013 Phi_va = sqrt(2.0) / 4.0 * pow(1 + Mw / Ma, -0.5) * pow(1 + sqrt(mu_w / mu_a) * pow(Ma / Mw, 0.25), 2); //[-]
1014 return (1 - psi_w) * k_a / ((1 - psi_w) + psi_w * Phi_av) + psi_w * k_w / (psi_w + (1 - psi_w) * Phi_va);
1015}
1022double MolarVolume(double T, double p, double psi_w) {
1023 // Output in m^3/mol_ha
1024 int iter = 0;
1025 double v_bar0 = NAN, v_bar = 0, R_bar = 8.314472, x1 = 0, x2 = 0, x3 = NAN, y1 = 0, y2 = NAN, resid = NAN, eps = NAN, Bm = NAN, Cm = NAN;
1026
1027 // -----------------------------
1028 // Iteratively find molar volume
1029 // -----------------------------
1030
1031 // Start by assuming it is an ideal gas to get initial guess
1032 v_bar0 = R_bar * T / p; // [m^3/mol_ha]
1033
1034 // Bring outside the loop since not a function of v_bar
1035 Bm = B_m(T, psi_w);
1036 Cm = C_m(T, psi_w);
1037
1038 iter = 1;
1039 eps = 1e-11;
1040 resid = 999;
1041 while ((iter <= 3 || std::abs(resid) > eps) && iter < 100) {
1042 if (iter == 1) {
1043 x1 = v_bar0;
1044 v_bar = x1;
1045 }
1046 if (iter == 2) {
1047 x2 = v_bar0 + 0.000001;
1048 v_bar = x2;
1049 }
1050 if (iter > 2) {
1051 v_bar = x2;
1052 }
1053
1054 // want v_bar in m^3/mol_ha and R_bar in J/mol_ha-K
1055 resid = (p - (R_bar)*T / v_bar * (1 + Bm / v_bar + Cm / (v_bar * v_bar))) / p;
1056
1057 if (iter == 1) {
1058 y1 = resid;
1059 }
1060 if (iter > 1) {
1061 y2 = resid;
1062 x3 = x2 - y2 / (y2 - y1) * (x2 - x1);
1063 y1 = y2;
1064 x1 = x2;
1065 x2 = x3;
1066 }
1067 iter = iter + 1;
1068 }
1069 return v_bar; // [J/mol_ha]
1070}
1071double Pressure(double T, double v_bar, double psi_w) {
1072 double R_bar = 8.314472;
1073 double Bm = B_m(T, psi_w);
1074 double Cm = C_m(T, psi_w);
1075 return (R_bar)*T / v_bar * (1 + Bm / v_bar + Cm / (v_bar * v_bar));
1076}
1077double IdealGasMolarEnthalpy_Water(double T, double p) {
1078 // All quantities from the T-keyed alpha0 cache (fill_alpha0_cache) and the
1079 // once-only reference-state offset cache (ensure_ref_offsets). No update() calls.
1080 fill_alpha0_cache(T);
1081 ensure_ref_offsets();
1082 double tau = s_ref.T_red_w / T;
1083 return -0.01102303806 + s_ref.hoffset_w + R_bar * T * (1.0 + tau * s_alpha0_cache.da0_dtau_w);
1084}
1085double IdealGasMolarEntropy_Water(double T, double p) {
1086 // Serious typo in RP-1485 - should use total pressure rather than
1087 // reference pressure in density calculation for water vapor molar entropy
1088 const double R_bar_ws = 8.314371; //[J/mol/K] — matches original local R_bar
1089 fill_alpha0_cache(T);
1090 ensure_ref_offsets();
1091 double tau = s_ref.T_red_w / T;
1092 // alpha0(T,p) = f(tau) + ln(delta) = a0_w + ln(rho/rho_red) where rho = p/(R_bar_ws*T)
1093 double ln_delta = log(p / (R_bar_ws * T * s_ref.rho_red_w));
1094 return s_ref.soffset_w + R_bar_ws * (tau * s_alpha0_cache.da0_dtau_w - s_alpha0_cache.a0_w - ln_delta);
1095}
1096double IdealGasMolarEnthalpy_Air(double T, double p) {
1097 const double R_bar_Lemmon = 8.314510; //[J/mol/K]
1098 fill_alpha0_cache(T);
1099 ensure_ref_offsets();
1100 double tau = 132.6312 / T; // Tj is given by 132.6312 K
1101 return -7914.149298 + s_ref.hoffset_a + R_bar_Lemmon * T * (1.0 + tau * s_alpha0_cache.da0_dtau_a);
1102}
1103double IdealGasMolarEntropy_Air(double T, double vmolar_a) {
1104 const double R_bar_Lemmon = 8.314510, T0 = 273.15, p0 = 101325; //[J/mol/K]
1105 const double vmolar_a_0 = R_bar_Lemmon * T0 / p0; //[m^3/mol]
1106 fill_alpha0_cache(T);
1107 ensure_ref_offsets();
1108 double tau = 132.6312 / T; // Tj and rhoj are given by 132.6312 and 302.5507652 respectively
1109 // alpha0 at fixed density (1/vmolar_a_0): a0_a + ln_delta_air_s where ln_delta_air_s = ln(delta_const)
1110 return -196.1375815 + s_ref.soffset_a + R_bar_Lemmon * (tau * s_alpha0_cache.da0_dtau_a - s_alpha0_cache.a0_a - s_ref.ln_delta_air_s)
1111 + R_bar_Lemmon * log(vmolar_a / vmolar_a_0);
1112}
1113
1121double MolarEnthalpy(double T, double p, double psi_w, double vmolar) {
1122 // In units of kJ/kmol
1123
1124 // vbar (molar volume) in m^3/kg
1125
1126 double hbar_0 = NAN, hbar_a = NAN, hbar_w = NAN, hbar = NAN, R_bar = 8.314472;
1127 // ----------------------------------------
1128 // Enthalpy
1129 // ----------------------------------------
1130 // Constant for enthalpy
1131 // Not clear why getting rid of this term yields the correct values in the table, but enthalpies are equal to an additive constant, so not a big deal
1132 hbar_0 = 0.0; //2.924425468; //[kJ/kmol]
1133
1134 if (FlagUseIdealGasEnthalpyCorrelations) {
1135 hbar_w = 2.7030251618E-03 * T * T + 3.1994361015E+01 * T + 3.6123174929E+04;
1136 hbar_a = 9.2486716590E-04 * T * T + 2.8557221776E+01 * T - 7.8616129429E+03;
1137 } else {
1138 hbar_w = IdealGasMolarEnthalpy_Water(T, p); // [J/mol[water]]
1139 hbar_a = IdealGasMolarEnthalpy_Air(T, p); // [J/mol[dry air]]
1140 }
1141
1142 // If the user changes the reference state for water or Air, we need to ensure that the values returned from this
1143 // function are always the same as the formulation expects. Therefore we can use a state point for which we know what the
1144 // enthalpy should be and then correct the calculated values for the enthalpy.
1145
1146 hbar = hbar_0 + (1 - psi_w) * hbar_a + psi_w * hbar_w
1147 + R_bar * T * ((B_m(T, psi_w) - T * dB_m_dT(T, psi_w)) / vmolar + (C_m(T, psi_w) - T / 2.0 * dC_m_dT(T, psi_w)) / (vmolar * vmolar));
1148 return hbar; //[J/mol_ha]
1149}
1150double MolarInternalEnergy(double T, double p, double psi_w, double vmolar) {
1151 return MolarEnthalpy(T, p, psi_w, vmolar) - p * vmolar;
1152}
1153double MassEnthalpy_per_kgha(double T, double p, double psi_w) {
1154 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1155 double h_bar = MolarEnthalpy(T, p, psi_w, vmolar); //[J/mol_ha]
1156 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1157 return h_bar / M_ha; //[J/kg_ha]
1158}
1159double MassEnthalpy_per_kgda(double T, double p, double psi_w) {
1160 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1161 double h_bar = MolarEnthalpy(T, p, psi_w, vmolar); //[J/mol_ha]
1162 double W = HumidityRatio(psi_w); //[kg_w/kg_da] // (1+W) is kg_ha/kg_da
1163 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1164 return h_bar * (1 + W) / M_ha; //[J/kg_da]
1165}
1166double MassInternalEnergy_per_kgha(double T, double p, double psi_w) {
1167 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1168 double h_bar = MolarInternalEnergy(T, p, psi_w, vmolar); //[J/mol_ha]
1169 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1170 return h_bar / M_ha; //[J/kg_ha]
1171}
1172double MassInternalEnergy_per_kgda(double T, double p, double psi_w) {
1173 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1174 double h_bar = MolarInternalEnergy(T, p, psi_w, vmolar); //[J/mol_da]
1175 double W = HumidityRatio(psi_w); //[kg_w/kg_da] // (1+W) is kg_ha/kg_da
1176 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1177 return h_bar * (1 + W) / M_ha; //[J/kg_da]
1178}
1179
1187double MolarEntropy(double T, double p, double psi_w, double v_bar) {
1188
1189 // vbar (molar volume) in m^3/mol
1190 double x1 = 0, x2 = 0, x3 = 0, y1 = 0, y2 = 0, eps = 1e-8, f = 999, R_bar_Lem = 8.314510;
1191 int iter = 1;
1192 double sbar_0 = NAN, sbar_a = 0, sbar_w = 0, sbar = NAN, R_bar = 8.314472, vbar_a_guess = NAN, Baa = NAN, Caaa = NAN, vbar_a = 0;
1193 double B = NAN, dBdT = NAN, C = NAN, dCdT = NAN;
1194 // Constant for entropy
1195 sbar_0 = 0.02366427495; //[J/mol/K]
1196
1197 // Calculate vbar_a, the molar volume of dry air
1198 // B_m, C_m, etc. functions take care of the units
1199 Baa = B_m(T, 0);
1200 B = B_m(T, psi_w);
1201 dBdT = dB_m_dT(T, psi_w);
1202 Caaa = C_m(T, 0);
1203 C = C_m(T, psi_w);
1204 dCdT = dC_m_dT(T, psi_w);
1205
1206 vbar_a_guess = R_bar_Lem * T / p; //[m^3/mol] since p in [Pa]
1207
1208 while ((iter <= 3 || std::abs(f) > eps) && iter < 100) {
1209 if (iter == 1) {
1210 x1 = vbar_a_guess;
1211 vbar_a = x1;
1212 }
1213 if (iter == 2) {
1214 x2 = vbar_a_guess + 0.001;
1215 vbar_a = x2;
1216 }
1217 if (iter > 2) {
1218 vbar_a = x2;
1219 }
1220 f = R_bar_Lem * T / vbar_a * (1 + Baa / vbar_a + Caaa / pow(vbar_a, 2)) - p;
1221 if (iter == 1) {
1222 y1 = f;
1223 }
1224 if (iter > 1) {
1225 y2 = f;
1226 x3 = x2 - y2 / (y2 - y1) * (x2 - x1);
1227 y1 = y2;
1228 x1 = x2;
1229 x2 = x3;
1230 }
1231 iter = iter + 1;
1232 }
1233
1234 if (FlagUseIdealGasEnthalpyCorrelations) {
1235 std::cout << "Not implemented" << '\n';
1236 } else {
1237 sbar_w = IdealGasMolarEntropy_Water(T, p);
1238 sbar_a = IdealGasMolarEntropy_Air(T, vbar_a);
1239 }
1240 if (psi_w != 0) {
1241 sbar = sbar_0 + (1 - psi_w) * sbar_a + psi_w * sbar_w
1242 - R_bar * ((B + T * dBdT) / v_bar + (C + T * dCdT) / (2 * pow(v_bar, 2)) + (1 - psi_w) * log(1 - psi_w) + psi_w * log(psi_w));
1243 } else {
1244 sbar = sbar_0 + sbar_a - R_bar * ((B + T * dBdT) / v_bar + (C + T * dCdT) / (2 * pow(v_bar, 2)));
1245 }
1246 return sbar; //[J/mol_ha/K]
1247}
1248
1249double MassEntropy_per_kgha(double T, double p, double psi_w) {
1250 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1251 double s_bar = MolarEntropy(T, p, psi_w, vmolar); //[J/mol_ha/K]
1252 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1253 return s_bar / M_ha; //[J/kg_ha/K]
1254}
1255double MassEntropy_per_kgda(double T, double p, double psi_w) {
1256 double vmolar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
1257 double s_bar = MolarEntropy(T, p, psi_w, vmolar); //[J/mol_ha/K]
1258 double M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966; // [kg_ha/mol_ha]
1259 double W = HumidityRatio(psi_w); //[kg_w/kg_da] // (1+W) is kg_ha/kg_da
1260 return s_bar * (1 + W) / M_ha; //[J/kg_da/K]
1261}
1262
1263double DewpointTemperature(double T, double p, double psi_w) {
1264 int iter = 0;
1265 double p_w = NAN, eps = NAN, resid = NAN, Tdp = 0, x1 = 0, x2 = 0, x3 = NAN, y1 = 0, y2 = NAN, T0 = NAN;
1266 double p_ws_dp = NAN, f_dp = NAN;
1267
1268 // Make sure it isn't dry air, return an impossible temperature otherwise
1269 if ((1 - psi_w) < 1e-16) {
1270 return -1;
1271 }
1272 // ------------------------------------------
1273 // Iteratively find the dewpoint temperature
1274 // ------------------------------------------
1275
1276 // The highest dewpoint temperature possible is the dry-bulb temperature.
1277 // When they are equal, the air is saturated (R=1)
1278
1279 p_w = psi_w * p;
1280
1281 // 611.65... is the triple point pressure of water in Pa
1282 if (p_w > 611.6547241637944) {
1283 T0 = IF97::Tsat97(p) - 1;
1284 } else {
1285 T0 = 268;
1286 }
1287 // A good guess for Tdp is that enhancement factor is unity, which yields
1288 // p_w_s = p_w, and get guess for T from saturation temperature
1289
1290 iter = 1;
1291 eps = 1e-5;
1292 resid = 999;
1293 while ((iter <= 3 || std::abs(resid) > eps) && iter < 100) {
1294 if (iter == 1) {
1295 x1 = T0;
1296 Tdp = x1;
1297 }
1298 if (iter == 2) {
1299 x2 = x1 + 0.1;
1300 Tdp = x2;
1301 }
1302 if (iter > 2) {
1303 Tdp = x2;
1304 }
1305
1306 if (Tdp >= 273.16) {
1307 // Saturation pressure at dewpoint [Pa]
1308 p_ws_dp = IF97::psat97(Tdp);
1309 } else {
1310 // Sublimation pressure at icepoint [Pa]
1311 p_ws_dp = psub_Ice(Tdp);
1312 }
1313 // Enhancement Factor at dewpoint temperature [-]
1314 f_dp = f_factor(Tdp, p);
1315 // Error between target and actual pressure [Pa]
1316 resid = p_w - p_ws_dp * f_dp;
1317
1318 if (iter == 1) {
1319 y1 = resid;
1320 }
1321 if (iter > 1) {
1322 y2 = resid;
1323 x3 = x2 - y2 / (y2 - y1) * (x2 - x1);
1324 y1 = y2;
1325 x1 = x2;
1326 x2 = x3;
1327 }
1328 iter = iter + 1;
1329 }
1330 return Tdp;
1331}
1332
1334{
1335 private:
1336 double _p, _W, LHS;
1337
1338 public:
1339 WetBulbSolver(double T, double p, double psi_w) : _p(p), _W(epsilon * psi_w / (1 - psi_w)) {
1340 //These things are all not a function of Twb
1341 double v_bar_w = MolarVolume(T, p, psi_w), M_ha = MM_Water() * psi_w + (1 - psi_w) * 0.028966;
1342 LHS = MolarEnthalpy(T, p, psi_w, v_bar_w) * (1 + _W) / M_ha;
1343 }
1344 double call(double Twb) override {
1345 double epsilon = 0.621945;
1346 double f_wb = NAN, p_ws_wb = NAN, p_s_wb = NAN, W_s_wb = NAN, h_w = NAN, M_ha_wb = NAN, psi_wb = NAN, v_bar_wb = NAN;
1347
1348 // Enhancement Factor at wetbulb temperature [-]
1349 f_wb = f_factor(Twb, _p);
1350 if (Twb > 273.16) {
1351 // Saturation pressure at wetbulb temperature [Pa]
1352 p_ws_wb = IF97::psat97(Twb);
1353 } else {
1354 // Sublimation pressure at wetbulb temperature [kPa]
1355 p_ws_wb = psub_Ice(Twb);
1356 }
1357
1358 // Vapor pressure
1359 p_s_wb = f_wb * p_ws_wb;
1360 // wetbulb humidity ratio
1361 W_s_wb = epsilon * p_s_wb / (_p - p_s_wb);
1362 // wetbulb water mole fraction
1363 psi_wb = W_s_wb / (epsilon + W_s_wb);
1364 if (Twb > 273.16) {
1365 // Use IF97 to do the flash
1366 WaterIF97->update(CoolProp::PT_INPUTS, _p, Twb);
1367 // Enthalpy of water [J/kg_water]
1368 Water->update(CoolProp::DmassT_INPUTS, WaterIF97->rhomass(), Twb);
1369 h_w = Water->keyed_output(CoolProp::iHmass); //[J/kg_water]
1370 } else {
1371 // Enthalpy of ice [J/kg_water]
1372 h_w = h_Ice(Twb, _p);
1373 }
1374 // Mole masses of wetbulb and humid air
1375
1376 M_ha_wb = MM_Water() * psi_wb + (1 - psi_wb) * 0.028966;
1377 v_bar_wb = MolarVolume(Twb, _p, psi_wb);
1378 double RHS = (MolarEnthalpy(Twb, _p, psi_wb, v_bar_wb) * (1 + W_s_wb) / M_ha_wb + (_W - W_s_wb) * h_w);
1379 if (!ValidNumber(LHS - RHS)) {
1380 throw CoolProp::ValueError();
1381 }
1382 return LHS - RHS;
1383 }
1384};
1385
1387{
1388 public:
1389 double p, hair_dry;
1391 double call(double Ts) override {
1392 //double RHS = HAPropsSI("H","T",Ts,"P",p,"R",1);
1393
1394 double psi_w = NAN, T = Ts;
1395 //std::vector<givens> inp = { HumidAir::GIVEN_T, HumidAir::GIVEN_RH }; // C++11
1396 std::vector<givens> inp(2);
1397 inp[0] = HumidAir::GIVEN_T;
1398 inp[1] = HumidAir::GIVEN_RH;
1399 //std::vector<double> val = { Ts, 1.0 }; // C++11
1400 std::vector<double> val(2);
1401 val[0] = Ts;
1402 val[1] = 1.0;
1403 _HAPropsSI_inputs(p, inp, val, T, psi_w);
1404 double RHS = _HAPropsSI_outputs(GIVEN_ENTHALPY, p, T, psi_w);
1405
1406 if (!ValidNumber(RHS)) {
1407 throw CoolProp::ValueError();
1408 }
1409 return RHS - this->hair_dry;
1410 }
1411};
1412
1413double WetbulbTemperature(double T, double p, double psi_w) {
1414 // ------------------------------------------
1415 // Iteratively find the wetbulb temperature
1416 // ------------------------------------------
1417 //
1418 // If the temperature is less than the saturation temperature of water
1419 // for the given atmospheric pressure, the highest wetbulb temperature that is possible is the dry bulb
1420 // temperature
1421 //
1422 // If the temperature is above the saturation temperature corresponding to the atmospheric pressure,
1423 // then the maximum value for the wetbulb temperature is the saturation temperature
1424 double Tmax = T;
1425 double Tsat = IF97::Tsat97(p);
1426 if (T >= Tsat) {
1427 Tmax = Tsat;
1428 }
1429
1430 // Instantiate the solver container class
1431 WetBulbSolver WBS(T, p, psi_w);
1432
1433 // Upper bracket for Brent. Historically this was Tmax + 1, which can
1434 // straddle Tsat from either side: above (T >= Tsat) makes W_s_wb in
1435 // the residual go negative since p_ws_wb > p (#2255), and from below
1436 // (Tsat - 1 < T < Tsat) gives Tupper = T + 1 > Tsat so internal
1437 // Brent steps still land on the singular point (#2690 isolated
1438 // outliers like T_wb=28, RH=1e-4, P=87550). Clamp Tupper strictly
1439 // below Tsat with a margin large enough to clear IF97's saturation-
1440 // tolerance band — WaterIF97->update(PT_INPUTS, p, T) throws when
1441 // |psat(T) - p|/p falls below ~3e-3 %, i.e. roughly 1 mK below Tsat
1442 // for atmospheric P. Use 0.1 K to give that check several orders of
1443 // headroom while preserving plenty of bracket for the true Twb,
1444 // which is typically tens of K below Tsat for the wet-bulb cases
1445 // we care about.
1446 double Tupper = std::min(Tmax + 1.0, Tsat - 0.1);
1447
1448 double return_val = NAN;
1449 try {
1450 return_val = Brent(WBS, Tupper, 100, DBL_EPSILON, 1e-12, 50);
1451
1452 // Solution obtained is out of range (T > Tmax)
1453 if (return_val > Tmax + 1) {
1454 throw CoolProp::ValueError();
1455 }
1456 // Reject solutions that are essentially the upper bracket — that
1457 // is Brent giving up rather than finding a true root in the
1458 // physical region (#2255). Derive the threshold from Tupper so
1459 // it tracks the margin chosen above; the extra 10 mK distinguishes
1460 // "found the bracket" from "found close to the bracket".
1461 if (T >= Tsat && return_val > Tupper - 1e-2) {
1462 throw CoolProp::ValueError();
1463 }
1464 } catch (...) {
1465 // The lowest wetbulb temperature that is possible for a given dry bulb temperature
1466 // is the saturated air temperature which yields the enthalpy of dry air at dry bulb temperature
1467
1468 try {
1469 double hair_dry = MassEnthalpy_per_kgda(T, p, 0); // both /kg_ha and /kg_da are the same here since dry air
1470
1471 // Directly solve for the saturated temperature that yields the enthalpy desired
1472 WetBulbTminSolver WBTS(p, hair_dry);
1473 double Tmin = Brent(WBTS, 210, Tsat - 1, 1e-12, 1e-12, 50);
1474
1475 return_val = Brent(WBS, Tmin - 30, Tmax - 1, 1e-12, 1e-12, 50);
1476 } catch (...) {
1477 return_val = _HUGE;
1478 }
1479 }
1480 return return_val;
1481}
1482static givens Name2Type(const std::string& Name) {
1483 if (!strcmp(Name, "Omega") || !strcmp(Name, "HumRat") || !strcmp(Name, "W"))
1484 return GIVEN_HUMRAT;
1485 else if (!strcmp(Name, "psi_w") || !strcmp(Name, "Y"))
1486 return GIVEN_PSIW;
1487 else if (!strcmp(Name, "Tdp") || !strcmp(Name, "T_dp") || !strcmp(Name, "DewPoint") || !strcmp(Name, "D"))
1488 return GIVEN_TDP;
1489 else if (!strcmp(Name, "Twb") || !strcmp(Name, "T_wb") || !strcmp(Name, "WetBulb") || !strcmp(Name, "B"))
1490 return GIVEN_TWB;
1491 else if (!strcmp(Name, "Enthalpy") || !strcmp(Name, "H") || !strcmp(Name, "Hda"))
1492 return GIVEN_ENTHALPY;
1493 else if (!strcmp(Name, "Hha"))
1494 return GIVEN_ENTHALPY_HA;
1495 else if (!strcmp(Name, "InternalEnergy") || !strcmp(Name, "U") || !strcmp(Name, "Uda"))
1496 return GIVEN_INTERNAL_ENERGY;
1497 else if (!strcmp(Name, "Uha"))
1499 else if (!strcmp(Name, "Entropy") || !strcmp(Name, "S") || !strcmp(Name, "Sda"))
1500 return GIVEN_ENTROPY;
1501 else if (!strcmp(Name, "Sha"))
1502 return GIVEN_ENTROPY_HA;
1503 else if (!strcmp(Name, "RH") || !strcmp(Name, "RelHum") || !strcmp(Name, "R"))
1504 return GIVEN_RH;
1505 else if (!strcmp(Name, "Tdb") || !strcmp(Name, "T_db") || !strcmp(Name, "T"))
1506 return GIVEN_T;
1507 else if (!strcmp(Name, "P"))
1508 return GIVEN_P;
1509 else if (!strcmp(Name, "V") || !strcmp(Name, "Vda"))
1510 return GIVEN_VDA;
1511 else if (!strcmp(Name, "Vha"))
1512 return GIVEN_VHA;
1513 else if (!strcmp(Name, "mu") || !strcmp(Name, "Visc") || !strcmp(Name, "M"))
1514 return GIVEN_VISC;
1515 else if (!strcmp(Name, "k") || !strcmp(Name, "Conductivity") || !strcmp(Name, "K"))
1516 return GIVEN_COND;
1517 else if (!strcmp(Name, "C") || !strcmp(Name, "cp"))
1518 return GIVEN_CP;
1519 else if (!strcmp(Name, "Cha") || !strcmp(Name, "cp_ha"))
1520 return GIVEN_CPHA;
1521 else if (!strcmp(Name, "CV"))
1522 return GIVEN_CV;
1523 else if (!strcmp(Name, "CVha") || !strcmp(Name, "cv_ha"))
1524 return GIVEN_CVHA;
1525 else if (!strcmp(Name, "P_w"))
1527 else if (!strcmp(Name, "isentropic_exponent"))
1529 else if (!strcmp(Name, "speed_of_sound"))
1530 return GIVEN_SPEED_OF_SOUND;
1531 else if (!strcmp(Name, "Z"))
1533 else
1535 "Sorry, your input [%s] was not understood to Name2Type. Acceptable values are T,P,R,W,D,B,H,S,M,K and aliases thereof\n", Name.c_str()));
1536}
1537int TypeMatch(int TypeCode, const std::string& Input1Name, const std::string& Input2Name, const std::string& Input3Name) {
1538 // Return the index of the input variable that matches the input, otherwise return -1 for failure
1539 if (TypeCode == Name2Type(Input1Name)) return 1;
1540 if (TypeCode == Name2Type(Input2Name)) return 2;
1541 if (TypeCode == Name2Type(Input3Name))
1542 return 3;
1543 else
1544 return -1;
1545}
1546double MoleFractionWater(double T, double p, int HumInput, double InVal) {
1547 double p_ws = NAN, f = NAN, W = NAN, epsilon = 0.621945, Tdp = NAN, p_ws_dp = NAN, f_dp = NAN, p_w_dp = NAN, p_s = NAN, RH = NAN;
1548
1549 if (HumInput == GIVEN_HUMRAT) //(2)
1550 {
1551 W = InVal;
1552 return W / (epsilon + W);
1553 } else if (HumInput == GIVEN_RH) {
1554 if (T >= 273.16) {
1555 // Saturation pressure [Pa]
1556 p_ws = IF97::psat97(T);
1557 } else {
1558 // Sublimation pressure [Pa]
1559 p_ws = psub_Ice(T);
1560 }
1561 // Enhancement Factor [-]
1562 f = f_factor(T, p);
1563 // Saturation pressure [Pa]
1564 p_s = f * p_ws; // Eq. 29
1565 RH = InVal;
1566 // Saturation mole fraction [-]
1567 double psi_ws = p_s / p; // Eq. 32
1568 // Mole fraction [-]
1569 return RH * psi_ws; // Eq. 43
1570 } else if (HumInput == GIVEN_TDP) {
1571 Tdp = InVal;
1572 // Saturation pressure at dewpoint [Pa]
1573 if (Tdp >= 273.16) {
1574 p_ws_dp = IF97::psat97(Tdp);
1575 } else {
1576 // Sublimation pressure [Pa]
1577 p_ws_dp = psub_Ice(Tdp);
1578 }
1579
1580 // Enhancement Factor at dewpoint temperature [-]
1581 f_dp = f_factor(Tdp, p);
1582 // Water vapor pressure at dewpoint [Pa]
1583 p_w_dp = f_dp * p_ws_dp;
1584 // Water mole fraction [-]
1585 return p_w_dp / p;
1586 } else {
1587 return -_HUGE;
1588 }
1589}
1590
1591double RelativeHumidity(double T, double p, double psi_w) {
1592 double p_ws = NAN, f = NAN, p_s = NAN;
1593 if (T >= 273.16) {
1594 // Saturation pressure [Pa]
1595 p_ws = IF97::psat97(T);
1596 } else {
1597 // sublimation pressure [Pa]
1598 p_ws = psub_Ice(T);
1599 }
1600 // Enhancement Factor [-]
1601 f = f_factor(T, p);
1602
1603 // Saturation pressure [Pa]
1604 p_s = f * p_ws;
1605
1606 // Calculate the relative humidity
1607 return psi_w * p / p_s;
1608}
1609
1610void convert_to_SI(const std::string& Name, double& val) {
1611 switch (Name2Type(Name)) {
1612 case GIVEN_COND:
1613 case GIVEN_ENTHALPY:
1614 case GIVEN_ENTHALPY_HA:
1615 case GIVEN_ENTROPY:
1616 case GIVEN_ENTROPY_HA:
1619 case GIVEN_CP:
1620 case GIVEN_CPHA:
1621 case GIVEN_CV:
1622 case GIVEN_CVHA:
1623 case GIVEN_P:
1627 val *= 1000;
1628 return;
1629 case GIVEN_T:
1630 case GIVEN_TDP:
1631 case GIVEN_TWB:
1632 case GIVEN_RH:
1633 case GIVEN_VDA:
1634 case GIVEN_VHA:
1635 case GIVEN_HUMRAT:
1636 case GIVEN_VISC:
1637 case GIVEN_PSIW:
1639 return;
1640 case GIVEN_INVALID:
1641 throw CoolProp::ValueError(format("invalid input to convert_to_SI"));
1642 }
1643}
1644void convert_from_SI(const std::string& Name, double& val) {
1645 switch (Name2Type(Name)) {
1646 case GIVEN_COND:
1647 case GIVEN_ENTHALPY:
1648 case GIVEN_ENTHALPY_HA:
1649 case GIVEN_ENTROPY:
1650 case GIVEN_ENTROPY_HA:
1653 case GIVEN_CP:
1654 case GIVEN_CPHA:
1655 case GIVEN_CV:
1656 case GIVEN_CVHA:
1657 case GIVEN_P:
1661 val /= 1000;
1662 return;
1663 case GIVEN_T:
1664 case GIVEN_TDP:
1665 case GIVEN_TWB:
1666 case GIVEN_RH:
1667 case GIVEN_VDA:
1668 case GIVEN_VHA:
1669 case GIVEN_HUMRAT:
1670 case GIVEN_VISC:
1671 case GIVEN_PSIW:
1673 return;
1674 case GIVEN_INVALID:
1675 throw CoolProp::ValueError(format("invalid input to convert_from_SI"));
1676 }
1677}
1678double HAProps(const std::string& OutputName, const std::string& Input1Name, double Input1, const std::string& Input2Name, double Input2,
1679 const std::string& Input3Name, double Input3) {
1680 convert_to_SI(Input1Name, Input1);
1681 convert_to_SI(Input2Name, Input2);
1682 convert_to_SI(Input3Name, Input3);
1683
1684 double out = HAPropsSI(OutputName, Input1Name, Input1, Input2Name, Input2, Input3Name, Input3);
1685
1686 convert_from_SI(OutputName, out);
1687
1688 return out;
1689}
1690long get_input_key(const std::vector<givens>& input_keys, givens key) {
1691 if (input_keys.size() != 2) {
1692 throw CoolProp::ValueError("input_keys is not 2-element vector");
1693 }
1694
1695 if (input_keys[0] == key) {
1696 return 0;
1697 } else if (input_keys[1] == key) {
1698 return 1;
1699 } else {
1700 return -1;
1701 }
1702}
1703bool match_input_key(const std::vector<givens>& input_keys, givens key) {
1704 return get_input_key(input_keys, key) >= 0;
1705}
1706
1708{
1709 private:
1710 const double p;
1711 const double target;
1712 const givens output;
1713 const std::vector<givens> input_keys = {GIVEN_T, GIVEN_HUMRAT};
1714 std::vector<double> input_vals;
1715 double _T, _psi_w;
1716
1717 public:
1718 HAProps_W_Residual(const double p, const double target, const givens output, const double T)
1719 : p(p), target(target), output(output), _T(_HUGE), _psi_w(_HUGE) {
1720 input_vals.resize(2, T);
1721 }
1722
1723 double call(double W) override {
1724 // Update inputs
1725 input_vals[1] = W;
1726 // Prepare calculation
1727 _HAPropsSI_inputs(p, input_keys, input_vals, _T, _psi_w);
1728 // Retrieve outputs
1729 return _HAPropsSI_outputs(output, p, _T, _psi_w) - target;
1730 }
1731};
1732
1734{
1735 private:
1736 const double p;
1737 const double target;
1738 const givens output;
1739 const std::vector<givens> input_keys = {GIVEN_T, GIVEN_HUMRAT};
1740 std::vector<double> input_vals;
1741 double _T, _psi_w;
1742
1743 public:
1744 HAProps_T_Residual(const double p, const double target, const givens output, const double W)
1745 : p(p), target(target), output(output), _T(_HUGE), _psi_w(_HUGE) {
1746 input_vals.resize(2, W);
1747 }
1748
1749 double call(double T) override {
1750 // Update inputs
1751 input_vals[0] = T;
1752 // Prepare calculation
1753 _HAPropsSI_inputs(p, input_keys, input_vals, _T, _psi_w);
1754 // Retrieve outputs
1755 return _HAPropsSI_outputs(output, p, _T, _psi_w) - target;
1756 }
1757};
1758
1760void _HAPropsSI_inputs(double p, const std::vector<givens>& input_keys, const std::vector<double>& input_vals, double& T, double& psi_w) {
1761 if (CoolProp::get_debug_level() > 0) {
1762 std::cout << format("length of input_keys is %d\n", input_keys.size());
1763 }
1764 if (input_keys.size() != input_vals.size()) {
1765 throw CoolProp::ValueError(format("Length of input_keys (%d) does not equal that of input_vals (%d)", input_keys.size(), input_vals.size()));
1766 }
1767
1768 long key = get_input_key(input_keys, GIVEN_T);
1769 if (key >= 0) // Found T (or alias) as an input
1770 {
1771 long other = 1 - key; // 2 element vector
1772 T = input_vals[key];
1773 if (CoolProp::get_debug_level() > 0) {
1774 std::cout << format("One of the inputs is T: %g K\n", T);
1775 }
1776 givens othergiven = input_keys[other];
1777 switch (othergiven) {
1778 case GIVEN_RH:
1779 case GIVEN_HUMRAT:
1780 case GIVEN_TDP:
1781 if (CoolProp::get_debug_level() > 0) {
1782 std::cout << format("other input value is %g\n", input_vals[other]);
1783 std::cout << format("other input index is %d\n", othergiven);
1784 }
1785 psi_w = MoleFractionWater(T, p, othergiven, input_vals[other]);
1786 break;
1787 default: {
1788 HAProps_W_Residual residual(p, input_vals[other], othergiven, T);
1789 double W = _HUGE;
1790 try {
1791 // Find the value for W using the Secant solver
1792 W = CoolProp::Secant(&residual, 0.0001, 0.00001, 1e-14, 100);
1793 if (!ValidNumber(W)) {
1794 throw CoolProp::ValueError("Iterative value for W is invalid");
1795 }
1796 } catch (...) {
1797 // Use the Brent's method solver to find W. Slow but reliable...
1798 //
1799 // Find the saturation value for the humidity ratio for given dry bulb T
1800 // This is this highest possible water content for the humidity ratio
1801 double psi_w_sat = MoleFractionWater(T, p, GIVEN_RH, 1.0);
1802 double W_max = HumidityRatio(psi_w_sat);
1803 double W_min = 0;
1804 givens MainInputKey = GIVEN_T;
1805 double MainInputValue = T;
1806 // Secondary input is the one that you are trying to match
1807 double SecondaryInputValue = input_vals[other];
1808 givens SecondaryInputKey = input_keys[other];
1809 W = Brent_HAProps_W(SecondaryInputKey, p, MainInputKey, MainInputValue, SecondaryInputValue, W_min, W_max);
1810 if (!ValidNumber(W)) {
1811 throw CoolProp::ValueError("Iterative value for W is invalid");
1812 }
1813 }
1814 // Mole fraction of water
1815 psi_w = MoleFractionWater(T, p, GIVEN_HUMRAT, W);
1816 }
1817 }
1818 } else {
1819 if (CoolProp::get_debug_level() > 0) {
1820 std::cout << format("The main input is not T\n", T);
1821 }
1822 // Need to iterate to find dry bulb temperature since temperature is not provided.
1823 // Pick the first available key in priority order: humidity ratio, then dewpoint,
1824 // then relative humidity. Prefer T_dp over R when both are present: T_dp determines
1825 // psi_w directly without requiring dry-bulb T, so it gives better iteration bounds.
1826 key = get_input_key(input_keys, GIVEN_HUMRAT); // Humidity ratio is given
1827 if (key < 0) {
1828 key = get_input_key(input_keys, GIVEN_TDP); // Dewpoint temperature is given
1829 }
1830 if (key < 0) {
1831 key = get_input_key(input_keys, GIVEN_RH); // Relative humidity is given
1832 }
1833 if (key < 0) {
1835 "Sorry, but currently at least one of the variables as an input to HAPropsSI() must be temperature, relative humidity, humidity ratio, "
1836 "or dewpoint\n Eventually will add a 2-D NR solver to find T and psi_w simultaneously, but not included now");
1837 }
1838 // Two water-content inputs are normally invalid, but two combinations uniquely
1839 // determine dry-bulb temperature because one fixes psi_w directly (independent of T)
1840 // while relative humidity provides the T-dependent equation to solve:
1841 // T_dp + R : psi_w from T_dp, solve R(T, p, psi_w) = R_target (issue #2670)
1842 // W + R : psi_w from W, solve R(T, p, psi_w) = R_target
1843 // W + T_dp is NOT valid: both fix psi_w independently, so T is unconstrained.
1844 int number_of_water_content_inputs =
1845 (get_input_key(input_keys, GIVEN_HUMRAT) >= 0) + (get_input_key(input_keys, GIVEN_RH) >= 0) + (get_input_key(input_keys, GIVEN_TDP) >= 0);
1846 bool has_humrat = get_input_key(input_keys, GIVEN_HUMRAT) >= 0;
1847 bool has_rh = get_input_key(input_keys, GIVEN_RH) >= 0;
1848 bool has_tdp = get_input_key(input_keys, GIVEN_TDP) >= 0;
1849 bool valid_two_water = has_rh && (has_tdp || has_humrat) && !(has_tdp && has_humrat);
1850 if (number_of_water_content_inputs > 1 && !valid_two_water) {
1852 "Sorry, but cannot provide two inputs that are both water-content (humidity ratio, relative humidity, absolute humidity");
1853 }
1854 // 2-element vector
1855 long other = 1 - key;
1856
1857 // Main input is the one that you are using in the call to HAPropsSI
1858 double MainInputValue = input_vals[key];
1859 givens MainInputKey = input_keys[key];
1860 // Secondary input is the one that you are trying to match
1861 double SecondaryInputValue = input_vals[other];
1862 givens SecondaryInputKey = input_keys[other];
1863
1864 if (CoolProp::get_debug_level() > 0) {
1865 std::cout << format("Main input is %g\n", MainInputValue);
1866 std::cout << format("Secondary input is %g\n", SecondaryInputValue);
1867 }
1868
1869 double T_min = 200;
1870 double T_max = 450;
1871 check_bounds(GIVEN_T, 273.15, T_min, T_max);
1872
1873 if (MainInputKey == GIVEN_RH) {
1874 if (MainInputValue < 1e-10) {
1875 T_max = 640;
1876 // For wetbulb, has to be below critical temp
1877 if (SecondaryInputKey == GIVEN_TWB || SecondaryInputKey == GIVEN_ENTHALPY) {
1878 T_max = 640;
1879 }
1880 if (SecondaryInputKey == GIVEN_TDP) {
1881 throw CoolProp::ValueError("For dry air, dewpoint is an invalid input variable\n");
1882 }
1883 } else {
1884 T_max = CoolProp::PropsSI("T", "P", p, "Q", 0, "Water") - 1;
1885 // For (RelHum, T_wb) at very low RH the wet-bulb energy balance
1886 // implies a T_db that can exceed Tsat(p): for moderate T_wb the
1887 // overshoot is small (Mode A/B), for high T_wb it diverges
1888 // (Mode C). Estimate T_db from the energy balance and either
1889 // (a) widen T_max so Brent brackets the root directly — avoiding
1890 // Secant overextrapolation into psi_w > 1 territory — or
1891 // (b) reject upfront when T_db_est is beyond model validity. (#2690)
1892 if (SecondaryInputKey == GIVEN_TWB && MainInputValue < 1e-3) {
1893 double T_wb_K = SecondaryInputValue;
1894 double psat_Twb = (T_wb_K > 273.16) ? IF97::psat97(T_wb_K) : psub_Ice(T_wb_K);
1895 if (psat_Twb < p) {
1896 const double eps_W = 0.621945;
1897 double W_sat_wb = eps_W * psat_Twb / (p - psat_Twb);
1898 double hfg = 2.501e6 - 2400.0 * (T_wb_K - 273.15);
1899 double cp_a = 1006.0;
1900 double T_db_est = T_wb_K + W_sat_wb * hfg / cp_a;
1901 if (T_db_est > 500.0) {
1902 throw CoolProp::ValueError(format("HAPropsSI(T_wb=%g K, RelHum=%g, P=%g Pa): the wet-bulb energy "
1903 "balance implies T_db ~ %g K, beyond the validity range (~500 K) "
1904 "of the humid-air mixture model for very dry conditions.",
1905 T_wb_K, MainInputValue, p, T_db_est)
1906 .c_str());
1907 }
1908 T_max = std::max(T_max, T_db_est + 20.0);
1909 }
1910 }
1911 }
1912 }
1913 // Minimum drybulb temperature is the drybulb temperature corresponding to saturated air for the humidity ratio
1914 // if the humidity ratio is provided
1915 else if (MainInputKey == GIVEN_HUMRAT) {
1916 if (MainInputValue < 1e-10) {
1917 T_min = 135; // Around the critical point of dry air
1918 T_max = 1000;
1919 } else {
1920 // Convert given humidity ratio to water mole fraction in vapor phase
1921 double T_dummy = -1, // Not actually needed
1922 psi_w_sat = MoleFractionWater(T_dummy, p, GIVEN_HUMRAT, MainInputValue);
1923 // Partial pressure of water, which is equal to f*p_{w_s}
1924 double pp_water_sat = psi_w_sat * p;
1925 // Assume unity enhancement factor, calculate guess for drybulb temperature
1926 // for given water phase composition
1927 if (pp_water_sat > Water->p_triple()) {
1928 T_min = IF97::Tsat97(pp_water_sat);
1929 } else {
1930 T_min = 230;
1931 }
1932 // Iteratively solve for temperature that will give desired pp_water_sat
1933 T_min = Secant_Tdb_at_saturated_W(psi_w_sat, p, T_min);
1934 }
1935 } else if (MainInputKey == GIVEN_TDP) {
1936 // By specifying the dewpoint, the water mole fraction is known directly
1937 // Otherwise, find psi_w for further calculations in the following section
1938 double psi_w = MoleFractionWater(-1, p, GIVEN_TDP, MainInputValue);
1939
1940 // Minimum drybulb temperature is saturated humid air at specified water mole fraction
1941 T_min = DewpointTemperature(T, p, psi_w);
1942 }
1943
1944 // #2906: the (X, T_wb) -> T_db inverse can land in the unreachable band
1945 // at the water triple point (273.16 K). There the wet-bulb energy
1946 // balance is discontinuous — h_w jumps by the latent heat of fusion as
1947 // the saturant wick switches liquid<->ice — so the forward map
1948 // Twb(T_db) skips a range of wet-bulb temperatures. For a target T_wb
1949 // in that gap the solve either fails outright, or (worse) Brent
1950 // converges onto the discontinuity T_db* and returns a T_db whose
1951 // actual wet-bulb is NOT the requested value — a silently-wrong "flat
1952 // section" spanning the band. Detect both modes and report a single,
1953 // honest infeasibility instead.
1954 //
1955 // The band always straddles 273.16 K but its width varies with
1956 // pressure: it is widest at low pressure (reaching ~1.2 K above the
1957 // triple point near 20-30 kPa for dry air) and narrows above
1958 // atmospheric. So the post-solve consistency check below runs for ANY
1959 // wet-bulb input — a silently-wrong result then cannot slip through at
1960 // any pressure — while the 2 K window only governs whether an outright
1961 // solve failure is reported as the triple-point infeasibility (vs the
1962 // generic out-of-range path).
1963 const bool twb_inverse = (SecondaryInputKey == GIVEN_TWB);
1964 const bool twb_near_triple = twb_inverse && (std::abs(SecondaryInputValue - 273.16) < 2.0);
1965 auto twb_triple_point_error = [&]() {
1966 const char* main_name = (MainInputKey == GIVEN_RH) ? "RelHum"
1967 : (MainInputKey == GIVEN_HUMRAT) ? "HumRat"
1968 : (MainInputKey == GIVEN_TDP) ? "T_dp"
1969 : "input";
1970 return CoolProp::ValueError(format("HAPropsSI(T_wb=%g K, %s=%g, P=%g Pa): no dry-bulb temperature reproduces this wet-bulb. It lies "
1971 "in the unreachable band at the water triple point (273.16 K), where the ice/liquid latent-heat "
1972 "discontinuity makes Twb(T_db) skip a range of wet-bulb temperatures; the state is infeasible for "
1973 "these conditions.",
1974 SecondaryInputValue, main_name, MainInputValue, p)
1975 .c_str());
1976 };
1977
1978 try {
1979 // Use the Brent's method solver to find T_drybulb. Slow but reliable
1980 T = Brent_HAProps_T(SecondaryInputKey, p, MainInputKey, MainInputValue, SecondaryInputValue, T_min, T_max);
1981 } catch (std::exception& e) {
1982 if (CoolProp::get_debug_level() > 0) {
1983 std::cout << "ERROR: " << e.what() << '\n';
1984 }
1985 if (twb_near_triple) {
1986 throw twb_triple_point_error(); // no root: target T_wb is in the triple-point gap (#2906)
1987 }
1989 T = _HUGE;
1990 psi_w = _HUGE;
1991 return;
1992 }
1993
1994 // Otherwise, find psi_w for further calculations in the following section
1995 std::vector<givens> input_keys(2, GIVEN_T);
1996 input_keys[1] = MainInputKey;
1997 std::vector<double> input_vals(2, T);
1998 input_vals[1] = MainInputValue;
1999 _HAPropsSI_inputs(p, input_keys, input_vals, T, psi_w);
2000
2001 // #2906 consistency check (unconditional for wet-bulb inputs): reject a
2002 // T_db whose wet-bulb does not match the request — the silently-wrong
2003 // flat section where Brent latched onto the discontinuity rather than a
2004 // true root. Running this for every wet-bulb input makes the guard
2005 // pressure-independent.
2006 if (twb_inverse) {
2007 const double wb_check = _HAPropsSI_outputs(GIVEN_TWB, p, T, psi_w);
2008 if (!ValidNumber(wb_check) || std::abs(wb_check - SecondaryInputValue) > 1e-2) {
2009 throw twb_triple_point_error();
2010 }
2011 }
2012 }
2013}
2014double _HAPropsSI_outputs(givens OutputType, double p, double T, double psi_w) {
2015 if (CoolProp::get_debug_level() > 0) {
2016 std::cout << format("_HAPropsSI_outputs :: T: %g K, psi_w: %g\n", T, psi_w);
2017 }
2018
2019 double M_ha = (1 - psi_w) * 0.028966 + MM_Water() * psi_w; //[kg_ha/mol_ha]
2020 // -----------------------------------------------------------------
2021 // Calculate and return the desired value for known set of p,T,psi_w
2022 // -----------------------------------------------------------------
2023 switch (OutputType) {
2024 case GIVEN_T:
2025 return T;
2026 case GIVEN_P:
2027 return p;
2028 case GIVEN_VDA: {
2029 double v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2030 double W = HumidityRatio(psi_w); //[kg_w/kg_a]
2031 return v_bar * (1 + W) / M_ha; //[m^3/kg_da]
2032 }
2033 case GIVEN_VHA: {
2034 double v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2035 return v_bar / M_ha; //[m^3/kg_ha]
2036 }
2037 case GIVEN_PSIW: {
2038 return psi_w; //[mol_w/mol]
2039 }
2041 return psi_w * p; //[Pa]
2042 }
2043 case GIVEN_ENTHALPY: {
2044 return MassEnthalpy_per_kgda(T, p, psi_w); //[J/kg_da]
2045 }
2046 case GIVEN_ENTHALPY_HA: {
2047 return MassEnthalpy_per_kgha(T, p, psi_w); //[J/kg_ha]
2048 }
2049 case GIVEN_INTERNAL_ENERGY: {
2050 return MassInternalEnergy_per_kgda(T, p, psi_w); //[J/kg_da]
2051 }
2053 return MassInternalEnergy_per_kgha(T, p, psi_w); //[J/kg_ha]
2054 }
2055 case GIVEN_ENTROPY: {
2056 return MassEntropy_per_kgda(T, p, psi_w); //[J/kg_da/K]
2057 }
2058 case GIVEN_ENTROPY_HA: {
2059 return MassEntropy_per_kgha(T, p, psi_w); //[J/kg_ha/K]
2060 }
2061 case GIVEN_TDP: {
2062 return DewpointTemperature(T, p, psi_w); //[K]
2063 }
2064 case GIVEN_TWB: {
2065 return WetbulbTemperature(T, p, psi_w); //[K]
2066 }
2067 case GIVEN_HUMRAT: {
2068 return HumidityRatio(psi_w);
2069 }
2070 case GIVEN_RH: {
2071 return RelativeHumidity(T, p, psi_w);
2072 }
2073 case GIVEN_VISC: {
2074 return Viscosity(T, p, psi_w);
2075 }
2076 case GIVEN_COND: {
2077 return Conductivity(T, p, psi_w);
2078 }
2079 case GIVEN_CP: {
2080 // [J/kg_ha/K]*[kg_ha/kg_da] because 1+W = kg_ha/kg_da
2081 return _HAPropsSI_outputs(GIVEN_CPHA, p, T, psi_w) * (1 + HumidityRatio(psi_w));
2082 }
2083 case GIVEN_CPHA: {
2084 double v_bar1 = NAN, v_bar2 = NAN, h_bar1 = NAN, h_bar2 = NAN, cp_ha = NAN, dT = 1e-3;
2085 v_bar1 = MolarVolume(T - dT, p, psi_w); //[m^3/mol_ha]
2086 h_bar1 = MolarEnthalpy(T - dT, p, psi_w, v_bar1); //[J/mol_ha]
2087 v_bar2 = MolarVolume(T + dT, p, psi_w); //[m^3/mol_ha]
2088 h_bar2 = MolarEnthalpy(T + dT, p, psi_w, v_bar2); //[J/mol_ha]
2089 cp_ha = (h_bar2 - h_bar1) / (2 * dT); //[J/mol_ha/K]
2090 return cp_ha / M_ha; //[J/kg_ha/K]
2091 }
2092 case GIVEN_CV: {
2093 // [J/kg_ha/K]*[kg_ha/kg_da] because 1+W = kg_ha/kg_da
2094 return _HAPropsSI_outputs(GIVEN_CVHA, p, T, psi_w) * (1 + HumidityRatio(psi_w));
2095 }
2096 case GIVEN_CVHA: {
2097 double v_bar = NAN, p_1 = NAN, p_2 = NAN, u_bar1 = NAN, u_bar2 = NAN, cv_bar = NAN, dT = 1e-3;
2098 v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2099 p_1 = Pressure(T - dT, v_bar, psi_w);
2100 u_bar1 = MolarInternalEnergy(T - dT, p_1, psi_w, v_bar); //[J/mol_ha]
2101 p_2 = Pressure(T + dT, v_bar, psi_w);
2102 u_bar2 = MolarInternalEnergy(T + dT, p_2, psi_w, v_bar); //[J/mol_ha]
2103 cv_bar = (u_bar2 - u_bar1) / (2 * dT); //[J/mol_ha/K]
2104 return cv_bar / M_ha; //[J/kg_ha/K]
2105 }
2107 CoolPropDbl v_bar = NAN, dv = 1e-8, p_1 = NAN, p_2 = NAN;
2108 CoolPropDbl cp = _HAPropsSI_outputs(GIVEN_CPHA, p, T, psi_w); //[J/kg_da/K]
2109 CoolPropDbl cv = _HAPropsSI_outputs(GIVEN_CVHA, p, T, psi_w); //[J/kg_da/K]
2110 v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2111 p_1 = Pressure(T, v_bar - dv, psi_w);
2112 p_2 = Pressure(T, v_bar + dv, psi_w);
2113 CoolPropDbl dpdv__constT = (p_2 - p_1) / (2 * dv);
2114 return -cp / cv * dpdv__constT * v_bar / p;
2115 }
2116 case GIVEN_SPEED_OF_SOUND: {
2117 CoolPropDbl v_bar = NAN, dv = 1e-8, p_1 = NAN, p_2 = NAN;
2118 CoolPropDbl cp = _HAPropsSI_outputs(GIVEN_CPHA, p, T, psi_w); //[J/kg_da/K]
2119 CoolPropDbl cv = _HAPropsSI_outputs(GIVEN_CVHA, p, T, psi_w); //[J/kg_da/K]
2120 v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2121 p_1 = Pressure(T, v_bar - dv, psi_w);
2122 p_2 = Pressure(T, v_bar + dv, psi_w);
2123 CoolPropDbl dvdrho = -v_bar * v_bar;
2124 CoolPropDbl dpdrho__constT = (p_2 - p_1) / (2 * dv) * dvdrho;
2125 return sqrt(1 / M_ha * cp / cv * dpdrho__constT);
2126 }
2128 double v_bar = MolarVolume(T, p, psi_w); //[m^3/mol_ha]
2129 double R_u_molar = 8.314472; // J/mol/K
2130 return p * v_bar / (R_u_molar * T);
2131 }
2132 default:
2133 return _HUGE;
2134 }
2135}
2136double HAPropsSI(const std::string& OutputName, const std::string& Input1Name, double Input1, const std::string& Input2Name, double Input2,
2137 const std::string& Input3Name, double Input3) {
2138 try {
2139 // Add a check to make sure that Air and Water fluid states have been properly instantiated
2141 Water->clear();
2142 Air->clear();
2143
2144 if (CoolProp::get_debug_level() > 0) {
2145 std::cout << format("HAPropsSI(%s,%s,%g,%s,%g,%s,%g)\n", OutputName.c_str(), Input1Name.c_str(), Input1, Input2Name.c_str(), Input2,
2146 Input3Name.c_str(), Input3);
2147 }
2148
2149 std::vector<givens> input_keys(2);
2150 std::vector<double> input_vals(2);
2151
2152 givens In1Type, In2Type, In3Type, OutputType;
2153 double p = NAN, T = _HUGE, psi_w = _HUGE;
2154
2155 // First figure out what kind of inputs you have, convert names to enum values
2156 In1Type = Name2Type(Input1Name.c_str());
2157 In2Type = Name2Type(Input2Name.c_str());
2158 In3Type = Name2Type(Input3Name.c_str());
2159
2160 // Output type
2161 OutputType = Name2Type(OutputName.c_str());
2162
2163 // Check for trivial inputs
2164 if (OutputType == In1Type) {
2165 return Input1;
2166 }
2167 if (OutputType == In2Type) {
2168 return Input2;
2169 }
2170 if (OutputType == In3Type) {
2171 return Input3;
2172 }
2173
2174 // Check that pressure is provided; load input vectors
2175 if (In1Type == GIVEN_P) {
2176 p = Input1;
2177 input_keys[0] = In2Type;
2178 input_keys[1] = In3Type;
2179 input_vals[0] = Input2;
2180 input_vals[1] = Input3;
2181 } else if (In2Type == GIVEN_P) {
2182 p = Input2;
2183 input_keys[0] = In1Type;
2184 input_keys[1] = In3Type;
2185 input_vals[0] = Input1;
2186 input_vals[1] = Input3;
2187 } else if (In3Type == GIVEN_P) {
2188 p = Input3;
2189 input_keys[0] = In1Type;
2190 input_keys[1] = In2Type;
2191 input_vals[0] = Input1;
2192 input_vals[1] = Input2;
2193 } else {
2194 throw CoolProp::ValueError("Pressure must be one of the inputs to HAPropsSI");
2195 }
2196
2197 if (input_keys[0] == input_keys[1]) {
2198 throw CoolProp::ValueError("Other two inputs to HAPropsSI aside from pressure cannot be the same");
2199 }
2200
2201 // Check the input values
2202 double min_val = _HUGE, max_val = -_HUGE; // Initialize with invalid values
2203 for (std::size_t i = 0; i < input_keys.size(); i++) {
2204 if (!check_bounds(input_keys[i], input_vals[i], min_val, max_val)) {
2205 throw CoolProp::ValueError(format("The input for key (%d) with value (%g) is outside the range of validity: (%g) to (%g)",
2206 input_keys[i], input_vals[i], min_val, max_val));
2207 //if (CoolProp::get_debug_level() > 0) {
2208 // std::cout << format("The input for key (%d) with value (%g) is outside the range of validity: (%g) to (%g)", input_keys[i], input_vals[i], min_val, max_val);
2209 //}
2210 }
2211 }
2212 // Parse the inputs to get to set of p, T, psi_w
2213 _HAPropsSI_inputs(p, input_keys, input_vals, T, psi_w);
2214
2215 if (CoolProp::get_debug_level() > 0) {
2216 std::cout << format("HAPropsSI input conversion yields T: %g, psi_w: %g\n", T, psi_w);
2217 }
2218
2219 // Check the standardized input values
2220 if (!check_bounds(GIVEN_P, p, min_val, max_val)) {
2221 throw CoolProp::ValueError(format("The pressure value (%g) is outside the range of validity: (%g) to (%g)", p, min_val, max_val));
2222 //if (CoolProp::get_debug_level() > 0) {
2223 // std::cout << format("The pressure value (%g) is outside the range of validity: (%g) to (%g)", p, min_val, max_val);
2224 //}
2225 }
2226 if (!check_bounds(GIVEN_T, T, min_val, max_val)) {
2227 throw CoolProp::ValueError(format("The temperature value (%g) is outside the range of validity: (%g) to (%g)", T, min_val, max_val));
2228 //if (CoolProp::get_debug_level() > 0) {
2229 // std::cout << format("The temperature value (%g) is outside the range of validity: (%g) to (%g)", T, min_val, max_val);
2230 //}
2231 }
2232 if (!check_bounds(GIVEN_PSIW, psi_w, min_val, max_val)) {
2234 format("The water mole fraction value (%g) is outside the range of validity: (%g) to (%g)", psi_w, min_val, max_val));
2235 //if (CoolProp::get_debug_level() > 0) {
2236 // std::cout << format("The water mole fraction value (%g) is outside the range of validity: (%g) to (%g)", psi_w, min_val, max_val);
2237 //}
2238 }
2239 // Calculate the output value desired
2240 double val = _HAPropsSI_outputs(OutputType, p, T, psi_w);
2241 // Check the output value
2242 if (!check_bounds(OutputType, val, min_val, max_val)) {
2244 format("The output for key (%d) with value (%g) is outside the range of validity: (%g) to (%g)", OutputType, val, min_val, max_val));
2245 //if (CoolProp::get_debug_level() > 0) {
2246 // std::cout << format("The output for key (%d) with value (%g) is outside the range of validity: (%g) to (%g)", OutputType, val, min_val, max_val);
2247 //}
2248 }
2249
2250 if (!ValidNumber(val)) {
2251 if (CoolProp::get_debug_level() > 0) {
2252 std::cout << format("HAPropsSI is about to return invalid number");
2253 }
2254 throw CoolProp::ValueError("Invalid value about to be returned");
2255 }
2256
2257 if (CoolProp::get_debug_level() > 0) {
2258 std::cout << format("HAPropsSI is about to return %g\n", val);
2259 }
2260 return val;
2261 } catch (std::exception& e) {
2263 return _HUGE;
2264 } catch (...) {
2265 return _HUGE;
2266 }
2267}
2268
2269double HAProps_Aux(const char* Name, double T, double p, double W, char* units) {
2270 // This function provides some things that are not usually needed, but could be interesting for debug purposes.
2271
2272 // Add a check to make sure that Air and Water fluid states have been properly instantiated
2274
2275 // Requires W since it is nice and fast and always defined. Put a dummy value if you want something that doesn't use humidity
2276
2277 // Takes temperature, pressure, and humidity ratio W as inputs;
2278 double psi_w = NAN, B_aa = NAN, C_aaa = NAN, B_ww = NAN, C_www = NAN, B_aw = NAN, C_aaw = NAN, C_aww = NAN, v_bar = NAN;
2279
2280 try {
2281 if (!strcmp(Name, "Baa")) {
2282 B_aa = B_Air(T); // [m^3/mol]
2283 strcpy(units, "m^3/mol");
2284 return B_aa;
2285 } else if (!strcmp(Name, "Caaa")) {
2286 C_aaa = C_Air(T); // [m^6/mol^2]
2287 strcpy(units, "m^6/mol^2");
2288 return C_aaa;
2289 } else if (!strcmp(Name, "Bww")) {
2290 B_ww = B_Water(T); // [m^3/mol]
2291 strcpy(units, "m^3/mol");
2292 return B_ww;
2293 } else if (!strcmp(Name, "Cwww")) {
2294 C_www = C_Water(T); // [m^6/mol^2]
2295 strcpy(units, "m^6/mol^2");
2296 return C_www;
2297 } else if (!strcmp(Name, "dBaa")) {
2298 B_aa = dBdT_Air(T); // [m^3/mol]
2299 strcpy(units, "m^3/mol");
2300 return B_aa;
2301 } else if (!strcmp(Name, "dCaaa")) {
2302 C_aaa = dCdT_Air(T); // [m^6/mol^2]
2303 strcpy(units, "m^6/mol^2");
2304 return C_aaa;
2305 } else if (!strcmp(Name, "dBww")) {
2306 B_ww = dBdT_Water(T); // [m^3/mol]
2307 strcpy(units, "m^3/mol");
2308 return B_ww;
2309 } else if (!strcmp(Name, "dCwww")) {
2310 C_www = dCdT_Water(T); // [m^6/mol^2]
2311 strcpy(units, "m^6/mol^2");
2312 return C_www;
2313 } else if (!strcmp(Name, "Baw")) {
2314 B_aw = _B_aw(T); // [m^3/mol]
2315 strcpy(units, "m^3/mol");
2316 return B_aw;
2317 } else if (!strcmp(Name, "Caww")) {
2318 C_aww = _C_aww(T); // [m^6/mol^2]
2319 strcpy(units, "m^6/mol^2");
2320 return C_aww;
2321 } else if (!strcmp(Name, "Caaw")) {
2322 C_aaw = _C_aaw(T); // [m^6/mol^2]
2323 strcpy(units, "m^6/mol^2");
2324 return C_aaw;
2325 } else if (!strcmp(Name, "dBaw")) {
2326 double dB_aw = _dB_aw_dT(T); // [m^3/mol]
2327 strcpy(units, "m^3/mol");
2328 return dB_aw;
2329 } else if (!strcmp(Name, "dCaww")) {
2330 double dC_aww = _dC_aww_dT(T); // [m^6/mol^2]
2331 strcpy(units, "m^6/mol^2");
2332 return dC_aww;
2333 } else if (!strcmp(Name, "dCaaw")) {
2334 double dC_aaw = _dC_aaw_dT(T); // [m^6/mol^2]
2335 strcpy(units, "m^6/mol^2");
2336 return dC_aaw;
2337 } else if (!strcmp(Name, "beta_H")) {
2338 strcpy(units, "1/Pa");
2339 return HenryConstant(T);
2340 } else if (!strcmp(Name, "kT")) {
2341 strcpy(units, "1/Pa");
2342 if (T > 273.16) {
2343 // Use IF97 to do the flash
2344 WaterIF97->update(CoolProp::PT_INPUTS, p, T);
2345 Water->update(CoolProp::PT_INPUTS, WaterIF97->rhomass(), T);
2346 return Water->keyed_output(CoolProp::iisothermal_compressibility);
2347 } else
2348 return IsothermCompress_Ice(T, p); //[1/Pa]
2349 } else if (!strcmp(Name, "p_ws")) {
2350 strcpy(units, "Pa");
2351 if (T > 273.16) {
2352 return IF97::psat97(T);
2353 } else
2354 return psub_Ice(T);
2355 } else if (!strcmp(Name, "vbar_ws")) {
2356 strcpy(units, "m^3/mol");
2357 if (T > 273.16) {
2358 Water->update(CoolProp::QT_INPUTS, 0, T);
2359 return 1.0 / Water->keyed_output(CoolProp::iDmolar);
2360 } else {
2361 // It is ice. dg_dp_Ice is the specific volume [m^3/kg] (rho_Ice = 1/dg_dp_Ice)
2362 // and MM_Water is [kg/mol], so the product is already [m^3/mol]; no further
2363 // unit conversion is needed. This mirrors the f_factor() computation. (GH #2657)
2364 return dg_dp_Ice(T, p) * MM_Water(); //[m^3/mol]
2365 }
2366 } else if (!strcmp(Name, "f")) {
2367 strcpy(units, "-");
2368 return f_factor(T, p);
2369 }
2370 // Get psi_w since everything else wants it
2371 psi_w = MoleFractionWater(T, p, GIVEN_HUMRAT, W);
2372 if (!strcmp(Name, "Bm")) {
2373 strcpy(units, "m^3/mol");
2374 return B_m(T, psi_w);
2375 } else if (!strcmp(Name, "Cm")) {
2376 strcpy(units, "m^6/mol^2");
2377 return C_m(T, psi_w);
2378 } else if (!strcmp(Name, "hvirial")) {
2379 v_bar = MolarVolume(T, p, psi_w);
2380 return 8.3145 * T * ((B_m(T, psi_w) - T * dB_m_dT(T, psi_w)) / v_bar + (C_m(T, psi_w) - T / 2.0 * dC_m_dT(T, psi_w)) / (v_bar * v_bar));
2381 }
2382 //else if (!strcmp(Name,"ha"))
2383 //{
2384 // delta=1.1/322; tau=132/T;
2385 // return 1+tau*DerivTerms("dphi0_dTau",tau,delta,"Water");
2386 //}
2387 //else if (!strcmp(Name,"hw"))
2388 //{
2389 // //~ return Props('D','T',T,'P',p,"Water")/322; tau=647/T;
2390 // delta=1000/322; tau=647/T;
2391 // //~ delta=rho_Water(T,p,TYPE_TP);tau=647/T;
2392 // return 1+tau*DerivTerms("dphi0_dTau",tau,delta,"Water");
2393 //}
2394 else if (!strcmp(Name, "hbaro_w")) {
2395 return IdealGasMolarEnthalpy_Water(T, p);
2396 } else if (!strcmp(Name, "hbaro_a")) {
2397 return IdealGasMolarEnthalpy_Air(T, p);
2398 } else if (!strcmp(Name, "h_Ice")) {
2399 strcpy(units, "J/kg");
2400 return h_Ice(T, p);
2401 } else if (!strcmp(Name, "s_Ice")) {
2402 strcpy(units, "J/kg/K");
2403 return s_Ice(T, p);
2404 } else if (!strcmp(Name, "psub_Ice")) {
2405 strcpy(units, "Pa");
2406 return psub_Ice(T);
2407 } else if (!strcmp(Name, "g_Ice")) {
2408 strcpy(units, "J/kg");
2409 return g_Ice(T, p);
2410 } else if (!strcmp(Name, "rho_Ice")) {
2411 strcpy(units, "kg/m^3");
2412 return rho_Ice(T, p);
2413 } else {
2414 std::cout << format("Sorry I didn't understand your input [%s] to HAProps_Aux\n", Name);
2415 return -1;
2416 }
2417 } catch (...) { // NOLINT(bugprone-empty-catch)
2418 // HAProps_Aux is a public-facing C API and must never throw —
2419 // anything that escapes the dispatch above is surfaced as
2420 // _HUGE so callers see "no value" rather than a crash.
2421 }
2422 return _HUGE;
2423}
2424double cair_sat(double T) {
2425 // Humid air saturation specific heat at 1 atmosphere.
2426 // Based on a correlation from EES, good from 250K to 300K.
2427 // No error bound checking is carried out
2428 // T: [K]
2429 // cair_s: [kJ/kg-K]
2430 return 2.14627073E+03 - 3.28917768E+01 * T + 1.89471075E-01 * T * T - 4.86290986E-04 * T * T * T + 4.69540143E-07 * T * T * T * T;
2431}
2432
2433double IceProps(const char* Name, double T, double p) {
2434 if (!strcmp(Name, "s")) {
2435 return s_Ice(T, p * 1000.0);
2436 } else if (!strcmp(Name, "rho")) {
2437 return rho_Ice(T, p * 1000.0);
2438 } else if (!strcmp(Name, "h")) {
2439 return h_Ice(T, p * 1000.0);
2440 } else {
2441 return 1e99;
2442 }
2443}
2444
2445} /* namespace HumidAir */
2446
2447#ifdef ENABLE_CATCH
2448# include <math.h>
2449# include <thread>
2450# include <catch2/catch_all.hpp>
2451
2452TEST_CASE("Check HA Virials from Table A.2.1", "[RP1485]") {
2453 SECTION("B_aa") {
2454 CHECK(std::abs(HumidAir::B_Air(-60 + 273.15) / (-33.065 / 1e6) - 1) < 1e-3);
2455 CHECK(std::abs(HumidAir::B_Air(0 + 273.15) / (-13.562 / 1e6) - 1) < 1e-3);
2456 CHECK(std::abs(HumidAir::B_Air(200 + 273.15) / (11.905 / 1e6) - 1) < 1e-3);
2457 CHECK(std::abs(HumidAir::B_Air(350 + 273.15) / (18.949 / 1e6) - 1) < 1e-3);
2458 }
2459 SECTION("B_ww") {
2460 CHECK(std::abs(HumidAir::B_Water(-60 + 273.15) / (-11174 / 1e6) - 1) < 1e-3);
2461 CHECK(std::abs(HumidAir::B_Water(0 + 273.15) / (-2025.6 / 1e6) - 1) < 1e-3);
2462 CHECK(std::abs(HumidAir::B_Water(200 + 273.15) / (-200.52 / 1e6) - 1) < 1e-3);
2463 CHECK(std::abs(HumidAir::B_Water(350 + 273.15) / (-89.888 / 1e6) - 1) < 1e-3);
2464 }
2465 SECTION("B_aw") {
2466 CHECK(std::abs(HumidAir::_B_aw(-60 + 273.15) / (-68.306 / 1e6) - 1) < 1e-3);
2467 CHECK(std::abs(HumidAir::_B_aw(0 + 273.15) / (-38.074 / 1e6) - 1) < 1e-3);
2468 CHECK(std::abs(HumidAir::_B_aw(200 + 273.15) / (-2.0472 / 1e6) - 1) < 1e-3);
2469 CHECK(std::abs(HumidAir::_B_aw(350 + 273.15) / (7.5200 / 1e6) - 1) < 1e-3);
2470 }
2471
2472 SECTION("C_aaa") {
2473 CHECK(std::abs(HumidAir::C_Air(-60 + 273.15) / (2177.9 / 1e12) - 1) < 1e-3);
2474 CHECK(std::abs(HumidAir::C_Air(0 + 273.15) / (1893.1 / 1e12) - 1) < 1e-3);
2475 CHECK(std::abs(HumidAir::C_Air(200 + 273.15) / (1551.2 / 1e12) - 1) < 1e-3);
2476 CHECK(std::abs(HumidAir::C_Air(350 + 273.15) / (1464.7 / 1e12) - 1) < 1e-3);
2477 }
2478 SECTION("C_www") {
2479 CHECK(std::abs(HumidAir::C_Water(-60 + 273.15) / (-1.5162999202e-04) - 1) < 1e-3); // Relaxed criterion for this parameter
2480 CHECK(std::abs(HumidAir::C_Water(0 + 273.15) / (-10981960 / 1e12) - 1) < 1e-3);
2481 CHECK(std::abs(HumidAir::C_Water(200 + 273.15) / (-0.00000003713759442) - 1) < 1e-3);
2482 CHECK(std::abs(HumidAir::C_Water(350 + 273.15) / (-0.000000001198914198) - 1) < 1e-3);
2483 }
2484 SECTION("C_aaw") {
2485 CHECK(std::abs(HumidAir::_C_aaw(-60 + 273.15) / (1027.3 / 1e12) - 1) < 1e-3);
2486 CHECK(std::abs(HumidAir::_C_aaw(0 + 273.15) / (861.02 / 1e12) - 1) < 1e-3);
2487 CHECK(std::abs(HumidAir::_C_aaw(200 + 273.15) / (627.15 / 1e12) - 1) < 1e-3);
2488 CHECK(std::abs(HumidAir::_C_aaw(350 + 273.15) / (583.79 / 1e12) - 1) < 1e-3);
2489 }
2490 SECTION("C_aww") {
2491 CHECK(std::abs(HumidAir::_C_aww(-60 + 273.15) / (-1821432 / 1e12) - 1) < 1e-3);
2492 CHECK(std::abs(HumidAir::_C_aww(0 + 273.15) / (-224234 / 1e12) - 1) < 1e-3);
2493 CHECK(std::abs(HumidAir::_C_aww(200 + 273.15) / (-8436.5 / 1e12) - 1) < 1e-3);
2494 CHECK(std::abs(HumidAir::_C_aww(350 + 273.15) / (-2486.9 / 1e12) - 1) < 1e-3);
2495 }
2496}
2497TEST_CASE("Enhancement factor from Table A.3", "[RP1485]") {
2498 CHECK(std::abs(HumidAir::f_factor(-60 + 273.15, 101325) / (1.00708) - 1) < 1e-3);
2499 CHECK(std::abs(HumidAir::f_factor(80 + 273.15, 101325) / (1.00573) - 1) < 1e-3);
2500 CHECK(std::abs(HumidAir::f_factor(-60 + 273.15, 10000e3) / (2.23918) - 1) < 1e-3);
2501 CHECK(std::abs(HumidAir::f_factor(300 + 273.15, 10000e3) / (1.04804) - 1) < 1e-3);
2502}
2503TEST_CASE("Isothermal compressibility from Table A.5", "[RP1485]") {
2504 CHECK(std::abs(HumidAir::isothermal_compressibility(-60 + 273.15, 101325) / (0.10771e-9) - 1) < 1e-3);
2505 CAPTURE(HumidAir::isothermal_compressibility(80 + 273.15, 101325));
2506 CHECK(std::abs(HumidAir::isothermal_compressibility(80 + 273.15, 101325) / (0.46009e-9) - 1) < 1e-2); // Relaxed criterion for this parameter
2507 CHECK(std::abs(HumidAir::isothermal_compressibility(-60 + 273.15, 10000e3) / (0.10701e-9) - 1) < 1e-3);
2508 CHECK(std::abs(HumidAir::isothermal_compressibility(300 + 273.15, 10000e3) / (3.05896e-9) - 1) < 1e-3);
2509}
2510TEST_CASE("Henry constant from Table A.6", "[RP1485]") {
2511 CHECK(std::abs(HumidAir::HenryConstant(0.010001 + 273.15) / (0.22600e-9) - 1) < 1e-3);
2512 CHECK(std::abs(HumidAir::HenryConstant(300 + 273.15) / (0.58389e-9) - 1) < 1e-3);
2513}
2514
2515// A structure to hold the values for one call to HAProps
2516struct hel
2517{
2518 public:
2519 std::string in1, in2, in3, out;
2520 double v1, v2, v3, expected;
2521 hel(std::string in1, double v1, std::string in2, double v2, std::string in3, double v3, std::string out, double expected)
2522 : in1(in1), in2(in2), in3(in3), v1(v1), v2(v2), v3(v3), expected(expected), out(out) {
2523
2524 };
2525};
2526std::vector<hel> table_A11 = {hel("T", 473.15, "W", 0.00, "P", 101325, "B", 45.07 + 273.15), hel("T", 473.15, "W", 0.00, "P", 101325, "V", 1.341),
2527 hel("T", 473.15, "W", 0.00, "P", 101325, "H", 202520), hel("T", 473.15, "W", 0.00, "P", 101325, "S", 555.8),
2528 hel("T", 473.15, "W", 0.50, "P", 101325, "B", 81.12 + 273.15), hel("T", 473.15, "W", 0.50, "P", 101325, "V", 2.416),
2529 hel("T", 473.15, "W", 0.50, "P", 101325, "H", 1641400), hel("T", 473.15, "W", 0.50, "P", 101325, "S", 4829.5),
2530 hel("T", 473.15, "W", 1.00, "P", 101325, "B", 88.15 + 273.15), hel("T", 473.15, "W", 1.00, "P", 101325, "V", 3.489),
2531 hel("T", 473.15, "W", 1.00, "P", 101325, "H", 3079550), hel("T", 473.15, "W", 1.00, "P", 101325, "S", 8889.0)};
2532
2533std::vector<hel> table_A12 = {
2534 hel("T", 473.15, "W", 0.00, "P", 1e6, "B", 90.47 + 273.15), hel("T", 473.15, "W", 0.00, "P", 1e6, "V", 0.136),
2535 hel("T", 473.15, "W", 0.00, "P", 1e6, "H", 201940),
2536 // hel("T", 473.15, "W", 0.00, "P", 1e6, "S", -101.1), Using CoolProp 4.2, this value seems incorrect from report
2537 hel("T", 473.15, "W", 0.50, "P", 1e6, "B", 148.49 + 273.15), hel("T", 473.15, "W", 0.50, "P", 1e6, "V", 0.243),
2538 hel("T", 473.15, "W", 0.50, "P", 1e6, "H", 1630140), hel("T", 473.15, "W", 0.50, "P", 1e6, "S", 3630.2),
2539 hel("T", 473.15, "W", 1.00, "P", 1e6, "B", 159.92 + 273.15), hel("T", 473.15, "W", 1.00, "P", 1e6, "V", 0.347),
2540 hel("T", 473.15, "W", 1.00, "P", 1e6, "H", 3050210), hel("T", 473.15, "W", 1.00, "P", 1e6, "S", 7141.3)};
2541
2542std::vector<hel> table_A15 = {
2543 hel("T", 473.15, "W", 0.10, "P", 1e7, "B", 188.92 + 273.15), hel("T", 473.15, "W", 0.10, "P", 1e7, "V", 0.016),
2544 hel("T", 473.15, "W", 0.10, "P", 1e7, "H", 473920), hel("T", 473.15, "W", 0.10, "P", 1e7, "S", -90.1),
2545 hel("T", 473.15, "W", 0.10, "P", 1e7, "R", 0.734594),
2546};
2547
2548class HAPropsConsistencyFixture
2549{
2550 public:
2551 std::vector<hel> inputs;
2552 std::string in1, in2, in3, out;
2553 double v1, v2, v3, expected, actual;
2554 void set_values(hel& h) {
2555 this->in1 = h.in1;
2556 this->in2 = h.in2;
2557 this->in3 = h.in3;
2558 this->v1 = h.v1;
2559 this->v2 = h.v2;
2560 this->v3 = h.v3;
2561 this->expected = h.expected;
2562 this->out = h.out;
2563 };
2564 void call() {
2565 actual = HumidAir::HAPropsSI(out.c_str(), in1.c_str(), v1, in2.c_str(), v2, in3.c_str(), v3);
2566 }
2567};
2568
2569TEST_CASE_METHOD(HAPropsConsistencyFixture, "ASHRAE RP1485 Tables", "[RP1485]") {
2570 SECTION("Table A.15") {
2571 inputs = table_A15;
2572 for (auto& input : inputs) {
2573 set_values(input);
2574 call();
2575 CAPTURE(input.in1);
2576 CAPTURE(input.v1);
2577 CAPTURE(input.in2);
2578 CAPTURE(input.v2);
2579 CAPTURE(input.in3);
2580 CAPTURE(input.v3);
2581 CAPTURE(out);
2582 CAPTURE(actual);
2583 CAPTURE(expected);
2584 std::string errmsg = CoolProp::get_global_param_string("errstring");
2585 CAPTURE(errmsg);
2586 CHECK(std::abs(actual / expected - 1) < 0.01);
2587 }
2588 }
2589 SECTION("Table A.11") {
2590 inputs = table_A11;
2591 for (auto& input : inputs) {
2592 set_values(input);
2593 call();
2594 CAPTURE(input.in1);
2595 CAPTURE(input.v1);
2596 CAPTURE(input.in2);
2597 CAPTURE(input.v2);
2598 CAPTURE(input.in3);
2599 CAPTURE(input.v3);
2600 CAPTURE(out);
2601 CAPTURE(actual);
2602 CAPTURE(expected);
2603 std::string errmsg = CoolProp::get_global_param_string("errstring");
2604 CAPTURE(errmsg);
2605 CHECK(std::abs(actual / expected - 1) < 0.01);
2606 }
2607 }
2608 SECTION("Table A.12") {
2609 inputs = table_A12;
2610 for (auto& input : inputs) {
2611 set_values(input);
2612 call();
2613 CAPTURE(input.in1);
2614 CAPTURE(input.v1);
2615 CAPTURE(input.in2);
2616 CAPTURE(input.v2);
2617 CAPTURE(input.in3);
2618 CAPTURE(input.v3);
2619 CAPTURE(out);
2620 CAPTURE(actual);
2621 CAPTURE(expected);
2622 std::string errmsg = CoolProp::get_global_param_string("errstring");
2623 CAPTURE(errmsg);
2624 CHECK(std::abs(actual / expected - 1) < 0.01);
2625 }
2626 }
2627}
2628TEST_CASE("Assorted tests", "[HAPropsSI]") {
2629 CHECK(ValidNumber(HumidAir::HAPropsSI("T", "H", 267769, "P", 104300, "W", 0.0)));
2630 CHECK(ValidNumber(HumidAir::HAPropsSI("T", "B", 252.84, "W", 5.097e-4, "P", 101325)));
2631 CHECK(ValidNumber(HumidAir::HAPropsSI("T", "B", 290, "R", 1, "P", 101325)));
2632}
2633
2634// An inverse solve (T-from-H or W-from-X) with a target that is not achievable for the other
2635// given inputs must fail, not silently return a plausible-but-wrong value. The unbracketed
2636// branch of Brent_HAProps_T / Brent_HAProps_W extrapolates with an unbounded secant that returns
2637// its last (non-converged) iterate; without the residual validation the call below returned
2638// 58.31 C, whose actual enthalpy is ~3.6e5 J -- an order of magnitude above the requested
2639// 2.8028e4 J. At W=0.1155 the minimum achievable humid-air enthalpy is ~2.35e5 J, so 2.8028e4 J
2640// is impossible. HumidAir::HAPropsSI catches the resulting out-of-range ValueError and returns
2641// _HUGE, so the result must NOT be a valid number (bd CoolProp-eaok).
2642TEST_CASE("HAPropsSI rejects out-of-range inverse inputs", "[HAPropsSI]") {
2643 const double p = 101325, W = 0.11552736;
2644 SECTION("T from an impossible enthalpy is rejected (reported case)") {
2645 CHECK_FALSE(ValidNumber(HumidAir::HAPropsSI("T", "H", 2.8028e4, "P", p, "W", W)));
2646 }
2647 SECTION("the achievable enthalpy still solves correctly and round-trips") {
2648 double T = HumidAir::HAPropsSI("T", "H", 3.5975e5, "P", p, "W", W);
2649 REQUIRE(ValidNumber(T));
2650 CHECK(T == Catch::Approx(58.31 + 273.15).margin(0.5));
2651 // the solved T must reproduce the requested enthalpy (no spurious root)
2652 CHECK(HumidAir::HAPropsSI("H", "T", T, "P", p, "W", W) == Catch::Approx(3.5975e5).epsilon(1e-3));
2653 }
2654 SECTION("W from an impossible enthalpy is rejected") {
2655 // At T = 300 K the dry-air (W=0) enthalpy is the minimum; a negative target enthalpy is
2656 // unreachable for any humidity ratio, so solving for W must fail.
2657 CHECK_FALSE(ValidNumber(HumidAir::HAPropsSI("W", "H", -1.0e5, "P", p, "T", 300.0)));
2658 }
2659}
2660
2661// Concurrent HAPropsSI calls must each return the per-thread expected result.
2662// The Water/Air/WaterIF97 backends used internally are mutated on every call
2663// (update(), specify_phase(), clear()), so a non-thread_local implementation
2664// races on shared backend state and produces NaN, _HUGE or wildly wrong
2665// numbers under the cargo parallel test runner that hits HAPropsSI from many
2666// threads (gh-2787 follow-up).
2667TEST_CASE("HAPropsSI is thread-safe under concurrent callers", "[HAPropsSI][threadsafe]") {
2668 constexpr int n_threads = 16;
2669 constexpr int iters_per_thread = 50;
2670 const double p = 101325;
2671
2672 // Reference values computed serially before spawning workers.
2673 struct Case
2674 {
2675 double T_dry, RH;
2676 double W_ref, h_ref;
2677 };
2678 std::vector<Case> cases;
2679 for (int i = 0; i < 8; ++i) {
2680 Case c;
2681 c.T_dry = 280.0 + 5.0 * i; // 280..315 K
2682 c.RH = 0.10 + 0.10 * (i % 9); // 0.10..0.90
2683 c.W_ref = HumidAir::HAPropsSI("W", "T", c.T_dry, "R", c.RH, "P", p);
2684 c.h_ref = HumidAir::HAPropsSI("H", "T", c.T_dry, "R", c.RH, "P", p);
2685 cases.push_back(c);
2686 }
2687
2688 std::atomic<int> mismatches{0};
2689 std::atomic<int> bad_numbers{0};
2690 std::vector<std::thread> threads;
2691 threads.reserve(n_threads);
2692 for (int t = 0; t < n_threads; ++t) {
2693 threads.emplace_back([&, t]() {
2694 for (int it = 0; it < iters_per_thread; ++it) {
2695 const Case& c = cases[(t + it) % cases.size()];
2696 double W = HumidAir::HAPropsSI("W", "T", c.T_dry, "R", c.RH, "P", p);
2697 double h = HumidAir::HAPropsSI("H", "T", c.T_dry, "R", c.RH, "P", p);
2698 if (!ValidNumber(W) || !ValidNumber(h)) {
2699 bad_numbers.fetch_add(1, std::memory_order_relaxed);
2700 continue;
2701 }
2702 if (std::abs(W / c.W_ref - 1) > 1e-9 || std::abs(h / c.h_ref - 1) > 1e-9) {
2703 mismatches.fetch_add(1, std::memory_order_relaxed);
2704 }
2705 }
2706 });
2707 }
2708 for (auto& th : threads)
2709 th.join();
2710 CHECK(bad_numbers.load() == 0);
2711 CHECK(mismatches.load() == 0);
2712}
2713
2714// Hidden by Catch's [!benchmark] convention; run with
2715// ./CatchTestRunner '[!benchmark][HAPropsSI]'
2716// to get per-call timings for the thread_local conversion (gh-2787 follow-up).
2717TEST_CASE("HAPropsSI thread_local cost", "[!benchmark][HAPropsSI][threadsafe]") {
2718 const double p = 101325;
2719
2720 // Warm this thread's Water/Air/WaterIF97 backends and the per-T caches so
2721 // the first-call construction cost doesn't pollute the hot-path number.
2722 HumidAir::HAPropsSI("H", "T", 300.0, "R", 0.5, "P", p);
2723
2724 BENCHMARK("HAPropsSI H,T=300,R=0.5,P [hot, single-thread]") {
2725 return HumidAir::HAPropsSI("H", "T", 300.0, "R", 0.5, "P", p);
2726 };
2727
2728 // Fresh-thread cost: each Catch sample spawns a thread that pays its own
2729 // backend construction (Water + Air + IF97) on first HAPropsSI entry, then
2730 // exits. Captures the per-thread one-shot init overhead introduced by the
2731 // thread_local conversion.
2732 BENCHMARK("HAPropsSI H,T=300,R=0.5,P [first call in fresh thread]") {
2733 double result = 0.0;
2734 std::thread th([&] { result = HumidAir::HAPropsSI("H", "T", 300.0, "R", 0.5, "P", p); });
2735 th.join();
2736 return result;
2737 };
2738}
2739
2740// a predicate implemented as a function:
2741bool is_not_a_pair(const std::set<std::size_t>& item) {
2742 return item.size() != 2;
2743}
2744
2745const int number_of_inputs = 6;
2746std::string inputs[number_of_inputs] = {"W", "D", "B", "R", "T", "V"}; //,"H","S"};
2747
2748class ConsistencyTestData
2749{
2750 public:
2751 bool is_built;
2752 std::vector<Dictionary> data;
2753 std::list<std::set<std::size_t>> inputs_list;
2754 ConsistencyTestData()
2755 : is_built(false) {
2756
2757 };
2758 void build() {
2759 if (is_built) {
2760 return;
2761 }
2762 std::vector<std::size_t> indices(number_of_inputs);
2763 for (std::size_t i = 0; i < number_of_inputs; ++i) {
2764 indices[i] = i;
2765 }
2766 // Generate a powerset of all the permutations of all lengths of inputs
2767 std::set<std::size_t> indices_set(indices.begin(), indices.end());
2768 std::set<std::set<std::size_t>> inputs_powerset = powerset(indices_set);
2769 inputs_list = std::list<std::set<std::size_t>>(inputs_powerset.begin(), inputs_powerset.end());
2770 inputs_list.remove_if(is_not_a_pair);
2771
2772 const int NT = 10, NW = 5;
2773 double p = 101325;
2774 // Integer-indexed grid (cert-flp30-c): the original
2775 // for (T = 210; T < 350; T += (350-210)/(NT-1))
2776 // yields NT-1 iterations under the `<` exit (FP-roundoff
2777 // sensitive at the upper bound); preserve that count exactly.
2778 const double T_lo = 210.0, T_hi = 350.0;
2779 const double dT = (T_hi - T_lo) / (NT - 1);
2780 for (int it = 0; it < NT - 1; ++it) {
2781 const double T = T_lo + dT * it;
2782 double Wsat = HumidAir::HAPropsSI("W", "T", T, "P", p, "R", 1.0);
2783 const double W_lo = 1e-5;
2784 const double dW = (Wsat - W_lo) / (NW - 1);
2785 for (int iw = 0; iw < NW - 1; ++iw) {
2786 const double W = W_lo + dW * iw;
2787 Dictionary vals;
2788 // Calculate all the values using T, W
2789 for (const auto& input : inputs) {
2790 double v = HumidAir::HAPropsSI(input, "T", T, "P", p, "W", W);
2791 vals.add_number(input, v);
2792 }
2793 data.push_back(vals);
2794 std::cout << format("T %g W %g\n", T, W);
2795 }
2796 }
2797 is_built = true;
2798 };
2799} consistency_data;
2800
2801TEST_CASE("HAProps tests", "[HAProps]") {
2802 Eigen::ArrayXd Tdb = Eigen::ArrayXd::LinSpaced(100, -10, 55) + 273.15;
2803 for (auto Tdb_ : Tdb) {
2804 CAPTURE(Tdb_);
2805 CHECK(ValidNumber(HumidAir::HAProps("W", "T", Tdb_, "R", 1, "P", 101.325)));
2806 }
2807}
2808
2809/*
2810 * This test is incredibly slow, which is why it is currently commented out. Many of the tests also fail
2811 *
2812TEST_CASE("HAPropsSI", "[HAPropsSI]")
2813{
2814 consistency_data.build();
2815 double p = 101325;
2816 for (std::size_t i = 0; i < consistency_data.data.size(); ++i)
2817 {
2818 for (std::list<std::set<std::size_t> >::iterator iter = consistency_data.inputs_list.begin(); iter != consistency_data.inputs_list.end(); ++iter)
2819 {
2820 std::vector<std::size_t> pair(iter->begin(), iter->end());
2821 std::string i0 = inputs[pair[0]], i1 = inputs[pair[1]];
2822 double v0 = consistency_data.data[i].get_double(i0), v1 = consistency_data.data[i].get_double(i1);
2823 if ((i0 == "B" && i1 == "V") || (i1 == "B" && i0 == "V")){continue;}
2824 std::ostringstream ss2;
2825 ss2 << "Inputs: \"" << i0 << "\"," << v0 << ",\"" << i1 << "\"," << v1;
2826 SECTION(ss2.str(), ""){
2827
2828 double T = consistency_data.data[i].get_double("T");
2829 double W = consistency_data.data[i].get_double("W");
2830 double Wcalc = HumidAir::HAPropsSI("W", i0, v0, i1, v1, "P", p);
2831 double Tcalc = HumidAir::HAPropsSI("T", i0, v0, i1, v1, "P", p);
2832 std::string err = CoolProp::get_global_param_string("errstring");
2833 CAPTURE(T);
2834 CAPTURE(W);
2835 CAPTURE(Tcalc);
2836 CAPTURE(Wcalc);
2837 CAPTURE(err);
2838 CHECK(std::abs(Tcalc - T) < 1e-1);
2839 CHECK(std::abs((Wcalc - W)/W) < 1e-3);
2840 }
2841 }
2842 }
2843}
2844 */
2845
2846#endif /* CATCH_ENABLED */