GCC Code Coverage Report


Directory: ../../../builds/dumux-repositories/
File: dumux/dumux/material/eos/pengrobinson.hh
Date: 2025-04-12 19:19:20
Exec Total Coverage
Lines: 114 123 92.7%
Functions: 7 9 77.8%
Branches: 45 158 28.5%

Line Branch Exec Source
1 // -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 // vi: set et ts=4 sw=4 sts=4:
3 //
4 // SPDX-FileCopyrightText: Copyright © DuMux Project contributors, see AUTHORS.md in root folder
5 // SPDX-License-Identifier: GPL-3.0-or-later
6 //
7 /*!
8 * \file
9 * \ingroup EOS
10 * \brief Implements the Peng-Robinson equation of state for liquids
11 * and gases.
12 *
13 * See:
14 *
15 * D.-Y. Peng, D.B. Robinson (1976, pp. 59–64) \cite peng1976 <BR>
16 * https://doi.org/10.1021/i160057a011
17 *
18 * R. Reid, et al. "The properties of gases and liquids" (1987, pp. 42-44, 82)
19 * \cite reid1987
20 */
21 #ifndef DUMUX_PENG_ROBINSON_HH
22 #define DUMUX_PENG_ROBINSON_HH
23
24 #include <dune/common/math.hh>
25 #include <dumux/material/fluidstates/temperatureoverlay.hh>
26 #include <dumux/material/idealgas.hh>
27 #include <dumux/common/exceptions.hh>
28 #include <dumux/common/math.hh>
29 #include <dumux/common/tabulated2dfunction.hh>
30
31 #include <iostream>
32
33 namespace Dumux {
34
35 /*!
36 * \ingroup EOS
37 * \brief Implements the Peng-Robinson equation of state for liquids
38 * and gases.
39 *
40 * See:
41 *
42 * D.-Y. Peng, D.B. Robinson (1976, pp. 59–64) \cite peng1976 <BR>
43 *
44 * R. Reid, et al. (1987, pp. 42-44, 82) \cite reid1987
45 */
46 template <class Scalar>
47 class PengRobinson
48 {
49 PengRobinson()
50 { }
51
52 public:
53 2 static void init(Scalar aMin, Scalar aMax, int na,
54 Scalar bMin, Scalar bMax, int nb)
55 {
56 // resize the tabulation for the critical points
57 2 criticalTemperature_.resize(aMin, aMax, na, bMin, bMax, nb);
58 2 criticalPressure_.resize(aMin, aMax, na, bMin, bMax, nb);
59 2 criticalMolarVolume_.resize(aMin, aMax, na, bMin, bMax, nb);
60
61 2 Scalar VmCrit = 18e-6;
62 2 Scalar pCrit = 1e7;
63 2 Scalar TCrit = 273;
64
2/2
✓ Branch 0 taken 200 times.
✓ Branch 1 taken 2 times.
202 for (int i = 0; i < na; ++i) {
65 200 Scalar a = criticalTemperature_.iToX(i);
66
2/2
✓ Branch 1 taken 40000 times.
✓ Branch 2 taken 200 times.
40200 for (int j = 0; j < nb; ++j) {
67 40000 Scalar b = criticalTemperature_.jToY(j);
68
69 40000 findCriticalPoint_(TCrit, pCrit, VmCrit, a, b);
70
71 40000 criticalTemperature_.setSamplePoint(i, j, TCrit);
72 40000 criticalPressure_.setSamplePoint(i, j, pCrit);
73 40000 criticalMolarVolume_.setSamplePoint(i, j, VmCrit);
74 }
75 }
76 2 };
77
78 /*!
79 * \brief Predicts the vapor pressure \f$\mathrm{[Pa]}\f$ for the temperature given in
80 * setTP().
81 * \param T temperature in \f$\mathrm{[K]}\f$
82 * \param params Parameters
83 *
84 * Initially, the vapor pressure is roughly estimated by using the
85 * Ambrose-Walton method, then the Newton method is used to make
86 * difference between the gas and liquid phase fugacity zero.
87 */
88 template <class Params>
89 static Scalar computeVaporPressure(const Params &params, Scalar T)
90 {
91 using Component = typename Params::Component;
92 if (T >= Component::criticalTemperature())
93 return Component::criticalPressure();
94
95 // initial guess of the vapor pressure
96 Scalar Vm[3];
97 const Scalar eps = Component::criticalPressure()*1e-10;
98
99 // use the Ambrose-Walton method to get an initial guess of
100 // the vapor pressure
101 Scalar pVap = ambroseWalton_(params, T);
102
103 // Newton-Raphson method
104 for (int i = 0; i < 5; ++i) {
105 // calculate the molar densities
106 assert(molarVolumes(Vm, params, T, pVap) == 3);
107
108 Scalar f = fugacityDifference_(params, T, pVap, Vm[0], Vm[2]);
109 Scalar df_dp =
110 fugacityDifference_(params, T, pVap + eps, Vm[0], Vm[2])
111 -
112 fugacityDifference_(params, T, pVap - eps, Vm[0], Vm[2]);
113 df_dp /= 2*eps;
114
115 Scalar delta = f/df_dp;
116 pVap = pVap - delta;
117 using std::abs;
118 if (abs(delta/pVap) < 1e-10)
119 break;
120 }
121
122 return pVap;
123 }
124
125 /*!
126 * \brief Computes molar volumes \f$\mathrm{[m^3 / mol]}\f$ where the Peng-Robinson EOS is
127 * true.
128 * \param fs Thermodynamic state of the fluids
129 * \param params Parameters
130 * \param phaseIdx The phase index
131 * \param isGasPhase Specifies the phase state
132 * \param handleUnphysicalPhase Special handling of the case when the EOS has only one
133 intersection with the pressure, and the intersection does not correspond to
134 the given phase (the phase is thus considered unphysical). If it happens in
135 the case of critical fluid, the critical molar volume is returned for the
136 unphysical phase. If the fluid is not critical, a proper extremum of the
137 EOS is returned for the unphysical phase. If the parameter is false and the
138 EOS has only one intersection with the pressure, the molar volume is computed
139 from that single intersection, not depending of the given phase (gas or
140 fluid). If the EOS has three intersections with the pressure, this parameter
141 is ignored.
142 */
143 template <class FluidState, class Params>
144 38009 static Scalar computeMolarVolume(const FluidState &fs,
145 Params &params,
146 int phaseIdx,
147 bool isGasPhase,
148 bool handleUnphysicalPhase = true)
149 {
150 38009 Scalar Vm = 0;
151
152 38009 Scalar T = fs.temperature(phaseIdx);
153 38009 Scalar p = fs.pressure(phaseIdx);
154
155 38009 Scalar a = params.a(phaseIdx); // "attractive factor"
156 38009 Scalar b = params.b(phaseIdx); // "co-volume"
157
158 38009 Scalar RT = Constants<Scalar>::R*T;
159 38009 Scalar Astar = a*p/(RT*RT);
160 38009 Scalar Bstar = b*p/RT;
161
162 38009 Scalar a1 = 1.0;
163 38009 Scalar a2 = - (1 - Bstar);
164 38009 Scalar a3 = Astar - Bstar*(3*Bstar + 2);
165 38009 Scalar a4 = Bstar*(- Astar + Bstar*(1 + Bstar));
166
167 38009 Scalar Z[3] = {}; // zero-initializer
168 38009 int numSol = invertCubicPolynomial(Z, a1, a2, a3, a4);
169
170 // ignore the first two results if the smallest
171 // compressibility factor is <= 0.0. (this means that if we
172 // would get negative molar volumes for the liquid phase, we
173 // consider the liquid phase non-existent.)
174 // Note that invertCubicPolynomial sorts the roots in ascending order
175
3/4
✓ Branch 0 taken 12270 times.
✓ Branch 1 taken 25739 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 12270 times.
38009 if (numSol > 1 && Z[0] <= 0.0) {
176 Z[0] = Z[numSol - 1];
177 numSol = 1;
178 }
179
180
2/4
✓ Branch 0 taken 38009 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 38009 times.
38009 if (numSol > 0 && Z[0] <= 0)
181 DUNE_THROW(NumericalProblem, "No positive compressibility factors found");
182
183
2/2
✓ Branch 0 taken 12270 times.
✓ Branch 1 taken 25739 times.
38009 if (numSol == 3) {
184 // the EOS has three intersections with the pressure,
185 // i.e. the molar volume of gas is the largest one and the
186 // molar volume of liquid is the smallest one
187
2/2
✓ Branch 0 taken 77 times.
✓ Branch 1 taken 12193 times.
12270 if (isGasPhase)
188 77 Vm = Z[2]*RT/p;
189 else
190 12193 Vm = Z[0]*RT/p;
191 }
192
1/2
✓ Branch 0 taken 25739 times.
✗ Branch 1 not taken.
25739 else if (numSol == 1) {
193 // the EOS has only one intersection with the pressure
194
195 25739 Vm = Z[0]*RT/p;
196
197 // Handle single root case specially unless told otherwise
198
1/2
✓ Branch 0 taken 25739 times.
✗ Branch 1 not taken.
25739 if (handleUnphysicalPhase)
199 25739 Vm = handleSingleRoot_(Vm, fs, params, phaseIdx, isGasPhase);
200 }
201 else
202
1/26
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✓ Branch 38 taken 38009 times.
✗ Branch 39 not taken.
38009 DUNE_THROW(NumericalProblem, "Unexpected number of roots in the equation for compressibility factor: " << numSol);
203
204 using std::isfinite;
205
206
2/4
✓ Branch 0 taken 38009 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38009 times.
✗ Branch 3 not taken.
38009 if (!isfinite(Vm) || Vm <= 0)
207 DUNE_THROW(NumericalProblem, "Bad molar volume");
208
209 38009 return Vm;
210 }
211
212 /*!
213 * \brief Returns the critical temperature for a given mix
214 *
215 * \param params EOS (equation of state) parameters of a single-component fluid \
216 * (usually PengRobinsonParms) or a mixture (usually PengRobinsonMixtureParams)
217 */
218 template <class Params>
219 static Scalar criticalTemperature(const Params &params)
220 {
221 // Here, a() is the attractive force parameter and b()
222 // is the repulsive force parameter of the EOS
223 return criticalTemperature_(params.a(), params.b());
224 }
225
226 /*!
227 * \brief Returns the fugacity coefficient \f$\mathrm{[-]}\f$ for a given pressure
228 * and molar volume.
229 *
230 * This is the same value as computeFugacity() because the mole
231 * fraction of a component in a pure fluid is obviously always
232 * 100%.
233 *
234 * \param params Parameters
235 */
236 template <class Params>
237 static Scalar computeFugacityCoeffient(const Params &params)
238 {
239 Scalar T = params.temperature();
240 Scalar p = params.pressure();
241 Scalar Vm = params.molarVolume();
242
243 Scalar RT = Constants<Scalar>::R*T;
244 Scalar Z = p*Vm/RT;
245 Scalar Bstar = p*params.b() / RT;
246 using std::sqrt;
247 Scalar tmp =
248 (Vm + params.b()*(1 + sqrt(2))) /
249 (Vm + params.b()*(1 - sqrt(2)));
250 Scalar expo = - params.a()/(RT * 2 * params.b() * sqrt(2));
251 using std::exp;
252 using std::pow;
253 Scalar fugCoeff =
254 exp(Z - 1) / (Z - Bstar) *
255 pow(tmp, expo);
256
257 return fugCoeff;
258 }
259
260 /*!
261 * \brief Returns the fugacity coefficient \f$\mathrm{[-]}\f$ for a given pressure
262 * and molar volume.
263 *
264 * This is the fugacity coefficient times the pressure. The mole
265 * fraction of a component in a pure fluid is obviously always
266 * 100%, so it is not required.
267 *
268 * \param params Parameters
269 */
270 template <class Params>
271 static Scalar computeFugacity(const Params &params)
272 { return params.pressure()*computeFugacityCoeff(params); }
273
274 protected:
275
276 /*
277 * Handle single-root case in the equation of state. The problem here is
278 * that only one phase exists in this case, while we should also figure out
279 * something to return for the other phase. For reference, see Andreas
280 * Lauser's thesis: http://dx.doi.org/10.18419/opus-516 (page 32).
281 */
282 template <class FluidState, class Params>
283 25739 static Scalar handleSingleRoot_(Scalar Vm,
284 const FluidState &fs,
285 const Params &params,
286 int phaseIdx,
287 bool isGasPhase)
288 {
289 25739 Scalar T = fs.temperature(phaseIdx);
290
291 // for the other phase, we take the extremum of the EOS
292 // with the largest distance from the intersection.
293
2/2
✓ Branch 3 taken 18553 times.
✓ Branch 4 taken 7186 times.
25739 if (T > criticalTemperature_(params.a(phaseIdx), params.b(phaseIdx))) {
294 // if the EOS does not exhibit any extrema, the fluid
295 // is critical...
296 18553 return handleCriticalFluid_(Vm, fs, params, phaseIdx, isGasPhase);
297 }
298 else {
299 // find the extrema (if they are present)
300 Scalar Vmin, Vmax, pmin, pmax;
301 using std::min;
302 using std::max;
303
1/2
✓ Branch 3 taken 7186 times.
✗ Branch 4 not taken.
7186 if (findExtrema_(Vmin, Vmax, pmin, pmax,
304 params.a(phaseIdx), params.b(phaseIdx), T))
305
2/2
✓ Branch 0 taken 396 times.
✓ Branch 1 taken 6790 times.
14372 return isGasPhase ? max(Vmax, Vm) : min(Vmin, Vm);
306 }
307 return Vm;
308 }
309
310 template <class FluidState, class Params>
311 18553 static Scalar handleCriticalFluid_(Scalar Vm,
312 const FluidState &fs,
313 const Params &params,
314 int phaseIdx,
315 bool isGasPhase)
316 {
317 18553 Scalar Vcrit = criticalMolarVolume_(params.a(phaseIdx), params.b(phaseIdx));
318
319 using std::max;
320 using std::min;
321
2/2
✓ Branch 0 taken 18531 times.
✓ Branch 1 taken 22 times.
37106 return isGasPhase ? max(Vm, Vcrit) : min(Vm, Vcrit);
322 }
323
324 40000 static void findCriticalPoint_(Scalar &Tcrit,
325 Scalar &pcrit,
326 Scalar &Vcrit,
327 Scalar a,
328 Scalar b)
329 {
330 Scalar minVm;
331 Scalar maxVm;
332
333 40000 Scalar minP(0);
334 40000 Scalar maxP(1e100);
335
336 // first, we need to find an isotherm where the EOS exhibits
337 // a maximum and a minimum
338 40000 Scalar Tmin = 250; // [K]
339
1/2
✓ Branch 0 taken 45194 times.
✗ Branch 1 not taken.
45194 for (int i = 0; i < 30; ++i) {
340 45194 bool hasExtrema = findExtrema_(minVm, maxVm, minP, maxP, a, b, Tmin);
341
2/2
✓ Branch 0 taken 5194 times.
✓ Branch 1 taken 40000 times.
45194 if (hasExtrema)
342 break;
343 5194 Tmin /= 2;
344 }
345
346 Scalar T = Tmin;
347
348 // Newton's method: Start at minimum temperature and minimize
349 // the "gap" between the extrema of the EOS
350
2/2
✓ Branch 0 taken 811690 times.
✓ Branch 1 taken 7924 times.
819614 for (int i = 0; i < 25; ++i) {
351 // calculate function we would like to minimize
352 811690 Scalar f = maxVm - minVm;
353
354 // check if we're converged
355
2/2
✓ Branch 0 taken 32076 times.
✓ Branch 1 taken 779614 times.
811690 if (f < 1e-10) {
356 32076 Tcrit = T;
357 32076 pcrit = (minP + maxP)/2;
358 32076 Vcrit = (maxVm + minVm)/2;
359 32076 return;
360 }
361
362 // backward differences. Forward differences are not
363 // robust, since the isotherm could be critical if an
364 // epsilon was added to the temperature. (this is case
365 // rarely happens, though)
366 779614 const Scalar eps = - 1e-8;
367 779614 [[maybe_unused]] bool hasExtrema = findExtrema_(minVm, maxVm, minP, maxP, a, b, T + eps);
368
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 779614 times.
779614 assert(hasExtrema);
369 779614 Scalar fStar = maxVm - minVm;
370
371 // derivative of the difference between the maximum's
372 // molar volume and the minimum's molar volume regarding
373 // temperature
374 779614 Scalar fPrime = (fStar - f)/eps;
375
376 // update value for the current iteration
377 779614 Scalar delta = f/fPrime;
378
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 779614 times.
779614 if (delta > 0)
379 delta = -10;
380
381 // line search (we have to make sure that both extrema
382 // still exist after the update)
383 1496330 for (int j = 0; ; ++j) {
384
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2275944 times.
2275944 if (j >= 20) {
385 DUNE_THROW(NumericalProblem,
386 "Could not determine the critical point for a=" << a << ", b=" << b);
387 }
388
389
2/2
✓ Branch 1 taken 779614 times.
✓ Branch 2 taken 1496330 times.
2275944 if (findExtrema_(minVm, maxVm, minP, maxP, a, b, T - delta)) {
390 // if the isotherm for T - delta exhibits two
391 // extrema the update is finished
392 779614 T -= delta;
393 break;
394 }
395 else
396 1496330 delta /= 2;
397 }
398 }
399 }
400
401 // find the two molar volumes where the EOS exhibits extrema and
402 // which are larger than the covolume of the phase
403 3107938 static bool findExtrema_(Scalar &Vmin,
404 Scalar &Vmax,
405 Scalar &pMin,
406 Scalar &pMax,
407 Scalar a,
408 Scalar b,
409 Scalar T)
410 {
411 3107938 Scalar u = 2;
412 3107938 Scalar w = -1;
413
414 3107938 Scalar RT = Constants<Scalar>::R*T;
415
416 // calculate coefficients of the 4th order polynominal in
417 // monomial basis
418 3107938 Scalar a1 = RT;
419 3107938 Scalar a2 = 2*RT*u*b - 2*a;
420 3107938 Scalar a3 = 2*RT*w*b*b + RT*u*u*b*b + 4*a*b - u*a*b;
421 3107938 Scalar a4 = 2*RT*u*w*b*b*b + 2*u*a*b*b - 2*a*b*b;
422 3107938 Scalar a5 = RT*w*w*b*b*b*b - u*a*b*b*b;
423
424 // Newton method to find first root
425
426 // if the values which we got on Vmin and Vmax are useful, we
427 // will reuse them as initial value, else we will start 10%
428 // above the covolume
429 3107938 Scalar V = b*1.1;
430 3107938 Scalar delta = 1.0;
431 using std::abs;
432
2/2
✓ Branch 0 taken 12360727 times.
✓ Branch 1 taken 3107880 times.
15468607 for (int i = 0; abs(delta) > 1e-9; ++i) {
433 12360727 Scalar f = a5 + V*(a4 + V*(a3 + V*(a2 + V*a1)));
434
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12360727 times.
12360727 Scalar fPrime = a4 + V*(2*a3 + V*(3*a2 + V*4*a1));
435
436
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12360727 times.
12360727 if (abs(fPrime) < 1e-20) {
437 // give up if the derivative is zero
438 Vmin = 0;
439 Vmax = 0;
440 return false;
441 }
442
443
444 12360727 delta = f/fPrime;
445 12360727 V -= delta;
446
447
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 12360669 times.
12360727 if (i > 20) {
448 // give up after 20 iterations...
449 58 Vmin = 0;
450 58 Vmax = 0;
451 58 return false;
452 }
453 }
454
455 // polynomial division
456 3107880 Scalar b1 = a1;
457 3107880 Scalar b2 = a2 + V*b1;
458 3107880 Scalar b3 = a3 + V*b2;
459 3107880 Scalar b4 = a4 + V*b3;
460
461 // invert resulting cubic polynomial analytically
462 Scalar allV[4];
463 3107880 allV[0] = V;
464 3107880 int numSol = 1 + invertCubicPolynomial(allV + 1, b1, b2, b3, b4);
465
466 // sort all roots of the derivative
467 3107880 std::sort(allV + 0, allV + numSol);
468
469 // check whether the result is physical
470
2/2
✓ Branch 0 taken 1501466 times.
✓ Branch 1 taken 1606414 times.
3107880 if (allV[numSol - 2] < b) {
471 // the second largest extremum is smaller than the phase's
472 // covolume which is physically impossible
473 1501466 Vmin = Vmax = 0;
474 1501466 return false;
475 }
476
477 // it seems that everything is okay...
478 1606414 Vmin = allV[numSol - 2];
479 1606414 Vmax = allV[numSol - 1];
480 1606414 return true;
481 }
482
483 /*!
484 * \brief The Ambrose-Walton method to estimate the vapor
485 * pressure.
486 *
487 * \return Vapor pressure estimate in bar
488 *
489 * See:
490 *
491 * D. Ambrose, J. Walton (1989, pp. 1395-1403) \cite ambrose1989
492 */
493 template <class Params>
494 static Scalar ambroseWalton_(const Params &params, Scalar T)
495 {
496 using Component = typename Params::Component;
497
498 Scalar Tr = T / Component::criticalTemperature();
499 Scalar tau = 1 - Tr;
500 Scalar omega = Component::acentricFactor();
501 using std::sqrt;
502 using Dune::power;
503 Scalar f0 = (tau*(-5.97616 + sqrt(tau)*(1.29874 - tau*0.60394)) - 1.06841*power(tau, 5))/Tr;
504 Scalar f1 = (tau*(-5.03365 + sqrt(tau)*(1.11505 - tau*5.41217)) - 7.46628*power(tau, 5))/Tr;
505 Scalar f2 = (tau*(-0.64771 + sqrt(tau)*(2.41539 - tau*4.26979)) + 3.25259*power(tau, 5))/Tr;
506 using std::exp;
507 return Component::criticalPressure()*exp(f0 + omega * (f1 + omega*f2));
508 }
509
510 /*!
511 * \brief Returns the difference between the liquid and the gas phase
512 * fugacities in [bar]
513 *
514 * \param params Parameters
515 * \param T Temperature [K]
516 * \param p Pressure [bar]
517 * \param VmLiquid Molar volume of the liquid phase [cm^3/mol]
518 * \param VmGas Molar volume of the gas phase [cm^3/mol]
519 */
520 template <class Params>
521 static Scalar fugacityDifference_(const Params &params,
522 Scalar T,
523 Scalar p,
524 Scalar VmLiquid,
525 Scalar VmGas)
526 { return fugacity(params, T, p, VmLiquid) - fugacity(params, T, p, VmGas); }
527
528 static Tabulated2DFunction<Scalar> criticalTemperature_;
529 static Tabulated2DFunction<Scalar> criticalPressure_;
530 static Tabulated2DFunction<Scalar> criticalMolarVolume_;
531 };
532
533 template <class Scalar>
534 Tabulated2DFunction<Scalar> PengRobinson<Scalar>::criticalTemperature_;
535
536 template <class Scalar>
537 Tabulated2DFunction<Scalar> PengRobinson<Scalar>::criticalPressure_;
538
539 template <class Scalar>
540 Tabulated2DFunction<Scalar> PengRobinson<Scalar>::criticalMolarVolume_;
541
542 } // end namespace
543
544 #endif
545