CoolProp 7.1.0
An open-source fluid property and humid air property database
FlashRoutines.cpp
Go to the documentation of this file.
1#include "VLERoutines.h"
2#include "FlashRoutines.h"
6#include "Configuration.h"
7
8#if defined(ENABLE_CATCH)
9# include <catch2/catch_all.hpp>
11#endif
12
13#include "boost/math/tools/toms748_solve.hpp"
14
15namespace CoolProp {
16
18 if (HEOS.PhaseEnvelope.built) {
19 // Use the phase envelope if already constructed to determine phase boundary
20 // Determine whether you are inside (two-phase) or outside (single-phase)
21 SimpleState closest_state;
22 std::size_t i;
23 bool twophase = PhaseEnvelopeRoutines::is_inside(HEOS.PhaseEnvelope, iP, HEOS._p, iT, HEOS._T, i, closest_state);
24 if (!twophase && HEOS._T > closest_state.T) {
25 // Gas solution - bounded between phase envelope temperature and very high temperature
26 //
27 // Start with a guess value from SRK
28 CoolPropDbl rhomolar_guess = HEOS.solver_rho_Tp_SRK(HEOS._T, HEOS._p, iphase_gas);
29
30 solver_TP_resid resid(HEOS, HEOS._T, HEOS._p);
31 std::string errstr;
33 try {
34 // Try using Newton's method
35 CoolPropDbl rhomolar = Newton(resid, rhomolar_guess, 1e-10, 100);
36 // Make sure the solution is within the bounds
37 if (!is_in_closed_range(static_cast<CoolPropDbl>(closest_state.rhomolar), static_cast<CoolPropDbl>(0.0), rhomolar)) {
38 throw ValueError("out of range");
39 }
40 HEOS.update_DmolarT_direct(rhomolar, HEOS._T);
41 } catch (...) {
42 // If that fails, try a bounded solver
43 CoolPropDbl rhomolar = Brent(resid, closest_state.rhomolar, 1e-10, DBL_EPSILON, 1e-10, 100);
44 // Make sure the solution is within the bounds
45 if (!is_in_closed_range(static_cast<CoolPropDbl>(closest_state.rhomolar), static_cast<CoolPropDbl>(0.0), rhomolar)) {
46 throw ValueError("out of range");
47 }
48 }
49 HEOS.unspecify_phase();
50 HEOS._Q = -1;
51 } else {
52 // Liquid solution
53 throw ValueError();
54 }
55 } else {
57 // Blind flash call
58 // Following the strategy of Gernert, 2014
60 if (!stability_tester.is_stable()) {
61 // There is a phase split and liquid and vapor phases are formed
62 CoolProp::SaturationSolvers::PTflash_twophase_options o;
63 stability_tester.get_liq(o.x, o.rhomolar_liq);
64 stability_tester.get_vap(o.y, o.rhomolar_vap);
65 o.z = HEOS.get_mole_fractions();
66 o.T = HEOS.T();
67 o.p = HEOS.p();
68 o.omega = 1.0;
69 CoolProp::SaturationSolvers::PTflash_twophase solver(HEOS, o);
70 solver.solve();
72 HEOS._Q = (o.z[0] - o.x[0]) / (o.y[0] - o.x[0]); // All vapor qualities are the same (these are the residuals in the solver)
73 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
74 } else {
75 // It's single-phase
76 double rho = HEOS.solver_rho_Tp_global(HEOS.T(), HEOS.p(), 20000);
77 HEOS.update_DmolarT_direct(rho, HEOS.T());
78 HEOS._Q = -1;
79 HEOS._phase = iphase_liquid;
80 }
81 } else {
82 // It's single-phase, and phase is imposed
83 double rho = HEOS.solver_rho_Tp(HEOS.T(), HEOS.p());
84 HEOS.update_DmolarT_direct(rho, HEOS.T());
85 HEOS._Q = -1;
86 HEOS._phase = HEOS.imposed_phase_index;
87 }
88 }
89}
91 if (HEOS.is_pure_or_pseudopure) {
92 if (HEOS.imposed_phase_index == iphase_not_imposed) // If no phase index is imposed (see set_components function)
93 {
94 // At very low temperature (near the triple point temp), the isotherms are VERY steep
95 // Thus it can be very difficult to determine state based on ps = f(T)
96 // So in this case, we do a phase determination based on p, generally it will be useful enough
97 if (HEOS._T < 0.9 * HEOS.Ttriple() + 0.1 * HEOS.calc_Tmax_sat()) {
98 // Find the phase, while updating all internal variables possible using the pressure
99 bool saturation_called = false;
100 HEOS.p_phase_determination_pure_or_pseudopure(iT, HEOS._T, saturation_called);
101 } else {
102 // Find the phase, while updating all internal variables possible using the temperature
104 }
105 // Check if twophase solution
106 if (!HEOS.isHomogeneousPhase()) {
107 throw ValueError("twophase not implemented yet");
108 }
109 } else {
110 // Phase is imposed. Update _phase in case it was reset elsewhere by another call
111 HEOS._phase = HEOS.imposed_phase_index;
112 }
113 // Find density
114 HEOS._rhomolar = HEOS.solver_rho_Tp(HEOS._T, HEOS._p);
115 HEOS._Q = -1;
116 } else {
117 PT_flash_mixtures(HEOS);
118 }
119}
120
121// Define the residual to be driven to zero
123{
124 public:
128 double call(double T) {
130 CoolPropDbl peos = HEOS->p();
131 CoolPropDbl r = (peos - p) / p;
132 return r;
133 };
134 double deriv(double T) {
135 // dp/dT|rho / pspecified
136 return HEOS->first_partial_deriv(iP, iT, iDmolar) / p;
137 };
138 double second_deriv(double T) {
139 // d2p/dT2|rho / pspecified
141 };
142};
143
144/***
145\f[
146\begin{array}{l}
147p = \frac{{RT}}{{v - b}} - \frac{{a\alpha }}{{v\left( {v + b} \right)}}\\
148\alpha = \left( {1 + \kappa \left( {1 - \sqrt {{T_r}} } \right)} \right)\left( {1 + \kappa \left( {1 - \sqrt {{T_r}} } \right)} \right) = 1 + 2\kappa \left( {1 - \sqrt {{T_r}} } \right) + {\kappa ^2}{\left( {1 - \sqrt {{T_r}} } \right)^2}\\
149\alpha = 1 + 2\kappa \left( {1 - \sqrt {{T_r}} } \right) + {\kappa ^2}{\left( {1 - \sqrt {{T_r}} } \right)^2}\\
150\alpha = 1 + 2\kappa - 2\kappa \sqrt {{T_r}} + {\kappa ^2}\left[ {1 - 2\sqrt {{T_r}} + {T_r}} \right]\\
151T = {T_r}{T_c}\\
152p = \frac{{R{T_r}{T_c}}}{{v - b}} - \frac{{a\left( {1 + 2\kappa - 2\kappa \sqrt {{T_r}} + {\kappa ^2}\left[ {1 - 2\sqrt {{T_r}} + {T_r}} \right]} \right)}}{{v\left( {v + b} \right)}}\\
153\\
154{\rm{Factor in terms of }}\sqrt {{T_r}} \\
155\\
156p = \frac{{R{T_r}{T_c}}}{{v - b}} - \frac{{a\left( {1 + 2\kappa + {\kappa ^2} - 2\kappa \sqrt {{T_r}} + {\kappa ^2}\left[ { - 2\sqrt {{T_r}} + {T_r}} \right]} \right)}}{{v\left( {v + b} \right)}}\\
157p = \frac{{R{T_r}{T_c}}}{{v - b}} - \frac{{a\left( {1 + 2\kappa + {\kappa ^2} - 2\kappa (1 + \kappa )\sqrt {{T_r}} + {\kappa ^2}{T_r}} \right)}}{{v\left( {v + b} \right)}}\\
158p = \frac{{R{T_r}{T_c}}}{{v - b}} - \frac{{a\left( {1 + 2\kappa + {\kappa ^2}} \right)}}{{v\left( {v + b} \right)}} + \frac{{2a\kappa (1 + \kappa )}}{{v\left( {v + b} \right)}}\sqrt {{T_r}} - \frac{{a{\kappa ^2}}}{{v\left( {v + b} \right)}}{T_r}\\
1590 = \left[ {\frac{{R{T_c}}}{{v - b}} - \frac{{a{\kappa ^2}}}{{v\left( {v + b} \right)}}} \right]{T_r} + \frac{{2a\kappa (1 + \kappa )}}{{v\left( {v + b} \right)}}\sqrt {{T_r}} - \frac{{a\left( {1 + 2\kappa + {\kappa ^2}} \right)}}{{v\left( {v + b} \right)}} - p
160\end{array}
161\f]
162 */
163double FlashRoutines::T_DP_PengRobinson(HelmholtzEOSMixtureBackend& HEOS, double rhomolar, double p) {
164 double omega, R, kappa, a, b, A, B, C, Tc, pc, V = 1 / rhomolar;
165 omega = HEOS.acentric_factor();
166 Tc = HEOS.T_critical();
167 pc = HEOS.p_critical();
168 R = HEOS.gas_constant();
169
170 kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega * omega;
171 a = 0.457235 * R * R * Tc * Tc / pc;
172 b = 0.077796 * R * Tc / pc;
173 double den = V * V + 2 * b * V - b * b;
174
175 // A sqrt(Tr)^2 + B sqrt(Tr) + C = 0
176 A = R * Tc / (V - b) - a * kappa * kappa / (den);
177 B = +2 * a * kappa * (1 + kappa) / (den);
178 C = -a * (1 + 2 * kappa + kappa * kappa) / (den)-p;
179
180 //D = B*B-4*A*C;
181
182 double sqrt_Tr1 = (-B + sqrt(B * B - 4 * A * C)) / (2 * A);
183 //double sqrt_Tr2 = (-B-sqrt(B*B-4*A*C))/(2*A);
184 return sqrt_Tr1 * sqrt_Tr1 * Tc;
185};
186
188 // Comment out the check for an imposed phase. There's no code to handle if it is!
189 // Solver below and flash calculations (if two phase) have to be called anyway.
190 //
191 // if (HEOS.imposed_phase_index == iphase_not_imposed) // If no phase index is imposed (see set_components function)
192 // {
193 if (HEOS.is_pure_or_pseudopure) {
194 // Find the phase, while updating all internal variables possible using the pressure
195 bool saturation_called = false;
196 HEOS.p_phase_determination_pure_or_pseudopure(iDmolar, HEOS._rhomolar, saturation_called);
197
198 if (HEOS.isHomogeneousPhase()) {
199 CoolPropDbl T0;
200 if (HEOS._phase == iphase_liquid) {
201 // If it is a liquid, start off at the ancillary value
202 if (saturation_called) {
203 T0 = HEOS.SatL->T();
204 } else {
205 T0 = HEOS._TLanc.pt();
206 }
207 } else if (HEOS._phase == iphase_supercritical_liquid) {
208 // If it is a supercritical
209 T0 = 1.1 * HEOS.T_critical();
210 } else if (HEOS._phase == iphase_gas || HEOS._phase == iphase_supercritical_gas || HEOS._phase == iphase_supercritical) {
211 // First, get a guess for density from Peng-Robinson
212 T0 = T_DP_PengRobinson(HEOS, HEOS.rhomolar(), HEOS.p());
213 } else {
214 throw ValueError("I should never get here");
215 }
216 if (!std::isfinite(T0)){
217 throw ValueError("Starting value of T0 is not valid in DP_flash");
218 }
219 // Then, do the solver using the full EOS
220 solver_DP_resid resid(&HEOS, HEOS.rhomolar(), HEOS.p());
221 std::string errstr;
222 Halley(resid, T0, 1e-10, 100);
223 HEOS._Q = -1;
224 // Update the state for conditions where the state was guessed
226 if (!get_config_bool(DONT_CHECK_PROPERTY_LIMITS) && HEOS._T > 1.5*HEOS.Tmax()){
227 throw CoolProp::OutOfRangeError(format("DP yielded T > 1.5Tmax w/ T (%g) K").c_str());
228 }
229 } else {
230 // Nothing to do here; phase determination has handled this already
231 }
232 } else {
233 throw NotImplementedError("DP_flash not ready for mixtures");
234 }
235 // }
236 // TO DO: Put the imposed phase check back in
237 // and provide the else code here if it is imposed.
238}
239
241{
242 public:
246 double call(double T) {
247 HEOS.update(QT_INPUTS, 0, T); // Doesn't matter whether liquid or vapor, we are just doing a full VLE call for given T
251 return (1 / rhomolar - 1 / rhoL) / (1 / rhoV - 1 / rhoL) - Q_target;
252 }
253 double deriv(double T) {
254 return _HUGE;
255 }
256 double second_deriv(double T) {
257 return _HUGE;
258 }
259};
260
263 options.use_logdelta = false;
265 if (HEOS.is_pure_or_pseudopure) {
266 // Bump the temperatures to hopefully yield more reliable results
267 double Tmax = HEOS.T_critical() - 0.1;
268 double Tmin = HEOS.Tmin() + 0.1;
269 double rhomolar = HEOS._rhomolar;
270 double Q = HEOS._Q;
271 const double eps = 1e-12; // small tolerance to allow for slop in iterative calculations
272 if (rhomolar >= (HEOS.rhomolar_critical() + eps) && Q > (0 + eps)){
273 throw CoolProp::OutOfRangeError(format("DQ inputs are not defined for density (%g) above critical density (%g) and Q>0", rhomolar, HEOS.rhomolar_critical()).c_str());
274 }
275 DQ_flash_residual resid(HEOS, rhomolar, Q);
276 Brent(resid, Tmin, Tmax, DBL_EPSILON, 1e-10, 100);
277 HEOS._p = HEOS.SatV->p();
278 HEOS._T = HEOS.SatV->T();
279 HEOS._rhomolar = rhomolar;
280 HEOS._Q = Q;
281 HEOS._phase = iphase_twophase;
282 } else {
283 throw NotImplementedError("DQ_flash not ready for mixtures");
284 }
285}
288 options.use_logdelta = false;
290 if (Tguess < 0) {
291 options.use_guesses = true;
292 options.T = Tguess;
293 CoolProp::SaturationAncillaryFunction& rhoL = HEOS.get_components()[0].ancillaries.rhoL;
294 CoolProp::SaturationAncillaryFunction& rhoV = HEOS.get_components()[0].ancillaries.rhoV;
295 options.rhoL = rhoL.evaluate(Tguess);
296 options.rhoV = rhoV.evaluate(Tguess);
297 }
298 if (HEOS.is_pure_or_pseudopure) {
299 if (std::abs(HEOS.Q() - 1) > 1e-10) {
300 throw ValueError(format("non-unity quality not currently allowed for HQ_flash"));
301 }
302 // Do a saturation call for given h for vapor, first with ancillaries, then with full saturation call
304 SaturationSolvers::saturation_PHSU_pure(HEOS, HEOS.hmolar(), options);
305 HEOS._p = HEOS.SatV->p();
306 HEOS._T = HEOS.SatV->T();
307 HEOS._rhomolar = HEOS.SatV->rhomolar();
308 HEOS._phase = iphase_twophase;
309 } else {
310 throw NotImplementedError("HQ_flash not ready for mixtures");
311 }
312}
314 if (HEOS.is_pure_or_pseudopure) {
315
316 if (std::abs(HEOS.smolar() - HEOS.get_state("reducing").smolar) < 0.001) {
317 HEOS._p = HEOS.p_critical();
318 HEOS._T = HEOS.T_critical();
319 HEOS._rhomolar = HEOS.rhomolar_critical();
321 } else if (std::abs(HEOS.Q()) < 1e-10) {
322 // Do a saturation call for given s for liquid, first with ancillaries, then with full saturation call
325 options.use_logdelta = false;
327 SaturationSolvers::saturation_PHSU_pure(HEOS, HEOS.smolar(), options);
328 HEOS._p = HEOS.SatL->p();
329 HEOS._T = HEOS.SatL->T();
330 HEOS._rhomolar = HEOS.SatL->rhomolar();
331 HEOS._phase = iphase_twophase;
332 } else if (std::abs(HEOS.Q() - 1) < 1e-10) {
333 // Do a saturation call for given s for vapor, first with ancillaries, then with full saturation call
336 options.use_logdelta = false;
338 SaturationSolvers::saturation_PHSU_pure(HEOS, HEOS.smolar(), options);
339 HEOS._p = HEOS.SatV->p();
340 HEOS._T = HEOS.SatV->T();
341 HEOS._rhomolar = HEOS.SatV->rhomolar();
342 HEOS._phase = iphase_twophase;
343 } else {
344 throw ValueError(format("non-zero or 1 quality not currently allowed for QS_flash"));
345 }
346 } else {
347 throw NotImplementedError("QS_flash not ready for mixtures");
348 }
349}
351 CoolPropDbl T = HEOS._T;
352 CoolPropDbl Q = HEOS._Q;
353 if (HEOS.is_pure_or_pseudopure) {
354
355 if (get_config_bool(ENABLE_SUPERANCILLARIES) && HEOS.is_pure()){
356 auto& optsuperanc = HEOS.get_superanc_optional();
357 if (optsuperanc){
358 auto& superanc = optsuperanc.value();
359
360 CoolPropDbl Tcrit_num = superanc.get_Tcrit_num();
361 if (T > Tcrit_num){
362 throw ValueError(format("Temperature to QT_flash [%0.8Lg K] may not be above the numerical critical point of %0.15Lg K", T, Tcrit_num));
363 }
364 auto rhoL = superanc.eval_sat(T, 'D', 0);
365 auto rhoV = superanc.eval_sat(T, 'D', 1);
366 auto p = superanc.eval_sat(T, 'P', 1);
367 HEOS.SatL->update_TDmolarP_unchecked(T, rhoL, p);
368 HEOS.SatV->update_TDmolarP_unchecked(T, rhoV, p);
369 HEOS._p = p;
370 HEOS._rhomolar = 1 / (Q / rhoV + (1 - Q) / rhoL);
371 HEOS._phase = iphase_twophase;
372 return;
373 }
374 }
375
376
377 // The maximum possible saturation temperature
378 // Critical point for pure fluids, slightly different for pseudo-pure, very different for mixtures
379 CoolPropDbl Tmax_sat = HEOS.calc_Tmax_sat() + 1e-13;
380
381 // Check what the minimum limits for the equation of state are
382 CoolPropDbl Tmin_satL, Tmin_satV, Tmin_sat;
383 HEOS.calc_Tmin_sat(Tmin_satL, Tmin_satV);
384 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
385
386 // Get a reference to keep the code a bit cleaner
387 const CriticalRegionSplines& splines = HEOS.components[0].EOS().critical_region_splines;
388
389 if ((get_config_bool(CRITICAL_WITHIN_1UK) && std::abs(T - Tmax_sat) < 1e-6) || std::abs(T - Tmax_sat) < 1e-12) {
390 // If exactly(ish) at the critical temperature, liquid and vapor have the critial density
391 HEOS.SatL->update(DmolarT_INPUTS, HEOS.rhomolar_critical(), HEOS._T);
392 HEOS.SatV->update(DmolarT_INPUTS, HEOS.rhomolar_critical(), HEOS._T);
393 HEOS._rhomolar = HEOS.rhomolar_critical();
394 HEOS._p = 0.5 * HEOS.SatV->p() + 0.5 * HEOS.SatL->p();
395 } else if (!is_in_closed_range(Tmin_sat - 0.1, Tmax_sat, T) && (CoolProp::get_config_bool(DONT_CHECK_PROPERTY_LIMITS) == false)) {
396 throw ValueError(format("Temperature to QT_flash [%0.8Lg K] must be in range [%0.8Lg K, %0.8Lg K]", T, Tmin_sat - 0.1, Tmax_sat));
397 } else if (get_config_bool(CRITICAL_SPLINES_ENABLED) && splines.enabled && HEOS._T > splines.T_min) {
398 double rhoL = _HUGE, rhoV = _HUGE;
399 // Use critical region spline if it has it and temperature is in its range
400 splines.get_densities(T, splines.rhomolar_min, HEOS.rhomolar_critical(), splines.rhomolar_max, rhoL, rhoV);
401 HEOS.SatL->update(DmolarT_INPUTS, rhoL, HEOS._T);
402 HEOS.SatV->update(DmolarT_INPUTS, rhoV, HEOS._T);
403 HEOS._p = 0.5 * HEOS.SatV->p() + 0.5 * HEOS.SatL->p();
404 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
405 } else if (!(HEOS.components[0].EOS().pseudo_pure)) {
406 // Set some input options
408
409 // Actually call the solver
411
412 HEOS._p = 0.5 * HEOS.SatV->p() + 0.5 * HEOS.SatL->p();
413 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
414 } else {
415 // Pseudo-pure fluid
416 CoolPropDbl rhoLanc = _HUGE, rhoVanc = _HUGE, rhoLsat = _HUGE, rhoVsat = _HUGE;
417 if (std::abs(HEOS._Q) < DBL_EPSILON) {
418 HEOS._p = HEOS.components[0].ancillaries.pL.evaluate(HEOS._T); // These ancillaries are used explicitly
419 rhoLanc = HEOS.components[0].ancillaries.rhoL.evaluate(HEOS._T);
420 HEOS.SatL->update_TP_guessrho(HEOS._T, HEOS._p, rhoLanc);
421 HEOS._rhomolar = HEOS.SatL->rhomolar();
422 } else if (std::abs(HEOS._Q - 1) < DBL_EPSILON) {
423 HEOS._p = HEOS.components[0].ancillaries.pV.evaluate(HEOS._T); // These ancillaries are used explicitly
424 rhoVanc = HEOS.components[0].ancillaries.rhoV.evaluate(HEOS._T);
425 HEOS.SatV->update_TP_guessrho(HEOS._T, HEOS._p, rhoVanc);
426 HEOS._rhomolar = HEOS.SatV->rhomolar();
427 } else {
428 throw CoolProp::ValueError(format("For pseudo-pure fluid, quality must be equal to 0 or 1. Two-phase quality is not defined"));
429 }
430
431 try {
432 } catch (...) {
433 // Near the critical point, the behavior is not very nice, so we will just use the ancillary
434 rhoLsat = rhoLanc;
435 rhoVsat = rhoVanc;
436 }
437 }
438 // Load the outputs
439 HEOS._phase = iphase_twophase;
440 } else {
441 if (HEOS.PhaseEnvelope.built) {
442 PT_Q_flash_mixtures(HEOS, iT, HEOS._T);
443 } else {
444 // Set some input options
447 options.Nstep_max = 20;
448
449 // Get an extremely rough guess by interpolation of ln(p) v. T curve where the limits are mole-fraction-weighted
451
452 // Use Wilson iteration to obtain updated guess for pressure
453 pguess = SaturationSolvers::saturation_Wilson(HEOS, HEOS._Q, HEOS._T, SaturationSolvers::imposed_T, HEOS.mole_fractions, pguess);
454
455 // Actually call the successive substitution solver
456 SaturationSolvers::successive_substitution(HEOS, HEOS._Q, HEOS._T, pguess, HEOS.mole_fractions, HEOS.K, options);
457
458 // -----
459 // Newton-Raphson
460 // -----
461
462 SaturationSolvers::newton_raphson_saturation NR;
463 SaturationSolvers::newton_raphson_saturation_options IO;
464
465 IO.bubble_point = (HEOS._Q < 0.5);
466
467 IO.x = options.x;
468 IO.y = options.y;
469 IO.rhomolar_liq = options.rhomolar_liq;
470 IO.rhomolar_vap = options.rhomolar_vap;
471 IO.T = options.T;
472 IO.p = options.p;
473 IO.Nstep_max = 30;
474
475 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::T_IMPOSED;
476
477 if (IO.bubble_point) {
478 // Compositions are z, z_incipient
479 NR.call(HEOS, IO.x, IO.y, IO);
480 } else {
481 // Compositions are z, z_incipient
482 NR.call(HEOS, IO.y, IO.x, IO);
483 }
484
485 HEOS._p = IO.p;
486 HEOS._rhomolar = 1 / (HEOS._Q / IO.rhomolar_vap + (1 - HEOS._Q) / IO.rhomolar_liq);
487 }
488 // Load the outputs
489 HEOS._phase = iphase_twophase;
490 HEOS._p = HEOS.SatV->p();
491 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
492 HEOS._T = HEOS.SatL->T();
493 }
494}
495
496void get_Henrys_coeffs_FP(const std::string& CAS, double& A, double& B, double& C, double& Tmin, double& Tmax) {
497 // Coeffs from Fernandez-Prini JPCRD 2003 DOI: 10.1063/1.1564818
498 if (CAS == "7440-59-7") //Helium
499 {
500 A = -3.52839;
501 B = 7.12983;
502 C = 4.47770;
503 Tmin = 273.21;
504 Tmax = 553.18;
505 } else if (CAS == "7440-01-9") // Ne
506 {
507 A = -3.18301;
508 B = 5.31448;
509 C = 5.43774;
510 Tmin = 273.20;
511 Tmax = 543.36;
512 } else if (CAS == "7440-37-1") // Ar
513 {
514 A = -8.40954;
515 B = 4.29587;
516 C = 10.52779;
517 Tmin = 273.19;
518 Tmax = 568.36;
519 } else if (CAS == "7439-90-9") // Kr
520 {
521 A = -8.97358;
522 B = 3.61508;
523 C = 11.29963;
524 Tmin = 273.19;
525 Tmax = 525.56;
526 } else if (CAS == "7440-63-3") // Xe
527 {
528 A = -14.21635;
529 B = 4.00041;
530 C = 15.60999;
531 Tmin = 273.22;
532 Tmax = 574.85;
533 } else if (CAS == "1333-74-0") // H2
534 {
535 A = -4.73284;
536 B = 6.08954;
537 C = 6.06066;
538 Tmin = 273.15;
539 Tmax = 636.09;
540 } else if (CAS == "7727-37-9") // N2
541 {
542 A = -9.67578;
543 B = 4.72162;
544 C = 11.70585;
545 Tmin = 278.12;
546 Tmax = 636.46;
547 } else if (CAS == "7782-44-7") // O2
548 {
549 A = -9.44833;
550 B = 4.43822;
551 C = 11.42005;
552 Tmin = 274.15;
553 Tmax = 616.52;
554 } else if (CAS == "630-08-0") // CO
555 {
556 A = -10.52862;
557 B = 5.13259;
558 C = 12.01421;
559 Tmin = 278.15;
560 Tmax = 588.67;
561 } else if (CAS == "124-38-9") // CO2
562 {
563 A = -8.55445;
564 B = 4.01195;
565 C = 9.52345;
566 Tmin = 274.19;
567 Tmax = 642.66;
568 } else if (CAS == "7783-06-4") // H2S
569 {
570 A = -4.51499;
571 B = 5.23538;
572 C = 4.42126;
573 Tmin = 273.15;
574 Tmax = 533.09;
575 } else if (CAS == "74-82-8") // CH4
576 {
577 A = -10.44708;
578 B = 4.66491;
579 C = 12.12986;
580 Tmin = 275.46;
581 Tmax = 633.11;
582 } else if (CAS == "74-84-0") // C2H6
583 {
584 A = -19.67563;
585 B = 4.51222;
586 C = 20.62567;
587 Tmin = 275.44;
588 Tmax = 473.46;
589 } else if (CAS == "2551-62-4") // SF6
590 {
591 A = -16.56118;
592 B = 2.15289;
593 C = 20.35440;
594 Tmin = 283.14;
595 Tmax = 505.55;
596 } else {
597 throw ValueError("Bad component in Henry's law constants");
598 }
599}
601 if (HEOS.is_pure_or_pseudopure) {
602
603 if (get_config_bool(ENABLE_SUPERANCILLARIES) && HEOS.is_pure()){
604 auto& optsuperanc = HEOS.get_superanc_optional();
605 if (optsuperanc){
606 auto& superanc = optsuperanc.value();
607 CoolPropDbl pmax_num = superanc.get_pmax();
608 if (HEOS._p > pmax_num){
609 throw ValueError(format("Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS._p, pmax_num));
610 }
611 auto T = superanc.get_T_from_p(HEOS._p);
612 auto rhoL = superanc.eval_sat(T, 'D', 0);
613 auto rhoV = superanc.eval_sat(T, 'D', 1);
614 auto p = HEOS._p;
615 HEOS.SatL->update_TDmolarP_unchecked(T, rhoL, p);
616 HEOS.SatV->update_TDmolarP_unchecked(T, rhoV, p);
617 HEOS._T = T;
618 HEOS._p = p;
619 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
620 HEOS._phase = iphase_twophase;
621 return;
622 }
623 }
624
625 if (HEOS.components[0].EOS().pseudo_pure) {
626 // It is a pseudo-pure mixture
627
628 HEOS._TLanc = HEOS.components[0].ancillaries.pL.invert(HEOS._p);
629 HEOS._TVanc = HEOS.components[0].ancillaries.pV.invert(HEOS._p);
630 // Get guesses for the ancillaries for density
631 CoolPropDbl rhoL = HEOS.components[0].ancillaries.rhoL.evaluate(HEOS._TLanc);
632 CoolPropDbl rhoV = HEOS.components[0].ancillaries.rhoV.evaluate(HEOS._TVanc);
633 // Solve for the density
634 HEOS.SatL->update_TP_guessrho(HEOS._TLanc, HEOS._p, rhoL);
635 HEOS.SatV->update_TP_guessrho(HEOS._TVanc, HEOS._p, rhoV);
636
637 // Load the outputs
638 HEOS._phase = iphase_twophase;
639 HEOS._p = HEOS._Q * HEOS.SatV->p() + (1 - HEOS._Q) * HEOS.SatL->p();
640 HEOS._T = HEOS._Q * HEOS.SatV->T() + (1 - HEOS._Q) * HEOS.SatL->T();
641 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
642 } else {
643 // Critical point for pure fluids, slightly different for pseudo-pure, very different for mixtures
644 CoolPropDbl pmax_sat = HEOS.calc_pmax_sat();
645
646 // Check what the minimum limits for the equation of state are
647 CoolPropDbl pmin_satL, pmin_satV, pmin_sat;
648 HEOS.calc_pmin_sat(pmin_satL, pmin_satV);
649 pmin_sat = std::max(pmin_satL, pmin_satV);
650
651 // Check for being AT the critical point
652 if (is_in_closed_range(pmax_sat * (1 - 1e-10), pmax_sat * (1 + 1e-10), static_cast<CoolPropDbl>(HEOS._p))) {
653 // Load the outputs
655 HEOS._p = HEOS.p_critical();
656 HEOS._rhomolar = HEOS.rhomolar_critical();
657 HEOS._T = HEOS.T_critical();
658 return;
659 }
660
661 // Check limits
662 if (CoolProp::get_config_bool(DONT_CHECK_PROPERTY_LIMITS) == false) {
663 if (!is_in_closed_range(pmin_sat * 0.999999, pmax_sat * 1.000001, static_cast<CoolPropDbl>(HEOS._p))) {
664 throw ValueError(format("Pressure to PQ_flash [%6g Pa] must be in range [%8Lg Pa, %8Lg Pa]", HEOS._p, pmin_sat, pmax_sat));
665 }
666 }
667 // ------------------
668 // It is a pure fluid
669 // ------------------
670
671 // Set some input options
673 // Specified variable is pressure
675 // Use logarithm of delta as independent variables
676 options.use_logdelta = false;
677
678 double increment = 0.4;
679
680 try {
681 for (double omega = 1.0; omega > 0; omega -= increment) {
682 try {
683 options.omega = omega;
684
685 // Actually call the solver
686 SaturationSolvers::saturation_PHSU_pure(HEOS, HEOS._p, options);
687
688 // If you get here, there was no error, all is well
689 break;
690 } catch (...) {
691 if (omega < 1.1 * increment) {
692 throw;
693 }
694 // else we are going to try again with a smaller omega
695 }
696 }
697 } catch (...) {
698 // We may need to polish the solution at low pressure
700 }
701
702 // Load the outputs
703 HEOS._phase = iphase_twophase;
704 HEOS._p = HEOS._Q * HEOS.SatV->p() + (1 - HEOS._Q) * HEOS.SatL->p();
705 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
706 HEOS._T = HEOS.SatL->T();
707 }
708 } else {
709 if (HEOS.PhaseEnvelope.built) {
710 PT_Q_flash_mixtures(HEOS, iP, HEOS._p);
711 } else {
712
713 // Set some input options
716 io.Nstep_max = 10;
717
718 // Get an extremely rough guess by interpolation of ln(p) v. T curve where the limits are mole-fraction-weighted
720
721 // Use Wilson iteration to obtain updated guess for temperature
722 Tguess = SaturationSolvers::saturation_Wilson(HEOS, HEOS._Q, HEOS._p, SaturationSolvers::imposed_p, HEOS.mole_fractions, Tguess);
723
724 std::vector<CoolPropDbl> K = HEOS.K;
725
726 if (get_config_bool(HENRYS_LAW_TO_GENERATE_VLE_GUESSES) && std::abs(HEOS._Q - 1) < 1e-10) {
727 const std::vector<CoolPropFluid>& components = HEOS.get_components();
728 std::size_t iWater = 0;
729 double p1star = PropsSI("P", "T", Tguess, "Q", 1, "Water");
730 const std::vector<CoolPropDbl> y = HEOS.mole_fractions;
731 std::vector<CoolPropDbl> x(y.size());
732 for (std::size_t i = 0; i < components.size(); ++i) {
733 if (components[i].CAS == "7732-18-5") {
734 iWater = i;
735 continue;
736 } else {
737 double A, B, C, Tmin, Tmax;
738 get_Henrys_coeffs_FP(components[i].CAS, A, B, C, Tmin, Tmax);
739 double T_R = Tguess / 647.096, tau = 1 - T_R;
740 double k_H = p1star * exp(A / T_R + B * pow(tau, 0.355) / T_R + C * pow(T_R, -0.41) * exp(tau));
741 x[i] = y[i] * HEOS._p / k_H;
742 //
743 K[i] = y[i] / x[i];
744 }
745 }
746 // Update water K factor
747 double summer = 0;
748 for (std::size_t i = 0; i < y.size(); ++i) {
749 if (i != iWater) {
750 summer += x[i];
751 }
752 }
753 x[iWater] = summer;
754 K[iWater] = y[iWater] / x[iWater];
755 }
756
757 // Actually call the successive substitution solver
758 SaturationSolvers::successive_substitution(HEOS, HEOS._Q, Tguess, HEOS._p, HEOS.mole_fractions, K, io);
759
760 // -----
761 // Newton-Raphson
762 // -----
763
764 SaturationSolvers::newton_raphson_saturation NR;
765 SaturationSolvers::newton_raphson_saturation_options IO;
766
767 IO.bubble_point = (HEOS._Q < 0.5);
768 IO.x = io.x;
769 IO.y = io.y;
770 IO.rhomolar_liq = io.rhomolar_liq;
771 IO.rhomolar_vap = io.rhomolar_vap;
772 IO.T = io.T;
773 IO.p = io.p;
774 IO.Nstep_max = 30;
775 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::P_IMPOSED;
776
777 if (IO.bubble_point) {
778 // Compositions are z, z_incipient
779 NR.call(HEOS, IO.x, IO.y, IO);
780 } else {
781 // Compositions are z, z_incipient
782 NR.call(HEOS, IO.y, IO.x, IO);
783 }
784 }
785
786 // Load the outputs
787 HEOS._phase = iphase_twophase;
788 HEOS._p = HEOS.SatV->p();
789 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
790 HEOS._T = HEOS.SatL->T();
791 }
792}
793
795 SaturationSolvers::newton_raphson_saturation NR;
796 SaturationSolvers::newton_raphson_saturation_options IO;
797 IO.rhomolar_liq = guess.rhomolar_liq;
798 IO.rhomolar_vap = guess.rhomolar_vap;
799 IO.x = std::vector<CoolPropDbl>(guess.x.begin(), guess.x.end());
800 IO.y = std::vector<CoolPropDbl>(guess.y.begin(), guess.y.end());
801 IO.T = guess.T;
802 IO.p = HEOS._p;
803 IO.bubble_point = false;
804 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::P_IMPOSED;
805
806 if (std::abs(HEOS.Q()) < 1e-10) {
807 IO.bubble_point = true;
808 NR.call(HEOS, IO.x, IO.y, IO);
809 } else if (std::abs(HEOS.Q() - 1) < 1e-10) {
810 IO.bubble_point = false;
811 NR.call(HEOS, IO.y, IO.x, IO);
812 } else {
813 throw ValueError(format("Quality must be 0 or 1"));
814 }
815
816 // Load the other outputs
817 HEOS._phase = iphase_twophase;
818 HEOS._rhomolar = 1 / (HEOS._Q / IO.rhomolar_vap + (1 - HEOS._Q) / IO.rhomolar_liq);
819 HEOS._T = IO.T;
820}
822 SaturationSolvers::newton_raphson_saturation NR;
823 SaturationSolvers::newton_raphson_saturation_options IO;
824 IO.rhomolar_liq = guess.rhomolar_liq;
825 IO.rhomolar_vap = guess.rhomolar_vap;
826 IO.x = std::vector<CoolPropDbl>(guess.x.begin(), guess.x.end());
827 IO.y = std::vector<CoolPropDbl>(guess.y.begin(), guess.y.end());
828 IO.T = HEOS._T;
829 IO.p = guess.p;
830 IO.bubble_point = false;
831 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::T_IMPOSED;
832
833 if (get_debug_level() > 9) {
834 std::cout << format(" QT w/ guess p %g T %g dl %g dv %g x %s y %s\n", IO.p, IO.T, IO.rhomolar_liq, IO.rhomolar_vap,
835 vec_to_string(IO.x, "%g").c_str(), vec_to_string(IO.y, "%g").c_str());
836 }
837
838 if (std::abs(HEOS.Q()) < 1e-10) {
839 IO.bubble_point = true;
840 NR.call(HEOS, IO.x, IO.y, IO);
841 } else if (std::abs(HEOS.Q() - 1) < 1e-10) {
842 IO.bubble_point = false;
843 NR.call(HEOS, IO.y, IO.x, IO);
844 } else {
845 throw ValueError(format("Quality must be 0 or 1"));
846 }
847
848 // Load the other outputs
849 HEOS._p = IO.p;
850 HEOS._phase = iphase_twophase;
851 HEOS._rhomolar = 1 / (HEOS._Q / IO.rhomolar_vap + (1 - HEOS._Q) / IO.rhomolar_liq);
852}
853
855 HEOS.solver_rho_Tp(HEOS.T(), HEOS.p(), guess.rhomolar);
856 // Load the other outputs
857 HEOS._phase = iphase_gas; // Guessed for mixtures
858 if (HEOS.is_pure_or_pseudopure) {
859 if (HEOS._p > HEOS.p_critical()) {
860 if (HEOS._T > HEOS.T_critical()) {
862 } else {
864 }
865 } else {
866 if (HEOS._T > HEOS.T_critical()) {
868 } else if (HEOS._rhomolar > HEOS.rhomolar_critical()) {
869 HEOS._phase = iphase_liquid;
870 } else {
871 HEOS._phase = iphase_gas;
872 }
873 }
874 }
875
876 HEOS._Q = -1;
877}
878
880
881 // Find the intersections in the phase envelope
882 std::vector<std::pair<std::size_t, std::size_t>> intersections =
884
886
887 enum quality_options
888 {
889 SATURATED_LIQUID,
890 SATURATED_VAPOR,
891 TWO_PHASE
892 };
893 quality_options quality;
894 if (std::abs(HEOS._Q) < 100 * DBL_EPSILON) {
895 quality = SATURATED_LIQUID;
896 } else if (std::abs(HEOS._Q - 1) < 100 * DBL_EPSILON) {
897 quality = SATURATED_VAPOR;
898 } else if (HEOS._Q > 0 && HEOS._Q < 1) {
899 quality = TWO_PHASE;
900 } else {
901 throw ValueError("Quality is not within 0 and 1");
902 }
903
904 if (quality == SATURATED_LIQUID || quality == SATURATED_VAPOR) {
905 // *********************************************************
906 // Bubble- or dew-point calculation
907 // *********************************************************
908 // Find the correct solution
909 std::vector<std::size_t> solutions;
910 for (std::vector<std::pair<std::size_t, std::size_t>>::const_iterator it = intersections.begin(); it != intersections.end(); ++it) {
911 if (std::abs(env.Q[it->first] - HEOS._Q) < 10 * DBL_EPSILON && std::abs(env.Q[it->second] - HEOS._Q) < 10 * DBL_EPSILON) {
912 solutions.push_back(it->first);
913 }
914 }
915
916 if (solutions.size() == 1) {
917
918 std::size_t& imax = solutions[0];
919
920 // Shift the solution if needed to ensure that imax+2 and imax-1 are both in range
921 if (imax + 2 >= env.T.size()) {
922 imax--;
923 } else if (imax == 0) {
924 imax++;
925 }
926 // Here imax+2 or imax-1 is still possibly out of range:
927 // 1. If imax initially is 1, and env.T.size() <= 3, then imax will become 0.
928 // 2. If imax initially is 0, and env.T.size() <= 2, then imax will become MAX_UINT.
929 // 3. If imax+2 initially is more than env.T.size(), then single decrement will not bring it to range
930
931 SaturationSolvers::newton_raphson_saturation NR;
932 SaturationSolvers::newton_raphson_saturation_options IO;
933
934 if (other == iP) {
935 IO.p = HEOS._p;
936 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::P_IMPOSED;
937 // p -> rhomolar_vap
938 IO.rhomolar_vap = CubicInterp(env.p, env.rhomolar_vap, imax - 1, imax, imax + 1, imax + 2, static_cast<CoolPropDbl>(IO.p));
939 IO.T = CubicInterp(env.rhomolar_vap, env.T, imax - 1, imax, imax + 1, imax + 2, IO.rhomolar_vap);
940 } else if (other == iT) {
941 IO.T = HEOS._T;
942 IO.imposed_variable = SaturationSolvers::newton_raphson_saturation_options::T_IMPOSED;
943 // T -> rhomolar_vap
944 IO.rhomolar_vap = CubicInterp(env.T, env.rhomolar_vap, imax - 1, imax, imax + 1, imax + 2, static_cast<CoolPropDbl>(IO.T));
945 IO.p = CubicInterp(env.rhomolar_vap, env.p, imax - 1, imax, imax + 1, imax + 2, IO.rhomolar_vap);
946 } else {
947 throw ValueError();
948 }
949 IO.rhomolar_liq = CubicInterp(env.rhomolar_vap, env.rhomolar_liq, imax - 1, imax, imax + 1, imax + 2, IO.rhomolar_vap);
950
951 if (quality == SATURATED_VAPOR) {
952 IO.bubble_point = false;
953 IO.y = HEOS.get_mole_fractions(); // Because Q = 1
954 IO.x.resize(IO.y.size());
955 for (std::size_t i = 0; i < IO.x.size() - 1; ++i) // First N-1 elements
956 {
957 IO.x[i] = CubicInterp(env.rhomolar_vap, env.x[i], imax - 1, imax, imax + 1, imax + 2, IO.rhomolar_vap);
958 }
959 IO.x[IO.x.size() - 1] = 1 - std::accumulate(IO.x.begin(), IO.x.end() - 1, 0.0);
960 NR.call(HEOS, IO.y, IO.x, IO);
961 } else {
962 IO.bubble_point = true;
963 IO.x = HEOS.get_mole_fractions(); // Because Q = 0
964 IO.y.resize(IO.x.size());
965 // Phases are inverted, so "liquid" is actually the lighter phase
966 std::swap(IO.rhomolar_liq, IO.rhomolar_vap);
967 for (std::size_t i = 0; i < IO.y.size() - 1; ++i) // First N-1 elements
968 {
969 // Phases are inverted, so liquid mole fraction (x) of phase envelope is actually the vapor phase mole fraction
970 // Use the liquid density as well
971 IO.y[i] = CubicInterp(env.rhomolar_vap, env.x[i], imax - 1, imax, imax + 1, imax + 2, IO.rhomolar_liq);
972 }
973 IO.y[IO.y.size() - 1] = 1 - std::accumulate(IO.y.begin(), IO.y.end() - 1, 0.0);
974 NR.call(HEOS, IO.x, IO.y, IO);
975 }
976 } else if (solutions.size() == 0) {
977 throw ValueError("No solution was found in PQ_flash");
978 } else {
979 throw ValueError("More than 1 solution was found in PQ_flash");
980 }
981 } else {
982 // *********************************************************
983 // Two-phase calculation for given vapor quality
984 // *********************************************************
985
986 // Find the correct solution
987 std::vector<std::size_t> liquid_solutions, vapor_solutions;
988 for (std::vector<std::pair<std::size_t, std::size_t>>::const_iterator it = intersections.begin(); it != intersections.end(); ++it) {
989 if (std::abs(env.Q[it->first] - 0) < 10 * DBL_EPSILON && std::abs(env.Q[it->second] - 0) < 10 * DBL_EPSILON) {
990 liquid_solutions.push_back(it->first);
991 }
992 if (std::abs(env.Q[it->first] - 1) < 10 * DBL_EPSILON && std::abs(env.Q[it->second] - 1) < 10 * DBL_EPSILON) {
993 vapor_solutions.push_back(it->first);
994 }
995 }
996
997 if (liquid_solutions.size() != 1 || vapor_solutions.size() != 1) {
998 throw ValueError(format("Number liquid solutions [%d] or vapor solutions [%d] != 1", liquid_solutions.size(), vapor_solutions.size()));
999 }
1000 std::size_t iliq = liquid_solutions[0], ivap = vapor_solutions[0];
1001
1002 SaturationSolvers::newton_raphson_twophase NR;
1003 SaturationSolvers::newton_raphson_twophase_options IO;
1004 IO.beta = HEOS._Q;
1005
1006 CoolPropDbl rhomolar_vap_sat_vap, T_sat_vap, rhomolar_liq_sat_vap, rhomolar_liq_sat_liq, T_sat_liq, rhomolar_vap_sat_liq, p_sat_liq,
1007 p_sat_vap;
1008
1009 if (other == iP) {
1010 IO.p = HEOS._p;
1011 p_sat_liq = IO.p;
1012 p_sat_vap = IO.p;
1013 IO.imposed_variable = SaturationSolvers::newton_raphson_twophase_options::P_IMPOSED;
1014
1015 // Calculate the interpolated values for beta = 0 and beta = 1
1016 rhomolar_vap_sat_vap = CubicInterp(env.p, env.rhomolar_vap, ivap - 1, ivap, ivap + 1, ivap + 2, static_cast<CoolPropDbl>(IO.p));
1017 T_sat_vap = CubicInterp(env.rhomolar_vap, env.T, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1018 rhomolar_liq_sat_vap = CubicInterp(env.rhomolar_vap, env.rhomolar_liq, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1019
1020 // Phase inversion for liquid solution (liquid is vapor and vice versa)
1021 rhomolar_liq_sat_liq = CubicInterp(env.p, env.rhomolar_vap, iliq - 1, iliq, iliq + 1, iliq + 2, static_cast<CoolPropDbl>(IO.p));
1022 T_sat_liq = CubicInterp(env.rhomolar_vap, env.T, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1023 rhomolar_vap_sat_liq = CubicInterp(env.rhomolar_vap, env.rhomolar_liq, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1024 } else if (other == iT) {
1025 IO.T = HEOS._T;
1026 T_sat_liq = IO.T;
1027 T_sat_vap = IO.T;
1028 IO.imposed_variable = SaturationSolvers::newton_raphson_twophase_options::T_IMPOSED;
1029
1030 // Calculate the interpolated values for beta = 0 and beta = 1
1031 rhomolar_vap_sat_vap = CubicInterp(env.T, env.rhomolar_vap, ivap - 1, ivap, ivap + 1, ivap + 2, static_cast<CoolPropDbl>(IO.T));
1032 p_sat_vap = CubicInterp(env.rhomolar_vap, env.p, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1033 rhomolar_liq_sat_vap = CubicInterp(env.rhomolar_vap, env.rhomolar_liq, ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1034
1035 // Phase inversion for liquid solution (liquid is vapor and vice versa)
1036 rhomolar_liq_sat_liq = CubicInterp(env.T, env.rhomolar_vap, iliq - 1, iliq, iliq + 1, iliq + 2, static_cast<CoolPropDbl>(IO.T));
1037 p_sat_liq = CubicInterp(env.rhomolar_vap, env.p, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1038 rhomolar_vap_sat_liq = CubicInterp(env.rhomolar_vap, env.rhomolar_liq, iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1039 } else {
1040 throw ValueError();
1041 }
1042
1043 // Weight the guesses by the vapor mole fraction
1044 IO.rhomolar_vap = IO.beta * rhomolar_vap_sat_vap + (1 - IO.beta) * rhomolar_vap_sat_liq;
1045 IO.rhomolar_liq = IO.beta * rhomolar_liq_sat_vap + (1 - IO.beta) * rhomolar_liq_sat_liq;
1046 IO.T = IO.beta * T_sat_vap + (1 - IO.beta) * T_sat_liq;
1047 IO.p = IO.beta * p_sat_vap + (1 - IO.beta) * p_sat_liq;
1048
1049 IO.z = HEOS.get_mole_fractions();
1050 IO.x.resize(IO.z.size());
1051 IO.y.resize(IO.z.size());
1052
1053 for (std::size_t i = 0; i < IO.x.size() - 1; ++i) // First N-1 elements
1054 {
1055 CoolPropDbl x_sat_vap = CubicInterp(env.rhomolar_vap, env.x[i], ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1056 CoolPropDbl y_sat_vap = CubicInterp(env.rhomolar_vap, env.y[i], ivap - 1, ivap, ivap + 1, ivap + 2, rhomolar_vap_sat_vap);
1057
1058 CoolPropDbl x_sat_liq = CubicInterp(env.rhomolar_vap, env.y[i], iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1059 CoolPropDbl y_sat_liq = CubicInterp(env.rhomolar_vap, env.x[i], iliq - 1, iliq, iliq + 1, iliq + 2, rhomolar_liq_sat_liq);
1060
1061 IO.x[i] = IO.beta * x_sat_vap + (1 - IO.beta) * x_sat_liq;
1062 IO.y[i] = IO.beta * y_sat_vap + (1 - IO.beta) * y_sat_liq;
1063 }
1064 IO.x[IO.x.size() - 1] = 1 - std::accumulate(IO.x.begin(), IO.x.end() - 1, 0.0);
1065 IO.y[IO.y.size() - 1] = 1 - std::accumulate(IO.y.begin(), IO.y.end() - 1, 0.0);
1066 NR.call(HEOS, IO);
1067 }
1068}
1070 class Residual : public FuncWrapper1D
1071 {
1072
1073 public:
1075 CoolPropDbl rhomolar_spec; // Specified value for density
1076 parameters other; // Key for other value
1077 CoolPropDbl value; // value for S,H,U
1078 CoolPropDbl Qd; // Quality from density
1079 Residual(HelmholtzEOSMixtureBackend& HEOS, CoolPropDbl rhomolar_spec, parameters other, CoolPropDbl value)
1080 : HEOS(HEOS), rhomolar_spec(rhomolar_spec), other(other), value(value) {
1081 Qd = _HUGE;
1082 };
1083 double call(double T) {
1084 HEOS.update(QT_INPUTS, 0, T);
1085 HelmholtzEOSMixtureBackend &SatL = HEOS.get_SatL(), &SatV = HEOS.get_SatV();
1086 // Quality from density
1087 Qd = (1 / rhomolar_spec - 1 / SatL.rhomolar()) / (1 / SatV.rhomolar() - 1 / SatL.rhomolar());
1088 // Quality from other parameter (H,S,U)
1089 CoolPropDbl Qo = (value - SatL.keyed_output(other)) / (SatV.keyed_output(other) - SatL.keyed_output(other));
1090 // Residual is the difference between the two
1091 return Qo - Qd;
1092 }
1093 } resid(HEOS, rhomolar_spec, other, value);
1094
1095 // Critical point for pure fluids, slightly different for pseudo-pure, very different for mixtures
1096 CoolPropDbl Tmax_sat = HEOS.calc_Tmax_sat() - 1e-13;
1097
1098 // Check what the minimum limits for the equation of state are
1099 CoolPropDbl Tmin_satL, Tmin_satV, Tmin_sat;
1100 HEOS.calc_Tmin_sat(Tmin_satL, Tmin_satV);
1101 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
1102
1103 Brent(resid, Tmin_sat, Tmax_sat - 0.01, DBL_EPSILON, 1e-12, 20);
1104 // Solve once more with the final vapor quality
1105 HEOS.update(QT_INPUTS, resid.Qd, HEOS.T());
1106}
1107// D given and one of P,H,S,U
1109 // Define the residual to be driven to zero
1110 class solver_resid : public FuncWrapper1DWithTwoDerivs
1111 {
1112 public:
1114 CoolPropDbl rhomolar, value;
1115 parameters other;
1116 CoolPropDbl Tmin, Tmax;
1117
1118 solver_resid(HelmholtzEOSMixtureBackend* HEOS, CoolPropDbl rhomolar, CoolPropDbl value, parameters other, CoolPropDbl Tmin, CoolPropDbl Tmax)
1119 : HEOS(HEOS), rhomolar(rhomolar), value(value), other(other), Tmin(Tmin), Tmax(Tmax) {
1122 };
1123 double call(double T) {
1124 HEOS->update_DmolarT_direct(rhomolar, T);
1125 double eos = HEOS->keyed_output(other);
1126 if (other == iP) {
1127 // For p, should use fractional error
1128 return (eos - value) / value;
1129 } else {
1130 // For everything else, use absolute error
1131 return eos - value;
1132 }
1133 };
1134 double deriv(double T) {
1135 if (other == iP) {
1136 return HEOS->first_partial_deriv(other, iT, iDmolar) / value;
1137 }
1138 return HEOS->first_partial_deriv(other, iT, iDmolar);
1139 };
1140 double second_deriv(double T) {
1141 if (other == iP) {
1142 return HEOS->second_partial_deriv(other, iT, iDmolar, iT, iDmolar) / value;
1143 }
1144 return HEOS->second_partial_deriv(other, iT, iDmolar, iT, iDmolar);
1145 };
1146 bool input_not_in_range(double T) {
1147 return (T < Tmin || T > Tmax);
1148 }
1149 };
1150
1151 if (HEOS.is_pure_or_pseudopure) {
1152 CoolPropFluid& component = HEOS.components[0];
1153
1154 shared_ptr<HelmholtzEOSMixtureBackend> Sat;
1155 CoolPropDbl rhoLtriple = component.triple_liquid.rhomolar;
1156 CoolPropDbl rhoVtriple = component.triple_vapor.rhomolar;
1157 // Check if in the "normal" region
1158 if (HEOS._rhomolar >= rhoVtriple && HEOS._rhomolar <= rhoLtriple) {
1159 CoolPropDbl yL, yV, value, y_solid;
1160 CoolPropDbl TLtriple = component.triple_liquid.T;
1161 CoolPropDbl TVtriple = component.triple_vapor.T;
1162
1163 // First check if solid (below the line connecting the triple point values) - this is an error for now
1164 switch (other) {
1165 case iSmolar:
1166 yL = HEOS.calc_smolar_nocache(TLtriple, rhoLtriple);
1167 yV = HEOS.calc_smolar_nocache(TVtriple, rhoVtriple);
1168 value = HEOS._smolar;
1169 break;
1170 case iHmolar:
1171 yL = HEOS.calc_hmolar_nocache(TLtriple, rhoLtriple);
1172 yV = HEOS.calc_hmolar_nocache(TVtriple, rhoVtriple);
1173 value = HEOS._hmolar;
1174 break;
1175 case iUmolar:
1176 yL = HEOS.calc_umolar_nocache(TLtriple, rhoLtriple);
1177 yV = HEOS.calc_umolar_nocache(TVtriple, rhoVtriple);
1178 value = HEOS._umolar;
1179 break;
1180 case iP:
1181 yL = HEOS.calc_pressure_nocache(TLtriple, rhoLtriple);
1182 yV = HEOS.calc_pressure_nocache(TVtriple, rhoVtriple);
1183 value = HEOS._p;
1184 break;
1185 default:
1186 throw ValueError(format("Input is invalid"));
1187 }
1188 y_solid = (yV - yL) / (1 / rhoVtriple - 1 / rhoLtriple) * (1 / HEOS._rhomolar - 1 / rhoLtriple) + yL;
1189
1190 if (value < y_solid) {
1191 throw ValueError(format("Other input [%d:%g] is solid", other, value));
1192 }
1193
1194 // Check if other is above the saturation value.
1196 optionsD.omega = 1;
1197 optionsD.use_logdelta = false;
1198 optionsD.max_iterations = 200;
1199 for (int i_try = 0; i_try < 7; i_try++)
1200 {
1201 try
1202 {
1203 if (HEOS._rhomolar > HEOS.rhomolar_critical())
1204 {
1206 SaturationSolvers::saturation_D_pure(HEOS, HEOS._rhomolar, optionsD);
1207 // SatL and SatV have the saturation values
1208 Sat = HEOS.SatL;
1209 }
1210 else
1211 {
1213 SaturationSolvers::saturation_D_pure(HEOS, HEOS._rhomolar, optionsD);
1214 // SatL and SatV have the saturation values
1215 Sat = HEOS.SatV;
1216 }
1217 break; // good solve
1218 }
1219 catch(CoolPropBaseError)
1220 {
1221 optionsD.omega /= 2;
1222 optionsD.max_iterations *= 2;
1223 if (i_try >= 6){throw;}
1224 }
1225 }
1226
1227 // If it is above, it is not two-phase and either liquid, vapor or supercritical
1228 if (value > Sat->keyed_output(other)) {
1229 solver_resid resid(&HEOS, HEOS._rhomolar, value, other, Sat->keyed_output(iT), HEOS.Tmax() * 1.5);
1230 try {
1231 HEOS._T = Halley(resid, 0.5 * (Sat->keyed_output(iT) + HEOS.Tmax() * 1.5), 1e-10, 100);
1232 } catch (...) {
1233 HEOS._T = Brent(resid, Sat->keyed_output(iT), HEOS.Tmax() * 1.5, DBL_EPSILON, 1e-12, 100);
1234 }
1235 HEOS._Q = 10000;
1236 HEOS._p = HEOS.calc_pressure_nocache(HEOS.T(), HEOS.rhomolar());
1237 HEOS.unspecify_phase();
1238 // Update the phase flag
1240 } else {
1241 // Now we know that temperature is between Tsat(D) +- tolerance and the minimum temperature for the fluid
1242 if (other == iP) {
1243 // Iterate to find T(p), its just a saturation call
1244
1245 // Set some input options
1247 // Specified variable is pressure
1249 // Use logarithm of delta as independent variables
1250 optionsPHSU.use_logdelta = false;
1251
1252 // Actually call the solver
1253 SaturationSolvers::saturation_PHSU_pure(HEOS, HEOS._p, optionsPHSU);
1254
1255 // Load the outputs
1256 HEOS._phase = iphase_twophase;
1257 HEOS._Q = (1 / HEOS._rhomolar - 1 / HEOS.SatL->rhomolar()) / (1 / HEOS.SatV->rhomolar() - 1 / HEOS.SatL->rhomolar());
1258 HEOS._T = HEOS.SatL->T();
1259 } else {
1260 // Residual is difference in quality calculated from density and quality calculated from the other parameter
1261 // Iterate to find T
1262 HSU_D_flash_twophase(HEOS, HEOS._rhomolar, other, value);
1263 HEOS._phase = iphase_twophase;
1264 }
1265 }
1266 }
1267 // Check if vapor/solid region below triple point vapor density
1268 else if (HEOS._rhomolar < component.triple_vapor.rhomolar) {
1269 CoolPropDbl y, value;
1270 CoolPropDbl TVtriple = component.triple_vapor.T; //TODO: separate TL and TV for ppure
1271
1272 // If value is above the value calculated from X(Ttriple, _rhomolar), it is vapor
1273 switch (other) {
1274 case iSmolar:
1275 y = HEOS.calc_smolar_nocache(TVtriple, HEOS._rhomolar);
1276 value = HEOS._smolar;
1277 break;
1278 case iHmolar:
1279 y = HEOS.calc_hmolar_nocache(TVtriple, HEOS._rhomolar);
1280 value = HEOS._hmolar;
1281 break;
1282 case iUmolar:
1283 y = HEOS.calc_umolar_nocache(TVtriple, HEOS._rhomolar);
1284 value = HEOS._umolar;
1285 break;
1286 case iP:
1287 y = HEOS.calc_pressure_nocache(TVtriple, HEOS._rhomolar);
1288 value = HEOS._p;
1289 break;
1290 default:
1291 throw ValueError(format("Input is invalid"));
1292 }
1293 if (value > y) {
1294 solver_resid resid(&HEOS, HEOS._rhomolar, value, other, TVtriple, HEOS.Tmax() * 1.5);
1295 HEOS._phase = iphase_gas;
1296 try {
1297 HEOS._T = Halley(resid, 0.5 * (TVtriple + HEOS.Tmax() * 1.5), DBL_EPSILON, 100);
1298 } catch (...) {
1299 HEOS._T = Brent(resid, TVtriple, HEOS.Tmax() * 1.5, DBL_EPSILON, 1e-12, 100);
1300 }
1301 HEOS._Q = 10000;
1302 HEOS.calc_pressure();
1303 } else {
1304 throw ValueError(format("D < DLtriple %g %g", value, y));
1305 }
1306
1307 }
1308 // Check in the liquid/solid region above the triple point density
1309 else {
1310 CoolPropDbl y, value;
1311 CoolPropDbl TLtriple = component.EOS().Ttriple;
1312
1313 // If value is above the value calculated from X(Ttriple, _rhomolar), it is vapor
1314 switch (other) {
1315 case iSmolar:
1316 y = HEOS.calc_smolar_nocache(TLtriple, HEOS._rhomolar);
1317 value = HEOS._smolar;
1318 break;
1319 case iHmolar:
1320 y = HEOS.calc_hmolar_nocache(TLtriple, HEOS._rhomolar);
1321 value = HEOS._hmolar;
1322 break;
1323 case iUmolar:
1324 y = HEOS.calc_umolar_nocache(TLtriple, HEOS._rhomolar);
1325 value = HEOS._umolar;
1326 break;
1327 case iP:
1328 y = HEOS.calc_pressure_nocache(TLtriple, HEOS._rhomolar);
1329 value = HEOS._p;
1330 break;
1331 default:
1332 throw ValueError(format("Input is invalid"));
1333 }
1334 if (value > y) {
1335 solver_resid resid(&HEOS, HEOS._rhomolar, value, other, TLtriple, HEOS.Tmax() * 1.5);
1336 HEOS._phase = iphase_liquid;
1337 try {
1338 HEOS._T = Halley(resid, 0.5 * (TLtriple + HEOS.Tmax() * 1.5), DBL_EPSILON, 100);
1339 } catch (...) {
1340 HEOS._T = Brent(resid, TLtriple, HEOS.Tmax() * 1.5, DBL_EPSILON, 1e-12, 100);
1341 }
1342 HEOS._Q = 10000;
1343 HEOS.calc_pressure();
1344 } else {
1345 throw ValueError(format("D < DLtriple %g %g", value, y));
1346 }
1347 }
1348 // Update the state for conditions where the state was guessed
1349 if (HEOS.phase() != iphase_twophase) {
1351 }
1352 } else
1353 throw NotImplementedError("PHSU_D_flash not ready for mixtures");
1354}
1355
1357 double A[2][2], B[2][2];
1358 CoolPropDbl y = _HUGE;
1360 _HEOS.update(DmolarT_INPUTS, rhomolar0, T0);
1361 CoolPropDbl Tc = HEOS.calc_T_critical();
1362 CoolPropDbl rhoc = HEOS.calc_rhomolar_critical();
1363 CoolPropDbl R = HEOS.gas_constant();
1364 CoolPropDbl p = HEOS.p();
1365 switch (other) {
1366 case iHmolar:
1367 y = HEOS.hmolar();
1368 break;
1369 case iSmolar:
1370 y = HEOS.smolar();
1371 break;
1372 default:
1373 throw ValueError("other is invalid in HSU_P_flash_singlephase_Newton");
1374 }
1375
1376 CoolPropDbl worst_error = 999;
1377 int iter = 0;
1378 bool failed = false;
1379 CoolPropDbl omega = 1.0, f2, df2_dtau, df2_ddelta;
1380 CoolPropDbl tau = _HEOS.tau(), delta = _HEOS.delta();
1381 while (worst_error > 1e-6 && failed == false) {
1382
1383 // All the required partial derivatives
1384 CoolPropDbl a0 = _HEOS.calc_alpha0_deriv_nocache(0, 0, HEOS.mole_fractions, tau, delta, Tc, rhoc);
1385 CoolPropDbl da0_ddelta = _HEOS.calc_alpha0_deriv_nocache(0, 1, HEOS.mole_fractions, tau, delta, Tc, rhoc);
1386 CoolPropDbl da0_dtau = _HEOS.calc_alpha0_deriv_nocache(1, 0, HEOS.mole_fractions, tau, delta, Tc, rhoc);
1387 CoolPropDbl d2a0_dtau2 = _HEOS.calc_alpha0_deriv_nocache(2, 0, HEOS.mole_fractions, tau, delta, Tc, rhoc);
1388 CoolPropDbl d2a0_ddelta_dtau = 0.0;
1389
1390 CoolPropDbl ar = _HEOS.calc_alphar_deriv_nocache(0, 0, HEOS.mole_fractions, tau, delta);
1391 CoolPropDbl dar_dtau = _HEOS.calc_alphar_deriv_nocache(1, 0, HEOS.mole_fractions, tau, delta);
1392 CoolPropDbl dar_ddelta = _HEOS.calc_alphar_deriv_nocache(0, 1, HEOS.mole_fractions, tau, delta);
1393 CoolPropDbl d2ar_ddelta_dtau = _HEOS.calc_alphar_deriv_nocache(1, 1, HEOS.mole_fractions, tau, delta);
1394 CoolPropDbl d2ar_ddelta2 = _HEOS.calc_alphar_deriv_nocache(0, 2, HEOS.mole_fractions, tau, delta);
1395 CoolPropDbl d2ar_dtau2 = _HEOS.calc_alphar_deriv_nocache(2, 0, HEOS.mole_fractions, tau, delta);
1396
1397 CoolPropDbl f1 = delta / tau * (1 + delta * dar_ddelta) - p / (rhoc * R * Tc);
1398 CoolPropDbl df1_dtau = (1 + delta * dar_ddelta) * (-delta / tau / tau) + delta / tau * (delta * d2ar_ddelta_dtau);
1399 CoolPropDbl df1_ddelta = (1.0 / tau) * (1 + 2.0 * delta * dar_ddelta + delta * delta * d2ar_ddelta2);
1400 switch (other) {
1401 case iHmolar: {
1402 f2 = (1 + delta * dar_ddelta) + tau * (da0_dtau + dar_dtau) - tau * y / (R * Tc);
1403 df2_dtau = delta * d2ar_ddelta_dtau + da0_dtau + dar_dtau + tau * (d2a0_dtau2 + d2ar_dtau2) - y / (R * Tc);
1404 df2_ddelta = (dar_ddelta + delta * d2ar_ddelta2) + tau * (d2a0_ddelta_dtau + d2ar_ddelta_dtau);
1405 break;
1406 }
1407 case iSmolar: {
1408 f2 = tau * (da0_dtau + dar_dtau) - ar - a0 - y / R;
1409 df2_dtau = tau * (d2a0_dtau2 + d2ar_dtau2) + (da0_dtau + dar_dtau) - dar_dtau - da0_dtau;
1410 df2_ddelta = tau * (d2a0_ddelta_dtau + d2ar_ddelta_dtau) - dar_ddelta - da0_ddelta;
1411 break;
1412 }
1413 default:
1414 throw ValueError("other variable in HSU_P_flash_singlephase_Newton is invalid");
1415 }
1416
1417 //First index is the row, second index is the column
1418 A[0][0] = df1_dtau;
1419 A[0][1] = df1_ddelta;
1420 A[1][0] = df2_dtau;
1421 A[1][1] = df2_ddelta;
1422
1423 //double det = A[0][0]*A[1][1]-A[1][0]*A[0][1];
1424
1425 MatInv_2(A, B);
1426 tau -= omega * (B[0][0] * f1 + B[0][1] * f2);
1427 delta -= omega * (B[1][0] * f1 + B[1][1] * f2);
1428
1429 if (std::abs(f1) > std::abs(f2))
1430 worst_error = std::abs(f1);
1431 else
1432 worst_error = std::abs(f2);
1433
1434 if (!ValidNumber(f1) || !ValidNumber(f2)) {
1435 throw SolutionError(format("Invalid values for inputs p=%g y=%g for fluid %s in HSU_P_flash_singlephase", p, y, _HEOS.name().c_str()));
1436 }
1437
1438 iter += 1;
1439 if (iter > 100) {
1440 throw SolutionError(format("HSU_P_flash_singlephase did not converge with inputs p=%g h=%g for fluid %s", p, y, _HEOS.name().c_str()));
1441 }
1442 }
1443
1444 HEOS.update(DmolarT_INPUTS, rhoc * delta, Tc / tau);
1445}
1447 CoolPropDbl Tmax, phases phase) {
1448 if (!ValidNumber(HEOS._p)) {
1449 throw ValueError("value for p in HSU_P_flash_singlephase_Brent is invalid");
1450 };
1451 if (!ValidNumber(value)) {
1452 throw ValueError("value for other in HSU_P_flash_singlephase_Brent is invalid");
1453 };
1454 class solver_resid : public FuncWrapper1DWithTwoDerivs
1455 {
1456 public:
1458 CoolPropDbl p, value;
1459 parameters other;
1460 int iter;
1461 CoolPropDbl eos0, eos1, rhomolar, rhomolar0, rhomolar1;
1462 CoolPropDbl Tmin, Tmax;
1463
1464 solver_resid(HelmholtzEOSMixtureBackend* HEOS, CoolPropDbl p, CoolPropDbl value, parameters other, double Tmin, double Tmax)
1465 : HEOS(HEOS),
1466 p(p),
1467 value(value),
1468 other(other),
1469 iter(0),
1470 eos0(-_HUGE),
1471 eos1(-_HUGE),
1472 rhomolar(_HUGE),
1473 rhomolar0(_HUGE),
1474 rhomolar1(_HUGE),
1475 Tmin(Tmin),
1476 Tmax(Tmax) {
1477 // Specify the state to avoid saturation calls, but only if phase is subcritical
1478 switch (CoolProp::phases phase = HEOS->phase()) {
1479 case iphase_liquid:
1480 case iphase_gas:
1481 HEOS->specify_phase(phase);
1482 default:
1483 // Otherwise don't do anything (this is to make compiler happy)
1484 {}
1485 }
1486 }
1487 double call(double T) {
1488
1489 if (iter < 2 || std::abs(rhomolar1 / rhomolar0 - 1) > 0.05) {
1490 // Run the solver with T,P as inputs; but only if the last change in density was greater than a few percent
1491 HEOS->update(PT_INPUTS, p, T);
1492 } else {
1493 // Run the solver with T,P as inputs; but use the guess value for density from before
1494 HEOS->update_TP_guessrho(T, p, rhomolar);
1495 }
1496
1497 // Get the value of the desired variable
1498 CoolPropDbl eos = HEOS->keyed_output(other);
1499
1500 // Store the value of density
1501 rhomolar = HEOS->rhomolar();
1502
1503 if (verbosity > 0 && iter == 0){
1504 std::cout << format("T: %0.15g rho: %0.15g eos: %0.15g", T, rhomolar, eos);
1505 }
1506
1507 // Difference between the two is to be driven to zero
1508 CoolPropDbl r = eos - value;
1509
1510 // Store values for later use if there are errors
1511 if (iter == 0) {
1512 eos0 = eos;
1513 rhomolar0 = rhomolar;
1514 } else if (iter == 1) {
1515 eos1 = eos;
1516 rhomolar1 = rhomolar;
1517 } else {
1518 eos0 = eos1;
1519 eos1 = eos;
1520 rhomolar0 = rhomolar1;
1521 rhomolar1 = rhomolar;
1522 }
1523
1524 iter++;
1525 return r;
1526 };
1527 double deriv(double T) {
1528 return HEOS->first_partial_deriv(other, iT, iP);
1529 }
1530 double second_deriv(double T) {
1531 return HEOS->second_partial_deriv(other, iT, iP, iT, iP);
1532 }
1533 bool input_not_in_range(double x) {
1534 return (x < Tmin || x > Tmax);
1535 }
1536 };
1537 solver_resid resid(&HEOS, HEOS._p, value, other, Tmin, Tmax);
1538
1539 class resid_2D : public FuncWrapperND{
1540 public:
1542 CoolPropDbl p, value;
1543 parameters other;
1544 int iter;
1545 std::vector<std::vector<double>> J = {{-1.0, -1.0},{-1.0, -1.0}};
1546
1547 resid_2D(HelmholtzEOSMixtureBackend* HEOS, double p, CoolPropDbl value, parameters other, double Tmin, double Tmax)
1548 : HEOS(HEOS),
1549 p(p),
1550 value(value),
1551 other(other),
1552 iter(0){}
1553 std::vector<double> call(const std::vector<double>&x) override{
1554 double T = x[0], rhomolar = x[1];
1555 HEOS->update_DmolarT_direct(rhomolar, T);
1556 J[0][0] = HEOS->first_partial_deriv(iP, iT, iDmolar)/p;
1557 J[0][1] = HEOS->first_partial_deriv(iP, iDmolar, iT)/p;
1558 J[1][0] = HEOS->first_partial_deriv(other, iT, iDmolar);
1559 J[1][1] = HEOS->first_partial_deriv(other, iDmolar, iT);
1560
1561 return {(HEOS->p()-p)/p, HEOS->keyed_output(other) - value};
1562 }
1563 std::vector<std::vector<double>> Jacobian(const std::vector<double>&/*x*/) override{
1564 // pre-calculated in call function
1565 return J;
1566 }
1567 };
1568 resid_2D solver_resid2d(&HEOS, HEOS._p, value, other, Tmin, Tmax);
1569
1570 // Get residual values at the bounds
1571 double resid_Tmin = resid.call(Tmin);
1572 double rhomolar_Tmin = HEOS.rhomolar();
1573
1574 double resid_Tmax = resid.call(Tmax);
1575 double rhomolar_Tmax = HEOS.rhomolar();
1576
1577 // For the derivative-based methods, figure out which point to start from
1578 bool use_min = std::abs(resid_Tmin) < std::abs(resid_Tmax);
1579 double Tstart = use_min ? Tmin : Tmax;
1580 double rhomolarstart = use_min ? rhomolar_Tmin : rhomolar_Tmax;
1581
1582 try {
1583 if (get_debug_level() > 0){
1584 resid.verbosity = 1;
1585 }
1586 if (resid_Tmin*resid_Tmax < 0){
1587 // The residual values bound zero, use the TOMS748 method (no derivatives)
1588 // See: https://www.boost.org/doc/libs/1_58_0/libs/math/doc/html/math_toolkit/internals1/roots2.html#math_toolkit.internals1.roots2.algorithm_toms_748_alefeld_potra
1589 //
1590 // It is like a supercharged version of Brent's method, which is practically guaranteed
1591 // to converge for any continuous function, and take the optimal step among bisection
1592 // and higher-order methods
1593 resid.iter = 0;
1594 boost::math::uintmax_t max_iter = 100;
1595
1596 auto f = [&resid](const double T){ return resid.call(T); };
1597 // Want 44 bits to be correct, tolerance is 2^(1-bits) ::
1598 // >>> 2**(1-44)
1599 // 1.1368683772161603e-13
1600 auto [l, r] = toms748_solve(f, Tmin, Tmax, resid_Tmin, resid_Tmax, boost::math::tools::eps_tolerance<double>(44), max_iter);
1601 if (!is_in_closed_range(Tmin, Tmax, static_cast<CoolPropDbl>(resid.HEOS->T()))) {
1602 throw ValueError(format("TOMS748 method yielded out of bound T of %g", static_cast<CoolPropDbl>(resid.HEOS->T())));
1603 }
1604
1605 // Un-specify the phase of the fluid
1606 HEOS.unspecify_phase();
1608 }
1609 else{
1610 resid.iter = 0;
1611 Halley(resid, Tstart, 1e-12, 100);
1612 if (!is_in_closed_range(Tmin, Tmax, static_cast<CoolPropDbl>(resid.HEOS->T())) || resid.HEOS->phase() != phase) {
1613 throw ValueError("Halley's method was unable to find a solution in HSU_P_flash_singlephase_Brent");
1614 }
1615 // Un-specify the phase of the fluid
1616 HEOS.unspecify_phase();
1618 }
1619 } catch (...) {
1620 try{
1621 double p_critical_ = HEOS.p_critical();
1622 if (0.95*p_critical_ > HEOS._p || HEOS._p > p_critical_){
1623 throw;
1624 }
1625 if (get_debug_level() > 0){
1626 std::cout << resid.errstring << std::endl;
1627 }
1628 std::vector<double> x0 = {Tstart, rhomolarstart};
1629 NDNewtonRaphson_Jacobian(&solver_resid2d, x0, 1e-12, 20, 1.0);
1630 if (!is_in_closed_range(Tmin, Tmax, static_cast<CoolPropDbl>(solver_resid2d.HEOS->T())) || solver_resid2d.HEOS->phase() != phase) {
1631 throw ValueError("2D Newton method was unable to find a solution in HSU_P_flash_singlephase_Brent");
1632 }
1633 // Un-specify the phase of the fluid
1634 HEOS.unspecify_phase();
1636 }
1637 catch(...){
1638 if (get_debug_level() > 0){
1639 std::cout << resid.errstring << std::endl;
1640 }
1641 // Un-specify the phase of the fluid
1642 HEOS.unspecify_phase();
1643
1644 // Determine why you were out of range if you can
1645 //
1646 CoolPropDbl eos0 = resid.eos0, eos1 = resid.eos1;
1647 std::string name = get_parameter_information(other, "short");
1648 std::string units = get_parameter_information(other, "units");
1649 if (eos1 > eos0 && value > eos1) {
1650 throw ValueError(
1651 format("HSU_P_flash_singlephase_Brent could not find a solution because %s [%Lg %s] is above the maximum value of %0.12Lg %s",
1652 name.c_str(), value, units.c_str(), eos1, units.c_str()));
1653 }
1654 if (eos1 > eos0 && value < eos0) {
1655 throw ValueError(
1656 format("HSU_P_flash_singlephase_Brent could not find a solution because %s [%Lg %s] is below the minimum value of %0.12Lg %s",
1657 name.c_str(), value, units.c_str(), eos0, units.c_str()));
1658 }
1659 throw;
1660 }
1661 }
1662}
1663
1664// P given and one of H, S, or U
1666 bool saturation_called = false;
1667 CoolPropDbl value;
1668
1669 // Find the phase, while updating all internal variables possible
1670 switch (other) {
1671 case iSmolar:
1672 value = HEOS.smolar();
1673 break;
1674 case iHmolar:
1675 value = HEOS.hmolar();
1676 break;
1677 case iUmolar:
1678 value = HEOS.umolar();
1679 break;
1680 default:
1681 throw ValueError(format("Input for other [%s] is invalid", get_parameter_information(other, "long").c_str()));
1682 }
1683 if (HEOS.is_pure_or_pseudopure) {
1684
1685 // Find the phase, while updating all internal variables possible
1686 HEOS.p_phase_determination_pure_or_pseudopure(other, value, saturation_called);
1687
1688 if (HEOS.isHomogeneousPhase()) {
1689 // Now we use the single-phase solver to find T,rho given P,Y using a
1690 // bounded 1D solver by adjusting T and using given value of p
1691 CoolPropDbl Tmin, Tmax;
1692 switch (HEOS._phase) {
1693 case iphase_gas: {
1694 Tmax = 1.5 * HEOS.Tmax();
1695 if (HEOS._p < HEOS.p_triple()) {
1696 Tmin = std::max(HEOS.Tmin(), HEOS.Ttriple());
1697 } else {
1698
1699 if (get_config_bool(ENABLE_SUPERANCILLARIES) && HEOS.is_pure()){
1700 auto& optsuperanc = HEOS.get_superanc_optional();
1701 if (optsuperanc){
1702 auto& superanc = optsuperanc.value();
1703 CoolPropDbl pmax_num = superanc.get_pmax();
1704 if (HEOS._p > pmax_num){
1705 throw ValueError(format("Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS._p, pmax_num));
1706 }
1707 Tmin = superanc.get_T_from_p(HEOS._p);
1708 break;
1709 }
1710 }
1711 if (saturation_called) {
1712 Tmin = HEOS.SatV->T();
1713 } else {
1714 Tmin = HEOS._TVanc.pt() - 0.01;
1715 }
1716 }
1717 break;
1718 }
1719 case iphase_liquid: {
1720
1721 // Sometimes the minimum pressure for the melting line is a bit above the triple point pressure
1722 if (HEOS.has_melting_line() && HEOS._p > HEOS.calc_melting_line(iP_min, -1, -1)) {
1723 Tmin = HEOS.calc_melting_line(iT, iP, HEOS._p) - 1e-3;
1724 } else {
1725 Tmin = HEOS.Tmin() - 1e-3;
1726 }
1727
1728 if (get_config_bool(ENABLE_SUPERANCILLARIES) && HEOS.is_pure()){
1729 auto& optsuperanc = HEOS.get_superanc_optional();
1730 if (optsuperanc){
1731 auto& superanc = optsuperanc.value();
1732 CoolPropDbl pmax_num = superanc.get_pmax();
1733 if (HEOS._p > pmax_num){
1734 throw ValueError(format("Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", HEOS._p, pmax_num));
1735 }
1736 Tmax = superanc.get_T_from_p(HEOS._p);
1737 break;
1738 }
1739 }
1740
1741 if (saturation_called) {
1742 Tmax = HEOS.SatL->T();
1743 } else {
1744 Tmax = HEOS._TLanc.pt() + 0.01;
1745 }
1746
1747
1748 break;
1749 }
1752 case iphase_supercritical: {
1753 Tmax = 1.5 * HEOS.Tmax();
1754 // Sometimes the minimum pressure for the melting line is a bit above the triple point pressure
1755 if (HEOS.has_melting_line() && HEOS._p > HEOS.calc_melting_line(iP_min, -1, -1)) {
1756 Tmin = HEOS.calc_melting_line(iT, iP, HEOS._p) - 1e-3;
1757 } else {
1758 Tmin = HEOS.Tmin() - 1e-3;
1759 }
1760 break;
1761 }
1762 default: {
1763 throw ValueError(format("Not a valid homogeneous state"));
1764 }
1765 }
1766 try {
1767 HSU_P_flash_singlephase_Brent(HEOS, other, value, Tmin, Tmax, HEOS._phase);
1768 } catch (std::exception& e) {
1769 throw ValueError(format("unable to solve 1phase PY flash with Tmin=%Lg, Tmax=%Lg due to error: %s", Tmin, Tmax, e.what()));
1770 }
1771 HEOS._Q = -1;
1772 // Update the state for conditions where the state was guessed
1774 }
1775 } else {
1776 if (HEOS.PhaseEnvelope.built) {
1777 // Determine whether you are inside or outside
1778 SimpleState closest_state;
1779 std::size_t iclosest;
1780 bool twophase = PhaseEnvelopeRoutines::is_inside(HEOS.PhaseEnvelope, iP, HEOS._p, other, value, iclosest, closest_state);
1781
1782 if (!twophase) {
1783 PY_singlephase_flash_resid resid(HEOS, HEOS._p, other, value);
1784 // If that fails, try a bounded solver
1785 Brent(resid, closest_state.T + 10, 1000, DBL_EPSILON, 1e-10, 100);
1786 HEOS.unspecify_phase();
1787 } else {
1788 throw ValueError("two-phase solution for Y");
1789 }
1790
1791 } else {
1792 throw ValueError("phase envelope must be built to carry out HSU_P_flash for mixture");
1793 }
1794 }
1795}
1797 // Define the residual to be driven to zero
1798 class solver_resid : public FuncWrapper1DWithTwoDerivs
1799 {
1800 public:
1802 CoolPropDbl T, value;
1803 parameters other;
1804
1805 solver_resid(HelmholtzEOSMixtureBackend* HEOS, CoolPropDbl T, CoolPropDbl value, parameters other)
1806 : HEOS(HEOS), T(T), value(value), other(other) {}
1807 double call(double rhomolar) {
1808 HEOS->update_DmolarT_direct(rhomolar, T);
1809 double eos = HEOS->keyed_output(other);
1810 return eos - value;
1811 };
1812 double deriv(double rhomolar) {
1813 return HEOS->first_partial_deriv(other, iDmolar, iT);
1814 }
1815 double second_deriv(double rhomolar) {
1816 return HEOS->second_partial_deriv(other, iDmolar, iT, iDmolar, iT);
1817 }
1818 };
1819 solver_resid resid(&HEOS, T, value, other);
1820
1821 double T_critical_ = (HEOS.is_pure_or_pseudopure) ? HEOS.T_critical() : HEOS._crit.T;
1822
1823 // Supercritical temperature
1824 if (HEOS._T > T_critical_) {
1825 CoolPropDbl yc, ymin, y;
1827 CoolPropDbl rhomin = 1e-10;
1828
1829 // Determine limits for the other variable
1830 switch (other) {
1831 case iSmolar: {
1832 yc = HEOS.calc_smolar_nocache(HEOS._T, rhoc);
1833 ymin = HEOS.calc_smolar_nocache(HEOS._T, rhomin);
1834 y = HEOS._smolar;
1835 break;
1836 }
1837 case iHmolar: {
1838 yc = HEOS.calc_hmolar_nocache(HEOS._T, rhoc);
1839 ymin = HEOS.calc_hmolar_nocache(HEOS._T, rhomin);
1840 y = HEOS._hmolar;
1841 break;
1842 }
1843 case iUmolar: {
1844 yc = HEOS.calc_umolar_nocache(HEOS._T, rhoc);
1845 ymin = HEOS.calc_umolar_nocache(HEOS._T, rhomin);
1846 y = HEOS._umolar;
1847 break;
1848 }
1849 default:
1850 throw ValueError();
1851 }
1852 if (is_in_closed_range(yc, ymin, y)) {
1853 Brent(resid, rhoc, rhomin, LDBL_EPSILON, 1e-9, 100);
1854 } else if (y < yc) {
1855 // Increase rhomelt until it bounds the solution
1856 int step_count = 0;
1857 while (!is_in_closed_range(ymin, yc, y)) {
1858 rhoc *= 1.1; // Increase density by a few percent
1859 switch (other) {
1860 case iSmolar:
1861 yc = HEOS.calc_smolar_nocache(HEOS._T, rhoc);
1862 break;
1863 case iHmolar:
1864 yc = HEOS.calc_hmolar_nocache(HEOS._T, rhoc);
1865 break;
1866 case iUmolar:
1867 yc = HEOS.calc_umolar_nocache(HEOS._T, rhoc);
1868 break;
1869 default:
1870 throw ValueError(format("Input is invalid"));
1871 }
1872 if (step_count > 30) {
1873 throw ValueError(format("Even by increasing rhoc, not able to bound input; input %Lg is not in range %Lg,%Lg", y, yc, ymin));
1874 }
1875 step_count++;
1876 }
1877 Brent(resid, rhomin, rhoc, LDBL_EPSILON, 1e-9, 100);
1878 } else {
1879 throw ValueError(format("input %Lg is not in range %Lg,%Lg,%Lg", y, yc, ymin));
1880 }
1881 // Update the state (T > Tc)
1882 if (HEOS._p < HEOS.p_critical()) {
1884 } else {
1886 }
1887 }
1888 // Subcritical temperature liquid
1889 else if ((HEOS._phase == iphase_liquid) || (HEOS._phase == iphase_supercritical_liquid)) {
1890 CoolPropDbl ymelt, yL, y;
1891 CoolPropDbl rhomelt = HEOS.components[0].triple_liquid.rhomolar;
1892 CoolPropDbl rhoL = static_cast<double>(HEOS._rhoLanc);
1893
1894 switch (other) {
1895 case iSmolar: {
1896 ymelt = HEOS.calc_smolar_nocache(HEOS._T, rhomelt);
1897 yL = HEOS.calc_smolar_nocache(HEOS._T, rhoL);
1898 y = HEOS._smolar;
1899 break;
1900 }
1901 case iHmolar: {
1902 ymelt = HEOS.calc_hmolar_nocache(HEOS._T, rhomelt);
1903 yL = HEOS.calc_hmolar_nocache(HEOS._T, rhoL);
1904 y = HEOS._hmolar;
1905 break;
1906 }
1907 case iUmolar: {
1908 ymelt = HEOS.calc_umolar_nocache(HEOS._T, rhomelt);
1909 yL = HEOS.calc_umolar_nocache(HEOS._T, rhoL);
1910 y = HEOS._umolar;
1911 break;
1912 }
1913 default:
1914 throw ValueError();
1915 }
1916
1917 CoolPropDbl rhomolar_guess = (rhomelt - rhoL) / (ymelt - yL) * (y - yL) + rhoL;
1918
1919 try {
1920 Halley(resid, rhomolar_guess, 1e-8, 100);
1921 } catch (...) {
1922 Secant(resid, rhomolar_guess, 0.0001 * rhomolar_guess, 1e-12, 100);
1923 }
1924 }
1925 // Subcritical temperature gas
1926 else if (HEOS._phase == iphase_gas) {
1927 CoolPropDbl rhomin = 1e-14;
1928 CoolPropDbl rhoV = static_cast<double>(HEOS._rhoVanc);
1929
1930 try {
1931 Halley(resid, 0.5 * (rhomin + rhoV), 1e-8, 100);
1932 } catch (...) {
1933 try {
1934 Brent(resid, rhomin, rhoV, LDBL_EPSILON, 1e-12, 100);
1935 } catch (...) {
1936 throw ValueError();
1937 }
1938 }
1939 } else {
1940 throw ValueError(format("phase to solver_for_rho_given_T_oneof_HSU is invalid"));
1941 }
1942};
1943
1946 // Use the phase defined by the imposed phase
1947 HEOS._phase = HEOS.imposed_phase_index;
1948 double T_critical_ = (HEOS.is_pure_or_pseudopure) ? HEOS.T_critical() : HEOS._crit.T ;
1949 // The remaining code in this branch was added to set some needed parameters if phase is imposed,
1950 // since HEOS.T_phase_determination_pure_or_pseudopure() is not being called.
1951 if (HEOS._T < T_critical_) //
1952 {
1953 // TODO: is it a bug that this branch can be accessed for mixtures?
1954 HEOS._rhoVanc = HEOS.components[0].ancillaries.rhoV.evaluate(HEOS._T);
1955 HEOS._rhoLanc = HEOS.components[0].ancillaries.rhoL.evaluate(HEOS._T);
1956 if (HEOS._phase == iphase_liquid) {
1957 HEOS._Q = -1000;
1958 } else if (HEOS._phase == iphase_gas) {
1959 HEOS._Q = 1000;
1960 } else if (HEOS._phase == iphase_twophase) {
1961 // Actually have to use saturation information sadly
1962 // For the given temperature, find the saturation state
1963 // Run the saturation routines to determine the saturation densities and pressures
1966 SaturationSolvers::saturation_T_pure(HEOS1, HEOS._T, options);
1967
1968 if (other != iDmolar) {
1969 // Update the states
1970 if (HEOS.SatL) HEOS.SatL->update(DmolarT_INPUTS, HEOS._rhoLanc, HEOS._T);
1971 if (HEOS.SatV) HEOS.SatV->update(DmolarT_INPUTS, HEOS._rhoVanc, HEOS._T);
1972 // Update the two-Phase variables
1973 HEOS._rhoLmolar = HEOS.SatL->rhomolar();
1974 HEOS._rhoVmolar = HEOS.SatV->rhomolar();
1975 }
1976
1977 CoolPropDbl Q;
1978
1979 switch (other) {
1980 case iDmolar:
1981 Q = (1 / HEOS.rhomolar() - 1 / HEOS1.SatL->rhomolar()) / (1 / HEOS1.SatV->rhomolar() - 1 / HEOS1.SatL->rhomolar());
1982 break;
1983 case iSmolar:
1984 Q = (HEOS.smolar() - HEOS1.SatL->smolar()) / (HEOS1.SatV->smolar() - HEOS1.SatL->smolar());
1985 break;
1986 case iHmolar:
1987 Q = (HEOS.hmolar() - HEOS1.SatL->hmolar()) / (HEOS1.SatV->hmolar() - HEOS1.SatL->hmolar());
1988 break;
1989 case iUmolar:
1990 Q = (HEOS.umolar() - HEOS1.SatL->umolar()) / (HEOS1.SatV->umolar() - HEOS1.SatL->umolar());
1991 break;
1992 default:
1993 throw ValueError(format("bad input for other"));
1994 }
1995 if (Q < 0) {
1996 HEOS._Q = -1;
1997 } else if (Q > 1) {
1998 HEOS._Q = 1;
1999 } else {
2000 HEOS._Q = Q;
2001 // Load the outputs
2002 HEOS._p = HEOS._Q * HEOS1.SatV->p() + (1 - HEOS._Q) * HEOS1.SatL->p();
2003 HEOS._rhomolar = 1 / (HEOS._Q / HEOS.SatV->rhomolar() + (1 - HEOS._Q) / HEOS.SatL->rhomolar());
2004 }
2005 } else if (HEOS._phase == iphase_supercritical_liquid) {
2006 HEOS._Q = -1000;
2007 } else
2008 throw ValueError(format("Temperature specified is not the imposed phase region."));
2009 } else if (HEOS._T > T_critical_ && HEOS._T > HEOS.components[0].EOS().Ttriple) {
2010 HEOS._Q = 1e9;
2011 }
2012 } else {
2013 if (HEOS.is_pure_or_pseudopure) {
2014 // Find the phase, while updating all internal variables possible
2015 switch (other) {
2016 case iDmolar:
2018 break;
2019 case iSmolar:
2021 break;
2022 case iHmolar:
2024 break;
2025 case iUmolar:
2027 break;
2028 default:
2029 throw ValueError(format("Input is invalid"));
2030 }
2031 } else {
2032 HEOS._phase = iphase_gas;
2033 throw NotImplementedError("DHSU_T_flash does not support mixtures (yet)");
2034 }
2035 }
2036
2037 //if (HEOS.isHomogeneousPhase() && !ValidNumber(HEOS._p)) // original, pre 1352
2038 // only the solver requires single phase
2039 if (((other == iDmolar) || HEOS.isHomogeneousPhase()) && !ValidNumber(HEOS._p)) // post 1352
2040 {
2041 switch (other) {
2042 case iDmolar:
2043 break;
2044 case iHmolar:
2046 break;
2047 case iSmolar:
2049 break;
2050 case iUmolar:
2052 break;
2053 default:
2054 break;
2055 }
2056 HEOS.calc_pressure();
2057 HEOS._Q = -1;
2058 }
2059 if (HEOS.is_pure_or_pseudopure && HEOS._phase != iphase_twophase) {
2060 // Update the state for conditions where the state was guessed
2062 }
2063}
2065 HS_flash_twophaseOptions& options) {
2066 class Residual : public FuncWrapper1D
2067 {
2068
2069 public:
2071 CoolPropDbl hmolar, smolar, Qs;
2072 Residual(HelmholtzEOSMixtureBackend& HEOS, CoolPropDbl hmolar_spec, CoolPropDbl smolar_spec)
2073 : HEOS(HEOS), hmolar(hmolar_spec), smolar(smolar_spec), Qs(_HUGE){};
2074 double call(double T) {
2075 HEOS.update(QT_INPUTS, 0, T);
2076 HelmholtzEOSMixtureBackend &SatL = HEOS.get_SatL(), &SatV = HEOS.get_SatV();
2077 // Quality from entropy
2078 Qs = (smolar - SatL.smolar()) / (SatV.smolar() - SatL.smolar());
2079 // Quality from enthalpy
2080 CoolPropDbl Qh = (hmolar - SatL.hmolar()) / (SatV.hmolar() - SatL.hmolar());
2081 // Residual is the difference between the two
2082 return Qh - Qs;
2083 }
2084 } resid(HEOS, hmolar_spec, smolar_spec);
2085
2086 // Critical point for pure fluids, slightly different for pseudo-pure, very different for mixtures
2087 CoolPropDbl Tmax_sat = HEOS.calc_Tmax_sat() - 1e-13;
2088
2089 // Check what the minimum limits for the equation of state are
2090 CoolPropDbl Tmin_satL, Tmin_satV, Tmin_sat;
2091 HEOS.calc_Tmin_sat(Tmin_satL, Tmin_satV);
2092 Tmin_sat = std::max(Tmin_satL, Tmin_satV) - 1e-13;
2093
2094 Brent(resid, Tmin_sat, Tmax_sat - 0.01, DBL_EPSILON, 1e-12, 20);
2095 // Run once more with the final vapor quality
2096 HEOS.update(QT_INPUTS, resid.Qs, HEOS.T());
2097}
2099 HS_flash_singlephaseOptions& options) {
2100 int iter = 0;
2101 double resid = 9e30, resid_old = 9e30;
2102 CoolProp::SimpleState reducing = HEOS.get_state("reducing");
2103 do {
2104 // Independent variables are T0 and rhomolar0, residuals are matching h and s
2105 Eigen::Vector2d r;
2106 Eigen::Matrix2d J;
2107 r(0) = HEOS.hmolar() - hmolar_spec;
2108 r(1) = HEOS.smolar() - smolar_spec;
2109 J(0, 0) = HEOS.first_partial_deriv(iHmolar, iTau, iDelta);
2110 J(0, 1) = HEOS.first_partial_deriv(iHmolar, iDelta, iTau);
2111 J(1, 0) = HEOS.first_partial_deriv(iSmolar, iTau, iDelta);
2112 J(1, 1) = HEOS.first_partial_deriv(iSmolar, iDelta, iTau);
2113 // Step in v obtained from Jv = -r
2114 Eigen::Vector2d v = J.colPivHouseholderQr().solve(-r);
2115 bool good_solution = false;
2116 double tau0 = HEOS.tau(), delta0 = HEOS.delta();
2117 // Calculate the old residual after the last step
2118 resid_old = sqrt(POW2(HEOS.hmolar() - hmolar_spec) + POW2(HEOS.smolar() - smolar_spec));
2119 for (double frac = 1.0; frac > 0.001; frac /= 2) {
2120 try {
2121 // Calculate new values
2122 double tau_new = tau0 + options.omega * frac * v(0);
2123 double delta_new = delta0 + options.omega * frac * v(1);
2124 double T_new = reducing.T / tau_new;
2125 double rhomolar_new = delta_new * reducing.rhomolar;
2126 // Update state with step
2127 HEOS.update(DmolarT_INPUTS, rhomolar_new, T_new);
2128 resid = sqrt(POW2(HEOS.hmolar() - hmolar_spec) + POW2(HEOS.smolar() - smolar_spec));
2129 if (resid > resid_old) {
2130 throw ValueError(format("residual not decreasing; frac: %g, resid: %g, resid_old: %g", frac, resid, resid_old));
2131 }
2132 good_solution = true;
2133 break;
2134 } catch (...) {
2135 HEOS.clear();
2136 continue;
2137 }
2138 }
2139 if (!good_solution) {
2140 throw ValueError(format("Not able to get a solution"));
2141 }
2142 iter++;
2143 if (iter > 50) {
2144 throw ValueError(format("HS_flash_singlephase took too many iterations; residual is %g; prior was %g", resid, resid_old));
2145 }
2146 } while (std::abs(resid) > 1e-9);
2147}
2149 // Randomly obtain a starting value that is single-phase
2150 double logp = ((double)rand() / (double)RAND_MAX) * (log(HEOS.pmax()) - log(HEOS.p_triple())) + log(HEOS.p_triple());
2151 T = ((double)rand() / (double)RAND_MAX) * (HEOS.Tmax() - HEOS.Ttriple()) + HEOS.Ttriple();
2152 p = exp(logp);
2153}
2155 // Use TS flash and iterate on T (known to be between Tmin and Tmax)
2156 // in order to find H
2157 double hmolar = HEOS.hmolar(), smolar = HEOS.smolar();
2158 class Residual : public FuncWrapper1D
2159 {
2160 public:
2162 CoolPropDbl hmolar, smolar;
2163 Residual(HelmholtzEOSMixtureBackend& HEOS, CoolPropDbl hmolar_spec, CoolPropDbl smolar_spec)
2164 : HEOS(HEOS), hmolar(hmolar_spec), smolar(smolar_spec){};
2165 double call(double T) {
2166 HEOS.update(SmolarT_INPUTS, smolar, T);
2167 double r = HEOS.hmolar() - hmolar;
2168 return r;
2169 }
2170 } resid(HEOS, hmolar, smolar);
2171 std::string errstr;
2172 // Find minimum temperature
2173 bool good_Tmin = false;
2174 double Tmin = HEOS.Ttriple();
2175 double rmin;
2176 do {
2177 try {
2178 rmin = resid.call(Tmin);
2179 good_Tmin = true;
2180 } catch (...) {
2181 Tmin += 0.5;
2182 }
2183 if (Tmin > HEOS.Tmax()) {
2184 throw ValueError("Cannot find good Tmin");
2185 }
2186 } while (!good_Tmin);
2187
2188 // Find maximum temperature
2189 bool good_Tmax = false;
2190 double Tmax = HEOS.Tmax() * 1.01; // Just a little above, so if we use Tmax as input, it should still work
2191 double rmax;
2192 do {
2193 try {
2194 rmax = resid.call(Tmax);
2195 good_Tmax = true;
2196 } catch (...) {
2197 Tmax -= 0.1;
2198 }
2199 if (Tmax < Tmin) {
2200 throw ValueError("Cannot find good Tmax");
2201 }
2202 } while (!good_Tmax);
2203 if (rmin * rmax > 0 && std::abs(rmax) < std::abs(rmin)) {
2204 throw CoolProp::ValueError(format("HS inputs correspond to temperature above maximum temperature of EOS [%g K]", HEOS.Tmax()));
2205 }
2206 Brent(resid, Tmin, Tmax, DBL_EPSILON, 1e-10, 100);
2207}
2208
2209#if defined(ENABLE_CATCH)
2210
2211TEST_CASE("PD with T very large should yield error", "[PDflash]") {
2212 shared_ptr<HelmholtzEOSBackend> HEOS(new HelmholtzEOSBackend("R134a"));
2213 double Tc = HEOS->T_critical();
2214 HEOS->update(DmassT_INPUTS, 1.1, 1.5 * Tc);
2215 CHECK_THROWS(HEOS->update(DmassP_INPUTS, 2, 5 * HEOS->p()));
2216}
2217
2218TEST_CASE("Stability testing", "[stability]") {
2219 shared_ptr<HelmholtzEOSMixtureBackend> HEOS(new HelmholtzEOSMixtureBackend(strsplit("n-Propane&n-Butane&n-Pentane&n-Hexane", '&')));
2220 std::vector<double> z(4);
2221 z[0] = 0.1;
2222 z[1] = 0.2;
2223 z[2] = 0.3;
2224 z[3] = 0.4;
2225 HEOS->set_mole_fractions(z);
2226
2227 HEOS->update(PQ_INPUTS, 101325, 0);
2228 double TL = HEOS->T();
2229
2230 HEOS->update(PQ_INPUTS, 101325, 1);
2231 double TV = HEOS->T();
2232
2233 SECTION("Liquid (feed is stable)") {
2234 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
2235 for (double T = TL - 1; T >= 100; T -= 1) {
2236 stability_tester.set_TP(T, 101325);
2237 CAPTURE(T);
2238 CHECK_NOTHROW(stability_tester.is_stable());
2239 }
2240 }
2241 SECTION("Vapor (feed is stable)") {
2242 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
2243 for (double T = TV + 1; T <= 500; T += 1) {
2244 stability_tester.set_TP(T, 101325);
2245 CAPTURE(T);
2246 CHECK_NOTHROW(stability_tester.is_stable());
2247 }
2248 }
2249 SECTION("Two-phase (feed is unstable)") {
2250 StabilityRoutines::StabilityEvaluationClass stability_tester(*HEOS);
2251 stability_tester.set_TP((TV + TL) / 2.0, 101325);
2252 CHECK(stability_tester.is_stable() == false);
2253 }
2254}
2255
2256TEST_CASE("Test critical points for methane + H2S", "[critical_points]") {
2257 shared_ptr<HelmholtzEOSMixtureBackend> HEOS(new HelmholtzEOSMixtureBackend(strsplit("Methane&H2S", '&')));
2258
2259 double zz[] = {0.998, 0.97, 0.9475, 0.94, 0.93, 0.86, 0.85, 0.84, 0.75, 0.53, 0.52, 0.51, 0.49, 0.36, 0.24, 0.229, 0.09};
2260 int Npts[] = {2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 1, 1, 1, 1}; // Number of critical points that should be found
2261 int imax = sizeof(zz) / sizeof(double);
2262
2263 for (int i = 0; i < imax; ++i) {
2264 double z0 = zz[i];
2265 std::vector<double> z(2);
2266 z[0] = z0;
2267 z[1] = 1 - z0;
2268 HEOS->set_mole_fractions(z);
2269 CAPTURE(z0);
2270 std::vector<CriticalState> pts = HEOS->all_critical_points();
2271 CHECK(pts.size() == Npts[i]);
2272 }
2273}
2274
2275TEST_CASE("Test critical points for nitrogen + ethane with HEOS", "[critical_points]") {
2276 shared_ptr<HelmholtzEOSMixtureBackend> HEOS(new HelmholtzEOSMixtureBackend(strsplit("Nitrogen&Ethane", '&')));
2277 std::vector<double> zz = linspace(0.001, 0.999, 21);
2278 int failure_count = 0;
2279 for (int i = 0; i < static_cast<std::size_t>(zz.size()); ++i) {
2280 double z0 = zz[i];
2281 std::vector<double> z(2);
2282 z[0] = z0;
2283 z[1] = 1 - z0;
2284 HEOS->set_mole_fractions(z);
2285 CAPTURE(z0);
2286 std::vector<CriticalState> pts;
2287 try{
2288 pts = HEOS->all_critical_points();
2289 }
2290 catch(std::exception& e){
2291 CAPTURE(e.what());
2292 failure_count++;
2293 }
2294 }
2295 // Only an error if more than half fail;
2296 CHECK(failure_count < 10);
2297}
2298
2299TEST_CASE("Test critical points for nitrogen + ethane with PR", "[critical_points]") {
2300 shared_ptr<PengRobinsonBackend> HEOS(new PengRobinsonBackend(strsplit("Nitrogen&Ethane", '&')));
2301 HEOS->set_binary_interaction_double(0, 1, "kij", 0.0407); // Ramırez-Jimenez et al.
2302 std::vector<double> zz = linspace(0.001, 0.999, 21);
2303 for (int i = 0; i < static_cast<std::size_t>(zz.size()); ++i) {
2304 double z0 = zz[i];
2305 std::vector<double> z(2);
2306 z[0] = z0;
2307 z[1] = 1 - z0;
2308 HEOS->set_mole_fractions(z);
2309 CAPTURE(z0);
2310 std::vector<CriticalState> pts;
2311 CHECK_NOTHROW(pts = HEOS->all_critical_points());
2312 }
2313}
2314
2315#endif
2316
2317} /* namespace CoolProp */