InitStateEquilComp.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 Copyright 2026 SINTEF Digital
5
6 This file is part of the Open Porous Media project (OPM).
7
8 OPM is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 OPM is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with OPM. If not, see <http://www.gnu.org/licenses/>.
20
21 Consult the COPYING file in the top-level source directory of this
22 module for the precise wording of the license and the list of
23 copyright holders.
24*/
31#ifndef OPM_INIT_STATE_EQUIL_COMP_HPP
32#define OPM_INIT_STATE_EQUIL_COMP_HPP
33
34#include <opm/common/ErrorMacros.hpp>
35#include <opm/common/OpmLog/OpmLog.hpp>
36
37#include <opm/material/common/Tabulated1DFunction.hpp>
38#include <opm/material/constraintsolvers/SaturationPressure.hpp>
39#include <opm/material/fluidstates/CompositionalFluidState.hpp>
40
41#include <opm/input/eclipse/EclipseState/Compositional/CompositionalConfig.hpp>
42#include <opm/input/eclipse/EclipseState/EclipseState.hpp>
43#include <opm/input/eclipse/EclipseState/InitConfig/Equil.hpp>
44#include <opm/input/eclipse/EclipseState/Tables/CompvdTable.hpp>
45#include <opm/input/eclipse/EclipseState/Tables/RtempvdTable.hpp>
46#include <opm/input/eclipse/EclipseState/Tables/SwfnTable.hpp>
47#include <opm/input/eclipse/EclipseState/Tables/TableContainer.hpp>
48#include <opm/input/eclipse/EclipseState/Tables/TableManager.hpp>
49#include <opm/input/eclipse/EclipseState/Tables/ZmfvdTable.hpp>
50#include <opm/input/eclipse/Units/Units.hpp>
51
55
56#include <fmt/format.h>
57#include <fmt/ranges.h>
58
59#include <algorithm>
60#include <array>
61#include <cmath>
62#include <cstddef>
63#include <functional>
64#include <iterator>
65#include <limits>
66#include <map>
67#include <numeric>
68#include <optional>
69#include <stdexcept>
70#include <string>
71#include <string_view>
72#include <utility>
73#include <vector>
74
75
77
78namespace Details {
79
83template <class Scalar>
84Scalar evalDepthTable(const Tabulated1DFunction<Scalar>& table, const Scalar depth)
85{
86 return table.eval(std::clamp(depth, table.xMin(), table.xMax()));
87}
88
93template <class FluidSystem>
95{
96public:
97 using Scalar = typename FluidSystem::Scalar;
98 using CompVec = std::array<Scalar, FluidSystem::numComponents>;
99 using CompositionFunction = std::function<CompVec(Scalar)>;
100 using TabulatedFunction = Tabulated1DFunction<Scalar>;
101
103 const TabulatedFunction& tempVdTable,
104 const unsigned phaseIdx,
105 const CompositionalConfig::EOSType eosType,
106 const Scalar normGrav)
107 : composition_(std::move(composition))
108 , tempVdTable_(tempVdTable)
109 , phaseIdx_(phaseIdx)
110 , eosType_(eosType)
111 , g_(normGrav)
112 {}
113
115 const Scalar press) const
116 {
117 const CompVec z = composition_(depth);
118 const Scalar temp = evalDepthTable(tempVdTable_, depth);
119
120 CompositionalFluidState<Scalar, FluidSystem> fs;
121 fs.setTemperature(temp);
122 fs.setPressure(FluidSystem::oilPhaseIdx, press);
123 fs.setPressure(FluidSystem::gasPhaseIdx, press);
124 for (unsigned compIdx = 0; compIdx < FluidSystem::numComponents; ++compIdx) {
125 fs.setMoleFraction(phaseIdx_, compIdx, z[compIdx]);
126 }
127
128 typename FluidSystem::template ParameterCache<Scalar> paramCache(eosType_);
129 paramCache.updatePhase(fs, phaseIdx_);
130
131 return FluidSystem::density(fs, paramCache, phaseIdx_) * g_;
132 }
133
134private:
135 CompositionFunction composition_;
136 const TabulatedFunction& tempVdTable_;
137 unsigned phaseIdx_;
138 CompositionalConfig::EOSType eosType_;
139 Scalar g_;
140};
141
144template <class FluidSystem>
146{
147public:
148 using Scalar = typename FluidSystem::Scalar;
149 using TabulatedFunction = Tabulated1DFunction<Scalar>;
150
152 const CompositionalConfig::EOSType eosType,
153 const unsigned pvtRegion,
154 const Scalar normGrav)
155 : tempVdTable_(tempVdTable)
156 , eosType_(eosType)
157 , pvtRegion_(pvtRegion)
158 , g_(normGrav)
159 {}
160
162 const Scalar press) const
163 {
164 CompositionalFluidState<Scalar, FluidSystem> fs;
165 fs.setTemperature(evalDepthTable(tempVdTable_, depth));
166 fs.setPressure(FluidSystem::waterPhaseIdx, press);
167
168 typename FluidSystem::template ParameterCache<Scalar> paramCache(eosType_);
169 paramCache.setPvtRegionIndex(pvtRegion_);
170
171 return FluidSystem::density(fs, paramCache, FluidSystem::waterPhaseIdx) * g_;
172 }
173
174private:
175 const TabulatedFunction& tempVdTable_;
176 CompositionalConfig::EOSType eosType_;
177 unsigned pvtRegion_;
178 Scalar g_;
179};
180
181} // namespace Details
182
238template <class FluidSystem>
240{
241public:
242 using Scalar = typename FluidSystem::Scalar;
243 using FluidState = CompositionalFluidState<Scalar, FluidSystem>;
244
259 InitialStateComputer(const EclipseState& inputState,
260 const CompositionalConfig::EOSType eosType,
261 const std::vector<Scalar>& cellCenterDepth,
262 const std::vector<int>& eqlnum,
263 const std::vector<int>& pvtnum,
264 const Parallel::Communication& comm,
265 const Scalar gravity,
266 const int numSamplePoints,
267 const std::vector<Scalar>& connateWater = {},
268 const std::vector<Scalar>& maxWater = {})
269 : eosType_(eosType)
270 , connateWater_(connateWater)
271 , maxWater_(maxWater)
272 {
273 const auto& records = inputState.getInitConfig().getEquil();
274 const auto& tables = inputState.getTableManager();
275
276 if (!tables.hasTables("ZMFVD") && !tables.hasTables("COMPVD")) {
277 OPM_THROW(std::runtime_error,
278 "Equilibration of a compositional model requires the composition "
279 "versus depth from the ZMFVD or the COMPVD keyword.");
280 }
281
282 const auto numPvtRegions = tables.getTabdims().getNumPVTTables();
284 if (eqlnum.size() != cellCenterDepth.size()) {
285 OPM_THROW(std::runtime_error,
286 fmt::format("EQLNUM contains {} entries for {} cell depths.",
287 eqlnum.size(), cellCenterDepth.size()));
288 }
289 for (std::size_t cell = 0; cell < eqlnum.size(); ++cell) {
290 const auto region = eqlnum[cell];
291 if (region < 0 || std::cmp_greater_equal(region, records.size())) {
292 OPM_THROW(std::runtime_error,
293 fmt::format("Cell {} has EQLNUM {} outside the {} "
294 "equilibration regions.",
295 cell, region + 1, records.size()));
296 }
297 }
298 if (!pvtnum.empty() && (pvtnum.size() != cellCenterDepth.size())) {
299 OPM_THROW(std::runtime_error,
300 fmt::format("PVTNUM contains {} entries for {} cell depths.",
301 pvtnum.size(), cellCenterDepth.size()));
302 }
303 for (std::size_t cell = 0; cell < pvtnum.size(); ++cell) {
304 const auto region = pvtnum[cell];
305 if (region < 0 || std::cmp_greater_equal(region, numPvtRegions)) {
306 OPM_THROW(std::runtime_error,
307 fmt::format("Cell {} has PVTNUM {} outside the {} "
308 "PVT regions.",
309 cell, region + 1, numPvtRegions));
310 }
311 }
312 // The endpoint vectors are optional, but a non-empty one is indexed for
313 // every cell.
314 for (const auto& [name, limits] : {std::pair{"connate water", std::cref(connateWater)},
315 std::pair{"maximum water", std::cref(maxWater)}}) {
316 if (!limits.get().empty() &&
317 (limits.get().size() != cellCenterDepth.size())) {
318 OPM_THROW(std::runtime_error,
319 fmt::format("The {} saturation has {} entries for {} cell depths.",
320 name, limits.get().size(), cellCenterDepth.size()));
321 }
322 }
323 OPM_END_PARALLEL_TRY_CATCH("Invalid equilibration input: ", comm);
324
325 const auto pvtRegions =
326 pvtRegionsOfEachRegion(eqlnum, pvtnum, records.size(), numPvtRegions, comm);
327 std::vector<Region> regions;
328 regions.reserve(records.size());
329 for (std::size_t r = 0; r < records.size(); ++r) {
330 regions.push_back(setupRegion(records.getRecord(r), tables, pvtRegions[r],
331 cellCenterDepth, eqlnum, pvtnum, comm, gravity,
332 numSamplePoints, r));
333 }
334
335 const bool waterActive = FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx);
336 fluidStates_.resize(cellCenterDepth.size());
337 referencePressures_.resize(cellCenterDepth.size());
338 twoPhaseCheckRegions_.assign(cellCenterDepth.size(), -1);
339 for (std::size_t cell = 0; cell < cellCenterDepth.size(); ++cell) {
340 const auto& reg = regions[eqlnum[cell]];
341 const auto pvtRegion = pvtnum.empty() ? 0u : static_cast<unsigned>(pvtnum[cell]);
342 referencePressures_[cell] = assignCell(fluidStates_[cell], reg,
343 pvtRegion, cellCenterDepth[cell], cell);
344 // The type 3 gas is saturated at the contact, so the lower pressure
345 // above it splits that gas slightly. Below the water-oil contact
346 // the pore space holds water.
347 if ((reg.initType == 1) && !(waterActive && (cellCenterDepth[cell] > reg.zwoc))) {
348 twoPhaseCheckRegions_[cell] = eqlnum[cell];
349 }
350 }
351 }
352
353 std::vector<FluidState>& fluidStates()
354 { return fluidStates_; }
355
356 const std::vector<FluidState>& fluidStates() const
357 { return fluidStates_; }
358
362 const std::vector<Scalar>& referencePressures() const
363 { return referencePressures_; }
364
369 std::vector<int>& twoPhaseCheckRegions()
370 { return twoPhaseCheckRegions_; }
371
372 const std::vector<int>& twoPhaseCheckRegions() const
373 { return twoPhaseCheckRegions_; }
374
375private:
376 using CompVec = std::array<Scalar, FluidSystem::numComponents>;
377 using TabulatedFunction = Tabulated1DFunction<Scalar>;
382
383 static constexpr int numComponents = FluidSystem::numComponents;
384
388 static constexpr Scalar constantTableSpan{1.0};
389
392 static constexpr Scalar minimumSpanExtent{1.0};
393
395 struct Region {
396 int initType{1}; // EQUIL item 10
398 std::vector<unsigned> pvtRegions;
400 Scalar zgoc{};
402 std::string_view compositionKeyword;
404 std::optional<unsigned> statedPhaseIdx{};
406 unsigned nominalPhaseIdx{FluidSystem::oilPhaseIdx};
409 bool twoZone{false};
413 Scalar lastVaporDepth{};
415 std::vector<TabulatedFunction> vaporVdTable;
417 CompVec vaporComposition{};
419 std::vector<TabulatedFunction> compositionVdTable;
420 TabulatedFunction tempVdTable;
421 std::optional<PressFunc> oilPressure;
422 std::optional<PressFunc> gasPressure; // two zones, or type 3
423
424 Scalar zwoc{}; // water-oil contact
428 bool anchoredAtWaterContact{false};
430 std::map<unsigned, WaterPressFunc> waterPressure;
431 };
432
435 static std::array<Scalar, 2> waterContactSpan(const Region& reg,
436 const std::array<Scalar, 2>& span)
437 {
438 if (FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)) {
439 return {std::min(span[0], reg.zwoc), std::max(span[1], reg.zwoc)};
440 }
441 return span;
442 }
443
445 static CompVec vaporComposition(const Region& reg, const Scalar depth)
446 {
447 CompVec z{};
448 Scalar sum = 0.0;
449 for (int c = 0; c < numComponents; ++c) {
450 z[c] = std::max(Scalar{0}, Details::evalDepthTable(reg.vaporVdTable[c], depth));
451 sum += z[c];
452 }
453 if (!(sum > 0.0)) {
454 OPM_THROW(std::runtime_error,
455 fmt::format("The COMPVD vapour composition vanishes at depth {} m.", depth));
456 }
457 std::ranges::transform(z, z.begin(), [sum](const Scalar zc) { return zc / sum; });
458 return z;
459 }
460
461 static CompVec composition(const Region& reg, const Scalar depth)
462 {
463 CompVec z{};
464 Scalar sum{};
465 for (int c = 0; c < numComponents; ++c) {
466 z[c] = std::max(Scalar{0}, Details::evalDepthTable(reg.compositionVdTable[c], depth));
467 sum += z[c];
468 }
469 if (!(sum > 0.0)) {
470 OPM_THROW(std::runtime_error,
471 fmt::format("The composition vanishes at depth {} m.", depth));
472 }
473 std::ranges::transform(z, z.begin(), [sum](const Scalar zc) { return zc / sum; });
474 return z;
475 }
476
479 template <class Table>
480 static void setupComposition(std::vector<TabulatedFunction>& out,
481 const Table& table,
482 const std::vector<std::size_t>& rows)
483 {
484 const auto& depthCol = table.getDepthColumn();
485
486 std::vector<Scalar> depths;
487 depths.reserve(rows.size() + 1);
488 for (const auto row : rows) {
489 depths.push_back(depthCol[row]);
490 }
491 // A single row means a depth-independent composition; the interpolant
492 // needs two sample points, so duplicate it onto an arbitrary interval.
493 const bool constantComposition = (depths.size() == 1);
494 if (constantComposition) {
495 depths.push_back(depths.front() + constantTableSpan);
496 }
497
498 out.resize(numComponents);
499 for (int c = 0; c < numComponents; ++c) {
500 const auto& col = table.getMoleFractionColumn(c);
501 std::vector<Scalar> values;
502 values.reserve(depths.size());
503 for (const auto row : rows) {
504 values.push_back(col[row]);
505 }
506 if (constantComposition) {
507 values.push_back(values.front());
508 }
509 out[c].setXYContainers(depths, values);
510 }
511 }
512
515 static std::optional<std::size_t> sourceTable(const TableContainer& container,
516 const std::size_t regionIdx)
517 {
518 const auto& byIndex = container.tables();
519 const auto after = byIndex.upper_bound(regionIdx);
520 if (after == byIndex.begin()) {
521 return std::nullopt;
522 }
523 return std::prev(after)->first;
524 }
525
527 static std::vector<std::size_t> allRows(const std::size_t count)
528 {
529 std::vector<std::size_t> rows(count);
530 std::iota(rows.begin(), rows.end(), std::size_t{0});
531 return rows;
532 }
533
540 static Scalar zoneBoundary(const CompvdTable& compvd,
541 const std::vector<std::size_t>& vaporRows,
542 const std::vector<std::size_t>& liquidRows,
543 const Region& reg,
544 const std::size_t regionIdx)
545 {
546 if (vaporRows.empty() || liquidRows.empty()) {
547 OPM_THROW(std::runtime_error,
548 fmt::format("The COMPVD table of region {} names both phases but has "
549 "no row for one of them.", regionIdx + 1));
550 }
551
552 const auto& depth = compvd.getDepthColumn();
553 const Scalar lastVapor = depth[vaporRows.back()];
554 const Scalar firstLiquid = depth[liquidRows.front()];
555 if (lastVapor > firstLiquid) {
556 OPM_THROW(std::runtime_error,
557 fmt::format("The COMPVD table of region {} has a vapour row at {} m "
558 "below its liquid row at {} m. All vapour rows must lie "
559 "above the liquid rows.",
560 regionIdx + 1, lastVapor, firstLiquid));
561 }
562
563 const Scalar boundary = std::clamp(reg.zgoc, lastVapor, firstLiquid);
564 if (boundary == reg.zgoc) {
565 return boundary;
566 }
567
568 if (reg.initType != 1) {
569 OPM_THROW(std::runtime_error,
570 fmt::format("The COMPVD table of region {} puts its vapour rows down to "
571 "{} m and its liquid rows from {} m, which do not meet at "
572 "the gas-oil contact at {} m. EQUIL item 10 = {} takes the "
573 "contact as the reference depth, so the rows must agree "
574 "with it.",
575 regionIdx + 1, lastVapor, firstLiquid, reg.zgoc,
576 reg.initType));
577 }
578
579 OpmLog::warning(fmt::format("Equilibration region {}: the gas-oil contact at {} m lies "
580 "outside the COMPVD phase change between the last vapour "
581 "row at {} m and the first liquid row at {} m. With EQUIL "
582 "item 10 = 1 the rows give the composition versus depth, "
583 "so the gas zone ends at {} m instead.",
584 regionIdx + 1, reg.zgoc, lastVapor, firstLiquid, boundary));
585 return boundary;
586 }
587
589 static std::vector<std::size_t> rowsOfPhase(const CompvdTable& compvd,
590 const CompvdTable::Phase phase)
591 {
592 const auto& flags = compvd.phaseFlags();
593 std::vector<std::size_t> rows;
594 for (std::size_t r = 0; r < flags.size(); ++r) {
595 if (flags[r] == phase) {
596 rows.push_back(r);
597 }
598 }
599 return rows;
600 }
601
604 static std::optional<unsigned> statedPhase(const CompvdTable& compvd,
605 const std::size_t regionIdx)
606 {
607 const auto& flags = compvd.phaseFlags();
608 if (flags.empty()) {
609 return std::nullopt;
610 }
611
612 const auto first = flags.front();
613 if (std::ranges::any_of(flags, [first](const auto f) { return f != first; })) {
614 OpmLog::info(fmt::format("Equilibration region {}: COMPVD names both phases, "
615 "so the contact and row labels determine the gas and "
616 "liquid zones.",
617 regionIdx + 1));
618 return std::nullopt;
619 }
620
621 return (first == CompvdTable::Phase::Vapor)
622 ? FluidSystem::gasPhaseIdx : FluidSystem::oilPhaseIdx;
623 }
624
626 static bool compositionVariesBetween(const Region& reg,
627 const Scalar depthA,
628 const Scalar depthB)
629 {
630 // Input mole fractions that differ physically across the contact should
631 // exceed this round-off tolerance.
632 constexpr Scalar sameComposition{1.0e-10};
633
634 const CompVec a = composition(reg, depthA);
635 const CompVec b = composition(reg, depthB);
636 return !std::ranges::equal(a, b, [](const Scalar x, const Scalar y) {
637 return std::abs(x - y) <= sameComposition;
638 });
639 }
640
643 static std::vector<std::vector<unsigned>>
644 pvtRegionsOfEachRegion(const std::vector<int>& eqlnum,
645 const std::vector<int>& pvtnum,
646 const std::size_t numRegions,
647 const std::size_t numPvtRegions,
648 const Parallel::Communication& comm)
649 {
650 // Whether equilibration region r holds a cell of PVT region p, at
651 // r * numPvtRegions + p.
652 std::vector<int> present(numRegions * numPvtRegions, 0);
653 for (std::size_t cell = 0; cell < eqlnum.size(); ++cell) {
654 const auto pvtRegion = pvtnum.empty() ? 0 : pvtnum[cell];
655 present[static_cast<std::size_t>(eqlnum[cell]) * numPvtRegions
656 + static_cast<std::size_t>(pvtRegion)] = 1;
657 }
658 comm.max(present.data(), static_cast<int>(present.size()));
659
660 std::vector<std::vector<unsigned>> pvtRegions(numRegions);
661 for (std::size_t r = 0; r < numRegions; ++r) {
662 for (std::size_t p = 0; p < numPvtRegions; ++p) {
663 if (present[r * numPvtRegions + p] != 0) {
664 pvtRegions[r].push_back(static_cast<unsigned>(p));
665 }
666 }
667 }
668 return pvtRegions;
669 }
670
671 Region setupRegion(const EquilRecord& record,
672 const TableManager& tables,
673 const std::vector<unsigned>& pvtRegions,
674 const std::vector<Scalar>& cellCenterDepth,
675 const std::vector<int>& eqlnum,
676 const std::vector<int>& pvtnum,
677 const Parallel::Communication& comm,
678 const Scalar gravity,
679 const int numSamplePoints,
680 const std::size_t regionIdx) const
681 {
682 Region reg;
683
684 reg.pvtRegions = pvtRegions;
685 reg.initType = record.compositionalInitType();
686 if (reg.initType != 1 && reg.initType != 3) {
687 OPM_THROW(std::runtime_error,
688 fmt::format("Compositional initialization type {} (EQUIL item 10) is "
689 "not supported for region {}; only type 1 (total "
690 "composition) and type 3 (liquid composition) are.",
691 reg.initType, regionIdx + 1));
692 }
693
694 if (record.gasOilContactCapillaryPressure() != 0.0) {
695 OPM_THROW(std::runtime_error,
696 fmt::format("Compositional equilibration only supports zero gas-oil "
697 "contact capillary pressure (EQUIL item 6); region {} "
698 "specifies {} bar.",
699 regionIdx + 1,
700 unit::convert::to(record.gasOilContactCapillaryPressure(),
701 unit::barsa)));
702 }
703
704 if (const auto accuracy = record.initializationTargetAccuracy(); accuracy != 0) {
705 OPM_THROW(std::runtime_error,
706 fmt::format("Compositional equilibration only supports cell-centre "
707 "initialization (EQUIL item 9 = 0); region {} specifies {}.",
708 regionIdx + 1, accuracy));
709 }
710
711 reg.zgoc = record.gasOilContactDepth();
712
713 // With one composition keyword, a missing regional record inherits the
714 // nearest preceding record. If both keywords are present, each region
715 // must select one explicitly because inheritance would be ambiguous.
716 const bool deckHasZmfvd = tables.hasTables("ZMFVD");
717 const bool deckHasCompvd = tables.hasTables("COMPVD");
718 const bool statesZmfvd = deckHasZmfvd &&
719 tables.getZmfvdTables().hasTable(regionIdx);
720 const bool statesCompvd = deckHasCompvd &&
721 tables.getCompvdTables().hasTable(regionIdx);
722
723 if (statesZmfvd && statesCompvd) {
724 OPM_THROW(std::runtime_error,
725 fmt::format("Region {} has both a ZMFVD and a COMPVD composition "
726 "versus depth; give only one of them.", regionIdx + 1));
727 }
728
729 std::optional<std::size_t> zmfvdAt;
730 std::optional<std::size_t> compvdAt;
731 if (deckHasZmfvd && deckHasCompvd) {
732 if (!statesZmfvd && !statesCompvd) {
733 OPM_THROW(std::runtime_error,
734 fmt::format("Region {} has neither a ZMFVD nor a COMPVD "
735 "composition versus depth. A deck using both "
736 "keywords has to give every region a record of "
737 "its own, as neither can be inherited.",
738 regionIdx + 1));
739 }
740 (statesZmfvd ? zmfvdAt : compvdAt) = regionIdx;
741 }
742 else if (deckHasZmfvd) {
743 zmfvdAt = sourceTable(tables.getZmfvdTables(), regionIdx);
744 }
745 else if (deckHasCompvd) {
746 compvdAt = sourceTable(tables.getCompvdTables(), regionIdx);
747 }
748
749 if (!zmfvdAt.has_value() && !compvdAt.has_value()) {
750 OPM_THROW(std::runtime_error,
751 fmt::format("Region {} has neither a ZMFVD nor a COMPVD composition "
752 "versus depth.", regionIdx + 1));
753 }
754
755 if (zmfvdAt.has_value()) {
756 reg.compositionKeyword = "ZMFVD";
757 const auto& zmfvd =
758 tables.getZmfvdTables().template getTable<ZmfvdTable>(*zmfvdAt);
759 setupComposition(reg.compositionVdTable, zmfvd,
760 allRows(zmfvd.getDepthColumn().size()));
761 }
762 else {
763 reg.compositionKeyword = "COMPVD";
764 const auto& compvd =
765 tables.getCompvdTables().template getTable<CompvdTable>(*compvdAt);
766 reg.statedPhaseIdx = statedPhase(compvd, regionIdx);
767
768 if (reg.statedPhaseIdx.has_value()) {
769 // One phase named: all rows form one composition profile, and
770 // the phase flag selects its EOS root.
771 if ((reg.initType == 3) &&
772 (*reg.statedPhaseIdx == FluidSystem::gasPhaseIdx)) {
773 OPM_THROW(std::runtime_error,
774 fmt::format("Region {} states a vapour composition in COMPVD "
775 "while EQUIL item 10 is 3, which takes the liquid "
776 "composition at the gas-oil contact.",
777 regionIdx + 1));
778 }
779 setupComposition(reg.compositionVdTable, compvd,
780 allRows(compvd.getDepthColumn().size()));
781 }
782 else {
783 // Both phases named: the gas root applies above the effective
784 // contact and the liquid root below it. For type 1 all rows give
785 // the total composition, interpolated across the gap between the
786 // two phases as well. Type 3 takes the gas from the vapour rows
787 // and the liquid from the liquid rows.
788 const auto vaporRows = rowsOfPhase(compvd, CompvdTable::Phase::Vapor);
789 const auto liquidRows = rowsOfPhase(compvd, CompvdTable::Phase::Liquid);
790 reg.zgoc = zoneBoundary(compvd, vaporRows, liquidRows, reg, regionIdx);
791 reg.twoZone = true;
792 reg.lastVaporDepth = compvd.getDepthColumn()[vaporRows.back()];
793 if (reg.initType == 1) {
794 setupComposition(reg.compositionVdTable, compvd,
795 allRows(compvd.getDepthColumn().size()));
796 }
797 else {
798 setupComposition(reg.vaporVdTable, compvd, vaporRows);
799 setupComposition(reg.compositionVdTable, compvd, liquidRows);
800 }
801 }
802 }
803
804 if (tables.hasTables("RTEMPVD")) {
805 const auto& rtempvd =
806 tables.getRtempvdTables().template getTable<RtempvdTable>(regionIdx);
807 std::vector<Scalar> tempDepths(rtempvd.getDepthColumn().begin(),
808 rtempvd.getDepthColumn().end());
809 const auto& tempCol = rtempvd.getTemperatureColumn();
810 std::vector<Scalar> temps(tempCol.begin(), tempCol.end());
811 // As for the composition tables, a single row is a depth-independent
812 // temperature and the interpolant needs a second sample point.
813 if (tempDepths.size() == 1) {
814 tempDepths.push_back(tempDepths.front() + constantTableSpan);
815 temps.push_back(temps.front());
816 }
817 reg.tempVdTable.setXYContainers(tempDepths, temps);
818 }
819 else {
820 const std::vector<Scalar> tempDepths{Scalar{0}, constantTableSpan};
821 const std::vector<Scalar> temps(tempDepths.size(), tables.rtemp());
822 reg.tempVdTable.setXYContainers(tempDepths, temps);
823 }
824
825 // Vertical extent of the region's cells across all processes.
826 auto span = std::array{std::numeric_limits<Scalar>::max(),
827 std::numeric_limits<Scalar>::lowest()};
828 for (std::size_t cell = 0; cell < cellCenterDepth.size(); ++cell) {
829 if (std::cmp_equal(eqlnum[cell], regionIdx)) {
830 span[0] = std::min(span[0], cellCenterDepth[cell]);
831 span[1] = std::max(span[1], cellCenterDepth[cell]);
832 }
833 }
834 span[0] = comm.min(span[0]);
835 span[1] = comm.max(span[1]);
836 if (span[0] > span[1]) {
837 // No cells anywhere in this region.
838 return reg;
839 }
840 const Scalar deepestCellDepth = span[1];
841 if (span[1] - span[0] < minimumSpanExtent) {
842 span = {span[0] - minimumSpanExtent, span[1] + minimumSpanExtent};
843 }
844
845 const bool waterActive = FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx);
846 std::vector<unsigned> waterPvtRegions;
847 if (waterActive) {
848 reg.zwoc = record.waterOilContactDepth();
849 if (pvtRegions.size() > 1) {
850 // Only PVT regions with cells below the contact have water
851 // columns that can anchor the hydrocarbon there. Include cells
852 // on all ranks.
853 std::vector<int> present(pvtRegions.back() + 1, 0);
854 for (std::size_t cell = 0; cell < cellCenterDepth.size(); ++cell) {
855 if (std::cmp_equal(eqlnum[cell], regionIdx) &&
856 cellCenterDepth[cell] > reg.zwoc) {
857 present[pvtnum[cell]] = 1;
858 }
859 }
860 comm.max(present.data(), static_cast<int>(present.size()));
861
862 for (const auto pvtRegion : pvtRegions) {
863 if (present[pvtRegion] != 0) {
864 waterPvtRegions.push_back(pvtRegion);
865 }
866 }
867 }
868 }
869
870 // A datum below the water-oil contact states the pressure of the water
871 // rather than of the hydrocarbon: integrate the water from there and
872 // hand the hydrocarbon its pressure at the contact. Otherwise the
873 // hydrocarbon is anchored at the datum and the water follows from the
874 // contact.
875 Scalar hcDatum = record.datumDepth();
876 Scalar hcPressure = record.datumDepthPressure();
877 const bool datumInWater = waterActive && (record.datumDepth() > reg.zwoc);
878 // A datum on the water-oil contact is no more a depth the hydrocarbon
879 // occupies than one below it, so the EOS root follows the fluid just
880 // above the contact in both cases.
881 reg.anchoredAtWaterContact = waterActive && (record.datumDepth() >= reg.zwoc);
882
883 if (datumInWater) {
884 // A PVT region confined above the contact has no water zone to
885 // which the datum applies. Its connate water follows the hydrocarbon
886 // pressure at the contact instead. With no water-zone cells, retain
887 // the existing interpretation of the datum for every PVT region.
888 const auto& datumPvtRegions =
889 waterPvtRegions.empty() ? reg.pvtRegions : waterPvtRegions;
890 integrateWaterPressure(reg, datumPvtRegions, span, gravity, numSamplePoints,
891 record.datumDepth(), record.datumDepthPressure());
892 hcDatum = reg.zwoc;
893 hcPressure = waterContactPressure(reg, record.datumDepth(), regionIdx)
894 + record.waterOilContactCapillaryPressure();
895 OpmLog::info(fmt::format("Equilibration region {}: the datum at {} m lies below the "
896 "water-oil contact at {} m, so it gives the water pressure; "
897 "the hydrocarbon pressure at the contact is {:.5} bar.",
898 regionIdx + 1, record.datumDepth(), reg.zwoc,
899 hcPressure / 1.0e5));
900 }
901
902 if (reg.initType == 1) {
903 setupSinglePhaseRegion(reg, span, gravity, numSamplePoints, regionIdx,
904 hcDatum, hcPressure);
905 }
906 else {
907 // Type 3 anchors the hydrocarbon at the gas-oil contact on its own
908 // saturation pressure, so it has no use for a datum that states the
909 // water pressure: the two columns would not meet at the water-oil
910 // contact.
911 if (datumInWater) {
912 OPM_THROW(std::runtime_error,
913 fmt::format("Compositional equilibration of region {} places the "
914 "datum at {} m, below the water-oil contact at {} m, "
915 "while EQUIL item 10 is 3. Put the datum in the "
916 "hydrocarbon column or use item 10 = 1.",
917 regionIdx + 1, record.datumDepth(), reg.zwoc));
918 }
919 setupTwoPhaseRegion(reg, record, span, gravity, numSamplePoints, regionIdx);
920 }
921
922 // Anchor any water columns not fixed by the datum at the contact.
923 if (waterActive) {
924 setupWaterZone(reg, record, span, gravity, numSamplePoints, regionIdx);
925 }
926
927 if (waterPvtRegions.size() > 1) {
928 Scalar minPressure = std::numeric_limits<Scalar>::max();
929 Scalar maxPressure = std::numeric_limits<Scalar>::lowest();
930 for (const auto pvtRegion : waterPvtRegions) {
931 const Scalar pressure = reg.waterPressure.at(pvtRegion).value(deepestCellDepth);
932 minPressure = std::min(minPressure, pressure);
933 maxPressure = std::max(maxPressure, pressure);
934 }
935 const Scalar difference = maxPressure - minPressure;
936 const Scalar scale = std::max(std::abs(minPressure), std::abs(maxPressure));
937 if (difference > 1.0e-10 * scale) {
938 auto displayedRegions = waterPvtRegions;
939 for (auto& pvtRegion : displayedRegions) {
940 ++pvtRegion;
941 }
942 OpmLog::warning(fmt::format(
943 "Equilibration region {} has water-zone cells in PVT regions {} "
944 "with pressures differing by {:.6g} bar at {} m. If those cells "
945 "connect (including numerical-aquifer NNCs), initial crossflow "
946 "may occur. Check PVTNUM and initial pressures.",
947 regionIdx + 1, fmt::join(displayedRegions, ", "),
948 unit::convert::to(difference, unit::barsa), deepestCellDepth));
949 }
950 }
951
952 return reg;
953 }
954
958 void setupWaterZone(Region& reg,
959 const EquilRecord& record,
960 const std::array<Scalar, 2>& span,
961 const Scalar gravity,
962 const int numSamplePoints,
963 const std::size_t regionIdx) const
964 {
965 // The capillary pressure at the contact (EQUIL item 4) offsets the
966 // water pressure from the hydrocarbon pressure there.
967 if (!reg.oilPressure.has_value() && !reg.gasPressure.has_value()) {
968 return;
969 }
970 // The water meets the hydrocarbon zone just above its contact: the gas
971 // zone when the gas-oil contact lies at or below the water-oil contact.
972 const bool gasAtContact = reg.gasPressure.has_value()
973 && (!reg.oilPressure.has_value() || (reg.zwoc <= reg.zgoc));
974 const auto& hcPressure = gasAtContact ? reg.gasPressure : reg.oilPressure;
975 const Scalar pcow = record.waterOilContactCapillaryPressure();
976 const Scalar pContact = hcPressure->value(reg.zwoc) - pcow;
977
978 integrateWaterPressure(reg, reg.pvtRegions, span, gravity, numSamplePoints,
979 reg.zwoc, pContact);
980
981 OpmLog::info(fmt::format("Equilibration region {}: the water-oil contact "
982 "is at {} m.", regionIdx + 1, reg.zwoc));
983 }
984
987 void integrateWaterPressure(Region& reg,
988 const std::vector<unsigned>& pvtRegions,
989 const std::array<Scalar, 2>& span,
990 const Scalar gravity,
991 const int numSamplePoints,
992 const Scalar depth,
993 const Scalar pressure) const
994 {
995 for (const auto pvtRegion : pvtRegions) {
996 const WaterODE ode(reg.tempVdTable, eosType_, pvtRegion, gravity);
997 reg.waterPressure.try_emplace(pvtRegion, ode,
998 typename WaterPressFunc::InitCond{depth, pressure},
999 numSamplePoints, waterContactSpan(reg, span));
1000 }
1001 }
1002
1006 static Scalar waterContactPressure(const Region& reg,
1007 const Scalar datum,
1008 const std::size_t regionIdx)
1009 {
1010 // Waters that differ physically part by more than this round-off
1011 // tolerance between the datum and the contact.
1012 constexpr Scalar samePressure{1.0e-10};
1013
1014 const auto& [firstPvtRegion, firstColumn] = *reg.waterPressure.begin();
1015 const Scalar pressure = firstColumn.value(reg.zwoc);
1016 for (const auto& [pvtRegion, column] : reg.waterPressure) {
1017 const Scalar other = column.value(reg.zwoc);
1018 if (std::abs(other - pressure) > samePressure * pressure) {
1019 OPM_THROW(std::runtime_error,
1020 fmt::format("Equilibration region {} places the datum at {} m, below "
1021 "the water-oil contact at {} m, but the water of its PVT "
1022 "regions {} and {} reaches the contact at {:.6g} and "
1023 "{:.6g} bar. Put the datum in the hydrocarbon column.",
1024 regionIdx + 1, datum, reg.zwoc,
1025 firstPvtRegion + 1, pvtRegion + 1,
1026 unit::convert::to(pressure, unit::barsa),
1027 unit::convert::to(other, unit::barsa)));
1028 }
1029 }
1030 return pressure;
1031 }
1032
1035 static Scalar waterLimit(const std::vector<Scalar>& limits,
1036 const std::size_t cell,
1037 const Scalar fallback)
1038 {
1039 return limits.empty() ? fallback : limits[cell];
1040 }
1041
1056 void setupSinglePhaseRegion(Region& reg,
1057 const std::array<Scalar, 2>& span,
1058 const Scalar gravity,
1059 const int numSamplePoints,
1060 const std::size_t regionIdx,
1061 const Scalar datum,
1062 const Scalar datumPressure) const
1063 {
1064 if (reg.twoZone) {
1065 setupTwoZoneRegion(reg, span, gravity, numSamplePoints, regionIdx,
1066 datum, datumPressure);
1067 return;
1068 }
1069
1070 // COMPVD states the phase its composition belongs to; without that the
1071 // datum's side of the gas-oil contact decides the EOS root. A datum on
1072 // the water-oil contact is not in the hydrocarbon at all, so there the
1073 // gas-oil contact itself still leaves gas above: a column whose two
1074 // contacts coincide holds no liquid.
1075 const bool gasAtDatum = reg.anchoredAtWaterContact ? (datum <= reg.zgoc)
1076 : (datum < reg.zgoc);
1077 const auto phaseIdx = reg.statedPhaseIdx.value_or(
1078 gasAtDatum ? FluidSystem::gasPhaseIdx : FluidSystem::oilPhaseIdx);
1079
1080 // A gas-oil contact inside a single-zone region requires the composition
1081 // to vary across it, so the flash can label the phases correctly. Only
1082 // the hydrocarbon above the water-oil contact needs labelling.
1083 const Scalar hydrocarbonBottom = FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)
1084 ? std::min(span[1], reg.zwoc) : span[1];
1085 if ((reg.zgoc > span[0]) && (reg.zgoc < hydrocarbonBottom) &&
1086 !compositionVariesBetween(reg, span[0], reg.zgoc) &&
1087 !compositionVariesBetween(reg, reg.zgoc, hydrocarbonBottom)) {
1088 OpmLog::warning(fmt::format("Equilibration region {}: the gas-oil contact "
1089 "at {} m lies inside a type-1 region, but the "
1090 "composition does not vary across the contact. "
1091 "Compositional variation is required for proper "
1092 "phase labeling.", regionIdx + 1, reg.zgoc));
1093 }
1094
1095 const ODE ode([&reg](const Scalar depth) { return composition(reg, depth); },
1096 reg.tempVdTable, phaseIdx, eosType_, gravity);
1097 reg.oilPressure.emplace(ode,
1098 typename PressFunc::InitCond{datum, datumPressure},
1099 numSamplePoints, waterContactSpan(reg, span));
1100 reg.nominalPhaseIdx = phaseIdx;
1101
1102 OpmLog::info(fmt::format("Equilibration region {}: pressure integrated with one "
1103 "EOS root and the total composition from {} "
1104 "(EQUIL item 10 = 1).",
1105 regionIdx + 1, reg.compositionKeyword));
1106 }
1107
1111 void setupTwoZoneRegion(Region& reg,
1112 const std::array<Scalar, 2>& span,
1113 const Scalar gravity,
1114 const int numSamplePoints,
1115 const std::size_t regionIdx,
1116 const Scalar datum,
1117 const Scalar datumPressure) const
1118 {
1119 const auto z = [&reg](const Scalar depth) { return composition(reg, depth); };
1120 const ODE liquidOde(z, reg.tempVdTable, FluidSystem::oilPhaseIdx, eosType_, gravity);
1121 const ODE gasOde(z, reg.tempVdTable, FluidSystem::gasPhaseIdx, eosType_, gravity);
1122
1123 // The datum-side pressure function must reach the actual contact even
1124 // when it lies outside the cell span, because the other pressure function
1125 // is initialized from its value at the contact.
1126 const auto pressureSpan = waterContactSpan(reg, span);
1127 const std::array<Scalar, 2> datumSpan{std::min(pressureSpan[0], reg.zgoc),
1128 std::max(pressureSpan[1], reg.zgoc)};
1129 if ((reg.zgoc < span[0]) || (reg.zgoc > span[1])) {
1130 OpmLog::warning(fmt::format("Equilibration region {}: the gas zone ends at {} m, "
1131 "outside the cells of the region, so all of them "
1132 "lie in the {} zone.",
1133 regionIdx + 1,
1134 reg.zgoc,
1135 (reg.zgoc < span[0]) ? "liquid" : "gas"));
1136 }
1137
1138 if (datum < reg.zgoc) {
1139 reg.gasPressure.emplace(gasOde,
1140 typename PressFunc::InitCond{datum, datumPressure},
1141 numSamplePoints, datumSpan);
1142 reg.oilPressure.emplace(liquidOde,
1143 typename PressFunc::InitCond{
1144 reg.zgoc, reg.gasPressure->value(reg.zgoc)},
1145 numSamplePoints, pressureSpan);
1146 }
1147 else {
1148 reg.oilPressure.emplace(liquidOde,
1149 typename PressFunc::InitCond{datum, datumPressure},
1150 numSamplePoints, datumSpan);
1151 reg.gasPressure.emplace(gasOde,
1152 typename PressFunc::InitCond{
1153 reg.zgoc, reg.oilPressure->value(reg.zgoc)},
1154 numSamplePoints, pressureSpan);
1155 }
1156
1157 OpmLog::info(fmt::format("Equilibration region {}: COMPVD gives a gas zone above "
1158 "{} m and a liquid one below it "
1159 "(EQUIL item 10 = 1).",
1160 regionIdx + 1,
1161 reg.zgoc));
1162 }
1163
1171 void setupTwoPhaseRegion(Region& reg,
1172 const EquilRecord& record,
1173 const std::array<Scalar, 2>& span,
1174 const Scalar gravity,
1175 const int numSamplePoints,
1176 const std::size_t regionIdx) const
1177 {
1178 const Scalar inputReferenceDepth = record.datumDepth();
1179 if (inputReferenceDepth != reg.zgoc) {
1180 OpmLog::warning(fmt::format("Equilibration region {}: the reference depth {} m "
1181 "does not coincide with the gas-oil contact when "
1182 "EQUIL item 10 is 3; resetting it to the contact "
1183 "depth {} m.",
1184 regionIdx + 1, inputReferenceDepth, reg.zgoc));
1185 }
1186
1187 const CompVec liquid = composition(reg, reg.zgoc);
1188 const Scalar temp = Details::evalDepthTable(reg.tempVdTable, reg.zgoc);
1189 Scalar psat{};
1190 CompVec vapor{};
1191 if (!SaturationPressure<Scalar, FluidSystem>::bubblePressure(liquid, temp, eosType_,
1192 psat, vapor)) {
1193 OPM_THROW(std::runtime_error,
1194 fmt::format("The saturation pressure calculation at the gas-oil "
1195 "contact of region {} did not converge.", regionIdx + 1));
1196 }
1197 reg.vaporComposition = vapor;
1198
1199 // For type 3, the contact is the reference depth. With item 11
1200 // defaulted, the input pressure must agree with the saturation pressure
1201 // to within one atmosphere and is reset to the computed value otherwise.
1202 // Item 11 = 1 retains the numeric input pressure at the contact regardless
1203 // of that test; the result need not be an equilibrium system in that case.
1204 constexpr Scalar oneAtmosphere = unit::atm;
1205 const Scalar inputPressure = record.datumDepthPressure();
1206 const bool resetToPsat = record.setToSaturationPressure()
1207 && (std::abs(inputPressure - psat) >= oneAtmosphere);
1208 const Scalar referencePressure = resetToPsat ? psat : inputPressure;
1209
1210 OpmLog::info(fmt::format("Equilibration region {}: two phases, liquid composition "
1211 "specified (EQUIL item 10 is 3). The saturation pressure "
1212 "at the gas-oil contact ({} m) is {:.6g} bar.",
1213 regionIdx + 1, reg.zgoc,
1214 unit::convert::to(psat, unit::barsa)));
1215
1216 if (resetToPsat) {
1217 OpmLog::warning(fmt::format("Equilibration region {}: the datum pressure {:.6g} bar "
1218 "differs from the saturation pressure {:.6g} bar at the "
1219 "gas-oil contact by one atmosphere or more; the "
1220 "saturation pressure is used instead.",
1221 regionIdx + 1,
1222 unit::convert::to(inputPressure, unit::barsa),
1223 unit::convert::to(psat, unit::barsa)));
1224 }
1225
1226 const ODE oilOde([&reg](const Scalar depth) { return composition(reg, depth); },
1227 reg.tempVdTable, FluidSystem::oilPhaseIdx, eosType_, gravity);
1228 reg.oilPressure.emplace(oilOde,
1229 typename PressFunc::InitCond{reg.zgoc, referencePressure},
1230 numSamplePoints, waterContactSpan(reg, span));
1231
1232 // Integrate a two-zone gas column with the same depth-dependent
1233 // composition assigned to its cells. Otherwise use the equilibrium
1234 // vapour composition at the contact throughout the gas column.
1235 typename ODE::CompositionFunction gasComposition;
1236 if (reg.twoZone) {
1237 gasComposition = [&reg](const Scalar depth) { return vaporComposition(reg, depth); };
1238 }
1239 else {
1240 gasComposition = [vapor](const Scalar) { return vapor; };
1241 }
1242 const ODE gasOde(gasComposition,
1243 reg.tempVdTable, FluidSystem::gasPhaseIdx, eosType_, gravity);
1244 reg.gasPressure.emplace(gasOde,
1245 typename PressFunc::InitCond{reg.zgoc, referencePressure},
1246 numSamplePoints, waterContactSpan(reg, span));
1247 }
1248
1254 static bool isInGasZone(const Region& reg, const Scalar depth)
1255 {
1256 if (reg.twoZone && (reg.initType == 1) && (depth <= reg.lastVaporDepth)) {
1257 return true;
1258 }
1259 return ((reg.initType == 3) || reg.twoZone) && (depth < reg.zgoc);
1260 }
1261
1262 Scalar assignCell(FluidState& fs, const Region& reg, const unsigned pvtRegion,
1263 const Scalar depth, const std::size_t cell) const
1264 {
1265 const bool inGasZone = isInGasZone(reg, depth);
1266
1267 const CompVec z = [&reg, depth, inGasZone]() {
1268 // Type 1 takes the total composition on either side of the contact.
1269 if (!inGasZone || (reg.initType == 1)) {
1270 return composition(reg, depth);
1271 }
1272 // Type 3 holds the contact vapour above the contact; a two-zone
1273 // COMPVD table gives the gas zone its own composition versus depth.
1274 return reg.twoZone ? vaporComposition(reg, depth) : reg.vaporComposition;
1275 }();
1276 const auto& pressFunc = inGasZone ? reg.gasPressure : reg.oilPressure;
1277 if (!pressFunc.has_value()) {
1278 OPM_THROW(std::runtime_error,
1279 "Evaluating the equilibrated pressure of a region without cells.");
1280 }
1281
1282 // The common pressure used by the current flash follows the water
1283 // column below the contact, including when the scaled maximum water
1284 // saturation leaves some residual hydrocarbon. Keep this separate from
1285 // the phase pressures so equilibration retains the capillary offset.
1286 const Scalar hydrocarbonPressure = pressFunc->value(depth);
1287 // The water column of the cell's own PVT region.
1288 const auto water = reg.waterPressure.find(pvtRegion);
1289 const WaterPressFunc* waterPressure =
1290 (water != reg.waterPressure.end()) ? &water->second : nullptr;
1291 const bool inWaterZone = (depth > reg.zwoc) && (waterPressure != nullptr);
1292 const Scalar press = inWaterZone ? waterPressure->value(depth)
1293 : hydrocarbonPressure;
1294
1295 fs.setTemperature(Details::evalDepthTable(reg.tempVdTable, depth));
1296 for (unsigned phaseIdx = 0; phaseIdx < FluidSystem::numPhases; ++phaseIdx) {
1297 if (FluidSystem::phaseIsActive(phaseIdx)) {
1298 fs.setPressure(phaseIdx, hydrocarbonPressure);
1299 fs.setSaturation(phaseIdx, 0.0);
1300 }
1301 }
1302
1303 // Below the water-oil contact the pore space holds water alone; above
1304 // it the hydrocarbon leaves room for the connate water only.
1305 Scalar sWat = 0.0;
1306 if (FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)) {
1307 // The saturation function's own endpoints, per cell: below the
1308 // contact the water fills what it can, above it only the connate
1309 // water remains.
1310 sWat = (depth > reg.zwoc) ? waterLimit(maxWater_, cell, Scalar{1})
1311 : waterLimit(connateWater_, cell, Scalar{0});
1312 fs.setSaturation(FluidSystem::waterPhaseIdx, sWat);
1313 if (waterPressure != nullptr) {
1314 fs.setPressure(FluidSystem::waterPhaseIdx, waterPressure->value(depth));
1315 }
1316 }
1317
1318 // Set a nominal single-phase saturation for the hydrocarbon using the
1319 // phase represented by the pressure integration. The downstream flash
1320 // recomputes the phase split from composition, pressure, and temperature.
1321 fs.setSaturation(inGasZone ? FluidSystem::gasPhaseIdx : reg.nominalPhaseIdx,
1322 Scalar{1} - sWat);
1323
1324 for (int c = 0; c < numComponents; ++c) {
1325 fs.setMoleFraction(c, z[c]);
1326 }
1327 return press;
1328 }
1329
1330 CompositionalConfig::EOSType eosType_;
1334 std::vector<Scalar> connateWater_;
1335 std::vector<Scalar> maxWater_;
1336 std::vector<FluidState> fluidStates_;
1337 std::vector<Scalar> referencePressures_;
1338 std::vector<int> twoPhaseCheckRegions_;
1339};
1340
1355template <class Scalar, class LiquidFraction>
1356void reportTwoPhaseCells(const std::vector<int>& checkRegions,
1357 const std::vector<Scalar>& cellCenterDepth,
1358 const LiquidFraction& liquidFraction,
1359 const std::size_t numRegions,
1360 const Parallel::Communication& comm)
1361{
1362 // The smallest mole fraction of the smaller phase that is reported. Gas
1363 // just above a saturated gas-oil contact lies slightly below its dew point
1364 // and condenses a trace of liquid well under it.
1365 constexpr Scalar minReportedFraction{1.0e-3};
1366
1367 // Ranges of the depth and of the smaller phase's mole fraction of the split
1368 // cells of each region, over all processes. A range is unaffected by the
1369 // cells that several processes hold.
1370 std::vector<Scalar> shallowest(numRegions, std::numeric_limits<Scalar>::max());
1371 std::vector<Scalar> deepest(numRegions, std::numeric_limits<Scalar>::lowest());
1372 std::vector<Scalar> leastFraction(numRegions, std::numeric_limits<Scalar>::max());
1373 std::vector<Scalar> mostFraction(numRegions, std::numeric_limits<Scalar>::lowest());
1374 for (std::size_t cell = 0; cell < checkRegions.size(); ++cell) {
1375 if (checkRegions[cell] < 0) {
1376 continue;
1377 }
1378 const Scalar liquid = liquidFraction(cell);
1379 const Scalar fraction = std::min(liquid, Scalar{1} - liquid);
1380 // Negated so that a NaN fraction is skipped too.
1381 if (!(fraction >= minReportedFraction)) {
1382 continue;
1383 }
1384 const auto r = static_cast<std::size_t>(checkRegions[cell]);
1385 const Scalar depth = cellCenterDepth[cell];
1386 shallowest[r] = std::min(shallowest[r], depth);
1387 deepest[r] = std::max(deepest[r], depth);
1388 leastFraction[r] = std::min(leastFraction[r], fraction);
1389 mostFraction[r] = std::max(mostFraction[r], fraction);
1390 }
1391 const auto count = static_cast<int>(numRegions);
1392 comm.min(shallowest.data(), count);
1393 comm.max(deepest.data(), count);
1394 comm.min(leastFraction.data(), count);
1395 comm.max(mostFraction.data(), count);
1396
1397 // A single depth needs no range.
1398 const auto depthRange = [](const Scalar top, const Scalar bottom) {
1399 const auto shallow = fmt::format("{:.2f} m", top);
1400 const auto deep = fmt::format("{:.2f} m", bottom);
1401 return (shallow == deep) ? "at " + shallow : "between " + shallow + " and " + deep;
1402 };
1403 for (std::size_t r = 0; r < numRegions; ++r) {
1404 if (shallowest[r] > deepest[r]) {
1405 continue;
1406 }
1407 // A single share needs no range. A share lies between the cut-off and
1408 // one half, so it cannot round to 0% or 100%.
1409 const auto least = fmt::format("{:.3g}%", 100 * leastFraction[r]);
1410 const auto most = fmt::format("{:.3g}%", 100 * mostFraction[r]);
1411 OpmLog::warning(fmt::format("Equilibration region {}: cells {} split into gas and "
1412 "liquid at their initial pressure, with the smaller phase "
1413 "holding {} of the hydrocarbon moles. With EQUIL "
1414 "item 10 = 1, that pressure comes from the density of a "
1415 "single phase, so the gas and liquid in these cells are "
1416 "not in hydrostatic equilibrium and may move up or down "
1417 "once the simulation starts.",
1418 r + 1,
1419 depthRange(shallowest[r], deepest[r]),
1420 (least == most) ? least : least + " to " + most));
1421 }
1422}
1423
1424} // namespace Opm::EQUIL::Comp
1425
1426#endif // OPM_INIT_STATE_EQUIL_COMP_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
The ODE integrator and phase-pressure function used to solve the hydrostatic equilibrium problem,...
Definition: InitStateEquilComp.hpp:95
Tabulated1DFunction< Scalar > TabulatedFunction
Definition: InitStateEquilComp.hpp:100
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquilComp.hpp:114
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:97
std::array< Scalar, FluidSystem::numComponents > CompVec
Definition: InitStateEquilComp.hpp:98
std::function< CompVec(Scalar)> CompositionFunction
Definition: InitStateEquilComp.hpp:99
EosDensityODE(CompositionFunction composition, const TabulatedFunction &tempVdTable, const unsigned phaseIdx, const CompositionalConfig::EOSType eosType, const Scalar normGrav)
Definition: InitStateEquilComp.hpp:102
Definition: InitStateEquilComp.hpp:146
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquilComp.hpp:161
Tabulated1DFunction< Scalar > TabulatedFunction
Definition: InitStateEquilComp.hpp:149
WaterDensityODE(const TabulatedFunction &tempVdTable, const CompositionalConfig::EOSType eosType, const unsigned pvtRegion, const Scalar normGrav)
Definition: InitStateEquilComp.hpp:151
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:148
Computes the initial state of a compositional model from hydrostatic equilibrium (the EQUIL and ZMFVD...
Definition: InitStateEquilComp.hpp:240
CompositionalFluidState< Scalar, FluidSystem > FluidState
Definition: InitStateEquilComp.hpp:243
std::vector< int > & twoPhaseCheckRegions()
Definition: InitStateEquilComp.hpp:369
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:242
InitialStateComputer(const EclipseState &inputState, const CompositionalConfig::EOSType eosType, const std::vector< Scalar > &cellCenterDepth, const std::vector< int > &eqlnum, const std::vector< int > &pvtnum, const Parallel::Communication &comm, const Scalar gravity, const int numSamplePoints, const std::vector< Scalar > &connateWater={}, const std::vector< Scalar > &maxWater={})
Definition: InitStateEquilComp.hpp:259
const std::vector< int > & twoPhaseCheckRegions() const
Definition: InitStateEquilComp.hpp:372
const std::vector< Scalar > & referencePressures() const
Definition: InitStateEquilComp.hpp:362
const std::vector< FluidState > & fluidStates() const
Definition: InitStateEquilComp.hpp:356
std::vector< FluidState > & fluidStates()
Definition: InitStateEquilComp.hpp:353
Definition: PressureFunction.hpp:128
Scalar evalDepthTable(const Tabulated1DFunction< Scalar > &table, const Scalar depth)
Definition: InitStateEquilComp.hpp:84
Definition: InitStateEquilComp.hpp:76
void reportTwoPhaseCells(const std::vector< int > &checkRegions, const std::vector< Scalar > &cellCenterDepth, const LiquidFraction &liquidFraction, const std::size_t numRegions, const Parallel::Communication &comm)
Definition: InitStateEquilComp.hpp:1356
Scalar cellCenterDepth(const Element &element)
Definition: InitStateEquil_impl.hpp:134
Dune::Communication< MPIComm > Communication
Definition: ParallelCommunication.hpp:30
Phase
Phase indices for reservoir coupling, we currently only support black-oil phases (oil,...
Definition: ReservoirCoupling.hpp:183