///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2026- Equinor ASA // // ResInsight 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. // // ResInsight 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 at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RifGroupNetworkReader.h" #include "RiaLogging.h" #include "RiaStdStringTools.h" #include "opm/input/eclipse/Deck/Deck.hpp" #include "opm/input/eclipse/Deck/DeckItem.hpp" #include "opm/input/eclipse/Deck/DeckKeyword.hpp" #include "opm/input/eclipse/Deck/DeckRecord.hpp" #include "opm/input/eclipse/Parser/InputErrorAction.hpp" #include "opm/input/eclipse/Parser/ParseContext.hpp" #include "opm/input/eclipse/Parser/Parser.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/A.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/B.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/D.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/E.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/G.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/I.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/N.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/P.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/S.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/T.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/W.hpp" #include #include #include #include #include #include namespace { const std::string fieldName = "FIELD"; const std::string terminalPressureKey = "TERMINAL_PRESSURE"; const std::string vfpTableKey = "VFP_TABLE"; const std::string alqKey = "ALQ"; constexpr int removeBranchVfpNumber = 9999; //-------------------------------------------------------------------------------------------------- /// Child to parent edges in the order they were first defined //-------------------------------------------------------------------------------------------------- struct EdgeMap { struct Value { std::string parent; std::map properties; }; void set( const std::string& child, const Value& value ) { if ( !values.contains( child ) ) order.push_back( child ); values[child] = value; } void remove( const std::string& child ) { if ( values.erase( child ) > 0 ) std::erase( order, child ); } // Removes the edge of a node and the edges of all nodes below it void removeSubtree( const std::string& child ) { std::vector pending = { child }; while ( !pending.empty() ) { const std::string current = pending.back(); pending.pop_back(); remove( current ); for ( const auto& [name, value] : values ) { if ( value.parent == current ) pending.push_back( name ); } } } std::vector order; std::map values; }; //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int monthFromText( const std::string& text ) { static const std::map> months = { { "JAN", 1 }, { "FEB", 2 }, { "MAR", 3 }, { "APR", 4 }, { "MAY", 5 }, { "JUN", 6 }, { "JUL", 7 }, { "JLY", 7 }, { "AUG", 8 }, { "SEP", 9 }, { "OCT", 10 }, { "NOV", 11 }, { "DEC", 12 } }; const std::string key = RiaStdStringTools::toUpper( RiaStdStringTools::trimString( text ).substr( 0, 3 ) ); auto it = months.find( key ); return it != months.end() ? it->second : 0; } //-------------------------------------------------------------------------------------------------- /// DATES and START records have the same items //-------------------------------------------------------------------------------------------------- std::optional dateFromRecord( const Opm::DeckRecord& record ) { using DATES = Opm::ParserKeywords::DATES; const int day = record.getItem().get( 0 ); const int month = monthFromText( record.getItem().getTrimmedString( 0 ) ); const int year = record.getItem().get( 0 ); const QDate date( year, month, day ); if ( !date.isValid() ) return std::nullopt; QTime time( 0, 0 ); if ( record.size() > 3 && record.getItem().hasValue( 0 ) ) { const QString timeText = QString::fromStdString( record.getItem().getTrimmedString( 0 ) ); const QTime parsed = QTime::fromString( timeText, "HH:mm:ss" ); if ( parsed.isValid() ) time = parsed; } return QDateTime( date, time, QTimeZone::utc() ).toSecsSinceEpoch(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::optional doubleValue( const Opm::DeckItem& item ) { if ( !item.hasValue( 0 ) || item.defaultApplied( 0 ) ) return std::nullopt; return item.get( 0 ); } //-------------------------------------------------------------------------------------------------- /// Collects the network keywords while walking through the deck //-------------------------------------------------------------------------------------------------- class NetworkAccumulator { public: void processKeyword( const Opm::DeckKeyword& keyword ) { const std::string& name = keyword.name(); // Keywords in ACTIONX blocks are only applied when the condition is met during the simulation, so they are // not part of the network defined by the schedule if ( name == Opm::ParserKeywords::ACTIONX::keywordName ) { if ( !m_actionReported ) { RiaLogging::warning( "Production network: Network keywords in ACTIONX blocks are ignored" ); m_actionReported = true; } m_isInAction = true; return; } if ( name == Opm::ParserKeywords::ENDACTIO::keywordName ) { m_isInAction = false; return; } if ( m_isInAction ) return; if ( name == Opm::ParserKeywords::DATES::keywordName || name == Opm::ParserKeywords::START::keywordName ) { for ( size_t i = 0; i < keyword.size(); ++i ) { flush(); if ( auto date = dateFromRecord( keyword.getRecord( i ) ) ) m_date = date; else RiaLogging::warning( "Production network: Invalid date in " + name + ", ignored" ); } } else if ( name == Opm::ParserKeywords::TSTEP::keywordName ) { if ( !m_date ) { RiaLogging::warning( "Production network: TSTEP found before the start date, ignored" ); return; } flush(); double days = 0.0; for ( size_t i = 0; i < keyword.size(); ++i ) { for ( double step : keyword.getRecord( i ).getItem( 0 ).getData() ) { days += step; } } *m_date += static_cast( days * 24.0 * 3600.0 ); } else if ( name == Opm::ParserKeywords::GRUPTREE::keywordName ) { using KW = Opm::ParserKeywords::GRUPTREE; for ( const auto& record : keyword ) { const auto child = record.getItem().getTrimmedString( 0 ); const auto parent = record.getItem().getTrimmedString( 0 ); m_gruptree.set( child, { parent, {} } ); } m_changedGruptree = true; } else if ( name == Opm::ParserKeywords::BRANPROP::keywordName ) { using KW = Opm::ParserKeywords::BRANPROP; for ( const auto& record : keyword ) { const auto child = record.getItem().getTrimmedString( 0 ); const auto parent = record.getItem().getTrimmedString( 0 ); const int vfp = record.getItem().get( 0 ); if ( vfp == removeBranchVfpNumber ) { m_branprop.removeSubtree( child ); continue; } std::map properties; properties[vfpTableKey] = vfp; if ( auto alq = doubleValue( record.getItem() ) ) properties[alqKey] = *alq; m_branprop.set( child, { parent, properties } ); } m_changedBranprop = true; } else if ( name == Opm::ParserKeywords::WELSPECS::keywordName ) { using KW = Opm::ParserKeywords::WELSPECS; for ( const auto& record : keyword ) { const auto well = record.getItem().getTrimmedString( 0 ); const auto group = record.getItem().getTrimmedString( 0 ); m_welspecs.set( well, { group, {} } ); } m_changedWelspecs = true; } else if ( name == Opm::ParserKeywords::GRUPNET::keywordName ) { using KW = Opm::ParserKeywords::GRUPNET; for ( const auto& record : keyword ) { const auto node = record.getItem().getTrimmedString( 0 ); if ( auto pressure = doubleValue( record.getItem() ) ) { m_grupnetPressure[node] = *pressure; } } m_changedGruptree = true; } else if ( name == Opm::ParserKeywords::NODEPROP::keywordName ) { using KW = Opm::ParserKeywords::NODEPROP; for ( const auto& record : keyword ) { const auto node = record.getItem().getTrimmedString( 0 ); if ( auto pressure = doubleValue( record.getItem() ) ) { m_nodepropPressure[node] = *pressure; } } m_changedBranprop = true; } } // Stores the current trees with the current date if any network keyword was seen since the last date void flush() { const bool writeGruptree = m_changedGruptree || m_changedWelspecs; const bool writeBranprop = m_changedBranprop || m_changedWelspecs; if ( !writeGruptree && !writeBranprop ) return; m_changedGruptree = m_changedBranprop = m_changedWelspecs = false; if ( !m_date ) { if ( !m_missingDateReported ) { RiaLogging::warning( "Production network: Network keywords found before the start date, ignored" ); m_missingDateReported = true; } m_gruptree = {}; m_branprop = {}; m_welspecs = {}; m_grupnetPressure.clear(); m_nodepropPressure.clear(); return; } if ( writeGruptree ) { auto tree = createTree( m_gruptree, RigGroupNetwork::EdgeKeyword::GRUPTREE, m_grupnetPressure, true ); if ( !tree.edges.empty() ) m_network.addTree( RigGroupNetwork::TreeType::GRUPTREE, tree ); } if ( writeBranprop && !m_branprop.values.empty() ) { auto tree = createTree( m_branprop, RigGroupNetwork::EdgeKeyword::BRANPROP, m_nodepropPressure, false ); if ( !tree.edges.empty() ) m_network.addTree( RigGroupNetwork::TreeType::BRANPROP, tree ); } } RigGroupNetwork network() const { return m_network; } private: RigGroupNetwork::TreeRevision createTree( const EdgeMap& groupEdges, RigGroupNetwork::EdgeKeyword keyword, const std::map& nodePressure, bool isStandardNetwork ) const { EdgeMap edges = groupEdges; std::set nodes; for ( const auto& [child, value] : edges.values ) { nodes.insert( child ); nodes.insert( value.parent ); } if ( isStandardNetwork ) { for ( const auto& well : m_welspecs.order ) { const auto& group = m_welspecs.values.at( well ).parent; if ( !nodes.contains( group ) && group != fieldName ) { edges.set( group, { fieldName, {} } ); nodes.insert( group ); nodes.insert( fieldName ); } else if ( group == fieldName ) { nodes.insert( fieldName ); } } } // Children of each parent, in the order they were defined std::map> children; for ( const auto& child : edges.order ) { children[edges.values.at( child ).parent].push_back( child ); } std::map> wellsOfGroup; for ( const auto& well : m_welspecs.order ) { const auto& group = m_welspecs.values.at( well ).parent; if ( nodes.contains( group ) ) wellsOfGroup[group].push_back( well ); } std::vector roots; for ( const auto& child : edges.order ) { const auto& parent = edges.values.at( child ).parent; if ( !edges.values.contains( parent ) && std::find( roots.begin(), roots.end(), parent ) == roots.end() ) { roots.push_back( parent ); } } if ( roots.empty() && nodes.contains( fieldName ) ) roots.push_back( fieldName ); auto nodeProperties = [&nodePressure]( const std::string& node ) { std::map properties; if ( auto it = nodePressure.find( node ); it != nodePressure.end() ) properties[terminalPressureKey] = it->second; return properties; }; RigGroupNetwork::TreeRevision tree; tree.date = *m_date; // Breadth first from the roots, so parents are listed before their children. Nodes on cycles // are not reachable from a root and are left out. std::set visited; std::deque queue; for ( const auto& root : roots ) { tree.edges.push_back( { root, "", keyword, nodeProperties( root ) } ); visited.insert( root ); queue.push_back( root ); } while ( !queue.empty() ) { const std::string node = queue.front(); queue.pop_front(); if ( auto it = children.find( node ); it != children.end() ) { for ( const auto& child : it->second ) { if ( !visited.insert( child ).second ) continue; auto properties = edges.values.at( child ).properties; properties.merge( nodeProperties( child ) ); tree.edges.push_back( { child, node, keyword, properties } ); queue.push_back( child ); } } if ( auto it = wellsOfGroup.find( node ); it != wellsOfGroup.end() ) { for ( const auto& well : it->second ) { tree.edges.push_back( { well, node, RigGroupNetwork::EdgeKeyword::WELSPECS, {} } ); } } } return tree; } private: std::optional m_date; EdgeMap m_gruptree; EdgeMap m_branprop; EdgeMap m_welspecs; std::map m_grupnetPressure; std::map m_nodepropPressure; bool m_changedGruptree = false; bool m_changedBranprop = false; bool m_changedWelspecs = false; bool m_missingDateReported = false; bool m_isInAction = false; bool m_actionReported = false; RigGroupNetwork m_network; }; } // namespace //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RigGroupNetwork RifGroupNetworkReader::readFromDeck( const Opm::Deck& deck ) { NetworkAccumulator accumulator; for ( const auto& keyword : deck ) { accumulator.processKeyword( keyword ); } accumulator.flush(); return accumulator.network(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::expected RifGroupNetworkReader::readFromFile( const QString& deckFileName ) { if ( deckFileName.isEmpty() ) return std::unexpected( "No deck file" ); if ( !std::filesystem::exists( deckFileName.toStdString() ) ) { return std::unexpected( QString( "Deck file '%1' does not exist" ).arg( deckFileName ) ); } try { // Only the keywords needed for the network are parsed. Other keywords are skipped. Some keywords // without network data are included as they have caused parse errors when skipped. Opm::Parser parser( false ); const std::vector parserKeywords = { Opm::ParserKeywords::START(), Opm::ParserKeywords::DATES(), Opm::ParserKeywords::TSTEP(), Opm::ParserKeywords::GRUPTREE(), Opm::ParserKeywords::BRANPROP(), Opm::ParserKeywords::WELSPECS(), Opm::ParserKeywords::GRUPNET(), Opm::ParserKeywords::NODEPROP(), Opm::ParserKeywords::ACTIONX(), Opm::ParserKeywords::ENDACTIO(), Opm::ParserKeywords::INCLUDE(), Opm::ParserKeywords::PATHS(), Opm::ParserKeywords::TUNING(), Opm::ParserKeywords::SLAVES(), Opm::ParserKeywords::ECHO() }; for ( const auto& keyword : parserKeywords ) { parser.addParserKeyword( keyword ); } Opm::ParseContext parseContext( Opm::InputErrorAction::WARN ); const auto deck = parser.parseFile( deckFileName.toStdString(), parseContext ); return readFromDeck( deck ); } catch ( const std::exception& e ) { return std::unexpected( QString( "Failed to parse '%1': %2" ).arg( deckFileName ).arg( QString::fromStdString( e.what() ) ) ); } catch ( ... ) { return std::unexpected( QString( "Failed to parse '%1'" ).arg( deckFileName ) ); } }