CompletionExport: Add unit tests, test models, and diagrams for MSW export

This commit is contained in:
Magne Sjaastad
2026-03-27 14:47:32 +01:00
parent 8d0c3f340b
commit 9b95d89d5c
13 changed files with 12805 additions and 2 deletions
+7 -1
View File
@@ -133,6 +133,10 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculation-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigWellTargetMappingTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportMswGeometryPath-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicMswBranchBuilder-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigMswTableData-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportMswTableData-Test.cpp
)
if(RESINSIGHT_ENABLE_GRPC)
@@ -211,7 +215,9 @@ target_include_directories(
ResInsight-tests
PUBLIC ${CMAKE_BINARY_DIR}/Generated
"$<TARGET_PROPERTY:ApplicationLibCode,PUBLIC_INCLUDE_DIRECTORIES>"
${RI_PRIVATE_INCLUDES} ${PROJECT_SOURCE_DIR}/Commands
${RI_PRIVATE_INCLUDES}
${PROJECT_SOURCE_DIR}/Commands
${PROJECT_SOURCE_DIR}/Commands/CompletionExportCommands
)
target_compile_features(ResInsight-tests PRIVATE cxx_std_20)
@@ -0,0 +1,260 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// 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<CellSegmentEntry> map;
EXPECT_EQ( 1, findOutletSegmentForMD( map, 500.0 ) );
}
//--------------------------------------------------------------------------------------------------
/// MD falls within the first cell's range.
//--------------------------------------------------------------------------------------------------
TEST( RicMswBranchBuilder, FindOutlet_MDInFirstCell )
{
std::vector<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
EXPECT_EQ( 5, findOutletSegmentForMD( map, 150.0 ) );
}
//--------------------------------------------------------------------------------------------------
/// MD falls within the middle cell's range.
//--------------------------------------------------------------------------------------------------
TEST( RicMswBranchBuilder, FindOutlet_MDInMiddleCell )
{
std::vector<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
EXPECT_EQ( 6, findOutletSegmentForMD( map, 250.0 ) );
}
//--------------------------------------------------------------------------------------------------
/// MD falls within the last cell's range.
//--------------------------------------------------------------------------------------------------
TEST( RicMswBranchBuilder, FindOutlet_MDInLastCell )
{
std::vector<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
EXPECT_EQ( 7, findOutletSegmentForMD( map, 999.0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Single-cell map.
//--------------------------------------------------------------------------------------------------
TEST( RicMswBranchBuilder, FindOutlet_SingleCell )
{
std::vector<CellSegmentEntry> map = { { 0.0, 100.0, 3 } };
EXPECT_EQ( 3, findOutletSegmentForMD( map, 50.0 ) );
EXPECT_EQ( 3, findOutletSegmentForMD( map, 0.0 ) );
EXPECT_EQ( 3, findOutletSegmentForMD( map, 200.0 ) ); // beyond → fallback = 3
}
//==================================================================================================
// toMswCellIntersection tests
//==================================================================================================
namespace
{
//--------------------------------------------------------------------------------------------------
/// Creates a minimal 2x2x3 mock grid (12 cells, IJK layout: I fast, then J, then K).
//--------------------------------------------------------------------------------------------------
cvf::ref<RigEclipseCaseData> makeMockGrid()
{
cvf::ref<RigEclipseCaseData> caseData = new RigEclipseCaseData( nullptr );
cvf::ref<RifReaderMockModel> 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 );
}
@@ -0,0 +1,419 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h"
#include "CompletionsMsw/RigMswSegment.h"
#include "CompletionsMsw/RigMswTableData.h"
#include "CompletionsMsw/RigMswTableRows.h"
#include "RiaDefines.h"
namespace
{
//--------------------------------------------------------------------------------------------------
/// Build a minimal valid RigMswSegment with no intersections or valve data.
//--------------------------------------------------------------------------------------------------
RigMswSegment makeSegment( int segNum, int outletSegNum, double length = 10.0, double depth = 5.0 )
{
RigMswSegment seg;
seg.segmentNumber = segNum;
seg.outletSegmentNumber = outletSegNum;
seg.length = length;
seg.depth = depth;
seg.diameter = 0.15;
seg.roughness = 1.0e-5;
seg.description = "test segment";
seg.sourceWellName = "TestWell";
return seg;
}
//--------------------------------------------------------------------------------------------------
/// Build a minimal RigMswBranch with the given branch number and segments.
//--------------------------------------------------------------------------------------------------
RigMswBranch makeBranch( int branchNum, std::vector<RigMswSegment> segs )
{
return RigMswBranch{ branchNum, std::nullopt, std::move( segs ) };
}
//--------------------------------------------------------------------------------------------------
/// Build a minimal valid WelsegsHeader.
//--------------------------------------------------------------------------------------------------
WelsegsHeader makeHeader( const std::string& wellName = "TestWell" )
{
WelsegsHeader hdr;
hdr.well = wellName;
hdr.topDepth = 100.0;
hdr.topLength = 200.0;
hdr.infoType = "INC";
return hdr;
}
} // anonymous namespace
//--------------------------------------------------------------------------------------------------
/// Empty segment list — returns valid RigMswTableData with no rows; header well name is set.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, EmptySegmentList )
{
RigMswWellExportData exportData;
exportData.header = makeHeader( "Well_A" );
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
EXPECT_EQ( "Well_A", result.wellName() );
EXPECT_EQ( "Well_A", result.welsegsHeader().well );
EXPECT_TRUE( result.welsegsData().empty() );
EXPECT_TRUE( result.compsegsData().empty() );
EXPECT_TRUE( result.wsegvalvData().empty() );
EXPECT_TRUE( result.wsegaicdData().empty() );
EXPECT_TRUE( result.wsegsicdData().empty() );
EXPECT_TRUE( result.mswBranches().empty() );
}
//--------------------------------------------------------------------------------------------------
/// Single segment with no intersections and no valves — 1 WELSEGS row, 0 COMPSEGS.
/// Verify all mapped fields.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, SingleSegmentNoIntersections )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 2, 1, 25.0, 12.5 );
seg.description = "main bore";
seg.sourceWellName = "WP_1";
exportData.branches = { makeBranch( 1, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.welsegsData().size() );
EXPECT_TRUE( result.compsegsData().empty() );
EXPECT_TRUE( result.wsegvalvData().empty() );
const WelsegsRow& row = result.welsegsData()[0];
EXPECT_EQ( 2, row.segment1 );
EXPECT_EQ( 2, row.segment2 );
EXPECT_EQ( 1, row.branch );
EXPECT_EQ( 1, row.joinSegment );
EXPECT_DOUBLE_EQ( 25.0, row.length );
EXPECT_DOUBLE_EQ( 12.5, row.depth );
EXPECT_TRUE( row.diameter.has_value() );
EXPECT_DOUBLE_EQ( 0.15, *row.diameter );
EXPECT_TRUE( row.roughness.has_value() );
EXPECT_DOUBLE_EQ( 1.0e-5, *row.roughness );
EXPECT_EQ( "main bore", row.description );
EXPECT_EQ( "WP_1", row.sourceWellName );
}
//--------------------------------------------------------------------------------------------------
/// Single segment with multiple cell intersections — COMPSEGS count equals intersection count.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, SegmentWithMultipleIntersections )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 2, 1 );
RigMswCellIntersection ci1{ 3, 5, 7, 100.0, 110.0, "" };
RigMswCellIntersection ci2{ 3, 5, 8, 110.0, 120.0, "" };
RigMswCellIntersection ci3{ 4, 5, 8, 120.0, 130.0, "" };
seg.intersections = { ci1, ci2, ci3 };
exportData.branches = { makeBranch( 1, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.welsegsData().size() );
ASSERT_EQ( 3u, result.compsegsData().size() );
const CompsegsRow& r0 = result.compsegsData()[0];
EXPECT_EQ( 3u, r0.i );
EXPECT_EQ( 5u, r0.j );
EXPECT_EQ( 7u, r0.k );
EXPECT_EQ( 1, r0.branch );
EXPECT_DOUBLE_EQ( 100.0, r0.distanceStart );
EXPECT_DOUBLE_EQ( 110.0, r0.distanceEnd );
EXPECT_TRUE( r0.isMainGrid() );
const CompsegsRow& r2 = result.compsegsData()[2];
EXPECT_EQ( 4u, r2.i );
EXPECT_EQ( 5u, r2.j );
EXPECT_EQ( 8u, r2.k );
}
//--------------------------------------------------------------------------------------------------
/// Main-grid vs LGR intersection — LGR row has non-empty gridName.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, MainGridAndLgrIntersections )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 2, 1 );
RigMswCellIntersection mainGridCell{ 1, 2, 3, 50.0, 60.0, "" };
RigMswCellIntersection lgrCell{ 4, 5, 6, 60.0, 70.0, "LGR_NEAR_WELL" };
seg.intersections = { mainGridCell, lgrCell };
exportData.branches = { makeBranch( 1, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 2u, result.compsegsData().size() );
const CompsegsRow& main = result.compsegsData()[0];
EXPECT_TRUE( main.isMainGrid() );
EXPECT_TRUE( main.gridName.empty() );
const CompsegsRow& lgr = result.compsegsData()[1];
EXPECT_TRUE( lgr.isLgrGrid() );
EXPECT_EQ( "LGR_NEAR_WELL", lgr.gridName );
EXPECT_EQ( 4u, lgr.i );
EXPECT_EQ( 5u, lgr.j );
EXPECT_EQ( 6u, lgr.k );
}
//--------------------------------------------------------------------------------------------------
/// Segment with wsegvalvData — 1 WSEGVALV row added; no WSEGAICD or WSEGSICD rows.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, SegmentWithWsegvalvData )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 3, 2 );
WsegvalvRow wv;
wv.well = "TestWell";
wv.segmentNumber = 3;
wv.cv = 0.75;
wv.area = 1.2e-4;
wv.status = "OPEN";
wv.description = "ICD valve";
seg.wsegvalvData = wv;
exportData.branches = { makeBranch( 2, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.welsegsData().size() );
ASSERT_EQ( 1u, result.wsegvalvData().size() );
EXPECT_TRUE( result.wsegaicdData().empty() );
EXPECT_TRUE( result.wsegsicdData().empty() );
const WsegvalvRow& row = result.wsegvalvData()[0];
EXPECT_EQ( "TestWell", row.well );
EXPECT_EQ( 3, row.segmentNumber );
EXPECT_DOUBLE_EQ( 0.75, row.cv );
EXPECT_DOUBLE_EQ( 1.2e-4, row.area );
ASSERT_TRUE( row.status.has_value() );
EXPECT_EQ( "OPEN", *row.status );
}
//--------------------------------------------------------------------------------------------------
/// Segment with wsegaicdData — 1 WSEGAICD row added; no WSEGVALV or WSEGSICD rows.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, SegmentWithWsegaicdData )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 4, 2 );
WsegaicdRow aicd;
aicd.well = "TestWell";
aicd.segment1 = 4;
aicd.segment2 = 4;
aicd.strength = 1.5e-5;
aicd.maxAbsRate = 1000.0;
aicd.flowRateExponent = 0.9;
aicd.viscExponent = 0.1;
seg.wsegaicdData = aicd;
exportData.branches = { makeBranch( 2, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.welsegsData().size() );
EXPECT_TRUE( result.wsegvalvData().empty() );
ASSERT_EQ( 1u, result.wsegaicdData().size() );
EXPECT_TRUE( result.wsegsicdData().empty() );
const WsegaicdRow& row = result.wsegaicdData()[0];
EXPECT_EQ( "TestWell", row.well );
EXPECT_EQ( 4, row.segment1 );
EXPECT_DOUBLE_EQ( 1.5e-5, row.strength );
}
//--------------------------------------------------------------------------------------------------
/// Segment with wsegsicdData — 1 WSEGSICD row added; no WSEGVALV or WSEGAICD rows.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, SegmentWithWsegsicdData )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 5, 3 );
WsegsicdRow sicd;
sicd.well = "TestWell";
sicd.segment1 = 5;
sicd.segment2 = 5;
sicd.strength = 2.0e-5;
sicd.maxAbsRate = 500.0;
seg.wsegsicdData = sicd;
exportData.branches = { makeBranch( 3, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.welsegsData().size() );
EXPECT_TRUE( result.wsegvalvData().empty() );
EXPECT_TRUE( result.wsegaicdData().empty() );
ASSERT_EQ( 1u, result.wsegsicdData().size() );
const WsegsicdRow& row = result.wsegsicdData()[0];
EXPECT_EQ( "TestWell", row.well );
EXPECT_EQ( 5, row.segment1 );
EXPECT_DOUBLE_EQ( 2.0e-5, row.strength );
}
//--------------------------------------------------------------------------------------------------
/// Multiple segments — WELSEGS count equals segment count;
/// COMPSEGS count equals total intersections across all segments.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, MultipleSegments )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg1 = makeSegment( 2, 1 );
seg1.intersections = { RigMswCellIntersection{ 1, 1, 1, 0.0, 10.0, "" }, RigMswCellIntersection{ 1, 1, 2, 10.0, 20.0, "" } };
RigMswSegment seg2 = makeSegment( 3, 2 );
seg2.intersections = { RigMswCellIntersection{ 2, 1, 2, 20.0, 30.0, "" } };
RigMswSegment seg3 = makeSegment( 4, 3 );
// No intersections
exportData.branches = { makeBranch( 1, { seg1, seg2, seg3 } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
EXPECT_EQ( 3u, result.welsegsData().size() );
EXPECT_EQ( 3u, result.compsegsData().size() ); // 2 + 1 + 0
ASSERT_EQ( 1u, result.mswBranches().size() );
EXPECT_EQ( 3u, result.mswBranches()[0].segments.size() );
}
//--------------------------------------------------------------------------------------------------
/// Segment numbering and outlet segment — joinSegment in WelsegsRow matches outletSegmentNumber.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, OutletSegmentNumberMapping )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
// Simulate a chain: 2->1 (heel), 3->2, 4->3
RigMswSegment seg2 = makeSegment( 2, 1 );
RigMswSegment seg3 = makeSegment( 3, 2 );
RigMswSegment seg4 = makeSegment( 4, 3 );
exportData.branches = { makeBranch( 1, { seg2, seg3, seg4 } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 3u, result.welsegsData().size() );
EXPECT_EQ( 2, result.welsegsData()[0].segment1 );
EXPECT_EQ( 1, result.welsegsData()[0].joinSegment );
EXPECT_EQ( 3, result.welsegsData()[1].segment1 );
EXPECT_EQ( 2, result.welsegsData()[1].joinSegment );
EXPECT_EQ( 4, result.welsegsData()[2].segment1 );
EXPECT_EQ( 3, result.welsegsData()[2].joinSegment );
}
//--------------------------------------------------------------------------------------------------
/// segment1 == segment2 in each WELSEGS row (single-segment entries).
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, Segment1EqualsSegment2 )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
exportData.branches = { makeBranch( 1, { makeSegment( 2, 1 ) } ),
makeBranch( 2, { makeSegment( 5, 2 ) } ),
makeBranch( 3, { makeSegment( 9, 5 ) } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
for ( const auto& row : result.welsegsData() )
{
EXPECT_EQ( row.segment1, row.segment2 );
}
}
//--------------------------------------------------------------------------------------------------
/// COMPSEGS branch number is inherited from the parent segment's branchNumber.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, CompsegsInheritsBranchFromSegment )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment seg = makeSegment( 3, 2 ); // branch comes from the enclosing RigMswBranch (branchNumber=7)
seg.intersections = { RigMswCellIntersection{ 1, 2, 3, 10.0, 20.0, "" }, RigMswCellIntersection{ 1, 2, 4, 20.0, 30.0, "" } };
exportData.branches = { makeBranch( 7, { seg } ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 2u, result.compsegsData().size() );
for ( const auto& row : result.compsegsData() )
{
EXPECT_EQ( 7, row.branch );
}
}
//--------------------------------------------------------------------------------------------------
/// Header fields (topDepth, topLength, infoType) are propagated into the result.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, HeaderFieldsPropagated )
{
WelsegsHeader hdr;
hdr.well = "DeepWell";
hdr.topDepth = 1234.5;
hdr.topLength = 2345.6;
hdr.infoType = "ABS";
RigMswWellExportData exportData;
exportData.header = hdr;
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_FIELD );
EXPECT_EQ( "DeepWell", result.welsegsHeader().well );
EXPECT_DOUBLE_EQ( 1234.5, result.welsegsHeader().topDepth );
EXPECT_DOUBLE_EQ( 2345.6, result.welsegsHeader().topLength );
EXPECT_EQ( "ABS", result.welsegsHeader().infoType );
EXPECT_EQ( RiaDefines::EclipseUnitSystem::UNITS_FIELD, result.unitSystem() );
}
@@ -0,0 +1,222 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "RiaApplication.h"
#include "RiaTestDataDirectory.h"
#include "CompletionExportCommands/RicWellPathExportMswTableData.h"
#include "CompletionsMsw/RigMswDataFormatter.h"
#include "CompletionsMsw/RigMswTableData.h"
#include "CompletionsMsw/RigMswTableRows.h"
#include "RifTextDataTableFormatter.h"
#include "RimEclipseCase.h"
#include "RimProject.h"
#include "RimWellPath.h"
#include <QDir>
#include <QFile>
#include <QTextStream>
#include <algorithm>
#include <string>
#include <tuple>
#include <vector>
namespace
{
//--------------------------------------------------------------------------------------------------
/// Write MSW table data for one well to a file, creating parent directories as needed.
//--------------------------------------------------------------------------------------------------
void writeMswTableDataToFile( const RigMswTableData& tableData, const QString& filePath )
{
QFileInfo fileInfo( filePath );
QDir().mkpath( fileInfo.absolutePath() );
QFile file( filePath );
if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) ) return;
QTextStream stream( &file );
RifTextDataTableFormatter formatter( stream );
RigMswDataFormatter::formatMswTables( formatter, tableData );
}
//--------------------------------------------------------------------------------------------------
/// Write COMPSEGL (LGR) table data to a separate file.
//--------------------------------------------------------------------------------------------------
void writeMswLgrTableDataToFile( const RigMswTableData& tableData, const QString& filePath )
{
if ( !tableData.hasLgrData() ) return;
QFileInfo fileInfo( filePath );
QDir().mkpath( fileInfo.absolutePath() );
QFile file( filePath );
if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) ) return;
QTextStream stream( &file );
RifTextDataTableFormatter formatter( stream );
RigMswDataFormatter::formatCompsegsTable( formatter, tableData, true );
}
//--------------------------------------------------------------------------------------------------
/// Extract {i, j, k, gridName} tuples from COMPSEGS data, sorted for stable comparison.
//--------------------------------------------------------------------------------------------------
std::vector<std::tuple<size_t, size_t, size_t, std::string>> extractSortedCells( const RigMswTableData& data )
{
std::vector<std::tuple<size_t, size_t, size_t, std::string>> cells;
for ( const auto& row : data.compsegsData() )
{
cells.emplace_back( row.i, row.j, row.k, row.gridName );
}
std::sort( cells.begin(), cells.end() );
return cells;
}
} // anonymous namespace
//==================================================================================================
//
// Parameterized integration tests: Tree mode vs Geometry mode for all MSW project files.
//
// The test loads a ResInsight project file, extracts well MSW data using both the tree-based
// (extractSingleWellMswDataTree) and geometry-based (extractSingleWellMswDataGeometry)
// algorithms, and verifies that both produce equivalent results for every well path:
//
// - The same set of reservoir cells in COMPSEGS (sorted {i,j,k,gridName} tuples)
// - The same number of WSEGVALV, WSEGAICD, and WSEGSICD valve rows
//
//==================================================================================================
class MswTreeVsGeometryTest : public testing::TestWithParam<std::string>
{
};
TEST_P( MswTreeVsGeometryTest, CompareTreeAndGeometryModes )
{
const std::string& projectFileName = GetParam();
QString projectFilePath =
QString( "%1/msw-export/project-files/%2" ).arg( TEST_MODEL_DIR ).arg( QString::fromStdString( projectFileName ) );
// Strip extension for use as folder name, e.g. "base.rsp" -> "base"
const QString projectStem = QFileInfo( QString::fromStdString( projectFileName ) ).completeBaseName();
const QString compareOutBase = QString( "%1/msw-export/compare-output/%2" ).arg( TEST_MODEL_DIR ).arg( projectStem );
if ( !QFile::exists( projectFilePath ) )
{
GTEST_SKIP() << "Project file not found: " << projectFilePath.toStdString();
}
bool loaded = RiaApplication::instance()->loadProject( projectFilePath );
ASSERT_TRUE( loaded ) << "Failed to load project: " << projectFilePath.toStdString();
RimProject* project = RiaApplication::instance()->project();
ASSERT_NE( project, nullptr );
auto eclipseCases = project->eclipseCases();
ASSERT_FALSE( eclipseCases.empty() ) << "No eclipse cases found in project";
RimEclipseCase* eclipseCase = eclipseCases[0];
ASSERT_NE( eclipseCase, nullptr );
if ( eclipseCase->eclipseCaseData() == nullptr )
{
GTEST_SKIP() << "Eclipse case data not loaded — EGRID file may be unavailable";
}
auto wellPaths = project->allWellPaths();
ASSERT_FALSE( wellPaths.empty() ) << "No well paths found in project";
int wellsWithData = 0;
for ( auto* wellPath : wellPaths )
{
ASSERT_NE( wellPath, nullptr );
if ( !wellPath->isTopLevelWellPath() ) continue;
auto treeResult = RicWellPathExportMswTableData::extractSingleWellMswDataTree( eclipseCase, wellPath );
auto geometryResult = RicWellPathExportMswTableData::extractSingleWellMswDataGeometry( eclipseCase, wellPath );
// If one mode fails, the other should fail too (no MSW data for this well path).
if ( !treeResult.has_value() && !geometryResult.has_value() )
{
continue;
}
ASSERT_TRUE( treeResult.has_value() ) << "Tree mode failed for well '" << wellPath->name().toStdString()
<< "': " << treeResult.error();
ASSERT_TRUE( geometryResult.has_value() )
<< "Geometry mode failed for well '" << wellPath->name().toStdString() << "': " << geometryResult.error();
const std::string wellName = treeResult->wellName();
const QString wellFileName = QString::fromStdString( wellName ) + ".txt";
// Export both modes to files for side-by-side comparison
writeMswTableDataToFile( *treeResult, compareOutBase + "/tree/" + wellFileName );
writeMswTableDataToFile( *geometryResult, compareOutBase + "/geometry/" + wellFileName );
// Export LGR (COMPSEGL) data to separate files if present
const QString lgrFileName = QString::fromStdString( wellName ) + "_LGR.txt";
writeMswLgrTableDataToFile( *treeResult, compareOutBase + "/tree/" + lgrFileName );
writeMswLgrTableDataToFile( *geometryResult, compareOutBase + "/geometry/" + lgrFileName );
// Both modes must produce data for the same well.
EXPECT_EQ( wellName, geometryResult->wellName() ) << "Well name mismatch for: " << wellPath->name().toStdString();
// Both modes must connect to the same set of reservoir cells.
auto treeCells = extractSortedCells( *treeResult );
auto geometryCells = extractSortedCells( *geometryResult );
EXPECT_EQ( treeCells, geometryCells ) << "COMPSEGS cells differ between Tree and Geometry modes for well '" << wellName << "'";
// Both modes must produce the same number of valve rows for each valve type.
EXPECT_EQ( treeResult->wsegvalvData().size(), geometryResult->wsegvalvData().size() )
<< "WSEGVALV row count differs for well '" << wellName << "'";
EXPECT_EQ( treeResult->wsegaicdData().size(), geometryResult->wsegaicdData().size() )
<< "WSEGAICD row count differs for well '" << wellName << "'";
EXPECT_EQ( treeResult->wsegsicdData().size(), geometryResult->wsegsicdData().size() )
<< "WSEGSICD row count differs for well '" << wellName << "'";
++wellsWithData;
}
EXPECT_GT( wellsWithData, 0 ) << "No well paths produced MSW data — check project file and well path MSW parameters";
}
INSTANTIATE_TEST_SUITE_P( MswExportProjectFiles,
MswTreeVsGeometryTest,
testing::Values( "base.rsp",
"fracture.rsp",
"perf_lateral.rsp",
"perf-lgr.rsp",
"perf_valve.rsp",
"two_wells.rsp",
"perf-lgr-two-wells.rsp",
"perf_aicd.rsp",
"fishbones.rsp",
"multiple_laterals.rsp",
"tie-in-custom-valve-location.rsp",
"standalone-valve.rsp" ) );
@@ -0,0 +1,300 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "CompletionsMsw/RigMswTableData.h"
#include "CompletionsMsw/RigMswTableRows.h"
#include "RiaDefines.h"
namespace
{
constexpr auto METRIC = RiaDefines::EclipseUnitSystem::UNITS_METRIC;
WelsegsRow makeWelsegsRow( int segNum, int joinSeg = 1 )
{
WelsegsRow row;
row.segment1 = segNum;
row.segment2 = segNum;
row.branch = 1;
row.joinSegment = joinSeg;
row.length = 10.0;
row.depth = 5.0;
return row;
}
CompsegsRow makeCompsegsRow( size_t i, size_t j, size_t k, const std::string& gridName = "" )
{
CompsegsRow row;
row.i = i;
row.j = j;
row.k = k;
row.branch = 1;
row.distanceStart = 0.0;
row.distanceEnd = 10.0;
row.gridName = gridName;
return row;
}
} // anonymous namespace
//==================================================================================================
// isEmpty
//==================================================================================================
TEST( RigMswTableData, IsEmpty_NewObject )
{
RigMswTableData td( "Well_A", METRIC );
EXPECT_TRUE( td.isEmpty() );
}
TEST( RigMswTableData, IsEmpty_WithWelsegsRow )
{
RigMswTableData td( "Well_A", METRIC );
td.addWelsegsRow( makeWelsegsRow( 2 ) );
EXPECT_FALSE( td.isEmpty() );
}
TEST( RigMswTableData, IsEmpty_WithCompsegsRow )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1 ) );
EXPECT_FALSE( td.isEmpty() );
}
TEST( RigMswTableData, IsEmpty_WithWsegvalvRow )
{
RigMswTableData td( "Well_A", METRIC );
WsegvalvRow wv;
wv.well = "Well_A";
wv.segmentNumber = 2;
wv.cv = 0.5;
wv.area = 1e-4;
td.addWsegvalvRow( wv );
EXPECT_FALSE( td.isEmpty() );
}
TEST( RigMswTableData, IsEmpty_WithWsegaicdRow )
{
RigMswTableData td( "Well_A", METRIC );
WsegaicdRow aicd;
aicd.well = "Well_A";
aicd.segment1 = 2;
aicd.segment2 = 2;
aicd.strength = 1e-5;
aicd.maxAbsRate = 1000.0;
aicd.flowRateExponent = 0.9;
aicd.viscExponent = 0.1;
td.addWsegaicdRow( aicd );
EXPECT_FALSE( td.isEmpty() );
}
// isEmpty() does not check wsegsicdData — document that behaviour explicitly.
TEST( RigMswTableData, IsEmpty_WsegsicdOnlyIsConsideredEmpty )
{
RigMswTableData td( "Well_A", METRIC );
WsegsicdRow sicd;
sicd.well = "Well_A";
sicd.segment1 = 2;
sicd.segment2 = 2;
sicd.strength = 2e-5;
sicd.maxAbsRate = 500.0;
td.addWsegsicdRow( sicd );
// isEmpty() does not inspect wsegsicdData, so it returns true even though
// there is a WSEGSICD row present.
EXPECT_TRUE( td.isEmpty() );
EXPECT_FALSE( td.wsegsicdData().empty() ); // data IS there
}
//==================================================================================================
// hasLgrData / mainGridCompsegsData / lgrCompsegsData
//==================================================================================================
TEST( RigMswTableData, HasLgrData_NoCompsegs )
{
RigMswTableData td( "Well_A", METRIC );
EXPECT_FALSE( td.hasLgrData() );
}
TEST( RigMswTableData, HasLgrData_OnlyMainGrid )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "" ) );
EXPECT_FALSE( td.hasLgrData() );
}
TEST( RigMswTableData, HasLgrData_OneLgrRow )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "LGR1" ) );
EXPECT_TRUE( td.hasLgrData() );
}
TEST( RigMswTableData, HasLgrData_AllLgrRows )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "LGR1" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "LGR2" ) );
EXPECT_TRUE( td.hasLgrData() );
}
TEST( RigMswTableData, MainGridCompsegsData_FiltersCorrectly )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "LGR1" ) );
td.addCompsegsRow( makeCompsegsRow( 3, 1, 1, "" ) );
auto mainRows = td.mainGridCompsegsData();
ASSERT_EQ( 2u, mainRows.size() );
EXPECT_EQ( 1u, mainRows[0].i );
EXPECT_EQ( 3u, mainRows[1].i );
for ( const auto& r : mainRows )
EXPECT_TRUE( r.isMainGrid() );
}
TEST( RigMswTableData, LgrCompsegsData_FiltersCorrectly )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "LGR1" ) );
td.addCompsegsRow( makeCompsegsRow( 3, 1, 1, "LGR2" ) );
auto lgrRows = td.lgrCompsegsData();
ASSERT_EQ( 2u, lgrRows.size() );
EXPECT_EQ( "LGR1", lgrRows[0].gridName );
EXPECT_EQ( "LGR2", lgrRows[1].gridName );
for ( const auto& r : lgrRows )
EXPECT_TRUE( r.isLgrGrid() );
}
TEST( RigMswTableData, MainGridAndLgrCompsegsData_NoOverlap )
{
RigMswTableData td( "Well_A", METRIC );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1, "" ) );
td.addCompsegsRow( makeCompsegsRow( 2, 1, 1, "LGR1" ) );
auto mainRows = td.mainGridCompsegsData();
auto lgrRows = td.lgrCompsegsData();
EXPECT_EQ( 1u, mainRows.size() );
EXPECT_EQ( 1u, lgrRows.size() );
EXPECT_EQ( mainRows.size() + lgrRows.size(), td.compsegsData().size() );
}
//==================================================================================================
// validationErrors / isValid
//==================================================================================================
TEST( RigMswTableData, ValidationErrors_EmptyWellName )
{
RigMswTableData td( "", METRIC );
td.addWelsegsRow( makeWelsegsRow( 2 ) );
auto errors = td.validationErrors();
EXPECT_FALSE( errors.empty() );
bool foundWellNameError = std::any_of( errors.begin(), errors.end(), []( const QString& e ) { return e.contains( "Well name" ); } );
EXPECT_TRUE( foundWellNameError );
EXPECT_FALSE( td.isValid() );
}
TEST( RigMswTableData, ValidationErrors_NoWelsegsRows )
{
RigMswTableData td( "Well_A", METRIC );
auto errors = td.validationErrors();
EXPECT_FALSE( errors.empty() );
bool foundWelsegsError = std::any_of( errors.begin(), errors.end(), []( const QString& e ) { return e.contains( "WELSEGS" ); } );
EXPECT_TRUE( foundWelsegsError );
EXPECT_FALSE( td.isValid() );
}
TEST( RigMswTableData, ValidationErrors_DuplicateSegmentNumber )
{
RigMswTableData td( "Well_A", METRIC );
td.addWelsegsRow( makeWelsegsRow( 2 ) );
td.addWelsegsRow( makeWelsegsRow( 3 ) );
td.addWelsegsRow( makeWelsegsRow( 2 ) ); // duplicate
auto errors = td.validationErrors();
EXPECT_FALSE( errors.empty() );
bool foundDuplicateError =
std::any_of( errors.begin(), errors.end(), []( const QString& e ) { return e.contains( "Duplicate" ) || e.contains( "2" ); } );
EXPECT_TRUE( foundDuplicateError );
EXPECT_FALSE( td.isValid() );
}
TEST( RigMswTableData, ValidationErrors_MultipleErrors )
{
// Empty well name AND no WELSEGS → two separate errors
RigMswTableData td( "", METRIC );
auto errors = td.validationErrors();
EXPECT_GE( errors.size(), 2u );
EXPECT_FALSE( td.isValid() );
}
TEST( RigMswTableData, IsValid_ValidData )
{
RigMswTableData td( "Well_A", METRIC );
td.addWelsegsRow( makeWelsegsRow( 2, 1 ) );
td.addWelsegsRow( makeWelsegsRow( 3, 2 ) );
EXPECT_TRUE( td.validationErrors().empty() );
EXPECT_TRUE( td.isValid() );
}
//==================================================================================================
// Metadata accessors
//==================================================================================================
TEST( RigMswTableData, WellNameAndUnitSystem )
{
RigMswTableData td( "MyWell", RiaDefines::EclipseUnitSystem::UNITS_FIELD );
EXPECT_EQ( "MyWell", td.wellName() );
EXPECT_EQ( RiaDefines::EclipseUnitSystem::UNITS_FIELD, td.unitSystem() );
}
TEST( RigMswTableData, HasWelsegsData_HasCompsegsData_HasValveData )
{
RigMswTableData td( "Well_A", METRIC );
EXPECT_FALSE( td.hasWelsegsData() );
EXPECT_FALSE( td.hasCompsegsData() );
EXPECT_FALSE( td.hasWsegvalvData() );
EXPECT_FALSE( td.hasWsegaicdData() );
EXPECT_FALSE( td.hasWsegsicdData() );
td.addWelsegsRow( makeWelsegsRow( 2 ) );
EXPECT_TRUE( td.hasWelsegsData() );
td.addCompsegsRow( makeCompsegsRow( 1, 1, 1 ) );
EXPECT_TRUE( td.hasCompsegsData() );
WsegvalvRow wv;
wv.well = "Well_A";
wv.segmentNumber = 2;
wv.cv = 0.5;
wv.area = 1e-4;
td.addWsegvalvRow( wv );
EXPECT_TRUE( td.hasWsegvalvData() );
}
+2 -1
View File
@@ -1 +1,2 @@
output
output
compare-output
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
# MSW Export: High-Level Pipeline Overview
```mermaid
flowchart TD
subgraph RIM["RIM (Project Data Model)"]
WP[RimWellPath]
MSW[RimMswCompletionParameters]
PERF[RimPerforationInterval]
VALVE["RimWellPathValve<br/>ICD / ICV / AICD / SICD"]
FRAC[RimWellPathFracture]
FB[RimFishbones]
EC[RimEclipseCase]
end
subgraph GEO["Geometry Extraction"]
GCS["generateCellSegments<br/>WellPathCellIntersectionInfo list"]
FI["filterIntersections<br/>clip to heel MD"]
end
subgraph BUILD["RicWellPathExportMswGeometryPath<br/>buildMswWellExportData"]
MB["buildMainBoreBranchFromGeometry<br/>RicMswBranchBuilder"]
VB["buildValveBranchesFromGeometry<br/>RicMswBranchBuilder"]
FRB["buildFractureBranchesFromGeometry<br/>RicMswBranchBuilder"]
FBB["buildFishbonesBranchesFromGeometry<br/>RicMswBranchBuilder"]
LAT["buildLateralBranches<br/>recursive"]
end
subgraph RIG["RIG (Intermediate Export Data)"]
WED["RigMswWellExportData<br/>header + branches"]
BR["RigMswBranch<br/>branchNumber + segments"]
SEG["RigMswSegment<br/>WELSEGS row + intersections + valve"]
end
subgraph COLLECT["collectTableData"]
TD["RigMswTableData<br/>flat table rows per well"]
end
subgraph OUTPUT["Output Tables"]
WH[WelsegsHeader]
WR[WelsegsRow]
CR[CompsegsRow]
VR["WsegvalvRow / WsegaicdRow / WsegsicdRow"]
end
RIM --> GEO
GEO --> BUILD
BUILD --> RIG
RIG --> COLLECT
COLLECT --> OUTPUT
WED --> BR --> SEG
```
@@ -0,0 +1,130 @@
# MSW Export: Data Structures
## RIG flat structs (new geometry path output)
```mermaid
classDiagram
class RigMswWellExportData {
WelsegsHeader header
vector~RigMswBranch~ branches
}
class RigMswBranch {
int branchNumber
optional~RigMswSegment~ tieInValve
vector~RigMswSegment~ segments
}
class RigMswSegment {
int segmentNumber
int outletSegmentNumber
double length
double depth
optional~double~ diameter
optional~double~ roughness
string description
string sourceWellName
vector~RigMswCellIntersection~ intersections
optional~WsegvalvRow~ wsegvalvData
optional~WsegaicdRow~ wsegaicdData
optional~WsegsicdRow~ wsegsicdData
}
class RigMswCellIntersection {
size_t i, j, k
double distanceStart
double distanceEnd
string gridName
}
class WelsegsHeader {
string well
double topDepth
double topLength
string infoType
optional~string~ pressureComponents
}
RigMswWellExportData "1" --> "1" WelsegsHeader
RigMswWellExportData "1" --> "*" RigMswBranch
RigMswBranch "1" --> "*" RigMswSegment
RigMswSegment "1" --> "*" RigMswCellIntersection
```
## Flat table rows collected into RigMswTableData
```mermaid
classDiagram
class RigMswTableData {
string wellName
WelsegsHeader welsegsHeader
vector~WelsegsRow~ welsegsData
vector~CompsegsRow~ compsegsData
vector~WsegvalvRow~ wsegvalvData
vector~WsegaicdRow~ wsegaicdData
vector~WsegsicdRow~ wsegsicdData
vector~RigMswBranch~ mswBranches
}
class WelsegsRow {
int segment1, segment2
int branch
int joinSegment
double length, depth
optional~double~ diameter
optional~double~ roughness
}
class CompsegsRow {
size_t i, j, k
int branch
double distanceStart
double distanceEnd
string gridName
}
class WsegvalvRow {
string well
int segmentNumber
double cv
double area
optional~string~ status
}
class WsegaicdRow {
string well
int segment1, segment2
double strength
double maxAbsRate
optional~string~ status
}
class WsegsicdRow {
string well
int segment1, segment2
double strength
double maxAbsRate
optional~string~ status
}
RigMswTableData "1" --> "*" WelsegsRow
RigMswTableData "1" --> "*" CompsegsRow
RigMswTableData "1" --> "*" WsegvalvRow
RigMswTableData "1" --> "*" WsegaicdRow
RigMswTableData "1" --> "*" WsegsicdRow
```
## RigMswUnifiedData aggregates multiple wells
```mermaid
classDiagram
class RigMswUnifiedData {
vector~RigMswTableData~ wellDataList
getAllCompsegsRows()
getAllWsegvalvRows()
getAllWsegaicdRows()
getAllWsegsicdRows()
}
RigMswUnifiedData "1" --> "*" RigMswTableData
```
@@ -0,0 +1,81 @@
# MSW Export: Detailed Build Pipeline
## buildMswWellExportData — main bore + completions
```mermaid
flowchart TD
ENTRY["buildMswWellExportData<br/>(RimEclipseCase, RimWellPath,<br/>maxSegmentLength, completionType)"]
ENTRY --> GCS["generateCellSegments<br/>-> WellPathCellIntersectionInfo list"]
GCS --> INITMD["computeInitialMeasuredDepth<br/>-> initialMD / initialTVD"]
INITMD --> FI["filterIntersections<br/>clip list to heel MD"]
FI --> HEADER["Build WelsegsHeader<br/>(well name, topLength, topDepth,<br/>infoType, pressureComponents)"]
FI --> MB["buildMainBoreBranchFromGeometry<br/>-> RigMswBranch (branch 1)"]
MB --> CSMAP["fills CellSegmentEntry map<br/>(MD range -> segment number)"]
CSMAP --> SVALVE["standalone valves from<br/>RimWellPathValve (valveCollection)<br/>-> embed WsegvalvRow on segment"]
CSMAP --> VB["buildValveBranchesFromGeometry<br/>(ICD/ICV/AICD/SICD inside perforations)<br/>-> vector~RigMswBranch~"]
CSMAP --> FRB["buildFractureBranchesFromGeometry<br/>(if FRACTURES flag set)<br/>-> vector~RigMswBranch~"]
CSMAP --> FBB["buildFishbonesBranchesFromGeometry<br/>(if FISHBONES flag set)<br/>-> vector~RigMswBranch~"]
CSMAP --> LAT["buildLateralBranches (recursive)<br/>for each child RimWellPath with tie-in<br/>-> vector~RigMswBranch~"]
HEADER & MB & VB & FRB & FBB & LAT --> RESULT["RigMswWellExportData<br/>{ header, branches }"]
```
## buildLateralBranches — recursive lateral handling
```mermaid
flowchart TD
LAT["buildLateralBranches<br/>(eclipseCase, wellPath, mainGrid,<br/>outletSegNum, completionType)"]
LAT --> TIEINMD["read tieInMD / tieInTVD<br/>from RimWellPathTieIn"]
TIEINMD --> TICV{"outletValve<br/>(ICV) active?"}
TICV -- yes --> TIEINSEG["build tie-in RigMswSegment<br/>+ WsegvalvRow<br/>-> stored as branch.tieInValve"]
TICV -- no --> SKIP[use outletSegNum directly]
TIEINSEG & SKIP --> GCS2["generateCellSegments<br/>filterIntersections"]
GCS2 --> MB2["buildMainBoreBranchFromGeometry<br/>-> RigMswBranch (lateral)"]
MB2 --> SVALVE2["standalone valves<br/>(valveCollection)"]
MB2 --> CSMAP2["CellSegmentEntry map"]
CSMAP2 --> VB2["buildValveBranchesFromGeometry"]
CSMAP2 --> FRB2["buildFractureBranchesFromGeometry"]
CSMAP2 --> FBB2["buildFishbonesBranchesFromGeometry"]
CSMAP2 --> GRANDCHILD["for each grandchild wellPath<br/>buildLateralBranches (recurse)"]
VB2 & FRB2 & FBB2 & GRANDCHILD --> RESULT2["vector~RigMswBranch~"]
```
## collectTableData — RigMswWellExportData to RigMswTableData
```mermaid
flowchart TD
INPUT["RigMswWellExportData<br/>{ header, branches }"]
INPUT --> HROW["setWelsegsHeader<br/>-> WelsegsHeader"]
INPUT --> LOOP["for each RigMswBranch"]
LOOP --> TIEIN{"tieInValve<br/>present?"}
TIEIN -- yes --> EMIT0["emitSegment(tieInValve)"]
TIEIN --> SEGLOOP["for each RigMswSegment"]
SEGLOOP --> WROW["addWelsegsRow<br/>(segment, branch, outlet, length, depth)"]
SEGLOOP --> CROW["addCompsegsRow per<br/>RigMswCellIntersection<br/>(i,j,k, branch, distStart, distEnd)"]
SEGLOOP --> VALVROW{"valve data?"}
VALVROW -- wsegvalvData --> VV["addWsegvalvRow"]
VALVROW -- wsegaicdData --> VA["addWsegaicdRow"]
VALVROW -- wsegsicdData --> VS["addWsegsicdRow"]
LOOP --> ADDMB["addMswBranch<br/>(store RigMswBranch reference)"]
HROW & WROW & CROW & VV & VA & VS & ADDMB --> TD["RigMswTableData"]
```
@@ -0,0 +1,79 @@
# Fishbones MSW Model
## Well path layout
Each fishbones "sub" position on the main bore spawns one ICD branch and one
lateral branch per installed tube. All segments belong to `RigMswBranch`
structs; connectivity is expressed through `outletSegmentNumber`.
```
Heel (seg 1)
|
| Branch 1 — main bore
|
o--[seg 2]--[seg 3]--[seg 4]--[seg 5]--[seg 6]-- ... --[seg N]-- Toe
| |
Sub position Sub position
| |
[ICD seg] [ICD seg] <- Branch 2, 5, ... (one per sub)
(WSEGVALV) (WSEGVALV)
/ \ / \
[lat seg] [lat seg] [lat seg] [lat seg] <- Branch 3/4, 6/7, ... (one per lateral)
COMPSEGS COMPSEGS COMPSEGS COMPSEGS
```
## Branch and segment hierarchy
```mermaid
graph LR
classDef mainBore fill:#4a90d9,color:#fff,stroke:#2c6fad
classDef icd fill:#e8a838,color:#fff,stroke:#b07e1e
classDef lateral fill:#5cb85c,color:#fff,stroke:#3d7a3d
classDef heel fill:#888,color:#fff,stroke:#555
HEEL(["Heel<br/>seg 1"]):::heel
HEEL --> S2["seg 2"]:::mainBore
S2 --> S3["seg 3"]:::mainBore
S3 --> S4["seg 4<br/>(sub A position)"]:::mainBore
S4 --> S5["seg 5"]:::mainBore
S5 --> S6["seg 6<br/>(sub B position)"]:::mainBore
S6 --> TOE(["Toe<br/>seg N"]):::mainBore
S4 --> ICD_A["ICD seg<br/>Branch 2<br/>WSEGVALV"]:::icd
ICD_A --> LA1["Lateral 1<br/>Branch 3<br/>COMPSEGS"]:::lateral
ICD_A --> LA2["Lateral 2<br/>Branch 4<br/>COMPSEGS"]:::lateral
S6 --> ICD_B["ICD seg<br/>Branch 5<br/>WSEGVALV"]:::icd
ICD_B --> LB1["Lateral 1<br/>Branch 6<br/>COMPSEGS"]:::lateral
ICD_B --> LB2["Lateral 2<br/>Branch 7<br/>COMPSEGS"]:::lateral
```
## Per-sub data structure
```mermaid
flowchart TD
SUB["RimFishbones sub<br/>(subIndex, measuredDepth)"]
SUB --> ICD_SEG["RigMswSegment — ICD<br/>- segmentNumber = icdSegNum<br/>- outletSegmentNumber = main-bore seg at sub MD<br/>- wsegvalvData: cv, area (from icdCount + orifice diameter)<br/>- intersections: cells closest to sub (COMPSEGS)"]
ICD_SEG --> LAT_BR["RigMswBranch per lateral<br/>(one branch per installed tube)"]
LAT_BR --> LAT_SEG["RigMswSegment per grid-cell intersection<br/>- outletSegmentNumber chains along lateral<br/> (first seg -> icdSegNum)<br/>- diameter = equivalentDiameter<br/>- roughness = openHoleRoughnessFactor<br/>- intersections: COMPSEGS (deduplicated<br/> against cells already used by ICD or<br/> other laterals on same sub)"]
```
## Outlet segment lookup
The ICD segment outlet is found with `findOutletSegmentForMD`:
```
cellSegMap (built during main-bore pass):
[startMD ---- midpoint ---- endMD] -> lastSubSegmentNumber
[ 200.0 ---- 212.5 ---- 225.0 ] -> seg 3
[ 225.0 ---- 237.5 ---- 250.0 ] -> seg 4 <- sub A at MD 243 connects here
[ 250.0 ---- 262.5 ---- 275.0 ] -> seg 5
[ 275.0 ---- 287.5 ---- 300.0 ] -> seg 6 <- sub B at MD 290 connects here
Rule: pick the entry whose midpoint is closest to, but not greater than, the sub MD.
```