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
58#include <algorithm>
59#include <array>
60#include <cmath>
61#include <cstddef>
62#include <functional>
63#include <iterator>
64#include <limits>
65#include <numeric>
66#include <optional>
67#include <stdexcept>
68#include <string>
69#include <string_view>
70#include <utility>
71#include <vector>
72
73
75
76namespace Details {
77
81template <class Scalar>
82Scalar evalDepthTable(const Tabulated1DFunction<Scalar>& table, const Scalar depth)
83{
84 return table.eval(std::clamp(depth, table.xMin(), table.xMax()));
85}
86
91template <class FluidSystem>
93{
94public:
95 using Scalar = typename FluidSystem::Scalar;
96 using CompVec = std::array<Scalar, FluidSystem::numComponents>;
97 using CompositionFunction = std::function<CompVec(Scalar)>;
98 using TabulatedFunction = Tabulated1DFunction<Scalar>;
99
101 const TabulatedFunction& tempVdTable,
102 const unsigned phaseIdx,
103 const CompositionalConfig::EOSType eosType,
104 const Scalar normGrav)
105 : composition_(std::move(composition))
106 , tempVdTable_(tempVdTable)
107 , phaseIdx_(phaseIdx)
108 , eosType_(eosType)
109 , g_(normGrav)
110 {}
111
113 const Scalar press) const
114 {
115 const CompVec z = composition_(depth);
116 const Scalar temp = evalDepthTable(tempVdTable_, depth);
117
118 CompositionalFluidState<Scalar, FluidSystem> fs;
119 fs.setTemperature(temp);
120 fs.setPressure(FluidSystem::oilPhaseIdx, press);
121 fs.setPressure(FluidSystem::gasPhaseIdx, press);
122 for (unsigned compIdx = 0; compIdx < FluidSystem::numComponents; ++compIdx) {
123 fs.setMoleFraction(phaseIdx_, compIdx, z[compIdx]);
124 }
125
126 typename FluidSystem::template ParameterCache<Scalar> paramCache(eosType_);
127 paramCache.updatePhase(fs, phaseIdx_);
128
129 return FluidSystem::density(fs, paramCache, phaseIdx_) * g_;
130 }
131
132private:
133 CompositionFunction composition_;
134 const TabulatedFunction& tempVdTable_;
135 unsigned phaseIdx_;
136 CompositionalConfig::EOSType eosType_;
137 Scalar g_;
138};
139
142template <class FluidSystem>
144{
145public:
146 using Scalar = typename FluidSystem::Scalar;
147 using TabulatedFunction = Tabulated1DFunction<Scalar>;
148
150 const CompositionalConfig::EOSType eosType,
151 const Scalar normGrav)
152 : tempVdTable_(tempVdTable)
153 , eosType_(eosType)
154 , g_(normGrav)
155 {}
156
158 const Scalar press) const
159 {
160 CompositionalFluidState<Scalar, FluidSystem> fs;
161 fs.setTemperature(evalDepthTable(tempVdTable_, depth));
162 fs.setPressure(FluidSystem::waterPhaseIdx, press);
163
164 typename FluidSystem::template ParameterCache<Scalar> paramCache(eosType_);
165
166 return FluidSystem::density(fs, paramCache, FluidSystem::waterPhaseIdx) * g_;
167 }
168
169private:
170 const TabulatedFunction& tempVdTable_;
171 CompositionalConfig::EOSType eosType_;
172 Scalar g_;
173};
174
175} // namespace Details
176
212template <class FluidSystem>
214{
215public:
216 using Scalar = typename FluidSystem::Scalar;
217 using FluidState = CompositionalFluidState<Scalar, FluidSystem>;
218
231 InitialStateComputer(const EclipseState& inputState,
232 const CompositionalConfig::EOSType eosType,
233 const std::vector<Scalar>& cellCenterDepth,
234 const std::vector<int>& eqlnum,
235 const Parallel::Communication& comm,
236 const Scalar gravity,
237 const int numSamplePoints,
238 const std::vector<Scalar>& connateWater = {},
239 const std::vector<Scalar>& maxWater = {})
240 : eosType_(eosType)
241 , connateWater_(connateWater)
242 , maxWater_(maxWater)
243 {
244 const auto& records = inputState.getInitConfig().getEquil();
245 const auto& tables = inputState.getTableManager();
246
247 if (!tables.hasTables("ZMFVD") && !tables.hasTables("COMPVD")) {
248 OPM_THROW(std::runtime_error,
249 "Equilibration of a compositional model requires the composition "
250 "versus depth from the ZMFVD or the COMPVD keyword.");
251 }
252
254 if (eqlnum.size() != cellCenterDepth.size()) {
255 OPM_THROW(std::runtime_error,
256 fmt::format("EQLNUM contains {} entries for {} cell depths.",
257 eqlnum.size(), cellCenterDepth.size()));
258 }
259 for (std::size_t cell = 0; cell < eqlnum.size(); ++cell) {
260 const auto region = eqlnum[cell];
261 if (region < 0 || std::cmp_greater_equal(region, records.size())) {
262 OPM_THROW(std::runtime_error,
263 fmt::format("Cell {} has EQLNUM {} outside the {} "
264 "equilibration regions.",
265 cell, region + 1, records.size()));
266 }
267 }
268 // The endpoint vectors are optional, but a non-empty one is indexed for
269 // every cell.
270 for (const auto& [name, limits] : {std::pair{"connate water", std::cref(connateWater)},
271 std::pair{"maximum water", std::cref(maxWater)}}) {
272 if (!limits.get().empty() &&
273 (limits.get().size() != cellCenterDepth.size())) {
274 OPM_THROW(std::runtime_error,
275 fmt::format("The {} saturation has {} entries for {} cell depths.",
276 name, limits.get().size(), cellCenterDepth.size()));
277 }
278 }
279 OPM_END_PARALLEL_TRY_CATCH("Invalid equilibration input: ", comm);
280
281 std::vector<Region> regions;
282 regions.reserve(records.size());
283 for (std::size_t r = 0; r < records.size(); ++r) {
284 regions.push_back(setupRegion(records.getRecord(r), tables, cellCenterDepth,
285 eqlnum, comm, gravity, numSamplePoints, r));
286 }
287
288 fluidStates_.resize(cellCenterDepth.size());
289 referencePressures_.resize(cellCenterDepth.size());
290 for (std::size_t cell = 0; cell < cellCenterDepth.size(); ++cell) {
291 referencePressures_[cell] =
292 assignCell(fluidStates_[cell], regions[eqlnum[cell]], cellCenterDepth[cell], cell);
293 }
294 }
295
296 std::vector<FluidState>& fluidStates()
297 { return fluidStates_; }
298
299 const std::vector<FluidState>& fluidStates() const
300 { return fluidStates_; }
301
305 const std::vector<Scalar>& referencePressures() const
306 { return referencePressures_; }
307
308private:
309 using CompVec = std::array<Scalar, FluidSystem::numComponents>;
310 using TabulatedFunction = Tabulated1DFunction<Scalar>;
315
316 static constexpr int numComponents = FluidSystem::numComponents;
317
321 static constexpr Scalar constantTableSpan{1.0};
322
325 static constexpr Scalar minimumSpanExtent{1.0};
326
328 struct Region {
329 int initType{1}; // EQUIL item 10
331 Scalar zgoc{};
333 std::string_view compositionKeyword;
335 std::optional<unsigned> statedPhaseIdx{};
337 unsigned nominalPhaseIdx{FluidSystem::oilPhaseIdx};
340 bool twoZone{false};
344 Scalar lastVaporDepth{};
346 std::vector<TabulatedFunction> vaporVdTable;
348 CompVec vaporComposition{};
351 std::vector<TabulatedFunction> compositionVdTable;
352 TabulatedFunction tempVdTable;
353 std::optional<PressFunc> oilPressure;
354 std::optional<PressFunc> gasPressure; // two zones, or type 3
355
356 Scalar zwoc{}; // water-oil contact
360 bool anchoredAtWaterContact{false};
361 std::optional<WaterPressFunc> waterPressure;
362 };
363
366 static std::array<Scalar, 2> waterContactSpan(const Region& reg,
367 const std::array<Scalar, 2>& span)
368 {
369 if (FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)) {
370 return {std::min(span[0], reg.zwoc), std::max(span[1], reg.zwoc)};
371 }
372 return span;
373 }
374
376 static CompVec vaporComposition(const Region& reg, const Scalar depth)
377 {
378 CompVec z{};
379 Scalar sum = 0.0;
380 for (int c = 0; c < numComponents; ++c) {
381 z[c] = std::max(Scalar{0}, Details::evalDepthTable(reg.vaporVdTable[c], depth));
382 sum += z[c];
383 }
384 if (!(sum > 0.0)) {
385 OPM_THROW(std::runtime_error,
386 fmt::format("The COMPVD vapour composition vanishes at depth {} m.", depth));
387 }
388 std::ranges::transform(z, z.begin(), [sum](const Scalar zc) { return zc / sum; });
389 return z;
390 }
391
392 static CompVec composition(const Region& reg, const Scalar depth)
393 {
394 CompVec z{};
395 Scalar sum{};
396 for (int c = 0; c < numComponents; ++c) {
397 z[c] = std::max(Scalar{0}, Details::evalDepthTable(reg.compositionVdTable[c], depth));
398 sum += z[c];
399 }
400 if (!(sum > 0.0)) {
401 OPM_THROW(std::runtime_error,
402 fmt::format("The composition vanishes at depth {} m.", depth));
403 }
404 std::ranges::transform(z, z.begin(), [sum](const Scalar zc) { return zc / sum; });
405 return z;
406 }
407
410 template <class Table>
411 static void setupComposition(std::vector<TabulatedFunction>& out,
412 const Table& table,
413 const std::vector<std::size_t>& rows)
414 {
415 const auto& depthCol = table.getDepthColumn();
416
417 std::vector<Scalar> depths;
418 depths.reserve(rows.size() + 1);
419 for (const auto row : rows) {
420 depths.push_back(depthCol[row]);
421 }
422 // A single row means a depth-independent composition; the interpolant
423 // needs two sample points, so duplicate it onto an arbitrary interval.
424 const bool constantComposition = (depths.size() == 1);
425 if (constantComposition) {
426 depths.push_back(depths.front() + constantTableSpan);
427 }
428
429 out.resize(numComponents);
430 for (int c = 0; c < numComponents; ++c) {
431 const auto& col = table.getMoleFractionColumn(c);
432 std::vector<Scalar> values;
433 values.reserve(depths.size());
434 for (const auto row : rows) {
435 values.push_back(col[row]);
436 }
437 if (constantComposition) {
438 values.push_back(values.front());
439 }
440 out[c].setXYContainers(depths, values);
441 }
442 }
443
446 static std::optional<std::size_t> sourceTable(const TableContainer& container,
447 const std::size_t regionIdx)
448 {
449 const auto& byIndex = container.tables();
450 const auto after = byIndex.upper_bound(regionIdx);
451 if (after == byIndex.begin()) {
452 return std::nullopt;
453 }
454 return std::prev(after)->first;
455 }
456
458 static std::vector<std::size_t> allRows(const std::size_t count)
459 {
460 std::vector<std::size_t> rows(count);
461 std::iota(rows.begin(), rows.end(), std::size_t{0});
462 return rows;
463 }
464
471 static Scalar zoneBoundary(const CompvdTable& compvd,
472 const std::vector<std::size_t>& vaporRows,
473 const std::vector<std::size_t>& liquidRows,
474 const Region& reg,
475 const std::size_t regionIdx)
476 {
477 if (vaporRows.empty() || liquidRows.empty()) {
478 OPM_THROW(std::runtime_error,
479 fmt::format("The COMPVD table of region {} names both phases but has "
480 "no row for one of them.", regionIdx + 1));
481 }
482
483 const auto& depth = compvd.getDepthColumn();
484 const Scalar lastVapor = depth[vaporRows.back()];
485 const Scalar firstLiquid = depth[liquidRows.front()];
486 if (lastVapor > firstLiquid) {
487 OPM_THROW(std::runtime_error,
488 fmt::format("The COMPVD table of region {} has a vapour row at {} m "
489 "below its liquid row at {} m. All vapour rows must lie "
490 "above the liquid rows.",
491 regionIdx + 1, lastVapor, firstLiquid));
492 }
493
494 const Scalar boundary = std::clamp(reg.zgoc, lastVapor, firstLiquid);
495 if (boundary == reg.zgoc) {
496 return boundary;
497 }
498
499 if (reg.initType != 1) {
500 OPM_THROW(std::runtime_error,
501 fmt::format("The COMPVD table of region {} puts its vapour rows down to "
502 "{} m and its liquid rows from {} m, which do not meet at "
503 "the gas-oil contact at {} m. EQUIL item 10 = {} takes the "
504 "contact as the reference depth, so the rows must agree "
505 "with it.",
506 regionIdx + 1, lastVapor, firstLiquid, reg.zgoc,
507 reg.initType));
508 }
509
510 OpmLog::warning(fmt::format("Equilibration region {}: the gas-oil contact at {} m lies "
511 "outside the COMPVD phase change between the last vapour "
512 "row at {} m and the first liquid row at {} m. With EQUIL "
513 "item 10 = 1 the rows give the composition versus depth, "
514 "so the gas zone ends at {} m instead.",
515 regionIdx + 1, reg.zgoc, lastVapor, firstLiquid, boundary));
516 return boundary;
517 }
518
520 static std::vector<std::size_t> rowsOfPhase(const CompvdTable& compvd,
521 const CompvdTable::Phase phase)
522 {
523 const auto& flags = compvd.phaseFlags();
524 std::vector<std::size_t> rows;
525 for (std::size_t r = 0; r < flags.size(); ++r) {
526 if (flags[r] == phase) {
527 rows.push_back(r);
528 }
529 }
530 return rows;
531 }
532
535 static std::optional<unsigned> statedPhase(const CompvdTable& compvd,
536 const std::size_t regionIdx)
537 {
538 const auto& flags = compvd.phaseFlags();
539 if (flags.empty()) {
540 return std::nullopt;
541 }
542
543 const auto first = flags.front();
544 if (std::ranges::any_of(flags, [first](const auto f) { return f != first; })) {
545 OpmLog::info(fmt::format("Equilibration region {}: COMPVD names both phases, "
546 "so the composition of each depth decides the phase.",
547 regionIdx + 1));
548 return std::nullopt;
549 }
550
551 return (first == CompvdTable::Phase::Vapor)
552 ? FluidSystem::gasPhaseIdx : FluidSystem::oilPhaseIdx;
553 }
554
556 static bool compositionVariesBetween(const Region& reg,
557 const Scalar depthA,
558 const Scalar depthB)
559 {
560 // Input mole fractions that differ physically across the contact should
561 // exceed this round-off tolerance.
562 constexpr Scalar sameComposition{1.0e-10};
563
564 const CompVec a = composition(reg, depthA);
565 const CompVec b = composition(reg, depthB);
566 return !std::ranges::equal(a, b, [](const Scalar x, const Scalar y) {
567 return std::abs(x - y) <= sameComposition;
568 });
569 }
570
571 Region setupRegion(const EquilRecord& record,
572 const TableManager& tables,
573 const std::vector<Scalar>& cellCenterDepth,
574 const std::vector<int>& eqlnum,
575 const Parallel::Communication& comm,
576 const Scalar gravity,
577 const int numSamplePoints,
578 const std::size_t regionIdx) const
579 {
580 Region reg;
581
582 reg.initType = record.compositionalInitType();
583 if (reg.initType != 1 && reg.initType != 3) {
584 OPM_THROW(std::runtime_error,
585 fmt::format("Compositional initialization type {} (EQUIL item 10) is "
586 "not supported for region {}; only type 1 (total "
587 "composition) and type 3 (liquid composition) are.",
588 reg.initType, regionIdx + 1));
589 }
590
591 if (record.gasOilContactCapillaryPressure() != 0.0) {
592 OPM_THROW(std::runtime_error,
593 fmt::format("Compositional equilibration only supports zero gas-oil "
594 "contact capillary pressure (EQUIL item 6); region {} "
595 "specifies {} bar.",
596 regionIdx + 1,
597 unit::convert::to(record.gasOilContactCapillaryPressure(),
598 unit::barsa)));
599 }
600
601 if (const auto accuracy = record.initializationTargetAccuracy(); accuracy != 0) {
602 OPM_THROW(std::runtime_error,
603 fmt::format("Compositional equilibration only supports cell-centre "
604 "initialization (EQUIL item 9 = 0); region {} specifies {}.",
605 regionIdx + 1, accuracy));
606 }
607
608 reg.zgoc = record.gasOilContactDepth();
609
610 // With one composition keyword, a missing regional record inherits the
611 // nearest preceding record. If both keywords are present, each region
612 // must select one explicitly because inheritance would be ambiguous.
613 const bool deckHasZmfvd = tables.hasTables("ZMFVD");
614 const bool deckHasCompvd = tables.hasTables("COMPVD");
615 const bool statesZmfvd = deckHasZmfvd &&
616 tables.getZmfvdTables().hasTable(regionIdx);
617 const bool statesCompvd = deckHasCompvd &&
618 tables.getCompvdTables().hasTable(regionIdx);
619
620 if (statesZmfvd && statesCompvd) {
621 OPM_THROW(std::runtime_error,
622 fmt::format("Region {} has both a ZMFVD and a COMPVD composition "
623 "versus depth; give only one of them.", regionIdx + 1));
624 }
625
626 std::optional<std::size_t> zmfvdAt;
627 std::optional<std::size_t> compvdAt;
628 if (deckHasZmfvd && deckHasCompvd) {
629 if (!statesZmfvd && !statesCompvd) {
630 OPM_THROW(std::runtime_error,
631 fmt::format("Region {} has neither a ZMFVD nor a COMPVD "
632 "composition versus depth. A deck using both "
633 "keywords has to give every region a record of "
634 "its own, as neither can be inherited.",
635 regionIdx + 1));
636 }
637 (statesZmfvd ? zmfvdAt : compvdAt) = regionIdx;
638 }
639 else if (deckHasZmfvd) {
640 zmfvdAt = sourceTable(tables.getZmfvdTables(), regionIdx);
641 }
642 else if (deckHasCompvd) {
643 compvdAt = sourceTable(tables.getCompvdTables(), regionIdx);
644 }
645
646 if (!zmfvdAt.has_value() && !compvdAt.has_value()) {
647 OPM_THROW(std::runtime_error,
648 fmt::format("Region {} has neither a ZMFVD nor a COMPVD composition "
649 "versus depth.", regionIdx + 1));
650 }
651
652 if (zmfvdAt.has_value()) {
653 reg.compositionKeyword = "ZMFVD";
654 const auto& zmfvd =
655 tables.getZmfvdTables().template getTable<ZmfvdTable>(*zmfvdAt);
656 setupComposition(reg.compositionVdTable, zmfvd,
657 allRows(zmfvd.getDepthColumn().size()));
658 }
659 else {
660 reg.compositionKeyword = "COMPVD";
661 const auto& compvd =
662 tables.getCompvdTables().template getTable<CompvdTable>(*compvdAt);
663 reg.statedPhaseIdx = statedPhase(compvd, regionIdx);
664
665 if (reg.statedPhaseIdx.has_value()) {
666 // One phase named: all rows form one composition profile, and
667 // the phase flag selects its EOS root.
668 if ((reg.initType == 3) &&
669 (*reg.statedPhaseIdx == FluidSystem::gasPhaseIdx)) {
670 OPM_THROW(std::runtime_error,
671 fmt::format("Region {} states a vapour composition in COMPVD "
672 "while EQUIL item 10 is 3, which takes the liquid "
673 "composition at the gas-oil contact.",
674 regionIdx + 1));
675 }
676 setupComposition(reg.compositionVdTable, compvd,
677 allRows(compvd.getDepthColumn().size()));
678 }
679 else {
680 // Both phases named: the vapour rows describe the gas zone and
681 // the liquid rows the one below the contact.
682 const auto vaporRows = rowsOfPhase(compvd, CompvdTable::Phase::Vapor);
683 const auto liquidRows = rowsOfPhase(compvd, CompvdTable::Phase::Liquid);
684 reg.zgoc = zoneBoundary(compvd, vaporRows, liquidRows, reg, regionIdx);
685 reg.twoZone = true;
686 reg.lastVaporDepth = compvd.getDepthColumn()[vaporRows.back()];
687 setupComposition(reg.vaporVdTable, compvd, vaporRows);
688 setupComposition(reg.compositionVdTable, compvd, liquidRows);
689 }
690 }
691
692 if (tables.hasTables("RTEMPVD")) {
693 const auto& rtempvd =
694 tables.getRtempvdTables().template getTable<RtempvdTable>(regionIdx);
695 std::vector<Scalar> tempDepths(rtempvd.getDepthColumn().begin(),
696 rtempvd.getDepthColumn().end());
697 const auto& tempCol = rtempvd.getTemperatureColumn();
698 std::vector<Scalar> temps(tempCol.begin(), tempCol.end());
699 // As for the composition tables, a single row is a depth-independent
700 // temperature and the interpolant needs a second sample point.
701 if (tempDepths.size() == 1) {
702 tempDepths.push_back(tempDepths.front() + constantTableSpan);
703 temps.push_back(temps.front());
704 }
705 reg.tempVdTable.setXYContainers(tempDepths, temps);
706 }
707 else {
708 const std::vector<Scalar> tempDepths{Scalar{0}, constantTableSpan};
709 const std::vector<Scalar> temps(tempDepths.size(), tables.rtemp());
710 reg.tempVdTable.setXYContainers(tempDepths, temps);
711 }
712
713 // Vertical extent of the region's cells across all processes.
714 auto span = std::array{std::numeric_limits<Scalar>::max(),
715 std::numeric_limits<Scalar>::lowest()};
716 for (std::size_t cell = 0; cell < cellCenterDepth.size(); ++cell) {
717 if (std::cmp_equal(eqlnum[cell], regionIdx)) {
718 span[0] = std::min(span[0], cellCenterDepth[cell]);
719 span[1] = std::max(span[1], cellCenterDepth[cell]);
720 }
721 }
722 span[0] = comm.min(span[0]);
723 span[1] = comm.max(span[1]);
724 if (span[0] > span[1]) {
725 // No cells anywhere in this region.
726 return reg;
727 }
728 if (span[1] - span[0] < minimumSpanExtent) {
729 span = {span[0] - minimumSpanExtent, span[1] + minimumSpanExtent};
730 }
731
732 const bool waterActive = FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx);
733 if (waterActive) {
734 reg.zwoc = record.waterOilContactDepth();
735 }
736
737 // A datum below the water-oil contact states the pressure of the water
738 // rather than of the hydrocarbon: integrate the water from there and
739 // hand the hydrocarbon its pressure at the contact. Otherwise the
740 // hydrocarbon is anchored at the datum and the water follows from the
741 // contact.
742 Scalar hcDatum = record.datumDepth();
743 Scalar hcPressure = record.datumDepthPressure();
744 const bool datumInWater = waterActive && (record.datumDepth() > reg.zwoc);
745 // A datum on the water-oil contact is no more a depth the hydrocarbon
746 // occupies than one below it, so the EOS root follows the fluid just
747 // above the contact in both cases.
748 reg.anchoredAtWaterContact = waterActive && (record.datumDepth() >= reg.zwoc);
749
750 if (datumInWater) {
751 integrateWaterPressure(reg, span, gravity, numSamplePoints,
752 record.datumDepth(), record.datumDepthPressure());
753 hcDatum = reg.zwoc;
754 hcPressure = reg.waterPressure->value(reg.zwoc)
755 + record.waterOilContactCapillaryPressure();
756 OpmLog::info(fmt::format("Equilibration region {}: the datum at {} m lies below the "
757 "water-oil contact at {} m, so it gives the water pressure; "
758 "the hydrocarbon pressure at the contact is {:.5} bar.",
759 regionIdx + 1, record.datumDepth(), reg.zwoc,
760 hcPressure / 1.0e5));
761 }
762
763 if (reg.initType == 1) {
764 setupSinglePhaseRegion(reg, span, gravity, numSamplePoints, regionIdx,
765 hcDatum, hcPressure);
766 }
767 else {
768 // Type 3 anchors the hydrocarbon at the gas-oil contact on its own
769 // saturation pressure, so it has no use for a datum that states the
770 // water pressure: the two columns would not meet at the water-oil
771 // contact.
772 if (datumInWater) {
773 OPM_THROW(std::runtime_error,
774 fmt::format("Compositional equilibration of region {} places the "
775 "datum at {} m, below the water-oil contact at {} m, "
776 "while EQUIL item 10 is 3. Put the datum in the "
777 "hydrocarbon column or use item 10 = 1.",
778 regionIdx + 1, record.datumDepth(), reg.zwoc));
779 }
780 setupTwoPhaseRegion(reg, record, span, gravity, numSamplePoints, regionIdx);
781 }
782
783 // With the datum in the hydrocarbon the water follows from the contact.
784 if (waterActive && !datumInWater) {
785 setupWaterZone(reg, record, span, gravity, numSamplePoints, regionIdx);
786 }
787 else if (waterActive) {
788 OpmLog::info(fmt::format("Equilibration region {}: the water-oil contact "
789 "is at {} m.", regionIdx + 1, reg.zwoc));
790 }
791
792 return reg;
793 }
794
801 void setupWaterZone(Region& reg,
802 const EquilRecord& record,
803 const std::array<Scalar, 2>& span,
804 const Scalar gravity,
805 const int numSamplePoints,
806 const std::size_t regionIdx) const
807 {
808 // The capillary pressure at the contact (EQUIL item 4) offsets the
809 // water pressure from the hydrocarbon pressure there.
810 if (!reg.oilPressure.has_value() && !reg.gasPressure.has_value()) {
811 return;
812 }
813 // The water meets the hydrocarbon zone just above its contact: the gas
814 // zone when the gas-oil contact lies at or below the water-oil contact.
815 const bool gasAtContact = reg.gasPressure.has_value()
816 && (!reg.oilPressure.has_value() || (reg.zwoc <= reg.zgoc));
817 const auto& hcPressure = gasAtContact ? reg.gasPressure : reg.oilPressure;
818 const Scalar pcow = record.waterOilContactCapillaryPressure();
819 const Scalar pContact = hcPressure->value(reg.zwoc) - pcow;
820
821 integrateWaterPressure(reg, span, gravity, numSamplePoints, reg.zwoc, pContact);
822
823 OpmLog::info(fmt::format("Equilibration region {}: the water-oil contact "
824 "is at {} m.", regionIdx + 1, reg.zwoc));
825 }
826
829 void integrateWaterPressure(Region& reg,
830 const std::array<Scalar, 2>& span,
831 const Scalar gravity,
832 const int numSamplePoints,
833 const Scalar depth,
834 const Scalar pressure) const
835 {
836 const WaterODE ode(reg.tempVdTable, eosType_, gravity);
837 reg.waterPressure.emplace(ode,
838 typename WaterPressFunc::InitCond{depth, pressure},
839 numSamplePoints, waterContactSpan(reg, span));
840 }
841
844 static Scalar waterLimit(const std::vector<Scalar>& limits,
845 const std::size_t cell,
846 const Scalar fallback)
847 {
848 return limits.empty() ? fallback : limits[cell];
849 }
850
865 void setupSinglePhaseRegion(Region& reg,
866 const std::array<Scalar, 2>& span,
867 const Scalar gravity,
868 const int numSamplePoints,
869 const std::size_t regionIdx,
870 const Scalar datum,
871 const Scalar datumPressure) const
872 {
873 if (reg.twoZone) {
874 setupTwoZoneRegion(reg, span, gravity, numSamplePoints, regionIdx,
875 datum, datumPressure);
876 return;
877 }
878
879 // COMPVD states the phase its composition belongs to; without that the
880 // datum's side of the gas-oil contact decides the EOS root. A datum on
881 // the water-oil contact is not in the hydrocarbon at all, so there the
882 // gas-oil contact itself still leaves gas above: a column whose two
883 // contacts coincide holds no liquid.
884 const bool gasAtDatum = reg.anchoredAtWaterContact ? (datum <= reg.zgoc)
885 : (datum < reg.zgoc);
886 const auto phaseIdx = reg.statedPhaseIdx.value_or(
887 gasAtDatum ? FluidSystem::gasPhaseIdx : FluidSystem::oilPhaseIdx);
888
889 // A gas-oil contact inside a single-zone region requires the composition
890 // to vary across it, so the flash can label the phases correctly.
891 if ((reg.zgoc > span[0]) && (reg.zgoc < span[1]) &&
892 !compositionVariesBetween(reg, span[0], reg.zgoc) &&
893 !compositionVariesBetween(reg, reg.zgoc, span[1])) {
894 OpmLog::warning(fmt::format("Equilibration region {}: the gas-oil contact "
895 "at {} m lies inside a type-1 region, but the "
896 "composition does not vary across the contact. "
897 "Compositional variation is required for proper "
898 "phase labeling.", regionIdx + 1, reg.zgoc));
899 }
900
901 const ODE ode([&reg](const Scalar depth) { return composition(reg, depth); },
902 reg.tempVdTable, phaseIdx, eosType_, gravity);
903 reg.oilPressure.emplace(ode,
904 typename PressFunc::InitCond{datum, datumPressure},
905 numSamplePoints, waterContactSpan(reg, span));
906 reg.nominalPhaseIdx = phaseIdx;
907
908 OpmLog::info(fmt::format("Equilibration region {}: pressure integrated with one "
909 "EOS root and the total composition from {} "
910 "(EQUIL item 10 = 1).",
911 regionIdx + 1, reg.compositionKeyword));
912 }
913
917 void setupTwoZoneRegion(Region& reg,
918 const std::array<Scalar, 2>& span,
919 const Scalar gravity,
920 const int numSamplePoints,
921 const std::size_t regionIdx,
922 const Scalar datum,
923 const Scalar datumPressure) const
924 {
925 const ODE liquidOde([&reg](const Scalar depth) { return composition(reg, depth); },
926 reg.tempVdTable, FluidSystem::oilPhaseIdx, eosType_, gravity);
927 const ODE gasOde([&reg](const Scalar depth) { return vaporComposition(reg, depth); },
928 reg.tempVdTable, FluidSystem::gasPhaseIdx, eosType_, gravity);
929
930 // The datum-side pressure function must reach the actual contact even
931 // when it lies outside the cell span, because the other pressure function
932 // is initialized from its value at the contact.
933 const auto pressureSpan = waterContactSpan(reg, span);
934 const std::array<Scalar, 2> datumSpan{std::min(pressureSpan[0], reg.zgoc),
935 std::max(pressureSpan[1], reg.zgoc)};
936 if ((reg.zgoc < span[0]) || (reg.zgoc > span[1])) {
937 OpmLog::warning(fmt::format("Equilibration region {}: the gas zone ends at {} m, "
938 "outside the cells of the region, so the COMPVD "
939 "rows of one phase describe no cell.",
940 regionIdx + 1,
941 reg.zgoc));
942 }
943
944 if (datum < reg.zgoc) {
945 reg.gasPressure.emplace(gasOde,
946 typename PressFunc::InitCond{datum, datumPressure},
947 numSamplePoints, datumSpan);
948 reg.oilPressure.emplace(liquidOde,
949 typename PressFunc::InitCond{
950 reg.zgoc, reg.gasPressure->value(reg.zgoc)},
951 numSamplePoints, pressureSpan);
952 }
953 else {
954 reg.oilPressure.emplace(liquidOde,
955 typename PressFunc::InitCond{datum, datumPressure},
956 numSamplePoints, datumSpan);
957 reg.gasPressure.emplace(gasOde,
958 typename PressFunc::InitCond{
959 reg.zgoc, reg.oilPressure->value(reg.zgoc)},
960 numSamplePoints, pressureSpan);
961 }
962
963 OpmLog::info(fmt::format("Equilibration region {}: COMPVD gives a gas zone above "
964 "{} m and a liquid one below it "
965 "(EQUIL item 10 = 1).",
966 regionIdx + 1,
967 reg.zgoc));
968 }
969
977 void setupTwoPhaseRegion(Region& reg,
978 const EquilRecord& record,
979 const std::array<Scalar, 2>& span,
980 const Scalar gravity,
981 const int numSamplePoints,
982 const std::size_t regionIdx) const
983 {
984 const Scalar inputReferenceDepth = record.datumDepth();
985 if (inputReferenceDepth != reg.zgoc) {
986 OpmLog::warning(fmt::format("Equilibration region {}: the reference depth {} m "
987 "does not coincide with the gas-oil contact when "
988 "EQUIL item 10 is 3; resetting it to the contact "
989 "depth {} m.",
990 regionIdx + 1, inputReferenceDepth, reg.zgoc));
991 }
992
993 const CompVec liquid = composition(reg, reg.zgoc);
994 const Scalar temp = Details::evalDepthTable(reg.tempVdTable, reg.zgoc);
995 Scalar psat{};
996 CompVec vapor{};
997 if (!SaturationPressure<Scalar, FluidSystem>::bubblePressure(liquid, temp, eosType_,
998 psat, vapor)) {
999 OPM_THROW(std::runtime_error,
1000 fmt::format("The saturation pressure calculation at the gas-oil "
1001 "contact of region {} did not converge.", regionIdx + 1));
1002 }
1003 reg.vaporComposition = vapor;
1004
1005 // For type 3, the contact is the reference depth. With item 11
1006 // defaulted, the input pressure must agree with the saturation pressure
1007 // to within one atmosphere and is reset to the computed value otherwise.
1008 // Item 11 = 1 retains the numeric input pressure at the contact regardless
1009 // of that test; the result need not be an equilibrium system in that case.
1010 constexpr Scalar oneAtmosphere = unit::atm;
1011 const Scalar inputPressure = record.datumDepthPressure();
1012 const bool resetToPsat = record.setToSaturationPressure()
1013 && (std::abs(inputPressure - psat) >= oneAtmosphere);
1014 const Scalar referencePressure = resetToPsat ? psat : inputPressure;
1015
1016 OpmLog::info(fmt::format("Equilibration region {}: two phases, liquid composition "
1017 "specified (EQUIL item 10 is 3). The saturation pressure "
1018 "at the gas-oil contact ({} m) is {:.6g} bar.",
1019 regionIdx + 1, reg.zgoc,
1020 unit::convert::to(psat, unit::barsa)));
1021
1022 if (resetToPsat) {
1023 OpmLog::warning(fmt::format("Equilibration region {}: the datum pressure {:.6g} bar "
1024 "differs from the saturation pressure {:.6g} bar at the "
1025 "gas-oil contact by one atmosphere or more; the "
1026 "saturation pressure is used instead.",
1027 regionIdx + 1,
1028 unit::convert::to(inputPressure, unit::barsa),
1029 unit::convert::to(psat, unit::barsa)));
1030 }
1031
1032 const ODE oilOde([&reg](const Scalar depth) { return composition(reg, depth); },
1033 reg.tempVdTable, FluidSystem::oilPhaseIdx, eosType_, gravity);
1034 reg.oilPressure.emplace(oilOde,
1035 typename PressFunc::InitCond{reg.zgoc, referencePressure},
1036 numSamplePoints, waterContactSpan(reg, span));
1037
1038 // Integrate a two-zone gas column with the same depth-dependent
1039 // composition assigned to its cells. Otherwise use the equilibrium
1040 // vapour composition at the contact throughout the gas column.
1041 typename ODE::CompositionFunction gasComposition;
1042 if (reg.twoZone) {
1043 gasComposition = [&reg](const Scalar depth) { return vaporComposition(reg, depth); };
1044 }
1045 else {
1046 gasComposition = [vapor](const Scalar) { return vapor; };
1047 }
1048 const ODE gasOde(gasComposition,
1049 reg.tempVdTable, FluidSystem::gasPhaseIdx, eosType_, gravity);
1050 reg.gasPressure.emplace(gasOde,
1051 typename PressFunc::InitCond{reg.zgoc, referencePressure},
1052 numSamplePoints, waterContactSpan(reg, span));
1053 }
1054
1060 static bool isInGasZone(const Region& reg, const Scalar depth)
1061 {
1062 if (reg.twoZone && (reg.initType == 1) && (depth <= reg.lastVaporDepth)) {
1063 return true;
1064 }
1065 return ((reg.initType == 3) || reg.twoZone) && (depth < reg.zgoc);
1066 }
1067
1068 Scalar assignCell(FluidState& fs, const Region& reg, const Scalar depth,
1069 const std::size_t cell) const
1070 {
1071 const bool inGasZone = isInGasZone(reg, depth);
1072
1073 const CompVec z = [&reg, depth, inGasZone]() {
1074 if (!inGasZone) {
1075 return composition(reg, depth);
1076 }
1077 // Type 3 holds the contact vapour above the contact; a two-zone
1078 // COMPVD table gives the gas zone its own composition versus depth.
1079 return reg.twoZone ? vaporComposition(reg, depth) : reg.vaporComposition;
1080 }();
1081 const auto& pressFunc = inGasZone ? reg.gasPressure : reg.oilPressure;
1082 if (!pressFunc.has_value()) {
1083 OPM_THROW(std::runtime_error,
1084 "Evaluating the equilibrated pressure of a region without cells.");
1085 }
1086
1087 // The common pressure used by the current flash follows the water
1088 // column below the contact, including when the scaled maximum water
1089 // saturation leaves some residual hydrocarbon. Keep this separate from
1090 // the phase pressures so equilibration retains the capillary offset.
1091 const Scalar hydrocarbonPressure = pressFunc->value(depth);
1092 const bool inWaterZone = (depth > reg.zwoc) && reg.waterPressure.has_value();
1093 const Scalar press = inWaterZone ? reg.waterPressure->value(depth)
1094 : hydrocarbonPressure;
1095
1096 fs.setTemperature(Details::evalDepthTable(reg.tempVdTable, depth));
1097 for (unsigned phaseIdx = 0; phaseIdx < FluidSystem::numPhases; ++phaseIdx) {
1098 if (FluidSystem::phaseIsActive(phaseIdx)) {
1099 fs.setPressure(phaseIdx, hydrocarbonPressure);
1100 fs.setSaturation(phaseIdx, 0.0);
1101 }
1102 }
1103
1104 // Below the water-oil contact the pore space holds water alone; above
1105 // it the hydrocarbon leaves room for the connate water only.
1106 Scalar sWat = 0.0;
1107 if (FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)) {
1108 // The saturation function's own endpoints, per cell: below the
1109 // contact the water fills what it can, above it only the connate
1110 // water remains.
1111 sWat = (depth > reg.zwoc) ? waterLimit(maxWater_, cell, Scalar{1})
1112 : waterLimit(connateWater_, cell, Scalar{0});
1113 fs.setSaturation(FluidSystem::waterPhaseIdx, sWat);
1114 if (reg.waterPressure.has_value()) {
1115 fs.setPressure(FluidSystem::waterPhaseIdx, reg.waterPressure->value(depth));
1116 }
1117 }
1118
1119 // Set a nominal single-phase saturation for the hydrocarbon using the
1120 // phase represented by the pressure integration. The downstream flash
1121 // recomputes the phase split from composition, pressure, and temperature.
1122 fs.setSaturation(inGasZone ? FluidSystem::gasPhaseIdx : reg.nominalPhaseIdx,
1123 Scalar{1} - sWat);
1124
1125 for (int c = 0; c < numComponents; ++c) {
1126 fs.setMoleFraction(c, z[c]);
1127 }
1128 return press;
1129 }
1130
1131 CompositionalConfig::EOSType eosType_;
1135 std::vector<Scalar> connateWater_;
1136 std::vector<Scalar> maxWater_;
1137 std::vector<FluidState> fluidStates_;
1138 std::vector<Scalar> referencePressures_;
1139};
1140
1141} // namespace Opm::EQUIL::Comp
1142
1143#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:93
Tabulated1DFunction< Scalar > TabulatedFunction
Definition: InitStateEquilComp.hpp:98
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquilComp.hpp:112
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:95
std::array< Scalar, FluidSystem::numComponents > CompVec
Definition: InitStateEquilComp.hpp:96
std::function< CompVec(Scalar)> CompositionFunction
Definition: InitStateEquilComp.hpp:97
EosDensityODE(CompositionFunction composition, const TabulatedFunction &tempVdTable, const unsigned phaseIdx, const CompositionalConfig::EOSType eosType, const Scalar normGrav)
Definition: InitStateEquilComp.hpp:100
Definition: InitStateEquilComp.hpp:144
Scalar operator()(const Scalar depth, const Scalar press) const
Definition: InitStateEquilComp.hpp:157
WaterDensityODE(const TabulatedFunction &tempVdTable, const CompositionalConfig::EOSType eosType, const Scalar normGrav)
Definition: InitStateEquilComp.hpp:149
Tabulated1DFunction< Scalar > TabulatedFunction
Definition: InitStateEquilComp.hpp:147
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:146
Computes the initial state of a compositional model from hydrostatic equilibrium (the EQUIL and ZMFVD...
Definition: InitStateEquilComp.hpp:214
CompositionalFluidState< Scalar, FluidSystem > FluidState
Definition: InitStateEquilComp.hpp:217
typename FluidSystem::Scalar Scalar
Definition: InitStateEquilComp.hpp:216
const std::vector< Scalar > & referencePressures() const
Definition: InitStateEquilComp.hpp:305
const std::vector< FluidState > & fluidStates() const
Definition: InitStateEquilComp.hpp:299
InitialStateComputer(const EclipseState &inputState, const CompositionalConfig::EOSType eosType, const std::vector< Scalar > &cellCenterDepth, const std::vector< int > &eqlnum, const Parallel::Communication &comm, const Scalar gravity, const int numSamplePoints, const std::vector< Scalar > &connateWater={}, const std::vector< Scalar > &maxWater={})
Definition: InitStateEquilComp.hpp:231
std::vector< FluidState > & fluidStates()
Definition: InitStateEquilComp.hpp:296
Definition: PressureFunction.hpp:128
Scalar evalDepthTable(const Tabulated1DFunction< Scalar > &table, const Scalar depth)
Definition: InitStateEquilComp.hpp:82
Definition: InitStateEquilComp.hpp:74
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