InitStateEquil_impl.hpp
Go to the documentation of this file.
1// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2// vi: set et ts=4 sw=4 sts=4:
3/*
4 This file is part of the Open Porous Media project (OPM).
5
6 OPM is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 OPM is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with OPM. If not, see <http://www.gnu.org/licenses/>.
18
19 Consult the COPYING file in the top-level source directory of this
20 module for the precise wording of the license and the list of
21 copyright holders.
22*/
23#ifndef OPM_INIT_STATE_EQUIL_IMPL_HPP
24#define OPM_INIT_STATE_EQUIL_IMPL_HPP
25
26#include <dune/grid/common/mcmgmapper.hh>
27
28#include <opm/common/OpmLog/OpmLog.hpp>
29
30#include <opm/grid/utility/RegionMapping.hpp>
31#include <opm/grid/LookUpData.hh>
32
33#include <opm/input/eclipse/EclipseState/EclipseState.hpp>
34#include <opm/input/eclipse/EclipseState/Tables/PbvdTable.hpp>
35#include <opm/input/eclipse/EclipseState/Tables/PdvdTable.hpp>
36#include <opm/input/eclipse/EclipseState/Tables/RsconstTable.hpp>
37#include <opm/input/eclipse/EclipseState/Tables/RsvdTable.hpp>
38#include <opm/input/eclipse/EclipseState/Tables/RtempvdTable.hpp>
39#include <opm/input/eclipse/EclipseState/Tables/RvvdTable.hpp>
40#include <opm/input/eclipse/EclipseState/Tables/RvwvdTable.hpp>
41#include <opm/input/eclipse/EclipseState/Tables/SaltvdTable.hpp>
42#include <opm/input/eclipse/EclipseState/Tables/SaltpvdTable.hpp>
43
44#include <opm/input/eclipse/Units/UnitSystem.hpp>
45
46#include <opm/material/fluidmatrixinteractions/EclMaterialLawManager.hpp>
47#include <opm/material/fluidsystems/BlackOilFluidSystem.hpp>
48
51
53
54#include <fmt/format.h>
55
56#include <algorithm>
57#include <cassert>
58#include <cmath>
59#include <cstddef>
60#include <limits>
61#include <numbers>
62#include <stdexcept>
63
64namespace Opm {
65namespace EQUIL {
66
67namespace Details {
68
69template <typename CellRange, class Scalar>
70void verticalExtent(const CellRange& cells,
71 const std::vector<std::pair<Scalar, Scalar>>& cellZMinMax,
72 const Parallel::Communication& comm,
73 std::array<Scalar,2>& span)
74{
75 span[0] = std::numeric_limits<Scalar>::max();
76 span[1] = std::numeric_limits<Scalar>::lowest();
77
78 // Define vertical span as
79 //
80 // [minimum(node depth(cells)), maximum(node depth(cells))]
81 //
82 // Note: The implementation of 'RK4IVP<>' implicitly
83 // imposes the requirement that cell centroids are all
84 // within this vertical span. That requirement is not
85 // checked.
86 for (const auto& cell : cells) {
87 if (cellZMinMax[cell].first < span[0]) { span[0] = cellZMinMax[cell].first; }
88 if (cellZMinMax[cell].second > span[1]) { span[1] = cellZMinMax[cell].second; }
89 }
90 span[0] = comm.min(span[0]);
91 span[1] = comm.max(span[1]);
92}
93
94template<class Scalar>
95void subdivisionCentrePoints(const Scalar left,
96 const Scalar right,
97 const int numIntervals,
98 std::vector<std::pair<Scalar, Scalar>>& subdiv)
99{
100 const auto h = (right - left) / numIntervals;
101
102 auto end = left;
103 for (auto i = 0*numIntervals; i < numIntervals; ++i) {
104 const auto start = end;
105 end = left + (i + 1)*h;
106
107 subdiv.emplace_back((start + end) / 2, h);
108 }
109}
110
111template <typename CellID, typename Scalar>
112std::vector<std::pair<Scalar, Scalar>>
113horizontalSubdivision(const CellID cell,
114 const std::pair<Scalar, Scalar> topbot,
115 const int numIntervals)
116{
117 auto subdiv = std::vector<std::pair<Scalar, Scalar>>{};
118 subdiv.reserve(2 * numIntervals);
119
120 if (topbot.first > topbot.second) {
121 throw std::out_of_range {
122 "Negative thickness (inverted top/bottom faces) in cell "
123 + std::to_string(cell)
124 };
125 }
126
127 subdivisionCentrePoints(topbot.first, topbot.second,
128 2*numIntervals, subdiv);
129
130 return subdiv;
131}
132
133template <class Scalar, class Element>
134Scalar cellCenterDepth(const Element& element)
135{
136 typedef typename Element::Geometry Geometry;
137 static constexpr int zCoord = Element::dimension - 1;
138 Scalar zz = 0.0;
139
140 const Geometry& geometry = element.geometry();
141 const int corners = geometry.corners();
142 for (int i=0; i < corners; ++i)
143 zz += geometry.corner(i)[zCoord];
144
145 return zz/corners;
146}
147
148template <class Scalar, class Element>
149std::pair<Scalar,Scalar> cellCenterXY(const Element& element)
150{
151 typedef typename Element::Geometry Geometry;
152 static constexpr int xCoord = Element::dimension - 3;
153 static constexpr int yCoord = Element::dimension - 2;
154 Scalar yy = 0.0;
155 Scalar xx = 0.0;
156
157
158 const Geometry& geometry = element.geometry();
159 const int corners = geometry.corners();
160 for (int i=0; i < corners; ++i) {
161 xx += geometry.corner(i)[xCoord];
162 yy += geometry.corner(i)[yCoord];
163 }
164 return std::make_pair(xx/corners, yy/corners);
165}
166
167template <class Scalar, class Element>
168std::pair<Scalar,Scalar> cellZSpan(const Element& element)
169{
170 typedef typename Element::Geometry Geometry;
171 static constexpr int zCoord = Element::dimension - 1;
172 Scalar bot = 0.0;
173 Scalar top = 0.0;
174
175 const Geometry& geometry = element.geometry();
176 const int corners = geometry.corners();
177 assert(corners == 8);
178 for (int i=0; i < 4; ++i)
179 bot += geometry.corner(i)[zCoord];
180 for (int i=4; i < corners; ++i)
181 top += geometry.corner(i)[zCoord];
182
183 return std::make_pair(bot/4, top/4);
184}
185
186template <class Scalar, class Element>
187std::pair<Scalar,Scalar> cellZMinMax(const Element& element)
188{
189 typedef typename Element::Geometry Geometry;
190 static constexpr int zCoord = Element::dimension - 1;
191 const Geometry& geometry = element.geometry();
192 const int corners = geometry.corners();
193 assert(corners == 8);
194 auto min = std::numeric_limits<Scalar>::max();
195 auto max = std::numeric_limits<Scalar>::lowest();
196
197
198 for (int i=0; i < corners; ++i) {
199 min = std::min(min, static_cast<Scalar>(geometry.corner(i)[zCoord]));
200 max = std::max(max, static_cast<Scalar>(geometry.corner(i)[zCoord]));
201 }
202 return std::make_pair(min, max);
203}
204
205template<class Scalar>
207 Scalar& dipAngle, Scalar& dipAzimuth)
208{
209 const auto& Xc = cellCorners.X;
210 const auto& Yc = cellCorners.Y;
211 const auto& Zc = cellCorners.Z;
212
213 Scalar v1x = Xc[1] - Xc[0];
214 Scalar v1y = Yc[1] - Yc[0];
215 Scalar v1z = Zc[1] - Zc[0];
216
217 Scalar v2x = Xc[2] - Xc[0];
218 Scalar v2y = Yc[2] - Yc[0];
219 Scalar v2z = Zc[2] - Zc[0];
220
221 // Cross product to get normal vector
222 Scalar nx = v1y * v2z - v1z * v2y;
223 Scalar ny = v1z * v2x - v1x * v2z;
224 Scalar nz = v1x * v2y - v1y * v2x;
225
226 // Normalize the normal vector
227 Scalar norm = std::hypot(nx, ny, nz);
228
229 if (norm > 1e-10) {
230 nx /= norm;
231 ny /= norm;
232 nz /= norm;
233
234 // Dip angle is the angle between normal and vertical (0,0,1)
235 dipAngle = std::acos(std::abs(nz));
236
237 // Dip azimuth (direction of dip)
238 if (std::abs(nx) > 1e-10 || std::abs(ny) > 1e-10) {
239 dipAzimuth = std::atan2(ny, nx);
240 // Convert to 0-2π range
241 dipAzimuth = std::fmod(dipAzimuth + 2*std::numbers::pi_v<Scalar>, 2*std::numbers::pi_v<Scalar>);
242 } else {
243 dipAzimuth = 0.0; // Vertical cell
244 }
245
246 // Clamp dip angle to reasonable values
247 const Scalar maxDip = std::numbers::pi_v<Scalar>/2 - static_cast<Scalar>(1e-6);
248 dipAngle = std::min(dipAngle, maxDip);
249 } else {
250 // Degenerate cell - assume horizontal
251 dipAngle = 0.0;
252 dipAzimuth = 0.0;
253 }
254}
255
256template <class Scalar, class Element>
258{
259 typedef typename Element::Geometry Geometry;
260 const Geometry& geometry = element.geometry();
261 static constexpr int zCoord = Element::dimension - 1;
262 static constexpr int yCoord = Element::dimension - 2;
263 static constexpr int xCoord = Element::dimension - 3;
264 const int corners = geometry.corners();
265 assert(corners == 8);
266 std::array<Scalar, 8> X {};
267 std::array<Scalar, 8> Y {};
268 std::array<Scalar, 8> Z {};
269 // Get all 8 corners of the hexahedral cell (maybe expensive)
270 for (int i = 0; i < corners; ++i) {
271 auto corner = geometry.corner(i);
272 X[i] = corner[xCoord];
273 Y[i] = corner[yCoord];
274 Z[i] = corner[zCoord];
275 }
276
277 return CellCornerData<Scalar>{X, Y, Z};
278}
279
280template<class Scalar>
281Scalar calculateTrueVerticalDepth(Scalar z, Scalar x, Scalar y,
282 Scalar dipAngle, Scalar dipAzimuth,
283 const std::array<Scalar, 3>& referencePoint)
284{
285 // For True Vertical Depth calculation:
286 // TVD = reference_depth + (z - reference_z) * cos(dipAngle)
287 // + lateral_distance * sin(dipAngle) * cos(azimuth_difference)
288
289 // Calculate lateral displacement from reference point
290 Scalar dx = x - referencePoint[0];
291 Scalar dy = y - referencePoint[1];
292 Scalar dz = z - referencePoint[2];
293
294 // If no dip, TVD is simply the depth
295 if (std::abs(dipAngle) < 1e-10) {
296 return referencePoint[2] + dz;
297 }
298
299 // Calculate the direction from reference point to current point
300 Scalar pointAzimuth = std::atan2(dy, dx);
301
302 // Calculate the angle between dip direction and point direction
303 Scalar azimuthDiff = pointAzimuth - dipAzimuth;
304
305 // Calculate lateral distance
306 Scalar lateralDist = std::hypot(dx, dy);
307
308 // Project lateral distance onto dip direction
309 Scalar lateralInDipDir = lateralDist * std::cos(azimuthDiff);
310
311 // True Vertical Depth calculation
312 // TVD increases with depth (more negative z means deeper)
313 // For a dipping plane: TVD = vertical_component + dip_component
314 Scalar tvd = referencePoint[2] + dz * std::cos(dipAngle) + lateralInDipDir * std::sin(dipAngle);
315
316 return tvd;
317}
318
319template<class Scalar, class RHS>
321 const std::array<Scalar,2>& span,
322 const Scalar y0,
323 const int N)
324 : N_(N)
325 , span_(span)
326{
327 // stepsize() divides by N_ and operator() evaluates interval N_ - 1. A
328 // non-positive sample count is rejected when the value is loaded in
329 // FlowGenericProblem, where its source is known.
330 assert(N >= 1);
331
332 const Scalar h = stepsize();
333 const Scalar h2 = h / 2;
334 const Scalar h6 = h / 6;
335
336 y_.reserve(N + 1);
337 f_.reserve(N + 1);
338
339 y_.push_back(y0);
340 f_.push_back(f(span_[0], y0));
341
342 for (int i = 0; i < N; ++i) {
343 const Scalar x = span_[0] + i*h;
344 const Scalar y = y_.back();
345
346 const Scalar k1 = f_[i];
347 const Scalar k2 = f(x + h2, y + h2*k1);
348 const Scalar k3 = f(x + h2, y + h2*k2);
349 const Scalar k4 = f(x + h, y + h*k3);
350
351 y_.push_back(y + h6*(k1 + 2*(k2 + k3) + k4));
352 f_.push_back(f(x + h, y_.back()));
353 }
354
355 assert (y_.size() == typename std::vector<Scalar>::size_type(N + 1));
356}
357
358template<class Scalar, class RHS>
360operator()(const Scalar x) const
361{
362 // Dense output (O(h**3)) according to Shampine
363 // (Hermite interpolation)
364 const Scalar h = stepsize();
365 int i = (x - span_[0]) / h;
366
367 // Crude handling of evaluation point outside "span_";
368 if (i < 0) { i = 0; }
369 if (N_ <= i) { i = N_ - 1; }
370
371 // Relative to the interval actually used, so that a point at the end of
372 // the span lands on t = 1 of the final interval rather than t = 0.
373 const Scalar t = (x - (span_[0] + i*h)) / h;
374
375 const Scalar y0 = y_[i], y1 = y_[i + 1];
376 const Scalar f0 = f_[i], f1 = f_[i + 1];
377
378 Scalar u = (1 - 2*t) * (y1 - y0);
379 u += h * ((t - 1)*f0 + t*f1);
380 u *= t * (t - 1);
381 u += (1 - t)*y0 + t*y1;
382
383 return u;
384}
385
386template<class Scalar, class RHS>
388stepsize() const
389{
390 return (span_[1] - span_[0]) / N_;
391}
392
393namespace PhasePressODE {
394
395template<class FluidSystem>
397Water(const TabulatedFunction& tempVdTable,
398 const TabulatedFunction& saltVdTable,
399 const int pvtRegionIdx,
400 const Scalar normGrav)
401 : tempVdTable_(tempVdTable)
402 , saltVdTable_(saltVdTable)
403 , pvtRegionIdx_(pvtRegionIdx)
404 , g_(normGrav)
405{
406}
407
408template<class FluidSystem>
409typename Water<FluidSystem>::Scalar
411operator()(const Scalar depth,
412 const Scalar press) const
413{
414 return this->density(depth, press) * g_;
415}
416
417template<class FluidSystem>
418typename Water<FluidSystem>::Scalar
420density(const Scalar depth,
421 const Scalar press) const
422{
423 // The initializing algorithm can give depths outside the range due to numerical noise i.e. we extrapolate
424 Scalar saltConcentration = saltVdTable_.eval(depth, /*extrapolate=*/true);
425 Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
426 Scalar rho = FluidSystem::waterPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
427 temp,
428 press,
429 Scalar{0.0} /*=Rsw*/,
430 saltConcentration);
431 rho *= FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
432 return rho;
433}
434
435template<class FluidSystem, class RS>
437Oil(const TabulatedFunction& tempVdTable,
438 const RS& rs,
439 const int pvtRegionIdx,
440 const Scalar normGrav)
441 : tempVdTable_(tempVdTable)
442 , rs_(rs)
443 , pvtRegionIdx_(pvtRegionIdx)
444 , g_(normGrav)
445{
446}
447
448template<class FluidSystem, class RS>
449typename Oil<FluidSystem,RS>::Scalar
451operator()(const Scalar depth,
452 const Scalar press) const
453{
454 return this->density(depth, press) * g_;
455}
456
457template<class FluidSystem, class RS>
458typename Oil<FluidSystem,RS>::Scalar
460density(const Scalar depth,
461 const Scalar press) const
462{
463 const Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
464 Scalar rs = 0.0;
465 if (FluidSystem::enableDissolvedGas() || FluidSystem::enableConstantRs())
466 rs = rs_(depth, press, temp);
467
468 Scalar bOil = 0.0;
469 if (rs >= FluidSystem::oilPvt().saturatedGasDissolutionFactor(pvtRegionIdx_, temp, press)) {
470 bOil = FluidSystem::oilPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
471 }
472 else {
473 bOil = FluidSystem::oilPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp, press, rs);
474 }
475 Scalar rho = bOil * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_);
476 if (FluidSystem::enableDissolvedGas() || FluidSystem::enableConstantRs()) {
477 rho += rs * bOil * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
478 }
479
480 return rho;
481}
482
483template<class FluidSystem, class RV, class RVW>
485Gas(const TabulatedFunction& tempVdTable,
486 const RV& rv,
487 const RVW& rvw,
488 const int pvtRegionIdx,
489 const Scalar normGrav)
490 : tempVdTable_(tempVdTable)
491 , rv_(rv)
492 , rvw_(rvw)
493 , pvtRegionIdx_(pvtRegionIdx)
494 , g_(normGrav)
495{
496}
497
498template<class FluidSystem, class RV, class RVW>
499typename Gas<FluidSystem,RV,RVW>::Scalar
501operator()(const Scalar depth,
502 const Scalar press) const
503{
504 return this->density(depth, press) * g_;
505}
506
507template<class FluidSystem, class RV, class RVW>
508typename Gas<FluidSystem,RV,RVW>::Scalar
510density(const Scalar depth,
511 const Scalar press) const
512{
513 const Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
514 Scalar rv = 0.0;
515 if (FluidSystem::enableVaporizedOil())
516 rv = rv_(depth, press, temp);
517
518 Scalar rvw = 0.0;
519 if (FluidSystem::enableVaporizedWater())
520 rvw = rvw_(depth, press, temp);
521
522 Scalar bGas = 0.0;
523
524 if (FluidSystem::enableVaporizedOil() && FluidSystem::enableVaporizedWater()) {
525 if (rv >= FluidSystem::gasPvt().saturatedOilVaporizationFactor(pvtRegionIdx_, temp, press)
526 && rvw >= FluidSystem::gasPvt().saturatedWaterVaporizationFactor(pvtRegionIdx_, temp, press))
527 {
528 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
529 } else {
530 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp, press, rv, rvw);
531 }
532 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
533 rho += rv * bGas * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_)
534 + rvw * bGas * FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
535 return rho;
536 }
537
538 if (FluidSystem::enableVaporizedOil()){
539 if (rv >= FluidSystem::gasPvt().saturatedOilVaporizationFactor(pvtRegionIdx_, temp, press)) {
540 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
541 } else {
542 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
543 temp,
544 press,
545 rv,
546 Scalar{0.0}/*=rvw*/);
547 }
548 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
549 rho += rv * bGas * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_);
550 return rho;
551 }
552
553 if (FluidSystem::enableVaporizedWater()){
554 if (rvw >= FluidSystem::gasPvt().saturatedWaterVaporizationFactor(pvtRegionIdx_, temp, press)) {
555 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
556 }
557 else {
558 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
559 temp,
560 press,
561 Scalar{0.0} /*=rv*/,
562 rvw);
563 }
564 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
565 rho += rvw * bGas * FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
566 return rho;
567 }
568
569 // immiscible gas
570 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp,
571 press,
572 Scalar{0.0} /*=rv*/,
573 Scalar{0.0} /*=rvw*/);
574 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
575
576 return rho;
577}
578
579}
580
581template<class FluidSystem, class Region>
582template<class ODE>
583PressureTable<FluidSystem,Region>::
584PressureFunction<ODE>::PressureFunction(const ODE& ode,
585 const InitCond& ic,
586 const int nsample,
587 const VSpan& span)
588 : initial_(ic)
589{
590 this->value_[Direction::Up] = std::make_unique<Distribution>
591 (ode, VSpan {{ ic.depth, span[0] }}, ic.pressure, nsample);
592
593 this->value_[Direction::Down] = std::make_unique<Distribution>
594 (ode, VSpan {{ ic.depth, span[1] }}, ic.pressure, nsample);
595}
596
597template<class FluidSystem, class Region>
598template<class ODE>
599PressureTable<FluidSystem,Region>::
600PressureFunction<ODE>::PressureFunction(const PressureFunction& rhs)
601 : initial_(rhs.initial_)
602{
603 this->value_[Direction::Up] =
604 std::make_unique<Distribution>(*rhs.value_[Direction::Up]);
605
606 this->value_[Direction::Down] =
607 std::make_unique<Distribution>(*rhs.value_[Direction::Down]);
608}
609
610template<class FluidSystem, class Region>
611template<class ODE>
612typename PressureTable<FluidSystem,Region>::template PressureFunction<ODE>&
615operator=(const PressureFunction& rhs)
616{
617 this->initial_ = rhs.initial_;
618
619 this->value_[Direction::Up] =
620 std::make_unique<Distribution>(*rhs.value_[Direction::Up]);
621
622 this->value_[Direction::Down] =
623 std::make_unique<Distribution>(*rhs.value_[Direction::Down]);
624
625 return *this;
626}
627
628template<class FluidSystem, class Region>
629template<class ODE>
630typename PressureTable<FluidSystem,Region>::template PressureFunction<ODE>&
633operator=(PressureFunction&& rhs)
634{
635 this->initial_ = rhs.initial_;
636 this->value_ = std::move(rhs.value_);
637
638 return *this;
639}
640
641template<class FluidSystem, class Region>
642template<class ODE>
644PressureTable<FluidSystem,Region>::
645PressureFunction<ODE>::
646value(const Scalar depth) const
647{
648 if (depth < this->initial_.depth) {
649 // Value above initial condition depth.
650 return (*this->value_[Direction::Up])(depth);
651 }
652 else if (depth > this->initial_.depth) {
653 // Value below initial condition depth.
654 return (*this->value_[Direction::Down])(depth);
655 }
656 else {
657 // Value *at* initial condition depth.
658 return this->initial_.pressure;
659 }
660}
661
662
663template<class FluidSystem, class Region>
664template<typename PressFunc>
665void PressureTable<FluidSystem,Region>::
666checkPtr(const PressFunc* phasePress,
667 const std::string& phaseName) const
668{
669 if (phasePress != nullptr) { return; }
670
671 throw std::invalid_argument {
672 "Phase pressure function for \"" + phaseName
673 + "\" most not be null"
674 };
675}
676
677template<class FluidSystem, class Region>
678typename PressureTable<FluidSystem,Region>::Strategy
679PressureTable<FluidSystem,Region>::
680selectEquilibrationStrategy(const Region& reg) const
681{
682 if (!this->oilActive()) {
683 if (reg.datum() > reg.zwoc()) { // Datum in water zone
684 return &PressureTable::equil_WOG;
685 }
686 return &PressureTable::equil_GOW;
687 }
688
689 if (reg.datum() > reg.zwoc()) { // Datum in water zone
690 return &PressureTable::equil_WOG;
691 }
692 else if (reg.datum() < reg.zgoc()) { // Datum in gas zone
693 return &PressureTable::equil_GOW;
694 }
695 else { // Datum in oil zone
696 return &PressureTable::equil_OWG;
697 }
698}
699
700template<class FluidSystem, class Region>
701void PressureTable<FluidSystem,Region>::
702copyInPointers(const PressureTable& rhs)
703{
704 if (rhs.oil_ != nullptr) {
705 this->oil_ = std::make_unique<OPress>(*rhs.oil_);
706 }
707
708 if (rhs.gas_ != nullptr) {
709 this->gas_ = std::make_unique<GPress>(*rhs.gas_);
710 }
711
712 if (rhs.wat_ != nullptr) {
713 this->wat_ = std::make_unique<WPress>(*rhs.wat_);
714 }
715}
716
717template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
719PhaseSaturations(MaterialLawManager& matLawMgr,
720 const std::vector<Scalar>& swatInit)
721 : matLawMgr_(matLawMgr)
722 , swatInit_ (swatInit)
723{
724}
725
726template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
729 : matLawMgr_(rhs.matLawMgr_)
730 , swatInit_ (rhs.swatInit_)
731 , sat_ (rhs.sat_)
732 , press_ (rhs.press_)
733{
734 // Note: We don't need to do anything to the 'fluidState_' here.
735 this->setEvaluationPoint(*rhs.evalPt_.position,
736 *rhs.evalPt_.region,
737 *rhs.evalPt_.ptable);
738}
739
740template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
744 const Region& reg,
745 const PTable& ptable)
746{
747 this->setEvaluationPoint(x, reg, ptable);
748 this->initializePhaseQuantities();
749
750 if (ptable.gasActive()) { this->deriveGasSat(); }
751
752 if (ptable.waterActive()) { this->deriveWaterSat(); }
753
754
755 if (this->isOverlappingTransition()) {
756 this->fixUnphysicalTransition();
757 }
758
759 if (ptable.oilActive()) { this->deriveOilSat(); }
760
761 this->accountForScaledSaturations();
762
763 return this->sat_;
764}
765
766template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
768setEvaluationPoint(const Position& x,
769 const Region& reg,
770 const PTable& ptable)
771{
772 this->evalPt_.position = &x;
773 this->evalPt_.region = &reg;
774 this->evalPt_.ptable = &ptable;
775}
776
777template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
778void PhaseSaturations<MaterialLawManager,FluidSystem,Region,CellID>::
779initializePhaseQuantities()
780{
781 this->sat_.reset();
782 this->press_.reset();
783
784 const auto depth = this->evalPt_.position->depth;
785 const auto& ptable = *this->evalPt_.ptable;
786
787 if (ptable.oilActive()) {
788 this->press_.oil = ptable.oil(depth);
789 }
790
791 if (ptable.gasActive()) {
792 this->press_.gas = ptable.gas(depth);
793 }
794
795 if (ptable.waterActive()) {
796 this->press_.water = ptable.water(depth);
797 }
798}
799
800template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
801void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveOilSat()
802{
803 this->sat_.oil = 1.0 - this->sat_.water - this->sat_.gas;
804}
805
806template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
807void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveGasSat()
808{
809 auto& sg = this->sat_.gas;
810
811 const auto isIncr = true; // dPcgo/dSg >= 0 for all Sg.
812 const auto oilActive = this->evalPt_.ptable->oilActive();
813
814 if (this->isConstCapPress(this->gasPos())) {
815 // Sharp interface between phases. Can derive phase saturation
816 // directly from knowing where 'depth' of evaluation point is
817 // relative to depth of O/G contact.
818 const auto gas_contact = oilActive? this->evalPt_.region->zgoc() : this->evalPt_.region->zwoc();
819 sg = this->fromDepthTable(gas_contact,
820 this->gasPos(), isIncr);
821 }
822 else {
823 // Capillary pressure curve is non-constant, meaning there is a
824 // transition zone between the gas and oil phases. Invert capillary
825 // pressure relation
826 //
827 // Pcgo(Sg) = Pg - Po
828 //
829 // Note that Pcgo is defined to be (Pg - Po), not (Po - Pg).
830 const auto pw = oilActive? this->press_.oil : this->press_.water;
831 const auto pcgo = this->press_.gas - pw;
832 sg = this->invertCapPress(pcgo, this->gasPos(), isIncr);
833 }
834}
835
836template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
837void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveWaterSat()
838{
839 auto& sw = this->sat_.water;
840
841 const auto oilActive = this->evalPt_.ptable->oilActive();
842 if (!oilActive) {
843 // for 2p gas+water we set the water saturation to 1.0 - sg
844 sw = 1.0 - this->sat_.gas;
845 }
846 else {
847 const auto isIncr = false; // dPcow/dSw <= 0 for all Sw.
848
849 if (this->isConstCapPress(this->waterPos())) {
850 // Sharp interface between phases. Can derive phase saturation
851 // directly from knowing where 'depth' of evaluation point is
852 // relative to depth of O/W contact.
853 sw = this->fromDepthTable(this->evalPt_.region->zwoc(),
854 this->waterPos(), isIncr);
855 }
856 else {
857 // Capillary pressure curve is non-constant, meaning there is a
858 // transition zone between the oil and water phases. Invert
859 // capillary pressure relation
860 //
861 // Pcow(Sw) = Po - Pw
862 //
863 // unless the model uses "SWATINIT". In the latter case, pick the
864 // saturation directly from the SWATINIT array of the pertinent
865 // cell.
866 const auto pcow = this->press_.oil - this->press_.water;
867
868 if (this->swatInit_.empty()) {
869 sw = this->invertCapPress(pcow, this->waterPos(), isIncr);
870 }
871 else {
872 auto [swout, newSwatInit] = this->applySwatInit(pcow);
873 if (newSwatInit)
874 sw = this->invertCapPress(pcow, this->waterPos(), isIncr);
875 else {
876 sw = swout;
877 }
878 }
879 }
880 }
881}
882
883template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
884void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
885fixUnphysicalTransition()
886{
887 auto& sg = this->sat_.gas;
888 auto& sw = this->sat_.water;
889
890 // Overlapping gas/oil and oil/water transition zones can lead to
891 // unphysical phase saturations when individual saturations are derived
892 // directly from inverting O/G and O/W capillary pressure curves.
893 //
894 // Recalculate phase saturations using the implied gas/water capillary
895 // pressure: Pg - Pw.
896 const auto pcgw = this->press_.gas - this->press_.water;
897 if (! this->swatInit_.empty()) {
898 // Re-scale Pc to reflect imposed sw for vanishing oil phase. This
899 // seems consistent with ECLIPSE, but fails to honour SWATINIT in
900 // case of non-trivial gas/oil capillary pressure.
901 auto [swout, newSwatInit] = this->applySwatInit(pcgw, sw);
902 if (newSwatInit){
903 const auto isIncr = false; // dPcow/dSw <= 0 for all Sw.
904 sw = this->invertCapPress(pcgw, this->waterPos(), isIncr);
905 }
906 else {
907 sw = swout;
908 }
909 }
910
911 sw = satFromSumOfPcs<FluidSystem>
912 (this->matLawMgr_, this->waterPos(), this->gasPos(),
913 this->evalPt_.position->cell, pcgw);
914 sg = 1.0 - sw;
915
916 this->fluidState_.setSaturation(this->oilPos(), 1.0 - sw - sg);
917 this->fluidState_.setSaturation(this->gasPos(), sg);
918 this->fluidState_.setSaturation(this->waterPos(), this->evalPt_
919 .ptable->waterActive() ? sw : 0.0);
920
921 // Pcgo = Pg - Po => Po = Pg - Pcgo
922 this->computeMaterialLawCapPress();
923 this->press_.oil = this->press_.gas - this->materialLawCapPressGasOil();
924}
925
926template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
927void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
928accountForScaledSaturations()
929{
930 const auto gasActive = this->evalPt_.ptable->gasActive();
931 const auto watActive = this->evalPt_.ptable->waterActive();
932 const auto oilActive = this->evalPt_.ptable->oilActive();
933
934 auto sg = gasActive? this->sat_.gas : 0.0;
935 auto sw = watActive? this->sat_.water : 0.0;
936 auto so = oilActive? this->sat_.oil : 0.0;
937
938 this->fluidState_.setSaturation(this->waterPos(), sw);
939 this->fluidState_.setSaturation(this->oilPos(), so);
940 this->fluidState_.setSaturation(this->gasPos(), sg);
941
942 const auto& scaledDrainageInfo = this->matLawMgr_
943 .oilWaterScaledEpsInfoDrainage(this->evalPt_.position->cell);
944
945 const auto thresholdSat = 1.0e-6;
946 if (watActive && ((sw + thresholdSat) > scaledDrainageInfo.Swu)) {
947 // Water saturation exceeds maximum possible value. Reset oil phase
948 // pressure to that which corresponds to maximum possible water
949 // saturation value.
950 this->fluidState_.setSaturation(this->waterPos(), scaledDrainageInfo.Swu);
951 if (oilActive) {
952 this->fluidState_.setSaturation(this->oilPos(), so + sw - scaledDrainageInfo.Swu);
953 } else if (gasActive) {
954 this->fluidState_.setSaturation(this->gasPos(), sg + sw - scaledDrainageInfo.Swu);
955 }
956 sw = scaledDrainageInfo.Swu;
957 this->computeMaterialLawCapPress();
958
959 if (oilActive) {
960 // Pcow = Po - Pw => Po = Pw + Pcow
961 this->press_.oil = this->press_.water + this->materialLawCapPressOilWater();
962 } else {
963 // Pcgw = Pg - Pw => Pg = Pw + Pcgw
964 this->press_.gas = this->press_.water + this->materialLawCapPressGasWater();
965 }
966
967 }
968 if (gasActive && ((sg + thresholdSat) > scaledDrainageInfo.Sgu)) {
969 // Gas saturation exceeds maximum possible value. Reset oil phase
970 // pressure to that which corresponds to maximum possible gas
971 // saturation value.
972 this->fluidState_.setSaturation(this->gasPos(), scaledDrainageInfo.Sgu);
973 if (oilActive) {
974 this->fluidState_.setSaturation(this->oilPos(), so + sg - scaledDrainageInfo.Sgu);
975 } else if (watActive) {
976 this->fluidState_.setSaturation(this->waterPos(), sw + sg - scaledDrainageInfo.Sgu);
977 }
978 sg = scaledDrainageInfo.Sgu;
979 this->computeMaterialLawCapPress();
980
981 if (oilActive) {
982 // Pcgo = Pg - Po => Po = Pg - Pcgo
983 this->press_.oil = this->press_.gas - this->materialLawCapPressGasOil();
984 } else {
985 // Pcgw = Pg - Pw => Pw = Pg - Pcgw
986 this->press_.water = this->press_.gas - this->materialLawCapPressGasWater();
987 }
988 }
989
990 if (watActive && ((sw - thresholdSat) < scaledDrainageInfo.Swl)) {
991 // Water saturation less than minimum possible value in cell. Reset
992 // water phase pressure to that which corresponds to minimum
993 // possible water saturation value.
994 this->fluidState_.setSaturation(this->waterPos(), scaledDrainageInfo.Swl);
995 if (oilActive) {
996 this->fluidState_.setSaturation(this->oilPos(), so + sw - scaledDrainageInfo.Swl);
997 } else if (gasActive) {
998 this->fluidState_.setSaturation(this->gasPos(), sg + sw - scaledDrainageInfo.Swl);
999 }
1000 sw = scaledDrainageInfo.Swl;
1001 this->computeMaterialLawCapPress();
1002
1003 if (oilActive) {
1004 // Pcwo = Po - Pw => Pw = Po - Pcow
1005 this->press_.water = this->press_.oil - this->materialLawCapPressOilWater();
1006 } else {
1007 // Pcgw = Pg - Pw => Pw = Pg - Pcgw
1008 this->press_.water = this->press_.gas - this->materialLawCapPressGasWater();
1009 }
1010 }
1011
1012 if (gasActive && ((sg - thresholdSat) < scaledDrainageInfo.Sgl)) {
1013 // Gas saturation less than minimum possible value in cell. Reset
1014 // gas phase pressure to that which corresponds to minimum possible
1015 // gas saturation.
1016 this->fluidState_.setSaturation(this->gasPos(), scaledDrainageInfo.Sgl);
1017 if (oilActive) {
1018 this->fluidState_.setSaturation(this->oilPos(), so + sg - scaledDrainageInfo.Sgl);
1019 } else if (watActive) {
1020 this->fluidState_.setSaturation(this->waterPos(), sw + sg - scaledDrainageInfo.Sgl);
1021 }
1022 sg = scaledDrainageInfo.Sgl;
1023 this->computeMaterialLawCapPress();
1024
1025 if (oilActive) {
1026 // Pcgo = Pg - Po => Pg = Po + Pcgo
1027 this->press_.gas = this->press_.oil + this->materialLawCapPressGasOil();
1028 } else {
1029 // Pcgw = Pg - Pw => Pg = Pw + Pcgw
1030 this->press_.gas = this->press_.water + this->materialLawCapPressGasWater();
1031 }
1032 }
1033}
1034
1035template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1036std::pair<typename FluidSystem::Scalar, bool>
1037PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1038applySwatInit(const Scalar pcow)
1039{
1040 return this->applySwatInit(pcow, this->swatInit_[this->evalPt_.position->cell]);
1041}
1042
1043template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1044std::pair<typename FluidSystem::Scalar, bool>
1045PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1046applySwatInit(const Scalar pcow, const Scalar sw)
1047{
1048 return this->matLawMgr_.applySwatinit(this->evalPt_.position->cell, pcow, sw);
1049}
1050
1051template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1052void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1053computeMaterialLawCapPress()
1054{
1055 const auto& matParams = this->matLawMgr_
1056 .materialLawParams(this->evalPt_.position->cell);
1057
1058 this->matLawCapPress_.fill(0.0);
1059 MaterialLaw::capillaryPressures(this->matLawCapPress_,
1060 matParams, this->fluidState_);
1061}
1062
1063template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1064typename FluidSystem::Scalar
1065PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1066materialLawCapPressGasOil() const
1067{
1068 return this->matLawCapPress_[this->oilPos()]
1069 + this->matLawCapPress_[this->gasPos()];
1070}
1071
1072template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1073typename FluidSystem::Scalar
1074PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1075materialLawCapPressOilWater() const
1076{
1077 return this->matLawCapPress_[this->oilPos()]
1078 - this->matLawCapPress_[this->waterPos()];
1079}
1080
1081template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1082typename FluidSystem::Scalar
1083PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1084materialLawCapPressGasWater() const
1085{
1086 return this->matLawCapPress_[this->gasPos()]
1087 - this->matLawCapPress_[this->waterPos()];
1088}
1089
1090template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1091bool PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1092isConstCapPress(const PhaseIdx phaseIdx) const
1093{
1094 return isConstPc<FluidSystem>
1095 (this->matLawMgr_, phaseIdx, this->evalPt_.position->cell);
1096}
1097
1098template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1099bool PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1100isOverlappingTransition() const
1101{
1102 return this->evalPt_.ptable->gasActive()
1103 && this->evalPt_.ptable->waterActive()
1104 && ((this->sat_.gas + this->sat_.water) > 1.0);
1105}
1106
1107template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1108typename FluidSystem::Scalar
1109PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1110fromDepthTable(const Scalar contactdepth,
1111 const PhaseIdx phasePos,
1112 const bool isincr) const
1113{
1114 return satFromDepth<FluidSystem>
1115 (this->matLawMgr_, this->evalPt_.position->depth,
1116 contactdepth, static_cast<int>(phasePos),
1117 this->evalPt_.position->cell, isincr);
1118}
1119
1120template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
1121typename FluidSystem::Scalar
1122PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
1123invertCapPress(const Scalar pc,
1124 const PhaseIdx phasePos,
1125 const bool isincr) const
1126{
1127 return satFromPc<FluidSystem>
1128 (this->matLawMgr_, static_cast<int>(phasePos),
1129 this->evalPt_.position->cell, pc, isincr);
1130}
1131
1132template<class FluidSystem, class Region>
1134PressureTable(const Scalar gravity,
1135 const int samplePoints)
1136 : gravity_(gravity)
1137 , nsample_(samplePoints)
1138{
1139}
1140
1141template <class FluidSystem, class Region>
1144 : gravity_(rhs.gravity_)
1145 , nsample_(rhs.nsample_)
1146{
1147 this->copyInPointers(rhs);
1148}
1149
1150template <class FluidSystem, class Region>
1153 : gravity_(rhs.gravity_)
1154 , nsample_(rhs.nsample_)
1155 , oil_ (std::move(rhs.oil_))
1156 , gas_ (std::move(rhs.gas_))
1157 , wat_ (std::move(rhs.wat_))
1158{
1159}
1160
1161template <class FluidSystem, class Region>
1165{
1166 this->gravity_ = rhs.gravity_;
1167 this->nsample_ = rhs.nsample_;
1168 this->copyInPointers(rhs);
1169
1170 return *this;
1171}
1172
1173template <class FluidSystem, class Region>
1177{
1178 this->gravity_ = rhs.gravity_;
1179 this->nsample_ = rhs.nsample_;
1180
1181 this->oil_ = std::move(rhs.oil_);
1182 this->gas_ = std::move(rhs.gas_);
1183 this->wat_ = std::move(rhs.wat_);
1184
1185 return *this;
1186}
1187
1188template <class FluidSystem, class Region>
1190equilibrate(const Region& reg,
1191 const VSpan& span)
1192{
1193 // One of the PressureTable::equil_*() member functions.
1194 auto equil = this->selectEquilibrationStrategy(reg);
1195
1196 (this->*equil)(reg, span);
1197}
1198
1199template <class FluidSystem, class Region>
1201oilActive() const
1202{
1203 return FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1204}
1205
1206template <class FluidSystem, class Region>
1208gasActive() const
1209{
1210 return FluidSystem::phaseIsActive(FluidSystem::gasPhaseIdx);
1211}
1212
1213template <class FluidSystem, class Region>
1215waterActive() const
1216{
1217 return FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx);
1218}
1219
1220template <class FluidSystem, class Region>
1221typename FluidSystem::Scalar
1223oil(const Scalar depth) const
1224{
1225 this->checkPtr(this->oil_.get(), "OIL");
1226
1227 return this->oil_->value(depth);
1228}
1229
1230template <class FluidSystem, class Region>
1231typename FluidSystem::Scalar
1233gas(const Scalar depth) const
1234{
1235 this->checkPtr(this->gas_.get(), "GAS");
1236
1237 return this->gas_->value(depth);
1238}
1239
1240
1241template <class FluidSystem, class Region>
1242typename FluidSystem::Scalar
1244water(const Scalar depth) const
1245{
1246 this->checkPtr(this->wat_.get(), "WATER");
1247
1248 return this->wat_->value(depth);
1249}
1250
1251template <class FluidSystem, class Region>
1253equil_WOG(const Region& reg, const VSpan& span)
1254{
1255 // Datum depth in water zone. Calculate phase pressure for water first,
1256 // followed by oil and gas if applicable.
1257
1258 if (! this->waterActive()) {
1259 throw std::invalid_argument {
1260 "Don't know how to interpret EQUIL datum depth in "
1261 "WATER zone in model without active water phase"
1262 };
1263 }
1264
1265 {
1266 const auto ic = typename WPress::InitCond {
1267 reg.datum(), reg.pressure()
1268 };
1269
1270 this->makeWatPressure(ic, reg, span);
1271 }
1272
1273 if (this->oilActive()) {
1274 // Pcow = Po - Pw => Po = Pw + Pcow
1275 const auto ic = typename OPress::InitCond {
1276 reg.zwoc(),
1277 this->water(reg.zwoc()) + reg.pcowWoc()
1278 };
1279
1280 this->makeOilPressure(ic, reg, span);
1281 }
1282
1283 if (this->gasActive() && this->oilActive()) {
1284 // Pcgo = Pg - Po => Pg = Po + Pcgo
1285 const auto ic = typename GPress::InitCond {
1286 reg.zgoc(),
1287 this->oil(reg.zgoc()) + reg.pcgoGoc()
1288 };
1289
1290 this->makeGasPressure(ic, reg, span);
1291 } else if (this->gasActive() && !this->oilActive()) {
1292 // No oil phase set Pg = Pw + Pcgw
1293 const auto ic = typename GPress::InitCond {
1294 reg.zwoc(), // The WOC is really the GWC for gas/water cases
1295 this->water(reg.zwoc()) + reg.pcowWoc() // Pcow(WOC) is really Pcgw(GWC) for gas/water cases
1296 };
1297 this->makeGasPressure(ic, reg, span);
1298 }
1299}
1300
1301template <class FluidSystem, class Region>
1302void PressureTable<FluidSystem, Region>::
1303equil_GOW(const Region& reg, const VSpan& span)
1304{
1305 // Datum depth in gas zone. Calculate phase pressure for gas first,
1306 // followed by oil and water if applicable.
1307
1308 if (! this->gasActive()) {
1309 throw std::invalid_argument {
1310 "Don't know how to interpret EQUIL datum depth in "
1311 "GAS zone in model without active gas phase"
1312 };
1313 }
1314
1315 {
1316 const auto ic = typename GPress::InitCond {
1317 reg.datum(), reg.pressure()
1318 };
1319
1320 this->makeGasPressure(ic, reg, span);
1321 }
1322
1323 if (this->oilActive()) {
1324 // Pcgo = Pg - Po => Po = Pg - Pcgo
1325 const auto ic = typename OPress::InitCond {
1326 reg.zgoc(),
1327 this->gas(reg.zgoc()) - reg.pcgoGoc()
1328 };
1329 this->makeOilPressure(ic, reg, span);
1330 }
1331
1332 if (this->waterActive() && this->oilActive()) {
1333 // Pcow = Po - Pw => Pw = Po - Pcow
1334 const auto ic = typename WPress::InitCond {
1335 reg.zwoc(),
1336 this->oil(reg.zwoc()) - reg.pcowWoc()
1337 };
1338
1339 this->makeWatPressure(ic, reg, span);
1340 } else if (this->waterActive() && !this->oilActive()) {
1341 // No oil phase set Pw = Pg - Pcgw
1342 const auto ic = typename WPress::InitCond {
1343 reg.zwoc(), // The WOC is really the GWC for gas/water cases
1344 this->gas(reg.zwoc()) - reg.pcowWoc() // Pcow(WOC) is really Pcgw(GWC) for gas/water cases
1345 };
1346 this->makeWatPressure(ic, reg, span);
1347 }
1348}
1349
1350template <class FluidSystem, class Region>
1351void PressureTable<FluidSystem, Region>::
1352equil_OWG(const Region& reg, const VSpan& span)
1353{
1354 // Datum depth in oil zone. Calculate phase pressure for oil first,
1355 // followed by gas and water if applicable.
1356
1357 if (! this->oilActive()) {
1358 throw std::invalid_argument {
1359 "Don't know how to interpret EQUIL datum depth in "
1360 "OIL zone in model without active oil phase"
1361 };
1362 }
1363
1364 {
1365 const auto ic = typename OPress::InitCond {
1366 reg.datum(), reg.pressure()
1367 };
1368
1369 this->makeOilPressure(ic, reg, span);
1370 }
1371
1372 if (this->waterActive()) {
1373 // Pcow = Po - Pw => Pw = Po - Pcow
1374 const auto ic = typename WPress::InitCond {
1375 reg.zwoc(),
1376 this->oil(reg.zwoc()) - reg.pcowWoc()
1377 };
1378
1379 this->makeWatPressure(ic, reg, span);
1380 }
1381
1382 if (this->gasActive()) {
1383 // Pcgo = Pg - Po => Pg = Po + Pcgo
1384 const auto ic = typename GPress::InitCond {
1385 reg.zgoc(),
1386 this->oil(reg.zgoc()) + reg.pcgoGoc()
1387 };
1388 this->makeGasPressure(ic, reg, span);
1389 }
1390}
1391
1392template <class FluidSystem, class Region>
1393void PressureTable<FluidSystem, Region>::
1394makeOilPressure(const typename OPress::InitCond& ic,
1395 const Region& reg,
1396 const VSpan& span)
1397{
1398 const auto drho = OilPressODE {
1399 reg.tempVdTable(), reg.dissolutionCalculator(),
1400 reg.pvtIdx(), this->gravity_
1401 };
1402
1403 this->oil_ = std::make_unique<OPress>(drho, ic, this->nsample_, span);
1404}
1405
1406template <class FluidSystem, class Region>
1407void PressureTable<FluidSystem, Region>::
1408makeGasPressure(const typename GPress::InitCond& ic,
1409 const Region& reg,
1410 const VSpan& span)
1411{
1412 const auto drho = GasPressODE {
1413 reg.tempVdTable(), reg.evaporationCalculator(), reg.waterEvaporationCalculator(),
1414 reg.pvtIdx(), this->gravity_
1415 };
1416
1417 this->gas_ = std::make_unique<GPress>(drho, ic, this->nsample_, span);
1418}
1419
1420template <class FluidSystem, class Region>
1421void PressureTable<FluidSystem, Region>::
1422makeWatPressure(const typename WPress::InitCond& ic,
1423 const Region& reg,
1424 const VSpan& span)
1425{
1426 const auto drho = WatPressODE {
1427 reg.tempVdTable(), reg.saltVdTable(), reg.pvtIdx(), this->gravity_
1428 };
1429
1430 this->wat_ = std::make_unique<WPress>(drho, ic, this->nsample_, span);
1431}
1432
1433}
1434
1435namespace DeckDependent {
1436
1437std::vector<EquilRecord>
1438getEquil(const EclipseState& state)
1439{
1440 const auto& init = state.getInitConfig();
1441
1442 if(!init.hasEquil()) {
1443 throw std::domain_error("Deck does not provide equilibration data.");
1444 }
1445
1446 const auto& equil = init.getEquil();
1447 return { equil.begin(), equil.end() };
1448}
1449
1450template<class GridView>
1451std::vector<int>
1452equilnum(const EclipseState& eclipseState,
1453 const GridView& gridview)
1454{
1455 std::vector<int> eqlnum(gridview.size(0), 0);
1456
1457 if (eclipseState.fieldProps().has_int("EQLNUM")) {
1458 // EQLNUM is given on the (unrefined) input grid, but the equilibration
1459 // works on the leaf grid. With LGRs the leaf has more cells than the
1460 // input grid and a different ordering, so copying the input array
1461 // directly into a leaf-sized vector misaligns it (and leaves refined
1462 // cells at region 1). LookUpData maps each leaf cell to its input-grid
1463 // origin - a refined cell inherits its parent cell's EQLNUM - which for
1464 // an unrefined grid reduces to the identity, so non-LGR cases are
1465 // unchanged. needsTranslation == true applies the 1-based -> 0-based
1466 // shift previously done by the transform.
1467 const LookUpData<typename GridView::Grid, GridView> lookUpData(gridview);
1468 eqlnum = lookUpData.template assignFieldPropsIntOnLeaf<int>(
1469 eclipseState.fieldProps(), "EQLNUM", /*needsTranslation=*/true);
1470 }
1472 const int num_regions = eclipseState.getTableManager().getEqldims().getNumEquilRegions();
1473 if (std::ranges::any_of(eqlnum, [num_regions](int n){return n >= num_regions;})) {
1474 throw std::runtime_error("Values larger than maximum Equil regions " +
1475 std::to_string(num_regions) + " provided in EQLNUM");
1476 }
1477 if (std::ranges::any_of(eqlnum, [](int n){return n < 0;})) {
1478 throw std::runtime_error("zero or negative values provided in EQLNUM");
1479 }
1480 OPM_END_PARALLEL_TRY_CATCH("Invalied EQLNUM numbers: ", gridview.comm());
1481
1482 return eqlnum;
1483}
1484
1485template<class FluidSystem,
1486 class Grid,
1487 class GridView,
1488 class ElementMapper,
1489 class CartesianIndexMapper>
1490template<class MaterialLawManager>
1491InitialStateComputer<FluidSystem,
1492 Grid,
1493 GridView,
1494 ElementMapper,
1495 CartesianIndexMapper>::
1496InitialStateComputer(MaterialLawManager& materialLawManager,
1497 const EclipseState& eclipseState,
1498 const Grid& grid,
1499 const GridView& gridView,
1500 const CartesianIndexMapper& cartMapper,
1501 const Scalar grav,
1502 const int num_pressure_points,
1503 const bool applySwatInit)
1504 : temperature_(grid.size(/*codim=*/0), eclipseState.getTableManager().rtemp()),
1505 saltConcentration_(grid.size(/*codim=*/0)),
1506 saltSaturation_(grid.size(/*codim=*/0)),
1507 pp_(FluidSystem::numPhases,
1508 std::vector<Scalar>(grid.size(/*codim=*/0))),
1509 sat_(FluidSystem::numPhases,
1510 std::vector<Scalar>(grid.size(/*codim=*/0))),
1511 rs_(grid.size(/*codim=*/0)),
1512 rv_(grid.size(/*codim=*/0)),
1513 rvw_(grid.size(/*codim=*/0)),
1514 cartesianIndexMapper_(cartMapper),
1515 num_pressure_points_(num_pressure_points)
1516{
1517 //Check for presence of kw SWATINIT
1518 if (applySwatInit) {
1519 if (eclipseState.fieldProps().has_double("SWATINIT")) {
1520 // SWATINIT is given on the (unrefined) input grid but is consumed per
1521 // leaf cell; with LGRs the leaf is larger and reordered, so map it
1522 // onto the leaf via LookUpData (a refined cell inherits its parent
1523 // cell's value; identity without LGRs).
1524 const LookUpData<Grid, GridView> lookUpData(gridView);
1525 auto input =
1526 lookUpData.assignFieldPropsDoubleOnLeaf(eclipseState.fieldProps(), "SWATINIT");
1527 if constexpr (std::is_same_v<Scalar, double>) {
1528 swatInit_ = std::move(input);
1529 } else {
1530 swatInit_.assign(input.begin(), input.end());
1531 }
1532 }
1533 }
1534
1535 // Querry cell depth, cell top-bottom.
1536 // numerical aquifer cells might be specified with different depths.
1537 const auto& num_aquifers = eclipseState.aquifer().numericalAquifers();
1538 updateCellProps_(gridView, num_aquifers);
1539
1540 // Get the equilibration records.
1541 const std::vector<EquilRecord> rec = getEquil(eclipseState);
1542 const auto& tables = eclipseState.getTableManager();
1543 // Create (inverse) region mapping.
1544 const RegionMapping<> eqlmap(equilnum(eclipseState, gridView));
1545 const int invalidRegion = -1;
1546 regionPvtIdx_.resize(rec.size(), invalidRegion);
1547 setRegionPvtIdx(eclipseState, gridView, eqlmap);
1548
1549 // Create Rs functions.
1550 rsFunc_.reserve(rec.size());
1551
1552 auto getArray = [](const std::vector<double>& input)
1553 {
1554 if constexpr (std::is_same_v<Scalar,double>) {
1555 return input;
1556 } else {
1557 std::vector<Scalar> output;
1558 output.resize(input.size());
1559 std::ranges::copy(input, output.begin());
1560 return output;
1561 }
1562 };
1563
1564 if (FluidSystem::enableDissolvedGas()) {
1565 for (std::size_t i = 0; i < rec.size(); ++i) {
1566 if (eqlmap.cells(i).empty()) {
1567 rsFunc_.push_back(std::shared_ptr<Miscibility::RsVD<FluidSystem>>());
1568 continue;
1569 }
1570 const int pvtIdx = regionPvtIdx_[i];
1571 if (!rec[i].liveOilInitConstantRs()) {
1572 const TableContainer& rsvdTables = tables.getRsvdTables();
1573 const TableContainer& pbvdTables = tables.getPbvdTables();
1574 if (rsvdTables.size() > 0) {
1575 const RsvdTable& rsvdTable = rsvdTables.getTable<RsvdTable>(i);
1576 auto depthColumn = getArray(rsvdTable.getColumn("DEPTH").vectorCopy());
1577 auto rsColumn = getArray(rsvdTable.getColumn("RS").vectorCopy());
1578 rsFunc_.push_back(std::make_shared<Miscibility::RsVD<FluidSystem>>(pvtIdx,
1579 depthColumn, rsColumn));
1580 } else if (pbvdTables.size() > 0) {
1581 const PbvdTable& pbvdTable = pbvdTables.getTable<PbvdTable>(i);
1582 auto depthColumn = getArray(pbvdTable.getColumn("DEPTH").vectorCopy());
1583 auto pbubColumn = getArray(pbvdTable.getColumn("PBUB").vectorCopy());
1584 rsFunc_.push_back(std::make_shared<Miscibility::PBVD<FluidSystem>>(pvtIdx,
1585 depthColumn, pbubColumn));
1586
1587 } else {
1588 throw std::runtime_error("Cannot initialise: RSVD or PBVD table not available.");
1589 }
1590
1591 }
1592 else {
1593 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1594 throw std::runtime_error("Cannot initialise: when no explicit RSVD table is given, \n"
1595 "datum depth must be at the gas-oil-contact. "
1596 "In EQUIL region "+std::to_string(i + 1)+" (counting from 1), this does not hold.");
1597 }
1598 const Scalar pContact = rec[i].datumDepthPressure();
1599 const Scalar TContact = 273.15 + 20; // standard temperature for now
1600 rsFunc_.push_back(std::make_shared<Miscibility::RsSatAtContact<FluidSystem>>(pvtIdx, pContact, TContact));
1601 }
1602 }
1603 }
1604 else if (FluidSystem::enableConstantRs() && tables.hasTables("RSCONST")) {
1605 const auto& rsconstTables = tables.getRsconstTables();
1606
1607 if (rsconstTables.empty()) {
1608 for (std::size_t i = 0; i < rec.size(); ++i) {
1609 // Normal dead oil (no dissolved gas and rsconst)
1610 rsFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1611 }
1612 }
1613 else {
1614 const auto& rsconstTable = rsconstTables.getTable<RsconstTable>(0);
1615
1616 const auto rsConst = rsconstTable.getRsColumn().front();
1617 const auto pBub = rsconstTable.getPbubColumn().front();
1618
1619 const auto& units = eclipseState.getUnits();
1620
1621 OpmLog::info(fmt::format("Using RSCONST keyword: Rs = {:.2} [{}], Pb = {:.2} [{}]",
1622 units.from_si(UnitSystem::measure::gas_oil_ratio, rsConst),
1623 units.name (UnitSystem::measure::gas_oil_ratio),
1624 units.from_si(UnitSystem::measure::pressure, pBub),
1625 units.name (UnitSystem::measure::pressure)));
1626
1627 for (std::size_t i = 0; i < rec.size(); ++i) {
1628 rsFunc_.push_back(std::make_shared<Miscibility::RsConst<FluidSystem>>(rsConst, pBub));
1629 }
1630 }
1631 }
1632 else {
1633 for (std::size_t i = 0; i < rec.size(); ++i) {
1634 // Normal dead oil (no dissolved gas and rsconst)
1635 rsFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1636 }
1637 }
1638
1639 rvFunc_.reserve(rec.size());
1640 if (FluidSystem::enableVaporizedOil()) {
1641 for (std::size_t i = 0; i < rec.size(); ++i) {
1642 if (eqlmap.cells(i).empty()) {
1643 rvFunc_.push_back(std::shared_ptr<Miscibility::RvVD<FluidSystem>>());
1644 continue;
1645 }
1646 const int pvtIdx = regionPvtIdx_[i];
1647 if (!rec[i].wetGasInitConstantRv()) {
1648 const TableContainer& rvvdTables = tables.getRvvdTables();
1649 const TableContainer& pdvdTables = tables.getPdvdTables();
1650
1651 if (rvvdTables.size() > 0) {
1652 const RvvdTable& rvvdTable = rvvdTables.getTable<RvvdTable>(i);
1653 auto depthColumn = getArray(rvvdTable.getColumn("DEPTH").vectorCopy());
1654 auto rvColumn = getArray(rvvdTable.getColumn("RV").vectorCopy());
1655 rvFunc_.push_back(std::make_shared<Miscibility::RvVD<FluidSystem>>(pvtIdx,
1656 depthColumn, rvColumn));
1657 } else if (pdvdTables.size() > 0) {
1658 const PdvdTable& pdvdTable = pdvdTables.getTable<PdvdTable>(i);
1659 auto depthColumn = getArray(pdvdTable.getColumn("DEPTH").vectorCopy());
1660 auto pdewColumn = getArray(pdvdTable.getColumn("PDEW").vectorCopy());
1661 rvFunc_.push_back(std::make_shared<Miscibility::PDVD<FluidSystem>>(pvtIdx,
1662 depthColumn, pdewColumn));
1663 } else {
1664 throw std::runtime_error("Cannot initialise: RVVD or PDCD table not available.");
1665 }
1666 }
1667 else {
1668 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1669 throw std::runtime_error(
1670 "Cannot initialise: when no explicit RVVD table is given, \n"
1671 "datum depth must be at the gas-oil-contact. "
1672 "In EQUIL region "+std::to_string(i + 1)+" (counting from 1), this does not hold.");
1673 }
1674 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].gasOilContactCapillaryPressure();
1675 const Scalar TContact = 273.15 + 20; // standard temperature for now
1676 rvFunc_.push_back(std::make_shared<Miscibility::RvSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1677 }
1678 }
1679 }
1680 else {
1681 for (std::size_t i = 0; i < rec.size(); ++i) {
1682 rvFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1683 }
1684 }
1685
1686 rvwFunc_.reserve(rec.size());
1687 if (FluidSystem::enableVaporizedWater()) {
1688 for (std::size_t i = 0; i < rec.size(); ++i) {
1689 if (eqlmap.cells(i).empty()) {
1690 rvwFunc_.push_back(std::shared_ptr<Miscibility::RvwVD<FluidSystem>>());
1691 continue;
1692 }
1693 const int pvtIdx = regionPvtIdx_[i];
1694 if (!rec[i].humidGasInitConstantRvw()) {
1695 const TableContainer& rvwvdTables = tables.getRvwvdTables();
1696
1697 if (rvwvdTables.size() > 0) {
1698 const RvwvdTable& rvwvdTable = rvwvdTables.getTable<RvwvdTable>(i);
1699 auto depthColumn = getArray(rvwvdTable.getColumn("DEPTH").vectorCopy());
1700 auto rvwvdColumn = getArray(rvwvdTable.getColumn("RVWVD").vectorCopy());
1701 rvwFunc_.push_back(std::make_shared<Miscibility::RvwVD<FluidSystem>>(pvtIdx,
1702 depthColumn, rvwvdColumn));
1703 } else {
1704 throw std::runtime_error("Cannot initialise: RVWVD table not available.");
1705 }
1706 }
1707 else {
1708 const auto oilActive = FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1709 if (oilActive) {
1710 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1711 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1712 const auto msg = "No explicit RVWVD table is given for EQUIL region " + std::to_string(i + 1) +". \n"
1713 "and datum depth is not at the gas-oil-contact. \n"
1714 "Rvw is set to 0.0 in all cells. \n";
1715 OpmLog::warning(msg);
1716 } else {
1717 // pg = po + Pcgo = po + (pg - po)
1718 // for gas-condensate with initial no oil zone: water-oil contact depth (OWC) equal gas-oil contact depth (GOC)
1719 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].gasOilContactCapillaryPressure();
1720 const Scalar TContact = 273.15 + 20; // standard temperature for now
1721 rvwFunc_.push_back(std::make_shared<Miscibility::RvwSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1722 }
1723 }
1724 else {
1725 // two-phase gas-water sytem: water-oil contact depth is taken equal to gas-water contact depth (GWC)
1726 // and water-oil capillary pressure (Pcwo) is taken equal to gas-water capillary pressure (Pcgw) at GWC
1727 if (rec[i].waterOilContactDepth() != rec[i].datumDepth()) {
1728 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1729 const auto msg = "No explicit RVWVD table is given for EQUIL region " + std::to_string(i + 1) +". \n"
1730 "and datum depth is not at the gas-water-contact. \n"
1731 "Rvw is set to 0.0 in all cells. \n";
1732 OpmLog::warning(msg);
1733 } else {
1734 // pg = pw + Pcgw = pw + (pg - pw)
1735 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].waterOilContactCapillaryPressure();
1736 const Scalar TContact = 273.15 + 20; // standard temperature for now
1737 rvwFunc_.push_back(std::make_shared<Miscibility::RvwSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1738 }
1739 }
1740 }
1741 }
1742 }
1743 else {
1744 for (std::size_t i = 0; i < rec.size(); ++i) {
1745 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1746 }
1747 }
1748
1749 // EXTRACT the initial temperature
1750 updateInitialTemperature_(eclipseState, eqlmap);
1751
1752 // EXTRACT the initial salt concentration
1753 updateInitialSaltConcentration_(eclipseState, eqlmap);
1754
1755 // EXTRACT the initial salt saturation
1756 updateInitialSaltSaturation_(eclipseState, eqlmap);
1757
1758 // Compute pressures, saturations, rs and rv factors.
1759 const auto& comm = grid.comm();
1760 calcPressSatRsRv(eqlmap, rec, materialLawManager, gridView, comm, grav);
1761
1762 // modify the pressure and saturation for numerical aquifer cells
1763 applyNumericalAquifers_(gridView, num_aquifers,
1764 eclipseState.runspec().co2Storage() ||
1765 eclipseState.runspec().h2Storage());
1766
1767 // Modify oil pressure in no-oil regions so that the pressures of present phases can
1768 // be recovered from the oil pressure and capillary relations.
1769}
1770
1771template<class FluidSystem,
1772 class Grid,
1773 class GridView,
1774 class ElementMapper,
1775 class CartesianIndexMapper>
1776template<class RMap>
1777void InitialStateComputer<FluidSystem,
1778 Grid,
1779 GridView,
1780 ElementMapper,
1781 CartesianIndexMapper>::
1782updateInitialTemperature_(const EclipseState& eclState, const RMap& reg)
1783{
1784 const int numEquilReg = rsFunc_.size();
1785 tempVdTable_.resize(numEquilReg);
1786 const auto& tables = eclState.getTableManager();
1787 if (!tables.hasTables("RTEMPVD")) {
1788 std::vector<Scalar> x = {0.0,1.0};
1789 std::vector<Scalar> y = {static_cast<Scalar>(tables.rtemp()),
1790 static_cast<Scalar>(tables.rtemp())};
1791 for (auto& table : this->tempVdTable_) {
1792 table.setXYContainers(x, y);
1793 }
1794 } else {
1795 const TableContainer& tempvdTables = tables.getRtempvdTables();
1796 for (std::size_t i = 0; i < tempvdTables.size(); ++i) {
1797 const RtempvdTable& tempvdTable = tempvdTables.getTable<RtempvdTable>(i);
1798 tempVdTable_[i].setXYContainers(tempvdTable.getDepthColumn(), tempvdTable.getTemperatureColumn());
1799 const auto& cells = reg.cells(i);
1800 for (const auto& cell : cells) {
1801 const Scalar depth = cellCenterDepth_[cell];
1802 this->temperature_[cell] = tempVdTable_[i].eval(depth, /*extrapolate=*/true);
1803 }
1804 }
1805 }
1806}
1807
1808template<class FluidSystem,
1809 class Grid,
1810 class GridView,
1811 class ElementMapper,
1812 class CartesianIndexMapper>
1813template<class RMap>
1814void InitialStateComputer<FluidSystem,
1815 Grid,
1816 GridView,
1817 ElementMapper,
1818 CartesianIndexMapper>::
1819updateInitialSaltConcentration_(const EclipseState& eclState, const RMap& reg)
1820{
1821 const int numEquilReg = rsFunc_.size();
1822 saltVdTable_.resize(numEquilReg);
1823 const auto& tables = eclState.getTableManager();
1824 const TableContainer& saltvdTables = tables.getSaltvdTables();
1825
1826 // If no saltvd table is given, we create a trivial table for the density calculations
1827 if (saltvdTables.empty()) {
1828 std::vector<Scalar> x = {0.0,1.0};
1829 std::vector<Scalar> y = {0.0,0.0};
1830 for (auto& table : this->saltVdTable_) {
1831 table.setXYContainers(x, y);
1832 }
1833 } else {
1834 for (std::size_t i = 0; i < saltvdTables.size(); ++i) {
1835 const SaltvdTable& saltvdTable = saltvdTables.getTable<SaltvdTable>(i);
1836 saltVdTable_[i].setXYContainers(saltvdTable.getDepthColumn(), saltvdTable.getSaltColumn());
1837
1838 const auto& cells = reg.cells(i);
1839 for (const auto& cell : cells) {
1840 const Scalar depth = cellCenterDepth_[cell];
1841 this->saltConcentration_[cell] = saltVdTable_[i].eval(depth, /*extrapolate=*/true);
1842 }
1843 }
1844 }
1845}
1846
1847template<class FluidSystem,
1848 class Grid,
1849 class GridView,
1850 class ElementMapper,
1851 class CartesianIndexMapper>
1852template<class RMap>
1853void InitialStateComputer<FluidSystem,
1854 Grid,
1855 GridView,
1856 ElementMapper,
1857 CartesianIndexMapper>::
1858updateInitialSaltSaturation_(const EclipseState& eclState, const RMap& reg)
1859{
1860 const int numEquilReg = rsFunc_.size();
1861 saltpVdTable_.resize(numEquilReg);
1862 const auto& tables = eclState.getTableManager();
1863 const TableContainer& saltpvdTables = tables.getSaltpvdTables();
1864
1865 for (std::size_t i = 0; i < saltpvdTables.size(); ++i) {
1866 const SaltpvdTable& saltpvdTable = saltpvdTables.getTable<SaltpvdTable>(i);
1867 saltpVdTable_[i].setXYContainers(saltpvdTable.getDepthColumn(), saltpvdTable.getSaltpColumn());
1868
1869 const auto& cells = reg.cells(i);
1870 for (const auto& cell : cells) {
1871 const Scalar depth = cellCenterDepth_[cell];
1872 this->saltSaturation_[cell] = saltpVdTable_[i].eval(depth, /*extrapolate=*/true);
1873 }
1874 }
1875}
1876
1877template<class FluidSystem,
1878 class Grid,
1879 class GridView,
1880 class ElementMapper,
1881 class CartesianIndexMapper>
1882void InitialStateComputer<FluidSystem,
1883 Grid,
1884 GridView,
1885 ElementMapper,
1886 CartesianIndexMapper>::
1887updateCellProps_(const GridView& gridView,
1888 const NumericalAquifers& aquifer)
1889{
1890 ElementMapper elemMapper(gridView, Dune::mcmgElementLayout());
1891 int numElements = gridView.size(/*codim=*/0);
1892 cellCenterDepth_.resize(numElements);
1893 cellCenterXY_.resize(numElements);
1894 cellCorners_.resize(numElements);
1895 cellZSpan_.resize(numElements);
1896 cellZMinMax_.resize(numElements);
1897
1898 auto elemIt = gridView.template begin</*codim=*/0>();
1899 const auto& elemEndIt = gridView.template end</*codim=*/0>();
1900 const auto num_aqu_cells = aquifer.allAquiferCells();
1901 for (; elemIt != elemEndIt; ++elemIt) {
1902 const Element& element = *elemIt;
1903 const unsigned int elemIdx = elemMapper.index(element);
1904 cellCenterDepth_[elemIdx] = Details::cellCenterDepth<Scalar>(element);
1905 cellCenterXY_[elemIdx] = Details::cellCenterXY<Scalar>(element);
1906 cellCorners_[elemIdx] = Details::getCellCornerXY<Scalar>(element);
1907 const auto cartIx = cartesianIndexMapper_.cartesianIndex(elemIdx);
1908 cellZSpan_[elemIdx] = Details::cellZSpan<Scalar>(element);
1909 cellZMinMax_[elemIdx] = Details::cellZMinMax<Scalar>(element);
1910 if (!num_aqu_cells.empty()) {
1911 const auto search = num_aqu_cells.find(cartIx);
1912 if (search != num_aqu_cells.end()) {
1913 const auto* aqu_cell = num_aqu_cells.at(cartIx);
1914 const Scalar depth_change_num_aqu = aqu_cell->depth - cellCenterDepth_[elemIdx];
1915 cellCenterDepth_[elemIdx] += depth_change_num_aqu;
1916 cellZSpan_[elemIdx].first += depth_change_num_aqu;
1917 cellZSpan_[elemIdx].second += depth_change_num_aqu;
1918 cellZMinMax_[elemIdx].first += depth_change_num_aqu;
1919 cellZMinMax_[elemIdx].second += depth_change_num_aqu;
1920 }
1921 }
1922 }
1923}
1924
1925template<class FluidSystem,
1926 class Grid,
1927 class GridView,
1928 class ElementMapper,
1929 class CartesianIndexMapper>
1930void InitialStateComputer<FluidSystem,
1931 Grid,
1932 GridView,
1933 ElementMapper,
1934 CartesianIndexMapper>::
1935applyNumericalAquifers_(const GridView& gridView,
1936 const NumericalAquifers& aquifer,
1937 const bool co2store_or_h2store)
1938{
1939 const auto num_aqu_cells = aquifer.allAquiferCells();
1940 if (num_aqu_cells.empty()) return;
1941
1942 // Check if water phase is active, or in the case of CO2STORE and H2STORE, water is modelled as oil phase
1943 bool oil_as_brine = co2store_or_h2store && FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1944 const auto watPos = oil_as_brine? FluidSystem::oilPhaseIdx : FluidSystem::waterPhaseIdx;
1945 if (!FluidSystem::phaseIsActive(watPos)){
1946 throw std::logic_error { "Water phase has to be active for numerical aquifer case" };
1947 }
1948
1949 ElementMapper elemMapper(gridView, Dune::mcmgElementLayout());
1950 auto elemIt = gridView.template begin</*codim=*/0>();
1951 const auto& elemEndIt = gridView.template end</*codim=*/0>();
1952 const auto oilPos = FluidSystem::oilPhaseIdx;
1953 const auto gasPos = FluidSystem::gasPhaseIdx;
1954 for (; elemIt != elemEndIt; ++elemIt) {
1955 const Element& element = *elemIt;
1956 const unsigned int elemIdx = elemMapper.index(element);
1957 const auto cartIx = cartesianIndexMapper_.cartesianIndex(elemIdx);
1958 const auto search = num_aqu_cells.find(cartIx);
1959 if (search != num_aqu_cells.end()) {
1960 // numerical aquifer cells are filled with water initially
1961 this->sat_[watPos][elemIdx] = 1.;
1962
1963 if (!co2store_or_h2store && FluidSystem::phaseIsActive(oilPos)) {
1964 this->sat_[oilPos][elemIdx] = 0.;
1965 }
1966
1967 if (FluidSystem::phaseIsActive(gasPos)) {
1968 this->sat_[gasPos][elemIdx] = 0.;
1969 }
1970 const auto* aqu_cell = num_aqu_cells.at(cartIx);
1971 const auto msg = fmt::format("FOR AQUIFER CELL AT ({}, {}, {}) OF NUMERICAL "
1972 "AQUIFER {}, WATER SATURATION IS SET TO BE UNITY",
1973 aqu_cell->I+1, aqu_cell->J+1, aqu_cell->K+1, aqu_cell->aquifer_id);
1974 OpmLog::info(msg);
1975
1976 // if pressure is specified for numerical aquifers, we use these pressure values
1977 // for numerical aquifer cells
1978 if (aqu_cell->init_pressure) {
1979 const Scalar pres = *(aqu_cell->init_pressure);
1980 this->pp_[watPos][elemIdx] = pres;
1981 if (FluidSystem::phaseIsActive(gasPos)) {
1982 this->pp_[gasPos][elemIdx] = pres;
1983 }
1984 if (FluidSystem::phaseIsActive(oilPos)) {
1985 this->pp_[oilPos][elemIdx] = pres;
1986 }
1987 }
1988 }
1989 }
1990}
1991
1992template<class FluidSystem,
1993 class Grid,
1994 class GridView,
1995 class ElementMapper,
1996 class CartesianIndexMapper>
1997template<class RMap>
1998void InitialStateComputer<FluidSystem,
1999 Grid,
2000 GridView,
2001 ElementMapper,
2002 CartesianIndexMapper>::
2003setRegionPvtIdx(const EclipseState& eclState, const GridView& gridView, const RMap& reg)
2004{
2005 // PVTNUM is given on the (unrefined) input grid, but reg.cells(r) are leaf
2006 // cell indices. With LGRs the leaf has more cells (and a different ordering)
2007 // than the input grid, so indexing the input PVTNUM array by a leaf index is
2008 // wrong (and out of bounds for refined cells). Map PVTNUM onto the leaf via
2009 // LookUpData - a refined cell inherits its parent cell's PVTNUM - which is
2010 // the identity without LGRs. needsTranslation == true applies the 1-based ->
2011 // 0-based shift previously done explicitly.
2012 const LookUpData<typename GridView::Grid, GridView> lookUpData(gridView);
2013 const auto pvtnumData = lookUpData.template assignFieldPropsIntOnLeaf<int>(
2014 eclState.fieldProps(), "PVTNUM", /*needsTranslation=*/true);
2015
2016 for (const auto& r : reg.activeRegions()) {
2017 const auto& cells = reg.cells(r);
2018 regionPvtIdx_[r] = pvtnumData[*cells.begin()];
2019 }
2020}
2021
2022template<class FluidSystem,
2023 class Grid,
2024 class GridView,
2025 class ElementMapper,
2026 class CartesianIndexMapper>
2027template<class RMap, class MaterialLawManager, class Comm>
2028void InitialStateComputer<FluidSystem,
2029 Grid,
2030 GridView,
2031 ElementMapper,
2032 CartesianIndexMapper>::
2033calcPressSatRsRv(const RMap& reg,
2034 const std::vector<EquilRecord>& rec,
2035 MaterialLawManager& materialLawManager,
2036 const GridView& gridView,
2037 const Comm& comm,
2038 const Scalar grav)
2039{
2040 using PhaseSat = Details::PhaseSaturations<
2041 MaterialLawManager, FluidSystem, EquilReg<Scalar>, typename RMap::CellId
2042 >;
2043
2044 auto ptable = Details::PressureTable<FluidSystem, EquilReg<Scalar>>{ grav, this->num_pressure_points_ };
2045 auto psat = PhaseSat { materialLawManager, this->swatInit_ };
2046 auto vspan = std::array<Scalar, 2>{};
2047
2048 std::vector<int> regionIsEmpty(rec.size(), 0);
2049 for (std::size_t r = 0; r < rec.size(); ++r) {
2050 const auto& cells = reg.cells(r);
2051
2052 Details::verticalExtent(cells, cellZMinMax_, comm, vspan);
2053
2054 const auto acc = rec[r].initializationTargetAccuracy();
2055 if (acc > 0) {
2056 // The grid blocks are treated as being tilted
2057 // First check if the region has cells
2058 if (cells.empty()) {
2059 regionIsEmpty[r] = 1;
2060 continue;
2061 }
2062 const auto eqreg = EquilReg {
2063 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
2064 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
2065 };
2066 // Ensure contacts are within the span
2067 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
2068 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
2069 ptable.equilibrate(eqreg, vspan);
2070 // For titled blocks, we can use a simple weightening based on title of the grid
2071 // this->equilibrateTiltedFaultBlockSimple(cells, eqreg, gridView, acc, ptable, psat);
2072 this->equilibrateTiltedFaultBlock(cells, eqreg, gridView, acc, ptable, psat);
2073 }
2074 else if (acc == 0) {
2075 if (cells.empty()) {
2076 regionIsEmpty[r] = 1;
2077 continue;
2078 }
2079 const auto eqreg = EquilReg {
2080 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
2081 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
2082 };
2083 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
2084 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
2085 ptable.equilibrate(eqreg, vspan);
2086 // Centre-point method
2087 this->equilibrateCellCentres(cells, eqreg, ptable, psat);
2088 }
2089 else if (acc < 0) {
2090 if (cells.empty()) {
2091 regionIsEmpty[r] = 1;
2092 continue;
2093 }
2094 const auto eqreg = EquilReg {
2095 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
2096 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
2097 };
2098 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
2099 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
2100 ptable.equilibrate(eqreg, vspan);
2101 // Horizontal subdivision
2102 this->equilibrateHorizontal(cells, eqreg, -acc, ptable, psat);
2103 }
2104 }
2105 comm.min(regionIsEmpty.data(),regionIsEmpty.size());
2106 if (comm.rank() == 0) {
2107 for (std::size_t r = 0; r < rec.size(); ++r) {
2108 if (regionIsEmpty[r]) //region is empty on all partitions
2109 OpmLog::warning("Equilibration region " + std::to_string(r + 1)
2110 + " has no active cells");
2111 }
2112 }
2113}
2114
2115template<class FluidSystem,
2116 class Grid,
2117 class GridView,
2118 class ElementMapper,
2119 class CartesianIndexMapper>
2120template<class CellRange, class EquilibrationMethod>
2121void InitialStateComputer<FluidSystem,
2122 Grid,
2123 GridView,
2124 ElementMapper,
2125 CartesianIndexMapper>::
2126cellLoop(const CellRange& cells,
2127 EquilibrationMethod&& eqmethod)
2128{
2129 const auto oilPos = FluidSystem::oilPhaseIdx;
2130 const auto gasPos = FluidSystem::gasPhaseIdx;
2131 const auto watPos = FluidSystem::waterPhaseIdx;
2132
2133 const auto oilActive = FluidSystem::phaseIsActive(oilPos);
2134 const auto gasActive = FluidSystem::phaseIsActive(gasPos);
2135 const auto watActive = FluidSystem::phaseIsActive(watPos);
2136
2137 auto pressures = Details::PhaseQuantityValue<Scalar>{};
2138 auto saturations = Details::PhaseQuantityValue<Scalar>{};
2139 Scalar Rs = 0.0;
2140 Scalar Rv = 0.0;
2141 Scalar Rvw = 0.0;
2142
2143 for (const auto& cell : cells) {
2144 eqmethod(cell, pressures, saturations, Rs, Rv, Rvw);
2145
2146 if (oilActive) {
2147 this->pp_ [oilPos][cell] = pressures.oil;
2148 this->sat_[oilPos][cell] = saturations.oil;
2149 }
2150
2151 if (gasActive) {
2152 this->pp_ [gasPos][cell] = pressures.gas;
2153 this->sat_[gasPos][cell] = saturations.gas;
2154 }
2155
2156 if (watActive) {
2157 this->pp_ [watPos][cell] = pressures.water;
2158 this->sat_[watPos][cell] = saturations.water;
2159 }
2160
2161 if (oilActive && gasActive) {
2162 this->rs_[cell] = Rs;
2163 this->rv_[cell] = Rv;
2164 }
2165
2166 if (watActive && gasActive) {
2167 this->rvw_[cell] = Rvw;
2168 }
2169 }
2170}
2171
2172template<class FluidSystem,
2173 class Grid,
2174 class GridView,
2175 class ElementMapper,
2176 class CartesianIndexMapper>
2177template<class CellRange, class PressTable, class PhaseSat>
2178void InitialStateComputer<FluidSystem,
2179 Grid,
2180 GridView,
2181 ElementMapper,
2182 CartesianIndexMapper>::
2183equilibrateCellCentres(const CellRange& cells,
2184 const EquilReg<Scalar>& eqreg,
2185 const PressTable& ptable,
2186 PhaseSat& psat)
2187{
2188 using CellPos = typename PhaseSat::Position;
2189 using CellID = std::remove_cv_t<std::remove_reference_t<
2190 decltype(std::declval<CellPos>().cell)>>;
2191 this->cellLoop(cells, [this, &eqreg, &ptable, &psat]
2192 (const CellID cell,
2193 Details::PhaseQuantityValue<Scalar>& pressures,
2194 Details::PhaseQuantityValue<Scalar>& saturations,
2195 Scalar& Rs,
2196 Scalar& Rv,
2197 Scalar& Rvw) -> void
2198 {
2199 const auto pos = CellPos {
2200 cell, cellCenterDepth_[cell]
2201 };
2202
2203 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2204 pressures = psat.correctedPhasePressures();
2205
2206 const auto temp = this->temperature_[cell];
2207
2208 Rs = eqreg.dissolutionCalculator()
2209 (pos.depth, pressures.oil, temp, saturations.gas);
2210
2211 Rv = eqreg.evaporationCalculator()
2212 (pos.depth, pressures.gas, temp, saturations.oil);
2213
2214 Rvw = eqreg.waterEvaporationCalculator()
2215 (pos.depth, pressures.gas, temp, saturations.water);
2216 });
2217}
2218
2219template<class FluidSystem,
2220 class Grid,
2221 class GridView,
2222 class ElementMapper,
2223 class CartesianIndexMapper>
2224template<class CellRange, class PressTable, class PhaseSat>
2225void InitialStateComputer<FluidSystem,
2226 Grid,
2227 GridView,
2228 ElementMapper,
2229 CartesianIndexMapper>::
2230equilibrateHorizontal(const CellRange& cells,
2231 const EquilReg<Scalar>& eqreg,
2232 const int acc,
2233 const PressTable& ptable,
2234 PhaseSat& psat)
2235{
2236 using CellPos = typename PhaseSat::Position;
2237 using CellID = std::remove_cv_t<std::remove_reference_t<
2238 decltype(std::declval<CellPos>().cell)>>;
2239
2240 this->cellLoop(cells, [this, acc, &eqreg, &ptable, &psat]
2241 (const CellID cell,
2242 Details::PhaseQuantityValue<Scalar>& pressures,
2243 Details::PhaseQuantityValue<Scalar>& saturations,
2244 Scalar& Rs,
2245 Scalar& Rv,
2246 Scalar& Rvw) -> void
2247 {
2248 pressures .reset();
2249 saturations.reset();
2250
2251 Scalar totfrac = 0.0;
2252 for (const auto& [depth, frac] : Details::horizontalSubdivision(cell, cellZSpan_[cell], acc)) {
2253 const auto pos = CellPos { cell, depth };
2254
2255 saturations.axpy(psat.deriveSaturations(pos, eqreg, ptable), frac);
2256 pressures .axpy(psat.correctedPhasePressures(), frac);
2257
2258 totfrac += frac;
2259 }
2260
2261 if (totfrac > 0.) {
2262 saturations /= totfrac;
2263 pressures /= totfrac;
2264 } else {
2265 // Fall back to centre point method for zero-thickness cells.
2266 const auto pos = CellPos {
2267 cell, cellCenterDepth_[cell]
2268 };
2269
2270 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2271 pressures = psat.correctedPhasePressures();
2272 }
2273
2274 const auto temp = this->temperature_[cell];
2275 const auto cz = cellCenterDepth_[cell];
2276
2277 Rs = eqreg.dissolutionCalculator()
2278 (cz, pressures.oil, temp, saturations.gas);
2279
2280 Rv = eqreg.evaporationCalculator()
2281 (cz, pressures.gas, temp, saturations.oil);
2282
2283 Rvw = eqreg.waterEvaporationCalculator()
2284 (cz, pressures.gas, temp, saturations.water);
2285 });
2286}
2287
2288template<class FluidSystem, class Grid, class GridView, class ElementMapper, class CartesianIndexMapper>
2289template<class CellRange, class PressTable, class PhaseSat>
2290void InitialStateComputer<FluidSystem, Grid, GridView, ElementMapper, CartesianIndexMapper>::
2291equilibrateTiltedFaultBlockSimple(const CellRange& cells,
2292 const EquilReg<Scalar>& eqreg,
2293 const GridView& gridView,
2294 const int acc,
2295 const PressTable& ptable,
2296 PhaseSat& psat)
2297{
2298 using CellPos = typename PhaseSat::Position;
2299 using CellID = std::remove_cv_t<std::remove_reference_t<
2300 decltype(std::declval<CellPos>().cell)>>;
2301
2302 this->cellLoop(cells, [this, acc, &eqreg, &ptable, &psat, &gridView]
2303 (const CellID cell,
2304 Details::PhaseQuantityValue<Scalar>& pressures,
2305 Details::PhaseQuantityValue<Scalar>& saturations,
2306 Scalar& Rs,
2307 Scalar& Rv,
2308 Scalar& Rvw) -> void
2309 {
2310 pressures.reset();
2311 saturations.reset();
2312 Scalar totalWeight = 0.0;
2313
2314 // We assume grid blocks are treated as being tilted
2315 const auto& [zmin, zmax] = cellZMinMax_[cell];
2316 const Scalar cellThickness = zmax - zmin;
2317 const Scalar halfThickness = cellThickness / 2.0;
2318
2319 // Calculate dip parameters from corner point geometry
2320 Scalar dipAngle, dipAzimuth;
2321 Details::computeBlockDip(cellCorners_[cell], dipAngle, dipAzimuth);
2322
2323 // Reference point for TVD calculations
2324 std::array<Scalar, 3> referencePoint = {
2325 cellCenterXY_[cell].first,
2326 cellCenterXY_[cell].second,
2327 cellCenterDepth_[cell]
2328 };
2329
2330 // We have acc levels within each half (upper and lower) of the block
2331 const int numLevelsPerHalf = std::min(20, acc);
2332
2333 // Create subdivisions for upper and lower halves with cross-section weighting
2334 std::vector<std::pair<Scalar, Scalar>> levels;
2335
2336 // Subdivide upper and lower halves separately
2337 for (int side = 0; side < 2; ++side) {
2338 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2339
2340 for (int i = 0; i < numLevelsPerHalf; ++i) {
2341 // Calculate depth at the center of this subdivision
2342 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2343
2344 // A simple way: we can estimate cross-section weight based on dip angle
2345 // For horizontal cells: weight = 1.0, for tilted cells: weight decreases with dip
2346 Scalar crossSectionWeight = (halfThickness / numLevelsPerHalf);
2347
2348 // Apply dip correction to weight (cross-section area decreases with dip)
2349 if (std::abs(dipAngle) > 1e-10) {
2350 crossSectionWeight /= std::cos(dipAngle);
2351 }
2352
2353 levels.emplace_back(depth, crossSectionWeight);
2354 }
2355 }
2356
2357 for (const auto& [depth, weight] : levels) {
2358 // Convert measured depth to True Vertical Depth for tilted blocks
2359 const auto& [x, y] = cellCenterXY_[cell];
2361 depth, x, y, dipAngle, dipAzimuth, referencePoint);
2362
2363 const auto pos = CellPos{cell, tvd};
2364
2365 auto localSaturations = psat.deriveSaturations(pos, eqreg, ptable);
2366 auto localPressures = psat.correctedPhasePressures();
2367
2368 // Apply cross-section weighted averaging
2369 saturations.axpy(localSaturations, weight);
2370 pressures.axpy(localPressures, weight);
2371 totalWeight += weight;
2372 }
2373
2374 // Normalize results
2375 if (totalWeight > 1e-10) {
2376 saturations /= totalWeight;
2377 pressures /= totalWeight;
2378 } else {
2379 // Fallback to center point method using TVD
2380 const auto& [x, y] = cellCenterXY_[cell];
2381 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2382 cellCenterDepth_[cell], x, y, dipAngle, dipAzimuth, referencePoint);
2383 const auto pos = CellPos{cell, tvdCenter};
2384 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2385 pressures = psat.correctedPhasePressures();
2386 }
2387
2388 // Compute solution ratios at cell center TVD
2389 const auto temp = this->temperature_[cell];
2390 const auto& [x, y] = cellCenterXY_[cell];
2391 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2392 cellCenterDepth_[cell], x, y, dipAngle, dipAzimuth, referencePoint);
2393
2394 Rs = eqreg.dissolutionCalculator()(tvdCenter, pressures.oil, temp, saturations.gas);
2395 Rv = eqreg.evaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.oil);
2396 Rvw = eqreg.waterEvaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.water);
2397 });
2398}
2399
2400template<class FluidSystem, class Grid, class GridView, class ElementMapper, class CartesianIndexMapper>
2401template<class CellRange, class PressTable, class PhaseSat>
2402void InitialStateComputer<FluidSystem, Grid, GridView, ElementMapper, CartesianIndexMapper>::
2403equilibrateTiltedFaultBlock(const CellRange& cells,
2404 const EquilReg<Scalar>& eqreg,
2405 const GridView& gridView,
2406 const int acc,
2407 const PressTable& ptable,
2408 PhaseSat& psat)
2409{
2410 using CellPos = typename PhaseSat::Position;
2411 using CellID = std::remove_cv_t<std::remove_reference_t<
2412 decltype(std::declval<CellPos>().cell)>>;
2413
2414 std::vector<typename GridView::template Codim<0>::Entity> entityMap(gridView.size(0));
2415 for (const auto& entity : entities(gridView, Dune::Codim<0>())) {
2416 CellID idx = gridView.indexSet().index(entity);
2417 entityMap[idx] = entity;
2418 }
2419
2420 // Face Area Calculation
2421 auto polygonArea = [](const std::vector<std::array<Scalar, 2>>& pts) {
2422 if (pts.size() < 3) return Scalar(0);
2423 Scalar area = 0;
2424 for (size_t i = 0; i < pts.size(); ++i) {
2425 size_t j = (i + 1) % pts.size();
2426 area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
2427 }
2428 return std::abs(area) * Scalar(0.5);
2429 };
2430
2431 // Compute horizontal cross-section at given depth
2432 auto computeCrossSectionArea = [&](const CellID cell, Scalar depth) -> Scalar {
2433 try {
2434 const auto& entity = entityMap[cell];
2435 const auto& geometry = entity.geometry();
2436 const int numCorners = geometry.corners();
2437
2438 std::vector<std::array<Scalar, 3>> corners(numCorners);
2439 for (int i = 0; i < numCorners; ++i) {
2440 const auto& corner = geometry.corner(i);
2441 corners[i] = {static_cast<Scalar>(corner[0]), static_cast<Scalar>(corner[1]), static_cast<Scalar>(corner[2])};
2442 }
2443
2444 // Find all intersections between horizontal plane and cell edges
2445 std::vector<std::array<Scalar, 2>> intersectionPoints;
2446 const Scalar tol = 1e-10;
2447
2448 // Check all edges between corners (could be optimized further)
2449 for (size_t i = 0; i < corners.size(); ++i) {
2450 for (size_t j = i + 1; j < corners.size(); ++j) {
2451 Scalar za = corners[i][2];
2452 Scalar zb = corners[j][2];
2453
2454 if ((za - depth) * (zb - depth) <= 0.0 && std::abs(za - zb) > tol) {
2455 // Edge crosses the horizontal plane
2456 Scalar t = (depth - za) / (zb - za);
2457 Scalar x = corners[i][0] + t * (corners[j][0] - corners[i][0]);
2458 Scalar y = corners[i][1] + t * (corners[j][1] - corners[i][1]);
2459 intersectionPoints.push_back({x, y});
2460 }
2461 }
2462 }
2463
2464 // Remove duplicates
2465 if (intersectionPoints.size() > 1) {
2466 auto pointsEqual = [tol](const std::array<Scalar, 2>& a, const std::array<Scalar, 2>& b) {
2467 return std::abs(a[0] - b[0]) < tol && std::abs(a[1] - b[1]) < tol;
2468 };
2469
2470 intersectionPoints.erase(
2471 std::unique(intersectionPoints.begin(), intersectionPoints.end(), pointsEqual),
2472 intersectionPoints.end()
2473 );
2474 }
2475
2476 if (intersectionPoints.size() < 3) {
2477 // No valid grid found, use fallback
2478 return 0.0;
2479 }
2480
2481 // Order points counter-clockwise around centroid
2482 Scalar cx = 0, cy = 0;
2483 for (const auto& p : intersectionPoints) {
2484 cx += p[0]; cy += p[1];
2485 }
2486 cx /= intersectionPoints.size();
2487 cy /= intersectionPoints.size();
2488
2489 // Sorting
2490 auto angleCompare = [cx, cy](const std::array<Scalar, 2>& a, const std::array<Scalar, 2>& b) {
2491 return std::atan2(a[1] - cy, a[0] - cx) < std::atan2(b[1] - cy, b[0] - cx);
2492 };
2493
2494 std::ranges::sort(intersectionPoints, angleCompare);
2495
2496 return polygonArea(intersectionPoints);
2497
2498 } catch (const std::exception& e) {
2499 return 0.0;
2500 }
2501 };
2502
2503 auto cellProcessor = [this, acc, &eqreg, &ptable, &psat, &computeCrossSectionArea]
2504 (const CellID cell,
2505 Details::PhaseQuantityValue<Scalar>& pressures,
2506 Details::PhaseQuantityValue<Scalar>& saturations,
2507 Scalar& Rs,
2508 Scalar& Rv,
2509 Scalar& Rvw) -> void
2510 {
2511 pressures.reset();
2512 saturations.reset();
2513 Scalar totalWeight = 0.0;
2514
2515 const auto& zmin = this->cellZMinMax_[cell].first;
2516 const auto& zmax = this->cellZMinMax_[cell].second;
2517 const Scalar cellThickness = zmax - zmin;
2518 const Scalar halfThickness = cellThickness / 2.0;
2519
2520 // Calculate dip parameters from corner point geometry
2521 Scalar dipAngle, dipAzimuth;
2522 Details::computeBlockDip(this->cellCorners_[cell], dipAngle, dipAzimuth);
2523
2524 // Reference point for TVD calculations
2525 std::array<Scalar, 3> referencePoint = {
2526 this->cellCenterXY_[cell].first,
2527 this->cellCenterXY_[cell].second,
2528 cellCenterDepth_[cell]
2529 };
2530
2531 // We have acc levels within each half (upper and lower) of the block
2532 const int numLevelsPerHalf = std::min(20, acc);
2533
2534 // Create subdivisions for upper and lower halves with cross-section weighting
2535 std::vector<std::pair<Scalar, Scalar>> levels;
2536
2537 // Subdivide upper and lower halves separately
2538 for (int side = 0; side < 2; ++side) {
2539 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2540
2541 for (int i = 0; i < numLevelsPerHalf; ++i) {
2542 // Calculate depth at the center of this subdivision
2543 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2544
2545 // Compute cross-section area at this depth
2546 Scalar crossSectionArea = computeCrossSectionArea(cell, depth);
2547
2548 // Weight is proportional to: area × Δz (volume element)
2549 Scalar weight = crossSectionArea * (halfThickness / numLevelsPerHalf);
2550
2551 levels.emplace_back(depth, weight);
2552 }
2553 }
2554
2555 // Maybe not necessary (for debug)
2556 bool hasValidAreas = false;
2557 for (const auto& level : levels) {
2558 if (level.second > 1e-10) {
2559 hasValidAreas = true;
2560 break;
2561 }
2562 }
2563
2564 if (!hasValidAreas) {
2565 // Fallback to dip-based weighting as used in equilibrateTiltedFaultBlockSimple
2566 levels.clear();
2567 for (int side = 0; side < 2; ++side) {
2568 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2569 for (int i = 0; i < numLevelsPerHalf; ++i) {
2570 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2571 Scalar weight = (halfThickness / numLevelsPerHalf);
2572 if (std::abs(dipAngle) > 1e-10) {
2573 weight /= std::cos(dipAngle);
2574 }
2575 levels.emplace_back(depth, weight);
2576 }
2577 }
2578 }
2579
2580 for (const auto& level : levels) {
2581 Scalar depth = level.first;
2582 Scalar weight = level.second;
2583
2584 // Convert measured depth to True Vertical Depth for tilted blocks
2585 const auto& xy = this->cellCenterXY_[cell];
2587 depth, xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2588
2589 const auto pos = CellPos{cell, tvd};
2590
2591 auto localSaturations = psat.deriveSaturations(pos, eqreg, ptable);
2592 auto localPressures = psat.correctedPhasePressures();
2593
2594 // Apply cross-section weighted averaging
2595 saturations.axpy(localSaturations, weight);
2596 pressures.axpy(localPressures, weight);
2597 totalWeight += weight;
2598 }
2599
2600 if (totalWeight > 1e-10) {
2601 saturations /= totalWeight;
2602 pressures /= totalWeight;
2603 } else {
2604 // Fallback to center point method using TVD
2605 const auto& xy = this->cellCenterXY_[cell];
2606 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2607 this->cellCenterDepth_[cell], xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2608 const auto pos = CellPos{cell, tvdCenter};
2609 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2610 pressures = psat.correctedPhasePressures();
2611 }
2612
2613 // Compute solution ratios at cell center TVD
2614 const auto temp = this->temperature_[cell];
2615 const auto& xy = this->cellCenterXY_[cell];
2616 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2617 this->cellCenterDepth_[cell], xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2618
2619 Rs = eqreg.dissolutionCalculator()(tvdCenter, pressures.oil, temp, saturations.gas);
2620 Rv = eqreg.evaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.oil);
2621 Rvw = eqreg.waterEvaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.water);
2622 };
2623
2624 this->cellLoop(cells, cellProcessor);
2625}
2626}
2627} // namespace EQUIL
2628} // namespace Opm
2629
2630#endif // OPM_INIT_STATE_EQUIL_IMPL_HPP
#define OPM_END_PARALLEL_TRY_CATCH(prefix, comm)
Catch exception and throw in a parallel try-catch clause.
Definition: DeferredLoggingErrorHelpers.hpp:197
#define OPM_BEGIN_PARALLEL_TRY_CATCH()
Macro to setup the try of a parallel try-catch.
Definition: DeferredLoggingErrorHelpers.hpp:160
Auxiliary routines that to solve the ODEs that emerge from the hydrostatic equilibrium problem.
Dune::OwnerOverlapCopyCommunication< int, int > Comm
Definition: FlexibleSolver_impl.hpp:394
Routines that actually solve the ODEs that emerge from the hydrostatic equilibrium problem.
Definition: InitStateEquil.hpp:704
Definition: InitStateEquil.hpp:151
Gas(const TabulatedFunction &tempVdTable, const RV &rv, const RVW &rvw, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:485
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:501
Definition: InitStateEquil.hpp:126
Oil(const TabulatedFunction &tempVdTable, const RS &rs, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:437
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:451
Definition: InitStateEquil.hpp:101
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:411
Water(const TabulatedFunction &tempVdTable, const TabulatedFunction &saltVdTable, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:397
Definition: InitStateEquil.hpp:399
const PhaseQuantityValue< Scalar > & deriveSaturations(const Position &x, const Region &reg, const PTable &ptable)
Definition: InitStateEquil_impl.hpp:743
PhaseSaturations(MaterialLawManager &matLawMgr, const std::vector< Scalar > &swatInit)
Definition: InitStateEquil_impl.hpp:719
Definition: InitStateEquil.hpp:180
PressureTable & operator=(const PressureTable &rhs)
Definition: InitStateEquil_impl.hpp:1164
Scalar water(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1244
Scalar gas(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1233
bool waterActive() const
Predicate for whether or not water is an active phase.
Definition: InitStateEquil_impl.hpp:1215
bool gasActive() const
Predicate for whether or not gas is an active phase.
Definition: InitStateEquil_impl.hpp:1208
Scalar oil(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1223
std::array< Scalar, 2 > VSpan
Definition: InitStateEquil.hpp:183
bool oilActive() const
Predicate for whether or not oil is an active phase.
Definition: InitStateEquil_impl.hpp:1201
typename FluidSystem::Scalar Scalar
Definition: InitStateEquil.hpp:182
void equilibrate(const Region &reg, const VSpan &span)
Definition: InitStateEquil_impl.hpp:1190
PressureTable(const Scalar gravity, const int samplePoints=2000)
Definition: InitStateEquil_impl.hpp:1134
Definition: InitStateEquil.hpp:80
Scalar operator()(const Scalar x) const
Definition: InitStateEquil_impl.hpp:360
RK4IVP(const RHS &f, const std::array< Scalar, 2 > &span, const Scalar y0, const int N)
Definition: InitStateEquil_impl.hpp:320
Definition: EquilibrationHelpers.hpp:135
Definition: EquilibrationHelpers.hpp:216
Definition: EquilibrationHelpers.hpp:269
Definition: EquilibrationHelpers.hpp:612
Definition: EquilibrationHelpers.hpp:439
Definition: EquilibrationHelpers.hpp:162
Definition: EquilibrationHelpers.hpp:500
Definition: EquilibrationHelpers.hpp:322
Definition: EquilibrationHelpers.hpp:560
Definition: EquilibrationHelpers.hpp:376
Definition: FlowGenericProblem.hpp:51
std::vector< EquilRecord > getEquil(const EclipseState &state)
Definition: InitStateEquil_impl.hpp:1438
std::vector< int > equilnum(const EclipseState &eclipseState, const GridView &gridview)
Definition: InitStateEquil_impl.hpp:1452
std::pair< Scalar, Scalar > cellZMinMax(const Element &element)
Definition: InitStateEquil_impl.hpp:187
Scalar cellCenterDepth(const Element &element)
Definition: InitStateEquil_impl.hpp:134
std::pair< Scalar, Scalar > cellZSpan(const Element &element)
Definition: InitStateEquil_impl.hpp:168
CellCornerData< Scalar > getCellCornerXY(const Element &element)
Definition: InitStateEquil_impl.hpp:257
void verticalExtent(const CellRange &cells, const std::vector< std::pair< Scalar, Scalar > > &cellZMinMax, const Parallel::Communication &comm, std::array< Scalar, 2 > &span)
Definition: InitStateEquil_impl.hpp:70
std::pair< Scalar, Scalar > cellCenterXY(const Element &element)
Definition: InitStateEquil_impl.hpp:149
Scalar calculateTrueVerticalDepth(Scalar z, Scalar x, Scalar y, Scalar dipAngle, Scalar dipAzimuth, const std::array< Scalar, 3 > &referencePoint)
Definition: InitStateEquil_impl.hpp:281
void subdivisionCentrePoints(const Scalar left, const Scalar right, const int numIntervals, std::vector< std::pair< Scalar, Scalar > > &subdiv)
Definition: InitStateEquil_impl.hpp:95
std::vector< std::pair< Scalar, Scalar > > horizontalSubdivision(const CellID cell, const std::pair< Scalar, Scalar > topbot, const int numIntervals)
Definition: InitStateEquil_impl.hpp:113
void computeBlockDip(const CellCornerData< Scalar > &cellCorners, Scalar &dipAngle, Scalar &dipAzimuth)
Definition: InitStateEquil_impl.hpp:206
Dune::Communication< MPIComm > Communication
Definition: ParallelCommunication.hpp:30
Definition: blackoilbioeffectsmodules.hh:45
std::string to_string(const ConvergenceReport::ReservoirFailure::Type t)
Definition: InitStateEquil.hpp:61
std::array< Scalar, 8 > X
Definition: InitStateEquil.hpp:62
std::array< Scalar, 8 > Y
Definition: InitStateEquil.hpp:63
std::array< Scalar, 8 > Z
Definition: InitStateEquil.hpp:64
Simple set of per-phase (named by primary component) quantities.
Definition: InitStateEquil.hpp:351
Definition: InitStateEquil.hpp:405