diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 9531c7d038..7ef15ac6f1 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -160,10 +160,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin mergeKeyword( keywordBlocks, "WELSPECS", std::move( *welspecs ) ); } - if ( auto compdat = generateCompdatForWell( timeline, eclipseCase, *well, date ) ) - { - mergeKeyword( keywordBlocks, "COMPDAT", std::move( *compdat ) ); - } + generateCompletionsForWell( timeline, eclipseCase, *well, date, keywordBlocks ); generateMswForWell( timeline, eclipseCase, *well, date, keywordBlocks, unmergedBlocks, mswWells ); generateWellControlForWell( timeline, *well, date, keywordBlocks ); @@ -262,10 +259,11 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::optional RicScheduleDataGenerator::generateCompdatForWell( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - RimWellPath& well, - const QDateTime& date ) +void RicScheduleDataGenerator::generateCompletionsForWell( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + RimWellPath& well, + const QDateTime& date, + std::map& keywordBlocks ) { auto events = timeline.getEventsAtDate( date ); @@ -279,14 +277,15 @@ std::optional RicScheduleDataGenerator::generateCompdatForWell } } - if ( !hasPerfEvents ) return std::nullopt; + if ( !hasPerfEvents ) return; + // Completion data is computed once and 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::completionDataForWellPath( &well, &eclipseCase, date ); auto wellName = well.completionSettings()->wellNameForExport().toStdString(); - auto compdatKw = RimKeywordFactory::compdatKeyword( compdata, wellName ); - if ( compdatKw.name().empty() ) return std::nullopt; - return compdatKw; + mergeKeyword( keywordBlocks, "COMPDAT", RimKeywordFactory::compdatKeyword( compdata, wellName ) ); + mergeKeyword( keywordBlocks, "COMPLUMP", RimKeywordFactory::complumpKeyword( compdata, wellName ) ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 034a4ea882..97757c3969 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -68,9 +68,13 @@ private: 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 std::optional - generateCompdatForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date ); + // Generate COMPDAT (and COMPLUMP, when perforations carry a completion number) for a well at a + // specific date based on events, merging both into the accumulator. + static void generateCompletionsForWell( 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. // WSEGVALV / WSEGAICD are merged into keywordBlocks; WELSEGS / COMPSEGS cannot be merged across diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.cpp index b42e786796..63de48c16e 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.cpp @@ -47,6 +47,7 @@ RimWellEventPerf::RimWellEventPerf() CAF_PDM_InitScriptableField( &m_diameter, "Diameter", 0.216, "Diameter" ); CAF_PDM_InitScriptableField( &m_skinFactor, "SkinFactor", 0.0, "Skin Factor" ); CAF_PDM_InitScriptableField( &m_state, "State", caf::AppEnum( State::OPEN ), "State" ); + CAF_PDM_InitScriptableField( &m_completionNumber, "CompletionNumber", 0, "Completion Number" ); setDeletable( true ); } @@ -98,6 +99,14 @@ RimWellEventPerf::State RimWellEventPerf::state() const return m_state(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimWellEventPerf::completionNumber() const +{ + return m_completionNumber(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -138,6 +147,14 @@ void RimWellEventPerf::setState( State state ) m_state = state; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventPerf::setCompletionNumber( int completionNumber ) +{ + m_completionNumber = completionNumber; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -181,6 +198,7 @@ void RimWellEventPerf::defineUiOrdering( QString uiConfigName, caf::PdmUiOrderin uiOrdering.add( &m_diameter ); uiOrdering.add( &m_skinFactor ); uiOrdering.add( &m_state ); + uiOrdering.add( &m_completionNumber ); uiOrdering.skipRemainingFields(); } diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.h index 6741c9e5cc..d1ab69b9e8 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventPerf.h @@ -48,6 +48,7 @@ public: double diameter() const; double skinFactor() const; State state() const; + int completionNumber() const; // Setters void setStartMD( double md ); @@ -55,6 +56,7 @@ public: void setDiameter( double diameter ); void setSkinFactor( double skinFactor ); void setState( State state ); + void setCompletionNumber( int completionNumber ); // Override from RimWellEvent EventType eventType() const override; @@ -70,6 +72,7 @@ private: caf::PdmField m_diameter; caf::PdmField m_skinFactor; caf::PdmField> m_state; + caf::PdmField m_completionNumber; }; namespace caf diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp index e503b722da..4d943d6b96 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp @@ -489,6 +489,12 @@ bool RimWellEventTimeline::applyPerfEvent( const RimWellEventPerf& event, RimWel perfInterval->setSkinFactor( event.skinFactor() ); perfInterval->setUnitSystemSpecificDefaults(); + // A non-zero completion number drives COMPLUMP generation downstream. + if ( event.completionNumber() > 0 ) + { + perfInterval->setCompletionNumber( event.completionNumber() ); + } + // Set the custom start date based on the event date perfInterval->enableCustomStartDate( true ); perfInterval->setCustomStartDate( event.eventDate().date() ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index ac93d58d19..3f8e3c04f3 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -58,6 +58,7 @@ RimcWellEventTimeline_addPerfEvent::RimcWellEventTimeline_addPerfEvent( caf::Pdm CAF_PDM_InitScriptableField( &m_skinFactor, "SkinFactor", 0.0, "", "", "", "Skin Factor" ); auto defaultState = RimWellEventPerf::State::OPEN; CAF_PDM_InitScriptableField( &m_state, "State", defaultState, "", "", "", "State" ); + CAF_PDM_InitScriptableField( &m_completionNumber, "CompletionNumber", 0, "", "", "", "Completion Number (for COMPLUMP, 0 = none)" ); } //-------------------------------------------------------------------------------------------------- @@ -84,6 +85,7 @@ std::expected RimcWellEventTimeline_addPerfEvent event->setDiameter( m_diameter() ); event->setSkinFactor( m_skinFactor() ); event->setState( m_state() ); + event->setCompletionNumber( m_completionNumber() ); return event; } diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h index 5ce7cf53c0..95cadb978f 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -57,6 +57,7 @@ private: caf::PdmField m_diameter; caf::PdmField m_skinFactor; caf::PdmField> m_state; + caf::PdmField m_completionNumber; }; //================================================================================================== diff --git a/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_event_schedule.py b/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_event_schedule.py index 430a49a8f1..369913e18f 100644 --- a/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_event_schedule.py +++ b/GrpcInterface/Python/rips/PythonExamples/wells_and_fractures/well_event_schedule.py @@ -52,7 +52,9 @@ def main(): ) print(" Added tubing event on 2024-01-01 (MD 0-2500m)") - # Add first perforation event + # Add first perforation event. + # completion_number assigns the perforation to a completion group, which makes + # the schedule emit a COMPLUMP keyword lumping these connections into group 1. _perf_event1 = timeline.add_perf_event( event_date="2024-02-01", well_path=well_path, @@ -61,10 +63,11 @@ def main(): diameter=0.1, skin_factor=0.5, state="OPEN", + completion_number=1, ) - print(" Added perforation event on 2024-02-01 (MD 2000-2200m)") + print(" Added perforation event on 2024-02-01 (MD 2000-2200m, completion 1)") - # Add second perforation event (later) + # Add second perforation event (later), assigned to a different completion group. _perf_event2 = timeline.add_perf_event( event_date="2024-04-01", well_path=well_path, @@ -73,8 +76,9 @@ def main(): diameter=0.1, skin_factor=0.3, state="OPEN", + completion_number=2, ) - print(" Added perforation event on 2024-04-01 (MD 2400-2600m)") + print(" Added perforation event on 2024-04-01 (MD 2400-2600m, completion 2)") # Add valve event (requires existing perforation) _valve_event = timeline.add_valve_event( @@ -227,6 +231,8 @@ def main(): "DATES", "WELSEGS", "COMPSEGS", + "COMPDAT", + "COMPLUMP", "WCONHIST", "WELTARG", "WRFTPLT", @@ -250,6 +256,9 @@ def main(): if "COMPSEGS" in schedule_text: print(" ✓ COMPSEGS keyword generated (completion segments)") + if "COMPLUMP" in schedule_text: + print(" ✓ COMPLUMP keyword generated (perforation completion groups)") + if "WSEGVALV" in schedule_text: print(" ✓ WSEGVALV keyword generated (segment valves)") # Extract valve parameters @@ -268,6 +277,7 @@ def main(): print(f" - DATES entries: {schedule_text.count('DATES')}") print(f" - WELSEGS entries: {schedule_text.count('WELSEGS')}") print(f" - COMPSEGS entries: {schedule_text.count('COMPSEGS')}") + print(f" - COMPLUMP entries: {schedule_text.count('COMPLUMP')}") print(f" - WSEGVALV entries: {schedule_text.count('WSEGVALV')}") print(f" - WCONHIST entries: {schedule_text.count('WCONHIST')}") print(f" - WELTARG entries: {schedule_text.count('WELTARG')}") diff --git a/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index 943032e28b..dedc9115b7 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -1202,6 +1202,67 @@ class TestScheduleGeneration: assert "COMPDAT" in schedule_text + def test_perf_completion_number_triggers_complump(self, project_with_case_and_well): + """#13273 follow-up: a completion_number on add_perf_event must surface as a + COMPLUMP keyword (with that number) in the generated schedule. + """ + project, case, timeline = project_with_case_and_well + well_path = project.well_paths()[0] + + 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", + completion_number=3, + ) + + timeline.set_timestamp(timestamp="2024-01-01") + + schedule_text = timeline.generate_schedule_text( + eclipse_case=case, export_msw_for_wells=project.well_paths() + ) + + print(f"\nSchedule text for COMPLUMP test:\n{schedule_text}") + + assert "COMPLUMP" in schedule_text, ( + "Schedule should contain COMPLUMP when a perforation has a completion number" + ) + # The completion number must appear inside the COMPLUMP block (header to trailing '/'). + complump_block = schedule_text.split("COMPLUMP\n", 1)[1].split("\n/\n", 1)[0] + assert " 3 " in complump_block or complump_block.rstrip().endswith("3 /"), ( + f"Completion number 3 missing from COMPLUMP block: {complump_block!r}" + ) + + def test_perf_without_completion_number_has_no_complump( + self, project_with_case_and_well + ): + """Without a completion_number, no COMPLUMP keyword should be emitted.""" + project, case, timeline = project_with_case_and_well + well_path = project.well_paths()[0] + + 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.set_timestamp(timestamp="2024-01-01") + + schedule_text = timeline.generate_schedule_text( + eclipse_case=case, export_msw_for_wells=project.well_paths() + ) + + assert "COMPDAT" in schedule_text + assert "COMPLUMP" not in schedule_text, ( + "COMPLUMP should not appear when no completion number is set" + ) + def test_schedule_multiple_dates_in_order(self, project_with_case_and_well): """Verify schedule dates are in chronological order.""" project, case, timeline = project_with_case_and_well