/* 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