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