COMPLUMP: Choose completion number by largest overlap in cell

When several perforation intervals cut the same cell, the completion number
was taken from the first contribution, which depended on the order events
were applied. Unrelated event changes (e.g. group tree members) could then
change COMPLUMP numbers on boundary cells.

Pick the number with the largest summed MD overlap in the cell, with ties
going to the lowest number. Also use stable_sort for same-date events so
they keep file order.
This commit is contained in:
Kristian Bendiksen committed 2026-10-21 14:43:07 +02:00
1 parent 66c2cd47d3
commit 409f175467
5 files changed
+144 -13

No files matched your search

@@ -379,6 +379,39 @@ std::vector<RigCompletionData> RicWellPathExportCompletionDataFeatureImpl::compu
return completionsPerEclipseCell;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<int> RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( const std::vector<RigCompletionData>& completions )
{
// One perforation interval can cross the same cell more than once, so lengths are summed per number
std::map<int, double> overlapPerNumber;
for ( const auto& completion : completions )
{
const auto number = completion.completionNumber();
if ( !number.has_value() ) continue;
double length = 0.0;
if ( completion.startMD().has_value() && completion.endMD().has_value() )
{
length = std::max( 0.0, completion.endMD().value() - completion.startMD().value() );
}
overlapPerNumber[number.value()] += length;
}
if ( overlapPerNumber.empty() ) return std::nullopt;
// Iterating in ascending number order and requiring a strictly larger length makes ties go to the lowest number
const double tolerance = 1.0e-6;
auto best = overlapPerNumber.begin();
for ( auto it = std::next( best ); it != overlapPerNumber.end(); ++it )
{
if ( it->second > best->second + tolerance ) best = it;
}
return best->first;
}
//==================================================================================================
///
//==================================================================================================
@@ -409,9 +442,8 @@ RigCompletionData RicWellPathExportCompletionDataFeatureImpl::combineEclipseCell
auto isValidTransmissibility = []( double transmissibility )
{ return RigStatisticsTools::isValidNumber<double>( transmissibility ) && transmissibility >= 0.0; };
auto startMD = completions[0].startMD();
auto endMD = completions[0].endMD();
auto completionNumber = completions[0].completionNumber();
auto startMD = completions[0].startMD();
auto endMD = completions[0].endMD();
for ( const RigCompletionData& completion : completions )
{
@@ -439,14 +471,6 @@ RigCompletionData RicWellPathExportCompletionDataFeatureImpl::combineEclipseCell
}
}
if ( !completionNumber.has_value() )
{
if ( completion.completionNumber().has_value() )
{
completionNumber = completion.completionNumber();
}
}
double transmissibility = completion.transmissibility();
if ( !isValidTransmissibility( transmissibility ) )
@@ -473,7 +497,7 @@ RigCompletionData RicWellPathExportCompletionDataFeatureImpl::combineEclipseCell
{
resultCompletion.setDepthRange( startMD.value(), endMD.value() );
}
if ( completionNumber.has_value() ) resultCompletion.setCompletionNumber( completionNumber.value() );
if ( auto completionNumber = completionNumberForCell( completions ) ) resultCompletion.setCompletionNumber( completionNumber.value() );
double combinedDiameter = diameterCalculator.weightedMean();
double combinedSkinFactor = skinFactorCalculator.weightedMean();
@@ -80,6 +80,10 @@ public:
static std::optional<QDateTime> exportDateForTimeStep( const RimEclipseCase* eclipseCase, size_t timeStepIndex );
// Completion number for a cell with several contributions: the number with the largest summed MD overlap
// (endMD - startMD) wins, ties go to the lowest number. Independent of contribution order.
static std::optional<int> completionNumberForCell( const std::vector<RigCompletionData>& completions );
private:
static std::vector<RigCompletionData> generatePerforationsCompdatValues( const RimWellPath* wellPath,
const std::vector<const RimPerforationInterval*>& intervals,
@@ -170,7 +170,9 @@ std::vector<RimWellEvent*> RimWellEventTimeline::getEventsUpToDate( const QDateT
}
// Sort by date
std::sort( result.begin(), result.end(), []( const RimWellEvent* a, const RimWellEvent* b ) { return a->eventDate() < b->eventDate(); } );
std::stable_sort( result.begin(),
result.end(),
[]( const RimWellEvent* a, const RimWellEvent* b ) { return a->eventDate() < b->eventDate(); } );
return result;
}
@@ -211,6 +211,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RigMswTableData-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigAllGridCellsResultAccessor-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportCompletionDataFeatureImpl-Test.cpp
)
if(RESINSIGHT_ENABLE_GRPC)
@@ -0,0 +1,100 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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/RicWellPathExportCompletionDataFeatureImpl.h"
#include "RigCompletionData.h"
#include <algorithm>
#include <optional>
namespace
{
RigCompletionData makeCompletion( double startMD, double endMD, std::optional<int> completionNumber )
{
RigCompletionData completion( "W", RigCompletionDataGridCell( 0, nullptr ), startMD );
completion.setDepthRange( startMD, endMD );
if ( completionNumber ) completion.setCompletionNumber( *completionNumber );
return completion;
}
std::optional<int> numberForAllOrders( std::vector<RigCompletionData> completions )
{
std::vector<size_t> order( completions.size() );
for ( size_t i = 0; i < order.size(); ++i )
order[i] = i;
std::optional<int> first;
bool firstSet = false;
do
{
std::vector<RigCompletionData> permuted;
for ( size_t i : order )
permuted.push_back( completions[i] );
auto number = RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( permuted );
if ( !firstSet )
{
first = number;
firstSet = true;
}
EXPECT_EQ( first, number );
} while ( std::next_permutation( order.begin(), order.end() ) );
return first;
}
} // namespace
//--------------------------------------------------------------------------------------------------
/// A cell cut by two perforation intervals gets the number of the interval with the largest overlap,
/// regardless of the order the contributions arrive in.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberUsesLargestOverlap )
{
EXPECT_EQ( 2, numberForAllOrders( { makeCompletion( 100.0, 101.0, 1 ), makeCompletion( 101.0, 105.0, 2 ) } ) );
EXPECT_EQ( 1, numberForAllOrders( { makeCompletion( 100.0, 104.0, 1 ), makeCompletion( 104.0, 105.0, 2 ) } ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberTieGoesToLowestNumber )
{
EXPECT_EQ( 3, numberForAllOrders( { makeCompletion( 100.0, 102.0, 5 ), makeCompletion( 102.0, 104.0, 3 ) } ) );
}
//--------------------------------------------------------------------------------------------------
/// The same interval can cross a cell more than once; its lengths are summed.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberSumsOverlapPerNumber )
{
EXPECT_EQ( 1,
numberForAllOrders(
{ makeCompletion( 100.0, 102.0, 1 ), makeCompletion( 102.0, 105.0, 2 ), makeCompletion( 110.0, 112.0, 1 ) } ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberIgnoresContributionsWithoutNumber )
{
EXPECT_EQ( 4, numberForAllOrders( { makeCompletion( 100.0, 110.0, std::nullopt ), makeCompletion( 110.0, 111.0, 4 ) } ) );
EXPECT_EQ( std::nullopt, numberForAllOrders( { makeCompletion( 100.0, 110.0, std::nullopt ) } ) );
}