Merge pull request #3313 from GitPaean/support_cake_filteration

A simple cake model to simulate formation damage due to suspended solids in injection water
This commit is contained in:
Bård Skaflestad 2023-06-30 15:51:41 +02:00 committed by GitHub
commit 4fb2a3557b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 463 additions and 1 deletions

View File

@ -201,6 +201,7 @@ if(ENABLE_ECL_INPUT)
src/opm/input/eclipse/Schedule/Tuning.cpp
src/opm/input/eclipse/Schedule/WriteRestartFileEvents.cpp
src/opm/input/eclipse/Schedule/Well/Connection.cpp
src/opm/input/eclipse/Schedule/Well/FilterCake.cpp
src/opm/input/eclipse/Schedule/Well/injection.cpp
src/opm/input/eclipse/Schedule/Well/NameOrder.cpp
src/opm/input/eclipse/Schedule/Well/PAvg.cpp
@ -1219,6 +1220,7 @@ if(ENABLE_ECL_INPUT)
opm/input/eclipse/Schedule/VFPInjTable.hpp
opm/input/eclipse/Schedule/VFPProdTable.hpp
opm/input/eclipse/Schedule/Well/Connection.hpp
opm/input/eclipse/Schedule/Well/FilterCake.hpp
opm/input/eclipse/Schedule/Well/PAvg.hpp
opm/input/eclipse/Schedule/Well/PAvgCalculator.hpp
opm/input/eclipse/Schedule/Well/PAvgCalculatorCollection.hpp

View File

@ -747,6 +747,9 @@ namespace Opm
void handleWVFPEXP (HandlerContext&);
void handleWWPAVE (HandlerContext&);
void handleWPIMULT (HandlerContext&);
void handleWINJCLN (HandlerContext&);
void handleWINJDAM (HandlerContext&);
void handleWINJFCNC (HandlerContext&);
void handleWINJMULT (HandlerContext&);
void handleWPMITAB (HandlerContext&);
void handleWPOLYMER (HandlerContext&);

View File

@ -30,6 +30,7 @@
#include <optional>
#include <limits>
#include <opm/input/eclipse/Schedule/Well/FilterCake.hpp>
#include <opm/input/eclipse/Schedule/Well/WINJMULT.hpp>
namespace Opm {
@ -125,6 +126,11 @@ namespace RestartIO {
const InjMult& injmult() const;
bool activeInjMult() const;
void setInjMult(const InjMult& inj_mult);
void setFilterCake(const FilterCake& filter_cake);
const FilterCake& getFilterCake() const;
bool filterCakeActive() const;
double getFilterCakeRadius() const;
double getFilterCakeArea() const;
void setState(State state);
void setComplnum(int compnum);
@ -174,6 +180,7 @@ namespace RestartIO {
serializer(m_defaultSatTabId);
serializer(segment_number);
serializer(m_subject_to_welpi);
serializer(m_filter_cake);
}
private:
@ -257,6 +264,8 @@ namespace RestartIO {
// Whether or not this Connection is subject to WELPI scaling.
bool m_subject_to_welpi = false;
std::optional<FilterCake> m_filter_cake;
static std::string CTFKindToString(const CTFKind);
};
}

View File

@ -0,0 +1,76 @@
/*
Copyright 2023 Equinor.
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/>.
*/
#ifndef OPM_FILTERCAKE_HPP
#define OPM_FILTERCAKE_HPP
#include <string>
#include <optional>
namespace Opm {
class DeckRecord;
class KeywordLocation;
struct FilterCake {
enum class FilterCakeGeometry {
LINEAR,
RADIAL,
NONE,
};
static FilterCakeGeometry filterCakeGeometryFromString(const std::string& str, const KeywordLocation& location);
static std::string filterCakeGeometryToString(const FilterCakeGeometry& geometry);
FilterCakeGeometry geometry{FilterCakeGeometry::NONE};
double perm{0.};
double poro{0.};
std::optional<double> radius;
std::optional<double> flow_area;
// skin factor multiplier
// it is controlled by keyword WINJCLN
double sf_multiplier{1.};
FilterCake() = default;
explicit FilterCake(const DeckRecord& record, const KeywordLocation& location);
template<class Serializer>
void serializeOp(Serializer& serializer) {
serializer(geometry);
serializer(perm);
serializer(poro);
serializer(radius);
serializer(flow_area);
serializer(sf_multiplier);
}
static FilterCake serializationTestObject();
bool operator==(const FilterCake& other) const;
void applyCleanMultiplier(const double factor);
static std::string filterCakeToString(const FilterCake& fc);
};
}
#endif //OPM_FILTERCAKE_HPP

View File

@ -481,7 +481,11 @@ public:
bool handleWELOPENConnections(const DeckRecord& record, Connection::State status);
bool handleCOMPLUMP(const DeckRecord& record);
bool handleWPIMULT(const DeckRecord& record);
bool handleWINJCLN(const DeckRecord& record, const KeywordLocation& location);
bool handleWINJDAM(const DeckRecord& record, const KeywordLocation& location);
bool handleWINJMULT(const DeckRecord& record, const KeywordLocation& location);
void setFilterConc(const double conc);
double getFilterConc() const;
bool applyGlobalWPIMULT(double scale_factor);
void filterConnections(const ActiveGridCells& grid);
@ -550,6 +554,7 @@ public:
serializer(well_temperature);
serializer(inj_mult_mode);
serializer(well_inj_mult);
serializer(m_filter_concentration);
}
private:
@ -598,6 +603,7 @@ private:
double well_temperature;
InjMultMode inj_mult_mode = InjMultMode::NONE;
std::optional<InjMult> well_inj_mult;
double m_filter_concentration = 0.;
};
std::ostream& operator<<( std::ostream&, const Well::WellInjectionProperties& );

