From c04edc9d396bd6618c185201399310ee6ceeb545 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Tue, 20 Oct 2026 13:06:00 +0200 Subject: [PATCH] #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. --- .../CMakeLists_files.cmake | 2 +- .../RicScheduleDataGenerator.cpp | 82 +++++++++++- .../RicScheduleDataGenerator.h | 13 +- ...ellPathExportCompletionDataFeatureImpl.cpp | 7 +- ...cWellPathExportCompletionDataFeatureImpl.h | 3 + .../RimcWellEventTimeline.cpp | 2 +- ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../RicScheduleDataGenerator-Test.cpp | 123 ++++++++++++++++++ 8 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RicScheduleDataGenerator-Test.cpp diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake index 6bec041b14..f9103b5ac5 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake @@ -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 ) diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 9ffb8e3421..58bf829a0f 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.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 #include +#include #include #include @@ -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>& completionsByWell, + std::vector& 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 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& topLevelWells, + std::vector& compdata ) +{ + auto wellOf = []( const RigCompletionData& completion ) -> const RimWellPath* + { + const caf::PdmObject* sourceObject = completion.sourcePdmObject(); + return sourceObject ? sourceObject->firstAncestorOrThisOfType() : 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> 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 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 ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 4fa27af2e5..e44aceb15f 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -27,6 +27,7 @@ #include #include +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& topLevelWells, std::vector& 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& wellGroup, const QDateTime& date, diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp index 7c56d55229..5594af5cba 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.cpp @@ -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 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 ) diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h index d5de74a65c..b108bae780 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h @@ -68,6 +68,9 @@ public: completionDataForWellPath( RimWellPath* wellPath, RimEclipseCase* eCase, const std::optional& 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 completionDataForWellPaths( const std::vector& wellPaths, RimEclipseCase* eCase, const std::optional& exportDate = std::nullopt ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index fa585ef4e8..404bfd047e 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -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" diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index d244ed2fd8..cbc6ba8bcf 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -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) diff --git a/ApplicationLibCode/UnitTests/RicScheduleDataGenerator-Test.cpp b/ApplicationLibCode/UnitTests/RicScheduleDataGenerator-Test.cpp new file mode 100644 index 0000000000..07897b12d1 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RicScheduleDataGenerator-Test.cpp @@ -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 +// 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 + +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 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 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 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() ); +}