mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
#14870 Schedule export: export multilateral wells once per well name
Wells sharing an export name now produce one WELSPECS/COMPORD record, one set of MSW keywords, and no duplicate COMPDAT/COMPLUMP connections. Events on any well in the group trigger the group's keywords. Connections in cells shared by several well paths of a group are combined into one record. MSW keywords are exported for the well tree of the first group member selected for MSW export. Fixes #14870
This commit is contained in:
1 parent
164dee3f4b
commit
f3d9d04281
5 files changed
+262
-48
No files matched your search
@@ -28,6 +28,7 @@
|
||||
#include "ProjectDataModel/Jobs/RimKeywordFactory.h"
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimKeywordEvent.h"
|
||||
#include "RimWellEvent.h"
|
||||
#include "RimWellEventRawText.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellEventWellSpec.h"
|
||||
@@ -51,6 +52,20 @@ bool shouldEmitKeyword( const Opm::DeckKeyword& keyword )
|
||||
{
|
||||
return keyword.size() > 0 || keyword.isDataKeyword() || RifEventKeywordFormatter::isRecordlessKeyword( keyword );
|
||||
}
|
||||
|
||||
QString exportNameForWell( const RimWellPath& well )
|
||||
{
|
||||
return well.completionSettings() ? well.completionSettings()->wellNameForExport() : well.name();
|
||||
}
|
||||
|
||||
// Wells in a multilateral group share one export name, so an event on any member applies to the whole group.
|
||||
bool eventBelongsToWell( const RimWellEvent& event, const RimWellPath& well )
|
||||
{
|
||||
if ( event.wellName() == well.name() ) return true;
|
||||
|
||||
const RimWellPath* eventWell = event.wellPath();
|
||||
return eventWell && exportNameForWell( *eventWell ) == exportNameForWell( well );
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -321,22 +336,53 @@ std::expected<QString, QString> RicScheduleDataGenerator::generateDateSection( c
|
||||
// (keyed by keyword name, preserving well order) and serialised individually.
|
||||
std::map<QString, std::vector<Opm::DeckKeyword>> unmergedBlocks;
|
||||
|
||||
for ( auto* well : wellPaths )
|
||||
// Wells sharing an export name (multilateral wells) are one well in the deck, so WELSPECS/COMPORD, COMPDAT and
|
||||
// the MSW keywords are generated once per export name. Groups keep the export-name order of wellPaths, and
|
||||
// top-level wells come first within a group so that a group is represented by its main well.
|
||||
std::vector<std::vector<RimWellPath*>> wellGroups;
|
||||
{
|
||||
if ( !well ) continue;
|
||||
|
||||
if ( auto welspecs = generateWelspecsForWell( timeline, eclipseCase, *well, date, events ) )
|
||||
std::map<QString, size_t> groupIndex;
|
||||
for ( auto* well : wellPaths )
|
||||
{
|
||||
mergeKeyword( keywordBlocks, "WELSPECS", std::move( *welspecs ) );
|
||||
mergeKeyword( keywordBlocks,
|
||||
"COMPORD",
|
||||
RimKeywordFactory::compordKeyword( well->completionSettings()->wellNameForExport().toStdString() ) );
|
||||
if ( !well ) continue;
|
||||
|
||||
auto [it, inserted] = groupIndex.try_emplace( exportNameForWell( *well ), wellGroups.size() );
|
||||
if ( inserted ) wellGroups.emplace_back();
|
||||
wellGroups[it->second].push_back( well );
|
||||
}
|
||||
|
||||
generateCompletionsForWell( eclipseCase, *well, date, events, keywordBlocks );
|
||||
for ( auto& group : wellGroups )
|
||||
{
|
||||
std::stable_partition( group.begin(), group.end(), []( const RimWellPath* w ) { return w->isTopLevelWellPath(); } );
|
||||
}
|
||||
}
|
||||
|
||||
generateMswForWell( eclipseCase, *well, date, events, keywordBlocks, unmergedBlocks, mswWells, emittedSegmentLayouts );
|
||||
generateWellControlForWell( *well, events, keywordBlocks );
|
||||
for ( const auto& group : wellGroups )
|
||||
{
|
||||
for ( auto* well : group )
|
||||
{
|
||||
if ( auto welspecs = generateWelspecsForWell( timeline, eclipseCase, *well, date, events ) )
|
||||
{
|
||||
mergeKeyword( keywordBlocks, "WELSPECS", std::move( *welspecs ) );
|
||||
mergeKeyword( keywordBlocks, "COMPORD", RimKeywordFactory::compordKeyword( exportNameForWell( *well ).toStdString() ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
generateCompletionsForWellGroup( eclipseCase, group, date, events, keywordBlocks );
|
||||
|
||||
// MSW keywords describe the whole well tree, so they are generated from the top-level well of the first
|
||||
// group member selected for MSW export.
|
||||
auto mswIt = std::find_if( group.begin(), group.end(), [&mswWells]( const RimWellPath* w ) { return mswWells.contains( w ); } );
|
||||
if ( mswIt != group.end() )
|
||||
{
|
||||
generateMswForWell( eclipseCase, *( *mswIt )->topLevelWellPath(), date, events, keywordBlocks, unmergedBlocks, emittedSegmentLayouts );
|
||||
}
|
||||
|
||||
for ( auto* well : group )
|
||||
{
|
||||
generateWellControlForWell( *well, events, keywordBlocks );
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule-level keyword events (not tied to a specific well)
|
||||
@@ -494,7 +540,7 @@ std::optional<Opm::DeckKeyword> RicScheduleDataGenerator::generateWelspecsForWel
|
||||
{
|
||||
if ( ( event->eventType() == RimWellEvent::EventType::PERF || event->eventType() == RimWellEvent::EventType::TUBING ||
|
||||
event->eventType() == RimWellEvent::EventType::WELLSPEC ) &&
|
||||
event->wellName() == well.name() )
|
||||
eventBelongsToWell( *event, well ) )
|
||||
{
|
||||
hasEvents = true;
|
||||
break;
|
||||
@@ -507,7 +553,7 @@ std::optional<Opm::DeckKeyword> RicScheduleDataGenerator::generateWelspecsForWel
|
||||
const RimWellEventWellSpec* latestWellSpec = nullptr;
|
||||
for ( auto* event : timeline.events() )
|
||||
{
|
||||
if ( event->eventType() != RimWellEvent::EventType::WELLSPEC || event->wellName() != well.name() ) continue;
|
||||
if ( event->eventType() != RimWellEvent::EventType::WELLSPEC || !eventBelongsToWell( *event, well ) ) continue;
|
||||
|
||||
auto* wellSpec = dynamic_cast<RimWellEventWellSpec*>( event );
|
||||
if ( !wellSpec ) continue;
|
||||
@@ -537,16 +583,20 @@ std::optional<Opm::DeckKeyword> RicScheduleDataGenerator::generateWelspecsForWel
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicScheduleDataGenerator::generateCompletionsForWell( RimEclipseCase& eclipseCase,
|
||||
RimWellPath& well,
|
||||
const QDateTime& date,
|
||||
const std::vector<RimWellEvent*>& eventsAtDate,
|
||||
std::map<QString, Opm::DeckKeyword>& keywordBlocks )
|
||||
void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellGroup,
|
||||
const QDateTime& date,
|
||||
const std::vector<RimWellEvent*>& eventsAtDate,
|
||||
std::map<QString, Opm::DeckKeyword>& keywordBlocks )
|
||||
{
|
||||
if ( wellGroup.empty() ) return;
|
||||
|
||||
const RimWellPath& representative = *wellGroup.front();
|
||||
|
||||
bool hasPerfEvents = false;
|
||||
for ( auto* event : eventsAtDate )
|
||||
{
|
||||
if ( event->eventType() == RimWellEvent::EventType::PERF && event->wellName() == well.name() )
|
||||
if ( event->eventType() == RimWellEvent::EventType::PERF && eventBelongsToWell( *event, representative ) )
|
||||
{
|
||||
hasPerfEvents = true;
|
||||
break;
|
||||
@@ -555,10 +605,20 @@ void RicScheduleDataGenerator::generateCompletionsForWell( RimEclipseCase&
|
||||
|
||||
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();
|
||||
// A connection must be exported once per well name. Completion data is computed for every distinct well tree
|
||||
// in the group (a top-level well includes its laterals) and combined per cell, so connections shared by several
|
||||
// well paths get their contributions combined instead of being duplicated or dropped.
|
||||
std::vector<RimWellPath*> topLevelWells;
|
||||
for ( auto* well : wellGroup )
|
||||
{
|
||||
auto* topLevel = well->topLevelWellPath();
|
||||
if ( std::find( topLevelWells.begin(), topLevelWells.end(), topLevel ) == topLevelWells.end() ) topLevelWells.push_back( topLevel );
|
||||
}
|
||||
|
||||
// Completion data 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::completionDataForWellPaths( topLevelWells, &eclipseCase, date );
|
||||
auto wellName = exportNameForWell( representative ).toStdString();
|
||||
|
||||
mergeKeyword( keywordBlocks, "COMPDAT", RimKeywordFactory::compdatKeyword( compdata, wellName ) );
|
||||
mergeKeyword( keywordBlocks, "COMPLUMP", RimKeywordFactory::complumpKeyword( compdata, wellName ) );
|
||||
@@ -573,15 +633,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,
|
||||
std::map<QString, QString>& emittedSegmentLayouts )
|
||||
{
|
||||
// MSW keywords are exported only for the wells the caller explicitly requested.
|
||||
if ( !mswWells.contains( &wellPath ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* segmentCollection = wellPath.segmentCollection();
|
||||
if ( !segmentCollection )
|
||||
{
|
||||
@@ -593,7 +646,7 @@ void RicScheduleDataGenerator::generateMswForWell( RimEclipseCase&
|
||||
bool hasGeometryEvents = false; // tubing or perforation changes require WELSEGS/COMPSEGS to be redefined
|
||||
for ( auto* event : eventsAtDate )
|
||||
{
|
||||
if ( event->wellName() != wellPath.name() ) continue;
|
||||
if ( !eventBelongsToWell( *event, wellPath ) ) continue;
|
||||
|
||||
if ( event->eventType() == RimWellEvent::EventType::VALVE )
|
||||
{
|
||||
@@ -638,7 +691,7 @@ void RicScheduleDataGenerator::generateMswForWell( RimEclipseCase&
|
||||
// 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();
|
||||
const QString wellName = exportNameForWell( wellPath );
|
||||
auto layoutIt = emittedSegmentLayouts.find( wellName );
|
||||
const bool layoutChanged = layoutIt == emittedSegmentLayouts.end() || layoutIt->second != segmentLayout;
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ public:
|
||||
// Generate schedule for multiple wells at specified dates.
|
||||
// mswWells lists the wells for which the multi-segment-well keywords (WELSEGS, COMPSEGS,
|
||||
// WSEGVALV, WSEGAICD) are exported. Wells not in the set get no MSW keywords; an empty set
|
||||
// suppresses MSW export for all wells.
|
||||
// suppresses MSW export for all wells. Well paths sharing an export name are one well in the deck:
|
||||
// selecting any of them exports the MSW keywords once, for the well tree of the first selected one.
|
||||
// When firstDateAsComment is true, the first (earliest) date is written as a comment line
|
||||
// instead of a DATES keyword, since some commercial simulators reject a DATES entry that
|
||||
// equals the simulation start date. Later dates are always emitted as DATES keywords.
|
||||
@@ -94,18 +95,19 @@ private:
|
||||
const QDateTime& date,
|
||||
const std::vector<RimWellEvent*>& eventsAtDate );
|
||||
|
||||
// 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( RimEclipseCase& eclipseCase,
|
||||
RimWellPath& well,
|
||||
const QDateTime& date,
|
||||
const std::vector<RimWellEvent*>& eventsAtDate,
|
||||
std::map<QString, Opm::DeckKeyword>& keywordBlocks );
|
||||
// Generate COMPDAT (and COMPLUMP, when perforations carry a completion number) for a group of well paths sharing
|
||||
// one export name at a specific date based on events, merging both into the accumulator. Connections in cells
|
||||
// shared by several well paths in the group are combined into one record.
|
||||
static void generateCompletionsForWellGroup( RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellGroup,
|
||||
const QDateTime& date,
|
||||
const std::vector<RimWellEvent*>& eventsAtDate,
|
||||
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
|
||||
// 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.
|
||||
// The caller decides which wells get MSW keywords (see mswWells in generateSchedule).
|
||||
// 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,
|
||||
@@ -114,7 +116,6 @@ private:
|
||||
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,
|
||||
std::map<QString, QString>& emittedSegmentLayouts );
|
||||
|
||||
// Generate well control / well keyword event keywords for a well at a specific date, merging into the accumulator
|
||||
|
||||
+26
-6
@@ -65,6 +65,7 @@
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
@@ -1383,6 +1384,19 @@ void RicWellPathExportCompletionDataFeatureImpl::exportCarfinForTemporaryLgrs( c
|
||||
std::vector<RigCompletionData> RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPath( RimWellPath* wellPath,
|
||||
RimEclipseCase* eCase,
|
||||
const std::optional<QDateTime>& exportDate )
|
||||
{
|
||||
if ( wellPath == nullptr ) return {};
|
||||
|
||||
return completionDataForWellPaths( { wellPath }, eCase, exportDate );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RigCompletionData>
|
||||
RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPaths( const std::vector<RimWellPath*>& wellPaths,
|
||||
RimEclipseCase* eCase,
|
||||
const std::optional<QDateTime>& exportDate )
|
||||
{
|
||||
if ( eCase == nullptr ) return {};
|
||||
|
||||
@@ -1404,14 +1418,20 @@ std::vector<RigCompletionData> RicWellPathExportCompletionDataFeatureImpl::compl
|
||||
|
||||
std::vector<RigCompletionData> completions;
|
||||
|
||||
// Completions from all given well paths (and their laterals) are combined per cell, so that cells shared
|
||||
// by several well paths are exported as a single connection.
|
||||
std::vector<RimWellPath*> allWellPathLaterals;
|
||||
if ( wellPath->unitSystem() == eCase->eclipseCaseData()->unitsType() )
|
||||
for ( auto* wellPath : wellPaths )
|
||||
{
|
||||
allWellPathLaterals = wellPath->allWellPathLaterals();
|
||||
}
|
||||
else
|
||||
{
|
||||
return {};
|
||||
if ( !wellPath || wellPath->unitSystem() != eCase->eclipseCaseData()->unitsType() ) continue;
|
||||
|
||||
for ( auto* lateral : wellPath->allWellPathLaterals() )
|
||||
{
|
||||
if ( std::find( allWellPathLaterals.begin(), allWellPathLaterals.end(), lateral ) == allWellPathLaterals.end() )
|
||||
{
|
||||
allWellPathLaterals.push_back( lateral );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::map<size_t, std::vector<RigCompletionData>> completionsPerEclipseCellAllCompletionTypes;
|
||||
|
||||
+5
@@ -67,6 +67,11 @@ public:
|
||||
static std::vector<RigCompletionData>
|
||||
completionDataForWellPath( RimWellPath* wellPath, RimEclipseCase* eCase, const std::optional<QDateTime>& exportDate = std::nullopt );
|
||||
|
||||
// Completion data for several well paths (including their laterals), combined per cell into one connection.
|
||||
static std::vector<RigCompletionData> completionDataForWellPaths( const std::vector<RimWellPath*>& wellPaths,
|
||||
RimEclipseCase* eCase,
|
||||
const std::optional<QDateTime>& exportDate = std::nullopt );
|
||||
|
||||
static std::pair<double, cvf::Vec2i>
|
||||
wellPathUpperGridIntersectionIJ( const RimEclipseCase* gridCase, const RimWellPath* wellPath, const QString& gridName = "" );
|
||||
|
||||
|
||||
@@ -316,6 +316,141 @@ class TestScheduleGeneration:
|
||||
assert "2024" in schedule_text, "Schedule should contain the event date"
|
||||
assert "WELSEGS" in schedule_text, "Schedule should contain MSW WELSEGS keyword"
|
||||
|
||||
def test_generate_schedule_shared_well_name_exported_once(
|
||||
self, project_with_case_and_well
|
||||
):
|
||||
"""Wells sharing an export name (multilateral) give one WELSPECS/COMPORD record and unique COMPDAT cells (issue #14870)."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
for index, well_path in enumerate(project.well_paths()):
|
||||
well_path.name = "OP5" if index == 0 else "OP5_Y2"
|
||||
well_path.update()
|
||||
settings = well_path.completion_settings()
|
||||
settings.well_name_for_export = "OP5"
|
||||
settings.update()
|
||||
|
||||
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.set_timestamp(timestamp="2024-12-31")
|
||||
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=[],
|
||||
first_date_as_comment=False,
|
||||
)
|
||||
|
||||
assert (
|
||||
len(re.findall(r"^\s*'OP5'\s+'", schedule_text.split("COMPORD")[0], re.M))
|
||||
== 1
|
||||
)
|
||||
assert schedule_text.count("'OP5' 'INPUT'") == 1
|
||||
|
||||
compdat = schedule_text.split("COMPDAT")[1]
|
||||
cells = re.findall(r"'OP5'\s+(\d+\s+\d+\s+\d+)", compdat)
|
||||
assert cells and len(cells) == len(set(cells))
|
||||
|
||||
@staticmethod
|
||||
def _two_wells_sharing_export_name(project, timeline, with_tubing=False):
|
||||
"""Import a second copy of well path A (renamed) and perforate both copies identically."""
|
||||
well_path_coll = project.descendants(rips.WellPathCollection)[0]
|
||||
well_path_coll.import_well_path(
|
||||
file_name=dataroot.PATH + "/TEST10K_FLT_LGR_NNC/wellpath_a.dev"
|
||||
)
|
||||
|
||||
well_paths = [wp for wp in project.well_paths() if wp.name == "Well Path A"]
|
||||
assert len(well_paths) == 2
|
||||
for index, well_path in enumerate(well_paths):
|
||||
well_path.name = f"Well Path A{index + 1}"
|
||||
well_path.update()
|
||||
for well_path in well_paths:
|
||||
if with_tubing:
|
||||
timeline.add_tubing_event(
|
||||
event_date="2024-01-01",
|
||||
well_path=well_path,
|
||||
start_md=1500.0,
|
||||
end_md=2300.0,
|
||||
inner_diameter=0.15,
|
||||
roughness=1.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.set_timestamp(timestamp="2024-12-31")
|
||||
return well_paths
|
||||
|
||||
@staticmethod
|
||||
def _set_export_names(well_paths, names):
|
||||
for well_path, name in zip(well_paths, names):
|
||||
settings = well_path.completion_settings()
|
||||
settings.well_name_for_export = name
|
||||
settings.update()
|
||||
|
||||
@staticmethod
|
||||
def _compdat_transmissibilities(schedule_text, well_name):
|
||||
"""Map 'I J K' -> connection transmissibility for the COMPDAT records of a well."""
|
||||
compdat = schedule_text.split("COMPDAT")[1].split("\n/")[0]
|
||||
pattern = rf"'{well_name}'\s+(\d+ \d+ \d+) \d+ '\w+' \S+ (\S+)"
|
||||
return {cell: float(trans) for cell, trans in re.findall(pattern, compdat)}
|
||||
|
||||
def test_shared_well_name_combines_overlapping_connections(
|
||||
self, project_with_case_and_well
|
||||
):
|
||||
"""Cells shared by well paths with the same export name are combined, not dropped (issue #14870)."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
well_paths = self._two_wells_sharing_export_name(project, timeline)
|
||||
|
||||
self._set_export_names(well_paths, ["OPA1", "OPA2"])
|
||||
separate = timeline.generate_schedule_text(
|
||||
eclipse_case=case, export_msw_for_wells=[], first_date_as_comment=False
|
||||
)
|
||||
single = self._compdat_transmissibilities(separate, "OPA1")
|
||||
assert single and single == self._compdat_transmissibilities(separate, "OPA2")
|
||||
|
||||
self._set_export_names(well_paths, ["OP5", "OP5"])
|
||||
shared = timeline.generate_schedule_text(
|
||||
eclipse_case=case, export_msw_for_wells=[], first_date_as_comment=False
|
||||
)
|
||||
combined = self._compdat_transmissibilities(shared, "OP5")
|
||||
compdat = shared.split("COMPDAT")[1].split("\n/")[0]
|
||||
assert len(re.findall(r"^\s*'OP5'", compdat, re.M)) == len(single)
|
||||
assert combined.keys() == single.keys()
|
||||
for cell, trans in single.items():
|
||||
assert combined[cell] == pytest.approx(2.0 * trans, rel=1e-6)
|
||||
|
||||
@pytest.mark.parametrize("selected_index", [0, 1])
|
||||
def test_shared_well_name_msw_exported_for_any_selected_member(
|
||||
self, project_with_case_and_well, selected_index
|
||||
):
|
||||
"""MSW for a well name is exported once when any member is selected, not only the first (issue #14870)."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
well_paths = self._two_wells_sharing_export_name(
|
||||
project, timeline, with_tubing=True
|
||||
)
|
||||
self._set_export_names(well_paths, ["OP5", "OP5"])
|
||||
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=[well_paths[selected_index]],
|
||||
first_date_as_comment=False,
|
||||
)
|
||||
|
||||
assert schedule_text.count("WELSEGS") == 1
|
||||
assert schedule_text.count("COMPSEGS") == 1
|
||||
|
||||
def test_generate_schedule_preserves_time_of_day(self, project_with_case_and_well):
|
||||
"""Event timestamps with a time-of-day must be preserved in the exported DATES keyword (issue #14111)."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
Reference in new issue
Block a user