View File

@ -1976,6 +1976,46 @@ Well{0} entered with 'FIELD' parent group:
}
}
void Schedule::handleWINJCLN(HandlerContext& handlerContext) {
for (const auto& record : handlerContext.keyword) {
const std::string& wellNamePattern = record.getItem<ParserKeywords::WINJCLN::WELL_NAME>().getTrimmedString(0);
const auto well_names = this->wellNames(wellNamePattern, handlerContext);
for (const auto& well_name: well_names) {
auto well = this->snapshots.back().wells(well_name);
well.handleWINJCLN(record, handlerContext.keyword.location());
this->snapshots.back().wells.update(std::move(well));
}
}
}
void Schedule::handleWINJDAM(HandlerContext& handlerContext) {
for (const auto& record : handlerContext.keyword) {
const std::string& wellNamePattern = record.getItem<ParserKeywords::WINJDAM::WELL_NAME>().getTrimmedString(0);
const auto well_names = wellNames(wellNamePattern);
for (const auto& well_name : well_names) {
auto well = this->snapshots.back().wells(well_name);
if (well.handleWINJDAM(record, handlerContext.keyword.location())) {
this->snapshots.back().wells.update( std::move(well) );
}
}
}
}
void Schedule::handleWINJFCNC(HandlerContext& handlerContext) {
for (const auto& record : handlerContext.keyword) {
const std::string& wellNamePattern = record.getItem<ParserKeywords::WINJFCNC::WELL>().getTrimmedString(0);
const auto well_names = this->wellNames(wellNamePattern, handlerContext);
for (const auto& well_name: well_names) {
auto well = this->snapshots.back().wells(well_name);
const auto filter_conc = record.getItem<ParserKeywords::WINJFCNC::VOL_CONCENTRATION>().get<double>(0);
// the unit is ppm_vol
well.setFilterConc(filter_conc/1.e6);
this->snapshots.back().wells.update(std::move(well));
}
}
}
void Schedule::handleWPMITAB(HandlerContext& handlerContext) {
for (const auto& record : handlerContext.keyword) {
const std::string& wellNamePattern = record.getItem("WELL").getTrimmedString(0);
@ -2558,6 +2598,9 @@ Well{0} entered with 'FIELD' parent group:
{ "WVFPEXP" , &Schedule::handleWVFPEXP },
{ "WWPAVE" , &Schedule::handleWWPAVE },
{ "WPIMULT" , &Schedule::handleWPIMULT },
{ "WINJDAM" , &Schedule::handleWINJDAM },
{ "WINJFCNC", &Schedule::handleWINJFCNC },
{ "WINJCLN", &Schedule::handleWINJCLN },
{ "WPMITAB" , &Schedule::handleWPMITAB },
{ "WPOLYMER", &Schedule::handleWPOLYMER },
{ "WRFT" , &Schedule::handleWRFT },

View File

@ -30,6 +30,7 @@
#include <opm/input/eclipse/Deck/DeckRecord.hpp>
#include <opm/input/eclipse/EclipseState/Grid/FieldPropsManager.hpp>
#include <opm/input/eclipse/Schedule/Well/Connection.hpp>
#include <opm/input/eclipse/Schedule/Well/FilterCake.hpp>
#include <opm/input/eclipse/Schedule/ScheduleGrid.hpp>
namespace Opm {
@ -136,6 +137,7 @@ Connection::Connection(const RestartIO::RstConnection& rst_connection, const Sch
result.m_defaultSatTabId = true;
result.segment_number = 16;
result.m_subject_to_welpi = true;
result.m_filter_cake = FilterCake::serializationTestObject();
return result;
}
@ -301,6 +303,9 @@ const std::optional<std::pair<double, double>>& Connection::perf_range() const {
if (this->m_injmult.has_value()) {
ss << "INJMULT " << InjMult::InjMultToString(this->m_injmult.value()) << std::endl;
}
if (this->m_filter_cake.has_value()) {
ss << "FilterCake " << FilterCake::filterCakeToString(this->m_filter_cake.value()) << std::endl;
}
return ss.str();
}
@ -323,7 +328,8 @@ const std::optional<std::pair<double, double>>& Connection::perf_range() const {
&& this->segment_number == rhs.segment_number
&& this->center_depth == rhs.center_depth
&& this->m_sort_value == rhs.m_sort_value
&& this->m_subject_to_welpi == rhs.m_subject_to_welpi;
&& this->m_subject_to_welpi == rhs.m_subject_to_welpi
&& this->m_filter_cake == rhs.m_filter_cake;
}
bool Connection::operator!=( const Connection& rhs ) const {
@ -461,4 +467,41 @@ void Connection::setInjMult(const InjMult& inj_mult) {
m_injmult = inj_mult;
}
void Connection::setFilterCake(const FilterCake& filter_cake) {
this->m_filter_cake = filter_cake;
}
bool Connection::filterCakeActive() const {
return this->m_filter_cake.has_value();
}
const FilterCake& Connection::getFilterCake() const {
assert(this->filterCakeActive());
return this->m_filter_cake.value();
}
double Connection::getFilterCakeRadius() const {
if (this->getFilterCake().radius.has_value()) {
return this->getFilterCake().radius.value();
} else {
return this->m_rw;
}
}
double Connection::getFilterCakeArea() const {
if (this->getFilterCake().flow_area.has_value()) {
return this->getFilterCake().flow_area.value();
} else {
const double radius = this->getFilterCakeRadius();
const double length = this->m_connection_length;
constexpr double pi = 3.14159265;
return 2. * pi * radius * length;
}
}
} // end of namespace Opm

View File

@ -0,0 +1,115 @@
/*
Copyright 2023 Equinor.
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 <opm/input/eclipse/Deck/DeckRecord.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/W.hpp>
#include <opm/input/eclipse/Schedule/Well/FilterCake.hpp>
#include <opm/common/OpmLog/KeywordLocation.hpp>
#include <opm/common/utility/OpmInputError.hpp>
#include <fmt/format.h>
namespace Opm {
FilterCake::FilterCakeGeometry FilterCake::filterCakeGeometryFromString(const std::string& str,
const KeywordLocation& location) {
if (str == "LINEAR")
return FilterCakeGeometry::LINEAR;
else if (str == "RADIAL")
return FilterCakeGeometry::RADIAL;
else
throw OpmInputError(fmt::format("Unknown geometry type {} is specified in WINJDAM keyword", str), location);
}
std::string FilterCake::filterCakeGeometryToString(const Opm::FilterCake::FilterCakeGeometry& geometry) {
switch (geometry) {
case FilterCakeGeometry::LINEAR:
return "LINEAR";
case FilterCakeGeometry::RADIAL:
return "RADIAL";
case FilterCakeGeometry::NONE:
return "NONE";
default:
return "unknown FileterCakeGeometry type";
}
}
FilterCake::FilterCake(const DeckRecord& record, const KeywordLocation& location) {
this->geometry = filterCakeGeometryFromString(record.getItem<ParserKeywords::WINJDAM::GEOMETRY>().getTrimmedString(0),
location);
this->perm = record.getItem<ParserKeywords::WINJDAM::FILTER_CAKE_PERM>().getSIDouble(0);
this->poro = record.getItem<ParserKeywords::WINJDAM::FILTER_CAKE_PORO>().getSIDouble(0);
const auto& item_radius = record.getItem<ParserKeywords::WINJDAM::FILTER_CAKE_RADIUS>();
if (!item_radius.defaultApplied(0)) {
this->radius = item_radius.getSIDouble(0);
}
const auto& item_area = record.getItem<ParserKeywords::WINJDAM::FILTER_CAKE_AREA>();
if (!item_area.defaultApplied(0)) {
this->flow_area = item_area.getSIDouble(0);
}
}
bool FilterCake::operator==(const FilterCake& other) const {
return geometry == other.geometry
&& perm == other.perm
&& poro == other.poro
&& radius == other.radius
&& flow_area == other.flow_area
&& sf_multiplier == other.sf_multiplier;
}
void FilterCake::applyCleanMultiplier(const double factor) {
this->sf_multiplier *= factor;
}
FilterCake FilterCake::serializationTestObject() {
FilterCake filter_cake;
filter_cake.geometry = FilterCakeGeometry::LINEAR;
filter_cake.perm = 1.e-8;
filter_cake.poro = 0.2;
filter_cake.radius = 0.1;
filter_cake.flow_area = 20.;
filter_cake.sf_multiplier = 0.2;
return filter_cake;
}
std::string FilterCake::filterCakeToString(const FilterCake& fc) {
std::string str = fmt::format("geometry type {}, perm {}, poro {}",
filterCakeGeometryToString(fc.geometry), fc.perm, fc.poro);
if (fc.radius.has_value()) {
fmt::format_to(std::back_inserter(str), ", radius {}", fc.radius.value());
} else {
fmt::format_to(std::back_inserter(str), ", radius DEFAULT");
}
if (fc.flow_area.has_value()) {
fmt::format_to(std::back_inserter(str), ", flow_area {}", fc.flow_area.value());
} else {
fmt::format_to(std::back_inserter(str), ", flow_area DEFAULT");
}
fmt::format_to(std::back_inserter(str), ", sf_multiplier {}.", fc.sf_multiplier);
return str;
}
} // end of Opm namespace

View File

@ -536,6 +536,7 @@ Well Well::serializationTestObject()
result.m_pavg = PAvg();
result.well_temperature = 10.0;
result.well_inj_mult = InjMult::serializationTestObject();
result.m_filter_concentration = 0.1;
return result;
}
@ -1303,6 +1304,54 @@ bool Well::handleWPIMULT(const DeckRecord& record) {
return this->updateConnections(std::move(new_connections), false);
}
bool Well::handleWINJCLN(const DeckRecord& record, const KeywordLocation& location) {
auto match = [=] ( const Connection& c) -> bool {
if (!match_eq(c.getI() , record, "I", -1)) return false;
if (!match_eq(c.getJ() , record, "J", -1)) return false;
if (!match_eq(c.getK() , record, "K", -1)) return false;
return true;
};
const double fraction_removal = record.getItem<ParserKeywords::WINJCLN::FRAC_REMOVE>().getSIDouble(0);
if (fraction_removal < 0. || fraction_removal > 1.) {
const auto reason = fmt::format(" the item 2 in keyword WINJCLN must be between 0 and 1, while a "
"value {} is given.", fraction_removal);
throw OpmInputError(reason, location);
}
const double fraction_remain = 1. - fraction_removal;
auto new_connections = std::make_shared<WellConnections>(this->connections->ordering(), this->headI, this->headJ);
for (auto c : *(this->connections)) {
if (match(c)) {
auto filter_cake = c.getFilterCake();
filter_cake.applyCleanMultiplier(fraction_remain);
c.setFilterCake(filter_cake);
}
new_connections->add(c);
}
return this->updateConnections(std::move(new_connections), false);
}
bool Well::handleWINJDAM(const DeckRecord& record, const KeywordLocation& location) {
auto match = [=] ( const Connection& c) -> bool {
if (!match_eq(c.getI() , record, "I", -1)) return false;
if (!match_eq(c.getJ() , record, "J", -1)) return false;
if (!match_eq(c.getK() , record, "K", -1)) return false;
return true;
};
const FilterCake filter_cake {record, location};
auto new_connections = std::make_shared<WellConnections>(this->connections->ordering(),
this->headI, this->headJ);
for (auto c : *(this->connections)) {
if (match(c)) {
c.setFilterCake(filter_cake);
}
new_connections->add(c);
}
return this->updateConnections(std::move(new_connections), false);
}
bool Well::handleWINJMULT(const Opm::DeckRecord& record, const KeywordLocation& location) {
// for this keyword, the default for I, J, K will be negative
@ -1618,6 +1667,7 @@ bool Well::operator==(const Well& data) const {
&& (this->well_temperature == data.well_temperature)
&& (this->inj_mult_mode == data.inj_mult_mode)
&& (this->well_inj_mult == data.well_inj_mult)
&& (this->m_filter_concentration == data.m_filter_concentration)
;
}
@ -1713,3 +1763,10 @@ bool Opm::Well::aciveWellInjMult() const {
}
void Opm::Well::setFilterConc(const double conc) {
this->m_filter_concentration = conc;
}
double Opm::Well::getFilterConc() const {
return this->m_filter_concentration;
}

View File

@ -0,0 +1,33 @@
{
"name": "WINJCLN",
"sections": [
"SCHEDULE"
],
"items": [
{
"name": "WELL_NAME",
"value_type": "STRING"
},
{
"name": "FRAC_REMOVE",
"value_type": "DOUBLE",
"dimension": "1",
"default": 1.0
},
{
"name": "I",
"value_type": "INT",
"default": -1
},
{
"name": "J",
"value_type": "INT",
"default": -1
},
{
"name": "K",
"value_type": "INT",
"default": -1
}
]
}

View File

@ -0,0 +1,52 @@
{
"name": "WINJDAM",
"sections": [
"SCHEDULE"
],
"items": [
{
"name": "WELL_NAME",
"value_type": "STRING"
},
{
"name": "GEOMETRY",
"value_type": "STRING"
},
{
"name": "FILTER_CAKE_PERM",
"value_type": "DOUBLE",
"dimension": "Permeability"
},
{
"name": "FILTER_CAKE_PORO",
"value_type": "DOUBLE",
"dimension": "1",
"default": 0.3
},
{
"name": "FILTER_CAKE_RADIUS",
"value_type": "DOUBLE",
"dimension": "Length"
},
{
"name": "FILTER_CAKE_AREA",
"value_type": "DOUBLE",
"dimension": "Length*Length"
},
{
"name": "I",
"value_type": "INT",
"default": -1
},
{
"name": "J",
"value_type": "INT",
"default": -1
},
{
"name": "K",
"value_type": "INT",
"default": -1
}
]
}

View File

@ -0,0 +1,18 @@
{
"name": "WINJFCNC",
"sections": [
"SCHEDULE"
],
"items": [
{
"name": "WELL",
"value_type": "STRING"
},
{
"name": "VOL_CONCENTRATION",
"value_type": "DOUBLE",
"dimension": "1",
"default" : 0
}
]
}

View File

@ -1153,6 +1153,9 @@ set( keywords
900_OPM/V/VAPWAT
900_OPM/W/WATJT
900_OPM/W/WELTRAJ
900_OPM/W/WINJCLN
900_OPM/W/WINJDAM
900_OPM/W/WINJFCNC
900_OPM/W/WMICP
900_OPM/W/WPMITAB
900_OPM/W/WSKPTAB)

View File

@ -75,6 +75,7 @@
#include <opm/input/eclipse/Schedule/MSW/WellSegments.hpp>
#include <opm/input/eclipse/Schedule/Network/Balance.hpp>
#include <opm/input/eclipse/Schedule/Network/ExtNetwork.hpp>
#include <opm/input/eclipse/Schedule/Well/FilterCake.hpp>
#include <opm/input/eclipse/Schedule/Network/Node.hpp>
#include <opm/input/eclipse/Schedule/OilVaporizationProperties.hpp>
#include <opm/input/eclipse/Schedule/RFTConfig.hpp>
@ -229,6 +230,7 @@ TEST_FOR_TYPE(Equil)
TEST_FOR_TYPE(TLMixpar)
TEST_FOR_TYPE(Ppcwmax)
TEST_FOR_TYPE(Events)
TEST_FOR_TYPE(FilterCake)
TEST_FOR_TYPE(Fault)
TEST_FOR_TYPE(FaultCollection)
TEST_FOR_TYPE(FaultFace)