OGS
WellboreSimulatorFEM-impl.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
2// SPDX-License-Identifier: BSD-3-Clause
3
4#pragma once
5
6#include <spdlog/fmt/fmt.h>
7
8#include <Eigen/Dense>
9#include <cmath>
10#include <optional>
11#include <vector>
12
19#include "NumLib/Exceptions.h"
25
26namespace ProcessLib
27{
28namespace WellboreSimulator
29{
30template <typename ShapeFunction, int GlobalDim>
32 double const t, double const dt, std::vector<double> const& local_x,
33 std::vector<double> const& local_x_prev, std::vector<double>& local_M_data,
34 std::vector<double>& local_K_data, std::vector<double>& local_b_data)
35{
36 auto const local_matrix_size = local_x.size();
37
38 assert(local_matrix_size == ShapeFunction::NPOINTS * NUM_NODAL_DOF);
39
41 local_M_data, local_matrix_size, local_matrix_size);
43 local_K_data, local_matrix_size, local_matrix_size);
45 local_b_data, local_matrix_size);
46
47 // Get block matrices
48 auto Mvv = local_M.template block<velocity_size, velocity_size>(
50
51 auto Mhp = local_M.template block<enthalpy_size, pressure_size>(
53 auto Mhh = local_M.template block<enthalpy_size, enthalpy_size>(
55
56 auto Kpv = local_K.template block<pressure_size, velocity_size>(
58
59 auto Kvp = local_K.template block<velocity_size, pressure_size>(
61 auto Kvv = local_K.template block<velocity_size, velocity_size>(
63
64 auto Khh = local_K.template block<enthalpy_size, enthalpy_size>(
66
67 auto Bp = local_b.template segment<pressure_size>(pressure_index);
68 auto Bv = local_b.template segment<velocity_size>(velocity_index);
69 auto Bh = local_b.template segment<enthalpy_size>(enthalpy_index);
70
71 unsigned const n_integration_points =
72 _integration_method.getNumberOfPoints();
73
75 pos.setElementID(_element.getID());
76
77 auto const& b = _process_data.specific_body_force;
78
80
81 // Get material properties
82 auto const& medium = *_process_data.media_map.getMedium(_element.getID());
83 auto const& liquid_phase =
85 auto const& gas_phase = medium.phase(MaterialPropertyLib::PhaseName::Gas);
86
87 // Get wellbore parameters
88 // casing thickness
89 auto const t_ca = _process_data.wellbore.casing_thickness(t, pos)[0];
90 // wellbore radius
91 auto const r_w = _process_data.wellbore.diameter(t, pos)[0] / 2;
92
93 // pipe thickness
94 auto const t_p = _process_data.wellbore.pipe_thickness(t, pos)[0];
95
96 // roughness of the wellbore
97 auto const xi = _process_data.wellbore.roughness(t, pos)[0];
98 // pipe outer radius
99 auto const r_o = r_w - t_ca;
100 // pipe inner radius
101 auto const r_i = r_o - t_p;
102
103 // get reservoir properties
104 NodalVectorType T_r =
105 _process_data.reservoir_properties.temperature.getNodalValuesOnElement(
106 _element, t);
107 NodalVectorType p_r =
108 _process_data.reservoir_properties.pressure.getNodalValuesOnElement(
109 _element, t);
111 _process_data.productivity_index.getNodalValuesOnElement(_element, t);
112 auto const k_r =
113 _process_data.reservoir_properties.thermal_conductivity(t, pos)[0];
114 auto const rho_r = _process_data.reservoir_properties.density(t, pos)[0];
115 auto const c_r =
116 _process_data.reservoir_properties.specific_heat_capacity(t, pos)[0];
117
118 for (unsigned ip(0); ip < n_integration_points; ip++)
119 {
120 auto& ip_data = _ip_data[ip];
121 auto const& N = ip_data.N;
122 auto const& dNdx = ip_data.dNdx;
123 auto const& w = ip_data.integration_weight;
124 auto& mix_density = ip_data.mix_density;
125 auto& temperature = ip_data.temperature;
126 auto& steam_mass_frac = ip_data.dryness;
127 auto& vapor_volume_frac = ip_data.vapor_volume_fraction;
128 auto& vapor_mass_flowrate = ip_data.vapor_mass_flow_rate;
129 auto& liquid_mass_flowrate = ip_data.liquid_mass_flow_rate;
130
131 pos = {
132 std::nullopt, _element.getID(),
135 _element, N))};
136
137 double p_int_pt = 0.0;
138 double v_int_pt = 0.0;
139 double h_int_pt = 0.0;
140
141 NumLib::shapeFunctionInterpolate(local_x, N, p_int_pt, v_int_pt,
142 h_int_pt);
143
144 double p_prev_int_pt = 0.0;
145 double v_prev_int_pt = 0.0;
146 double h_prev_int_pt = 0.0;
147
148 NumLib::shapeFunctionInterpolate(local_x_prev, N, p_prev_int_pt,
149 v_prev_int_pt, h_prev_int_pt);
150
151 double vdot_int_pt = (v_int_pt - v_prev_int_pt) / dt;
152
153 // calculate fluid properties
154
155 const double pi = std::numbers::pi;
156
157 vars.liquid_phase_pressure = p_int_pt;
158 vars.enthalpy = h_int_pt;
159
160 // Above the critical pressure the region 4 saturation line ends, so
161 // there is no two-phase state to describe and the saturation
162 // properties are not evaluated at all: they would be extrapolated,
163 // and the closure they feed has no admissible void fraction there.
164 // Such a section is compressed liquid, which the region 1 properties
165 // of the liquid phase describe, and it is solved as one. Below the
166 // lower bound of the saturation line there is no such fall-back - the
167 // water is vapour, which this process has no properties for - so the
168 // range check of the saturation properties aborts the assembly as
169 // before.
170 double dryness = 0.;
171 double T_int_pt = 0.;
172 double liquid_water_density = 0.;
173 double vapour_water_density = 0.;
174 double alpha = 0.;
175 std::optional<MaterialPropertyLib::DriftFluxState> drift_flux_state;
176
177 if (p_int_pt >
179 {
180 T_int_pt =
181 liquid_phase
183 .template value<double>(vars, pos, t, dt);
184 vars.temperature = T_int_pt;
185
187 p_int_pt, T_int_pt,
188 "the compressed liquid state of the WellboreSimulator "
189 "process");
190
191 liquid_water_density =
192 liquid_phase
194 .template value<double>(vars, pos, t, dt);
195 }
196 else
197 {
198 liquid_water_density =
199 liquid_phase
200 .property(
202 .template value<double>(vars, pos, t, dt);
203 vapour_water_density =
204 gas_phase
205 .property(
207 .template value<double>(vars, pos, t, dt);
208
209 double const h_sat_liq_w =
210 liquid_phase
211 .property(
213 .template value<double>(vars, pos, t, dt);
214 double const h_sat_vap_w =
215 gas_phase
216 .property(
218 .template value<double>(vars, pos, t, dt);
219
220 dryness = MaterialPropertyLib::steamDryness(h_int_pt, h_sat_liq_w,
221 h_sat_vap_w);
222
223 T_int_pt =
224 (dryness == 0)
225 ? liquid_phase
226 .property(
228 .template value<double>(vars, pos, t, dt)
229 : gas_phase
231 saturation_temperature)
232 .template value<double>(vars, pos, t, dt);
233 vars.temperature = T_int_pt;
234
235 // For the calculation of the void fraction of vapour,
236 // see Rohuani, Z., and E. Axelsson. "Calculation of volume void
237 // fraction in a subcooled and quality region." International
238 // Journal of Heat and Mass Transfer 17 (1970): 383-393.
239
240 // The drift is aligned with the mixture flow so that the closure
241 // below and the slip momentum term further down are consistent,
242 // see MaterialPropertyLib::alignedDriftFluxVelocity().
243 drift_flux_state = MaterialPropertyLib::driftFluxState(
244 dryness, T_int_pt, vapour_water_density, liquid_water_density,
245 v_int_pt);
246
247 // solving void fraction of vapour: Rouhani-Axelsson
248 auto const alpha_solution =
250 *drift_flux_state);
251
252 if (!alpha_solution)
253 {
254 throw NumLib::AssemblyException(fmt::format(
255 "The drift-flux closure of the WellboreSimulator process "
256 "has no admissible vapour void fraction in element {:d}, "
257 "integration point {:d}: pressure {:g} Pa, mixture "
258 "velocity {:g} m/s, specific enthalpy {:g} J/kg, "
259 "temperature {:g} K, {}",
260 _element.getID(), ip, p_int_pt, v_int_pt, h_int_pt,
261 T_int_pt,
263 *drift_flux_state)));
264 }
265
266 alpha = *alpha_solution;
267
268 if (alpha == 0)
269 {
270 liquid_water_density =
271 liquid_phase
273 .template value<double>(vars, pos, t, dt);
274 }
275 }
276
277 steam_mass_frac = dryness;
278 temperature = T_int_pt;
279 vapor_volume_frac = alpha;
280
281 mix_density =
282 vapour_water_density * alpha + liquid_water_density * (1 - alpha);
283
284 auto& mix_density_prev = ip_data.mix_density_prev;
285 vars.density = mix_density;
286
287 auto const rho_dot = (mix_density - mix_density_prev) / dt;
288
289 double const liquid_water_velocity_act =
290 (alpha == 0) ? v_int_pt
291 : (alpha == 1) ? 0
292 : (1 - dryness) * mix_density * v_int_pt /
293 (1 - alpha) / liquid_water_density;
294 double const vapor_water_velocity_act =
295 (alpha == 0) ? 0
296 : dryness * mix_density * v_int_pt /
297 (alpha * vapour_water_density);
298
299 vapor_mass_flowrate = vapor_water_velocity_act * vapour_water_density *
300 pi * r_i * r_i * alpha;
301
302 liquid_mass_flowrate = liquid_water_velocity_act *
303 liquid_water_density * pi * r_i * r_i *
304 (1 - alpha);
305
306 double const gamma = drift_flux_state
308 alpha, *drift_flux_state)
309 : 0.;
310
311 double const miu =
313 .template value<double>(vars, pos, t, dt);
314 double const Re = mix_density * v_int_pt * 2 * r_i / miu;
315
316 // Wall friction coefficient,
317 // Musivand Arzanfudi, Mehdi, and Rafid Al‐Khoury. "A compressible
318 // two‐fluid multiphase model for CO2 leakage through a wellbore."
319 // International Journal for Numerical Methods in Fluids 77.8 (2015):
320 // 477-507.
321 double f = 0.0;
322 if (Re > 10 && Re <= 2400)
323 {
324 f = 16 / Re;
325 }
326 else if (Re > 2400)
327 {
328 f = std::pow(std::log(xi / 3.7 / r_i) -
329 5.02 / Re * std::log(xi / 3.7 / r_i + 13 / Re),
330 -2) /
331 16;
332 }
333
334 double Q_hx = 0;
335 double const T_r_int_pt = N.dot(T_r);
336 // conductive heat exchange between wellbore and formation
337 if (_process_data.has_heat_exchange_with_formation)
338 {
339 // See Zhang, Pan, Pruess, Finsterle (2011). A time-convolution
340 // approach for modeling heat exchange between a wellbore and
341 // surrounding formation. Geothermics 40, 261-266.
342 const double alpha_r = k_r / rho_r / c_r;
343 const double t_d = alpha_r * t / (r_i * r_i);
344
345 double beta;
346 if (t_d < 2.8)
347 {
348 beta = 1 / std::sqrt(pi * t_d) + 0.5 -
349 0.25 * std::sqrt(t_d / pi) + 0.125 * t_d;
350 }
351 else
352 {
353 beta = 2 * (1 / (std::log(4 * t_d) - 2 * 0.57722) -
354 0.57722 /
355 std::pow((std::log(4 * t_d) - 2 * 0.57722), 2));
356 }
357
358 const double P_c = 2 * pi * r_i;
359 Q_hx = P_c * k_r * (T_r_int_pt - T_int_pt) / r_i * beta;
360 }
361
362 // mass exchange with reservoir
363 double const p_r_int_pt = N.dot(p_r);
364 double const PI_int_pt = N.dot(PI);
365 double Q_mx = PI_int_pt * (p_int_pt - p_r_int_pt);
366
367 // advective momentum and energy exchange with reservoir due to the mass
368 // exchange
369 double Q_mom = 0;
370 double Q_ene = 0;
371 if (Q_mx != 0)
372 {
373 Q_mom = Q_mx * v_int_pt;
374 // Only single-phase liquid condition is considered now
375 // TODO: update the two-phase flowing enthalpy from the feed zone.
376 vars.liquid_phase_pressure = p_r_int_pt;
377 vars.temperature = T_r_int_pt;
378 double const h_fres =
379 liquid_phase
381 .template value<double>(vars, pos, t, dt);
382 Q_ene = Q_mx * h_fres;
383 }
384
385 // M matrix assembly
386 Mvv.noalias() += w * N.transpose() * mix_density * N;
387
388 Mhp.noalias() += -w * N.transpose() * N;
389 Mhh.noalias() += w * N.transpose() * mix_density * N;
390
391 // K matrix assembly
392 Kpv.noalias() += w * dNdx.transpose() * N * mix_density;
393
394 Kvp.noalias() += w * N.transpose() * dNdx;
395 Kvv.noalias() += w * N.transpose() * rho_dot * N;
396
397 Khh.noalias() += w * N.transpose() * mix_density * v_int_pt * dNdx;
398
399 // b matrix assembly
400 Bp.noalias() += w * N.transpose() * rho_dot + w * N.transpose() * Q_mx;
401
402 Bv.noalias() +=
403 w * dNdx.transpose() * mix_density * v_int_pt * v_int_pt +
404 w * dNdx.transpose() * gamma -
405 w * N.transpose() * f * mix_density * std::abs(v_int_pt) *
406 v_int_pt / (4 * r_i) -
407 w * N.transpose() * Q_mom;
408
409 Bh.noalias() +=
410 -1 / 2 * w * N.transpose() * rho_dot * v_int_pt * v_int_pt -
411 w * N.transpose() * mix_density * v_int_pt * vdot_int_pt +
412 1 / 2 * w * dNdx.transpose() * mix_density * v_int_pt * v_int_pt *
413 v_int_pt +
414 w * N.transpose() * (Q_hx / pi / r_i / r_i) -
415 w * N.transpose() * Q_ene;
416
417 if (_process_data.has_gravity)
418 {
419 NodalVectorType gravity_operator =
420 N.transpose() * b * w * _element_direction[2];
421
422 Bv.noalias() += gravity_operator * mix_density;
423 Bh.noalias() += gravity_operator * mix_density * v_int_pt;
424 }
425 }
426
427 // debugging
428 // std::string sep = "\n----------------------------------------\n";
429 // Eigen::IOFormat CleanFmt(6, 0, ", ", "\n", "[", "]");
430 // std::cout << local_M.format(CleanFmt) << sep;
431 // std::cout << local_K.format(CleanFmt) << sep;
432 // std::cout << local_b.format(CleanFmt) << sep;
433}
434
435} // namespace WellboreSimulator
436} // namespace ProcessLib
const double PI
Definition QArrow.h:9
void setElementID(std::size_t element_id)
typename ShapeMatricesType::NodalVectorType NodalVectorType
NumLib::GenericIntegrationMethod const & _integration_method
ShapeMatrixPolicyType< ShapeFunction, GlobalDim > ShapeMatricesType
void assemble(double const t, double const dt, std::vector< double > const &local_x, std::vector< double > const &local_x_prev, std::vector< double > &local_M_data, std::vector< double > &local_K_data, std::vector< double > &local_b_data) override
std::vector< IpData, Eigen::aligned_allocator< IpData > > _ip_data
void checkStateInRange(double const pressure, double const temperature, std::string_view const quantity)
double mixtureSlipParameter(double const alpha, DriftFluxState const &state)
std::string voidFractionClosureDiagnostics(DriftFluxState const &state)
std::optional< double > computeVapourVoidFraction(DriftFluxState const &state)
double steamDryness(double const enthalpy, double const h_sat_liquid, double const h_sat_vapour)
DriftFluxState driftFluxState(double const dryness, double const temperature, double const vapour_water_density, double const liquid_water_density, double const v_mix)
Eigen::Map< Vector > createZeroedVector(std::vector< double > &data, Eigen::VectorXd::Index size)
Eigen::Map< Matrix > createZeroedMatrix(std::vector< double > &data, Eigen::MatrixXd::Index rows, Eigen::MatrixXd::Index cols)
void shapeFunctionInterpolate(const NodalValues &, const ShapeMatrix &)
std::array< double, 3 > interpolateCoordinates(MeshLib::Element const &e, typename ShapeMatricesType::ShapeMatrices::ShapeType const &N)