From 03a94a72888a9074761d4af2a44b80dd15733d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A5rd=20Skaflestad?= Date: Thu, 4 Aug 2022 22:59:03 +0200 Subject: [PATCH] Add New Summary Node Category of Completion This enables detecting the last remaining case that has a valid NUMS entry despite nominally being a well-level keyword. --- opm/io/eclipse/SummaryNode.hpp | 15 +- .../SummaryConfig/SummaryConfig.cpp | 356 +++++++++++------- src/opm/io/eclipse/ESmry.cpp | 115 ++++-- src/opm/io/eclipse/SummaryNode.cpp | 90 ++++- src/opm/output/eclipse/Summary.cpp | 95 +++-- tests/test_Summary.cpp | 42 +-- tests/test_SummaryNode.cpp | 104 ++++- 7 files changed, 563 insertions(+), 254 deletions(-) diff --git a/opm/io/eclipse/SummaryNode.hpp b/opm/io/eclipse/SummaryNode.hpp index 8bcc213b4..aa5c7aa3c 100644 --- a/opm/io/eclipse/SummaryNode.hpp +++ b/opm/io/eclipse/SummaryNode.hpp @@ -20,12 +20,11 @@ #ifndef OPM_IO_SUMMARYNODE_HPP #define OPM_IO_SUMMARYNODE_HPP +#include #include +#include #include #include -#include -#include -#include namespace Opm { namespace EclIO { @@ -42,6 +41,7 @@ struct SummaryNode { Region, Block, Connection, + Completion, Segment, Aquifer, Node, @@ -59,7 +59,6 @@ struct SummaryNode { Undefined, }; - std::string keyword; Category category; Type type; @@ -79,6 +78,14 @@ struct SummaryNode { static Category category_from_keyword(const std::string&); + static std::string normalise_keyword(const Category category, + const std::string& keyword); + + static inline std::string normalise_keyword(const std::string& keyword) + { + return normalise_keyword(category_from_keyword(keyword), keyword); + } + // Return true for keywords which should be Miscellaneous, although the // naive first-character-based classification suggests something else. static bool miscellaneous_exception(const std::string& keyword); diff --git a/src/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp b/src/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp index 86b7ad619..287a01231 100644 --- a/src/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp +++ b/src/opm/input/eclipse/EclipseState/SummaryConfig/SummaryConfig.cpp @@ -310,10 +310,6 @@ struct SummaryConfigContext { && is_in_set(countkw, keyword.substr(1)); } - bool is_liquid_phase(const std::string& keyword) { - return keyword == "WPIL"; - } - bool is_supported_region_to_region(const std::string& keyword) { static const auto supported_kw = std::regex { @@ -363,39 +359,22 @@ struct SummaryConfigContext { bool is_connection_completion(const std::string& keyword) { - if (keyword[0] != 'C') - return false; + static const auto conn_compl_kw = std::regex { + R"(C[OGW][IP][RT]L)" + }; - if (keyword.back() != 'L') - return false; - - if (is_udq(keyword)) - return false; - - if (keyword.size() != 5) - return false; - - return true; + return std::regex_match(keyword, conn_compl_kw); } bool is_well_completion(const std::string& keyword) { - if (keyword[0] != 'W') - return false; + static const auto well_compl_kw = std::regex { + R"(W[OGWLV][PIGOLCF][RT]L([0-9_]{2}[0-9])?)" + }; - if (keyword.back() != 'L') - return false; - - if (is_liquid_phase(keyword)) - return false; - - if (is_udq(keyword)) - return false; - - if (keyword == "WMCTL") - return false; - - return true; + // True, e.g., for WOPRL, WOPRL__8, WOPRL123, but not WOPRL___ or + // WKITL. + return std::regex_match(keyword, well_compl_kw); } bool is_node_keyword(const std::string& keyword) @@ -447,6 +426,21 @@ struct SummaryConfigContext { : SummaryConfigNode::Category::Group; } + SummaryConfigNode::Category + distinguish_connection_from_completion(const std::string& keyword) + { + return is_connection_completion(keyword) + ? SummaryConfigNode::Category::Completion + : SummaryConfigNode::Category::Connection; + } + + SummaryConfigNode::Category + distinguish_well_from_completion(const std::string& keyword) + { + return is_well_completion(keyword) + ? SummaryConfigNode::Category::Completion + : SummaryConfigNode::Category::Well; + } void handleMissingWell( const ParseContext& parseContext, ErrorGuard& errors, const KeywordLocation& location, const std::string& well) { std::string msg_fmt = fmt::format("Request for missing well {} in {{keyword}}\n" @@ -561,34 +555,36 @@ inline std::array< int, 3 > getijk( const DeckRecord& record ) { } -inline void keywordCL( SummaryConfig::keyword_list& list, - const ParseContext& parseContext, - ErrorGuard& errors, - const DeckKeyword& keyword, - const Schedule& schedule , - const GridDims& dims) +inline void keywordCL(SummaryConfig::keyword_list& list, + const ParseContext& parseContext, + ErrorGuard& errors, + const DeckKeyword& keyword, + const Schedule& schedule, + const GridDims& dims) { - auto node = SummaryConfigNode{keyword.name(), SummaryConfigNode::Category::Connection, keyword.location()}; - node.parameterType( parseKeywordType(keyword.name()) ); - node.isUserDefined( is_udq(keyword.name()) ); + auto node = SummaryConfigNode { + keyword.name(), SummaryConfigNode::Category::Completion, keyword.location() + } + .parameterType(parseKeywordType(keyword.name())) + .isUserDefined(is_udq(keyword.name())); for (const auto& record : keyword) { const auto& pattern = record.getItem(0).get(0); - auto well_names = schedule.wellNames( pattern, schedule.size() - 1 ); + auto well_names = schedule.wellNames(pattern, schedule.size() - 1); + if (well_names.empty()) { + handleMissingWell(parseContext, errors, keyword.location(), pattern); + } - if( well_names.empty() ) - handleMissingWell( parseContext, errors, keyword.location(), pattern ); - - const auto ijk_defaulted = record.getItem( 1 ).defaultApplied( 0 ); + const auto ijk_defaulted = record.getItem(1).defaultApplied(0); for (const auto& wname : well_names) { const auto& well = schedule.getWellatEnd(wname); const auto& all_connections = well.getConnections(); - node.namedEntity( wname ); + node.namedEntity(wname); if (ijk_defaulted) { for (const auto& conn : all_connections) - list.push_back( node.number( 1 + conn.global_index())); + list.push_back(node.number(1 + conn.global_index())); } else { const auto& ijk = getijk(record); auto global_index = dims.getGlobalIndex(ijk[0], ijk[1], ijk[2]); @@ -599,7 +595,8 @@ inline void keywordCL( SummaryConfig::keyword_list& list, } else { std::string msg = fmt::format("Problem with keyword {{keyword}}\n" "In {{file}} line {{line}}\n" - "Connection ({},{},{}) not defined for well {} ", ijk[0], ijk[1], ijk[2], wname); + "Connection ({},{},{}) not defined for well {}", + ijk[0] + 1, ijk[1] + 1, ijk[2] + 1, wname); parseContext.handleError( ParseContext::SUMMARY_UNHANDLED_KEYWORD, msg, keyword.location(), errors); } } @@ -607,37 +604,46 @@ inline void keywordCL( SummaryConfig::keyword_list& list, } } -inline void keywordWL( SummaryConfig::keyword_list& list, +inline void keywordWL(SummaryConfig::keyword_list& list, const ParseContext& parseContext, ErrorGuard& errors, const DeckKeyword& keyword, - const Schedule& schedule ) + const Schedule& schedule) { for (const auto& record : keyword) { const auto& pattern = record.getItem(0).get(0); - const int completion = record.getItem(1).get(0); - auto well_names = schedule.wellNames( pattern, schedule.size() - 1 ); + const auto well_names = schedule.wellNames(pattern, schedule.size() - 1); - // We add the completion number both the extra field which contains - // parsed data from the keywordname - i.e. WOPRL__8 and also to the - // numeric member which will be written to the NUMS field. - auto node = SummaryConfigNode{ fmt::format("{}{:_>3}", keyword.name(), completion), SummaryConfigNode::Category::Well, keyword.location()}; - node.parameterType( parseKeywordType(keyword.name()) ); - node.isUserDefined( is_udq(keyword.name()) ); - node.number(completion); + if (well_names.empty()) { + handleMissingWell(parseContext, errors, keyword.location(), pattern); + continue; + } - if( well_names.empty() ) - handleMissingWell( parseContext, errors, keyword.location(), pattern ); + const auto completion = record.getItem(1).get(0); + + // Use an amended KEYWORDS entry incorporating the completion ID, + // e.g. "WOPRL_12", for the W*L summary vectors. This is special + // case treatment for compatibility reasons as the more common entry + // here would be to just use "keyword.name()". + auto node = SummaryConfigNode { + fmt::format("{}{:_>3}", keyword.name(), completion), + SummaryConfigNode::Category::Completion, keyword.location() + } + .parameterType(parseKeywordType(keyword.name())) + .isUserDefined(is_udq(keyword.name())) + .number(completion); for (const auto& wname : well_names) { - const auto& well = schedule.getWellatEnd(wname); - if (well.hasCompletion(completion)) - list.push_back( node.namedEntity( wname ) ); + if (schedule.getWellatEnd(wname).hasCompletion(completion)) { + list.push_back(node.namedEntity(wname)); + } else { - std::string msg = fmt::format("Problem with keyword {{keyword}}\n" - "In {{file}} line {{line}}\n" - "Completion number {} not defined for well {} ", completion, wname); - parseContext.handleError( ParseContext::SUMMARY_UNHANDLED_KEYWORD, msg, keyword.location(), errors); + const auto msg = fmt::format("Problem with keyword {{keyword}}\n" + "In {{file}} line {{line}}\n" + "Completion number {} not defined for well {}", + completion, wname); + parseContext.handleError(ParseContext::SUMMARY_UNHANDLED_KEYWORD, + msg, keyword.location(), errors); } } } @@ -1170,7 +1176,7 @@ inline void keywordMISC( SummaryConfig::keyword_list& list, void keywordSWithRecords(const std::size_t last_timestep, const ParseContext& parseContext, - ErrorGuard& errors, + ErrorGuard& errors, const DeckKeyword& keyword, const Schedule& schedule, SummaryConfig::keyword_list& list) @@ -1213,7 +1219,7 @@ inline void keywordMISC( SummaryConfig::keyword_list& list, inline void keywordS(SummaryConfig::keyword_list& list, const ParseContext& parseContext, - ErrorGuard& errors, + ErrorGuard& errors, const DeckKeyword& keyword, const Schedule& schedule) { @@ -1253,18 +1259,20 @@ inline void keywordMISC( SummaryConfig::keyword_list& list, } } - std::string to_string(const SummaryConfigNode::Category cat) { - switch( cat ) { - case SummaryConfigNode::Category::Aquifer: return "Aquifer"; - case SummaryConfigNode::Category::Well: return "Well"; - case SummaryConfigNode::Category::Group: return "Group"; - case SummaryConfigNode::Category::Field: return "Field"; - case SummaryConfigNode::Category::Region: return "Region"; - case SummaryConfigNode::Category::Block: return "Block"; - case SummaryConfigNode::Category::Connection: return "Connection"; - case SummaryConfigNode::Category::Segment: return "Segment"; - case SummaryConfigNode::Category::Node: return "Node"; - case SummaryConfigNode::Category::Miscellaneous: return "Miscellaneous"; + std::string to_string(const SummaryConfigNode::Category cat) + { + switch (cat) { + case SummaryConfigNode::Category::Aquifer: return "Aquifer"; + case SummaryConfigNode::Category::Well: return "Well"; + case SummaryConfigNode::Category::Group: return "Group"; + case SummaryConfigNode::Category::Field: return "Field"; + case SummaryConfigNode::Category::Region: return "Region"; + case SummaryConfigNode::Category::Block: return "Block"; + case SummaryConfigNode::Category::Connection: return "Connection"; + case SummaryConfigNode::Category::Completion: return "Completion"; + case SummaryConfigNode::Category::Segment: return "Segment"; + case SummaryConfigNode::Category::Node: return "Node"; + case SummaryConfigNode::Category::Miscellaneous: return "Miscellaneous"; } throw std::invalid_argument { @@ -1299,40 +1307,84 @@ inline void keywordMISC( SummaryConfig::keyword_list& list, } } - inline void handleKW( SummaryConfig::keyword_list& list, - SummaryConfigContext& context, - const std::vector& node_names, - const std::vector& analyticAquiferIDs, - const std::vector& numericAquiferIDs, - const DeckKeyword& keyword, - const Schedule& schedule, - const FieldPropsManager& field_props, - const ParseContext& parseContext, - ErrorGuard& errors, - const GridDims& dims) { +inline void handleKW( SummaryConfig::keyword_list& list, + SummaryConfigContext& context, + const std::vector& node_names, + const std::vector& analyticAquiferIDs, + const std::vector& numericAquiferIDs, + const DeckKeyword& keyword, + const Schedule& schedule, + const FieldPropsManager& field_props, + const ParseContext& parseContext, + ErrorGuard& errors, + const GridDims& dims) +{ using Cat = SummaryConfigNode::Category; const auto& name = keyword.name(); - check_udq( keyword.location(), schedule, parseContext, errors ); + check_udq(keyword.location(), schedule, parseContext, errors); - const auto cat = parseKeywordCategory( name ); - switch( cat ) { - case Cat::Well: return keywordW( list, parseContext, errors, keyword, schedule ); - case Cat::Group: return keywordG( list, parseContext, errors, keyword, schedule ); - case Cat::Field: return keywordF( list, keyword ); - case Cat::Block: return keywordB( list, keyword, dims ); - case Cat::Region: return keywordR( list, context, keyword, schedule, field_props, parseContext, errors ); - case Cat::Connection: return keywordC( list, parseContext, errors, keyword, schedule, dims); - case Cat::Segment: return keywordS( list, parseContext, errors, keyword, schedule ); - case Cat::Node: return keyword_node( list, node_names, parseContext, errors, keyword ); - case Cat::Aquifer: return keywordAquifer(list, analyticAquiferIDs, numericAquiferIDs, parseContext, errors, keyword); - case Cat::Miscellaneous: return keywordMISC( list, keyword ); + const auto cat = parseKeywordCategory(name); + switch (cat) { + case Cat::Well: + keywordW(list, parseContext, errors, keyword, schedule); + break; - default: - std::string msg_fmt = fmt::format("Summary output keyword {{keyword}} of type {} is not supported\n" - "In {{file}} line {{line}}", to_string(cat)); - parseContext.handleError(ParseContext::SUMMARY_UNHANDLED_KEYWORD, msg_fmt, keyword.location(), errors); - return; + case Cat::Group: + keywordG(list, parseContext, errors, keyword, schedule); + break; + + case Cat::Field: + keywordF(list, keyword); + break; + + case Cat::Block: + keywordB(list, keyword, dims); + break; + + case Cat::Region: + keywordR(list, context, keyword, schedule, field_props, parseContext, errors); + break; + + case Cat::Connection: + keywordC(list, parseContext, errors, keyword, schedule, dims); + break; + + case Cat::Completion: + if (is_well_completion(name)) { + keywordWL(list, parseContext, errors, keyword, schedule); + } + else { + keywordCL(list, parseContext, errors, keyword, schedule, dims); + } + break; + + case Cat::Segment: + keywordS(list, parseContext, errors, keyword, schedule); + break; + + case Cat::Node: + keyword_node(list, node_names, parseContext, errors, keyword); + break; + + case Cat::Aquifer: + keywordAquifer(list, analyticAquiferIDs, numericAquiferIDs, parseContext, errors, keyword); + break; + + case Cat::Miscellaneous: + keywordMISC(list, keyword); + break; + + default: { + const auto msg_fmt = fmt::format("Summary output keyword {{keyword}} of " + "type {} is not supported\n" + "In {{file}} line {{line}}", + to_string(cat)); + + parseContext.handleError(ParseContext::SUMMARY_UNHANDLED_KEYWORD, + msg_fmt, keyword.location(), errors); + } + break; } } @@ -1343,24 +1395,45 @@ inline void handleKW( SummaryConfig::keyword_list& list, const KeywordLocation& location, const Schedule& schedule, const ParseContext& /* parseContext */, - ErrorGuard& /* errors */) { - - - if (is_udq(keyword)) - throw std::logic_error("UDQ keywords not handleded when expanding alias list"); + ErrorGuard& /* errors */) +{ + if (is_udq(keyword)) { + throw std::logic_error { + "UDQ keywords not handleded when expanding alias list" + }; + } using Cat = SummaryConfigNode::Category; - const auto cat = parseKeywordCategory( keyword ); + const auto cat = parseKeywordCategory(keyword); - switch( cat ) { - case Cat::Well: return keywordW( list, keyword, location, schedule ); - case Cat::Group: return keywordG( list, keyword, location, schedule ); - case Cat::Field: return keywordF( list, keyword, location ); - case Cat::Aquifer: return keywordAquifer( list, keyword, analyticAquiferIDs, numericAquiferIDs, location ); - case Cat::Miscellaneous: return keywordMISC( list, keyword, location); + switch (cat) { + case Cat::Well: + keywordW(list, keyword, location, schedule); + break; - default: - throw std::logic_error("Keyword type: " + to_string( cat ) + " is not supported in alias lists. Internal error handling: " + keyword); + case Cat::Group: + keywordG(list, keyword, location, schedule); + break; + + case Cat::Field: + keywordF(list, keyword, location); + break; + + case Cat::Aquifer: + keywordAquifer(list, keyword, analyticAquiferIDs, + numericAquiferIDs, location); + break; + + case Cat::Miscellaneous: + keywordMISC(list, keyword, location); + break; + + default: + throw std::logic_error { + fmt::format("Keyword type {} is not supported in alias " + "lists. Internal error handling keyword {}", + to_string(cat), keyword) + }; } } @@ -1395,7 +1468,8 @@ inline void handleKW( SummaryConfig::keyword_list& list, // ===================================================================== -SummaryConfigNode::Type parseKeywordType(std::string keyword) { +SummaryConfigNode::Type parseKeywordType(std::string keyword) +{ if (is_well_completion(keyword)) keyword.pop_back(); @@ -1413,20 +1487,21 @@ SummaryConfigNode::Type parseKeywordType(std::string keyword) { return SummaryConfigNode::Type::Undefined; } -SummaryConfigNode::Category parseKeywordCategory(const std::string& keyword) { +SummaryConfigNode::Category parseKeywordCategory(const std::string& keyword) +{ using Cat = SummaryConfigNode::Category; if (is_special(keyword)) { return Cat::Miscellaneous; } switch (keyword[0]) { - case 'A': if (is_aquifer(keyword)) return Cat::Aquifer; break; - case 'W': return Cat::Well; - case 'G': return distinguish_group_from_node(keyword); - case 'F': return Cat::Field; - case 'C': return Cat::Connection; - case 'R': return Cat::Region; - case 'B': return Cat::Block; - case 'S': return Cat::Segment; + case 'A': if (is_aquifer(keyword)) return Cat::Aquifer; break; + case 'W': return distinguish_well_from_completion(keyword); + case 'G': return distinguish_group_from_node(keyword); + case 'F': return Cat::Field; + case 'C': return distinguish_connection_from_completion(keyword); + case 'R': return Cat::Region; + case 'B': return Cat::Block; + case 'S': return Cat::Segment; } // TCPU, MLINEARS, NEWTON, &c @@ -1434,10 +1509,10 @@ SummaryConfigNode::Category parseKeywordCategory(const std::string& keyword) { } -SummaryConfigNode::SummaryConfigNode(std::string keyword, const Category cat, KeywordLocation loc_arg) : - keyword_(std::move(keyword)), - category_(cat), - loc(std::move(loc_arg)) +SummaryConfigNode::SummaryConfigNode(std::string keyword, const Category cat, KeywordLocation loc_arg) + : keyword_ (std::move(keyword)) + , category_(cat) + , loc (std::move(loc_arg)) {} SummaryConfigNode SummaryConfigNode::serializeObject() @@ -1503,6 +1578,7 @@ std::string SummaryConfigNode::uniqueNodeKey() const return this->keyword() + ':' + std::to_string(this->number()); case SummaryConfigNode::Category::Connection: [[fallthrough]]; + case SummaryConfigNode::Category::Completion: [[fallthrough]]; case SummaryConfigNode::Category::Segment: return this->keyword() + ':' + this->namedEntity() + ':' + std::to_string(this->number()); } @@ -1538,6 +1614,7 @@ bool operator==(const SummaryConfigNode& lhs, const SummaryConfigNode& rhs) return lhs.number() == rhs.number(); case SummaryConfigNode::Category::Connection: [[fallthrough]]; + case SummaryConfigNode::Category::Completion: [[fallthrough]]; case SummaryConfigNode::Category::Segment: // Equal if associated to same numeric // sub-entity of same named entity @@ -1575,6 +1652,7 @@ bool operator<(const SummaryConfigNode& lhs, const SummaryConfigNode& rhs) return lhs.number() < rhs.number(); case SummaryConfigNode::Category::Connection: [[fallthrough]]; + case SummaryConfigNode::Category::Completion: [[fallthrough]]; case SummaryConfigNode::Category::Segment: { // Ordering determined by pair of named entity and numeric ID. diff --git a/src/opm/io/eclipse/ESmry.cpp b/src/opm/io/eclipse/ESmry.cpp index f3733512d..9a193a3bb 100644 --- a/src/opm/io/eclipse/ESmry.cpp +++ b/src/opm/io/eclipse/ESmry.cpp @@ -18,9 +18,12 @@ #include +#include + #include #include #include + #include #include #include @@ -34,10 +37,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include @@ -46,19 +52,21 @@ KEYWORDS WGNAMES NUMS | PARAM index Corresponding ERT key ---------------------------------------------------+-------------------------------------------------- WGOR OP_1 0 | 0 WGOR:OP_1 - FOPT :+:+:+:+ 0 | 1 FOPT - WWCT OP_1 0 | 2 WWCT:OP_1 - WIR OP_1 0 | 3 WIR:OP_1 - WGOR WI_1 0 | 4 WWCT:OP_1 - WWCT W1_1 0 | 5 WWCT:WI_1 - BPR :+:+:+:+ 12675 | 6 BPR:12675, BPR:i,j,k - RPR :+:+:+:+ 1 | 7 RPR:1 - FOPT :+:+:+:+ 0 | 8 FOPT - GGPR NORTH 0 | 9 GGPR:NORTH - COPR OP_1 5628 | 10 COPR:OP_1:56286, COPR:OP_1:i,j,k - RXF :+:+:+:+ R1 + 32768*(R2 + 10) | 11 RXF:2-3 - SOFX OP_1 12675 | 12 SOFX:OP_1:12675, SOFX:OP_1:i,j,jk - AAQX :+:+:+:+ 12 | 13 AAQX:12 + WOPRL__1 OP_1 1 | 1 WOPRL:OP_1:1 -- KEYWORDS is strictly speaking "WOPRL__1" here. + FOPT :+:+:+:+ 0 | 2 FOPT + WWCT OP_1 0 | 3 WWCT:OP_1 + WIR OP_1 0 | 4 WIR:OP_1 + WGOR WI_1 0 | 5 WWCT:OP_1 + WWCT W1_1 0 | 6 WWCT:WI_1 + BPR :+:+:+:+ 12675 | 7 BPR:12675, BPR:i,j,k + RPR :+:+:+:+ 1 | 8 RPR:1 + FOPT :+:+:+:+ 0 | 9 FOPT + GGPR NORTH 0 | 10 GGPR:NORTH + COPR OP_1 5628 | 11 COPR:OP_1:56286, COPR:OP_1:i,j,k + COPRL OP_1 5628 | 12 COPRL:OP_1:5628, COPRL:OP_1:i,j,k + RXF :+:+:+:+ R1 + 32768*(R2 + 10) | 13 RXF:2-3 + SOFX OP_1 12675 | 14 SOFX:OP_1:12675, SOFX:OP_1:i,j,jk + AAQX :+:+:+:+ 12 | 15 AAQX:12 */ @@ -85,6 +93,25 @@ Opm::time_point make_date(const std::vector& datetime) { return Opm::TimeService::from_time_t( Opm::asTimeT(ts) ); } +bool is_connection_completion(const std::string& keyword) +{ + static const auto conn_compl_kw = std::regex { + R"(C[OGW][IP][RT]L)" + }; + + return std::regex_match(keyword, conn_compl_kw); +} + +bool is_well_completion(const std::string& keyword) +{ + static const auto well_compl_kw = std::regex { + R"(W[OGWLV][PIGOLCF][RT]L([0-9_]{2}[0-9])?)" + }; + + // True, e.g., for WOPRL, WOPRL__8, WOPRL123, but not WOPRL___ or + // WKITL. + return std::regex_match(keyword, well_compl_kw); +} } @@ -186,13 +213,17 @@ ESmry::ESmry(const std::string &filename, bool loadBaseRunData) : for (unsigned int i=0; i key_parts { keyword } ; +Opm::EclIO::SummaryNode::Category +distinguish_well_from_completion(const std::string& keyword) +{ + return is_well_completion(keyword) + ? Opm::EclIO::SummaryNode::Category::Completion + : Opm::EclIO::SummaryNode::Category::Well; +} + +std::string normalise_well_completion_keyword(const std::string& keyword) +{ + static const auto well_compl_kw = std::regex { + R"((W[OGWLV][PIGOLCF][RT]L)([0-9_]{2}[0-9])?)" + }; + + auto keywordPieces = std::smatch {}; + if (std::regex_match(keyword, keywordPieces, well_compl_kw)) { + return keywordPieces[1].str(); + } + + return keyword; +} + +} // Anonymous namespace + +std::string Opm::EclIO::SummaryNode::unique_key(number_renderer render_number) const +{ + auto key_parts = std::vector { normalise_keyword(this->category, this->keyword) }; if (auto opt = display_name()) key_parts.emplace_back(opt.value()); @@ -146,43 +199,50 @@ bool Opm::EclIO::SummaryNode::is_user_defined() const { return matched && !blacklisted; } - /* Observe that this function started out as a slight generalisation of the special case handling of segment variables; i.e. variables starting with 'S'. In general there are many other expecptions e.g. 'NEWTON' is an Miscellaneous variable and not a network variable - but they will be added when/if required. */ -bool Opm::EclIO::SummaryNode::miscellaneous_exception(const std::string& keyword) { +bool Opm::EclIO::SummaryNode::miscellaneous_exception(const std::string& keyword) +{ static const std::unordered_set miscellaneous_keywords = {"SEPARATE", "STEPTYPE", "SUMTHIN"}; return miscellaneous_keywords.count(keyword) == 1; } - -Opm::EclIO::SummaryNode::Category Opm::EclIO::SummaryNode::category_from_keyword( - const std::string& keyword -) { - static const std::unordered_set miscellaneous_keywords = {"SEPARATE", "STEPTYPE", "SUMTHIN"}; - if (keyword.length() == 0) { +Opm::EclIO::SummaryNode::Category +Opm::EclIO::SummaryNode::category_from_keyword(const std::string& keyword) +{ + if (keyword.empty() || + Opm::EclIO::SummaryNode::miscellaneous_exception(keyword)) + { return Category::Miscellaneous; } - if (Opm::EclIO::SummaryNode::miscellaneous_exception(keyword)) - return Category::Miscellaneous; - switch (keyword[0]) { case 'A': return Category::Aquifer; case 'B': return Category::Block; - case 'C': return Category::Connection; + case 'C': return distinguish_connection_from_completion(keyword); case 'F': return Category::Field; case 'G': return distinguish_group_from_node(keyword); case 'R': return Category::Region; case 'S': return Category::Segment; - case 'W': return Category::Well; + case 'W': return distinguish_well_from_completion(keyword); default: return Category::Miscellaneous; } } +std::string +Opm::EclIO::SummaryNode::normalise_keyword(const Opm::EclIO::SummaryNode::Category category, + const std::string& keyword) +{ + return ((category == Opm::EclIO::SummaryNode::Category::Completion) && + is_well_completion(keyword)) + ? normalise_well_completion_keyword(keyword) + : keyword; +} + std::optional Opm::EclIO::SummaryNode::display_name() const { if (use_name(category)) { return wgname; diff --git a/src/opm/output/eclipse/Summary.cpp b/src/opm/output/eclipse/Summary.cpp index 386aaa97e..a8991799c 100644 --- a/src/opm/output/eclipse/Summary.cpp +++ b/src/opm/output/eclipse/Summary.cpp @@ -99,6 +99,7 @@ template <> struct fmt::formatter: fmt::forma case Category::Region: name = "Region"; break; case Category::Block: name = "Block"; break; case Category::Connection: name = "Connection"; break; + case Category::Completion: name = "Completion"; break; case Category::Segment: name = "Segment"; break; case Category::Aquifer: name = "Aquifer"; break; case Category::Node: name = "Node"; break; @@ -817,7 +818,7 @@ inline quantity cpr( const fn_args& args ) { // The args.num value is the literal value which will go to the // NUMS array in the eclipse SMSPEC file; the values in this array // are offset 1 - whereas we need to use this index here to look - // up a completion with offset 0. + // up a connection with offset 0. const size_t global_index = args.num - 1; if (args.schedule_wells.empty()) return zero; @@ -920,7 +921,7 @@ inline quantity crate( const fn_args& args ) { // The args.num value is the literal value which will go to the // NUMS array in the eclipse SMSPEC file; the values in this array // are offset 1 - whereas we need to use this index here to look - // up a completion with offset 0. + // up a connection with offset 0. const size_t global_index = args.num - 1; if (args.schedule_wells.empty()) return zero; @@ -975,7 +976,7 @@ quantity crate_resv( const fn_args& args ) { // The args.num value is the literal value which will go to the // NUMS array in the eclipse SMSPEC file; the values in this array // are offset 1 - whereas we need to use this index here to look - // up a completion with offset 0. + // up a connection with offset 0. const auto global_index = static_cast(args.num - 1); const auto& well_data = xwPos->second; @@ -1004,7 +1005,7 @@ inline quantity srate( const fn_args& args ) { // The args.num value is the literal value which will go to the // NUMS array in the eclispe SMSPEC file; the values in this array // are offset 1 - whereas we need to use this index here to look - // up a completion with offset 0. + // up a connection with offset 0. if (args.schedule_wells.empty()) { return zero; } @@ -1049,7 +1050,7 @@ inline quantity trans_factors ( const fn_args& args ) { // No dynamic results for this well. Not open? return zero; - // Like completion rate we need to look up a connection with offset 0. + // Like connection rate we need to look up a connection with offset 0. const size_t global_index = args.num - 1; const auto& connections = xwPos->second.connections; auto connPos = std::find_if(connections.begin(), connections.end(), @@ -1082,7 +1083,7 @@ inline quantity segpress ( const fn_args& args ) return zero; } - // Like completion rate we need to look up a connection with offset 0. + // Like connection rate we need to look up a connection with offset 0. const size_t segNumber = args.num; const auto& well_data = xwPos->second; @@ -1410,7 +1411,7 @@ inline quantity connection_productivity_index(const fn_args& args) // The args.num value is the literal value which will go to the // NUMS array in the eclipse SMSPEC file; the values in this array // are offset 1 - whereas we need to use this index here to look - // up a completion with offset 0. + // up a connection with offset 0. const auto global_index = static_cast(args.num) - 1; const auto& xcon = xwPos->second.connections; @@ -2429,6 +2430,7 @@ find_wells(const Opm::Schedule& schedule, switch (node.category) { case Opm::EclIO::SummaryNode::Category::Well: case Opm::EclIO::SummaryNode::Category::Connection: + case Opm::EclIO::SummaryNode::Category::Completion: case Opm::EclIO::SummaryNode::Category::Segment: return find_single_well(schedule, node.wgname, sim_step); @@ -2450,7 +2452,7 @@ find_wells(const Opm::Schedule& schedule, throw std::runtime_error { fmt::format("Unhandled summary node category \"{}\" in find_wells()", - node.category) + static_cast(node.category)) }; } @@ -2463,6 +2465,7 @@ bool need_wells(const Opm::EclIO::SummaryNode& node) switch (node.category) { case Cat::Connection: [[fallthrough]]; + case Cat::Completion: [[fallthrough]]; case Cat::Field: [[fallthrough]]; case Cat::Group: [[fallthrough]]; case Cat::Segment: [[fallthrough]]; @@ -2484,7 +2487,10 @@ bool need_wells(const Opm::EclIO::SummaryNode& node) return false; } - throw std::runtime_error("Unhandled summary node category in need_wells"); + throw std::runtime_error { + fmt::format("Unhandled summary node category \"{}\" in need_wells()", + static_cast(node.category)) + }; } void updateValue(const Opm::EclIO::SummaryNode& node, const double value, Opm::SummaryState& st) @@ -3396,7 +3402,10 @@ namespace Evaluator { bool Factory::isFunctionRelation() { - auto pos = funs.find(this->node_->keyword); + const auto normKw = Opm::EclIO::SummaryNode:: + normalise_keyword(this->node_->category, this->node_->keyword); + + auto pos = funs.find(normKw); if (pos != funs.end()) { // 'node_' represents a functional relation. // Capture evaluation function and return true. @@ -3404,41 +3413,45 @@ namespace Evaluator { return true; } - auto keyword = this->node_->keyword; - auto dash_pos = keyword.find("_"); - if (dash_pos != std::string::npos) - keyword = keyword.substr(0, dash_pos); - - pos = funs.find(keyword); - if (pos != funs.end()) { - // 'node_' represents a functional relation. - // Capture evaluation function and return true. - this->paramFunction_ = pos->second; - return true; + if (normKw.length() <= std::string::size_type{4}) { + return false; } - if (keyword.length() > 4 ) { - std::string tracer_tag = keyword.substr(0, 4); - std::string tracer_name = keyword.substr(4); - const auto& tracers = es_.tracer(); - for (const auto& tracer : tracers) { - if (tracer.name == tracer_name) { - if (tracer.phase == Opm::Phase::WATER) - tracer_tag += "#W"; - else if (tracer.phase == Opm::Phase::OIL) - tracer_tag += "#O"; - else if (tracer.phase == Opm::Phase::GAS) - tracer_tag += "#G"; + const auto tracer_name = normKw.substr(4); - pos = funs.find(tracer_tag); - if (pos != funs.end()) { - this->paramFunction_ = pos->second; - return true; - } + const auto& tracers = this->es_.tracer(); + auto trPos = std::find_if(tracers.begin(), tracers.end(), + [&tracer_name](const auto& tracer) + { + return tracer.name == tracer_name; + }); - break; - } - } + if (trPos == tracers.end()) { + return false; + } + + auto tracer_tag = normKw.substr(0, 4); + switch (trPos->phase) { + case Opm::Phase::WATER: + tracer_tag += "#W"; + break; + + case Opm::Phase::OIL: + tracer_tag += "#O"; + break; + + case Opm::Phase::GAS: + tracer_tag += "#G"; + break; + + default: + return false; + } + + pos = funs.find(tracer_tag); + if (pos != funs.end()) { + this->paramFunction_ = pos->second; + return true; } return false; diff --git a/tests/test_Summary.cpp b/tests/test_Summary.cpp index e7f80caad..bdac99a78 100644 --- a/tests/test_Summary.cpp +++ b/tests/test_Summary.cpp @@ -445,21 +445,12 @@ double ecl_sum_get_general_var(const EclIO::ESmry* smry, return smry->get(var)[timeIdx]; } -#if 0 -bool ecl_sum_has_well_var( const EclIO::ESmry* smry, - const std::string& wellname, - const std::string& variable ) -{ - return smry->hasKey(variable + ':' + wellname); -} -#endif - double ecl_sum_get_well_var( const EclIO::ESmry* smry, const int timeIdx, const std::string& wellname, const std::string& variable ) { - return smry->get(variable + ':' + wellname)[timeIdx]; + return smry->get(fmt::format("{}:{}", variable, wellname))[timeIdx]; } double ecl_sum_get_group_var( const EclIO::ESmry* smry, @@ -467,7 +458,16 @@ double ecl_sum_get_group_var( const EclIO::ESmry* smry, const std::string& groupname, const std::string& variable ) { - return smry->get(variable + ':' + groupname)[timeIdx]; + return smry->get(fmt::format("{}:{}", variable, groupname))[timeIdx]; +} + +double ecl_sum_get_well_completion_var( const EclIO::ESmry* smry, + const int timeIdx, + const std::string& wellname, + const std::string& variable, + const int completion) +{ + return smry->get(fmt::format("{}:{}:{}", variable, wellname, completion))[timeIdx]; } double ecl_sum_get_well_connection_var( const EclIO::ESmry* smry, @@ -478,8 +478,7 @@ double ecl_sum_get_well_connection_var( const EclIO::ESmry* smry, const int j, const int k) { - const auto ijk = std::to_string(i) + ',' + std::to_string(j) + ',' + std::to_string(k); - return smry->get(variable + ':' + wellname + ':' + ijk)[timeIdx]; + return smry->get(fmt::format("{}:{}:{},{},{}", variable, wellname, i, j, k))[timeIdx]; } bool ecl_sum_has_well_connection_var( const EclIO::ESmry* smry, @@ -489,7 +488,7 @@ bool ecl_sum_has_well_connection_var( const EclIO::ESmry* smry, const int j, const int k) { - const auto key = fmt::format("{}:{}:{},{},{}", wellname, variable, i, j, k); + const auto key = fmt::format("{}:{}:{},{},{}", variable, wellname, i, j, k); return ecl_sum_has_key(smry, key); } @@ -497,7 +496,6 @@ struct setup { Deck deck; EclipseState es; const EclipseGrid& grid; - std::shared_ptr python; Schedule schedule; SummaryConfig config; data::Wells wells; @@ -511,8 +509,7 @@ struct setup { deck( Parser().parseFile( path) ), es( deck ), grid( es.getInputGrid() ), - python( std::make_shared() ), - schedule( deck, es, python), + schedule( deck, es, std::make_shared()), config( deck, schedule, es.fieldProps(), es.aquifer()), wells( result_wells(w3_injector) ), grp_nwrk( result_group_nwrk() ), @@ -520,6 +517,7 @@ struct setup { ta( "summary_test" ) {} }; + } // Anonymous namespace BOOST_AUTO_TEST_SUITE(Summary) @@ -1265,13 +1263,13 @@ BOOST_AUTO_TEST_CASE(connection_kewords) { BOOST_CHECK_CLOSE( 234.5, ecl_sum_get_well_connection_var( resp, 2, "W_2", "CVPR", 2, 1, 2 ), 1e-5 ); BOOST_CHECK_CLOSE( 0.0, ecl_sum_get_well_connection_var( resp, 1, "W_3", "CVPR", 3, 1, 1 ), 1e-5 ); - BOOST_CHECK_CLOSE(ecl_sum_get_well_var(resp, 1, "W_1", "WOPRL__1"), ecl_sum_get_well_connection_var(resp, 1, "W_1", "COPR", 1,1,1), 1e-5); - BOOST_CHECK_CLOSE(ecl_sum_get_well_var(resp, 1, "W_2", "WOPRL__2"), ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,1) + + BOOST_CHECK_CLOSE(ecl_sum_get_well_completion_var(resp, 1, "W_1", "WOPRL", 1), ecl_sum_get_well_connection_var(resp, 1, "W_1", "COPR", 1,1,1), 1e-5); + BOOST_CHECK_CLOSE(ecl_sum_get_well_completion_var(resp, 1, "W_2", "WOPRL", 2), ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,1) + ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,2), 1e-5); - BOOST_CHECK_CLOSE(ecl_sum_get_well_var(resp, 1, "W_3", "WOPRL__3"), ecl_sum_get_well_connection_var(resp, 1, "W_3", "COPR", 3,1,1), 1e-5); - BOOST_CHECK_EQUAL(ecl_sum_get_well_var(resp, 1, "W_2", "WOPRL__2"), ecl_sum_get_well_var(resp, 1, "W_2", "WOFRL__2")); + BOOST_CHECK_CLOSE(ecl_sum_get_well_completion_var(resp, 1, "W_3", "WOPRL", 3), ecl_sum_get_well_connection_var(resp, 1, "W_3", "COPR", 3,1,1), 1e-5); + BOOST_CHECK_EQUAL(ecl_sum_get_well_completion_var(resp, 1, "W_2", "WOPRL", 2), ecl_sum_get_well_completion_var(resp, 1, "W_2", "WOFRL", 2)); - BOOST_CHECK_CLOSE(ecl_sum_get_well_var(resp, 1, "W_1", "WOPRL__1"), ecl_sum_get_well_connection_var(resp, 1, "W_1", "COPRL", 1,1,1), 1e-5); + BOOST_CHECK_CLOSE(ecl_sum_get_well_completion_var(resp, 1, "W_1", "WOPRL", 1), ecl_sum_get_well_connection_var(resp, 1, "W_1", "COPRL", 1,1,1), 1e-5); BOOST_CHECK_CLOSE(ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPRL", 2, 1, 1), ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,1) + ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,2), 1e-5); BOOST_CHECK_CLOSE(ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPRL", 2, 1, 2), ecl_sum_get_well_connection_var(resp, 1, "W_2", "COPR", 2,1,1) + diff --git a/tests/test_SummaryNode.cpp b/tests/test_SummaryNode.cpp index df584defd..356e68b64 100644 --- a/tests/test_SummaryNode.cpp +++ b/tests/test_SummaryNode.cpp @@ -23,11 +23,16 @@ #include +#include +#include +#include +#include + namespace { void expect_key(const Opm::EclIO::SummaryNode& node, const std::string& unique_key) { BOOST_CHECK_EQUAL(node.unique_key(), unique_key); } -} +} // Anonymous namespace BOOST_AUTO_TEST_SUITE(UniqueKey) @@ -86,3 +91,100 @@ BOOST_AUTO_TEST_CASE(user_defined) { } BOOST_AUTO_TEST_SUITE_END() // UniqueKey + +// =========================================================================== + +BOOST_AUTO_TEST_SUITE(Category) + +namespace { + std::string to_string(const Opm::EclIO::SummaryNode::Category cat) + { + using Cat = Opm::EclIO::SummaryNode::Category; + + switch (cat) { + case Cat::Aquifer: return "Aquifer"; + case Cat::Well: return "Well"; + case Cat::Group: return "Group"; + case Cat::Field: return "Field"; + case Cat::Region: return "Region"; + case Cat::Block: return "Block"; + case Cat::Connection: return "Connection"; + case Cat::Completion: return "Completion"; + case Cat::Segment: return "Segment"; + case Cat::Node: return "Node"; + case Cat::Miscellaneous: return "Miscellaneous"; + } + + throw std::invalid_argument { + "Unhandled Summary Parameter Category '" + + std::to_string(static_cast(cat)) + '\'' + }; + } + + Opm::EclIO::SummaryNode::Category category(const std::string& kw) + { + return Opm::EclIO::SummaryNode::category_from_keyword(kw); + } +} // Anonymous namespace + +BOOST_AUTO_TEST_CASE(Well) +{ + const auto well_kw = std::vector { + "WOPR", "WOPT", "WGIR", "WWIR", + }; + + for (const auto& kw : well_kw) { + BOOST_CHECK_MESSAGE(category(kw) == Opm::EclIO::SummaryNode::Category::Well, + "Keyword '" << kw << "' must be category 'Well'. Got '" << + to_string(category(kw)) << "' instead"); + } + + BOOST_CHECK_MESSAGE(category("WOPRL") != Opm::EclIO::SummaryNode::Category::Well, + "Keyword 'WOPRL' must NOT be category 'Well'"); +} + +BOOST_AUTO_TEST_CASE(Connection) +{ + const auto connection_kw = std::vector { + "COPR", "COPT", "CGIR", "CWIR", + }; + + for (const auto& kw : connection_kw) { + BOOST_CHECK_MESSAGE(category(kw) == Opm::EclIO::SummaryNode::Category::Connection, + "Keyword '" << kw << "' must be category 'Connection'. Got '" << + to_string(category(kw)) << "' instead"); + } + + BOOST_CHECK_MESSAGE(category("COPRL") != Opm::EclIO::SummaryNode::Category::Connection, + "Keyword 'COPRL' must NOT be category 'Connection'"); +} + +BOOST_AUTO_TEST_CASE(Completion) +{ + const auto compl_kw = std::vector { + "OPRL", "OPTL", "GIRL", "WIRL", + }; + + for (const auto& kw_base : compl_kw) { + const auto kw = 'C' + kw_base; + BOOST_CHECK_MESSAGE(category(kw) == Opm::EclIO::SummaryNode::Category::Completion, + "Keyword '" << kw << "' must be category 'Completion'. Got '" << + to_string(category(kw)) << "' instead"); + } + + for (const auto* suffix : { "", "__1", "_12", "123" }) { + for (const auto& kw_base : compl_kw) { + const auto kw = 'W' + kw_base + suffix; + BOOST_CHECK_MESSAGE(category(kw) == Opm::EclIO::SummaryNode::Category::Completion, + "Keyword '" << kw << "' must be category 'Completion'. Got '" << + to_string(category(kw)) << "' instead"); + } + } + + for (const auto* kw : { "WOPRLK", "CGIR", "WKITL__8", "WOOOOPRL", "WHIRL" }) { + BOOST_CHECK_MESSAGE(category(kw) != Opm::EclIO::SummaryNode::Category::Completion, + "Keyword '" << kw << "' must NOT be category 'Completion'"); + } +} + +BOOST_AUTO_TEST_SUITE_END() // Category