#14873 Schedule export: valve events only emit WSEGVALV/WSEGAICD

VALVE events no longer trigger WELSPECS, COMPORD, WELSEGS and COMPSEGS.
WELSEGS/COMPSEGS are only regenerated for tubing and perforation events.

Fixes #14873
This commit is contained in:
Kristian Bendiksen committed 2026-10-12 18:47:03 +02:00
1 parent f0dc8c47f0
commit 164dee3f4b
3 files changed
+98 -17

No files matched your search

@@ -107,6 +107,10 @@ std::expected<QString, QString> 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<QString, QString> 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<QString, QString> 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<QString, QString> RicScheduleDataGenerator::generateDateSection( c
const std::vector<RimWellPath*>& wellPaths,
const QDateTime& date,
const std::set<const RimWellPath*>& mswWells,
std::map<QString, QString>& emittedSegmentLayouts,
bool dateAsComment,
bool alignColumns )
{
@@ -324,7 +335,7 @@ std::expected<QString, QString> 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<Opm::DeckKeyword> 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<RimWellEvent*>& eventsAtDate,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
std::map<QString, std::vector<Opm::DeckKeyword>>& unmergedBlocks,
const std::set<const RimWellPath*>& mswWells )
const std::set<const RimWellPath*>& mswWells,
std::map<QString, QString>& 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 ) );
@@ -84,6 +84,7 @@ private:
const std::vector<RimWellPath*>& wellPaths,
const QDateTime& date,
const std::set<const RimWellPath*>& mswWells,
std::map<QString, QString>& 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<RimWellEvent*>& eventsAtDate,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
std::map<QString, std::vector<Opm::DeckKeyword>>& unmergedBlocks,
const std::set<const RimWellPath*>& mswWells );
const std::set<const RimWellPath*>& mswWells,
std::map<QString, QString>& 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,
@@ -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