diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 2c447e4e5b..13650e0d92 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -33,6 +33,7 @@ #include "RimWellPathCompletionSettings.h" #include "opm/input/eclipse/Deck/DeckKeyword.hpp" +#include "opm/input/eclipse/Deck/DeckRecord.hpp" #include #include @@ -43,7 +44,9 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, - const std::vector& dates ) + const std::vector& dates, + bool includeWelsegs, + bool includeCompsegs ) { QString result; @@ -55,7 +58,7 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& // Generate section for each date for ( const auto& date : dates ) { - QString dateSection = generateDateSection( timeline, eclipseCase, wellPaths, date ); + QString dateSection = generateDateSection( timeline, eclipseCase, wellPaths, date, includeWelsegs, includeCompsegs ); if ( !dateSection.isEmpty() ) { result += dateSection; @@ -88,13 +91,35 @@ std::vector RicScheduleDataGenerator::collectAllDates( const RimWellE return std::vector( uniqueDates.begin(), uniqueDates.end() ); } +//-------------------------------------------------------------------------------------------------- +/// Append the records of `kw` into the entry for `name`, creating that entry from a copy of `kw` +/// on first encounter so the accumulated DeckKeyword is bound to the right ParserKeyword. +//-------------------------------------------------------------------------------------------------- +void RicScheduleDataGenerator::mergeKeyword( std::map& acc, const QString& name, Opm::DeckKeyword kw ) +{ + if ( kw.size() == 0 && !kw.isDataKeyword() ) return; + + auto it = acc.find( name ); + if ( it == acc.end() ) + { + acc.emplace( name, std::move( kw ) ); + return; + } + for ( const auto& rec : kw ) + { + it->second.addRecord( Opm::DeckRecord( rec ) ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, - const QDateTime& date ) + const QDateTime& date, + bool includeWelsegs, + bool includeCompsegs ) { // Keyword priority order for output static const std::vector keywordOrder = { "WELSPECS", @@ -120,66 +145,64 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin // Generate DATES keyword result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n"; - // Collect all keyword output into a map keyed by keyword name - std::map keywordBlocks; + // Records for each keyword name are accumulated across wells, then serialised once below. + std::map keywordBlocks; for ( auto* well : wellPaths ) { if ( !well ) continue; - QString welspecsKw = generateWelspecsForWell( timeline, eclipseCase, *well, date ); - if ( !welspecsKw.isEmpty() ) + if ( auto welspecs = generateWelspecsForWell( timeline, eclipseCase, *well, date ) ) { - keywordBlocks["WELSPECS"] += welspecsKw; + mergeKeyword( keywordBlocks, "WELSPECS", std::move( *welspecs ) ); } - QString wellCompdat = generateCompdatForWell( timeline, eclipseCase, *well, date ); - if ( !wellCompdat.isEmpty() ) + if ( auto compdat = generateCompdatForWell( timeline, eclipseCase, *well, date ) ) { - keywordBlocks["COMPDAT"] += wellCompdat; + mergeKeyword( keywordBlocks, "COMPDAT", std::move( *compdat ) ); } - generateMswForWell( timeline, eclipseCase, *well, date, keywordBlocks ); + generateMswForWell( timeline, eclipseCase, *well, date, keywordBlocks, includeWelsegs, includeCompsegs ); generateWellControlForWell( timeline, *well, date, keywordBlocks ); } - // Process schedule-level keyword events (not tied to a specific well) + // Schedule-level keyword events (not tied to a specific well) auto events = timeline.getEventsAtDate( date ); for ( auto* event : events ) { - if ( event->eventType() == RimWellEvent::EventType::SCHEDULE_KEYWORD ) + if ( event->eventType() != RimWellEvent::EventType::SCHEDULE_KEYWORD ) continue; + auto* keywordEvent = dynamic_cast( event ); + if ( !keywordEvent ) continue; + if ( auto kw = keywordEvent->generateDeckKeyword( "" ) ) { - auto* keywordEvent = dynamic_cast( event ); - if ( keywordEvent ) - { - QString keywordStr = keywordEvent->generateScheduleKeyword( "" ); - if ( !keywordStr.isEmpty() ) - { - keywordBlocks[keywordEvent->keywordName().toUpper()] += keywordStr + "\n"; - } - } + mergeKeyword( keywordBlocks, keywordEvent->keywordName().toUpper(), std::move( *kw ) ); } } + auto appendKeywordText = [&]( const Opm::DeckKeyword& kw ) + { + if ( kw.size() == 0 && !kw.isDataKeyword() ) return; + result += RimKeywordFactory::deckKeywordToString( kw ); + result += "\n"; + }; + // Output keywords in priority order std::set emitted; for ( const auto& kw : keywordOrder ) { auto it = keywordBlocks.find( kw ); - if ( it != keywordBlocks.end() && !it->second.isEmpty() ) + if ( it != keywordBlocks.end() ) { - result += it->second; + appendKeywordText( it->second ); emitted.insert( kw ); } } - // Output remaining keywords not in priority list - for ( const auto& [kw, data] : keywordBlocks ) + // Output remaining keywords not in the priority list + for ( const auto& [kw, deckKw] : keywordBlocks ) { - if ( emitted.find( kw ) == emitted.end() && !data.isEmpty() ) - { - result += data; - } + if ( emitted.contains( kw ) ) continue; + appendKeywordText( deckKw ); } return result; @@ -188,12 +211,11 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RicScheduleDataGenerator::generateWelspecsForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - RimWellPath& well, - const QDateTime& date ) +std::optional RicScheduleDataGenerator::generateWelspecsForWell( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + RimWellPath& well, + const QDateTime& date ) { - // Get perforation events at this exact date for this well auto events = timeline.getEventsAtDate( date ); bool hasEvents = false; @@ -208,27 +230,22 @@ QString RicScheduleDataGenerator::generateWelspecsForWell( const RimWellEventTim } } - if ( !hasEvents ) return QString(); + if ( !hasEvents ) return std::nullopt; std::string wellGroupName = well.completionSettings()->groupNameForExport().toStdString(); auto welspecsKw = RimKeywordFactory::welspecsKeyword( wellGroupName, &eclipseCase, &well ); - - QString result; - result += RimKeywordFactory::deckKeywordToString( welspecsKw ); - result += "\n"; - - return result; + if ( welspecsKw.name().empty() ) return std::nullopt; + return welspecsKw; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RicScheduleDataGenerator::generateCompdatForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - RimWellPath& well, - const QDateTime& date ) +std::optional RicScheduleDataGenerator::generateCompdatForWell( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + RimWellPath& well, + const QDateTime& date ) { - // Get perforation events at this exact date for this well auto events = timeline.getEventsAtDate( date ); bool hasPerfEvents = false; @@ -241,38 +258,34 @@ QString RicScheduleDataGenerator::generateCompdatForWell( const RimWellEventTime } } - if ( !hasPerfEvents ) return QString(); + if ( !hasPerfEvents ) return std::nullopt; auto compdata = RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPath( &well, &eclipseCase, date ); auto wellName = well.completionSettings()->wellNameForExport().toStdString(); - auto compdatKw = RimKeywordFactory::compdatKeyword( compdata, wellName ); - QString result; - result += RimKeywordFactory::deckKeywordToString( compdatKw ); - result += "\n"; - - return result; + auto compdatKw = RimKeywordFactory::compdatKeyword( compdata, wellName ); + if ( compdatKw.name().empty() ) return std::nullopt; + return compdatKw; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - RimWellPath& wellPath, - const QDateTime& date, - std::map& keywordBlocks ) +void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + RimWellPath& wellPath, + const QDateTime& date, + std::map& keywordBlocks, + bool includeWelsegs, + bool includeCompsegs ) { - // Check if the well has MSW configured (from any previous tubing events applied via set_timestamp) - // If MSW is enabled, we should generate WELSEGS/COMPSEGS instead of COMPDAT auto* mswParams = wellPath.mswCompletionParameters(); if ( !mswParams ) { return; } - // Also check if there are valve or tubing events at this specific date for this well - // (these trigger updates to the MSW configuration) + // MSW is generated only when there are valve/tubing/perforation events at this exact date. auto events = timeline.getEventsAtDate( date ); bool hasMswEvents = false; bool hasPerfEvents = false; @@ -290,15 +303,11 @@ void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& t } } - // Only generate MSW output if there are events at this specific date for this well - // This ensures wells only appear in schedule sections at their event dates if ( !hasMswEvents && !hasPerfEvents ) { return; } - // Extract MSW data using the existing infrastructure - // Pass the export date to filter valve/perforation output based on their creation dates bool exportAfterMainBore = true; auto mswDataResult = RicWellPathExportMswTableData::extractSingleWellMswData( &eclipseCase, &wellPath, @@ -310,77 +319,46 @@ void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& t const auto& mswData = mswDataResult.value(); - // Generate WELSEGS keyword - int maxSegments = 0; - int maxBranches = 0; - Opm::DeckKeyword welsegsKw = RimKeywordFactory::welsegsKeyword( mswData, maxSegments, maxBranches ); - if ( welsegsKw.isDataKeyword() || welsegsKw.size() > 0 ) + if ( includeWelsegs ) { - keywordBlocks["WELSEGS"] += RimKeywordFactory::deckKeywordToString( welsegsKw ) + "\n"; + int maxSegments = 0; + int maxBranches = 0; + auto welsegsKw = RimKeywordFactory::welsegsKeyword( mswData, maxSegments, maxBranches ); + mergeKeyword( keywordBlocks, "WELSEGS", std::move( welsegsKw ) ); } - // Generate COMPSEGS keyword - Opm::DeckKeyword compsegsKw = RimKeywordFactory::compsegsKeyword( mswData ); - if ( compsegsKw.isDataKeyword() || compsegsKw.size() > 0 ) + if ( includeCompsegs ) { - keywordBlocks["COMPSEGS"] += RimKeywordFactory::deckKeywordToString( compsegsKw ) + "\n"; + auto compsegsKw = RimKeywordFactory::compsegsKeyword( mswData ); + mergeKeyword( keywordBlocks, "COMPSEGS", std::move( compsegsKw ) ); } - // Generate WSEGVALV keyword (for valve events) - Opm::DeckKeyword wsegvalvKw = RimKeywordFactory::wsegvalvKeyword( mswData ); - if ( wsegvalvKw.isDataKeyword() || wsegvalvKw.size() > 0 ) - { - keywordBlocks["WSEGVALV"] += RimKeywordFactory::deckKeywordToString( wsegvalvKw ) + "\n"; - } + auto wsegvalvKw = RimKeywordFactory::wsegvalvKeyword( mswData ); + mergeKeyword( keywordBlocks, "WSEGVALV", std::move( wsegvalvKw ) ); - // Generate WSEGAICD keyword (if AICD data present) - Opm::DeckKeyword wsegaicdKw = RimKeywordFactory::wsegaicdKeyword( mswData ); - if ( wsegaicdKw.isDataKeyword() || wsegaicdKw.size() > 0 ) - { - keywordBlocks["WSEGAICD"] += RimKeywordFactory::deckKeywordToString( wsegaicdKw ) + "\n"; - } + auto wsegaicdKw = RimKeywordFactory::wsegaicdKeyword( mswData ); + mergeKeyword( keywordBlocks, "WSEGAICD", std::move( wsegaicdKw ) ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RicScheduleDataGenerator::generateWellControlForWell( const RimWellEventTimeline& timeline, - const RimWellPath& well, - const QDateTime& date, - std::map& keywordBlocks ) +void RicScheduleDataGenerator::generateWellControlForWell( const RimWellEventTimeline& timeline, + const RimWellPath& well, + const QDateTime& date, + std::map& keywordBlocks ) { - // Get state and control events at this exact date for this well auto events = timeline.getEventsAtDate( date ); for ( auto* event : events ) { if ( event->wellName() != well.name() ) continue; - QString keywordStr = RifEventKeywordFormatter::formatWellEvent( event, well.name() ); - if ( !keywordStr.isEmpty() ) - { - QString kwName = extractKeywordName( keywordStr ); - if ( !kwName.isEmpty() ) - { - keywordBlocks[kwName] += keywordStr + "\n"; - } - } - } -} + auto kw = RifEventKeywordFormatter::buildWellEvent( event, well.name() ); + if ( !kw ) continue; -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -QString RicScheduleDataGenerator::extractKeywordName( const QString& block ) -{ - const auto lines = block.split( '\n' ); - for ( const auto& line : lines ) - { - QString trimmed = line.trimmed(); - if ( !trimmed.isEmpty() && !trimmed.startsWith( "--" ) ) - { - return trimmed; - } + const QString name = QString::fromStdString( kw->name() ); + if ( name.isEmpty() ) continue; + mergeKeyword( keywordBlocks, name, std::move( *kw ) ); } - return {}; } diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index ed4512af94..e11207f8d1 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -22,6 +22,7 @@ #include #include +#include #include class RimEclipseCase; @@ -29,6 +30,11 @@ class RimWellPath; class RimWellEvent; class RimWellEventTimeline; +namespace Opm +{ +class DeckKeyword; +} // namespace Opm + //================================================================================================== /// /// Generator for Eclipse schedule data based on well events @@ -37,11 +43,15 @@ class RimWellEventTimeline; class RicScheduleDataGenerator { public: - // Generate schedule for multiple wells at specified dates + // Generate schedule for multiple wells at specified dates. + // includeWelsegs / includeCompsegs gate the corresponding multi-segment-well keywords; + // WSEGVALV and WSEGAICD remain unaffected. static QString generateSchedule( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, - const std::vector& dates ); + const std::vector& dates, + bool includeWelsegs = true, + bool includeCompsegs = true ); // Collect all unique dates from all wells' timelines static std::vector collectAllDates( const RimWellEventTimeline& timeline, const std::vector& wellPaths ); @@ -51,28 +61,33 @@ class RicScheduleDataGenerator static QString generateDateSection( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, - const QDateTime& date ); + const QDateTime& date, + bool includeWelsegs, + bool includeCompsegs ); - static QString + static std::optional generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date ); // Generate COMPDAT for a well at a specific date based on events - static QString + static std::optional generateCompdatForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date ); - // Generate WELSEGS and COMPSEGS for a well at a specific date, populating keyword blocks map - static void generateMswForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - RimWellPath& well, - const QDateTime& date, - std::map& keywordBlocks ); + // Generate WELSEGS / COMPSEGS / WSEGVALV / WSEGAICD for a well at a specific date, merging into the accumulator. + // includeWelsegs/includeCompsegs suppress only those two keywords; WSEGVALV/WSEGAICD always emit. + static void generateMswForWell( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + RimWellPath& well, + const QDateTime& date, + std::map& keywordBlocks, + bool includeWelsegs, + bool includeCompsegs ); - // Generate well control keywords (WCONPROD, WCONINJE, WELOPEN) for a well at a specific date, populating keyword blocks map - static void generateWellControlForWell( const RimWellEventTimeline& timeline, - const RimWellPath& well, - const QDateTime& date, - std::map& keywordBlocks ); + // Generate well control / well keyword event keywords for a well at a specific date, merging into the accumulator + static void generateWellControlForWell( const RimWellEventTimeline& timeline, + const RimWellPath& well, + const QDateTime& date, + std::map& keywordBlocks ); - // Extract keyword name from the first non-comment line of a keyword block - static QString extractKeywordName( const QString& block ); + // Append records of `kw` into the entry for `name`, creating it from `kw` if absent + static void mergeKeyword( std::map& acc, const QString& name, Opm::DeckKeyword kw ); }; diff --git a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp index a8e3ebb1f5..8897458d42 100644 --- a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp +++ b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp @@ -33,15 +33,33 @@ #include "opm/input/eclipse/Deck/DeckOutput.hpp" #include "opm/input/eclipse/Deck/DeckRecord.hpp" #include "opm/input/eclipse/Parser/Parser.hpp" +#include "opm/input/eclipse/Parser/ParserItem.hpp" #include "opm/input/eclipse/Parser/ParserKeyword.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/W.hpp" +#include "opm/input/eclipse/Parser/ParserRecord.hpp" +#include #include +#include +#include + +namespace +{ +QString keywordToString( const Opm::DeckKeyword& kw ) +{ + if ( kw.name().empty() ) return {}; + std::ostringstream oss; + Opm::DeckOutput out( oss, 10 ); + kw.write( out ); + return QString::fromStdString( oss.str() ); +} +} // namespace //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RifEventKeywordFormatter::formatKeyword( const QString& keywordName, const std::vector& items ) +std::optional RifEventKeywordFormatter::buildKeyword( const QString& keywordName, + const std::vector& items ) { QString keyword = keywordName.toUpper(); std::string kwName = keyword.toStdString(); @@ -52,52 +70,148 @@ QString RifEventKeywordFormatter::formatKeyword( const QString& keywordName, con const Opm::ParserKeyword& parserKw = parser.getKeyword( kwName ); Opm::DeckKeyword kw( parserKw ); - // Build deck items from stored data - std::vector deckItems; - for ( const auto* item : items ) + const Opm::ParserRecord& parserRecord = parserKw.getRecord( 0 ); + + auto stringValue = []( const RimWellEventKeywordItem* item ) -> std::string { switch ( item->itemType() ) { case RimWellEventKeywordItem::ItemType::STRING: - deckItems.push_back( RifOpmDeckTools::item( item->itemName().toStdString(), item->stringValue().toStdString() ) ); - break; + return item->stringValue().toStdString(); case RimWellEventKeywordItem::ItemType::INTEGER: - deckItems.push_back( RifOpmDeckTools::item( item->itemName().toStdString(), item->intValue() ) ); - break; + return std::to_string( item->intValue() ); case RimWellEventKeywordItem::ItemType::DOUBLE: - deckItems.push_back( RifOpmDeckTools::item( item->itemName().toStdString(), item->doubleValue() ) ); - break; + return std::to_string( item->doubleValue() ); + case RimWellEventKeywordItem::ItemType::FLAG: + return ""; } - } + return ""; + }; - if ( !deckItems.empty() ) + // RPTRST / RPTSCHED-style keywords have a single ALL-sized item that holds a free-form + // list of mnemonics ("BASIC=2 DEN ROCKC ..."). Emit each user-supplied key either as + // "KEY=VALUE" (typed value) or bare "KEY" (FLAG), all packed into one DeckItem. + const bool isMnemonicList = parserRecord.size() == 1 && parserRecord.get( 0 ).sizeType() == Opm::ParserItem::item_size::ALL; + + if ( isMnemonicList ) { - kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } ); - } + std::vector tokens; + tokens.reserve( items.size() ); + for ( const auto* item : items ) + { + const std::string name = item->itemName().toStdString(); + if ( item->itemType() == RimWellEventKeywordItem::ItemType::FLAG ) + { + tokens.push_back( name ); + } + else + { + tokens.push_back( name + "=" + stringValue( item ) ); + } + } - // Return empty string if keyword creation failed - if ( kw.name().empty() ) + if ( !tokens.empty() ) + { + std::vector deckItems; + deckItems.push_back( RifOpmDeckTools::rawStringItem( parserRecord.get( 0 ).name(), std::move( tokens ) ) ); + kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } ); + } + } + else { - return ""; + // Positional keyword (WCONHIST, WELTARG, ...): emit items in the schema-defined order + // regardless of caller-supplied order (e.g. Python dict insertion order). + std::unordered_map userItemsByName; + userItemsByName.reserve( items.size() ); + for ( const auto* item : items ) + { + userItemsByName.emplace( item->itemName().toStdString(), item ); + } + + // Highest canonical index actually provided by the user. Trailing unspecified items are + // dropped, intermediate gaps become default markers ("1*"). + std::optional lastProvidedIdx; + std::unordered_set canonicalNames; + canonicalNames.reserve( parserRecord.size() ); + for ( size_t i = 0; i < parserRecord.size(); ++i ) + { + const std::string& name = parserRecord.get( i ).name(); + canonicalNames.insert( name ); + if ( userItemsByName.contains( name ) ) + { + lastProvidedIdx = i; + } + } + + auto appendDeckItem = [&]( std::vector& out, const std::string& name, const RimWellEventKeywordItem* item ) + { + switch ( item->itemType() ) + { + case RimWellEventKeywordItem::ItemType::STRING: + out.push_back( RifOpmDeckTools::item( name, item->stringValue().toStdString() ) ); + break; + case RimWellEventKeywordItem::ItemType::INTEGER: + out.push_back( RifOpmDeckTools::item( name, item->intValue() ) ); + break; + case RimWellEventKeywordItem::ItemType::DOUBLE: + out.push_back( RifOpmDeckTools::item( name, item->doubleValue() ) ); + break; + case RimWellEventKeywordItem::ItemType::FLAG: + // FLAG only makes sense for mnemonic-list keywords; for positional schemas + // there is no meaningful value to emit, so render as a default marker. + out.push_back( RifOpmDeckTools::defaultItem( name ) ); + break; + } + }; + + std::vector deckItems; + if ( lastProvidedIdx.has_value() ) + { + deckItems.reserve( *lastProvidedIdx + 1 ); + for ( size_t i = 0; i <= *lastProvidedIdx; ++i ) + { + const std::string& name = parserRecord.get( i ).name(); + auto it = userItemsByName.find( name ); + if ( it == userItemsByName.end() ) + { + deckItems.push_back( RifOpmDeckTools::defaultItem( name ) ); + } + else + { + appendDeckItem( deckItems, name, it->second ); + } + } + } + + // Items not in the keyword's schema are still emitted in caller-supplied order + // after the canonical block. + for ( const auto* item : items ) + { + const std::string name = item->itemName().toStdString(); + if ( canonicalNames.contains( name ) ) continue; + appendDeckItem( deckItems, name, item ); + } + + if ( !deckItems.empty() ) + { + kw.addRecord( Opm::DeckRecord{ std::move( deckItems ) } ); + } } - // Format with OPM DeckOutput - std::ostringstream oss; - Opm::DeckOutput out( oss, 10 ); - kw.write( out ); - return QString::fromStdString( oss.str() ); + if ( kw.name().empty() ) return std::nullopt; + return kw; } catch ( const std::exception& e ) { RiaLogging::error( std::format( "Failed to create keyword '{}': {}", keyword, e.what() ) ); - return ""; + return std::nullopt; } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RifEventKeywordFormatter::formatWconprod( const RimWellEventControl* controlEvent, const QString& wellName ) +std::optional RifEventKeywordFormatter::buildWconprod( const RimWellEventControl* controlEvent, const QString& wellName ) { using W = Opm::ParserKeywords::WCONPROD; @@ -133,17 +247,13 @@ QString RifEventKeywordFormatter::formatWconprod( const RimWellEventControl* con Opm::DeckKeyword kw{ W() }; kw.addRecord( Opm::DeckRecord{ std::move( items ) } ); - - std::ostringstream oss; - Opm::DeckOutput out( oss, 10 ); - kw.write( out ); - return QString::fromStdString( oss.str() ); + return kw; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RifEventKeywordFormatter::formatWconinje( const RimWellEventControl* controlEvent, const QString& wellName ) +std::optional RifEventKeywordFormatter::buildWconinje( const RimWellEventControl* controlEvent, const QString& wellName ) { using W = Opm::ParserKeywords::WCONINJE; @@ -171,41 +281,57 @@ QString RifEventKeywordFormatter::formatWconinje( const RimWellEventControl* con Opm::DeckKeyword kw{ W() }; kw.addRecord( Opm::DeckRecord{ std::move( items ) } ); - - std::ostringstream oss; - Opm::DeckOutput out( oss, 10 ); - kw.write( out ); - return QString::fromStdString( oss.str() ); + return kw; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -QString RifEventKeywordFormatter::formatWellEvent( const RimWellEvent* event, const QString& wellName ) +std::optional RifEventKeywordFormatter::buildWellEvent( const RimWellEvent* event, const QString& wellName ) { if ( event->eventType() == RimWellEvent::EventType::WCONTROL ) { - auto* controlEvent = dynamic_cast( event ); + const auto* controlEvent = dynamic_cast( event ); if ( controlEvent ) { - if ( controlEvent->isProducer() ) - { - return formatWconprod( controlEvent, wellName ); - } - else - { - return formatWconinje( controlEvent, wellName ); - } + return controlEvent->isProducer() ? buildWconprod( controlEvent, wellName ) : buildWconinje( controlEvent, wellName ); } } else if ( event->eventType() == RimWellEvent::EventType::KEYWORD ) { - auto* keywordEvent = dynamic_cast( event ); + const auto* keywordEvent = dynamic_cast( event ); if ( keywordEvent ) { - return formatKeyword( keywordEvent->keywordName(), keywordEvent->items() ); + return buildKeyword( keywordEvent->keywordName(), keywordEvent->items() ); } } - return {}; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// QString-returning wrappers: build the keyword and serialize once. +//-------------------------------------------------------------------------------------------------- +QString RifEventKeywordFormatter::formatKeyword( const QString& keywordName, const std::vector& items ) +{ + auto kw = buildKeyword( keywordName, items ); + return kw ? keywordToString( *kw ) : QString(); +} + +QString RifEventKeywordFormatter::formatWconprod( const RimWellEventControl* controlEvent, const QString& wellName ) +{ + auto kw = buildWconprod( controlEvent, wellName ); + return kw ? keywordToString( *kw ) : QString(); +} + +QString RifEventKeywordFormatter::formatWconinje( const RimWellEventControl* controlEvent, const QString& wellName ) +{ + auto kw = buildWconinje( controlEvent, wellName ); + return kw ? keywordToString( *kw ) : QString(); +} + +QString RifEventKeywordFormatter::formatWellEvent( const RimWellEvent* event, const QString& wellName ) +{ + auto kw = buildWellEvent( event, wellName ); + return kw ? keywordToString( *kw ) : QString(); } diff --git a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h index d33e8bc0d3..6185f97d6c 100644 --- a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h +++ b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h @@ -19,14 +19,27 @@ #pragma once #include +#include #include +namespace Opm +{ +class DeckKeyword; +} // namespace Opm + class RimWellEventControl; class RimWellEvent; class RimWellEventKeywordItem; namespace RifEventKeywordFormatter { +// build*: produce a structured Opm::DeckKeyword (used by callers that merge records across wells). +std::optional buildKeyword( const QString& keywordName, const std::vector& items ); +std::optional buildWconprod( const RimWellEventControl* controlEvent, const QString& wellName ); +std::optional buildWconinje( const RimWellEventControl* controlEvent, const QString& wellName ); +std::optional buildWellEvent( const RimWellEvent* event, const QString& wellName ); + +// format*: serialize the build* result to text. Convenience for single-keyword stringification. QString formatKeyword( const QString& keywordName, const std::vector& items ); QString formatWconprod( const RimWellEventControl* controlEvent, const QString& wellName ); QString formatWconinje( const RimWellEventControl* controlEvent, const QString& wellName ); diff --git a/ApplicationLibCode/FileInterface/RifOpmDeckTools.cpp b/ApplicationLibCode/FileInterface/RifOpmDeckTools.cpp index 6600f17455..6c29999c27 100644 --- a/ApplicationLibCode/FileInterface/RifOpmDeckTools.cpp +++ b/ApplicationLibCode/FileInterface/RifOpmDeckTools.cpp @@ -20,6 +20,7 @@ #include "opm/input/eclipse/Deck/DeckItem.hpp" #include "opm/input/eclipse/Units/Dimension.hpp" +#include "opm/input/eclipse/Utility/Typetools.hpp" namespace RifOpmDeckTools { @@ -157,4 +158,16 @@ Opm::DeckItem item( std::string name, std::vector values ) return item1; } +//-------------------------------------------------------------------------------------------------- +/// Raw-string DeckItem: emitted by OPM DeckOutput without surrounding quotes. +/// Used for free-form mnemonic lists (e.g. RPTRST tokens like "BASIC=2", "DEN"). +//-------------------------------------------------------------------------------------------------- +Opm::DeckItem rawStringItem( std::string name, std::vector values ) +{ + Opm::DeckItem item1( name, Opm::RawString{} ); + for ( const auto& value : values ) + item1.push_back( Opm::RawString{ value } ); + return item1; +} + } // namespace RifOpmDeckTools diff --git a/ApplicationLibCode/FileInterface/RifOpmDeckTools.h b/ApplicationLibCode/FileInterface/RifOpmDeckTools.h index f27cd6c785..dda8993634 100644 --- a/ApplicationLibCode/FileInterface/RifOpmDeckTools.h +++ b/ApplicationLibCode/FileInterface/RifOpmDeckTools.h @@ -42,5 +42,6 @@ Opm::DeckItem optionalItem( std::string name, std::optional value ); Opm::DeckItem defaultItem( std::string name ); Opm::DeckItem item( std::string name, std::set values ); Opm::DeckItem item( std::string name, std::vector values ); +Opm::DeckItem rawStringItem( std::string name, std::vector values ); } // namespace RifOpmDeckTools diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.cpp index 2700ae0688..899834c1f2 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.cpp @@ -22,6 +22,8 @@ #include "FileInterface/RifEventKeywordFormatter.h" +#include "opm/input/eclipse/Deck/DeckKeyword.hpp" + #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiOrdering.h" @@ -98,6 +100,17 @@ void RimKeywordEvent::addDoubleItem( const QString& name, double value ) m_items.push_back( item ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimKeywordEvent::addFlagItem( const QString& name ) +{ + auto* item = new RimWellEventKeywordItem(); + item->setItemName( name ); + item->setFlag(); + m_items.push_back( item ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -122,6 +135,14 @@ QString RimKeywordEvent::generateScheduleKeyword( const QString& wellName ) cons return RifEventKeywordFormatter::formatKeyword( m_keywordName(), items() ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimKeywordEvent::generateDeckKeyword( const QString& wellName ) const +{ + return RifEventKeywordFormatter::buildKeyword( m_keywordName(), items() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.h index 0b3531184f..759a03b777 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimKeywordEvent.h @@ -22,8 +22,14 @@ #include "cafPdmField.h" +#include #include +namespace Opm +{ +class DeckKeyword; +} // namespace Opm + //================================================================================================== /// /// Schedule-level keyword event - represents a schedule keyword with arbitrary keyword items @@ -46,12 +52,14 @@ class RimKeywordEvent : public RimWellEvent void addStringItem( const QString& name, const QString& value ); void addIntItem( const QString& name, int value ); void addDoubleItem( const QString& name, double value ); + void addFlagItem( const QString& name ); std::vector items() const; // Override from RimWellEvent - EventType eventType() const override { return EventType::SCHEDULE_KEYWORD; } - QString generateScheduleKeyword( const QString& wellName ) const override; + EventType eventType() const override { return EventType::SCHEDULE_KEYWORD; } + QString generateScheduleKeyword( const QString& wellName ) const override; + std::optional generateDeckKeyword( const QString& wellName ) const; protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.cpp index 9edf353212..3c0e727f4d 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.cpp @@ -22,6 +22,8 @@ #include "FileInterface/RifEventKeywordFormatter.h" +#include "opm/input/eclipse/Deck/DeckKeyword.hpp" + #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiOrdering.h" @@ -98,6 +100,17 @@ void RimWellEventKeyword::addDoubleItem( const QString& name, double value ) m_items.push_back( item ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventKeyword::addFlagItem( const QString& name ) +{ + auto* item = new RimWellEventKeywordItem(); + item->setItemName( name ); + item->setFlag(); + m_items.push_back( item ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -122,6 +135,14 @@ QString RimWellEventKeyword::generateScheduleKeyword( const QString& wellName ) return RifEventKeywordFormatter::formatKeyword( m_keywordName(), items() ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimWellEventKeyword::generateDeckKeyword( const QString& wellName ) const +{ + return RifEventKeywordFormatter::buildKeyword( m_keywordName(), items() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.h index 23a3edd46d..ced96a84ab 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeyword.h @@ -22,8 +22,14 @@ #include "cafPdmField.h" +#include #include +namespace Opm +{ +class DeckKeyword; +} // namespace Opm + //================================================================================================== /// /// Well keyword event - represents a schedule keyword with arbitrary keyword items @@ -45,12 +51,14 @@ class RimWellEventKeyword : public RimWellEvent void addStringItem( const QString& name, const QString& value ); void addIntItem( const QString& name, int value ); void addDoubleItem( const QString& name, double value ); + void addFlagItem( const QString& name ); std::vector items() const; // Override from RimWellEvent - EventType eventType() const override { return EventType::KEYWORD; } - QString generateScheduleKeyword( const QString& wellName ) const override; + EventType eventType() const override { return EventType::KEYWORD; } + QString generateScheduleKeyword( const QString& wellName ) const override; + std::optional generateDeckKeyword( const QString& wellName ) const; protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.cpp index 21b63b183b..1d96470aeb 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.cpp @@ -116,6 +116,14 @@ void RimWellEventKeywordItem::setDoubleValue( double value ) m_doubleValue = value; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventKeywordItem::setFlag() +{ + m_itemType = ItemType::FLAG; +} + namespace caf { template <> @@ -124,6 +132,7 @@ void AppEnum::setUp() addItem( RimWellEventKeywordItem::ItemType::STRING, "STRING", "String" ); addItem( RimWellEventKeywordItem::ItemType::INTEGER, "INTEGER", "Integer" ); addItem( RimWellEventKeywordItem::ItemType::DOUBLE, "DOUBLE", "Double" ); + addItem( RimWellEventKeywordItem::ItemType::FLAG, "FLAG", "Flag" ); setDefault( RimWellEventKeywordItem::ItemType::STRING ); } } // namespace caf diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.h index 627bcd42d7..7d1426512f 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventKeywordItem.h @@ -38,7 +38,8 @@ class RimWellEventKeywordItem : public caf::PdmObject { STRING, INTEGER, - DOUBLE + DOUBLE, + FLAG }; RimWellEventKeywordItem(); @@ -56,6 +57,7 @@ class RimWellEventKeywordItem : public caf::PdmObject void setStringValue( const QString& value ); void setIntValue( int value ); void setDoubleValue( double value ); + void setFlag(); private: caf::PdmField m_itemName; diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index 690863aec4..c7fee530f1 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -21,7 +21,6 @@ #include "CompletionExportCommands/RicScheduleDataGenerator.h" #include "RimEclipseCase.h" #include "RimKeywordEvent.h" -#include "RimProject.h" #include "RimWellEventControl.h" #include "RimWellEventKeyword.h" #include "RimWellEventPerf.h" @@ -30,7 +29,6 @@ #include "RimWellEventTubing.h" #include "RimWellEventValve.h" #include "RimWellPath.h" -#include "RimWellPathCollection.h" #include "RimcDataContainerString.h" #include "cafAppEnum.h" @@ -406,9 +404,13 @@ std::expected RimcWellEventTimeline_addWellKeywo } event->addDoubleItem( name, doubleValue ); } + else if ( type == "FLAG" ) + { + event->addFlagItem( name ); + } else { - return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, or DOUBLE" ).arg( type ) ); + return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, DOUBLE, or FLAG" ).arg( type ) ); } } @@ -498,9 +500,13 @@ std::expected RimcWellEventTimeline_addKeywordEv } event->addDoubleItem( name, doubleValue ); } + else if ( type == "FLAG" ) + { + event->addFlagItem( name ); + } else { - return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, or DOUBLE" ).arg( type ) ); + return std::unexpected( QString( "Unknown item type: %1. Must be STRING, INT, DOUBLE, or FLAG" ).arg( type ) ); } } @@ -561,7 +567,9 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule( { CAF_PDM_InitObject( "Generate Schedule", "", "", "Generate Eclipse schedule text for all wells in the collection" ); - CAF_PDM_InitScriptableField( &m_eclipseCaseId, "EclipseCaseId", -1, "", "", "", "Eclipse Case ID" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_eclipseCase, "EclipseCase", "", "", "", "Eclipse Case" ); + CAF_PDM_InitScriptableField( &m_includeWelsegs, "IncludeWelsegs", true, "", "", "", "Include WELSEGS keyword in the exported schedule" ); + CAF_PDM_InitScriptableField( &m_includeCompsegs, "IncludeCompsegs", true, "", "", "", "Include COMPSEGS keyword in the exported schedule" ); } //-------------------------------------------------------------------------------------------------- @@ -571,24 +579,10 @@ std::expected RimcWellEventTimeline_generateSche { auto timeline = self(); - // Get the parent well path collection - RimWellPathCollection* wellPathCollection = timeline->firstAncestorOrThisOfType(); - if ( !wellPathCollection ) - { - return std::unexpected( QString( "Could not find parent well path collection" ) ); - } - - // Find the Eclipse case by ID - RimProject* project = wellPathCollection->firstAncestorOrThisOfType(); - if ( !project ) - { - return std::unexpected( QString( "Could not find project" ) ); - } - - RimEclipseCase* eclipseCase = project->eclipseCaseFromCaseId( m_eclipseCaseId() ); + RimEclipseCase* eclipseCase = m_eclipseCase(); if ( !eclipseCase ) { - return std::unexpected( QString( "Eclipse case with ID %1 not found" ).arg( m_eclipseCaseId() ) ); + return std::unexpected( QString( "Eclipse case is required" ) ); } // Get the last applied timestamp (from set_timestamp call) @@ -625,7 +619,8 @@ std::expected RimcWellEventTimeline_generateSche return std::unexpected( QString( "No well paths with events found" ) ); } - QString scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates ); + QString scheduleText = + RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, m_includeWelsegs(), m_includeCompsegs() ); // Return the schedule text in a data container auto* dataObject = new RimcDataContainerString(); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h index 4db2cfa781..bff6e758b1 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -225,5 +225,7 @@ class RimcWellEventTimeline_generateSchedule : public caf::PdmObjectMethod QString classKeywordReturnedType() const override; private: - caf::PdmField m_eclipseCaseId; + caf::PdmPtrField m_eclipseCase; + caf::PdmField m_includeWelsegs; + caf::PdmField m_includeCompsegs; }; diff --git a/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index b2495d4a20..cc5f597575 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -1231,6 +1231,102 @@ def test_schedule_multiple_dates_in_order(self, project_with_case_and_well): assert jan_pos < feb_pos, "January should come before February" assert feb_pos < jun_pos, "February should come before June" + def test_welsegs_compsegs_optional(self, project_with_case_and_well): + """Regression for #14064: callers can suppress WELSEGS and/or COMPSEGS + without affecting other keywords (WSEGVALV/WSEGAICD/COMPDAT/WELSPECS). + """ + project, case, timeline = project_with_case_and_well + well_paths = project.well_paths() + assert len(well_paths) >= 1 + + wp = well_paths[0] + timeline.add_tubing_event( + event_date="2024-01-01", + well_path=wp, + start_md=0.0, + end_md=2500.0, + inner_diameter=0.15, + roughness=1.0e-5, + ) + timeline.add_perf_event( + event_date="2024-01-01", + well_path=wp, + start_md=2000.0, + end_md=2200.0, + diameter=0.1, + state="OPEN", + ) + timeline.set_timestamp(timestamp="2024-12-31") + + # Baseline (defaults) must still emit WELSEGS and COMPSEGS. + baseline = timeline.generate_schedule_text(eclipse_case=case) + assert "WELSEGS" in baseline + assert "COMPSEGS" in baseline + + # Suppress both. + suppressed = timeline.generate_schedule_text( + eclipse_case=case, include_welsegs=False, include_compsegs=False + ) + print(f"\nSchedule with WELSEGS/COMPSEGS suppressed:\n{suppressed}") + assert "WELSEGS" not in suppressed, ( + f"WELSEGS should be suppressed:\n{suppressed}" + ) + assert "COMPSEGS" not in suppressed, ( + f"COMPSEGS should be suppressed:\n{suppressed}" + ) + # COMPDAT (perforation completions) is unrelated to the MSW flags. + assert "COMPDAT" in suppressed + + # Independent toggling. + only_compsegs_off = timeline.generate_schedule_text( + eclipse_case=case, include_compsegs=False + ) + assert "WELSEGS" in only_compsegs_off + assert "COMPSEGS" not in only_compsegs_off + + only_welsegs_off = timeline.generate_schedule_text( + eclipse_case=case, include_welsegs=False + ) + assert "WELSEGS" not in only_welsegs_off + assert "COMPSEGS" in only_welsegs_off + + def test_keywords_grouped_across_wells(self, project_with_case_and_well): + """Regression for #14063: WELSPECS / COMPDAT records for multiple wells on + the same date must appear under a single keyword header (not one block per well). + """ + project, case, timeline = project_with_case_and_well + well_paths = project.well_paths() + assert len(well_paths) >= 2, ( + "Test requires at least two well paths in the fixture" + ) + + for wp in well_paths[:2]: + timeline.add_perf_event( + event_date="2024-01-01", + well_path=wp, + start_md=2000.0, + end_md=2200.0, + diameter=0.1, + state="OPEN", + ) + + timeline.set_timestamp(timestamp="2024-12-31") + schedule_text = timeline.generate_schedule_text(eclipse_case=case) + print(f"\nSchedule text for multi-well grouping:\n{schedule_text}") + + # Exactly one "WELSPECS\n" header for all wells. + welspecs_count = schedule_text.count("WELSPECS\n") + assert welspecs_count == 1, ( + f"WELSPECS should appear once for grouped output; got {welspecs_count}:\n{schedule_text}" + ) + + # Both well names must appear inside the WELSPECS block (between header and trailing '/' line). + welspecs_block = schedule_text.split("WELSPECS\n", 1)[1].split("\n/\n", 1)[0] + for wp in well_paths[:2]: + assert wp.name.replace(" ", "") in welspecs_block.replace(" ", ""), ( + f"Well {wp.name!r} missing from grouped WELSPECS block: {welspecs_block!r}" + ) + class TestKeywordEvents: """Tests for well keyword event functionality.""" @@ -1428,6 +1524,53 @@ def test_keyword_event_schedule_output_multiple_keywords( assert "WRFTPLT" in schedule_text, "Schedule should contain WRFTPLT keyword" assert "DATES" in schedule_text, "Schedule should contain DATES keyword" + def test_wconhist_item_order_canonical(self, project_with_case_and_well): + """Regression for #14065: WCONHIST items must appear in the Eclipse-defined + canonical order regardless of how keyword_data was constructed in Python. + + Canonical WCONHIST order: WELL, STATUS, CMODE, ORAT, WRAT, GRAT, VFP_TABLE, + ALQ, THP, BHP, WGASRAT_HIS, NGLRAT_HIS. + """ + project, case, timeline = project_with_case_and_well + well_path = project.well_paths()[0] + + # Intentionally non-canonical insertion order: BHP placed before ORAT/WRAT/GRAT. + timeline.add_well_keyword_event( + event_date="2024-01-15", + well_path=well_path, + keyword_name="WCONHIST", + keyword_data={ + "WELL": well_path.name, + "STATUS": "OPEN", + "CMODE": "RESV", + "BHP": 250.0, + "ORAT": 3999.99, + "WRAT": 0.01, + "GRAT": 550678.44, + "VFP_TABLE": 1, + }, + ) + + schedule_text = timeline.generate_schedule_text(eclipse_case=case) + print(f"\nSchedule text for canonical-order check:\n{schedule_text}") + + assert "WCONHIST" in schedule_text + # Extract the WCONHIST record body (between the keyword and its terminating '/'). + wconhist_block = schedule_text.split("WCONHIST", 1)[1].split("/", 1)[0] + + orat_pos = wconhist_block.find("3999.99") + grat_pos = wconhist_block.find("550678") + bhp_pos = wconhist_block.find("250") + assert orat_pos >= 0, "ORAT value missing from WCONHIST output" + assert grat_pos >= 0, "GRAT value missing from WCONHIST output" + assert bhp_pos >= 0, "BHP value missing from WCONHIST output" + assert orat_pos < bhp_pos, ( + f"ORAT must precede BHP in canonical WCONHIST order; got block: {wconhist_block!r}" + ) + assert grat_pos < bhp_pos, ( + f"GRAT must precede BHP in canonical WCONHIST order; got block: {wconhist_block!r}" + ) + def test_invalid_keyword_data_unsupported_type(self, project_with_case_and_well): """Test error handling for unsupported data types in keyword events.""" project, case, timeline = project_with_case_and_well @@ -1848,3 +1991,55 @@ def test_keyword_event_type_inference(self, project_with_case_and_well): ) assert event is not None, "Event with mixed types should be created" + + def test_rptrst_mnemonic_output(self, project_with_case_and_well): + """RPTRST/RPTSCHED are mnemonic-list keywords. bool True must emit a + bare KEY, int/float/str must emit KEY=VALUE, bool False must be omitted. + """ + project, case, timeline = project_with_case_and_well + well_path = project.well_paths()[0] + + # Schedule generation requires at least one well event so a well path is + # selected for output; the assertions below target only the RPTRST block. + timeline.add_control_event( + event_date="2024-01-01", + well_path=well_path, + control_mode="ORAT", + control_value=1000.0, + oil_rate=1000.0, + is_producer=True, + ) + + timeline.add_keyword_event( + event_date="2024-01-01", + keyword_name="RPTRST", + keyword_data={ + "BASIC": 2, + "DEN": True, + "ROCKC": True, + "RPORV": True, + "RFIP": True, + "FLOWS": True, + "NORST": 1, + "FLORES": True, + "OBSOLETE": False, + }, + ) + + schedule_text = timeline.generate_schedule_text(eclipse_case=case) + print(f"\nRPTRST mnemonic output:\n{schedule_text}") + + assert "RPTRST" in schedule_text + rptrst_block = schedule_text.split("RPTRST", 1)[1].split("/", 1)[0] + + # Keyed mnemonics rendered as KEY=VALUE. + assert "BASIC=2" in rptrst_block + assert "NORST=1" in rptrst_block + # Flag mnemonics rendered as bare tokens. Whitespace-bounded so we don't accept + # accidental substring matches like 'DEN' inside another token. + for flag in ("DEN", "ROCKC", "RPORV", "RFIP", "FLOWS", "FLORES"): + assert f" {flag} " in rptrst_block or rptrst_block.rstrip().endswith( + f" {flag}" + ), f"flag {flag!r} missing from RPTRST output: {rptrst_block!r}" + # False-valued flag must be omitted entirely. + assert "OBSOLETE" not in rptrst_block diff --git a/GrpcInterface/Python/rips/well_events.py b/GrpcInterface/Python/rips/well_events.py index 863b737e5e..68ff0f6b77 100644 --- a/GrpcInterface/Python/rips/well_events.py +++ b/GrpcInterface/Python/rips/well_events.py @@ -10,7 +10,7 @@ from datetime import date, datetime from .pdmobject import add_method -from .resinsight_classes import Case +from .resinsight_classes import EclipseCase from .generated.generated_classes import ( KeywordEvent, WellEventKeyword, @@ -121,15 +121,24 @@ def add_well_keyword_event( item_values = [] for name, value in keyword_data.items(): + # Handle bool before int (bool is subclass of int in Python). + # bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords, + # ignored elsewhere); False -> the entry is dropped entirely. + if isinstance(value, bool): + if not value: + continue + item_names.append(name) + item_types.append("FLAG") + # The value is ignored for FLAG items but must be a non-empty string; + # empty strings get dropped by the GRPC vector serialization. + item_values.append("1") + continue + item_names.append(name) if isinstance(value, str): item_types.append("STRING") item_values.append(value) - elif isinstance(value, bool): - # Handle bool before int (bool is subclass of int in Python) - item_types.append("INT") - item_values.append("1" if value else "0") elif isinstance(value, int): item_types.append("INT") item_values.append(str(value)) @@ -228,15 +237,24 @@ def add_keyword_event( item_values = [] for name, value in keyword_data.items(): + # Handle bool before int (bool is subclass of int in Python). + # bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords, + # ignored elsewhere); False -> the entry is dropped entirely. + if isinstance(value, bool): + if not value: + continue + item_names.append(name) + item_types.append("FLAG") + # The value is ignored for FLAG items but must be a non-empty string; + # empty strings get dropped by the GRPC vector serialization. + item_values.append("1") + continue + item_names.append(name) if isinstance(value, str): item_types.append("STRING") item_values.append(value) - elif isinstance(value, bool): - # Handle bool before int (bool is subclass of int in Python) - item_types.append("INT") - item_values.append("1" if value else "0") elif isinstance(value, int): item_types.append("INT") item_values.append(str(value)) @@ -263,7 +281,12 @@ def add_keyword_event( @add_method(WellEventTimeline) -def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str: +def generate_schedule_text( + self: WellEventTimeline, + eclipse_case: EclipseCase, + include_welsegs: bool = True, + include_compsegs: bool = True, +) -> str: """Generate Eclipse schedule text for all wells in the collection. The timeline is shared across all wells in the well path collection. @@ -274,7 +297,13 @@ def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str: text directly instead of a DataContainerString. Arguments: - eclipse_case (Case): Eclipse case to use for schedule generation. + eclipse_case (EclipseCase): Eclipse case to use for schedule generation. + include_welsegs (bool): When False, omit the WELSEGS keyword from the + output. Other multi-segment-well keywords (COMPSEGS, WSEGVALV, + WSEGAICD) are not affected. Defaults to True. + include_compsegs (bool): When False, omit the COMPSEGS keyword from + the output. WELSEGS, WSEGVALV, WSEGAICD are not affected. + Defaults to True. Returns: str: Eclipse schedule text containing DATES, COMPDAT, WELSEGS, WCONPROD, etc. @@ -312,7 +341,11 @@ def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str: print(schedule_text) ``` """ - container = self.generate_schedule(eclipse_case_id=eclipse_case.id) + container = self.generate_schedule( + eclipse_case=eclipse_case, + include_welsegs=include_welsegs, + include_compsegs=include_compsegs, + ) if container and container.values: return "".join(container.values) return ""