diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp index 264b1751c4..0c25664312 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp @@ -455,6 +455,13 @@ void RicWellPathExportMswTableData::appendFishbonesMswExportInfo( const RimEclip size_t i, j, k; localGrid->ijkFromCellIndex( localGridCellIndex, &i, &j, &k ); + + // Shift K to fracture section for dual porosity models + if ( mainGrid->isDualPorosity() ) + { + k += mainGrid->cellCountK(); + } + caf::VecIjk0 localIJK( i, j, k ); auto mswIntersect = std::make_shared( gridName, @@ -1346,6 +1353,13 @@ void RicWellPathExportMswTableData::assignFishbonesLateralIntersections( const R size_t i = 0u, j = 0u, k = 0u; localGrid->ijkFromCellIndex( localGridCellIndex, &i, &j, &k ); + + // For dual porosity models, shift K to the fracture section so exported completion data references the correct K-layer. + if ( grid->isDualPorosity() ) + { + k += grid->cellCountK(); + } + auto subSegment = std::make_unique( "Sub segment", previousExitMD, cellIntInfo.endMD, diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.cpp b/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.cpp index d20c97bddf..f5a0933a77 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.cpp +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.cpp @@ -455,6 +455,14 @@ std::vector RicExportLgrFeature::buildLgrsForWellPaths( std::vectormainGrid(); mainGrid && mainGrid->isDualPorosity() ) + { + kLayerOffset = mainGrid->cellCountK(); + } + int firstLgrId = firstAvailableLgrId( eclipseCase->mainGrid() ); if ( splitType == Lgr::LGR_PER_CELL ) @@ -503,7 +511,7 @@ std::vector RicExportLgrFeature::buildLgrsForWellPaths( std::vectorname(), intersectingCells, refinement ) ); + lgrs.push_back( buildLgr( lgrId, lgrName, wellPath->name(), intersectingCells, refinement, kLayerOffset ) ); } if ( isIntersectingOtherLgrs ) wellsIntersectingOtherLgrs->push_back( wellPath->name() ); @@ -524,11 +532,20 @@ std::vector RicExportLgrFeature::buildLgrsPerMainCell( int LgrNameFactory& lgrNameFactory ) { std::vector lgrs; - int lgrId = firstLgrId; + + // For dual porosity models, localCellIndexK() includes the fracture section offset. + // Subtract it here to get the original grid K for LGR bounding box computation. + size_t kLayerOffset = 0; + if ( auto mainGrid = eclipseCase->mainGrid(); mainGrid && mainGrid->isDualPorosity() ) + { + kLayerOffset = mainGrid->cellCountK(); + } + + int lgrId = firstLgrId; for ( const auto& intersectionCell : intersectingCells ) { auto lgrName = lgrNameFactory.newName( "", lgrId ); - lgrs.push_back( buildLgr( lgrId++, lgrName, wellPath->name(), { intersectionCell }, refinement ) ); + lgrs.push_back( buildLgr( lgrId++, lgrName, wellPath->name(), { intersectionCell }, refinement, kLayerOffset ) ); } return lgrs; } @@ -545,6 +562,14 @@ std::vector { std::vector lgrs; + // For dual porosity models, localCellIndexK() includes the fracture section offset. + // Subtract it here to get the original grid K for LGR bounding box computation. + size_t kLayerOffset = 0; + if ( auto mainGrid = eclipseCase->mainGrid(); mainGrid && mainGrid->isDualPorosity() ) + { + kLayerOffset = mainGrid->cellCountK(); + } + std::vector> occupiedBbs; for ( const auto& complInfo : completionInfo ) @@ -559,7 +584,7 @@ std::vector for ( const auto& cell : complCells ) { auto candidateBb = maxBb; - candidateBb.addCell( cell.localCellIndexI(), cell.localCellIndexJ(), cell.localCellIndexK() ); + candidateBb.addCell( cell.localCellIndexI(), cell.localCellIndexJ(), cell.localCellIndexK() - kLayerOffset ); // Test bounding box bool intersectsExistingBb = false; @@ -606,12 +631,13 @@ LgrInfo RicExportLgrFeature::buildLgr( int const QString& lgrName, const QString& wellPathName, const std::vector& intersectingCells, - const cvf::Vec3st& refinement ) + const cvf::Vec3st& refinement, + size_t kLayerOffset ) { IjkBoundingBox boundingBox; for ( const auto& cell : intersectingCells ) { - boundingBox.addCell( cell.localCellIndexI(), cell.localCellIndexJ(), cell.localCellIndexK() ); + boundingBox.addCell( cell.localCellIndexI(), cell.localCellIndexJ(), cell.localCellIndexK() - kLayerOffset ); } return buildLgr( lgrId, lgrName, wellPathName, boundingBox, refinement ); diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.h b/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.h index c185e85401..37c75c3c63 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.h +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportLgrFeature.h @@ -211,7 +211,8 @@ private: const QString& lgrName, const QString& wellPathName, const std::vector& intersectingCells, - const cvf::Vec3st& refinement ); + const cvf::Vec3st& refinement, + size_t kLayerOffset = 0 ); static LgrInfo buildLgr( int lgrId, const QString& lgrName, diff --git a/ApplicationLibCode/ReservoirDataModel/Completions/RigCompletionDataGridCell.cpp b/ApplicationLibCode/ReservoirDataModel/Completions/RigCompletionDataGridCell.cpp index 747b1a40ff..cd94591afd 100644 --- a/ApplicationLibCode/ReservoirDataModel/Completions/RigCompletionDataGridCell.cpp +++ b/ApplicationLibCode/ReservoirDataModel/Completions/RigCompletionDataGridCell.cpp @@ -55,6 +55,13 @@ RigCompletionDataGridCell::RigCompletionDataGridCell( size_t globalCellIndex, co m_localCellIndexJ = j; m_localCellIndexK = k; + // For dual porosity models, the first N K-layers represent MATRIX and K-layers N+1 to 2N represent FRACTURE. + // Shift K to the fracture section so exported completion data references the correct K-layer. + if ( mainGrid->isDualPorosity() ) + { + m_localCellIndexK += mainGrid->cellCountK(); + } + if ( grid != mainGrid ) { m_lgrName = QString::fromStdString( grid->gridName() ); diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 9cf9912192..cc09345ba8 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -125,6 +125,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RifRmsWellPathReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaAngleUtils-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RicTransmissibilityCalculator-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigCompletionDataGridCell-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/cafVecIjk-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaSummaryAddressCollectionTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase.h diff --git a/ApplicationLibCode/UnitTests/RigCompletionDataGridCell-Test.cpp b/ApplicationLibCode/UnitTests/RigCompletionDataGridCell-Test.cpp new file mode 100644 index 0000000000..a2ab3a7a8e --- /dev/null +++ b/ApplicationLibCode/UnitTests/RigCompletionDataGridCell-Test.cpp @@ -0,0 +1,109 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026 Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "RigCompletionDataGridCell.h" + +#include "RifReaderMockModel.h" + +#include "RigEclipseCaseData.h" +#include "RigMainGrid.h" + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigCompletionDataGridCellTest, LocalCellIndexK_StandardModel ) +{ + cvf::ref caseData = new RigEclipseCaseData( nullptr ); + + cvf::ref mockReader = new RifReaderMockModel; + mockReader->setWorldCoordinates( cvf::Vec3d( 0, 0, 0 ), cvf::Vec3d( 100, 100, 100 ) ); + mockReader->setCellCounts( cvf::Vec3st( 2, 2, 3 ) ); + mockReader->enableWellData( false ); + mockReader->open( "", caseData.p() ); + caseData->mainGrid()->computeCachedData(); + + RigMainGrid* mainGrid = caseData->mainGrid(); + ASSERT_FALSE( mainGrid->isDualPorosity() ); + ASSERT_EQ( 3u, mainGrid->cellCountK() ); + + // Cell at (0, 0, 0) - first K-layer + RigCompletionDataGridCell cell0( 0, mainGrid ); + EXPECT_EQ( 0u, cell0.localCellIndexK() ); + + // Cell at (0, 0, 2) - last K-layer (global index = 2 * 2*2 = 8) + RigCompletionDataGridCell cell2( 8, mainGrid ); + EXPECT_EQ( 2u, cell2.localCellIndexK() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigCompletionDataGridCellTest, LocalCellIndexK_DualPorosityModel ) +{ + cvf::ref caseData = new RigEclipseCaseData( nullptr ); + + cvf::ref mockReader = new RifReaderMockModel; + mockReader->setWorldCoordinates( cvf::Vec3d( 0, 0, 0 ), cvf::Vec3d( 100, 100, 100 ) ); + mockReader->setCellCounts( cvf::Vec3st( 2, 2, 3 ) ); + mockReader->enableWellData( false ); + mockReader->open( "", caseData.p() ); + caseData->mainGrid()->computeCachedData(); + + RigMainGrid* mainGrid = caseData->mainGrid(); + mainGrid->setDualPorosity( true ); + ASSERT_TRUE( mainGrid->isDualPorosity() ); + + size_t cellCountK = mainGrid->cellCountK(); + ASSERT_EQ( 3u, cellCountK ); + + // Cell at (0, 0, 0) - first K-layer: should be offset by cellCountK + RigCompletionDataGridCell cell0( 0, mainGrid ); + EXPECT_EQ( 0u + cellCountK, cell0.localCellIndexK() ); + + // Cell at (0, 0, 2) - last K-layer: should be offset by cellCountK + RigCompletionDataGridCell cell2( 8, mainGrid ); + EXPECT_EQ( 2u + cellCountK, cell2.localCellIndexK() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigCompletionDataGridCellTest, OneBasedString_DualPorosityModel ) +{ + cvf::ref caseData = new RigEclipseCaseData( nullptr ); + + cvf::ref mockReader = new RifReaderMockModel; + mockReader->setWorldCoordinates( cvf::Vec3d( 0, 0, 0 ), cvf::Vec3d( 100, 100, 100 ) ); + mockReader->setCellCounts( cvf::Vec3st( 2, 2, 3 ) ); + mockReader->enableWellData( false ); + mockReader->open( "", caseData.p() ); + caseData->mainGrid()->computeCachedData(); + + RigMainGrid* mainGrid = caseData->mainGrid(); + + // Without dual porosity: cell (0,0,0) should give "[1, 1, 1]" + RigCompletionDataGridCell cellStandard( 0, mainGrid ); + EXPECT_EQ( QString( "[1, 1, 1]" ), cellStandard.oneBasedLocalCellIndexString() ); + + // With dual porosity: cell (0,0,0) should give "[1, 1, 4]" (K offset by 3) + mainGrid->setDualPorosity( true ); + RigCompletionDataGridCell cellDual( 0, mainGrid ); + EXPECT_EQ( QString( "[1, 1, 4]" ), cellDual.oneBasedLocalCellIndexString() ); +}