GCC Code Coverage Report


Directory: ../../../builds/dumux-repositories/
File: /builds/dumux-repositories/dumux/dumux/material/eos/pengrobinson.hh
Date: 2024-05-04 19:09:25
Exec Total Coverage
Lines: 113 124 91.1%
Functions: 7 9 77.8%
Branches: 53 136 39.0%

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-FileCopyrightInfo: 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 DUNE_THROW(NumericalProblem, "Unexpected number of roots in the equation for compressibility factor: " << numSol);
203
204 using std::isfinite;
205
206
3/6
✓ Branch 0 taken 38009 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38009 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 38009 times.
✗ Branch 5 not taken.
76018 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
5/6
✓ Branch 0 taken 396 times.
✓ Branch 1 taken 6790 times.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 385 times.
✓ Branch 4 taken 6790 times.
✗ Branch 5 not taken.
13987 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
5/6
✓ Branch 0 taken 18531 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 334 times.
✓ Branch 3 taken 18197 times.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
18909 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 40000 times.
✓ Branch 1 taken 5194 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 811658 times.
✓ Branch 1 taken 7920 times.
819578 for (int i = 0; i < 25; ++i) {
351 // calculate function we would like to minimize
352 811658 Scalar f = maxVm - minVm;
353
354 // check if we're converged
355
2/2
✓ Branch 0 taken 32080 times.
✓ Branch 1 taken 779578 times.
811658 if (f < 1e-10) {
356 32080 Tcrit = T;
357 32080 pcrit = (minP + maxP)/2;
358 32080 Vcrit = (maxVm + minVm)/2;
359 32080 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 779578 const Scalar eps = - 1e-8;
367 779578 [[maybe_unused]] bool hasExtrema = findExtrema_(minVm, maxVm, minP, maxP, a, b, T + eps);
368
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 779578 times.
779578 assert(hasExtrema);
369 779578 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 779578 Scalar fPrime = (fStar - f)/eps;
375
376 // update value for the current iteration
377 779578 Scalar delta = f/fPrime;
378
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 779578 times.
779578 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 1496318 for (int j = 0; ; ++j) {
384
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2275896 times.
2275896 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 779578 times.
✓ Branch 2 taken 1496318 times.
2275896 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 779578 T -= delta;
393 break;
394 }
395 else
396 1496318 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 3107854 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 3107854 Scalar u = 2;
412 3107854 Scalar w = -1;
413
414 3107854 Scalar RT = Constants<Scalar>::R*T;
415
416 // calculate coefficients of the 4th order polynominal in
417 // monomial basis
418 3107854 Scalar a1 = RT;
419 3107854 Scalar a2 = 2*RT*u*b - 2*a;
420 3107854 Scalar a3 = 2*RT*w*b*b + RT*u*u*b*b + 4*a*b - u*a*b;
421 3107854 Scalar a4 = 2*RT*u*w*b*b*b + 2*u*a*b*b - 2*a*b*b;
422 3107854 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 3107854 Scalar V = b*1.1;
430 3107854 Scalar delta = 1.0;
431 using std::abs;
432
4/4
✓ Branch 0 taken 12360375 times.
✓ Branch 1 taken 3107796 times.
✓ Branch 2 taken 12360375 times.
✓ Branch 3 taken 3107796 times.
15468171 for (int i = 0; abs(delta) > 1e-9; ++i) {
433 12360375 Scalar f = a5 + V*(a4 + V*(a3 + V*(a2 + V*a1)));
434 12360375 Scalar fPrime = a4 + V*(2*a3 + V*(3*a2 + V*4*a1));
435
436
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 12360375 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 12360375 times.
24720750 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 12360375 delta = f/fPrime;
445 12360375 V -= delta;
446
447
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 12360317 times.
12360375 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 3107796 Scalar b1 = a1;
457 3107796 Scalar b2 = a2 + V*b1;
458 3107796 Scalar b3 = a3 + V*b2;
459 3107796 Scalar b4 = a4 + V*b3;
460
461 // invert resulting cubic polynomial analytically
462 Scalar allV[4];
463 3107796 allV[0] = V;
464 3107796 int numSol = 1 + invertCubicPolynomial(allV + 1, b1, b2, b3, b4);
465
466 // sort all roots of the derivative
467 3107796 std::sort(allV + 0, allV + numSol);
468
469 // check whether the result is physical
470
2/2
✓ Branch 0 taken 1501454 times.
✓ Branch 1 taken 1606342 times.
3107796 if (allV[numSol - 2] < b) {
471 // the second largest extremum is smaller than the phase's
472 // covolume which is physically impossible
473 1501454 Vmin = Vmax = 0;
474 1501454 return false;
475 }
476
477 // it seems that everything is okay...
478 1606342 Vmin = allV[numSol - 2];
479 1606342 Vmax = allV[numSol - 1];
480 1606342 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