Skip to content

Commit ef64102

Browse files
committed
Write compositional saturation pressure to restart output
Handle PSAT requests in compositional restart output. Use the cell pressure when both hydrocarbon phases are present, solve the bubble or dew pressure for exact single-phase flash labels, and write zero when no boundary can be resolved. Allocate PSAT only for restart output and calculate it in the OpenMP cell loop. Make output validation virtual and run the MPI reductions after rank-wide exception handling, preventing a local failure from leaving ranks in different collectives. Reduce the failure count across ranks and log it once with clear singular and plural wording. Keep the phase decision independent of simulator state and test restart allocation, phase labels near round-off boundaries, solver results, and the failure contract.
1 parent 22cff7c commit ef64102

7 files changed

Lines changed: 341 additions & 8 deletions

CMakeLists_files.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ list (APPEND TEST_SOURCE_FILES
468468
tests/test_aqantrc_flow_keyword.cpp
469469
tests/test_blackoil_amg.cpp
470470
tests/test_blackoilprimaryvariables.cpp
471+
tests/test_compositionalcontainer.cpp
471472
tests/test_compwell_equations.cpp
472473
tests/test_compwell_jacobian.cpp
473474
tests/test_convergenceoutputconfiguration.cpp

opm/simulators/flow/CompositionalContainer.cpp

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@
2323
#include <config.h>
2424
#include <opm/simulators/flow/CompositionalContainer.hpp>
2525

26+
#include <opm/input/eclipse/EclipseState/Compositional/CompositionalConfig.hpp>
27+
28+
#include <opm/material/constraintsolvers/SaturationPressure.hpp>
2629
#include <opm/material/fluidsystems/GenericOilGasWaterFluidSystem.hpp>
2730

2831
#include <opm/output/data/Solution.hpp>
2932

3033
#include <algorithm>
34+
#include <optional>
3135
#include <tuple>
3236

3337
#include <fmt/format.h>
@@ -37,7 +41,8 @@ namespace Opm {
3741
template<class FluidSystem>
3842
void CompositionalContainer<FluidSystem>::
3943
allocate(const unsigned bufferSize,
40-
std::map<std::string, int>& rstKeywords)
44+
std::map<std::string, int>& rstKeywords,
45+
const bool isRestartOutput)
4146
{
4247
if (auto& zmf = rstKeywords["ZMF"]; zmf > 0) {
4348
this->allocated_ = true;
@@ -75,6 +80,17 @@ allocate(const unsigned bufferSize,
7580
gasPressure_.resize(bufferSize, 0.0);
7681
}
7782

83+
// Summary-only substeps must not retain a PSAT buffer from a previous
84+
// preparation pass: its presence enables a nonlinear solve in every cell.
85+
saturationPressure_.clear();
86+
if (auto& psat = rstKeywords["PSAT"]; psat > 0) {
87+
psat = 0;
88+
if (isRestartOutput) {
89+
this->allocated_ = true;
90+
saturationPressure_.resize(bufferSize, 0.0);
91+
}
92+
}
93+
7894
if (auto& vmf = rstKeywords["VMF"]; vmf > 0) {
7995
this->allocated_ = true;
8096
vmf = 0;
@@ -140,6 +156,16 @@ assignPhasePressures(const unsigned globalDofIdx,
140156
}
141157
}
142158

159+
template<class FluidSystem>
160+
void CompositionalContainer<FluidSystem>::
161+
assignSaturationPressure(const unsigned globalDofIdx,
162+
const Scalar psat)
163+
{
164+
if (!saturationPressure_.empty()) {
165+
saturationPressure_[globalDofIdx] = psat;
166+
}
167+
}
168+
143169
template<class FluidSystem>
144170
void CompositionalContainer<FluidSystem>::
145171
assignVaporFraction(const unsigned globalDofIdx,
@@ -205,6 +231,7 @@ outputRestart(data::Solution& sol,
205231

206232
entries.emplace_back("POIL", UnitSystem::measure::pressure, oilPressure_);
207233
entries.emplace_back("PGAS", UnitSystem::measure::pressure, gasPressure_);
234+
entries.emplace_back("PSAT", UnitSystem::measure::pressure, saturationPressure_);
208235
entries.emplace_back("VMF", UnitSystem::measure::identity, vaporFraction_);
209236

210237
std::ranges::for_each(entries,
@@ -214,6 +241,38 @@ outputRestart(data::Solution& sol,
214241
this->allocated_ = false;
215242
}
216243

244+
template<class FluidSystem>
245+
auto CompositionalContainer<FluidSystem>::
246+
cellSaturationPressure(const Scalar liquidFraction,
247+
const Scalar oilPressure,
248+
const std::array<Scalar, numComponents>& moleFractions,
249+
const Scalar temperature,
250+
const CompositionalConfig::EOSType eosType) -> std::optional<Scalar>
251+
{
252+
// Compare the flash's exact single-phase labels: a two-phase Rachford-Rice
253+
// result can round slightly outside (0, 1).
254+
const bool liquidOnly = (liquidFraction == 1.0);
255+
const bool vapourOnly = (liquidFraction == 0.0);
256+
if (!liquidOnly && !vapourOnly) {
257+
return oilPressure;
258+
}
259+
260+
// The zero-component instantiation exists only to register a parameter
261+
// and has no equation of state to solve.
262+
if constexpr (numComponents > 0) {
263+
using Solver = SaturationPressure<Scalar, FluidSystem>;
264+
typename Solver::CompVec incipient;
265+
Scalar psat = 0.0;
266+
const bool found = liquidOnly
267+
? Solver::bubblePressure(moleFractions, temperature, eosType, psat, incipient)
268+
: Solver::dewPressure(moleFractions, temperature, eosType, psat, incipient);
269+
if (found) {
270+
return psat;
271+
}
272+
}
273+
return std::nullopt;
274+
}
275+
217276
#define INSTANTIATE_COMP_THREEPHASE(NUM) \
218277
template<class T> using FS##NUM = GenericOilGasWaterFluidSystem<T, NUM, true>; \
219278
template class CompositionalContainer<FS##NUM<double>>;

opm/simulators/flow/CompositionalContainer.hpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
#ifndef OPM_COMPOSITIONAL_CONTAINER_HPP
2727
#define OPM_COMPOSITIONAL_CONTAINER_HPP
2828

29+
#include <opm/input/eclipse/EclipseState/Compositional/CompositionalConfig.hpp>
30+
2931
#include <array>
3032
#include <functional>
3133
#include <map>
34+
#include <optional>
3235
#include <string>
3336
#include <vector>
3437

@@ -51,7 +54,8 @@ class CompositionalContainer
5154

5255
public:
5356
void allocate(const unsigned bufferSize,
54-
std::map<std::string, int>& rstKeywords);
57+
std::map<std::string, int>& rstKeywords,
58+
const bool isRestartOutput);
5559

5660
using AssignFunction = std::function<Scalar(const unsigned)>;
5761

@@ -68,6 +72,24 @@ class CompositionalContainer
6872
const Scalar oilPressure,
6973
const Scalar gasPressure);
7074

75+
void assignSaturationPressure(const unsigned globalDofIdx,
76+
const Scalar psat);
77+
78+
/// Return the cell pressure for two hydrocarbon phases, or the bubble/dew
79+
/// pressure of the total composition for a single phase. Return std::nullopt
80+
/// if the solver cannot determine a saturation pressure.
81+
///
82+
/// \p liquidFraction is the flash liquid fraction L: exactly one denotes
83+
/// liquid only, exactly zero vapour only, and other values denote two phases.
84+
/// Use L because computed phase saturations can contain round-off residuals.
85+
/// Plain-value arguments allow testing phase selection without a simulator.
86+
[[nodiscard]] static std::optional<Scalar>
87+
cellSaturationPressure(const Scalar liquidFraction,
88+
const Scalar oilPressure,
89+
const std::array<Scalar, numComponents>& moleFractions,
90+
const Scalar temperature,
91+
const CompositionalConfig::EOSType eosType);
92+
7193
void assignVaporFraction(const unsigned globalDofIdx,
7294
const Scalar vmf);
7395

@@ -89,6 +111,9 @@ class CompositionalContainer
89111
bool vaporFractionAllocated() const
90112
{ return !vaporFraction_.empty(); }
91113

114+
bool saturationPressureAllocated() const
115+
{ return !saturationPressure_.empty(); }
116+
92117
bool allocated() const
93118
{ return allocated_; }
94119

@@ -101,6 +126,8 @@ class CompositionalContainer
101126
// phase pressures (POIL, PGAS)
102127
ScalarBuffer oilPressure_;
103128
ScalarBuffer gasPressure_;
129+
// saturation pressure (PSAT)
130+
ScalarBuffer saturationPressure_;
104131
// vapour mole fraction of the total mixture (VMF)
105132
ScalarBuffer vaporFraction_;
106133
};

opm/simulators/flow/EclWriter.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -863,8 +863,6 @@ class EclWriter : public EclGenericWriter<GetPropType<TypeTag, Properties::Grid>
863863
this->outputModule_->processElementBlockData(elemCtx);
864864
}
865865
this->outputModule_->clearExtractors();
866-
867-
this->outputModule_->accumulateDensityParallel();
868866
}
869867

870868
{
@@ -881,10 +879,12 @@ class EclWriter : public EclGenericWriter<GetPropType<TypeTag, Properties::Grid>
881879
}
882880
}
883881

