1 |
/* ASCEND modelling environment |
2 |
Copyright (C) 2011-2012 Carnegie Mellon University |
3 |
|
4 |
This program is free software; you can redistribute it and/or modify |
5 |
it under the terms of the GNU General Public License as published by |
6 |
the Free Software Foundation; either version 2, or (at your option) |
7 |
any later version. |
8 |
|
9 |
This program is distributed in the hope that it will be useful, |
10 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 |
GNU General Public License for more details. |
13 |
|
14 |
You should have received a copy of the GNU General Public License |
15 |
along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 |
*//** @file |
17 |
Implementation of the Peng-Robinson equation of state. |
18 |
|
19 |
For nomenclature see Sandler, 5e and IAPWS-95 and IAPWS Derivatives. |
20 |
|
21 |
Authors: John Pye, Richard Towers, Sean Muratet |
22 |
*/ |
23 |
|
24 |
#include "pengrob.h" |
25 |
#include "rundata.h" |
26 |
#include "cubicroots.h" |
27 |
#include "fprops.h" |
28 |
#include "sat.h" |
29 |
#include "ideal_impl.h" |
30 |
#include "cp0.h" |
31 |
#include "zeroin.h" |
32 |
#include <math.h> |
33 |
#include <stdio.h> |
34 |
|
35 |
#include "helmholtz.h" // for helmholtz_prepare |
36 |
|
37 |
/* these are the 'raw' functions, they don't do phase equilibrium. */ |
38 |
PropEvalFn pengrob_p; |
39 |
PropEvalFn pengrob_u; |
40 |
PropEvalFn pengrob_h; |
41 |
PropEvalFn pengrob_s; |
42 |
PropEvalFn pengrob_a; |
43 |
PropEvalFn pengrob_g; |
44 |
PropEvalFn pengrob_cp; |
45 |
PropEvalFn pengrob_cv; |
46 |
PropEvalFn pengrob_w; |
47 |
PropEvalFn pengrob_dpdrho_T; |
48 |
PropEvalFn pengrob_alphap; |
49 |
PropEvalFn pengrob_betap; |
50 |
SatEvalFn pengrob_sat; |
51 |
//SatEvalFn pengrob_sat_akasaka; |
52 |
|
53 |
static double MidpointPressureCubic(double T, const FluidData *data, FpropsError *err); |
54 |
|
55 |
#define PR_DEBUG |
56 |
#define PR_ERRORS |
57 |
|
58 |
#ifdef PR_DEBUG |
59 |
# include "color.h" |
60 |
# define MSG(FMT, ...) \ |
61 |
color_on(stderr,ASC_FG_BRIGHTRED);\ |
62 |
fprintf(stderr,"%s:%d: ",__FILE__,__LINE__);\ |
63 |
color_on(stderr,ASC_FG_BRIGHTBLUE);\ |
64 |
fprintf(stderr,"%s: ",__func__);\ |
65 |
color_off(stderr);\ |
66 |
fprintf(stderr,FMT "\n",##__VA_ARGS__) |
67 |
#else |
68 |
# define MSG(ARGS...) ((void)0) |
69 |
#endif |
70 |
|
71 |
/* TODO centralise declaration of our error-reporting function somehow...? */ |
72 |
#ifdef PR_ERRORS |
73 |
# include "color.h" |
74 |
# define ERRMSG(STR,...) \ |
75 |
color_on(stderr,ASC_FG_BRIGHTRED);\ |
76 |
fprintf(stderr,"ERROR:");\ |
77 |
color_off(stderr);\ |
78 |
fprintf(stderr," %s:%d:" STR "\n", __func__, __LINE__ ,##__VA_ARGS__) |
79 |
#else |
80 |
# define ERRMSG(ARGS...) ((void)0) |
81 |
#endif |
82 |
|
83 |
|
84 |
PureFluid *pengrob_prepare(const EosData *E, const ReferenceState *ref){ |
85 |
MSG("Preparing PR fluid..."); |
86 |
PureFluid *P = FPROPS_NEW(PureFluid); |
87 |
P->data = FPROPS_NEW(FluidData); |
88 |
|
89 |
/* metadata */ |
90 |
P->name = E->name; |
91 |
P->source = E->source; |
92 |
P->type = FPROPS_PENGROB; |
93 |
|
94 |
#define D P->data |
95 |
/* common data across all correlation types */ |
96 |
switch(E->type){ |
97 |
case FPROPS_HELMHOLTZ: |
98 |
#define I E->data.helm |
99 |
D->M = I->M; |
100 |
D->R = I->R; |
101 |
D->T_t = I->T_t; |
102 |
D->T_c = I->T_c; |
103 |
D->rho_c = I->rho_c; |
104 |
D->omega = I->omega; |
105 |
D->cp0 = cp0_prepare(I->ideal, D->R, D->T_c); |
106 |
|
107 |
// helmholtz data doesn't include p_c so we need to calculate it somehow... |
108 |
|
109 |
#ifdef USE_HELMHOLTZ_RHO_C |
110 |
// use Zc = 0.307 to calculate p_c from the T_c and rho_c in the |
111 |
// helmholtz data. This seems to be a bad choice |
112 |
// since eg for CO2 it gives a significantly overestimated p_c. |
113 |
{ |
114 |
double Zc = 0.307; |
115 |
D->p_c = Zc * D->R * D->T_c * D->rho_c; |
116 |
MSG("p_c = %f", D->p_c); |
117 |
} |
118 |
#else // USE HELMHOLTZ P_C |
119 |
// Probably the preferred alternative in this case is to |
120 |
// use rho_c and T_c to calculated helmholtz_p(T_c, rho_c), then |
121 |
// use that pressure as the PR p_c, together with updating the |
122 |
// value of rho_c for consistency with PR EOS (from the known |
123 |
// Z_c = 0.307. |
124 |
{ |
125 |
FpropsError herr = FPROPS_NO_ERROR; |
126 |
MSG("Preparing helmholtz data '%s'...",E->name); |
127 |
PureFluid *PH = helmholtz_prepare(E,ref); |
128 |
if(!PH){ |
129 |
ERRMSG("Failed to create Helmholtz runtime data"); |
130 |
return NULL; |
131 |
} |
132 |
D->p_c = PH->p_fn(D->T_c, D->rho_c, PH->data, &herr); |
133 |
MSG("Calculated p_c = %f from Helmholtz data",D->p_c); |
134 |
if(herr){ |
135 |
ERRMSG("Failed to calculate critical pressure (%s)",fprops_error(herr)); |
136 |
return NULL; |
137 |
} |
138 |
double Zc = 0.307; |
139 |
D->rho_c = D->p_c / (Zc * D->R * D->T_c); |
140 |
} |
141 |
#endif |
142 |
break; |
143 |
#undef I |
144 |
case FPROPS_CUBIC: |
145 |
#define I E->data.cubic |
146 |
D->M = I->M; |
147 |
D->R = R_UNIVERSAL / I->M; |
148 |
D->T_t = I->T_t; |
149 |
D->T_c = I->T_c; |
150 |
D->p_c = I->p_c; |
151 |
D->rho_c = I->rho_c; |
152 |
D->omega = I->omega; |
153 |
D->cp0 = cp0_prepare(I->ideal, D->R, D->T_c); |
154 |
break; |
155 |
default: |
156 |
fprintf(stderr,"Invalid EOS data\n"); |
157 |
return NULL; |
158 |
} |
159 |
|
160 |
if(D->p_c == 0){ |
161 |
ERRMSG("ERROR p_c is zero in this data, need to calculate it here somehow"); |
162 |
return NULL; |
163 |
} |
164 |
|
165 |
/* FIXME note that in the following paper, the constants in the PR EOS |
166 |
are given with lots more decimal places. Need to figure out if it's |
167 |
preferable to use those extra DPs, or not...? |
168 |
http://www.che.uah.edu/courseware/che641/peng-robinson-derivatives-joule-thompson.pdf |
169 |
*/ |
170 |
|
171 |
/* NOTE: we're using a mass basis for all our property calculations. That |
172 |
means that our 'b' is the usual 'b/M' since our 'R' is 'Rm/M'. Because of |
173 |
this, our p is still OK, since Ru/M / (Vm/M - b/M) is still the same value.*/ |
174 |
#define C P->data->corr.pengrob |
175 |
C = FPROPS_NEW(PengrobRunData); |
176 |
C->aTc = 0.45724 * SQ(D->R * D->T_c) / D->p_c; |
177 |
C->b = 0.07780 * D->R * D->T_c / D->p_c; |
178 |
C->kappa = 0.37464 + (1.54226 - 0.26992 * D->omega) * D->omega; |
179 |
|
180 |
/* function pointers... more to come still? */ |
181 |
#define FN(VAR) P->VAR##_fn = &pengrob_##VAR |
182 |
FN(p); FN(u); FN(h); FN(s); FN(a); FN(g); FN(cp); FN(cv); FN(w); |
183 |
FN(dpdrho_T); FN(alphap); FN(betap); |
184 |
FN(sat); |
185 |
#undef FN |
186 |
#undef I |
187 |
#undef D |
188 |
#undef C |
189 |
//P->sat_fn = &pengrob_sat_akasaka; |
190 |
|
191 |
return P; |
192 |
} |
193 |
|
194 |
|
195 |
/* shortcuts take us straight into the correct data structure */ |
196 |
#define PD data->corr.pengrob |
197 |
#define PD_TCRIT data->T_c |
198 |
#define PD_RHOCRIT data->rho_c |
199 |
#define PD_M data->M |
200 |
#define SQRT2 1.4142135623730951 |
201 |
|
202 |
#define DEFINE_SQRTALPHA \ |
203 |
double sqrtalpha = 1 + PD->kappa * (1 - sqrt(T / PD_TCRIT)); |
204 |
#define DEFINE_A \ |
205 |
double a = PD->aTc * SQ(sqrtalpha); |
206 |
|
207 |
#define DEFINE_V double v = 1. / rho; |
208 |
|
209 |
/** |
210 |
Maxima code: |
211 |
a(T) := aTc * (1 + kappa * (1 - sqrt(T/Tc))); |
212 |
diff(a(T),T,1); |
213 |
XXX |
214 |
*/ |
215 |
#define DEFINE_DADT \ |
216 |
double dadT = -PD->kappa * PD->aTc * sqrtalpha / sqrt(T * PD_TCRIT) |
217 |
|
218 |
/** |
219 |
Maxima code: |
220 |
a(T) := aTc * (1 + kappa * (1 - sqrt(T/Tc))); |
221 |
diff(a(T),T,2); |
222 |
XXX |
223 |
*/ |
224 |
#define DEFINE_D2ADT2 \ |
225 |
double d2adt2 = PD->aTc * PD->kappa * sqrt(PD_TCRIT/T) * (1 + PD->kappa) / (2 * T * PD_TCRIT); |
226 |
|
227 |
#define DEFINE_DPDT_RHO \ |
228 |
double dpdT_rho = data->R/(v - PD->b) - dadT/(v*(v + PD->b) + PD->b*(v - PD->b)) |
229 |
|
230 |
double pengrob_p(double T, double rho, const FluidData *data, FpropsError *err){ |
231 |
DEFINE_SQRTALPHA; |
232 |
DEFINE_A; |
233 |
DEFINE_V; |
234 |
double b = PD->b; |
235 |
if(rho > 1./b){ |
236 |
/* TODO check this: is it because it gives p < 0? */ |
237 |
MSG("Density exceeds limit value 1/b = %f",1./b); |
238 |
*err = FPROPS_RANGE_ERROR; |
239 |
} |
240 |
//MSG("v = %f, b = %f",v,b); |
241 |
double p = (data->R * T)/(v - b) - a/(v*(v + b) + b*(v - b)); |
242 |
//MSG("p(T = %f, rho = %f) = %f",T,rho,p); |
243 |
return p; |
244 |
} |
245 |
|
246 |
|
247 |
double pengrob_h(double T, double rho, const FluidData *data, FpropsError *err){ |
248 |
DEFINE_SQRTALPHA; |
249 |
DEFINE_A; |
250 |
DEFINE_V; |
251 |
if(rho > 1./PD->b){ |
252 |
MSG("Density exceeds limit value 1/b = %f",1./PD->b); |
253 |
*err = FPROPS_RANGE_ERROR; |
254 |
return 0; |
255 |
} |
256 |
double h0 = ideal_h(T,rho,data,err); |
257 |
double p = pengrob_p(T, rho, data, err); |
258 |
double Z = p * v / (data->R * T); |
259 |
double B = p * PD->b / (data->R * T); |
260 |
DEFINE_DADT; |
261 |
double hr = data->R * T * (Z - 1) + (T*dadT - a)/(2*SQRT2 * PD->b) * log((Z + (1+SQRT2)*B) / (Z + (1-SQRT2)*B)); |
262 |
return h0 + hr; |
263 |
} |
264 |
|
265 |
|
266 |
double pengrob_s(double T, double rho, const FluidData *data, FpropsError *err){ |
267 |
DEFINE_SQRTALPHA; |
268 |
DEFINE_V; |
269 |
double b = PD->b; |
270 |
if(rho > 1./b){ |
271 |
MSG("Density exceeds limit value 1/b = %f",1./b); |
272 |
*err = FPROPS_RANGE_ERROR; |
273 |
return 0; |
274 |
} |
275 |
double s0 = ideal_s(T,rho,data,err); |
276 |
double p = pengrob_p(T, rho, data, err); |
277 |
double Z = p * v / (data->R * T); |
278 |
double B = p * b / (data->R * T); |
279 |
DEFINE_DADT; |
280 |
//MSG("s0 = %f, p = %f, Z = %f, B = %f, dadt = %f", s0, p, Z, B, dadt); |
281 |
//MSG("log(Z-B) = %f, log((Z+(1+SQRT2)*B)/(Z+(1-SQRT2)*B = %f", log(Z-B), log((Z+(1+SQRT2)*B)/(Z+(1-SQRT2)*B))); |
282 |
double sr = data->R * log(Z-B) + dadT/(2*SQRT2*b) * log((Z+(1+SQRT2)*B)/(Z+(1-SQRT2)*B)); |
283 |
return s0 + sr; |
284 |
} |
285 |
|
286 |
|
287 |
double pengrob_a(double T, double rho, const FluidData *data, FpropsError *err){ |
288 |
// FIXME maybe we can improve this with more direct maths |
289 |
double h = pengrob_h(T,rho,data,err); |
290 |
double s = pengrob_s(T,rho,data,err); // duplicated calculation of p! |
291 |
double p = pengrob_p(T,rho,data,err); // duplicated calculation of p! |
292 |
MSG("h = %f, p = %f, s = %f, rho = %f, T = %f",h,p,s,rho,T); |
293 |
return (h - p/rho) - T * s; |
294 |
// previous code from Richard, probably fine but need to check |
295 |
// DEFINE_A; |
296 |
// double vm = PD_M / rho; |
297 |
// double p = pengrob_p(T, rho, data, err); |
298 |
// return -log(p*(v - PD->b)/(data->R * T))+a/(2*SQRT2 * PD->b * data->R *T)*log((v + (1-SQRT2)*PD->b)/(v + (1+SQRT2)*PD->b)); |
299 |
} |
300 |
|
301 |
|
302 |
double pengrob_u(double T, double rho, const FluidData *data, FpropsError *err){ |
303 |
// FIXME work out a cleaner approach to this... |
304 |
double p = pengrob_p(T, rho, data, err); |
305 |
return pengrob_h(T,rho,data,err) - p/rho; // duplicated calculation of p! |
306 |
} |
307 |
|
308 |
double pengrob_g(double T, double rho, const FluidData *data, FpropsError *err){ |
309 |
if(rho > 1./PD->b){ |
310 |
MSG("Density exceeds limit value 1/b = %f",1./PD->b); |
311 |
*err = FPROPS_RANGE_ERROR; |
312 |
} |
313 |
#if 0 |
314 |
double h = pengrob_h(T,rho,data,err); |
315 |
double s = pengrob_s(T,rho,data,err); // duplicated calculation of p! |
316 |
if(isnan(h))MSG("h is nan"); |
317 |
if(isnan(s))MSG("s is nan"); |
318 |
return h - T*s; |
319 |
#else |
320 |
// previous code from Richard, probably fine but need to check |
321 |
DEFINE_SQRTALPHA; |
322 |
DEFINE_A; |
323 |
DEFINE_V; |
324 |
double p = pengrob_p(T, rho, data, err); |
325 |
double Z = p*v/(data->R * T); |
326 |
double B = p*PD->b/(data->R * T); |
327 |
double A = p * a / SQ(data->R * T); |
328 |
return log(fabs(Z-B))-(A/(sqrt(8)*B))*log(fabs((Z+(1+sqrt(2))*B)/(Z+(1-sqrt(2))*B)))+Z-1; |
329 |
#endif |
330 |
} |
331 |
|
332 |
/** |
333 |
\f[ c_p = \left(\frac{\partial u}{\partial T} \right)_v \f] |
334 |
|
335 |
See Pratt, 2001. "Thermodynamic Properties Involving Derivatives", |
336 |
ASEE Chemical Engineering Division Newsletter, Malaysia |
337 |
http://www.che.uah.edu/courseware/che641/peng-robinson-derivatives-joule-thompson.pdf |
338 |
|
339 |
Maxima code: |
340 |
u(T,x):= (T*diff(a(T),T) - a(T))/b/sqrt(8) * log((Z(T) + B(T)*(1+sqrt(2)))/(Z(T)+B(T)*(1-sqrt(2)))); |
341 |
diff(u(T,v),T); |
342 |
subst(B(T)*(alpha_p - 1/T), diff(B(T),T), %); |
343 |
subst(Z(T)*(alpha_p - 1/T), diff(Z(T),T), %); |
344 |
subst(B(T)*v/Z(T),b,%); |
345 |
subst(b*Z(T)/B(T),v,%); |
346 |
ratsimp(%); |
347 |
*/ |
348 |
double pengrob_cv(double T, double rho, const FluidData *data, FpropsError *err){ |
349 |
DEFINE_V; |
350 |
DEFINE_D2ADT2; |
351 |
double cv0 = ideal_cv(T, rho, data, err); |
352 |
MSG("cv0 = %f",cv0); |
353 |
#define DEFINE_CVR \ |
354 |
double p = pengrob_p(T, rho, data, err); \ |
355 |
double Z = p * v / (data->R * T); \ |
356 |
double B = p * PD->b / (data->R * T); \ |
357 |
double cvr1 = T * d2adt2/ (PD->b * 2*SQRT2); \ |
358 |
double cvr2 = (Z+B*(1+SQRT2))/(Z+B*(1-SQRT2)); \ |
359 |
double cvr = cvr1*log(cvr2); |
360 |
DEFINE_CVR; |
361 |
MSG("d2adT2 = %f", d2adt2); |
362 |
MSG("b = %f",PD->b); |
363 |
MSG("cvr1 = %f, cvr2 = %f, log(cvr2) = %f", cvr1, cvr2, log(cvr2)); |
364 |
MSG("cvr = %f",cvr); |
365 |
return cv0 + cvr;// J/K*Kg |
366 |
} |
367 |
|
368 |
/** |
369 |
Isobaric specific heat capacity |
370 |
\f[ c_p = \left(\frac{\partial h}{\partial T} \right)_p \f] |
371 |
|
372 |
See Pratt, 2001. "Thermodynamic Properties Involving Derivatives", |
373 |
ASEE Chemical Engineering Division Newsletter, Malaysia |
374 |
http://www.che.uah.edu/courseware/che641/peng-robinson-derivatives-joule-thompson.pdf |
375 |
|
376 |
TODO this function needs to be checked. |
377 |
*/ |
378 |
double pengrob_cp(double T, double rho, const FluidData *data, FpropsError *err){ |
379 |
//these calculations are broken apart intentionally to help provide clarity as the calculations are tedious |
380 |
DEFINE_SQRTALPHA; |
381 |
DEFINE_A; |
382 |
DEFINE_V; |
383 |
DEFINE_DADT; |
384 |
DEFINE_D2ADT2; |
385 |
DEFINE_CVR; |
386 |
double cp0 = ideal_cp(T, rho, data, err); |
387 |
DEFINE_DPDT_RHO; |
388 |
|
389 |
#define DEFINE_CPR \ |
390 |
double A = p * a / SQ(data->R * T); \ |
391 |
double dAdT_p = p / SQ(data->R*T) * (dadT - 2*a/ T); \ |
392 |
double dBdT_p = - PD->b * p / (data->R * SQ(T)); \ |
393 |
double num = dAdT_p * (B-Z) + dBdT_p * (6*B*Z + 2*Z - 3*SQ(B) - 2*B + A - SQ(Z)); \ |
394 |
double den = 3*SQ(Z) + 2*(B-1)*Z + (A - 2*B - 3*SQ(B)); \ |
395 |
double dZdT_p = num / den; \ |
396 |
double dvdT_p = data->R / p * (T * dZdT_p + Z); \ |
397 |
double cpr = cvr + T * dpdT_rho * dvdT_p - data->R |
398 |
DEFINE_CPR; |
399 |
return cp0 + cpr; |
400 |
} |
401 |
|
402 |
/** |
403 |
Speed of sound (see ��engel and Boles, 2012. "Thermodynamics, An Engineering Approach", McGraw-Hill, 7SIe): |
404 |
\f[ w = \sqrt{ \left(\frac{\partial p}{\partial \rho} \right)_s } |
405 |
= v \sqrt{ -\frac{c_p}{c_v} \left(\frac{\partial p}{\partial v}\right)_T} |
406 |
\f] |
407 |
|
408 |
See Pratt, 2001. "Thermodynamic Properties Involving Derivatives", |
409 |
ASEE Chemical Engineering Division Newsletter, Malaysia |
410 |
http://www.che.uah.edu/courseware/che641/peng-robinson-derivatives-joule-thompson.pdf |
411 |
|
412 |
TODO this function needs to be checked. |
413 |
*/ |
414 |
double pengrob_w(double T, double rho, const FluidData *data, FpropsError *err){ |
415 |
DEFINE_SQRTALPHA; |
416 |
DEFINE_V; |
417 |
DEFINE_DADT; |
418 |
DEFINE_D2ADT2; |
419 |
DEFINE_A; |
420 |
DEFINE_DPDT_RHO; |
421 |
double cv0 = ideal_cv(T, rho, data, err); |
422 |
double cp0 = cv0 + data->R; |
423 |
DEFINE_CVR; |
424 |
DEFINE_CPR; |
425 |
double k = (cp0 + cpr) / (cv0 + cvr); |
426 |
double dpdv_T = - SQ(rho) * pengrob_dpdrho_T(T,rho,data,err); |
427 |
return v * sqrt(-k * dpdv_T); |
428 |
} |
429 |
|
430 |
double pengrob_dpdrho_T(double T, double rho, const FluidData *data, FpropsError *err){ |
431 |
DEFINE_SQRTALPHA; |
432 |
DEFINE_A; |
433 |
DEFINE_V; |
434 |
#define b PD->b |
435 |
// CHECK: I check this...I think the previous expression was for dpdv_T |
436 |
return -SQ(v)*( data->R*T / SQ(v-b) - 2 * a * (v + b) / SQ(v*(v+b) + b*(v-b))); |
437 |
#undef b |
438 |
} |
439 |
|
440 |
/** |
441 |
Relative pressure coefficient, as defined in http://www.iapws.org/relguide/Advise3.pdf |
442 |
\f[ \alpha_p = \frac{1}{p} \left( \frac{\partial p}{\partial T} \right)_v \f] |
443 |
|
444 |
Maxima code: |
445 |
p(T,v) := R*T/(v-b) - a(T)/(v*(v+b)+b*(v-b)) |
446 |
alpha_p = 1/p * diff(p(T,v),T) |
447 |
|
448 |
TODO the function is not yet checked/tested. |
449 |
*/ |
450 |
double pengrob_alphap(double T, double rho, const FluidData *data, FpropsError *err){ |
451 |
DEFINE_SQRTALPHA; |
452 |
DEFINE_V; |
453 |
DEFINE_DADT; |
454 |
double p = pengrob_p(T, rho, data, err); |
455 |
DEFINE_DPDT_RHO; |
456 |
return 1/p * dpdT_rho; |
457 |
} |
458 |
|
459 |
|
460 |
/** |
461 |
Isothermal stress coefficient, as defined in http://www.iapws.org/relguide/Advise3.pdf |
462 |
\f[ \beta_p = - \frac{1}{p} \left( \frac{\partial p}{\partial v} \right)_T \f] |
463 |
|
464 |
Maxima code: |
465 |
p(T,v) := R*T/(v-b) - a(T)/(v*(v+b)+b*(v-b)) |
466 |
*/ |
467 |
double pengrob_betap(double T, double rho, const FluidData *data, FpropsError *err){ |
468 |
double p = pengrob_p(T, rho, data, err); |
469 |
return -1/p * SQ(rho) * pengrob_dpdrho_T(T,rho,data,err); |
470 |
} |
471 |
|
472 |
/** |
473 |
Saturation calculation for a pure Peng-Robinson fluid. Algorithm as |
474 |
outlined in Sandler 5e, sect 7.5. Another source of information is at |
475 |
https://www.e-education.psu.edu/png520/m17.html |
476 |
|
477 |
@return psat(T) |
478 |
@param T temperature [K] |
479 |
@param rhof_ret (output) saturated liquid density |
480 |
@param rhog_ret (output) saturated vapour density |
481 |
@param err (output) error flag, if any. should be reset first by caller if needed. |
482 |
|
483 |
TODO implement accelerated successive substitution method (ASSM) or the |
484 |
'Minimum Variable Newton Raphson (MVNR) Method' as detailed at the above |
485 |
link. |
486 |
|
487 |
Possible reference: Ghanbari & Davoodi Majd, 2004, "Liquid-Vapor Equilibrium |
488 |
Curves for Methane System by Using Peng-Robinson Equation of State", |
489 |
Petroleum & Coal 46(1) 23-27. |
490 |
http://www.vurup.sk/sites/vurup.sk/archivedsite/www.vurup.sk/pc/vol46_2004/issue1/pdf/pc_ghanbari.pdf |
491 |
http://www.doaj.org/doaj?func=abstract&id=251368 |
492 |
*/ |
493 |
double pengrob_sat(double T,double *rhof_ret, double *rhog_ret, const FluidData *data, FpropsError *err){ |
494 |
/* rewritten, using GSOC code from Sean Muratet as a starting point -- thanks Sean! */ |
495 |
|
496 |
if(fabs(T - data->T_c) < 1e-3){ |
497 |
MSG("Saturation conditions requested at critical temperature"); |
498 |
*rhof_ret = data->rho_c; |
499 |
*rhog_ret = data->rho_c; |
500 |
return data->p_c; |
501 |
} |
502 |
|
503 |
//double rhog; |
504 |
double A, B; |
505 |
double sqrt2 = sqrt(2); |
506 |
|
507 |
double p = fprops_psat_T_acentric(T, data); |
508 |
MSG("Initial guess: p = %f from acentric factor",p); |
509 |
|
510 |
int i = 0; |
511 |
double Zg, Z1, Zf, vg, vf; |
512 |
|
513 |
FILE *F1 = fopen("pf.txt","w"); |
514 |
|
515 |
// FIXME test upper iteration limit required |
516 |
while(++i < 300){ |
517 |
MSG("iter %d: p = %f, rhof = %f, rhog = %f", i, p, 1/vf, 1/vg); |
518 |
// Peng & Robinson eq 17 |
519 |
double sqrtalpha = 1 + PD->kappa * (1 - sqrt(T / PD_TCRIT)); |
520 |
// Peng & Robinson eq 12 |
521 |
double a = PD->aTc * SQ(sqrtalpha); |
522 |
// Peng & Robinson eq 6 |
523 |
A = a * p / SQ(data->R*T); |
524 |
B = PD->b * p / (data->R*T); |
525 |
|
526 |
// use GSL function to return real roots of polynomial: Peng & Robinson eq 5 |
527 |
if(3 == cubicroots(-(1-B), A-3*SQ(B)-2*B, -(A*B-SQ(B)*(1+B)), &Zf,&Z1,&Zg)){ |
528 |
//MSG(" roots: Z = %f, %f, %f", Zf, Z1, Zg); |
529 |
//MSG(" Zf = %f, Zg = %f", Zf, Zg); |
530 |
// three real roots in this case |
531 |
vg = Zg*data->R*T / p; |
532 |
vf = Zf*data->R*T / p; |
533 |
if(vf < 0 || vg < 0){ |
534 |
// FIXME find out what does this mean; how does it happen? |
535 |
MSG("Got a density root less than 0"); |
536 |
*err = FPROPS_SAT_CVGC_ERROR; |
537 |
return 0; |
538 |
} |
539 |
//MSG(" vf = %f, vg = %f",vf,vg); |
540 |
//MSG(" VMf = %f, VMg = %f",vf*data->M,vg*data->M); |
541 |
|
542 |
// TODO can we use a function pointer for the fugacity expression, so that we can reuse this function for other cubic EOS? |
543 |
#define FUG(Z,A,B) \ |
544 |
exp( (Z-1) - log(Z - B) - A/(2*sqrt2*B)*log( (Z + (1+sqrt2) * B) / (Z + (1-sqrt2) * B)) ) |
545 |
|
546 |
double ff = FUG(Zf,A,B); |
547 |
double fg = FUG(Zg,A,B); |
548 |
double fratio = ff/fg; |
549 |
MSG(" ff = %f, fg = %f, fratio = %f", ff, fg, fratio); |
550 |
|
551 |
//double hf = pengrob_h(T, 1/vf, data, err); |
552 |
//double hg = pengrob_h(T, 1/vg, data, err); |
553 |
//MSG(" HMf = %f, HMg = %f", hf*data->M/1000, hg*data->M/1000); |
554 |
|
555 |
if(fabs(fratio - 1) < 1e-7){ |
556 |
*rhof_ret = 1 / vf; |
557 |
*rhog_ret = 1 / vg; |
558 |
p = pengrob_p(T, *rhog_ret, data, err); |
559 |
MSG("Solved for T = %f: p = %f, rhof = %f, rhog = %f", T, p, *rhof_ret, *rhog_ret); |
560 |
fclose(F1); |
561 |
return p; |
562 |
} |
563 |
fprintf(F1,"%f\t%f\n",p,fratio); |
564 |
p *= fratio; |
565 |
}else{ |
566 |
/* In this case we need to adjust our guess p(T) such that we get |
567 |
into the narrow range of values that gives multiple solutions. */ |
568 |
p = MidpointPressureCubic(T, data, err); |
569 |
if(*err){ |
570 |
ERRMSG("Failed to solve for a midpoint pressure"); |
571 |
fclose(F1); |
572 |
return p; |
573 |
} |
574 |
MSG(" single root: Z = %f. new pressure guess: %f", Zf, p); |
575 |
} |
576 |
} |
577 |
MSG("Did not converge"); |
578 |
*err = FPROPS_SAT_CVGC_ERROR; |
579 |
fclose(F1); |
580 |
return 0; |
581 |
} |
582 |
|
583 |
/* |
584 |
FIXME can we generalise this to work with other *cubic* EOS as well? |
585 |
Currently not easily done since pointers are kept at a higher level in our |
586 |
data structures than FluidData. |
587 |
*/ |
588 |
|
589 |
typedef struct{ |
590 |
const FluidData *data; |
591 |
FpropsError *err; |
592 |
double T; |
593 |
} MidpointSolveData; |
594 |
|
595 |
static ZeroInSubjectFunction resid_dpdrho_T; |
596 |
|
597 |
/** |
598 |
This function is trying to find the locations of the stationary points |
599 |
of the p(rho,T) function at a specified temperature, assumed to be |
600 |
close to the critical temperature. |
601 |
|
602 |
We return the midpoint of the pressure between these two. |
603 |
|
604 |
It is assumed that if using this function we are close enough to the |
605 |
critical point that vf < vc < vg, and that the stationary points |
606 |
will be within those interval, ie vf < v1 < vc < v2 < vg. |
607 |
*/ |
608 |
double MidpointPressureCubic(double T, const FluidData *data, FpropsError *err){ |
609 |
MidpointSolveData msd = {data, err, T}; |
610 |
double rhomin = 0.5 * data->rho_c; |
611 |
double rhomax = data->rho_c; |
612 |
double rho, resid; |
613 |
|
614 |
if(T > data->T_c){ |
615 |
ERRMSG("Invalid temperature T > T_c"); |
616 |
*err = FPROPS_RANGE_ERROR; |
617 |
return data->p_c; |
618 |
} |
619 |
|
620 |
// look for a stationary point in density range less than rho_c |
621 |
int res = zeroin_solve(&resid_dpdrho_T, &msd, rhomin, rhomax, 1e-9, &rho, &resid); |
622 |
if(res){ |
623 |
ERRMSG("Failed to solve density for first stationary point"); |
624 |
*err = FPROPS_NUMERIC_ERROR; |
625 |
return data->p_c; |
626 |
} |
627 |
double p1 = pengrob_p(T,rho, data, err); |
628 |
|
629 |
// look for the other stationary point in density range less than rho_c |
630 |
rhomin = data->rho_c; |
631 |
rhomax = 2 * data->rho_c; |
632 |
if(rhomax + 1e-2 > 1./(data->corr.pengrob->b)) rhomax = 1./(data->corr.pengrob->b) - 1e-3; |
633 |
|
634 |
res = zeroin_solve(&resid_dpdrho_T, &msd, rhomin, rhomax, 1e-9, &rho, &resid); |
635 |
if(res){ |
636 |
ERRMSG("Failed to solve density for second stationary point"); |
637 |
*err = FPROPS_NUMERIC_ERROR; |
638 |
return data->p_c; |
639 |
} |
640 |
|
641 |
double p2 = pengrob_p(T,rho, data, err); |
642 |
return 0.5*(p1 + p2); |
643 |
} |
644 |
|
645 |
static double resid_dpdrho_T(double rho, void *user_data){ |
646 |
#define D ((MidpointSolveData *)user_data) |
647 |
return pengrob_dpdrho_T(D->T,rho,D->data,D->err); |
648 |
#undef D |
649 |
} |
650 |
|
651 |
|
652 |
#if 0 |
653 |
|
654 |
/* we would like to try the Akasaka approach and use it with cubic EOS. |
655 |
but at this stage it's not working correctly. Not sure why... */ |
656 |
|
657 |
/** |
658 |
Solve saturation condition for a specified temperature using approach of |
659 |
Akasaka, but adapted for general use to non-helmholtz property correlations. |
660 |
@param T temperature [K] |
661 |
@param psat_out output, saturation pressure [Pa] |
662 |
@param rhof_out output, saturated liquid density [kg/m^3] |
663 |
@param rhog_out output, saturated vapour density [kg/m^3] |
664 |
@param d helmholtz data object for the fluid in question. |
665 |
@return 0 on success, non-zero on error (eg algorithm failed to converge, T out of range, etc.) |
666 |
*/ |
667 |
double pengrob_sat_akasaka(double T, double *rhof_out, double * rhog_out, const FluidData *data, FpropsError *err){ |
668 |
if(T < data->T_t - 1e-8){ |
669 |
ERRMSG("Input temperature %f K is below triple-point temperature %f K",T,data->T_t); |
670 |
return FPROPS_RANGE_ERROR; |
671 |
} |
672 |
|
673 |
if(T > data->T_c){ |
674 |
ERRMSG("Input temperature is above critical point temperature"); |
675 |
return FPROPS_RANGE_ERROR; |
676 |
} |
677 |
|
678 |
// we're at the critical point |
679 |
if(fabs(T - data->T_c) < 1e-9){ |
680 |
*rhof_out = data->rho_c; |
681 |
*rhog_out = data->rho_c; |
682 |
return data->p_c; |
683 |
} |
684 |
|
685 |
// FIXME at present step-length multiplier is set to 0.4 just because of |
686 |
// ONE FLUID, ethanol. Probably our initial guess data isn't good enough, |
687 |
// or maybe there's a problem with the acentric factor or something like |
688 |
// that. This factor 0.4 will be slowing down the whole system, so it's not |
689 |
// good. TODO XXX. |
690 |
|
691 |
// initial guesses for liquid and vapour density |
692 |
double rhof = fprops_rhof_T_rackett(T,data); |
693 |
double rhog= fprops_rhog_T_chouaieb(T,data); |
694 |
double R = data->R; |
695 |
double pc = data->p_c; |
696 |
|
697 |
MSG("initial guess rho_f = %f, rho_g = %f\n",rhof,rhog); |
698 |
MSG("calculating for T = %.12e",T); |
699 |
|
700 |
int i = 0; |
701 |
while(i++ < 70){ |
702 |
if(rhof > 1/PD->b){ |
703 |
MSG("limit rhof to 1/b"); |
704 |
rhof = 1/PD->b; |
705 |
} |
706 |
|
707 |
MSG("iter %d: T = %f, rhof = %f, rhog = %f",i,T, rhof, rhog); |
708 |
|
709 |
double pf = pengrob_p(T,rhof,data,err); |
710 |
double pg = pengrob_p(T,rhog,data,err); |
711 |
double gf = pengrob_g(T,rhof,data,err); |
712 |
double gg = pengrob_g(T,rhog,data,err); |
713 |
double dpdrf = pengrob_dpdrho_T(T,rhof,data,err); |
714 |
double dpdrg = pengrob_dpdrho_T(T,rhog,data,err); |
715 |
if(*err){ |
716 |
ERRMSG("error returned"); |
717 |
} |
718 |
|
719 |
MSG("gf = %f, gg = %f", gf, gg); |
720 |
|
721 |
// jacobian for [F;G](rhof, rhog) --- derivatives wrt rhof and rhog |
722 |
double F = (pf - pg)/pc; |
723 |
double G = (gf - gg)/R/T; |
724 |
|
725 |
if(fabs(F) + fabs(G) < 1e-12){ |
726 |
//fprintf(stderr,"%s: CONVERGED\n",__func__); |
727 |
*rhof_out = rhof; |
728 |
*rhog_out = rhog; |
729 |
return pengrob_p(T, *rhog_out, data, err); |
730 |
/* SUCCESS */ |
731 |
} |
732 |
|
733 |
double Ff = dpdrf/pc; |
734 |
double Fg = -dpdrg/pc; |
735 |
MSG("Ff = %e, Fg = %e",Ff,Fg); |
736 |
|
737 |
double Gf = dpdrf/rhof/R/T; |
738 |
double Gg = -dpdrg/rhog/R/T; |
739 |
MSG("Gf = %e, Gg = %e",Gf,Gg); |
740 |
|
741 |
double DET = Ff*Gg - Fg*Gf; |
742 |
MSG("DET = %f",DET); |
743 |
|
744 |
MSG("F = %f, G = %f", F, G); |
745 |
|
746 |
// 'gamma' needs to be increased to 0.5 for water to solve correctly (see 'test/sat.c') |
747 |
#define gamma 1 |
748 |
rhof += gamma/DET * (Fg*G - Gg*F); |
749 |
rhog += gamma/DET * ( Gf*F - Ff*G); |
750 |
#undef gamma |
751 |
|
752 |
if(rhog < 0)rhog = -0.5*rhog; |
753 |
if(rhof < 0)rhof = -0.5*rhof; |
754 |
} |
755 |
*rhof_out = rhof; |
756 |
*rhog_out = rhog; |
757 |
*err = FPROPS_SAT_CVGC_ERROR; |
758 |
ERRMSG("Not converged: with T = %e (rhof=%f, rhog=%f).",T,*rhof_out,*rhog_out); |
759 |
return pengrob_p(T, rhog, data, err); |
760 |
} |
761 |
|
762 |
#endif |
763 |
|
764 |
|