diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 79e00b42cf..d2568d0467 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -107,6 +107,10 @@ std::expected RicScheduleDataGenerator::generateSchedule( cons } } + // Segment layout (WELSEGS + COMPSEGS) last written per export well name, used to detect when a + // valve-only date changes the segment structure and the segments must be redefined. + std::map emittedSegmentLayouts; + // Generate section for each date. The first retained date may be emitted as a comment instead // of a DATES keyword when firstDateAsComment is set. A RESTART marker truncates earlier dates, // including dates that only carry an insert-date event. @@ -115,8 +119,14 @@ std::expected RicScheduleDataGenerator::generateSchedule( cons { if ( restartDate.has_value() && date < *restartDate ) continue; - auto dateSection = - generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment, alignColumns ); + auto dateSection = generateDateSection( timeline, + eclipseCase, + sortedWellPaths, + date, + mswWells, + emittedSegmentLayouts, + isFirstDate && firstDateAsComment, + alignColumns ); if ( !dateSection ) return std::unexpected( dateSection.error() ); result += *dateSection; isFirstDate = false; @@ -223,6 +233,7 @@ std::expected RicScheduleDataGenerator::generateDateSection( c const std::vector& wellPaths, const QDateTime& date, const std::set& mswWells, + std::map& emittedSegmentLayouts, bool dateAsComment, bool alignColumns ) { @@ -324,7 +335,7 @@ std::expected RicScheduleDataGenerator::generateDateSection( c generateCompletionsForWell( eclipseCase, *well, date, events, keywordBlocks ); - generateMswForWell( eclipseCase, *well, date, events, keywordBlocks, unmergedBlocks, mswWells ); + generateMswForWell( eclipseCase, *well, date, events, keywordBlocks, unmergedBlocks, mswWells, emittedSegmentLayouts ); generateWellControlForWell( *well, events, keywordBlocks ); } @@ -481,8 +492,8 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel bool hasEvents = false; for ( auto* event : eventsAtDate ) { - if ( ( event->eventType() == RimWellEvent::EventType::PERF || event->eventType() == RimWellEvent::EventType::VALVE || - event->eventType() == RimWellEvent::EventType::TUBING || event->eventType() == RimWellEvent::EventType::WELLSPEC ) && + if ( ( event->eventType() == RimWellEvent::EventType::PERF || event->eventType() == RimWellEvent::EventType::TUBING || + event->eventType() == RimWellEvent::EventType::WELLSPEC ) && event->wellName() == well.name() ) { hasEvents = true; @@ -562,7 +573,8 @@ void RicScheduleDataGenerator::generateMswForWell( RimEclipseCase& const std::vector& eventsAtDate, std::map& keywordBlocks, std::map>& unmergedBlocks, - const std::set& mswWells ) + const std::set& mswWells, + std::map& emittedSegmentLayouts ) { // MSW keywords are exported only for the wells the caller explicitly requested. if ( !mswWells.contains( &wellPath ) ) @@ -577,23 +589,23 @@ void RicScheduleDataGenerator::generateMswForWell( RimEclipseCase& } // MSW is generated only when there are valve/tubing/perforation events at this exact date. - bool hasMswEvents = false; - bool hasPerfEvents = false; + bool hasValveEvents = false; + bool hasGeometryEvents = false; // tubing or perforation changes require WELSEGS/COMPSEGS to be redefined for ( auto* event : eventsAtDate ) { if ( event->wellName() != wellPath.name() ) continue; - if ( event->eventType() == RimWellEvent::EventType::VALVE || event->eventType() == RimWellEvent::EventType::TUBING ) + if ( event->eventType() == RimWellEvent::EventType::VALVE ) { - hasMswEvents = true; + hasValveEvents = true; } - else if ( event->eventType() == RimWellEvent::EventType::PERF ) + else if ( event->eventType() == RimWellEvent::EventType::TUBING || event->eventType() == RimWellEvent::EventType::PERF ) { - hasPerfEvents = true; + hasGeometryEvents = true; } } - if ( !hasMswEvents && !hasPerfEvents ) + if ( !hasValveEvents && !hasGeometryEvents ) { return; } @@ -620,10 +632,22 @@ void RicScheduleDataGenerator::generateMswForWell( RimEclipseCase& int maxSegments = 0; int maxBranches = 0; auto welsegsKw = RimKeywordFactory::welsegsKeyword( mswData, maxSegments, maxBranches ); - appendUnmerged( "WELSEGS", std::move( welsegsKw ) ); + auto compsegsKw = RimKeywordFactory::compsegsKeyword( mswData ); - auto compsegsKw = RimKeywordFactory::compsegsKeyword( mswData ); - appendUnmerged( "COMPSEGS", std::move( compsegsKw ) ); + // Tubing and perforation changes always redefine the segments. A valve event usually only updates + // WSEGVALV/WSEGAICD, but a valve becoming active adds valve segments and remaps connections, so the + // segments are also redefined when the layout differs from the one last written for this well. + const QString segmentLayout = RimKeywordFactory::deckKeywordToString( welsegsKw ) + RimKeywordFactory::deckKeywordToString( compsegsKw ); + const QString wellName = wellPath.completionSettings() ? wellPath.completionSettings()->wellNameForExport() : wellPath.name(); + auto layoutIt = emittedSegmentLayouts.find( wellName ); + const bool layoutChanged = layoutIt == emittedSegmentLayouts.end() || layoutIt->second != segmentLayout; + + if ( hasGeometryEvents || layoutChanged ) + { + emittedSegmentLayouts[wellName] = segmentLayout; + appendUnmerged( "WELSEGS", std::move( welsegsKw ) ); + appendUnmerged( "COMPSEGS", std::move( compsegsKw ) ); + } auto wsegvalvKw = RimKeywordFactory::wsegvalvKeyword( mswData ); mergeKeyword( keywordBlocks, "WSEGVALV", std::move( wsegvalvKw ) ); diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 9a57a618b4..7ecec82057 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -84,6 +84,7 @@ private: const std::vector& wellPaths, const QDateTime& date, const std::set& mswWells, + std::map& emittedSegmentLayouts, bool dateAsComment = false, bool alignColumns = false ); @@ -105,13 +106,16 @@ private: // WSEGVALV / WSEGAICD are merged into keywordBlocks; WELSEGS / COMPSEGS cannot be merged across // wells and are appended as separate per-well blocks in unmergedBlocks. // All four keywords are emitted only when the well is present in mswWells; otherwise none are. + // WELSEGS / COMPSEGS are written for tubing/perforation events, and for valve events when the + // segment layout differs from the one last written for the well (tracked in emittedSegmentLayouts). static void generateMswForWell( RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date, const std::vector& eventsAtDate, std::map& keywordBlocks, std::map>& unmergedBlocks, - const std::set& mswWells ); + const std::set& mswWells, + std::map& emittedSegmentLayouts ); // 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/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index bd060adcc6..96dc7f96a2 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -1342,6 +1342,59 @@ class TestScheduleGeneration: f"Schedule should contain well name ({well_path.name} or {well_name_no_spaces})" ) + def test_valve_only_date_redefines_segments_when_layout_changes( + self, project_with_case_and_well + ): + """A valve activated on a later date adds valve segments, so WELSEGS/COMPSEGS must be redefined (issue #14873).""" + project, case, timeline = project_with_case_and_well + well_path = project.well_paths()[0] + + timeline.add_tubing_event( + event_date="2024-01-01", + well_path=well_path, + start_md=1500.0, + end_md=2300.0, + inner_diameter=0.22, + roughness=9.0e-5, + ) + timeline.add_perf_event( + event_date="2024-01-01", + well_path=well_path, + start_md=2000.0, + end_md=2200.0, + diameter=0.1, + skin_factor=0.5, + state="OPEN", + ) + timeline.add_valve_event( + event_date="2024-03-01", + well_path=well_path, + measured_depth=2100.0, + valve_type="ICV", + state="OPEN", + flow_coefficient=0.7, + area=0.0001, + ) + + timeline.set_timestamp(timestamp="2024-12-31") + schedule_text = timeline.generate_schedule_text( + eclipse_case=case, + export_msw_for_wells=[well_path], + first_date_as_comment=False, + ) + + sections = schedule_text.split("DATES\n") + january = [s for s in sections if "1 'JAN' 2024" in s or "1 JAN 2024" in s] + march = [s for s in sections if "1 'MAR' 2024" in s or "1 MAR 2024" in s] + assert len(january) == 1 and len(march) == 1 + + assert "WELSEGS" in january[0] + assert "WSEGVALV" in march[0] + assert "WELSEGS" in march[0], "New valve segments must be defined" + assert "COMPSEGS" in march[0], "Connections must be remapped to valve segments" + assert "WELSPECS" not in march[0] + assert "COMPDAT" not in march[0] + def test_schedule_contains_wsegvalv_keyword(self, project_with_case_and_well): """Verify WSEGVALV keyword is generated for valve events.""" project, case, timeline = project_with_case_and_well