884-
this->outputModule_->validateLocalData();
885-
886882
OPM_END_PARALLEL_TRY_CATCH("EclWriter::prepareLocalCellData() failed: ",
887883
this->simulator_.vanguard().grid().comm());
884+
885+
// Complete rank-wide exception handling before entering output collectives.
886+
this->outputModule_->accumulateDensityParallel();
887+
this->outputModule_->validateLocalData();
888888
}
889889

890890
void captureLocalFluxData()

opm/simulators/flow/GenericOutputModule.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,10 @@ class GenericOutputModule {
286286
local_data_valid_ = false;
287287
}
288288

289-
void validateLocalData(){
289+
/// Mark per-cell output data valid. All ranks must call this method,
290+
/// since overrides may reduce per-rank data.
291+
virtual void validateLocalData()
292+
{
290293
local_data_valid_ = true;
291294
}
292295

opm/simulators/flow/OutputCompositionalModule.hpp

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include <opm/common/TimingMacros.hpp>
3737
#include <opm/common/OpmLog/OpmLog.hpp>
3838

39+
#include <opm/input/eclipse/EclipseState/Compositional/CompositionalConfig.hpp>
3940
#include <opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.hpp>
4041

4142
#include <opm/material/common/Valgrind.hpp>
@@ -52,6 +53,7 @@
5253
#include <opm/simulators/flow/OutputExtractor.hpp>
5354

5455
#include <algorithm>
56+
#include <array>
5557
#include <cstddef>
5658
#include <fstream>
5759
#include <memory>
@@ -62,6 +64,7 @@
6264
#include <utility>
6365
#include <vector>
6466

67+
#include <fmt/format.h>
6568

6669
namespace Opm {
6770

@@ -127,6 +130,7 @@ class OutputCompositionalModule : public GenericOutputModule<GetPropType<TypeTag
127130
getPropValue<TypeTag, Properties::EnableBioeffects>(),
128131
getPropValue<TypeTag, Properties::EnableGeochemistry>())
129132
, simulator_(simulator)
133+
, eosType_(simulator.vanguard().eclState().compositionalConfig().eosType(0))
130134
{
131135
for (auto& region_pair : this->regions_) {
132136
this->createLocalRegion_(region_pair.second);
@@ -184,7 +188,10 @@ class OutputCompositionalModule : public GenericOutputModule<GetPropType<TypeTag
184188
}
185189

186190
auto rstKeywords = this->schedule_.rst_keywords(reportStepNum);
187-
this->compC_.allocate(bufferSize, rstKeywords);
191+
const bool isRestartOutput =
192+
isRestart || (!substep && this->schedule_.write_rst_file(reportStepNum));
193+
this->compC_.allocate(bufferSize, rstKeywords, isRestartOutput);
194+
this->numFailedSaturationPressures_ = 0;
188195

189196
this->doAllocBuffers(bufferSize, reportStepNum, substep, log, isRestart,
190197
/* hysteresisConfig = */ nullptr,
@@ -714,6 +721,26 @@ class OutputCompositionalModule : public GenericOutputModule<GetPropType<TypeTag
714721
intQuants,
715722
totVolume,
716723
referencePorosity);
724+
725+
// Run the nonlinear PSAT solve in the caller's OpenMP loop.
726+
// The assignment skips cells when no restart buffer is allocated.
727+
this->assignSaturationPressure_(globalDofIdx, intQuants.fluidState());
728+
}
729+
730+
/// Reduce PSAT failures across all ranks before marking the output data valid.
731+
void validateLocalData() override
732+
{
733+
const auto& comm = this->simulator_.gridView().comm();
734+
const auto totalFailures = comm.sum(this->numFailedSaturationPressures_);
735+
this->numFailedSaturationPressures_ = 0;
736+
if (totalFailures > 0 && comm.rank() == 0) {
737+
const auto* const cell = totalFailures == 1 ? "cell" : "cells";
738+
OpmLog::info(fmt::format("Could not determine saturation pressure in {} {}; "
739+
"PSAT is written as zero for every affected cell.",
740+
totalFailures,
741+
cell));
742+
}
743+
BaseType::validateLocalData();
717744
}
718745

719746
protected:
@@ -779,8 +806,41 @@ class OutputCompositionalModule : public GenericOutputModule<GetPropType<TypeTag
779806
}
780807
}
781808

