/*
Copyright 2013 SINTEF ICT, Applied Mathematics.
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 .
*/
#ifndef OPM_GEOPROPS_HEADER_INCLUDED
#define OPM_GEOPROPS_HEADER_INCLUDED
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef HAVE_OPM_GRID
#include
#include
#include
#endif
#include
#include
namespace Opm
{
/// Class containing static geological properties that are
/// derived from grid and petrophysical properties:
/// - pore volume
/// - transmissibilities
/// - gravity potentials
class DerivedGeology
{
public:
typedef Eigen::ArrayXd Vector;
/// Construct contained derived geological properties
/// from grid and property information.
template
DerivedGeology(const Grid& grid,
const Props& props ,
const EclipseState& eclState,
const bool use_local_perm,
const double* grav = 0
)
: pvol_ (Opm::AutoDiffGrid::numCells(grid))
, trans_(Opm::AutoDiffGrid::numFaces(grid))
, gpot_ (Vector::Zero(Opm::AutoDiffGrid::cell2Faces(grid).noEntries(), 1))
, z_(Opm::AutoDiffGrid::numCells(grid))
, use_local_perm_(use_local_perm)
{
update(grid, props, eclState, grav);
}
/// compute the all geological properties at a given report step
template
void update(const Grid& grid,
const Props& props ,
const EclipseState& eclState,
const double* grav)
{
int numCells = AutoDiffGrid::numCells(grid);
int numFaces = AutoDiffGrid::numFaces(grid);
const int *cartDims = AutoDiffGrid::cartDims(grid);
int numCartesianCells =
cartDims[0]
* cartDims[1]
* cartDims[2];
// get the pore volume multipliers from the EclipseState
std::vector multpv(numCartesianCells, 1.0);
const auto& eclProps = eclState.get3DProperties();
if (eclProps.hasDeckDoubleGridProperty("MULTPV")) {
multpv = eclProps.getDoubleGridProperty("MULTPV").getData();
}
// get the net-to-gross cell thickness from the EclipseState
std::vector ntg(numCartesianCells, 1.0);
if (eclProps.hasDeckDoubleGridProperty("NTG")) {
ntg = eclProps.getDoubleGridProperty("NTG").getData();
}
// Get grid from parser.
const auto& eclgrid = eclState.getInputGrid();
// update the pore volume of all active cells in the grid
computePoreVolume_(grid, eclState);
// Non-neighbour connections.
nnc_ = eclState.getInputNNC();
// Transmissibility
Vector htrans(AutoDiffGrid::numCellFaces(grid));
Grid* ug = const_cast(& grid);
if (! use_local_perm_) {
tpfa_htrans_compute(ug, props.permeability(), htrans.data());
}
else {
tpfa_loc_trans_compute_(grid,eclgrid, props.permeability(),htrans);
}
// Use volume weighted arithmetic average of the NTG values for
// the cells effected by the current OPM cpgrid process algorithm
// for MINPV. Note that the change does not effect the pore volume calculations
// as the pore volume is currently defaulted to be comparable to ECLIPSE, but
// only the transmissibility calculations.
bool opmfil = eclgrid.getMinpvMode() == MinpvMode::ModeEnum::OpmFIL;
// opmfil is hardcoded to be true. i.e the volume weighting is always used
opmfil = true;
if (opmfil) {
minPvFillProps_(grid, eclState, ntg);
}
std::vector mult;
multiplyHalfIntersections_(grid, eclState, ntg, htrans, mult);
if (!opmfil && eclgrid.isPinchActive()) {
// opmfil is hardcoded to be true. i.e the pinch processor is never used
pinchProcess_(grid, eclState, htrans, numCells);
}
// combine the half-face transmissibilites into the final face
// transmissibilites.
tpfa_trans_compute(ug, htrans.data(), trans_.data());
// multiply the face transmissibilities with their appropriate
// transmissibility multipliers
for (int faceIdx = 0; faceIdx < numFaces; faceIdx++) {
trans_[faceIdx] *= mult[faceIdx];
}
// Create the set of noncartesian connections.
noncartesian_ = nnc_;
exportNncStructure(grid);
// Compute z coordinates
for (int c = 0; c::Type Cell2Faces;
Cell2Faces c2f=AutoDiffGrid::cell2Faces(grid);
std::size_t i = 0;
for (typename Vector::Index c = 0; c < numCells; ++c) {
const double* const cc = AutoDiffGrid::cellCentroid(grid, c);
typename Cell2Faces::row_type faces=c2f[c];
typedef typename Cell2Faces::row_type::iterator Iter;
for (Iter f=faces.begin(), end=faces.end(); f!=end; ++f, ++i) {
auto fc = AutoDiffGrid::faceCentroid(grid, *f);
for (typename Vector::Index d = 0; d < nd; ++d) {
gpot_[i] += grav[d] * (fc[d] - cc[d]);
}
}
}
std::copy(grav, grav + nd, gravity_);
}
}
const Vector& poreVolume() const { return pvol_ ;}
const Vector& transmissibility() const { return trans_ ;}
const Vector& gravityPotential() const { return gpot_ ;}
const Vector& z() const { return z_ ;}
const double* gravity() const { return gravity_;}
Vector& poreVolume() { return pvol_ ;}
Vector& transmissibility() { return trans_ ;}
const NNC& nnc() const { return nnc_;}
const NNC& nonCartesianConnections() const { return noncartesian_;}
/// Most properties are loaded by the parser, and managed by
/// the EclipseState class in the opm-parser. However - some
/// properties must be calculated by the simulator, the
/// purpose of this method is to calculate these properties in
/// a form suitable for output. Currently the transmissibility
/// is the only property calculated this way:
///
/// The grid properties TRANX, TRANY and TRANZ are initialized
/// in a form suitable for writing to the INIT file. These
/// properties should be interpreted with a
/// 'the-grid-is-nearly-cartesian' mindset:
///
/// TRANX[i,j,k] = T on face between cells (i,j,k) and (i+1,j ,k )
/// TRANY[i,j,k] = T on face between cells (i,j,k) and (i ,j+1,k )
/// TRANZ[i,j,k] = T on face between cells (i,j,k) and (i ,j ,k+1)
///
/// If the grid structure has no resemblance to a cartesian
/// grid the whole TRAN keyword is quite meaningless.
template
data::Solution simProps( const Grid& grid ) const {
using namespace UgGridHelpers;
const int* dims = cartDims( grid );
const int globalSize = dims[0] * dims[1] * dims[2];
const auto& trans = this->transmissibility( );
data::CellData tranx = {UnitSystem::measure::transmissibility, std::vector( globalSize ), data::TargetType::INIT};
data::CellData trany = {UnitSystem::measure::transmissibility, std::vector( globalSize ), data::TargetType::INIT};
data::CellData tranz = {UnitSystem::measure::transmissibility, std::vector( globalSize ), data::TargetType::INIT};
size_t num_faces = numFaces(grid);
auto fc = faceCells(grid);
for (size_t i = 0; i < num_faces; ++i) {
auto c1 = std::min( fc(i,0) , fc(i,1));
auto c2 = std::max( fc(i,0) , fc(i,1));
if (c1 == -1 || c2 == -1)
continue;
c1 = globalCell(grid) ? globalCell(grid)[c1] : c1;
c2 = globalCell(grid) ? globalCell(grid)[c2] : c2;
if ((c2 - c1) == 1) {
tranx.data[c1] = trans[i];
}
if ((c2 - c1) == dims[0]) {
trany.data[c1] = trans[i];
}
if ((c2 - c1) == dims[0]*dims[1]) {
tranz.data[c1] = trans[i];
}
}
return { {"TRANX" , tranx},
{"TRANY" , trany} ,
{"TRANZ" , tranz } };
}
private:
template
void multiplyHalfIntersections_(const Grid &grid,
const EclipseState& eclState,
const std::vector &ntg,
Vector &halfIntersectTransmissibility,
std::vector &intersectionTransMult);
template
void tpfa_loc_trans_compute_(const Grid &grid,
const EclipseGrid& eclGrid,
const double* perm,
Vector &hTrans);
template
void minPvFillProps_(const Grid &grid,
const EclipseState& eclState,
std::vector &ntg);
template
void computePoreVolume_(const GridType &grid,
const EclipseState& eclState)
{
int numCells = Opm::AutoDiffGrid::numCells(grid);
const int* globalCell = Opm::UgGridHelpers::globalCell(grid);
const auto& eclGrid = eclState.getInputGrid();
const int nx = eclGrid.getNX();
const int ny = eclGrid.getNY();
// the "raw" pore volume.
const std::vector& porvData =
eclState.get3DProperties().getDoubleGridProperty("PORV").getData();
pvol_.resize(numCells);
// the "activation number" grid property
const std::vector& actnumData =
eclState.get3DProperties().getIntGridProperty("ACTNUM").getData();
for (int cellIdx = 0; cellIdx < numCells; ++cellIdx) {
const int cellCartIdx = globalCell[cellIdx];
double cellPoreVolume = porvData[cellCartIdx];
if (eclGrid.getMinpvMode() == MinpvMode::ModeEnum::OpmFIL) {
// Sum the pore volumes of the cells above which have been deactivated
// because their volume less is less than the MINPV threshold
for (int aboveCellCartIdx = cellCartIdx - nx*ny;
aboveCellCartIdx >= 0;
aboveCellCartIdx -= nx*ny)
{
if (porvData[aboveCellCartIdx] >= eclGrid.getMinpvValue()) {
// stop if we encounter a cell which has a pore volume which is
// at least as large as the minimum one
break;
}
const double aboveCellVolume = eclGrid.getCellVolume(aboveCellCartIdx);
if (actnumData[aboveCellCartIdx] == 0 && aboveCellVolume > 1e-6) {
// stop at explicitly disabled cells, but only if their volume is
// greater than 10^-6 m^3
break;
}
cellPoreVolume += porvData[aboveCellCartIdx];
}
}
pvol_[cellIdx] = cellPoreVolume;
}
}
template
void pinchProcess_(const Grid& grid,
const Opm::EclipseState& eclState,
const Vector& htrans,
int numCells);
/// checks cartesian adjacency of global indices g1 and g2
template
bool cartesianAdjacent(const Grid& grid, int g1, int g2) {
int diff = std::abs(g1 - g2);
const int * dimens = UgGridHelpers::cartDims(grid);
if (diff == 1)
return true;
if (diff == dimens[0])
return true;
if (diff == dimens[0] * dimens[1])
return true;
return false;
}
/// Write the NNC structure of the given grid to NNC.
///
/// Write cell adjacencies beyond Cartesian neighborhoods to NNC.
///
/// The trans vector is indexed by face number as it is in grid.
template
void exportNncStructure(const Grid& grid) {
// we use numFaces, numCells, cell2Faces, globalCell from UgGridHelpers
using namespace UgGridHelpers;
size_t num_faces = numFaces(grid);
auto fc = faceCells(grid);
for (size_t i = 0; i < num_faces; ++i) {
auto c1 = fc(i, 0);
auto c2 = fc(i, 1);
if (c1 == -1 || c2 == -1)
continue; // face on grid boundary
// translate from active cell idx (ac1,ac2) to global cell idx
c1 = globalCell(grid) ? globalCell(grid)[c1] : c1;
c2 = globalCell(grid) ? globalCell(grid)[c2] : c2;
if (!cartesianAdjacent(grid, c1, c2)) {
// suppose c1,c2 is specified in ECLIPSE input
// we here overwrite its trans by grid's
noncartesian_.addNNC(c1, c2, trans_[i]);
}
}
}
Vector pvol_ ;
Vector trans_;
Vector gpot_ ;
Vector z_;
double gravity_[3]; // Size 3 even if grid is 2-dim.
bool use_local_perm_;
// Non-neighboring connections
NNC nnc_;
// Non-cartesian connections
NNC noncartesian_;
};
template
inline void DerivedGeology::minPvFillProps_(const GridType &grid,
const EclipseState& eclState,
std::vector &ntg)
{
int numCells = Opm::AutoDiffGrid::numCells(grid);
const int* global_cell = Opm::UgGridHelpers::globalCell(grid);
const int* cartdims = Opm::UgGridHelpers::cartDims(grid);
const auto& eclgrid = eclState.getInputGrid();
const auto& porv = eclState.get3DProperties().getDoubleGridProperty("PORV").getData();
const auto& actnum = eclState.get3DProperties().getIntGridProperty("ACTNUM").getData();
for (int cellIdx = 0; cellIdx < numCells; ++cellIdx) {
const int nx = cartdims[0];
const int ny = cartdims[1];
const int cartesianCellIdx = global_cell[cellIdx];
const double cellVolume = eclgrid.getCellVolume(cartesianCellIdx);
ntg[cartesianCellIdx] *= cellVolume;
double totalCellVolume = cellVolume;
// Average properties as long as there exist cells above
// that has pore volume less than the MINPV threshold
int cartesianCellIdxAbove = cartesianCellIdx - nx*ny;
while ( cartesianCellIdxAbove >= 0 &&
actnum[cartesianCellIdxAbove] > 0 &&
porv[cartesianCellIdxAbove] < eclgrid.getMinpvValue() ) {
// Volume weighted arithmetic average of NTG
const double cellAboveVolume = eclgrid.getCellVolume(cartesianCellIdxAbove);
totalCellVolume += cellAboveVolume;
ntg[cartesianCellIdx] += ntg[cartesianCellIdxAbove]*cellAboveVolume;
cartesianCellIdxAbove -= nx*ny;
}
ntg[cartesianCellIdx] /= totalCellVolume;
}
}
template
inline void DerivedGeology::pinchProcess_(const GridType& grid,
const Opm::EclipseState& eclState,
const Vector& htrans,
int numCells)
{
// NOTE that this function is currently never invoked due to
// opmfil being hardcoded to be true.
auto eclgrid = eclState.getInputGrid();
auto& eclProps = eclState.get3DProperties();
const double minpv = eclgrid.getMinpvValue();
const double thickness = eclgrid.getPinchThresholdThickness();
auto transMode = eclgrid.getPinchOption();
auto multzMode = eclgrid.getMultzOption();
PinchProcessor pinch(minpv, thickness, transMode, multzMode);
std::vector htrans_copy(htrans.size());
std::copy_n(htrans.data(), htrans.size(), htrans_copy.begin());
std::vector actnum;
eclgrid.exportACTNUM(actnum);
const auto& transMult = eclState.getTransMult();
std::vector multz(numCells, 0.0);
const int* global_cell = Opm::UgGridHelpers::globalCell(grid);
for (int i = 0; i < numCells; ++i) {
multz[i] = transMult.getMultiplier(global_cell[i], Opm::FaceDir::ZPlus);
}
// Note the pore volume from eclState is used and not the pvol_ calculated above
const auto& porv = eclProps.getDoubleGridProperty("PORV").getData();
pinch.process(grid, htrans_copy, actnum, multz, porv, nnc_);
}
template
inline void DerivedGeology::multiplyHalfIntersections_(const GridType &grid,
const EclipseState& eclState,
const std::vector &ntg,
Vector &halfIntersectTransmissibility,
std::vector &intersectionTransMult)
{
int numCells = Opm::AutoDiffGrid::numCells(grid);
int numIntersections = Opm::AutoDiffGrid::numFaces(grid);
intersectionTransMult.resize(numIntersections);
std::fill(intersectionTransMult.begin(), intersectionTransMult.end(), 1.0);
const TransMult& multipliers = eclState.getTransMult();
auto cell2Faces = Opm::UgGridHelpers::cell2Faces(grid);
auto faceCells = Opm::AutoDiffGrid::faceCells(grid);
const int* global_cell = Opm::UgGridHelpers::globalCell(grid);
int cellFaceIdx = 0;
for (int cellIdx = 0; cellIdx < numCells; ++cellIdx) {
// loop over all logically-Cartesian faces of the current cell
auto cellFacesRange = cell2Faces[cellIdx];
for(auto cellFaceIter = cellFacesRange.begin(), cellFaceEnd = cellFacesRange.end();
cellFaceIter != cellFaceEnd; ++cellFaceIter, ++cellFaceIdx)
{
// the index of the current cell in arrays for the logically-Cartesian grid
int cartesianCellIdx = global_cell[cellIdx];
// The index of the face in the compressed grid
int faceIdx = *cellFaceIter;
// the logically-Cartesian direction of the face
int faceTag = Opm::UgGridHelpers::faceTag(grid, cellFaceIter);
// Translate the C face tag into the enum used by opm-parser's TransMult class
Opm::FaceDir::DirEnum faceDirection;
if (faceTag == 0) // left
faceDirection = Opm::FaceDir::XMinus;
else if (faceTag == 1) // right
faceDirection = Opm::FaceDir::XPlus;
else if (faceTag == 2) // back
faceDirection = Opm::FaceDir::YMinus;
else if (faceTag == 3) // front
faceDirection = Opm::FaceDir::YPlus;
else if (faceTag == 4) // bottom
faceDirection = Opm::FaceDir::ZMinus;
else if (faceTag == 5) // top
faceDirection = Opm::FaceDir::ZPlus;
else
OPM_THROW(std::logic_error, "Unhandled face direction: " << faceTag);
// Account for NTG in horizontal one-sided transmissibilities
switch (faceDirection) {
case Opm::FaceDir::XMinus:
case Opm::FaceDir::XPlus:
case Opm::FaceDir::YMinus:
case Opm::FaceDir::YPlus:
halfIntersectTransmissibility[cellFaceIdx] *= ntg[cartesianCellIdx];
break;
default:
// do nothing for the top and bottom faces
break;
}
// Multiplier contribution on this face for MULT[XYZ] logical cartesian multipliers
intersectionTransMult[faceIdx] *=
multipliers.getMultiplier(cartesianCellIdx, faceDirection);
// Multiplier contribution on this fase for region multipliers
const int cellIdxInside = faceCells(faceIdx, 0);
const int cellIdxOutside = faceCells(faceIdx, 1);
// Do not apply region multipliers in the case of boundary connections
if (cellIdxInside < 0 || cellIdxOutside < 0) {
continue;
}
const int cartesianCellIdxInside = global_cell[cellIdxInside];
const int cartesianCellIdxOutside = global_cell[cellIdxOutside];
// Only apply the region multipliers from the inside
if (cartesianCellIdx == cartesianCellIdxInside) {
intersectionTransMult[faceIdx] *= multipliers.getRegionMultiplier(cartesianCellIdxInside,cartesianCellIdxOutside,faceDirection);
}
}
}
}
template
inline void DerivedGeology::tpfa_loc_trans_compute_(const GridType& grid,
const EclipseGrid& eclGrid,
const double* perm,
Vector& hTrans){
// Using Local coordinate system for the transmissibility calculations
// hTrans(cellFaceIdx) = K(cellNo,j) * sum( C(:,i) .* N(:,j), 2) / sum(C.*C, 2)
// where K is a diagonal permeability tensor, C is the distance from cell centroid
// to face centroid and N is the normal vector pointing outwards with norm equal to the face area.
// Off-diagonal permeability values are ignored without warning
int numCells = AutoDiffGrid::numCells(grid);
int cellFaceIdx = 0;
auto cell2Faces = Opm::UgGridHelpers::cell2Faces(grid);
auto faceCells = Opm::UgGridHelpers::faceCells(grid);
for (int cellIdx = 0; cellIdx < numCells; ++cellIdx) {
// loop over all logically-Cartesian faces of the current cell
auto cellFacesRange = cell2Faces[cellIdx];
for(auto cellFaceIter = cellFacesRange.begin(), cellFaceEnd = cellFacesRange.end();
cellFaceIter != cellFaceEnd; ++cellFaceIter, ++cellFaceIdx)
{
// The index of the face in the compressed grid
const int faceIdx = *cellFaceIter;
// the logically-Cartesian direction of the face
const int faceTag = Opm::UgGridHelpers::faceTag(grid, cellFaceIter);
// d = 0: XPERM d = 4: YPERM d = 8: ZPERM ignores off-diagonal permeability values.
const int d = std::floor(faceTag/2) * 4;
// compute the half transmissibility
double dist = 0.0;
double cn = 0.0;
double sgn = 2.0 * (faceCells(faceIdx, 0) == cellIdx) - 1;
const int dim = Opm::UgGridHelpers::dimensions(grid);
int cartesianCellIdx = AutoDiffGrid::globalCell(grid)[cellIdx];
auto cellCenter = eclGrid.getCellCenter(cartesianCellIdx);
const auto& faceCenter = Opm::UgGridHelpers::faceCenterEcl(grid, cellIdx, faceTag);
const auto& faceAreaNormalEcl = Opm::UgGridHelpers::faceAreaNormalEcl(grid, faceIdx);
for (int indx = 0; indx < dim; ++indx) {
const double Ci = faceCenter[indx] - cellCenter[indx];
dist += Ci*Ci;
cn += sgn * Ci * faceAreaNormalEcl[ indx ];
}
if (cn < 0){
switch (d) {
case 0:
OPM_MESSAGE("Warning: negative X-transmissibility value in cell: " << cellIdx << " replace by absolute value") ;
break;
case 4:
OPM_MESSAGE("Warning: negative Y-transmissibility value in cell: " << cellIdx << " replace by absolute value") ;
break;
case 8:
OPM_MESSAGE("Warning: negative Z-transmissibility value in cell: " << cellIdx << " replace by absolute value") ;
break;
default:
OPM_THROW(std::logic_error, "Inconsistency in the faceTag in cell: " << cellIdx);
}
cn = -cn;
}
hTrans[cellFaceIdx] = perm[cellIdx*dim*dim + d] * cn / dist;
}
}
}
}
#endif // OPM_GEOPROPS_HEADER_INCLUDED