diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 4957ced810..f69d5fcc4d 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -45,6 +45,14 @@ #include #include +namespace +{ +bool shouldEmitKeyword( const Opm::DeckKeyword& keyword ) +{ + return keyword.size() > 0 || keyword.isDataKeyword() || RifEventKeywordFormatter::isRecordlessKeyword( keyword ); +} +} // namespace + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -146,7 +154,7 @@ std::vector RicScheduleDataGenerator::collectAllDates( const RimWellE //-------------------------------------------------------------------------------------------------- void RicScheduleDataGenerator::mergeKeyword( std::map& acc, const QString& name, Opm::DeckKeyword kw ) { - if ( kw.size() == 0 && !kw.isDataKeyword() ) return; + if ( !shouldEmitKeyword( kw ) ) return; auto it = acc.find( name ); if ( it == acc.end() ) @@ -172,7 +180,8 @@ std::expected RicScheduleDataGenerator::generateDateSection( c bool alignColumns ) { // Keyword priority order for output - static const std::vector keywordOrder = { "WELSPECS", + static const std::vector keywordOrder = { "END", + "WELSPECS", "COMPORD", "GRUPTREE", "COMPDAT", @@ -333,7 +342,7 @@ std::expected RicScheduleDataGenerator::generateDateSection( c auto appendKeywordText = [&]( const Opm::DeckKeyword& kw ) { - if ( kw.size() == 0 && !kw.isDataKeyword() ) return; + if ( !shouldEmitKeyword( kw ) ) return; appendEventComments( QString::fromStdString( kw.name() ) ); result += serializeKeyword( kw ); result += "\n"; diff --git a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp index 308f05df92..7cac27cf57 100644 --- a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp +++ b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.cpp @@ -56,6 +56,23 @@ QString keywordToString( const Opm::DeckKeyword& kw ) } } // namespace +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RifEventKeywordFormatter::isRecordlessKeyword( const Opm::DeckKeyword& keyword ) +{ + try + { + static const Opm::Parser parser; + const auto& parserKeyword = parser.getKeyword( keyword.name() ); + return parserKeyword.begin() == parserKeyword.end(); + } + catch ( const std::exception& ) + { + return false; + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -71,7 +88,17 @@ std::optional RifEventKeywordFormatter::buildKeyword( const QS const Opm::ParserKeyword& parserKw = parser.getKeyword( kwName ); Opm::DeckKeyword kw( parserKw ); - const size_t numRecords = static_cast( std::distance( parserKw.begin(), parserKw.end() ) ); + const size_t numRecords = static_cast( std::distance( parserKw.begin(), parserKw.end() ) ); + if ( numRecords == 0 ) + { + if ( !items.empty() ) + { + RiaLogging::error( std::format( "Recordless keyword '{}' does not accept items.", keyword ) ); + return std::nullopt; + } + return kw; + } + const Opm::ParserRecord& parserRecord = parserKw.getRecord( 0 ); auto stringValue = []( const RimWellEventKeywordItem* item ) -> std::string diff --git a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h index 6185f97d6c..20e21968ef 100644 --- a/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h +++ b/ApplicationLibCode/FileInterface/RifEventKeywordFormatter.h @@ -33,6 +33,8 @@ class RimWellEventKeywordItem; namespace RifEventKeywordFormatter { +bool isRecordlessKeyword( const Opm::DeckKeyword& keyword ); + // 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 ); diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index 03c9f09c46..b4cec53b42 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -2169,6 +2169,26 @@ class TestOrionEventsIntegration: assert "1 'FEB' 2024" in schedule assert "1 'JUN' 2024" in schedule + def test_end_is_first_keyword_after_date(self, project_with_case_and_wells): + project, case, timeline = project_with_case_and_wells + document = parse_orion_events( + "ORIONEVENTS 2.0\nSCHEDULE\n" + "2024-01-01 RPTRST BASIC=2 FREQ=1\n" + "2024-01-01 END\n" + ) + + report = apply_orion_document(document, timeline, project) + assert report.errors == [] + schedule = timeline.generate_schedule_text( + eclipse_case=case, + first_date_as_comment=False, + ) + + date_position = schedule.index("1 'JAN' 2024") + end_position = schedule.index("\nEND\n", date_position) + rptrst_position = schedule.index("\nRPTRST\n", date_position) + assert date_position < end_position < rptrst_position + def test_apply_creates_perforations_and_schedule(self, project_with_case_and_wells): """End-to-end: parse -> apply -> set_timestamp -> generate schedule.""" project, case, timeline = project_with_case_and_wells