diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.cpp index 81ae69a3af..ec1b084950 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.cpp @@ -57,7 +57,7 @@ const QString featureTitle = "Export Schedule"; /// //-------------------------------------------------------------------------------------------------- std::expected - RicExportScheduleFeature::exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const QString& fileName ) + RicExportScheduleFeature::exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase* eclipseCase, const QString& fileName ) { if ( fileName.isEmpty() ) return std::unexpected( QString( "No file name specified" ) ); @@ -138,7 +138,7 @@ RimEclipseCase* RicExportScheduleFeature::defaultEclipseCase() //-------------------------------------------------------------------------------------------------- bool RicExportScheduleFeature::isCommandEnabled() const { - return timelineToExport() != nullptr && defaultEclipseCase() != nullptr; + return timelineToExport() != nullptr; } //-------------------------------------------------------------------------------------------------- @@ -155,12 +155,8 @@ void RicExportScheduleFeature::onActionTriggered( bool isChecked ) return; } + // Without a case, the grid-dependent keywords are skipped and reported as warnings in the file header. RimEclipseCase* eclipseCase = defaultEclipseCase(); - if ( !eclipseCase ) - { - RiuMessageDialog::showError( parentWidget, featureTitle, "Schedule export requires a loaded Eclipse case." ); - return; - } RiaApplication* app = RiaApplication::instance(); QString defaultDir = app->lastUsedDialogDirectoryWithFallbackToProjectFolder( exportDialogName ); @@ -174,7 +170,7 @@ void RicExportScheduleFeature::onActionTriggered( bool isChecked ) app->setLastUsedDialogDirectory( exportDialogName, QFileInfo( fileName ).absolutePath() ); - auto exportedFile = exportScheduleToFile( *timeline, *eclipseCase, fileName ); + auto exportedFile = exportScheduleToFile( *timeline, eclipseCase, fileName ); if ( !exportedFile ) { RiaLogging::error( std::format( "Schedule export failed: {}", exportedFile.error() ) ); diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.h index e998a4545c..0a3035b1e4 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicExportScheduleFeature.h @@ -41,9 +41,10 @@ public: // Generate the schedule for the timeline and write it to fileName. MSW keywords are exported // for every well with events, the first (simulation start) date is written as a comment // instead of a DATES keyword, and keywords are emitted with aligned columns. + // eclipseCase may be null; the grid-dependent keywords are then skipped (see RicScheduleDataGenerator). // Returns the written file name, or an error message. static std::expected - exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const QString& fileName ); + exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase* eclipseCase, const QString& fileName ); protected: bool isCommandEnabled() const override; @@ -57,6 +58,6 @@ private: // events. static RimWellEventTimeline* timelineToExport(); - // The Eclipse case of the active view, or the first Eclipse case of the project. + // The Eclipse case of the active view, or the first Eclipse case of the project, or nullptr. static RimEclipseCase* defaultEclipseCase(); }; diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 8f68e5bf15..9ffb8e3421 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -23,6 +23,8 @@ #include "RicWellPathExportCompletionDataFeatureImpl.h" #include "RicWellPathExportMswTableData.h" +#include "RiaLogging.h" + #include "RifEventKeywordFormatter.h" #include "ProjectDataModel/Jobs/RimKeywordFactory.h" @@ -48,6 +50,11 @@ namespace { +QString missingGridWarning( const QString& keywords, const QString& wellName ) +{ + return QString( "No grid case: %1 skipped for well '%2'" ).arg( keywords, wellName ); +} + bool shouldEmitKeyword( const Opm::DeckKeyword& keyword ) { return keyword.size() > 0 || keyword.isDataKeyword() || RifEventKeywordFormatter::isRecordlessKeyword( keyword ); @@ -103,7 +110,7 @@ std::optional wellSpecDataAtDate( const RimWellEventTimeline& t /// //-------------------------------------------------------------------------------------------------- std::expected RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::vector& wellPaths, const std::vector& dates, const std::set& mswWells, @@ -128,6 +135,9 @@ std::expected RicScheduleDataGenerator::generateSchedule( cons result += "-- Schedule data based on well events\n"; result += "--\n"; + std::set warnings; + QString body; + // The incoming well list is ordered by pointer (it originates from a std::set), which makes the // per-well keyword records appear in a non-deterministic order. Sort once by the deck export // name so every per-well keyword (WELSPECS, COMPDAT, COMPLUMP, WELSEGS/COMPSEGS, WSEGVALV/ @@ -171,14 +181,22 @@ std::expected RicScheduleDataGenerator::generateSchedule( cons date, mswWells, emittedSegmentLayouts, + warnings, isFirstDate && firstDateAsComment, alignColumns ); if ( !dateSection ) return std::unexpected( dateSection.error() ); - result += *dateSection; + body += *dateSection; isFirstDate = false; } - return result; + for ( const auto& warning : warnings ) + { + RiaLogging::warning( ( "Schedule generation: " + warning ).toStdString() ); + result += QString( "-- WARNING: %1\n" ).arg( warning ); + } + if ( !warnings.empty() ) result += "--\n"; + + return result + body; } //-------------------------------------------------------------------------------------------------- @@ -208,7 +226,7 @@ std::vector RicScheduleDataGenerator::collectAllDates( const RimWellE /// //-------------------------------------------------------------------------------------------------- std::expected RicScheduleDataGenerator::generateScheduleForTimeline( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::set& mswWells, bool firstDateAsComment, bool alignColumns ) @@ -275,11 +293,12 @@ void RicScheduleDataGenerator::mergeKeyword( std::map /// //-------------------------------------------------------------------------------------------------- std::expected RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::vector& wellPaths, const QDateTime& date, const std::set& mswWells, std::map& emittedSegmentLayouts, + std::set& warnings, bool dateAsComment, bool alignColumns ) { @@ -406,7 +425,7 @@ std::expected RicScheduleDataGenerator::generateDateSection( c for ( auto* well : group ) { - if ( auto welspecs = generateWelspecsForWell( timeline, eclipseCase, *well, date, events ) ) + if ( auto welspecs = generateWelspecsForWell( timeline, eclipseCase, *well, date, events, warnings ) ) { mergeKeyword( keywordBlocks, "WELSPECS", std::move( *welspecs ) ); mergeKeyword( keywordBlocks, "COMPORD", RimKeywordFactory::compordKeyword( exportNameForWell( *well ).toStdString() ) ); @@ -414,12 +433,20 @@ std::expected RicScheduleDataGenerator::generateDateSection( c } } - generateCompletionsForWellGroup( eclipseCase, group, date, events, keywordBlocks ); + generateCompletionsForWellGroup( eclipseCase, group, date, events, keywordBlocks, warnings ); // MSW keywords describe the shared well tree, including all its laterals. if ( mswIt != group.end() ) { - generateMswForWell( timeline, eclipseCase, *( *mswIt )->topLevelWellPath(), date, events, keywordBlocks, unmergedBlocks, emittedSegmentLayouts ); + generateMswForWell( timeline, + eclipseCase, + *( *mswIt )->topLevelWellPath(), + date, + events, + keywordBlocks, + unmergedBlocks, + emittedSegmentLayouts, + warnings ); } for ( auto* well : group ) @@ -573,10 +600,11 @@ std::expected RicScheduleDataGenerator::generateDateSection( c /// //-------------------------------------------------------------------------------------------------- std::optional RicScheduleDataGenerator::generateWelspecsForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, RimWellPath& well, const QDateTime& date, - const std::vector& eventsAtDate ) + const std::vector& eventsAtDate, + std::set& warnings ) { bool hasEvents = false; for ( auto* event : eventsAtDate ) @@ -592,10 +620,17 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel if ( !hasEvents ) return std::nullopt; + // The well head location (HEAD_I/HEAD_J) is computed from the grid. + if ( !eclipseCase ) + { + warnings.insert( missingGridWarning( "WELSPECS/COMPORD", exportNameForWell( well ) ) ); + return std::nullopt; + } + const auto wellSpecData = wellSpecDataAtDate( timeline, well, date ); std::string wellGroupName = well.completionSettings()->groupNameForExport().toStdString(); - auto welspecsKw = RimKeywordFactory::welspecsKeyword( wellGroupName, &eclipseCase, &well, wellSpecData ? &wellSpecData.value() : nullptr ); + auto welspecsKw = RimKeywordFactory::welspecsKeyword( wellGroupName, eclipseCase, &well, wellSpecData ? &wellSpecData.value() : nullptr ); if ( welspecsKw.name().empty() ) return std::nullopt; return welspecsKw; } @@ -603,11 +638,12 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase& eclipseCase, +void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase* eclipseCase, const std::vector& wellGroup, const QDateTime& date, const std::vector& eventsAtDate, - std::map& keywordBlocks ) + std::map& keywordBlocks, + std::set& warnings ) { if ( wellGroup.empty() ) return; @@ -625,6 +661,12 @@ void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase& if ( !hasPerfEvents ) return; + if ( !eclipseCase ) + { + warnings.insert( missingGridWarning( "COMPDAT/COMPLUMP", exportNameForWell( representative ) ) ); + return; + } + // A connection must be exported once per well name. Completion data is computed for every distinct well tree // in the group (a top-level well includes its laterals) and combined per cell, so connections shared by several // well paths get their contributions combined instead of being duplicated or dropped. @@ -637,7 +679,7 @@ void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase& // Completion data feeds both COMPDAT and COMPLUMP (the latter only emits records whose perforation has a // completion number assigned). Both keywords merge across wells. - auto compdata = RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPaths( topLevelWells, &eclipseCase, date ); + auto compdata = RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPaths( topLevelWells, eclipseCase, date ); auto wellName = exportNameForWell( representative ).toStdString(); mergeKeyword( keywordBlocks, "COMPDAT", RimKeywordFactory::compdatKeyword( compdata, wellName ) ); @@ -648,13 +690,14 @@ void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase& /// //-------------------------------------------------------------------------------------------------- void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, RimWellPath& wellPath, const QDateTime& date, const std::vector& eventsAtDate, std::map& keywordBlocks, std::map>& unmergedBlocks, - std::map& emittedSegmentLayouts ) + std::map& emittedSegmentLayouts, + std::set& warnings ) { auto* segmentCollection = wellPath.segmentCollection(); if ( !segmentCollection ) @@ -684,8 +727,14 @@ void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& return; } + if ( !eclipseCase ) + { + warnings.insert( missingGridWarning( "WELSEGS/COMPSEGS/WSEGVALV/WSEGAICD", exportNameForWell( wellPath ) ) ); + return; + } + bool exportAfterMainBore = true; - auto mswDataResult = RicWellPathExportMswTableData::extractSingleWellMswData( &eclipseCase, + auto mswDataResult = RicWellPathExportMswTableData::extractSingleWellMswData( eclipseCase, &wellPath, exportAfterMainBore, RicWellPathExportMswTableData::CompletionType::ALL, diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 3718c5edaf..4fa27af2e5 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -56,8 +56,11 @@ public: // equals the simulation start date. Later dates are always emitted as DATES keywords. // When alignColumns is true, keywords are serialised with a "--"-prefixed column-header comment // and right-aligned, fixed-width columns instead of the compact default form. + // eclipseCase may be null. The grid-dependent keywords (WELSPECS, COMPORD, COMPDAT, COMPLUMP, WELSEGS, + // COMPSEGS, WSEGVALV, WSEGAICD) are then skipped, and a "-- WARNING:" comment line is written to the + // schedule header (and logged) for each well that had such keywords skipped. static std::expected generateSchedule( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::vector& wellPaths, const std::vector& dates, const std::set& mswWells, @@ -73,7 +76,7 @@ public: // events only force a DATES keyword and are therefore never filtered by that timestamp. // The remaining arguments are passed on to generateSchedule(). static std::expected generateScheduleForTimeline( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::set& mswWells, bool firstDateAsComment = true, bool alignColumns = false ); @@ -82,28 +85,31 @@ private: // 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 std::expected generateDateSection( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, const std::vector& wellPaths, const QDateTime& date, const std::set& mswWells, std::map& emittedSegmentLayouts, + std::set& warnings, bool dateAsComment = false, bool alignColumns = false ); static std::optional generateWelspecsForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, RimWellPath& well, const QDateTime& date, - const std::vector& eventsAtDate ); + const std::vector& eventsAtDate, + std::set& warnings ); // Generate COMPDAT (and COMPLUMP, when perforations carry a completion number) for a group of well paths sharing // one export name at a specific date based on events, merging both into the accumulator. Connections in cells // shared by several well paths in the group are combined into one record. - static void generateCompletionsForWellGroup( RimEclipseCase& eclipseCase, + static void generateCompletionsForWellGroup( RimEclipseCase* eclipseCase, const std::vector& wellGroup, const QDateTime& date, const std::vector& eventsAtDate, - std::map& keywordBlocks ); + std::map& keywordBlocks, + std::set& warnings ); // Generate WELSEGS / COMPSEGS / WSEGVALV / WSEGAICD for a well at a specific date. // WSEGVALV / WSEGAICD are merged into keywordBlocks; WELSEGS / COMPSEGS cannot be merged across @@ -113,13 +119,14 @@ private: // segment layout differs from the one last written for the well (tracked in emittedSegmentLayouts). // The WELSEGS pressure drop components come from the WELSPECS event in effect at the date. static void generateMswForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, + RimEclipseCase* eclipseCase, RimWellPath& well, const QDateTime& date, const std::vector& eventsAtDate, std::map& keywordBlocks, std::map>& unmergedBlocks, - std::map& emittedSegmentLayouts ); + std::map& emittedSegmentLayouts, + std::set& warnings ); // Generate well control / well keyword event keywords for a well at a specific date, merging into the accumulator static void generateWellControlForWell( const RimWellPath& well, diff --git a/ApplicationLibCode/FeatureTests/Tests/RicExportScheduleFeature-Test.cpp b/ApplicationLibCode/FeatureTests/Tests/RicExportScheduleFeature-Test.cpp index c2719797bc..b07aaa05e8 100644 --- a/ApplicationLibCode/FeatureTests/Tests/RicExportScheduleFeature-Test.cpp +++ b/ApplicationLibCode/FeatureTests/Tests/RicExportScheduleFeature-Test.cpp @@ -96,7 +96,7 @@ TEST_F( RicExportScheduleFeatureTest, ExportWritesScheduleForTimelineEvents ) ASSERT_TRUE( temporaryDir.isValid() ); const QString fileName = temporaryDir.filePath( "schedule.SCH" ); - auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, *model.eclipseCase, fileName ); + auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, model.eclipseCase, fileName ); ASSERT_TRUE( exportedFile.has_value() ) << exportedFile.error().toStdString(); EXPECT_EQ( fileName.toStdString(), exportedFile->toStdString() ); @@ -150,7 +150,7 @@ TEST_F( RicExportScheduleFeatureTest, ExportKeepsKeywordPriorityOrderRegardlessO ASSERT_TRUE( temporaryDir.isValid() ); const QString fileName = temporaryDir.filePath( "schedule_order.SCH" ); - auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, *model.eclipseCase, fileName ); + auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, model.eclipseCase, fileName ); ASSERT_TRUE( exportedFile.has_value() ) << exportedFile.error().toStdString(); QFile file( fileName ); @@ -209,7 +209,61 @@ TEST_F( RicExportScheduleFeatureTest, ExportOfEmptyTimelineFails ) QTemporaryDir temporaryDir; ASSERT_TRUE( temporaryDir.isValid() ); - auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, *model.eclipseCase, temporaryDir.filePath( "empty.SCH" ) ); + auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, model.eclipseCase, temporaryDir.filePath( "empty.SCH" ) ); ASSERT_FALSE( exportedFile.has_value() ); EXPECT_EQ( std::string( "No events found in timeline" ), exportedFile.error().toStdString() ); } + +TEST_F( RicExportScheduleFeatureTest, ExportWithoutCaseWritesGridIndependentKeywords ) +{ + FeatureTestModel model = RiaFeatureTestModelBuilder::combinedModel(); + + RimWellEventTimeline* timeline = timelineWithEvents(); + ASSERT_TRUE( timeline != nullptr ); + + QTemporaryDir temporaryDir; + ASSERT_TRUE( temporaryDir.isValid() ); + const QString fileName = temporaryDir.filePath( "no_case.SCH" ); + + auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, nullptr, fileName ); + ASSERT_TRUE( exportedFile.has_value() ) << exportedFile.error().toStdString(); + + QFile file( fileName ); + ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) ); + const QString content = QTextStream( &file ).readAll(); + + EXPECT_TRUE( content.contains( "-- Date: 1 JAN 2024" ) ); + EXPECT_TRUE( content.contains( "RPTRST" ) ); + EXPECT_TRUE( content.contains( "'JUL'" ) ); + // No grid-dependent keywords were requested, so nothing is skipped. + EXPECT_FALSE( content.contains( "WARNING" ) ); +} + +TEST_F( RicExportScheduleFeatureTest, ExportWithoutCaseWarnsForPerforationEvents ) +{ + FeatureTestModel model = RiaFeatureTestModelBuilder::wellPath(); + ASSERT_TRUE( model.wellPath != nullptr ); + + RimWellEventTimeline* timeline = timelineWithEvents(); + ASSERT_TRUE( timeline != nullptr ); + timeline->addPerforationEvent( model.wellPath, QDateTime( QDate( 2024, 1, 1 ), QTime( 0, 0, 0 ) ) ); + + QTemporaryDir temporaryDir; + ASSERT_TRUE( temporaryDir.isValid() ); + const QString fileName = temporaryDir.filePath( "no_case_perforation.SCH" ); + + auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, nullptr, fileName ); + ASSERT_TRUE( exportedFile.has_value() ) << exportedFile.error().toStdString(); + + QFile file( fileName ); + ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) ); + const QString content = QTextStream( &file ).readAll(); + + EXPECT_TRUE( content.contains( "-- WARNING: No grid case: WELSPECS/COMPORD skipped for well '" ) ); + EXPECT_TRUE( content.contains( "-- WARNING: No grid case: COMPDAT/COMPLUMP skipped for well '" ) ); + EXPECT_FALSE( content.contains( "\nWELSPECS\n" ) ); + EXPECT_FALSE( content.contains( "\nCOMPORD\n" ) ); + EXPECT_FALSE( content.contains( "\nCOMPDAT\n" ) ); + EXPECT_FALSE( content.contains( "\nCOMPLUMP\n" ) ); + EXPECT_TRUE( content.contains( "\nRPTRST\n" ) ); +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index 44d9a0b69c..a8ebfb56ad 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -838,7 +838,14 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule( { CAF_PDM_InitObject( "Generate Schedule", "", "", "Generate Eclipse schedule text for all wells in the collection" ); - CAF_PDM_InitScriptableFieldNoDefault( &m_eclipseCase, "EclipseCase", "", "", "", "Eclipse Case" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_eclipseCase, + "EclipseCase", + "", + "", + "", + "Optional grid case (Eclipse, Roff or corner point). Without a case, the grid-dependent keywords " + "(WELSPECS, COMPORD, COMPDAT, COMPLUMP, WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are skipped and " + "reported as WARNING comments in the schedule header" ); CAF_PDM_InitScriptableFieldNoDefault( &m_exportMswForWells, "ExportMswForWells", "", @@ -869,18 +876,12 @@ std::expected RimcWellEventTimeline_generateSche { auto timeline = self(); - RimEclipseCase* eclipseCase = m_eclipseCase(); - if ( !eclipseCase ) - { - return std::unexpected( QString( "Eclipse case is required" ) ); - } - // The dates and wells are collected from the timeline, honoring any set_timestamp() call. std::vector mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType(); std::set mswWells( mswWellPaths.begin(), mswWellPaths.end() ); auto scheduleText = - RicScheduleDataGenerator::generateScheduleForTimeline( *timeline, *eclipseCase, mswWells, m_firstDateAsComment(), m_alignColumns() ); + RicScheduleDataGenerator::generateScheduleForTimeline( *timeline, m_eclipseCase(), mswWells, m_firstDateAsComment(), m_alignColumns() ); if ( !scheduleText ) return std::unexpected( scheduleText.error() ); // Return the schedule text in a data container diff --git a/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index f2307c66cc..ad2c2e7042 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -3107,3 +3107,74 @@ class TestScheduleKeywordEvents: ), f"flag {flag!r} missing from RPTRST output: {rptrst_block!r}" # False-valued flag must be omitted entirely. assert "OBSOLETE" not in rptrst_block + + +class TestScheduleGenerationWithoutCase: + """Schedule generation without a grid case emits only grid-independent keywords.""" + + @pytest.fixture + def project_with_well(self, rips_instance, initialize_test): + case_root = dataroot.PATH + "/TEST10K_FLT_LGR_NNC" + project = rips_instance.project + project.import_well_paths(well_path_files=[case_root + "/wellpath_a.dev"]) + well_path_coll = project.descendants(rips.WellPathCollection)[0] + return project, project.well_paths()[0], well_path_coll.event_timeline() + + def test_wconhist_without_case(self, project_with_well): + project, well_path, timeline = project_with_well + assert not project.cases() + + for event_date, rate in (("2024-01-01", 1000.0), ("2024-02-01", 2000.0)): + timeline.add_well_keyword_event( + event_date=event_date, + well_path=well_path, + keyword_name="WCONHIST", + keyword_data={ + "WELL": well_path.name, + "STATUS": "OPEN", + "CMODE": "ORAT", + "ORAT": rate, + }, + ) + + schedule = timeline.generate_schedule_text() + + assert schedule.count("WCONHIST") == 2 + assert "DATES" in schedule + assert "WELSPECS" not in schedule + assert "COMPDAT" not in schedule + assert "WARNING" not in schedule + + def test_perforation_without_case_is_skipped_with_warning(self, project_with_well): + project, well_path, timeline = project_with_well + + timeline.add_perf_event( + event_date="2024-01-01", + well_path=well_path, + start_md=2000.0, + end_md=2200.0, + diameter=0.1, + state="OPEN", + ) + timeline.add_well_keyword_event( + event_date="2024-01-01", + well_path=well_path, + keyword_name="WCONHIST", + keyword_data={"WELL": well_path.name, "STATUS": "OPEN"}, + ) + timeline.set_timestamp(timestamp="2024-12-31") + + schedule = timeline.generate_schedule_text( + eclipse_case=None, export_msw_for_wells=[well_path] + ) + + assert "WCONHIST" in schedule + for keyword in ("WELSPECS", "COMPDAT", "WELSEGS", "COMPSEGS"): + assert f"\n{keyword}\n" not in schedule + warnings = [ + line for line in schedule.splitlines() if line.startswith("-- WARNING:") + ] + export_name = well_path.completion_settings().well_name_for_export + assert any("COMPDAT" in w and export_name in w for w in warnings) + assert any("WELSPECS" in w for w in warnings) + assert any("WELSEGS" in w for w in warnings) diff --git a/GrpcInterface/Python/rips/well_events.py b/GrpcInterface/Python/rips/well_events.py index 79f41cd5ab..163f1c1e16 100644 --- a/GrpcInterface/Python/rips/well_events.py +++ b/GrpcInterface/Python/rips/well_events.py @@ -6,7 +6,7 @@ in a timeline-based event system. Events can be perforation events, valve events segment changes, well state changes, and production/injection control changes. """ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from datetime import date, datetime from .pdmobject import add_method @@ -330,7 +330,7 @@ def add_insert_date_event( @add_method(WellEventTimeline) def generate_schedule_text( self: WellEventTimeline, - eclipse_case: EclipseCase, + eclipse_case: Optional[EclipseCase] = None, export_msw_for_wells: List[WellPath] = [], first_date_as_comment: bool = True, align_columns: bool = False, @@ -345,7 +345,14 @@ def generate_schedule_text( text directly instead of a DataContainerString. Arguments: - eclipse_case (EclipseCase): Eclipse case to use for schedule generation. + eclipse_case (Optional[EclipseCase]): Grid case used for the + grid-dependent keywords. Any Eclipse-type case works, including + Roff and corner point cases. When None (the default), the + keywords that need grid geometry (WELSPECS, COMPORD, COMPDAT, + COMPLUMP, WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are skipped. Each + well with skipped keywords gets a "-- WARNING:" comment line in the + schedule header. DATES, well controls (WCONHIST, WCONINJH, WELTARG, + ...), schedule keywords and raw text are still emitted. export_msw_for_wells (List[WellPath]): Wells for which the multi-segment-well keywords (WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD) are exported. Wells not in the list get no MSW keywords. An empty diff --git a/docs/simulatorEvents.md b/docs/simulatorEvents.md index 601fcb6a5e..3dc820620b 100644 --- a/docs/simulatorEvents.md +++ b/docs/simulatorEvents.md @@ -363,6 +363,23 @@ They also accept two policies, each `"warn"` (default), `"error"` or `"skip"`: The returned `ApplyReport` carries `events_applied`, `events_skipped`, `warnings` and `errors`. +### Generating a schedule without a grid + +`eclipse_case` is optional in `generate_schedule_text()`. It accepts any Eclipse-type case, including Roff and +corner point cases. Without a case, the keywords that need grid geometry are skipped: `WELSPECS`, `COMPORD`, +`COMPDAT`, `COMPLUMP`, `WELSEGS`, `COMPSEGS`, `WSEGVALV` and `WSEGAICD`. `DATES`, well controls (`WCONHIST`, +`WCONINJH`, `WELTARG`, ...), schedule keywords and raw text are still emitted. This supports setups driven only by +production data, or by a simple event file that controls reporting and production constraints. A well that has +skipped keywords gets one `-- WARNING:` comment line in the schedule header, for example: + +```text +-- WARNING: No grid case: COMPDAT/COMPLUMP skipped for well 'OP_1' +``` + +```python +schedule_text = timeline.generate_schedule_text() # no grid case +``` + ## Exporting a schedule from the GUI The schedule generated from the well event timeline can be written to file without scripting. @@ -380,15 +397,15 @@ Everything else uses fixed defaults: | Setting | Value | |---|---| -| Eclipse case | the case of the active 3D view, else the first Eclipse case of the project | +| Eclipse case | the case of the active 3D view, else the first Eclipse case of the project, else none (grid-dependent keywords are skipped, see above) | | Multi-segment-well keywords | exported for every well with events | | First date | written as a `-- Date: ...` comment instead of a `DATES` keyword | | Formatting | aligned columns with a `--`-prefixed column header per keyword | As in `generate_schedule_text()`, a `set_timestamp()`/*Apply events up to date* timestamp limits the export to the events up to that date, while inserted dates (`INSERT_DATE`) are always emitted. -The written file name and the event count are logged to the message panel; an export without events -or without a loaded Eclipse case reports the reason in a dialog. +The written file name and the event count are logged to the message panel. An export without events +reports the reason in a dialog. ## Version history