#14079 Well Event: Keep WELSEGS and COMPSEGS per-well instead of merging

WELSEGS and COMPSEGS carry a per-well header record followed by that
well's segment records, so segments from different wells cannot share a
single header. Commit 6ef024c merged records of the same keyword across
wells, which is invalid for these two keywords.

Accumulate WELSEGS and COMPSEGS as separate per-well blocks in a
dedicated container and serialise each block individually, while the
remaining keywords (WELSPECS, COMPDAT, WSEGVALV, WSEGAICD, well-control
and schedule keywords) continue to merge. Emission preserves the
existing priority ordering.
This commit is contained in:
Kristian Bendiksen committed 2026-05-29 15:43:30 +02:00
1 parent 24269b07d5
commit fab4a4c472
3 files changed
+102 -19

No files matched your search

@@ -146,6 +146,11 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
// Records for each keyword name are accumulated across wells, then serialised once below.
std::map<QString, Opm::DeckKeyword> keywordBlocks;
// WELSEGS and COMPSEGS cannot be merged across wells: each block carries a per-well header
// record followed by that well's segment records, so they are kept as separate per-well blocks
// (keyed by keyword name, preserving well order) and serialised individually.
std::map<QString, std::vector<Opm::DeckKeyword>> unmergedBlocks;
for ( auto* well : wellPaths )
{
if ( !well ) continue;
@@ -160,7 +165,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
mergeKeyword( keywordBlocks, "COMPDAT", std::move( *compdat ) );
}
generateMswForWell( timeline, eclipseCase, *well, date, keywordBlocks, mswWells );
generateMswForWell( timeline, eclipseCase, *well, date, keywordBlocks, unmergedBlocks, mswWells );
generateWellControlForWell( timeline, *well, date, keywordBlocks );
}
@@ -184,16 +189,26 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
result += "\n";
};
auto appendUnmergedBlocks = [&]( const QString& name )
{
auto it = unmergedBlocks.find( name );
if ( it == unmergedBlocks.end() ) return;
for ( const auto& kw : it->second )
{
appendKeywordText( kw );
}
};
// Output keywords in priority order
std::set<QString> emitted;
for ( const auto& kw : keywordOrder )
{
auto it = keywordBlocks.find( kw );
if ( it != keywordBlocks.end() )
if ( auto it = keywordBlocks.find( kw ); it != keywordBlocks.end() )
{
appendKeywordText( it->second );
emitted.insert( kw );
}
appendUnmergedBlocks( kw );
emitted.insert( kw );
}
// Output remaining keywords not in the priority list
@@ -202,6 +217,14 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
if ( emitted.contains( kw ) ) continue;
appendKeywordText( deckKw );
}
for ( const auto& [kw, blocks] : unmergedBlocks )
{
if ( emitted.contains( kw ) ) continue;
for ( const auto& deckKw : blocks )
{
appendKeywordText( deckKw );
}
}
return result;
}
@@ -269,12 +292,13 @@ std::optional<Opm::DeckKeyword> RicScheduleDataGenerator::generateCompdatForWell
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& timeline,
RimEclipseCase& eclipseCase,
RimWellPath& wellPath,
const QDateTime& date,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
const std::set<const RimWellPath*>& mswWells )
void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline& timeline,
RimEclipseCase& eclipseCase,
RimWellPath& wellPath,
const QDateTime& date,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
std::map<QString, std::vector<Opm::DeckKeyword>>& unmergedBlocks,
const std::set<const RimWellPath*>& mswWells )
{
// MSW keywords are exported only for the wells the caller explicitly requested.
if ( !mswWells.contains( &wellPath ) )
@@ -322,13 +346,21 @@ void RicScheduleDataGenerator::generateMswForWell( const RimWellEventTimeline&
const auto& mswData = mswDataResult.value();
// WELSEGS and COMPSEGS are not merged across wells (see generateDateSection): each well keeps
// its own block, appended in well order.
auto appendUnmerged = [&unmergedBlocks]( const QString& name, Opm::DeckKeyword kw )
{
if ( kw.size() == 0 && !kw.isDataKeyword() ) return;
unmergedBlocks[name].push_back( std::move( kw ) );
};
int maxSegments = 0;
int maxBranches = 0;
auto welsegsKw = RimKeywordFactory::welsegsKeyword( mswData, maxSegments, maxBranches );
mergeKeyword( keywordBlocks, "WELSEGS", std::move( welsegsKw ) );
appendUnmerged( "WELSEGS", std::move( welsegsKw ) );
auto compsegsKw = RimKeywordFactory::compsegsKeyword( mswData );
mergeKeyword( keywordBlocks, "COMPSEGS", std::move( compsegsKw ) );
appendUnmerged( "COMPSEGS", std::move( compsegsKw ) );
auto wsegvalvKw = RimKeywordFactory::wsegvalvKeyword( mswData );
mergeKeyword( keywordBlocks, "WSEGVALV", std::move( wsegvalvKw ) );
@@ -72,14 +72,17 @@ private:
static std::optional<Opm::DeckKeyword>
generateCompdatForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date );
// Generate WELSEGS / COMPSEGS / WSEGVALV / WSEGAICD for a well at a specific date, merging into the accumulator.
// Generate WELSEGS / COMPSEGS / WSEGVALV / WSEGAICD for a well at a specific date.
// 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.
static void generateMswForWell( const RimWellEventTimeline& timeline,
RimEclipseCase& eclipseCase,
RimWellPath& well,
const QDateTime& date,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
const std::set<const RimWellPath*>& mswWells );
static void generateMswForWell( const RimWellEventTimeline& timeline,
RimEclipseCase& eclipseCase,
RimWellPath& well,
const QDateTime& date,
std::map<QString, Opm::DeckKeyword>& keywordBlocks,
std::map<QString, std::vector<Opm::DeckKeyword>>& unmergedBlocks,
const std::set<const RimWellPath*>& mswWells );
// Generate well control / well keyword event keywords for a well at a specific date, merging into the accumulator
static void generateWellControlForWell( const RimWellEventTimeline& timeline,
@@ -1455,6 +1455,54 @@ class TestScheduleGeneration:
f"Well {wp.name!r} missing from grouped WELSPECS block: {welspecs_block!r}"
)
def test_welsegs_compsegs_not_merged_across_wells(self, project_with_case_and_well):
"""WELSEGS and COMPSEGS carry a per-well header record and therefore cannot
be merged across wells: each MSW well must get its own keyword block on the
same date, while the other keywords stay grouped.
"""
project, case, timeline = project_with_case_and_well
well_paths = project.well_paths()
assert len(well_paths) >= 2, (
"Test requires at least two well paths in the fixture"
)
for wp in well_paths[:2]:
timeline.add_tubing_event(
event_date="2024-01-01",
well_path=wp,
start_md=0.0,
end_md=2500.0,
inner_diameter=0.15,
roughness=1.0e-5,
)
timeline.add_perf_event(
event_date="2024-01-01",
well_path=wp,
start_md=2000.0,
end_md=2200.0,
diameter=0.1,
state="OPEN",
)
timeline.set_timestamp(timestamp="2024-12-31")
schedule_text = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=well_paths[:2]
)
print(f"\nSchedule text for unmerged MSW keywords:\n{schedule_text}")
# One WELSEGS and one COMPSEGS header per MSW well (not merged into one block).
assert schedule_text.count("WELSEGS\n") == 2, (
f"WELSEGS should appear once per well; got {schedule_text.count('WELSEGS')}:\n{schedule_text}"
)
assert schedule_text.count("COMPSEGS\n") == 2, (
f"COMPSEGS should appear once per well; got {schedule_text.count('COMPSEGS')}:\n{schedule_text}"
)
# COMPDAT, by contrast, stays grouped under a single header.
assert schedule_text.count("COMPDAT\n") == 1, (
f"COMPDAT should stay grouped under one header; got {schedule_text.count('COMPDAT')}:\n{schedule_text}"
)
class TestKeywordEvents:
"""Tests for well keyword event functionality."""