Files
opm-upscaling/opm/upscaling/RelPermUtils.cpp
T

1222 lines
43 KiB
C++

/*
Copyright 2010 Statoil ASA.
This file is part of The Open Porous Media project (OPM).
OPM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OPM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <opm/upscaling/RelPermUtils.hpp>
#include <opm/parser/eclipse/Parser/ParserKeywords.hpp>
#include <opm/parser/eclipse/Units/Units.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <fstream>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
namespace {
double to_double(const std::string& x)
{
return std::strtod(x.c_str(), nullptr);
}
int to_int(const std::string& x)
{
return static_cast<int>(std::strtol(x.c_str(), nullptr, 10));
}
std::vector<double>::size_type
sub2ind(std::array<std::vector<double>::size_type, 2>&& n,
std::array<std::vector<double>::size_type, 3>&& ijk)
{
return ijk[0] + n[0]*(ijk[1] + n[1]*ijk[2]);
}
std::vector<std::vector<double>::size_type>
zcorn_offset(const std::vector<double>::size_type nx,
const std::vector<double>::size_type ny)
{
using sz_t = std::vector<double>::size_type;
auto off = std::vector<sz_t>{};
off.reserve(2 * 2 * 2);
const auto n1 = 2 * nx;
const auto n2 = 2 * ny;
for (sz_t k = 0; k < 2; ++k) {
for (sz_t j = 0; j < 2; ++j) {
for (sz_t i = 0; i < 2; ++i) {
off.push_back(sub2ind({n1, n2}, { i, j, k }));
}}}
return off;
}
std::vector<double>::size_type
zcorn_start(const std::vector<double>::size_type nx,
const std::vector<double>::size_type ny,
const std::array<int,3>& ijk)
{
using sz_t = std::vector<double>::size_type;
auto x2 = [](const sz_t x) { return 2 * x; };
return sub2ind({ x2(nx) , x2(ny) },
{ x2(ijk[0]), x2(ijk[1]), x2(ijk[2]) });
}
} // Anonymous
class ArithmeticAverage
{
public:
void add(const double x);
double value() const;
private:
double x_{ 0.0 };
double n_{ 0.0 };
};
void ArithmeticAverage::add(const double x)
{
x_ += x;
n_ += 1.0;
}
double ArithmeticAverage::value() const
{
return x_ / n_;
}
class ModelThickness
{
public:
ModelThickness(const std::array<int, 3>& cdim,
const std::vector<double>& zcorn);
void activateCell(const std::array<int, 3>& ijk);
double thickness() const;
private:
using SizeType = std::vector<double>::size_type;
const std::array<int, 3> cdim_;
const std::vector<double>& zcorn_;
const std::vector<SizeType> off_;
std::vector<ArithmeticAverage> horizon_;
};
ModelThickness::ModelThickness(const std::array<int, 3>& cdim,
const std::vector<double>& zcorn)
: cdim_ (cdim)
, zcorn_ (zcorn)
, off_ (zcorn_offset(cdim[0], cdim[1]))
, horizon_(2 * cdim[2])
{}
void ModelThickness::activateCell(const std::array<int, 3>& ijk)
{
const auto start = zcorn_start(cdim_[0], cdim_[1], ijk);
auto* const layer = horizon_.data() +
static_cast<SizeType>(2 * ijk[2]);
const auto corners_per_layer = off_.size() / 2;
auto ncorners = SizeType{0};
for (const auto& corner : off_) {
layer[ncorners++ / corners_per_layer]
.add(zcorn_[start + corner]);
}
}
double ModelThickness::thickness() const
{
const auto& top = horizon_.front();
const auto& bot = horizon_.back();
return bot.value() - top.value();
}
namespace Opm {
static const std::vector<size_t> voigt_idx_tab = {0,4,8,5,2,1,7,6,3}; //!< Voigt-to-C index table
// Assumes that permtensor_t use C ordering.
double getVoigtValue(const SinglePhaseUpscaler::permtensor_t& K, int voigt_idx)
{
#if !defined(NDEBUG)
OPM_ERROR_IF(not ((K.numRows() == 3) && (K.numCols() == 3)),
"Function getVoigtValue() is only supported "
"for 3-by-3 tensors");
#endif
if (voigt_idx < 0 || voigt_idx > 8) {
std::cerr << "Voigt index out of bounds (only 0-8 allowed)" << std::endl;
throw std::exception();
}
return K.data()[voigt_idx_tab[voigt_idx]];
}
// Assumes that permtensor_t use C ordering.
void setVoigtValue(SinglePhaseUpscaler::permtensor_t& K, int voigt_idx, double val)
{
#if !defined(NDEBUG)
OPM_ERROR_IF(not ((K.numRows() == 3) && (K.numCols() == 3)),
"Function setVoigtValue() is only supported "
"for 3-by-3 tensors.");
#endif
if (voigt_idx < 0 || voigt_idx > 8) {
std::cerr << "Voigt index out of bounds (only 0-8 allowed)" << std::endl;
throw std::exception();
}
K.data()[voigt_idx_tab[voigt_idx]] = val;
}
RelPermUpscaleHelper::RelPermUpscaleHelper(int mpi_rank,
std::map<std::string,std::string>& options_)
: isMaster (mpi_rank == 0)
, anisotropic_input(false)
, permTensor (3, 3, nullptr)
, tesselatedCells (0)
, permTensorInv (3, 3, nullptr)
, options (options_)
{
{
const auto& fluids = options["fluids"];
if ((fluids == "ow") || (fluids == "wo")) {
saturationstring = "Sw";
}
else if ((fluids == "go") || (fluids == "og")) {
saturationstring = "Sg";
}
else {
std::stringstream str;
str << "Fluidsystem " << fluids
<< " not valid (-fluids option). Should be ow or go";
throw std::runtime_error(str.str());
}
}
critRelpThresh = to_double(options["critRelpermThresh"]);
doEclipseCheck = options["doEclipseCheck"] == "true";
}
void RelPermUpscaleHelper::collectResults()
{
#ifdef HAVE_MPI
/* Step 8b: Transfer all computed data to master node.
Master node should post a receive for all values missing,
other nodes should post a send for all the values they have.
*/
int mpi_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
MPI_Barrier(MPI_COMM_WORLD); // Not strictly necessary.
if (isMaster) {
// Loop over all values, receive data and put into local data structure
for (int idx=0; idx < points; ++idx) {
if (node_vs_pressurepoint[idx] != 0) {
// Receive data
if (upscaleBothPhases) {
std::vector<double> recvbuffer(2+2*tensorElementCount);
MPI_Recv(recvbuffer.data(), recvbuffer.size(), MPI_DOUBLE,
node_vs_pressurepoint[idx], 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// Put received data into correct place.
WaterSaturation[(int)recvbuffer[0]] = recvbuffer[1];
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
PhasePerm[0][(int)recvbuffer[0]][voigtIdx] = recvbuffer[2+voigtIdx];
}
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
PhasePerm[1][(int)recvbuffer[0]][voigtIdx] = recvbuffer[2+tensorElementCount+voigtIdx];
}
}
else {
std::vector<double> recvbuffer(2+tensorElementCount);
MPI_Recv(recvbuffer.data(), recvbuffer.size(), MPI_DOUBLE,
node_vs_pressurepoint[idx], 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// Put received data into correct place.
WaterSaturation[(int)recvbuffer[0]] = recvbuffer[1];
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
PhasePerm[0][(int)recvbuffer[0]][voigtIdx] = recvbuffer[2+voigtIdx];
}
}
}
}
}
else {
for (int idx=0; idx < points; ++idx) {
if (node_vs_pressurepoint[idx] == mpi_rank) {
// Pack and send data. C-style.
if (upscaleBothPhases) {
std::vector<double> sendbuffer(2+2*tensorElementCount);
sendbuffer[0] = (double)idx;
sendbuffer[1] = WaterSaturation[idx];
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
sendbuffer[2+voigtIdx] = PhasePerm[0][idx][voigtIdx];
}
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
sendbuffer[2+tensorElementCount+voigtIdx] = PhasePerm[1][idx][voigtIdx];
}
MPI_Send(sendbuffer.data(), sendbuffer.size(), MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
}
else {
std::vector<double> sendbuffer(2+tensorElementCount);
sendbuffer[0] = (double)idx;
sendbuffer[1] = WaterSaturation[idx];
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
sendbuffer[2+voigtIdx] = PhasePerm[0][idx][voigtIdx];
}
MPI_Send(sendbuffer.data(), sendbuffer.size(), MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
}
}
}
}
#endif
}
std::vector<std::vector<double>> RelPermUpscaleHelper::getRelPerm(int phase) const
{
SinglePhaseUpscaler::permtensor_t zeroMatrix(3,3,(double*)0);
zero(zeroMatrix);
std::vector<std::vector<double>> RelPermValues;
if (isMaster) {
RelPermValues.resize(tensorElementCount);
// Loop over all pressure points
for (int idx=0; idx < points; ++idx) {
SinglePhaseUpscaler::permtensor_t phasePermTensor = zeroMatrix;
zero(phasePermTensor);
for (int voigtIdx = 0; voigtIdx < tensorElementCount; ++voigtIdx) {
setVoigtValue(phasePermTensor, voigtIdx, PhasePerm[phase][idx][voigtIdx]);
}
SinglePhaseUpscaler::permtensor_t relPermTensor = zeroMatrix;
prod(phasePermTensor, permTensorInv, relPermTensor);
for (int voigtIdx = 0; voigtIdx < tensorElementCount; ++voigtIdx) {
RelPermValues[voigtIdx].push_back(getVoigtValue(relPermTensor, voigtIdx));
}
}
// If doEclipseCheck, critical saturation points should be specified by 0 relperm
// Numerical errors and maxpermcontrast violate this even if the input has specified
// these points
if (doEclipseCheck) {
for (int voigtIdx = 0; voigtIdx < tensorElementCount; ++voigtIdx) {
int minidx;
if (RelPermValues[voigtIdx][0] < RelPermValues[voigtIdx][points-1]) minidx = 0; else minidx = points-1;
if (RelPermValues[voigtIdx][minidx] < critRelpThresh) {
RelPermValues[voigtIdx][minidx] = 0.0;
}
else {
std::stringstream str;
str << "Minimum upscaled relperm value for phase " << phase+1 << " is "
<< RelPermValues[voigtIdx][minidx] << ", larger than critRelpermThresh." << std::endl
<< " (voigtidx = " << voigtIdx << ")";
throw std::runtime_error(str.str());
}
}
}
}
return RelPermValues;
}
void RelPermUpscaleHelper::upscaleSinglePhasePermeability()
{
if (isMaster) {
auto cellperm = SinglePhaseUpscaler::permtensor_t(3, 3, nullptr);
const auto& ecl_idx = upscaler.grid().globalCell();
for (decltype(ecl_idx.size())
i = 0, n = ecl_idx.size(); i < n; ++i)
{
const auto cell_idx = ecl_idx[i];
zero(cellperm);
if (! anisotropic_input) {
const auto kval = std::max(perms[0][cell_idx], minSinglePhasePerm);
cellperm(0,0) = kval;
cellperm(1,1) = kval;
cellperm(2,2) = kval;
}
else {
cellperm(0,0) = std::max(minSinglePhasePerm, perms[0][cell_idx]);
cellperm(1,1) = std::max(minSinglePhasePerm, perms[1][cell_idx]);
cellperm(2,2) = std::max(minSinglePhasePerm, perms[2][cell_idx]);
}
upscaler.setPermeability(i, cellperm);
}
permTensor = upscaler.upscaleSinglePhase();
permTensorInv = permTensor;
invert(permTensorInv);
}
}
void RelPermUpscaleHelper::sanityCheckInput(const Opm::Deck& deck,
const double minPerm,
const double maxPerm,
const double minPoro)
{
{
using kw_poro = ParserKeywords::PORO;
using kw_permx = ParserKeywords::PERMX;
using kw_permy = ParserKeywords::PERMY;
using kw_permz = ParserKeywords::PERMZ;
using kw_satnum = ParserKeywords::SATNUM;
// Check that we have the information we need from the eclipse file:
if (! (deck.hasKeyword<kw_poro >() &&
deck.hasKeyword<kw_permx>()))
{
throw std::runtime_error("Error: Did not find complete set of "
"PORO and PERMX in ECLIPSE file.");
}
poros = deck.getKeyword<kw_poro >().getSIDoubleData();
perms[0] = deck.getKeyword<kw_permx>().getSIDoubleData();
EclipseGrid(deck).exportZCORN(zcorns);
// Load anisotropic (only diagonal supported) input if present in grid
if (deck.hasKeyword<kw_permy>() && deck.hasKeyword<kw_permz>()) {
anisotropic_input = true;
perms[1] = deck.getKeyword<kw_permy>().getSIDoubleData();
perms[2] = deck.getKeyword<kw_permz>().getSIDoubleData();
if (isMaster) {
std::cout << "Info: PERMY and PERMZ present, going into "
<< "anisotropic input mode, no J-functions\n"
<< " Options -relPermCurve and -jFunctionCurve "
<< "are meaningless.\n";
}
}
if (deck.hasKeyword<kw_satnum>()) {
satnums = deck.getKeyword<kw_satnum>().getIntData();
}
else {
if (isMaster) {
std::cout << "SATNUM not found in input file, "
<< "assuming only one rocktype" << std::endl;
}
// Initialize a default satnums-vector with only "ones" (meaning
// only one rocktype).
satnums.assign(poros.size(), 1);
}
}
// Sanity check/fix on input for each cell:
//
// - Check that SATNUM are set sensibly, that is => 0 and < 1000, error
// if not.
//
// - Check that porosity is between 0 and 1, error if not. Set to
// minPoro if zero or less than minPoro (due to pcmin/max computation)
//
// - Check that permeability is zero or positive. Error if negative.
// Set to minPerm if zero or less than minPerm.
//
// - Check maximum number of SATNUM values (can be number of rock types
// present)
{
auto it = std::find_if(satnums.begin(), satnums.end(), [](int a) { return a < 0; });
auto it2 = std::find_if(satnums.begin(), satnums.end(), [](int a) { return a > 1000;});
if (it != satnums.end() || it2 != satnums.end()) {
std::stringstream str;
str << "satnums[";
if (it == satnums.end()) {
str << it2 - satnums.begin() << "] = " << *it2;
}
else {
str << it - satnums.begin() << "] = " << *it;
}
str << ", not sane, quitting.";
throw std::runtime_error(str.str());
}
}
{
auto it3 = std::find_if(poros.begin(), poros.end(),
[](double value)
{
return (value < 0) || (value > 1);
});
if (it3 != poros.end()) {
std::stringstream str;
str << "poros[" << it3 - poros.begin() << "] = "
<< *it3 << ", not sane, quitting.";
throw std::runtime_error(str.str());
}
}
{
auto name = std::string("permx");
auto check_perm = [&name](const std::vector<double>& perm)
{
auto it = std::find_if(perm.begin(), perm.end(),
[](double value) { return value < 0; });
if (it != perm.end()) {
std::stringstream str;
str << name << "[" << it - perm.begin() << "] = "
<< *it << ", not sane, quitting.";
throw std::runtime_error(str.str());
}
};
check_perm(perms[0]);
if (anisotropic_input) {
name = "permy"; check_perm(perms[1]);
name = "permz"; check_perm(perms[2]);
}
}
auto cells_truncated_from_below_poro = 0;
{
for (auto& poro : poros) {
if ((! (poro < 0)) && (poro < minPoro)) {
poro = minPoro;
++ cells_truncated_from_below_poro;
}
}
}
auto cells_truncated_from_below_permx = 0;
auto cells_truncated_from_above_permx = 0;
{
const auto lowPerm =
unit::convert::from(minPerm, prefix::milli*unit::darcy);
const auto highPerm =
unit::convert::from(maxPerm, prefix::milli*unit::darcy);
for (auto& kx : perms[0]) {
if ((! (kx < 0)) && (kx < lowPerm)) { // Truncate permeability from below
++cells_truncated_from_below_permx;
kx = lowPerm;
}
else if (kx > highPerm) { // Truncate permeability from above
++ cells_truncated_from_above_permx;
kx = highPerm;
}
}
for (decltype(satnums.size())
i = 0, n = satnums.size(); i < n; ++i)
{
// Explicitly handle "no rock" cells, set them to minimum perm
// and zero porosity.
if (satnums[i] == 0) {
perms[0][i] = lowPerm;
if (anisotropic_input) {
perms[1][i] = lowPerm;
perms[2][i] = lowPerm;
}
// zero poro is fine for these cells, as they are not used
// in pcmin/max computation.
poros[i] = 0;
}
}
}
if (isMaster) {
if (cells_truncated_from_below_poro > 0) {
std::cout << "Cells with truncated porosity: "
<< cells_truncated_from_below_poro << std::endl;
}
if (cells_truncated_from_below_permx > 0) {
std::cout << "Cells with permx truncated from below: "
<< cells_truncated_from_below_permx << std::endl;
}
if (cells_truncated_from_above_permx > 0) {
std::cout << "Cells with permx truncated from above: "
<< cells_truncated_from_above_permx << std::endl;
}
}
}
bool RelPermUpscaleHelper::checkCurve(MonotCubicInterpolator& func)
{
double minrelp = func.getMinimumF().second;
if (minrelp == 0)
return true;
else if (minrelp < critRelpThresh) {
// set to 0
std::vector<double> svec = func.get_xVector();
std::vector<double> kvec = func.get_fVector();
if (kvec[0] < critRelpThresh) {
kvec[0] = 0.0;
}
else if (kvec[kvec.size()-1] < critRelpThresh) {
kvec[kvec.size()-1] = 0.0;
}
func = MonotCubicInterpolator(svec, kvec);
return true;
}
else
return false;
}
void RelPermUpscaleHelper::checkCriticalSaturations()
{
for (size_t i=0 ; i < Krfunctions[0][0].size(); ++i) {
for (size_t j=0;j<(anisotropic_input?3:1);++j) {
for (size_t k=0;k<(upscaleBothPhases?2:1);++k) {
if (!checkCurve(Krfunctions[j][k][i])) {
std::stringstream str;
// Error message
str << "Relperm curve for rock " << i
<< " does not specify critical saturation." << std::endl
<< "Minimum relperm value is "
<< Krfunctions[j][k][i].getMinimumF().second
<< ", critRelpermThresh is " << critRelpThresh;
throw std::runtime_error(str.str());
}
}
}
}
}
void RelPermUpscaleHelper::setupBoundaryConditions()
{
static const std::map<char, std::pair<SinglePhaseUpscaler::BoundaryConditionType, size_t>> bcmap =
{{'f', {SinglePhaseUpscaler::Fixed, 3}},
{'l', {SinglePhaseUpscaler::Linear, 9}},
{'p', {SinglePhaseUpscaler::Periodic, 9}}};
auto it = bcmap.find(options["bc"][0]);
if (it != bcmap.end()) {
boundaryCondition = it->second.first;
tensorElementCount = it->second.second;
} else
throw std::runtime_error("Invalid boundary condition. Only one of the letters f, l or p are allowed.");
}
double RelPermUpscaleHelper::tesselateGrid(const Opm::Deck& deck)
{
const auto linsolver_tolerance = to_double(options["linsolver_tolerance"]);
const auto linsolver_verbosity = to_int (options["linsolver_verbosity"]);
const auto linsolver_type = to_int (options["linsolver_type"]);
const auto twodim_hack = false;
const auto linsolver_maxit = to_int (options["linsolver_max_iterations"]);
const auto smooth_steps = to_int (options["linsolver_smooth_steps"]);
const auto linsolver_prolongate_factor = to_double(options["linsolver_prolongate_factor"]);
const auto minPerm = to_double(options["minPerm"]);
const auto gravity = to_double(options["gravity"]);
if (isMaster) {
std::cout << "Tesselating grid... ";
}
std::flush(std::cout);
const auto start = clock();
upscaler.init(deck, boundaryCondition,
unit::convert::from(minPerm, prefix::milli*unit::darcy),
linsolver_tolerance, linsolver_verbosity, linsolver_type,
twodim_hack, linsolver_maxit, linsolver_prolongate_factor,
smooth_steps, gravity);
const auto finish = clock();
const auto timeused_tesselation =
(static_cast<double>(finish) - start) / CLOCKS_PER_SEC;
if (isMaster) {
std::cout << " (" << timeused_tesselation << " secs)" << std::endl;
}
return timeused_tesselation;
}
void RelPermUpscaleHelper::calculateCellPressureGradients()
{
const auto& grid = upscaler.grid();
auto celldepth = std::vector<double>(grid.numCells(), 0.0);
auto mt = ModelThickness{grid.logicalCartesianSize(), zcorns};
{
auto ijk = std::array<int,3>{{0, 0, 0}};
for (auto c = grid.leafbegin<0>(),
end = grid.leafend <0>(); c != end; ++c)
{
const auto ix = c->index();
grid.getIJK(ix, ijk);
mt.activateCell(ijk);
const auto& cc = c->geometry().center();
celldepth[ix] = cc[ cc.size() - 1 ];
}
}
{
const auto gravity = to_double(options["gravity"]);
const auto waterDensity = to_double(options["waterDensity"]);
const auto oilDensity = to_double(options["oilDensity"]);
// Input water and oil density is given in g/cm3.
const auto dRho = unit::convert::
from(waterDensity - oilDensity,
prefix::milli*unit::kilogram /* 'g' missing from set */
/ unit::cubic(prefix::centi * unit::meter));
const auto thick = mt.thickness();
dP.clear(); dP.reserve(celldepth.size());
for (const auto& depth : celldepth) {
dP.push_back(dRho * gravity * (depth - (thick / 2.0)));
}
}
}
void RelPermUpscaleHelper::calculateMinMaxCapillaryPressure()
{
const auto maxPermContrast = to_double(options["maxPermContrast"]);
const auto minPerm = to_double(options["minPerm"]);
const auto gravity = to_double(options["gravity"]);
const auto linsolver_tolerance = to_double(options["linsolver_tolerance"]);
const auto includeGravity =
(std::fabs(gravity) > std::numeric_limits<double>::min()); // true for non-zero gravity
if (maxPermContrast == 0) {
throw std::runtime_error("Illegal contrast value");
}
auto cellVolumes = std::vector<double>(satnums.size(), 0.0);
cellPoreVolumes.resize(satnums.size(), 0.0);
auto dPmin = std::numeric_limits<double>::max();
auto dPmax = -std::numeric_limits<double>::max();
if (!dP.empty()) {
const auto m = std::minmax_element(dP.begin(), dP.end());
dPmax = *m.second;
dPmin = *m.first;
}
// Find minimium and maximum capillary pressure values in each
// cell, and use the global min/max as the two initial pressure
// points for computations.
//
// Also find max single-phase permeability, used to obey the
// maxPermContrast option.
//
// Also find properly upscaled saturation endpoints, these are
// printed out to stdout for reference during computations, but will
// automatically appear as the lowest and highest saturation points
// in finished output.
Pcmin = std::numeric_limits<double>::max();
Pcmax = -std::numeric_limits<double>::max();
auto maxSinglePhasePerm = 0.0;
auto Swirvolume = 0.0;
auto Sworvolume = 0.0;
const auto& ecl_idx = upscaler.grid().globalCell();
for (auto c = upscaler.grid().leafbegin<0>(),
end = upscaler.grid().leafend<0>(); c != end; ++c)
{
const auto cell_idx = ecl_idx[c->index()];
if (satnums[cell_idx] > 0) { // Satnum zero is "no rock"
cellVolumes [cell_idx] = c->geometry().volume();
cellPoreVolumes[cell_idx] = cellVolumes[cell_idx] * poros[cell_idx];
double Pcmincandidate, Pcmaxcandidate, minSw, maxSw;
if (! anisotropic_input) {
const auto& invJ = InvJfunctions[satnums[cell_idx] - 1];
const auto kx = perms[0][cell_idx];
const auto denom = std::sqrt(kx / poros[cell_idx]);
Pcmincandidate = invJ.getMinimumX().first / denom;
Pcmaxcandidate = invJ.getMaximumX().first / denom;
minSw = invJ.getMinimumF().second;
maxSw = invJ.getMaximumF().second;
}
else { // anisotropic input, we do not to J-function scaling
const auto& satfunc = SwPcfunctions[satnums[cell_idx] - 1];
Pcmincandidate = satfunc.getMinimumX().first;
Pcmaxcandidate = satfunc.getMaximumX().first;
minSw = satfunc.getMinimumF().second;
maxSw = satfunc.getMaximumF().second;
}
Pcmin = std::min(Pcmincandidate, Pcmin);
Pcmax = std::max(Pcmaxcandidate, Pcmax);
maxSinglePhasePerm = std::max(maxSinglePhasePerm, perms[0][cell_idx]);
// Add irreducible water saturation volume
Swirvolume += minSw * cellPoreVolumes[cell_idx];
Sworvolume += maxSw * cellPoreVolumes[cell_idx];
}
++tesselatedCells; // keep count.
}
minSinglePhasePerm =
std::max(maxSinglePhasePerm / maxPermContrast,
unit::convert::from(minPerm, prefix::milli*unit::darcy));
if (includeGravity) {
Pcmin -= dPmax;
Pcmax -= dPmin;
}
if (isMaster) {
std::cout << "Pcmin: "
<< unit::convert::to(Pcmin, unit::barsa)
<< " [bar]\n"
<< "Pcmax: "
<< unit::convert::to(Pcmax, unit::barsa)
<< " [bar]" << std::endl;
}
if (Pcmin > Pcmax) {
throw std::runtime_error("ERROR: No legal capillary pressures "
"found for this system. Exiting...");
}
// Total porevolume and total volume -> upscaled porosity:
poreVolume = std::accumulate(cellPoreVolumes.begin(), cellPoreVolumes.end(), 0.0);
volume = std::accumulate(cellVolumes.begin() , cellVolumes.end() , 0.0);
Swir = Swirvolume / poreVolume;
Swor = Sworvolume / poreVolume;
if (isMaster) {
std::cout << "LF Pore volume: " << poreVolume << '\n'
<< "LF Volume: " << volume << '\n'
<< "Upscaled porosity: " << (poreVolume / volume) << '\n'
<< "Upscaled " << saturationstring << "ir: " << Swir << '\n'
<< "Upscaled " << saturationstring << "max: " << Swor << '\n' //Swor=1-Swmax
<< "Saturation points to be computed: "
<< points << std::endl;
}
// Sometimes, if Swmax=1 or Swir=0 in the input tables, the upscaled
// values can be a little bit larger (within machine precision) and the
// check below fails. Hence, check if these values are within the [0,1]
// interval within some precision (use linsolver_tolerance)
if ((Swor > 1.0) && (Swor - linsolver_tolerance < 1.0)) {
Swor = 1.0;
}
if ((Swir < 0.0) && (Swir + linsolver_tolerance > 0.0)) {
Swir = 0.0;
}
if ((Swir < 0) || (Swir > 1) || (Swor < 0) || (Swor > 1)) {
std::stringstream str;
str << "ERROR: " // e.g., Swir/Swor nonsensical...
<< saturationstring << "ir/"
<< saturationstring << "or "
<< "nonsensical. Check your input. Exiting";
throw std::runtime_error(str.str());
}
}
void RelPermUpscaleHelper::upscaleCapillaryPressure()
{
const auto saturationThreshold =
to_double(options["saturationThreshold"]);
auto largestSaturationInterval = Swor - Swir;
decltype(Pcmax) Ptestvalue;
std::stringstream errstr;
const auto& ecl_idx = upscaler.grid().globalCell();
while (largestSaturationInterval > (Swor - Swir) / 500.0) {
if (Pcmax == Pcmin) {
// This is a dummy situation, we go through once and then we are
// finished (this will be triggered by zero permeability)
Ptestvalue = Pcmin;
largestSaturationInterval = 0;
}
else if (WaterSaturationVsCapPressure.getSize() == 0) {
// No data values previously computed
Ptestvalue = Pcmax;
}
else if (WaterSaturationVsCapPressure.getSize() == 1) {
// If only one point has been computed, it was for Pcmax. So now
// do Pcmin.
Ptestvalue = Pcmin;
}
else {
// Search for largest saturation interval in which there are no
// computed saturation points (and estimate the capillary
// pressure that will fall in the center of this saturation
// interval)
const auto& SatDiff = WaterSaturationVsCapPressure.getMissingX();
Ptestvalue = SatDiff.first;
largestSaturationInterval = SatDiff.second;
}
// Check for saneness of Ptestvalue:
if (std::isnan(Ptestvalue) || std::isinf(Ptestvalue)) {
errstr << "ERROR: Ptestvalue was inf or nan\n";
break; // Jump out of while-loop, just print out the results
// up to now and exit the program
}
auto waterVolume = 0.0;
for (decltype(ecl_idx.size())
i = 0, n = ecl_idx.size(); i < n; ++i)
{
const auto cell_idx = ecl_idx[i];
double waterSaturationCell = 0.0;
if (satnums[cell_idx] > 0) { // handle "no rock" cells with satnum zero
const auto ix = satnums[cell_idx] - 1;
auto PtestvalueCell = Ptestvalue;
if (!dP.empty()) {
PtestvalueCell -= dP[cell_idx];
}
if (!anisotropic_input) {
const auto arg = perms[0][cell_idx] / poros[cell_idx];
const auto Jvalue = std::sqrt(arg) * PtestvalueCell;
waterSaturationCell = InvJfunctions[ix].evaluate(Jvalue);
}
else {
// anisotropic_input, then we do not do J-function-scaling
waterSaturationCell = SwPcfunctions[ix].evaluate(PtestvalueCell);
}
}
waterVolume += waterSaturationCell * cellPoreVolumes[cell_idx];
}
WaterSaturationVsCapPressure.addPair(Ptestvalue, waterVolume / poreVolume);
}
// Now, it may happen that we have a large number of cells, and
// some cells with near zero poro and perm. This may cause that
// Pcmax has been estimated so high that it does not affect Sw
// within machine precision, and then we need to truncate the
// largest Pc values:
WaterSaturationVsCapPressure.chopFlatEndpoints(saturationThreshold);
// Now we can also invert the upscaled water saturation
// (it should be monotonic)
if (! WaterSaturationVsCapPressure.isStrictlyMonotone()) {
errstr << "Error: Upscaled water saturation not strictly "
<< "monotone in capillary pressure.\n"
<< " Unphysical input data, exiting.\n"
<< " Trying to dump " << saturationstring
<< " vs Pc to file swvspc_debug.txt for inspection";
if (isMaster) {
std::ofstream outfile("swvspc_debug.txt");
outfile << "# Pc " << saturationstring << std::endl;
outfile << WaterSaturationVsCapPressure.toString();
}
throw std::runtime_error(errstr.str());
}
}
std::tuple<double, double>
RelPermUpscaleHelper::upscalePermeability(int mpi_rank)
{
const auto minPerm = to_double(options["minPerm"]);
const auto maxPermContrast = to_double(options["maxPermContrast"]);
// Put correct number of zeros in, just to be able to access RelPerm[index] later
WaterSaturation.resize(points);
for (size_t i = 0; i < (upscaleBothPhases ? 2 : 1); ++i) {
PhasePerm[i].resize(points, std::vector<double>(tensorElementCount));
}
// Make vector of capillary pressure points corresponding to uniformly
// distribued saturation points between Swor and Swir.
{
auto CapPressureVsWaterSaturation =
MonotCubicInterpolator(WaterSaturationVsCapPressure.get_fVector(),
WaterSaturationVsCapPressure.get_xVector());
for (int pointidx = 1; pointidx <= points; ++pointidx) {
// pointidx=1 corresponds to Swir, pointidx=points to Swor.
double saturation = Swir + (Swor-Swir)/(points-1)*(pointidx-1);
pressurePoints.push_back(CapPressureVsWaterSaturation.evaluate(saturation));
}
}
// Preserve max and min pressures
pressurePoints.front() = Pcmax;
pressurePoints.back() = Pcmin;
// Fill with zeros initially (in case of non-mpi)
node_vs_pressurepoint.resize(points);
#if defined(HAVE_MPI) && HAVE_MPI
// Distribute work load over mpi nodes.
int mpi_nodecount;
MPI_Comm_size(MPI_COMM_WORLD, &mpi_nodecount);
for (int idx = 0; idx < points; ++idx) {
// Ensure master node gets equal or less work than the other nodes,
// since master node also computes single phase perm.
node_vs_pressurepoint[idx] = (mpi_nodecount-1) - idx % mpi_nodecount;
}
#endif
const auto& ecl_idx = upscaler.grid().globalCell();
const auto start_upscale_wallclock = clock();
double waterVolumeLF = 0.0;
// Now loop through the vector of capillary pressure points that this
// node should compute.
for (int pointidx = 0; pointidx < points; ++pointidx) {
// Should "I" (mpi-wise) compute this pressure point?
if (node_vs_pressurepoint[pointidx] == mpi_rank) {
const auto Ptestvalue = pressurePoints[pointidx];
std::array<double,2> maxPhasePerm{{0.0, 0.0}};
std::array<std::vector<double>,2> phasePermValues;
std::array<std::vector<std::vector<double>>,2> phasePermValuesDiag;
std::array<double,2> minPhasePerm;
std::array<SinglePhaseUpscaler::permtensor_t,2> phasePermTensor;
for (size_t p = 0; p < (upscaleBothPhases ? 2 : 1); ++p) {
waterVolumeLF = 0.0;
phasePermValues [p].resize(satnums.size());
phasePermValuesDiag[p].resize(satnums.size());
for (decltype(ecl_idx.size())
i = 0, n = ecl_idx.size(); i < n; ++i)
{
const auto cell_idx = ecl_idx[i];
double cellPhasePerm =
unit::convert::from(minPerm, prefix::milli*unit::darcy);
auto cellPhasePermDiag =
std::vector<double>(3, cellPhasePerm);
const auto ix = satnums[cell_idx] - 1;
const auto kx = perms[0][cell_idx];
if (satnums[cell_idx] > 0) { // handle "no rock" cells with satnum zero
auto PtestvalueCell = Ptestvalue;
if (!dP.empty()) {
PtestvalueCell -= dP[cell_idx];
}
if (!anisotropic_input) {
const auto Jvalue =
std::sqrt(kx / poros[cell_idx]) * PtestvalueCell;
const auto WaterSaturationCell =
InvJfunctions[ix].evaluate(Jvalue);
waterVolumeLF += WaterSaturationCell * cellPoreVolumes[cell_idx];
// Compute cell relative permeability. We use a lower cutoff-value as we
// easily divide by zero here. When water saturation is
// zero, we get 'inf', which is circumvented by the cutoff value.
cellPhasePerm =
Krfunctions[0][p][ix].evaluate(WaterSaturationCell) * kx;
}
else {
const auto WaterSaturationCell =
SwPcfunctions[ix].evaluate(PtestvalueCell);
waterVolumeLF += WaterSaturationCell * cellPoreVolumes[cell_idx];
cellPhasePermDiag[0] =
Krfunctions[0][p][ix].evaluate(WaterSaturationCell) * kx;
cellPhasePermDiag[1] =
Krfunctions[1][p][ix].evaluate(WaterSaturationCell) * perms[1][cell_idx];
cellPhasePermDiag[2] =
Krfunctions[2][p][ix].evaluate(WaterSaturationCell) * perms[2][cell_idx];
}
phasePermValues [p][cell_idx] = cellPhasePerm;
phasePermValuesDiag[p][cell_idx] = cellPhasePermDiag;
maxPhasePerm[p] = std::max(maxPhasePerm[p], cellPhasePerm);
maxPhasePerm[p] = std::max(maxPhasePerm[p],
*std::max_element(cellPhasePermDiag.begin(),
cellPhasePermDiag.end()));
}
}
// Now we can determine the smallest permitted permeability
// we can calculate for We have both a fixed bottom limit,
// as well as a possible higher limit determined by a
// maximum allowable permeability.
minPhasePerm[p] = std::max(maxPhasePerm[p] / maxPermContrast,
unit::convert::from(minPerm, prefix::milli*unit::darcy));
// Now remodel the phase permeabilities obeying minPhasePerm
SinglePhaseUpscaler::permtensor_t cellperm(3, 3, nullptr);
for (decltype(ecl_idx.size())
i = 0, n = ecl_idx.size(); i < n; ++i)
{
const auto cell_idx = ecl_idx[i];
zero(cellperm);
if (!anisotropic_input) {
const auto cellPhasePerm =
std::max(minPhasePerm[p], phasePermValues[p][cell_idx]);
const auto kval = std::max(minPhasePerm[p], cellPhasePerm);
cellperm(0,0) = kval;
cellperm(1,1) = kval;
cellperm(2,2) = kval;
}
else { // anisotropic_input
// Truncate values lower than minPhasePerm upwards.
auto& k = phasePermValuesDiag[p][cell_idx];
cellperm(0,0) = k[0] = std::max(minPhasePerm[p], k[0]);
cellperm(1,1) = k[1] = std::max(minPhasePerm[p], k[1]);
cellperm(2,2) = k[2] = std::max(minPhasePerm[p], k[2]);
}
upscaler.setPermeability(i, cellperm);
}
// Call single-phase upscaling code
phasePermTensor[p] = upscaler.upscaleSinglePhase();
}
// Here we recalculate the upscaled water saturation,
// although it is already known when we asked for the
// pressure point to compute for. Nonetheless, we
// recalculate here to avoid any minor roundoff-error and
// interpolation error (this means that the saturation
// points are not perfectly uniformly distributed)
WaterSaturation[pointidx] = waterVolumeLF/poreVolume;
#if defined(HAVE_MPI) && HAVE_MPI
std::cout << "Rank " << mpi_rank << ": ";
#endif // HAVE_MPI
std::cout << Ptestvalue << "\t" << WaterSaturation[pointidx];
// Store and print phase-perm-result
for (int voigtIdx=0; voigtIdx < tensorElementCount; ++voigtIdx) {
for (size_t p = 0; p < (upscaleBothPhases ? 2 : 1); ++p) {
PhasePerm[p][pointidx][voigtIdx] =
getVoigtValue(phasePermTensor[p], voigtIdx);
std::cout << "\t" << PhasePerm[p][pointidx][voigtIdx];
}
}
std::cout << '\n';
}
}
clock_t finish_upscale_wallclock = clock();
double timeused_upscale_wallclock =
(double(finish_upscale_wallclock) -
double(start_upscale_wallclock)) / CLOCKS_PER_SEC;
collectResults();
// Average time pr. upscaling point:
#if defined(HAVE_MPI) && HAVE_MPI
// Sum the upscaling time used by all processes
double timeused_total;
MPI_Reduce(&timeused_upscale_wallclock, &timeused_total, 1,
MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
const auto avg_upscaling_time_pr_point = timeused_total / points;
#else
const auto avg_upscaling_time_pr_point =
timeused_upscale_wallclock / points;
#endif // HAVE_MPI
return std::make_tuple(timeused_upscale_wallclock,
avg_upscaling_time_pr_point);
}
}