proteus 1.9.0
C/C++/Fortran libraries
Loading...
Searching...
No Matches
m_comp_co2.h
Go to the documentation of this file.
1// Two-component (H2O/CO2) compositional two-phase flow kernel.
2//
3// Co-authored-by: Claude Opus 5 <noreply@anthropic.com> and abarua_ce
4#ifndef MPHASE_CO2_H
5#define MPHASE_CO2_H
6#include <cmath>
7#include <iostream>
8#include <stdexcept>
9#include <valarray>
10#include "CompKernel.h"
11#include "ModelFactory.h"
13#include "xtensor-python/pyarray.hpp"
14#include "../pskRelations.h"
15// Compositional CO2-brine EOS + analytic (p,z) flash (P3). Standalone headers
16// in global namespace ::m_comp_co2 (distinct from proteus::m_comp_co2 below);
17// call sites use the fully-qualified ::m_comp_co2::flash:: form.
18#include "co2_brine_flash.h"
19#define nnz nSpace
20
21namespace py = pybind11;
22#define POWER_SMOOTHNESS_INDICATOR 2
23#define IS_BETAij_ONE 0
24#define GLOBAL_FCT 0
25namespace proteus
26{
27enum class STABILIZATION : int {
32};
33
34namespace m_comp_co2
35{
36//cek todo: revisit entry for mass transport form
37// Power entropy //
38inline double ENTROPY(const double &phi, const double &phiL, const double &phiR)
39{
40 return 1. / 2. * std::pow(fabs(phi), 2.);
41}
42inline double DENTROPY(const double &phi, const double &phiL, const double &phiR)
43{
44 return fabs(phi) * (phi >= 0 ? 1 : -1);
45}
46// Log entropy // for level set from 0 to 1
47inline double ENTROPY_LOG(const double &phi, const double &phiL, const double &phiR)
48{
49 return std::log(fabs((phi - phiL) * (phiR - phi)) + 1E-14);
50}
51inline double DENTROPY_LOG(const double &phi, const double &phiL, const double &phiR)
52{
53 return (phiL + phiR - 2 * phi) * ((phi - phiL) * (phiR - phi) >= 0 ? 1 : -1) / (fabs((phi - phiL) * (phiR - phi)) + 1E-14);
54}
55} // namespace m_comp_co2
56} // namespace proteus
57namespace proteus
58{
59namespace m_comp_co2
60{
62 //The base class defining the interface
63public:
64 virtual ~M_comp_co2_base() { double anb_seepage_flux = 1e-16; }
65 virtual void calculateResidual(arguments_dict &args) = 0;
66 virtual void calculateJacobian(arguments_dict &args) = 0;
67 virtual void invert(arguments_dict &args) = 0;
68 virtual void FCTStep(arguments_dict &args) = 0;
69 virtual void kth_FCT_step(arguments_dict &args) = 0;
71 virtual void calculateMassMatrix(arguments_dict &args) = 0;
72 virtual void dissolutionFlash(arguments_dict &args) = 0;
73 virtual void calculateFlashFields(arguments_dict &args) = 0;
74};
75
76template <class CompKernelType, int nSpace, int nQuadraturePoints_element, int nDOF_mesh_trial_element, int nDOF_trial_element, int nDOF_test_element, int nQuadraturePoints_elementBoundary>
78public:
80 CompKernelType ck;
81 std::vector<double> rho_dof_member;
82 std::vector<double> rho_n_phi_dof_member;
84 // Compositional (p,z) state for the flash. Isothermal / fixed-salinity per
85 // solve. T_C_member is now wired through argsDict["T_C"] and set (like
86 // PSK_TYPE_member) at every top-level entry point from Coefficients.T_C, so
87 // temperature can be changed from the input deck without recompiling. The
88 // 20.0 default is only the pre-first-entry fallback. m_NaCl is still fixed
89 // (SP2005 salting-out is TODO); wire it the same way if salinity is needed.
90 double T_C_member = 20.0; // temperature [degC] (default; overwritten from argsDict["T_C"])
91 double m_NaCl_member = 0.0; // salinity [mol/kg] (SP2005 salting-out is TODO)
92 bool immiscible_member = false;
93 M_comp_co2() : nDOF_test_X_trial_element(nDOF_test_element * nDOF_trial_element), ck() { }
94 //
95 // Closure functions vgm_/bc_*_from_Se expect wetting effective saturation
96 // S_e = (S_w - S_wr)/(1 - S_wr) = (1 - u_n - S_wr)/(1 - S_wr).
97 // dS_e/du_n = -1/(1 - S_wr) (negative); the sign flip propagates through
98 // every chain rule (k_rw, theta_w, p_c) when caller uses dSe_du_n.
99 inline void evaluateCoefficients_from_Se(const int rowptr[nSpace], const int colind[nnz],
100 const double rho0, const double rho_transport, const double beta,
101 const double gravity[nSpace],
102 const double alpha, const double n_vg,
103 const double thetaR, const double thetaSR,
104 const double KWs[nnz],
105 const double &u_w, const double &u_n,
106 double &m, double &dm_du_w, double &dm_du_n,
107 double f[nSpace], double df_du_w[nSpace], double df_du_n[nSpace],
108 double a[nnz], double da_du_w[nnz], double da_du_n[nnz],
109 double as[nnz],
110 double &kr, double &dkr_du_w, double &dkr_du_n,
111 double &thetaW_out)
112 {
113 const double phi = thetaR + thetaSR; // == thetaS
114 const double S_wr = thetaR / phi; // residual S_w
115 const double one_m_Sr = 1.0 - S_wr; // = thetaSR/phi
116 // Se in wetting form, expressed in terms of u_n = S_n.
117 const double Se_raw = (1.0 - u_n - S_wr) / one_m_Sr;
118 // at the clips Se is held constant, so dSe/du_n=0.
119 // Without this, the closure's DthetaW_DSe (returned regardless of
120 // clipping) gets multiplied by +-1/(1-S_wr) and produces a non-zero (0,1)
121 // mass Jacobian entry in the infeasible-u_n region, which tricks Newton
122 // into overshooting further past saturation/residual.
123 double Se, dSe_du_n;
124 if (Se_raw <= 0.0) { Se = 0.0; dSe_du_n = 0.0; }
125 else if (Se_raw >= 1.0) { Se = 1.0; dSe_du_n = 0.0; }
126 else { Se = Se_raw; dSe_du_n = -1.0 / one_m_Sr; }
127
128 double thetaW, DthetaW_DSe, KWr, DKWr_DSe;
129 if (PSK_TYPE_member == 1) {
131 Se, alpha, n_vg, thetaR, thetaSR,
132 thetaW, DthetaW_DSe, KWr, DKWr_DSe);
133 } else {
135 Se, alpha, n_vg, thetaR, thetaSR,
136 thetaW, DthetaW_DSe, KWr, DKWr_DSe);
137 }
138 thetaW_out = thetaW;
139 // Density: rho_w(p_w) = rho_transport * exp(beta * p_w); beta in 1/Pa.
140 const double rhom = rho_transport * exp(beta * u_w);
141 const double drhom = beta * rhom;
142 // component-0 (H2O) accumulation, compositional (p,z) form:
143 // m_0 = phi * N * (1 - z), N = rho_g*S_g + rho_a*(1-S_g)
144 // u_w = p [Pa], u_n = z [-]. Flash gives S_g, rho_a, rho_g + (p,z) derivs.
145 // NOTE (P3c complete): the phase-based flux coeffs (f, kr) computed below are
146 // NO LONGER used for the H2O residual -- calculateResidual assembles the
147 // compositional flux F_0 directly. `a` (and rhom/thetaW) is retained only
148 // for the water-phase Darcy velocity projection (q_velocity) + diagnostics.
149 {
150 const double z_cl = fmin(fmax(u_n, 1.0e-8), 1.0 - 1.0e-8);
151 const double p_cl = fmax(u_w, 1.0e2);
154 const double Sa = 1.0 - fs.S_g;
155 const double N = fs.rho_g*fs.S_g + fs.rho_a*Sa;
156 const double dN_dp = fs.drho_g_dp*fs.S_g + fs.rho_g*fs.dS_g_dp
157 + fs.drho_a_dp*Sa - fs.rho_a*fs.dS_g_dp;
158 const double dN_dz = fs.drho_g_dz*fs.S_g + fs.rho_g*fs.dS_g_dz
159 + fs.drho_a_dz*Sa - fs.rho_a*fs.dS_g_dz;
160 m = phi * N * (1.0 - z_cl);
161 dm_du_w = phi * (1.0 - z_cl) * dN_dp;
162 dm_du_n = phi * ((1.0 - z_cl) * dN_dz - N);
163 }
164 // Chain-rule factor for k_rw and downstream Se-derivatives.
165 const double DKWr_Du_n = DKWr_DSe * dSe_du_n;
166 for (int I = 0; I < nSpace; I++) {
167 f[I] = 0.0;
168 df_du_w[I] = 0.0;
169 df_du_n[I] = 0.0;
170 for (int ii = rowptr[I]; ii < rowptr[I + 1]; ii++) {
171 // Diffusion tensor a_w = rho_w * k_rw * (K/mu_w).
172 a[ii] = rhom * KWr * KWs[ii];
173 da_du_w[ii] = drhom * KWr * KWs[ii];
174 da_du_n[ii] = rhom * DKWr_Du_n * KWs[ii];
175 // Gravity flux f_w = rho_w^2 * k_rw * (K/mu_w) * g (pressure form;
176 // no /rho0 factor).
177 f[I] += rhom * rhom * KWr * KWs[ii] * gravity[colind[ii]];
178 df_du_w[I] += 2.0 * drhom * rhom * KWr * KWs[ii] * gravity[colind[ii]];
179 df_du_n[I] += rhom * rhom * DKWr_Du_n * KWs[ii] * gravity[colind[ii]];
180 as[ii] = rhom * KWs[ii];
181 kr = KWr;
182 dkr_du_w = 0.0; // k_rw depends on S_w (= 1-u_n) only
183 dkr_du_n = DKWr_Du_n;
184 }
185 }
186 }
187
188 // ---------------------------------------------------------------------------
189 // Component-1 (CO2) per-node closure -- the symmetric analog of
190 // evaluateCoefficients_from_Se for the gas/CO2 equation. Where the comp-0
191 // evaluator returns the wetting accumulation + tensor (a,f,kr), this returns
192 // the flash+psk PRIMITIVES that BOTH compositional CO2 flux forms are built
193 // from, all with analytic d/d{p,z}:
194 // * QP / tensor form (calculateResidual STAB=0, calculateJacobian):
195 // F_1 = rho_g*Y*u_g + rho_a*X*u_a,
196 // u_a = -(krw*KWs)(grad p - rho_a_mass g),
197 // u_g = -(krn*KWs/mu_n)(grad p + pcp*grad S_a - rho_g_mass g).
198 // * edge / scalar-mobility form (calculateResidual_entropy_viscosity P2):
199 // F_1 = tau*lam_g_up*gate*dPhi_g + tau*lam_a_up*dPhi_a,
200 // lam_g = (1/mu_n)*rho_g*Y*krn, lam_a = rho_a*X*krw.
201 // Conventions match the verified inline kernels: krn already carries krn_end
202 // (so the edge mobility uses cg = 1/mu_n); krw is the bare wetting relperm;
203 // KWs = K/mu_w is applied by the caller; gas molar density rho_g is treated
204 // as z-independent in rho_g_mass (drho_g/dz ~ 0, as in eftest / P2).
205 // Primary vars u_w = p [Pa], u_n = z [-]; PSK_TYPE_member / T_C_member /
206 // m_NaCl_member are read from the class state (set at every entry point).
207 // FD-verified standalone in comp1_closure_test.cpp; the lam/pc/rgm/ram values
208 // match the eftest-verified CO2 closure (COMP_CO2=true) to round-off.
210 double S_g, dS_g_dp, dS_g_dz; // flash gas saturation (gate + grad S_a)
211 double N, m, dm_dp, dm_dz; // total molar density N; accumulation m_1 = phi*N*z
212 double krw, dkrw_dp, dkrw_dz; // wetting relperm (aqueous Darcy)
213 double krn, dkrn_dp, dkrn_dz; // nonwetting relperm * krn_end (gas Darcy)
214 double pc, dpc_dp, dpc_dz, pcp; // capillary pressure; pcp = dp_c/dS_a (QP cross term)
215 double rho_g, rho_a, Y, X; // flash phase props (QP weights)
218 double rgm, drgm_dp, drgm_dz; // rho_g_mass = rho_g * Mbar_g (gravity)
219 double ram, dram_dp, dram_dz; // rho_a_mass = rho_a * Mbar_a (gravity)
220 double lam_g, dlam_g_dp, dlam_g_dz; // gas molar mobility (1/mu_n)*rho_g*Y*krn (edge)
221 double lam_a, dlam_a_dp, dlam_a_dz; // aqueous molar mobility rho_a*X*krw (edge)
222 };
223
224 inline Comp1Closure evaluateCoefficients_comp1(const double alpha, const double n_vg,
225 const double thetaR, const double thetaSR,
226 const double krn_end, const double mu_n,
227 const double u_w, const double u_n)
228 {
229 Comp1Closure o;
230 const double phi = thetaR + thetaSR; // == thetaS
231 const double S_wr = thetaR / phi;
232 const double one_m_Sr = 1.0 - S_wr;
233 const double cg = 1.0 / mu_n; // krn_end folded into krn below
235 const double z_cl = fmin(fmax(u_n, 1.0e-8), 1.0 - 1.0e-8);
236 const double p_cl = fmax(u_w, 1.0e2);
239 o.S_g = f.S_g; o.dS_g_dp = f.dS_g_dp; o.dS_g_dz = f.dS_g_dz;
240 o.rho_g = f.rho_g; o.rho_a = f.rho_a; o.Y = f.Y; o.X = f.X;
241 o.drho_g_dp = f.drho_g_dp; o.drho_g_dz = f.drho_g_dz;
242 o.drho_a_dp = f.drho_a_dp; o.drho_a_dz = f.drho_a_dz;
243 o.dY_dp = f.dY_dp; o.dY_dz = f.dY_dz; o.dX_dp = f.dX_dp; o.dX_dz = f.dX_dz;
244 // accumulation m_1 = phi*N*z, N = rho_g*S_g + rho_a*S_a.
245 const double Sa = 1.0 - f.S_g;
246 o.N = f.rho_g*f.S_g + f.rho_a*Sa;
247 const double dN_dp = f.drho_g_dp*f.S_g + f.rho_g*f.dS_g_dp + f.drho_a_dp*Sa - f.rho_a*f.dS_g_dp;
248 const double dN_dz = f.drho_g_dz*f.S_g + f.rho_g*f.dS_g_dz + f.drho_a_dz*Sa - f.rho_a*f.dS_g_dz;
249 o.m = phi * o.N * z_cl;
250 o.dm_dp = phi * dN_dp * z_cl;
251 o.dm_dz = phi * (dN_dz * z_cl + o.N); // d(phi*N*z)/dz
252 // wetting effective saturation from S_a; clipped derivatives.
253 const double Se_raw = (Sa - S_wr) / one_m_Sr;
254 double Se, dSe_dp, dSe_dz;
255 if (Se_raw <= 0.0) { Se = 0.0; dSe_dp = 0.0; dSe_dz = 0.0; }
256 else if (Se_raw >= 1.0) { Se = 1.0; dSe_dp = 0.0; dSe_dz = 0.0; }
257 else { Se = Se_raw; dSe_dp = -f.dS_g_dp/one_m_Sr; dSe_dz = -f.dS_g_dz/one_m_Sr; }
258 double krn=0,dkrn=0,krw=0,dkrw=0,thW=0,DthW=0,pc=0,dpc_dSe=0,d2pc=0;
259 if (PSK_TYPE_member == 1) {
260 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se, alpha, n_vg, krn, dkrn);
261 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se, alpha, n_vg, thetaR, thetaSR, thW, DthW, krw, dkrw);
262 proteus::m_comp_co2::psk::bc_pc_from_Se(Se, alpha, n_vg, pc, dpc_dSe, d2pc);
263 } else {
264 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se, alpha, n_vg, krn, dkrn);
265 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se, alpha, n_vg, thetaR, thetaSR, thW, DthW, krw, dkrw);
266 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se, alpha, n_vg, pc, dpc_dSe, d2pc);
267 }
268 krn *= krn_end; // fold krn_end into the relperm
269 dkrn *= krn_end;
270 o.krw = krw; o.dkrw_dp = dkrw*dSe_dp; o.dkrw_dz = dkrw*dSe_dz;
271 o.krn = krn; o.dkrn_dp = dkrn*dSe_dp; o.dkrn_dz = dkrn*dSe_dz;
272 o.pc = pc; o.dpc_dp = dpc_dSe*dSe_dp; o.dpc_dz = dpc_dSe*dSe_dz;
273 o.pcp = dpc_dSe / one_m_Sr; // dp_c/dS_a (QP capillary cross term)
274 const double Mbar_g = f.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-f.Y)*::m_comp_co2::eos::M_H2O_KG;
275 const double Mbar_a = f.X*::m_comp_co2::eos::M_CO2_KG + (1.0-f.X)*::m_comp_co2::eos::M_H2O_KG;
276 o.rgm = f.rho_g*Mbar_g; o.ram = f.rho_a*Mbar_a;
277 o.drgm_dp = f.drho_g_dp*Mbar_g + f.rho_g*f.dY_dp*dMm;
278 o.drgm_dz = f.drho_g_dz*Mbar_g + f.rho_g*f.dY_dz*dMm; // drho_g/dz ~ 0
279 o.dram_dp = f.drho_a_dp*Mbar_a + f.rho_a*f.dX_dp*dMm;
280 o.dram_dz = f.drho_a_dz*Mbar_a + f.rho_a*f.dX_dz*dMm;
281 // gas molar mobility lam_g = cg*rho_g*Y*krn (krn already carries krn_end).
282 o.lam_g = cg*f.rho_g*f.Y*o.krn;
283 o.dlam_g_dp = cg*(f.drho_g_dp*f.Y*o.krn + f.rho_g*f.dY_dp*o.krn + f.rho_g*f.Y*o.dkrn_dp);
284 o.dlam_g_dz = cg*(f.drho_g_dz*f.Y*o.krn + f.rho_g*f.dY_dz*o.krn + f.rho_g*f.Y*o.dkrn_dz);
285 // aqueous molar mobility lam_a = rho_a*X*krw.
286 o.lam_a = f.rho_a*f.X*o.krw;
287 o.dlam_a_dp = f.drho_a_dp*f.X*o.krw + f.rho_a*f.dX_dp*o.krw + f.rho_a*f.X*o.dkrw_dp;
288 o.dlam_a_dz = f.drho_a_dz*f.X*o.krw + f.rho_a*f.dX_dz*o.krw + f.rho_a*f.X*o.dkrw_dz;
289 return o;
290 }
291
292 // Inversion routines (vgm_invert_analytic / vgm_invert_newton) are not used by
293 // this module; the maintained copies live in proteus/pskRelations.h.
294
295 inline void calculateCFL(const double &elementDiameter, const double df[nSpace], double &cfl)
296 {
297 double h, nrm_v;
298 h = elementDiameter;
299 nrm_v = 0.0;
300 for (int I = 0; I < nSpace; I++) nrm_v += df[I] * df[I];
301 nrm_v = sqrt(nrm_v);
302 cfl = nrm_v / h;
303 }
304
305 inline void calculateSubgridError_tau(const double &elementDiameter, const double &dmt, const double dH[nSpace], double &cfl, double &tau)
306 {
307 double h, nrm_v, oneByAbsdt;
308 h = elementDiameter;
309 nrm_v = 0.0;
310 for (int I = 0; I < nSpace; I++) nrm_v += dH[I] * dH[I];
311 nrm_v = sqrt(nrm_v);
312 cfl = nrm_v / h;
313 oneByAbsdt = fabs(dmt);
314 tau = 1.0 / (2.0 * nrm_v / h + oneByAbsdt + 1.0e-8);
315 }
316
317 inline void calculateSubgridError_tau(const double &Ct_sge, const double G[nSpace * nSpace], const double &A0, const double Ai[nSpace], double &tau_v, double &cfl)
318 {
319 double v_d_Gv = 0.0;
320 for (int I = 0; I < nSpace; I++)
321 for (int J = 0; J < nSpace; J++) v_d_Gv += Ai[I] * G[I * nSpace + J] * Ai[J];
322 tau_v = 1.0 / sqrt(Ct_sge * A0 * A0 + v_d_Gv);
323 }
324
325 inline void calculateNumericalDiffusion(const double &shockCapturingDiffusion, const double &elementDiameter, const double &strong_residual, const double grad_u[nSpace], double &numDiff)
326 {
327 double h, num, den, n_grad_u;
328 h = elementDiameter;
329 n_grad_u = 0.0;
330 for (int I = 0; I < nSpace; I++) n_grad_u += grad_u[I] * grad_u[I];
331 num = shockCapturingDiffusion * 0.5 * h * fabs(strong_residual);
332 den = sqrt(n_grad_u) + 1.0e-8;
333 numDiff = num / den;
334 }
335
336 inline void exteriorNumericalFlux(const double &bc_flux, int rowptr[nSpace], int colind[nnz], int isSeepageFace, int &isDOFBoundary, double n[nSpace], double bc_u, double K[nnz], double grad_psi[nSpace], double u, double K_rho_g[nSpace], double penalty, double &flux)
337 {
338 double v_I, bc_u_seepage = 0.0;
339 if (isSeepageFace || isDOFBoundary) {
340 flux = 0.0;
341 for (int I = 0; I < nSpace; I++) {
342 //gravity
343 v_I = K_rho_g[I];
344 //pressure head
345 for (int m = rowptr[I]; m < rowptr[I + 1]; m++) { v_I -= K[m] * grad_psi[colind[m]]; }
346 flux += v_I * n[I];
347 }
348 if (isSeepageFace) bc_u = bc_u_seepage;
349 flux += penalty * (u - bc_u);
350 //flux -= penalty * bc_u;
351 if (isSeepageFace) {
352 if (flux > 0.0) {
353 isDOFBoundary = 1;
354 bc_u = bc_u_seepage;
355 } else {
356 isDOFBoundary = 0;
357 flux = 0.0;
358 }
359 }
360 } else flux = bc_flux;
361 }
362
363 void exteriorNumericalFluxJacobian(const int rowptr[nSpace], const int colind[nnz], const int isDOFBoundary, const double n[nSpace], const double K[nnz], const double dK[nnz], const double grad_psi[nSpace], const double grad_v[nSpace], const double dK_rho_g[nSpace], const double v, const double penalty, double &fluxJacobian)
364 {
365 if (isDOFBoundary) {
366 fluxJacobian = 0.0;
367 for (int I = 0; I < nSpace; I++) {
368 //gravity
369 fluxJacobian += dK_rho_g[I] * v * n[I];
370 //pressure head
371 for (int m = rowptr[I]; m < rowptr[I + 1]; m++) { fluxJacobian -= (K[m] * grad_v[colind[m]] + dK[m] * v * grad_psi[colind[m]]) * n[I]; }
372 }
373 //Dirichlet penalty
374 fluxJacobian += penalty * v;
375 } else fluxJacobian = 0.0;
376 }
377
378inline void exteriorNumericalFlux2(const double &bc_flux, int rowptr[nSpace], int colind[nnz], int isSeepageFace, int &isDOFBoundary, double n[nSpace], double bc_u, double K[nnz], double grad_psi[nSpace], double u, double K_rho_g[nSpace], double penalty, double &flux, double &bflux)
379 {
380 double v_I, bc_u_seepage = 0.0;
381 if (isSeepageFace || isDOFBoundary) {
382 flux = 0.0;
383 bflux = 0.0;
384 for (int I = 0; I < nSpace; I++) {
385 //gravity
386 v_I = K_rho_g[I];
387 //pressure head
388 for (int m = rowptr[I]; m < rowptr[I + 1]; m++) { v_I -= K[m] * grad_psi[colind[m]]; }
389 flux += v_I * n[I];
390 }
391 if (isSeepageFace) bc_u = bc_u_seepage;
392 flux += penalty * (u - bc_u);
393 bflux += penalty * (u - bc_u);
394 if (isSeepageFace) {
395 if (flux > 0.0) {
396 isDOFBoundary = 1;
397 } else {
398 isDOFBoundary = 0;
399 flux = 0.0;
400 bflux = 0.0;
401 }
402 }
403 } else {
404 flux = bc_flux;
405 bflux = bc_flux;
406 }
407 }
408
409 void exteriorNumericalFluxJacobian2(const int rowptr[nSpace], const int colind[nnz], const int isDOFBoundary, const double n[nSpace], const double Ks[nnz], const double K[nnz], const double dK[nnz], const double grad_psi[nSpace], const double grad_v[nSpace], const double dK_rho_g[nSpace], const double v, const double penalty, double &fluxJacobian, double &bfluxJacobian)
410 {
411 if (isDOFBoundary) {
412 fluxJacobian = 0.0;
413 bfluxJacobian = 0.0;
414 for (int I = 0; I < nSpace; I++) {
415 for (int m = rowptr[I]; m < rowptr[I + 1]; m++) {
416 fluxJacobian -= Ks[m] * grad_v[colind[m]] * n[I];
417 }
418 }
419 //Dirichlet penalty
420 bfluxJacobian = penalty * v;
421 } else {
422 fluxJacobian = 0.0;
423 bfluxJacobian = 0.0;
424 }
425 }
426
427 double seepagefluxcalculator(double anb_seepage_flux, int isSeepageFace, double dS, double flux_ext)
428 {
429 if (isSeepageFace) { anb_seepage_flux += flux_ext * dS; }
430 return anb_seepage_flux;
431 }
432
434 {
435 xt::pyarray<double> &mesh_trial_ref = args.array<double>("mesh_trial_ref");
436 xt::pyarray<double> &mesh_grad_trial_ref = args.array<double>("mesh_grad_trial_ref");
437 xt::pyarray<double> &mesh_dof = args.array<double>("mesh_dof");
438 xt::pyarray<double> &mesh_velocity_dof = args.array<double>("mesh_velocity_dof");
439 double MOVING_DOMAIN = args.scalar<double>("MOVING_DOMAIN");
440 xt::pyarray<int> &mesh_l2g = args.array<int>("mesh_l2g");
441 xt::pyarray<double> &dV_ref = args.array<double>("dV_ref");
442 xt::pyarray<double> &u_trial_ref = args.array<double>("u_trial_ref");
443 xt::pyarray<double> &u_grad_trial_ref = args.array<double>("u_grad_trial_ref");
444 xt::pyarray<double> &u_test_ref = args.array<double>("u_test_ref");
445 xt::pyarray<double> &u_grad_test_ref = args.array<double>("u_grad_test_ref");
446 xt::pyarray<double> &mesh_trial_trace_ref = args.array<double>("mesh_trial_trace_ref");
447 xt::pyarray<double> &mesh_grad_trial_trace_ref = args.array<double>("mesh_grad_trial_trace_ref");
448 xt::pyarray<double> &dS_ref = args.array<double>("dS_ref");
449 xt::pyarray<double> &u_trial_trace_ref = args.array<double>("u_trial_trace_ref");
450 xt::pyarray<double> &u_grad_trial_trace_ref = args.array<double>("u_grad_trial_trace_ref");
451 xt::pyarray<double> &u_test_trace_ref = args.array<double>("u_test_trace_ref");
452 xt::pyarray<double> &u_grad_test_trace_ref = args.array<double>("u_grad_test_trace_ref");
453 xt::pyarray<double> &normal_ref = args.array<double>("normal_ref");
454 xt::pyarray<double> &boundaryJac_ref = args.array<double>("boundaryJac_ref");
455 int nElements_global = args.scalar<int>("nElements_global");
456 xt::pyarray<double> &ebqe_penalty_ext = args.array<double>("ebqe_penalty_ext");
457 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
458 xt::pyarray<int> &isSeepageFace = args.array<int>("isSeepageFace");
459 xt::pyarray<int> &a_rowptr = args.array<int>("a_rowptr");
460 xt::pyarray<int> &a_colind = args.array<int>("a_colind");
461 double rho = args.scalar<double>("rho");
462 double beta = args.scalar<double>("beta");
463
465 xt::pyarray<double> &q_rho = args.array<double>("q_rho");
466 xt::pyarray<double> &ebqe_rho = args.array<double>("ebqe_rho");
467
468 xt::pyarray<double> &gravity = args.array<double>("gravity");
469 xt::pyarray<double> &alpha = args.array<double>("alpha");
470 xt::pyarray<double> &n = args.array<double>("n");
471 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
472 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
473 xt::pyarray<double> &KWs = args.array<double>("KWs");
474 xt::pyarray<double> &krn_end = args.array<double>("krn_end");
475 xt::pyarray<double> &S_gr = args.array<double>("S_gr");
476 double mu_n = args.scalar<double>("mu_n");
477 double useMetrics = args.scalar<double>("useMetrics");
478 double alphaBDF = args.scalar<double>("alphaBDF");
479 int lag_shockCapturing = args.scalar<int>("lag_shockCapturing");
480 double shockCapturingDiffusion = args.scalar<double>("shockCapturingDiffusion");
481 double sc_uref = args.scalar<double>("sc_uref");
482 double sc_alpha = args.scalar<double>("sc_alpha");
483 xt::pyarray<int> &u_l2g = args.array<int>("u_l2g");
484 xt::pyarray<double> &elementDiameter = args.array<double>("elementDiameter");
485 xt::pyarray<double> &u_dof = args.array<double>("u_dof");
486 xt::pyarray<double> &u_dof_old = args.array<double>("u_dof_old");
487 xt::pyarray<double> &velocity = args.array<double>("velocity");
488 xt::pyarray<double> &q_m = args.array<double>("q_m");
489 xt::pyarray<double> &q_theta = args.array<double>("q_theta");
490 xt::pyarray<double> &q_u = args.array<double>("q_u");
491 xt::pyarray<double> &q_dV = args.array<double>("q_dV");
492 xt::pyarray<double> &q_m_betaBDF = args.array<double>("q_m_betaBDF");
493 xt::pyarray<double> &cfl = args.array<double>("cfl");
494 xt::pyarray<double> &q_numDiff_u = args.array<double>("q_numDiff_u");
495 xt::pyarray<double> &q_numDiff_u_last = args.array<double>("q_numDiff_u_last");
496 int offset_u = args.scalar<int>("offset_u");
497 int stride_u = args.scalar<int>("stride_u");
498 // component-1 (S_n) mass equation args.
499 // Used in the dedicated component-1 element loop appended at the end
500 // of this function. Not consumed by the existing component-0 logic.
501 const double dt = args.scalar<double>("dt");
502 xt::pyarray<double> &u_dof_n = args.array<double>("u_dof_n");
503 xt::pyarray<double> &u_dof_n_old = args.array<double>("u_dof_n_old");
504 // gas-phase density (linear EOS). rho_n is the reference density and
505 // p_ref_n the reference pressure: rho_n_local(p_n) = rho_n*p_n/p_ref_n
506 // when p_ref_n > 0; constant rho_n otherwise.
507 const double rho_n = args.scalar<double>("rho_n");
508 const double p_ref_n = args.scalar<double>("p_ref_n");
509 const bool rho_n_compressible = (p_ref_n > 0.0);
510 const double c_n = rho_n_compressible ? (rho_n / p_ref_n) : 0.0;
511 const int offset_n = args.scalar<int>("offset_n");
512 const int stride_n = args.scalar<int>("stride_n");
513 // Stage 3b: gas-side kinetic dissolution sink. R_diss = k_d * S_n *
514 // (1 - S_n) * theta_w * rho_w(c) * (c_sat - c) is subtracted from the
515 // gas-equation residual at each quadrature point. c is read from TADR's
516 // u[0].dof aliased Python-side and passed in as c_dof. k_d=0 disables
517 // the sink (legacy behavior).
518 xt::pyarray<double> &c_dof = args.array<double>("c_dof");
519 const double k_d = args.scalar<double>("k_d");
520 const double c_sat = args.scalar<double>("c_sat");
521 // CO2 injection: per-node source field (built Python-side, schedule-gated).
522 // Applied like R_diss but with opposite sign -- a source, not a sink.
523 // All-zero array when no injection is configured.
524 xt::pyarray<double> &injection_dof = args.array<double>("injection_dof");
525 xt::pyarray<double> &globalResidual = args.array<double>("globalResidual");
526 int nExteriorElementBoundaries_global = args.scalar<int>("nExteriorElementBoundaries_global");
527 xt::pyarray<int> &exteriorElementBoundariesArray = args.array<int>("exteriorElementBoundariesArray");
528 xt::pyarray<int> &elementBoundaryElementsArray = args.array<int>("elementBoundaryElementsArray");
529 xt::pyarray<int> &elementBoundaryLocalElementBoundariesArray = args.array<int>("elementBoundaryLocalElementBoundariesArray");
530 xt::pyarray<double> &ebqe_velocity_ext = args.array<double>("ebqe_velocity_ext");
531 xt::pyarray<int> &isDOFBoundary_u = args.array<int>("isDOFBoundary_u");
532 xt::pyarray<double> &ebqe_bc_u_ext = args.array<double>("ebqe_bc_u_ext");
533 // component-1 (S_n) boundary arrays.
534 xt::pyarray<int> &isDOFBoundary_n = args.array<int>("isDOFBoundary_n");
535 xt::pyarray<double> &ebqe_bc_u_n_ext = args.array<double>("ebqe_bc_u_n_ext");
536 xt::pyarray<int> &isFluxBoundary_u = args.array<int>("isFluxBoundary_u");
537 xt::pyarray<double> &ebqe_bc_flux_ext = args.array<double>("ebqe_bc_flux_ext");
538 xt::pyarray<double> &ebqe_phi = args.array<double>("ebqe_phi");
539 double epsFact = args.scalar<double>("epsFact");
540 xt::pyarray<double> &ebqe_u = args.array<double>("ebqe_u");
541 xt::pyarray<double> &ebqe_theta = args.array<double>("ebqe_theta");
542 xt::pyarray<double> &ebqe_flux = args.array<double>("ebqe_flux");
543 // VMS
544 double VMS = args.scalar<double>("VMS");
545 // PARAMETERS FOR EDGE BASED STABILIZATION
546 double cE = args.scalar<double>("cE");
547 double cK = args.scalar<double>("cK");
548 // PARAMETERS FOR LOG BASED ENTROPY FUNCTION
549 double uL = args.scalar<double>("uL");
550 double uR = args.scalar<double>("uR");
551 // PARAMETERS FOR EDGE VISCOSITY
552 int numDOFs = args.scalar<int>("numDOFs");
553 // numDOFs is the compact component-0 free-DOF count used by the stabilized
554 // DOF loops. Full-matrix slots are recovered from offset/stride-aware CSR
555 // indexing against the interleaved global matrix.
556 int numDOFs_u = args.scalar<int>("numDOFs_u");
557 int NNZ = args.scalar<int>("NNZ");
558 xt::pyarray<int> &csrRowIndeces_DofLoops = args.array<int>("csrRowIndeces_DofLoops");
559 xt::pyarray<int> &csrColumnOffsets_DofLoops = args.array<int>("csrColumnOffsets_DofLoops");
560 xt::pyarray<int> &csrRowIndeces_Full = args.array<int>("csrRowIndeces_Full");
561 xt::pyarray<int> &csrColumnOffsets_Full = args.array<int>("csrColumnOffsets_Full");
562 xt::pyarray<int> &csrRowIndeces_CellLoops = args.array<int>("csrRowIndeces_CellLoops");
563 xt::pyarray<int> &csrColumnOffsets_CellLoops = args.array<int>("csrColumnOffsets_CellLoops");
564 xt::pyarray<int> &csrColumnOffsets_eb_CellLoops = args.array<int>("csrColumnOffsets_eb_CellLoops");
565 // C matrices
566 xt::pyarray<double> &Cx = args.array<double>("Cx");
567 xt::pyarray<double> &Cy = args.array<double>("Cy");
568 xt::pyarray<double> &Cz = args.array<double>("Cz");
569 xt::pyarray<double> &CTx = args.array<double>("CTx");
570 xt::pyarray<double> &CTy = args.array<double>("CTy");
571 xt::pyarray<double> &CTz = args.array<double>("CTz");
572 xt::pyarray<double> &ML = args.array<double>("ML");
573 xt::pyarray<double> &delta_x_ij = args.array<double>("delta_x_ij");
574 // PARAMETERS FOR 1st or 2nd ORDER MPP METHOD
575 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
576 STABILIZATION STABILIZATION_TYPE{static_cast<STABILIZATION>(args.scalar<int>("STABILIZATION_TYPE"))};
577 int ENTROPY_TYPE = args.scalar<int>("ENTROPY_TYPE");
578 // PSK closure selector for evaluateCoefficients (read from argsDict).
579 PSK_TYPE_member = args.scalar<int>("PSK_TYPE");
580 immiscible_member = (args.scalar<int>("immiscible") != 0);
581 T_C_member = args.scalar<double>("T_C"); // temperature [degC] from input
582 // FOR FCT
583 xt::pyarray<double> &dLow = args.array<double>("dLow");
584 xt::pyarray<double> &fluxMatrix = args.array<double>("fluxMatrix");
585 // AUX QUANTITIES OF INTEREST
586 xt::pyarray<double> &quantDOFs = args.array<double>("quantDOFs");
587
588 assert(a_rowptr.data()[nSpace] == nnz);
589 assert(a_rowptr.data()[nSpace] == nSpace);
590 //cek should this be read in?
591 double Ct_sge = 4.0;
592
593 xt::pyarray<double> &anb_seepage_flux_n = args.array<double>("anb_seepage_flux_n");
594
595 xt::pyarray<double> &velocity_couple = args.array<double>("velocity_couple");
596 xt::pyarray<double> &ebqe_velocity_ext_couple = args.array<double>("ebqe_velocity_ext_couple");
597
598 // xt::pyarray<double> &q_x = args.array<double>("q_x");
599 // xt::pyarray<double> &ebqe_x = args.array<double>("ebqe_x");
600
601 //double anb_seepage_flux=0.0;
602 double &anb_seepage_flux(args.scalar<double>("anb_seepage_flux"));
603 xt::pyarray<double> &q_velocity = args.array<double>("q_velocity");
604 anb_seepage_flux = 0.0;
605
606 //loop over elements to compute volume integrals and load them into element and global residual
607 //
608 //eN is the element index
609 //eN_k is the quadrature point index for a scalar
610 //eN_k_nSpace is the quadrature point index for a vector
611 //eN_i is the element test function index
612 //eN_j is the element trial function index
613 //eN_k_j is the quadrature point index for a trial function
614 //eN_k_i is the quadrature point index for a trial function
615 for (int eN = 0; eN < nElements_global; eN++) {
616 //declare local storage for element residual and initialize
617 double elementResidual_u[nDOF_test_element];
618 for (int i = 0; i < nDOF_test_element; i++) { elementResidual_u[i] = 0.0; } //i
619 //loop over quadrature points and compute integrands
620 for (int k = 0; k < nQuadraturePoints_element; k++) {
621 //compute indeces and declare local storage
622 int eN_k = eN * nQuadraturePoints_element + k, eN_k_nSpace = eN_k * nSpace, eN_nDOF_trial_element = eN * nDOF_trial_element;
623 double u = 0.0, grad_u[nSpace], grad_u_old[nSpace], m = 0.0, dm = 0.0, f[nSpace], df[nSpace], a[nnz], da[nnz], as[nnz], m_t = 0.0, dm_t = 0.0, pdeResidual_u = 0.0, Lstar_u[nDOF_test_element], subgridError_u = 0.0, tau = 0.0, tau0 = 0.0, tau1 = 0.0, numDiff0 = 0.0, numDiff1 = 0.0, jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], u_grad_trial[nDOF_trial_element * nSpace], u_test_dV[nDOF_trial_element], u_grad_test_dV[nDOF_test_element * nSpace], dV, x, y, z, xt, yt, zt, G[nSpace * nSpace], G_dd_G, tr_G, norm_Rv;
624 //
625 //compute solution and gradients at quadrature points
626 //
627 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), mesh_grad_trial_ref.data(), jac, jacDet, jacInv, x, y, z);
628 ck.calculateMappingVelocity_element(eN, k, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), xt, yt, zt);
629 //get the physical integration weight
630 dV = fabs(jacDet) * dV_ref.data()[k];
631 q_dV.data()[eN_k] = dV;
632 ck.calculateG(jacInv, G, G_dd_G, tr_G);
633 //get the trial function gradients
634 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace], jacInv, u_grad_trial);
635 //get the solution
636 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_ref.data()[k * nDOF_trial_element], u);
637 //get the solution gradients
638 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial, grad_u);
639 // P3c: gradient of z (comp-1 DOF) at this QP -- the comp-0 component flux
640 // needs grad S_a = -(dSg/dp grad p + dSg/dz grad z).
641 double grad_u_n[nSpace];
642 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial, grad_u_n);
643
644 // //populate q_x
645 // const int eN_k_3d = eN_k * 3;
646 // q_x.data()[eN_k_3d + 0] = x;
647 // q_x.data()[eN_k_3d + 1] = y;
648 // q_x.data()[eN_k_3d + 2] = z;
649
650 //precalculate test function products with integration weights
651 for (int j = 0; j < nDOF_trial_element; j++) {
652 u_test_dV[j] = u_test_ref.data()[k * nDOF_trial_element + j] * dV;
653 for (int I = 0; I < nSpace; I++) {
654 u_grad_test_dV[j * nSpace + I] = u_grad_trial[j * nSpace + I] * dV; //cek warning won't work for Petrov-Galerkin
655 }
656 }
657 //
658 //calculate pde coefficients at quadrature points
659 //
660 double Kr, dKr, thetaW;
661 const double rho_local = q_rho.data()[eN_k];
662 const double rho_velocity = std::fabs(rho_local) > 1.0e-12 ? rho_local : rho;
663 // Cross-derivatives (dm_du_n, df_du_n, da_du_n, dkr_du_n) are unused in
664 // the residual; the (0,1) Jacobian cross-block consumes them elsewhere.
665 double dm_du_n_qp = 0.0, dkr_du_n_qp = 0.0;
666 double df_du_n_qp[nSpace];
667 double da_du_n_qp[nnz];
668 for (int I = 0; I < nSpace; I++) df_du_n_qp[I] = 0.0;
669 for (int ii = 0; ii < nnz; ii++) da_du_n_qp[ii] = 0.0;
670 double u_n_qp = 0.0;
671 ck.valFromDOF(u_dof_n.data(),
672 &u_l2g.data()[eN_nDOF_trial_element],
673 &u_trial_ref.data()[k * nDOF_trial_element], u_n_qp);
674 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_local, beta, gravity.data(),
675 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
676 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
677 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], u, u_n_qp,
678 m, dm, dm_du_n_qp, f, df, df_du_n_qp, a, da, da_du_n_qp,
679 as, Kr, dKr, dkr_du_n_qp, thetaW);
680 q_theta.data()[eN_k] = thetaW;
681
682
683 for (int I = 0; I < nSpace; ++I) {
684 q_velocity.data()[eN_k_nSpace + I] = grad_u[I];
685 }
686 // Darcy Velocity
687 double pressure_gradient[nSpace];
688 for (int J=0; J<nSpace; ++J)
689 pressure_gradient[J] = grad_u[J] - rho_velocity * gravity.data()[J];
690 // q_w = -(a/rho_w) * (grad p_w - rho_w g) = -(k_rw K/mu_w) * (grad p_w - rho_w g).
691 for (int I=0; I<nSpace; ++I) {
692 double acc = 0.0;
693 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I+1]; ++ii) {
694 const int J = a_colind.data()[ii];
695 acc += (a[ii] / rho_velocity) * pressure_gradient[J];
696 }
697 velocity.data()[eN_k_nSpace + I] = -acc;
698 velocity_couple.data()[eN_k_nSpace + I] = -acc ;
699 }
700 //
701 // Compositional H2O accumulation m_0 = phi*N*(1-z), N = rho_g*S_g +
702 // rho_a*S_a (overrides the phase-based m from evaluateCoefficients_from_Se;
703 // (p,z) formulation -- consistent with the (0,0)/(0,1) mass Jacobian).
704 {
705 const int mat0 = elementMaterialTypes.data()[eN];
706 const double phi0_m = thetaR.data()[mat0] + thetaSR.data()[mat0];
707 const double z0_m = fmin(fmax(u_n_qp, 1.0e-8), 1.0 - 1.0e-8);
708 const double p0_m = fmax(u, 1.0e2);
711 const double Nm = fsm.rho_g*fsm.S_g + fsm.rho_a*(1.0 - fsm.S_g);
712 const double dNm_dp = fsm.drho_g_dp*fsm.S_g + fsm.rho_g*fsm.dS_g_dp
713 + fsm.drho_a_dp*(1.0-fsm.S_g) - fsm.rho_a*fsm.dS_g_dp;
714 m = phi0_m * Nm * (1.0 - z0_m);
715 dm = phi0_m * dNm_dp * (1.0 - z0_m); // d m_0/dp (used by bdf/subgrid only)
716 }
717 //
718 //calculate time derivative at quadrature points
719 //
720 ck.bdf(alphaBDF, q_m_betaBDF.data()[eN_k], m, dm, m_t, dm_t);
721 //
722 //calculate subgrid error (strong residual and adjoint)
723 //
724 //calculate strong residual
725 // pdeResidual_u = ck.Mass_strong(m_t) + ck.Advection_strong(df, grad_u);
726 // //calculate adjoint
727 // for (int i = 0; i < nDOF_test_element; i++) {
728 // int i_nSpace = i * nSpace;
729 // Lstar_u[i] = ck.Advection_adjoint(df, &u_grad_test_dV[i_nSpace]);
730 // }
731 // //calculate tau and tau*Res
732 // calculateSubgridError_tau(elementDiameter[eN], dm_t, df, cfl[eN_k], tau0);
733 // calculateSubgridError_tau(Ct_sge, G, dm_t, df, tau1, cfl[eN_k]);
734
735 // tau = useMetrics * tau1 + (1.0 - useMetrics) * tau0;
736
737 // subgridError_u = -tau * pdeResidual_u;
738 // //
739 // //calculate shock capturing diffusion
740 // //
741 // ck.calculateNumericalDiffusion(shockCapturingDiffusion, elementDiameter[eN], pdeResidual_u, grad_u, numDiff0);
742 // ck.calculateNumericalDiffusion(shockCapturingDiffusion, sc_uref, sc_alpha, G, G_dd_G, pdeResidual_u, grad_u, numDiff1);
743 // q_numDiff_u[eN_k] = useMetrics * numDiff1 + (1.0 - useMetrics) * numDiff0;
744 // ===== P3c: component H2O molar flux F_0 = rho_g*(1-Y)*u_g + rho_a*(1-X)*u_a =====
745 // Same two phase Darcy velocities as the comp-1 flux, weighted by the H2O
746 // mole fractions (1-Y) in gas, (1-X) in aqueous. Props from the flash
747 // saturation S_g (S_a = 1 - S_g); psk closures take wetting Se_a0.
748 const int mat_eN0 = elementMaterialTypes.data()[eN];
749 const double alpha_eN0 = alpha.data()[mat_eN0];
750 const double n_vg_eN0 = n.data()[mat_eN0];
751 const double krn_end0 = krn_end.data()[mat_eN0];
752 const double *KWs_eN0 = &KWs.data()[mat_eN0 * nnz];
753 const double phi0 = thetaR.data()[mat_eN0] + thetaSR.data()[mat_eN0];
754 const double S_wr0 = thetaR.data()[mat_eN0] / phi0;
755 const double one_m_Sr0 = 1.0 - S_wr0;
756 const double Se_trap_L771 = 1.0 - S_gr.data()[mat_eN0] / one_m_Sr0; // gas-only residual trapping
757 const double z_cl0 = fmin(fmax(u_n_qp, 1.0e-8), 1.0 - 1.0e-8);
758 const double p_cl0 = fmax(u, 1.0e2);
761 const double Se_a0 = fmin(fmax((1.0 - fs0.S_g - S_wr0) / one_m_Sr0, 0.0), 1.0);
762 double KWr0 = 0.0, DKWr0 = 0.0, thW0 = 0.0, DthW0 = 0.0;
763 double KNr0 = 0.0, DKNr0 = 0.0, pc0 = 0.0, dpc_dSe0 = 0.0, d2pc0 = 0.0;
764 if (PSK_TYPE_member == 1) {
765 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0,
766 thetaR.data()[mat_eN0], thetaSR.data()[mat_eN0], thW0, DthW0, KWr0, DKWr0);
767 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0, KNr0, DKNr0, Se_trap_L771);
768 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_a0, alpha_eN0, n_vg_eN0, pc0, dpc_dSe0, d2pc0);
769 } else {
770 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0,
771 thetaR.data()[mat_eN0], thetaSR.data()[mat_eN0], thW0, DthW0, KWr0, DKWr0);
772 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0, KNr0, DKNr0, Se_trap_L771);
773 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_a0, alpha_eN0, n_vg_eN0, pc0, dpc_dSe0, d2pc0);
774 }
775 KNr0 *= krn_end0;
776 const double pcp0 = dpc_dSe0 / one_m_Sr0;
777 const double Mbar_g0 = fs0.Y*::m_comp_co2::eos::M_CO2_KG + (1.0 - fs0.Y)*::m_comp_co2::eos::M_H2O_KG;
778 const double Mbar_a0 = fs0.X*::m_comp_co2::eos::M_CO2_KG + (1.0 - fs0.X)*::m_comp_co2::eos::M_H2O_KG;
779 const double rho_g_mass0 = fs0.rho_g*Mbar_g0;
780 const double rho_a_mass0 = fs0.rho_a*Mbar_a0;
781 double F0[nSpace];
782 for (int I = 0; I < nSpace; I++) {
783 double ua = 0.0, ug = 0.0;
784 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
785 const int J = a_colind.data()[ii];
786 const double gradSa_J = -(fs0.dS_g_dp*grad_u[J] + fs0.dS_g_dz*grad_u_n[J]);
787 const double gp_a = grad_u[J] - rho_a_mass0*gravity.data()[J];
788 const double gp_g = grad_u[J] + pcp0*gradSa_J - rho_g_mass0*gravity.data()[J];
789 ua -= (KWr0*KWs_eN0[ii]) * gp_a;
790 ug -= (KNr0*KWs_eN0[ii]/mu_n) * gp_g;
791 }
792 F0[I] = fs0.rho_g*(1.0 - fs0.Y)*ug + fs0.rho_a*(1.0 - fs0.X)*ua;
793 }
794 //
795 //update element residual
796 //
797 for (int i = 0; i < nDOF_test_element; i++) {
798 int eN_k_i = eN_k * nDOF_test_element + i, eN_k_i_nSpace = eN_k_i * nSpace, i_nSpace = i * nSpace;
799 // P3c: mass + component flux (divergence form, -= F_0 . grad N_i).
800 elementResidual_u[i] += ck.Mass_weak(m_t, u_test_dV[i]) + VMS * ck.SubgridError(subgridError_u, Lstar_u[i]) + VMS * ck.NumericalDiffusion(q_numDiff_u_last[eN_k], grad_u, &u_grad_test_dV[i_nSpace]);
801 for (int I = 0; I < nSpace; I++)
802 elementResidual_u[i] -= F0[I] * u_grad_test_dV[i_nSpace + I];
803 } //i
804 //
805 q_m.data()[eN_k] = m;
806 q_u.data()[eN_k] = u;
807 }
808 //
809 //load element into global residual and save element residual
810 //
811 for (int i = 0; i < nDOF_test_element; i++) {
812 int eN_i = eN * nDOF_test_element + i;
813
814 globalResidual.data()[offset_u + stride_u * u_l2g.data()[eN_i]] += elementResidual_u[i];
815 } //i
816 } //elements
817 //
818 //loop over exterior element boundaries to calculate surface integrals and load into element and global residuals
819 //
820 //ebNE is the Exterior element boundary INdex
821 //ebN is the element boundary INdex
822 //eN is the element index
823 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
824 int ebN = exteriorElementBoundariesArray.data()[ebNE], eN = elementBoundaryElementsArray.data()[ebN * 2 + 0], ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0], eN_nDOF_trial_element = eN * nDOF_trial_element;
825 double elementResidual_u[nDOF_test_element];
826 for (int i = 0; i < nDOF_test_element; i++) { elementResidual_u[i] = 0.0; }
827 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
828 int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb, ebNE_kb_nSpace = ebNE_kb * nSpace, ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb, ebN_local_kb_nSpace = ebN_local_kb * nSpace;
829 double u_ext = 0.0, grad_u_ext[nSpace], m_ext = 0.0, dm_ext = 0.0, f_ext[nSpace], df_ext[nSpace], a_ext[nnz], da_ext[nnz], as_ext[nnz], flux_ext = 0.0,
830 //anb_seepage_flux=0.0, // for flux calculation
831 bc_u_ext = 0.0, bc_grad_u_ext[nSpace], bc_m_ext = 0.0, bc_dm_ext = 0.0, bc_f_ext[nSpace], bc_df_ext[nSpace], bc_a_ext[nnz], bc_da_ext[nnz], bc_as_ext[nnz], jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace], boundaryJac[nSpace * (nSpace - 1)], metricTensor[(nSpace - 1) * (nSpace - 1)], metricTensorDetSqrt, dS, u_test_dS[nDOF_test_element], u_grad_trial_trace[nDOF_trial_element * nSpace], normal[3], x_ext, y_ext, z_ext, xt_ext, yt_ext, zt_ext, integralScaling, G[nSpace * nSpace], G_dd_G, tr_G;
832 //
833 //calculate the solution and gradients at quadrature points
834 //
835 //compute information about mapping from reference element to physical element
836 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(), jac_ext, jacDet_ext, jacInv_ext, boundaryJac, metricTensor, metricTensorDetSqrt,
837 normal_ref.data(), normal, x_ext, y_ext, z_ext);
838 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), xt_ext, yt_ext, zt_ext, normal, boundaryJac, metricTensor, integralScaling);
839 dS = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt + MOVING_DOMAIN * integralScaling) * dS_ref.data()[kb];
840 //get the metric tensor
841 //cek todo use symmetry
842 ck.calculateG(jacInv_ext, G, G_dd_G, tr_G);
843 //compute shape and solution information
844 //shape
845 ck.gradTrialFromRef(&u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element], jacInv_ext, u_grad_trial_trace);
846 //solution and gradient
847 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_ext);
848 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial_trace, grad_u_ext);
849
850 //populate ebqe_x
851 // const int ebNE_kb_3d = ebNE_kb * 3;
852 // ebqe_x.data()[ebNE_kb_3d + 0] = x_ext;
853 // ebqe_x.data()[ebNE_kb_3d + 1] = y_ext;
854 // ebqe_x.data()[ebNE_kb_3d + 2] = z_ext;
855
856 //precalculate test function products with integration weights
857 for (int j = 0; j < nDOF_trial_element; j++) { u_test_dS[j] = u_test_trace_ref.data()[ebN_local_kb * nDOF_test_element + j] * dS; }
858 //
859 //load the boundary values
860 //
861 bc_u_ext = isDOFBoundary_u.data()[ebNE_kb] * ebqe_bc_u_ext.data()[ebNE_kb] + (1 - isDOFBoundary_u.data()[ebNE_kb]) * u_ext;
862 //
863 //calculate the pde coefficients using the solution and the boundary values for the solution
864 //
865 const double rho_ext = ebqe_rho.data()[ebNE_kb];
866 const double rho_velocity_ext = std::fabs(rho_ext) > 1.0e-12 ? rho_ext : rho;
867 double Kr, dKr, thetaW_ext, thetaW_bc;
868 // Boundary closure cross-derivatives wrt u_n; (0,1) boundary Jacobian
869 // is not assembled here.
870 double dm_du_n_ext = 0.0, dkr_du_n_ext = 0.0;
871 double df_du_n_ext[nSpace];
872 double da_du_n_ext[nnz];
873 double bc_dm_du_n = 0.0, bc_dkr_du_n = 0.0;
874 double bc_df_du_n[nSpace];
875 double bc_da_du_n[nnz];
876 for (int I = 0; I < nSpace; I++) { df_du_n_ext[I] = 0.0; bc_df_du_n[I] = 0.0; }
877 for (int ii = 0; ii < nnz; ii++) { da_du_n_ext[ii] = 0.0; bc_da_du_n[ii] = 0.0; }
878 double u_n_ext_qp = 0.0;
879 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
880 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_n_ext_qp);
881 // Component-1 boundary Dirichlet via the standard
882 // mask = isDOFBoundary_n * bc + (1 - isDOFBoundary_n) * interior.
883 const double bc_u_n_ext_qp = isDOFBoundary_n.data()[ebNE_kb] * ebqe_bc_u_n_ext.data()[ebNE_kb]
884 + (1 - isDOFBoundary_n.data()[ebNE_kb]) * u_n_ext_qp;
885 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
886 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
887 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
888 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], u_ext, u_n_ext_qp,
889 m_ext, dm_ext, dm_du_n_ext, f_ext, df_ext, df_du_n_ext, a_ext, da_ext, da_du_n_ext,
890 as_ext, Kr, dKr, dkr_du_n_ext, thetaW_ext);
891 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
892 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
893 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
894 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], bc_u_ext, bc_u_n_ext_qp,
895 bc_m_ext, bc_dm_ext, bc_dm_du_n, bc_f_ext, bc_df_ext, bc_df_du_n, bc_a_ext, bc_da_ext, bc_da_du_n,
896 bc_as_ext, Kr, dKr, bc_dkr_du_n, thetaW_bc);
897 ebqe_theta.data()[ebNE_kb] = thetaW_ext;
898
899 //
900 //Calculate Darcy velocity on exterior face : v_ext = -(a_ext/rho) * (grad_u_ext + gravity) ---
901 //
902 double ext_pressure_gradient[nSpace];
903 for (int J=0; J<nSpace; ++J)
904 ext_pressure_gradient[J] = grad_u_ext[J] - rho_velocity_ext * gravity.data()[J];
905
906 for (int I=0; I<nSpace; ++I) {
907 double acc = 0.0;
908 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I+1]; ++ii) {
909 const int J = a_colind.data()[ii];
910 acc += (a_ext[ii] / rho_velocity_ext) * ext_pressure_gradient[J];
911 }
912 ebqe_velocity_ext.data()[ebNE_kb_nSpace + I] = -acc;
913 ebqe_velocity_ext_couple.data()[ebNE_kb_nSpace + I] = -acc ; // store vector at this boundary qp
914 }
915
916
917 //
918 // ===== P3c boundary: compositional comp-0 (H2O) flux F_0 . n =====
919 // Richards-style Nitsche trace flux on a pressure-Dirichlet (or seepage)
920 // face: flux = F_0.n + penalty*(p - p_BC); no-flow faces keep bc_flux.
921 // F_0 mirrors the interior H2O flux; FD-verified in boundary0_test.cpp.
922 {
923 double grad_u_n_ext[nSpace];
924 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
925 u_grad_trial_trace, grad_u_n_ext);
926 const int mat_b = elementMaterialTypes.data()[eN];
927 const double alpha_b = alpha.data()[mat_b];
928 const double n_vg_b = n.data()[mat_b];
929 const double krn_end_b = krn_end.data()[mat_b];
930 const double *KWs_b = &KWs.data()[mat_b * nnz];
931 const double phi_b = thetaR.data()[mat_b] + thetaSR.data()[mat_b];
932 const double S_wr_b = thetaR.data()[mat_b] / phi_b;
933 const double one_m_Sr_b = 1.0 - S_wr_b;
934 const double Se_trap_L956 = 1.0 - S_gr.data()[mat_b] / one_m_Sr_b; // gas-only residual trapping
935 const double z_clb = fmin(fmax(u_n_ext_qp, 1.0e-8), 1.0 - 1.0e-8);
936 const double p_clb = fmax(u_ext, 1.0e2);
939 const double Se_ab = fmin(fmax((1.0 - fsb.S_g - S_wr_b)/one_m_Sr_b, 0.0), 1.0);
940 double KWrb=0,DKWrb=0,thWb=0,DthWb=0,KNrb=0,DKNrb=0,pcb=0,dpc_dSeb=0,d2pcb=0;
941 if (PSK_TYPE_member == 1) {
942 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_ab, alpha_b, n_vg_b, thetaR.data()[mat_b], thetaSR.data()[mat_b], thWb, DthWb, KWrb, DKWrb);
943 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_ab, alpha_b, n_vg_b, KNrb, DKNrb, Se_trap_L956);
944 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_ab, alpha_b, n_vg_b, pcb, dpc_dSeb, d2pcb);
945 } else {
946 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_ab, alpha_b, n_vg_b, thetaR.data()[mat_b], thetaSR.data()[mat_b], thWb, DthWb, KWrb, DKWrb);
947 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_ab, alpha_b, n_vg_b, KNrb, DKNrb, Se_trap_L956);
948 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_ab, alpha_b, n_vg_b, pcb, dpc_dSeb, d2pcb);
949 }
950 KNrb *= krn_end_b;
951 const double pcpb = dpc_dSeb / one_m_Sr_b;
952 const double Mbar_gb = fsb.Y*::m_comp_co2::eos::M_CO2_KG + (1.0 - fsb.Y)*::m_comp_co2::eos::M_H2O_KG;
953 const double Mbar_ab = fsb.X*::m_comp_co2::eos::M_CO2_KG + (1.0 - fsb.X)*::m_comp_co2::eos::M_H2O_KG;
954 const double rho_g_mass_b = fsb.rho_g*Mbar_gb;
955 const double rho_a_mass_b = fsb.rho_a*Mbar_ab;
956 double F0n = 0.0;
957 for (int I = 0; I < nSpace; I++) {
958 double ua = 0.0, ug = 0.0;
959 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I+1]; ii++) {
960 const int J = a_colind.data()[ii];
961 const double gradSa_J = -(fsb.dS_g_dp*grad_u_ext[J] + fsb.dS_g_dz*grad_u_n_ext[J]);
962 const double gp_a = grad_u_ext[J] - rho_a_mass_b*gravity.data()[J];
963 const double gp_g = grad_u_ext[J] + pcpb*gradSa_J - rho_g_mass_b*gravity.data()[J];
964 ua -= (KWrb*KWs_b[ii]) * gp_a;
965 ug -= (KNrb*KWs_b[ii]/mu_n) * gp_g;
966 }
967 F0n += (fsb.rho_g*(1.0 - fsb.Y)*ug + fsb.rho_a*(1.0 - fsb.X)*ua) * normal[I];
968 }
969 const int isSeep = isSeepageFace.data()[ebNE];
970 if (isSeep || isDOFBoundary_u.data()[ebNE_kb]) {
971 const double bc_u_pen = isSeep ? 0.0 : bc_u_ext;
972 flux_ext = F0n + ebqe_penalty_ext.data()[ebNE_kb] * (u_ext - bc_u_pen);
973 if (isSeep && flux_ext <= 0.0) flux_ext = 0.0; // closed unless outflow
974 } else {
975 flux_ext = ebqe_bc_flux_ext[ebNE_kb]; // no-flow / prescribed flux
976 }
977 }
978 ebqe_flux.data()[ebNE_kb] = flux_ext;
979
980 anb_seepage_flux = seepagefluxcalculator(anb_seepage_flux, isSeepageFace.data()[ebNE], dS, flux_ext);
981 anb_seepage_flux_n.data()[0] = anb_seepage_flux;
982 ebqe_u.data()[ebNE_kb] = u_ext;
983 //
984 //update residuals
985 //
986 for (int i = 0; i < nDOF_test_element; i++) {
987 elementResidual_u[i] += ck.ExteriorElementBoundaryFlux(flux_ext, u_test_dS[i]);
988 } //i
989 } //kb
990
991 //
992 //update the element and global residual storage
993 //
994 for (int i = 0; i < nDOF_test_element; i++) {
995 int eN_i = eN * nDOF_test_element + i;
996 globalResidual.data()[offset_u + stride_u * u_l2g.data()[eN_i]] += elementResidual_u[i];
997 } //i
998 } //ebNE
999
1000 // ============================================================================
1001 // component-1 (S_n) gas-mass equation.
1002 //
1003 // d(phi*rho_n*S_n)/dt + div F_n = 0
1004 // m_n = phi * rho_n * u_n
1005 // m_n_old = phi * rho_n * u_n_old
1006 //
1007 // Gas Darcy flux F_n = -(K/mu_n) k_rn (grad p_n - rho_n g)
1008 // = -(K/mu_n) k_rn (grad p_w + dp_c/dS_n grad S_n - rho_n g)
1009 // Splitting into Proteus form for u_w = p_w, u_n = S_n:
1010 // f_n = rho_n^2 * k_rn * (K/mu_n) * g (gravity advection)
1011 // a_n = rho_n * k_rn * (K/mu_n) (diffusion against grad u_w)
1012 // a_n_pc = rho_n * k_rn * (K/mu_n) * dp_c/dS_n (capillary diffusion against grad u_n)
1013 // At u_n = 0 (fully wet) the closure returns k_rn = 0 -> all flux contributions
1014 // vanish, so this loop should reproduce the single-phase result for the
1015 // Bioswale.
1016 for (int eN = 0; eN < nElements_global; eN++) {
1017 const int mat_eN = elementMaterialTypes.data()[eN];
1018 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
1019 const double alpha_eN = alpha.data()[mat_eN];
1020 const double krn_end_eN = krn_end.data()[mat_eN];
1021 const double n_vg_eN = n.data()[mat_eN];
1022 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
1023 double elementResidual_n[nDOF_test_element];
1024 // Row-sum lumped weight per node: M_lump[i] = int N_i dV over the element.
1025 // Used to apply the CO2 injection source as a pure diagonal contribution
1026 // at the port nodes (no smearing onto neighbor basis functions), which
1027 // matches the localised disk source and avoids exciting the BC closure
1028 // on rim nodes where k_rn is still zero.
1029 double lumped_w_n[nDOF_test_element];
1030 for (int i = 0; i < nDOF_test_element; i++) {
1031 elementResidual_n[i] = 0.0;
1032 lumped_w_n[i] = 0.0;
1033 }
1034 for (int k = 0; k < nQuadraturePoints_element; k++) {
1035 const int eN_k = eN * nQuadraturePoints_element + k;
1036 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
1037 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x_q, y_q, z_q;
1038 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
1039 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
1040 jac, jacDet, jacInv, x_q, y_q, z_q);
1041 const double dV = std::fabs(jacDet) * dV_ref.data()[k];
1042 // Trial gradients in physical coords (used for grad u_w and grad N_i).
1043 double u_grad_trial_qp[nDOF_trial_element * nSpace];
1044 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace],
1045 jacInv, u_grad_trial_qp);
1046 // Saturation u_n and old at QP.
1047 double u_n = 0.0, u_n_old = 0.0;
1048 ck.valFromDOF(u_dof_n.data(),
1049 &u_l2g.data()[eN_nDOF_trial_element],
1050 &u_trial_ref.data()[k * nDOF_trial_element], u_n);
1051 ck.valFromDOF(u_dof_n_old.data(),
1052 &u_l2g.data()[eN_nDOF_trial_element],
1053 &u_trial_ref.data()[k * nDOF_trial_element], u_n_old);
1054 // Wetting-pressure value and gradient at QP. u_w (and u_w_old) feed
1055 // the linear EOS rho_n(p_n) = c_n*(u_w + p_c(u_n)).
1056 double u_w_qp = 0.0, u_w_qp_old = 0.0;
1057 ck.valFromDOF(u_dof.data(),
1058 &u_l2g.data()[eN_nDOF_trial_element],
1059 &u_trial_ref.data()[k * nDOF_trial_element], u_w_qp);
1060 ck.valFromDOF(u_dof_old.data(),
1061 &u_l2g.data()[eN_nDOF_trial_element],
1062 &u_trial_ref.data()[k * nDOF_trial_element], u_w_qp_old);
1063 double grad_u_w[nSpace];
1064 ck.gradFromDOF(u_dof.data(),
1065 &u_l2g.data()[eN_nDOF_trial_element],
1066 u_grad_trial_qp, grad_u_w);
1067 // Saturation gradient at QP (Step 3d: needed for p_c(S_n) flux contribution).
1068 double grad_u_n[nSpace];
1069 ck.gradFromDOF(u_dof_n.data(),
1070 &u_l2g.data()[eN_nDOF_trial_element],
1071 u_grad_trial_qp, grad_u_n);
1072 // Rock pore fraction and residual wetting saturation; consumed by the
1073 // compositional flux F_1 below (Se_a = (S_a - S_wr)/(1 - S_wr)).
1074 // P3c (complete): the gas relperm / capillary / linear-gas-EOS closures
1075 // that used to be evaluated here -- phase-based, keyed on u_n as S_n --
1076 // are removed. The F_1 block recomputes every saturation-dependent
1077 // property from the FLASH saturation S_g, so nothing downstream reads a
1078 // phase-based k_rn(u_n), p_c(u_n) or rho_n(p_n) any more.
1079 const double phi_loc = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
1080 const double S_wr_loc = thetaR.data()[mat_eN] / phi_loc;
1081 const double one_m_Sr_loc = 1.0 - S_wr_loc;
1082 const double Se_trap_L1103 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_loc; // gas-only residual trapping
1083 // component-1 (CO2) accumulation, compositional (p,z) form:
1084 // m_1 = phi * N * z, N = rho_g*S_g + rho_a*(1-S_g), z = u_n.
1085 const double z_cl = fmin(fmax(u_n, 1.0e-8), 1.0 - 1.0e-8);
1086 const double z_cl_old = fmin(fmax(u_n_old, 1.0e-8), 1.0 - 1.0e-8);
1087 const double p_cl = fmax(u_w_qp, 1.0e2);
1088 const double p_cl_old = fmax(u_w_qp_old, 1.0e2);
1093 const double N_cur = fs_n.rho_g*fs_n.S_g + fs_n.rho_a*(1.0 - fs_n.S_g);
1094 const double N_old = fs_n_old.rho_g*fs_n_old.S_g
1095 + fs_n_old.rho_a*(1.0 - fs_n_old.S_g);
1096 const double m_n = phi_eN * N_cur * z_cl;
1097 const double m_n_old = phi_eN * N_old * z_cl_old;
1098 const double m_n_t = (m_n - m_n_old) / dt;
1099 // ===== P3c: component CO2 molar flux F_1 = rho_g*Y*u_g + rho_a*X*u_a =====
1100 // Saturation-dependent props are recomputed from the FLASH saturation S_g
1101 // (NOT u_n, which is now z). S_a = 1 - S_g; psk closures take wetting Se_a.
1102 const double S_a_qp = 1.0 - fs_n.S_g;
1103 const double Se_a = fmin(fmax((S_a_qp - S_wr_loc) / one_m_Sr_loc, 0.0), 1.0);
1104 double KWr_a = 0.0, DKWr_a = 0.0, thW_a = 0.0, DthW_a = 0.0;
1105 double KNr_a = 0.0, DKNr_a = 0.0;
1106 double pc_a = 0.0, dpc_dSe_a = 0.0, d2pc_a = 0.0;
1107 if (PSK_TYPE_member == 1) {
1108 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_a, alpha_eN, n_vg_eN,
1109 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_a, DthW_a, KWr_a, DKWr_a);
1110 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_a, alpha_eN, n_vg_eN, KNr_a, DKNr_a, Se_trap_L1103);
1111 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_a, alpha_eN, n_vg_eN, pc_a, dpc_dSe_a, d2pc_a);
1112 } else {
1113 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_a, alpha_eN, n_vg_eN,
1114 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_a, DthW_a, KWr_a, DKWr_a);
1115 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_a, alpha_eN, n_vg_eN, KNr_a, DKNr_a, Se_trap_L1103);
1116 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_a, alpha_eN, n_vg_eN, pc_a, dpc_dSe_a, d2pc_a);
1117 }
1118 KNr_a *= krn_end_eN;
1119 // p_c'(S_a) = dp_c/dSe * dSe/dS_a = dpc_dSe_a / (1 - S_wr).
1120 const double pcp_a = dpc_dSe_a / one_m_Sr_loc;
1121 // phase mass densities for gravity (molar density * mean molar mass):
1122 const double Mbar_g = fs_n.Y*::m_comp_co2::eos::M_CO2_KG
1123 + (1.0 - fs_n.Y)*::m_comp_co2::eos::M_H2O_KG;
1124 const double Mbar_a = fs_n.X*::m_comp_co2::eos::M_CO2_KG
1125 + (1.0 - fs_n.X)*::m_comp_co2::eos::M_H2O_KG;
1126 const double rho_g_mass = fs_n.rho_g*Mbar_g;
1127 const double rho_a_mass = fs_n.rho_a*Mbar_a;
1128 // u_a = -(K krw/mu_w)(grad p_a - rho_a_mass g), p_a = u_w
1129 // u_g = -(K krg/mu_g)(grad p_g - rho_g_mass g), p_g = u_w + p_c(S_a)
1130 // grad p_g = grad u_w + p_c'(S_a) grad S_a, grad S_a = -(dSg/dp grad u_w + dSg/dz grad u_n)
1131 // KWs = K/mu_w (aqueous base); mu_n = mu_g/mu_w so KWs/mu_n = K/mu_g.
1132 double F1[nSpace];
1133 for (int I = 0; I < nSpace; I++) {
1134 double ua = 0.0, ug = 0.0;
1135 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
1136 const int J = a_colind.data()[ii];
1137 const double gradSa_J = -(fs_n.dS_g_dp*grad_u_w[J] + fs_n.dS_g_dz*grad_u_n[J]);
1138 const double gp_a = grad_u_w[J] - rho_a_mass*gravity.data()[J];
1139 const double gp_g = grad_u_w[J] + pcp_a*gradSa_J - rho_g_mass*gravity.data()[J];
1140 ua -= (KWr_a*KWs_eN[ii]) * gp_a;
1141 ug -= (KNr_a*KWs_eN[ii]/mu_n) * gp_g;
1142 }
1143 F1[I] = fs_n.rho_g*fs_n.Y*ug + fs_n.rho_a*fs_n.X*ua;
1144 }
1145 // Dissolution is handled thermodynamically by the inline flash (aqueous
1146 // CO2 = rho_a*X advected in F_1); the old kinetic R_diss sink is removed.
1147 // Residual integration: mass + component CO2 flux (- injection, lumped).
1148 for (int i = 0; i < nDOF_test_element; i++) {
1149 const double test_i = u_test_ref.data()[k * nDOF_test_element + i];
1150 // Mass contribution.
1151 elementResidual_n[i] += m_n_t * test_i * dV;
1152 lumped_w_n[i] += test_i * dV;
1153 for (int I = 0; I < nSpace; I++) {
1154 elementResidual_n[i] -= F1[I] * u_grad_trial_qp[i * nSpace + I] * dV;
1155 }
1156 }
1157 }
1158 // Row-sum lumped CO2 injection: subtract Q_inj at port nodes only.
1159 for (int i = 0; i < nDOF_test_element; i++) {
1160 const int gi = u_l2g.data()[eN * nDOF_test_element + i];
1161 elementResidual_n[i] -= injection_dof.data()[gi] * lumped_w_n[i];
1162 }
1163 for (int i = 0; i < nDOF_test_element; i++) {
1164 const int eN_i = eN * nDOF_test_element + i;
1165 globalResidual.data()[offset_n + stride_n * u_l2g.data()[eN_i]] += elementResidual_n[i];
1166 }
1167 }
1168
1169 // ============================================================================
1170 // Comp-1 (CO2 / z) exterior boundary loop -- STAB=0 path. COMPOSITIONAL.
1171 //
1172 // P3c STATUS: BOUNDARY PORTED (2026-06-06). Slot 1 is the overall CO2
1173 // composition z, NOT a saturation. Computes the compositional molar CO2
1174 // trace flux F_1.n = rho_g*Y*u_g + rho_a*X*u_a, mirroring the FD-verified
1175 // interior element flux (~line 1152) and the STAB=2 boundary loop; every
1176 // saturation-dependent property is recomputed from the FLASH saturation
1177 // S_g(p,z). The surface term from div(F_1) by parts is +(F_1.n) N_i dS.
1178 // isDir_n : F_1.n = consistent compositional flux
1179 // + penalty*(z - z_BC) [Nitsche]
1180 // not isDir_n : F_1.n = 0 (no-flow / closed -- no spurious boundary flux)
1181 // so STAB=0 and STAB=2 now enforce identical compositional comp-1 BCs.
1182 // McWhorter-Sunada drives this via getDBC_z at the inlet (z_BC = z(S_n_BC)).
1183 // Jacobian is the matching (1,1)/(1,0) blocks in calculateJacobian.
1184 // ============================================================================
1185 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
1186 const int ebN = exteriorElementBoundariesArray.data()[ebNE];
1187 const int eN = elementBoundaryElementsArray.data()[ebN * 2 + 0];
1188 const int ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0];
1189 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
1190 const int mat_eN = elementMaterialTypes.data()[eN];
1191 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
1192 const double alpha_eN = alpha.data()[mat_eN];
1193 const double krn_end_eN = krn_end.data()[mat_eN];
1194 const double n_vg_eN = n.data()[mat_eN];
1195 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
1196 const double S_wr_loc = thetaR.data()[mat_eN] / phi_eN;
1197 const double one_m_Sr_loc = 1.0 - S_wr_loc;
1198 const double Se_trap_L1235 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_loc; // gas-only residual trapping
1199
1200 double elementResidual_n_eb[nDOF_test_element];
1201 for (int i = 0; i < nDOF_test_element; i++) elementResidual_n_eb[i] = 0.0;
1202
1203 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
1204 const int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb;
1205 const int ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb;
1206 const int ebN_local_kb_nSpace = ebN_local_kb * nSpace;
1207
1208 double jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace];
1209 double boundaryJac_b[nSpace * (nSpace - 1)];
1210 double metricTensor_b[(nSpace - 1) * (nSpace - 1)];
1211 double metricTensorDetSqrt_b, dS_eb, normal_b[3];
1212 double xt_b, yt_b, zt_b, integralScaling_b;
1213 double x_eb, y_eb, z_eb;
1214 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
1215 mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
1216 mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(),
1217 jac_ext, jacDet_ext, jacInv_ext, boundaryJac_b, metricTensor_b,
1218 metricTensorDetSqrt_b, normal_ref.data(), normal_b,
1219 x_eb, y_eb, z_eb);
1220 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
1221 mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
1222 xt_b, yt_b, zt_b, normal_b, boundaryJac_b, metricTensor_b,
1223 integralScaling_b);
1224 dS_eb = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt_b
1225 + MOVING_DOMAIN * integralScaling_b) * dS_ref.data()[kb];
1226
1227 double u_grad_trial_trace_b[nDOF_trial_element * nSpace];
1228 ck.gradTrialFromRef(
1229 &u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element],
1230 jacInv_ext, u_grad_trial_trace_b);
1231 double u_w_ext_b = 0.0, u_n_ext_b = 0.0;
1232 double grad_u_w_ext_b[nSpace], grad_u_n_ext_b[nSpace];
1233 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
1234 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element],
1235 u_w_ext_b);
1236 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1237 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element],
1238 u_n_ext_b);
1239 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
1240 u_grad_trial_trace_b, grad_u_w_ext_b);
1241 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1242 u_grad_trial_trace_b, grad_u_n_ext_b);
1243
1244 const int isDir_n = isDOFBoundary_n.data()[ebNE_kb];
1245 const double bc_u_n_ext_b = isDir_n * ebqe_bc_u_n_ext.data()[ebNE_kb]
1246 + (1 - isDir_n) * u_n_ext_b;
1247
1248 // ====================================================================
1249 // P3c boundary (STAB=0): compositional comp-1 (CO2) trace flux F_1.n.
1250 // Slot 1 is the overall CO2 composition z, NOT a saturation. Mirrors
1251 // the FD-verified interior element flux F_1 = rho_g*Y*u_g + rho_a*X*u_a
1252 // (~line 1152) and the STAB=2 boundary loop; every saturation-dependent
1253 // property is recomputed from the FLASH saturation S_g(p,z) (psk
1254 // closures on the wetting Se_a = (1-S_g-S_wr)/(1-S_wr)). Consistent flux
1255 // on Dirichlet-z faces + a Nitsche penalty driving z at the trace toward
1256 // bc_u_n_ext_b (= z_BC; McWhorter-Sunada inlet); no-flow faces contribute
1257 // nothing (closed-box conservation). Jacobian is in calculateJacobian.
1258 // ====================================================================
1259 const double z_clb = fmin(fmax(u_n_ext_b, 1.0e-8), 1.0 - 1.0e-8);
1260 const double p_clb = fmax(u_w_ext_b, 1.0e2);
1263 const double Se_ab = fmin(fmax((1.0 - fsb.S_g - S_wr_loc)/one_m_Sr_loc, 0.0), 1.0);
1264 double KWr_b=0,DKWr_b=0,thW_b=0,DthW_b=0,KNr_b=0,DKNr_b=0,pc_b=0,dpc_dSe_b=0,d2pc_b=0;
1265 if (PSK_TYPE_member == 1) {
1266 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_ab, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
1267 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_ab, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L1235);
1268 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_ab, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
1269 } else {
1270 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_ab, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
1271 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_ab, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L1235);
1272 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_ab, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
1273 }
1274 KNr_b *= krn_end_eN;
1275 const double pcpb = dpc_dSe_b / one_m_Sr_loc;
1276 const double Mbar_gb = fsb.Y*::m_comp_co2::eos::M_CO2_KG + (1.0 - fsb.Y)*::m_comp_co2::eos::M_H2O_KG;
1277 const double Mbar_ab = fsb.X*::m_comp_co2::eos::M_CO2_KG + (1.0 - fsb.X)*::m_comp_co2::eos::M_H2O_KG;
1278 const double rho_g_mass_b = fsb.rho_g*Mbar_gb;
1279 const double rho_a_mass_b = fsb.rho_a*Mbar_ab;
1280 double F_n_dot_n = 0.0;
1281 for (int I = 0; I < nSpace; I++) {
1282 double ua = 0.0, ug = 0.0;
1283 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
1284 const int J = a_colind.data()[ii];
1285 const double gradSa_J = -(fsb.dS_g_dp*grad_u_w_ext_b[J] + fsb.dS_g_dz*grad_u_n_ext_b[J]);
1286 const double gp_a = grad_u_w_ext_b[J] - rho_a_mass_b*gravity.data()[J];
1287 const double gp_g = grad_u_w_ext_b[J] + pcpb*gradSa_J - rho_g_mass_b*gravity.data()[J];
1288 ua -= (KWr_b*KWs_eN[ii]) * gp_a;
1289 ug -= (KNr_b*KWs_eN[ii]/mu_n) * gp_g;
1290 }
1291 F_n_dot_n += (fsb.rho_g*fsb.Y*ug + fsb.rho_a*fsb.X*ua) * normal_b[I];
1292 }
1293 if (isDir_n) {
1294 // IIPG penalty scaled by the local comp-1 diffusion magnitude a_n
1295 // (= rho_g*Y*krn/mu_n + rho_a*X*krw, times a representative K/mu_w).
1296 // The framework penalty is const/h, UNSCALED by the coefficient; for
1297 // comp-1 the ~1e4 molar density makes the bare penalty ~1e4x too weak
1298 // to enforce z=z_BC, so the inlet floats (the McWhorter-Sunada
1299 // convergence killer). Coefficient treated frozen in the Jacobian.
1300 double Kw_rep = 0.0;
1301 for (int ii = 0; ii < nnz; ii++) Kw_rep = fmax(Kw_rep, fabs(KWs_eN[ii]));
1302 const double a_n_scale = (fsb.rho_g*fsb.Y*KNr_b/mu_n
1303 + fsb.rho_a*fsb.X*KWr_b) * Kw_rep;
1304 const double pen_eff = ebqe_penalty_ext.data()[ebNE_kb] * a_n_scale;
1305 F_n_dot_n += pen_eff * (u_n_ext_b - bc_u_n_ext_b); // Nitsche, drives z->z_BC
1306 } else {
1307 F_n_dot_n = 0.0; // no-flow
1308 }
1309
1310 for (int i = 0; i < nDOF_test_element; i++) {
1311 const double test_i_dS = u_test_trace_ref.data()[
1312 ebN_local_kb * nDOF_test_element + i] * dS_eb;
1313 elementResidual_n_eb[i] += F_n_dot_n * test_i_dS;
1314 }
1315 } // kb
1316
1317 for (int i = 0; i < nDOF_test_element; i++) {
1318 const int gi = u_l2g.data()[eN * nDOF_test_element + i];
1319 globalResidual.data()[offset_n + stride_n * gi] += elementResidual_n_eb[i];
1320 }
1321 } // ebNE
1322 }
1323
1325 {
1326 xt::pyarray<double> &mesh_trial_ref = args.array<double>("mesh_trial_ref");
1327 xt::pyarray<double> &mesh_grad_trial_ref = args.array<double>("mesh_grad_trial_ref");
1328 xt::pyarray<double> &mesh_dof = args.array<double>("mesh_dof");
1329 xt::pyarray<double> &mesh_velocity_dof = args.array<double>("mesh_velocity_dof");
1330 double MOVING_DOMAIN = args.scalar<double>("MOVING_DOMAIN");
1331 xt::pyarray<int> &mesh_l2g = args.array<int>("mesh_l2g");
1332 xt::pyarray<double> &dV_ref = args.array<double>("dV_ref");
1333 xt::pyarray<double> &u_trial_ref = args.array<double>("u_trial_ref");
1334 xt::pyarray<double> &u_grad_trial_ref = args.array<double>("u_grad_trial_ref");
1335 xt::pyarray<double> &u_test_ref = args.array<double>("u_test_ref");
1336 xt::pyarray<double> &u_grad_test_ref = args.array<double>("u_grad_test_ref");
1337 xt::pyarray<double> &mesh_trial_trace_ref = args.array<double>("mesh_trial_trace_ref");
1338 xt::pyarray<double> &mesh_grad_trial_trace_ref = args.array<double>("mesh_grad_trial_trace_ref");
1339 xt::pyarray<double> &dS_ref = args.array<double>("dS_ref");
1340 xt::pyarray<double> &u_trial_trace_ref = args.array<double>("u_trial_trace_ref");
1341 xt::pyarray<double> &u_grad_trial_trace_ref = args.array<double>("u_grad_trial_trace_ref");
1342 xt::pyarray<double> &u_test_trace_ref = args.array<double>("u_test_trace_ref");
1343 xt::pyarray<double> &u_grad_test_trace_ref = args.array<double>("u_grad_test_trace_ref");
1344 xt::pyarray<double> &normal_ref = args.array<double>("normal_ref");
1345 xt::pyarray<double> &boundaryJac_ref = args.array<double>("boundaryJac_ref");
1346 int nElements_global = args.scalar<int>("nElements_global");
1347 xt::pyarray<double> &ebqe_penalty_ext = args.array<double>("ebqe_penalty_ext");
1348 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
1349 xt::pyarray<int> &isSeepageFace = args.array<int>("isSeepageFace");
1350 xt::pyarray<int> &a_rowptr = args.array<int>("a_rowptr");
1351 xt::pyarray<int> &a_colind = args.array<int>("a_colind");
1352 double rho = args.scalar<double>("rho");
1353 double beta = args.scalar<double>("beta");
1354
1356 xt::pyarray<double> &q_rho = args.array<double>("q_rho");
1357 xt::pyarray<double> &ebqe_rho = args.array<double>("ebqe_rho");
1359 // Stage 3b: gas-side kinetic dissolution sink reads (residual-only at
1360 // this stage; the Jacobian contribution -d(R_diss)/du_n is small
1361 // (proportional to k_d * dt) and we approximate it as zero in the
1362 // initial port -- Newton recovers it through outer iteration since the
1363 // sink is also bounded. Promote to a proper Jacobian contribution if
1364 // Newton stalls in the FluidFlower setup.
1365 xt::pyarray<double> &c_dof_jac = args.array<double>("c_dof");
1366 const double k_d_jac = args.scalar<double>("k_d");
1367 const double c_sat_jac = args.scalar<double>("c_sat");
1368
1369 xt::pyarray<double> &gravity = args.array<double>("gravity");
1370 xt::pyarray<double> &alpha = args.array<double>("alpha");
1371 xt::pyarray<double> &n = args.array<double>("n");
1372 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
1373 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
1374 xt::pyarray<double> &KWs = args.array<double>("KWs");
1375 xt::pyarray<double> &krn_end = args.array<double>("krn_end");
1376 xt::pyarray<double> &S_gr = args.array<double>("S_gr");
1377 double mu_n = args.scalar<double>("mu_n");
1378 double useMetrics = args.scalar<double>("useMetrics");
1379 double alphaBDF = args.scalar<double>("alphaBDF");
1380 int lag_shockCapturing = args.scalar<int>("lag_shockCapturing");
1381 double shockCapturingDiffusion = args.scalar<double>("shockCapturingDiffusion");
1382 // VMS
1383 double VMS = args.scalar<double>("VMS");
1384 xt::pyarray<int> &u_l2g = args.array<int>("u_l2g");
1385 xt::pyarray<double> &elementDiameter = args.array<double>("elementDiameter");
1386 xt::pyarray<double> &u_dof = args.array<double>("u_dof");
1387 // component-1 saturation DOFs (needed by the gas-eq
1388 // Jacobian element loop appended at the end of this function).
1389 xt::pyarray<double> &u_dof_n = args.array<double>("u_dof_n");
1390 xt::pyarray<double> &velocity = args.array<double>("velocity");
1391 xt::pyarray<double> &q_m_betaBDF = args.array<double>("q_m_betaBDF");
1392 xt::pyarray<double> &cfl = args.array<double>("cfl");
1393 xt::pyarray<double> &q_numDiff_u = args.array<double>("q_numDiff_u");
1394 xt::pyarray<double> &q_numDiff_u_last = args.array<double>("q_numDiff_u_last");
1395 xt::pyarray<int> &csrRowIndeces_u_u = args.array<int>("csrRowIndeces_u_u");
1396 xt::pyarray<int> &csrColumnOffsets_u_u = args.array<int>("csrColumnOffsets_u_u");
1397 xt::pyarray<double> &globalJacobian = args.array<double>("globalJacobian");
1398 // component-1 (S_n) Jacobian args. Used by
1399 // the dedicated component-1 element loop appended at the end of this
1400 // function. (1,1) block is the consistent mass matrix / dt; (0,1) and
1401 // (1,0) cross-blocks are zero in Step 1.
1402 const double dt_n = args.scalar<double>("dt");
1403 // gas-phase density (linear EOS rho_n*p_n/p_ref_n when p_ref_n>0,
1404 // constant rho_n otherwise). c_n = drho_n/dp_n is constant for the
1405 // linear EOS and drives the new (1,1)/(1,0) compressibility terms.
1406 const double rho_n = args.scalar<double>("rho_n");
1407 const double p_ref_n = args.scalar<double>("p_ref_n");
1408 const bool rho_n_compressible = (p_ref_n > 0.0);
1409 const double c_n = rho_n_compressible ? (rho_n / p_ref_n) : 0.0;
1410 xt::pyarray<int> &csrRowIndeces_n_n = args.array<int>("csrRowIndeces_n_n");
1411 // (1,0) cross-block CSR maps for the gas-eq diffusion
1412 // against grad u_w. Currently allocated by the framework but unused by
1413 // the C++ assembly - 3c.2/3c.3 will write into them.
1414 xt::pyarray<int> &csrRowIndeces_n_w = args.array<int>("csrRowIndeces_n_w");
1415 xt::pyarray<int> &csrColumnOffsets_n_n = args.array<int>("csrColumnOffsets_n_n");
1416 xt::pyarray<int> &csrColumnOffsets_n_w = args.array<int>("csrColumnOffsets_n_w");
1417 // Exterior-boundary CSR column offsets for the comp-1 boundary Jacobian
1418 // loop ported from calculateResidual_entropy_viscosity (STAB=0 comp-1
1419 // Dirichlet enforcement). Added to getJacobian's argsDict in Python.
1420 xt::pyarray<int> &csrColumnOffsets_eb_n_n = args.array<int>("csrColumnOffsets_eb_n_n");
1421 xt::pyarray<int> &csrColumnOffsets_eb_n_w = args.array<int>("csrColumnOffsets_eb_n_w");
1422 // (0,1) cross-block CSR maps for the wetting eq.
1423 // J_{wv,ij} = (dm/du_n)*N_i*N_j + (df/du_n)*grad N_i*N_j
1424 // + (da/du_n)*grad u_w*grad N_i*N_j
1425 // The wetting equation row index is offset_u + stride_u * dof, and the column
1426 // index is offset_n + stride_n * dof.
1427 xt::pyarray<int> &csrRowIndeces_w_n = args.array<int>("csrRowIndeces_w_n");
1428 xt::pyarray<int> &csrColumnOffsets_w_n = args.array<int>("csrColumnOffsets_w_n");
1429 // (0,1) cross-block boundary CSR for the wetting-eq
1430 // exterior-flux Jacobian.
1431 xt::pyarray<int> &csrColumnOffsets_eb_w_n = args.array<int>("csrColumnOffsets_eb_w_n");
1432 // PSK closure selector for evaluateCoefficients (read from argsDict).
1433 PSK_TYPE_member = args.scalar<int>("PSK_TYPE");
1434 immiscible_member = (args.scalar<int>("immiscible") != 0);
1435 T_C_member = args.scalar<double>("T_C"); // temperature [degC] from input
1436 int nExteriorElementBoundaries_global = args.scalar<int>("nExteriorElementBoundaries_global");
1437 xt::pyarray<int> &exteriorElementBoundariesArray = args.array<int>("exteriorElementBoundariesArray");
1438 xt::pyarray<int> &elementBoundaryElementsArray = args.array<int>("elementBoundaryElementsArray");
1439 xt::pyarray<int> &elementBoundaryLocalElementBoundariesArray = args.array<int>("elementBoundaryLocalElementBoundariesArray");
1440 xt::pyarray<double> &ebqe_velocity_ext = args.array<double>("ebqe_velocity_ext");
1441 xt::pyarray<int> &isDOFBoundary_u = args.array<int>("isDOFBoundary_u");
1442 xt::pyarray<double> &ebqe_bc_u_ext = args.array<double>("ebqe_bc_u_ext");
1443 // component-1 (S_n) boundary arrays.
1444 xt::pyarray<int> &isDOFBoundary_n = args.array<int>("isDOFBoundary_n");
1445 xt::pyarray<double> &ebqe_bc_u_n_ext = args.array<double>("ebqe_bc_u_n_ext");
1446 xt::pyarray<int> &isFluxBoundary_u = args.array<int>("isFluxBoundary_u");
1447 xt::pyarray<double> &ebqe_bc_flux_ext = args.array<double>("ebqe_bc_flux_ext");
1448 xt::pyarray<int> &csrColumnOffsets_eb_u_u = args.array<int>("csrColumnOffsets_eb_u_u");
1449 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
1450 assert(a_rowptr.data()[nSpace] == nnz);
1451 assert(a_rowptr.data()[nSpace] == nSpace);
1452 double Ct_sge = 4.0;
1453
1454 //
1455 //loop over elements to compute volume integrals and load them into the element Jacobians and global Jacobian
1456 //
1457 for (int eN = 0; eN < nElements_global; eN++) {
1458 double elementJacobian_u_u[nDOF_test_element][nDOF_trial_element];
1459 // (0,1) cross-block element storage.
1460 double elementJacobian_u_n[nDOF_test_element][nDOF_trial_element];
1461 for (int i = 0; i < nDOF_test_element; i++) {
1462 for (int j = 0; j < nDOF_trial_element; j++) {
1463 elementJacobian_u_u[i][j] = 0.0;
1464 elementJacobian_u_n[i][j] = 0.0;
1465 }
1466 }
1467 for (int k = 0; k < nQuadraturePoints_element; k++) {
1468 int eN_k = eN * nQuadraturePoints_element + k, //index to a scalar at a quadrature point
1469 eN_k_nSpace = eN_k * nSpace,
1470 eN_nDOF_trial_element = eN * nDOF_trial_element; //index to a vector at a quadrature point
1471
1472 //declare local storage
1473 double u = 0.0, grad_u[nSpace], m = 0.0, dm = 0.0, f[nSpace], df[nSpace], a[nnz], da[nnz], as[nnz], m_t = 0.0, dm_t = 0.0, dpdeResidual_u_u[nDOF_trial_element], Lstar_u[nDOF_test_element], dsubgridError_u_u[nDOF_trial_element], tau = 0.0, tau0 = 0.0, tau1 = 0.0, jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], u_grad_trial[nDOF_trial_element * nSpace], dV, u_test_dV[nDOF_test_element], u_grad_test_dV[nDOF_test_element * nSpace], x, y, z, xt, yt, zt, G[nSpace * nSpace], G_dd_G, tr_G;
1474 //
1475 //calculate solution and gradients at quadrature points
1476 //
1477 //get jacobian, etc for mapping reference element
1478 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), mesh_grad_trial_ref.data(), jac, jacDet, jacInv, x, y, z);
1479 ck.calculateMappingVelocity_element(eN, k, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), xt, yt, zt);
1480 //get the physical integration weight
1481 dV = fabs(jacDet) * dV_ref.data()[k];
1482 ck.calculateG(jacInv, G, G_dd_G, tr_G);
1483 //get the trial function gradients
1484 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace], jacInv, u_grad_trial);
1485 //get the solution
1486 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_ref.data()[k * nDOF_trial_element], u);
1487 //get the solution gradients
1488 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial, grad_u);
1489 //precalculate test function products with integration weights
1490 for (int j = 0; j < nDOF_trial_element; j++) {
1491 u_test_dV[j] = u_test_ref.data()[k * nDOF_trial_element + j] * dV;
1492 for (int I = 0; I < nSpace; I++) {
1493 u_grad_test_dV[j * nSpace + I] = u_grad_trial[j * nSpace + I] * dV; //cek warning won't work for Petrov-Galerkin
1494 }
1495 }
1496 //
1497 // Compositional (p,z) H2O-equation Jacobian (P3c). Residual:
1498 // R^w_i = (phi*N*(1-z))_t * N_i dV + VMS num.diff - F_0.gradN_i dV,
1499 // F_0 = rho_g*(1-Y)*u_g + rho_a*(1-X)*u_a. Differentiate wrt the
1500 // primaries p=u_w (->(0,0)) and z=u_n (->(0,1)) and their gradients,
1501 // chain-ruled through the analytic flash. FD-verified in
1502 // flux_jac_test.cpp. (u is p, grad_u is grad p at this QP.)
1503 //
1504 const int mat_eN0 = elementMaterialTypes.data()[eN];
1505 const double alpha_eN0 = alpha.data()[mat_eN0];
1506 const double n_vg_eN0 = n.data()[mat_eN0];
1507 const double krn_end_eN0 = krn_end.data()[mat_eN0];
1508 const double *KWs_eN0 = &KWs.data()[mat_eN0 * nnz];
1509 const double phi_eN0 = thetaR.data()[mat_eN0] + thetaSR.data()[mat_eN0];
1510 const double S_wr_loc0 = thetaR.data()[mat_eN0] / phi_eN0;
1511 const double one_m_Sr0 = 1.0 - S_wr_loc0;
1512 const double Se_trap_L1547 = 1.0 - S_gr.data()[mat_eN0] / one_m_Sr0; // gas-only residual trapping
1513 double u_n_qp = 0.0, grad_u_n[nSpace];
1514 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1515 &u_trial_ref.data()[k * nDOF_trial_element], u_n_qp);
1516 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1517 u_grad_trial, grad_u_n);
1518 const double z_cl0 = fmin(fmax(u_n_qp, 1.0e-8), 1.0 - 1.0e-8);
1519 const double p_cl0 = fmax(u, 1.0e2);
1522 const double S_g0 = fs0.S_g, Sa0 = 1.0 - S_g0;
1523 // accumulation m_0 = phi*N*(1-z), N = rho_g*S_g + rho_a*S_a:
1524 const double N0 = fs0.rho_g*S_g0 + fs0.rho_a*Sa0;
1525 const double dN0_dp = fs0.drho_g_dp*S_g0 + fs0.rho_g*fs0.dS_g_dp
1526 + fs0.drho_a_dp*Sa0 - fs0.rho_a*fs0.dS_g_dp;
1527 const double dN0_dz = fs0.drho_g_dz*S_g0 + fs0.rho_g*fs0.dS_g_dz
1528 + fs0.drho_a_dz*Sa0 - fs0.rho_a*fs0.dS_g_dz;
1529 const double dm0_dp = phi_eN0 * dN0_dp * (1.0 - z_cl0);
1530 const double dm0_dz = phi_eN0 * (dN0_dz * (1.0 - z_cl0) - N0);
1531 // wetting effective saturation from the flash saturation + (p,z) derivs.
1532 const double Se_raw0 = (Sa0 - S_wr_loc0) / one_m_Sr0;
1533 double Se_a0, dSe0_dp, dSe0_dz;
1534 if (Se_raw0 <= 0.0) { Se_a0 = 0.0; dSe0_dp = 0.0; dSe0_dz = 0.0; }
1535 else if (Se_raw0 >= 1.0) { Se_a0 = 1.0; dSe0_dp = 0.0; dSe0_dz = 0.0; }
1536 else { Se_a0 = Se_raw0; dSe0_dp = -fs0.dS_g_dp/one_m_Sr0; dSe0_dz = -fs0.dS_g_dz/one_m_Sr0; }
1537 double KWr0=0.0, DKWr0=0.0, thW0=0.0, DthW0=0.0, KNr0=0.0, DKNr0=0.0;
1538 double pc0=0.0, dpc_dSe0=0.0, d2pc0=0.0;
1539 if (PSK_TYPE_member == 1) {
1540 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0,
1541 thetaR.data()[mat_eN0], thetaSR.data()[mat_eN0], thW0, DthW0, KWr0, DKWr0);
1542 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0, KNr0, DKNr0, Se_trap_L1547);
1543 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_a0, alpha_eN0, n_vg_eN0, pc0, dpc_dSe0, d2pc0);
1544 } else {
1545 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0,
1546 thetaR.data()[mat_eN0], thetaSR.data()[mat_eN0], thW0, DthW0, KWr0, DKWr0);
1547 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_a0, alpha_eN0, n_vg_eN0, KNr0, DKNr0, Se_trap_L1547);
1548 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_a0, alpha_eN0, n_vg_eN0, pc0, dpc_dSe0, d2pc0);
1549 }
1550 KNr0 *= krn_end_eN0; DKNr0 *= krn_end_eN0;
1551 const double pcp0 = dpc_dSe0 / one_m_Sr0; // pc'(S_a)
1552 const double dpcp0_dp = (d2pc0 / one_m_Sr0) * dSe0_dp;
1553 const double dpcp0_dz = (d2pc0 / one_m_Sr0) * dSe0_dz;
1555 const double Mbar_g0 = fs0.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fs0.Y)*::m_comp_co2::eos::M_H2O_KG;
1556 const double Mbar_a0 = fs0.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fs0.X)*::m_comp_co2::eos::M_H2O_KG;
1557 const double rho_g_mass0 = fs0.rho_g*Mbar_g0, rho_a_mass0 = fs0.rho_a*Mbar_a0;
1558 const double drgm0_dp = fs0.drho_g_dp*Mbar_g0 + fs0.rho_g*fs0.dY_dp*dMm0;
1559 const double drgm0_dz = fs0.rho_g*fs0.dY_dz*dMm0;
1560 const double dram0_dp = fs0.drho_a_dp*Mbar_a0 + fs0.rho_a*fs0.dX_dp*dMm0;
1561 const double dram0_dz = fs0.drho_a_dz*Mbar_a0 + fs0.rho_a*fs0.dX_dz*dMm0;
1562 // H2O transport coefficients Ag=rho_g*(1-Y), Aa=rho_a*(1-X) + (p,z) derivs.
1563 const double Ag0 = fs0.rho_g*(1.0-fs0.Y), Aa0 = fs0.rho_a*(1.0-fs0.X);
1564 const double dAg0_dp = fs0.drho_g_dp*(1.0-fs0.Y) - fs0.rho_g*fs0.dY_dp;
1565 const double dAg0_dz = - fs0.rho_g*fs0.dY_dz;
1566 const double dAa0_dp = fs0.drho_a_dp*(1.0-fs0.X) - fs0.rho_a*fs0.dX_dp;
1567 const double dAa0_dz = fs0.drho_a_dz*(1.0-fs0.X) - fs0.rho_a*fs0.dX_dz;
1568 // per-direction Darcy velocities + value-block partials (gradients fixed).
1569 double ug0[nSpace], ua0[nSpace];
1570 double dug0_dp[nSpace], dug0_dz[nSpace], dua0_dp[nSpace], dua0_dz[nSpace];
1571 for (int I = 0; I < nSpace; I++) {
1572 double ugI=0.0, uaI=0.0, dugp=0.0, dugz=0.0, duap=0.0, duaz=0.0;
1573 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
1574 const int J = a_colind.data()[ii];
1575 const double Kii = KWs_eN0[ii];
1576 const double Mob_g = KNr0*Kii/mu_n, Mob_a = KWr0*Kii;
1577 const double dMobg_dp = (DKNr0*Kii/mu_n)*dSe0_dp, dMobg_dz = (DKNr0*Kii/mu_n)*dSe0_dz;
1578 const double dMoba_dp = (DKWr0*Kii)*dSe0_dp, dMoba_dz = (DKWr0*Kii)*dSe0_dz;
1579 const double gJ = gravity.data()[J];
1580 const double gradSa = -(fs0.dS_g_dp*grad_u[J] + fs0.dS_g_dz*grad_u_n[J]);
1581 const double gp_a = grad_u[J] - rho_a_mass0*gJ;
1582 const double gp_g = grad_u[J] + pcp0*gradSa - rho_g_mass0*gJ;
1583 ugI -= Mob_g*gp_g; uaI -= Mob_a*gp_a;
1584 const double dgradSa_dp = -(fs0.d2S_g_dp2 *grad_u[J] + fs0.d2S_g_dpdz*grad_u_n[J]);
1585 const double dgradSa_dz = -(fs0.d2S_g_dpdz*grad_u[J] + fs0.d2S_g_dz2 *grad_u_n[J]);
1586 const double dgpg_dp = dpcp0_dp*gradSa + pcp0*dgradSa_dp - drgm0_dp*gJ;
1587 const double dgpg_dz = dpcp0_dz*gradSa + pcp0*dgradSa_dz - drgm0_dz*gJ;
1588 dugp -= dMobg_dp*gp_g + Mob_g*dgpg_dp;
1589 dugz -= dMobg_dz*gp_g + Mob_g*dgpg_dz;
1590 duap -= dMoba_dp*gp_a + Mob_a*(-dram0_dp*gJ);
1591 duaz -= dMoba_dz*gp_a + Mob_a*(-dram0_dz*gJ);
1592 }
1593 ug0[I]=ugI; ua0[I]=uaI;
1594 dug0_dp[I]=dugp; dug0_dz[I]=dugz; dua0_dp[I]=duap; dua0_dz[I]=duaz;
1595 }
1596 // Assemble (0,0) [d/dp] and (0,1) [d/dz].
1597 // dR^w_i/d(p_j) = alphaBDF*dm0/dp * N_j N_i dV
1598 // - [ (dF_0/dp)_val N_j + (dF_0/dgrad_p).gradN_j ].gradN_i dV
1599 // + VMS num.diff
1600 for (int i = 0; i < nDOF_test_element; i++) {
1601 const int i_nSpace = i * nSpace;
1602 double Sval_p = 0.0, Sval_z = 0.0;
1603 for (int I = 0; I < nSpace; I++) {
1604 const double gNiI = u_grad_trial[i_nSpace + I];
1605 Sval_p += (dAg0_dp*ug0[I] + Ag0*dug0_dp[I] + dAa0_dp*ua0[I] + Aa0*dua0_dp[I]) * gNiI;
1606 Sval_z += (dAg0_dz*ug0[I] + Ag0*dug0_dz[I] + dAa0_dz*ua0[I] + Aa0*dua0_dz[I]) * gNiI;
1607 }
1608 for (int j = 0; j < nDOF_trial_element; j++) {
1609 const int j_nSpace = j * nSpace;
1610 const double trial_j = u_trial_ref.data()[k * nDOF_trial_element + j];
1611 double Sgrad_p = 0.0, Sgrad_z = 0.0;
1612 for (int I = 0; I < nSpace; I++) {
1613 const double gNiI = u_grad_trial[i_nSpace + I];
1614 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
1615 const int J = a_colind.data()[ii];
1616 const double Kii = KWs_eN0[ii];
1617 const double Mob_g = KNr0*Kii/mu_n, Mob_a = KWr0*Kii;
1618 const double gNjJ = u_grad_trial[j_nSpace + J];
1619 const double dFdgp = Ag0*(-Mob_g*(1.0 - pcp0*fs0.dS_g_dp)) + Aa0*(-Mob_a);
1620 const double dFdgz = Ag0*(-Mob_g*(-pcp0*fs0.dS_g_dz));
1621 Sgrad_p += dFdgp * gNjJ * gNiI;
1622 Sgrad_z += dFdgz * gNjJ * gNiI;
1623 }
1624 }
1625 elementJacobian_u_u[i][j] += alphaBDF * dm0_dp * trial_j * u_test_dV[i];
1626 elementJacobian_u_u[i][j] -= (Sval_p * trial_j + Sgrad_p) * dV;
1627 elementJacobian_u_u[i][j] += VMS * ck.NumericalDiffusionJacobian(
1628 q_numDiff_u_last[eN_k], &u_grad_trial[j_nSpace], &u_grad_test_dV[i_nSpace]);
1629 elementJacobian_u_n[i][j] += alphaBDF * dm0_dz * trial_j * u_test_dV[i];
1630 elementJacobian_u_n[i][j] -= (Sval_z * trial_j + Sgrad_z) * dV;
1631 }
1632 }
1633 } //k
1634 //
1635 //load into element Jacobian into global Jacobian
1636 //
1637 for (int i = 0; i < nDOF_test_element; i++) {
1638 int eN_i = eN * nDOF_test_element + i;
1639 for (int j = 0; j < nDOF_trial_element; j++) {
1640 int eN_i_j = eN_i * nDOF_trial_element + j;
1641 globalJacobian.data()[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_u_u[eN_i_j]] += elementJacobian_u_u[i][j];
1642 // load (0,1) cross-block coupling the wetting eq to S_n through
1643 // dm/du_n, df/du_n, da/du_n.
1644 globalJacobian.data()[csrRowIndeces_w_n.data()[eN_i] + csrColumnOffsets_w_n.data()[eN_i_j]] += elementJacobian_u_n[i][j];
1645 } //j
1646 } //i
1647 } //elements
1648 //
1649 //loop over exterior element boundaries to compute the surface integrals and load them into the global Jacobian
1650 //
1651 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
1652 int ebN = exteriorElementBoundariesArray.data()[ebNE];
1653 int eN = elementBoundaryElementsArray.data()[ebN * 2 + 0], ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0], eN_nDOF_trial_element = eN * nDOF_trial_element;
1654 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
1655 int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb, ebNE_kb_nSpace = ebNE_kb * nSpace, ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb, ebN_local_kb_nSpace = ebN_local_kb * nSpace;
1656
1657 double u_ext = 0.0, grad_u_ext[nSpace], m_ext = 0.0, dm_ext = 0.0, f_ext[nSpace], df_ext[nSpace], a_ext[nnz], da_ext[nnz], as_ext[nnz], dflux_u_u_ext = 0.0, bc_u_ext = 0.0,
1658 //bc_grad_u_ext[nSpace],
1659 bc_m_ext = 0.0, bc_dm_ext = 0.0, bc_f_ext[nSpace], bc_df_ext[nSpace], bc_a_ext[nnz], bc_da_ext[nnz], bc_as_ext[nnz], fluxJacobian_u_u[nDOF_trial_element], jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace], boundaryJac[nSpace * (nSpace - 1)], metricTensor[(nSpace - 1) * (nSpace - 1)], metricTensorDetSqrt, dS, u_test_dS[nDOF_test_element], u_grad_trial_trace[nDOF_trial_element * nSpace], normal[3], x_ext, y_ext, z_ext, xt_ext, yt_ext, zt_ext, integralScaling, G[nSpace * nSpace], G_dd_G, tr_G;
1660 //
1661 //calculate the solution and gradients at quadrature points
1662 //
1663 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(), jac_ext, jacDet_ext, jacInv_ext, boundaryJac, metricTensor, metricTensorDetSqrt,
1664 normal_ref.data(), normal, x_ext, y_ext, z_ext);
1665 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), xt_ext, yt_ext, zt_ext, normal, boundaryJac, metricTensor, integralScaling);
1666 dS = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt + MOVING_DOMAIN * integralScaling) * dS_ref.data()[kb];
1667 ck.calculateG(jacInv_ext, G, G_dd_G, tr_G);
1668 //compute shape and solution information
1669 //shape
1670 ck.gradTrialFromRef(&u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element], jacInv_ext, u_grad_trial_trace);
1671 //solution and gradients
1672 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_ext);
1673 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial_trace, grad_u_ext);
1674 //precalculate test function products with integration weights
1675 for (int j = 0; j < nDOF_trial_element; j++) { u_test_dS[j] = u_test_trace_ref.data()[ebN_local_kb * nDOF_test_element + j] * dS; }
1676 //
1677 //load the boundary values
1678 //
1679 bc_u_ext = isDOFBoundary_u.data()[ebNE_kb] * ebqe_bc_u_ext.data()[ebNE_kb] + (1 - isDOFBoundary_u.data()[ebNE_kb]) * u_ext;
1680 //
1681 //calculate the internal and external trace of the pde coefficients
1682 //
1683 double Kr, dKr, thetaW, thetaW_bc;
1684 const double rho_ext = ebqe_rho.data()[ebNE_kb];
1685 // Boundary closure for the wetting-equation Jacobian. (0,1) boundary
1686 // contribution is not assembled here.
1687 double dm_du_n_ext = 0.0, dkr_du_n_ext = 0.0;
1688 double df_du_n_ext[nSpace];
1689 double da_du_n_ext[nnz];
1690 double bc_dm_du_n = 0.0, bc_dkr_du_n = 0.0;
1691 double bc_df_du_n[nSpace];
1692 double bc_da_du_n[nnz];
1693 for (int I = 0; I < nSpace; I++) { df_du_n_ext[I] = 0.0; bc_df_du_n[I] = 0.0; }
1694 for (int ii = 0; ii < nnz; ii++) { da_du_n_ext[ii] = 0.0; bc_da_du_n[ii] = 0.0; }
1695 // u_n_ext_qp & bc_u_n_ext_qp held at outer scope for the (0,1) boundary
1696 // Jacobian assembly below.
1697 double u_n_ext_qp_outer = 0.0;
1698 double bc_u_n_ext_qp_outer = 0.0;
1699 {
1700 double u_n_ext_qp = 0.0;
1701 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1702 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_n_ext_qp);
1703 const double bc_u_n_ext_qp = isDOFBoundary_n.data()[ebNE_kb] * ebqe_bc_u_n_ext.data()[ebNE_kb]
1704 + (1 - isDOFBoundary_n.data()[ebNE_kb]) * u_n_ext_qp;
1705 u_n_ext_qp_outer = u_n_ext_qp;
1706 bc_u_n_ext_qp_outer = bc_u_n_ext_qp;
1707 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
1708 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
1709 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
1710 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], u_ext, u_n_ext_qp,
1711 m_ext, dm_ext, dm_du_n_ext, f_ext, df_ext, df_du_n_ext, a_ext, da_ext, da_du_n_ext,
1712 as_ext, Kr, dKr, dkr_du_n_ext, thetaW);
1713 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
1714 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
1715 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
1716 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], bc_u_ext, bc_u_n_ext_qp,
1717 bc_m_ext, bc_dm_ext, bc_dm_du_n, bc_f_ext, bc_df_ext, bc_df_du_n, bc_a_ext, bc_da_ext, bc_da_du_n,
1718 bc_as_ext, Kr, dKr, bc_dkr_du_n, thetaW_bc);
1719 }
1720 //
1721 // ===== P3c boundary: compositional comp-0 (H2O) flux Jacobian =====
1722 // d(flux_0)/d(p_j) -> fluxJacobian_u_u, d(flux_0)/d(z_j) -> fluxJacobian_u_n,
1723 // where flux_0 = F_0.n + penalty*(p - p_BC). Mirrors the interior comp-0
1724 // flux Jacobian (grad N_i -> normal, + penalty*trial_j); the F_0 value/grad
1725 // partials are FD-verified (boundary0_test.cpp / flux_jac_test.cpp).
1726 // Nonzero only on Dirichlet-p faces; no-flow faces contribute 0.
1727 double fluxJacobian_u_n[nDOF_trial_element];
1728 for (int j = 0; j < nDOF_trial_element; j++) { fluxJacobian_u_u[j] = 0.0; fluxJacobian_u_n[j] = 0.0; }
1729 if (isDOFBoundary_u.data()[ebNE_kb]) {
1730 double grad_u_n_ext[nSpace];
1731 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
1732 u_grad_trial_trace, grad_u_n_ext);
1733 const int mat_b = elementMaterialTypes.data()[eN];
1734 const double alpha_b = alpha.data()[mat_b];
1735 const double n_vg_b = n.data()[mat_b];
1736 const double krn_end_b = krn_end.data()[mat_b];
1737 const double *KWs_b = &KWs.data()[mat_b * nnz];
1738 const double phi_b = thetaR.data()[mat_b] + thetaSR.data()[mat_b];
1739 const double S_wr_b = thetaR.data()[mat_b] / phi_b;
1740 const double one_m_Sr_b = 1.0 - S_wr_b;
1741 const double Se_trap_L1775 = 1.0 - S_gr.data()[mat_b] / one_m_Sr_b; // gas-only residual trapping
1742 const double z_clb = fmin(fmax(u_n_ext_qp_outer, 1.0e-8), 1.0 - 1.0e-8);
1743 const double p_clb = fmax(u_ext, 1.0e2);
1746 const double Sab = 1.0 - fsb.S_g;
1747 const double Se_rawb = (Sab - S_wr_b)/one_m_Sr_b;
1748 double Se_ab, dSeb_dp, dSeb_dz;
1749 if (Se_rawb<=0.0){Se_ab=0.0;dSeb_dp=0.0;dSeb_dz=0.0;}
1750 else if (Se_rawb>=1.0){Se_ab=1.0;dSeb_dp=0.0;dSeb_dz=0.0;}
1751 else {Se_ab=Se_rawb;dSeb_dp=-fsb.dS_g_dp/one_m_Sr_b;dSeb_dz=-fsb.dS_g_dz/one_m_Sr_b;}
1752 double KWrb=0,DKWrb=0,thWb=0,DthWb=0,KNrb=0,DKNrb=0,pcb=0,dpc_dSeb=0,d2pcb=0;
1753 if (PSK_TYPE_member == 1) {
1754 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_ab, alpha_b, n_vg_b, thetaR.data()[mat_b], thetaSR.data()[mat_b], thWb, DthWb, KWrb, DKWrb);
1755 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_ab, alpha_b, n_vg_b, KNrb, DKNrb, Se_trap_L1775);
1756 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_ab, alpha_b, n_vg_b, pcb, dpc_dSeb, d2pcb);
1757 } else {
1758 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_ab, alpha_b, n_vg_b, thetaR.data()[mat_b], thetaSR.data()[mat_b], thWb, DthWb, KWrb, DKWrb);
1759 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_ab, alpha_b, n_vg_b, KNrb, DKNrb, Se_trap_L1775);
1760 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_ab, alpha_b, n_vg_b, pcb, dpc_dSeb, d2pcb);
1761 }
1762 KNrb *= krn_end_b; DKNrb *= krn_end_b;
1763 const double pcpb = dpc_dSeb / one_m_Sr_b;
1764 const double dpcpb_dp = (d2pcb / one_m_Sr_b) * dSeb_dp;
1765 const double dpcpb_dz = (d2pcb / one_m_Sr_b) * dSeb_dz;
1767 const double Mbar_gb = fsb.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.Y)*::m_comp_co2::eos::M_H2O_KG;
1768 const double Mbar_ab = fsb.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.X)*::m_comp_co2::eos::M_H2O_KG;
1769 const double rgmb = fsb.rho_g*Mbar_gb, ramb = fsb.rho_a*Mbar_ab;
1770 const double drgmb_dp = fsb.drho_g_dp*Mbar_gb + fsb.rho_g*fsb.dY_dp*dMmb;
1771 const double drgmb_dz = fsb.rho_g*fsb.dY_dz*dMmb;
1772 const double dramb_dp = fsb.drho_a_dp*Mbar_ab + fsb.rho_a*fsb.dX_dp*dMmb;
1773 const double dramb_dz = fsb.drho_a_dz*Mbar_ab + fsb.rho_a*fsb.dX_dz*dMmb;
1774 const double Agb = fsb.rho_g*(1.0-fsb.Y), Aab = fsb.rho_a*(1.0-fsb.X);
1775 const double dAgb_dp = fsb.drho_g_dp*(1.0-fsb.Y) - fsb.rho_g*fsb.dY_dp;
1776 const double dAgb_dz = - fsb.rho_g*fsb.dY_dz;
1777 const double dAab_dp = fsb.drho_a_dp*(1.0-fsb.X) - fsb.rho_a*fsb.dX_dp;
1778 const double dAab_dz = fsb.drho_a_dz*(1.0-fsb.X) - fsb.rho_a*fsb.dX_dz;
1779 double ugb[nSpace], uab[nSpace], dugb_dp[nSpace], dugb_dz[nSpace], duab_dp[nSpace], duab_dz[nSpace];
1780 for (int I=0;I<nSpace;I++){
1781 double ugI=0,uaI=0,dugp=0,dugz=0,duap=0,duaz=0;
1782 for (int ii=a_rowptr.data()[I];ii<a_rowptr.data()[I+1];ii++){
1783 const int J=a_colind.data()[ii];
1784 const double Kii=KWs_b[ii];
1785 const double Mob_g=KNrb*Kii/mu_n, Mob_a=KWrb*Kii;
1786 const double dMobg_dp=(DKNrb*Kii/mu_n)*dSeb_dp, dMobg_dz=(DKNrb*Kii/mu_n)*dSeb_dz;
1787 const double dMoba_dp=(DKWrb*Kii)*dSeb_dp, dMoba_dz=(DKWrb*Kii)*dSeb_dz;
1788 const double gJ=gravity.data()[J];
1789 const double gradSa=-(fsb.dS_g_dp*grad_u_ext[J]+fsb.dS_g_dz*grad_u_n_ext[J]);
1790 const double gp_a=grad_u_ext[J]-ramb*gJ;
1791 const double gp_g=grad_u_ext[J]+pcpb*gradSa-rgmb*gJ;
1792 ugI-=Mob_g*gp_g; uaI-=Mob_a*gp_a;
1793 const double dgradSa_dp=-(fsb.d2S_g_dp2*grad_u_ext[J]+fsb.d2S_g_dpdz*grad_u_n_ext[J]);
1794 const double dgradSa_dz=-(fsb.d2S_g_dpdz*grad_u_ext[J]+fsb.d2S_g_dz2*grad_u_n_ext[J]);
1795 const double dgpg_dp=dpcpb_dp*gradSa+pcpb*dgradSa_dp-drgmb_dp*gJ;
1796 const double dgpg_dz=dpcpb_dz*gradSa+pcpb*dgradSa_dz-drgmb_dz*gJ;
1797 dugp-=dMobg_dp*gp_g+Mob_g*dgpg_dp;
1798 dugz-=dMobg_dz*gp_g+Mob_g*dgpg_dz;
1799 duap-=dMoba_dp*gp_a+Mob_a*(-dramb_dp*gJ);
1800 duaz-=dMoba_dz*gp_a+Mob_a*(-dramb_dz*gJ);
1801 }
1802 ugb[I]=ugI;uab[I]=uaI;dugb_dp[I]=dugp;dugb_dz[I]=dugz;duab_dp[I]=duap;duab_dz[I]=duaz;
1803 }
1804 const double penb = ebqe_penalty_ext.data()[ebNE_kb];
1805 for (int j=0;j<nDOF_trial_element;j++){
1806 const int j_nSpace = j*nSpace;
1807 const double trial_j = u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element + j];
1808 double accp=0.0, accz=0.0;
1809 for (int I=0;I<nSpace;I++){
1810 const double nI = normal[I];
1811 const double valp = dAgb_dp*ugb[I]+Agb*dugb_dp[I]+dAab_dp*uab[I]+Aab*duab_dp[I];
1812 const double valz = dAgb_dz*ugb[I]+Agb*dugb_dz[I]+dAab_dz*uab[I]+Aab*duab_dz[I];
1813 double gradp=0.0, gradz=0.0;
1814 for (int ii=a_rowptr.data()[I];ii<a_rowptr.data()[I+1];ii++){
1815 const int J=a_colind.data()[ii];
1816 const double Kii=KWs_b[ii];
1817 const double Mob_g=KNrb*Kii/mu_n, Mob_a=KWrb*Kii;
1818 const double gNjJ = u_grad_trial_trace[j_nSpace + J];
1819 const double dFdgp = Agb*(-Mob_g*(1.0 - pcpb*fsb.dS_g_dp)) + Aab*(-Mob_a);
1820 const double dFdgz = Agb*(-Mob_g*(-pcpb*fsb.dS_g_dz));
1821 gradp += dFdgp*gNjJ;
1822 gradz += dFdgz*gNjJ;
1823 }
1824 accp += nI*(valp*trial_j + gradp);
1825 accz += nI*(valz*trial_j + gradz);
1826 }
1827 fluxJacobian_u_u[j] = accp + penb*trial_j;
1828 fluxJacobian_u_n[j] = accz;
1829 }
1830 }
1831 //
1832 //update the global Jacobian from the flux Jacobian
1833 //
1834 for (int i = 0; i < nDOF_test_element; i++) {
1835 int eN_i = eN * nDOF_test_element + i;
1836 for (int j = 0; j < nDOF_trial_element; j++) {
1837 int ebN_i_j = ebN * 4 * nDOF_test_X_trial_element + i * nDOF_trial_element + j;
1838 globalJacobian.data()[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_u_u[j] * u_test_dS[i];
1839 // (0,1) cross-block boundary contribution (nonzero only on
1840 // Dirichlet faces for u_w).
1841 globalJacobian.data()[csrRowIndeces_w_n.data()[eN_i] + csrColumnOffsets_eb_w_n.data()[ebN_i_j]] += fluxJacobian_u_n[j] * u_test_dS[i];
1842 } //j
1843 } //i
1844 } //kb
1845 } //ebNE
1846
1847 // ============================================================================
1848 // Compositional (p,z) component-1 (CO2) Jacobian -- accumulation + component
1849 // molar flux F_1 = rho_g*Y*u_g + rho_a*X*u_a, all flash-derived. u_w = p,
1850 // u_n = z (overall CO2 mole fraction; NOT a saturation).
1851 //
1852 // m_1 = phi*N*z, N = rho_g*S_g + rho_a*S_a (S_g, rho_a, rho_g from flash)
1853 //
1854 // J_(1,1)[i,j] = dm_1/dz/dt * N_i N_j dV
1855 // - [ (dF_1/dz)_val N_j + (dF_1/dgrad_z).gradN_j ].gradN_i dV
1856 // J_(1,0)[i,j] = dm_1/dp/dt * N_i N_j dV
1857 // - [ (dF_1/dp)_val N_j + (dF_1/dgrad_p).gradN_j ].gradN_i dV
1858 //
1859 // (0,1) cross-block (wetting eq dependence on z) is assembled in the element
1860 // loop above. Per-QP flux partials FD-verified in flux_jac_test.cpp.
1861 // ============================================================================
1862 for (int eN = 0; eN < nElements_global; eN++) {
1863 const int mat_eN = elementMaterialTypes.data()[eN];
1864 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
1865 const double alpha_eN = alpha.data()[mat_eN];
1866 const double krn_end_eN = krn_end.data()[mat_eN];
1867 const double n_vg_eN = n.data()[mat_eN];
1868 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
1869 // dm_n_du_n is no longer hoisted: ρ_n depends on the Newton iterate
1870 // through the linear EOS, so phi*ρ_n*S_n derivative is QP-local.
1871 double elementJacobian_n_n[nDOF_test_element][nDOF_trial_element];
1872 double elementJacobian_n_w[nDOF_test_element][nDOF_trial_element];
1873 for (int i = 0; i < nDOF_test_element; i++)
1874 for (int j = 0; j < nDOF_trial_element; j++) {
1875 elementJacobian_n_n[i][j] = 0.0;
1876 elementJacobian_n_w[i][j] = 0.0;
1877 }
1878 for (int k = 0; k < nQuadraturePoints_element; k++) {
1879 const int eN_k = eN * nQuadraturePoints_element + k;
1880 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
1881 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x_q, y_q, z_q;
1882 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
1883 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
1884 jac, jacDet, jacInv, x_q, y_q, z_q);
1885 const double dV = std::fabs(jacDet) * dV_ref.data()[k];
1886 // Trial gradients in physical coords.
1887 double u_grad_trial_qp[nDOF_trial_element * nSpace];
1888 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace],
1889 jacInv, u_grad_trial_qp);
1890 // Saturation u_n at QP.
1891 double u_n = 0.0;
1892 ck.valFromDOF(u_dof_n.data(),
1893 &u_l2g.data()[eN_nDOF_trial_element],
1894 &u_trial_ref.data()[k * nDOF_trial_element], u_n);
1895 // Wetting-pressure value (for linear EOS) and gradient at QP.
1896 double u_w_qp = 0.0;
1897 ck.valFromDOF(u_dof.data(),
1898 &u_l2g.data()[eN_nDOF_trial_element],
1899 &u_trial_ref.data()[k * nDOF_trial_element], u_w_qp);
1900 double grad_u_w[nSpace];
1901 ck.gradFromDOF(u_dof.data(),
1902 &u_l2g.data()[eN_nDOF_trial_element],
1903 u_grad_trial_qp, grad_u_w);
1904 // Step 3d: saturation gradient at QP for the capillary diffusion term.
1905 double grad_u_n[nSpace];
1906 ck.gradFromDOF(u_dof_n.data(),
1907 &u_l2g.data()[eN_nDOF_trial_element],
1908 u_grad_trial_qp, grad_u_n);
1909 // =====================================================================
1910 // Compositional (p,z) CO2-equation Jacobian (P3c). The flux is the
1911 // component molar flux F_1 = rho_g*Y*u_g + rho_a*X*u_a assembled in the
1912 // residual; here we differentiate it (and the accumulation m_1=phi*N*z)
1913 // wrt the primaries (p=u_w, z=u_n) AND their gradients, chain-ruled
1914 // through the analytic flash. Formulas FD-verified in flux_jac_test.cpp.
1915 // =====================================================================
1916 const double z_cl_j = fmin(fmax(u_n, 1.0e-8), 1.0 - 1.0e-8);
1917 const double p_cl_j = fmax(u_w_qp, 1.0e2);
1920 const double S_g_j = fs_j.S_g, Sa_j = 1.0 - S_g_j;
1921 // accumulation m_1 = phi*N*z, N = rho_g*S_g + rho_a*S_a:
1922 const double N_j = fs_j.rho_g*S_g_j + fs_j.rho_a*Sa_j;
1923 const double dN_dp_j = fs_j.drho_g_dp*S_g_j + fs_j.rho_g*fs_j.dS_g_dp
1924 + fs_j.drho_a_dp*Sa_j - fs_j.rho_a*fs_j.dS_g_dp;
1925 const double dN_dz_j = fs_j.drho_g_dz*S_g_j + fs_j.rho_g*fs_j.dS_g_dz
1926 + fs_j.drho_a_dz*Sa_j - fs_j.rho_a*fs_j.dS_g_dz;
1927 const double dm_n_du_n = phi_eN * (dN_dz_j * z_cl_j + N_j); // d m_1/dz
1928 const double dm_n_du_w = phi_eN * (dN_dp_j * z_cl_j); // d m_1/dp
1929 // wetting effective saturation from the FLASH saturation + (p,z) derivs.
1930 const double S_wr_loc = thetaR.data()[mat_eN] / phi_eN;
1931 const double one_m_Sr_loc = 1.0 - S_wr_loc;
1932 const double Se_trap_L1965 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_loc; // gas-only residual trapping
1933 const double Se_raw_j = (Sa_j - S_wr_loc) / one_m_Sr_loc;
1934 double Se_a, dSe_dp, dSe_dz;
1935 if (Se_raw_j <= 0.0) { Se_a = 0.0; dSe_dp = 0.0; dSe_dz = 0.0; }
1936 else if (Se_raw_j >= 1.0) { Se_a = 1.0; dSe_dp = 0.0; dSe_dz = 0.0; }
1937 else { Se_a = Se_raw_j; dSe_dp = -fs_j.dS_g_dp/one_m_Sr_loc; dSe_dz = -fs_j.dS_g_dz/one_m_Sr_loc; }
1938 double KWr_a=0.0, DKWr_a=0.0, thW_a=0.0, DthW_a=0.0, KNr_a=0.0, DKNr_a=0.0;
1939 double pc_a=0.0, dpc_dSe_a=0.0, d2pc_a=0.0;
1940 if (PSK_TYPE_member == 1) {
1941 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_a, alpha_eN, n_vg_eN,
1942 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_a, DthW_a, KWr_a, DKWr_a);
1943 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_a, alpha_eN, n_vg_eN, KNr_a, DKNr_a, Se_trap_L1965);
1944 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_a, alpha_eN, n_vg_eN, pc_a, dpc_dSe_a, d2pc_a);
1945 } else {
1946 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_a, alpha_eN, n_vg_eN,
1947 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_a, DthW_a, KWr_a, DKWr_a);
1948 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_a, alpha_eN, n_vg_eN, KNr_a, DKNr_a, Se_trap_L1965);
1949 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_a, alpha_eN, n_vg_eN, pc_a, dpc_dSe_a, d2pc_a);
1950 }
1951 KNr_a *= krn_end_eN; DKNr_a *= krn_end_eN;
1952 const double pcp_a = dpc_dSe_a / one_m_Sr_loc; // pc'(S_a)
1953 const double dpcp_dp = (d2pc_a / one_m_Sr_loc) * dSe_dp; // d pc'(S_a)/dp
1954 const double dpcp_dz = (d2pc_a / one_m_Sr_loc) * dSe_dz;
1955 // mass densities for gravity (molar density * mean molar mass) + derivs.
1957 const double Mbar_g = fs_j.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fs_j.Y)*::m_comp_co2::eos::M_H2O_KG;
1958 const double Mbar_a = fs_j.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fs_j.X)*::m_comp_co2::eos::M_H2O_KG;
1959 const double rho_g_mass = fs_j.rho_g*Mbar_g, rho_a_mass = fs_j.rho_a*Mbar_a;
1960 const double drgm_dp = fs_j.drho_g_dp*Mbar_g + fs_j.rho_g*fs_j.dY_dp*dMm;
1961 const double drgm_dz = fs_j.rho_g*fs_j.dY_dz*dMm;
1962 const double dram_dp = fs_j.drho_a_dp*Mbar_a + fs_j.rho_a*fs_j.dX_dp*dMm;
1963 const double dram_dz = fs_j.drho_a_dz*Mbar_a + fs_j.rho_a*fs_j.dX_dz*dMm;
1964 // CO2 transport coefficients Ag=rho_g*Y, Aa=rho_a*X + (p,z) derivatives.
1965 const double Ag = fs_j.rho_g*fs_j.Y, Aa = fs_j.rho_a*fs_j.X;
1966 const double dAg_dp = fs_j.drho_g_dp*fs_j.Y + fs_j.rho_g*fs_j.dY_dp;
1967 const double dAg_dz = fs_j.rho_g*fs_j.dY_dz;
1968 const double dAa_dp = fs_j.drho_a_dp*fs_j.X + fs_j.rho_a*fs_j.dX_dp;
1969 const double dAa_dz = fs_j.drho_a_dz*fs_j.X + fs_j.rho_a*fs_j.dX_dz;
1970 // per-direction Darcy velocities ug[I], ua[I] + their value-block partials
1971 // (gradients held fixed). Mobilities carry the 1/mu_n (gas) factor.
1972 double ug[nSpace], ua[nSpace];
1973 double dug_dp[nSpace], dug_dz[nSpace], dua_dp[nSpace], dua_dz[nSpace];
1974 for (int I = 0; I < nSpace; I++) {
1975 double ugI=0.0, uaI=0.0, dugp=0.0, dugz=0.0, duap=0.0, duaz=0.0;
1976 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
1977 const int J = a_colind.data()[ii];
1978 const double Kii = KWs_eN[ii];
1979 const double Mob_g = KNr_a*Kii/mu_n, Mob_a = KWr_a*Kii;
1980 const double dMobg_dp = (DKNr_a*Kii/mu_n)*dSe_dp, dMobg_dz = (DKNr_a*Kii/mu_n)*dSe_dz;
1981 const double dMoba_dp = (DKWr_a*Kii)*dSe_dp, dMoba_dz = (DKWr_a*Kii)*dSe_dz;
1982 const double gJ = gravity.data()[J];
1983 const double gradSa = -(fs_j.dS_g_dp*grad_u_w[J] + fs_j.dS_g_dz*grad_u_n[J]);
1984 const double gp_a = grad_u_w[J] - rho_a_mass*gJ;
1985 const double gp_g = grad_u_w[J] + pcp_a*gradSa - rho_g_mass*gJ;
1986 ugI -= Mob_g*gp_g; uaI -= Mob_a*gp_a;
1987 const double dgradSa_dp = -(fs_j.d2S_g_dp2 *grad_u_w[J] + fs_j.d2S_g_dpdz*grad_u_n[J]);
1988 const double dgradSa_dz = -(fs_j.d2S_g_dpdz*grad_u_w[J] + fs_j.d2S_g_dz2 *grad_u_n[J]);
1989 const double dgpg_dp = dpcp_dp*gradSa + pcp_a*dgradSa_dp - drgm_dp*gJ;
1990 const double dgpg_dz = dpcp_dz*gradSa + pcp_a*dgradSa_dz - drgm_dz*gJ;
1991 dugp -= dMobg_dp*gp_g + Mob_g*dgpg_dp;
1992 dugz -= dMobg_dz*gp_g + Mob_g*dgpg_dz;
1993 duap -= dMoba_dp*gp_a + Mob_a*(-dram_dp*gJ);
1994 duaz -= dMoba_dz*gp_a + Mob_a*(-dram_dz*gJ);
1995 }
1996 ug[I]=ugI; ua[I]=uaI;
1997 dug_dp[I]=dugp; dug_dz[I]=dugz; dua_dp[I]=duap; dua_dz[I]=duaz;
1998 }
1999 // (Kinetic R_diss dissolution sink removed -- dissolution is now handled
2000 // thermodynamically by the inline flash, so there is no sink Jacobian.)
2001 // Assemble per (i, j). Residual: R^c_i = m_1_t*N_i*dV - F_1.gradN_i*dV.
2002 // Flux Jacobian (chain rule through the flash):
2003 // dR^c_i/d(z_j) = -[ (dF_1/dz)_val * N_j + (dF_1/dgrad_z) . gradN_j ] . gradN_i dV -> (1,1)
2004 // dR^c_i/d(p_j) = -[ (dF_1/dp)_val * N_j + (dF_1/dgrad_p) . gradN_j ] . gradN_i dV -> (1,0)
2005 for (int i = 0; i < nDOF_test_element; i++) {
2006 const double test_i = u_test_ref.data()[k * nDOF_test_element + i];
2007 // value-block scalars: sum_I (dF_1/dval) * gradN_i[I] (val = p, z)
2008 double Sval_p = 0.0, Sval_z = 0.0;
2009 for (int I = 0; I < nSpace; I++) {
2010 const double gNiI = u_grad_trial_qp[i * nSpace + I];
2011 Sval_p += (dAg_dp*ug[I] + Ag*dug_dp[I] + dAa_dp*ua[I] + Aa*dua_dp[I]) * gNiI;
2012 Sval_z += (dAg_dz*ug[I] + Ag*dug_dz[I] + dAa_dz*ua[I] + Aa*dua_dz[I]) * gNiI;
2013 }
2014 for (int j = 0; j < nDOF_trial_element; j++) {
2015 const double trial_j = u_trial_ref.data()[k * nDOF_trial_element + j];
2016 // gradient-block scalars: sum_{I,ii} (dF_1[I]/dgrad_var[J]) gradN_j[J] gradN_i[I]
2017 double Sgrad_p = 0.0, Sgrad_z = 0.0;
2018 for (int I = 0; I < nSpace; I++) {
2019 const double gNiI = u_grad_trial_qp[i * nSpace + I];
2020 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
2021 const int J = a_colind.data()[ii];
2022 const double Kii = KWs_eN[ii];
2023 const double Mob_g = KNr_a*Kii/mu_n, Mob_a = KWr_a*Kii;
2024 const double gNjJ = u_grad_trial_qp[j * nSpace + J];
2025 // d ug[I]/d grad_p[J] = -Mob_g*(1 - pcp*dSg_dp); d ua[I]/d grad_p[J] = -Mob_a
2026 const double dFdgp = Ag*(-Mob_g*(1.0 - pcp_a*fs_j.dS_g_dp)) + Aa*(-Mob_a);
2027 // d ug[I]/d grad_z[J] = -Mob_g*(-pcp*dSg_dz); d ua[I]/d grad_z[J] = 0
2028 const double dFdgz = Ag*(-Mob_g*(-pcp_a*fs_j.dS_g_dz));
2029 Sgrad_p += dFdgp * gNjJ * gNiI;
2030 Sgrad_z += dFdgz * gNjJ * gNiI;
2031 }
2032 }
2033 // (1,1): accumulation (implicit BDF) + CO2 flux d/dz.
2034 elementJacobian_n_n[i][j] += (dm_n_du_n * test_i * trial_j * dV) / dt_n;
2035 elementJacobian_n_n[i][j] -= (Sval_z * trial_j + Sgrad_z) * dV;
2036 // (1,0): accumulation d/dp + CO2 flux d/dp.
2037 elementJacobian_n_w[i][j] += (dm_n_du_w * test_i * trial_j * dV) / dt_n;
2038 elementJacobian_n_w[i][j] -= (Sval_p * trial_j + Sgrad_p) * dV;
2039 }
2040 }
2041 }
2042 for (int i = 0; i < nDOF_test_element; i++) {
2043 const int eN_i = eN * nDOF_test_element + i;
2044 for (int j = 0; j < nDOF_trial_element; j++) {
2045 const int eN_i_j = eN_i * nDOF_trial_element + j;
2046 globalJacobian.data()[csrRowIndeces_n_n.data()[eN_i] + csrColumnOffsets_n_n.data()[eN_i_j]]
2047 += elementJacobian_n_n[i][j];
2048 globalJacobian.data()[csrRowIndeces_n_w.data()[eN_i] + csrColumnOffsets_n_w.data()[eN_i_j]]
2049 += elementJacobian_n_w[i][j];
2050 }
2051 }
2052 }
2053
2054 // ============================================================================
2055 // Comp-1 (CO2 / z) exterior boundary Jacobian -- STAB=0 path. COMPOSITIONAL.
2056 // Matching Jacobian for the compositional F_1.n boundary residual term in
2057 // calculateResidual. Mirrors the FD-verified interior comp-1 flux Jacobian
2058 // (~line 1942) chain-ruled through the analytic flash, with the interior
2059 // gradN_i replaced by the boundary normal n_I:
2060 // (1,1): d(F_1.n)/dz (value-block * trial_j + grad-block . gradN_j)
2061 // + Nitsche penalty * trial_j (Dirichlet faces only)
2062 // (1,0): d(F_1.n)/dp (value-block * trial_j + grad-block . gradN_j)
2063 // Gated on isDir_n: no-flow faces contribute nothing (matches the residual).
2064 // ============================================================================
2065 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
2066 const int ebN = exteriorElementBoundariesArray.data()[ebNE];
2067 const int eN = elementBoundaryElementsArray.data()[ebN * 2 + 0];
2068 const int ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0];
2069 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
2070 const int mat_eN = elementMaterialTypes.data()[eN];
2071 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
2072 const double alpha_eN = alpha.data()[mat_eN];
2073 const double krn_end_eN = krn_end.data()[mat_eN];
2074 const double n_vg_eN = n.data()[mat_eN];
2075 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
2076 const double S_wr_loc = thetaR.data()[mat_eN] / phi_eN;
2077 const double one_m_Sr_loc = 1.0 - S_wr_loc;
2078 const double Se_trap_L2110 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_loc; // gas-only residual trapping
2079
2080 double elementJacobian_n_n_eb[nDOF_test_element][nDOF_trial_element];
2081 double elementJacobian_n_w_eb[nDOF_test_element][nDOF_trial_element];
2082 for (int i = 0; i < nDOF_test_element; i++)
2083 for (int j = 0; j < nDOF_trial_element; j++) {
2084 elementJacobian_n_n_eb[i][j] = 0.0;
2085 elementJacobian_n_w_eb[i][j] = 0.0;
2086 }
2087
2088 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
2089 const int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb;
2090 const int ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb;
2091 const int ebN_local_kb_nSpace = ebN_local_kb * nSpace;
2092
2093 double jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace];
2094 double boundaryJac_b[nSpace * (nSpace - 1)];
2095 double metricTensor_b[(nSpace - 1) * (nSpace - 1)];
2096 double metricTensorDetSqrt_b, dS_eb, normal_b[3];
2097 double xt_b, yt_b, zt_b, integralScaling_b;
2098 double x_eb, y_eb, z_eb;
2099 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
2100 mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
2101 mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(),
2102 jac_ext, jacDet_ext, jacInv_ext, boundaryJac_b, metricTensor_b,
2103 metricTensorDetSqrt_b, normal_ref.data(), normal_b,
2104 x_eb, y_eb, z_eb);
2105 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
2106 mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
2107 xt_b, yt_b, zt_b, normal_b, boundaryJac_b, metricTensor_b,
2108 integralScaling_b);
2109 dS_eb = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt_b
2110 + MOVING_DOMAIN * integralScaling_b) * dS_ref.data()[kb];
2111
2112 double u_grad_trial_trace_b[nDOF_trial_element * nSpace];
2113 ck.gradTrialFromRef(
2114 &u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element],
2115 jacInv_ext, u_grad_trial_trace_b);
2116 double u_w_ext_b = 0.0, u_n_ext_b = 0.0;
2117 double grad_u_w_ext_b[nSpace], grad_u_n_ext_b[nSpace];
2118 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
2119 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_w_ext_b);
2120 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
2121 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_n_ext_b);
2122 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
2123 u_grad_trial_trace_b, grad_u_w_ext_b);
2124 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
2125 u_grad_trial_trace_b, grad_u_n_ext_b);
2126
2127 const int isDir_n = isDOFBoundary_n.data()[ebNE_kb];
2128 const double penalty = ebqe_penalty_ext.data()[ebNE_kb];
2129
2130 // ====================================================================
2131 // P3c boundary (STAB=0): compositional comp-1 (CO2) flux Jacobian.
2132 // Matches the F_1.n residual boundary; mirrors the FD-verified interior
2133 // comp-1 Jacobian (~line 1942) chain-ruled through the analytic flash,
2134 // with the interior gradN_i replaced by the boundary normal n_I:
2135 // (1,1) d(F_1.n)/dz + Nitsche penalty * trial_j (Dirichlet faces),
2136 // (1,0) d(F_1.n)/dp.
2137 // ====================================================================
2138 const double z_clb = fmin(fmax(u_n_ext_b, 1.0e-8), 1.0 - 1.0e-8);
2139 const double p_clb = fmax(u_w_ext_b, 1.0e2);
2142 const double Sa_b = 1.0 - fsb.S_g;
2143 const double Se_raw_b = (Sa_b - S_wr_loc) / one_m_Sr_loc;
2144 double Se_b, dSe_dp_b, dSe_dz_b;
2145 if (Se_raw_b <= 0.0) { Se_b = 0.0; dSe_dp_b = 0.0; dSe_dz_b = 0.0; }
2146 else if (Se_raw_b >= 1.0) { Se_b = 1.0; dSe_dp_b = 0.0; dSe_dz_b = 0.0; }
2147 else { Se_b = Se_raw_b; dSe_dp_b = -fsb.dS_g_dp/one_m_Sr_loc; dSe_dz_b = -fsb.dS_g_dz/one_m_Sr_loc; }
2148 double KWr_b=0,DKWr_b=0,thW_b=0,DthW_b=0,KNr_b=0,DKNr_b=0,pc_b=0,dpc_dSe_b=0,d2pc_b=0;
2149 if (PSK_TYPE_member == 1) {
2150 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_b, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
2151 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_b, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L2110);
2152 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_b, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
2153 } else {
2154 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_b, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
2155 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_b, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L2110);
2156 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_b, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
2157 }
2158 KNr_b *= krn_end_eN; DKNr_b *= krn_end_eN;
2159 const double pcp_b = dpc_dSe_b / one_m_Sr_loc;
2160 const double dpcp_dp_b = (d2pc_b / one_m_Sr_loc) * dSe_dp_b;
2161 const double dpcp_dz_b = (d2pc_b / one_m_Sr_loc) * dSe_dz_b;
2163 const double Mbar_g_b = fsb.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.Y)*::m_comp_co2::eos::M_H2O_KG;
2164 const double Mbar_a_b = fsb.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.X)*::m_comp_co2::eos::M_H2O_KG;
2165 const double rho_g_mass_b = fsb.rho_g*Mbar_g_b, rho_a_mass_b = fsb.rho_a*Mbar_a_b;
2166 const double drgm_dp_b = fsb.drho_g_dp*Mbar_g_b + fsb.rho_g*fsb.dY_dp*dMm_b;
2167 const double drgm_dz_b = fsb.rho_g*fsb.dY_dz*dMm_b;
2168 const double dram_dp_b = fsb.drho_a_dp*Mbar_a_b + fsb.rho_a*fsb.dX_dp*dMm_b;
2169 const double dram_dz_b = fsb.drho_a_dz*Mbar_a_b + fsb.rho_a*fsb.dX_dz*dMm_b;
2170 const double Ag = fsb.rho_g*fsb.Y, Aa = fsb.rho_a*fsb.X;
2171 const double dAg_dp = fsb.drho_g_dp*fsb.Y + fsb.rho_g*fsb.dY_dp;
2172 const double dAg_dz = fsb.rho_g*fsb.dY_dz;
2173 const double dAa_dp = fsb.drho_a_dp*fsb.X + fsb.rho_a*fsb.dX_dp;
2174 const double dAa_dz = fsb.drho_a_dz*fsb.X + fsb.rho_a*fsb.dX_dz;
2175 double ug_b[nSpace], ua_b[nSpace];
2176 double dug_dp_b[nSpace], dug_dz_b[nSpace], dua_dp_b[nSpace], dua_dz_b[nSpace];
2177 for (int I = 0; I < nSpace; I++) {
2178 double ugI=0.0, uaI=0.0, dugp=0.0, dugz=0.0, duap=0.0, duaz=0.0;
2179 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
2180 const int J = a_colind.data()[ii];
2181 const double Kii = KWs_eN[ii];
2182 const double Mob_g = KNr_b*Kii/mu_n, Mob_a = KWr_b*Kii;
2183 const double dMobg_dp = (DKNr_b*Kii/mu_n)*dSe_dp_b, dMobg_dz = (DKNr_b*Kii/mu_n)*dSe_dz_b;
2184 const double dMoba_dp = (DKWr_b*Kii)*dSe_dp_b, dMoba_dz = (DKWr_b*Kii)*dSe_dz_b;
2185 const double gJ = gravity.data()[J];
2186 const double gradSa = -(fsb.dS_g_dp*grad_u_w_ext_b[J] + fsb.dS_g_dz*grad_u_n_ext_b[J]);
2187 const double gp_a = grad_u_w_ext_b[J] - rho_a_mass_b*gJ;
2188 const double gp_g = grad_u_w_ext_b[J] + pcp_b*gradSa - rho_g_mass_b*gJ;
2189 ugI -= Mob_g*gp_g; uaI -= Mob_a*gp_a;
2190 const double dgradSa_dp = -(fsb.d2S_g_dp2 *grad_u_w_ext_b[J] + fsb.d2S_g_dpdz*grad_u_n_ext_b[J]);
2191 const double dgradSa_dz = -(fsb.d2S_g_dpdz*grad_u_w_ext_b[J] + fsb.d2S_g_dz2 *grad_u_n_ext_b[J]);
2192 const double dgpg_dp = dpcp_dp_b*gradSa + pcp_b*dgradSa_dp - drgm_dp_b*gJ;
2193 const double dgpg_dz = dpcp_dz_b*gradSa + pcp_b*dgradSa_dz - drgm_dz_b*gJ;
2194 dugp -= dMobg_dp*gp_g + Mob_g*dgpg_dp;
2195 dugz -= dMobg_dz*gp_g + Mob_g*dgpg_dz;
2196 duap -= dMoba_dp*gp_a + Mob_a*(-dram_dp_b*gJ);
2197 duaz -= dMoba_dz*gp_a + Mob_a*(-dram_dz_b*gJ);
2198 }
2199 ug_b[I]=ugI; ua_b[I]=uaI;
2200 dug_dp_b[I]=dugp; dug_dz_b[I]=dugz; dua_dp_b[I]=duap; dua_dz_b[I]=duaz;
2201 }
2202 // value-block scalars dotted with the normal (interior gradN_i -> n_I).
2203 double Sval_p_b = 0.0, Sval_z_b = 0.0;
2204 for (int I = 0; I < nSpace; I++) {
2205 Sval_p_b += (dAg_dp*ug_b[I] + Ag*dug_dp_b[I] + dAa_dp*ua_b[I] + Aa*dua_dp_b[I]) * normal_b[I];
2206 Sval_z_b += (dAg_dz*ug_b[I] + Ag*dug_dz_b[I] + dAa_dz*ua_b[I] + Aa*dua_dz_b[I]) * normal_b[I];
2207 }
2208 // IIPG penalty scaled by the comp-1 diffusion magnitude a_n (frozen
2209 // coefficient) -- MUST match the residual loop in calculateResidual.
2210 double Kw_rep = 0.0;
2211 for (int ii = 0; ii < nnz; ii++) Kw_rep = fmax(Kw_rep, fabs(KWs_eN[ii]));
2212 const double a_n_scale = (Ag*KNr_b/mu_n + Aa*KWr_b) * Kw_rep;
2213 const double pen_eff = penalty * a_n_scale;
2214
2215 for (int i = 0; i < nDOF_test_element; i++) {
2216 const double test_i_dS = u_test_trace_ref.data()[
2217 ebN_local_kb * nDOF_test_element + i] * dS_eb;
2218 for (int j = 0; j < nDOF_trial_element; j++) {
2219 const double trial_j_b = u_trial_trace_ref.data()[
2220 ebN_local_kb * nDOF_test_element + j];
2221 double Sgrad_p_b = 0.0, Sgrad_z_b = 0.0;
2222 for (int I = 0; I < nSpace; I++) {
2223 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
2224 const int J = a_colind.data()[ii];
2225 const double Kii = KWs_eN[ii];
2226 const double Mob_g = KNr_b*Kii/mu_n, Mob_a = KWr_b*Kii;
2227 const double gNjJ = u_grad_trial_trace_b[j * nSpace + J];
2228 const double dFdgp = Ag*(-Mob_g*(1.0 - pcp_b*fsb.dS_g_dp)) + Aa*(-Mob_a);
2229 const double dFdgz = Ag*(-Mob_g*(-pcp_b*fsb.dS_g_dz));
2230 Sgrad_p_b += dFdgp * gNjJ * normal_b[I];
2231 Sgrad_z_b += dFdgz * gNjJ * normal_b[I];
2232 }
2233 }
2234 double jac_nn = Sval_z_b * trial_j_b + Sgrad_z_b;
2235 double jac_nw = Sval_p_b * trial_j_b + Sgrad_p_b;
2236 if (isDir_n) {
2237 jac_nn += pen_eff * trial_j_b;
2238 } else {
2239 jac_nn = 0.0;
2240 jac_nw = 0.0;
2241 }
2242 elementJacobian_n_n_eb[i][j] += jac_nn * test_i_dS;
2243 elementJacobian_n_w_eb[i][j] += jac_nw * test_i_dS;
2244 }
2245 }
2246 } // kb
2247
2248 for (int i = 0; i < nDOF_test_element; i++) {
2249 const int eN_i = eN * nDOF_test_element + i;
2250 for (int j = 0; j < nDOF_trial_element; j++) {
2251 const int ebN_i_j = ebN * 4 * nDOF_test_X_trial_element
2252 + i * nDOF_trial_element + j;
2253 globalJacobian.data()[csrRowIndeces_n_n.data()[eN_i]
2254 + csrColumnOffsets_eb_n_n.data()[ebN_i_j]] += elementJacobian_n_n_eb[i][j];
2255 globalJacobian.data()[csrRowIndeces_n_w.data()[eN_i]
2256 + csrColumnOffsets_eb_n_w.data()[ebN_i_j]] += elementJacobian_n_w_eb[i][j];
2257 }
2258 }
2259 } // ebNE
2260 } //computeJacobian
2261
2262
2263
2264 // ============================================================================
2265 // FCTStep -- Zalesak FCT limiter, component- and pass-dispatched (ONE function).
2266 //
2267 // if component == 0 (wetting, p_w) / else component == 1 (non-wetting, S_n);
2268 // within each, if pass == 1 / else pass == 2.
2269 //
2270 // Split into two passes for MPI parallel-correctness: the limiter is
2271 // mass-conservative only if both ranks sharing an edge compute the SAME
2272 // L_ij, which needs Rpos/Rneg consistent for BOTH endpoints. With 1-layer
2273 // overlap a ghost DOF's stencil is incomplete, so pass 1 computes Rpos/Rneg
2274 // locally, Python ghost-scatters them, then pass 2 applies the limiter.
2275 // In serial the scatter is a no-op.
2276 //
2277 // pass 1: inputs -> FluxCorrectionMatrix, Rpos, Rneg
2278 // [Python: scatter_forward Rpos, Rneg]
2279 // pass 2: Rpos, Rneg, FluxCorrectionMatrix -> limited_solution, fluxCorrection
2280 //
2281 // comp-0 indexes MC / dt_times_fH_minus_fL by the FULL CSR offset (via
2282 // full_offset_from_compact); comp-1 indexes MC_n / dt_times_fH_minus_fL_n by
2283 // the COMPACT comp-1 CSR position directly.
2284 // ============================================================================
2286 {
2287 const int component = args.scalar<int>("component");
2288 const int pass = args.scalar<int>("pass");
2289
2290 if (component == 0) {
2291 // ======================= wetting (p_w) =======================
2292 if (pass == 1) {
2293 int numDOFs = args.scalar<int>("numDOFs");
2294 double dt = args.scalar<double>("dt");
2295 xt::pyarray<double> &ML = args.array<double>("ML");
2296 xt::pyarray<double> &mn = args.array<double>("mn");
2297 xt::pyarray<double> &mLow = args.array<double>("mLow");
2298 xt::pyarray<double> &mDotLow = args.array<double>("mDotLow");
2299 xt::pyarray<int> &csrRowIndeces_DofLoops = args.array<int>("csrRowIndeces_DofLoops");
2300 xt::pyarray<int> &csrColumnOffsets_DofLoops = args.array<int>("csrColumnOffsets_DofLoops");
2301 xt::pyarray<int> &csrRowIndeces_Full = args.array<int>("csrRowIndeces_Full");
2302 xt::pyarray<int> &csrColumnOffsets_Full = args.array<int>("csrColumnOffsets_Full");
2303 xt::pyarray<double> &MC = args.array<double>("MC");
2304 xt::pyarray<double> &dt_times_fH_minus_fL = args.array<double>("dt_times_fH_minus_fL");
2305 xt::pyarray<double> &min_m_bc = args.array<double>("min_m_bc");
2306 xt::pyarray<double> &max_m_bc = args.array<double>("max_m_bc");
2307 xt::pyarray<double> &FluxCorrectionMatrix = args.array<double>("FluxCorrectionMatrix");
2308 xt::pyarray<double> &Rpos = args.array<double>("Rpos");
2309 xt::pyarray<double> &Rneg = args.array<double>("Rneg");
2310 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
2311 int MONOLITHIC = args.scalar<int>("MONOLITHIC");
2312 const int offset_u = args.scalar<int>("offset_u");
2313 const int stride_u = args.scalar<int>("stride_u");
2314 auto full_offset_from_compact = [&](int i_compact, int j_compact) -> int {
2315 const int full_i = offset_u + stride_u * i_compact;
2316 const int full_j = offset_u + stride_u * j_compact;
2317 for (int offset = csrRowIndeces_Full.at(full_i); offset < csrRowIndeces_Full.at(full_i + 1); ++offset)
2318 if (csrColumnOffsets_Full.at(offset) == full_j) return offset;
2319 return -1;
2320 };
2321 int ij = 0;
2322 for (int i = 0; i < numDOFs; i++) {
2323 double mini = min_m_bc.at(i);
2324 double maxi = max_m_bc.at(i);
2325 double Pposi = 0.0, Pnegi = 0.0;
2326 for (int offset = csrRowIndeces_DofLoops.at(i); offset < csrRowIndeces_DofLoops.at(i + 1); offset++) {
2327 int j = csrColumnOffsets_DofLoops.at(offset);
2328 const int full_offset = full_offset_from_compact(i, j);
2329 assert(full_offset >= 0);
2330 if (GLOBAL_FCT == 0) {
2331 if (MONOLITHIC == 0) { mini = fmin(mini, mLow.at(j)); maxi = fmax(maxi, mLow.at(j)); }
2332 else { mini = fmin(mini, mn.at(j)); maxi = fmax(maxi, mn.at(j)); }
2333 }
2334 if (MONOLITHIC == 0) {
2335 FluxCorrectionMatrix.at(ij) = (LUMPED_MASS_MATRIX == 1 ? 0. : 1.) * dt
2336 * MC.at(full_offset) * (mDotLow.at(i) - mDotLow.at(j))
2337 + dt_times_fH_minus_fL.at(full_offset);
2338 } else {
2339 FluxCorrectionMatrix.at(ij) = dt_times_fH_minus_fL.at(full_offset);
2340 }
2341 Pposi += FluxCorrectionMatrix.at(ij) * ((FluxCorrectionMatrix.at(ij) > 0) ? 1. : 0.);
2342 Pnegi += FluxCorrectionMatrix.at(ij) * ((FluxCorrectionMatrix.at(ij) < 0) ? 1. : 0.);
2343 ij += 1;
2344 }
2345 double Qposi, Qnegi;
2346 if (MONOLITHIC == 0) {
2347 Qposi = ML.at(i) * (maxi - mLow.at(i));
2348 Qnegi = ML.at(i) * (mini - mLow.at(i));
2349 } else {
2350 const double gamma = 10.0 * ML.at(i);
2351 Qposi = fmin(0.5 * ML.at(i) * (1.0 - mn.at(i)), gamma * (maxi - mn.at(i)));
2352 Qnegi = fmax(0.5 * ML.at(i) * (0.0 - mn.at(i)), gamma * (mini - mn.at(i)));
2353 }
2354 Rpos.at(i) = ((Pposi == 0.0) ? 1.0 : fmin(1.0, Qposi / Pposi));
2355 Rneg.at(i) = ((Pnegi == 0.0) ? 1.0 : fmin(1.0, Qnegi / Pnegi));
2356 }
2357 } else {
2358 // comp-0 pass 2
2359 xt::pyarray<double> &bc_mask = args.array<double>("bc_mask");
2360 int numDOFs = args.scalar<int>("numDOFs");
2361 double dt = args.scalar<double>("dt");
2362 xt::pyarray<double> &ML = args.array<double>("ML");
2363 xt::pyarray<double> &mn = args.array<double>("mn");
2364 xt::pyarray<double> &mLow = args.array<double>("mLow");
2365 xt::pyarray<int> &csrRowIndeces_DofLoops = args.array<int>("csrRowIndeces_DofLoops");
2366 xt::pyarray<int> &csrColumnOffsets_DofLoops = args.array<int>("csrColumnOffsets_DofLoops");
2367 xt::pyarray<int> &csrRowIndeces_Full = args.array<int>("csrRowIndeces_Full");
2368 xt::pyarray<int> &csrColumnOffsets_Full = args.array<int>("csrColumnOffsets_Full");
2369 xt::pyarray<double> &MC = args.array<double>("MC");
2370 xt::pyarray<double> &FluxCorrectionMatrix = args.array<double>("FluxCorrectionMatrix");
2371 xt::pyarray<double> &Rpos = args.array<double>("Rpos");
2372 xt::pyarray<double> &Rneg = args.array<double>("Rneg");
2373 xt::pyarray<double> &fluxCorrection = args.array<double>("fluxCorrection");
2374 xt::pyarray<double> &limited_solution = args.array<double>("limited_solution");
2375 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
2376 int MONOLITHIC = args.scalar<int>("MONOLITHIC");
2377 const int offset_u = args.scalar<int>("offset_u");
2378 const int stride_u = args.scalar<int>("stride_u");
2379 auto full_offset_from_compact = [&](int i_compact, int j_compact) -> int {
2380 const int full_i = offset_u + stride_u * i_compact;
2381 const int full_j = offset_u + stride_u * j_compact;
2382 for (int offset = csrRowIndeces_Full.at(full_i); offset < csrRowIndeces_Full.at(full_i + 1); ++offset)
2383 if (csrColumnOffsets_Full.at(offset) == full_j) return offset;
2384 return -1;
2385 };
2386 int ij = 0;
2387 for (int i = 0; i < numDOFs; i++) {
2388 double ith_Limiter_times_FluxCorrectionMatrix = 0.0;
2389 const double beta_ij = 1.0;
2390 const double mDot_i = (mLow.at(i) - mn.at(i)) / dt;
2391 for (int offset = csrRowIndeces_DofLoops.at(i); offset < csrRowIndeces_DofLoops.at(i + 1); offset++) {
2392 int j = csrColumnOffsets_DofLoops.at(offset);
2393 const int full_offset = full_offset_from_compact(i, j);
2394 assert(full_offset >= 0);
2395 const double alpha_fA = ((FluxCorrectionMatrix.at(ij) > 0.0)
2396 ? fmin(Rpos.at(i), Rneg.at(j)) : fmin(Rneg.at(i), Rpos.at(j)))
2397 * FluxCorrectionMatrix.at(ij);
2398 if (MONOLITHIC == 0) {
2399 ith_Limiter_times_FluxCorrectionMatrix += alpha_fA;
2400 } else {
2401 const double mDot_j = (mLow.at(j) - mn.at(j)) / dt;
2402 const double alpha_dot = fmin(1.0, beta_ij * fabs(alpha_fA) / MC.at(full_offset)
2403 / fmax(1.0e-8, fabs(mDot_i - mDot_j)));
2404 ith_Limiter_times_FluxCorrectionMatrix += alpha_fA
2405 + (LUMPED_MASS_MATRIX == 1 ? 0. : 1.) * dt * alpha_dot
2406 * MC.at(full_offset) * (mDot_i - mDot_j);
2407 }
2408 ij += 1;
2409 }
2410 fluxCorrection.at(i) = -ith_Limiter_times_FluxCorrectionMatrix * bc_mask.at(i) / dt;
2411 limited_solution.at(i) = mLow.at(i)
2412 + 1.0 / ML.at(i) * ith_Limiter_times_FluxCorrectionMatrix * bc_mask.at(i);
2413 }
2414 }
2415 } else {
2416 // ===================== non-wetting (S_n) =====================
2417 if (pass == 1) {
2418 int numDOFs_n = args.scalar<int>("numDOFs_n");
2419 double dt = args.scalar<double>("dt");
2420 xt::pyarray<double> &ML_n = args.array<double>("ML_n");
2421 xt::pyarray<double> &MC_n = args.array<double>("MC_n");
2422 xt::pyarray<double> &mLow_n = args.array<double>("mLow_n");
2423 xt::pyarray<double> &mDotLow_n = args.array<double>("mDotLow_n");
2424 xt::pyarray<double> &dt_times_fH_minus_fL_n = args.array<double>("dt_times_fH_minus_fL_n");
2425 xt::pyarray<double> &min_m_bc_n = args.array<double>("min_m_bc_n");
2426 xt::pyarray<double> &max_m_bc_n = args.array<double>("max_m_bc_n");
2427 xt::pyarray<double> &FluxCorrectionMatrix_n = args.array<double>("FluxCorrectionMatrix_n");
2428 xt::pyarray<double> &Rpos_n = args.array<double>("Rpos_n");
2429 xt::pyarray<double> &Rneg_n = args.array<double>("Rneg_n");
2430 xt::pyarray<int> &csrRowIndeces_n_DofLoops = args.array<int>("csrRowIndeces_n_DofLoops");
2431 xt::pyarray<int> &csrColumnOffsets_n_DofLoops = args.array<int>("csrColumnOffsets_n_DofLoops");
2432 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
2433 int ij = 0;
2434 for (int i = 0; i < numDOFs_n; i++) {
2435 double mini = min_m_bc_n.at(i);
2436 double maxi = max_m_bc_n.at(i);
2437 double Pposi = 0.0, Pnegi = 0.0;
2438 for (int offset = csrRowIndeces_n_DofLoops.at(i);
2439 offset < csrRowIndeces_n_DofLoops.at(i + 1); offset++) {
2440 const int j = csrColumnOffsets_n_DofLoops.at(offset);
2441 if (GLOBAL_FCT == 0) {
2442 mini = std::fmin(mini, mLow_n.at(j));
2443 maxi = std::fmax(maxi, mLow_n.at(j));
2444 }
2445 FluxCorrectionMatrix_n.at(ij) =
2446 (LUMPED_MASS_MATRIX == 1 ? 0.0 : 1.0)
2447 * dt * MC_n.at(offset) * (mDotLow_n.at(i) - mDotLow_n.at(j))
2448 + dt_times_fH_minus_fL_n.at(offset);
2449 Pposi += (FluxCorrectionMatrix_n.at(ij) > 0.0) ? FluxCorrectionMatrix_n.at(ij) : 0.0;
2450 Pnegi += (FluxCorrectionMatrix_n.at(ij) < 0.0) ? FluxCorrectionMatrix_n.at(ij) : 0.0;
2451 ij += 1;
2452 }
2453 const double Qposi = ML_n.at(i) * (maxi - mLow_n.at(i));
2454 const double Qnegi = ML_n.at(i) * (mini - mLow_n.at(i));
2455 Rpos_n.at(i) = (Pposi == 0.0) ? 1.0 : std::fmin(1.0, Qposi / Pposi);
2456 Rneg_n.at(i) = (Pnegi == 0.0) ? 1.0 : std::fmin(1.0, Qnegi / Pnegi);
2457 }
2458 } else {
2459 // comp-1 pass 2
2460 int numDOFs_n = args.scalar<int>("numDOFs_n");
2461 double dt = args.scalar<double>("dt");
2462 xt::pyarray<double> &ML_n = args.array<double>("ML_n");
2463 xt::pyarray<double> &mLow_n = args.array<double>("mLow_n");
2464 xt::pyarray<double> &FluxCorrectionMatrix_n = args.array<double>("FluxCorrectionMatrix_n");
2465 xt::pyarray<double> &Rpos_n = args.array<double>("Rpos_n");
2466 xt::pyarray<double> &Rneg_n = args.array<double>("Rneg_n");
2467 xt::pyarray<double> &fluxCorrection_n = args.array<double>("fluxCorrection_n");
2468 xt::pyarray<double> &limited_solution_n = args.array<double>("limited_solution_n");
2469 xt::pyarray<double> &bc_mask_n = args.array<double>("bc_mask_n");
2470 xt::pyarray<int> &csrRowIndeces_n_DofLoops = args.array<int>("csrRowIndeces_n_DofLoops");
2471 xt::pyarray<int> &csrColumnOffsets_n_DofLoops = args.array<int>("csrColumnOffsets_n_DofLoops");
2472 int ij = 0;
2473 for (int i = 0; i < numDOFs_n; i++) {
2474 double ith_Limited_FCM = 0.0;
2475 for (int offset = csrRowIndeces_n_DofLoops.at(i);
2476 offset < csrRowIndeces_n_DofLoops.at(i + 1); offset++) {
2477 const int j = csrColumnOffsets_n_DofLoops.at(offset);
2478 const double alpha_fA =
2479 ((FluxCorrectionMatrix_n.at(ij) > 0.0)
2480 ? std::fmin(Rpos_n.at(i), Rneg_n.at(j))
2481 : std::fmin(Rneg_n.at(i), Rpos_n.at(j)))
2482 * FluxCorrectionMatrix_n.at(ij);
2483 ith_Limited_FCM += alpha_fA;
2484 ij += 1;
2485 }
2486 fluxCorrection_n.at(i) = -ith_Limited_FCM * bc_mask_n.at(i) / dt;
2487 limited_solution_n.at(i) = mLow_n.at(i)
2488 + (1.0 / ML_n.at(i)) * ith_Limited_FCM * bc_mask_n.at(i);
2489 }
2490 }
2491 }
2492 }
2493
2494
2496 {
2497 int NNZ = args.scalar<int>("NNZ"); //number on non-zero entries on sparsity pattern
2498 int numDOFs = args.scalar<int>("numDOFs"); //number of DOFs
2499 int num_fct_iter = args.scalar<int>("num_fct_iter");
2500 double dt = args.scalar<double>("dt");
2501 xt::pyarray<double> &lumped_mass_matrix = args.array<double>("lumped_mass_matrix"); //lumped mass matrix (as vector)
2502 xt::pyarray<double> &soln = args.array<double>("soln"); //DOFs of solution at time tn
2503 xt::pyarray<double> &pn = args.array<double>("pn"); //DOFs of solution at time tn
2504 xt::pyarray<double> &solH = args.array<double>("solH"); //DOFs of high order solution at tnp1
2505 xt::pyarray<double> &uLow = args.array<double>("uLow");
2506 xt::pyarray<double> &uDotLow = args.array<double>("uDotLow");
2507 xt::pyarray<double> &dLow = args.array<double>("dLow");
2508 xt::pyarray<double> &solLim = args.array<double>("limited_solution");
2509 xt::pyarray<double> &MC = args.array<double>("MC");
2510 xt::pyarray<double> &ML = args.array<double>("ML");
2511 xt::pyarray<double> &FluxMatrix = args.array<double>("FluxMatrix");
2512 xt::pyarray<double> &limitedFlux = args.array<double>("limited_Flux");
2513 xt::pyarray<int> &csrRowIndeces_DofLoops = args.array<int>("csrRowIndeces_DofLoops"); //csr row indeces
2514 xt::pyarray<int> &csrColumnOffsets_DofLoops = args.array<int>("csrColumnOffsets_DofLoops"); //csr column offsets
2515 xt::pyarray<double> &MassMatrix = args.array<double>("MassMatrix"); //mass matrix
2516 xt::pyarray<double> &dt_times_fH_minus_fL = args.array<double>("dt_times_fH_minus_fL"); //low minus high order dissipative matrices
2517 xt::pyarray<double> &min_m_bc = args.array<double>("min_m_bc"); //min/max value at BCs. If DOF is not at boundary then min=1E10, max=-1E10
2518 xt::pyarray<double> &max_m_bc = args.array<double>("max_m_bc");
2519 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
2520 int MONOLITHIC = args.scalar<int>("MONOLITHIC");
2521 double Rpos[numDOFs], Rneg[numDOFs];
2522 int ij = 0;
2523
2525 // ********** COMPUTE LOW ORDER SOLUTION ********** //
2527 if (num_fct_iter == 0) { // No FCT for global bounds
2528 for (int i = 0; i < numDOFs; i++) { solLim.data()[i] = uLow.data()[i]; }
2529 } else // do FCT iterations (with global bounds) on low order solution
2530 {
2531 for (int iter = 0; iter < num_fct_iter; iter++) {
2532 ij = 0;
2533 for (int i = 0; i < numDOFs; i++) {
2534 double maxi = 1.0, Pposi = 0;
2535 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) {
2536 int j = csrColumnOffsets_DofLoops.data()[offset];
2537 // compute Flux correction
2538 double Fluxij = FluxMatrix.data()[ij] - limitedFlux.data()[ij];
2539 Pposi += Fluxij * ((Fluxij > 0) ? 1. : 0.);
2540 // update ij
2541 ij += 1;
2542 }
2543 // compute Q vectors
2544 double mi = ML.data()[i];
2545 double solLimi = solLim.data()[i];
2546 double Qposi = mi * (maxi - solLimi);
2547 // compute R vectors
2548 Rpos[i] = ((Pposi == 0) ? 1. : fmin(1.0, Qposi / Pposi));
2549 }
2550 ij = 0;
2551 for (int i = 0; i < numDOFs; i++) {
2552 double ith_Limiter_times_FluxCorrectionMatrix = 0.;
2553 double Rposi = Rpos[i];
2554 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) {
2555 int j = csrColumnOffsets_DofLoops.data()[offset];
2556 // Flux Correction
2557 double Fluxij = FluxMatrix.data()[ij] - limitedFlux.data()[ij];
2558 // compute limiter
2559 double Lij = 1.0;
2560 Lij = (Fluxij > 0 ? Rposi : Rpos[j]);
2561 // compute limited flux
2562 ith_Limiter_times_FluxCorrectionMatrix += Lij * Fluxij;
2563
2564 // update limited flux
2565 limitedFlux.data()[ij] = Lij * Fluxij;
2566
2567 //update FluxMatrix
2568 FluxMatrix.data()[ij] = Fluxij;
2569
2570 //update ij
2571 ij += 1;
2572 }
2573 //update limited solution
2574 double mi = ML.data()[i];
2575 }
2576 }
2577 }
2578
2579 // ***************************************** //
2580 // ********** HIGH ORDER SOLUTION ********** //
2581 // ***************************************** //
2582 ij = 0;
2583 for (int i = 0; i < numDOFs; i++) {
2584 double mini = soln.data()[i], maxi = soln.data()[i];
2585 double Pposi = 0, Pnegi = 0.;
2586 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) {
2587 int j = csrColumnOffsets_DofLoops.data()[offset];
2588 // compute local bounds //
2589 mini = fmin(mini, soln.data()[j]);
2590 maxi = fmax(maxi, soln.data()[j]);
2591 // compute P vectors //
2592 double fij = (MC.data()[ij] * (uDotLow.data()[i] - uDotLow.data()[j]) / dt + dLow.data()[ij] * (uLow.data()[i] - uLow.data()[j]));
2593 Pposi += fij * (fij > 0 ? 1. : 0.);
2594 Pnegi += fij * (fij < 0 ? 1. : 0.);
2595 //update ij
2596 ij += 1;
2597 }
2598 // compute Q vectors //
2599 double mi = ML.data()[i];
2600 double Qposi = mi * (maxi - solLim.data()[i]);
2601 double Qnegi = mi * (mini - solLim.data()[i]);
2602 // compute R vectors //
2603 Rpos[i] = ((Pposi == 0) ? 1. : fmin(1.0, Qposi / Pposi));
2604 Rneg[i] = ((Pnegi == 0) ? 1. : fmin(1.0, Qnegi / Pnegi));
2605 }
2606
2607 // COMPUTE LIMITERS //
2608 ij = 0;
2609 for (int i = 0; i < numDOFs; i++) {
2610 double ith_limited_flux_correction = 0;
2611 double Rposi = Rpos[i];
2612 double Rnegi = Rneg[i];
2613 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) {
2614 int j = csrColumnOffsets_DofLoops.data()[offset];
2615 // compute flux correction
2616 double fij = (MC.data()[ij] * (uDotLow.data()[i] - uDotLow.data()[j]) / dt + dLow.data()[ij] * (uLow.data()[i] - uLow.data()[j]));
2617
2618 // compute limiters
2619 double Lij = 1.0;
2620 Lij = fij > 0 ? fmin(Rposi, Rneg[j]) : fmin(Rnegi, Rpos[j]);
2621 // compute ith_limited_flux_correction
2622 ith_limited_flux_correction += Lij * fij;
2623 ij += 1;
2624 }
2625 double mi = ML.data()[i];
2626 solLim[i] += 1. / mi * ith_limited_flux_correction;
2627 }
2628 }
2629
2631 {
2632 xt::pyarray<double> &globalJacobian = args.array<double>("globalJacobian");
2633 double Theta = args.scalar<double>("Theta");
2634 double Theta_h = args.scalar<double>("Theta_h");
2635 xt::pyarray<double> &bc_mask = args.array<double>("bc_mask");
2636 double dt = args.scalar<double>("dt");
2637 xt::pyarray<double> &mesh_trial_ref = args.array<double>("mesh_trial_ref");
2638 xt::pyarray<double> &mesh_grad_trial_ref = args.array<double>("mesh_grad_trial_ref");
2639 xt::pyarray<double> &mesh_dof = args.array<double>("mesh_dof");
2640 xt::pyarray<double> &mesh_velocity_dof = args.array<double>("mesh_velocity_dof");
2641 double MOVING_DOMAIN = args.scalar<double>("MOVING_DOMAIN");
2642 xt::pyarray<int> &mesh_l2g = args.array<int>("mesh_l2g");
2643 xt::pyarray<double> &dV_ref = args.array<double>("dV_ref");
2644 xt::pyarray<double> &u_trial_ref = args.array<double>("u_trial_ref");
2645 xt::pyarray<double> &u_grad_trial_ref = args.array<double>("u_grad_trial_ref");
2646 xt::pyarray<double> &u_test_ref = args.array<double>("u_test_ref");
2647 xt::pyarray<double> &u_grad_test_ref = args.array<double>("u_grad_test_ref");
2648 xt::pyarray<double> &mesh_trial_trace_ref = args.array<double>("mesh_trial_trace_ref");
2649 xt::pyarray<double> &mesh_grad_trial_trace_ref = args.array<double>("mesh_grad_trial_trace_ref");
2650 xt::pyarray<double> &dS_ref = args.array<double>("dS_ref");
2651 xt::pyarray<double> &u_trial_trace_ref = args.array<double>("u_trial_trace_ref");
2652
2653 xt::pyarray<double> &u_grad_trial_trace_ref = args.array<double>("u_grad_trial_trace_ref");
2654 xt::pyarray<double> &u_test_trace_ref = args.array<double>("u_test_trace_ref");
2655 xt::pyarray<double> &u_grad_test_trace_ref = args.array<double>("u_grad_test_trace_ref");
2656 xt::pyarray<double> &normal_ref = args.array<double>("normal_ref");
2657 xt::pyarray<double> &boundaryJac_ref = args.array<double>("boundaryJac_ref");
2658 int nElements_global = args.scalar<int>("nElements_global");
2659 xt::pyarray<double> &ebqe_penalty_ext = args.array<double>("ebqe_penalty_ext");
2660 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
2661 xt::pyarray<int> &isSeepageFace = args.array<int>("isSeepageFace");
2662 xt::pyarray<int> &a_rowptr = args.array<int>("a_rowptr");
2663 xt::pyarray<int> &a_colind = args.array<int>("a_colind");
2664 double rho = args.scalar<double>("rho");
2665 double beta = args.scalar<double>("beta");
2667 xt::pyarray<double> &q_rho = args.array<double>("q_rho");
2668 xt::pyarray<double> &ebqe_rho = args.array<double>("ebqe_rho");
2670
2671
2672 xt::pyarray<double> &gravity = args.array<double>("gravity");
2673 xt::pyarray<double> &alpha = args.array<double>("alpha");
2674 xt::pyarray<double> &n = args.array<double>("n");
2675 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
2676 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
2677 xt::pyarray<double> &KWs = args.array<double>("KWs");
2678 xt::pyarray<double> &krn_end = args.array<double>("krn_end");
2679 xt::pyarray<double> &S_gr = args.array<double>("S_gr");
2680 double mu_n = args.scalar<double>("mu_n");
2681 double useMetrics = args.scalar<double>("useMetrics");
2682 double alphaBDF = args.scalar<double>("alphaBDF");
2683 int lag_shockCapturing = args.scalar<int>("lag_shockCapturing");
2684 double shockCapturingDiffusion = args.scalar<double>("shockCapturingDiffusion");
2685 double sc_uref = args.scalar<double>("sc_uref");
2686 double sc_alpha = args.scalar<double>("sc_alpha");
2687 xt::pyarray<int> &u_l2g = args.array<int>("u_l2g");
2688 // ---- Node-split component-1 (z) map (DESIGN_nodesplit_consistent.md) -------
2689 // u_l2g_n routes each element's local nodes to ITS material side's z-DOF, so
2690 // an interface node carries an independent z (=> S_g) per facies and the
2691 // capillary-pressure jump is representable. p (comp-0) and geometry stay on
2692 // the single mesh node u_l2g. Python passes u_l2g_n == u_l2g, n_interface_pairs
2693 // == 0 and split_z == 0 on a homogeneous / non-split run, so EVERY read/scatter
2694 // below is BYTE-IDENTICAL to the nodal kernel (regression guard). Style: the
2695 // z_c<->z_f coupling is assembled as an EDGE in the split DOF graph (Richards.h
2696 // edge-based DOF-loop), not a bolted-on flux -- see the interface loop below.
2697 xt::pyarray<int> &u_l2g_n = args.array<int>("u_l2g_n");
2698 const int split_z = args.scalar<int>("split_z");
2699 const double D_m = args.scalar<double>("D_m"); // molecular diffusion of dissolved CO2 [m2/s]
2700 xt::pyarray<int> &interface_pairs = args.array<int>("interface_pairs"); // 5*n: [node, z_a, mat_a, z_b, mat_b]
2701 const int n_interface_pairs = args.scalar<int>("n_interface_pairs");
2702 // CO2-free anchor strength (CONSERVATIVE anchor applied per comp-1 DOF after the
2703 // element/interface assembly below -- see the "CO2-FREE ANCHOR" block). alpha is
2704 // the coupling stiffness as a fraction of the nodal accumulation capacity:
2705 // lam = alpha * min(cap_i,cap_j) / dt. alpha = 0 -> inactive (byte-identical).
2706 const double split_anchor_alpha = args.scalar<double>("split_anchor_alpha");
2707 // CO2-free anchor gate tolerances (from Coefficients, tunable without recompiling
2708 // once built). A DOF is anchored only where the flash says S_g < Sg_tol AND
2709 // X < X_tol (genuinely no CO2); X_tol just above the CO2-free background z excludes
2710 // the dilute dissolution fringe from the gate.
2711 const double split_anchor_Sg_tol = args.scalar<double>("split_anchor_Sg_tol");
2712 const double split_anchor_X_tol = args.scalar<double>("split_anchor_X_tol");
2713 // RETAINED for API compatibility; UNUSED by the conservative anchor (no floor).
2714 const double split_anchor_zfloor = args.scalar<double>("split_anchor_zfloor");
2715 (void)split_anchor_zfloor;
2716 // Layer-1 (domain-wide graph-Laplacian) toggle. The gate (X < X_tol) catches the
2717 // ENTIRE CO2-free background, so Layer 1 imposes a stiff domain-wide z-diffusion
2718 // that slows Newton; the actual split-node runaway is fixed by Layer 2 alone (the
2719 // local fine<->coarse spring). layer1 = 0 -> Layer-2-only (well-conditioned);
2720 // layer1 = 1 (default) -> both layers. Layer 2 is always on (gated by alpha).
2721 const int split_anchor_layer1 = args.scalar<int>("split_anchor_layer1");
2722 xt::pyarray<int> &r_l2g = args.array<int>("r_l2g");
2723 xt::pyarray<double> &elementDiameter = args.array<double>("elementDiameter");
2724 int degree_polynomial = args.scalar<int>("degree_polynomial");
2725 xt::pyarray<double> &u_dof = args.array<double>("u_dof");
2726 xt::pyarray<double> &u_dof_old = args.array<double>("u_dof_old");
2727 xt::pyarray<double> &velocity = args.array<double>("velocity");
2728 xt::pyarray<double> &q_m = args.array<double>("q_m");
2729 xt::pyarray<double> &q_theta = args.array<double>("q_theta");
2730 xt::pyarray<double> &q_u = args.array<double>("q_u");
2731 xt::pyarray<double> &q_dV = args.array<double>("q_dV");
2732 xt::pyarray<double> &q_m_betaBDF = args.array<double>("q_m_betaBDF");
2733 xt::pyarray<double> &cfl = args.array<double>("cfl");
2734 xt::pyarray<double> &q_numDiff_u = args.array<double>("q_numDiff_u");
2735 xt::pyarray<double> &q_numDiff_u_last = args.array<double>("q_numDiff_u_last");
2736 int offset_u = args.scalar<int>("offset_u");
2737 int stride_u = args.scalar<int>("stride_u");
2738 // component-1 (S_n) mass equation args.
2739 // (dt is already declared at the top of this function for the EV path.)
2740 xt::pyarray<double> &u_dof_n = args.array<double>("u_dof_n");
2741 xt::pyarray<double> &u_dof_n_old = args.array<double>("u_dof_n_old");
2742 // gas-phase density: EXPONENTIAL EOS mirroring comp-0's slightly-compressible
2743 // water (rho_w = rho*exp(beta*u_w)). rho_n is the reference density at
2744 // p_n = 0 (gauge = atmospheric); p_ref_n is the e-folding pressure scale:
2745 // rho_n(p_n) = rho_n * exp(p_n / p_ref_n), p_n = u_w + p_c,
2746 // drho_n/dp_n = rho_n(p_n) / p_ref_n (state-dependent, NOT a constant).
2747 // beta_n = 1/p_ref_n is the constant gas compressibility. CO2 near
2748 // atmospheric in a lab rig is ideal-gas-like (rho ~ P_abs), so p_ref_n ~
2749 // atmospheric in head ~ 10.3 m gives beta_n ~ 0.1 /m. p_ref_n <= 0 ->
2750 // incompressible (constant rho_n). Exponential (vs the old linear c_n*p_n)
2751 // keeps rho_n = rho_n at gauge p_n = 0 rather than collapsing to 0.
2752 const double rho_n = args.scalar<double>("rho_n");
2753 const double p_ref_n = args.scalar<double>("p_ref_n");
2754 const bool rho_n_compressible = (p_ref_n > 0.0);
2755 const double inv_p_ref_n = rho_n_compressible ? (1.0 / p_ref_n) : 0.0;
2756 const int offset_n = args.scalar<int>("offset_n");
2757 const int stride_n = args.scalar<int>("stride_n");
2758 // Consistent (Galerkin) point-source injection (MOOSE DiracKernel form):
2759 // R^c_i -= Q_port * N_i(x_p) on the element containing the port.
2760 // sum_i N_i = 1 (partition of unity) -> total injected mass is EXACT and
2761 // mesh-independent; no elementMass lumping. Solution-independent => zero
2762 // Jacobian (same as the lumped path). inj_point_mode==0 keeps the legacy
2763 // lumped volumetric-disk source (injection_dof) byte-identical.
2764 const int inj_point_mode = args.scalar<int>("inj_point_mode");
2765 const int inj_n_ports = args.scalar<int>("inj_n_ports");
2766 xt::pyarray<int> &inj_element = args.array<int>("inj_element"); // containing elem id / rank (-1 if absent)
2767 xt::pyarray<double> &inj_weight = args.array<double>("inj_weight"); // N_i(x_p), [port*nDOF_test_element + i]
2768 xt::pyarray<double> &inj_rate = args.array<double>("inj_rate"); // Q_port * ramp(t) [mol/(s*m_depth)]
2769 // Stage 3b: gas-side kinetic dissolution sink. R_diss = k_d * S_n *
2770 // (1 - S_n) * theta_w * rho_w(c) * (c_sat - c) is subtracted from the
2771 // gas-equation residual at each quadrature point. c is read from TADR's
2772 // u[0].dof aliased Python-side and passed in as c_dof. k_d=0 disables
2773 // the sink (legacy behavior).
2774 xt::pyarray<double> &c_dof = args.array<double>("c_dof");
2775 const double k_d = args.scalar<double>("k_d");
2776 const double c_sat = args.scalar<double>("c_sat");
2777 // CO2 injection: per-node source field (built Python-side, schedule-gated).
2778 // Applied like R_diss but with opposite sign -- a source, not a sink.
2779 // All-zero array when no injection is configured.
2780 xt::pyarray<double> &injection_dof = args.array<double>("injection_dof");
2781 xt::pyarray<double> &globalResidual = args.array<double>("globalResidual");
2782 int nExteriorElementBoundaries_global = args.scalar<int>("nExteriorElementBoundaries_global");
2783 xt::pyarray<int> &exteriorElementBoundariesArray = args.array<int>("exteriorElementBoundariesArray");
2784 xt::pyarray<int> &elementBoundaryElementsArray = args.array<int>("elementBoundaryElementsArray");
2785 xt::pyarray<int> &elementBoundaryLocalElementBoundariesArray = args.array<int>("elementBoundaryLocalElementBoundariesArray");
2786 xt::pyarray<double> &ebqe_velocity_ext = args.array<double>("ebqe_velocity_ext");
2787 xt::pyarray<int> &isDOFBoundary_u = args.array<int>("isDOFBoundary_u");
2788 xt::pyarray<double> &ebqe_bc_u_ext = args.array<double>("ebqe_bc_u_ext");
2789 // component-1 (S_n) boundary arrays.
2790 xt::pyarray<int> &isDOFBoundary_n = args.array<int>("isDOFBoundary_n");
2791 xt::pyarray<double> &ebqe_bc_u_n_ext = args.array<double>("ebqe_bc_u_n_ext");
2792 xt::pyarray<int> &isFluxBoundary_u = args.array<int>("isFluxBoundary_u");
2793 xt::pyarray<double> &ebqe_bc_flux_ext = args.array<double>("ebqe_bc_flux_ext");
2794 xt::pyarray<double> &ebqe_phi = args.array<double>("ebqe_phi");
2795 double epsFact = args.scalar<double>("epsFact");
2796 xt::pyarray<double> &ebqe_u = args.array<double>("ebqe_u");
2797 xt::pyarray<double> &ebqe_theta = args.array<double>("ebqe_theta");
2798 xt::pyarray<double> &ebqe_flux = args.array<double>("ebqe_flux");
2799 // PARAMETERS FOR EDGE BASED STABILIZATION
2800 double cE = args.scalar<double>("cE");
2801 double cK = args.scalar<double>("cK");
2802 // PARAMETERS FOR LOG BASED ENTROPY FUNCTION
2803 double uL = args.scalar<double>("uL");
2804 double uR = args.scalar<double>("uR");
2805 // PARAMETERS FOR EDGE VISCOSITY
2806 int numDOFs = args.scalar<int>("numDOFs");
2807 // numDOFs is the compact component-0 free-DOF count used by the stabilized
2808 // DOF loops. Full matrix slots are recovered from the interleaved global
2809 // CSR using offset/stride-aware indexing.
2810 int numDOFs_u = args.scalar<int>("numDOFs_u");
2811 int NNZ = args.scalar<int>("NNZ");
2812 xt::pyarray<int> &csrRowIndeces_DofLoops = args.array<int>("csrRowIndeces_DofLoops");
2813 xt::pyarray<int> &csrColumnOffsets_DofLoops = args.array<int>("csrColumnOffsets_DofLoops");
2814 xt::pyarray<int> &csrRowIndeces_Full = args.array<int>("csrRowIndeces_Full");
2815 xt::pyarray<int> &csrColumnOffsets_Full = args.array<int>("csrColumnOffsets_Full");
2816 xt::pyarray<int> &csrRowIndeces_CellLoops = args.array<int>("csrRowIndeces_CellLoops");
2817 xt::pyarray<int> &csrColumnOffsets_CellLoops = args.array<int>("csrColumnOffsets_CellLoops");
2818 xt::pyarray<int> &csrColumnOffsets_eb_CellLoops = args.array<int>("csrColumnOffsets_eb_CellLoops");
2819 // C matrices
2820 xt::pyarray<double> &Cx = args.array<double>("Cx");
2821 xt::pyarray<double> &Cy = args.array<double>("Cy");
2822 xt::pyarray<double> &Cz = args.array<double>("Cz");
2823 xt::pyarray<double> &CTx = args.array<double>("CTx");
2824 xt::pyarray<double> &CTy = args.array<double>("CTy");
2825 xt::pyarray<double> &CTz = args.array<double>("CTz");
2826 xt::pyarray<double> &ML = args.array<double>("ML");
2827 xt::pyarray<double> &MC = args.array<double>("MC");
2828
2829 xt::pyarray<double> &delta_x_ij = args.array<double>("delta_x_ij");
2830 // PARAMETERS FOR 1st or 2nd ORDER MPP METHOD
2831 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
2832 STABILIZATION STABILIZATION_TYPE{static_cast<STABILIZATION>(args.scalar<int>("STABILIZATION_TYPE"))};
2833
2834 int ENTROPY_TYPE = args.scalar<int>("ENTROPY_TYPE");
2835 PSK_TYPE_member = args.scalar<int>("PSK_TYPE");
2836 immiscible_member = (args.scalar<int>("immiscible") != 0);
2837 T_C_member = args.scalar<double>("T_C"); // temperature [degC] from input
2838 // FOR FCT
2839 xt::pyarray<double> &dLow = args.array<double>("dLow");
2840 xt::pyarray<double> &fluxMatrix = args.array<double>("fluxMatrix");
2841 xt::pyarray<double> &mDotLow = args.array<double>("mDotLow");
2842 xt::pyarray<double> &mLow = args.array<double>("mLow");
2843 xt::pyarray<double> &dt_times_fH_minus_fL = args.array<double>("dt_times_fH_minus_fL");
2844 xt::pyarray<double> &min_m_bc = args.array<double>("min_m_bc");
2845 xt::pyarray<double> &max_m_bc = args.array<double>("max_m_bc");
2846 // AUX QUANTITIES OF INTEREST
2847 xt::pyarray<double> &quantDOFs = args.array<double>("quantDOFs");
2848 xt::pyarray<double> &mn = args.array<double>("mn");
2849 xt::pyarray<double> &fluxCorrection = args.array<double>("fluxCorrection");
2850 xt::pyarray<double> &limited_solution = args.array<double>("limited_solution");
2851 xt::pyarray<int> &freeDOFMaterialTypes = args.array<int>("freeDOFMaterialTypes");
2852 xt::pyarray<int> &freeDOFToNode_u = args.array<int>("freeDOFToNode_u");
2853 // node2zdof[mesh node] -> comp-1 (z) split DOF for that node (primary copy at a
2854 // facies interface). Under split_z the comp-1 DOF numbering is RENUMBERED off the
2855 // mesh-node index, so the comp-0 lumped-mass DOF-graph loop (indexed by mesh node
2856 // via freeDOFToNode_u) must map node -> z-DOF to read the right z and to write the
2857 // (0,1) dR_w/dz tangent to the column that actually exists in the split sparsity.
2858 // Identity (== mesh node) when split_z == 0 -> byte-identical.
2859 xt::pyarray<int> &node2zdof = args.array<int>("node2zdof");
2860 // Per-comp-1-DOF volume material (mesh node index == comp-1 DOF index, since
2861 // gas has no Dirichlet). Used by the nodal closure-eval block below to
2862 // evaluate krn / p_c / rho_n with the node's actual sand parameters --
2863 // exact at the node, so dkrn/dS_n and dp_c/dS_n in the Jacobian match the
2864 // lambda used in the residual (needed for quadratic Newton / the mass-leak fix).
2865 xt::pyarray<int> &nodeMaterialTypes_n = args.array<int>("nodeMaterialTypes_n");
2866 // Coarsest incident capillary entry pressure p_d=1/alpha [head] per mesh
2867 // node, for the comp-1 element-side capillary entry-pressure barrier.
2868 xt::pyarray<double> &node_pd_min = args.array<double>("node_pd_min");
2869 // Full gas saturation 1-S_wr of the coarsest incident medium per node (the
2870 // saturation the coarse pool fills to against a seal); anchors the valve.
2871 xt::pyarray<double> &node_Sn_max = args.array<double>("node_Sn_max");
2872 // DIAGNOSTIC (mass-creation hunt): gas_diag[0]=max|T_ij - T_ji| (tau
2873 // symmetry), [1]=max|T_ij| (scale), [2]=sum_ij F_ij (flux imbalance ->
2874 // net mass created/destroyed by the edge flux), [3]=sum_ij|F_ij| (scale).
2875 xt::pyarray<double> &gas_diag = args.array<double>("gas_diag");
2876
2877 xt::pyarray<double> &velocity_couple = args.array<double>("velocity_couple");
2878 xt::pyarray<double> &ebqe_velocity_ext_couple = args.array<double>("ebqe_velocity_ext_couple");
2879 // xt::pyarray<double> &q_x = args.array<double>("q_x");
2880 // xt::pyarray<double> &ebqe_x = args.array<double>("ebqe_x");
2881
2882 xt::pyarray<double> &anb_seepage_flux_n = args.array<double>("anb_seepage_flux_n");
2883 xt::pyarray<double> &q_velocity = args.array<double>("q_velocity");
2884 double &anb_seepage_flux(args.scalar<double>("anb_seepage_flux"));
2885 anb_seepage_flux = 0.0;
2886 xt::pyarray<int> &csrRowIndeces_u_u = args.array<int>("csrRowIndeces_u_u");
2887 xt::pyarray<int> &csrColumnOffsets_u_u = args.array<int>("csrColumnOffsets_u_u");
2888 xt::pyarray<int> &csrColumnOffsets_eb_u_u = args.array<int>("csrColumnOffsets_eb_u_u");
2889 // CSR maps for the (1,1) Jacobian block. Used by the
2890 // dedicated component-1 element loop appended at the end to assemble
2891 // the gas-side mass-matrix Jacobian / dt.
2892 xt::pyarray<int> &csrRowIndeces_n_n = args.array<int>("csrRowIndeces_n_n");
2893 // (1,0) cross-block CSR maps for the gas-eq diffusion
2894 // against grad u_w. Currently allocated by the framework but unused by
2895 // the C++ assembly - 3c.2/3c.3 will write into them.
2896 xt::pyarray<int> &csrRowIndeces_n_w = args.array<int>("csrRowIndeces_n_w");
2897 xt::pyarray<int> &csrColumnOffsets_n_n = args.array<int>("csrColumnOffsets_n_n");
2898 xt::pyarray<int> &csrColumnOffsets_n_w = args.array<int>("csrColumnOffsets_n_w");
2899 // P1: comp-0 (H2O) (0,0) and (0,1) block CSR maps -- the framework's
2900 // authoritative (row,col)->flat-nzval offsets for the water equation's
2901 // dependence on p (w_w) and z (w_n). Used by the Richards-style block
2902 // scatter of the two-sided water flux Jacobian (replaces the Full-CSR
2903 // column search that dropped the (0,1) off-diagonal coupling).
2904 xt::pyarray<int> &csrRowIndeces_w_w = args.array<int>("csrRowIndeces_w_w");
2905 xt::pyarray<int> &csrColumnOffsets_w_w = args.array<int>("csrColumnOffsets_w_w");
2906 xt::pyarray<int> &csrRowIndeces_w_n = args.array<int>("csrRowIndeces_w_n");
2907 xt::pyarray<int> &csrColumnOffsets_w_n = args.array<int>("csrColumnOffsets_w_n");
2908 // Comp-1 boundary CSR maps used by the exterior boundary loop appended
2909 // at the end of this routine.
2910 xt::pyarray<int> &csrColumnOffsets_eb_n_n = args.array<int>("csrColumnOffsets_eb_n_n");
2911 xt::pyarray<int> &csrColumnOffsets_eb_n_w = args.array<int>("csrColumnOffsets_eb_n_w");
2912 // COMPONENT-1 (m_n = phi*rho_n*u_n) EV plumbing. Mirrors the comp-0
2913 // EV scaffolding: a compact DOF graph (csrRowIndeces_n_DofLoops), per-edge
2914 // dLow_n / dEV_n storage, and per-DOF mLow_n / mDotLow_n. Sensor / sensor
2915 // bounds (u_n_L, u_n_R) operate on S_n; the stabilization itself acts on
2916 // m_n through chain rule dm_n/du_n = -phi*rho_n.
2917 int numDOFs_n = args.scalar<int>("numDOFs_n");
2918 int NNZ_n = args.scalar<int>("NNZ_n");
2919 xt::pyarray<int> &csrRowIndeces_n_DofLoops = args.array<int>("csrRowIndeces_n_DofLoops");
2920 xt::pyarray<int> &csrColumnOffsets_n_DofLoops = args.array<int>("csrColumnOffsets_n_DofLoops");
2921 // comp1_full_offsets[k] = full globalJacobian flat offset of the k-th compact
2922 // comp-1 (1,1) CSR entry -- lets the node-split interface loop write the comp-1
2923 // tangent directly into globalJacobian (Richards DOF-graph style).
2924 xt::pyarray<int> &comp1_full_offsets = args.array<int>("comp1_full_offsets");
2925 // comp10_full_offsets[2*ip+s] = full globalJacobian flat offset of the (1,0)
2926 // cross-block entry (row = comp-1 DOF z_{a|b} of interface pair ip, col = the
2927 // shared pressure DOF p_node), s=0 -> z_a, s=1 -> z_b. The node-split interface
2928 // CO2 flux F is evaluated from flashPZ(p_node, z) so dF/dp_node != 0; this map
2929 // lets the interface loop scatter that pressure tangent Richards-style (direct
2930 // globalJacobian[off] write), mirroring comp1_full_offsets for the (1,1) block.
2931 // -1 sentinel => slot absent (skipped). Empty / unused when split_z == 0.
2932 xt::pyarray<int> &comp10_full_offsets = args.array<int>("comp10_full_offsets");
2933 // comp1_iface_offsets[2*ip+s] = full globalJacobian flat offset of the (1,1)
2934 // interface OFF-DIAGONAL: s=0 -> (z_a row, z_b col), s=1 -> (z_b row, z_a col).
2935 // These slots are allocated by getExtraSparsityElements (the two z-copies share no
2936 // element, so findNonzeros never makes them) and EXCLUDED from the compact comp-1
2937 // graph, so the off-diagonal tangent is scattered HERE -- not via comp1_offset /
2938 // comp1_full_offsets, which no longer carry it. -1 => absent (warns).
2939 xt::pyarray<int> &comp1_iface_offsets = args.array<int>("comp1_iface_offsets");
2940 xt::pyarray<double> &dLow_n = args.array<double>("dLow_n");
2941 xt::pyarray<double> &dEV_n = args.array<double>("dEV_n");
2942 xt::pyarray<double> &fluxMatrix_n = args.array<double>("fluxMatrix_n");
2943 xt::pyarray<double> &mLow_n = args.array<double>("mLow_n");
2944 xt::pyarray<double> &mDotLow_n = args.array<double>("mDotLow_n");
2945 double u_n_L = args.scalar<double>("u_n_L");
2946 double u_n_R = args.scalar<double>("u_n_R");
2947 xt::pyarray<double> &mn_n = args.array<double>("mn_n"); // m_n at t^n (numDOFs_u)
2948 xt::pyarray<double> &quantDOFs_n = args.array<double>("quantDOFs_n"); // sensor scratch (numDOFs_u)
2949 // Per-node gas-residual BUDGET (mass-creation hunt). Each gas-equation
2950 // residual contribution is accumulated into its own slot at the node i it
2951 // is scattered to, so Python can sum over OWNED nodes only and MPI-reduce
2952 // (parallel-exact, no overlap double-count -- the owned-node pattern, like
2953 // mLow_n/mn_n). Layout is term-major, size 6*numDOFs_n:
2954 // [0]=accumulation (m_n - mn_n)/dt [1]=interior upwind flux -sum F
2955 // [2]=dissolution sink +R_diss [3]=injection -Q_inj
2956 // [4]=exterior boundary flux [5]=TOTAL scattered residual (~0 @ conv)
2957 // At convergence slot5~0, so slot0 = -(slot1+slot2+slot3+slot4). If the gas
2958 // mass grows yet slots 1+4 (the only non-telescoping, non-source terms) sum
2959 // to ~0, the creation is POST-kernel (FCT/inversion/coupling); if slot1 or
2960 // slot4 is the culprit it shows up here directly -- including a per-rank
2961 // boundary leak (slot4 != 0 on a partition that mis-tags an interior face).
2962 xt::pyarray<double> &gas_budget_node = args.array<double>("gas_budget_node");
2963 // Comp-1 FCT plumbing read here so the gate at the end of this routine
2964 // can call FCTStep_n with all args present.
2965 xt::pyarray<double> &dt_times_fH_minus_fL_n = args.array<double>("dt_times_fH_minus_fL_n");
2966 xt::pyarray<double> &fluxCorrection_n = args.array<double>("fluxCorrection_n");
2967 int FCT_n = args.scalar<int>("FCT_n");
2968 // double Rpos[numDOFs], Rneg[numDOFs];
2969 std::vector<double> Rpos(numDOFs, 0.0), Rneg(numDOFs, 0.0);
2970 std::vector<double> TransportMatrix(NNZ, 0.0),
2971 TransportMatrixConsistent(NNZ, 0.0),
2972 TransportMatrixn(NNZ, 0.0),
2973 TransportMatrixConsistentn(NNZ, 0.0);
2974 //double FluxCorrectionMatrix[NNZ];
2975 // NOTE: This function follows a different (but equivalent) implementation of the smoothness based indicator than NCLS.h
2976 // Allocate space for the transport matrices
2977 // This is used for first order KUZMIN'S METHOD
2978 // double TransportMatrix[NNZ], TransportMatrixConsistent[NNZ];
2979 // double TransportMatrixn[NNZ], TransportMatrixConsistentn[NNZ];
2980 std::valarray<double> u_free_dof(numDOFs);
2981 std::valarray<double> u_free_dof_old(numDOFs);
2982 std::valarray<double> ML2(numDOFs);
2983 // Lumped L2 projection buffers for density
2984 std::vector<double> rho_dof(numDOFs, 0.0);
2985 std::vector<double> ML_rho(numDOFs, 0.0);
2986 std::fill(velocity_couple.data(), velocity_couple.data() + velocity_couple.size(), 0.0);
2987 std::fill(ebqe_velocity_ext_couple.data(), ebqe_velocity_ext_couple.data() + ebqe_velocity_ext_couple.size(), 0.0);
2988 auto full_offset_from_compact = [&](int i_compact, int j_compact) -> int
2989 {
2990 const int full_i = offset_u + stride_u * i_compact;
2991 const int full_j = offset_u + stride_u * j_compact;
2992 for (int offset = csrRowIndeces_Full.data()[full_i]; offset < csrRowIndeces_Full.data()[full_i + 1]; ++offset)
2993 if (csrColumnOffsets_Full.data()[offset] == full_j) return offset;
2994 return -1;
2995 };
2996
2997 for (int eN = 0; eN < nElements_global; eN++)
2998 for (int j = 0; j < nDOF_trial_element; j++) {
2999 int eN_nDOF_trial_element = eN * nDOF_trial_element;
3000 u_free_dof[r_l2g.data()[eN_nDOF_trial_element + j]] = u_dof.data()[u_l2g.data()[eN_nDOF_trial_element + j]];
3001 u_free_dof_old[r_l2g.data()[eN_nDOF_trial_element + j]] = u_dof_old.data()[u_l2g.data()[eN_nDOF_trial_element + j]];
3002 }
3003 for (int i = 0; i < NNZ; i++) {
3004 TransportMatrix[i] = 0.;
3005 TransportMatrixConsistent[i] = 0.;
3006 TransportMatrixn[i] = 0.;
3007 TransportMatrixConsistentn[i] = 0.;
3008 }
3009
3010 // Project quadrature density to nodal DOFs before constructing nodal
3011 // stabilization potentials so Phi uses a true nodal density field.
3012 for (int eN = 0; eN < nElements_global; eN++) {
3013 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
3014 for (int k = 0; k < nQuadraturePoints_element; k++) {
3015 const int eN_k = eN * nQuadraturePoints_element + k;
3016 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x, y, z;
3017 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
3018 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
3019 jac, jacDet, jacInv, x, y, z);
3020 const double dV = fabs(jacDet) * dV_ref.data()[k];
3021 for (int i = 0; i < nDOF_test_element; i++) {
3022 const int eN_i = eN * nDOF_test_element + i;
3023 const int free_gi = r_l2g.data()[eN_i];
3024 const double u_test_dV = u_test_ref.data()[k * nDOF_trial_element + i] * dV;
3025 rho_dof[free_gi] += q_rho.data()[eN_k] * u_test_dV;
3026 ML_rho[free_gi] += u_test_dV;
3027 }
3028 }
3029 }
3030 for (int i = 0; i < numDOFs; ++i) {
3031 if (ML_rho[i] > 0.0) rho_dof[i] /= ML_rho[i];
3032 else rho_dof[i] = rho;
3033 }
3034 // Cache the projected, salinity-coupled density for use in invert() so
3035 // the m -> u inversion is consistent with the forward residual.
3036 rho_dof_member = rho_dof;
3037
3038 // -------- Comp-1: lumped L2 projections for the upwind potential flux --------
3039 // rho_n_phi_dof, rho_w_dof: as before -- invert(COMPONENT=1) and R_diss.
3040 // pc_dof, dpc_dof, krn_dof, dkrn_dof, rho_n_dof: nodal closure values
3041
3042 std::vector<double> rho_n_phi_dof(numDOFs_n, 0.0);
3043 // Old-time phi*N for the accumulation old mass m_c_old = (phi*N_old)*z_old.
3044 std::vector<double> rho_n_phi_dof_old(numDOFs_n, 0.0);
3045 // Lumped nodal phi*dN/dz and phi*dN/dp -- the compositional accumulation
3046 // Jacobian needs dm_c/dz = phi*(dN/dz*z + N) and dm_c/dp = phi*dN/dp*z.
3047 std::vector<double> dphiN_dz_dof(numDOFs_n, 0.0);
3048 std::vector<double> dphiN_dp_dof(numDOFs_n, 0.0);
3049 std::vector<double> rho_w_dof(numDOFs_n, 0.0);
3050 std::vector<double> rho_n_dof(numDOFs_n, 0.0);
3051 std::vector<double> pc_dof(numDOFs_n, 0.0);
3052 std::vector<double> dpc_dof(numDOFs_n, 0.0);
3053 std::vector<double> krn_dof(numDOFs_n, 0.0);
3054 std::vector<double> dkrn_dof(numDOFs_n, 0.0);
3055 std::vector<double> rho_n_dof_old(numDOFs_n, 0.0);
3056 std::vector<double> pc_dof_old(numDOFs_n, 0.0);
3057 std::vector<double> krn_dof_old(numDOFs_n, 0.0);
3058 // Uncapped Brooks-Corey p_c (+ dp_c/dS_n and old-time sibling), used ONLY on
3059 // material-interface edges for entry-pressure capillary breakthrough.
3060 std::vector<double> pc_uncap_dof(numDOFs_n, 0.0);
3061 std::vector<double> dpc_uncap_dof(numDOFs_n, 0.0);
3062 std::vector<double> pc_uncap_dof_old(numDOFs_n, 0.0);
3063 std::vector<double> ML_n(numDOFs_n, 0.0);
3064 std::vector<double> Sg_dof_old(numDOFs_n, 0.0);
3065 std::vector<double> X_dof_old(numDOFs_n, 0.0);
3066 for (int eN = 0; eN < nElements_global; eN++) {
3067 const int mat_eN_proj = elementMaterialTypes.data()[eN];
3068 const double phi_eN = thetaR.data()[mat_eN_proj] + thetaSR.data()[mat_eN_proj];
3069 const double alpha_eN_p = alpha.data()[mat_eN_proj];
3070 const double n_vg_eN_p = n.data()[mat_eN_proj];
3071 const double krn_end_p = krn_end.data()[mat_eN_proj];
3072 const double S_wr_p = thetaR.data()[mat_eN_proj] / phi_eN;
3073 const double one_m_Sr_p = 1.0 - S_wr_p;
3074 const double Se_trap_L3043 = 1.0 - S_gr.data()[mat_eN_proj] / one_m_Sr_p; // gas-only residual trapping
3075 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
3076 for (int k = 0; k < nQuadraturePoints_element; k++) {
3077 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x_p, y_p, z_p;
3078 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
3079 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
3080 jac, jacDet, jacInv, x_p, y_p, z_p);
3081 const double dV = std::fabs(jacDet) * dV_ref.data()[k];
3082 // Current iterate values at QP.
3083 double u_w_p = 0.0, u_n_p = 0.0;
3084 ck.valFromDOF(u_dof.data(),
3085 &u_l2g.data()[eN_nDOF_trial_element],
3086 &u_trial_ref.data()[k * nDOF_trial_element], u_w_p);
3087 ck.valFromDOF(u_dof_n.data(),
3088 &u_l2g_n.data()[eN_nDOF_trial_element],
3089 &u_trial_ref.data()[k * nDOF_trial_element], u_n_p);
3090 // Closure at current iterate -- Se, p_c, dp_c/dS_n, k_rn, dk_rn/dS_n.
3091 const double Se_p_raw = (1.0 - u_n_p - S_wr_p) / one_m_Sr_p;
3092 double Se_p, dSe_du_n_p;
3093 if (Se_p_raw <= 0.0) { Se_p = 0.0; dSe_du_n_p = 0.0; }
3094 else if (Se_p_raw >= 1.0) { Se_p = 1.0; dSe_du_n_p = 0.0; }
3095 else { Se_p = Se_p_raw; dSe_du_n_p = -1.0 / one_m_Sr_p; }
3096 double pc_p = 0.0, dpc_dSe_p = 0.0, d2pc_p_unused = 0.0;
3097 if (PSK_TYPE_member == 1)
3098 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_p, alpha_eN_p, n_vg_eN_p, pc_p, dpc_dSe_p, d2pc_p_unused);
3099 else
3100 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_p, alpha_eN_p, n_vg_eN_p, pc_p, dpc_dSe_p, d2pc_p_unused);
3101 const double dpc_dSn_p = dpc_dSe_p * dSe_du_n_p;
3102 double krn_p = 0.0, dkrn_dSe_p = 0.0;
3103 if (PSK_TYPE_member == 1)
3104 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_p, alpha_eN_p, n_vg_eN_p, krn_p, dkrn_dSe_p, Se_trap_L3043);
3105 else
3106 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_p, alpha_eN_p, n_vg_eN_p, krn_p, dkrn_dSe_p, Se_trap_L3043);
3107 krn_p = krn_p * krn_end_p / mu_n;
3108 const double dkrn_dSn_p = dkrn_dSe_p * krn_end_p * dSe_du_n_p / mu_n;
3109 const double rho_n_p = rho_n_compressible ? (rho_n * exp(fmin((u_w_p + pc_p) * inv_p_ref_n, 50.0))) : rho_n;
3110 const double phi_rho_n_qp = phi_eN * rho_n_p;
3111 // Old-time-level values for the (1-Theta) part of the edge flux.
3112 double u_w_p_old = 0.0, u_n_p_old = 0.0;
3113 ck.valFromDOF(u_dof_old.data(),
3114 &u_l2g.data()[eN_nDOF_trial_element],
3115 &u_trial_ref.data()[k * nDOF_trial_element], u_w_p_old);
3116 ck.valFromDOF(u_dof_n_old.data(),
3117 &u_l2g_n.data()[eN_nDOF_trial_element],
3118 &u_trial_ref.data()[k * nDOF_trial_element], u_n_p_old);
3119 const double Se_p_old_raw = (1.0 - u_n_p_old - S_wr_p) / one_m_Sr_p;
3120 double Se_p_old;
3121 if (Se_p_old_raw <= 0.0) Se_p_old = 0.0;
3122 else if (Se_p_old_raw >= 1.0) Se_p_old = 1.0;
3123 else Se_p_old = Se_p_old_raw;
3124 double pc_p_old = 0.0, dpc_p_old_unused = 0.0, d2pc_p_old_unused = 0.0;
3125 if (PSK_TYPE_member == 1)
3126 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_p_old, alpha_eN_p, n_vg_eN_p, pc_p_old, dpc_p_old_unused, d2pc_p_old_unused);
3127 else
3128 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_p_old, alpha_eN_p, n_vg_eN_p, pc_p_old, dpc_p_old_unused, d2pc_p_old_unused);
3129 double krn_p_old = 0.0, dkrn_p_old_unused = 0.0;
3130 if (PSK_TYPE_member == 1)
3131 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_p_old, alpha_eN_p, n_vg_eN_p, krn_p_old, dkrn_p_old_unused, Se_trap_L3043);
3132 else
3133 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_p_old, alpha_eN_p, n_vg_eN_p, krn_p_old, dkrn_p_old_unused, Se_trap_L3043);
3134 krn_p_old = krn_p_old * krn_end_p / mu_n;
3135 const double rho_n_p_old = rho_n_compressible ? (rho_n * exp(fmin((u_w_p_old + pc_p_old) * inv_p_ref_n, 50.0))) : rho_n;
3136 // TADR brine density at this QP.
3137 const int eN_k_proj = eN * nQuadraturePoints_element + k;
3138 const double rho_w_qp_proj = q_rho.data()[eN_k_proj];
3139 const double z_cl_pr = fmin(fmax(u_n_p, 1.0e-8), 1.0 - 1.0e-8);
3140 const double p_cl_pr = fmax(u_w_p, 1.0e2);
3141 const double z_cl_pr_old = fmin(fmax(u_n_p_old, 1.0e-8), 1.0 - 1.0e-8);
3142 const double p_cl_pr_old = fmax(u_w_p_old, 1.0e2);
3147 const double Sa_pr = 1.0 - fs_pr.S_g;
3148 const double N_pr = fs_pr.rho_g*fs_pr.S_g + fs_pr.rho_a*Sa_pr;
3149 const double dN_dp_pr = fs_pr.drho_g_dp*fs_pr.S_g + fs_pr.rho_g*fs_pr.dS_g_dp
3150 + fs_pr.drho_a_dp*Sa_pr - fs_pr.rho_a*fs_pr.dS_g_dp;
3151 const double dN_dz_pr = fs_pr.drho_g_dz*fs_pr.S_g + fs_pr.rho_g*fs_pr.dS_g_dz
3152 + fs_pr.drho_a_dz*Sa_pr - fs_pr.rho_a*fs_pr.dS_g_dz;
3153 const double N_pr_old = fs_pr_old.rho_g*fs_pr_old.S_g
3154 + fs_pr_old.rho_a*(1.0 - fs_pr_old.S_g);
3155 const double phiN_qp = phi_eN * N_pr;
3156 const double phiN_old_qp = phi_eN * N_pr_old;
3157 const double dphiN_dz_qp = phi_eN * dN_dz_pr;
3158 const double dphiN_dp_qp = phi_eN * dN_dp_pr;
3159 for (int i = 0; i < nDOF_test_element; i++) {
3160 const int eN_i = eN * nDOF_test_element + i;
3161 const int gi = u_l2g_n.data()[eN_i]; // comp-1 split DOF (lumped phi*N projection)
3162 const double u_test_dV = u_test_ref.data()[k * nDOF_trial_element + i] * dV;
3163 // P1: phi*N (and derivatives) for the compositional CO2 accumulation.
3164 rho_n_phi_dof[gi] += phiN_qp * u_test_dV;
3165 rho_n_phi_dof_old[gi] += phiN_old_qp * u_test_dV;
3166 dphiN_dz_dof[gi] += dphiN_dz_qp * u_test_dV;
3167 dphiN_dp_dof[gi] += dphiN_dp_qp * u_test_dV;
3168 // Phase-based closures below still feed the (P2) upwind flux pass.
3169 rho_w_dof[gi] += rho_w_qp_proj * u_test_dV;
3170 rho_n_dof[gi] += rho_n_p * u_test_dV;
3171 pc_dof[gi] += pc_p * u_test_dV;
3172 dpc_dof[gi] += dpc_dSn_p * u_test_dV;
3173 krn_dof[gi] += krn_p * u_test_dV;
3174 dkrn_dof[gi] += dkrn_dSn_p * u_test_dV;
3175 rho_n_dof_old[gi] += rho_n_p_old * u_test_dV;
3176 pc_dof_old[gi] += pc_p_old * u_test_dV;
3177 krn_dof_old[gi] += krn_p_old * u_test_dV;
3178 // CO2-free-anchor gate inputs, OLD-time flash (lagged -> frozen active set).
3179 Sg_dof_old[gi] += fs_pr_old.S_g * u_test_dV;
3180 X_dof_old[gi] += fs_pr_old.X * u_test_dV;
3181 ML_n[gi] += u_test_dV;
3182 }
3183 }
3184 }
3185 // Normalize over ALL comp-1 nodes (numDOFs_n), not numDOFs_u -- the arrays
3186 // and the projection above span every mesh node, including the top-boundary
3187 // p_w-Dirichlet nodes that are absent from the comp-0 free-DOF count.
3188 for (int i = 0; i < numDOFs_n; ++i) {
3189 if (ML_n[i] > 0.0) {
3190 rho_n_phi_dof[i] /= ML_n[i];
3191 rho_n_phi_dof_old[i] /= ML_n[i];
3192 dphiN_dz_dof[i] /= ML_n[i];
3193 dphiN_dp_dof[i] /= ML_n[i];
3194 rho_w_dof[i] /= ML_n[i];
3195 rho_n_dof[i] /= ML_n[i];
3196 pc_dof[i] /= ML_n[i];
3197 dpc_dof[i] /= ML_n[i];
3198 krn_dof[i] /= ML_n[i];
3199 dkrn_dof[i] /= ML_n[i];
3200 rho_n_dof_old[i] /= ML_n[i];
3201 pc_dof_old[i] /= ML_n[i];
3202 krn_dof_old[i] /= ML_n[i];
3203 Sg_dof_old[i] /= ML_n[i];
3204 X_dof_old[i] /= ML_n[i];
3205 } else {
3206 rho_n_phi_dof[i] = thetaR.data()[0] + thetaSR.data()[0]; // fallback
3207 rho_n_phi_dof_old[i] = thetaR.data()[0] + thetaSR.data()[0]; // fallback
3208 rho_w_dof[i] = rho; // fallback
3209 rho_n_dof[i] = rho_n; // fallback
3210 rho_n_dof_old[i] = rho_n;
3211 // pc, dpc, krn, dkrn default to 0 -- only nodes with no element
3212 // contributions ever hit this branch.
3213 }
3214 rho_n_phi_dof[i] = std::max(rho_n_phi_dof[i], 1.0e-16);
3215 }
3216 rho_n_phi_dof_member = rho_n_phi_dof;
3217
3218 // compute entropy and init global_entropy_residual and boundary_integral
3219 double psi[numDOFs], eta[numDOFs], global_entropy_residual[numDOFs], boundary_integral[numDOFs];
3220 for (int i = 0; i < numDOFs; i++) {
3221 // NODAL ENTROPY //
3222 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) //EV stab
3223 {
3224 double solni = 1.0 * u_free_dof_old[i];
3225 eta[i] = ENTROPY_TYPE == 1 ? ENTROPY(solni, uL, uR) : ENTROPY_LOG(solni, uL, uR);
3226 global_entropy_residual[i] = 0.;
3227 }
3228 boundary_integral[i] = 0.;
3229 ML2[i] = 0.0;
3230 }
3231
3233 // ** LOOP IN CELLS FOR CELL BASED TERMS ** //
3235 // HERE WE COMPUTE:
3236 // * Time derivative term. u_t
3237 // * cell based CFL (for reference)
3238 // * Entropy residual
3239 // * Transport matrices
3240
3241 for (int eN = 0; eN < nElements_global; eN++) {
3242 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
3243 const int eN_nDOF_mesh_trial_element = eN * nDOF_mesh_trial_element;
3244 //declare local storage for local contributions and initialize
3245 double elementResidual_u[nDOF_test_element], element_entropy_residual[nDOF_test_element], Phi[nDOF_trial_element], Phi_n[nDOF_trial_element];
3246 double elementTransport[nDOF_test_element][nDOF_trial_element], elementTransportConsistent[nDOF_test_element][nDOF_trial_element];
3247 double elementTransportn[nDOF_test_element][nDOF_trial_element], elementTransportConsistentn[nDOF_test_element][nDOF_trial_element];
3248 for (int j = 0; j < nDOF_trial_element; j++) {
3249 const int u_gj = u_l2g.data()[eN_nDOF_trial_element + j];
3250 const int free_gj = r_l2g.data()[eN_nDOF_trial_element + j];
3251 const int x_gj = mesh_l2g.data()[eN_nDOF_mesh_trial_element + j];
3252 const double rho_node_j = rho_dof[free_gj];
3253 Phi[j] = u_dof.data()[u_gj];
3254 Phi_n[j] = u_dof_old.data()[u_gj];
3255 for (int I = 0; I < nSpace; I++) {
3256 // Pressure form: grad(p) - rho_w g (no /rho0 factor).
3257 Phi[j] -= rho_node_j * mesh_dof.data()[x_gj * 3 + I] * gravity[I];
3258 Phi_n[j] -= rho_node_j * mesh_dof.data()[x_gj * 3 + I] * gravity[I];
3259 }
3260 }
3261 for (int i = 0; i < nDOF_test_element; i++) {
3262 elementResidual_u[i] = 0.0;
3263 element_entropy_residual[i] = 0.0;
3264 for (int j = 0; j < nDOF_trial_element; j++) {
3265 elementTransport[i][j] = 0.0;
3266 elementTransportConsistent[i][j] = 0.0;
3267 elementTransportn[i][j] = 0.0;
3268 elementTransportConsistentn[i][j] = 0.0;
3269 }
3270 }
3271 //loop over quadrature points and compute integrands
3272 for (int k = 0; k < nQuadraturePoints_element; k++) {
3273 //compute indeces and declare local storage
3274 int eN_k = eN * nQuadraturePoints_element + k, eN_k_nSpace = eN_k * nSpace;
3275 double
3276 // for entropy residual
3277 aux_entropy_residual = 0.,
3278 DENTROPY_un, DENTROPY_uni,
3279 //for mass matrix contributions
3280 u = 0.0, un = 0.0, grad_phi[nSpace], grad_phi_n[nSpace], grad_u_velocity[nSpace], velocity_loc[nSpace], u_test_dV[nDOF_trial_element], u_grad_trial[nDOF_trial_element * nSpace], u_grad_test_dV[nDOF_test_element * nSpace],
3281 //for general use
3282 jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], dV, x, y, z, xt, yt, zt, m, dm, f[nSpace], df[nSpace], a[nnz], da[nnz], as[nnz], mn, dmn, fn[nSpace], dfn[nSpace], an[nnz], dan[nnz], asn[nnz];
3283 //get the physical integration weight
3284 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), mesh_grad_trial_ref.data(), jac, jacDet, jacInv, x, y, z);
3285 ck.calculateMappingVelocity_element(eN, k, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), xt, yt, zt);
3286 dV = fabs(jacDet) * dV_ref.data()[k];
3287 //get the solution (of Newton's solver). To compute time derivative term
3288 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_ref.data()[k * nDOF_trial_element], u);
3289 //get the solution at quad point at tn and tnm1 for entropy viscosity
3290 ck.valFromDOF(u_dof_old.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_ref.data()[k * nDOF_trial_element], un);
3291 //get the solution gradients at tn for entropy viscosity
3292 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace], jacInv, u_grad_trial);
3293 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial, grad_u_velocity);
3294
3295 //populate q_x
3296 // const int eN_k_3d = eN_k * 3;
3297 // q_x.data()[eN_k_3d + 0] = x;
3298 // q_x.data()[eN_k_3d + 1] = y;
3299 // q_x.data()[eN_k_3d + 2] = z;
3300 //precalculate test function products with integration weights for mass matrix terms
3301 for (int I = 0; I < nSpace; I++) {
3302 grad_phi[I] = 0.0;
3303 grad_phi_n[I] = 0.0;
3304 }
3305 for (int j = 0; j < nDOF_trial_element; j++) {
3306 u_test_dV[j] = u_test_ref.data()[k * nDOF_trial_element + j] * dV;
3307 for (int I = 0; I < nSpace; I++) {
3308 grad_phi_n[I] += Phi_n[j] * u_grad_trial[j * nSpace + I];
3309 grad_phi[I] += Phi[j] * u_grad_trial[j * nSpace + I];
3310 u_grad_test_dV[j * nSpace + I] = u_grad_trial[j * nSpace + I] * dV; //cek warning won't work for Petrov-Galerkin
3311 }
3312 }
3313 //
3314 //calculate pde coefficients at quadrature points
3315 //
3316 double Kr, dKr, Krn, dKrn, thetaW, thetaWn;
3317 const double rho_local = q_rho.data()[eN_k];
3318 const double rho_velocity = std::fabs(rho_local) > 1.0e-12 ? rho_local : rho;
3319
3320 // Cross-derivative buffers are filled by _from_Se but ignored downstream;
3321 // the EV residual only uses (mn, fn, an) and (m, f, a).
3322 double dm_du_n_qp_n = 0.0, dkr_du_n_qp_n = 0.0;
3323 double dm_du_n_qp = 0.0, dkr_du_n_qp = 0.0;
3324 double df_du_n_qp_n[nSpace], df_du_n_qp[nSpace];
3325 double da_du_n_qp_n[nnz], da_du_n_qp[nnz];
3326 for (int I = 0; I < nSpace; I++) { df_du_n_qp_n[I] = 0.0; df_du_n_qp[I] = 0.0; }
3327 for (int ii = 0; ii < nnz; ii++) { da_du_n_qp_n[ii] = 0.0; da_du_n_qp[ii] = 0.0; }
3328 double u_n_qp = 0.0, u_n_qp_old = 0.0;
3329 ck.valFromDOF(u_dof_n.data(),
3330 &u_l2g.data()[eN_nDOF_trial_element],
3331 &u_trial_ref.data()[k * nDOF_trial_element], u_n_qp);
3332 ck.valFromDOF(u_dof_n_old.data(),
3333 &u_l2g.data()[eN_nDOF_trial_element],
3334 &u_trial_ref.data()[k * nDOF_trial_element], u_n_qp_old);
3335 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_local, beta, gravity.data(),
3336 alpha.data()[elementMaterialTypes[eN]], n.data()[elementMaterialTypes[eN]],
3337 thetaR.data()[elementMaterialTypes[eN]], thetaSR.data()[elementMaterialTypes[eN]],
3338 &KWs.data()[elementMaterialTypes[eN] * nnz], un, u_n_qp_old,
3339 mn, dmn, dm_du_n_qp_n, fn, dfn, df_du_n_qp_n, an, dan, da_du_n_qp_n,
3340 asn, Krn, dKrn, dkr_du_n_qp_n, thetaWn);
3341 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_local, beta, gravity.data(),
3342 alpha.data()[elementMaterialTypes[eN]], n.data()[elementMaterialTypes[eN]],
3343 thetaR.data()[elementMaterialTypes[eN]], thetaSR.data()[elementMaterialTypes[eN]],
3344 &KWs.data()[elementMaterialTypes[eN] * nnz], u, u_n_qp,
3345 m, dm, dm_du_n_qp, f, df, df_du_n_qp, a, da, da_du_n_qp,
3346 as, Kr, dKr, dkr_du_n_qp, thetaW);
3347 q_theta.data()[eN_k] = thetaW;
3348
3349 // Darcy velocity for coupling should use the direct FE gradient of the
3350 // pressure head. The Phi-based gradients are only for stabilization.
3351 for (int I = 0; I < nSpace; ++I) {
3352 q_velocity.data()[eN_k_nSpace + I] = grad_u_velocity[I];
3353 }
3354
3355 double pressure_gradient[nSpace];
3356 for (int J = 0; J < nSpace; ++J)
3357 pressure_gradient[J] = grad_u_velocity[J] - rho_velocity * gravity.data()[J];
3358
3359 for (int I = 0; I < nSpace; ++I) {
3360 double acc = 0.0;
3361 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I+1]; ++ii) {
3362 const int J = a_colind.data()[ii];
3363 acc += (a[ii] / rho_velocity) * pressure_gradient[J];
3364 }
3365 velocity.data()[eN_k_nSpace + I] = -acc;
3366 velocity_couple.data()[eN_k_nSpace + I] = -acc;
3367 }
3368 // static int debug_ev_prints = 0;
3369 // if (debug_ev_prints < 6 && eN < 2 && k < 2)
3370 // {
3371 // std::cout << "[Richards EV fixed] eN=" << eN
3372 // << " k=" << k
3373 // << " u=" << u
3374 // << " un=" << un
3375 // << " grad_phi=(" << grad_phi[0];
3376 // for (int I = 1; I < nSpace; ++I)
3377 // std::cout << "," << grad_phi[I];
3378 // std::cout << ") grad_u_velocity=(" << grad_u_velocity[0];
3379 // for (int I = 1; I < nSpace; ++I)
3380 // std::cout << "," << grad_u_velocity[I];
3381 // std::cout << ") velocity_couple=(" << velocity_couple.data()[eN_k_nSpace + 0];
3382 // for (int I = 1; I < nSpace; ++I)
3383 // std::cout << "," << velocity_couple.data()[eN_k_nSpace + I];
3384 // std::cout << ")" << std::endl;
3385 // for (int j = 0; j < nDOF_trial_element; ++j)
3386 // {
3387 // const int u_gj = u_l2g.data()[eN_nDOF_trial_element + j];
3388 // const int x_gj = mesh_l2g.data()[eN_nDOF_mesh_trial_element + j];
3389 // const int free_gj = r_l2g.data()[eN_nDOF_trial_element + j];
3390 // std::cout << " [EV fixed dof] j=" << j
3391 // << " mapped_u=" << u_dof.data()[u_gj]
3392 // << " mapped_u_old=" << u_dof_old.data()[u_gj]
3393 // << " free_material=" << freeDOFMaterialTypes.data()[free_gj]
3394 // << " mapped_x=(" << mesh_dof.data()[x_gj * 3 + 0]
3395 // << "," << mesh_dof.data()[x_gj * 3 + 1]
3396 // << "," << mesh_dof.data()[x_gj * 3 + 2]
3397 // << ") Phi=" << Phi[j]
3398 // << " Phi_n=" << Phi_n[j]
3399 // << std::endl;
3400 // }
3401 // debug_ev_prints++;
3402 // }
3403 // if (nSpace != 2) {std::cout << "WARNING nSpace=" << nSpace << std::endl;}
3404 //
3405 //moving mesh
3406 //
3407 double mesh_velocity[3];
3408 mesh_velocity[0] = xt;
3409 mesh_velocity[1] = yt;
3410 mesh_velocity[2] = zt;
3411 //relative velocity at tn
3412 for (int I = 0; I < nSpace; I++) {
3413 f[I] -= MOVING_DOMAIN * m * mesh_velocity[I];
3414 velocity_loc[I] = df[I] * (2.0 * dm * dm / (dm * dm + fmax(1.0e-16, dm * dm)));
3415 }
3417 // CALCULATE CELL BASED CFL //
3419 calculateCFL(elementDiameter.data()[eN] / degree_polynomial, velocity_loc, cfl.data()[eN_k]);
3421 // CALCULATE ENTROPY RESIDUAL AT QUAD POINT //
3423 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) // EV stab
3424 {
3425 for (int I = 0; I < nSpace; I++) aux_entropy_residual += velocity_loc[I] * grad_phi_n[I];
3426 DENTROPY_un = ENTROPY_TYPE == 1 ? DENTROPY(un, uL, uR) : DENTROPY_LOG(un, uL, uR);
3427 }
3429 // ith-LOOP //
3431 for (int i = 0; i < nDOF_test_element; i++) {
3432 // VECTOR OF ENTROPY RESIDUAL //
3433 int eN_i = eN * nDOF_test_element + i;
3434 ML2[r_l2g.data()[eN_i]] += u_test_dV[i];
3435 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) // EV stab
3436 {
3437 double uni = u_dof_old.data()[u_l2g.data()[eN_i]];
3438 DENTROPY_uni = ENTROPY_TYPE == 1 ? DENTROPY(uni, uL, uR) : DENTROPY_LOG(uni, uL, uR);
3439 element_entropy_residual[i] += (DENTROPY_un - DENTROPY_uni) * aux_entropy_residual * u_test_dV[i];
3440 }
3441
3442 elementResidual_u[i] += m * u_test_dV[i];
3444 // j-th LOOP // To construct transport matrices
3446
3447 for (int j = 0; j < nDOF_trial_element; j++) {
3448 int j_nSpace = j * nSpace;
3449 int i_nSpace = i * nSpace;
3450 elementTransport[i][j] += ck.SimpleDiffusionJacobian_weak(a_rowptr.data(), a_colind.data(), as, &u_grad_trial[j_nSpace], &u_grad_test_dV[i_nSpace]);
3451 elementTransportConsistent[i][j] += ck.SimpleDiffusionJacobian_weak(a_rowptr.data(), a_colind.data(), a, &u_grad_trial[j_nSpace], &u_grad_test_dV[i_nSpace]);
3452 elementTransportn[i][j] += ck.SimpleDiffusionJacobian_weak(a_rowptr.data(), a_colind.data(), asn, &u_grad_trial[j_nSpace], &u_grad_test_dV[i_nSpace]);
3453 elementTransportConsistentn[i][j] += ck.SimpleDiffusionJacobian_weak(a_rowptr.data(), a_colind.data(), an, &u_grad_trial[j_nSpace], &u_grad_test_dV[i_nSpace]);
3454 }
3455 } //i
3456 //save solution for other models
3457 q_u.data()[eN_k] = u;
3458 q_m.data()[eN_k] = m;
3459 }
3461 // DISTRIBUTE // load cell based element into global residual
3463 for (int i = 0; i < nDOF_test_element; i++) {
3464 int eN_i = eN * nDOF_test_element + i;
3465 int gi = r_l2g.data()[eN_i];
3466 // distribute entropy_residual
3467 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) // EV Stab
3468 global_entropy_residual[gi] += element_entropy_residual[i];
3469 // distribute transport matrices
3470 for (int j = 0; j < nDOF_trial_element; j++) {
3471 int eN_i_j = eN_i * nDOF_trial_element + j;
3472 TransportMatrix[csrRowIndeces_CellLoops.data()[eN_i] + csrColumnOffsets_CellLoops.data()[eN_i_j]] += elementTransport[i][j];
3473 TransportMatrixConsistent[csrRowIndeces_CellLoops.data()[eN_i] + csrColumnOffsets_CellLoops.data()[eN_i_j]] += elementTransportConsistent[i][j];
3474 TransportMatrixn[csrRowIndeces_CellLoops.data()[eN_i] + csrColumnOffsets_CellLoops.data()[eN_i_j]] += elementTransportn[i][j];
3475 TransportMatrixConsistentn[csrRowIndeces_CellLoops.data()[eN_i] + csrColumnOffsets_CellLoops.data()[eN_i_j]] += elementTransportConsistentn[i][j];
3476 } //j
3477 } //i
3478
3479 } //elementsxw
3480
3481 // double s = 0.0, sabs = 0.0;
3482 // double vmin = 1e300, vmax = -1e300;
3483 // size_t n_nonzero = 0;
3484
3485 // for (size_t i = 0; i < velocity.size(); i++) {
3486 // double v = velocity.data()[i];
3487 // s += v;
3488 // sabs += std::abs(v);
3489 // vmin = std::min(vmin, v);
3490 // vmax = std::max(vmax, v);
3491 // if (std::abs(v) > 1e-14) n_nonzero++;
3492 // }
3493
3494 // std::cout << "[after compute] size=" << velocity.size()
3495 // << " nnz=" << n_nonzero
3496 // << " min=" << vmin << " max=" << vmax
3497 // << " sumabs=" << sabs << std::endl;
3498
3499 //loop over exterior element boundaries to calculate surface integrals and load into element and global residuals
3500 //
3501 //ebNE is the Exterior element boundary INdex
3502 //ebN is the element boundary INdex
3503 //eN is the element index
3504 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
3505 int ebN = exteriorElementBoundariesArray.data()[ebNE], eN = elementBoundaryElementsArray.data()[ebN * 2 + 0], ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0], eN_nDOF_trial_element = eN * nDOF_trial_element;
3506 double elementResidual_u[nDOF_test_element];
3507 for (int i = 0; i < nDOF_test_element; i++) { elementResidual_u[i] = 0.0; }
3508 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
3509 int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb, ebNE_kb_nSpace = ebNE_kb * nSpace, ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb, ebN_local_kb_nSpace = ebN_local_kb * nSpace;
3510 double u_ext = 0.0, un_ext, grad_u_ext[nSpace], m_ext = 0.0, dm_ext = 0.0, f_ext[nSpace], df_ext[nSpace], a_ext[nnz], da_ext[nnz], as_ext[nnz],
3511 mn_ext = 0.0, dmn_ext = 0.0, fn_ext[nSpace], dfn_ext[nSpace], an_ext[nnz], dan_ext[nnz], asn_ext[nnz], flux_ext = 0.0, bflux_ext = 0.0,
3512 //anb_seepage_flux=0.0, // for flux calculation
3513 bc_u_ext = 0.0, bc_grad_u_ext[nSpace], bc_m_ext = 0.0, bc_dm_ext = 0.0, bc_f_ext[nSpace], bc_df_ext[nSpace], bc_a_ext[nnz], bc_da_ext[nnz], bc_as_ext[nnz], jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace], boundaryJac[nSpace * (nSpace - 1)], metricTensor[(nSpace - 1) * (nSpace - 1)], metricTensorDetSqrt, dS, u_test_dS[nDOF_test_element], u_grad_trial_trace[nDOF_trial_element * nSpace], normal[3], x_ext, y_ext, z_ext, xt_ext, yt_ext, zt_ext, integralScaling, G[nSpace * nSpace], G_dd_G, tr_G, fluxJacobian_u_u[nDOF_trial_element], bfluxJacobian_u_u[nDOF_trial_element], fluxJacobian_un_un[nDOF_trial_element];
3514 //
3515 //calculate the solution and gradients at quadrature points
3516 //
3517 //compute information about mapping from reference element to physical element
3518 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(), jac_ext, jacDet_ext, jacInv_ext, boundaryJac, metricTensor, metricTensorDetSqrt,
3519 normal_ref.data(), normal, x_ext, y_ext, z_ext);
3520 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(), xt_ext, yt_ext, zt_ext, normal, boundaryJac, metricTensor, integralScaling);
3521 dS = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt + MOVING_DOMAIN * integralScaling) * dS_ref.data()[kb];
3522 //get the metric tensor
3523 //cek todo use symmetry
3524 ck.calculateG(jacInv_ext, G, G_dd_G, tr_G);
3525 //compute shape and solution information
3526 //shape
3527 ck.gradTrialFromRef(&u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element], jacInv_ext, u_grad_trial_trace);
3528 //solution and gradient
3529 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_ext);
3530 ck.valFromDOF(u_dof_old.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], un_ext);
3531 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial_trace, grad_u_ext);
3532
3533 //populate ebqe_x
3534 // const int ebNE_kb_3d = ebNE_kb * 3;
3535 // ebqe_x.data()[ebNE_kb_3d + 0] = x_ext;
3536 // ebqe_x.data()[ebNE_kb_3d + 1] = y_ext;
3537 // ebqe_x.data()[ebNE_kb_3d + 2] = z_ext;
3538
3539
3540 //precalculate test function products with integration weights
3541 for (int j = 0; j < nDOF_trial_element; j++) { u_test_dS[j] = u_test_trace_ref.data()[ebN_local_kb * nDOF_test_element + j] * dS; }
3542 //
3543 //load the boundary values
3544 //
3545 bc_u_ext = isDOFBoundary_u.data()[ebNE_kb] * ebqe_bc_u_ext.data()[ebNE_kb] + (1 - isDOFBoundary_u.data()[ebNE_kb]) * u_ext;
3546 //
3547 //calculate the pde coefficients using the solution and the boundary values for the solution
3548 //
3549 double bc_Kr, bc_dKr,bc_Kr_ext, bc_dKr_ext, bc_Krn, bc_dKrn, thetaW_ext, thetaWn_ext, thetaW_bc_ext;
3550 const double rho_ext = ebqe_rho.data()[ebNE_kb];
3551 const double rho_velocity_ext = std::fabs(rho_ext) > 1.0e-12 ? rho_ext : rho;
3552
3553 // EV exterior boundary closure.
3554 double dm_du_n_ext = 0.0, dkr_du_n_ext = 0.0;
3555 double dmn_du_n_ext = 0.0, dkrn_du_n_ext = 0.0;
3556 double bc_dm_du_n = 0.0, bc_dkr_du_n = 0.0;
3557 double df_du_n_ext[nSpace], dfn_du_n_ext[nSpace], bc_df_du_n[nSpace];
3558 double da_du_n_ext[nnz], dan_du_n_ext[nnz], bc_da_du_n[nnz];
3559 for (int I = 0; I < nSpace; I++) {
3560 df_du_n_ext[I] = 0.0; dfn_du_n_ext[I] = 0.0; bc_df_du_n[I] = 0.0;
3561 }
3562 for (int ii = 0; ii < nnz; ii++) {
3563 da_du_n_ext[ii] = 0.0; dan_du_n_ext[ii] = 0.0; bc_da_du_n[ii] = 0.0;
3564 }
3565 double u_n_ext_qp = 0.0, u_n_ext_qp_old = 0.0;
3566 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
3567 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_n_ext_qp);
3568 ck.valFromDOF(u_dof_n_old.data(), &u_l2g.data()[eN_nDOF_trial_element],
3569 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element], u_n_ext_qp_old);
3570 const double bc_u_n_ext_qp = isDOFBoundary_n.data()[ebNE_kb] * ebqe_bc_u_n_ext.data()[ebNE_kb]
3571 + (1 - isDOFBoundary_n.data()[ebNE_kb]) * u_n_ext_qp;
3572 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
3573 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
3574 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
3575 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], u_ext, u_n_ext_qp,
3576 m_ext, dm_ext, dm_du_n_ext, f_ext, df_ext, df_du_n_ext, a_ext, da_ext, da_du_n_ext,
3577 as_ext, bc_Kr, bc_dKr, dkr_du_n_ext, thetaW_ext);
3578 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
3579 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
3580 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
3581 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], un_ext, u_n_ext_qp_old,
3582 mn_ext, dmn_ext, dmn_du_n_ext, fn_ext, dfn_ext, dfn_du_n_ext, an_ext, dan_ext, dan_du_n_ext,
3583 asn_ext, bc_Krn, bc_dKrn, dkrn_du_n_ext, thetaWn_ext);
3584 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_ext, beta, gravity.data(),
3585 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
3586 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
3587 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], bc_u_ext, bc_u_n_ext_qp,
3588 bc_m_ext, bc_dm_ext, bc_dm_du_n, bc_f_ext, bc_df_ext, bc_df_du_n, bc_a_ext, bc_da_ext, bc_da_du_n,
3589 bc_as_ext, bc_Kr_ext, bc_dKr_ext, bc_dkr_du_n, thetaW_bc_ext);
3590 ebqe_theta.data()[ebNE_kb] = thetaW_ext;
3591
3592 //
3593 //Calculate Darcy Velocity at external faces
3594 //
3595
3596 // double darcy_velocity_loc_ext[nSpace];
3597 // for (int I = 0; I < nSpace; I++) { darcy_velocity_loc_ext[I] = 0.0; }
3598
3599 // for (int I = 0; I < nSpace; I++) {
3600 // for (int J = 0; J < nSpace; J++) { darcy_velocity_loc_ext[I] -= bc_Kr * KWs.data()[elementMaterialTypes[eN] * nSpace * nSpace + I * nSpace + J] * (grad_u_ext[J]+ gravity.data()[J]); }
3601 // }
3602 // for (int I = 0; I < nSpace; I++) { ebqe_velocity_ext_couple.data()[ebNE_kb_nSpace + I] = darcy_velocity_loc_ext[I] ; }
3603
3604 double ext_pressure_gradient[nSpace];
3605 for (int J = 0; J < nSpace; ++J)
3606 ext_pressure_gradient[J] = grad_u_ext[J] - rho_velocity_ext * gravity.data()[J];
3607
3608 for (int I = 0; I < nSpace; ++I) {
3609 double acc = 0.0;
3610 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I+1]; ++ii) {
3611 const int J = a_colind.data()[ii];
3612 acc += (a_ext[ii] / rho_velocity_ext) * ext_pressure_gradient[J];
3613 }
3614 ebqe_velocity_ext.data()[ebNE_kb_nSpace + I] = -acc;
3615 ebqe_velocity_ext_couple.data()[ebNE_kb_nSpace + I] = -acc;
3616 }
3617 bool useConsistentFlux=false;
3618 double grad_u_n_ext_b[nSpace];
3619 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
3620 u_grad_trial_trace, grad_u_n_ext_b);
3621 const int mat_b0 = elementMaterialTypes.data()[eN];
3622 const double alpha_b0 = alpha.data()[mat_b0];
3623 const double n_vg_b0 = n.data()[mat_b0];
3624 const double krn_end_b0 = krn_end.data()[mat_b0];
3625 const double *KWs_b0 = &KWs.data()[mat_b0 * nnz];
3626 const double phi_b0 = thetaR.data()[mat_b0] + thetaSR.data()[mat_b0];
3627 const double S_wr_b0 = thetaR.data()[mat_b0] / phi_b0;
3628 const double one_m_Sr_b0 = 1.0 - S_wr_b0;
3629 const double Se_trap_L3616 = 1.0 - S_gr.data()[mat_b0] / one_m_Sr_b0; // gas-only residual trapping
3630 const double z_clb0 = fmin(fmax(u_n_ext_qp, 1.0e-8), 1.0 - 1.0e-8);
3631 const double p_clb0 = fmax(u_ext, 1.0e2);
3634 const double Sab0 = 1.0 - fsb0.S_g;
3635 const double Se_rawb0 = (Sab0 - S_wr_b0)/one_m_Sr_b0;
3636 double Se_ab0, dSeb0_dp, dSeb0_dz;
3637 if (Se_rawb0<=0.0){Se_ab0=0.0;dSeb0_dp=0.0;dSeb0_dz=0.0;}
3638 else if (Se_rawb0>=1.0){Se_ab0=1.0;dSeb0_dp=0.0;dSeb0_dz=0.0;}
3639 else {Se_ab0=Se_rawb0;dSeb0_dp=-fsb0.dS_g_dp/one_m_Sr_b0;dSeb0_dz=-fsb0.dS_g_dz/one_m_Sr_b0;}
3640 double KWrb0=0,DKWrb0=0,thWb0=0,DthWb0=0,KNrb0=0,DKNrb0=0,pcb0=0,dpc_dSeb0=0,d2pcb0=0;
3641 if (PSK_TYPE_member == 1) {
3642 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_ab0, alpha_b0, n_vg_b0, thetaR.data()[mat_b0], thetaSR.data()[mat_b0], thWb0, DthWb0, KWrb0, DKWrb0);
3643 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_ab0, alpha_b0, n_vg_b0, KNrb0, DKNrb0, Se_trap_L3616);
3644 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_ab0, alpha_b0, n_vg_b0, pcb0, dpc_dSeb0, d2pcb0);
3645 } else {
3646 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_ab0, alpha_b0, n_vg_b0, thetaR.data()[mat_b0], thetaSR.data()[mat_b0], thWb0, DthWb0, KWrb0, DKWrb0);
3647 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_ab0, alpha_b0, n_vg_b0, KNrb0, DKNrb0, Se_trap_L3616);
3648 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_ab0, alpha_b0, n_vg_b0, pcb0, dpc_dSeb0, d2pcb0);
3649 }
3650 KNrb0 *= krn_end_b0; DKNrb0 *= krn_end_b0;
3651 const double pcpb0 = dpc_dSeb0/one_m_Sr_b0;
3652 const double dpcpb0_dp = (d2pcb0/one_m_Sr_b0)*dSeb0_dp;
3653 const double dpcpb0_dz = (d2pcb0/one_m_Sr_b0)*dSeb0_dz;
3655 const double Mbar_gb0 = fsb0.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb0.Y)*::m_comp_co2::eos::M_H2O_KG;
3656 const double Mbar_ab0 = fsb0.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb0.X)*::m_comp_co2::eos::M_H2O_KG;
3657 const double rgmb0 = fsb0.rho_g*Mbar_gb0, ramb0 = fsb0.rho_a*Mbar_ab0;
3658 const double drgmb0_dp = fsb0.drho_g_dp*Mbar_gb0 + fsb0.rho_g*fsb0.dY_dp*dMmb0;
3659 const double drgmb0_dz = fsb0.rho_g*fsb0.dY_dz*dMmb0;
3660 const double dramb0_dp = fsb0.drho_a_dp*Mbar_ab0 + fsb0.rho_a*fsb0.dX_dp*dMmb0;
3661 const double dramb0_dz = fsb0.drho_a_dz*Mbar_ab0 + fsb0.rho_a*fsb0.dX_dz*dMmb0;
3662 const double Agb0 = fsb0.rho_g*(1.0-fsb0.Y), Aab0 = fsb0.rho_a*(1.0-fsb0.X);
3663 const double dAgb0_dp = fsb0.drho_g_dp*(1.0-fsb0.Y) - fsb0.rho_g*fsb0.dY_dp;
3664 const double dAgb0_dz = - fsb0.rho_g*fsb0.dY_dz;
3665 const double dAab0_dp = fsb0.drho_a_dp*(1.0-fsb0.X) - fsb0.rho_a*fsb0.dX_dp;
3666 const double dAab0_dz = fsb0.drho_a_dz*(1.0-fsb0.X) - fsb0.rho_a*fsb0.dX_dz;
3667 double F0n_b = 0.0;
3668 double valp_b[nSpace], valz_b[nSpace];
3669 for (int I=0;I<nSpace;I++){
3670 double ugI=0,uaI=0,dugp=0,dugz=0,duap=0,duaz=0;
3671 for (int ii=a_rowptr.data()[I];ii<a_rowptr.data()[I+1];ii++){
3672 const int J=a_colind.data()[ii];
3673 const double Kii=KWs_b0[ii];
3674 const double Mob_g=KNrb0*Kii/mu_n, Mob_a=KWrb0*Kii;
3675 const double dMobg_dp=(DKNrb0*Kii/mu_n)*dSeb0_dp, dMobg_dz=(DKNrb0*Kii/mu_n)*dSeb0_dz;
3676 const double dMoba_dp=(DKWrb0*Kii)*dSeb0_dp, dMoba_dz=(DKWrb0*Kii)*dSeb0_dz;
3677 const double gJ=gravity.data()[J];
3678 const double gradSa=-(fsb0.dS_g_dp*grad_u_ext[J]+fsb0.dS_g_dz*grad_u_n_ext_b[J]);
3679 const double gp_a=grad_u_ext[J]-ramb0*gJ;
3680 const double gp_g=grad_u_ext[J]+pcpb0*gradSa-rgmb0*gJ;
3681 ugI-=Mob_g*gp_g; uaI-=Mob_a*gp_a;
3682 const double dgradSa_dp=-(fsb0.d2S_g_dp2*grad_u_ext[J]+fsb0.d2S_g_dpdz*grad_u_n_ext_b[J]);
3683 const double dgradSa_dz=-(fsb0.d2S_g_dpdz*grad_u_ext[J]+fsb0.d2S_g_dz2*grad_u_n_ext_b[J]);
3684 const double dgpg_dp=dpcpb0_dp*gradSa+pcpb0*dgradSa_dp-drgmb0_dp*gJ;
3685 const double dgpg_dz=dpcpb0_dz*gradSa+pcpb0*dgradSa_dz-drgmb0_dz*gJ;
3686 dugp-=dMobg_dp*gp_g+Mob_g*dgpg_dp;
3687 dugz-=dMobg_dz*gp_g+Mob_g*dgpg_dz;
3688 duap-=dMoba_dp*gp_a+Mob_a*(-dramb0_dp*gJ);
3689 duaz-=dMoba_dz*gp_a+Mob_a*(-dramb0_dz*gJ);
3690 }
3691 F0n_b += (Agb0*ugI + Aab0*uaI) * normal[I];
3692 valp_b[I] = (dAgb0_dp*ugI + Agb0*dugp + dAab0_dp*uaI + Aab0*duap);
3693 valz_b[I] = (dAgb0_dz*ugI + Agb0*dugz + dAab0_dz*uaI + Aab0*duaz);
3694 }
3695 const double penb0 = ebqe_penalty_ext.data()[ebNE_kb];
3696 (void)valz_b;
3697 {
3698 const int isSeep = isSeepageFace.data()[ebNE];
3699 if (isSeep || isDOFBoundary_u.data()[ebNE_kb]) {
3700 const double bc_u_pen = isSeep ? 0.0 : bc_u_ext;
3701 const double pen_term = penb0*(u_ext - bc_u_pen);
3702 flux_ext = F0n_b + pen_term; // consistent -> TransportMatrix*
3703 bflux_ext = pen_term; // penalty -> globalResidual
3704 if (isSeep && flux_ext <= 0.0) { flux_ext = 0.0; bflux_ext = 0.0; }
3705 } else {
3706 flux_ext = ebqe_bc_flux_ext[ebNE_kb];
3707 bflux_ext = ebqe_bc_flux_ext[ebNE_kb];
3708 }
3709 }
3710
3711 ebqe_flux.data()[ebNE_kb] = flux_ext;
3712
3713 anb_seepage_flux = seepagefluxcalculator(anb_seepage_flux, isSeepageFace.data()[ebNE], dS, flux_ext);
3714 anb_seepage_flux_n.data()[0] = anb_seepage_flux;
3715 ebqe_u.data()[ebNE_kb] = u_ext;
3716 //
3717 //update residuals
3718 //
3719 for (int i = 0; i < nDOF_test_element; i++) {
3720 if (useConsistentFlux) {
3721 elementResidual_u[i] += ck.ExteriorElementBoundaryFlux(flux_ext, u_test_dS[i]);
3722 } else {
3723 elementResidual_u[i] += ck.ExteriorElementBoundaryFlux(bflux_ext, u_test_dS[i]);
3724 }
3725 } //i
3726 for (int j = 0; j < nDOF_trial_element; j++) {
3727 if (useConsistentFlux) {
3728 exteriorNumericalFluxJacobian(a_rowptr.data(), a_colind.data(), isDOFBoundary_u.data()[ebNE_kb], normal, a_ext, da_ext, grad_u_ext, &u_grad_trial_trace[j * nSpace], df_ext, u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element + j],
3729 ebqe_penalty_ext.data()[ebNE_kb], //penalty,
3730 fluxJacobian_u_u[j]);
3731 } else {
3732 // P3c boundary (STAB=2): compositional comp-0 flux Jacobian.
3733 // Consistent d(F_0.n)/d(p_j) -> fluxJacobian_u_u (TransportMatrix*);
3734 // penalty -> bfluxJacobian_u_u (globalJacobian). value/grad blocks
3735 // reuse valp_b + the stored flash/psk state (FD-verified, b0test).
3736 // (0,1) cross-block not assembled here (matches the original).
3737 if (isDOFBoundary_u.data()[ebNE_kb]) {
3738 const double trial_j = u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element + j];
3739 double accp = 0.0;
3740 for (int I=0;I<nSpace;I++){
3741 accp += normal[I]*valp_b[I]*trial_j;
3742 for (int ii=a_rowptr.data()[I];ii<a_rowptr.data()[I+1];ii++){
3743 const int J=a_colind.data()[ii];
3744 const double Kii=KWs_b0[ii];
3745 const double Mob_g=KNrb0*Kii/mu_n, Mob_a=KWrb0*Kii;
3746 const double dFdgp = Agb0*(-Mob_g*(1.0 - pcpb0*fsb0.dS_g_dp)) + Aab0*(-Mob_a);
3747 accp += normal[I]*dFdgp*u_grad_trial_trace[j*nSpace + J];
3748 }
3749 }
3750 fluxJacobian_u_u[j] = accp; // consistent (no penalty)
3751 bfluxJacobian_u_u[j] = penb0*trial_j; // penalty
3752 } else {
3753 fluxJacobian_u_u[j] = 0.0;
3754 bfluxJacobian_u_u[j] = 0.0;
3755 }
3756 }
3757 exteriorNumericalFluxJacobian(a_rowptr.data(), a_colind.data(),
3758 isDOFBoundary_u.data()[ebNE_kb], normal,
3759 asn_ext, dan_ext, grad_u_ext,
3760 &u_grad_trial_trace[j * nSpace], dfn_ext,
3761 u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element + j],
3762 ebqe_penalty_ext.data()[ebNE_kb],
3763 fluxJacobian_un_un[j]);
3764 } //j
3765 //
3766 //update the element and global residual storage
3767 //
3768 for (int i = 0; i < nDOF_test_element; i++) {
3769 int eN_i = eN * nDOF_test_element + i;
3770 for (int j = 0; j < nDOF_trial_element; j++) {
3771 int ebN_i_j = ebN * 4 * nDOF_test_X_trial_element + i * nDOF_trial_element + j;
3772 if (useConsistentFlux) {
3773 globalJacobian.data()[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_u_u[j] * u_test_dS[i];
3774 } else {
3775 globalJacobian.data()[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += bfluxJacobian_u_u[j] * u_test_dS[i];
3776 TransportMatrix[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_u_u[j] * u_test_dS[i];
3777 TransportMatrixConsistent[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_u_u[j] * u_test_dS[i];
3778 TransportMatrixn[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_un_un[j] * u_test_dS[i];
3779 TransportMatrixConsistentn[csrRowIndeces_u_u[eN_i] + csrColumnOffsets_eb_u_u[ebN_i_j]] += fluxJacobian_un_un[j] * u_test_dS[i];
3780 }
3781 } //j
3782 } //i
3783 } //kb
3784 for (int i = 0; i < nDOF_test_element; i++) {
3785 int eN_i = eN * nDOF_test_element + i;
3786 globalResidual.data()[offset_u + stride_u * u_l2g.data()[eN_i]] += elementResidual_u[i];
3787 }//i
3788 } //ebNE
3790 // COMPUTE SMOOTHNESS INDICATOR and NORMALIZE ENTROPY RESIDUAL //
3792 // NOTE: see NCLS.h for a different but equivalent implementation of this.
3793 std::vector<double> cflux(numDOFs, 0.0);
3794 for (int i = 0; i < numDOFs_u; i++) {
3795 double gi[nSpace], Cij[nSpace], xi[nSpace], etaMaxi, etaMini;
3796 const int node_i = freeDOFToNode_u.data()[i];
3797 double solni = u_free_dof_old[i];
3798 for (int I = 0; I < nSpace; I++) {
3799 solni -= rho_dof[i] * gravity.data()[I] * mesh_dof.data()[node_i * 3 + I];
3800 }
3801 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) //EV Stabilization
3802 {
3803 // For eta min and max
3804 etaMaxi = fabs(eta[i]);
3805 etaMini = fabs(eta[i]);
3806 }
3807 // initialize gi and compute xi
3808 for (int I = 0; I < nSpace; I++) {
3809 gi[I] = 0.;
3810 xi[I] = mesh_dof.data()[node_i * 3 + I];
3811 }
3812 // for smoothness indicator //
3813 double alpha_numerator_pos = 0., alpha_numerator_neg = 0., alpha_denominator_pos = 0., alpha_denominator_neg = 0.;
3814 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) { // First loop in j (sparsity pattern)
3815 int j = csrColumnOffsets_DofLoops.data()[offset];
3816 const int full_offset = full_offset_from_compact(i, j);
3817 assert(full_offset >= 0);
3818 const int node_j = freeDOFToNode_u.data()[j];
3819 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) //EV Stabilization
3820 {
3821 // COMPUTE ETA MIN AND ETA MAX //
3822 etaMaxi = fmax(etaMaxi, fabs(eta[j]));
3823 etaMini = fmin(etaMini, fabs(eta[j]));
3824 }
3825 double solnj = u_free_dof_old[j];
3826 for (int I = 0; I < nSpace; I++) {
3827 solnj -= rho_dof[j] * gravity.data()[I] * mesh_dof.data()[node_j * 3 + I];
3828 }
3829 // Update Cij matrices
3830 Cij[0] = Cx[full_offset];
3831#if nSpace == 2
3832 Cij[1] = Cy[full_offset];
3833#endif
3834#if nSpace == 3
3835 Cij[2] = Cz[full_offset];
3836#endif
3837 // COMPUTE gi VECTOR. gi=1/mi*sum_j(Cij*solj)
3838 for (int I = 0; I < nSpace; I++) gi[I] += Cij[I] * solnj;
3839
3840 // COMPUTE numerator and denominator of smoothness indicator
3841 double alpha_num = solni - solnj;
3842 if (alpha_num >= 0.) {
3843 alpha_numerator_pos += alpha_num;
3844 alpha_denominator_pos += alpha_num;
3845 } else {
3846 alpha_numerator_neg += alpha_num;
3847 alpha_denominator_neg += fabs(alpha_num);
3848 }
3849 }
3850 // scale g vector by lumped mass matrix
3851 //double mass_matrix_error = abs(ML.data()[i] - ML2[i]);
3852 //if (mass_matrix_error > 1.0e-16) std::cout << mass_matrix_error<<" ML " << ML.data()[i] << '\t' << ML2[i] << std::endl;
3853 for (int I = 0; I < nSpace; I++) gi[I] /= ML.data()[i];
3854 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) //EV Stab
3855 {
3856 // Normalizae entropy residual
3857 global_entropy_residual[i] *= etaMini == etaMaxi ? 0. : 2 * cE / (etaMaxi - etaMini);
3858 quantDOFs.data()[i] = fabs(global_entropy_residual[i]);
3859 }
3860
3861 // Now that I have the gi vectors, I can use them for the current i-th DOF
3862 double SumPos = 0., SumNeg = 0.;
3863 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) { // second loop in j (sparsity pattern)
3864 int j = csrColumnOffsets_DofLoops.data()[offset];
3865 const int full_offset = full_offset_from_compact(i, j);
3866 assert(full_offset >= 0);
3867 // compute gi*(xi-xj)
3868 double gi_times_x = 0.;
3869 for (int I = 0; I < nSpace; I++) {
3870 gi_times_x += gi[I] * delta_x_ij.data()[full_offset * 3 + I];
3871 }
3872 // compute the positive and negative part of gi*(xi-xj)
3873 SumPos += gi_times_x > 0 ? gi_times_x : 0;
3874 SumNeg += gi_times_x < 0 ? gi_times_x : 0;
3875 }
3876 double sigmaPosi = fmin(1., (fabs(SumNeg) + 1E-15) / (SumPos + 1E-15));
3877 double sigmaNegi = fmin(1., (SumPos + 1E-15) / (fabs(SumNeg) + 1E-15));
3878 double alpha_numi = fabs(sigmaPosi * alpha_numerator_pos + sigmaNegi * alpha_numerator_neg);
3879 double alpha_deni = sigmaPosi * alpha_denominator_pos + sigmaNegi * alpha_denominator_neg;
3880 if (IS_BETAij_ONE == 1) {
3881 alpha_numi = fabs(alpha_numerator_pos + alpha_numerator_neg);
3882 alpha_deni = alpha_denominator_pos + alpha_denominator_neg;
3883 }
3884 double alphai = alpha_numi / (alpha_deni + 1E-15);
3885 quantDOFs.data()[i] = alphai;
3886
3887 if (POWER_SMOOTHNESS_INDICATOR == 0) psi[i] = 1.0;
3888 else psi[i] = std::pow(alphai, POWER_SMOOTHNESS_INDICATOR); //NOTE: they use alpha^2 in the paper
3889 }
3890 std::vector<double> w_lam_g(numDOFs_u, 0.0), w_lam_a(numDOFs_u, 0.0);
3891 std::vector<double> w_dlam_g_dp(numDOFs_u, 0.0), w_dlam_g_dz(numDOFs_u, 0.0);
3892 std::vector<double> w_dlam_a_dp(numDOFs_u, 0.0), w_dlam_a_dz(numDOFs_u, 0.0);
3893 std::vector<double> w_pc(numDOFs_u, 0.0), w_dpc_dp(numDOFs_u, 0.0), w_dpc_dz(numDOFs_u, 0.0);
3894 std::vector<double> w_rgm(numDOFs_u, 0.0), w_ram(numDOFs_u, 0.0);
3895 std::vector<double> w_drgm_dp(numDOFs_u, 0.0), w_drgm_dz(numDOFs_u, 0.0);
3896 std::vector<double> w_dram_dp(numDOFs_u, 0.0), w_dram_dz(numDOFs_u, 0.0);
3897 std::vector<double> w_lam_g_old(numDOFs_u, 0.0), w_lam_a_old(numDOFs_u, 0.0);
3898 std::vector<double> w_pc_old(numDOFs_u, 0.0);
3899 std::vector<double> w_rgm_old(numDOFs_u, 0.0), w_ram_old(numDOFs_u, 0.0);
3900 {
3902 for (int i = 0; i < numDOFs_u; i++) {
3903 const int node_i = freeDOFToNode_u.data()[i];
3904 // comp-1 (z) split DOF for this node -- u_dof_n is in the split numbering,
3905 // so reading z must go through node2zdof (identity when split_z == 0).
3906 const int z_i = (split_z != 0) ? node2zdof.data()[node_i] : node_i;
3907 const int mat_i = freeDOFMaterialTypes.data()[i];
3908 const double alpha_i = alpha.data()[mat_i];
3909 const double n_vg_i = n.data()[mat_i];
3910 const double krn_end_i= krn_end.data()[mat_i];
3911 const double phi_i = thetaR.data()[mat_i] + thetaSR.data()[mat_i];
3912 const double S_wr_i = thetaR.data()[mat_i] / phi_i;
3913 const double one_m_Sr_i = 1.0 - S_wr_i;
3914 const double Se_trap_L3923 = 1.0 - S_gr.data()[mat_i] / one_m_Sr_i; // gas-only residual trapping
3915 const double cg_i = krn_end_i / mu_n;
3916 // --- current iterate ---
3917 const double z_cl = fmin(fmax(u_dof_n.data()[z_i], 1.0e-8), 1.0 - 1.0e-8);
3918 const double p_cl = fmax(u_free_dof[i], 1.0e2);
3921 const double Sa = 1.0 - f.S_g;
3922 const double Se_raw = (Sa - S_wr_i) / one_m_Sr_i;
3923 double Se, dSe_dp, dSe_dz;
3924 if (Se_raw <= 0.0) { Se = 0.0; dSe_dp = 0.0; dSe_dz = 0.0; }
3925 else if (Se_raw >= 1.0) { Se = 1.0; dSe_dp = 0.0; dSe_dz = 0.0; }
3926 else { Se = Se_raw; dSe_dp = -f.dS_g_dp/one_m_Sr_i; dSe_dz = -f.dS_g_dz/one_m_Sr_i; }
3927 double krn=0,dkrn=0,krw=0,dkrw=0,thW=0,DthW=0,pc=0,dpc_dSe=0,d2pc=0;
3928 if (PSK_TYPE_member == 1) {
3929 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se, alpha_i, n_vg_i, krn, dkrn, Se_trap_L3923);
3930 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se, alpha_i, n_vg_i, thetaR.data()[mat_i], thetaSR.data()[mat_i], thW, DthW, krw, dkrw);
3931 proteus::m_comp_co2::psk::bc_pc_from_Se(Se, alpha_i, n_vg_i, pc, dpc_dSe, d2pc);
3932 } else {
3933 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se, alpha_i, n_vg_i, krn, dkrn, Se_trap_L3923);
3934 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se, alpha_i, n_vg_i, thetaR.data()[mat_i], thetaSR.data()[mat_i], thW, DthW, krw, dkrw);
3935 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se, alpha_i, n_vg_i, pc, dpc_dSe, d2pc);
3936 }
3937 w_pc[i] = pc; w_dpc_dp[i] = dpc_dSe*dSe_dp; w_dpc_dz[i] = dpc_dSe*dSe_dz;
3938 const double Mg = f.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-f.Y)*::m_comp_co2::eos::M_H2O_KG;
3939 const double Ma = f.X*::m_comp_co2::eos::M_CO2_KG + (1.0-f.X)*::m_comp_co2::eos::M_H2O_KG;
3940 w_rgm[i] = f.rho_g*Mg; w_ram[i] = f.rho_a*Ma;
3941 w_drgm_dp[i] = f.drho_g_dp*Mg + f.rho_g*f.dY_dp*dMm_w; w_drgm_dz[i] = f.rho_g*f.dY_dz*dMm_w;
3942 w_dram_dp[i] = f.drho_a_dp*Ma + f.rho_a*f.dX_dp*dMm_w; w_dram_dz[i] = f.drho_a_dz*Ma + f.rho_a*f.dX_dz*dMm_w;
3943 // H2O molar mobilities: gas weight (1-Y), aqueous weight (1-X).
3944 const double yw = 1.0 - f.Y, dyw_dp = -f.dY_dp, dyw_dz = -f.dY_dz;
3945 const double xw = 1.0 - f.X, dxw_dp = -f.dX_dp, dxw_dz = -f.dX_dz;
3946 w_lam_g[i] = cg_i*f.rho_g*yw*krn;
3947 w_dlam_g_dp[i] = cg_i*(f.drho_g_dp*yw*krn + f.rho_g*dyw_dp*krn + f.rho_g*yw*dkrn*dSe_dp);
3948 w_dlam_g_dz[i] = cg_i*( f.rho_g*dyw_dz*krn + f.rho_g*yw*dkrn*dSe_dz);
3949 w_lam_a[i] = f.rho_a*xw*krw;
3950 w_dlam_a_dp[i] = f.drho_a_dp*xw*krw + f.rho_a*dxw_dp*krw + f.rho_a*xw*dkrw*dSe_dp;
3951 w_dlam_a_dz[i] = f.drho_a_dz*xw*krw + f.rho_a*dxw_dz*krw + f.rho_a*xw*dkrw*dSe_dz;
3952 // --- old time level (frozen; feeds the (1-Theta) part, no derivatives) ---
3953 const double z_cl_o = fmin(fmax(u_dof_n_old.data()[z_i], 1.0e-8), 1.0 - 1.0e-8);
3954 const double p_cl_o = fmax(u_free_dof_old[i], 1.0e2);
3957 const double Sa_o = 1.0 - fo.S_g;
3958 const double Se_o_raw = (Sa_o - S_wr_i) / one_m_Sr_i;
3959 const double Se_o = Se_o_raw <= 0.0 ? 0.0 : (Se_o_raw >= 1.0 ? 1.0 : Se_o_raw);
3960 double krn_o=0,dkrn_o=0,krw_o=0,dkrw_o=0,thW_o=0,DthW_o=0,pc_o=0,dpc_o=0,d2pc_o=0;
3961 if (PSK_TYPE_member == 1) {
3962 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_o, alpha_i, n_vg_i, krn_o, dkrn_o, Se_trap_L3923);
3963 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_o, alpha_i, n_vg_i, thetaR.data()[mat_i], thetaSR.data()[mat_i], thW_o, DthW_o, krw_o, dkrw_o);
3964 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_o, alpha_i, n_vg_i, pc_o, dpc_o, d2pc_o);
3965 } else {
3966 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_o, alpha_i, n_vg_i, krn_o, dkrn_o, Se_trap_L3923);
3967 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_o, alpha_i, n_vg_i, thetaR.data()[mat_i], thetaSR.data()[mat_i], thW_o, DthW_o, krw_o, dkrw_o);
3968 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_o, alpha_i, n_vg_i, pc_o, dpc_o, d2pc_o);
3969 }
3970 w_pc_old[i] = pc_o;
3971 const double Mg_o = fo.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fo.Y)*::m_comp_co2::eos::M_H2O_KG;
3972 const double Ma_o = fo.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fo.X)*::m_comp_co2::eos::M_H2O_KG;
3973 w_rgm_old[i] = fo.rho_g*Mg_o; w_ram_old[i] = fo.rho_a*Ma_o;
3974 w_lam_g_old[i] = cg_i*fo.rho_g*(1.0-fo.Y)*krn_o;
3975 w_lam_a_old[i] = fo.rho_a*(1.0-fo.X)*krw_o;
3976 }
3977 }
3979 // ** LOOP IN DOFs FOR EDGE BASED TERMS ** //
3981 // only iterate component-0 DOFs here. Component 1
3982 // (trivial gas eq) is assembled by the dedicated element loop appended
3983 // at the end of this function.
3984 for (int i = 0; i < numDOFs_u; i++) {
3985 int ii = -1;
3986 double sum_abs_dt_times_fH_minus_fL = 0.0, MLi = ML.data()[i];
3987 double Kr, dKr, Krn, dKrn;
3988 double J_ii = 0.0;
3989 double ith_dissipative_term = 0;
3990 double ith_low_order_dissipative_term = 0;
3991 double ith_flux_term = 0;
3992 double ith_consistent_flux_term = 0;
3993 double dLii = 0.;
3994 double m, dm, f[nSpace], df[nSpace], a[nnz], da[nnz], as[nnz];
3995 double dmn, fn[nSpace], dfn[nSpace], an[nnz], dan[nnz], asn[nnz];
3996 // Cross-derivatives used by the (0,1) coupling wired into the low-order
3997 // wetting operator below: dm/du_n is added to the lumped-mass coupling
3998 // (row_w, col_n_i), and dkr/du_n[upwind] is added to the Kuzmin upwind
3999 // term at (row_w, col_n_upwind). The 'n_old' variants describe the
4000 // frozen time history -- they don't contribute to the Jacobian.
4001 double dm_du_n_fct, dkr_du_n_fct, df_du_n_fct[nSpace], da_du_n_fct[nnz];
4002 double dmn_du_n_fct, dkrn_du_n_fct, dfn_du_n_fct[nSpace], dan_du_n_fct[nnz];
4003
4004 const double rho_i = rho_dof[i];
4005 const int node_i = freeDOFToNode_u.data()[i];
4006 // comp-1 (z) split DOF for this node (see node2zdof note above): u_dof_n reads
4007 // and the (0,1) dR_w/dz column both index the split numbering, not the mesh node.
4008 const int z_i = (split_z != 0) ? node2zdof.data()[node_i] : node_i;
4009
4010 double thetaW_tmp = 0.0;
4011 // loop over the sparsity pattern of the i-th DOF
4012 for (int offset = csrRowIndeces_DofLoops.data()[i]; offset < csrRowIndeces_DofLoops.data()[i + 1]; offset++) {
4013 int j = csrColumnOffsets_DofLoops.data()[offset];
4014 const int full_offset = full_offset_from_compact(i, j);
4015 assert(full_offset >= 0);
4016 if (i == j) ii = full_offset;
4017 const double rho_j = rho_dof[j];
4018 const int node_j = freeDOFToNode_u.data()[j];
4019 const double rho_edge = 0.5 * (rho_i + rho_j);
4020 // ===== P3c: compositional H2O edge flux F_0 = F_g^w + F_a^w =====
4021 // Replaces the single-phase Kuzmin wetting flux fL = krw*tau*delta_phi.
4022 // F_g^w = tau * lam_g^w_up * dPhi_g, lam_g^w = (krn_end/mu_n)*rho_g*(1-Y)*krn
4023 // F_a^w = tau * lam_a^w_up * dPhi_a, lam_a^w = rho_a*(1-X)*krw
4024 // dPhi_g = d(p+pc) - rho_g_mass_edge*g.dx, dPhi_a = dp - rho_a_mass_edge*g.dx
4025 // The two-phase potentials, mobilities and densities come from the
4026 // per-free-DOF flash cache (w_*) built above; node closures use the
4027 // single-nodal rock freeDOFMaterialTypes[i] (comp-0 skeleton kept).
4028 // BARE transmissibility tau = -TransportMatrix[ij]/rho_edge strips the
4029 // rho_w baked into as=rhom*KWs (TransportMatrix = int rhom*KWs gg) so
4030 // density/mobility live in lam^w/dPhi (the memory's tau/rho_edge option).
4031 // No high-order term, no EV graph dissipation, no capillary entry-pressure
4032 // gate (comp-0 gas branch carries only water vapor 1-Y ~ tiny; the DOF-graph
4033 // loop has no per-element context for the two-sided barrier). comp-0 FCT
4034 // is a pass-through: dt_times_fH_minus_fL is zeroed and never consumed
4035 // (postStep runs only FCTStep(component=1)).
4036 dt_times_fH_minus_fL.data()[full_offset] = 0.0;
4037 if (i == j) continue; // ii captured above; no self-flux
4038 const double tau = fmax(0.0, -TransportMatrix[full_offset]) / rho_edge;
4039 if (tau == 0.0) continue;
4040 double g_dot_dx = 0.0;
4041 for (int I = 0; I < nSpace; I++)
4042 g_dot_dx += gravity.data()[I]
4043 * (mesh_dof.data()[node_j * 3 + I] - mesh_dof.data()[node_i * 3 + I]);
4044 // ---- gas branch (water vapor, ungated) ----
4045 const double rgm_edge = 0.5 * (w_rgm[i] + w_rgm[j]);
4046 const double rgm_edge_old = 0.5 * (w_rgm_old[i] + w_rgm_old[j]);
4047 const double dPhi_g = (u_free_dof[j] + w_pc[j]) - (u_free_dof[i] + w_pc[i])
4048 - rgm_edge * g_dot_dx;
4049 const double dPhi_g_old = (u_free_dof_old[j] + w_pc_old[j]) - (u_free_dof_old[i] + w_pc_old[i])
4050 - rgm_edge_old * g_dot_dx;
4051 const bool up_i_g = (dPhi_g <= 0.0);
4052 const bool up_i_g_old = (dPhi_g_old <= 0.0);
4053 const double lam_g_up = up_i_g ? w_lam_g[i] : w_lam_g[j];
4054 const double lam_g_up_old = up_i_g_old ? w_lam_g_old[i] : w_lam_g_old[j];
4055 const double Fg = Theta * tau * lam_g_up * dPhi_g
4056 + (1.0 - Theta) * tau * lam_g_up_old * dPhi_g_old;
4057 // ---- aqueous branch (H2O in brine, no gate) ----
4058 const double ram_edge = 0.5 * (w_ram[i] + w_ram[j]);
4059 const double ram_edge_old = 0.5 * (w_ram_old[i] + w_ram_old[j]);
4060 const double dPhi_a = u_free_dof[j] - u_free_dof[i] - ram_edge * g_dot_dx;
4061 const double dPhi_a_old = u_free_dof_old[j] - u_free_dof_old[i] - ram_edge_old * g_dot_dx;
4062 const bool up_i_a = (dPhi_a <= 0.0);
4063 const bool up_i_a_old = (dPhi_a_old <= 0.0);
4064 const double lam_a_up = up_i_a ? w_lam_a[i] : w_lam_a[j];
4065 const double lam_a_up_old = up_i_a_old ? w_lam_a_old[i] : w_lam_a_old[j];
4066 const double Fa = Theta * tau * lam_a_up * dPhi_a
4067 + (1.0 - Theta) * tau * lam_a_up_old * dPhi_a_old;
4068 // R_w[i] -= F_0 (the residual subtracts ith_flux_term below). Same +sign
4069 // convention as the old fL (~ +delta_phi = p_j - p_i).
4070 // P1: the single-nodal-rock comp-0 water flux is RETIRED here -- it is now
4071 // assembled TWO-SIDED per element-side (elementMaterialTypes[eN]) in the
4072 // comp-1 element loop below (elementResidual_w + full-CSR scatter). Keeping
4073 // ith_flux_term = 0 makes this loop comp-0 ACCUMULATION-ONLY:
4074 // R_w[i] = MLi*(m-mn)/dt. (The w_* per-DOF cache above is now unused by the
4075 // residual; left in place for this correctness pass, TODO: drop for speed.)
4076 (void)Fg; (void)Fa;
4077 // ===== Theta-part Jacobian wrt (p_i,z_i,p_j,z_j). Mirrors comp-1 (eftest). =====
4078 const double ddPhig_dpi = -1.0 - w_dpc_dp[i] - 0.5 * w_drgm_dp[i] * g_dot_dx;
4079 const double ddPhig_dzi = - w_dpc_dz[i] - 0.5 * w_drgm_dz[i] * g_dot_dx;
4080 const double ddPhig_dpj = +1.0 + w_dpc_dp[j] - 0.5 * w_drgm_dp[j] * g_dot_dx;
4081 const double ddPhig_dzj = + w_dpc_dz[j] - 0.5 * w_drgm_dz[j] * g_dot_dx;
4082 const double ddPhia_dpi = -1.0 - 0.5 * w_dram_dp[i] * g_dot_dx;
4083 const double ddPhia_dzi = - 0.5 * w_dram_dz[i] * g_dot_dx;
4084 const double ddPhia_dpj = +1.0 - 0.5 * w_dram_dp[j] * g_dot_dx;
4085 const double ddPhia_dzj = - 0.5 * w_dram_dz[j] * g_dot_dx;
4086 const double Tt = Theta * tau;
4087 double dF_dpi = 0.0, dF_dzi = 0.0, dF_dpj = 0.0, dF_dzj = 0.0;
4088 // gas potential part (all four DOFs)
4089 dF_dpi += Tt*lam_g_up*ddPhig_dpi; dF_dzi += Tt*lam_g_up*ddPhig_dzi;
4090 dF_dpj += Tt*lam_g_up*ddPhig_dpj; dF_dzj += Tt*lam_g_up*ddPhig_dzj;
4091 // gas mobility part (upstream node only)
4092 if (up_i_g) { dF_dpi += Tt*w_dlam_g_dp[i]*dPhi_g; dF_dzi += Tt*w_dlam_g_dz[i]*dPhi_g; }
4093 else { dF_dpj += Tt*w_dlam_g_dp[j]*dPhi_g; dF_dzj += Tt*w_dlam_g_dz[j]*dPhi_g; }
4094 // aqueous potential part (all four DOFs)
4095 dF_dpi += Tt*lam_a_up*ddPhia_dpi; dF_dzi += Tt*lam_a_up*ddPhia_dzi;
4096 dF_dpj += Tt*lam_a_up*ddPhia_dpj; dF_dzj += Tt*lam_a_up*ddPhia_dzj;
4097 // aqueous mobility part (upstream node only)
4098 if (up_i_a) { dF_dpi += Tt*w_dlam_a_dp[i]*dPhi_a; dF_dzi += Tt*w_dlam_a_dz[i]*dPhi_a; }
4099 else { dF_dpj += Tt*w_dlam_a_dp[j]*dPhi_a; dF_dzj += Tt*w_dlam_a_dz[j]*dPhi_a; }
4100 // P1: the comp-0 flux Jacobian is RETIRED here too -- its (0,0)/(0,1)
4101 // blocks are assembled two-sided in the element loop below. J_ii stays 0,
4102 // so globalJacobian[ii] below carries only the mass diagonal MLi*dm/dt.
4103 (void)dF_dpi; (void)dF_dpj; (void)dF_dzi; (void)dF_dzj;
4104 }
4105 mDotLow.data()[i] = ith_flux_term/MLi;
4106 cflux[i] = ith_consistent_flux_term;
4107 // Final per-DOF coefficient evaluations at (u_w, u_n) and (u_w_old, u_n_old):
4108 // m -> mLow.data()[i] (current low-order mass)
4109 // mn -> mn.data()[i] (time-history mass for the dt difference)
4110 // dm_du_n_fct -> consumed by the (0,1) lumped-mass coupling below.
4111 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_i, beta, gravity.data(),
4112 alpha.data()[freeDOFMaterialTypes.data()[i]],
4113 n.data()[freeDOFMaterialTypes.data()[i]], thetaR.data()[freeDOFMaterialTypes.data()[i]], thetaSR.data()[freeDOFMaterialTypes.data()[i]], &KWs.data()[freeDOFMaterialTypes.data()[i] * nnz],
4114 u_free_dof[i], u_dof_n.data()[z_i],
4115 m, dm, dm_du_n_fct, f, df, df_du_n_fct, a, da, da_du_n_fct, as, Kr, dKr, dkr_du_n_fct, thetaW_tmp);
4116 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, rho_i, beta, gravity.data(),
4117 alpha.data()[freeDOFMaterialTypes.data()[i]],
4118 n.data()[freeDOFMaterialTypes.data()[i]], thetaR.data()[freeDOFMaterialTypes.data()[i]], thetaSR.data()[freeDOFMaterialTypes.data()[i]], &KWs.data()[freeDOFMaterialTypes.data()[i] * nnz],
4119 u_free_dof_old[i], u_dof_n_old.data()[z_i],
4120 mn.data()[i], dmn, dmn_du_n_fct, fn, dfn, dfn_du_n_fct, an, dan, dan_du_n_fct, asn, Krn, dKrn, dkrn_du_n_fct, thetaW_tmp);
4121 mLow.data()[i] = m;
4122 globalResidual.data()[offset_u + stride_u * i] += bc_mask.data()[i] * (MLi * (m - mn.data()[i]) / dt - ith_flux_term);
4123 globalJacobian.data()[ii] += bc_mask.data()[i] * (MLi * dm / dt + J_ii) + (1.0 - bc_mask.data()[i]);
4124 // (0,1) cross-block: lumped-mass contribution MLi * dm/du_n / dt on the
4125 // (row_w=i, col_n=node_i) entry. This complements the per-edge
4126 // dkr/du_n * max(0,-T) * delta_phi contributions wired into (row_w=i,
4127 // col_n=upwind_node) inside the j-loop above. Together they make the
4128 // low-order operator fully consistent with d R_w / d u_n.
4129 {
4130 const int row_w = offset_u + stride_u * i;
4131 const int col_n = offset_n + stride_n * z_i; // split z-DOF column (== node_i if split_z==0)
4132 int off_wv = -1;
4133 for (int o = csrRowIndeces_Full.data()[row_w];
4134 o < csrRowIndeces_Full.data()[row_w + 1]; o++) {
4135 if (csrColumnOffsets_Full.data()[o] == col_n) { off_wv = o; break; }
4136 }
4137 if (off_wv >= 0) {
4138 globalJacobian.data()[off_wv] += bc_mask.data()[i] * MLi * dm_du_n_fct / dt;
4139 }
4140 }
4141 }
4142 // FCT limiter execution.
4143 //
4144 // Two operating modes:
4145 // * STABILIZATION_TYPE == Implicit_FCT (Richards-style, in-Newton FCT):
4146 // comp-0's fluxCorrection is injected into globalResidual so
4147 // Newton sees the limited residual. This is the legacy path; it
4148 // relies on small enough alpha-derivative deviations to converge.
4149 // * FCT_n == 1 with STABILIZATION_TYPE == EntropyViscosity (TADR-style
4150 // defect-correction):
4151 // Newton solves the LOW-ORDER R_low cleanly (no Zalesak
4152 // contribution to globalResidual). The limiter is computed here
4153 // so limited_solution_n and fluxCorrection_n are available, but
4154 // the actual scatter to self.u[1].dof happens in Python after
4155 // Newton convergence (Coefficients.postStep). This matches TADR /
4156 // Richards Newton.solve flow where FCT is a post-step.
4157 //
4158 // FCTStep is NOT called here. Both components' FCT run as a Python-
4159 // orchestrated post-step (Coefficients.postStep -> LevelModel.FCTStep):
4160 // FCTStep(component, pass=1) -> ghost-scatter Rpos/Rneg -> FCTStep(pass=2),
4161 // which is the requirement for MPI-parallel mass conservation. This
4162 // routine just leaves the comp-0 and comp-1 FCT predictor arrays
4163 // (mLow, mDotLow, dt_times_fH_minus_fL, dLow, min/max_m_bc, ...) populated
4164 // from the converged iterate.
4165 if (STABILIZATION_TYPE == STABILIZATION::Implicit_FCT) {
4166 // Legacy in-Newton injection (kept for STAB=Implicit_FCT only; the
4167 // Python Coefficients class rejects Implicit_FCT, so this is dead).
4168 for (int i = 0; i < numDOFs; i++) {
4169 globalResidual.data()[offset_u + stride_u * i] += fluxCorrection.data()[i];
4170 }
4171 for (int i_n = 0; i_n < numDOFs_n; i_n++) {
4172 globalResidual.data()[offset_n + stride_n * i_n] += fluxCorrection_n.data()[i_n];
4173 }
4174 }
4175
4176 // ============================================================================
4177 // Component-1 (S_n) -- upwind gas-potential flux on the conserved m_n.
4178 //
4179 // The gas equation is recast in potential form:
4180 // d(phi*rho_n*S_n)/dt + div F_n = source/sink
4181 // F_n = -lambda_n K grad(Phi_n)
4182 // Phi_n = u_w + p_c(S_n) - rho_n g . x
4183 // lambda_n(S_n,p_n) = rho_n(p_n) * k_rn(S_n) / mu_n
4184 // Darcy, capillary, and buoyancy collapse into the single edge flux.
4185 // ============================================================================
4186
4187 // -------- Per-DOF nodal CO2 mass (m_c, mc_old for the lumped mass). --------
4188 // rho_n_phi_dof (= lumped phi*N) and ML_n were projected above for the
4189 // invert(COMPONENT=1) path; reuse them here. Compositional (p,z):
4190 // m_c = (phi*N) * z (= u_n). invert: z = m_c / (phi*N).
4191 std::vector<double> m_n_DOF(numDOFs_n, 0.0);
4192 std::vector<double> mn_n_DOF(numDOFs_n, 0.0);
4193 for (int i_n = 0; i_n < numDOFs_n; i_n++) {
4194 const double sat = u_dof_n.data()[i_n]; // z at DOF i (current)
4195 const double sat_old = u_dof_n_old.data()[i_n]; // z at DOF i (t^n)
4196 m_n_DOF[i_n] = rho_n_phi_dof[i_n] * sat;
4197 mn_n_DOF[i_n] = rho_n_phi_dof_old[i_n] * sat_old; // old mass uses OLD phi*N
4198 mn_n.data()[i_n] = mn_n_DOF[i_n]; // diagnostic
4199 quantDOFs_n.data()[i_n] = 0.0; // reset
4200 }
4201
4202 double diag_sumF = 0.0, diag_absF = 0.0;
4203
4204 // Zero the per-node gas-residual budget (6 slots, term-major over numDOFs_n).
4205 const bool have_gas_budget = (gas_budget_node.size() >= (size_t)(6 * numDOFs_n));
4206 if (have_gas_budget)
4207 for (int s = 0; s < 6 * numDOFs_n; ++s) gas_budget_node.data()[s] = 0.0;
4208
4209 std::vector<double> psi_n(numDOFs_n, 1.0);
4210 if (STABILIZATION_TYPE == STABILIZATION::EV_Stab) {
4211 for (int i_n = 0; i_n < numDOFs_n; i_n++) {
4212 const double zi = u_dof_n_old.data()[i_n];
4213 double num = 0.0, den = 0.0;
4214 for (int offset = csrRowIndeces_n_DofLoops.data()[i_n];
4215 offset < csrRowIndeces_n_DofLoops.data()[i_n + 1]; offset++) {
4216 const int j_n = csrColumnOffsets_n_DofLoops.data()[offset];
4217 if (j_n == i_n) continue;
4218 const double d = zi - u_dof_n_old.data()[j_n];
4219 num += d;
4220 den += fabs(d);
4221 }
4222 const double alpha_i = fabs(num) / (den + 1.0e-15);
4223 psi_n[i_n] = (POWER_SMOOTHNESS_INDICATOR == 0)
4224 ? 1.0 : std::pow(alpha_i, POWER_SMOOTHNESS_INDICATOR);
4225 }
4226 }
4227 std::vector<int> node_iface(numDOFs_n, 0);
4228 {
4229 std::vector<int> node_mat0(numDOFs_n, -1);
4230 for (int eN = 0; eN < nElements_global; eN++) {
4231 const int mat_eN_if = elementMaterialTypes.data()[eN];
4232 for (int a = 0; a < nDOF_trial_element; a++) {
4233 const int gN = u_l2g.data()[eN * nDOF_trial_element + a];
4234 if (node_mat0[gN] < 0) node_mat0[gN] = mat_eN_if;
4235 else if (node_mat0[gN] != mat_eN_if) node_iface[gN] = 1;
4236 }
4237 }
4238 }
4239 auto comp1_offset = [&](int i_n, int j_n) -> int {
4240 for (int off = csrRowIndeces_n_DofLoops.data()[i_n];
4241 off < csrRowIndeces_n_DofLoops.data()[i_n + 1]; ++off)
4242 if (csrColumnOffsets_n_DofLoops.data()[off] == j_n) return off;
4243 return -1;
4244 };
4245 for (int off = 0; off < NNZ_n; ++off) {
4246 dLow_n.data()[off] = 0.0;
4247 dEV_n.data()[off] = 0.0;
4248 dt_times_fH_minus_fL_n.data()[off] = 0.0;
4249 }
4250 std::vector<double> node_Kdiag(numDOFs_n, 0.0);
4251
4252 for (int eN = 0; eN < nElements_global; eN++) {
4253 const int mat_eN = elementMaterialTypes.data()[eN];
4254 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
4255 const double alpha_eN = alpha.data()[mat_eN];
4256 const double krn_end_eN = krn_end.data()[mat_eN];
4257 const double n_vg_eN = n.data()[mat_eN];
4258 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
4259 // dm_n_du_n is no longer hoisted: when rho_n is compressible the
4260 // per-DOF lumped diagonal is taken from rho_n_phi_dof, so the
4261 // (1,1) lumped contribution lives inside the per-i loop below.
4262 double elementResidual_n[nDOF_test_element];
4263 // P1: comp-0 (H2O) two-sided water flux residual assembled in THIS element
4264 // loop (replaces the single-nodal-rock DOF-graph water flux). Its (0,0)/
4265 // (0,1) Jacobian scatters directly into globalJacobian via the full CSR
4266 // inside the edge loop (same mapping the DOF-graph loop used).
4267 double elementResidual_w[nDOF_test_element];
4268 double elementMass_n[nDOF_test_element];
4269 double u_n_local[nDOF_trial_element];
4270 double u_n_old_local[nDOF_trial_element];
4271 double elementJacobian_n_n[nDOF_test_element][nDOF_trial_element];
4272 double elementJacobian_n_w[nDOF_test_element][nDOF_trial_element];
4273 // P1 (Richards-style block assembly): the comp-0 (H2O) water-flux
4274 // Jacobian collects into dedicated element arrays and scatters via the
4275 // framework's (0,0)/(0,1) block CSR maps (csr*_w_w / csr*_w_n) with the
4276 // element-local eN_i_j offset -- EXACTLY mirroring the comp-1 (1,1)/(1,0)
4277 // scatter below and Richards.h's direct globalJacobian[ij] write. This
4278 // replaces the old Full-CSR column SEARCH (col==col_n_j ...), which
4279 // silently dropped the (0,1) off-diagonal water<-neighbor-z coupling when
4280 // the hand-computed column index didn't match the matrix layout (the
4281 // "structural misses" in the FD Jacobian probe -> stalled Newton).
4282 double elementJacobian_w_w[nDOF_test_element][nDOF_trial_element];
4283 double elementJacobian_w_n[nDOF_test_element][nDOF_trial_element];
4284 // elementTransport_n collects the mobility-free gas transmissibility
4285 // tau_ij = int K . grad N_j . grad N_i dV
4286 // consumed by the post-element-loop upwind potential-flux edge pass.
4287 // NO rho_n, NO k_rn, NO p_c factors -- those go into lambda_up and
4288 // delta_Phi at the edge level.
4289 double elementTransport_n[nDOF_test_element][nDOF_trial_element];
4290 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
4291 for (int i = 0; i < nDOF_test_element; i++) {
4292 elementResidual_n[i] = 0.0;
4293 elementResidual_w[i] = 0.0;
4294 elementMass_n[i] = 0.0;
4295 for (int j = 0; j < nDOF_trial_element; j++) {
4296 elementJacobian_n_n[i][j] = 0.0;
4297 elementJacobian_n_w[i][j] = 0.0;
4298 elementJacobian_w_w[i][j] = 0.0;
4299 elementJacobian_w_n[i][j] = 0.0;
4300 elementTransport_n[i][j] = 0.0;
4301 }
4302 }
4303 for (int j = 0; j < nDOF_trial_element; j++) {
4304 u_n_local[j] = u_dof_n.data()[u_l2g_n.data()[eN_nDOF_trial_element + j]];
4305 u_n_old_local[j] = u_dof_n_old.data()[u_l2g_n.data()[eN_nDOF_trial_element + j]];
4306 }
4307 for (int k = 0; k < nQuadraturePoints_element; k++) {
4308 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x_q, y_q, z_q;
4309 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
4310 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
4311 jac, jacDet, jacInv, x_q, y_q, z_q);
4312 const double dV = std::fabs(jacDet) * dV_ref.data()[k];
4313 double u_grad_trial_qp[nDOF_trial_element * nSpace];
4314 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace],
4315 jacInv, u_grad_trial_qp);
4316 // Cell loop only accumulates:
4317 // elementMass_n[i] = int N_i dV (lumped row volume, consumed
4318 // below by time-derivative,
4319 // R_diss, Q_inj at element scope)
4320 // elementTransport_n[i][j] = int K . grad N_j . grad N_i dV
4321 // (tau_ij used by the
4322 // post-element-loop upwind
4323 // potential-flux edge pass)
4324 // No closure evaluation, no flux residual, no coefficient sensitivities:
4325 // those collapse into the edge flux F_ij = tau_ij * lambda_up * delta_Phi.
4326 for (int i = 0; i < nDOF_test_element; i++) {
4327 const double test_i = u_test_ref.data()[k * nDOF_test_element + i];
4328 elementMass_n[i] += test_i * dV;
4329 for (int j = 0; j < nDOF_trial_element; j++) {
4330 double K_trial_ij = 0.0;
4331 for (int I = 0; I < nSpace; I++) {
4332 const double grad_Ni_I = u_grad_trial_qp[i * nSpace + I];
4333 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
4334 const int J = a_colind.data()[ii];
4335 K_trial_ij += KWs_eN[ii] * u_grad_trial_qp[j * nSpace + J] * grad_Ni_I;
4336 }
4337 }
4338 elementTransport_n[i][j] += K_trial_ij * dV;
4339 }
4340 }
4341 } // end QP loop
4342
4343 // -------- Lumped mass: ML_n[i] * (m_c - mc_old)/dt on the diagonal. --------
4344 // Applied at element level so it sums to the global lumped mass.
4345 // Compositional (p,z): m_c = (phi*N)*z, with phi*N (and d/dz, d/dp) taken
4346 // from the lumped projections rho_n_phi_dof / dphiN_dz_dof / dphiN_dp_dof.
4347 // dm_c/dz = phi*N + z*phi*dN/dz (1,1) diagonal
4348 // dm_c/dp = z*phi*dN/dp (1,0) diagonal
4349 // Accumulation Jacobian formulas FD-verified in kernel_math_test.cpp (kmtest).
4350 for (int i = 0; i < nDOF_test_element; i++) {
4351 const int gi = u_l2g_n.data()[eN * nDOF_test_element + i]; // comp-1 split DOF
4352 node_Kdiag[gi] += fmax(0.0, elementTransport_n[i][i]); // K-weighted nodal conductance
4353 const double phiN_i = rho_n_phi_dof[gi]; // lumped phi*N (current)
4354 const double phiN_old_i= rho_n_phi_dof_old[gi]; // lumped phi*N (t^n)
4355 const double z_i = u_n_local[i];
4356 const double m_n_loc = phiN_i * z_i; // m_c
4357 const double m_n_old_loc = phiN_old_i * u_n_old_local[i]; // m_c_old
4358 elementResidual_n[i] += elementMass_n[i] * (m_n_loc - m_n_old_loc) / dt;
4359 if (have_gas_budget)
4360 gas_budget_node.data()[0 * numDOFs_n + gi] += elementMass_n[i] * (m_n_loc - m_n_old_loc) / dt;
4361 // (1,1): dm_c/dz = phi*N + z*phi*dN/dz.
4362 elementJacobian_n_n[i][i] += elementMass_n[i] * (phiN_i + z_i * dphiN_dz_dof[gi]) / dt;
4363 // (1,0): dm_c/dp = z*phi*dN/dp.
4364 elementJacobian_n_w[i][i] += elementMass_n[i] * (z_i * dphiN_dp_dof[gi]) / dt;
4365 if (inj_point_mode == 0) {
4366 const double Q_inj_n = injection_dof.data()[gi];
4367 elementResidual_n[i] -= elementMass_n[i] * Q_inj_n;
4368 if (have_gas_budget)
4369 gas_budget_node.data()[3 * numDOFs_n + gi] -= elementMass_n[i] * Q_inj_n;
4370 }
4371 }
4372
4373 if (inj_point_mode == 1) {
4374 for (int p = 0; p < inj_n_ports; p++) {
4375 if (inj_element.data()[p] == eN && inj_rate.data()[p] != 0.0) {
4376 const double qp = inj_rate.data()[p];
4377 for (int i = 0; i < nDOF_test_element; i++) {
4378 const double w = inj_weight.data()[p * nDOF_test_element + i];
4379 elementResidual_n[i] -= qp * w;
4380 if (have_gas_budget) {
4381 const int gii = u_l2g_n.data()[eN * nDOF_test_element + i]; // comp-1 split DOF
4382 gas_budget_node.data()[3 * numDOFs_n + gii] -= qp * w;
4383 }
4384 }
4385 }
4386 }
4387 }
4388
4389 {
4390 const double S_wr_eN = thetaR.data()[mat_eN] / phi_eN;
4391 const double one_m_Sr_eN = 1.0 - S_wr_eN;
4392 const double Se_trap_L4415 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_eN; // gas-only residual trapping
4393 // Capillary entry pressure of THIS element's rock, p_d_e = 1/alpha [head].
4394 // The barrier below charges the gas flux the entry-pressure JUMP
4395 // (p_d_e - p_d_coarsest_neighbor) when gas crosses an edge INTO this
4396 // element from a coarser-medium (lower p_d) node.
4397 const double p_d_e = (alpha_eN > 0.0) ? (1.0 / alpha_eN) : 0.0;
4398 // P2 (compositional): per-node closure for the TWO-PHASE CO2 edge flux
4399 // F_1 = F_g + F_a, F_g = tau*lam_g_up*gate*dPhi_g, F_a = tau*lam_a_up*dPhi_a
4400 // lam_g = (krn_end/mu_n)*rho_g*Y*krn, lam_a = rho_a*X*krw (molar)
4401 // dPhi_g = d(p+pc) - rho_g_mass_edge*g.dx, dPhi_a = dp - rho_a_mass_edge*g.dx
4402 // All flash-derived; FD-verified in edge_flux_test.cpp.
4403 int gN_e[nDOF_trial_element]; // mesh-node id (p, geometry)
4404 int zN_e[nDOF_trial_element]; // split comp-1 z-DOF (== gN_e off interfaces)
4405 double uw_e[nDOF_trial_element], uw_old_e[nDOF_trial_element];
4406 double pc_e[nDOF_trial_element], pc_old_e[nDOF_trial_element];
4407 double dpc_dp_e[nDOF_trial_element], dpc_dz_e[nDOF_trial_element];
4408 double lam_g_e[nDOF_trial_element], lam_a_e[nDOF_trial_element];
4409 double lam_g_old_e[nDOF_trial_element], lam_a_old_e[nDOF_trial_element];
4410 double dlam_g_dp_e[nDOF_trial_element], dlam_g_dz_e[nDOF_trial_element];
4411 double dlam_a_dp_e[nDOF_trial_element], dlam_a_dz_e[nDOF_trial_element];
4412 // P1: H2O-weighted molar mobilities for the two-sided comp-0 water flux.
4413 // lam_g^w = cg*rho_g*(1-Y)*krn, lam_a^w = rho_a*(1-X)*krw -- same flash /
4414 // krn / krw primitives as the CO2 mobilities, weights flipped Y->(1-Y),
4415 // X->(1-X), no gate. FD-verified in comp0_elem_test.cpp (c0etest).
4416 double lwg_e[nDOF_trial_element], lwa_e[nDOF_trial_element];
4417 double lwg_old_e[nDOF_trial_element], lwa_old_e[nDOF_trial_element];
4418 double dlwg_dp_e[nDOF_trial_element], dlwg_dz_e[nDOF_trial_element];
4419 double dlwa_dp_e[nDOF_trial_element], dlwa_dz_e[nDOF_trial_element];
4420 double rgm_e[nDOF_trial_element], ram_e[nDOF_trial_element];
4421 double rgm_old_e[nDOF_trial_element], ram_old_e[nDOF_trial_element];
4422 double drgm_dp_e[nDOF_trial_element], drgm_dz_e[nDOF_trial_element];
4423 double dram_dp_e[nDOF_trial_element], dram_dz_e[nDOF_trial_element];
4424 double Sg_e[nDOF_trial_element], Sg_old_e[nDOF_trial_element];
4425 double dSg_dp_e[nDOF_trial_element], dSg_dz_e[nDOF_trial_element];
4426 double dlg_dz_o[nDOF_trial_element], dla_dz_o[nDOF_trial_element], dpc_dz_o[nDOF_trial_element]; // old-time z-derivs for lagged graph viscosity
4427 const double cg_eN = krn_end_eN / mu_n;
4429 for (int a = 0; a < nDOF_trial_element; a++) {
4430 const int gN = u_l2g.data()[eN_nDOF_trial_element + a]; // mesh node: p + geometry
4431 const int zN = u_l2g_n.data()[eN_nDOF_trial_element + a]; // split z-DOF (== gN if not split)
4432 gN_e[a] = gN;
4433 zN_e[a] = zN;
4434 const double p_a = u_dof.data()[gN];
4435 const double z_a = u_dof_n.data()[zN]; // u_n = z (compositional), split copy
4436 const double p_a_o = u_dof_old.data()[gN];
4437 const double z_a_o = u_dof_n_old.data()[zN];
4438 uw_e[a] = p_a; uw_old_e[a] = p_a_o;
4439 // --- current iterate: flash + closures ---
4440 const double z_cl = fmin(fmax(z_a, 1.0e-8), 1.0 - 1.0e-8);
4441 const double p_cl = fmax(p_a, 1.0e2);
4444 Sg_e[a] = f.S_g; dSg_dp_e[a] = f.dS_g_dp; dSg_dz_e[a] = f.dS_g_dz;
4445 const double Sa = 1.0 - f.S_g;
4446 const double Se_raw = (Sa - S_wr_eN) / one_m_Sr_eN;
4447 double Se, dSe_dp, dSe_dz;
4448 if (Se_raw <= 0.0) { Se = 0.0; dSe_dp = 0.0; dSe_dz = 0.0; }
4449 else if (Se_raw >= 1.0) { Se = 1.0; dSe_dp = 0.0; dSe_dz = 0.0; }
4450 else { Se = Se_raw; dSe_dp = -f.dS_g_dp/one_m_Sr_eN; dSe_dz = -f.dS_g_dz/one_m_Sr_eN; }
4451 double krn=0,dkrn=0,krw=0,dkrw=0,thW=0,DthW=0,pc=0,dpc_dSe=0,d2pc=0;
4452 if (PSK_TYPE_member == 1) {
4453 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se, alpha_eN, n_vg_eN, krn, dkrn, Se_trap_L4415);
4454 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW, DthW, krw, dkrw);
4455 proteus::m_comp_co2::psk::bc_pc_from_Se(Se, alpha_eN, n_vg_eN, pc, dpc_dSe, d2pc);
4456 } else {
4457 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se, alpha_eN, n_vg_eN, krn, dkrn, Se_trap_L4415);
4458 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW, DthW, krw, dkrw);
4459 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se, alpha_eN, n_vg_eN, pc, dpc_dSe, d2pc);
4460 }
4461 pc_e[a] = pc; dpc_dp_e[a] = dpc_dSe*dSe_dp; dpc_dz_e[a] = dpc_dSe*dSe_dz;
4462 const double Mg = f.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-f.Y)*::m_comp_co2::eos::M_H2O_KG;
4463 const double Ma = f.X*::m_comp_co2::eos::M_CO2_KG + (1.0-f.X)*::m_comp_co2::eos::M_H2O_KG;
4464 rgm_e[a] = f.rho_g*Mg; ram_e[a] = f.rho_a*Ma;
4465 drgm_dp_e[a] = f.drho_g_dp*Mg + f.rho_g*f.dY_dp*dMm_e; drgm_dz_e[a] = f.rho_g*f.dY_dz*dMm_e;
4466 dram_dp_e[a] = f.drho_a_dp*Ma + f.rho_a*f.dX_dp*dMm_e; dram_dz_e[a] = f.drho_a_dz*Ma + f.rho_a*f.dX_dz*dMm_e;
4467 // molar mobilities lam_g = cg*rho_g*Y*krn, lam_a = rho_a*X*krw
4468 lam_g_e[a] = cg_eN*f.rho_g*f.Y*krn;
4469 dlam_g_dp_e[a] = cg_eN*(f.drho_g_dp*f.Y*krn + f.rho_g*f.dY_dp*krn + f.rho_g*f.Y*dkrn*dSe_dp);
4470 dlam_g_dz_e[a] = cg_eN*( f.rho_g*f.dY_dz*krn + f.rho_g*f.Y*dkrn*dSe_dz);
4471 lam_a_e[a] = f.rho_a*f.X*krw;
4472 dlam_a_dp_e[a] = f.drho_a_dp*f.X*krw + f.rho_a*f.dX_dp*krw + f.rho_a*f.X*dkrw*dSe_dp;
4473 dlam_a_dz_e[a] = f.drho_a_dz*f.X*krw + f.rho_a*f.dX_dz*krw + f.rho_a*f.X*dkrw*dSe_dz;
4474 // P1: H2O-weighted siblings (1-Y),(1-X) -- same krn/krw/dSe, no gate.
4475 {
4476 const double yw=1.0-f.Y, dyw_dp=-f.dY_dp, dyw_dz=-f.dY_dz;
4477 const double xw=1.0-f.X, dxw_dp=-f.dX_dp, dxw_dz=-f.dX_dz;
4478 lwg_e[a] = cg_eN*f.rho_g*yw*krn;
4479 dlwg_dp_e[a] = cg_eN*(f.drho_g_dp*yw*krn + f.rho_g*dyw_dp*krn + f.rho_g*yw*dkrn*dSe_dp);
4480 dlwg_dz_e[a] = cg_eN*( f.rho_g*dyw_dz*krn + f.rho_g*yw*dkrn*dSe_dz);
4481 lwa_e[a] = f.rho_a*xw*krw;
4482 dlwa_dp_e[a] = f.drho_a_dp*xw*krw + f.rho_a*dxw_dp*krw + f.rho_a*xw*dkrw*dSe_dp;
4483 dlwa_dz_e[a] = f.drho_a_dz*xw*krw + f.rho_a*dxw_dz*krw + f.rho_a*xw*dkrw*dSe_dz;
4484 }
4485 // --- old time level (frozen; feeds the (1-Theta) part, no derivatives) ---
4486 const double z_cl_o = fmin(fmax(z_a_o, 1.0e-8), 1.0 - 1.0e-8);
4487 const double p_cl_o = fmax(p_a_o, 1.0e2);
4490 Sg_old_e[a] = fo.S_g;
4491 const double Sa_o = 1.0 - fo.S_g;
4492 const double Se_o_raw = (Sa_o - S_wr_eN) / one_m_Sr_eN;
4493 const double Se_o = Se_o_raw <= 0.0 ? 0.0 : (Se_o_raw >= 1.0 ? 1.0 : Se_o_raw);
4494 double krn_o=0,dkrn_o=0,krw_o=0,dkrw_o=0,thW_o=0,DthW_o=0,pc_o=0,dpc_o=0,d2pc_o=0;
4495 if (PSK_TYPE_member == 1) {
4496 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_o, alpha_eN, n_vg_eN, krn_o, dkrn_o, Se_trap_L4415);
4497 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_o, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_o, DthW_o, krw_o, dkrw_o);
4498 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_o, alpha_eN, n_vg_eN, pc_o, dpc_o, d2pc_o);
4499 } else {
4500 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_o, alpha_eN, n_vg_eN, krn_o, dkrn_o, Se_trap_L4415);
4501 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_o, alpha_eN, n_vg_eN, thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_o, DthW_o, krw_o, dkrw_o);
4502 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_o, alpha_eN, n_vg_eN, pc_o, dpc_o, d2pc_o);
4503 }
4504 pc_old_e[a] = pc_o;
4505 const double Mg_o = fo.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fo.Y)*::m_comp_co2::eos::M_H2O_KG;
4506 const double Ma_o = fo.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fo.X)*::m_comp_co2::eos::M_H2O_KG;
4507 rgm_old_e[a] = fo.rho_g*Mg_o; ram_old_e[a] = fo.rho_a*Ma_o;
4508 lam_g_old_e[a] = cg_eN*fo.rho_g*fo.Y*krn_o;
4509 lam_a_old_e[a] = fo.rho_a*fo.X*krw_o;
4510 lwg_old_e[a] = cg_eN*fo.rho_g*(1.0-fo.Y)*krn_o; // P1: H2O old-time
4511 lwa_old_e[a] = fo.rho_a*(1.0-fo.X)*krw_o;
4512 const double dSe_o = (Se_o_raw>0.0 && Se_o_raw<1.0) ? -fo.dS_g_dz/one_m_Sr_eN : 0.0;
4513 dlg_dz_o[a] = cg_eN*(fo.rho_g*fo.dY_dz*krn_o + fo.rho_g*fo.Y*dkrn_o*dSe_o);
4514 dla_dz_o[a] = fo.drho_a_dz*fo.X*krw_o + fo.rho_a*fo.dX_dz*krw_o + fo.rho_a*fo.X*dkrw_o*dSe_o;
4515 dpc_dz_o[a] = dpc_o*dSe_o;
4516 }
4517 // Edge pair flux. tau^e is symmetric and lambda_up picks the same
4518 // physical upstream node for (i,j) and (j,i), so F^e_ij = -F^e_ji
4519 // (per-element conservation; summing the <=2 elements sharing an edge
4520 // reconstructs the full two-sided edge flux).
4521 for (int i = 0; i < nDOF_test_element; i++) {
4522 for (int j = 0; j < nDOF_trial_element; j++) {
4523 if (i == j) continue;
4524 const double tau = fmax(0.0, -elementTransport_n[i][j]);
4525 if (tau == 0.0) continue;
4526 double g_dot_dx = 0.0;
4527 for (int I = 0; I < nSpace; I++) {
4528 g_dot_dx += gravity.data()[I] * (mesh_dof.data()[gN_e[j] * 3 + I]
4529 - mesh_dof.data()[gN_e[i] * 3 + I]);
4530 }
4531 // ===================== GAS branch (free CO2, gated) ================
4532 // lam_g = rho_g*Y*krn*krn_end/mu_n ; dPhi_g = d(p+pc) - rho_g_mass*g.dx
4533 const double rgm_edge = 0.5 * (rgm_e[i] + rgm_e[j]);
4534 const double rgm_edge_old = 0.5 * (rgm_old_e[i] + rgm_old_e[j]);
4535 const double dPhi_g = (uw_e[j] + pc_e[j]) - (uw_e[i] + pc_e[i])
4536 - rgm_edge * g_dot_dx;
4537 const double dPhi_g_old = (uw_old_e[j] + pc_old_e[j]) - (uw_old_e[i] + pc_old_e[i])
4538 - rgm_edge_old * g_dot_dx;
4539 const bool up_i_g = (dPhi_g <= 0.0);
4540 const bool up_i_g_old = (dPhi_g_old <= 0.0);
4541 const double lam_g_up = up_i_g ? lam_g_e[i] : lam_g_e[j];
4542 const double lam_g_up_old = up_i_g_old ? lam_g_old_e[i] : lam_g_old_e[j];
4543 // GATE-FREE gas branch -- the capillary entry-pressure valve is DELETED.
4544 // The seal barrier is now carried by the genuinely two-sided capillary
4545 // pressure: each facies side holds its OWN z (node-split via u_l2g_n)
4546 // hence its OWN S_g and p_c, and the explicit interface-pair flux below
4547 // charges the gas the p_c(S_g,coarse) - p_c(S_g,seal) jump -- the van
4548 // Duijn entry condition, emergent from physics. No node_pd_min /
4549 // node_Sn_max valve, no gate Jacobian, no smoothstep.
4550 const double Fg = Theta * tau * lam_g_up * dPhi_g
4551 + (1.0 - Theta) * tau * lam_g_up_old * dPhi_g_old;
4552 // ================= AQUEOUS branch (dissolved CO2, no gate) =========
4553 // lam_a = rho_a*X*krw ; dPhi_a = dp - rho_a_mass*g.dx
4554 const double ram_edge = 0.5 * (ram_e[i] + ram_e[j]);
4555 const double ram_edge_old = 0.5 * (ram_old_e[i] + ram_old_e[j]);
4556 const double dPhi_a = uw_e[j] - uw_e[i] - ram_edge * g_dot_dx;
4557 const double dPhi_a_old = uw_old_e[j] - uw_old_e[i] - ram_edge_old * g_dot_dx;
4558 const bool up_i_a = (dPhi_a <= 0.0);
4559 const bool up_i_a_old = (dPhi_a_old <= 0.0);
4560 const double lam_a_up = up_i_a ? lam_a_e[i] : lam_a_e[j];
4561 const double lam_a_up_old = up_i_a_old ? lam_a_old_e[i] : lam_a_old_e[j];
4562 const double Fa = Theta * tau * lam_a_up * dPhi_a
4563 + (1.0 - Theta) * tau * lam_a_up_old * dPhi_a_old;
4564 // R_n[i] -= F_g + F_a.
4565 elementResidual_n[i] -= Fg + Fa;
4566 if (have_gas_budget)
4567 gas_budget_node.data()[1 * numDOFs_n + zN_e[i]] -= Fg + Fa;
4568 diag_sumF += Fg + Fa;
4569 diag_absF += std::fabs(Fg + Fa);
4570 // ===== Theta-part Jacobian wrt (p_i,z_i,p_j,z_j). FD-verified. =====
4571 const double ddPhig_dpi = -1.0 - dpc_dp_e[i] - 0.5 * drgm_dp_e[i] * g_dot_dx;
4572 const double ddPhig_dzi = - dpc_dz_e[i] - 0.5 * drgm_dz_e[i] * g_dot_dx;
4573 const double ddPhig_dpj = +1.0 + dpc_dp_e[j] - 0.5 * drgm_dp_e[j] * g_dot_dx;
4574 const double ddPhig_dzj = + dpc_dz_e[j] - 0.5 * drgm_dz_e[j] * g_dot_dx;
4575 const double ddPhia_dpi = -1.0 - 0.5 * dram_dp_e[i] * g_dot_dx;
4576 const double ddPhia_dzi = - 0.5 * dram_dz_e[i] * g_dot_dx;
4577 const double ddPhia_dpj = +1.0 - 0.5 * dram_dp_e[j] * g_dot_dx;
4578 const double ddPhia_dzj = - 0.5 * dram_dz_e[j] * g_dot_dx;
4579 const double Tt = Theta * tau;
4580 double dF_dpi = 0.0, dF_dzi = 0.0, dF_dpj = 0.0, dF_dzj = 0.0;
4581 // gas potential part (all four DOFs) -- gate-free
4582 dF_dpi += Tt*lam_g_up*ddPhig_dpi; dF_dzi += Tt*lam_g_up*ddPhig_dzi;
4583 dF_dpj += Tt*lam_g_up*ddPhig_dpj; dF_dzj += Tt*lam_g_up*ddPhig_dzj;
4584 // gas mobility part (upstream node only)
4585 if (up_i_g) { dF_dpi += Tt*dlam_g_dp_e[i]*dPhi_g; dF_dzi += Tt*dlam_g_dz_e[i]*dPhi_g; }
4586 else { dF_dpj += Tt*dlam_g_dp_e[j]*dPhi_g; dF_dzj += Tt*dlam_g_dz_e[j]*dPhi_g; }
4587 // (gate Jacobian removed with the gate)
4588 // aqueous potential part (all four DOFs)
4589 dF_dpi += Tt*lam_a_up*ddPhia_dpi; dF_dzi += Tt*lam_a_up*ddPhia_dzi;
4590 dF_dpj += Tt*lam_a_up*ddPhia_dpj; dF_dzj += Tt*lam_a_up*ddPhia_dzj;
4591 // aqueous mobility part (upstream node only)
4592 if (up_i_a) { dF_dpi += Tt*dlam_a_dp_e[i]*dPhi_a; dF_dzi += Tt*dlam_a_dz_e[i]*dPhi_a; }
4593 else { dF_dpj += Tt*dlam_a_dp_e[j]*dPhi_a; dF_dzj += Tt*dlam_a_dz_e[j]*dPhi_a; }
4594 // dR_n[i]/dx = -dF/dx. (1,1) = z columns, (1,0) = p columns.
4595 elementJacobian_n_n[i][i] += -dF_dzi;
4596 elementJacobian_n_n[i][j] += -dF_dzj;
4597 elementJacobian_n_w[i][i] += -dF_dpi;
4598 elementJacobian_n_w[i][j] += -dF_dpj;
4599 // ===== P1: comp-0 (H2O) two-sided water flux F_0^e = F_g^w + F_a^w.
4600 // Reuses THIS edge's dPhi_g/dPhi_a/tau and the SAME upstream switches
4601 // (phase potentials are composition-independent); only the mobilities
4602 // change to the H2O weights lwg=cg*rho_g*(1-Y)*krn, lwa=rho_a*(1-X)*krw.
4603 // NO gate (matches the verified comp-0 kernel c0test/c0etest; the
4604 // (1-Y) gas branch is water vapor ~ 0 anyway). R_w[i] -= F_0^e and the
4605 // (0,0)/(0,1) Jacobian scatter into comp-0 rows via the full CSR -- the
4606 // SAME mapping the retired single-nodal DOF-graph water loop used. The
4607 // water MASS + Dirichlet identity stay in the DOF-graph accumulation.
4608 {
4609 const double lwg_up = up_i_g ? lwg_e[i] : lwg_e[j];
4610 const double lwg_up_old = up_i_g_old ? lwg_old_e[i] : lwg_old_e[j];
4611 const double lwa_up = up_i_a ? lwa_e[i] : lwa_e[j];
4612 const double lwa_up_old = up_i_a_old ? lwa_old_e[i] : lwa_old_e[j];
4613 const double Fwg = Theta * tau * lwg_up * dPhi_g
4614 + (1.0 - Theta) * tau * lwg_up_old * dPhi_g_old;
4615 const double Fwa = Theta * tau * lwa_up * dPhi_a
4616 + (1.0 - Theta) * tau * lwa_up_old * dPhi_a_old;
4617 elementResidual_w[i] -= Fwg + Fwa;
4618 // Theta-part Jacobian wrt (p_i,z_i,p_j,z_j) (gate=1, no gate deriv).
4619 double dFw_dpi=0.0, dFw_dzi=0.0, dFw_dpj=0.0, dFw_dzj=0.0;
4620 dFw_dpi += Tt*lwg_up*ddPhig_dpi; dFw_dzi += Tt*lwg_up*ddPhig_dzi;
4621 dFw_dpj += Tt*lwg_up*ddPhig_dpj; dFw_dzj += Tt*lwg_up*ddPhig_dzj;
4622 if (up_i_g) { dFw_dpi += Tt*dlwg_dp_e[i]*dPhi_g; dFw_dzi += Tt*dlwg_dz_e[i]*dPhi_g; }
4623 else { dFw_dpj += Tt*dlwg_dp_e[j]*dPhi_g; dFw_dzj += Tt*dlwg_dz_e[j]*dPhi_g; }
4624 dFw_dpi += Tt*lwa_up*ddPhia_dpi; dFw_dzi += Tt*lwa_up*ddPhia_dzi;
4625 dFw_dpj += Tt*lwa_up*ddPhia_dpj; dFw_dzj += Tt*lwa_up*ddPhia_dzj;
4626 if (up_i_a) { dFw_dpi += Tt*dlwa_dp_e[i]*dPhi_a; dFw_dzi += Tt*dlwa_dz_e[i]*dPhi_a; }
4627 else { dFw_dpj += Tt*dlwa_dp_e[j]*dPhi_a; dFw_dzj += Tt*dlwa_dz_e[j]*dPhi_a; }
4628 // Richards-style block scatter: accumulate into the element
4629 // (0,0)/(0,1) arrays; the end-of-element loop loads them into the
4630 // global Jacobian via the framework's csr*_w_w / csr*_w_n block
4631 // maps (eN_i_j offset), so the off-diagonal water<-neighbor-z
4632 // slot is ALWAYS hit (no column search, no dropped coupling).
4633 // Dirichlet mask uses the comp-0 free-DOF tag of the test node.
4634 const int fi = r_l2g.data()[eN_nDOF_trial_element + i];
4635 const double mwi = bc_mask.data()[fi];
4636 elementJacobian_w_w[i][i] += mwi * (-dFw_dpi);
4637 elementJacobian_w_w[i][j] += mwi * (-dFw_dpj);
4638 elementJacobian_w_n[i][i] += mwi * (-dFw_dzi);
4639 elementJacobian_w_n[i][j] += mwi * (-dFw_dzj);
4640 }
4641 // Consistent STAB=2: lagged, PHASE-SPLIT graph viscosity on z.
4642 // Split into a GAS part and an AQUEOUS (dissolved-CO2) part so each
4643 // is gated like the flux branch it stabilizes:
4644 // * gas part -> multiplied by gate_old, so it vanishes across a
4645 // sand->seal entry-pressure jump and does NOT diffuse free CO2
4646 // through the seal (gas pools & spreads laterally as intended).
4647 // * aqueous part -> NOT gated. The aqueous flux Fa = tau*lam_a*dPhi_a
4648 // has no gate (dissolved CO2 freely crosses the interface with the
4649 // brine), so its stabilization must stay ON across interfaces too.
4650 // Gating it (the old `*gate_old` on the whole dvg) switched off all
4651 // z-dissipation exactly at sand interfaces, leaving the ungated
4652 // aqueous advection unstabilized -> interface-aligned z (=> c)
4653 // fingers in the dissolved tongues. This split removes them while
4654 // preserving the seal barrier for the gas.
4655 // Old-time coeff => constant in Newton => the +d/-d Jacobian is exact;
4656 // antisymmetric => global CO2 mass conserved.
4657 const double si_g = std::fabs(dlg_dz_o[i])*std::fabs(dPhi_g_old)
4658 + std::fabs(lam_g_old_e[i])*std::fabs(dpc_dz_o[i]);
4659 const double si_a = std::fabs(dla_dz_o[i])*std::fabs(dPhi_a_old);
4660 const double sj_g = std::fabs(dlg_dz_o[j])*std::fabs(dPhi_g_old)
4661 + std::fabs(lam_g_old_e[j])*std::fabs(dpc_dz_o[j]);
4662 const double sj_a = std::fabs(dla_dz_o[j])*std::fabs(dPhi_a_old);
4663 const double psi_edge = fmax(psi_n[zN_e[i]], psi_n[zN_e[j]]);
4664 const double dEV = tau * ( cE*psi_edge*fmax(si_g,sj_g)
4665 + cE*psi_edge*fmax(si_a,sj_a) );
4666 const double dLow = tau * ( fmax(si_g,sj_g) + fmax(si_a,sj_a) );
4667 const double dHi = fmin(dEV, dLow); // high-order target, clamped <= dLow
4668 const double dResid = (FCT_n == 1) ? dLow : dEV;
4669 if (dResid > 0.0) {
4670 const double Fv = dResid*(u_dof_n.data()[zN_e[j]] - u_dof_n.data()[zN_e[i]]);
4671 elementResidual_n[i] -= Fv;
4672 elementJacobian_n_n[i][i] += dResid;
4673 elementJacobian_n_n[i][j] -= dResid;
4674 }
4675 if (FCT_n == 1) {
4676 const int off_n = comp1_offset(zN_e[i], zN_e[j]);
4677 if (off_n >= 0) {
4678 const double dz = u_dof_n.data()[zN_e[j]] - u_dof_n.data()[zN_e[i]];
4679 dLow_n.data()[off_n] += dLow;
4680 dEV_n.data()[off_n] += dHi;
4681 dt_times_fH_minus_fL_n.data()[off_n] += dt * (dLow - dHi) * dz;
4682 }
4683 }
4684 }
4685 }
4686 }
4687
4688 // -------- Distribute element arrays to global storage. --------
4689 for (int i = 0; i < nDOF_test_element; i++) {
4690 const int eN_i = eN * nDOF_test_element + i;
4691 const int gi = u_l2g_n.data()[eN_i]; // comp-1 split DOF (residual + budget)
4692 globalResidual.data()[offset_n + stride_n * gi] += elementResidual_n[i];
4693 if (have_gas_budget)
4694 gas_budget_node.data()[5 * numDOFs_n + gi] += elementResidual_n[i];
4695 // P1: comp-0 (H2O) two-sided water flux residual (Dirichlet-masked).
4696 const int fi_w = r_l2g.data()[eN_nDOF_trial_element + i];
4697 globalResidual.data()[offset_u + stride_u * fi_w] += bc_mask.data()[fi_w] * elementResidual_w[i];
4698 for (int j = 0; j < nDOF_trial_element; j++) {
4699 const int eN_i_j = eN_i * nDOF_trial_element + j;
4700 globalJacobian.data()[csrRowIndeces_n_n.data()[eN_i] + csrColumnOffsets_n_n.data()[eN_i_j]]
4701 += elementJacobian_n_n[i][j];
4702 globalJacobian.data()[csrRowIndeces_n_w.data()[eN_i] + csrColumnOffsets_n_w.data()[eN_i_j]]
4703 += elementJacobian_n_w[i][j];
4704 globalJacobian.data()[csrRowIndeces_w_w.data()[eN_i] + csrColumnOffsets_w_w.data()[eN_i_j]]
4705 += elementJacobian_w_w[i][j];
4706 globalJacobian.data()[csrRowIndeces_w_n.data()[eN_i] + csrColumnOffsets_w_n.data()[eN_i_j]]
4707 += elementJacobian_w_n[i][j];
4708 }
4709 }
4710 }
4711
4712 for (int i_n = 0; i_n < numDOFs_n; i_n++) {
4713 mLow_n.data()[i_n] = m_n_DOF[i_n];
4714 mDotLow_n.data()[i_n] = (m_n_DOF[i_n] - mn_n.data()[i_n]) / dt;
4715 }
4716
4717 if (split_z != 0) {
4718 const double mu_n_loc = mu_n;
4719 int n_drop_offdiag = 0, n_drop_pcol = 0;
4720 for (int ip = 0; ip < n_interface_pairs; ip++) {
4721 const int nodeN = interface_pairs.data()[5 * ip + 0]; // shared mesh node (for p)
4722 const int zc[2] = { interface_pairs.data()[5 * ip + 1], interface_pairs.data()[5 * ip + 3] };
4723 const int mc[2] = { interface_pairs.data()[5 * ip + 2], interface_pairs.data()[5 * ip + 4] };
4724 const double p_node = u_dof.data()[nodeN];
4725 double pc2[2], dpc_dz2[2], lam_g2[2], dlam_g_dz2[2], C2[2], dC_dz2[2];
4726 double dpc_dp2[2], dlam_g_dp2[2], dC_dp2[2];
4727 for (int s = 0; s < 2; s++) {
4728 const int mat = mc[s];
4729 const double phi_m = thetaR.data()[mat] + thetaSR.data()[mat];
4730 const double S_wr_m = thetaR.data()[mat] / phi_m;
4731 const double oneSr_m = 1.0 - S_wr_m;
4732 const double alpha_m = alpha.data()[mat];
4733 const double n_m = n.data()[mat];
4734 const double cg_m = krn_end.data()[mat] / mu_n_loc;
4735 const double Setrap_m= 1.0 - S_gr.data()[mat] / oneSr_m;
4736 const double z_cl = fmin(fmax(u_dof_n.data()[zc[s]], 1.0e-8), 1.0 - 1.0e-8);
4737 const double p_cl = fmax(p_node, 1.0e2);
4740 const double Sa = 1.0 - f.S_g;
4741 const double Se_raw = (Sa - S_wr_m) / oneSr_m;
4742 double Se, dSe_dz, dSe_dp;
4743 if (Se_raw <= 0.0) { Se = 0.0; dSe_dz = 0.0; dSe_dp = 0.0; }
4744 else if (Se_raw >= 1.0) { Se = 1.0; dSe_dz = 0.0; dSe_dp = 0.0; }
4745 else { Se = Se_raw; dSe_dz = -f.dS_g_dz / oneSr_m; dSe_dp = -f.dS_g_dp / oneSr_m; }
4746 double krn = 0, dkrn = 0, kpc = 0, dpc_dSe = 0, d2pc = 0, thw = 0, Dthw = 0, krw = 0, dkrw = 0;
4747 if (PSK_TYPE_member == 1) {
4748 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se, alpha_m, n_m, krn, dkrn, Setrap_m);
4749 proteus::m_comp_co2::psk::bc_pc_from_Se(Se, alpha_m, n_m, kpc, dpc_dSe, d2pc);
4750 } else {
4751 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se, alpha_m, n_m, krn, dkrn, Setrap_m);
4752 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se, alpha_m, n_m, kpc, dpc_dSe, d2pc);
4753 }
4754 pc2[s] = kpc;
4755 dpc_dz2[s] = dpc_dSe * dSe_dz;
4756 dpc_dp2[s] = dpc_dSe * dSe_dp;
4757 lam_g2[s] = cg_m * f.rho_g * f.Y * krn;
4758 dlam_g_dz2[s]= cg_m * (f.rho_g * f.dY_dz * krn + f.rho_g * f.Y * dkrn * dSe_dz);
4759 // gas molar density rho_g is p-dependent (compressible) -> keep drho_g_dp
4760 // (same convention as the boundary loop; the z-deriv drops it as ~0).
4761 dlam_g_dp2[s]= cg_m * (f.drho_g_dp * f.Y * krn + f.rho_g * f.dY_dp * krn
4762 + f.rho_g * f.Y * dkrn * dSe_dp);
4763 C2[s] = f.rho_a * f.X;
4764 dC_dz2[s] = f.drho_a_dz * f.X + f.rho_a * f.dX_dz;
4765 dC_dp2[s] = f.drho_a_dp * f.X + f.rho_a * f.dX_dp;
4766 }
4767 const int z_a = zc[0], z_b = zc[1];
4768 const double Ka = node_Kdiag[z_a], Kb = node_Kdiag[z_b];
4769 const double tau_if = (Ka + Kb > 0.0) ? (2.0 * Ka * Kb / (Ka + Kb)) : 0.0;
4770 if (tau_if == 0.0) continue;
4771 const double dPhi_g = pc2[0] - pc2[1]; // two-sided p_c (p continuous, dx=0)
4772 const bool up_a = (dPhi_g >= 0.0);
4773 const double lam_up = up_a ? lam_g2[0] : lam_g2[1];
4774 const double dlam_up_dza = up_a ? dlam_g_dz2[0] : 0.0;
4775 const double dlam_up_dzb = up_a ? 0.0 : dlam_g_dz2[1];
4776 const double Fg = tau_if * lam_up * dPhi_g;
4777 const double Fd = D_m * tau_if * (C2[0] - C2[1]); // Fickian (D_m effective, tunable)
4778 const double F = Fg + Fd;
4779 const double dF_dza = tau_if * (dlam_up_dza * dPhi_g + lam_up * dpc_dz2[0]) + D_m * tau_if * ( dC_dz2[0]);
4780 const double dF_dzb = tau_if * (dlam_up_dzb * dPhi_g - lam_up * dpc_dz2[1]) + D_m * tau_if * (-dC_dz2[1]);
4781 // (1,0) pressure tangent: F = tau_if*lam_up*(pc_a - pc_b) + D_m*tau_if*(C_a - C_b)
4782 // depends on the SHARED p_node through the flash on both sides, so
4783 // dF/dp = tau_if*( dlam_up_dp*dPhi_g + lam_up*(dpc_dp_a - dpc_dp_b) )
4784 // + D_m*tau_if*( dC_dp_a - dC_dp_b ).
4785 const double dlam_up_dp = up_a ? dlam_g_dp2[0] : dlam_g_dp2[1];
4786 const double dF_dp = tau_if * (dlam_up_dp * dPhi_g + lam_up * (dpc_dp2[0] - dpc_dp2[1]))
4787 + D_m * tau_if * (dC_dp2[0] - dC_dp2[1]);
4788 // residual (antisymmetric): R_a += F, R_b -= F
4789 globalResidual.data()[offset_n + stride_n * z_a] += F;
4790 globalResidual.data()[offset_n + stride_n * z_b] -= F;
4791 diag_sumF += 0.0; // interface flux telescopes to 0 globally (antisymmetric)
4792 // diagonal tangent into the existing (z,z) slots (always present)
4793 const int caa = comp1_offset(z_a, z_a), cbb = comp1_offset(z_b, z_b);
4794 if (caa >= 0) globalJacobian.data()[comp1_full_offsets.data()[caa]] += dF_dza;
4795 if (cbb >= 0) globalJacobian.data()[comp1_full_offsets.data()[cbb]] += -dF_dzb;
4796 // off-diagonal z_a<->z_b tangent: scatter into the dedicated interface slots
4797 // allocated by getExtraSparsityElements (NOT comp1_offset, which excludes them
4798 // from the compact graph). d(R_a)/dz_b = dF_dzb, d(R_b)/dz_a = -dF_dza.
4799 const int cab = comp1_iface_offsets.data()[2 * ip + 0]; // (z_a row, z_b col)
4800 const int cba = comp1_iface_offsets.data()[2 * ip + 1]; // (z_b row, z_a col)
4801 if (cab >= 0) globalJacobian.data()[cab] += dF_dzb;
4802 else ++n_drop_offdiag;
4803 if (cba >= 0) globalJacobian.data()[cba] += -dF_dza;
4804 else ++n_drop_offdiag;
4805 // (1,0) cross-block: R_a/R_b vs the shared pressure DOF p_node. Flat offsets
4806 // precomputed in Python (comp10_full_offsets), -1 if the slot is absent.
4807 const int oa = comp10_full_offsets.data()[2 * ip + 0];
4808 const int ob = comp10_full_offsets.data()[2 * ip + 1];
4809 if (oa >= 0) globalJacobian.data()[oa] += dF_dp;
4810 else ++n_drop_pcol;
4811 if (ob >= 0) globalJacobian.data()[ob] += -dF_dp;
4812 else ++n_drop_pcol;
4813 }
4814 if (n_drop_offdiag > 0 || n_drop_pcol > 0) {
4815 std::cerr << "[m_comp_co2 split_z] WARNING: dropped interface tangents -- "
4816 << n_drop_offdiag << " z_a<->z_b off-diagonal, "
4817 << n_drop_pcol << " z<->p_node (1,0) slot(s) absent "
4818 << "(stale/incomplete Jacobian sparsity -> degraded Newton)"
4819 << std::endl;
4820 }
4821 }
4822 if (split_anchor_alpha > 0.0 && dt > 0.0) {
4823 const double Sg_tol = split_anchor_Sg_tol; // S_g below this => no free gas
4824 const double X_tol = split_anchor_X_tol; // dissolved-CO2 mole frac below this => no CO2
4825 // ---- LAYER 1: conservative graph-Laplacian among CO2-free neighbours. ----
4826 // Disabled by split_anchor_layer1 == 0 (Layer-2-only): the gate catches the whole
4827 // CO2-free background so this is a stiff domain-wide z-diffusion -> slows Newton.
4828 if (split_anchor_layer1 != 0)
4829 for (int i = 0; i < numDOFs_n; i++) {
4830 if (ML_n[i] <= 0.0) continue;
4831 if (Sg_dof_old[i] >= Sg_tol || X_dof_old[i] >= X_tol) continue; // i holds CO2 -> skip
4832 const double cap_i = rho_n_phi_dof_old[i] * ML_n[i]; // phi*N_old*V_node [mol]
4833 const double zi = u_dof_n.data()[i];
4834 double diag = 0.0;
4835 for (int off = csrRowIndeces_n_DofLoops.data()[i];
4836 off < csrRowIndeces_n_DofLoops.data()[i + 1]; ++off) {
4837 const int j = csrColumnOffsets_n_DofLoops.data()[off];
4838 if (j == i || ML_n[j] <= 0.0) continue;
4839 if (Sg_dof_old[j] >= Sg_tol || X_dof_old[j] >= X_tol) continue; // CO2-free<->CO2-free only
4840 const double cap_j = rho_n_phi_dof_old[j] * ML_n[j];
4841 const double lam_ij = split_anchor_alpha * fmin(cap_i, cap_j) / dt;
4842 if (lam_ij <= 0.0) continue;
4843 // F_ij = lam_ij*(z_i - z_j): row i only (row j adds the antisymmetric -F_ij).
4844 globalResidual.data()[offset_n + stride_n * i] += lam_ij * (zi - u_dof_n.data()[j]);
4845 diag += lam_ij;
4846 // off-diagonal tangent d(R_i)/dz_j = -lam_ij (compact-graph slot, present).
4847 globalJacobian.data()[comp1_full_offsets.data()[off]] += -lam_ij;
4848 }
4849 // diagonal tangent d(R_i)/dz_i = +sum_j lam_ij (adds to the comp-1 mass diagonal).
4850 if (diag != 0.0) {
4851 const int ii = comp1_offset(i, i);
4852 if (ii >= 0) globalJacobian.data()[comp1_full_offsets.data()[ii]] += diag;
4853 }
4854 }
4855 // ---- LAYER 2: conservative fine<->coarse spring on split-interface pairs. ----
4856 for (int ip = 0; ip < n_interface_pairs; ip++) {
4857 const int z_a = interface_pairs.data()[5 * ip + 1];
4858 const int z_b = interface_pairs.data()[5 * ip + 3];
4859 if (ML_n[z_a] <= 0.0 || ML_n[z_b] <= 0.0) continue;
4860 // Spring only where BOTH copies are CO2-free; a real plume / capillary jump is
4861 // carried by the gate-free interface flux above and must never be sprung over.
4862 if (Sg_dof_old[z_a] >= Sg_tol || X_dof_old[z_a] >= X_tol) continue;
4863 if (Sg_dof_old[z_b] >= Sg_tol || X_dof_old[z_b] >= X_tol) continue;
4864 const double cap_a = rho_n_phi_dof_old[z_a] * ML_n[z_a];
4865 const double cap_b = rho_n_phi_dof_old[z_b] * ML_n[z_b];
4866 const double lam_s = split_anchor_alpha * fmin(cap_a, cap_b) / dt;
4867 if (lam_s <= 0.0) continue;
4868 const double F_s = lam_s * (u_dof_n.data()[z_a] - u_dof_n.data()[z_b]);
4869 globalResidual.data()[offset_n + stride_n * z_a] += F_s;
4870 globalResidual.data()[offset_n + stride_n * z_b] -= F_s;
4871 // diagonal tangents into the existing (z,z) slots (always present).
4872 const int caa = comp1_offset(z_a, z_a), cbb = comp1_offset(z_b, z_b);
4873 if (caa >= 0) globalJacobian.data()[comp1_full_offsets.data()[caa]] += lam_s;
4874 if (cbb >= 0) globalJacobian.data()[comp1_full_offsets.data()[cbb]] += lam_s;
4875 // off-diagonal z_a<->z_b tangents: d(R_a)/dz_b = -lam_s, d(R_b)/dz_a = -lam_s,
4876 // into the dedicated interface slots (NOT the compact graph, which excludes them).
4877 const int cab = comp1_iface_offsets.data()[2 * ip + 0]; // (z_a row, z_b col)
4878 const int cba = comp1_iface_offsets.data()[2 * ip + 1]; // (z_b row, z_a col)
4879 if (cab >= 0) globalJacobian.data()[cab] += -lam_s;
4880 if (cba >= 0) globalJacobian.data()[cba] += -lam_s;
4881 }
4882 }
4883 // DIAG: net gas-flux imbalance (Python prints + MPI-reduces). The T-asymmetry
4884 // probes [0]/[1] retired with the DOF-graph loop; [2]=sum F, [3]=sum|F|.
4885 if (gas_diag.size() >= 4) {
4886 gas_diag.data()[0] = 0.0;
4887 gas_diag.data()[1] = 0.0;
4888 gas_diag.data()[2] = diag_sumF;
4889 gas_diag.data()[3] = diag_absF;
4890 }
4891
4892
4893 // ============================================================================
4894 // Comp-1 (CO2 / z) exterior boundary loop -- STAB=2 path. COMPOSITIONAL.
4895 //
4896 // P3c STATUS: BOUNDARY PORTED (2026-06-06). Slot 1 is the overall CO2
4897 // composition z, NOT a saturation. This loop computes the compositional
4898 // molar CO2 trace flux F_1.n, mirroring the FD-verified interior element
4899 // flux (calculateResidual / calculateJacobian, lines ~1119/1942):
4900 // F_1 = rho_g*Y*u_g + rho_a*X*u_a,
4901 // u_a = -(K krw/mu_w)(grad p - rho_a_mass g), p_a = p
4902 // u_g = -(K krg/mu_g)(grad p + pc'(S_a) grad S_a - rho_g_mass g),
4903 // grad S_a = -(dSg/dp grad p + dSg/dz grad z),
4904 // with every saturation-dependent property recomputed from the FLASH
4905 // saturation S_g(p,z) (psk closures take the wetting Se_a = (1-S_g-S_wr)/
4906 // (1-S_wr)). The surface term from integrating div(F_1) by parts is
4907 // +(F_1.n) N_i dS, so the residual mirrors the interior with gradN_i -> n.
4908 //
4909 // BC handling: a Nitsche-style penalty drives z at the trace toward the
4910 // prescribed bc_u_n_ext_b (= z_BC) on Dirichlet-z faces (isDir_n != 0;
4911 // McWhorter-Sunada inlet). No-Dirichlet faces are no-flow (F_1.n = 0) so a
4912 // closed box conserves mass.
4913 //
4914 // Jacobian contributions (chain rule through the analytic flash; interior
4915 // gradN_i replaced by the boundary normal n_I):
4916 // (1,1) self : d(F_1.n)/dz (value-block * trial_j + grad-block . gradN_j)
4917 // + Dirichlet penalty * trial_j.
4918 // (1,0) cross : d(F_1.n)/dp (value-block * trial_j + grad-block . gradN_j).
4919 //
4920 // NOTE: structurally a consistent CG/Nitsche trace flux (lambda at the
4921 // trace, no upwind), not the interior edge-based upwind potential flux. A
4922 // ghost-node TPFA boundary (F_b = tau_b lambda_up (Phi_BC - Phi_trace)) is
4923 // the eventual sharper form; see [[m_comp_co2_stab2_upwind_potential_flux]].
4924 // Closure dispatch uses PSK_TYPE_member set at the top of this routine.
4925 // ============================================================================
4926 for (int ebNE = 0; ebNE < nExteriorElementBoundaries_global; ebNE++) {
4927 const int ebN = exteriorElementBoundariesArray.data()[ebNE];
4928 const int eN = elementBoundaryElementsArray.data()[ebN * 2 + 0];
4929 const int ebN_local = elementBoundaryLocalElementBoundariesArray.data()[ebN * 2 + 0];
4930 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
4931 const int mat_eN = elementMaterialTypes.data()[eN];
4932 const double phi_eN = thetaR.data()[mat_eN] + thetaSR.data()[mat_eN];
4933 const double alpha_eN = alpha.data()[mat_eN];
4934 const double krn_end_eN = krn_end.data()[mat_eN];
4935 const double n_vg_eN = n.data()[mat_eN];
4936 const double *KWs_eN = &KWs.data()[mat_eN * nnz];
4937 const double S_wr_loc = thetaR.data()[mat_eN] / phi_eN;
4938 const double one_m_Sr_loc = 1.0 - S_wr_loc;
4939 const double Se_trap_L4846 = 1.0 - S_gr.data()[mat_eN] / one_m_Sr_loc; // gas-only residual trapping
4940
4941 double elementResidual_n_eb[nDOF_test_element];
4942 double elementJacobian_n_n_eb[nDOF_test_element][nDOF_trial_element];
4943 double elementJacobian_n_w_eb[nDOF_test_element][nDOF_trial_element];
4944 for (int i = 0; i < nDOF_test_element; i++) {
4945 elementResidual_n_eb[i] = 0.0;
4946 for (int j = 0; j < nDOF_trial_element; j++) {
4947 elementJacobian_n_n_eb[i][j] = 0.0;
4948 elementJacobian_n_w_eb[i][j] = 0.0;
4949 }
4950 }
4951
4952 for (int kb = 0; kb < nQuadraturePoints_elementBoundary; kb++) {
4953 const int ebNE_kb = ebNE * nQuadraturePoints_elementBoundary + kb;
4954 const int ebN_local_kb = ebN_local * nQuadraturePoints_elementBoundary + kb;
4955 const int ebN_local_kb_nSpace = ebN_local_kb * nSpace;
4956
4957 double jac_ext[nSpace * nSpace], jacDet_ext, jacInv_ext[nSpace * nSpace];
4958 double boundaryJac_b[nSpace * (nSpace - 1)];
4959 double metricTensor_b[(nSpace - 1) * (nSpace - 1)];
4960 double metricTensorDetSqrt_b, dS_eb, normal_b[3];
4961 double xt_b, yt_b, zt_b, integralScaling_b;
4962 double x_eb, y_eb, z_eb;
4963 ck.calculateMapping_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
4964 mesh_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
4965 mesh_grad_trial_trace_ref.data(), boundaryJac_ref.data(),
4966 jac_ext, jacDet_ext, jacInv_ext, boundaryJac_b, metricTensor_b,
4967 metricTensorDetSqrt_b, normal_ref.data(), normal_b,
4968 x_eb, y_eb, z_eb);
4969 ck.calculateMappingVelocity_elementBoundary(eN, ebN_local, kb, ebN_local_kb,
4970 mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_trace_ref.data(),
4971 xt_b, yt_b, zt_b, normal_b, boundaryJac_b, metricTensor_b,
4972 integralScaling_b);
4973 dS_eb = ((1.0 - MOVING_DOMAIN) * metricTensorDetSqrt_b
4974 + MOVING_DOMAIN * integralScaling_b) * dS_ref.data()[kb];
4975
4976 // Trace solution and gradients.
4977 double u_grad_trial_trace_b[nDOF_trial_element * nSpace];
4978 ck.gradTrialFromRef(
4979 &u_grad_trial_trace_ref.data()[ebN_local_kb_nSpace * nDOF_trial_element],
4980 jacInv_ext, u_grad_trial_trace_b);
4981 double u_w_ext_b = 0.0, u_n_ext_b = 0.0;
4982 double grad_u_w_ext_b[nSpace], grad_u_n_ext_b[nSpace];
4983 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
4984 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element],
4985 u_w_ext_b);
4986 ck.valFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
4987 &u_trial_trace_ref.data()[ebN_local_kb * nDOF_test_element],
4988 u_n_ext_b);
4989 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element],
4990 u_grad_trial_trace_b, grad_u_w_ext_b);
4991 ck.gradFromDOF(u_dof_n.data(), &u_l2g.data()[eN_nDOF_trial_element],
4992 u_grad_trial_trace_b, grad_u_n_ext_b);
4993
4994 // Dirichlet target for u_n (no-op when isDOFBoundary_n == 0).
4995 const int isDir_n = isDOFBoundary_n.data()[ebNE_kb];
4996 const double bc_u_n_ext_b = isDir_n * ebqe_bc_u_n_ext.data()[ebNE_kb]
4997 + (1 - isDir_n) * u_n_ext_b;
4998
4999
5000 const double z_clb = fmin(fmax(u_n_ext_b, 1.0e-8), 1.0 - 1.0e-8);
5001 const double p_clb = fmax(u_w_ext_b, 1.0e2);
5004 const double S_g_b = fsb.S_g, Sa_b = 1.0 - S_g_b;
5005 // wetting effective saturation from the FLASH saturation + (p,z) derivs.
5006 const double Se_raw_b = (Sa_b - S_wr_loc) / one_m_Sr_loc;
5007 double Se_b, dSe_dp_b, dSe_dz_b;
5008 if (Se_raw_b <= 0.0) { Se_b = 0.0; dSe_dp_b = 0.0; dSe_dz_b = 0.0; }
5009 else if (Se_raw_b >= 1.0) { Se_b = 1.0; dSe_dp_b = 0.0; dSe_dz_b = 0.0; }
5010 else { Se_b = Se_raw_b; dSe_dp_b = -fsb.dS_g_dp/one_m_Sr_loc; dSe_dz_b = -fsb.dS_g_dz/one_m_Sr_loc; }
5011 double KWr_b=0.0, DKWr_b=0.0, thW_b=0.0, DthW_b=0.0, KNr_b=0.0, DKNr_b=0.0;
5012 double pc_b=0.0, dpc_dSe_b=0.0, d2pc_b=0.0;
5013 if (PSK_TYPE_member == 1) {
5014 proteus::m_comp_co2::psk::bc_wetting_from_Se(Se_b, alpha_eN, n_vg_eN,
5015 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
5016 proteus::m_comp_co2::psk::bc_kr_nonwetting_from_Se(Se_b, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L4846);
5017 proteus::m_comp_co2::psk::bc_pc_from_Se(Se_b, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
5018 } else {
5019 proteus::m_comp_co2::psk::vgm_wetting_from_Se(Se_b, alpha_eN, n_vg_eN,
5020 thetaR.data()[mat_eN], thetaSR.data()[mat_eN], thW_b, DthW_b, KWr_b, DKWr_b);
5021 proteus::m_comp_co2::psk::vgm_kr_nonwetting_from_Se(Se_b, alpha_eN, n_vg_eN, KNr_b, DKNr_b, Se_trap_L4846);
5022 proteus::m_comp_co2::psk::vgm_pc_from_Se(Se_b, alpha_eN, n_vg_eN, pc_b, dpc_dSe_b, d2pc_b);
5023 }
5024 KNr_b *= krn_end_eN; DKNr_b *= krn_end_eN;
5025 const double pcp_b = dpc_dSe_b / one_m_Sr_loc; // pc'(S_a)
5026 const double dpcp_dp_b = (d2pc_b / one_m_Sr_loc) * dSe_dp_b; // d pc'(S_a)/dp
5027 const double dpcp_dz_b = (d2pc_b / one_m_Sr_loc) * dSe_dz_b;
5028 // mass densities for gravity (molar density * mean molar mass) + derivs.
5030 const double Mbar_g_b = fsb.Y*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.Y)*::m_comp_co2::eos::M_H2O_KG;
5031 const double Mbar_a_b = fsb.X*::m_comp_co2::eos::M_CO2_KG + (1.0-fsb.X)*::m_comp_co2::eos::M_H2O_KG;
5032 const double rho_g_mass_b = fsb.rho_g*Mbar_g_b, rho_a_mass_b = fsb.rho_a*Mbar_a_b;
5033 const double drgm_dp_b = fsb.drho_g_dp*Mbar_g_b + fsb.rho_g*fsb.dY_dp*dMm_b;
5034 const double drgm_dz_b = fsb.rho_g*fsb.dY_dz*dMm_b;
5035 const double dram_dp_b = fsb.drho_a_dp*Mbar_a_b + fsb.rho_a*fsb.dX_dp*dMm_b;
5036 const double dram_dz_b = fsb.drho_a_dz*Mbar_a_b + fsb.rho_a*fsb.dX_dz*dMm_b;
5037 // CO2 transport coefficients Ag=rho_g*Y, Aa=rho_a*X + (p,z) derivatives.
5038 const double Ag = fsb.rho_g*fsb.Y, Aa = fsb.rho_a*fsb.X;
5039 const double dAg_dp = fsb.drho_g_dp*fsb.Y + fsb.rho_g*fsb.dY_dp;
5040 const double dAg_dz = fsb.rho_g*fsb.dY_dz;
5041 const double dAa_dp = fsb.drho_a_dp*fsb.X + fsb.rho_a*fsb.dX_dp;
5042 const double dAa_dz = fsb.drho_a_dz*fsb.X + fsb.rho_a*fsb.dX_dz;
5043 // per-direction Darcy velocities ug[I], ua[I] + value-block partials
5044 // (gradients held fixed). Mobilities carry the 1/mu_n (gas) factor.
5045 double ug_b[nSpace], ua_b[nSpace];
5046 double dug_dp_b[nSpace], dug_dz_b[nSpace], dua_dp_b[nSpace], dua_dz_b[nSpace];
5047 for (int I = 0; I < nSpace; I++) {
5048 double ugI=0.0, uaI=0.0, dugp=0.0, dugz=0.0, duap=0.0, duaz=0.0;
5049 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
5050 const int J = a_colind.data()[ii];
5051 const double Kii = KWs_eN[ii];
5052 const double Mob_g = KNr_b*Kii/mu_n, Mob_a = KWr_b*Kii;
5053 const double dMobg_dp = (DKNr_b*Kii/mu_n)*dSe_dp_b, dMobg_dz = (DKNr_b*Kii/mu_n)*dSe_dz_b;
5054 const double dMoba_dp = (DKWr_b*Kii)*dSe_dp_b, dMoba_dz = (DKWr_b*Kii)*dSe_dz_b;
5055 const double gJ = gravity.data()[J];
5056 const double gradSa = -(fsb.dS_g_dp*grad_u_w_ext_b[J] + fsb.dS_g_dz*grad_u_n_ext_b[J]);
5057 const double gp_a = grad_u_w_ext_b[J] - rho_a_mass_b*gJ;
5058 const double gp_g = grad_u_w_ext_b[J] + pcp_b*gradSa - rho_g_mass_b*gJ;
5059 ugI -= Mob_g*gp_g; uaI -= Mob_a*gp_a;
5060 const double dgradSa_dp = -(fsb.d2S_g_dp2 *grad_u_w_ext_b[J] + fsb.d2S_g_dpdz*grad_u_n_ext_b[J]);
5061 const double dgradSa_dz = -(fsb.d2S_g_dpdz*grad_u_w_ext_b[J] + fsb.d2S_g_dz2 *grad_u_n_ext_b[J]);
5062 const double dgpg_dp = dpcp_dp_b*gradSa + pcp_b*dgradSa_dp - drgm_dp_b*gJ;
5063 const double dgpg_dz = dpcp_dz_b*gradSa + pcp_b*dgradSa_dz - drgm_dz_b*gJ;
5064 dugp -= dMobg_dp*gp_g + Mob_g*dgpg_dp;
5065 dugz -= dMobg_dz*gp_g + Mob_g*dgpg_dz;
5066 duap -= dMoba_dp*gp_a + Mob_a*(-dram_dp_b*gJ);
5067 duaz -= dMoba_dz*gp_a + Mob_a*(-dram_dz_b*gJ);
5068 }
5069 ug_b[I]=ugI; ua_b[I]=uaI;
5070 dug_dp_b[I]=dugp; dug_dz_b[I]=dugz; dua_dp_b[I]=duap; dua_dz_b[I]=duaz;
5071 }
5072
5073 // F_1 . n at this QP (consistent flux, before the penalty term which
5074 // depends only on the test-function row).
5075 double F_n_dot_n = 0.0;
5076 for (int I = 0; I < nSpace; I++) {
5077 F_n_dot_n += (Ag*ug_b[I] + Aa*ua_b[I]) * normal_b[I];
5078 }
5079 // value-block scalars dotted with the normal (interior gradN_i -> n_I).
5080 double Sval_p_b = 0.0, Sval_z_b = 0.0;
5081 for (int I = 0; I < nSpace; I++) {
5082 Sval_p_b += (dAg_dp*ug_b[I] + Ag*dug_dp_b[I] + dAa_dp*ua_b[I] + Aa*dua_dp_b[I]) * normal_b[I];
5083 Sval_z_b += (dAg_dz*ug_b[I] + Ag*dug_dz_b[I] + dAa_dz*ua_b[I] + Aa*dua_dz_b[I]) * normal_b[I];
5084 }
5085 // IIPG penalty scaled by the comp-1 diffusion magnitude a_n (= rho_g*Y*
5086 // krn/mu_n + rho_a*X*krw, times a representative K/mu_w; frozen in the
5087 // Jacobian). The bare framework penalty (const/h) is ~1e4x too weak vs
5088 // the molar-density-scaled comp-1 equation, so z floats off the BC.
5089 double Kw_rep = 0.0;
5090 for (int ii = 0; ii < nnz; ii++) Kw_rep = fmax(Kw_rep, fabs(KWs_eN[ii]));
5091 const double a_n_scale = (Ag*KNr_b/mu_n + Aa*KWr_b) * Kw_rep;
5092 const double penalty = ebqe_penalty_ext.data()[ebNE_kb] * a_n_scale;
5093 if (isDir_n) {
5094 // Nitsche penalty drives z at the trace toward the prescribed BC.
5095 F_n_dot_n += penalty * (u_n_ext_b - bc_u_n_ext_b);
5096 } else {
5097 // No Dirichlet on z => no-flow / closed boundary for the CO2 eq.
5098 // The consistent interior-trace flux must NOT be applied here: it
5099 // is generally nonzero (gravity + capillary at the trace) and would
5100 // inject a spurious boundary flux that breaks mass conservation on
5101 // a closed box. This mirrors STAB=0's calculateResidual, whose
5102 // exterior loop adds nothing to the gas equation. Only true
5103 // Dirichlet faces get the consistent flux + Nitsche penalty.
5104 F_n_dot_n = 0.0;
5105 }
5106
5107 for (int i = 0; i < nDOF_test_element; i++) {
5108 const double test_i_dS = u_test_trace_ref.data()[
5109 ebN_local_kb * nDOF_test_element + i] * dS_eb;
5110 // Residual contribution: +(F_1.n) N_i dS.
5111 elementResidual_n_eb[i] += F_n_dot_n * test_i_dS;
5112 // Jacobian (per trial j). d(F_1.n)/du_j = value-block * trial_j +
5113 // gradient-block . gradN_j, the interior chain rule with gradN_i->n.
5114 for (int j = 0; j < nDOF_trial_element; j++) {
5115 const double trial_j_b = u_trial_trace_ref.data()[
5116 ebN_local_kb * nDOF_test_element + j];
5117 // gradient-block scalars dotted with the normal:
5118 // sum_{I,ii} (dF_1[I]/dgrad_var[J]) gradN_j[J] n_I.
5119 double Sgrad_p_b = 0.0, Sgrad_z_b = 0.0;
5120 for (int I = 0; I < nSpace; I++) {
5121 for (int ii = a_rowptr.data()[I]; ii < a_rowptr.data()[I + 1]; ii++) {
5122 const int J = a_colind.data()[ii];
5123 const double Kii = KWs_eN[ii];
5124 const double Mob_g = KNr_b*Kii/mu_n, Mob_a = KWr_b*Kii;
5125 const double gNjJ = u_grad_trial_trace_b[j * nSpace + J];
5126 // d ug[I]/d grad_p[J] = -Mob_g*(1 - pcp*dSg_dp); d ua[I]/d grad_p[J] = -Mob_a
5127 const double dFdgp = Ag*(-Mob_g*(1.0 - pcp_b*fsb.dS_g_dp)) + Aa*(-Mob_a);
5128 // d ug[I]/d grad_z[J] = -Mob_g*(-pcp*dSg_dz); d ua[I]/d grad_z[J] = 0
5129 const double dFdgz = Ag*(-Mob_g*(-pcp_b*fsb.dS_g_dz));
5130 Sgrad_p_b += dFdgp * gNjJ * normal_b[I];
5131 Sgrad_z_b += dFdgz * gNjJ * normal_b[I];
5132 }
5133 }
5134 // (1,1) self: d(F_1.n)/dz ; (1,0) cross: d(F_1.n)/dp.
5135 double jac_nn = Sval_z_b * trial_j_b + Sgrad_z_b;
5136 double jac_nw = Sval_p_b * trial_j_b + Sgrad_p_b;
5137 // Same no-flow gate as the residual: only Dirichlet faces
5138 // contribute a boundary flux (consistent flux + Nitsche penalty).
5139 if (isDir_n) {
5140 jac_nn += penalty * trial_j_b;
5141 } else {
5142 jac_nn = 0.0;
5143 jac_nw = 0.0;
5144 }
5145 elementJacobian_n_n_eb[i][j] += jac_nn * test_i_dS;
5146 elementJacobian_n_w_eb[i][j] += jac_nw * test_i_dS;
5147 }
5148 }
5149 } // kb
5150
5151 // Scatter element-boundary contributions to global storage.
5152 for (int i = 0; i < nDOF_test_element; i++) {
5153 const int eN_i = eN * nDOF_test_element + i;
5154 const int gi = u_l2g.data()[eN_i];
5155 globalResidual.data()[offset_n + stride_n * gi] += elementResidual_n_eb[i];
5156 if (have_gas_budget) {
5157 gas_budget_node.data()[4 * numDOFs_n + gi] += elementResidual_n_eb[i];
5158 gas_budget_node.data()[5 * numDOFs_n + gi] += elementResidual_n_eb[i];
5159 }
5160 for (int j = 0; j < nDOF_trial_element; j++) {
5161 const int ebN_i_j = ebN * 4 * nDOF_test_X_trial_element
5162 + i * nDOF_trial_element + j;
5163 globalJacobian.data()[csrRowIndeces_n_n.data()[eN_i]
5164 + csrColumnOffsets_eb_n_n.data()[ebN_i_j]]
5165 += elementJacobian_n_n_eb[i][j];
5166 globalJacobian.data()[csrRowIndeces_n_w.data()[eN_i]
5167 + csrColumnOffsets_eb_n_w.data()[ebN_i_j]]
5168 += elementJacobian_n_w_eb[i][j];
5169 }
5170 }
5171 } // ebNE
5172 }
5173
5175 {
5176 xt::pyarray<int> &a_rowptr = args.array<int>("a_rowptr");
5177 xt::pyarray<int> &a_colind = args.array<int>("a_colind");
5178 double rho = args.scalar<double>("rho"); // freshwater reference (fallback)
5179 double beta = args.scalar<double>("beta");
5180 xt::pyarray<double> &gravity = args.array<double>("gravity");
5181 xt::pyarray<double> &alpha = args.array<double>("alpha");
5182 xt::pyarray<double> &n = args.array<double>("n");
5183 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
5184 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
5185 xt::pyarray<double> &KWs = args.array<double>("KWs");
5186 xt::pyarray<double> &krn_end = args.array<double>("krn_end");
5187 xt::pyarray<double> &S_gr = args.array<double>("S_gr");
5188 double mu_n = args.scalar<double>("mu_n");
5189 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
5190 xt::pyarray<int> &freeDOFMaterialTypes = args.array<int>("freeDOFMaterialTypes");
5191 int numDOFs = args.scalar<int>("numDOFs");
5192 xt::pyarray<double> &mIn = args.array<double>("limited_solution");
5193 xt::pyarray<double> &pOut = args.array<double>("u_dof");
5194 int USE_NEWTON_INVERT = args.scalar<int>("USE_NEWTON_INVERT");
5195 const int PSK_TYPE = args.scalar<int>("PSK_TYPE");
5196 const int COMPONENT = args.scalar<int>("COMPONENT");
5197
5198 if (COMPONENT == 1) {
5199 // -------- Component-1 inverse (compositional): m_c -> u_n = z. --------
5200 // m_c = (phi*N)*z => z = m_c / (phi*N). rho_n_phi_dof_member caches the
5201 // lumped phi*N built by the residual / MassMatrix. Clamp z to [0,1]
5202 // (overall CO2 mole fraction), NOT a saturation range.
5203 const int numDOFs_u = static_cast<int>(pOut.size());
5204 const bool have_rho_n_phi = (rho_n_phi_dof_member.size() ==
5205 static_cast<std::size_t>(numDOFs_u));
5206 // Fallback divisor (safety net only -- the residual/MassMatrix always
5207 // populate the cache first). A representative aqueous-rich molar density
5208 // keeps the magnitude sane if an orphan node ever reaches here.
5209 const int mat0 = elementMaterialTypes.data()[0];
5210 const double phi_mat = thetaR.data()[mat0] + thetaSR.data()[mat0];
5211 const double phiN_fallback = phi_mat * 5.0e4; // ~brine molar density [mol/m^3]
5212 for (int i = 0; i < numDOFs_u; i++) {
5213 const double phiN_i = have_rho_n_phi ? rho_n_phi_dof_member[i]
5214 : phiN_fallback;
5215 double z = mIn.data()[i] / std::max(phiN_i, 1.0e-16);
5216 // Safety clamp to [0,1]. A correct conservative FCT keeps
5217 // limited_solution_n in-bounds, so this should not trigger; it guards
5218 // against round-off / misconfiguration.
5219 if (z < 0.0) z = 0.0;
5220 if (z > 1.0) z = 1.0;
5221 pOut.data()[i] = z;
5222 }
5223 return;
5224 }
5225
5226 // -------- Component-0 inverse: m_w -> p_w (Richards-style, 2-stage). --------
5227 // m_w = rho_w(p_w) * thetaW(Se(S_n)), rho_w(p_w) = rho * exp(beta*p_w).
5228 // "Ill-posed for p_w ALONE" only means: you cannot recover p_w without S_n.
5229 // But S_n is the OTHER primary variable -- always available -- so:
5230 // stage 1: rho_w = m_w / thetaW(Se(S_n)) (S_n known => thetaW known)
5231 // stage 2: p_w = ln(rho_w / rho) / beta (analytic inverse of EOS)
5232 // Well-posed whenever beta != 0 and thetaW > 0 (always: S_n <= 1-S_wr).
5233 // The (limited) S_n field is passed as "u_dof_n".
5234 if (COMPONENT == 0) {
5235 if (beta == 0.0)
5236 throw std::runtime_error(
5237 "m_comp_co2::invert COMPONENT=0: beta == 0 makes rho_w(p_w) "
5238 "constant, so m_w carries no p_w information. Use beta > 0.");
5239 xt::pyarray<double> &u_dof_n = args.array<double>("u_dof_n");
5240 const int numDOFs_w = static_cast<int>(pOut.size());
5241 // Material 0 fallback -- matches the COMPONENT==1 convention above.
5242 const int mat0 = elementMaterialTypes.data()[0];
5243 const double alpha0 = alpha.data()[mat0];
5244 const double n_vg0 = n.data()[mat0];
5245 const double thetaR0 = thetaR.data()[mat0];
5246 const double thetaSR0 = thetaSR.data()[mat0];
5247 const double S_wr0 = thetaR0 / (thetaR0 + thetaSR0);
5248 const double one_m_Sr = 1.0 - S_wr0;
5249 for (int i = 0; i < numDOFs_w; i++) {
5250 const double S_n = u_dof_n.data()[i];
5251 double Se = (1.0 - S_n - S_wr0) / one_m_Sr;
5252 if (Se < 0.0) Se = 0.0; else if (Se > 1.0) Se = 1.0;
5253 double thetaW, DthetaW_DSe, KWr, DKWr_DSe;
5254 if (PSK_TYPE == 1)
5256 Se, alpha0, n_vg0, thetaR0, thetaSR0, thetaW, DthetaW_DSe, KWr, DKWr_DSe);
5257 else
5259 Se, alpha0, n_vg0, thetaR0, thetaSR0, thetaW, DthetaW_DSe, KWr, DKWr_DSe);
5260 const double rho_w = (thetaW > 1.0e-14) ? mIn.data()[i] / thetaW : rho;
5261 pOut.data()[i] = std::log(std::max(rho_w / rho, 1.0e-300)) / beta;
5262 }
5263 (void)a_rowptr; (void)a_colind; (void)gravity; (void)KWs;
5264 (void)freeDOFMaterialTypes; (void)numDOFs; (void)USE_NEWTON_INVERT;
5265 return;
5266 }
5267
5268 throw std::runtime_error(
5269 "m_comp_co2::invert: COMPONENT must be 0 (m_w -> p_w) or 1 (m_n -> S_n).");
5270 }
5271
5273 {
5274 //element
5275 double dt = args.scalar<double>("dt");
5276 xt::pyarray<double> &mesh_trial_ref = args.array<double>("mesh_trial_ref");
5277 xt::pyarray<double> &mesh_grad_trial_ref = args.array<double>("mesh_grad_trial_ref");
5278 xt::pyarray<double> &mesh_dof = args.array<double>("mesh_dof");
5279 xt::pyarray<double> &mesh_velocity_dof = args.array<double>("mesh_velocity_dof");
5280 double MOVING_DOMAIN = args.scalar<double>("MOVING_DOMAIN");
5281 xt::pyarray<int> &mesh_l2g = args.array<int>("mesh_l2g");
5282 xt::pyarray<double> &dV_ref = args.array<double>("dV_ref");
5283 xt::pyarray<double> &u_trial_ref = args.array<double>("u_trial_ref");
5284 xt::pyarray<double> &u_grad_trial_ref = args.array<double>("u_grad_trial_ref");
5285 xt::pyarray<double> &u_test_ref = args.array<double>("u_test_ref");
5286 xt::pyarray<double> &u_grad_test_ref = args.array<double>("u_grad_test_ref");
5287 //element boundary
5288 xt::pyarray<double> &mesh_trial_trace_ref = args.array<double>("mesh_trial_trace_ref");
5289 xt::pyarray<double> &mesh_grad_trial_trace_ref = args.array<double>("mesh_grad_trial_trace_ref");
5290 xt::pyarray<double> &dS_ref = args.array<double>("dS_ref");
5291 xt::pyarray<double> &u_trial_trace_ref = args.array<double>("u_trial_trace_ref");
5292 xt::pyarray<double> &u_grad_trial_trace_ref = args.array<double>("u_grad_trial_trace_ref");
5293 xt::pyarray<double> &u_test_trace_ref = args.array<double>("u_test_trace_ref");
5294 xt::pyarray<double> &u_grad_test_trace_ref = args.array<double>("u_grad_test_trace_ref");
5295 xt::pyarray<double> &normal_ref = args.array<double>("normal_ref");
5296 xt::pyarray<double> &boundaryJac_ref = args.array<double>("boundaryJac_ref");
5297 //physics
5298 int nElements_global = args.scalar<int>("nElements_global");
5299 //new
5300 xt::pyarray<double> &ebqe_penalty_ext = args.array<double>("ebqe_penalty_ext");
5301 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
5302 xt::pyarray<int> &isSeepageFace = args.array<int>("isSeepageFace");
5303 xt::pyarray<int> &a_rowptr = args.array<int>("a_rowptr");
5304 xt::pyarray<int> &a_colind = args.array<int>("a_colind");
5305 double rho = args.scalar<double>("rho");
5306 double beta = args.scalar<double>("beta");
5307 // gas-phase reference density and reference pressure for the linear EOS
5308 // rho_n_local(p_n) = rho_n * p_n / p_ref_n (p_ref_n>0) or constant rho_n.
5309 const double rho_n_mm = args.scalar<double>("rho_n");
5310 const double p_ref_n = args.scalar<double>("p_ref_n");
5311 const bool rho_n_compressible = (p_ref_n > 0.0);
5312
5313 xt::pyarray<double> &q_rho = args.array<double>("q_rho");
5314
5315 xt::pyarray<double> &gravity = args.array<double>("gravity");
5316 xt::pyarray<double> &alpha = args.array<double>("alpha");
5317 xt::pyarray<double> &n = args.array<double>("n");
5318 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
5319 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
5320 xt::pyarray<double> &KWs = args.array<double>("KWs");
5321 xt::pyarray<double> &krn_end = args.array<double>("krn_end");
5322 xt::pyarray<double> &S_gr = args.array<double>("S_gr");
5323 double mu_n = args.scalar<double>("mu_n");
5324 //end new
5325 double useMetrics = args.scalar<double>("useMetrics");
5326 double alphaBDF = args.scalar<double>("alphaBDF");
5327 int lag_shockCapturing = args.scalar<int>("lag_shockCapturing");
5328 double shockCapturingDiffusion = args.scalar<double>("shockCapturingDiffusion");
5329 xt::pyarray<int> &u_l2g = args.array<int>("u_l2g");
5330 // Node-split comp-1 (z) map -- MUST match the residual's u_l2g_n so the mass /
5331 // (1,1) tangent is assembled on the same split DOFs (== u_l2g off interfaces).
5332 xt::pyarray<int> &u_l2g_n = args.array<int>("u_l2g_n");
5333 xt::pyarray<int> &r_l2g = args.array<int>("r_l2g");
5334 xt::pyarray<double> &elementDiameter = args.array<double>("elementDiameter");
5335 int degree_polynomial = args.scalar<int>("degree_polynomial");
5336 xt::pyarray<double> &u_dof = args.array<double>("u_dof");
5337 // u_dof_n always present in argsDict (getJacobian sets it from
5338 // self.u[1].dof in m_comp_co2.py).
5339 xt::pyarray<double> &u_dof_n = args.array<double>("u_dof_n");
5340 xt::pyarray<double> &velocity = args.array<double>("velocity");
5341 xt::pyarray<double> &q_m_betaBDF = args.array<double>("q_m_betaBDF");
5342 xt::pyarray<double> &cfl = args.array<double>("cfl");
5343 xt::pyarray<double> &q_numDiff_u_last = args.array<double>("q_numDiff_u_last");
5344 xt::pyarray<int> &csrRowIndeces_u_u = args.array<int>("csrRowIndeces_u_u");
5345 xt::pyarray<int> &csrColumnOffsets_u_u = args.array<int>("csrColumnOffsets_u_u");
5346 xt::pyarray<int> &csrRowIndeces_n_n = args.array<int>("csrRowIndeces_n_n");
5347 // (1,0) cross-block CSR maps for the gas-eq diffusion
5348 // against grad u_w. Currently allocated by the framework but unused by
5349 // the C++ assembly - 3c.2/3c.3 will write into them.
5350 xt::pyarray<int> &csrRowIndeces_n_w = args.array<int>("csrRowIndeces_n_w");
5351 xt::pyarray<int> &csrColumnOffsets_n_n = args.array<int>("csrColumnOffsets_n_n");
5352 xt::pyarray<int> &csrColumnOffsets_n_w = args.array<int>("csrColumnOffsets_n_w");
5353 xt::pyarray<double> &globalJacobian = args.array<double>("globalJacobian");
5354 xt::pyarray<double> &delta_x_ij = args.array<double>("delta_x_ij");
5355 int nExteriorElementBoundaries_global = args.scalar<int>("nExteriorElementBoundaries_global");
5356 xt::pyarray<int> &exteriorElementBoundariesArray = args.array<int>("exteriorElementBoundariesArray");
5357 xt::pyarray<int> &elementBoundaryElementsArray = args.array<int>("elementBoundaryElementsArray");
5358 xt::pyarray<int> &elementBoundaryLocalElementBoundariesArray = args.array<int>("elementBoundaryLocalElementBoundariesArray");
5359 xt::pyarray<double> &ebqe_velocity_ext = args.array<double>("ebqe_velocity_ext");
5360 xt::pyarray<int> &isDOFBoundary_u = args.array<int>("isDOFBoundary_u");
5361 xt::pyarray<double> &ebqe_bc_u_ext = args.array<double>("ebqe_bc_u_ext");
5362 // component-1 (S_n) boundary arrays.
5363 xt::pyarray<int> &isDOFBoundary_n = args.array<int>("isDOFBoundary_n");
5364 xt::pyarray<double> &ebqe_bc_u_n_ext = args.array<double>("ebqe_bc_u_n_ext");
5365 xt::pyarray<int> &isFluxBoundary_u = args.array<int>("isFluxBoundary_u");
5366 xt::pyarray<double> &ebqe_bc_flux_ext = args.array<double>("ebqe_bc_flux_ext");
5367 xt::pyarray<int> &csrColumnOffsets_eb_u_u = args.array<int>("csrColumnOffsets_eb_u_u");
5368 int LUMPED_MASS_MATRIX = args.scalar<int>("LUMPED_MASS_MATRIX");
5369 // PSK closure selector for evaluateCoefficients (read from argsDict).
5370 PSK_TYPE_member = args.scalar<int>("PSK_TYPE");
5371 immiscible_member = (args.scalar<int>("immiscible") != 0);
5372 T_C_member = args.scalar<double>("T_C"); // temperature [degC] from input
5373 double Ct_sge = 4.0;
5374 //
5375 //loop over elements to compute volume integrals and load them into the element Jacobians and global Jacobian
5376 //
5377 for (int eN = 0; eN < nElements_global; eN++) {
5378 double elementJacobian_u_u[nDOF_test_element][nDOF_trial_element];
5379 for (int i = 0; i < nDOF_test_element; i++)
5380 for (int j = 0; j < nDOF_trial_element; j++) { elementJacobian_u_u[i][j] = 0.0; }
5381 for (int k = 0; k < nQuadraturePoints_element; k++) {
5382 int eN_k = eN * nQuadraturePoints_element + k, //index to a scalar at a quadrature point
5383 eN_k_nSpace = eN_k * nSpace,
5384 eN_nDOF_trial_element = eN * nDOF_trial_element; //index to a vector at a quadrature point
5385 //declare local storage
5386 double u = 0.0, grad_u[nSpace], m = 0.0, dm = 0.0, f[nSpace], df[nSpace], a[nnz], da[nnz], as[nnz], m_t = 0.0, dm_t = 0.0, dpdeResidual_u_u[nDOF_trial_element], Lstar_u[nDOF_test_element], dsubgridError_u_u[nDOF_trial_element], tau = 0.0, tau0 = 0.0, tau1 = 0.0, jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], u_grad_trial[nDOF_trial_element * nSpace], dV, u_test_dV[nDOF_test_element], u_grad_test_dV[nDOF_test_element * nSpace], x, y, z, xt, yt, zt,
5387 G[nSpace * nSpace], G_dd_G, tr_G;
5388
5389 //get jacobian, etc for mapping reference element
5390 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), mesh_grad_trial_ref.data(), jac, jacDet, jacInv, x, y, z);
5391 ck.calculateMappingVelocity_element(eN, k, mesh_velocity_dof.data(), mesh_l2g.data(), mesh_trial_ref.data(), xt, yt, zt);
5392 //get the physical integration weight
5393 dV = fabs(jacDet) * dV_ref.data()[k];
5394 ck.calculateG(jacInv, G, G_dd_G, tr_G);
5395 //get the trial function gradients
5396 ck.gradTrialFromRef(&u_grad_trial_ref.data()[k * nDOF_trial_element * nSpace], jacInv, u_grad_trial);
5397 //get the solution
5398 ck.valFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], &u_trial_ref.data()[k * nDOF_trial_element], u);
5399 //get the solution gradients
5400 ck.gradFromDOF(u_dof.data(), &u_l2g.data()[eN_nDOF_trial_element], u_grad_trial, grad_u);
5401 //precalculate test function products with integration weights
5402 for (int j = 0; j < nDOF_trial_element; j++) {
5403 u_test_dV[j] = u_test_ref.data()[k * nDOF_trial_element + j] * dV;
5404 for (int I = 0; I < nSpace; I++) {
5405 u_grad_test_dV[j * nSpace + I] = u_grad_trial[j * nSpace + I] * dV; //cek warning won't work for Petrov-Galerkin
5406 }
5407 }
5408 //
5409 //calculate pde coefficients and derivatives at quadrature points
5410 //
5411 double Kr, dKr, thetaW;
5412 //const double rho_local = q_rho.data()[eN_k];
5413 // NOTE: the cek hack at line ~3170 (`dm = 1.0`) and the dm_t=1.0
5414 // override at the Jacobian assembly below make the final mass matrix
5415 // output independent of the coefficients here (result is just
5416 // M = integral N_i N_j dV regardless). Coefficient call is kept for
5417 // moving-mesh / VMS consistency.
5418 double dm_du_n_mm = 0.0, dkr_du_n_mm = 0.0;
5419 double df_du_n_mm[nSpace], da_du_n_mm[nnz];
5420 for (int I = 0; I < nSpace; I++) df_du_n_mm[I] = 0.0;
5421 for (int ii = 0; ii < nnz; ii++) da_du_n_mm[ii] = 0.0;
5422 double u_n_qp = 0.0;
5423 ck.valFromDOF(u_dof_n.data(),
5424 &u_l2g_n.data()[eN_nDOF_trial_element],
5425 &u_trial_ref.data()[k * nDOF_trial_element], u_n_qp);
5426 evaluateCoefficients_from_Se(a_rowptr.data(), a_colind.data(), rho, q_rho.data()[eN_k], beta, gravity.data(),
5427 alpha.data()[elementMaterialTypes.data()[eN]], n.data()[elementMaterialTypes.data()[eN]],
5428 thetaR.data()[elementMaterialTypes.data()[eN]], thetaSR.data()[elementMaterialTypes.data()[eN]],
5429 &KWs.data()[elementMaterialTypes.data()[eN] * nnz], u, u_n_qp,
5430 m, dm, dm_du_n_mm, f, df, df_du_n_mm, a, da, da_du_n_mm,
5431 as, Kr, dKr, dkr_du_n_mm, thetaW);
5432 //
5433 //moving mesh
5434 //
5435 double mesh_velocity[3];
5436 mesh_velocity[0] = xt;
5437 mesh_velocity[1] = yt;
5438 mesh_velocity[2] = zt;
5439 for (int I = 0; I < nSpace; I++) {
5440 f[I] -= MOVING_DOMAIN * m * mesh_velocity[I];
5441 df[I] -= MOVING_DOMAIN * dm * mesh_velocity[I];
5442 }
5443 //
5444 //calculate time derivatives
5445 //
5446 //cek hack
5447 dm = 1.0;
5448 ck.bdf(alphaBDF,
5449 q_m_betaBDF.data()[eN_k], //since m_t isn't used, we don't have to correct mass
5450 m, dm, m_t, dm_t);
5451 //
5452 //calculate subgrid error contribution to the Jacobian (strong residual, adjoint, jacobian of strong residual)
5453 //
5454 //calculate the adjoint times the test functions
5455 for (int i = 0; i < nDOF_test_element; i++) {
5456 int i_nSpace = i * nSpace;
5457 Lstar_u[i] = ck.Advection_adjoint(df, &u_grad_test_dV[i_nSpace]);
5458 }
5459 //calculate the Jacobian of strong residual
5460 for (int j = 0; j < nDOF_trial_element; j++) {
5461 int j_nSpace = j * nSpace;
5462 dpdeResidual_u_u[j] = ck.MassJacobian_strong(dm_t, u_trial_ref.data()[k * nDOF_trial_element + j]) + ck.AdvectionJacobian_strong(df, &u_grad_trial[j_nSpace]);
5463 }
5464 //tau and tau*Res
5465 calculateSubgridError_tau(elementDiameter.data()[eN], dm_t, df, cfl.data()[eN_k], tau0);
5466
5467 calculateSubgridError_tau(Ct_sge, G, dm_t, df, tau1, cfl.data()[eN_k]);
5468 tau = useMetrics * tau1 + (1.0 - useMetrics) * tau0;
5469
5470 for (int j = 0; j < nDOF_trial_element; j++) dsubgridError_u_u[j] = -tau * dpdeResidual_u_u[j];
5471 for (int i = 0; i < nDOF_test_element; i++) {
5472 for (int j = 0; j < nDOF_trial_element; j++) {
5473 if (LUMPED_MASS_MATRIX == 1) {
5474 if (i == j) elementJacobian_u_u[i][j] += u_test_dV[i];
5475 } else {
5476 int j_nSpace = j * nSpace;
5477 int i_nSpace = i * nSpace;
5478 dm_t = 1.0; //we are solving for continuum density explicitly
5479 elementJacobian_u_u[i][j] += ck.MassJacobian_weak(dm_t, u_trial_ref.data()[k * nDOF_trial_element + j], u_test_dV[i]);
5480 }
5481 } //j
5482 } //i
5483 } //k
5484 //
5485 //load into element Jacobian into global Jacobian
5486 //
5487 for (int i = 0; i < nDOF_test_element; i++) {
5488 int eN_i = eN * nDOF_test_element + i;
5489 int I = u_l2g.data()[eN_i];
5490 for (int j = 0; j < nDOF_trial_element; j++) {
5491 int eN_i_j = eN_i * nDOF_trial_element + j;
5492 int J = u_l2g.data()[eN * nDOF_trial_element + j];
5493 //globalJacobian.data()[csrRowIndeces_u_u.data()[eN_i] + csrColumnOffsets_u_u.data()[eN_i_j]] += elementJacobian_u_u[i][j];
5494 delta_x_ij.data()[3 * (csrRowIndeces_u_u.data()[eN_i] + csrColumnOffsets_u_u.data()[eN_i_j]) + 0] = mesh_dof.data()[I * 3 + 0] - mesh_dof.data()[J * 3 + 0];
5495 delta_x_ij.data()[3 * (csrRowIndeces_u_u.data()[eN_i] + csrColumnOffsets_u_u.data()[eN_i_j]) + 1] = mesh_dof.data()[I * 3 + 1] - mesh_dof.data()[J * 3 + 1];
5496 delta_x_ij.data()[3 * (csrRowIndeces_u_u.data()[eN_i] + csrColumnOffsets_u_u.data()[eN_i_j]) + 2] = mesh_dof.data()[I * 3 + 2] - mesh_dof.data()[J * 3 + 2];
5497 } //j
5498 } //i
5499 } //elements
5500 // (1,1) block: unit consistent mass / dt. Used by the time integrator.
5501 // Also refresh the cached nodal phi*rho_n so invert(COMPONENT=1) sees the
5502 // correct projection if mass-matrix assembly runs more recently than the
5503 // residual.
5504 const int numDOFs_u_mm = static_cast<int>(u_dof.size());
5505 // Node-split: these are indexed by the comp-1 split DOF (u_l2g_n), so they MUST
5506 // be sized by the comp-1 DOF count (= u_dof_n.size(), the split count), NOT the
5507 // comp-0/mesh-node count u_dof.size() -- otherwise the projection writes past the
5508 // array end (heap corruption). Off interfaces the two counts coincide.
5509 const int numDOFs_n_mm = static_cast<int>(u_dof_n.size());
5510 std::vector<double> rho_n_phi_mm(numDOFs_n_mm, 0.0);
5511 std::vector<double> ML_n_mm(numDOFs_n_mm, 0.0);
5512 for (int eN = 0; eN < nElements_global; eN++) {
5513 double elementJacobian_n_n[nDOF_test_element][nDOF_trial_element];
5514 for (int i = 0; i < nDOF_test_element; i++)
5515 for (int j = 0; j < nDOF_trial_element; j++) { elementJacobian_n_n[i][j] = 0.0; }
5516 const int mat_eN_mm = elementMaterialTypes.data()[eN];
5517 const double phi_eN_mm = thetaR.data()[mat_eN_mm] + thetaSR.data()[mat_eN_mm];
5518 const double alpha_eN_mm = alpha.data()[mat_eN_mm];
5519 const double n_vg_eN_mm = n.data()[mat_eN_mm];
5520 const double S_wr_mm = thetaR.data()[mat_eN_mm] / phi_eN_mm;
5521 const double one_m_Sr_mm = 1.0 - S_wr_mm;
5522 for (int k = 0; k < nQuadraturePoints_element; k++) {
5523 const int eN_nDOF_trial_element = eN * nDOF_trial_element;
5524 double jac[nSpace * nSpace], jacDet, jacInv[nSpace * nSpace], x, y, z;
5525 ck.calculateMapping_element(eN, k, mesh_dof.data(), mesh_l2g.data(),
5526 mesh_trial_ref.data(), mesh_grad_trial_ref.data(),
5527 jac, jacDet, jacInv, x, y, z);
5528 const double dV = fabs(jacDet) * dV_ref.data()[k];
5529 // P1 (compositional): project phi*N (total molar density * porosity) from
5530 // the flash, IDENTICAL to the residual's rho_n_phi_dof, so invert(COMP=1)
5531 // recovers z = m_c/(phi*N) consistently. N = rho_g*S_g + rho_a*S_a.
5532 double u_w_p = 0.0, u_n_p = 0.0;
5533 ck.valFromDOF(u_dof.data(),
5534 &u_l2g.data()[eN_nDOF_trial_element],
5535 &u_trial_ref.data()[k * nDOF_trial_element], u_w_p);
5536 ck.valFromDOF(u_dof_n.data(),
5537 &u_l2g_n.data()[eN_nDOF_trial_element],
5538 &u_trial_ref.data()[k * nDOF_trial_element], u_n_p);
5539 const double z_cl_mm = fmin(fmax(u_n_p, 1.0e-8), 1.0 - 1.0e-8);
5540 const double p_cl_mm = fmax(u_w_p, 1.0e2);
5543 const double phi_rho_n_qp = phi_eN_mm * (fs_mm.rho_g*fs_mm.S_g
5544 + fs_mm.rho_a*(1.0 - fs_mm.S_g));
5545 for (int i = 0; i < nDOF_test_element; i++) {
5546 const double test_i = u_test_ref.data()[k * nDOF_test_element + i];
5547 const int gi = u_l2g_n.data()[eN * nDOF_test_element + i]; // comp-1 split DOF
5548 rho_n_phi_mm[gi] += phi_rho_n_qp * test_i * dV;
5549 ML_n_mm[gi] += test_i * dV;
5550 for (int j = 0; j < nDOF_trial_element; j++) {
5551 const double trial_j = u_trial_ref.data()[k * nDOF_trial_element + j];
5552 elementJacobian_n_n[i][j] += (test_i * trial_j * dV) / dt;
5553 }
5554 }
5555 }
5556 for (int i = 0; i < nDOF_test_element; i++) {
5557 const int eN_i = eN * nDOF_test_element + i;
5558 for (int j = 0; j < nDOF_trial_element; j++) {
5559 const int eN_i_j = eN_i * nDOF_trial_element + j;
5560 const int full_offset = csrRowIndeces_n_n.data()[eN_i] + csrColumnOffsets_n_n.data()[eN_i_j];
5561 if (std::fabs(globalJacobian.data()[full_offset]) < 1.0e-14)
5562 globalJacobian.data()[full_offset] += elementJacobian_n_n[i][j];
5563 }
5564 }
5565 }
5566 for (int i = 0; i < numDOFs_n_mm; ++i) {
5567 if (ML_n_mm[i] > 0.0) rho_n_phi_mm[i] /= ML_n_mm[i];
5568 else rho_n_phi_mm[i] = thetaR.data()[0] + thetaSR.data()[0];
5569 rho_n_phi_mm[i] = std::max(rho_n_phi_mm[i], 1.0e-16);
5570 }
5571 rho_n_phi_dof_member = rho_n_phi_mm;
5572 } //computeMassMatrix
5573
5575 {
5576 xt::pyarray<double> &c_dof = args.array<double>("c_dof"); // c (in/out)
5577 xt::pyarray<double> &Sn_dof = args.array<double>("Sn_dof"); // S_n (in/out)
5578 const double rho_n = args.scalar<double>("rho_n");
5579 const double X_sat = args.scalar<double>("X_sat");
5580 const double c_sat = args.scalar<double>("c_sat");
5581 const double k_d = args.scalar<double>("k_d"); // dissolution rate [1/time]
5582 const double dt = args.scalar<double>("dt"); // step size
5583 const int numDOFs = args.scalar<int>("numDOFs");
5584 if (X_sat <= 0.0 || k_d <= 0.0)
5585 return; // dissolution disabled
5586 const double a = X_sat / c_sat; // dissolved CO2 per unit c
5587 for (int i = 0; i < numDOFs; i++)
5588 {
5589 const double Sn = Sn_dof.data()[i];
5590 const double c = c_dof.data()[i];
5591 if (Sn > 0.0 && c < c_sat)
5592 {
5593 const double M = rho_n * Sn + a * (1.0 - Sn) * c;
5594 const double c_eq = (M <= X_sat) ? (M / a) : c_sat; // gas-limited; in [c, c_sat]
5595 const double r = k_d * Sn * dt;
5596 const double frac = (r > 0.0) ? r / (1.0 + r) : 0.0;
5597 const double c_new = c + frac * (c_eq - c); // in [c, c_eq]
5598 const double denom = rho_n - a * c_new; // > 0 since a*c_new <= X_sat < rho_n
5599 double Sn_new = (denom != 0.0) ? (M - a * c_new) / denom : Sn;
5600 if (Sn_new < 0.0) Sn_new = 0.0; // safety (denom>0 keeps it >=0)
5601 c_dof.data()[i] = c_new;
5602 Sn_dof.data()[i] = Sn_new;
5603 }
5604 }
5605 // --- PART 2: rebuild TADR's quadrature old-mass from the flashed fields ---
5606 xt::pyarray<double> &q_m_tadr = args.array<double>("q_m_tadr"); // TADR q[('m',0)] (out)
5607 xt::pyarray<int> &u_l2g = args.array<int>("u_l2g");
5608 xt::pyarray<double> &u_trial_ref = args.array<double>("u_trial_ref");
5609 xt::pyarray<int> &elementMaterialTypes = args.array<int>("elementMaterialTypes");
5610 xt::pyarray<double> &thetaR = args.array<double>("thetaR");
5611 xt::pyarray<double> &thetaSR = args.array<double>("thetaSR");
5612 const double rho_f = args.scalar<double>("rho_f");
5613 const double rho_s = args.scalar<double>("rho_s");
5614 const int nElements_global = args.scalar<int>("nElements_global");
5615 // Renamed (nQP_flash / nDOF_flash) to avoid shadowing the struct's
5616 // template parameters nQuadraturePoints_element / nDOF_trial_element.
5617 // These come from Python (derived from q[('m',0)].shape and the l2g),
5618 // so the q_m_tadr write stays in bounds.
5619 const int nQP_flash = args.scalar<int>("nQuadraturePoints_element");
5620 const int nDOF_flash = args.scalar<int>("nDOF_trial_element");
5621 const double eps = (rho_f != 0.0) ? (rho_s - rho_f) / rho_f : 0.0;
5622 for (int eN = 0; eN < nElements_global; eN++)
5623 {
5624 const int mat = elementMaterialTypes.data()[eN];
5625 const double phi_eN = thetaR.data()[mat] + thetaSR.data()[mat];
5626 const int eN_nDOF = eN * nDOF_flash;
5627 for (int k = 0; k < nQP_flash; k++)
5628 {
5629 double c_k = 0.0, Sn_k = 0.0;
5630 ck.valFromDOF(c_dof.data(), &u_l2g.data()[eN_nDOF],
5631 &u_trial_ref.data()[k * nDOF_flash], c_k);
5632 ck.valFromDOF(Sn_dof.data(), &u_l2g.data()[eN_nDOF],
5633 &u_trial_ref.data()[k * nDOF_flash], Sn_k);
5634 const double thetaW = phi_eN * (1.0 - Sn_k);
5635 const double rho = rho_f * (1.0 + eps * c_k);
5636 q_m_tadr.data()[eN * nQP_flash + k] = thetaW * rho * c_k;
5637 }
5638 }
5639 } //dissolutionFlash
5640
5641 // ---- Post-step derived-field export ---------------------------------------
5642 // Per-node value-only flash of the PRIMARY (p,z) state into the three
5643 // human-facing compositional fields that the archiver writes to the XDMF:
5644 // Sg = free-gas saturation (FlashState.S_g)
5645 // X = CO2 mole fraction dissolved in brine (FlashState.X)
5646 // c = brine CO2 MASS concentration [kg/m^3] = rho_a * X * M_CO2
5648 {
5649 xt::pyarray<double> &p_dof = args.array<double>("p_dof"); // comp-0 (pressure) [Pa]
5650 xt::pyarray<double> &z_dof = args.array<double>("z_dof"); // comp-1 (overall CO2 mole frac)
5651 xt::pyarray<double> &Sg_dof = args.array<double>("Sg_dof"); // out: gas saturation
5652 xt::pyarray<double> &X_dof = args.array<double>("X_dof"); // out: CO2 mole frac in brine
5653 xt::pyarray<double> &c_dof = args.array<double>("c_dof"); // out: brine CO2 mass conc [kg/m^3]
5654 const int numDOFs = args.scalar<int>("numDOFs");
5655 xt::pyarray<int> &node2zdof = args.array<int>("node2zdof");
5656 immiscible_member = (args.scalar<int>("immiscible") != 0);
5657 T_C_member = args.scalar<double>("T_C"); // temperature [degC] from input
5658 const double M_CO2 = 0.04401; // CO2 molar mass [kg/mol]
5659 for (int i = 0; i < numDOFs; i++)
5660 {
5661 const double p_i = (p_dof.data()[i] > 1.0e2) ? p_dof.data()[i] : 1.0e2;
5662 double z_i = z_dof.data()[node2zdof.data()[i]]; // split z-DOF for mesh node i
5663 if (z_i < 1.0e-12) z_i = 1.0e-12; // match plot-side clamp
5664 if (z_i > 1.0 - 1.0e-12) z_i = 1.0 - 1.0e-12;
5669 Sg_dof.data()[i] = fs.S_g;
5670 X_dof.data()[i] = (fs.X > 0.0) ? fs.X : 0.0;
5671 c_dof.data()[i] = (fs.rho_a * fs.X > 0.0) ? fs.rho_a * fs.X * M_CO2 : 0.0;
5672 }
5673 } //calculateFlashFields
5674}; //M_comp_co2
5675
5676inline M_comp_co2_base *newm_comp_co2(int nSpaceIn, int nQuadraturePoints_elementIn, int nDOF_mesh_trial_elementIn, int nDOF_trial_elementIn, int nDOF_test_elementIn, int nQuadraturePoints_elementBoundaryIn, int CompKernelFlag)
5677{
5678 if (nSpaceIn == 1)
5679 return proteus::chooseAndAllocateDiscretization1D<M_comp_co2_base, M_comp_co2, CompKernel>(nSpaceIn, nQuadraturePoints_elementIn, nDOF_mesh_trial_elementIn, nDOF_trial_elementIn, nDOF_test_elementIn, nQuadraturePoints_elementBoundaryIn, CompKernelFlag);
5680 else if (nSpaceIn == 2)
5681 return proteus::chooseAndAllocateDiscretization2D<M_comp_co2_base, M_comp_co2, CompKernel>(nSpaceIn, nQuadraturePoints_elementIn, nDOF_mesh_trial_elementIn, nDOF_trial_elementIn, nDOF_test_elementIn, nQuadraturePoints_elementBoundaryIn, CompKernelFlag);
5682 else {
5683 assert(nSpaceIn == 3);
5684 return proteus::chooseAndAllocateDiscretization<M_comp_co2_base, M_comp_co2, CompKernel>(nSpaceIn, nQuadraturePoints_elementIn, nDOF_mesh_trial_elementIn, nDOF_trial_elementIn, nDOF_test_elementIn, nQuadraturePoints_elementBoundaryIn, CompKernelFlag);
5685 }
5686}
5687} // namespace m_comp_co2
5688} // namespace proteus
5689#endif
Int n
Definition Headers.h:28
Double r
Definition Headers.h:83
Double s
Definition Headers.h:84
Double u
Definition Headers.h:89
Int num
Definition Headers.h:32
Double * z
Definition Headers.h:49
Double v
Definition Headers.h:95
Double psi
Definition Headers.h:78
#define cE
Definition NCLS3P.h:10
virtual void calculateResidual_entropy_viscosity(arguments_dict &args)=0
virtual void calculateMassMatrix(arguments_dict &args)=0
virtual void FCTStep(arguments_dict &args)=0
virtual void calculateJacobian(arguments_dict &args)=0
virtual void dissolutionFlash(arguments_dict &args)=0
virtual void calculateFlashFields(arguments_dict &args)=0
virtual void kth_FCT_step(arguments_dict &args)=0
virtual void calculateResidual(arguments_dict &args)=0
virtual void invert(arguments_dict &args)=0
void calculateResidual_entropy_viscosity(arguments_dict &args)
std::vector< double > rho_n_phi_dof_member
Definition m_comp_co2.h:82
void calculateJacobian(arguments_dict &args)
void calculateFlashFields(arguments_dict &args)
void exteriorNumericalFlux(const double &bc_flux, int rowptr[nSpace], int colind[nnz], int isSeepageFace, int &isDOFBoundary, double n[nSpace], double bc_u, double K[nnz], double grad_psi[nSpace], double u, double K_rho_g[nSpace], double penalty, double &flux)
Definition m_comp_co2.h:336
void calculateNumericalDiffusion(const double &shockCapturingDiffusion, const double &elementDiameter, const double &strong_residual, const double grad_u[nSpace], double &numDiff)
Definition m_comp_co2.h:325
Comp1Closure evaluateCoefficients_comp1(const double alpha, const double n_vg, const double thetaR, const double thetaSR, const double krn_end, const double mu_n, const double u_w, const double u_n)
Definition m_comp_co2.h:224
void evaluateCoefficients_from_Se(const int rowptr[nSpace], const int colind[nnz], const double rho0, const double rho_transport, const double beta, const double gravity[nSpace], const double alpha, const double n_vg, const double thetaR, const double thetaSR, const double KWs[nnz], const double &u_w, const double &u_n, double &m, double &dm_du_w, double &dm_du_n, double f[nSpace], double df_du_w[nSpace], double df_du_n[nSpace], double a[nnz], double da_du_w[nnz], double da_du_n[nnz], double as[nnz], double &kr, double &dkr_du_w, double &dkr_du_n, double &thetaW_out)
Definition m_comp_co2.h:99
void invert(arguments_dict &args)
void calculateSubgridError_tau(const double &Ct_sge, const double G[nSpace *nSpace], const double &A0, const double Ai[nSpace], double &tau_v, double &cfl)
Definition m_comp_co2.h:317
void exteriorNumericalFluxJacobian(const int rowptr[nSpace], const int colind[nnz], const int isDOFBoundary, const double n[nSpace], const double K[nnz], const double dK[nnz], const double grad_psi[nSpace], const double grad_v[nSpace], const double dK_rho_g[nSpace], const double v, const double penalty, double &fluxJacobian)
Definition m_comp_co2.h:363
void calculateCFL(const double &elementDiameter, const double df[nSpace], double &cfl)
Definition m_comp_co2.h:295
void dissolutionFlash(arguments_dict &args)
void calculateMassMatrix(arguments_dict &args)
void exteriorNumericalFluxJacobian2(const int rowptr[nSpace], const int colind[nnz], const int isDOFBoundary, const double n[nSpace], const double Ks[nnz], const double K[nnz], const double dK[nnz], const double grad_psi[nSpace], const double grad_v[nSpace], const double dK_rho_g[nSpace], const double v, const double penalty, double &fluxJacobian, double &bfluxJacobian)
Definition m_comp_co2.h:409
void FCTStep(arguments_dict &args)
void calculateResidual(arguments_dict &args)
Definition m_comp_co2.h:433
std::vector< double > rho_dof_member
Definition m_comp_co2.h:81
void exteriorNumericalFlux2(const double &bc_flux, int rowptr[nSpace], int colind[nnz], int isSeepageFace, int &isDOFBoundary, double n[nSpace], double bc_u, double K[nnz], double grad_psi[nSpace], double u, double K_rho_g[nSpace], double penalty, double &flux, double &bflux)
Definition m_comp_co2.h:378
void calculateSubgridError_tau(const double &elementDiameter, const double &dmt, const double dH[nSpace], double &cfl, double &tau)
Definition m_comp_co2.h:305
void kth_FCT_step(arguments_dict &args)
double seepagefluxcalculator(double anb_seepage_flux, int isSeepageFace, double dS, double flux_ext)
Definition m_comp_co2.h:427
double df(double C, double b, double a, int q, int r)
#define c(i)
Definition jf.h:21
#define w(x)
Definition jf.h:22
#define POWER_SMOOTHNESS_INDICATOR
Definition m_comp_co2.h:22
#define nnz
Definition m_comp_co2.h:19
#define GLOBAL_FCT
Definition m_comp_co2.h:24
#define IS_BETAij_ONE
Definition m_comp_co2.h:23
constexpr double M_H2O_KG
constexpr double M_CO2_KG
FlashState flashPZ(double p_Pa, double z, double T_C, double=0.0, double eps=EPS_Z, bool immiscible=false)
constexpr double EPS_Z
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)
double ENTROPY(const double &phi, const double &phiL, const double &phiR)
Definition m_comp_co2.h:38
double DENTROPY(const double &phi, const double &phiL, const double &phiR)
Definition m_comp_co2.h:42
M_comp_co2_base * newm_comp_co2(int nSpaceIn, int nQuadraturePoints_elementIn, int nDOF_mesh_trial_elementIn, int nDOF_trial_elementIn, int nDOF_test_elementIn, int nQuadraturePoints_elementBoundaryIn, int CompKernelFlag)
double DENTROPY_LOG(const double &phi, const double &phiL, const double &phiR)
Definition m_comp_co2.h:51
double ENTROPY_LOG(const double &phi, const double &phiL, const double &phiR)
Definition m_comp_co2.h:47
Definition ADR.h:19
double phi(const double &g, const double &h, const double &hL, const double &hR, const double &uL, const double &uR)
Definition SW2DCV.h:62
Model_Base * chooseAndAllocateDiscretization(int nSpaceIn, int nQuadraturePoints_elementIn, int nDOF_mesh_trial_elementIn, int nDOF_trial_elementIn, int nDOF_test_elementIn, int nDOF_v_trial_elementIn, int nDOF_v_test_elementIn, int nQuadraturePoints_elementBoundaryIn, int CompKernelFlag)
Model_Base * chooseAndAllocateDiscretization1D(int nSpaceIn, int nQuadraturePoints_elementIn, int nDOF_mesh_trial_elementIn, int nDOF_trial_elementIn, int nDOF_test_elementIn, int nQuadraturePoints_elementBoundaryIn, int CompKernelFlag)
Model_Base * chooseAndAllocateDiscretization2D(int nSpaceIn, int nQuadraturePoints_elementIn, int nDOF_mesh_trial_elementIn, int nDOF_trial_elementIn, int nDOF_test_elementIn, int nDOF_v_trial_elementIn, int nDOF_v_test_elementIn, int nQuadraturePoints_elementBoundaryIn, int CompKernelFlag)
double f(const double &g, const double &h, const double &hZ)
Definition SW2DCV.h:58
Richards free-function PSK closures (moved here from proteus/richards/psk_models.h).
T & scalar(const std::string &key)
xt::pyarray< T > & array(const std::string &key)