proteus 1.9.0
C/C++/Fortran libraries
Loading...
Searching...
No Matches
pskRelations.h
Go to the documentation of this file.
1#ifndef PSKRELATIONS_H
2#define PSKRELATIONS_H
3#include <algorithm>
4#include <cmath>
5#include <iostream>
6#include <cassert>
7#include "densityRelations.h"
14
16
17/* jcc two phase flow, modified by cek and mwf*/
18using namespace std;
19
21{
22public:
23 double Se,
33 //in case need psic--> Se form
35 PskRelation(const double* rwork, const int* iwork = 0):
36 Se(1.0),dSe_dSw(1.0),
37 Sw_min(rwork[0]),
38 Sw_max(rwork[1]),
39 krw(1.0),dkrw(0.0),
40 krn(0.0),dkrn(0.0),psic(0.0),
41 dpsic(0.0),dSe_dpsic(0.0)
42 {}
44 Se(1.0),dSe_dSw(1.0),
45 Sw_min(0.0),
46 Sw_max(1),
47 krw(1.0),dkrw(0.0),
48 krn(0.0),dkrn(0.0),psic(0.0),
49 dpsic(0.0),dSe_dpsic(0.0)
50 {}
51 virtual inline void setParams(const double* rwork, const int* iwork = 0)
52 {
53 Sw_min = rwork[0];
54 Sw_max = rwork[1];
55 }
56 /*for fudge factors aka tolerances in various models*/
57 virtual inline void setTolerances(const double* rwork_tol)
58 {
59 }
60 virtual ~PskRelation(){}
61
62 /*linear coefficients*/
63 inline void calc(const double& Sw)
64 {
65 calc_Se(Sw);
66
67 krw = Se;
68 dkrw = dSe_dSw;
69
70 krn = (1.0-Se);
71 dkrn = -dSe_dSw;
72
73 psic = Se;
74 dpsic = dSe_dSw;
75 }
76
77 inline void calc_Se(const double& Sw)
78 {
79 if (Sw < Sw_min)
80 {
81 Se = 0.0;
82 dSe_dSw = 0.0;
83 }
84 else if (Sw > Sw_max)
85 {
86 Se = 1.0;
87 dSe_dSw =0.0;
88 }
89 else
90 {
91 Se = (Sw - Sw_min)/(Sw_max-Sw_min);
92 dSe_dSw = 1.0/(Sw_max - Sw_min);
93 }
94 }
95 //for Sw as a function of capillary pressure head
96 //note dkrw,dkrn are set to be wrt psic
97 virtual inline void calc_from_psic(const double& psicIn)
98 {
99 bool implemented = false;
100 assert(implemented);
101 }
102
103};
104
105/* quadratic kr */
106class SimplePSK : public PskRelation
107{
108public:
109 SimplePSK(const double* rwork, const int* iwork = 0):
110 PskRelation(rwork)
111 {}
112
113 inline void calc(const double& Sw)
114 {
115 calc_Se(Sw);
116
117 krw = Se*Se;
118 dkrw = 2.0*Se*dSe_dSw;
119
120 krn = (1.0-Se)*(1.0-Se);
121 dkrn = 2.0*(Se-1.0)*dSe_dSw;
122
123 psic = (1.0-Se)/Sw_min; /*mwf change to test het -Se;*/
124 dpsic = -dSe_dSw;
125 }
126 //
127 virtual inline void calc_from_psic(const double& psicIn)
128 {
129 psic = max(0.0,psicIn);
130 dpsic= 1.0;
131 Se = 1.0-psic*Sw_min;
132 Se = max(0.0,min(1.0,Se));
133
134 krw = Se*Se;
135 dkrw = 2.0*Se*dSe_dSw;
136
137 krn = (1.0-Se)*(1.0-Se);
138 dkrn = 2.0*(Se-1.0)*dSe_dSw;
139
140 }
141};
142
143/* Van Genuchten-Mualem */
144class VGMorig: public PskRelation
145{
146public:
147 double alpha,
153 {}
154 VGMorig(const double* rwork, const int* iwork = 0):
155 PskRelation(rwork),
156 alpha(rwork[2]),
157 m(rwork[3])
158 {
159 n = (1.0)/(1.0-m);
160 Se_eps_const=1.0e-4;
161 //mwf debug
162 //std::cout<<"VGMorig ctor rwork[2]= "<<rwork[2]<<" alpha= "<<alpha<<" rwork[3]= "<<rwork[3]<<" m= "<<m<<" n= "<<n<<std::endl;
163 }
164 /*for fudge factors aka tolerances in various models*/
165 virtual inline void setTolerances(const double* rwork_tol)
166 {
167 Se_eps_const = rwork_tol[0];
168 }
169
170 inline void setParams(const double* rwork, const int* iwork = 0)
171 {
172 Sw_min = rwork[0];
173 Sw_max = rwork[1];
174 alpha = rwork[2];
175 m = rwork[3];
176 n = (1.0)/(1.0-m);
177 //mwf debug
178 //std::cout<<"VGMorig setParams rwork[2]= "<<rwork[2]<<" alpha= "<<alpha<<" rwork[3]= "<<rwork[3]<<" m= "<<m<<" n= "<<n<<std::endl;
179 }
180 inline void calc_Se_eps(const double& Se)
181 {
182 /* cek todo, work out when you need to stay away from Se=0 and 1 and what to assign in these cases */
183 if(Se <= Se_eps_const)
185 else if(Se >= (1.0-Se_eps_const))
187 else
188 Se_eps=Se;
189 }
190
191 inline void calc(const double& Sw)
192 {
193 double Seovmmo,Seovm,Seovmmh,S,Smmo,S2mmo,Sm,S2m,pcesub1,pcesub2;
194 calc_Se(Sw);
196
197 Seovmmo = pow(Se_eps,((1.0/m)-1.0));
198 Seovm = Seovmmo*Se_eps;
199 Seovmmh = pow(Se_eps,((1.0/m)-0.5));
200 S = 1.0 - Seovm;
201 Smmo = pow(S,m-1.0);
202 S2mmo = pow(S,2.0*m-1.0);
203 Sm = Smmo*S;
204 S2m = S2mmo*S;
205
206 pcesub1 = pow(((1.0/Seovm)-1.0),((1.0/n)-1.0));
207 pcesub2 = pcesub1*((1.0/Seovm)-1.0);
208
209 krw = sqrt(Se)*(1.0-Sm)*(1.0-Sm);
210 dkrw = (0.5*(1.0/sqrt(Se_eps))*(1.0-Sm)*(1.0-Sm) + 2.0*(1-Sm)*Smmo*Seovmmh)*dSe_dSw;
211
212 krn = sqrt(1.0-Se)*S2m;
213 dkrn = (-0.5*(1.0/sqrt(1.0-Se_eps))*S2m - 2.0*sqrt(1.0-Se)*(S2mmo)*Seovmmo)*dSe_dSw;
214
215 psic = pow((pow(Se_eps,(-1.0/m)) - 1.0),(1.0/n))/alpha;
216 dpsic = ((-1.0/(alpha*n*m))*(pow((pow(Se_eps,-1.0/m)-1.0),-m))*(pow(Se_eps,((-1.0/m)-1.0))))*dSe_dSw;
217 }
218};
219
220class VGM : public VGMorig
221{
222 public:
225 {}
226 VGM(const double* rwork, const int* iwork = 0):
227 VGMorig(rwork,iwork),
228 ns_del(1.0e-8),
229 eps_small(1.0e-16),
230 sqrt_eps_small(1.0e-8)
231 {}
232 VGM(const VGM& r)
233 {
234 Se=r.Se;
235 dSe_dSw=r.dSe_dSw;
236 Sw_min=r.Sw_min;
237 Sw_max=r.Sw_max;
238 krw=r.krw;
239 dkrw=r.dkrw;
240 krn=r.krn;
241 dkrn=r.dkrn;
242 psic=r.psic;
243 dpsic=r.dpsic;
244 dSe_dpsic=r.dSe_dpsic;
245 alpha=r.alpha;
246 m=r.m;
247 n=r.n;
248 Se_eps_const=r.Se_eps_const;
249 ns_del=r.ns_del;
250 eps_small=r.eps_small;
251 sqrt_eps_small=r.sqrt_eps_small;
252 }
253 /*for fudge factors aka tolerances in various models*/
254 virtual inline void setTolerances(const double* rwork_tol)
255 {
256 eps_small = rwork_tol[0]; //mwf 072110 don't tie to eps_small? sqrt_eps_small = sqrt(eps_small);
257 ns_del = rwork_tol[1];
258 }
259
260 inline void calc_Se(const double& Sw)
261 {
262 Se = (Sw - Sw_min)/(Sw_max-Sw_min);
263 dSe_dSw = 1.0/(Sw_max - Sw_min);
264 Se = max(eps_small,min(Se,1.0-eps_small));
265 }
266
267 inline void calc(const double& Sw)
268 {
269 calc_Se(Sw);
270 //taken from MualemVanGenuchten2p in pdetk
271 double sBar,psiC,DsBar_DpC,DDsBar_DDpC,DkrW_DpC,DkrN_DpC;
272 double vBar,uBar,
273 alphaPsiC, alphaPsiC_n, alphaPsiC_nM1, alphaPsiC_nM2,
274 onePlus_alphaPsiC_n,
275 sqrt_sBar, sqrt_1minusSbar,
276 sBarByOnePlus_alphaPsiC_n, sBarBy_onePlus_alphaPsiC_n_2;
277
278 sBar = Se;
279 //begin MualemVanGenuchten2p setVFraction
280 onePlus_alphaPsiC_n = pow(sBar,1.0/-m);
281 alphaPsiC_n = onePlus_alphaPsiC_n - 1.0;
282 alphaPsiC = pow(alphaPsiC_n,1.0/n);
283 psiC = alphaPsiC/alpha;
284
285 alphaPsiC_nM1 = alphaPsiC_n/alphaPsiC;
286 sBarByOnePlus_alphaPsiC_n = sBar/onePlus_alphaPsiC_n;
287 sqrt_sBar = sqrt(sBar);
288 sqrt_1minusSbar = sqrt(1.0 - sBar);
289
290 DsBar_DpC = -alpha*(n-1.0)*alphaPsiC_nM1
291 *sBarByOnePlus_alphaPsiC_n;
292 //DthetaW_DpC = thetaSR[i] * DsBar_DpC;
293
294 vBar = 1.0-alphaPsiC_nM1*sBar;
295 uBar = alphaPsiC_nM1*sBar;
296
297 //change names krW--> krw, krN--> krn
298 krw = sqrt_sBar*vBar*vBar;
299 krn = sqrt_1minusSbar*uBar*uBar;
300 psic= psiC;
301 if(psiC<=0.0)
302 {
303 DsBar_DpC = 0.0;
304 //DthetaW_DpC = 0.0;
305 krw = 1.0;
306 krn = 0.0;
307 }
308
309 //begin MualemVanGenuchten2p calculateDerivatives
310 alphaPsiC_nM2 = alphaPsiC_nM1/alphaPsiC;
311
312 sBarBy_onePlus_alphaPsiC_n_2 = sBarByOnePlus_alphaPsiC_n
313 /onePlus_alphaPsiC_n;
314 DDsBar_DDpC = alpha*alpha*(n-1.)
315 *((2*n-1.)*alphaPsiC_nM1*alphaPsiC_nM1
316 *sBarBy_onePlus_alphaPsiC_n_2
317 -
318 (n-1.)*alphaPsiC_nM2
319 *sBarByOnePlus_alphaPsiC_n);
320
321 //DDthetaW_DDpC = thetaSR[i]*DDsBar_DDpC;
322
323 DkrW_DpC = (0.5/sqrt_sBar)*DsBar_DpC*vBar*vBar
324 -
325 2.0*sqrt_sBar*vBar*
326 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
327 + alphaPsiC_nM1 * DsBar_DpC);
328
329 //DKW_DpC = KWs[i]*DkrW_DpC;
330
331
332 //recalculate if necessary
333 if (sqrt_1minusSbar >= sqrt_eps_small)//SQRT_MACHINE_EPSILON)
334 {
335 DkrN_DpC = -(0.5/sqrt_1minusSbar)*DsBar_DpC*uBar*uBar
336 +
337 2.0*sqrt_1minusSbar*uBar*
338 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
339 + alphaPsiC_nM1 * DsBar_DpC);
340 }
341 else
342 {
343 DkrN_DpC =((1.0 - sBar)/eps_small)*2.0*sqrt_eps_small*uBar*
344 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
345 + alphaPsiC_nM1 * DsBar_DpC)
346 - (DsBar_DpC/eps_small)*sqrt_eps_small*uBar*uBar;
347 }
348
349 //if we're in the nonsmooth regime
350 if (psiC < ns_del && psiC > 0.0 )
351 {
352 DkrW_DpC = 0.0;
353 }
354
355 if (psiC <= 0.0)
356 {
357 DDsBar_DDpC = 0.0;
358 //DDthetaW_DDpC = 0.0;
359 DkrW_DpC = 0.0;
360 DkrN_DpC = 0.0;
361 }
362 //end calculateDerivatives
363 double DpC_Dse = 0.0;
364 if (fabs(DsBar_DpC) > 0.0)
365 DpC_Dse = 1.0/DsBar_DpC;
366 double DpC_Dsw = DpC_Dse*dSe_dSw;
367 dkrw = DkrW_DpC*DpC_Dsw;
368 dkrn = DkrN_DpC*DpC_Dsw;
369 dpsic= DpC_Dsw;
370
371 }
372 //TODO add this to other classes
373 virtual inline void calc_from_psic(const double& psicIn)
374 {
375 //taken from MualemVanGenuchten2p in pdetk
376 double sBar,psiC,DsBar_DpC,DDsBar_DDpC,DkrW_DpC,DkrN_DpC;
377 double vBar,uBar,
378 alphaPsiC, alphaPsiC_n, alphaPsiC_nM1, alphaPsiC_nM2,
379 onePlus_alphaPsiC_n,
380 sqrt_sBar, sqrt_1minusSbar,
381 sBarByOnePlus_alphaPsiC_n, sBarBy_onePlus_alphaPsiC_n_2;
382
383
384 psiC = max(0.0,psicIn);
385 alphaPsiC = alpha*psiC;
386 alphaPsiC_n = pow(alphaPsiC,n);
387 alphaPsiC_nM1 = alphaPsiC_n/alphaPsiC;
388 onePlus_alphaPsiC_n = 1.0 + alphaPsiC_n;
389 sBar = pow(onePlus_alphaPsiC_n,-m);
390 sBarByOnePlus_alphaPsiC_n = sBar/onePlus_alphaPsiC_n;
391 sqrt_sBar = sqrt(sBar);
392 sqrt_1minusSbar = sqrt(1.0 - sBar);
393 //thetaW = thetaSR[i]*sBar + thetaR[i];
394 DsBar_DpC = -alpha*(n-1.0)*alphaPsiC_nM1
395 *sBarByOnePlus_alphaPsiC_n;
396 //DthetaW_DpC = thetaSR[i] * DsBar_DpC;
397 vBar = 1.0-alphaPsiC_nM1*sBar;
398 uBar = alphaPsiC_nM1*sBar;
399
400 //change names krW--> krw, krN--> krn
401 krw = sqrt_sBar*vBar*vBar;
402 krn = sqrt_1minusSbar*uBar*uBar;
403 Se = sBar;
404 psic=psiC;
405 if(psiC<=0.0)
406 {
407 sBar = 1.0;
408 Se = sBar;
409 //thetaW = thetaS[i];
410 DsBar_DpC = 0.0;
411 //DthetaW_DpC = 0.0;
412 krw = 1.0;
413 krn = 0.0;
414 }
415 //mwf debug
416 //std::cout<<"vgm_calc_from_psic alpha= "<<alpha<<" m= "<<m<<" psic= "<<psic<<" sBar= "<<sBar<<" krw= "<<krw<<" krn= "<<krn
417 // <<std::endl;
418 //begin MualemVanGenuchten2p calculateDerivatives
419 alphaPsiC_nM2 = alphaPsiC_nM1/alphaPsiC;
420
421 sBarBy_onePlus_alphaPsiC_n_2 = sBarByOnePlus_alphaPsiC_n
422 /onePlus_alphaPsiC_n;
423 DDsBar_DDpC = alpha*alpha*(n-1.)
424 *((2*n-1.)*alphaPsiC_nM1*alphaPsiC_nM1
425 *sBarBy_onePlus_alphaPsiC_n_2
426 -
427 (n-1.)*alphaPsiC_nM2
428 *sBarByOnePlus_alphaPsiC_n);
429
430 //DDthetaW_DDpC = thetaSR[i]*DDsBar_DDpC;
431
432 DkrW_DpC = (0.5/sqrt_sBar)*DsBar_DpC*vBar*vBar
433 -
434 2.0*sqrt_sBar*vBar*
435 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
436 + alphaPsiC_nM1 * DsBar_DpC);
437
438 //DKW_DpC = KWs[i]*DkrW_DpC;
439
440
441 //recalculate if necessary
442 if (sqrt_1minusSbar >= sqrt_eps_small)//SQRT_MACHINE_EPSILON)
443 {
444 DkrN_DpC = -(0.5/sqrt_1minusSbar)*DsBar_DpC*uBar*uBar
445 +
446 2.0*sqrt_1minusSbar*uBar*
447 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
448 + alphaPsiC_nM1 * DsBar_DpC);
449 }
450 else
451 {
452 DkrN_DpC =((1.0 - sBar)/eps_small)*2.0*sqrt_eps_small*uBar*
453 (alpha*(n-1.0)*alphaPsiC_nM2*sBar
454 + alphaPsiC_nM1 * DsBar_DpC)
455 - (DsBar_DpC/eps_small)*sqrt_eps_small*uBar*uBar;
456 }
457
458 //if we're in the nonsmooth regime
459 if (psiC < ns_del && psiC > 0.0 )
460 {
461 DkrW_DpC = 0.0;
462 }
463
464 if (psiC <= 0.0)
465 {
466 DDsBar_DDpC = 0.0;
467 //DDthetaW_DDpC = 0.0;
468 DkrW_DpC = 0.0;
469 DkrN_DpC = 0.0;
470 }
471 //end calculateDerivatives
472 dkrw = DkrW_DpC; //note: \od{k_{rw}}{\psi_c} not \od{k_{rw}}{S_w}
473 dkrn = DkrN_DpC;
474 dpsic= 1.0;
475 dSe_dpsic=DsBar_DpC;
476 }
477};
478/* Van Genuchten-Burdine */
479class VGB : public VGM
480{
481public:
482 VGB(const double* rwork, const int* iwork = 0):
483 VGM(rwork,iwork)
484 {}
485
486 inline void calc(const double& Sw)
487 {
488 double S,Smmo,Sm,alpha,Se1ovMmo;
489 calc_Se(Sw);
491
492 Se1ovMmo = pow(Se_eps,((1.0/m)-1.0));
493 S = 1.0-Se1ovMmo*Se_eps;
494 Smmo = pow(S,(m-1.0));
495 Sm = Smmo*S;
496
497 //cek from matlab, needs optimizing
498
499 krw = Se_eps*Se_eps*(1.0-pow(1.0-pow(Se_eps,1.0/m),1.0/m));
500 dkrw = (2.0*Se_eps*(1.0-pow(1.0-pow(Se_eps,1.0/m),1.0/m))+Se_eps*pow(1.0-pow(Se_eps,1.0/m),1.0/m)/(m*m)*pow(Se_eps,1.0/m)/(1.0-pow(Se_eps,1.0/m)))*dSe_dSw;
501
502 krn = pow(1.0-Se_eps,2.0)*pow(1.0-pow(Se_eps,1.0/m),1.0*m);
503 dkrn = (-2.0*(1.0-Se_eps)*pow(1.0-pow(Se_eps,1.0/m),1.0*m)-pow(1.0-Se_eps,2.0)*pow(1.0-pow(Se_eps,1.0/m),1.0*m)*pow(Se_eps,1.0/m)/Se_eps/(1.0-pow(Se_eps,1.0/m)))*dSe_dSw;
504
505// krw = (Se*Se)*(1.0-Sm);
506// dkrw = ((2.0*Se)*(1.0-Sm)+(Se*Se)*(Se1ovMmo)*Smmo)*dSe_dSw;
507
508// krn = (1.0-Se)*(1.0-Se)*(Sm);
509// dkrn = (2.0*(Se-1.0)*Sm-((1.0-Se)*(1.0-Se))*Smmo*Se1ovMmo)*dSe_dSw;
510 psic = pow((pow(Se_eps,(-1.0/m)) - 1.0),(1.0/n))/alpha;
511 dpsic = ((-1.0/(alpha*n*m))*(pow((pow(Se_eps,-1.0/m)-1.0),-m))*(pow(Se_eps,((-1.0/m)-1.0))))*dSe_dSw;
512 }
513};
514
515/* Brooks-Corey-Mualem */
516class BCM : public PskRelation
517{
518 public:
519 double pd,lambda;
520
521 BCM(const double* rwork, const int* iwork = 0):
522 PskRelation(rwork,iwork),
523 pd(rwork[2]),
524 lambda(rwork[3])
525 {}
526
527 inline void setParams(const double* rwork, const int* iwork = 0)
528 {
529 Sw_min = rwork[0];
530 Sw_max = rwork[1];
531 pd = rwork[2];
532 lambda = rwork[3];
533 }
534 inline void calc(const double& Sw)
535 {
536 double Value,Expon,krwovSe,Oovbclpo,Oovbcl,X,sqrt1mu;
537 calc_Se(Sw);
538
539 Oovbcl = 1.0/lambda;
540 Oovbclpo = Oovbcl+1.0;
541 X = pow(Se,Oovbcl);
542 Value = 1.0-X*Se;
543 Expon = ((4.0+5.0*lambda)/(2.0*lambda));
544 krwovSe = pow(Se,(Expon-1.0));
545 sqrt1mu = sqrt(1.0-Se);
546
547 krw = krwovSe*Se;
548 dkrw = (Expon*krwovSe)*dSe_dSw;
549
550 krn = sqrt1mu*Value*Value;
551 dkrn = (-0.5*(1.0/sqrt1mu)*Value*Value - 2.0*sqrt1mu*Value*Oovbclpo*X )*dSe_dSw;
552
553 psic = 1.0/X;
554 dpsic = (-Oovbcl/(X*Se))*dSe_dSw;
555 }
556};
557
558/* Brooks-Corey-Burdine */
559class BCB : public BCM
560{
561public:
562 BCB(const double* rwork, const int* iwork = 0):BCM(rwork,iwork)
563 {}
564
565 inline void calc(const double& Sw)
566 {
567 double Se2ovL,Se2,Se3,omSe,Expon,Semoovlmo,Se_cutOff;
568 calc_Se(Sw);
569
570 Se2ovL = pow(Se,(2.0/lambda));
571 Se2 = Se*Se;
572 Se3 = Se*Se2;
573 omSe = 1.0-Se;
574 Expon = (2.0+3.0*lambda)/lambda;
575 Se_cutOff = max(1.0e-4,Se);
576 Semoovlmo = pow(Se_cutOff,((-1.0/lambda)-1.0));
577
578 krw = Se2ovL*Se3;
579 dkrw = Expon*Se2ovL*Se2*dSe_dSw;
580
581 krn = (omSe*omSe)*(1.0-Se2ovL*Se);
582 dkrn = (2.*omSe*(1.0-Se2ovL*Se) - (omSe*omSe)*(Expon-2.0)*Se2ovL*dSe_dSw)*dSe_dSw;
583
584 /* cek debug */
585 krw = pow(Se,(2.0+3.0*lambda)/lambda);
586 dkrw = (((2.0+3.0*lambda)/lambda)*pow(Se,(2.0+3.0*lambda)/lambda - 1.0))*dSe_dSw;
587
588 krn = (1.0-Se)*(1.0-Se)*(1.0-pow(Se,(2.0+lambda)/lambda));
589 dkrn = (-2.0*(1.0-Se)*(1.0-pow(Se,(2.0+lambda)/lambda))
590 -((2.0+lambda)/lambda)*(1.0-Se)*(1.0-Se)*pow(Se,(2.0+lambda)/lambda-1.0))*dSe_dSw;
591
592 psic = pd*Semoovlmo*Se;
593 dpsic = pd*(-1.0/lambda)*Semoovlmo*dSe_dSw;
594 }
595};
596
598{
599public:
600 double muw,
612
613 FractionalFlowVariables(double muwIn,double munIn):
614 muw(muwIn),
615 mun(munIn)
616 {}
617
618 inline void calc(const PskRelation& psk,
619 const DensityRelation& density_w,
620 const DensityRelation& density_n)
621 {
622 lambdaw = density_w.rho*psk.krw/muw;
623 dlambdaw =(density_w.rho/muw)*psk.dkrw;
624
625 lambdan =(density_n.rho*psk.krn)/mun;
626 dlambdan =(density_n.rho/mun)*psk.dkrn;
627
630
633
636 }
637};
638
640{
641public:
642 CompressibleN_FractionalFlowVariables(double muwIn,double munIn):
643 FractionalFlowVariables(muwIn,munIn)
644 {}
645
646 double drhon,
653
654 inline void calc(const PskRelation& psk,
655 const DensityRelation& density_w,
656 const DensityRelation& density_n)
657 {
658 lambdaw = density_w.rho*psk.krw/muw;
659 dlambdaw = (density_w.rho/muw)*psk.dkrw;
660 dlambdaw_psiw = 0.0;
661
662 drhon = density_n.drho*psk.dpsic;
663 drhon_psiw = density_n.drho;
664
665 lambdan = (density_n.rho*psk.krn)/mun;
666 dlambdan = (1.0/mun)*(psk.dkrn*density_n.rho + drhon*psk.krn);
668
672
676
680 }
681};
682
683
685{
686 /*************************************************************
687 spline psk relations. tables of values are held (externally)
688 in splineArray in the order
689
690 u (sw, or psic), u^-1 (either sw or psic), krw,krn
691
692 force dSe_dSw = 1.0, so that will be consistent with analytical evaluations
693 where dkrw, etc are wrt to se
694
695 ************************************************************/
696public:
697 PskSpline(const double* rworkIn, const int* iworkIn=0):
698 PskRelation(),
699 nknots(2),
700 lastIndex(0),
701 uinvOffset(1),
702 krwOffset(2),
703 krnOffset(3),
704 splineArray(rworkIn)
705 {
706 assert(iworkIn);
707 nknots = iworkIn[0];
708 }
709 virtual ~PskSpline() {}
710 virtual inline void calc(const double& Sw)
711 {
712 assert(splineArray);
713 //
714 Se = Sw; //note force Se = Sw since splines are evaluated directly
715 dSe_dSw = 1.0;
717 nknots,
718 &lastIndex,
719 &psic,
720 &dpsic,
723
724 //
726 nknots,
727 &lastIndex,
728 &krw,
729 &dkrw,
732 //
734 nknots,
735 &lastIndex,
736 &krn,
737 &dkrn,
740
741 }
742 virtual inline void calc_from_psic(const double& psicIn)
743 {
744 assert(splineArray);
745 //
746 psic = psicIn;
748 nknots,
749 &lastIndex,
750 &Se, //same as Sw
751 &dSe_dpsic, //same as dSw_dpsic
754
755 //
757 nknots,
758 &lastIndex,
759 &krw,
760 &dkrw,
763 //
765 nknots,
766 &lastIndex,
767 &krn,
768 &dkrn,
771
772 }
773 virtual inline void setParams(const double* rwork, const int* iwork = 0)
774 {
775 splineArray = rwork;
776 if (iwork)
777 nknots = iwork[0];
778 }
779public:
781 const double* splineArray;
782};
783
796
797
798// =============================================================================
799// Pore size distribution / relative permeability (PSK) closures for Richards.
800//
801// Richards.h owns the PDE coefficient assembly (mass, diffusion tensor,
802// buoyancy flux, their Jacobians); this header owns *only* the constitutive
803// relations theta_w(psiC) and k_rw(psiC) plus their inverses. Selecting a
804// model is a single branch on PSK_TYPE_member at each call site:
805//
806// PSK_TYPE 0 -> vgm_* van Genuchten retention + Mualem k_rw (default)
807// PSK_TYPE 1 -> bc_* Brooks-Corey retention + Burdine k_rw
808// PSK_TYPE 2 -> bc_* Brooks-Corey retention + Mualem k_rw
809// PSK_TYPE 3 -> gardner_* Gardner exponential retention + k_rw = S_e
810//
811// Codes 1 and 2 share one retention curve and differ only in the exponent of
812// k_rw = S_e^eta, so only the forward closure branches on them; the inverses
813// (which see theta_w alone) treat them alike. Code 3 is a model of its own in
814// both halves; it is the closure Tracy's analytical solutions assume.
815//
816// Conventions shared by every routine here:
817// psiC is the suction (-u). psiC > 0 is unsaturated, psiC <= 0 saturated.
818// vgm_ and bc_ clamp that saturated side to (thetaS, 1) with zero slope;
819// gardner_ deliberately does NOT, and the block below says why -- a zero
820// dtheta/dpsiC is a lost Jacobian diagonal wherever beta = 0.
821// Derivatives are with respect to psiC, NOT with respect to u; the caller in
822// Richards.h owns the sign flip d(psiC)/du = -1.
823// The vgm_, bc_ and gardner_ routines take the same parameter slots in the
824// same order, so the branch is a one-line swap. In the BC parameterisation
825// the second numeric parameter is the pore-size index lambda (taking n_vg's
826// slot) and alpha = 1/p_d is the inverse entry-pressure head; under Gardner
827// alpha is the exponential decay rate [1/m] and the n_vg slot is unused.
828//
829// The Brooks-Corey conductivity exponent eta is NOT fixed by lambda. Two
830// closures are in common use and they do not agree: Burdine gives
831// eta = (2+3*lambda)/lambda, Mualem gives eta = 2.5 + 2/lambda. At the
832// lambda = 0.592 sand of Szymkiewicz [2009], WRR 45, W10403 (Table 1, soil 5)
833// that is 6.378 against 5.878 -- not a rounding difference: Burdine is a factor
834// ~4 drier in k_rw by psiC = 7.5 m and ~7 by 50 m, which moves a gravity-driven
835// wetting front. Which one applies is a property of the parameter set being
836// reproduced, so it is selected by PSK_TYPE and the exponent is derived from
837// lambda here, in bc_eta, rather than computed by the caller.
838// =============================================================================
839
840namespace proteus
841{
842namespace richards
843{
844namespace psk
845{
846
847// -----------------------------------------------------------------------------
848// van Genuchten - Mualem (pore-connectivity exponent l = 1/2)
849//
850// S_e = (1 + (alpha*psiC)^n)^(-m), m = 1 - 1/n
851// theta_w = thetaR + thetaSR * S_e
852// k_rw = sqrt(S_e) * (1 - (alpha*psiC)^(n-1) * S_e)^2
853//
854// pcBarStar floors alpha*psiC at 1e-8 so pow(pcBar, n-2) stays finite as
855// psiC -> 0 for n < 2; sqrt_sBarStar floors the same way inside the k_rw
856// derivative only.
857// -----------------------------------------------------------------------------
858inline void vgm_wetting(const double psiC,
859 const double alpha,
860 const double n_vg,
861 const double thetaR,
862 const double thetaSR,
863 double &thetaW,
864 double &DthetaW_DpsiC,
865 double &KWr,
866 double &DKWr_DpsiC)
867{
868 const double m_vg = 1.0 - 1.0 / n_vg;
869 const double thetaS = thetaR + thetaSR;
870 if (psiC > 0.0) {
871 const double pcBar = alpha * psiC;
872 double pcBarStar = pcBar;
873 if (pcBar < 1.0e-8) pcBarStar = 1.0e-8;
874 const double pcBar_nM2 = pow(pcBarStar, n_vg - 2);
875 const double pcBar_nM1 = pcBar_nM2 * pcBar;
876 const double pcBar_n = pcBar_nM1 * pcBar;
877 const double onePlus_pcBar_n = 1.0 + pcBar_n;
878
879 const double sBar = pow(onePlus_pcBar_n, -m_vg);
880 /* using -mn = 1-n */
881 const double DsBar_DpsiC =
882 alpha * (1.0 - n_vg) * (sBar / onePlus_pcBar_n) * pcBar_nM1;
883
884 const double vBar = 1.0 - pcBar_nM1 * sBar;
885 const double vBar2 = vBar * vBar;
886 const double DvBar_DpsiC =
887 -alpha * (n_vg - 1.0) * pcBar_nM2 * sBar - pcBar_nM1 * DsBar_DpsiC;
888
889 thetaW = thetaSR * sBar + thetaR; //thetaS;//
890 DthetaW_DpsiC = thetaSR * DsBar_DpsiC; //0.0;//
891
892 const double sqrt_sBar = sqrt(sBar);
893 double sqrt_sBarStar = sqrt_sBar;
894 if (sqrt_sBar < 1.0e-8) sqrt_sBarStar = 1.0e-8;
895 KWr = sqrt_sBar * vBar2;
896 DKWr_DpsiC = ((0.5 / sqrt_sBarStar) * DsBar_DpsiC * vBar2
897 + 2.0 * sqrt_sBar * vBar * DvBar_DpsiC);
898 } else {
899 thetaW = thetaS;
900 DthetaW_DpsiC = 0.0;
901 KWr = 1.0;
902 DKWr_DpsiC = 0.0;
903 }
904}
905
906// Analytic van Genuchten inverse: theta_w -> psiC. Leaves u untouched (so the
907// caller keeps its previous iterate) outside the strictly unsaturated range.
908//
909// The dry limit is a cap on psiC, not a fraction of thetaR -- same convention as
910// bc_invert_analytic below. 1.01*thetaR looks harmless but is a band in theta,
911// and theta -> psiC is exponentially steep in the tail, so it silently swallows
912// a huge band in head: for a sand with alpha=14.5 1/m and n=2.68 it refuses to
913// invert anything drier than psiC = 3.8 m, which for a 20 m column is the whole
914// unwetted region. Every FCT-limited mass landing there was discarded (u kept
915// its pre-limiter value), which breaks the conservation chain theta_limited ->
916// psi and shows up as spurious infiltration. pcBar <= 1e4 puts the cut at
917// psiC = 1e4/alpha instead, i.e. far outside any physical range.
918inline void vgm_invert_analytic(const double m,
919 const double rho,
920 const double alpha,
921 const double n_vg,
922 const double thetaR,
923 const double thetaSR,
924 double &u)
925{
926 double psiC, pcBar, pcBar_n, sBar, thetaW, thetaS, m_vg;
927 m_vg = 1.0 - 1.0 / n_vg;
928 thetaS = thetaR + thetaSR;
929 thetaW = m / rho;
930 const double pcBarMax = 1.0e4;
931 const double SeMin = pow(1.0 + pow(pcBarMax, n_vg), -m_vg);
932 if (thetaW > thetaR + SeMin * thetaSR && thetaW < thetaS) {
933 sBar = (thetaW - thetaR) / thetaSR;
934 pcBar_n = pow(sBar, -1.0 / m_vg) - 1.0;
935 pcBar = pow(pcBar_n, 1.0 / n_vg);
936 psiC = pcBar / alpha;
937 u = -psiC;
938 } else if (thetaW >= thetaS) {
939 if (u < 0.0) u = 0.0;
940 } else {
941 u = -pcBarMax / alpha;
942 }
943}
944
945// Newton inverse of the FULL forward mass m = rho(u) * theta_w(u), so the
946// exp(beta*u) factor the analytic inverse ignores is included. The analytic
947// inverse seeds the iteration.
948//
949// The step cap is one capillary length, not an absolute 5 cm. dtheta/du spans
950// ~4 decades over the retention curve, so a fixed step cap cannot reach the
951// root at either end: with duMax = 5e-2 the 50 iterations here covered only
952// 2.5 m of head, and any correction needing more than that fell through to the
953// u = u_prev revert at the bottom of the loop.
954inline void vgm_invert_newton(const double m,
955 const double rho,
956 const double beta,
957 const double alpha,
958 const double n_vg,
959 const double thetaR,
960 const double thetaSR,
961 double &u)
962{
963 const double u_prev = u;
964
965 const double thetaS = thetaR + thetaSR;
966 const double m_vg = 1.0 - 1.0 / n_vg;
967
968 const double psiC0 = -u;
969 if (psiC0 <= 0.0) { return; } //saturated, no inversion
970 const double rhom0 = rho * std::exp(beta * u);//first guess
971 const double thetaW_imp = m / rhom0;
972 // Dry limit stated the same way as in vgm_invert_analytic: a cap on psiC, not
973 // a fraction of thetaR. Same reason -- theta -> psiC is exponentially steep
974 // in the tail, so 1.01*thetaR is a narrow band in theta but a huge one in
975 // head (psiC = 3.8 m for alpha = 14.5 1/m, n = 2.68), and every FCT-limited
976 // mass landing in it was silently discarded, breaking theta_limited -> psi.
977 // The analytic seed below is exact at beta = 0 and lands within a couple of
978 // Newton steps otherwise, so widening the range does not lengthen the solve.
979 const double pcBarMax = 1.0e4;
980 const double SeMin = std::pow(1.0 + std::pow(pcBarMax, n_vg), -m_vg);
981 if (thetaW_imp < thetaR + SeMin * thetaSR) { return; } //below the dry cut
982 const double thetaEps = 1e-12;
983 double m_target = m;
984 if (thetaW_imp > 0.99 * thetaS) {
985 const double thetaWc = std::min(thetaW_imp, thetaS - thetaEps);
986 m_target = rhom0 * thetaWc;
987 }
988
989 // Use the analytic van Genuchten inverse as the initial guess for Newton.
990 {
991 const double thetaW_guess = std::min(m_target / rhom0, thetaS - thetaEps);
992 if (thetaW_guess > thetaR + thetaEps && thetaW_guess < thetaS - thetaEps) {
993 const double sBar = (thetaW_guess - thetaR) / thetaSR;
994 if (sBar > 0.0 && sBar < 1.0) {
995 const double pcBar_n = std::pow(sBar, -1.0 / m_vg) - 1.0;
996 if (pcBar_n > 0.0) {
997 const double pcBar = std::pow(pcBar_n, 1.0 / n_vg);
998 const double u_guess = -pcBar / alpha;
999 if (std::isfinite(u_guess) && u_guess < 0.0) {
1000 u = u_guess;
1001 }
1002 }
1003 }
1004 }
1005 }
1006
1007 /*----------------------------------------------------
1008 Newton solve (UNSATURATED ONLY)
1009 ----------------------------------------------------*/
1010 const int maxIts = 50;
1011 const double tol = 1e-12 * std::max(1.0, std::fabs(m));
1012 const double duMax = 1.0 / alpha;
1013
1014 auto theta_and_dtheta_du = [&](double u,
1015 double &thetaW,
1016 double &dtheta_du) -> bool
1017 {
1018 const double psiC = -u;
1019
1020 if (psiC <= 0.0) { return false; } //no inversion in saturation
1021 //van Genuchten relations
1022 const double pcBar = alpha * psiC;
1023 const double pcBarStar = (pcBar < 1e-12) ? 1e-12 : pcBar;
1024 const double pcBar_nM2 = std::pow(pcBarStar, n_vg - 2.0);
1025 const double pcBar_nM1 = pcBar_nM2 * pcBar;
1026 const double pcBar_n = pcBar_nM1 * pcBar;
1027 const double onePlus_pcBar_n = 1.0 + pcBar_n;
1028 const double sBar = std::pow(onePlus_pcBar_n, -m_vg);
1029
1030 const double DsBar_DpsiC =
1031 alpha * (1.0 - n_vg) * (sBar / onePlus_pcBar_n) * pcBar_nM1;
1032
1033 thetaW = thetaR + thetaSR * sBar;
1034 dtheta_du = -thetaSR * DsBar_DpsiC;
1035 if (thetaW <= thetaR + thetaEps) return false;
1036 if (thetaW >= thetaS - thetaEps) return false;
1037 return true;
1038 };
1039 for (int it = 0; it < maxIts; ++it)
1040 {
1041 if (-u <= 0.0) { u = u_prev; return; }
1042
1043 double thetaW, dtheta_du;
1044 if (!theta_and_dtheta_du(u, thetaW, dtheta_du)) {
1045 u = u_prev; return;
1046 }
1047
1048 const double rhom = rho * std::exp(beta * u);
1049 const double g = rhom * thetaW - m_target;
1050 if (std::fabs(g) < tol) return;
1051
1052 const double gp = rhom * (beta * thetaW + dtheta_du);
1053 // guard against near-zero derivative
1054 const double gpTol = 1e-14 * std::max(1.0, std::fabs(rhom * thetaW));
1055 if (std::fabs(gp) < gpTol) { u = u_prev; return; }
1056
1057 double du = -g / gp;
1058 if (du > duMax) du = duMax;
1059 if (du < -duMax) du = -duMax;
1060
1061 u += du;
1062 if (-u <= 0.0) { u = u_prev; return; }
1063 }
1064 u = u_prev;
1065}
1066
1067// -----------------------------------------------------------------------------
1068// Brooks-Corey
1069//
1070// S_e = (alpha*psiC)^(-lambda) for alpha*psiC >= 1
1071// S_e = 1 for alpha*psiC < 1 (saturated)
1072// theta_w = thetaR + thetaSR * S_e
1073// k_rw = S_e^eta, eta = bc_eta(lambda, kr_model)
1074// psiC(S_e) = S_e^(-1/lambda) / alpha (analytic inverse)
1075//
1076// BC has a derivative discontinuity in (theta_w, k_rw) at the entry pressure
1077// alpha*psiC = 1. Newton typically needs a smoothed regularisation there in
1078// production; that is deliberately not added at this layer - callers can wrap
1079// these closures with their own smoothing if needed.
1080// -----------------------------------------------------------------------------
1081
1082// Which k_rw closure supplies the Brooks-Corey exponent. Maps onto PSK_TYPE:
1083// 1 -> burdine, 2 -> mualem. burdine is first so it is the zero value and
1084// stays the default, matching what this header did before the choice existed.
1085enum class bc_kr { burdine = 0, mualem = 1 };
1086
1087// eta(lambda) for the two closures. Kept as a named function rather than
1088// inlined into bc_wetting so the two formulas sit side by side and the caller
1089// never has to restate either one.
1090inline double bc_eta(const double lam, const bc_kr kr_model)
1091{
1092 return (kr_model == bc_kr::mualem) ? (2.5 + 2.0 / lam)
1093 : ((2.0 + 3.0 * lam) / lam);
1094}
1095
1096inline void bc_wetting(const double psiC,
1097 const double alpha,
1098 const double lam,
1099 const double thetaR,
1100 const double thetaSR,
1101 double &thetaW,
1102 double &DthetaW_DpsiC,
1103 double &KWr,
1104 double &DKWr_DpsiC,
1105 const bc_kr kr_model = bc_kr::burdine)
1106{
1107 const double thetaS = thetaR + thetaSR;
1108 if (psiC <= 0.0) {
1109 // saturated (no suction)
1110 thetaW = thetaS;
1111 DthetaW_DpsiC = 0.0;
1112 KWr = 1.0;
1113 DKWr_DpsiC = 0.0;
1114 return;
1115 }
1116 const double pcBar = alpha * psiC;
1117 if (pcBar <= 1.0) {
1118 // suction below entry pressure: still fully wetting-saturated
1119 thetaW = thetaS;
1120 DthetaW_DpsiC = 0.0;
1121 KWr = 1.0;
1122 DKWr_DpsiC = 0.0;
1123 return;
1124 }
1125 // unsaturated branch (pcBar > 1)
1126 const double Se = pow(pcBar, -lam);
1127 const double dSe_DpsiC = -lam * alpha * pow(pcBar, -lam - 1.0);
1128 thetaW = thetaR + thetaSR * Se;
1129 DthetaW_DpsiC = thetaSR * dSe_DpsiC;
1130 // k_rw = Se^eta, exponent set by the selected closure
1131 const double exp_w = bc_eta(lam, kr_model);
1132 KWr = pow(Se, exp_w);
1133 DKWr_DpsiC = exp_w * pow(Se, exp_w - 1.0) * dSe_DpsiC;
1134}
1135
1136inline void bc_invert_analytic(const double m,
1137 const double rho,
1138 const double alpha,
1139 const double lam,
1140 const double thetaR,
1141 const double thetaSR,
1142 double &u)
1143{
1144 const double thetaS = thetaR + thetaSR;
1145 const double thetaW = m / rho;
1146 // Dry limit of the inversion, expressed the same way as in bc_invert_newton:
1147 // a multiple of the entry pressure, not a fraction of thetaR. See the note
1148 // there for why 1.01*thetaR cannot be used with the BC tail.
1149 const double psiCMax_over_pd = 1.0e4;
1150 const double SeMin = pow(psiCMax_over_pd, -lam);
1151 if (thetaW > thetaR + SeMin * thetaSR && thetaW < thetaS) {
1152 const double Se = (thetaW - thetaR) / thetaSR;
1153 if (Se > 0.0 && Se < 1.0) {
1154 const double pcBar = pow(Se, -1.0 / lam);
1155 const double psiC = pcBar / alpha;
1156 u = -psiC;
1157 }
1158 } else if (thetaW >= thetaS) {
1159 if (u < -1.0 / alpha) u = -1.0 / alpha;
1160 } else {
1161 u = -psiCMax_over_pd / alpha;
1162 }
1163}
1164
1165// Inverse of the FULL forward mass m = rho(u)*theta_w(u) against the BC
1166// retention curve. Solved as a bracketed (safeguarded) Newton rather than the
1167// plain Newton vgm_invert_newton uses, for three reasons specific to BC:
1168//
1169// * Dead band. Both routines cut the dry tail at a multiple of the entry
1170// pressure rather than at a fraction of thetaR (see vgm_invert_newton for
1171// why a band in theta is the wrong variable to state it in); BC needs that
1172// the more, since its Se ~ psiC^-lambda tail is far fatter than van
1173// Genuchten's. With thetaR = 0.05, thetaSR = 0.35, p_d = 0.5 m, lambda = 2
1174// a 1.01*thetaR band would fire for every node drier than psiC ~ 13 m, where
1175// under VG (n = 1.8) it does not fire until psiC ~ 1800 m. Here the limit is
1176// enforced exactly rather than through a proxy in theta: if g at that head
1177// has not yet changed sign the root is drier than the band and u is left
1178// alone.
1179//
1180// * Entry-pressure wall. theta is flat (== thetaS) for psiC <= p_d, so the
1181// root can lie inside a region the retention curve cannot resolve. That is
1182// detected up front (g(u_wall) <= 0) and answered with the entry pressure
1183// itself -- the driest head consistent with full saturation, and the
1184// correct inverse of the mass handed in -- rather than by discarding the
1185// solve and reverting to a stale iterate carrying an unrelated theta.
1186//
1187// * Conditioning. In the dry tail the compressibility term beta*theta
1188// dominates thetaSR*dSe/du (at Se = 1e-6, beta = 1e-5 it is ~350x larger),
1189// so the root is set almost entirely by rho(u). Any seed that evaluates
1190// exp(beta*u) at a stale iterate is then wrong by orders of magnitude in
1191// Se, and unguarded Newton walks off. Bracketing removes the dependence on
1192// the seed: g is monotone increasing in u, so [u_floor, u_wall] brackets the
1193// root by construction and every iterate stays inside it.
1194inline void bc_invert_newton(const double m,
1195 const double rho,
1196 const double beta,
1197 const double alpha,
1198 const double lam,
1199 const double thetaR,
1200 const double thetaSR,
1201 double &u)
1202{
1203 if (-u <= 0.0) { return; } //ponded / saturated head: no inversion
1204
1205 // g(u) = rho(u)*theta_w(u) - m, evaluated with the BC closure (theta_w
1206 // clamped to thetaS inside the entry pressure). Monotone increasing in u.
1207 auto g_of = [&](const double uu) -> double {
1208 const double pcBar = alpha * (-uu);
1209 const double Se = (pcBar <= 1.0) ? 1.0 : pow(pcBar, -lam);
1210 return rho * std::exp(beta * uu) * (thetaR + thetaSR * Se) - m;
1211 };
1212
1213 // Upper end of the bracket: one part in 1e9 outside the entry pressure, so
1214 // the BC derivative (which drops to zero across it) stays defined.
1215 const double u_entry = -1.0 / alpha;
1216 const double u_wall = -(1.0 + 1.0e-9) / alpha;
1217 if (g_of(u_wall) <= 0.0) { u = u_entry; return; } //root at or inside the wall
1218
1219 // Lower end: the driest head the inversion is defined down to, stated as a
1220 // multiple of the entry pressure. A floor on Se instead would sit at
1221 // psiC = p_d*Se^(-1/lambda) and so swing with lambda -- Se = 1e-6 is
1222 // psiC = 1e3*p_d at lambda = 2 but 1e12*p_d at lambda = 0.5, far outside any
1223 // head the solver will ever see and wide enough to make the bracket useless.
1224 const double psiCMax_over_pd = 1.0e4;
1225 const double u_floor = -psiCMax_over_pd / alpha;
1226 if (g_of(u_floor) > 0.0) { return; } //root drier than the band: leave u alone
1227
1228 double lo = u_floor, hi = u_wall; // g(lo) <= 0 < g(hi)
1229
1230 // Analytic BC inverse (density-free) as the opening guess; the bracket
1231 // catches it if the neglected exp(beta*u) puts it in the wrong place.
1232 {
1233 const double Se = (m / rho - thetaR) / thetaSR;
1234 if (Se > 0.0 && Se < 1.0) {
1235 const double u_guess = -pow(Se, -1.0 / lam) / alpha;
1236 if (std::isfinite(u_guess) && u_guess > lo && u_guess < hi) u = u_guess;
1237 else u = 0.5 * (lo + hi);
1238 } else u = 0.5 * (lo + hi);
1239 }
1240 if (!(u > lo && u < hi)) u = 0.5 * (lo + hi);
1241
1242 // maxIts covers the bisection worst case over the widest bracket; Newton
1243 // reaches the root in a handful of steps whenever the guess is sane.
1244 const int maxIts = 100;
1245 const double tol = 1e-12 * std::max(1.0, std::fabs(m));
1246
1247 for (int it = 0; it < maxIts; ++it) {
1248 // u is strictly inside (u_floor, u_wall), so pcBar > 1 and the BC
1249 // derivative below is always the unsaturated-branch one.
1250 const double pcBar = alpha * (-u);
1251 const double Se = pow(pcBar, -lam);
1252 const double dSe_DpsiC = -lam * alpha * pow(pcBar, -lam - 1.0);
1253 const double thetaW = thetaR + thetaSR * Se;
1254 const double dtheta_du = -thetaSR * dSe_DpsiC; // d(psiC)/d(u) = -1
1255
1256 const double rhom = rho * std::exp(beta * u);
1257 const double g = rhom * thetaW - m;
1258 if (std::fabs(g) < tol) return;
1259 if (g > 0.0) hi = u; else lo = u;
1260
1261 const double gp = rhom * (beta * thetaW + dtheta_du);
1262 const double gpTol = 1e-14 * std::max(1.0, std::fabs(rhom * thetaW));
1263 double u_next = (std::fabs(gp) > gpTol) ? (u - g / gp) : 0.5 * (lo + hi);
1264 // Bisect whenever Newton leaves the bracket.
1265 if (!(u_next > lo && u_next < hi)) u_next = 0.5 * (lo + hi);
1266
1267 if (std::fabs(u_next - u) <= 1e-15 * std::fabs(u)) return;
1268 u = u_next;
1269 }
1270 // Bracketed throughout, so the last iterate is the best available answer.
1271}
1272
1273// -----------------------------------------------------------------------------
1274// Gardner (quasi-linear) retention + conductivity, with the Irmay-style linear
1275// theta_w(k_rw) pairing:
1276//
1277// S_e = exp(-alpha*psiC) = exp(alpha*u)
1278// theta_w = thetaR + thetaSR * S_e
1279// k_rw = S_e i.e. K(psi) = Ks*exp(alpha*psi)
1280// psiC(S_e) = -ln(S_e)/alpha (analytic inverse, exact)
1281//
1282// alpha here is Gardner's exponent [1/m]; it takes the same parameter slot as
1283// van Genuchten's alpha and the Brooks-Corey inverse entry pressure, and the
1284// second numeric parameter (n_vg / lambda) is unused.
1285//
1286// This is the pairing Tracy's analytical solutions are built on, and both
1287// halves of it matter: with k_rw = S_e and theta_w affine in S_e, the Kirchhoff
1288// transform hbar = exp(alpha*psi) turns Richards into a linear advection-
1289// diffusion equation for hbar, which is what makes the closed-form steady and
1290// transient solutions exist. Substituting Mualem k_rw here would keep the
1291// retention curve but destroy the linearisation, so the two are one model, not
1292// two independent choices.
1293//
1294// There is no entry pressure and no dry-end kink: theta_w and k_rw are C-inf
1295// for psiC > 0 and both approach saturation smoothly as psiC -> 0. What it
1296// does have is an unbounded dry tail -- S_e -> 0 only as psiC -> inf -- so the
1297// inversions cut at alpha*psiC = 7e2, stated as a cap on head like the vgm_/bc_
1298// routines rather than as a band in theta. That is where exp() underflows,
1299// i.e. as far out as double precision can represent the curve at all, and
1300// orders of magnitude beyond any head a solve will see.
1301//
1302// UNLIKE vgm_ and bc_, there is NO saturated branch: the exponential is
1303// continued through psiC = 0 into psiC < 0 rather than clamped to
1304// (theta_w, k_rw) = (thetaS, 1). That is deliberate and it is not cosmetic.
1305// Clamping sets DthetaW_DpsiC = 0, hence dm = drhom*thetaW, which at beta = 0
1306// is exactly zero -- and the low-order diagonal in Richards.h is
1307//
1308// globalJacobian[ii] += bc_mask[i]*(MLi*dm/dt + J_ii) + (1-bc_mask[i])
1309//
1310// where MLi*dm/dt outweighs the graph term J_ii by ~3 orders at a small dt. A
1311// free DOF that reaches psi >= 0 therefore keeps its residual row but loses its
1312// diagonal, takes a correction ~3 orders too large, falls back below zero where
1313// dm > 0 again, and overshoots once more: Newton locks into an exact period-2
1314// orbit that no tolerance or step cap will break. vgm_/bc_ carry the same
1315// clamp harmlessly because the only node pinned at psi = 0 is normally a
1316// Dirichlet node, which bc_mask = 0 replaces with an identity row; Gardner
1317// reaches psi = 0 at *free* nodes because its diffusivity is
1318// Ks/(alpha*thetaSR), constant over the whole curve and typically orders above
1319// van Genuchten's in the unsaturated range, so a sharp boundary layer drives
1320// interior nodes into saturation within one step.
1321//
1322// The continuation is the same exponential, so the closure stays C-inf and
1323// every derivative used to build the diagonal stays strictly positive. It does
1324// mean k_rw > 1 and theta_w > thetaS for psi > 0, which is unphysical -- but
1325// Gardner is a psi <= 0 model with no saturated branch to speak of, and these
1326// values are only ever visited by transient iterates on the way back down. Any
1327// case that genuinely ponds wants vgm_ or bc_, which cap properly.
1328// -----------------------------------------------------------------------------
1329
1330// Dry-end cut for the Gardner inversions, in units of alpha*psiC. exp(-7e2) is
1331// ~1e-304, one decade off denormal, so this is the widest cut that keeps S_e a
1332// normal double.
1333constexpr double gardner_alphaPsiCMax = 7.0e2;
1334
1335// Wet-end rail on the SAME exponent, so exp(alpha*u) cannot overflow if an
1336// iterate runs away. This is an arithmetic guard, not a saturation limit: at a
1337// typical alpha it sits hundreds of metres above ground, far outside any head a
1338// solve can reach without having already failed, and it is placed on the
1339// exponent (not on theta) so it can never fire near psi = 0 where the clamp
1340// above would cost the diagonal.
1341constexpr double gardner_alphaPsiMax = 5.0e1;
1342
1343inline void gardner_wetting(const double psiC,
1344 const double alpha,
1345 const double n_vg,
1346 const double thetaR,
1347 const double thetaSR,
1348 double &thetaW,
1349 double &DthetaW_DpsiC,
1350 double &KWr,
1351 double &DKWr_DpsiC)
1352{
1353 (void)n_vg; // Gardner is a one-parameter curve
1354 // One branch for every psiC. exp(-alpha*psiC) underflows to 0 past the dry
1355 // cut (theta_w -> thetaR, k_rw -> 0 with zero slope, all correct); the wet
1356 // rail below only bounds a runaway iterate.
1357 const double x = -alpha * psiC;
1358 const double Se = std::exp(x < gardner_alphaPsiMax ? x : gardner_alphaPsiMax);
1359 const double dSe_DpsiC = (x < gardner_alphaPsiMax) ? (-alpha * Se) : 0.0;
1360 thetaW = thetaR + thetaSR * Se;
1361 DthetaW_DpsiC = thetaSR * dSe_DpsiC;
1362 KWr = Se;
1363 DKWr_DpsiC = dSe_DpsiC;
1364}
1365
1366// Analytic Gardner inverse: theta_w -> psiC. Exact (the retention curve is a
1367// plain exponential), so this is the whole inversion whenever beta = 0.
1368//
1369// The admissible band is the forward curve's own range, dry cut to wet rail --
1370// NOT thetaW < thetaS. Since gardner_wetting continues the exponential above
1371// psi = 0, theta_w > thetaS is a value the forward model genuinely produces and
1372// the inverse has to be able to return the u > 0 that generated it; cutting at
1373// thetaS would leave those DOFs holding a stale iterate whose theta is
1374// unrelated to the mass handed in, which is the conservation break the vgm_/bc_
1375// dry-cut comments describe, at the other end of the curve.
1376inline void gardner_invert_analytic(const double m,
1377 const double rho,
1378 const double alpha,
1379 const double n_vg,
1380 const double thetaR,
1381 const double thetaSR,
1382 double &u)
1383{
1384 (void)n_vg;
1385 const double thetaW = m / rho;
1386 const double SeMin = std::exp(-gardner_alphaPsiCMax);
1387 const double SeMax = std::exp(gardner_alphaPsiMax);
1388 if (thetaW > thetaR + SeMin * thetaSR && thetaW < thetaR + SeMax * thetaSR) {
1389 const double Se = (thetaW - thetaR) / thetaSR;
1390 const double u_new = std::log(Se) / alpha; // = -psiC; sign follows Se vs 1
1391 if (std::isfinite(u_new)) u = u_new;
1392 }
1393}
1394
1395// Inverse of the FULL forward mass m = rho(u)*theta_w(u) against the Gardner
1396// curve, i.e. including the exp(beta*u) the analytic inverse drops.
1397//
1398// Bracketed Newton, like bc_invert_newton and for the same conditioning
1399// reason: g(u) = rho*exp(beta*u)*(thetaR + thetaSR*exp(alpha*u)) - m is a sum
1400// of increasing exponentials, so it is monotone in u and [u_floor, u_rail]
1401// brackets the root by construction. In the dry tail the beta*theta term
1402// dominates thetaSR*alpha*Se, so a plain Newton seeded from a stale iterate can
1403// walk off exactly as it does under BC; bracketing removes the dependence on
1404// the seed.
1405//
1406// Both ends of the bracket are the forward curve's own limits. There is no
1407// wall at u = 0: gardner_wetting has no saturated branch, so psi > 0 is inside
1408// the model here and a mass above rho*thetaS inverts to the positive head that
1409// produced it rather than being flattened onto zero. Nothing is refused for
1410// being "ponded" either -- an iterate that has overshot into psi > 0 is exactly
1411// the state the inversion has to be able to walk back down.
1412inline void gardner_invert_newton(const double m,
1413 const double rho,
1414 const double beta,
1415 const double alpha,
1416 const double n_vg,
1417 const double thetaR,
1418 const double thetaSR,
1419 double &u)
1420{
1421 (void)n_vg;
1422
1423 auto g_of = [&](const double uu) -> double {
1424 const double x = alpha * uu;
1425 const double Se = std::exp(x < gardner_alphaPsiMax ? x : gardner_alphaPsiMax);
1426 return rho * std::exp(beta * uu) * (thetaR + thetaSR * Se) - m;
1427 };
1428
1429 const double u_rail = gardner_alphaPsiMax / alpha;
1430 if (g_of(u_rail) <= 0.0) { u = u_rail; return; } //root at or above the rail
1431
1432 const double u_floor = -gardner_alphaPsiCMax / alpha;
1433 if (g_of(u_floor) > 0.0) { return; } //root drier than the band: leave u alone
1434
1435 double lo = u_floor, hi = u_rail; // g(lo) <= 0 < g(hi)
1436
1437 // Density-free analytic inverse as the opening guess; the bracket catches it
1438 // if the neglected exp(beta*u) puts it in the wrong place.
1439 {
1440 const double Se = (m / rho - thetaR) / thetaSR;
1441 double u_guess = 0.5 * (lo + hi);
1442 if (Se > 0.0) { // Se >= 1 is admissible now: it is the psi > 0 continuation
1443 const double u_analytic = std::log(Se) / alpha;
1444 if (std::isfinite(u_analytic) && u_analytic > lo && u_analytic < hi)
1445 u_guess = u_analytic;
1446 }
1447 u = u_guess;
1448 }
1449 if (!(u > lo && u < hi)) u = 0.5 * (lo + hi);
1450
1451 // maxIts covers the bisection worst case over the widest bracket; from the
1452 // analytic seed Newton lands in a couple of steps for any sane beta.
1453 const int maxIts = 100;
1454 const double tol = 1e-12 * std::max(1.0, std::fabs(m));
1455
1456 for (int it = 0; it < maxIts; ++it) {
1457 const double Se = std::exp(alpha * u);
1458 const double thetaW = thetaR + thetaSR * Se;
1459 const double dtheta_du = thetaSR * alpha * Se;
1460
1461 const double rhom = rho * std::exp(beta * u);
1462 const double g = rhom * thetaW - m;
1463 if (std::fabs(g) < tol) return;
1464 if (g > 0.0) hi = u; else lo = u;
1465
1466 const double gp = rhom * (beta * thetaW + dtheta_du);
1467 const double gpTol = 1e-14 * std::max(1.0, std::fabs(rhom * thetaW));
1468 double u_next = (std::fabs(gp) > gpTol) ? (u - g / gp) : 0.5 * (lo + hi);
1469 // Bisect whenever Newton leaves the bracket.
1470 if (!(u_next > lo && u_next < hi)) u_next = 0.5 * (lo + hi);
1471
1472 if (std::fabs(u_next - u) <= 1e-15 * std::fabs(u)) return;
1473 u = u_next;
1474 }
1475 // Bracketed throughout, so the last iterate is the best available answer.
1476}
1477
1478} // namespace psk
1479} // namespace richards
1480} // namespace proteus
1481
1482namespace proteus
1483{
1484namespace m_comp_co2
1485{
1486namespace psk
1487{
1488
1489// =============================================================================
1490// Two-phase PSK closures for m_comp_co2, parameterised by effective saturation
1491// S_e rather than by suction psiC. These six routines are the only ones
1492// m_comp_co2.h calls.
1493//
1494// vgm_pc_from_Se / bc_pc_from_Se capillary curve p_c(S_e)
1495// vgm_wetting_from_Se / bc_wetting_from_Se theta_w(S_e), k_rw(S_e)
1496// vgm_kr_nonwetting_from_Se / bc_..._from_Se k_rn(S_e)
1497//
1498// The underlying constitutive models are NOT new here: the VGM branch is the
1499// same Mualem-van Genuchten curve as VGM::calc in proteus/pskRelations.h (the
1500// two forms are related by (1 - S_e^(1/m))^m = (alpha*psiC)^(n-1) * S_e), and
1501// the BC branch is Brooks-Corey-Burdine, the same as BCB::calc there. What
1502// these routines add, and pskRelations.h does not have, is the regularisation
1503// the compositional Newton solve needs: second derivatives d2p_c/dS_e2, the C1
1504// cubic-Hermite ramp across gas appearance at S_e = 1, the linear cap on the
1505// diverging p_c tail below Se_min_pc, the residual-gas remap Se_trap, and the
1506// Hermite bridge over k_rn's triple zero at S_e = 1. Unifying the two layers
1507// is post-1.9.0 work.
1508//
1509// The psiC-based closures (vgm_wetting, bc_wetting, vgm_kr_nonwetting,
1510// bc_kr_nonwetting and the four invert routines) used to be duplicated here as
1511// well. They had no call site in m_comp_co2.h and were a stale fork of the
1512// Richards versions, so they have been removed; the maintained copies live in
1513// proteus/pskRelations.h under namespace proteus::richards::psk.
1514//
1515// vgm_pc_from_Se : Inverse capillary curve, given effective saturation
1516// S_e = (theta_w - theta_R) / theta_SR (= sBar).
1517// pcBar = (S_e^(-1/m) - 1)^(1/n); pc = pcBar / alpha.
1518// Returns (pc, dpc/dSe) with dpc/dSe < 0. Both clamped to
1519// 0 at S_e >= 1 (no capillary suction in the fully-wet
1520// branch); near S_e = 0 the value diverges - clamped via
1521// a small floor so derivatives remain finite.
1522// =============================================================================
1523
1524inline void vgm_pc_from_Se(const double Se,
1525 const double alpha,
1526 const double n_vg,
1527 double &pc,
1528 double &Dpc_DSe,
1529 double &D2pc_DSe2)
1530{
1531 const double m_vg = 1.0 - 1.0 / n_vg;
1532 // Floor Se from below to keep pcBar_n bounded for Se near 0 (gas-saturated).
1533 // Raised from 1e-12 to 1e-3 to bound pc at the saturation cap; otherwise
1534 // pc reaches ~1e12/alpha at the floor and floating-point cancellation in
1535 // the antisymmetric edge flux F_ij + F_ji = 0 leaks mass on the order of
1536 // |pc|*eps_machine per edge. With the 1e-3 floor, |pc_max| ~ 1e3/alpha and
1537 // the antisymmetric roundoff drops by ~9 orders of magnitude. bc_pc_from_Se
1538 // bounds the same tail with its own cap (Se_min_pc), but the two are not
1539 // equivalent: below the floor VGM holds pc flat (Dpc_DSe = 0 via
1540 // dSeStar_dSe), whereas BC extrapolates linearly with a finite slope.
1541 const double SeStar = (Se < 1.0e-3) ? 1.0e-3 : Se;
1542 const double SemInvM = pow(SeStar, -1.0 / m_vg);
1543 const double pcBar_n_raw = SemInvM - 1.0;
1544 // ------------------------------------------------------------------------
1545 // VGM regularization: SOFT FLOOR on pcBar_n.
1546 //
1547 // pcBar_n = pcBar_n_raw + eps_pcBar
1548 //
1549 // This is C-infinity smooth across Se -> 1 (no kinks), bounds Dpc_DSe by
1550 // pcBar_n_soft^(1/n - 1) * (constant), and recovers the physical
1551 // pcBar_n_raw whenever raw >> eps_pcBar (i.e., away from full saturation).
1552 //
1553 // The earlier hard-cutoff regularization (pcBar_n = max(raw, eps)) made
1554 // Dpc_DSe C0-discontinuous at the threshold; Newton would stall when its
1555 // iterate crossed the threshold mid-step. The soft floor eliminates that.
1556 //
1557 // Cost: pc(Se = 1) = (eps_pcBar)^(1/n)/alpha ~ 0.001 instead of exactly 0.
1558 // Physically negligible (any DOF actually AT Se = 1 has zero gas flux
1559 // already because k_rn = 0 there).
1560 //
1561 // The legacy `if (Se >= 1.0)` and `pcBar_n_raw <= 0.0` early-returns are
1562 // dropped: with the soft floor, the formula is well-defined everywhere
1563 // including Se = 1 (pcBar_n_raw can dip slightly negative due to roundoff
1564 // when Se >= 1, but pcBar_n_soft remains >= eps_pcBar/2 > 0 as long as
1565 // |pcBar_n_raw| < eps_pcBar/2 which holds for Se in [1 - small, 1 + small]).
1566 // ------------------------------------------------------------------------
1567 const double eps_pcBar = 1.0e-3;
1568 // For Se >= 1 the raw value can be slightly negative due to roundoff;
1569 // clamp from below to keep the soft-floored pcBar_n strictly positive.
1570 const double pcBar_n_raw_clamped = (pcBar_n_raw > -0.5 * eps_pcBar)
1571 ? pcBar_n_raw
1572 : -0.5 * eps_pcBar;
1573 const double pcBar_n = pcBar_n_raw_clamped + eps_pcBar;
1574 const double pcBar = pow(pcBar_n, 1.0 / n_vg);
1575 pc = pcBar / alpha;
1576
1577 // First derivative -- C-infinity smooth in Se now.
1578 // d(pcBar_n)/d(Se) = d(pcBar_n_raw)/d(Se) (since eps_pcBar is constant)
1579 // = -(1/m) * Se^(-1/m - 1)
1580 // = -(1/m) * SemInvM / SeStar
1581 // d(pcBar)/d(Se) = (1/n) * pcBar_n^(1/n - 1) * d(pcBar_n)/d(Se)
1582 // For Se > 1 (numerical excursion) we hold dpcBar_n/dSe at its Se = 1 value
1583 // (which is small) -- no discontinuity introduced because pcBar_n_raw was
1584 // clamped above.
1585 const double dpcBar_n_dSe = -(1.0 / m_vg) * (SemInvM / SeStar);
1586 const double dpcBar_dSe = (1.0 / n_vg) * (pcBar / pcBar_n) * dpcBar_n_dSe;
1587 // pc is built from SeStar = max(Se, 1e-3); below the floor pc is FLAT, so the
1588 // chain rule gives d(pc)/d(Se) = d(pc)/d(SeStar) * d(SeStar)/d(Se) with
1589 // d(SeStar)/d(Se) = 0 for Se < 1e-3. Without this factor pc(Se) is flat but
1590 // Dpc_DSe is nonzero -> residual/Jacobian inconsistency at gas-saturated nodes
1591 // (Se_a -> 0), which stalls Newton near the saturation ceiling.
1592 const double dSeStar_dSe = (Se < 1.0e-3) ? 0.0 : 1.0;
1593 Dpc_DSe = (dpcBar_dSe / alpha) * dSeStar_dSe;
1594
1595 // Second derivative (gas-eq (1,1) capillary sensitivity), also C-infinity.
1596 // d2(pcBar_n)/d(Se)2 = (1/m)*(1/m + 1) * Se^(-1/m - 2)
1597 // = (1/m)*(1/m + 1) * SemInvM / Se^2
1598 // d2(pcBar)/d(Se)2 = (1/n) * [(1/n - 1) * pcBar_n^(1/n - 2) * (d pcBar_n / dSe)^2
1599 // + pcBar_n^(1/n - 1) * d2(pcBar_n)/d(Se)2]
1600 const double inv_m = 1.0 / m_vg;
1601 const double inv_n = 1.0 / n_vg;
1602 const double d2pcBar_n_dSe2 = inv_m * (inv_m + 1.0) * (SemInvM / (SeStar * SeStar));
1603 const double pcBar_n_pow_a = pow(pcBar_n, inv_n - 2.0); // pcBar_n^(1/n - 2)
1604 const double pcBar_n_pow_b = pow(pcBar_n, inv_n - 1.0); // pcBar_n^(1/n - 1)
1605 const double d2pcBar_dSe2 = inv_n * ((inv_n - 1.0) * pcBar_n_pow_a * dpcBar_n_dSe * dpcBar_n_dSe
1606 + pcBar_n_pow_b * d2pcBar_n_dSe2);
1607 D2pc_DSe2 = (d2pcBar_dSe2 / alpha) * dSeStar_dSe; // 0 below the Se floor (flat pc)
1608}
1609
1610// =============================================================================
1611// Brooks-Corey (Burdine relative-permeability) closures.
1612// Parallel API to the vgm_* functions: same parameter slots, same output args.
1613// In the BC parameterisation the second numeric parameter is the pore-size
1614// index lambda (taking the slot of vgm's n_vg). alpha = 1/p_d is the inverse
1615// entry-pressure head.
1616//
1617// S_e = (alpha * p_c)^(-lambda) for alpha*p_c >= 1
1618// S_e = 1 for alpha*p_c < 1 (saturated)
1619// theta_w = theta_R + theta_SR * S_e
1620// k_rw = S_e^((2+3*lambda)/lambda) (Burdine)
1621// k_rn = (1-S_e)^2 * (1 - S_e^((2+lambda)/lambda))
1622// p_c(S_e) = S_e^(-1/lambda) / alpha (analytic inverse)
1623//
1624// BC has a discontinuity in (k_rw, k_rn, theta_w) derivatives at p_c = p_d
1625// (alpha*p_c = 1), i.e. at S_e = 1. Unlike the raw model, the routines below
1626// regularise it: bc_pc_from_Se carries the C1 Hermite ramp across S_e = 1 and
1627// the linear cap below Se_min_pc, and bc_kr_nonwetting_from_Se bridges k_rn's
1628// triple zero at S_e = 1. Each is documented at its own definition.
1629// =============================================================================
1630
1631inline void bc_pc_from_Se(const double Se,
1632 const double alpha,
1633 const double lam,
1634 double &pc,
1635 double &Dpc_DSe,
1636 double &D2pc_DSe2)
1637{
1638 // ----------------------------------------------------------------------
1639 // Brooks-Corey p_c(S_e) with smoothed transition out of the physical range
1640 // (so Newton can move through Se = 1 without seeing a value-jump).
1641 //
1642 // Se in [0, 1): pc = (1/alpha) * Se^(-1/lambda) (standard BC)
1643 // At Se -> 1: pc -> 1/alpha (the "entry pressure"
1644 // p_d). At Se -> 0: pc -> +infinity (asymptotic).
1645 //
1646 // Se in [1, 1+delta]: pc smoothly ramps from 1/alpha down to 0 using a
1647 // cubic Hermite smoothstep phi(t) = 1 - 3t^2 + 2t^3
1648 // in t = (Se - 1)/delta. phi(0) = 1, phi(1) = 0,
1649 // phi'(0) = phi'(1) = 0.
1650 //
1651 // Se > 1 + delta: pc = 0.
1652 //
1653 // Value continuity at Se = 1: pc(1-) = pc(1+) = 1/alpha. ENTRY PRESSURE
1654 // PRESERVED. The previous "if (Se >= 1.0) { pc = 0 }" cutoff produced a
1655 // finite jump of size 1/alpha at Se = 1 and stalled Newton whenever an
1656 // iterate crossed this threshold.
1657 //
1658 // C1 at Se = 1: the Se >= 1 ramp's left slope is matched to the BC branch
1659 // slope -(1/lambda)/alpha (see that branch), so dpc/dSe is now continuous
1660 // across gas appearance. (Previously the ramp left with slope 0, leaving a
1661 // slope kink that mispredicted Newton steps overshooting across Se = 1.)
1662 // ----------------------------------------------------------------------
1663 const double delta_smooth = 5.0e-2;
1664
1665 if (Se >= 1.0 + delta_smooth) {
1666 pc = 0.0;
1667 Dpc_DSe = 0.0;
1668 D2pc_DSe2 = 0.0;
1669 return;
1670 }
1671
1672 if (Se >= 1.0) {
1673 // Non-physical overshoot region (S_g < 0, only reached when a Newton iterate
1674 // crosses gas appearance from above). Cubic-Hermite ramp from pc(1) = 1/alpha
1675 // (the Brooks-Corey entry pressure p_d) down to pc(1+delta) = 0.
1676 //
1677 // C1 ACROSS GAS APPEARANCE: the LEFT-end slope is matched to the BC branch
1678 // slope at Se = 1, dpc/dSe = -1/(alpha*lam) (see the Se in [Se_min_pc,1)
1679 // branch below). The earlier ramp used phi'(0) = 0, leaving a slope kink at
1680 // Se = 1 (BC side -p_d/lam vs ramp side 0); a Newton iterate overshooting
1681 // across Se = 1 then saw an inconsistent linearization and mispredicted the
1682 // step. Right end keeps value 0 and slope 0 so pc joins the pc = 0 region C1.
1683 //
1684 // Hermite basis on t = (Se - 1)/delta in [0,1] with endpoints
1685 // left (t=0): value p_d = 1/alpha, slope_t m0_t = (-1/(alpha*lam))*delta
1686 // right (t=1): value 0, slope_t 0
1687 // so only H00 (value) and H10 (left slope) contribute.
1688 const double inv_alpha = 1.0 / alpha;
1689 const double m0_t = (-inv_alpha / lam) * delta_smooth; // BC slope*delta at Se=1
1690 const double t = (Se - 1.0) / delta_smooth;
1691 const double t2 = t * t, t3 = t2 * t;
1692 const double H00 = 2.0 * t3 - 3.0 * t2 + 1.0;
1693 const double H10 = t3 - 2.0 * t2 + t;
1694 const double dH00 = 6.0 * t2 - 6.0 * t;
1695 const double dH10 = 3.0 * t2 - 4.0 * t + 1.0;
1696 const double d2H00 = 12.0 * t - 6.0;
1697 const double d2H10 = 6.0 * t - 4.0;
1698 pc = H00 * inv_alpha + H10 * m0_t;
1699 Dpc_DSe = (dH00 * inv_alpha + dH10 * m0_t) / delta_smooth;
1700 D2pc_DSe2 = (d2H00 * inv_alpha + d2H10 * m0_t) / (delta_smooth * delta_smooth);
1701 return;
1702 }
1703
1704 // ----------------------------------------------------------------------
1705 // Gas-saturated cap. BC's pc and dpc/dSe diverge as Se -> 0 (i.e.
1706 // S_n -> 1 - S_wr). In the FluidFlower disk-injection case this kills
1707 // Newton once the disk fills toward the saturation ceiling -- the
1708 // Jacobian sees |dpc/dSe| ~ 1e2 .. 1e8 m/unit and the S_n step blows up.
1709 //
1710 // Fix: below Se_min_pc, freeze the BC tangent so pc and dpc/dSe stay
1711 // finite and the Jacobian stays bounded. C^1 continuous at Se_min_pc
1712 // (value + slope match by construction). d2pc/dSe2 has a finite jump
1713 // at the seam, which Newton tolerates (only pc and dpc/dSe enter the
1714 // residual and Jacobian). Standard "Pc cap" trick from reservoir
1715 // simulation; doesn't change pc above Se_min_pc, so undisturbed in
1716 // normal operation.
1717 //
1718 // Tuning Se_min_pc: smaller = sharper cap (closer to true BC), larger
1719 // = more robust Newton but more physics distortion near the gas-sat
1720 // endpoint. 1e-2 is a typical default; reduce to 1e-3 if the cap is
1721 // active across an unacceptable fraction of the domain.
1722 // ----------------------------------------------------------------------
1723 const double Se_min_pc = 1.0e-2;
1724 if (Se < Se_min_pc) {
1725 const double SemInvLam_min = pow(Se_min_pc, -1.0 / lam);
1726 const double pc_min = SemInvLam_min / alpha;
1727 const double slope_min = -(SemInvLam_min / Se_min_pc) / (alpha * lam);
1728 pc = pc_min + slope_min * (Se - Se_min_pc);
1729 Dpc_DSe = slope_min;
1730 D2pc_DSe2 = 0.0;
1731 return;
1732 }
1733
1734 // Standard BC in the physical range Se in [Se_min_pc, 1).
1735 const double SemInvLam = pow(Se, -1.0 / lam);
1736 pc = SemInvLam / alpha;
1737 // dpc/dS_e = -(1/(alpha*lambda)) * S_e^(-1/lambda - 1)
1738 // = -(1/(alpha*lambda)) * SemInvLam / S_e
1739 Dpc_DSe = -(SemInvLam / Se) / (alpha * lam);
1740 // d2pc/dS_e2 = (1/(alpha*lambda)) * (1/lambda + 1) * S_e^(-1/lambda - 2)
1741 // = (1/(alpha*lambda)) * (1/lambda + 1) * SemInvLam / S_e^2
1742 const double inv_lam = 1.0 / lam;
1743 D2pc_DSe2 = inv_lam * (inv_lam + 1.0) * (SemInvLam / (Se * Se)) / alpha;
1744}
1745
1746// =============================================================================
1747// theta_w(Se) and k_r(Se) (Phase B Step 3a). Same physics as the psiC-based
1748// closures in proteus/pskRelations.h, but parameterised directly by effective
1749// saturation Se. Used by the formulation-(psi_w, S_w) wetting equation, where
1750// saturation is the primary variable rather than something inverted from psiC.
1751//
1752// Conventions:
1753// Se in [0, 1] is the effective saturation = (theta_w - theta_R)/theta_SR.
1754// In formulation B, u_v IS S_w; if a residual saturation theta_R/theta_S is
1755// in play the caller must convert before calling these.
1756// Outputs are derivatives with respect to Se (NOT psiC).
1757// =============================================================================
1758
1759inline void vgm_wetting_from_Se(const double Se,
1760 const double /*alpha*/, // unused, kept for API parity
1761 const double n_vg,
1762 const double thetaR,
1763 const double thetaSR,
1764 double &thetaW,
1765 double &DthetaW_DSe,
1766 double &KWr,
1767 double &DKWr_DSe)
1768{
1769 const double m_vg = 1.0 - 1.0 / n_vg;
1770 thetaW = thetaR + thetaSR * Se;
1771 DthetaW_DSe = thetaSR;
1772 if (Se >= 1.0) {
1773 KWr = 1.0;
1774 DKWr_DSe = 0.0;
1775 return;
1776 }
1777 if (Se <= 0.0) {
1778 KWr = 0.0;
1779 DKWr_DSe = 0.0;
1780 return;
1781 }
1782 // Floor for the powers near the endpoints.
1783 const double SeStar = (Se < 1.0e-12) ? 1.0e-12 : Se;
1784 const double SeInvM = pow(SeStar, 1.0 / m_vg); // Se^(1/m)
1785 const double x_ = 1.0 - SeInvM; // 1 - Se^(1/m)
1786 const double xStar = (x_ < 1.0e-12) ? 1.0e-12 : x_;
1787 const double y_ = pow(xStar, m_vg); // (1 - Se^(1/m))^m
1788 const double term = 1.0 - y_; // 1 - (1 - Se^(1/m))^m
1789 const double sqrtSe = sqrt(SeStar);
1790 const double sqrtSe_safe = (sqrtSe < 1.0e-12) ? 1.0e-12 : sqrtSe;
1791 // Mualem k_rw = sqrt(Se) * [1 - (1 - Se^(1/m))^m]^2
1792 KWr = sqrtSe * term * term;
1793 // dterm/dSe = x^(m-1) * Se^(1/m - 1) (chain rule through y = x^m, x = 1 - Se^(1/m))
1794 const double Dterm_DSe = pow(xStar, m_vg - 1.0) * pow(SeStar, 1.0 / m_vg - 1.0);
1795 DKWr_DSe = (0.5 / sqrtSe_safe) * term * term + 2.0 * sqrtSe * term * Dterm_DSe;
1796}
1797
1798inline void vgm_kr_nonwetting_from_Se(const double Se_w,
1799 const double /*alpha*/,
1800 const double n_vg,
1801 double &KNr,
1802 double &DKNr_DSe,
1803 const double Se_trap = 1.0)
1804{
1805 // Gas-only residual trapping: see bc_kr_nonwetting_from_Se. Remap the
1806 // wetting Se_w over the mobile range [0,Se_trap]; k_rn=0 for S_g<=S_gr.
1807 // Se_trap=1 -> original. DKNr_DSe is d k_rn/d Se_w (1/Se_trap folded in).
1808 const double inv_trap = 1.0 / Se_trap;
1809 const double Se = Se_w * inv_trap;
1810 const double m_vg = 1.0 - 1.0 / n_vg;
1811 if (Se >= 1.0) {
1812 KNr = 0.0;
1813 DKNr_DSe = 0.0;
1814 return;
1815 }
1816 if (Se <= 0.0) {
1817 // S_n = 1 -> k_rn = 1 in the limit; derivative degenerate.
1818 KNr = 1.0;
1819 DKNr_DSe = 0.0;
1820 return;
1821 }
1822 const double SeStar = (Se < 1.0e-12) ? 1.0e-12 : Se;
1823 const double SeInvM = pow(SeStar, 1.0 / m_vg);
1824 const double x_ = 1.0 - SeInvM;
1825 const double xStar = (x_ < 1.0e-12) ? 1.0e-12 : x_;
1826 // Mualem k_rn = sqrt(1 - Se) * (1 - Se^(1/m))^(2m)
1827 const double y2m = pow(xStar, 2.0 * m_vg);
1828 const double oneMinusSe = 1.0 - Se;
1829 const double oneMinusSeStar = (oneMinusSe < 1.0e-12) ? 1.0e-12 : oneMinusSe;
1830 const double sqrt_oms = sqrt(oneMinusSeStar);
1831 KNr = sqrt_oms * y2m;
1832 // d(y^(2m))/dSe = 2m * x^(2m - 1) * dx/dSe = -2 * x^(2m - 1) * Se^(1/m - 1)
1833 const double Dy2m_DSe = -2.0 * pow(xStar, 2.0 * m_vg - 1.0) * pow(SeStar, 1.0 / m_vg - 1.0);
1834 DKNr_DSe = ((-0.5 / sqrt_oms) * y2m + sqrt_oms * Dy2m_DSe) * inv_trap;
1835}
1836
1837inline void bc_wetting_from_Se(const double Se,
1838 const double /*alpha*/,
1839 const double lam,
1840 const double thetaR,
1841 const double thetaSR,
1842 double &thetaW,
1843 double &DthetaW_DSe,
1844 double &KWr,
1845 double &DKWr_DSe)
1846{
1847 thetaW = thetaR + thetaSR * Se;
1848 DthetaW_DSe = thetaSR;
1849 if (Se >= 1.0) {
1850 // Saturated branch; BC has a one-sided derivative jump here.
1851 KWr = 1.0;
1852 DKWr_DSe = 0.0;
1853 return;
1854 }
1855 if (Se <= 0.0) {
1856 KWr = 0.0;
1857 DKWr_DSe = 0.0;
1858 return;
1859 }
1860 // Burdine k_rw = Se^((2 + 3*lambda)/lambda)
1861 const double exp_w = (2.0 + 3.0 * lam) / lam;
1862 KWr = pow(Se, exp_w);
1863 DKWr_DSe = exp_w * pow(Se, exp_w - 1.0);
1864}
1865
1866inline void bc_kr_nonwetting_from_Se(const double Se_w,
1867 const double /*alpha*/,
1868 const double lam,
1869 double &KNr,
1870 double &DKNr_DSe,
1871 const double Se_trap = 1.0)
1872{
1873 // ---------------------------------------------------------------------
1874 // Gas-only residual trapping (Jun 2026): remap the wetting effective
1875 // saturation Se_w onto [0,1] over the MOBILE gas range Se_w in [0,Se_trap],
1876 // Se_trap = (1 - S_wr - S_gr)/(1 - S_wr) = 1 - S_gr/(1 - S_wr).
1877 // Gas is immobile (k_rn = 0) for Se_w >= Se_trap, i.e. S_g <= S_gr.
1878 // Se_trap = 1.0 recovers the original no-trapping closure. Only k_rn is
1879 // remapped here (gas-only) -- k_rw and p_c keep the drainage Se_w. The
1880 // returned DKNr_DSe is d k_rn / d Se_w (the 1/Se_trap chain factor is folded
1881 // in), so every caller's existing dkrn*dSe_dp chain stays correct unchanged.
1882 const double inv_trap = 1.0 / Se_trap;
1883 const double Se = Se_w * inv_trap;
1884 // ---------------------------------------------------------------------
1885 // Option A regularisation (May 2026): cubic-Hermite endpoint bridge for
1886 // k_rn near the wet endpoint Se = 1.
1887 //
1888 // Burdine k_rn = (1 - Se)^2 * (1 - Se^((2+lambda)/lambda)) has a TRIPLE
1889 // zero at Se = 1 (value, 1st, and 2nd derivative all vanish). Newton
1890 // cannot predict gas-mobile breakthrough from local linearisation: when
1891 // its trial step lands at Se = 1 - epsilon, the residual jumps and the
1892 // step direction was wrong. This is the FluidFlower disk-injection
1893 // breakthrough stall (k_rn turning on at t ~ 47 s under benchmark
1894 // conditions).
1895 //
1896 // Fix: on a narrow window Se in [Se_a, 1] with Se_a = 1 - delta_brn,
1897 // replace BC by a cubic Hermite that matches BC value+slope at Se = Se_a
1898 // and ends at (k_rn = 0, dk_rn/dSe = -s_min_brn) at Se = 1. Bulk physics
1899 // (Se in [0, Se_a]) is pure Brooks-Corey.
1900 //
1901 // Tuning:
1902 // delta_brn = 5e-2: bridge spans 5% of the S_e range
1903 // s_min_brn = 1e-3: |slope| at Se = 1; controls how much "advance
1904 // warning" Newton gets about k_rn turning on. Larger ->
1905 // earlier breakthrough prediction, more distortion in the
1906 // bridge. At Se = 1-delta the bridge value is ~21% above
1907 // pure BC k_rn (negligible compared to absolute magnitudes
1908 // here ~1e-4).
1909 //
1910 // For Se > 1 (Newton overshoot) the value clamps to 0 but the derivative
1911 // continues at -s_min_brn so the chain rule has C^1 continuity across
1912 // the seam.
1913 // ---------------------------------------------------------------------
1914 const double delta_brn = 5.0e-2;
1915 const double s_min_brn = 1.0e-3;
1916 const double Se_a = 1.0 - delta_brn;
1917
1918 if (Se <= 0.0) {
1919 // S_n = 1 - S_wr (gas-saturated): non-wetting endpoint.
1920 KNr = 1.0;
1921 DKNr_DSe = 0.0;
1922 return;
1923 }
1924 if (Se >= 1.0) {
1925 // Beyond the wet endpoint: value clamps to 0, derivative continues at
1926 // -s_min_brn for C^1 consistency with the bridge at Se = 1.
1927 KNr = 0.0;
1928 DKNr_DSe = -s_min_brn * inv_trap;
1929 return;
1930 }
1931
1932 const double exp_n = (2.0 + lam) / lam;
1933
1934 if (Se < Se_a) {
1935 // Bulk Brooks-Corey (Burdine) k_rn = (1 - Se)^2 * (1 - Se^((2+lambda)/lambda))
1936 const double SeExp = pow(Se, exp_n);
1937 const double oneMinusSe = 1.0 - Se;
1938 const double Y = 1.0 - SeExp;
1939 KNr = oneMinusSe * oneMinusSe * Y;
1940 DKNr_DSe = (-2.0 * oneMinusSe * Y
1941 - oneMinusSe * oneMinusSe * exp_n * pow(Se, exp_n - 1.0)) * inv_trap;
1942 return;
1943 }
1944
1945 // Cubic-Hermite bridge on [Se_a, 1].
1946 // Anchors:
1947 // left (t=0): k_rn = fa = k_rn_BC(Se_a), dk/dSe = da = dk_rn_BC/dSe(Se_a)
1948 // right (t=1): k_rn = 0, dk/dSe = -s_min_brn
1949 // Parameterised by t = (Se - Se_a)/delta_brn in [0, 1].
1950 const double SeExp_a = pow(Se_a, exp_n);
1951 const double oneMinusSe_a = 1.0 - Se_a;
1952 const double Y_a = 1.0 - SeExp_a;
1953 const double fa = oneMinusSe_a * oneMinusSe_a * Y_a;
1954 const double da = -2.0 * oneMinusSe_a * Y_a
1955 - oneMinusSe_a * oneMinusSe_a * exp_n * pow(Se_a, exp_n - 1.0);
1956
1957 const double t = (Se - Se_a) / delta_brn;
1958 const double t2 = t * t;
1959 const double t3 = t2 * t;
1960
1961 // Hermite basis (only H00, H10, H11 contribute since fb = 0).
1962 const double H00 = 2.0 * t3 - 3.0 * t2 + 1.0;
1963 const double H10 = t3 - 2.0 * t2 + t;
1964 const double H11 = t3 - t2;
1965 const double dH00 = 6.0 * t2 - 6.0 * t;
1966 const double dH10 = 3.0 * t2 - 4.0 * t + 1.0;
1967 const double dH11 = 3.0 * t2 - 2.0 * t;
1968
1969 KNr = H00 * fa + H10 * delta_brn * da + H11 * delta_brn * (-s_min_brn);
1970 DKNr_DSe = (dH00 * fa + dH10 * delta_brn * da
1971 + dH11 * delta_brn * (-s_min_brn)) / delta_brn * inv_trap;
1972}
1973
1974} // namespace psk
1975} // namespace m_comp_co2
1976} // namespace proteus
1977
1979#endif
Double r
Definition Headers.h:83
Double u
Definition Headers.h:89
Double * Y
Definition Headers.h:48
void piecewiseLinearTableLookup(double x, int nv, int *start, double *y, double *dy, const double *xv, const double *yv)
BCB(const double *rwork, const int *iwork=0)
void calc(const double &Sw)
double lambda
void calc(const double &Sw)
double pd
void setParams(const double *rwork, const int *iwork=0)
BCM(const double *rwork, const int *iwork=0)
void calc(const PskRelation &psk, const DensityRelation &density_w, const DensityRelation &density_n)
FractionalFlowVariables(double muwIn, double munIn)
void calc(const double &Sw)
virtual void setParams(const double *rwork, const int *iwork=0)
PskRelation(const double *rwork, const int *iwork=0)
double dSe_dpsic
double dSe_dSw
double Sw_min
virtual ~PskRelation()
double Sw_max
virtual void setTolerances(const double *rwork_tol)
virtual void calc_from_psic(const double &psicIn)
void calc_Se(const double &Sw)
virtual ~PskSpline()
virtual void calc_from_psic(const double &psicIn)
PskSpline(const double *rworkIn, const int *iworkIn=0)
virtual void calc(const double &Sw)
const double * splineArray
virtual void setParams(const double *rwork, const int *iwork=0)
void calc(const double &Sw)
virtual void calc_from_psic(const double &psicIn)
SimplePSK(const double *rwork, const int *iwork=0)
void calc(const double &Sw)
VGB(const double *rwork, const int *iwork=0)
virtual void setTolerances(const double *rwork_tol)
void calc(const double &Sw)
virtual void calc_from_psic(const double &psicIn)
double eps_small
double sqrt_eps_small
VGM(const VGM &r)
VGM(const double *rwork, const int *iwork=0)
double ns_del
void calc_Se(const double &Sw)
double n
void calc_Se_eps(const double &Se)
double Se_eps_const
double Se_eps
void setParams(const double *rwork, const int *iwork=0)
double alpha
void calc(const double &Sw)
virtual void setTolerances(const double *rwork_tol)
VGMorig(const double *rwork, const int *iwork=0)
double m
#define max(a, b)
#define min(a, b)
Definition jf.h:71
void vgm_kr_nonwetting_from_Se(const double Se_w, const double, const double n_vg, double &KNr, double &DKNr_DSe, const double Se_trap=1.0)
void bc_wetting_from_Se(const double Se, const double, const double lam, const double thetaR, const double thetaSR, double &thetaW, double &DthetaW_DSe, double &KWr, double &DKWr_DSe)
void bc_kr_nonwetting_from_Se(const double Se_w, const double, const double lam, double &KNr, double &DKNr_DSe, const double Se_trap=1.0)
void vgm_pc_from_Se(const double Se, const double alpha, const double n_vg, double &pc, double &Dpc_DSe, double &D2pc_DSe2)
void vgm_wetting_from_Se(const double Se, const double, const double n_vg, const double thetaR, const double thetaSR, double &thetaW, double &DthetaW_DSe, double &KWr, double &DKWr_DSe)
void bc_pc_from_Se(const double Se, const double alpha, const double lam, double &pc, double &Dpc_DSe, double &D2pc_DSe2)
void vgm_invert_analytic(const double m, const double rho, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &u)
void gardner_invert_analytic(const double m, const double rho, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &u)
constexpr double gardner_alphaPsiCMax
void bc_wetting(const double psiC, const double alpha, const double lam, const double thetaR, const double thetaSR, double &thetaW, double &DthetaW_DpsiC, double &KWr, double &DKWr_DpsiC, const bc_kr kr_model=bc_kr::burdine)
double bc_eta(const double lam, const bc_kr kr_model)
void vgm_wetting(const double psiC, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &thetaW, double &DthetaW_DpsiC, double &KWr, double &DKWr_DpsiC)
constexpr double gardner_alphaPsiMax
void bc_invert_newton(const double m, const double rho, const double beta, const double alpha, const double lam, const double thetaR, const double thetaSR, double &u)
void gardner_invert_newton(const double m, const double rho, const double beta, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &u)
void vgm_invert_newton(const double m, const double rho, const double beta, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &u)
void gardner_wetting(const double psiC, const double alpha, const double n_vg, const double thetaR, const double thetaSR, double &thetaW, double &DthetaW_DpsiC, double &KWr, double &DKWr_DpsiC)
void bc_invert_analytic(const double m, const double rho, const double alpha, const double lam, const double thetaR, const double thetaSR, double &u)
Definition ADR.h:19
CompressibleN_FractionalFlowVariables(double muwIn, double munIn)
void calc(const PskRelation &psk, const DensityRelation &density_w, const DensityRelation &density_n)