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
319namespace PhasePressODE {
320
321template<class FluidSystem>
323Water(const TabulatedFunction& tempVdTable,
324 const TabulatedFunction& saltVdTable,
325 const int pvtRegionIdx,
326 const Scalar normGrav)
327 : tempVdTable_(tempVdTable)
328 , saltVdTable_(saltVdTable)
329 , pvtRegionIdx_(pvtRegionIdx)
330 , g_(normGrav)
331{
332}
333
334template<class FluidSystem>
335typename Water<FluidSystem>::Scalar
337operator()(const Scalar depth,
338 const Scalar press) const
339{
340 return this->density(depth, press) * g_;
341}
342
343template<class FluidSystem>
344typename Water<FluidSystem>::Scalar
346density(const Scalar depth,
347 const Scalar press) const
348{
349 // The initializing algorithm can give depths outside the range due to numerical noise i.e. we extrapolate
350 Scalar saltConcentration = saltVdTable_.eval(depth, /*extrapolate=*/true);
351 Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
352 Scalar rho = FluidSystem::waterPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
353 temp,
354 press,
355 Scalar{0.0} /*=Rsw*/,
356 saltConcentration);
357 rho *= FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
358 return rho;
359}
360
361template<class FluidSystem, class RS>
363Oil(const TabulatedFunction& tempVdTable,
364 const RS& rs,
365 const int pvtRegionIdx,
366 const Scalar normGrav)
367 : tempVdTable_(tempVdTable)
368 , rs_(rs)
369 , pvtRegionIdx_(pvtRegionIdx)
370 , g_(normGrav)
371{
372}
373
374template<class FluidSystem, class RS>
375typename Oil<FluidSystem,RS>::Scalar
377operator()(const Scalar depth,
378 const Scalar press) const
379{
380 return this->density(depth, press) * g_;
381}
382
383template<class FluidSystem, class RS>
384typename Oil<FluidSystem,RS>::Scalar
386density(const Scalar depth,
387 const Scalar press) const
388{
389 const Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
390 Scalar rs = 0.0;
391 if (FluidSystem::enableDissolvedGas() || FluidSystem::enableConstantRs())
392 rs = rs_(depth, press, temp);
393
394 Scalar bOil = 0.0;
395 if (rs >= FluidSystem::oilPvt().saturatedGasDissolutionFactor(pvtRegionIdx_, temp, press)) {
396 bOil = FluidSystem::oilPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
397 }
398 else {
399 bOil = FluidSystem::oilPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp, press, rs);
400 }
401 Scalar rho = bOil * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_);
402 if (FluidSystem::enableDissolvedGas() || FluidSystem::enableConstantRs()) {
403 rho += rs * bOil * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
404 }
405
406 return rho;
407}
408
409template<class FluidSystem, class RV, class RVW>
411Gas(const TabulatedFunction& tempVdTable,
412 const RV& rv,
413 const RVW& rvw,
414 const int pvtRegionIdx,
415 const Scalar normGrav)
416 : tempVdTable_(tempVdTable)
417 , rv_(rv)
418 , rvw_(rvw)
419 , pvtRegionIdx_(pvtRegionIdx)
420 , g_(normGrav)
421{
422}
423
424template<class FluidSystem, class RV, class RVW>
425typename Gas<FluidSystem,RV,RVW>::Scalar
427operator()(const Scalar depth,
428 const Scalar press) const
429{
430 return this->density(depth, press) * g_;
431}
432
433template<class FluidSystem, class RV, class RVW>
434typename Gas<FluidSystem,RV,RVW>::Scalar
436density(const Scalar depth,
437 const Scalar press) const
438{
439 const Scalar temp = tempVdTable_.eval(depth, /*extrapolate=*/true);
440 Scalar rv = 0.0;
441 if (FluidSystem::enableVaporizedOil())
442 rv = rv_(depth, press, temp);
443
444 Scalar rvw = 0.0;
445 if (FluidSystem::enableVaporizedWater())
446 rvw = rvw_(depth, press, temp);
447
448 Scalar bGas = 0.0;
449
450 if (FluidSystem::enableVaporizedOil() && FluidSystem::enableVaporizedWater()) {
451 if (rv >= FluidSystem::gasPvt().saturatedOilVaporizationFactor(pvtRegionIdx_, temp, press)
452 && rvw >= FluidSystem::gasPvt().saturatedWaterVaporizationFactor(pvtRegionIdx_, temp, press))
453 {
454 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
455 } else {
456 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp, press, rv, rvw);
457 }
458 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
459 rho += rv * bGas * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_)
460 + rvw * bGas * FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
461 return rho;
462 }
463
464 if (FluidSystem::enableVaporizedOil()){
465 if (rv >= FluidSystem::gasPvt().saturatedOilVaporizationFactor(pvtRegionIdx_, temp, press)) {
466 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
467 } else {
468 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
469 temp,
470 press,
471 rv,
472 Scalar{0.0}/*=rvw*/);
473 }
474 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
475 rho += rv * bGas * FluidSystem::referenceDensity(FluidSystem::oilPhaseIdx, pvtRegionIdx_);
476 return rho;
477 }
478
479 if (FluidSystem::enableVaporizedWater()){
480 if (rvw >= FluidSystem::gasPvt().saturatedWaterVaporizationFactor(pvtRegionIdx_, temp, press)) {
481 bGas = FluidSystem::gasPvt().saturatedInverseFormationVolumeFactor(pvtRegionIdx_, temp, press);
482 }
483 else {
484 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_,
485 temp,
486 press,
487 Scalar{0.0} /*=rv*/,
488 rvw);
489 }
490 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
491 rho += rvw * bGas * FluidSystem::referenceDensity(FluidSystem::waterPhaseIdx, pvtRegionIdx_);
492 return rho;
493 }
494
495 // immiscible gas
496 bGas = FluidSystem::gasPvt().inverseFormationVolumeFactor(pvtRegionIdx_, temp,
497 press,
498 Scalar{0.0} /*=rv*/,
499 Scalar{0.0} /*=rvw*/);
500 Scalar rho = bGas * FluidSystem::referenceDensity(FluidSystem::gasPhaseIdx, pvtRegionIdx_);
501
502 return rho;
503}
504
505}
506
507template<class FluidSystem, class Region>
508template<typename PressFunc>
509void PressureTable<FluidSystem,Region>::
510checkPtr(const PressFunc* phasePress,
511 const std::string& phaseName) const
512{
513 if (phasePress != nullptr) { return; }
514
515 throw std::invalid_argument {
516 "Phase pressure function for \"" + phaseName
517 + "\" most not be null"
518 };
519}
520
521template<class FluidSystem, class Region>
522typename PressureTable<FluidSystem,Region>::Strategy
523PressureTable<FluidSystem,Region>::
524selectEquilibrationStrategy(const Region& reg) const
525{
526 if (!this->oilActive()) {
527 if (reg.datum() > reg.zwoc()) { // Datum in water zone
528 return &PressureTable::equil_WOG;
529 }
530 return &PressureTable::equil_GOW;
531 }
532
533 if (reg.datum() > reg.zwoc()) { // Datum in water zone
534 return &PressureTable::equil_WOG;
535 }
536 else if (reg.datum() < reg.zgoc()) { // Datum in gas zone
537 return &PressureTable::equil_GOW;
538 }
539 else { // Datum in oil zone
540 return &PressureTable::equil_OWG;
541 }
542}
543
544template<class FluidSystem, class Region>
545void PressureTable<FluidSystem,Region>::
546copyInPointers(const PressureTable& rhs)
547{
548 if (rhs.oil_ != nullptr) {
549 this->oil_ = std::make_unique<OPress>(*rhs.oil_);
550 }
551
552 if (rhs.gas_ != nullptr) {
553 this->gas_ = std::make_unique<GPress>(*rhs.gas_);
554 }
555
556 if (rhs.wat_ != nullptr) {
557 this->wat_ = std::make_unique<WPress>(*rhs.wat_);
558 }
559}
560
561template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
563PhaseSaturations(MaterialLawManager& matLawMgr,
564 const std::vector<Scalar>& swatInit)
565 : matLawMgr_(matLawMgr)
566 , swatInit_ (swatInit)
567{
568}
569
570template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
573 : matLawMgr_(rhs.matLawMgr_)
574 , swatInit_ (rhs.swatInit_)
575 , sat_ (rhs.sat_)
576 , press_ (rhs.press_)
577{
578 // Note: We don't need to do anything to the 'fluidState_' here.
579 this->setEvaluationPoint(*rhs.evalPt_.position,
580 *rhs.evalPt_.region,
581 *rhs.evalPt_.ptable);
582}
583
584template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
588 const Region& reg,
589 const PTable& ptable)
590{
591 this->setEvaluationPoint(x, reg, ptable);
592 this->initializePhaseQuantities();
593
594 if (ptable.gasActive()) { this->deriveGasSat(); }
595
596 if (ptable.waterActive()) { this->deriveWaterSat(); }
597
598
599 if (this->isOverlappingTransition()) {
600 this->fixUnphysicalTransition();
601 }
602
603 if (ptable.oilActive()) { this->deriveOilSat(); }
604
605 this->accountForScaledSaturations();
606
607 return this->sat_;
608}
609
610template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
612setEvaluationPoint(const Position& x,
613 const Region& reg,
614 const PTable& ptable)
615{
616 this->evalPt_.position = &x;
617 this->evalPt_.region = &reg;
618 this->evalPt_.ptable = &ptable;
619}
620
621template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
622void PhaseSaturations<MaterialLawManager,FluidSystem,Region,CellID>::
623initializePhaseQuantities()
624{
625 this->sat_.reset();
626 this->press_.reset();
627
628 const auto depth = this->evalPt_.position->depth;
629 const auto& ptable = *this->evalPt_.ptable;
630
631 if (ptable.oilActive()) {
632 this->press_.oil = ptable.oil(depth);
633 }
634
635 if (ptable.gasActive()) {
636 this->press_.gas = ptable.gas(depth);
637 }
638
639 if (ptable.waterActive()) {
640 this->press_.water = ptable.water(depth);
641 }
642}
643
644template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
645void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveOilSat()
646{
647 this->sat_.oil = 1.0 - this->sat_.water - this->sat_.gas;
648}
649
650template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
651void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveGasSat()
652{
653 auto& sg = this->sat_.gas;
654
655 const auto isIncr = true; // dPcgo/dSg >= 0 for all Sg.
656 const auto oilActive = this->evalPt_.ptable->oilActive();
657
658 if (this->isConstCapPress(this->gasPos())) {
659 // Sharp interface between phases. Can derive phase saturation
660 // directly from knowing where 'depth' of evaluation point is
661 // relative to depth of O/G contact.
662 const auto gas_contact = oilActive? this->evalPt_.region->zgoc() : this->evalPt_.region->zwoc();
663 sg = this->fromDepthTable(gas_contact,
664 this->gasPos(), isIncr);
665 }
666 else {
667 // Capillary pressure curve is non-constant, meaning there is a
668 // transition zone between the gas and oil phases. Invert capillary
669 // pressure relation
670 //
671 // Pcgo(Sg) = Pg - Po
672 //
673 // Note that Pcgo is defined to be (Pg - Po), not (Po - Pg).
674 const auto pw = oilActive? this->press_.oil : this->press_.water;
675 const auto pcgo = this->press_.gas - pw;
676 sg = this->invertCapPress(pcgo, this->gasPos(), isIncr);
677 }
678}
679
680template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
681void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::deriveWaterSat()
682{
683 auto& sw = this->sat_.water;
684
685 const auto oilActive = this->evalPt_.ptable->oilActive();
686 if (!oilActive) {
687 // for 2p gas+water we set the water saturation to 1.0 - sg
688 sw = 1.0 - this->sat_.gas;
689
690 // Honour SWATINIT by scaling the gas/water capillary pressure curve,
691 // as is done for oil/water below. Not applicable for a sharp
692 // gas/water interface (constant Pc).
693 if (! this->swatInit_.empty() && ! this->isConstCapPress(this->gasPos())) {
694 const auto pcgw = this->press_.gas - this->press_.water;
695
696 auto [swout, newSwatInit] = this->applySwatInit(pcgw);
697 if (newSwatInit) {
698 // Curve possibly changed (e.g., PPCWMAX): re-invert.
699 const auto isIncr = true; // dPcgw/dSg >= 0 for all Sg.
700 this->sat_.gas = this->invertCapPress(pcgw, this->gasPos(), isIncr);
701 sw = 1.0 - this->sat_.gas;
702 }
703 else {
704 sw = swout;
705 this->sat_.gas = 1.0 - sw;
706 }
707 }
708 }
709 else {
710 const auto isIncr = false; // dPcow/dSw <= 0 for all Sw.
711
712 if (this->isConstCapPress(this->waterPos())) {
713 // Sharp interface between phases. Can derive phase saturation
714 // directly from knowing where 'depth' of evaluation point is
715 // relative to depth of O/W contact.
716 sw = this->fromDepthTable(this->evalPt_.region->zwoc(),
717 this->waterPos(), isIncr);
718 }
719 else {
720 // Capillary pressure curve is non-constant, meaning there is a
721 // transition zone between the oil and water phases. Invert
722 // capillary pressure relation
723 //
724 // Pcow(Sw) = Po - Pw
725 //
726 // unless the model uses "SWATINIT". In the latter case, pick the
727 // saturation directly from the SWATINIT array of the pertinent
728 // cell.
729 const auto pcow = this->press_.oil - this->press_.water;
730
731 if (this->swatInit_.empty()) {
732 sw = this->invertCapPress(pcow, this->waterPos(), isIncr);
733 }
734 else {
735 auto [swout, newSwatInit] = this->applySwatInit(pcow);
736 if (newSwatInit)
737 sw = this->invertCapPress(pcow, this->waterPos(), isIncr);
738 else {
739 sw = swout;
740 }
741 }
742 }
743 }
744}
745
746template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
747void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
748fixUnphysicalTransition()
749{
750 auto& sg = this->sat_.gas;
751 auto& sw = this->sat_.water;
752
753 // Overlapping gas/oil and oil/water transition zones can lead to
754 // unphysical phase saturations when individual saturations are derived
755 // directly from inverting O/G and O/W capillary pressure curves.
756 //
757 // Recalculate phase saturations using the implied gas/water capillary
758 // pressure: Pg - Pw.
759 const auto pcgw = this->press_.gas - this->press_.water;
760 if (! this->swatInit_.empty()) {
761 // Re-scale Pc to reflect imposed sw for vanishing oil phase. This
762 // seems consistent with ECLIPSE, but fails to honour SWATINIT in
763 // case of non-trivial gas/oil capillary pressure.
764 auto [swout, newSwatInit] = this->applySwatInit(pcgw, sw);
765 if (newSwatInit){
766 const auto isIncr = false; // dPcow/dSw <= 0 for all Sw.
767 sw = this->invertCapPress(pcgw, this->waterPos(), isIncr);
768 }
769 else {
770 sw = swout;
771 }
772 }
773
774 sw = satFromSumOfPcs<FluidSystem>
775 (this->matLawMgr_, this->waterPos(), this->gasPos(),
776 this->evalPt_.position->cell, pcgw);
777 sg = 1.0 - sw;
778
779 this->fluidState_.setSaturation(this->oilPos(), 1.0 - sw - sg);
780 this->fluidState_.setSaturation(this->gasPos(), sg);
781 this->fluidState_.setSaturation(this->waterPos(), this->evalPt_
782 .ptable->waterActive() ? sw : 0.0);
783
784 // Pcgo = Pg - Po => Po = Pg - Pcgo
785 this->computeMaterialLawCapPress();
786 this->press_.oil = this->press_.gas - this->materialLawCapPressGasOil();
787}
788
789template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
790void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
791accountForScaledSaturations()
792{
793 const auto gasActive = this->evalPt_.ptable->gasActive();
794 const auto watActive = this->evalPt_.ptable->waterActive();
795 const auto oilActive = this->evalPt_.ptable->oilActive();
796
797 auto sg = gasActive? this->sat_.gas : 0.0;
798 auto sw = watActive? this->sat_.water : 0.0;
799 auto so = oilActive? this->sat_.oil : 0.0;
800
801 this->fluidState_.setSaturation(this->waterPos(), sw);
802 this->fluidState_.setSaturation(this->oilPos(), so);
803 this->fluidState_.setSaturation(this->gasPos(), sg);
804
805 const auto& scaledDrainageInfo = this->matLawMgr_
806 .oilWaterScaledEpsInfoDrainage(this->evalPt_.position->cell);
807
808 const auto thresholdSat = 1.0e-6;
809 if (watActive && ((sw + thresholdSat) > scaledDrainageInfo.Swu)) {
810 // Water saturation exceeds maximum possible value. Reset oil phase
811 // pressure to that which corresponds to maximum possible water
812 // saturation value.
813 this->fluidState_.setSaturation(this->waterPos(), scaledDrainageInfo.Swu);
814 if (oilActive) {
815 this->fluidState_.setSaturation(this->oilPos(), so + sw - scaledDrainageInfo.Swu);
816 } else if (gasActive) {
817 this->fluidState_.setSaturation(this->gasPos(), sg + sw - scaledDrainageInfo.Swu);
818 }
819 sw = scaledDrainageInfo.Swu;
820 this->computeMaterialLawCapPress();
821
822 if (oilActive) {
823 // Pcow = Po - Pw => Po = Pw + Pcow
824 this->press_.oil = this->press_.water + this->materialLawCapPressOilWater();
825 } else {
826 // Pcgw = Pg - Pw => Pg = Pw + Pcgw
827 this->press_.gas = this->press_.water + this->materialLawCapPressGasWater();
828 }
829
830 }
831 if (gasActive && ((sg + thresholdSat) > scaledDrainageInfo.Sgu)) {
832 // Gas saturation exceeds maximum possible value. Reset oil phase
833 // pressure to that which corresponds to maximum possible gas
834 // saturation value.
835 this->fluidState_.setSaturation(this->gasPos(), scaledDrainageInfo.Sgu);
836 if (oilActive) {
837 this->fluidState_.setSaturation(this->oilPos(), so + sg - scaledDrainageInfo.Sgu);
838 } else if (watActive) {
839 this->fluidState_.setSaturation(this->waterPos(), sw + sg - scaledDrainageInfo.Sgu);
840 }
841 sg = scaledDrainageInfo.Sgu;
842 this->computeMaterialLawCapPress();
843
844 if (oilActive) {
845 // Pcgo = Pg - Po => Po = Pg - Pcgo
846 this->press_.oil = this->press_.gas - this->materialLawCapPressGasOil();
847 } else {
848 // Pcgw = Pg - Pw => Pw = Pg - Pcgw
849 this->press_.water = this->press_.gas - this->materialLawCapPressGasWater();
850 }
851 }
852
853 if (watActive && ((sw - thresholdSat) < scaledDrainageInfo.Swl)) {
854 // Water saturation less than minimum possible value in cell. Reset
855 // water phase pressure to that which corresponds to minimum
856 // possible water saturation value.
857 this->fluidState_.setSaturation(this->waterPos(), scaledDrainageInfo.Swl);
858 if (oilActive) {
859 this->fluidState_.setSaturation(this->oilPos(), so + sw - scaledDrainageInfo.Swl);
860 } else if (gasActive) {
861 this->fluidState_.setSaturation(this->gasPos(), sg + sw - scaledDrainageInfo.Swl);
862 }
863 sw = scaledDrainageInfo.Swl;
864 this->computeMaterialLawCapPress();
865
866 if (oilActive) {
867 // Pcwo = Po - Pw => Pw = Po - Pcow
868 this->press_.water = this->press_.oil - this->materialLawCapPressOilWater();
869 } else {
870 // Pcgw = Pg - Pw => Pw = Pg - Pcgw
871 this->press_.water = this->press_.gas - this->materialLawCapPressGasWater();
872 }
873 }
874
875 if (gasActive && ((sg - thresholdSat) < scaledDrainageInfo.Sgl)) {
876 // Gas saturation less than minimum possible value in cell. Reset
877 // gas phase pressure to that which corresponds to minimum possible
878 // gas saturation.
879 this->fluidState_.setSaturation(this->gasPos(), scaledDrainageInfo.Sgl);
880 if (oilActive) {
881 this->fluidState_.setSaturation(this->oilPos(), so + sg - scaledDrainageInfo.Sgl);
882 } else if (watActive) {
883 this->fluidState_.setSaturation(this->waterPos(), sw + sg - scaledDrainageInfo.Sgl);
884 }
885 sg = scaledDrainageInfo.Sgl;
886 this->computeMaterialLawCapPress();
887
888 if (oilActive) {
889 // Pcgo = Pg - Po => Pg = Po + Pcgo
890 this->press_.gas = this->press_.oil + this->materialLawCapPressGasOil();
891 } else {
892 // Pcgw = Pg - Pw => Pg = Pw + Pcgw
893 this->press_.gas = this->press_.water + this->materialLawCapPressGasWater();
894 }
895 }
896}
897
898template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
899std::pair<typename FluidSystem::Scalar, bool>
900PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
901applySwatInit(const Scalar pcow)
902{
903 return this->applySwatInit(pcow, this->swatInit_[this->evalPt_.position->cell]);
904}
905
906template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
907std::pair<typename FluidSystem::Scalar, bool>
908PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
909applySwatInit(const Scalar pcow, const Scalar sw)
910{
911 return this->matLawMgr_.applySwatinit(this->evalPt_.position->cell, pcow, sw);
912}
913
914template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
915void PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
916computeMaterialLawCapPress()
917{
918 const auto& matParams = this->matLawMgr_
919 .materialLawParams(this->evalPt_.position->cell);
920
921 this->matLawCapPress_.fill(0.0);
922 MaterialLaw::capillaryPressures(this->matLawCapPress_,
923 matParams, this->fluidState_);
924}
925
926template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
927typename FluidSystem::Scalar
928PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
929materialLawCapPressGasOil() const
930{
931 return this->matLawCapPress_[this->oilPos()]
932 + this->matLawCapPress_[this->gasPos()];
933}
934
935template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
936typename FluidSystem::Scalar
937PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
938materialLawCapPressOilWater() const
939{
940 return this->matLawCapPress_[this->oilPos()]
941 - this->matLawCapPress_[this->waterPos()];
942}
943
944template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
945typename FluidSystem::Scalar
946PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
947materialLawCapPressGasWater() const
948{
949 return this->matLawCapPress_[this->gasPos()]
950 - this->matLawCapPress_[this->waterPos()];
951}
952
953template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
954bool PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
955isConstCapPress(const PhaseIdx phaseIdx) const
956{
957 return isConstPc<FluidSystem>
958 (this->matLawMgr_, phaseIdx, this->evalPt_.position->cell);
959}
960
961template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
962bool PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
963isOverlappingTransition() const
964{
965 return this->evalPt_.ptable->gasActive()
966 && this->evalPt_.ptable->waterActive()
967 && ((this->sat_.gas + this->sat_.water) > 1.0);
968}
969
970template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
971typename FluidSystem::Scalar
972PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
973fromDepthTable(const Scalar contactdepth,
974 const PhaseIdx phasePos,
975 const bool isincr) const
976{
977 return satFromDepth<FluidSystem>
978 (this->matLawMgr_, this->evalPt_.position->depth,
979 contactdepth, static_cast<int>(phasePos),
980 this->evalPt_.position->cell, isincr);
981}
982
983template <class MaterialLawManager, class FluidSystem, class Region, typename CellID>
984typename FluidSystem::Scalar
985PhaseSaturations<MaterialLawManager, FluidSystem, Region, CellID>::
986invertCapPress(const Scalar pc,
987 const PhaseIdx phasePos,
988 const bool isincr) const
989{
990 return satFromPc<FluidSystem>
991 (this->matLawMgr_, static_cast<int>(phasePos),
992 this->evalPt_.position->cell, pc, isincr);
993}
994
995template<class FluidSystem, class Region>
997PressureTable(const Scalar gravity,
998 const int samplePoints)
999 : gravity_(gravity)
1000 , nsample_(samplePoints)
1001{
1002}
1003
1004template <class FluidSystem, class Region>
1007 : gravity_(rhs.gravity_)
1008 , nsample_(rhs.nsample_)
1009{
1010 this->copyInPointers(rhs);
1011}
1012
1013template <class FluidSystem, class Region>
1016 : gravity_(rhs.gravity_)
1017 , nsample_(rhs.nsample_)
1018 , oil_ (std::move(rhs.oil_))
1019 , gas_ (std::move(rhs.gas_))
1020 , wat_ (std::move(rhs.wat_))
1021{
1022}
1023
1024template <class FluidSystem, class Region>
1028{
1029 this->gravity_ = rhs.gravity_;
1030 this->nsample_ = rhs.nsample_;
1031 this->copyInPointers(rhs);
1032
1033 return *this;
1034}
1035
1036template <class FluidSystem, class Region>
1040{
1041 this->gravity_ = rhs.gravity_;
1042 this->nsample_ = rhs.nsample_;
1043
1044 this->oil_ = std::move(rhs.oil_);
1045 this->gas_ = std::move(rhs.gas_);
1046 this->wat_ = std::move(rhs.wat_);
1047
1048 return *this;
1049}
1050
1051template <class FluidSystem, class Region>
1053equilibrate(const Region& reg,
1054 const VSpan& span)
1055{
1056 // One of the PressureTable::equil_*() member functions.
1057 auto equil = this->selectEquilibrationStrategy(reg);
1058
1059 (this->*equil)(reg, span);
1060}
1061
1062template <class FluidSystem, class Region>
1064oilActive() const
1065{
1066 return FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1067}
1068
1069template <class FluidSystem, class Region>
1071gasActive() const
1072{
1073 return FluidSystem::phaseIsActive(FluidSystem::gasPhaseIdx);
1074}
1075
1076template <class FluidSystem, class Region>
1078waterActive() const
1079{
1080 return FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx);
1081}
1082
1083template <class FluidSystem, class Region>
1084typename FluidSystem::Scalar
1086oil(const Scalar depth) const
1087{
1088 this->checkPtr(this->oil_.get(), "OIL");
1089
1090 return this->oil_->value(depth);
1091}
1092
1093template <class FluidSystem, class Region>
1094typename FluidSystem::Scalar
1096gas(const Scalar depth) const
1097{
1098 this->checkPtr(this->gas_.get(), "GAS");
1099
1100 return this->gas_->value(depth);
1101}
1102
1103
1104template <class FluidSystem, class Region>
1105typename FluidSystem::Scalar
1107water(const Scalar depth) const
1108{
1109 this->checkPtr(this->wat_.get(), "WATER");
1110
1111 return this->wat_->value(depth);
1112}
1113
1114template <class FluidSystem, class Region>
1116equil_WOG(const Region& reg, const VSpan& span)
1117{
1118 // Datum depth in water zone. Calculate phase pressure for water first,
1119 // followed by oil and gas if applicable.
1120
1121 if (! this->waterActive()) {
1122 throw std::invalid_argument {
1123 "Don't know how to interpret EQUIL datum depth in "
1124 "WATER zone in model without active water phase"
1125 };
1126 }
1127
1128 {
1129 const auto ic = typename WPress::InitCond {
1130 reg.datum(), reg.pressure()
1131 };
1132
1133 this->makeWatPressure(ic, reg, span);
1134 }
1135
1136 if (this->oilActive()) {
1137 // Pcow = Po - Pw => Po = Pw + Pcow
1138 const auto ic = typename OPress::InitCond {
1139 reg.zwoc(),
1140 this->water(reg.zwoc()) + reg.pcowWoc()
1141 };
1142
1143 this->makeOilPressure(ic, reg, span);
1144 }
1145
1146 if (this->gasActive() && this->oilActive()) {
1147 // Pcgo = Pg - Po => Pg = Po + Pcgo
1148 const auto ic = typename GPress::InitCond {
1149 reg.zgoc(),
1150 this->oil(reg.zgoc()) + reg.pcgoGoc()
1151 };
1152
1153 this->makeGasPressure(ic, reg, span);
1154 } else if (this->gasActive() && !this->oilActive()) {
1155 // No oil phase set Pg = Pw + Pcgw
1156 const auto ic = typename GPress::InitCond {
1157 reg.zwoc(), // The WOC is really the GWC for gas/water cases
1158 this->water(reg.zwoc()) + reg.pcowWoc() // Pcow(WOC) is really Pcgw(GWC) for gas/water cases
1159 };
1160 this->makeGasPressure(ic, reg, span);
1161 }
1162}
1163
1164template <class FluidSystem, class Region>
1165void PressureTable<FluidSystem, Region>::
1166equil_GOW(const Region& reg, const VSpan& span)
1167{
1168 // Datum depth in gas zone. Calculate phase pressure for gas first,
1169 // followed by oil and water if applicable.
1170
1171 if (! this->gasActive()) {
1172 throw std::invalid_argument {
1173 "Don't know how to interpret EQUIL datum depth in "
1174 "GAS zone in model without active gas phase"
1175 };
1176 }
1177
1178 {
1179 const auto ic = typename GPress::InitCond {
1180 reg.datum(), reg.pressure()
1181 };
1182
1183 this->makeGasPressure(ic, reg, span);
1184 }
1185
1186 if (this->oilActive()) {
1187 // Pcgo = Pg - Po => Po = Pg - Pcgo
1188 const auto ic = typename OPress::InitCond {
1189 reg.zgoc(),
1190 this->gas(reg.zgoc()) - reg.pcgoGoc()
1191 };
1192 this->makeOilPressure(ic, reg, span);
1193 }
1194
1195 if (this->waterActive() && this->oilActive()) {
1196 // Pcow = Po - Pw => Pw = Po - Pcow
1197 const auto ic = typename WPress::InitCond {
1198 reg.zwoc(),
1199 this->oil(reg.zwoc()) - reg.pcowWoc()
1200 };
1201
1202 this->makeWatPressure(ic, reg, span);
1203 } else if (this->waterActive() && !this->oilActive()) {
1204 // No oil phase set Pw = Pg - Pcgw
1205 const auto ic = typename WPress::InitCond {
1206 reg.zwoc(), // The WOC is really the GWC for gas/water cases
1207 this->gas(reg.zwoc()) - reg.pcowWoc() // Pcow(WOC) is really Pcgw(GWC) for gas/water cases
1208 };
1209 this->makeWatPressure(ic, reg, span);
1210 }
1211}
1212
1213template <class FluidSystem, class Region>
1214void PressureTable<FluidSystem, Region>::
1215equil_OWG(const Region& reg, const VSpan& span)
1216{
1217 // Datum depth in oil zone. Calculate phase pressure for oil first,
1218 // followed by gas and water if applicable.
1219
1220 if (! this->oilActive()) {
1221 throw std::invalid_argument {
1222 "Don't know how to interpret EQUIL datum depth in "
1223 "OIL zone in model without active oil phase"
1224 };
1225 }
1226
1227 {
1228 const auto ic = typename OPress::InitCond {
1229 reg.datum(), reg.pressure()
1230 };
1231
1232 this->makeOilPressure(ic, reg, span);
1233 }
1234
1235 if (this->waterActive()) {
1236 // Pcow = Po - Pw => Pw = Po - Pcow
1237 const auto ic = typename WPress::InitCond {
1238 reg.zwoc(),
1239 this->oil(reg.zwoc()) - reg.pcowWoc()
1240 };
1241
1242 this->makeWatPressure(ic, reg, span);
1243 }
1244
1245 if (this->gasActive()) {
1246 // Pcgo = Pg - Po => Pg = Po + Pcgo
1247 const auto ic = typename GPress::InitCond {
1248 reg.zgoc(),
1249 this->oil(reg.zgoc()) + reg.pcgoGoc()
1250 };
1251 this->makeGasPressure(ic, reg, span);
1252 }
1253}
1254
1255template <class FluidSystem, class Region>
1256void PressureTable<FluidSystem, Region>::
1257makeOilPressure(const typename OPress::InitCond& ic,
1258 const Region& reg,
1259 const VSpan& span)
1260{
1261 const auto drho = OilPressODE {
1262 reg.tempVdTable(), reg.dissolutionCalculator(),
1263 reg.pvtIdx(), this->gravity_
1264 };
1265
1266 this->oil_ = std::make_unique<OPress>(drho, ic, this->nsample_, span);
1267}
1268
1269template <class FluidSystem, class Region>
1270void PressureTable<FluidSystem, Region>::
1271makeGasPressure(const typename GPress::InitCond& ic,
1272 const Region& reg,
1273 const VSpan& span)
1274{
1275 const auto drho = GasPressODE {
1276 reg.tempVdTable(), reg.evaporationCalculator(), reg.waterEvaporationCalculator(),
1277 reg.pvtIdx(), this->gravity_
1278 };
1279
1280 this->gas_ = std::make_unique<GPress>(drho, ic, this->nsample_, span);
1281}
1282
1283template <class FluidSystem, class Region>
1284void PressureTable<FluidSystem, Region>::
1285makeWatPressure(const typename WPress::InitCond& ic,
1286 const Region& reg,
1287 const VSpan& span)
1288{
1289 const auto drho = WatPressODE {
1290 reg.tempVdTable(), reg.saltVdTable(), reg.pvtIdx(), this->gravity_
1291 };
1292
1293 this->wat_ = std::make_unique<WPress>(drho, ic, this->nsample_, span);
1294}
1295
1296}
1297
1298namespace DeckDependent {
1299
1300std::vector<EquilRecord>
1301getEquil(const EclipseState& state)
1302{
1303 const auto& init = state.getInitConfig();
1304
1305 if(!init.hasEquil()) {
1306 throw std::domain_error("Deck does not provide equilibration data.");
1307 }
1308
1309 const auto& equil = init.getEquil();
1310 return { equil.begin(), equil.end() };
1311}
1312
1313template<class GridView>
1314std::vector<int>
1315equilnum(const EclipseState& eclipseState,
1316 const GridView& gridview)
1317{
1318 std::vector<int> eqlnum(gridview.size(0), 0);
1319
1320 if (eclipseState.fieldProps().has_int("EQLNUM")) {
1321 // EQLNUM is given on the (unrefined) input grid, but the equilibration
1322 // works on the leaf grid. With LGRs the leaf has more cells than the
1323 // input grid and a different ordering, so copying the input array
1324 // directly into a leaf-sized vector misaligns it (and leaves refined
1325 // cells at region 1). LookUpData maps each leaf cell to its input-grid
1326 // origin - a refined cell inherits its parent cell's EQLNUM - which for
1327 // an unrefined grid reduces to the identity, so non-LGR cases are
1328 // unchanged. needsTranslation == true applies the 1-based -> 0-based
1329 // shift previously done by the transform.
1330 const LookUpData<typename GridView::Grid, GridView> lookUpData(gridview);
1331 eqlnum = lookUpData.template assignFieldPropsIntOnLeaf<int>(
1332 eclipseState.fieldProps(), "EQLNUM", /*needsTranslation=*/true);
1333 }
1335 const int num_regions = eclipseState.getTableManager().getEqldims().getNumEquilRegions();
1336 if (std::ranges::any_of(eqlnum, [num_regions](int n){return n >= num_regions;})) {
1337 throw std::runtime_error("Values larger than maximum Equil regions " +
1338 std::to_string(num_regions) + " provided in EQLNUM");
1339 }
1340 if (std::ranges::any_of(eqlnum, [](int n){return n < 0;})) {
1341 throw std::runtime_error("zero or negative values provided in EQLNUM");
1342 }
1343 OPM_END_PARALLEL_TRY_CATCH("Invalied EQLNUM numbers: ", gridview.comm());
1344
1345 return eqlnum;
1346}
1347
1348template<class FluidSystem,
1349 class Grid,
1350 class GridView,
1351 class ElementMapper,
1352 class CartesianIndexMapper>
1353template<class MaterialLawManager>
1354InitialStateComputer<FluidSystem,
1355 Grid,
1356 GridView,
1357 ElementMapper,
1358 CartesianIndexMapper>::
1359InitialStateComputer(MaterialLawManager& materialLawManager,
1360 const EclipseState& eclipseState,
1361 const Grid& grid,
1362 const GridView& gridView,
1363 const CartesianIndexMapper& cartMapper,
1364 const Scalar grav,
1365 const int num_pressure_points,
1366 const bool applySwatInit,
1367 const std::vector<Scalar>* editedCellDepths)
1368 : temperature_(grid.size(/*codim=*/0), eclipseState.getTableManager().rtemp()),
1369 saltConcentration_(grid.size(/*codim=*/0)),
1370 saltSaturation_(grid.size(/*codim=*/0)),
1371 pp_(FluidSystem::numPhases,
1372 std::vector<Scalar>(grid.size(/*codim=*/0))),
1373 sat_(FluidSystem::numPhases,
1374 std::vector<Scalar>(grid.size(/*codim=*/0))),
1375 rs_(grid.size(/*codim=*/0)),
1376 rv_(grid.size(/*codim=*/0)),
1377 rvw_(grid.size(/*codim=*/0)),
1378 cartesianIndexMapper_(cartMapper),
1379 num_pressure_points_(num_pressure_points)
1380{
1381 //Check for presence of kw SWATINIT
1382 if (applySwatInit) {
1383 if (eclipseState.fieldProps().has_double("SWATINIT")) {
1384 // SWATINIT is given on the (unrefined) input grid but is consumed per
1385 // leaf cell; with LGRs the leaf is larger and reordered, so map it
1386 // onto the leaf via LookUpData (a refined cell inherits its parent
1387 // cell's value; identity without LGRs).
1388 const LookUpData<Grid, GridView> lookUpData(gridView);
1389 auto input =
1390 lookUpData.assignFieldPropsDoubleOnLeaf(eclipseState.fieldProps(), "SWATINIT");
1391 if constexpr (std::is_same_v<Scalar, double>) {
1392 swatInit_ = std::move(input);
1393 } else {
1394 swatInit_.assign(input.begin(), input.end());
1395 }
1396 }
1397 }
1398
1399 // Querry cell depth, cell top-bottom.
1400 // numerical aquifer cells might be specified with different depths.
1401 const auto& num_aquifers = eclipseState.aquifer().numericalAquifers();
1402 updateCellProps_(gridView, num_aquifers, editedCellDepths);
1403
1404 // Get the equilibration records.
1405 const std::vector<EquilRecord> rec = getEquil(eclipseState);
1406 const auto& tables = eclipseState.getTableManager();
1407 // Create (inverse) region mapping.
1408 const RegionMapping<> eqlmap(equilnum(eclipseState, gridView));
1409 const int invalidRegion = -1;
1410 regionPvtIdx_.resize(rec.size(), invalidRegion);
1411 setRegionPvtIdx(eclipseState, gridView, eqlmap);
1412
1413 // Create Rs functions.
1414 rsFunc_.reserve(rec.size());
1415
1416 auto getArray = [](const std::vector<double>& input)
1417 {
1418 if constexpr (std::is_same_v<Scalar,double>) {
1419 return input;
1420 } else {
1421 std::vector<Scalar> output;
1422 output.resize(input.size());
1423 std::ranges::copy(input, output.begin());
1424 return output;
1425 }
1426 };
1427
1428 if (FluidSystem::enableDissolvedGas()) {
1429 for (std::size_t i = 0; i < rec.size(); ++i) {
1430 if (eqlmap.cells(i).empty()) {
1431 rsFunc_.push_back(std::shared_ptr<Miscibility::RsVD<FluidSystem>>());
1432 continue;
1433 }
1434 const int pvtIdx = regionPvtIdx_[i];
1435 if (!rec[i].liveOilInitConstantRs()) {
1436 const TableContainer& rsvdTables = tables.getRsvdTables();
1437 const TableContainer& pbvdTables = tables.getPbvdTables();
1438 if (rsvdTables.size() > 0) {
1439 const RsvdTable& rsvdTable = rsvdTables.getTable<RsvdTable>(i);
1440 auto depthColumn = getArray(rsvdTable.getColumn("DEPTH").vectorCopy());
1441 auto rsColumn = getArray(rsvdTable.getColumn("RS").vectorCopy());
1442 rsFunc_.push_back(std::make_shared<Miscibility::RsVD<FluidSystem>>(pvtIdx,
1443 depthColumn, rsColumn));
1444 } else if (pbvdTables.size() > 0) {
1445 const PbvdTable& pbvdTable = pbvdTables.getTable<PbvdTable>(i);
1446 auto depthColumn = getArray(pbvdTable.getColumn("DEPTH").vectorCopy());
1447 auto pbubColumn = getArray(pbvdTable.getColumn("PBUB").vectorCopy());
1448 rsFunc_.push_back(std::make_shared<Miscibility::PBVD<FluidSystem>>(pvtIdx,
1449 depthColumn, pbubColumn));
1450
1451 } else {
1452 throw std::runtime_error("Cannot initialise: RSVD or PBVD table not available.");
1453 }
1454
1455 }
1456 else {
1457 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1458 throw std::runtime_error("Cannot initialise: when no explicit RSVD table is given, \n"
1459 "datum depth must be at the gas-oil-contact. "
1460 "In EQUIL region "+std::to_string(i + 1)+" (counting from 1), this does not hold.");
1461 }
1462 const Scalar pContact = rec[i].datumDepthPressure();
1463 const Scalar TContact = 273.15 + 20; // standard temperature for now
1464 rsFunc_.push_back(std::make_shared<Miscibility::RsSatAtContact<FluidSystem>>(pvtIdx, pContact, TContact));
1465 }
1466 }
1467 }
1468 else if (FluidSystem::enableConstantRs() && tables.hasTables("RSCONST")) {
1469 const auto& rsconstTables = tables.getRsconstTables();
1470
1471 if (rsconstTables.empty()) {
1472 for (std::size_t i = 0; i < rec.size(); ++i) {
1473 // Normal dead oil (no dissolved gas and rsconst)
1474 rsFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1475 }
1476 }
1477 else {
1478 const auto& rsconstTable = rsconstTables.getTable<RsconstTable>(0);
1479
1480 const auto rsConst = rsconstTable.getRsColumn().front();
1481 const auto pBub = rsconstTable.getPbubColumn().front();
1482
1483 const auto& units = eclipseState.getUnits();
1484
1485 OpmLog::info(fmt::format("Using RSCONST keyword: Rs = {:.2} [{}], Pb = {:.2} [{}]",
1486 units.from_si(UnitSystem::measure::gas_oil_ratio, rsConst),
1487 units.name (UnitSystem::measure::gas_oil_ratio),
1488 units.from_si(UnitSystem::measure::pressure, pBub),
1489 units.name (UnitSystem::measure::pressure)));
1490
1491 for (std::size_t i = 0; i < rec.size(); ++i) {
1492 rsFunc_.push_back(std::make_shared<Miscibility::RsConst<FluidSystem>>(rsConst, pBub));
1493 }
1494 }
1495 }
1496 else {
1497 for (std::size_t i = 0; i < rec.size(); ++i) {
1498 // Normal dead oil (no dissolved gas and rsconst)
1499 rsFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1500 }
1501 }
1502
1503 rvFunc_.reserve(rec.size());
1504 if (FluidSystem::enableVaporizedOil()) {
1505 for (std::size_t i = 0; i < rec.size(); ++i) {
1506 if (eqlmap.cells(i).empty()) {
1507 rvFunc_.push_back(std::shared_ptr<Miscibility::RvVD<FluidSystem>>());
1508 continue;
1509 }
1510 const int pvtIdx = regionPvtIdx_[i];
1511 if (!rec[i].wetGasInitConstantRv()) {
1512 const TableContainer& rvvdTables = tables.getRvvdTables();
1513 const TableContainer& pdvdTables = tables.getPdvdTables();
1514
1515 if (rvvdTables.size() > 0) {
1516 const RvvdTable& rvvdTable = rvvdTables.getTable<RvvdTable>(i);
1517 auto depthColumn = getArray(rvvdTable.getColumn("DEPTH").vectorCopy());
1518 auto rvColumn = getArray(rvvdTable.getColumn("RV").vectorCopy());
1519 rvFunc_.push_back(std::make_shared<Miscibility::RvVD<FluidSystem>>(pvtIdx,
1520 depthColumn, rvColumn));
1521 } else if (pdvdTables.size() > 0) {
1522 const PdvdTable& pdvdTable = pdvdTables.getTable<PdvdTable>(i);
1523 auto depthColumn = getArray(pdvdTable.getColumn("DEPTH").vectorCopy());
1524 auto pdewColumn = getArray(pdvdTable.getColumn("PDEW").vectorCopy());
1525 rvFunc_.push_back(std::make_shared<Miscibility::PDVD<FluidSystem>>(pvtIdx,
1526 depthColumn, pdewColumn));
1527 } else {
1528 throw std::runtime_error("Cannot initialise: RVVD or PDCD table not available.");
1529 }
1530 }
1531 else {
1532 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1533 throw std::runtime_error(
1534 "Cannot initialise: when no explicit RVVD table is given, \n"
1535 "datum depth must be at the gas-oil-contact. "
1536 "In EQUIL region "+std::to_string(i + 1)+" (counting from 1), this does not hold.");
1537 }
1538 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].gasOilContactCapillaryPressure();
1539 const Scalar TContact = 273.15 + 20; // standard temperature for now
1540 rvFunc_.push_back(std::make_shared<Miscibility::RvSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1541 }
1542 }
1543 }
1544 else {
1545 for (std::size_t i = 0; i < rec.size(); ++i) {
1546 rvFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1547 }
1548 }
1549
1550 rvwFunc_.reserve(rec.size());
1551 if (FluidSystem::enableVaporizedWater()) {
1552 for (std::size_t i = 0; i < rec.size(); ++i) {
1553 if (eqlmap.cells(i).empty()) {
1554 rvwFunc_.push_back(std::shared_ptr<Miscibility::RvwVD<FluidSystem>>());
1555 continue;
1556 }
1557 const int pvtIdx = regionPvtIdx_[i];
1558 if (!rec[i].humidGasInitConstantRvw()) {
1559 const TableContainer& rvwvdTables = tables.getRvwvdTables();
1560
1561 if (rvwvdTables.size() > 0) {
1562 const RvwvdTable& rvwvdTable = rvwvdTables.getTable<RvwvdTable>(i);
1563 auto depthColumn = getArray(rvwvdTable.getColumn("DEPTH").vectorCopy());
1564 auto rvwvdColumn = getArray(rvwvdTable.getColumn("RVWVD").vectorCopy());
1565 rvwFunc_.push_back(std::make_shared<Miscibility::RvwVD<FluidSystem>>(pvtIdx,
1566 depthColumn, rvwvdColumn));
1567 } else {
1568 throw std::runtime_error("Cannot initialise: RVWVD table not available.");
1569 }
1570 }
1571 else {
1572 const auto oilActive = FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1573 if (oilActive) {
1574 if (rec[i].gasOilContactDepth() != rec[i].datumDepth()) {
1575 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1576 const auto msg = "No explicit RVWVD table is given for EQUIL region " + std::to_string(i + 1) +". \n"
1577 "and datum depth is not at the gas-oil-contact. \n"
1578 "Rvw is set to 0.0 in all cells. \n";
1579 OpmLog::warning(msg);
1580 } else {
1581 // pg = po + Pcgo = po + (pg - po)
1582 // for gas-condensate with initial no oil zone: water-oil contact depth (OWC) equal gas-oil contact depth (GOC)
1583 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].gasOilContactCapillaryPressure();
1584 const Scalar TContact = 273.15 + 20; // standard temperature for now
1585 rvwFunc_.push_back(std::make_shared<Miscibility::RvwSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1586 }
1587 }
1588 else {
1589 // two-phase gas-water sytem: water-oil contact depth is taken equal to gas-water contact depth (GWC)
1590 // and water-oil capillary pressure (Pcwo) is taken equal to gas-water capillary pressure (Pcgw) at GWC
1591 if (rec[i].waterOilContactDepth() != rec[i].datumDepth()) {
1592 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1593 const auto msg = "No explicit RVWVD table is given for EQUIL region " + std::to_string(i + 1) +". \n"
1594 "and datum depth is not at the gas-water-contact. \n"
1595 "Rvw is set to 0.0 in all cells. \n";
1596 OpmLog::warning(msg);
1597 } else {
1598 // pg = pw + Pcgw = pw + (pg - pw)
1599 const Scalar pContact = rec[i].datumDepthPressure() + rec[i].waterOilContactCapillaryPressure();
1600 const Scalar TContact = 273.15 + 20; // standard temperature for now
1601 rvwFunc_.push_back(std::make_shared<Miscibility::RvwSatAtContact<FluidSystem>>(pvtIdx,pContact, TContact));
1602 }
1603 }
1604 }
1605 }
1606 }
1607 else {
1608 for (std::size_t i = 0; i < rec.size(); ++i) {
1609 rvwFunc_.push_back(std::make_shared<Miscibility::NoMixing<Scalar>>());
1610 }
1611 }
1612
1613 // EXTRACT the initial temperature
1614 updateInitialTemperature_(eclipseState, eqlmap);
1615
1616 // EXTRACT the initial salt concentration
1617 updateInitialSaltConcentration_(eclipseState, eqlmap);
1618
1619 // EXTRACT the initial salt saturation
1620 updateInitialSaltSaturation_(eclipseState, eqlmap);
1621
1622 // Compute pressures, saturations, rs and rv factors.
1623 const auto& comm = grid.comm();
1624 calcPressSatRsRv(eqlmap, rec, materialLawManager, gridView, comm, grav);
1625
1626 // modify the pressure and saturation for numerical aquifer cells
1627 applyNumericalAquifers_(gridView, num_aquifers,
1628 eclipseState.runspec().co2Storage() ||
1629 eclipseState.runspec().h2Storage());
1630
1631 // Modify oil pressure in no-oil regions so that the pressures of present phases can
1632 // be recovered from the oil pressure and capillary relations.
1633}
1634
1635template<class FluidSystem,
1636 class Grid,
1637 class GridView,
1638 class ElementMapper,
1639 class CartesianIndexMapper>
1640template<class RMap>
1641void InitialStateComputer<FluidSystem,
1642 Grid,
1643 GridView,
1644 ElementMapper,
1645 CartesianIndexMapper>::
1646updateInitialTemperature_(const EclipseState& eclState, const RMap& reg)
1647{
1648 const int numEquilReg = rsFunc_.size();
1649 tempVdTable_.resize(numEquilReg);
1650 const auto& tables = eclState.getTableManager();
1651 if (!tables.hasTables("RTEMPVD")) {
1652 std::vector<Scalar> x = {0.0,1.0};
1653 std::vector<Scalar> y = {static_cast<Scalar>(tables.rtemp()),
1654 static_cast<Scalar>(tables.rtemp())};
1655 for (auto& table : this->tempVdTable_) {
1656 table.setXYContainers(x, y);
1657 }
1658 } else {
1659 const TableContainer& tempvdTables = tables.getRtempvdTables();
1660 for (std::size_t i = 0; i < tempvdTables.size(); ++i) {
1661 const RtempvdTable& tempvdTable = tempvdTables.getTable<RtempvdTable>(i);
1662 tempVdTable_[i].setXYContainers(tempvdTable.getDepthColumn(), tempvdTable.getTemperatureColumn());
1663 const auto& cells = reg.cells(i);
1664 for (const auto& cell : cells) {
1665 const Scalar depth = cellCenterDepth_[cell];
1666 this->temperature_[cell] = tempVdTable_[i].eval(depth, /*extrapolate=*/true);
1667 }
1668 }
1669 }
1670}
1671
1672template<class FluidSystem,
1673 class Grid,
1674 class GridView,
1675 class ElementMapper,
1676 class CartesianIndexMapper>
1677template<class RMap>
1678void InitialStateComputer<FluidSystem,
1679 Grid,
1680 GridView,
1681 ElementMapper,
1682 CartesianIndexMapper>::
1683updateInitialSaltConcentration_(const EclipseState& eclState, const RMap& reg)
1684{
1685 const int numEquilReg = rsFunc_.size();
1686 saltVdTable_.resize(numEquilReg);
1687 const auto& tables = eclState.getTableManager();
1688 const TableContainer& saltvdTables = tables.getSaltvdTables();
1689
1690 // If no saltvd table is given, we create a trivial table for the density calculations
1691 if (saltvdTables.empty()) {
1692 std::vector<Scalar> x = {0.0,1.0};
1693 std::vector<Scalar> y = {0.0,0.0};
1694 for (auto& table : this->saltVdTable_) {
1695 table.setXYContainers(x, y);
1696 }
1697 } else {
1698 for (std::size_t i = 0; i < saltvdTables.size(); ++i) {
1699 const SaltvdTable& saltvdTable = saltvdTables.getTable<SaltvdTable>(i);
1700 saltVdTable_[i].setXYContainers(saltvdTable.getDepthColumn(), saltvdTable.getSaltColumn());
1701
1702 const auto& cells = reg.cells(i);
1703 for (const auto& cell : cells) {
1704 const Scalar depth = cellCenterDepth_[cell];
1705 this->saltConcentration_[cell] = saltVdTable_[i].eval(depth, /*extrapolate=*/true);
1706 }
1707 }
1708 }
1709}
1710
1711template<class FluidSystem,
1712 class Grid,
1713 class GridView,
1714 class ElementMapper,
1715 class CartesianIndexMapper>
1716template<class RMap>
1717void InitialStateComputer<FluidSystem,
1718 Grid,
1719 GridView,
1720 ElementMapper,
1721 CartesianIndexMapper>::
1722updateInitialSaltSaturation_(const EclipseState& eclState, const RMap& reg)
1723{
1724 const int numEquilReg = rsFunc_.size();
1725 saltpVdTable_.resize(numEquilReg);
1726 const auto& tables = eclState.getTableManager();
1727 const TableContainer& saltpvdTables = tables.getSaltpvdTables();
1728
1729 for (std::size_t i = 0; i < saltpvdTables.size(); ++i) {
1730 const SaltpvdTable& saltpvdTable = saltpvdTables.getTable<SaltpvdTable>(i);
1731 saltpVdTable_[i].setXYContainers(saltpvdTable.getDepthColumn(), saltpvdTable.getSaltpColumn());
1732
1733 const auto& cells = reg.cells(i);
1734 for (const auto& cell : cells) {
1735 const Scalar depth = cellCenterDepth_[cell];
1736 this->saltSaturation_[cell] = saltpVdTable_[i].eval(depth, /*extrapolate=*/true);
1737 }
1738 }
1739}
1740
1741template<class FluidSystem,
1742 class Grid,
1743 class GridView,
1744 class ElementMapper,
1745 class CartesianIndexMapper>
1746void InitialStateComputer<FluidSystem,
1747 Grid,
1748 GridView,
1749 ElementMapper,
1750 CartesianIndexMapper>::
1751updateCellProps_(const GridView& gridView,
1752 const NumericalAquifers& aquifer,
1753 const std::vector<Scalar>* editedCellDepths)
1754{
1755 depthEdited_ = (editedCellDepths != nullptr);
1756 ElementMapper elemMapper(gridView, Dune::mcmgElementLayout());
1757 int numElements = gridView.size(/*codim=*/0);
1758 cellCenterDepth_.resize(numElements);
1759 cellCenterXY_.resize(numElements);
1760 cellCorners_.resize(numElements);
1761 cellZSpan_.resize(numElements);
1762 cellZMinMax_.resize(numElements);
1763
1764 auto elemIt = gridView.template begin</*codim=*/0>();
1765 const auto& elemEndIt = gridView.template end</*codim=*/0>();
1766 const auto num_aqu_cells = aquifer.allAquiferCells();
1767 for (; elemIt != elemEndIt; ++elemIt) {
1768 const Element& element = *elemIt;
1769 const unsigned int elemIdx = elemMapper.index(element);
1770 cellCenterDepth_[elemIdx] = Details::cellCenterDepth<Scalar>(element);
1771 cellCenterXY_[elemIdx] = Details::cellCenterXY<Scalar>(element);
1772 cellCorners_[elemIdx] = Details::getCellCornerXY<Scalar>(element);
1773 const auto cartIx = cartesianIndexMapper_.cartesianIndex(elemIdx);
1774 cellZSpan_[elemIdx] = Details::cellZSpan<Scalar>(element);
1775 cellZMinMax_[elemIdx] = Details::cellZMinMax<Scalar>(element);
1776 if (editedCellDepths) {
1777 // DEPTH edited in the EDIT section: shift the cell rigidly so
1778 // that its centre is at the edited depth.
1779 const Scalar shift = (*editedCellDepths)[elemIdx] - cellCenterDepth_[elemIdx];
1780 cellCenterDepth_[elemIdx] += shift;
1781 cellZSpan_[elemIdx].first += shift;
1782 cellZSpan_[elemIdx].second += shift;
1783 cellZMinMax_[elemIdx].first += shift;
1784 cellZMinMax_[elemIdx].second += shift;
1785 }
1786 if (!num_aqu_cells.empty()) {
1787 const auto search = num_aqu_cells.find(cartIx);
1788 if (search != num_aqu_cells.end()) {
1789 const auto* aqu_cell = num_aqu_cells.at(cartIx);
1790 const Scalar depth_change_num_aqu = aqu_cell->depth - cellCenterDepth_[elemIdx];
1791 cellCenterDepth_[elemIdx] += depth_change_num_aqu;
1792 cellZSpan_[elemIdx].first += depth_change_num_aqu;
1793 cellZSpan_[elemIdx].second += depth_change_num_aqu;
1794 cellZMinMax_[elemIdx].first += depth_change_num_aqu;
1795 cellZMinMax_[elemIdx].second += depth_change_num_aqu;
1796 }
1797 }
1798 }
1799}
1800
1801template<class FluidSystem,
1802 class Grid,
1803 class GridView,
1804 class ElementMapper,
1805 class CartesianIndexMapper>
1806void InitialStateComputer<FluidSystem,
1807 Grid,
1808 GridView,
1809 ElementMapper,
1810 CartesianIndexMapper>::
1811applyNumericalAquifers_(const GridView& gridView,
1812 const NumericalAquifers& aquifer,
1813 const bool co2store_or_h2store)
1814{
1815 const auto num_aqu_cells = aquifer.allAquiferCells();
1816 if (num_aqu_cells.empty()) return;
1817
1818 // Check if water phase is active, or in the case of CO2STORE and H2STORE, water is modelled as oil phase
1819 bool oil_as_brine = co2store_or_h2store && FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx);
1820 const auto watPos = oil_as_brine? FluidSystem::oilPhaseIdx : FluidSystem::waterPhaseIdx;
1821 if (!FluidSystem::phaseIsActive(watPos)){
1822 throw std::logic_error { "Water phase has to be active for numerical aquifer case" };
1823 }
1824
1825 ElementMapper elemMapper(gridView, Dune::mcmgElementLayout());
1826 auto elemIt = gridView.template begin</*codim=*/0>();
1827 const auto& elemEndIt = gridView.template end</*codim=*/0>();
1828 const auto oilPos = FluidSystem::oilPhaseIdx;
1829 const auto gasPos = FluidSystem::gasPhaseIdx;
1830 for (; elemIt != elemEndIt; ++elemIt) {
1831 const Element& element = *elemIt;
1832 const unsigned int elemIdx = elemMapper.index(element);
1833 const auto cartIx = cartesianIndexMapper_.cartesianIndex(elemIdx);
1834 const auto search = num_aqu_cells.find(cartIx);
1835 if (search != num_aqu_cells.end()) {
1836 // numerical aquifer cells are filled with water initially
1837 this->sat_[watPos][elemIdx] = 1.;
1838
1839 if (!co2store_or_h2store && FluidSystem::phaseIsActive(oilPos)) {
1840 this->sat_[oilPos][elemIdx] = 0.;
1841 }
1842
1843 if (FluidSystem::phaseIsActive(gasPos)) {
1844 this->sat_[gasPos][elemIdx] = 0.;
1845 }
1846 const auto* aqu_cell = num_aqu_cells.at(cartIx);
1847 const auto msg = fmt::format("FOR AQUIFER CELL AT ({}, {}, {}) OF NUMERICAL "
1848 "AQUIFER {}, WATER SATURATION IS SET TO BE UNITY",
1849 aqu_cell->I+1, aqu_cell->J+1, aqu_cell->K+1, aqu_cell->aquifer_id);
1850 OpmLog::info(msg);
1851
1852 // if pressure is specified for numerical aquifers, we use these pressure values
1853 // for numerical aquifer cells
1854 if (aqu_cell->init_pressure) {
1855 const Scalar pres = *(aqu_cell->init_pressure);
1856 this->pp_[watPos][elemIdx] = pres;
1857 if (FluidSystem::phaseIsActive(gasPos)) {
1858 this->pp_[gasPos][elemIdx] = pres;
1859 }
1860 if (FluidSystem::phaseIsActive(oilPos)) {
1861 this->pp_[oilPos][elemIdx] = pres;
1862 }
1863 }
1864 }
1865 }
1866}
1867
1868template<class FluidSystem,
1869 class Grid,
1870 class GridView,
1871 class ElementMapper,
1872 class CartesianIndexMapper>
1873template<class RMap>
1874void InitialStateComputer<FluidSystem,
1875 Grid,
1876 GridView,
1877 ElementMapper,
1878 CartesianIndexMapper>::
1879setRegionPvtIdx(const EclipseState& eclState, const GridView& gridView, const RMap& reg)
1880{
1881 // PVTNUM is given on the (unrefined) input grid, but reg.cells(r) are leaf
1882 // cell indices. With LGRs the leaf has more cells (and a different ordering)
1883 // than the input grid, so indexing the input PVTNUM array by a leaf index is
1884 // wrong (and out of bounds for refined cells). Map PVTNUM onto the leaf via
1885 // LookUpData - a refined cell inherits its parent cell's PVTNUM - which is
1886 // the identity without LGRs. needsTranslation == true applies the 1-based ->
1887 // 0-based shift previously done explicitly.
1888 const LookUpData<typename GridView::Grid, GridView> lookUpData(gridView);
1889 const auto pvtnumData = lookUpData.template assignFieldPropsIntOnLeaf<int>(
1890 eclState.fieldProps(), "PVTNUM", /*needsTranslation=*/true);
1891
1892 for (const auto& r : reg.activeRegions()) {
1893 const auto& cells = reg.cells(r);
1894 regionPvtIdx_[r] = pvtnumData[*cells.begin()];
1895 }
1896}
1897
1898template<class FluidSystem,
1899 class Grid,
1900 class GridView,
1901 class ElementMapper,
1902 class CartesianIndexMapper>
1903template<class RMap, class MaterialLawManager, class Comm>
1904void InitialStateComputer<FluidSystem,
1905 Grid,
1906 GridView,
1907 ElementMapper,
1908 CartesianIndexMapper>::
1909calcPressSatRsRv(const RMap& reg,
1910 const std::vector<EquilRecord>& rec,
1911 MaterialLawManager& materialLawManager,
1912 const GridView& gridView,
1913 const Comm& comm,
1914 const Scalar grav)
1915{
1916 using PhaseSat = Details::PhaseSaturations<
1917 MaterialLawManager, FluidSystem, EquilReg<Scalar>, typename RMap::CellId
1918 >;
1919
1920 auto ptable = Details::PressureTable<FluidSystem, EquilReg<Scalar>>{ grav, this->num_pressure_points_ };
1921 auto psat = PhaseSat { materialLawManager, this->swatInit_ };
1922 auto vspan = std::array<Scalar, 2>{};
1923
1924 std::vector<int> regionIsEmpty(rec.size(), 0);
1925 for (std::size_t r = 0; r < rec.size(); ++r) {
1926 const auto& cells = reg.cells(r);
1927
1928 Details::verticalExtent(cells, cellZMinMax_, comm, vspan);
1929
1930 const auto acc = rec[r].initializationTargetAccuracy();
1931 if (acc > 0) {
1932 if (this->depthEdited_) {
1933 OpmLog::warning("DEPTH has been edited in the EDIT section, but equilibration "
1934 "region " + std::to_string(r + 1) + " uses a tilted block "
1935 "accuracy (EQUIL item 9 > 0). The cells are shifted rigidly "
1936 "to the edited depths, but the block geometry used for the "
1937 "tilted cross-sections is not, so the result may be wrong. "
1938 "Use item 9 = 0 or < 0.");
1939 }
1940 // The grid blocks are treated as being tilted
1941 // First check if the region has cells
1942 if (cells.empty()) {
1943 regionIsEmpty[r] = 1;
1944 continue;
1945 }
1946 const auto eqreg = EquilReg {
1947 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
1948 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
1949 };
1950 // Ensure contacts are within the span
1951 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
1952 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
1953 ptable.equilibrate(eqreg, vspan);
1954 // For titled blocks, we can use a simple weightening based on title of the grid
1955 // this->equilibrateTiltedFaultBlockSimple(cells, eqreg, gridView, acc, ptable, psat);
1956 this->equilibrateTiltedFaultBlock(cells, eqreg, gridView, acc, ptable, psat);
1957 }
1958 else if (acc == 0) {
1959 if (cells.empty()) {
1960 regionIsEmpty[r] = 1;
1961 continue;
1962 }
1963 const auto eqreg = EquilReg {
1964 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
1965 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
1966 };
1967 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
1968 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
1969 ptable.equilibrate(eqreg, vspan);
1970 // Centre-point method
1971 this->equilibrateCellCentres(cells, eqreg, ptable, psat);
1972 }
1973 else if (acc < 0) {
1974 if (cells.empty()) {
1975 regionIsEmpty[r] = 1;
1976 continue;
1977 }
1978 const auto eqreg = EquilReg {
1979 rec[r], this->rsFunc_[r], this->rvFunc_[r], this->rvwFunc_[r],
1980 this->tempVdTable_[r], this->saltVdTable_[r], this->regionPvtIdx_[r]
1981 };
1982 vspan[0] = std::min(vspan[0], std::min(eqreg.zgoc(), eqreg.zwoc()));
1983 vspan[1] = std::max(vspan[1], std::max(eqreg.zgoc(), eqreg.zwoc()));
1984 ptable.equilibrate(eqreg, vspan);
1985 // Horizontal subdivision
1986 this->equilibrateHorizontal(cells, eqreg, -acc, ptable, psat);
1987 }
1988 }
1989 comm.min(regionIsEmpty.data(),regionIsEmpty.size());
1990 if (comm.rank() == 0) {
1991 for (std::size_t r = 0; r < rec.size(); ++r) {
1992 if (regionIsEmpty[r]) //region is empty on all partitions
1993 OpmLog::warning("Equilibration region " + std::to_string(r + 1)
1994 + " has no active cells");
1995 }
1996 }
1997}
1998
1999template<class FluidSystem,
2000 class Grid,
2001 class GridView,
2002 class ElementMapper,
2003 class CartesianIndexMapper>
2004template<class CellRange, class EquilibrationMethod>
2005void InitialStateComputer<FluidSystem,
2006 Grid,
2007 GridView,
2008 ElementMapper,
2009 CartesianIndexMapper>::
2010cellLoop(const CellRange& cells,
2011 EquilibrationMethod&& eqmethod)
2012{
2013 const auto oilPos = FluidSystem::oilPhaseIdx;
2014 const auto gasPos = FluidSystem::gasPhaseIdx;
2015 const auto watPos = FluidSystem::waterPhaseIdx;
2016
2017 const auto oilActive = FluidSystem::phaseIsActive(oilPos);
2018 const auto gasActive = FluidSystem::phaseIsActive(gasPos);
2019 const auto watActive = FluidSystem::phaseIsActive(watPos);
2020
2021 auto pressures = Details::PhaseQuantityValue<Scalar>{};
2022 auto saturations = Details::PhaseQuantityValue<Scalar>{};
2023 Scalar Rs = 0.0;
2024 Scalar Rv = 0.0;
2025 Scalar Rvw = 0.0;
2026
2027 for (const auto& cell : cells) {
2028 eqmethod(cell, pressures, saturations, Rs, Rv, Rvw);
2029
2030 if (oilActive) {
2031 this->pp_ [oilPos][cell] = pressures.oil;
2032 this->sat_[oilPos][cell] = saturations.oil;
2033 }
2034
2035 if (gasActive) {
2036 this->pp_ [gasPos][cell] = pressures.gas;
2037 this->sat_[gasPos][cell] = saturations.gas;
2038 }
2039
2040 if (watActive) {
2041 this->pp_ [watPos][cell] = pressures.water;
2042 this->sat_[watPos][cell] = saturations.water;
2043 }
2044
2045 if (oilActive && gasActive) {
2046 this->rs_[cell] = Rs;
2047 this->rv_[cell] = Rv;
2048 }
2049
2050 if (watActive && gasActive) {
2051 this->rvw_[cell] = Rvw;
2052 }
2053 }
2054}
2055
2056template<class FluidSystem,
2057 class Grid,
2058 class GridView,
2059 class ElementMapper,
2060 class CartesianIndexMapper>
2061template<class CellRange, class PressTable, class PhaseSat>
2062void InitialStateComputer<FluidSystem,
2063 Grid,
2064 GridView,
2065 ElementMapper,
2066 CartesianIndexMapper>::
2067equilibrateCellCentres(const CellRange& cells,
2068 const EquilReg<Scalar>& eqreg,
2069 const PressTable& ptable,
2070 PhaseSat& psat)
2071{
2072 using CellPos = typename PhaseSat::Position;
2073 using CellID = std::remove_cv_t<std::remove_reference_t<
2074 decltype(std::declval<CellPos>().cell)>>;
2075 this->cellLoop(cells, [this, &eqreg, &ptable, &psat]
2076 (const CellID cell,
2077 Details::PhaseQuantityValue<Scalar>& pressures,
2078 Details::PhaseQuantityValue<Scalar>& saturations,
2079 Scalar& Rs,
2080 Scalar& Rv,
2081 Scalar& Rvw) -> void
2082 {
2083 const auto pos = CellPos {
2084 cell, cellCenterDepth_[cell]
2085 };
2086
2087 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2088 pressures = psat.correctedPhasePressures();
2089
2090 const auto temp = this->temperature_[cell];
2091
2092 Rs = eqreg.dissolutionCalculator()
2093 (pos.depth, pressures.oil, temp, saturations.gas);
2094
2095 Rv = eqreg.evaporationCalculator()
2096 (pos.depth, pressures.gas, temp, saturations.oil);
2097
2098 Rvw = eqreg.waterEvaporationCalculator()
2099 (pos.depth, pressures.gas, temp, saturations.water);
2100 });
2101}
2102
2103template<class FluidSystem,
2104 class Grid,
2105 class GridView,
2106 class ElementMapper,
2107 class CartesianIndexMapper>
2108template<class CellRange, class PressTable, class PhaseSat>
2109void InitialStateComputer<FluidSystem,
2110 Grid,
2111 GridView,
2112 ElementMapper,
2113 CartesianIndexMapper>::
2114equilibrateHorizontal(const CellRange& cells,
2115 const EquilReg<Scalar>& eqreg,
2116 const int acc,
2117 const PressTable& ptable,
2118 PhaseSat& psat)
2119{
2120 using CellPos = typename PhaseSat::Position;
2121 using CellID = std::remove_cv_t<std::remove_reference_t<
2122 decltype(std::declval<CellPos>().cell)>>;
2123
2124 this->cellLoop(cells, [this, acc, &eqreg, &ptable, &psat]
2125 (const CellID cell,
2126 Details::PhaseQuantityValue<Scalar>& pressures,
2127 Details::PhaseQuantityValue<Scalar>& saturations,
2128 Scalar& Rs,
2129 Scalar& Rv,
2130 Scalar& Rvw) -> void
2131 {
2132 pressures .reset();
2133 saturations.reset();
2134
2135 Scalar totfrac = 0.0;
2136 for (const auto& [depth, frac] : Details::horizontalSubdivision(cell, cellZSpan_[cell], acc)) {
2137 const auto pos = CellPos { cell, depth };
2138
2139 saturations.axpy(psat.deriveSaturations(pos, eqreg, ptable), frac);
2140 pressures .axpy(psat.correctedPhasePressures(), frac);
2141
2142 totfrac += frac;
2143 }
2144
2145 if (totfrac > 0.) {
2146 saturations /= totfrac;
2147 pressures /= totfrac;
2148 } else {
2149 // Fall back to centre point method for zero-thickness cells.
2150 const auto pos = CellPos {
2151 cell, cellCenterDepth_[cell]
2152 };
2153
2154 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2155 pressures = psat.correctedPhasePressures();
2156 }
2157
2158 const auto temp = this->temperature_[cell];
2159 const auto cz = cellCenterDepth_[cell];
2160
2161 Rs = eqreg.dissolutionCalculator()
2162 (cz, pressures.oil, temp, saturations.gas);
2163
2164 Rv = eqreg.evaporationCalculator()
2165 (cz, pressures.gas, temp, saturations.oil);
2166
2167 Rvw = eqreg.waterEvaporationCalculator()
2168 (cz, pressures.gas, temp, saturations.water);
2169 });
2170}
2171
2172template<class FluidSystem, class Grid, class GridView, class ElementMapper, class CartesianIndexMapper>
2173template<class CellRange, class PressTable, class PhaseSat>
2174void InitialStateComputer<FluidSystem, Grid, GridView, ElementMapper, CartesianIndexMapper>::
2175equilibrateTiltedFaultBlockSimple(const CellRange& cells,
2176 const EquilReg<Scalar>& eqreg,
2177 const GridView& gridView,
2178 const int acc,
2179 const PressTable& ptable,
2180 PhaseSat& psat)
2181{
2182 using CellPos = typename PhaseSat::Position;
2183 using CellID = std::remove_cv_t<std::remove_reference_t<
2184 decltype(std::declval<CellPos>().cell)>>;
2185
2186 this->cellLoop(cells, [this, acc, &eqreg, &ptable, &psat, &gridView]
2187 (const CellID cell,
2188 Details::PhaseQuantityValue<Scalar>& pressures,
2189 Details::PhaseQuantityValue<Scalar>& saturations,
2190 Scalar& Rs,
2191 Scalar& Rv,
2192 Scalar& Rvw) -> void
2193 {
2194 pressures.reset();
2195 saturations.reset();
2196 Scalar totalWeight = 0.0;
2197
2198 // We assume grid blocks are treated as being tilted
2199 const auto& [zmin, zmax] = cellZMinMax_[cell];
2200 const Scalar cellThickness = zmax - zmin;
2201 const Scalar halfThickness = cellThickness / 2.0;
2202
2203 // Calculate dip parameters from corner point geometry
2204 Scalar dipAngle, dipAzimuth;
2205 Details::computeBlockDip(cellCorners_[cell], dipAngle, dipAzimuth);
2206
2207 // Reference point for TVD calculations
2208 std::array<Scalar, 3> referencePoint = {
2209 cellCenterXY_[cell].first,
2210 cellCenterXY_[cell].second,
2211 cellCenterDepth_[cell]
2212 };
2213
2214 // We have acc levels within each half (upper and lower) of the block
2215 const int numLevelsPerHalf = std::min(20, acc);
2216
2217 // Create subdivisions for upper and lower halves with cross-section weighting
2218 std::vector<std::pair<Scalar, Scalar>> levels;
2219
2220 // Subdivide upper and lower halves separately
2221 for (int side = 0; side < 2; ++side) {
2222 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2223
2224 for (int i = 0; i < numLevelsPerHalf; ++i) {
2225 // Calculate depth at the center of this subdivision
2226 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2227
2228 // A simple way: we can estimate cross-section weight based on dip angle
2229 // For horizontal cells: weight = 1.0, for tilted cells: weight decreases with dip
2230 Scalar crossSectionWeight = (halfThickness / numLevelsPerHalf);
2231
2232 // Apply dip correction to weight (cross-section area decreases with dip)
2233 if (std::abs(dipAngle) > 1e-10) {
2234 crossSectionWeight /= std::cos(dipAngle);
2235 }
2236
2237 levels.emplace_back(depth, crossSectionWeight);
2238 }
2239 }
2240
2241 for (const auto& [depth, weight] : levels) {
2242 // Convert measured depth to True Vertical Depth for tilted blocks
2243 const auto& [x, y] = cellCenterXY_[cell];
2245 depth, x, y, dipAngle, dipAzimuth, referencePoint);
2246
2247 const auto pos = CellPos{cell, tvd};
2248
2249 auto localSaturations = psat.deriveSaturations(pos, eqreg, ptable);
2250 auto localPressures = psat.correctedPhasePressures();
2251
2252 // Apply cross-section weighted averaging
2253 saturations.axpy(localSaturations, weight);
2254 pressures.axpy(localPressures, weight);
2255 totalWeight += weight;
2256 }
2257
2258 // Normalize results
2259 if (totalWeight > 1e-10) {
2260 saturations /= totalWeight;
2261 pressures /= totalWeight;
2262 } else {
2263 // Fallback to center point method using TVD
2264 const auto& [x, y] = cellCenterXY_[cell];
2265 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2266 cellCenterDepth_[cell], x, y, dipAngle, dipAzimuth, referencePoint);
2267 const auto pos = CellPos{cell, tvdCenter};
2268 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2269 pressures = psat.correctedPhasePressures();
2270 }
2271
2272 // Compute solution ratios at cell center TVD
2273 const auto temp = this->temperature_[cell];
2274 const auto& [x, y] = cellCenterXY_[cell];
2275 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2276 cellCenterDepth_[cell], x, y, dipAngle, dipAzimuth, referencePoint);
2277
2278 Rs = eqreg.dissolutionCalculator()(tvdCenter, pressures.oil, temp, saturations.gas);
2279 Rv = eqreg.evaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.oil);
2280 Rvw = eqreg.waterEvaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.water);
2281 });
2282}
2283
2284template<class FluidSystem, class Grid, class GridView, class ElementMapper, class CartesianIndexMapper>
2285template<class CellRange, class PressTable, class PhaseSat>
2286void InitialStateComputer<FluidSystem, Grid, GridView, ElementMapper, CartesianIndexMapper>::
2287equilibrateTiltedFaultBlock(const CellRange& cells,
2288 const EquilReg<Scalar>& eqreg,
2289 const GridView& gridView,
2290 const int acc,
2291 const PressTable& ptable,
2292 PhaseSat& psat)
2293{
2294 using CellPos = typename PhaseSat::Position;
2295 using CellID = std::remove_cv_t<std::remove_reference_t<
2296 decltype(std::declval<CellPos>().cell)>>;
2297
2298 std::vector<typename GridView::template Codim<0>::Entity> entityMap(gridView.size(0));
2299 for (const auto& entity : entities(gridView, Dune::Codim<0>())) {
2300 CellID idx = gridView.indexSet().index(entity);
2301 entityMap[idx] = entity;
2302 }
2303
2304 // Face Area Calculation
2305 auto polygonArea = [](const std::vector<std::array<Scalar, 2>>& pts) {
2306 if (pts.size() < 3) return Scalar(0);
2307 Scalar area = 0;
2308 for (size_t i = 0; i < pts.size(); ++i) {
2309 size_t j = (i + 1) % pts.size();
2310 area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
2311 }
2312 return std::abs(area) * Scalar(0.5);
2313 };
2314
2315 // Compute horizontal cross-section at given depth
2316 auto computeCrossSectionArea = [&](const CellID cell, Scalar depth) -> Scalar {
2317 try {
2318 const auto& entity = entityMap[cell];
2319 const auto& geometry = entity.geometry();
2320 const int numCorners = geometry.corners();
2321
2322 std::vector<std::array<Scalar, 3>> corners(numCorners);
2323 for (int i = 0; i < numCorners; ++i) {
2324 const auto& corner = geometry.corner(i);
2325 corners[i] = {static_cast<Scalar>(corner[0]), static_cast<Scalar>(corner[1]), static_cast<Scalar>(corner[2])};
2326 }
2327
2328 // Find all intersections between horizontal plane and cell edges
2329 std::vector<std::array<Scalar, 2>> intersectionPoints;
2330 const Scalar tol = 1e-10;
2331
2332 // Check all edges between corners (could be optimized further)
2333 for (size_t i = 0; i < corners.size(); ++i) {
2334 for (size_t j = i + 1; j < corners.size(); ++j) {
2335 Scalar za = corners[i][2];
2336 Scalar zb = corners[j][2];
2337
2338 if ((za - depth) * (zb - depth) <= 0.0 && std::abs(za - zb) > tol) {
2339 // Edge crosses the horizontal plane
2340 Scalar t = (depth - za) / (zb - za);
2341 Scalar x = corners[i][0] + t * (corners[j][0] - corners[i][0]);
2342 Scalar y = corners[i][1] + t * (corners[j][1] - corners[i][1]);
2343 intersectionPoints.push_back({x, y});
2344 }
2345 }
2346 }
2347
2348 // Remove duplicates
2349 if (intersectionPoints.size() > 1) {
2350 auto pointsEqual = [tol](const std::array<Scalar, 2>& a, const std::array<Scalar, 2>& b) {
2351 return std::abs(a[0] - b[0]) < tol && std::abs(a[1] - b[1]) < tol;
2352 };
2353
2354 intersectionPoints.erase(
2355 std::unique(intersectionPoints.begin(), intersectionPoints.end(), pointsEqual),
2356 intersectionPoints.end()
2357 );
2358 }
2359
2360 if (intersectionPoints.size() < 3) {
2361 // No valid grid found, use fallback
2362 return 0.0;
2363 }
2364
2365 // Order points counter-clockwise around centroid
2366 Scalar cx = 0, cy = 0;
2367 for (const auto& p : intersectionPoints) {
2368 cx += p[0]; cy += p[1];
2369 }
2370 cx /= intersectionPoints.size();
2371 cy /= intersectionPoints.size();
2372
2373 // Sorting
2374 auto angleCompare = [cx, cy](const std::array<Scalar, 2>& a, const std::array<Scalar, 2>& b) {
2375 return std::atan2(a[1] - cy, a[0] - cx) < std::atan2(b[1] - cy, b[0] - cx);
2376 };
2377
2378 std::ranges::sort(intersectionPoints, angleCompare);
2379
2380 return polygonArea(intersectionPoints);
2381
2382 } catch (const std::exception& e) {
2383 return 0.0;
2384 }
2385 };
2386
2387 auto cellProcessor = [this, acc, &eqreg, &ptable, &psat, &computeCrossSectionArea]
2388 (const CellID cell,
2389 Details::PhaseQuantityValue<Scalar>& pressures,
2390 Details::PhaseQuantityValue<Scalar>& saturations,
2391 Scalar& Rs,
2392 Scalar& Rv,
2393 Scalar& Rvw) -> void
2394 {
2395 pressures.reset();
2396 saturations.reset();
2397 Scalar totalWeight = 0.0;
2398
2399 const auto& zmin = this->cellZMinMax_[cell].first;
2400 const auto& zmax = this->cellZMinMax_[cell].second;
2401 const Scalar cellThickness = zmax - zmin;
2402 const Scalar halfThickness = cellThickness / 2.0;
2403
2404 // Calculate dip parameters from corner point geometry
2405 Scalar dipAngle, dipAzimuth;
2406 Details::computeBlockDip(this->cellCorners_[cell], dipAngle, dipAzimuth);
2407
2408 // Reference point for TVD calculations
2409 std::array<Scalar, 3> referencePoint = {
2410 this->cellCenterXY_[cell].first,
2411 this->cellCenterXY_[cell].second,
2412 cellCenterDepth_[cell]
2413 };
2414
2415 // We have acc levels within each half (upper and lower) of the block
2416 const int numLevelsPerHalf = std::min(20, acc);
2417
2418 // Create subdivisions for upper and lower halves with cross-section weighting
2419 std::vector<std::pair<Scalar, Scalar>> levels;
2420
2421 // Subdivide upper and lower halves separately
2422 for (int side = 0; side < 2; ++side) {
2423 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2424
2425 for (int i = 0; i < numLevelsPerHalf; ++i) {
2426 // Calculate depth at the center of this subdivision
2427 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2428
2429 // Compute cross-section area at this depth
2430 Scalar crossSectionArea = computeCrossSectionArea(cell, depth);
2431
2432 // Weight is proportional to: area × Δz (volume element)
2433 Scalar weight = crossSectionArea * (halfThickness / numLevelsPerHalf);
2434
2435 levels.emplace_back(depth, weight);
2436 }
2437 }
2438
2439 // Maybe not necessary (for debug)
2440 bool hasValidAreas = false;
2441 for (const auto& level : levels) {
2442 if (level.second > 1e-10) {
2443 hasValidAreas = true;
2444 break;
2445 }
2446 }
2447
2448 if (!hasValidAreas) {
2449 // Fallback to dip-based weighting as used in equilibrateTiltedFaultBlockSimple
2450 levels.clear();
2451 for (int side = 0; side < 2; ++side) {
2452 Scalar halfStart = (side == 0) ? zmin : zmin + halfThickness;
2453 for (int i = 0; i < numLevelsPerHalf; ++i) {
2454 Scalar depth = halfStart + (i + 0.5) * (halfThickness / numLevelsPerHalf);
2455 Scalar weight = (halfThickness / numLevelsPerHalf);
2456 if (std::abs(dipAngle) > 1e-10) {
2457 weight /= std::cos(dipAngle);
2458 }
2459 levels.emplace_back(depth, weight);
2460 }
2461 }
2462 }
2463
2464 for (const auto& level : levels) {
2465 Scalar depth = level.first;
2466 Scalar weight = level.second;
2467
2468 // Convert measured depth to True Vertical Depth for tilted blocks
2469 const auto& xy = this->cellCenterXY_[cell];
2471 depth, xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2472
2473 const auto pos = CellPos{cell, tvd};
2474
2475 auto localSaturations = psat.deriveSaturations(pos, eqreg, ptable);
2476 auto localPressures = psat.correctedPhasePressures();
2477
2478 // Apply cross-section weighted averaging
2479 saturations.axpy(localSaturations, weight);
2480 pressures.axpy(localPressures, weight);
2481 totalWeight += weight;
2482 }
2483
2484 if (totalWeight > 1e-10) {
2485 saturations /= totalWeight;
2486 pressures /= totalWeight;
2487 } else {
2488 // Fallback to center point method using TVD
2489 const auto& xy = this->cellCenterXY_[cell];
2490 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2491 this->cellCenterDepth_[cell], xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2492 const auto pos = CellPos{cell, tvdCenter};
2493 saturations = psat.deriveSaturations(pos, eqreg, ptable);
2494 pressures = psat.correctedPhasePressures();
2495 }
2496
2497 // Compute solution ratios at cell center TVD
2498 const auto temp = this->temperature_[cell];
2499 const auto& xy = this->cellCenterXY_[cell];
2500 Scalar tvdCenter = Details::calculateTrueVerticalDepth(
2501 this->cellCenterDepth_[cell], xy.first, xy.second, dipAngle, dipAzimuth, referencePoint);
2502
2503 Rs = eqreg.dissolutionCalculator()(tvdCenter, pressures.oil, temp, saturations.gas);
2504 Rv = eqreg.evaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.oil);
2505 Rvw = eqreg.waterEvaporationCalculator()(tvdCenter, pressures.gas, temp, saturations.water);
2506 };
2507
2508 this->cellLoop(cells, cellProcessor);
2509}
2510}
2511} // namespace EQUIL
2512} // namespace Opm
2513
2514#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:404
Routines that actually solve the ODEs that emerge from the hydrostatic equilibrium problem.
Definition: InitStateEquil.hpp:655
Definition: InitStateEquil.hpp:133
Gas(const TabulatedFunction &tempVdTable, const RV &rv, const RVW &rvw, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:411
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:427
Definition: InitStateEquil.hpp:108
Oil(const TabulatedFunction &tempVdTable, const RS &rs, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:363
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:377
Definition: InitStateEquil.hpp:83
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquil_impl.hpp:337
Water(const TabulatedFunction &tempVdTable, const TabulatedFunction &saltVdTable, const int pvtRegionIdx, const Scalar normGrav)
Definition: InitStateEquil_impl.hpp:323
Definition: InitStateEquil.hpp:350
const PhaseQuantityValue< Scalar > & deriveSaturations(const Position &x, const Region &reg, const PTable &ptable)
Definition: InitStateEquil_impl.hpp:587
PhaseSaturations(MaterialLawManager &matLawMgr, const std::vector< Scalar > &swatInit)
Definition: InitStateEquil_impl.hpp:563
Definition: InitStateEquil.hpp:162
PressureTable & operator=(const PressureTable &rhs)
Definition: InitStateEquil_impl.hpp:1027
Scalar water(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1107
Scalar gas(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1096
bool waterActive() const
Predicate for whether or not water is an active phase.
Definition: InitStateEquil_impl.hpp:1078
bool gasActive() const
Predicate for whether or not gas is an active phase.
Definition: InitStateEquil_impl.hpp:1071
Scalar oil(const Scalar depth) const
Definition: InitStateEquil_impl.hpp:1086
std::array< Scalar, 2 > VSpan
Definition: InitStateEquil.hpp:165
bool oilActive() const
Predicate for whether or not oil is an active phase.
Definition: InitStateEquil_impl.hpp:1064
typename FluidSystem::Scalar Scalar
Definition: InitStateEquil.hpp:164
void equilibrate(const Region &reg, const VSpan &span)
Definition: InitStateEquil_impl.hpp:1053
PressureTable(const Scalar gravity, const int samplePoints=2000)
Definition: InitStateEquil_impl.hpp:997
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:52
std::vector< EquilRecord > getEquil(const EclipseState &state)
Definition: InitStateEquil_impl.hpp:1301
std::vector< int > equilnum(const EclipseState &eclipseState, const GridView &gridview)
Definition: InitStateEquil_impl.hpp:1315
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:62
std::array< Scalar, 8 > X
Definition: InitStateEquil.hpp:63
std::array< Scalar, 8 > Y
Definition: InitStateEquil.hpp:64
std::array< Scalar, 8 > Z
Definition: InitStateEquil.hpp:65
Simple set of per-phase (named by primary component) quantities.
Definition: InitStateEquil.hpp:302
Definition: InitStateEquil.hpp:356