mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
#14963: Order COMPDAT connections mainbore-first, then laterals
Completions could interleave mainbore and lateral connections, since the final sort ordered by well name, completion type and measured depth, and each lateral has its own independent MD scale. Connections for a well's mainbore should all come before connections for its side steps. Added RicScheduleDataGenerator::sortCompletionsMainBoreFirst(), which groups completions by owning well path (via RigCompletionData::sourcePdmObject()) and emits them in the traversal order of RimWellPath::allWellPathLaterals(), mainbore first. Sibling laterals are ordered by tie-in measured depth, as allWellPathLaterals() itself is not deterministic. The sort is applied in completionDataForWellPaths() so schedule export, COMPDAT/COMPLUMP export and the gRPC/Python query all get the same ordering.
This commit is contained in:
1 parent
6e8e658f6d
commit
c04edc9d39
8 files changed
+228
-5
No files matched your search
@@ -16,8 +16,8 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicWellPathExportMswGeometryPath.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicMswBranchBuilder.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicMswTableDataTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicExportScheduleFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicTransmissibilityCalculator.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicPerforationCellFilterEvaluator.cpp
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "RicWellPathExportCompletionDataFeatureImpl.h"
|
||||
#include "RicWellPathExportMswTableData.h"
|
||||
|
||||
#include "RigCompletionData.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
|
||||
#include "RifEventKeywordFormatter.h"
|
||||
@@ -36,6 +38,7 @@
|
||||
#include "RimWellEventWellSpec.h"
|
||||
#include "RimWellPath.h"
|
||||
#include "RimWellPathCompletionSettings.h"
|
||||
#include "RimWellPathTieIn.h"
|
||||
|
||||
#include "opm/input/eclipse/Deck/DeckKeyword.hpp"
|
||||
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
|
||||
@@ -45,6 +48,7 @@
|
||||
#include <QStringList>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
@@ -678,14 +682,90 @@ void RicScheduleDataGenerator::generateCompletionsForWellGroup( RimEclipseCase*
|
||||
}
|
||||
|
||||
// Completion data feeds both COMPDAT and COMPLUMP (the latter only emits records whose perforation has a
|
||||
// completion number assigned). Both keywords merge across wells.
|
||||
// completion number assigned). Both keywords merge across wells. completionDataForWellPaths() already orders
|
||||
// mainbore connections before each lateral's (see RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPaths()).
|
||||
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 ) );
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Appends the connections (if any) belonging to `well`, then recurses depth-first into its laterals.
|
||||
/// Sibling laterals are visited in tie-in measured depth order: RimWellPath::wellPathLaterals() orders
|
||||
/// siblings by an internal pointer-based multiset and does not guarantee a stable, well-tree-derived
|
||||
/// order between them.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void appendCompletionsDepthFirst( const RimWellPath* well,
|
||||
std::map<const RimWellPath*, std::vector<RigCompletionData>>& completionsByWell,
|
||||
std::vector<RigCompletionData>& sortedCompdata )
|
||||
{
|
||||
auto it = completionsByWell.find( well );
|
||||
if ( it != completionsByWell.end() )
|
||||
{
|
||||
for ( const auto& completion : it->second )
|
||||
sortedCompdata.push_back( completion );
|
||||
completionsByWell.erase( it );
|
||||
}
|
||||
|
||||
std::vector<RimWellPath*> laterals = well->wellPathLaterals();
|
||||
std::stable_sort( laterals.begin(),
|
||||
laterals.end(),
|
||||
[]( const RimWellPath* a, const RimWellPath* b )
|
||||
{
|
||||
double mdA = a->wellPathTieIn() ? a->wellPathTieIn()->tieInMeasuredDepth() : 0.0;
|
||||
double mdB = b->wellPathTieIn() ? b->wellPathTieIn()->tieInMeasuredDepth() : 0.0;
|
||||
return mdA < mdB;
|
||||
} );
|
||||
|
||||
for ( auto* lateral : laterals )
|
||||
appendCompletionsDepthFirst( lateral, completionsByWell, sortedCompdata );
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicScheduleDataGenerator::sortCompletionsMainBoreFirst( const std::vector<RimWellPath*>& topLevelWells,
|
||||
std::vector<RigCompletionData>& compdata )
|
||||
{
|
||||
auto wellOf = []( const RigCompletionData& completion ) -> const RimWellPath*
|
||||
{
|
||||
const caf::PdmObject* sourceObject = completion.sourcePdmObject();
|
||||
return sourceObject ? sourceObject->firstAncestorOrThisOfType<RimWellPath>() : nullptr;
|
||||
};
|
||||
|
||||
// Group connections by well, keeping each well's original relative order (e.g. measured depth along
|
||||
// that well). Connections whose well cannot be determined end up under the nullptr key.
|
||||
std::map<const RimWellPath*, std::vector<RigCompletionData>> completionsByWell;
|
||||
for ( const auto& completion : compdata )
|
||||
{
|
||||
completionsByWell[wellOf( completion )].push_back( completion );
|
||||
}
|
||||
|
||||
// Visit wells mainbore-first, depth-first: each top-level well, then its laterals in tie-in
|
||||
// measured depth order.
|
||||
std::vector<RigCompletionData> sortedCompdata;
|
||||
sortedCompdata.reserve( compdata.size() );
|
||||
for ( auto* topLevel : topLevelWells )
|
||||
{
|
||||
appendCompletionsDepthFirst( topLevel, completionsByWell, sortedCompdata );
|
||||
}
|
||||
|
||||
// Append any remaining connections (unknown well) last, in their original relative order.
|
||||
for ( const auto& [well, completions] : completionsByWell )
|
||||
{
|
||||
for ( const auto& completion : completions )
|
||||
sortedCompdata.push_back( completion );
|
||||
}
|
||||
|
||||
compdata = std::move( sortedCompdata );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
class RigCompletionData;
|
||||
class RimEclipseCase;
|
||||
class RimWellPath;
|
||||
class RimWellEvent;
|
||||
@@ -81,6 +82,15 @@ public:
|
||||
bool firstDateAsComment = true,
|
||||
bool alignColumns = false );
|
||||
|
||||
// Reorder completions so all connections for a well's mainbore come before connections for its side steps
|
||||
// (laterals), matching ResInsight's internal completion ordering (see
|
||||
// https://github.com/OPM/ResInsight/issues/14963). Connections are otherwise left in the relative order
|
||||
// produced by RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPaths(), which already
|
||||
// combines connections sharing a grid cell into a single record and applies this ordering itself.
|
||||
// A connection whose owning well path cannot be determined (e.g. unexpected source object) is left
|
||||
// after all connections with a known well.
|
||||
static void sortCompletionsMainBoreFirst( const std::vector<RimWellPath*>& topLevelWells, std::vector<RigCompletionData>& compdata );
|
||||
|
||||
private:
|
||||
// Generate schedule section for a single date. When dateAsComment is true, the date is
|
||||
// written as a comment line instead of a DATES keyword.
|
||||
@@ -103,7 +113,8 @@ private:
|
||||
|
||||
// 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.
|
||||
// shared by several well paths in the group are combined into one record. Records are ordered with the
|
||||
// mainbore's connections first, followed by each lateral's (see completionDataForWellPaths()).
|
||||
static void generateCompletionsForWellGroup( RimEclipseCase* eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellGroup,
|
||||
const QDateTime& date,
|
||||
|
||||
+6
-1
@@ -29,6 +29,7 @@
|
||||
#include "RicExportCompletionDataSettingsUi.h"
|
||||
#include "RicExportFractureCompletionsImpl.h"
|
||||
#include "RicFishbonesTransmissibilityCalculationFeatureImp.h"
|
||||
#include "RicScheduleDataGenerator.h"
|
||||
#include "RicTransmissibilityCalculator.h"
|
||||
#include "RicWellPathExportCompletionsFileTools.h"
|
||||
#include "RicWellPathExportMswCompletionsImpl.h"
|
||||
@@ -1519,7 +1520,11 @@ std::vector<RigCompletionData>
|
||||
completions.push_back( combineEclipseCellCompletions( data.second, exportSettings ) );
|
||||
}
|
||||
|
||||
std::sort( completions.begin(), completions.end() );
|
||||
// Order completions with each well's mainbore connections first, followed by each lateral's
|
||||
// (see https://github.com/OPM/ResInsight/issues/14963), instead of the plain well
|
||||
// name/completion type/measured depth ordering from RigCompletionData::operator<(), since each
|
||||
// lateral has its own independent MD scale.
|
||||
RicScheduleDataGenerator::sortCompletionsMainBoreFirst( wellPaths, completions );
|
||||
|
||||
// make sure we set per-connection D-factors, must be done after combining completions
|
||||
for ( auto& completion : completions )
|
||||
|
||||
+3
@@ -68,6 +68,9 @@ public:
|
||||
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.
|
||||
// Connections are ordered with each well path's mainbore connections first, followed by each lateral's
|
||||
// (depth-first), matching ResInsight's internal completion ordering (see
|
||||
// https://github.com/OPM/ResInsight/issues/14963).
|
||||
static std::vector<RigCompletionData> completionDataForWellPaths( const std::vector<RimWellPath*>& wellPaths,
|
||||
RimEclipseCase* eCase,
|
||||
const std::optional<QDateTime>& exportDate = std::nullopt );
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#include "RimcWellEventTimeline.h"
|
||||
|
||||
#include "CompletionExportCommands/RicScheduleDataGenerator.h"
|
||||
#include "Commands/CompletionExportCommands/RicScheduleDataGenerator.h"
|
||||
#include "RiaOpmKeywordTools.h"
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimKeywordEvent.h"
|
||||
|
||||
@@ -209,6 +209,7 @@ set(SOURCE_UNITTEST_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicMswBranchBuilder-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigMswTableData-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigAllGridCellsResultAccessor-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator-Test.cpp
|
||||
)
|
||||
|
||||
if(RESINSIGHT_ENABLE_GRPC)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026 Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "Commands/CompletionExportCommands/RicScheduleDataGenerator.h"
|
||||
|
||||
#include "RigCompletionData.h"
|
||||
|
||||
#include "RimPerforationCollection.h"
|
||||
#include "RimPerforationInterval.h"
|
||||
#include "RimWellPath.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
RigCompletionData makeCompletion( const QString& wellName, size_t globalCellIndex, double md, const RimPerforationInterval* source )
|
||||
{
|
||||
RigCompletionData completion( wellName, RigCompletionDataGridCell( globalCellIndex, nullptr ), md );
|
||||
completion.setSourcePdmObject( source );
|
||||
return completion;
|
||||
}
|
||||
|
||||
RimPerforationInterval* addPerforation( RimWellPath& well )
|
||||
{
|
||||
auto* interval = new RimPerforationInterval;
|
||||
well.perforationIntervalCollection()->appendPerforation( interval );
|
||||
return interval;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Completions produced for a well's mainbore and its lateral can be interleaved by measured depth
|
||||
/// (see #14963). sortCompletionsMainBoreFirst() must restore mainbore-first ordering while keeping
|
||||
/// the relative order of connections belonging to the same well.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
TEST( RicScheduleDataGeneratorTest, MainBoreConnectionsSortBeforeLateralConnections )
|
||||
{
|
||||
RimWellPath mainBore;
|
||||
RimWellPath lateral;
|
||||
lateral.connectWellPaths( &mainBore, 10.0 );
|
||||
|
||||
auto* mainBorePerf1 = addPerforation( mainBore );
|
||||
auto* mainBorePerf2 = addPerforation( mainBore );
|
||||
auto* lateralPerf1 = addPerforation( lateral );
|
||||
auto* lateralPerf2 = addPerforation( lateral );
|
||||
|
||||
// Deliberately interleaved by measured depth, as produced by the plain MD-based sort.
|
||||
std::vector<RigCompletionData> compdata = { makeCompletion( "WELL", 1, 15.0, lateralPerf1 ),
|
||||
makeCompletion( "WELL", 2, 20.0, mainBorePerf1 ),
|
||||
makeCompletion( "WELL", 3, 25.0, lateralPerf2 ),
|
||||
makeCompletion( "WELL", 4, 30.0, mainBorePerf2 ) };
|
||||
|
||||
RicScheduleDataGenerator::sortCompletionsMainBoreFirst( { &mainBore }, compdata );
|
||||
|
||||
ASSERT_EQ( 4u, compdata.size() );
|
||||
EXPECT_EQ( mainBorePerf1, compdata[0].sourcePdmObject() );
|
||||
EXPECT_EQ( mainBorePerf2, compdata[1].sourcePdmObject() );
|
||||
EXPECT_EQ( lateralPerf1, compdata[2].sourcePdmObject() );
|
||||
EXPECT_EQ( lateralPerf2, compdata[3].sourcePdmObject() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A connection whose owning well path cannot be determined (e.g. no source object) must not be
|
||||
/// dropped; it is sorted after all connections with a known well.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
TEST( RicScheduleDataGeneratorTest, ConnectionsWithUnknownWellAreSortedLast )
|
||||
{
|
||||
RimWellPath mainBore;
|
||||
auto* mainBorePerf = addPerforation( mainBore );
|
||||
|
||||
std::vector<RigCompletionData> compdata = { makeCompletion( "WELL", 1, 5.0, nullptr ), makeCompletion( "WELL", 2, 10.0, mainBorePerf ) };
|
||||
|
||||
RicScheduleDataGenerator::sortCompletionsMainBoreFirst( { &mainBore }, compdata );
|
||||
|
||||
ASSERT_EQ( 2u, compdata.size() );
|
||||
EXPECT_EQ( mainBorePerf, compdata[0].sourcePdmObject() );
|
||||
EXPECT_EQ( nullptr, compdata[1].sourcePdmObject() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Several laterals on the same mainbore are ordered depth-first, matching
|
||||
/// RimWellPath::allWellPathLaterals().
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
TEST( RicScheduleDataGeneratorTest, MultipleLateralsAreOrderedDepthFirst )
|
||||
{
|
||||
RimWellPath mainBore;
|
||||
RimWellPath lateral1;
|
||||
RimWellPath lateral2;
|
||||
lateral1.connectWellPaths( &mainBore, 10.0 );
|
||||
lateral2.connectWellPaths( &mainBore, 20.0 );
|
||||
|
||||
auto* mainBorePerf = addPerforation( mainBore );
|
||||
auto* lateral1Perf = addPerforation( lateral1 );
|
||||
auto* lateral2Perf = addPerforation( lateral2 );
|
||||
|
||||
std::vector<RigCompletionData> compdata = { makeCompletion( "WELL", 1, 5.0, lateral2Perf ),
|
||||
makeCompletion( "WELL", 2, 10.0, lateral1Perf ),
|
||||
makeCompletion( "WELL", 3, 1.0, mainBorePerf ) };
|
||||
|
||||
RicScheduleDataGenerator::sortCompletionsMainBoreFirst( { &mainBore }, compdata );
|
||||
|
||||
ASSERT_EQ( 3u, compdata.size() );
|
||||
EXPECT_EQ( mainBorePerf, compdata[0].sourcePdmObject() );
|
||||
EXPECT_EQ( lateral1Perf, compdata[1].sourcePdmObject() );
|
||||
EXPECT_EQ( lateral2Perf, compdata[2].sourcePdmObject() );
|
||||
}
|
||||
Reference in new issue
Block a user