mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 06:38:13 -05:00
#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:
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;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
+14
-4
@@ -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
|
||||
|
||||
Reference in new issue
Block a user