OGS
DriftFluxModel.cpp
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#include "DriftFluxModel.h"
5
6#include <spdlog/fmt/fmt.h>
7
8#include <algorithm>
9#include <cmath>
10#include <string>
11
13#include "NumLib/Exceptions.h"
14
15namespace MaterialPropertyLib
16{
19static double closureSlipParameter(DriftFluxState const& state)
20{
21 return state.C_0 *
22 (state.dryness + (1 - state.dryness) * state.vapour_water_density /
24}
25
29{
30 DriftFluxState aligned = state;
31 aligned.u_gu = alignedDriftFluxVelocity(state.u_gu, state.v_mix);
32 return aligned;
33}
34
35double alignedDriftFluxVelocity(double const u_gu, double const v_mix)
36{
37 return std::copysign(u_gu, v_mix);
38}
39
40double driftFluxProfileParameter(double const dryness)
41{
42 return 1 + 0.12 * (1 - dryness);
43}
44
46{
47 double const S = closureSlipParameter(state);
48 double const delta =
50
51 return {
52 -S * delta * state.v_mix,
53 state.v_mix * (state.dryness * delta - S * state.liquid_water_density) -
54 state.vapour_water_density * state.u_gu,
55 state.dryness * state.v_mix * state.liquid_water_density};
56}
57
58double voidFractionResidual(double const alpha, DriftFluxState const& state)
59{
60 double const rho_mix = alpha * state.vapour_water_density +
61 (1 - alpha) * state.liquid_water_density;
62 double const S = closureSlipParameter(state);
63
64 return state.liquid_water_density *
65 (rho_mix * state.v_mix * (state.dryness - alpha * S) -
66 alpha * state.vapour_water_density * state.u_gu);
67}
68
70{
71 DriftFluxState const aligned = alignDriftWithFlow(state);
72
73 // computeVapourVoidFraction() accepts a root only from [0, alpha_max], so
74 // the sign change has to be reported over that interval. Over [0, 1] it
75 // says nothing: the second root of the closure is always larger than one,
76 // and the residual changes sign between alpha_max and one whenever the
77 // admissible interval holds no root at all.
78 double const alpha_max =
79 std::min(1., aligned.dryness / closureSlipParameter(aligned));
80
81 return fmt::format(
82 "dryness {:g}, liquid density {:g} kg/m^3, vapour density {:g} kg/m^3, "
83 "profile parameter {:g}, aligned drift flux velocity {:g} m/s. The "
84 "closure residual is {:g} at a void fraction of zero and {:g} at the "
85 "upper bound {:g} of the admissible interval; a root exists in between "
86 "only if the two have opposite signs.",
87 aligned.dryness, aligned.liquid_water_density,
88 aligned.vapour_water_density, aligned.C_0, aligned.u_gu,
89 voidFractionResidual(0., aligned),
90 voidFractionResidual(alpha_max, aligned), alpha_max);
91}
92
93double driftFluxVelocity(double const dryness, double const temperature,
94 double const vapour_water_density,
95 double const liquid_water_density)
96{
97 // The rounded value the original implementation of this closure used, not
98 // standard gravity, whose defined value is 9.80665 m/s^2. It stays rounded
99 // because sharpening it would move every existing two-phase result, and it
100 // stays local because a rounded stand-in is not the physical constant and
101 // so does not belong in PhysicalConstant.h beside the critical point.
102 constexpr double gravity = 9.81; // m/s^2
103
104 // Both caps are written as comparisons rather than with std::max, which
105 // returns its first argument for a NaN second one and would hand out a
106 // finite drift flux velocity for a non-finite state. The NaN is kept so
107 // that computeVapourVoidFraction() sees it and aborts the assembly.
108 double const temperature_ratio =
109 1 - temperature /
111 double const reduced_temperature =
112 temperature_ratio < 0 ? 0. : temperature_ratio;
113 double const sigma_gl = 0.2358 * std::pow(reduced_temperature, 1.256) *
114 (1 - 0.625 * reduced_temperature);
115
116 double const buoyancy =
117 gravity * sigma_gl * (liquid_water_density - vapour_water_density);
118 double const drift_buoyancy = buoyancy < 0 ? 0. : buoyancy;
119
120 return 1.18 * (1 - dryness) * std::sqrt(std::sqrt(drift_buoyancy)) /
121 std::sqrt(liquid_water_density);
122}
123
124DriftFluxState driftFluxState(double const dryness, double const temperature,
125 double const vapour_water_density,
126 double const liquid_water_density,
127 double const v_mix)
128{
129 return {.dryness = dryness,
130 .vapour_water_density = vapour_water_density,
131 .liquid_water_density = liquid_water_density,
132 .v_mix = v_mix,
133 .C_0 = driftFluxProfileParameter(dryness),
135 driftFluxVelocity(dryness, temperature, vapour_water_density,
136 liquid_water_density),
137 v_mix)};
138}
139
140std::optional<double> computeVapourVoidFraction(DriftFluxState const& state)
141{
142 auto const& [dryness, vapour_water_density, liquid_water_density, v_mix,
143 C_0, u_gu] = state;
144
145 // Single phase states are exact, no closure is needed. They are settled
146 // before the state validation below because the density of the absent
147 // phase does not enter the result: it is the one evaluated far off the
148 // saturation line and hence the one that may be non-positive, and a pure
149 // liquid or pure vapour section must not abort the assembly for it. A NaN
150 // dryness satisfies neither comparison and reaches the validation.
151 if (dryness <= 0)
152 {
153 return 0.;
154 }
155 if (dryness >= 1)
156 {
157 return 1.;
158 }
159
160 // All arguments are computed from the current solution iterate, so a
161 // non-physical value is a diverging global Newton step, not a broken
162 // input. Aborting the assembly ends the nonlinear iteration and lets the
163 // time stepping repeat the step under either solver, whereas OGS_FATAL
164 // would end the run; see the documentation of this function.
165 //
166 // The phase densities are evaluated on the IAPWS-IF97 region 4 saturation
167 // line, which is extrapolated outside of its pressure range 611.213 Pa to
168 // 22.064 MPa and then may return non-positive or NaN values. The negated
169 // comparison is deliberate; `density <= 0` lets a NaN density pass.
170 if (!(liquid_water_density > 0) || !(vapour_water_density > 0))
171 {
172 throw NumLib::AssemblyException(fmt::format(
173 "Non-positive phase density in the vapour void fraction closure: "
174 "liquid density {:g} kg/m^3, vapour density {:g} kg/m^3.",
175 liquid_water_density, vapour_water_density));
176 }
177 if (!std::isfinite(dryness) || !std::isfinite(v_mix) ||
178 !std::isfinite(u_gu) || !std::isfinite(C_0))
179 {
180 throw NumLib::AssemblyException(fmt::format(
181 "Non-finite state in the vapour void fraction closure: dryness "
182 "{:g}, mixture velocity {:g} m/s, drift flux velocity {:g} m/s, "
183 "profile parameter {:g}.",
184 dryness, v_mix, u_gu, C_0));
185 }
186
187 double const S = closureSlipParameter(state);
188
189 // Upper bound of the admissible interval. The slip transports vapour out
190 // of the control volume, so the void fraction stays below the homogeneous
191 // one, alpha <= dryness / S <= alpha_homogeneous. That bound is at most
192 // one for a profile parameter of at least one, which is what the
193 // Rouhani-Axelsson correlation of driftFluxProfileParameter() gives. A
194 // profile parameter below one describes a void profile peaking at the
195 // wall rather than at the centre; the bound then exceeds one and the
196 // volume fraction of the vapour, which cannot, hence the minimum.
197 double const alpha_max = std::min(1., dryness / S);
198
199 // The drift is aligned with the mixture flow, see above.
200 auto const [a, b, c] = voidFractionQuadratic(alignDriftWithFlow(state));
201
202 // The quadratic degenerates to a linear equation at vanishing mixture
203 // velocity and at the critical point, where both phase densities coincide.
204 // The tolerance is relative, to the linear coefficient below and to
205 // b^2 for the discriminant, and some tens of machine epsilons, 45 of
206 // them: that is the scale on which the coefficients, each a difference
207 // of products of the closure state, lose their last digits, while a
208 // quadratic whose leading coefficient is that much smaller than the linear
209 // one has its small root within the accuracy of the linear solve anyway.
210 constexpr double degeneracy_tolerance = 1e-14;
211
212 // A root and the bound alpha_max are computed by different expressions, so
213 // a root that coincides with the bound can come out just outside of it.
214 // Roots are therefore accepted with a tolerance and clamped afterwards.
215 // The tolerance is about the square root of the machine epsilon, which is
216 // the accuracy a root of a quadratic is worth near a double root, where the
217 // root shifts with the square root of the perturbation of the
218 // coefficients.
219 constexpr double interval_tolerance = 1e-8;
220
221 auto const admissible = [&](double const alpha) -> std::optional<double>
222 {
223 double const tolerance = interval_tolerance * std::max(1., alpha_max);
224 // The negated comparisons are deliberate; the plain form lets a NaN
225 // root through to the clamp below, which returns it as an admissible
226 // void fraction.
227 if (!(alpha >= -tolerance) || !(alpha <= alpha_max + tolerance))
228 {
229 return std::nullopt;
230 }
231 return std::clamp(alpha, 0., alpha_max);
232 };
233
234 if (std::abs(a) <= degeneracy_tolerance * std::abs(b))
235 {
236 if (b == 0)
237 {
238 // Every coefficient vanishes, which for a validated two-phase
239 // state happens exactly for a mixture at rest without drift, that
240 // is v_mix = 0 and u_gu = 0. The closure then holds for every void
241 // fraction. Retreating would not resolve it, because repeating the
242 // time step does not change the velocity of the current iterate,
243 // so the value is taken from the limit instead: without drift the
244 // closure reads x = alpha S for every non-zero mixture velocity,
245 // independently of it, hence alpha = x / S = alpha_max, the
246 // profile slip alone.
247 if (c == 0)
248 {
249 return admissible(alpha_max);
250 }
251 return std::nullopt;
252 }
253 return admissible(-c / b);
254 }
255
256 // Both terms underflow to zero for a mixture velocity in the subnormal
257 // range, which loses the small root and is reported as no admissible root
258 // rather than as a wrong one. Rescaling the coefficients would avoid it,
259 // at the price of perturbing the rounding of every reachable state, and
260 // the velocities in question are some 1e-160 m/s.
261 double discriminant = b * b - 4 * a * c;
262 if (discriminant < 0)
263 {
264 // A double root cannot be distinguished from a pair of complex roots
265 // within the accuracy of the cancelling difference above.
266 if (discriminant < -degeneracy_tolerance * b * b)
267 {
268 return std::nullopt;
269 }
270 discriminant = 0;
271 }
272
273 // Stable roots: the direct formula loses the small root to cancellation
274 // whenever 4 a c is small compared to b^2, which is the case for small
275 // dryness.
276 double const q = -0.5 * (b + std::copysign(std::sqrt(discriminant), b));
277 if (q == 0)
278 {
279 return admissible(0.);
280 }
281
282 // The smaller root is the one continuous with the single phase limit
283 // alpha(dryness -> 0) = 0.
284 double const root_1 = q / a;
285 double const root_2 = c / q;
286
287 auto const first = admissible(std::min(root_1, root_2));
288 return first ? first : admissible(std::max(root_1, root_2));
289}
290
291double mixtureSlipParameter(double const alpha, DriftFluxState const& state)
292{
293 // The zero slip limit at a void fraction of one, see the documentation of
294 // this function.
295 if (alpha == 1)
296 {
297 return 0.;
298 }
299
300 double const rho_v = state.vapour_water_density;
301 double const rho_l = state.liquid_water_density;
302 double const C_0 = state.C_0;
303
304 double const rho_mix = alpha * rho_v + (1 - alpha) * rho_l;
305
306 return alpha * rho_l * rho_v * rho_mix / (1 - alpha) /
307 std::pow((alpha * C_0 * rho_v + (1 - alpha * C_0) * rho_l), 2) *
308 std::pow((C_0 - 1) * state.v_mix + state.u_gu, 2);
309}
310} // namespace MaterialPropertyLib
VoidFractionQuadratic voidFractionQuadratic(DriftFluxState const &state)
double voidFractionResidual(double const alpha, DriftFluxState const &state)
double mixtureSlipParameter(double const alpha, DriftFluxState const &state)
std::string voidFractionClosureDiagnostics(DriftFluxState const &state)
static double closureSlipParameter(DriftFluxState const &state)
static DriftFluxState alignDriftWithFlow(DriftFluxState const &state)
std::optional< double > computeVapourVoidFraction(DriftFluxState const &state)
double alignedDriftFluxVelocity(double const u_gu, double const v_mix)
double driftFluxVelocity(double const dryness, double const temperature, double const vapour_water_density, double const liquid_water_density)
DriftFluxState driftFluxState(double const dryness, double const temperature, double const vapour_water_density, double const liquid_water_density, double const v_mix)
double driftFluxProfileParameter(double const dryness)
double dryness
The vapour mass fraction, dimensionless.
double v_mix
The mixture velocity in m/s.
double u_gu
The drift flux velocity in m/s, see driftFluxVelocity().