mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 22:50:25 -05:00
#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:
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."""
|
||||
|
||||
Reference in new issue
Block a user