From 409f17546706d10e8fc37a94e7c16c62b125400b Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 21 Oct 2026 10:32:39 +0200 Subject: [PATCH] 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. --- ...ellPathExportCompletionDataFeatureImpl.cpp | 48 ++++++--- ...cWellPathExportCompletionDataFeatureImpl.h | 4 + .../WellEvents/RimWellEventTimeline.cpp | 4 +- ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + ...thExportCompletionDataFeatureImpl-Test.cpp | 100 ++++++++++++++++++ 5 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp index 5594af5cba..f1a26010cb 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp @@ -379,6 +379,39 @@ std::vector RicWellPathExportCompletionDataFeatureImpl::compu return completionsPerEclipseCell; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( const std::vector& completions ) +{ + // One perforation interval can cross the same cell more than once, so lengths are summed per number + std::map 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( 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(); diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h index b108bae780..b5e9e13923 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h @@ -80,6 +80,10 @@ public: static std::optional 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 completionNumberForCell( const std::vector& completions ); + private: static std::vector generatePerforationsCompdatValues( const RimWellPath* wellPath, const std::vector& intervals, diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp index 64d018a201..bcaddfe9f9 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp @@ -170,7 +170,9 @@ std::vector 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; } diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 3179148892..e1a6ca35e9 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -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) diff --git a/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp b/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp new file mode 100644 index 0000000000..3cdc0378ae --- /dev/null +++ b/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp @@ -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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h" + +#include "RigCompletionData.h" + +#include +#include + +namespace +{ +RigCompletionData makeCompletion( double startMD, double endMD, std::optional completionNumber ) +{ + RigCompletionData completion( "W", RigCompletionDataGridCell( 0, nullptr ), startMD ); + completion.setDepthRange( startMD, endMD ); + if ( completionNumber ) completion.setCompletionNumber( *completionNumber ); + return completion; +} + +std::optional numberForAllOrders( std::vector completions ) +{ + std::vector order( completions.size() ); + for ( size_t i = 0; i < order.size(); ++i ) + order[i] = i; + + std::optional first; + bool firstSet = false; + do + { + std::vector 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 ) } ) ); +}