809+
/// Store the cell's saturation pressure, using zero for an unsuccessful solve.
810+
/// Concurrent calls must use distinct cell indices; the failure count is atomic.
811+
template<class FluidState>
812+
void assignSaturationPressure_(const unsigned globalDofIdx, const FluidState& fluidState)
813+
{
814+
if (!this->compC_.saturationPressureAllocated()) {
815+
return;
816+
}
817+
818+
std::array<Scalar, numComponents> moleFractions;
819+
for (int c = 0; c < numComponents; ++c) {
820+
moleFractions[c] = getValue(fluidState.moleFraction(c));
821+
}
822+
const auto psat = CompositionalContainer<FluidSystem>::cellSaturationPressure(
823+
getValue(fluidState.L()),
824+
getValue(fluidState.pressure(oilPhaseIdx)),
825+
moleFractions,
826+
getValue(fluidState.temperature(oilPhaseIdx)),
827+
this->eosType_);
828+
if (!psat) {
829+
// Failure includes supercritical mixtures; it does not establish
830+
// whether a saturation pressure exists.
831+
#ifdef _OPENMP
832+
#pragma omp atomic
833+
#endif
834+
++this->numFailedSaturationPressures_;
835+
}
836+
837+
this->compC_.assignSaturationPressure(globalDofIdx, psat.value_or(Scalar{0}));
838+
}
839+
782840
const Simulator& simulator_;
783841
CompositionalContainer<FluidSystem> compC_;
842+
CompositionalConfig::EOSType eosType_;
843+
std::size_t numFailedSaturationPressures_{};
784844
std::vector<typename Extractor::Entry> extractors_;
785845
typename BlockExtractor::ExecMap blockExtractors_;
786846
};

0 commit comments

Comments
 (0)