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 ) } ) ); +}