1 /**
2  * @file WaterPropsIAPWS.cpp
3  * Definitions for a class for calculating the equation of state of water
4  * from the IAPWS 1995 Formulation based on the steam tables thermodynamic
5  * basis (See class \link Cantera::WaterPropsIAPWS WaterPropsIAPWS\endlink).
6  */
7 
8 // This file is part of Cantera. See License.txt in the top-level directory or
9 // at https://cantera.org/license.txt for license and copyright information.
10 
11 #include "cantera/thermo/WaterPropsIAPWS.h"
12 #include "cantera/base/ctexceptions.h"
13 #include "cantera/base/stringUtils.h"
14 
15 namespace Cantera
16 {
17 // Critical Point values of water in mks units
18 
19 //! Critical Temperature value (kelvin)
20 const doublereal T_c = 647.096;
21 //! Critical Pressure (Pascals)
22 static const doublereal P_c = 22.064E6;
23 //! Value of the Density at the critical point (kg m-3)
24 const doublereal Rho_c = 322.;
25 //! Molecular Weight of water that is consistent with the paper (kg kmol-1)
26 static const doublereal M_water = 18.015268;
27 
28 //! Gas constant that is quoted in the paper
29 /*
30  * Note, this is the Rgas value quoted in the paper. For consistency
31  * we have to use that value and not the updated value
32  *
33  * The Ratio of R/M = 0.46151805 kJ kg-1 K-1 , which is Eqn. (6.3) in the paper.
34  */
35 static const doublereal Rgas = 8.314371E3; // Joules kmol-1 K-1
36 
37 // Base constructor
WaterPropsIAPWS()38 WaterPropsIAPWS::WaterPropsIAPWS() :
39     tau(-1.0),
40     delta(-1.0),
41     iState(-30000)
42 {
43 }
44 
calcDim(doublereal temperature,doublereal rho)45 void WaterPropsIAPWS::calcDim(doublereal temperature, doublereal rho)
46 {
47     tau = T_c / temperature;
48     delta = rho / Rho_c;
49 
50     // Determine the internal state
51     if (temperature > T_c) {
52         iState = WATER_SUPERCRIT;
53     } else {
54         if (delta < 1.0) {
55             iState = WATER_GAS;
56         } else {
57             iState = WATER_LIQUID;
58         }
59     }
60 }
61 
helmholtzFE() const62 doublereal WaterPropsIAPWS::helmholtzFE() const
63 {
64     doublereal retn = m_phi.phi(tau, delta);
65     doublereal temperature = T_c/tau;
66     doublereal RT = Rgas * temperature;
67     return retn * RT;
68 }
69 
pressure() const70 doublereal WaterPropsIAPWS::pressure() const
71 {
72     doublereal retn = m_phi.pressureM_rhoRT(tau, delta);
73     doublereal rho = delta * Rho_c;
74     doublereal temperature = T_c / tau;
75     return retn * rho * Rgas * temperature/M_water;
76 }
77 
density(doublereal temperature,doublereal pressure,int phase,doublereal rhoguess)78 doublereal WaterPropsIAPWS::density(doublereal temperature, doublereal pressure,
79                                     int phase, doublereal rhoguess)
80 {
81     if (fabs(pressure - P_c) / P_c < 1.e-8 &&
82         fabs(temperature - T_c) / T_c < 1.e-8) {
83         // Catch critical point, as no solution is found otherwise
84         setState_TR(temperature, Rho_c);
85         return Rho_c;
86     }
87     doublereal deltaGuess = 0.0;
88     if (rhoguess == -1.0) {
89         if (phase != -1) {
90             if (temperature > T_c) {
91                 rhoguess = pressure * M_water / (Rgas * temperature);
92             } else {
93                 if (phase == WATER_GAS || phase == WATER_SUPERCRIT) {
94                     rhoguess = pressure * M_water / (Rgas * temperature);
95                 } else if (phase == WATER_LIQUID) {
96                     // Provide a guess about the liquid density that is
97                     // relatively high -> convergence from above seems robust.
98                     rhoguess = 1000.;
99                 } else if (phase == WATER_UNSTABLELIQUID || phase == WATER_UNSTABLEGAS) {
100                     throw CanteraError("WaterPropsIAPWS::density",
101                                        "Unstable Branch finder is untested");
102                 } else {
103                     throw CanteraError("WaterPropsIAPWS::density",
104                                        "unknown state: {}", phase);
105                 }
106             }
107         } else {
108             // Assume the Gas phase initial guess, if nothing is specified to
109             // the routine
110             rhoguess = pressure * M_water / (Rgas * temperature);
111         }
112     }
113     doublereal p_red = pressure * M_water / (Rgas * temperature * Rho_c);
114     deltaGuess = rhoguess / Rho_c;
115     setState_TR(temperature, rhoguess);
116     doublereal delta_retn = m_phi.dfind(p_red, tau, deltaGuess);
117     if (delta_retn <= 0) {
118         // No solution found for first initial guess; perturb initial guess once
119         // to avoid spurious failures (band-aid fix)
120         delta_retn = m_phi.dfind(p_red, tau, 0.9 * deltaGuess);
121     }
122     doublereal density_retn;
123     if (delta_retn > 0.0) {
124         delta = delta_retn;
125 
126         // Dimensionalize the density before returning
127         density_retn = delta_retn * Rho_c;
128 
129         // Set the internal state -> this may be a duplication. However, let's
130         // just be sure.
131         setState_TR(temperature, density_retn);
132     } else {
133         density_retn = -1.0;
134     }
135     return density_retn;
136 }
137 
density_const(doublereal pressure,int phase,doublereal rhoguess) const138 doublereal WaterPropsIAPWS::density_const(doublereal pressure,
139         int phase, doublereal rhoguess) const
140 {
141     doublereal temperature = T_c / tau;
142     doublereal deltaGuess = 0.0;
143     doublereal deltaSave = delta;
144     if (rhoguess == -1.0) {
145         if (phase != -1) {
146             if (temperature > T_c) {
147                 rhoguess = pressure * M_water / (Rgas * temperature);
148             } else {
149                 if (phase == WATER_GAS || phase == WATER_SUPERCRIT) {
150                     rhoguess = pressure * M_water / (Rgas * temperature);
151                 } else if (phase == WATER_LIQUID) {
152                     // Provide a guess about the liquid density that is
153                     // relatively high -> convergence from above seems robust.
154                     rhoguess = 1000.;
155                 } else if (phase == WATER_UNSTABLELIQUID || phase == WATER_UNSTABLEGAS) {
156                     throw CanteraError("WaterPropsIAPWS::density_const",
157                                        "Unstable Branch finder is untested");
158                 } else {
159                     throw CanteraError("WaterPropsIAPWS::density_const",
160                                        "unknown state: {}", phase);
161                 }
162             }
163         } else {
164             // Assume the Gas phase initial guess, if nothing is specified to
165             // the routine
166             rhoguess = pressure * M_water / (Rgas * temperature);
167         }
168     }
169     doublereal p_red = pressure * M_water / (Rgas * temperature * Rho_c);
170     deltaGuess = rhoguess / Rho_c;
171 
172     delta = deltaGuess;
173     m_phi.tdpolycalc(tau, delta);
174 
175     doublereal delta_retn = m_phi.dfind(p_red, tau, deltaGuess);
176     doublereal density_retn;
177     if (delta_retn > 0.0) {
178         delta = delta_retn;
179 
180         // Dimensionalize the density before returning
181         density_retn = delta_retn * Rho_c;
182 
183     } else {
184         density_retn = -1.0;
185     }
186 
187     delta = deltaSave;
188     m_phi.tdpolycalc(tau, delta);
189     return density_retn;
190 }
191 
density() const192 doublereal WaterPropsIAPWS::density() const
193 {
194     return delta * Rho_c;
195 }
196 
temperature() const197 doublereal WaterPropsIAPWS::temperature() const
198 {
199     return T_c / tau;
200 }
201 
psat_est(doublereal temperature) const202 doublereal WaterPropsIAPWS::psat_est(doublereal temperature) const
203 {
204     // Formula and constants from: "NBS/NRC Steam Tables: Thermodynamic and
205     // Transport Properties and Computer Programs for Vapor and Liquid States of
206     // Water in SI Units". L. Haar, J. S. Gallagher, G. S. Kell. Hemisphere
207     // Publishing. 1984.
208     static const doublereal A[8] = {
209         -7.8889166E0,
210         2.5514255E0,
211         -6.716169E0,
212         33.2239495E0,
213         -105.38479E0,
214         174.35319E0,
215         -148.39348E0,
216         48.631602E0
217     };
218     doublereal ps;
219     if (temperature < 314.) {
220         doublereal pl = 6.3573118E0 - 8858.843E0 / temperature
221                         + 607.56335E0 * pow(temperature, -0.6);
222         ps = 0.1 * exp(pl);
223     } else {
224         doublereal v = temperature / 647.25;
225         doublereal w = fabs(1.0-v);
226         doublereal b = 0.0;
227         for (int i = 0; i < 8; i++) {
228             doublereal z = i + 1;
229             b += A[i] * pow(w, ((z+1.0)/2.0));
230         }
231         doublereal q = b / v;
232         ps = 22.093*exp(q);
233     }
234 
235     // Original correlation was in cgs. Convert to mks
236     ps *= 1.0E6;
237     return ps;
238 }
239 
isothermalCompressibility() const240 doublereal WaterPropsIAPWS::isothermalCompressibility() const
241 {
242     doublereal dpdrho_val = dpdrho();
243     doublereal dens = delta * Rho_c;
244     return 1.0 / (dens * dpdrho_val);
245 }
246 
dpdrho() const247 doublereal WaterPropsIAPWS::dpdrho() const
248 {
249     doublereal retn = m_phi.dimdpdrho(tau, delta);
250     doublereal temperature = T_c/tau;
251     return retn * Rgas * temperature / M_water;
252 }
253 
coeffPresExp() const254 doublereal WaterPropsIAPWS::coeffPresExp() const
255 {
256     return m_phi.dimdpdT(tau, delta);
257 }
258 
coeffThermExp() const259 doublereal WaterPropsIAPWS::coeffThermExp() const
260 {
261     doublereal kappa = isothermalCompressibility();
262     doublereal beta = coeffPresExp();
263     doublereal dens = delta * Rho_c;
264     return kappa * dens * Rgas * beta / M_water;
265 }
266 
Gibbs() const267 doublereal WaterPropsIAPWS::Gibbs() const
268 {
269     doublereal gRT = m_phi.gibbs_RT();
270     doublereal temperature = T_c/tau;
271     return gRT * Rgas * temperature;
272 }
273 
corr(doublereal temperature,doublereal pressure,doublereal & densLiq,doublereal & densGas,doublereal & delGRT)274 void WaterPropsIAPWS::corr(doublereal temperature, doublereal pressure,
275     doublereal& densLiq, doublereal& densGas, doublereal& delGRT)
276 {
277     densLiq = density(temperature, pressure, WATER_LIQUID, densLiq);
278     if (densLiq <= 0.0) {
279         throw CanteraError("WaterPropsIAPWS::corr",
280             "Error occurred trying to find liquid density at (T,P) = {}  {}",
281             temperature, pressure);
282     }
283     setState_TR(temperature, densLiq);
284     doublereal gibbsLiqRT = m_phi.gibbs_RT();
285 
286     densGas = density(temperature, pressure, WATER_GAS, densGas);
287     if (densGas <= 0.0) {
288         throw CanteraError("WaterPropsIAPWS::corr",
289             "Error occurred trying to find gas density at (T,P) = {}  {}",
290             temperature, pressure);
291     }
292     setState_TR(temperature, densGas);
293     doublereal gibbsGasRT = m_phi.gibbs_RT();
294 
295     delGRT = gibbsLiqRT - gibbsGasRT;
296 }
297 
corr1(doublereal temperature,doublereal pressure,doublereal & densLiq,doublereal & densGas,doublereal & pcorr)298 void WaterPropsIAPWS::corr1(doublereal temperature, doublereal pressure,
299     doublereal& densLiq, doublereal& densGas, doublereal& pcorr)
300 {
301     densLiq = density(temperature, pressure, WATER_LIQUID, densLiq);
302     if (densLiq <= 0.0) {
303         throw CanteraError("WaterPropsIAPWS::corr1",
304             "Error occurred trying to find liquid density at (T,P) = {}  {}",
305             temperature, pressure);
306     }
307     setState_TR(temperature, densLiq);
308     doublereal prL = m_phi.phiR();
309 
310     densGas = density(temperature, pressure, WATER_GAS, densGas);
311     if (densGas <= 0.0) {
312         throw CanteraError("WaterPropsIAPWS::corr1",
313             "Error occurred trying to find gas density at (T,P) = {}  {}",
314             temperature, pressure);
315     }
316     setState_TR(temperature, densGas);
317     doublereal prG = m_phi.phiR();
318     doublereal rhs = (prL - prG) + log(densLiq/densGas);
319     rhs /= (1.0/densGas - 1.0/densLiq);
320     pcorr = rhs * Rgas * temperature / M_water;
321 }
322 
psat(doublereal temperature,int waterState)323 doublereal WaterPropsIAPWS::psat(doublereal temperature, int waterState)
324 {
325     static int method = 1;
326     doublereal densLiq = -1.0, densGas = -1.0, delGRT = 0.0;
327     doublereal dp, pcorr;
328     if (temperature >= T_c) {
329         densGas = density(temperature, P_c, WATER_SUPERCRIT);
330         setState_TR(temperature, densGas);
331         return P_c;
332     }
333     doublereal p = psat_est(temperature);
334     for (int i = 0; i < 30; i++) {
335         if (method == 1) {
336             corr(temperature, p, densLiq, densGas, delGRT);
337             doublereal delV = M_water * (1.0/densLiq - 1.0/densGas);
338             dp = - delGRT * Rgas * temperature / delV;
339         } else {
340             corr1(temperature, p, densLiq, densGas, pcorr);
341             dp = pcorr - p;
342         }
343         p += dp;
344 
345         if ((method == 1) && delGRT < 1.0E-8) {
346             break;
347         } else {
348             if (fabs(dp/p) < 1.0E-9) {
349                 break;
350             }
351         }
352     }
353     // Put the fluid in the desired end condition
354     if (waterState == WATER_LIQUID) {
355         setState_TR(temperature, densLiq);
356     } else if (waterState == WATER_GAS) {
357         setState_TR(temperature, densGas);
358     } else {
359         throw CanteraError("WaterPropsIAPWS::psat",
360                            "unknown water state input: {}", waterState);
361     }
362     return p;
363 }
364 
phaseState(bool checkState) const365 int WaterPropsIAPWS::phaseState(bool checkState) const
366 {
367     if (checkState) {
368         if (tau <= 1.0) {
369             iState = WATER_SUPERCRIT;
370         } else {
371             doublereal T = T_c / tau;
372             doublereal rho = delta * Rho_c;
373             doublereal rhoMidAtm = 0.5 * (OneAtm * M_water / (Rgas * 373.15) + 1.0E3);
374             doublereal rhoMid = Rho_c + (T - T_c) * (Rho_c - rhoMidAtm) / (T_c - 373.15);
375             int iStateGuess = WATER_LIQUID;
376             if (rho < rhoMid) {
377                 iStateGuess = WATER_GAS;
378             }
379             doublereal kappa = isothermalCompressibility();
380             if (kappa >= 0.0) {
381                 iState = iStateGuess;
382             } else {
383                 // When we are here we are between the spinodal curves
384                 doublereal rhoDel = rho * 1.000001;
385                 doublereal deltaSave = delta;
386                 doublereal deltaDel = rhoDel / Rho_c;
387                 delta = deltaDel;
388                 m_phi.tdpolycalc(tau, deltaDel);
389 
390                 doublereal kappaDel = isothermalCompressibility();
391                 doublereal d2rhodp2 = (rhoDel * kappaDel - rho * kappa) / (rhoDel - rho);
392                 if (d2rhodp2 > 0.0) {
393                     iState = WATER_UNSTABLELIQUID;
394                 } else {
395                     iState = WATER_UNSTABLEGAS;
396                 }
397                 delta = deltaSave;
398                 m_phi.tdpolycalc(tau, delta);
399             }
400         }
401     }
402     return iState;
403 }
404 
densSpinodalWater() const405 doublereal WaterPropsIAPWS::densSpinodalWater() const
406 {
407     doublereal temperature = T_c/tau;
408     doublereal delta_save = delta;
409     // return the critical density if we are above or even just a little below
410     // the critical temperature. We just don't want to worry about the critical
411     // point at this juncture.
412     if (temperature >= T_c - 0.001) {
413         return Rho_c;
414     }
415     doublereal p = psat_est(temperature);
416     doublereal rho_low = 0.0;
417     doublereal rho_high = 1000;
418     doublereal densSatLiq = density_const(p, WATER_LIQUID);
419     doublereal dens_old = densSatLiq;
420     delta = dens_old / Rho_c;
421     m_phi.tdpolycalc(tau, delta);
422     doublereal dpdrho_old = dpdrho();
423     if (dpdrho_old > 0.0) {
424         rho_high = std::min(dens_old, rho_high);
425     } else {
426         rho_low = std::max(rho_low, dens_old);
427     }
428     doublereal dens_new = densSatLiq* (1.0001);
429     delta = dens_new / Rho_c;
430     m_phi.tdpolycalc(tau, delta);
431     doublereal dpdrho_new = dpdrho();
432     if (dpdrho_new > 0.0) {
433         rho_high = std::min(dens_new, rho_high);
434     } else {
435         rho_low = std::max(rho_low, dens_new);
436     }
437     bool conv = false;
438 
439     for (int it = 0; it < 50; it++) {
440         doublereal slope = (dpdrho_new - dpdrho_old)/(dens_new - dens_old);
441         if (slope >= 0.0) {
442             slope = std::max(slope, dpdrho_new *5.0/ dens_new);
443         } else {
444             slope = -dpdrho_new;
445             // shouldn't be here for liquid spinodal
446         }
447         doublereal delta_rho = - dpdrho_new / slope;
448         if (delta_rho > 0.0) {
449             delta_rho = std::min(delta_rho, dens_new * 0.1);
450         } else {
451             delta_rho = std::max(delta_rho, - dens_new * 0.1);
452         }
453         doublereal dens_est = dens_new + delta_rho;
454         if (dens_est < rho_low) {
455             dens_est = 0.5 * (rho_low + dens_new);
456         }
457         if (dens_est > rho_high) {
458             dens_est = 0.5 * (rho_high + dens_new);
459         }
460 
461         dens_old = dens_new;
462         dpdrho_old = dpdrho_new;
463         dens_new = dens_est;
464 
465         delta = dens_new / Rho_c;
466         m_phi.tdpolycalc(tau, delta);
467         dpdrho_new = dpdrho();
468         if (dpdrho_new > 0.0) {
469             rho_high = std::min(dens_new, rho_high);
470         } else if (dpdrho_new < 0.0) {
471             rho_low = std::max(rho_low, dens_new);
472         } else {
473             conv = true;
474             break;
475         }
476 
477         if (fabs(dpdrho_new) < 1.0E-5) {
478             conv = true;
479             break;
480         }
481     }
482 
483     if (!conv) {
484         throw CanteraError("WaterPropsIAPWS::densSpinodalWater",
485                            "convergence failure");
486     }
487     // Restore the original delta
488     delta = delta_save;
489     m_phi.tdpolycalc(tau, delta);
490     return dens_new;
491 }
492 
densSpinodalSteam() const493 doublereal WaterPropsIAPWS::densSpinodalSteam() const
494 {
495     doublereal temperature = T_c/tau;
496     doublereal delta_save = delta;
497     // return the critical density if we are above or even just a little below
498     // the critical temperature. We just don't want to worry about the critical
499     // point at this juncture.
500     if (temperature >= T_c - 0.001) {
501         return Rho_c;
502     }
503     doublereal p = psat_est(temperature);
504     doublereal rho_low = 0.0;
505     doublereal rho_high = 1000;
506     doublereal densSatGas = density_const(p, WATER_GAS);
507     doublereal dens_old = densSatGas;
508     delta = dens_old / Rho_c;
509     m_phi.tdpolycalc(tau, delta);
510     doublereal dpdrho_old = dpdrho();
511     if (dpdrho_old < 0.0) {
512         rho_high = std::min(dens_old, rho_high);
513     } else {
514         rho_low = std::max(rho_low, dens_old);
515     }
516     doublereal dens_new = densSatGas * (0.99);
517     delta = dens_new / Rho_c;
518     m_phi.tdpolycalc(tau, delta);
519     doublereal dpdrho_new = dpdrho();
520     if (dpdrho_new < 0.0) {
521         rho_high = std::min(dens_new, rho_high);
522     } else {
523         rho_low = std::max(rho_low, dens_new);
524     }
525     bool conv = false;
526     for (int it = 0; it < 50; it++) {
527         doublereal slope = (dpdrho_new - dpdrho_old)/(dens_new - dens_old);
528         if (slope >= 0.0) {
529             slope = dpdrho_new;
530             // shouldn't be here for gas spinodal
531         } else {
532             slope = std::min(slope, dpdrho_new *5.0 / dens_new);
533 
534         }
535         doublereal delta_rho = - dpdrho_new / slope;
536         if (delta_rho > 0.0) {
537             delta_rho = std::min(delta_rho, dens_new * 0.1);
538         } else {
539             delta_rho = std::max(delta_rho, - dens_new * 0.1);
540         }
541         doublereal dens_est = dens_new + delta_rho;
542         if (dens_est < rho_low) {
543             dens_est = 0.5 * (rho_low + dens_new);
544         }
545         if (dens_est > rho_high) {
546             dens_est = 0.5 * (rho_high + dens_new);
547         }
548 
549         dens_old = dens_new;
550         dpdrho_old = dpdrho_new;
551         dens_new = dens_est;
552         delta = dens_new / Rho_c;
553         m_phi.tdpolycalc(tau, delta);
554         dpdrho_new = dpdrho();
555         if (dpdrho_new < 0.0) {
556             rho_high = std::min(dens_new, rho_high);
557         } else if (dpdrho_new > 0.0) {
558             rho_low = std::max(rho_low, dens_new);
559         } else {
560             conv = true;
561             break;
562         }
563 
564         if (fabs(dpdrho_new) < 1.0E-5) {
565             conv = true;
566             break;
567         }
568     }
569 
570     if (!conv) {
571         throw CanteraError("WaterPropsIAPWS::densSpinodalSteam",
572                            "convergence failure");
573     }
574     // Restore the original delta
575     delta = delta_save;
576     m_phi.tdpolycalc(tau, delta);
577     return dens_new;
578 }
579 
setState_TR(doublereal temperature,doublereal rho)580 void WaterPropsIAPWS::setState_TR(doublereal temperature, doublereal rho)
581 {
582     calcDim(temperature, rho);
583     m_phi.tdpolycalc(tau, delta);
584 }
585 
enthalpy() const586 doublereal WaterPropsIAPWS::enthalpy() const
587 {
588     doublereal temperature = T_c/tau;
589     doublereal hRT = m_phi.enthalpy_RT();
590     return hRT * Rgas * temperature;
591 }
592 
intEnergy() const593 doublereal WaterPropsIAPWS::intEnergy() const
594 {
595     doublereal temperature = T_c / tau;
596     doublereal uRT = m_phi.intEnergy_RT();
597     return uRT * Rgas * temperature;
598 }
599 
entropy() const600 doublereal WaterPropsIAPWS::entropy() const
601 {
602     doublereal sR = m_phi.entropy_R();
603     return sR * Rgas;
604 }
605 
cv() const606 doublereal WaterPropsIAPWS::cv() const
607 {
608     doublereal cvR = m_phi.cv_R();
609     return cvR * Rgas;
610 }
611 
cp() const612 doublereal WaterPropsIAPWS::cp() const
613 {
614     doublereal cpR = m_phi.cp_R();
615     return cpR * Rgas;
616 }
617 
molarVolume() const618 doublereal WaterPropsIAPWS::molarVolume() const
619 {
620     doublereal rho = delta * Rho_c;
621     return M_water / rho;
622 }
623 
624 }
625