#13273 Well Event: Support completion number on perforation events for COMPLUMP

Add a completion_number parameter to the add_perf_event Python API
(AddPerfEvent scriptable method) and a matching field on
RimWellEventPerf. When a timeline perforation event is materialised into
a RimPerforationInterval, a non-zero completion number is copied across,
so it flows through the existing completion-data path onto
RigCompletionData.

The schedule generator now emits COMPLUMP alongside COMPDAT: COMPDAT
generation is folded into generateCompletionsForWell, which computes the
completion data once and builds both keywords from it via the existing
RimKeywordFactory::complumpKeyword (added in #13273). COMPLUMP only
produces records for perforations that have a completion number and is
merged across wells like COMPDAT.
This commit is contained in:
Kristian Bendiksen committed 2026-05-29 15:43:30 +02:00
1 parent fab4a4c472
commit 9515e6c277
9 files changed
+123 -19

No files matched your search

@@ -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<Opm::DeckKeyword> RicScheduleDataGenerator::generateWelspecsForWel
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<Opm::DeckKeyword> 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<QString, Opm::DeckKeyword>& keywordBlocks )
{
auto events = timeline.getEventsAtDate( date );
@@ -279,14 +277,15 @@ std::optional<Opm::DeckKeyword> 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 ) );
}
//--------------------------------------------------------------------------------------------------
@@ -68,9 +68,13 @@ private:
static std::optional<Opm::DeckKeyword>
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<Opm::DeckKeyword>
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<QString, Opm::DeckKeyword>& 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
@@ -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>( 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();
}
@@ -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<double> m_diameter;
caf::PdmField<double> m_skinFactor;
caf::PdmField<caf::AppEnum<State>> m_state;
caf::PdmField<int> m_completionNumber;
};
namespace caf
@@ -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() );
@@ -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<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addPerfEvent
event->setDiameter( m_diameter() );
event->setSkinFactor( m_skinFactor() );
event->setState( m_state() );
event->setCompletionNumber( m_completionNumber() );
return event;
}
@@ -57,6 +57,7 @@ private:
caf::PdmField<double> m_diameter;
caf::PdmField<double> m_skinFactor;
caf::PdmField<caf::AppEnum<RimWellEventPerf::State>> m_state;
caf::PdmField<int> m_completionNumber;
};
//==================================================================================================
@@ -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')}")
@@ -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