///////////////////////////////////////////////////////////////////////////////// // // 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 "CompletionExportCommands/MswExport/RicMswBranchBuilder.h" #include "RifReaderMockModel.h" #include "RigEclipseCaseData.h" #include "RigMainGrid.h" using namespace RicMswBranchBuilder; //================================================================================================== // findOutletSegmentForMD tests //================================================================================================== //-------------------------------------------------------------------------------------------------- /// Empty map returns 1 (heel segment). //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_EmptyMap ) { std::vector map; EXPECT_EQ( 1, findOutletSegmentForMD( map, 500.0 ) ); } //-------------------------------------------------------------------------------------------------- /// MD falls within the first cell's range. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInFirstCell ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 150.0 ) ); } //-------------------------------------------------------------------------------------------------- /// MD falls within the middle cell's range. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInMiddleCell ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 6, findOutletSegmentForMD( map, 250.0 ) ); } //-------------------------------------------------------------------------------------------------- /// MD falls within the last cell's range. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInLastCell ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 7, findOutletSegmentForMD( map, 350.0 ) ); } //-------------------------------------------------------------------------------------------------- /// MD exactly at the start of the first cell — shallower than the first midpoint (100), /// so no midpoint is at or below md; fallback = first (shallowest) segment. /// MD exactly at the shared boundary (200) — midpoint of first cell (150) is below 200, /// midpoint of second cell (250) is above 200; closest-below midpoint is 150 → seg 5. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDAtCellStart ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 100.0 ) ); // shallower than midpoint 150 → first seg EXPECT_EQ( 5, findOutletSegmentForMD( map, 200.0 ) ); // midpoint 150 ≤ 200 < midpoint 250 → seg 5 } //-------------------------------------------------------------------------------------------------- /// MD=200 sits between midpoint 150 (seg 5) and midpoint 250 (seg 6). /// Closest midpoint at-or-below is 150 → seg 5. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDAtCellEnd_ExclusiveBoundary ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 200.0 ) ); } //-------------------------------------------------------------------------------------------------- /// MD before all segment midpoints — no midpoint is at or below md; /// fallback = first (shallowest) segment. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDBelowAllCells ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 50.0 ) ); // shallower than all midpoints → first seg } //-------------------------------------------------------------------------------------------------- /// MD beyond the last cell returns the last cell's segment number (fallback). //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDBeyondAllCells ) { std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 7, findOutletSegmentForMD( map, 999.0 ) ); } //-------------------------------------------------------------------------------------------------- /// Single-cell map. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_SingleCell ) { std::vector map = { { 0.0, 100.0, 3, 50.0 } }; EXPECT_EQ( 3, findOutletSegmentForMD( map, 50.0 ) ); EXPECT_EQ( 3, findOutletSegmentForMD( map, 0.0 ) ); EXPECT_EQ( 3, findOutletSegmentForMD( map, 200.0 ) ); // beyond → fallback = 3 } //================================================================================================== // Segmentation helpers //================================================================================================== namespace { //-------------------------------------------------------------------------------------------------- /// Cells [0,10] [10,14] [14,30] [30,33] [33,60] from the heel at MD 0, centres 5, 12, 22, 31.5, 46.5 //-------------------------------------------------------------------------------------------------- const std::vector> exampleCells = { { 0.0, 10.0 }, { 10.0, 14.0 }, { 14.0, 30.0 }, { 30.0, 33.0 }, { 33.0, 60.0 } }; std::vector centres( const std::vector>& pieces ) { std::vector result; for ( const auto& [start, end] : pieces ) { result.push_back( 0.5 * ( start + end ) ); } return result; } void expectNodes( const std::vector& expected, const std::vector& actual ) { ASSERT_EQ( expected.size(), actual.size() ); for ( size_t i = 0; i < expected.size(); ++i ) { EXPECT_NEAR( expected[i], actual[i], 1.0e-9 ) << "node " << i; } } } // namespace //================================================================================================== // Segment node placement tests //================================================================================================== using RicMswTableDataTools::SegmentationInterval; TEST( RicMswBranchBuilder, PlaceSegmentNodes_NoRuleKeepsCellCentres ) { const auto candidates = centres( exampleCells ); expectNodes( candidates, RicMswTableDataTools::placeSegmentNodes( 0.0, candidates, {} ) ); expectNodes( candidates, RicMswTableDataTools::placeSegmentNodes( 0.0, candidates, { { 0.0, 60.0, std::nullopt, std::nullopt } } ) ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinLength ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 12.0, std::nullopt } } ); expectNodes( { 12.0, 31.5, 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MaxLength ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, std::nullopt, 10.0 } } ); expectNodes( { 5.0, 12.0, 22.0, 31.5, 39.0, 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MaxLengthInsertsSeveralNodes ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, { 5.0, 35.0 }, { { 0.0, 60.0, std::nullopt, 10.0 } } ); expectNodes( { 5.0, 15.0, 25.0, 35.0 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinLengthLargerThanIntervalKeepsOneNode ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 100.0, std::nullopt } } ); expectNodes( { 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_IntervalStartingMidCell ) { // The cell [14,30] is split at the interval start 20 const std::vector> pieces = { { 0.0, 10.0 }, { 10.0, 14.0 }, { 14.0, 20.0 }, { 20.0, 30.0 }, { 30.0, 33.0 }, { 33.0, 60.0 } }; const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( pieces ), { { 20.0, 60.0, 12.0, std::nullopt } } ); // The spacing inside the interval is measured from the last node before it, 17 expectNodes( { 5.0, 12.0, 17.0, 31.5, 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_BackToBackIntervals ) { const std::vector intervals = { { 0.0, 14.0, 12.0, std::nullopt }, { 14.0, 60.0, std::nullopt, 10.0 } }; const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), intervals ); expectNodes( { 12.0, 22.0, 31.5, 39.0, 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MeasuredFromOutletNode ) { // The outlet node at 100 is the heel of the branch, the first candidate is too close to it const auto nodes = RicMswTableDataTools::placeSegmentNodes( 100.0, { 105.0, 115.0, 125.0 }, { { 100.0, 130.0, 12.0, std::nullopt } } ); expectNodes( { 115.0 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinAndMaxLengthCombined ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 12.0, 15.0 } } ); expectNodes( { 12.0, 21.75, 31.5, 46.5 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_FixedLength ) { // Segments [0,20], [20,40], [40,60] with nodes at their centres expectNodes( { 10.0, 30.0, 50.0 }, RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, std::nullopt, std::nullopt, 20.0 } } ) ); // Cell centres outside the interval are kept, the fixed segments start at the interval start expectNodes( { 5.0, 12.0, 30.0, 50.0 }, RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 20.0, 60.0, std::nullopt, std::nullopt, 20.0 } } ) ); // The last segment is cut at the end of the branch expectNodes( { 10.0, 30.0, 42.5 }, RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 50.0, std::nullopt, std::nullopt, 20.0 } }, 45.0 ) ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_MaxLengthNotBeforeIntervalStart ) { // Heel at 0, interval [1000,1100], first candidate at 1050: no nodes are added before the interval const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, { 1050.0 }, { { 1000.0, 1100.0, std::nullopt, 25.0 } } ); expectNodes( { 1025.0, 1050.0 }, nodes ); } TEST( RicMswBranchBuilder, PlaceSegmentNodes_FixedLengthNotBeforeFirstCell ) { const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, { 1050.0 }, { { 1000.0, 1100.0, std::nullopt, std::nullopt, 20.0 } }, 1100.0, 1040.0 ); expectNodes( { 1050.0, 1070.0, 1090.0 }, nodes ); } TEST( RicMswBranchBuilder, CreateSubSegmentMDPairs_NoIntervalsKeepsWholeCell ) { const auto pairs = RicMswTableDataTools::createSubSegmentMDPairs( 10.0, 30.0, {} ); ASSERT_EQ( 1u, pairs.size() ); EXPECT_DOUBLE_EQ( 10.0, pairs[0].first ); EXPECT_DOUBLE_EQ( 30.0, pairs[0].second ); } TEST( RicMswBranchBuilder, CreateSubSegmentMDPairs_IntervalInsideCellSplitsInThree ) { const auto pairs = RicMswTableDataTools::createSubSegmentMDPairs( 10.0, 30.0, { { 15.0, 25.0 } } ); ASSERT_EQ( 3u, pairs.size() ); EXPECT_DOUBLE_EQ( 10.0, pairs[0].first ); EXPECT_DOUBLE_EQ( 15.0, pairs[0].second ); EXPECT_DOUBLE_EQ( 15.0, pairs[1].first ); EXPECT_DOUBLE_EQ( 25.0, pairs[1].second ); EXPECT_DOUBLE_EQ( 25.0, pairs[2].first ); EXPECT_DOUBLE_EQ( 30.0, pairs[2].second ); } TEST( RicMswBranchBuilder, CreateSubSegmentMDPairs_IntervalOverlappingCellIsClipped ) { const auto pairs = RicMswTableDataTools::createSubSegmentMDPairs( 10.0, 30.0, { { 0.0, 20.0 } } ); ASSERT_EQ( 2u, pairs.size() ); EXPECT_DOUBLE_EQ( 10.0, pairs[0].first ); EXPECT_DOUBLE_EQ( 20.0, pairs[0].second ); EXPECT_DOUBLE_EQ( 20.0, pairs[1].first ); EXPECT_DOUBLE_EQ( 30.0, pairs[1].second ); } TEST( RicMswBranchBuilder, CreateSubSegmentMDPairs_IntervalOutsideOrTouchingCellIsIgnored ) { for ( const auto& interval : { std::pair( 30.0, 40.0 ), std::pair( 0.0, 10.0 ), std::pair( 40.0, 50.0 ) } ) { const auto pairs = RicMswTableDataTools::createSubSegmentMDPairs( 10.0, 30.0, { interval } ); ASSERT_EQ( 1u, pairs.size() ); EXPECT_DOUBLE_EQ( 10.0, pairs[0].first ); EXPECT_DOUBLE_EQ( 30.0, pairs[0].second ); } } TEST( RicMswBranchBuilder, CreateSubSegmentMDPairs_AdjacentIntervalsShareBoundary ) { const auto pairs = RicMswTableDataTools::createSubSegmentMDPairs( 10.0, 30.0, { { 10.0, 20.0 }, { 20.0, 30.0 } } ); ASSERT_EQ( 2u, pairs.size() ); EXPECT_DOUBLE_EQ( 20.0, pairs[0].second ); EXPECT_DOUBLE_EQ( 20.0, pairs[1].first ); } TEST( RicMswBranchBuilder, CollapseNodesOutsidePerforations ) { const std::vector nodes = { 5.0, 12.0, 22.0, 31.5, 46.5, 55.0 }; expectNodes( { 10.0, 22.0, 31.5, 46.5, 55.0 }, RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, true, false ) ); expectNodes( { 5.0, 12.0, 22.0, 31.5, 50.0 }, RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, false, true ) ); expectNodes( nodes, RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, false, false ) ); // A single node outside the perforations is unchanged expectNodes( { 5.0, 22.0, 31.5 }, RicMswTableDataTools::collapseNodesOutsidePerforations( { 5.0, 22.0, 31.5 }, 0.0, 60.0, 20.0, 40.0, true, true ) ); } //================================================================================================== // Upstream node tests //================================================================================================== TEST( RicMswBranchBuilder, UpstreamNodeIndex ) { const std::vector nodes = { 12.0, 31.5, 46.5 }; EXPECT_FALSE( RicMswTableDataTools::upstreamNodeIndex( {}, 10.0 ).has_value() ); // Upstream of all nodes, the most upstream node is used EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 5.0 ) ); // Cell 3 in the example, centre 22, is closer to 31.5 but 12 is the nearest node upstream EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 22.0 ) ); EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 46.0 ) ); EXPECT_EQ( std::optional( 2 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 100.0 ) ); } TEST( RicMswBranchBuilder, UpstreamNodeIndex_NodeAtMDIsUsed ) { EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 20.0 ) ); EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 19.999 ) ); } //================================================================================================== // Nearest node tests //================================================================================================== TEST( RicMswBranchBuilder, NearestNodeIndex ) { const std::vector nodes = { 12.0, 31.5, 46.5 }; EXPECT_FALSE( RicMswTableDataTools::nearestNodeIndex( {}, 10.0 ).has_value() ); EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::nearestNodeIndex( nodes, 5.0 ) ); // Cell 3 in the example, centre 22, is 9.5 from 31.5 and 10 from 12 EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::nearestNodeIndex( nodes, 22.0 ) ); EXPECT_EQ( std::optional( 2 ), RicMswTableDataTools::nearestNodeIndex( nodes, 100.0 ) ); } TEST( RicMswBranchBuilder, NearestNodeIndex_TieGoesToFirstNode ) { EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::nearestNodeIndex( { 10.0, 20.0 }, 15.0 ) ); } //================================================================================================== // Segment span tests //================================================================================================== namespace { void expectSpans( const std::vector& expected, const std::vector& actual ) { ASSERT_EQ( expected.size(), actual.size() ); for ( size_t i = 0; i < expected.size(); ++i ) { EXPECT_NEAR( expected[i].startMD, actual[i].startMD, 1.0e-9 ) << "span " << i; EXPECT_NEAR( expected[i].endMD, actual[i].endMD, 1.0e-9 ) << "span " << i; EXPECT_EQ( expected[i].nodeIndex, actual[i].nodeIndex ) << "span " << i; } } } // namespace TEST( RicMswBranchBuilder, SegmentSpans_NoRuleOneSpanPerCell ) { expectSpans( { { 0.0, 10.0, 0 }, { 10.0, 14.0, 1 }, { 14.0, 30.0, 2 }, { 30.0, 33.0, 3 }, { 33.0, 60.0, 4 } }, segmentSpans( exampleCells, centres( exampleCells ) ) ); } TEST( RicMswBranchBuilder, SegmentSpans_MinLengthSpansSeveralCells ) { expectSpans( { { 0.0, 31.5, 0 }, { 31.5, 46.5, 1 }, { 46.5, 60.0, 2 } }, segmentSpans( exampleCells, { 12.0, 31.5, 46.5 } ) ); } TEST( RicMswBranchBuilder, SegmentSpans_MaxLengthSplitsCellsAtNodes ) { expectSpans( { { 0.0, 12.0, 0 }, { 12.0, 22.0, 1 }, { 22.0, 31.5, 2 }, { 31.5, 39.0, 3 }, { 39.0, 46.5, 4 }, { 46.5, 60.0, 5 } }, segmentSpans( exampleCells, { 5.0, 12.0, 22.0, 31.5, 39.0, 46.5 } ) ); } //================================================================================================== // toMswCellIntersection tests //================================================================================================== namespace { //-------------------------------------------------------------------------------------------------- /// Creates a minimal 2x2x3 mock grid (12 cells, IJK layout: I fast, then J, then K). //-------------------------------------------------------------------------------------------------- cvf::ref makeMockGrid() { cvf::ref caseData = new RigEclipseCaseData( nullptr ); cvf::ref reader = new RifReaderMockModel; reader->setWorldCoordinates( cvf::Vec3d( 0, 0, 0 ), cvf::Vec3d( 100, 100, 100 ) ); reader->setCellCounts( cvf::Vec3st( 2, 2, 3 ) ); reader->enableWellData( false ); reader->open( "", caseData.p() ); caseData->mainGrid()->computeCachedData(); return caseData; } //-------------------------------------------------------------------------------------------------- /// Build a minimal WellPathCellIntersectionInfo with only globCellIndex set. //-------------------------------------------------------------------------------------------------- WellPathCellIntersectionInfo makeCellInfo( size_t globCellIndex ) { WellPathCellIntersectionInfo info{}; info.globCellIndex = globCellIndex; return info; } } // anonymous namespace //-------------------------------------------------------------------------------------------------- /// A gap segment (globCellIndex >= totalCellCount) returns nullopt. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_GapSegmentReturnsNullopt ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); // 2x2x3 = 12 cells; index 12 is out of range auto result = toMswCellIntersection( makeCellInfo( 12 ), grid, 0.0, 10.0 ); EXPECT_FALSE( result.has_value() ); auto result2 = toMswCellIntersection( makeCellInfo( 999 ), grid, 0.0, 10.0 ); EXPECT_FALSE( result2.has_value() ); } //-------------------------------------------------------------------------------------------------- /// Cell index 0 is (I=0,J=0,K=0) in 0-based → (1,1,1) in 1-based. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_CellZero_OneBasedIJK ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); auto result = toMswCellIntersection( makeCellInfo( 0 ), grid, 50.0, 60.0 ); ASSERT_TRUE( result.has_value() ); EXPECT_EQ( 1u, result->i ); EXPECT_EQ( 1u, result->j ); EXPECT_EQ( 1u, result->k ); } //-------------------------------------------------------------------------------------------------- /// Distance parameters are passed through unchanged. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_DistancesPassedThrough ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); auto result = toMswCellIntersection( makeCellInfo( 0 ), grid, 123.4, 567.8 ); ASSERT_TRUE( result.has_value() ); EXPECT_DOUBLE_EQ( 123.4, result->distanceStart ); EXPECT_DOUBLE_EQ( 567.8, result->distanceEnd ); } //-------------------------------------------------------------------------------------------------- /// A main-grid cell has an empty gridName. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_MainGridHasEmptyGridName ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); auto result = toMswCellIntersection( makeCellInfo( 0 ), grid, 0.0, 1.0 ); ASSERT_TRUE( result.has_value() ); EXPECT_TRUE( result->gridName.empty() ); } //-------------------------------------------------------------------------------------------------- /// IJK indexing: for a 2x2x3 grid (nI=2, nJ=2, nK=3), globalIdx = i + j*nI + k*nI*nJ. /// Cell at (1,0,0) has globalIdx = 1 → (2,1,1) in 1-based. /// Cell at (0,1,0) has globalIdx = 2 → (1,2,1) in 1-based. /// Cell at (0,0,2) has globalIdx = 8 → (1,1,3) in 1-based. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_IJKMapping ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); // globalIdx=1: i=1,j=0,k=0 → (2,1,1) 1-based auto r1 = toMswCellIntersection( makeCellInfo( 1 ), grid, 0.0, 1.0 ); ASSERT_TRUE( r1.has_value() ); EXPECT_EQ( 2u, r1->i ); EXPECT_EQ( 1u, r1->j ); EXPECT_EQ( 1u, r1->k ); // globalIdx=2: i=0,j=1,k=0 → (1,2,1) 1-based auto r2 = toMswCellIntersection( makeCellInfo( 2 ), grid, 0.0, 1.0 ); ASSERT_TRUE( r2.has_value() ); EXPECT_EQ( 1u, r2->i ); EXPECT_EQ( 2u, r2->j ); EXPECT_EQ( 1u, r2->k ); // globalIdx=8: i=0,j=0,k=2 → (1,1,3) 1-based auto r8 = toMswCellIntersection( makeCellInfo( 8 ), grid, 0.0, 1.0 ); ASSERT_TRUE( r8.has_value() ); EXPECT_EQ( 1u, r8->i ); EXPECT_EQ( 1u, r8->j ); EXPECT_EQ( 3u, r8->k ); } //-------------------------------------------------------------------------------------------------- /// Dual-porosity: K index is shifted up by cellCountK. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, ToMswCellIntersection_DualPorosity_KShifted ) { auto caseData = makeMockGrid(); RigMainGrid* grid = caseData->mainGrid(); grid->setDualPorosity( true ); const size_t cellCountK = grid->cellCountK(); // 3 // Cell index 0: (i=0,j=0,k=0) → 1-based k = 1 + cellCountK = 4 auto result = toMswCellIntersection( makeCellInfo( 0 ), grid, 0.0, 1.0 ); ASSERT_TRUE( result.has_value() ); EXPECT_EQ( 1u + cellCountK, result->k ); } //================================================================================================== // applyEffectiveDiameters / applyIcdAreaPerCell tests //================================================================================================== namespace { //-------------------------------------------------------------------------------------------------- /// Build export data with one branch holding the given segment numbers, each with a WSEGVALV area. //-------------------------------------------------------------------------------------------------- RigMswWellExportData makeExportData( const std::vector>& segmentNumberAndArea ) { RigMswWellExportData exportData; RigMswBranch branch; branch.branchNumber = 1; for ( const auto& [segmentNumber, area] : segmentNumberAndArea ) { RigMswSegment segment; segment.segmentNumber = segmentNumber; segment.outletSegmentNumber = 1; segment.length = 1.0; segment.depth = 1.0; WsegvalvRow row; row.segmentNumber = segmentNumber; row.cv = 1.5; row.area = area; segment.wsegvalvData = row; branch.segments.push_back( segment ); } exportData.branches.push_back( branch ); return exportData; } double areaOfSegment( const RigMswWellExportData& exportData, int segmentNumber ) { for ( const auto& branch : exportData.branches ) { for ( const auto& segment : branch.segments ) { if ( segment.segmentNumber == segmentNumber && segment.wsegvalvData.has_value() ) return segment.wsegvalvData->area; } } return -1.0; } } // namespace //-------------------------------------------------------------------------------------------------- /// A single ICD sub in a cell keeps its own area. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_SingleIcdIsUnchanged ) { FishbonesExportContext context; context.icdSegments.push_back( { 10, { 100 } } ); auto exportData = makeExportData( { { 10, 0.002 } } ); applyIcdAreaPerCell( context, exportData ); EXPECT_DOUBLE_EQ( 0.002, areaOfSegment( exportData, 10 ) ); } //-------------------------------------------------------------------------------------------------- /// Two ICD subs connected to the same cell both report the sum of their areas. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_SharedCellGetsAreaSum ) { FishbonesExportContext context; context.icdSegments.push_back( { 10, { 100 } } ); context.icdSegments.push_back( { 20, { 100 } } ); auto exportData = makeExportData( { { 10, 0.002 }, { 20, 0.003 } } ); applyIcdAreaPerCell( context, exportData ); EXPECT_DOUBLE_EQ( 0.005, areaOfSegment( exportData, 10 ) ); EXPECT_DOUBLE_EQ( 0.005, areaOfSegment( exportData, 20 ) ); } //-------------------------------------------------------------------------------------------------- /// ICD subs in different cells are not combined. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_SeparateCellsAreIndependent ) { FishbonesExportContext context; context.icdSegments.push_back( { 10, { 100 } } ); context.icdSegments.push_back( { 20, { 200 } } ); auto exportData = makeExportData( { { 10, 0.002 }, { 20, 0.003 } } ); applyIcdAreaPerCell( context, exportData ); EXPECT_DOUBLE_EQ( 0.002, areaOfSegment( exportData, 10 ) ); EXPECT_DOUBLE_EQ( 0.003, areaOfSegment( exportData, 20 ) ); } //-------------------------------------------------------------------------------------------------- /// An ICD sub reaching into two cells reports the largest sum, computed from the original areas. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_MultipleCellsUseLargestSum ) { FishbonesExportContext context; context.icdSegments.push_back( { 10, { 100, 200 } } ); context.icdSegments.push_back( { 20, { 100 } } ); context.icdSegments.push_back( { 30, { 200 } } ); context.icdSegments.push_back( { 40, { 200 } } ); auto exportData = makeExportData( { { 10, 0.001 }, { 20, 0.002 }, { 30, 0.003 }, { 40, 0.004 } } ); applyIcdAreaPerCell( context, exportData ); // Cell 100 sums to 0.003, cell 200 to 0.008. Segment 10 takes part in both and reports the larger. EXPECT_DOUBLE_EQ( 0.008, areaOfSegment( exportData, 10 ) ); EXPECT_DOUBLE_EQ( 0.003, areaOfSegment( exportData, 20 ) ); EXPECT_DOUBLE_EQ( 0.008, areaOfSegment( exportData, 30 ) ); EXPECT_DOUBLE_EQ( 0.008, areaOfSegment( exportData, 40 ) ); } //-------------------------------------------------------------------------------------------------- /// The result does not depend on the order the ICD subs are recorded in. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_IsIndependentOfRecordingOrder ) { FishbonesExportContext forward; forward.icdSegments.push_back( { 10, { 100, 200 } } ); forward.icdSegments.push_back( { 20, { 100 } } ); forward.icdSegments.push_back( { 30, { 200 } } ); FishbonesExportContext reversed; reversed.icdSegments.push_back( { 30, { 200 } } ); reversed.icdSegments.push_back( { 20, { 100 } } ); reversed.icdSegments.push_back( { 10, { 200, 100 } } ); const std::vector> segments = { { 10, 0.001 }, { 20, 0.002 }, { 30, 0.003 } }; auto forwardData = makeExportData( segments ); applyIcdAreaPerCell( forward, forwardData ); auto reversedData = makeExportData( segments ); applyIcdAreaPerCell( reversed, reversedData ); for ( int segmentNumber : { 10, 20, 30 } ) { EXPECT_DOUBLE_EQ( areaOfSegment( forwardData, segmentNumber ), areaOfSegment( reversedData, segmentNumber ) ); } } //-------------------------------------------------------------------------------------------------- /// Valve segments that are not fishbones ICD subs are left alone. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, IcdAreaPerCell_NonIcdValveIsUntouched ) { FishbonesExportContext context; context.icdSegments.push_back( { 10, { 100 } } ); context.icdSegments.push_back( { 20, { 100 } } ); auto exportData = makeExportData( { { 10, 0.002 }, { 20, 0.003 }, { 30, 0.007 } } ); applyIcdAreaPerCell( context, exportData ); EXPECT_DOUBLE_EQ( 0.007, areaOfSegment( exportData, 30 ) ); } //-------------------------------------------------------------------------------------------------- /// Three laterals in the same cell give Deff = sqrt(3) * d. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, EffectiveDiameters_SharedCellCombinesDiameters ) { FishbonesExportContext context; context.laterals.push_back( { { { { 10 }, 100, 0.0096 } } } ); context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } ); context.laterals.push_back( { { { { 30 }, 100, 0.0096 } } } ); auto exportData = makeExportData( { { 10, 0.0 }, { 20, 0.0 }, { 30, 0.0 } } ); applyEffectiveDiameters( context, exportData ); const double expected = std::sqrt( 3.0 ) * 0.0096; for ( const auto& segment : exportData.branches[0].segments ) { ASSERT_TRUE( segment.diameter.has_value() ); EXPECT_NEAR( expected, *segment.diameter, 1.0e-12 ); } } //-------------------------------------------------------------------------------------------------- /// A cell intersection split into several WELSEGS rows contributes once to the sum, and all its rows /// get the same effective diameter. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, EffectiveDiameters_SplitIntersectionCountsOnce ) { FishbonesExportContext context; context.laterals.push_back( { { { { 10, 11, 12 }, 100, 0.0096 } } } ); context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } ); auto exportData = makeExportData( { { 10, 0.0 }, { 11, 0.0 }, { 12, 0.0 }, { 20, 0.0 } } ); applyEffectiveDiameters( context, exportData ); // Two intersections in cell 100, not four rows. const double expected = std::sqrt( 2.0 ) * 0.0096; for ( const auto& segment : exportData.branches[0].segments ) { ASSERT_TRUE( segment.diameter.has_value() ); EXPECT_NEAR( expected, *segment.diameter, 1.0e-12 ); } } //-------------------------------------------------------------------------------------------------- /// The first segment of a lateral inherits the effective diameter of the second segment. //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, EffectiveDiameters_FirstSegmentInheritsSecond ) { FishbonesExportContext context; // Two laterals start in cell 100, only the first one continues into cell 200. context.laterals.push_back( { { { { 10 }, 100, 0.0096 }, { { 11 }, 200, 0.0096 } } } ); context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } ); auto exportData = makeExportData( { { 10, 0.0 }, { 11, 0.0 }, { 20, 0.0 } } ); applyEffectiveDiameters( context, exportData ); const auto& segments = exportData.branches[0].segments; // Segment 10 is alone with segment 20 in cell 100, but takes the value of segment 11. EXPECT_NEAR( 0.0096, *segments[0].diameter, 1.0e-12 ); EXPECT_NEAR( 0.0096, *segments[1].diameter, 1.0e-12 ); // The second lateral has a single intersection and keeps the combined value of cell 100. EXPECT_NEAR( std::sqrt( 2.0 ) * 0.0096, *segments[2].diameter, 1.0e-12 ); }