mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-27 05:37:21 -05:00
CompletionExport: Add unit tests, test models, and diagrams for MSW export
This commit is contained in:
@@ -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() );
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
```
|
||||
Reference in New Issue
Block a user