From cb9274ecd6f48fc9df23deda130977f361e86056 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Mon, 1 Jun 2026 09:07:05 +0200 Subject: [PATCH] Python: Add first_date_as_comment option to generate_schedule_text Some commercial simulators reject a DATES keyword whose value equals the simulation start date. The earliest event date is, by construction, that start date, so generate_schedule_text now emits it as a comment line (e.g. "-- Date: 1 JAN 2024") instead of a DATES keyword by default. The new first_date_as_comment parameter (default True) threads from the Python wrapper through the GenerateSchedule scriptable method into RicScheduleDataGenerator. Only the first/earliest date is affected; later dates are always emitted as DATES keywords. Pass first_date_as_comment=False to restore the previous behavior. --- .../RicScheduleDataGenerator.cpp | 29 +++++-- .../RicScheduleDataGenerator.h | 12 ++- .../RimcWellEventTimeline.cpp | 10 ++- .../RimcWellEventTimeline.h | 1 + .../Python/rips/tests/test_well_events.py | 81 +++++++++++++++++-- GrpcInterface/Python/rips/well_events.py | 7 ++ 6 files changed, 122 insertions(+), 18 deletions(-) diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 6a3d03448f..f124a199eb 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -35,6 +35,8 @@ #include "opm/input/eclipse/Deck/DeckKeyword.hpp" #include "opm/input/eclipse/Deck/DeckRecord.hpp" +#include + #include #include #include @@ -46,7 +48,8 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& RimEclipseCase& eclipseCase, const std::vector& wellPaths, const std::vector& dates, - const std::set& mswWells ) + const std::set& mswWells, + bool firstDateAsComment ) { QString result; @@ -68,14 +71,17 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& [&exportName]( const RimWellPath* a, const RimWellPath* b ) { return QString::compare( exportName( a ), exportName( b ) ) < 0; } ); - // Generate section for each date + // Generate section for each date. The first (earliest) date may be emitted as a comment + // instead of a DATES keyword when firstDateAsComment is set. + bool isFirstDate = true; for ( const auto& date : dates ) { - QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells ); + QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment ); if ( !dateSection.isEmpty() ) { result += dateSection; } + isFirstDate = false; } return result; @@ -131,7 +137,8 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin RimEclipseCase& eclipseCase, const std::vector& wellPaths, const QDateTime& date, - const std::set& mswWells ) + const std::set& mswWells, + bool dateAsComment ) { // Keyword priority order for output static const std::vector keywordOrder = { "WELSPECS", @@ -154,8 +161,18 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin QString result; - // Generate DATES keyword - result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n"; + // Generate DATES keyword, or a date comment when requested (e.g. for the first date, which + // equals the simulation start date and is rejected as a DATES entry by some simulators). + if ( dateAsComment ) + { + QLocale locale( QLocale::English ); + QString month = locale.monthName( date.date().month(), QLocale::ShortFormat ).toUpper(); + result += QString( "-- Date: %1 %2 %3\n" ).arg( date.date().day() ).arg( month ).arg( date.date().year() ); + } + else + { + result += RimKeywordFactory::deckKeywordToString( RimKeywordFactory::datesKeyword( date ) ) + "\n"; + } // Records for each keyword name are accumulated across wells, then serialised once below. std::map keywordBlocks; diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 97757c3969..643af707ae 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -48,22 +48,28 @@ public: // mswWells lists the wells for which the multi-segment-well keywords (WELSEGS, COMPSEGS, // WSEGVALV, WSEGAICD) are exported. Wells not in the set get no MSW keywords; an empty set // suppresses MSW export for all wells. + // When firstDateAsComment is true, the first (earliest) date is written as a comment line + // instead of a DATES keyword, since some commercial simulators reject a DATES entry that + // equals the simulation start date. Later dates are always emitted as DATES keywords. static QString generateSchedule( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, const std::vector& dates, - const std::set& mswWells ); + const std::set& mswWells, + bool firstDateAsComment = true ); // Collect all unique dates from all wells' timelines static std::vector collectAllDates( const RimWellEventTimeline& timeline, const std::vector& wellPaths ); private: - // Generate schedule section for a single date + // Generate schedule section for a single date. When dateAsComment is true, the date is + // written as a comment line instead of a DATES keyword. static QString generateDateSection( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const std::vector& wellPaths, const QDateTime& date, - const std::set& mswWells ); + const std::set& mswWells, + bool dateAsComment = false ); static std::optional generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index 3f8e3c04f3..af6685fa23 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -578,6 +578,13 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule( "", "Wells for which multi-segment-well keywords (WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are " "exported" ); + CAF_PDM_InitScriptableField( &m_firstDateAsComment, + "FirstDateAsComment", + true, + "", + "", + "", + "Emit the first (simulation-start) date as a comment instead of a DATES keyword" ); } //-------------------------------------------------------------------------------------------------- @@ -630,7 +637,8 @@ std::expected RimcWellEventTimeline_generateSche std::vector mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType(); std::set mswWells( mswWellPaths.begin(), mswWellPaths.end() ); - QString scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells ); + QString scheduleText = + RicScheduleDataGenerator::generateSchedule( *timeline, *eclipseCase, wellPathsWithEvents, dates, mswWells, m_firstDateAsComment() ); // 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 95cadb978f..e682747341 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -229,4 +229,5 @@ public: private: caf::PdmPtrField m_eclipseCase; caf::PdmPtrArrayField m_exportMswForWells; + caf::PdmField m_firstDateAsComment; }; diff --git a/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index bfa2487368..fcb63a6d9e 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -296,9 +296,11 @@ class TestScheduleGeneration: # Apply events to create actual completions timeline.set_timestamp(timestamp="2024-12-31") - # Generate schedule text + # Generate schedule text (keep the first date as a DATES keyword for this assertion) schedule_text = timeline.generate_schedule_text( - eclipse_case=case, export_msw_for_wells=project.well_paths() + eclipse_case=case, + export_msw_for_wells=project.well_paths(), + first_date_as_comment=False, ) # Verify schedule text contains expected keywords @@ -348,9 +350,11 @@ class TestScheduleGeneration: # Apply events to create actual completions timeline.set_timestamp(timestamp="2024-12-31") - # Generate schedule text + # Generate schedule text (keep the first date as a DATES keyword for this assertion) schedule_text = timeline.generate_schedule_text( - eclipse_case=case, export_msw_for_wells=project.well_paths() + eclipse_case=case, + export_msw_for_wells=project.well_paths(), + first_date_as_comment=False, ) # Verify we have schedule text with completions and controls @@ -401,6 +405,62 @@ class TestScheduleGeneration: assert "DATES" in schedule_text, "Schedule should contain DATES keyword" assert "2024" in schedule_text, "Schedule should contain event dates" + def test_first_date_as_comment(self, project_with_case_and_well): + """Test that the first (earliest) date can be emitted as a comment. + + The first date equals the simulation start date, which some commercial + simulators reject as a DATES entry. With first_date_as_comment (the + default), the earliest date becomes a comment line while later dates + remain real DATES keywords. + """ + project, case, timeline = project_with_case_and_well + + well_paths = project.well_paths() + well_path_a = [wp for wp in well_paths if "A" in wp.name][0] + + # Two distinct dates: earliest 2024-01-01, later 2024-06-01 + timeline.add_perf_event( + event_date="2024-01-01", + well_path=well_path_a, + start_md=1800.0, + end_md=2000.0, + diameter=0.1, + state="OPEN", + ) + timeline.add_perf_event( + event_date="2024-06-01", + well_path=well_path_a, + start_md=2000.0, + end_md=2200.0, + diameter=0.1, + state="OPEN", + ) + + timeline.set_timestamp(timestamp="2024-12-31") + + # Default (first_date_as_comment=True): first date is a comment, later date a DATES keyword + default_text = timeline.generate_schedule_text( + eclipse_case=case, export_msw_for_wells=project.well_paths() + ) + assert default_text, "Schedule text should not be empty" + assert "-- Date: 1 JAN 2024" in default_text, ( + "First date should be emitted as a comment" + ) + assert default_text.count("DATES") == 1, ( + "Only the later date should be a DATES keyword" + ) + + # Explicit False: both dates emitted as DATES keywords (legacy behavior) + legacy_text = timeline.generate_schedule_text( + eclipse_case=case, + export_msw_for_wells=project.well_paths(), + first_date_as_comment=False, + ) + assert "-- Date:" not in legacy_text, ( + "No date comment should be present when first_date_as_comment is False" + ) + assert legacy_text.count("DATES") == 2, "Both dates should be DATES keywords" + def test_timestamp_filters_wells_in_schedule_output( self, project_with_case_and_well ): @@ -1296,9 +1356,12 @@ class TestScheduleGeneration: is_producer=True, ) - # Control events don't require set_timestamp + # Control events don't require set_timestamp. Keep all dates as DATES keywords + # so the chronological ordering of the keyword can be verified. schedule_text = timeline.generate_schedule_text( - eclipse_case=case, export_msw_for_wells=project.well_paths() + eclipse_case=case, + export_msw_for_wells=project.well_paths(), + first_date_as_comment=False, ) print(f"\nSchedule text for date ordering test:\n{schedule_text}") @@ -2224,9 +2287,11 @@ class TestScheduleKeywordEvents: }, ) - # Generate schedule text + # Generate schedule text (keep the first date as a DATES keyword for this assertion) schedule_text = timeline.generate_schedule_text( - eclipse_case=case, export_msw_for_wells=project.well_paths() + eclipse_case=case, + export_msw_for_wells=project.well_paths(), + first_date_as_comment=False, ) print(f"\nSchedule text with RPTRST keyword:\n{schedule_text}") diff --git a/GrpcInterface/Python/rips/well_events.py b/GrpcInterface/Python/rips/well_events.py index 60970f4472..9355a3acef 100644 --- a/GrpcInterface/Python/rips/well_events.py +++ b/GrpcInterface/Python/rips/well_events.py @@ -288,6 +288,7 @@ def generate_schedule_text( self: WellEventTimeline, eclipse_case: EclipseCase, export_msw_for_wells: List[WellPath] = [], + first_date_as_comment: bool = True, ) -> str: """Generate Eclipse schedule text for all wells in the collection. @@ -304,6 +305,11 @@ def generate_schedule_text( multi-segment-well keywords (WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are exported. Wells not in the list get no MSW keywords. An empty list (the default) suppresses MSW export for all wells. + first_date_as_comment (bool): When True (the default), the first + (earliest) date is written as a comment line (e.g. "-- Date: 1 JAN + 2024") instead of a DATES keyword. This avoids a DATES entry equal + to the simulation start date, which some commercial simulators + reject. Later dates are always emitted as DATES keywords. Returns: str: Eclipse schedule text containing DATES, COMPDAT, WELSEGS, WCONPROD, etc. @@ -346,6 +352,7 @@ def generate_schedule_text( container = self.generate_schedule( eclipse_case=eclipse_case, export_msw_for_wells=export_msw_for_wells, + first_date_as_comment=first_date_as_comment, ) if container and container.values: return "".join(container.values)