#13333 Simulation Input: Add EDITNNC export functionality for sector models

This commit is contained in:
Kristian Bendiksen authored and JJ committed 2026-01-12 17:19:38 +01:00
1 parent e681b4603a
commit 1c9c5a85ad
5 files changed
+491 -16

No files matched your search

@@ -29,6 +29,7 @@
#include "RigEclipseResultTools.h"
#include "RigFault.h"
#include "RigMainGrid.h"
#include "RigSimulationInputTool.h"
#include "RimEclipseCase.h"
#include "RimWellPath.h"
@@ -42,7 +43,9 @@
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/B.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/C.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/E.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/F.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/N.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/O.hpp"
#include "opm/input/eclipse/Parser/ParserKeywords/W.hpp"
@@ -656,4 +659,32 @@ Opm::DeckKeyword operaterKeyword( std::string targetProperty,
return operaterKw;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
Opm::DeckKeyword editnncKeyword( const std::vector<RigSimulationInputTool::TransformedNNCConnection>& connections )
{
using E = Opm::ParserKeywords::EDITNNC;
Opm::DeckKeyword kw{ Opm::ParserKeywords::EDITNNC{} };
for ( const auto& conn : connections )
{
std::vector<Opm::DeckItem> items;
// Convert from 0-based sector coordinates to 1-based Eclipse convention
items.push_back( RifOpmDeckTools::item( E::I1::itemName, static_cast<int>( conn.cell1.i() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::J1::itemName, static_cast<int>( conn.cell1.j() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::K1::itemName, static_cast<int>( conn.cell1.k() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::I2::itemName, static_cast<int>( conn.cell2.i() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::J2::itemName, static_cast<int>( conn.cell2.j() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::K2::itemName, static_cast<int>( conn.cell2.k() + 1 ) ) );
items.push_back( RifOpmDeckTools::item( E::TRAN_MULT::itemName, conn.transmissibility ) );
kw.addRecord( Opm::DeckRecord{ std::move( items ) } );
}
return kw;
}
} // namespace RimKeywordFactory
@@ -18,6 +18,8 @@
#pragma once
#include "ReservoirDataModel/SimulationFile/RigSimulationInputTool.h"
#include "cvfVector3.h"
#include <optional>
@@ -71,5 +73,6 @@ Opm::DeckKeyword operaterKeyword( std::string targetProperty,
std::string inputProperty,
std::optional<float> alpha = std::nullopt,
std::optional<float> beta = std::nullopt );
Opm::DeckKeyword editnncKeyword( const std::vector<RigSimulationInputTool::TransformedNNCConnection>& connections );
} // namespace RimKeywordFactory
@@ -19,6 +19,7 @@
#include "RigSimulationInputTool.h"
#include "RiaLogging.h"
#include "RiaNncDefines.h"
#include "RifEclipseInputFileTools.h"
#include "RifOpmDeckTools.h"
@@ -31,6 +32,7 @@
#include "RigEclipseCaseData.h"
#include "RigEclipseResultTools.h"
#include "RigGridExportAdapter.h"
#include "RigMainGrid.h"
#include "RigResdataGridConverter.h"
#include "RigSimulationInputSettings.h"
#include "Well/RigSimWellData.h"
@@ -145,6 +147,11 @@ std::expected<void, QString> RigSimulationInputTool::exportSimulationInput( RimE
return result;
}
if ( auto result = exportEditNncKeyword( &eclipseCase, settings, deckFile ); !result )
{
return result;
}
// Remove SKIP keywords that were used as placeholders for filtered-out keywords
deckFile.removeKeywords( "SKIP" );
@@ -865,8 +872,8 @@ std::expected<RigBoundingBoxIjk<caf::VecIjk0>, QString>
}
// Get the clamped coordinates from the intersection
cvf::Vec3st clampedMin = intersection->min();
cvf::Vec3st clampedMax = intersection->max();
caf::VecIjk0 clampedMin = intersection->min();
caf::VecIjk0 clampedMax = intersection->max();
// Log if clamping occurred (partial overlap)
const auto& origMin = inputBox.min();
@@ -893,20 +900,8 @@ std::expected<RigBoundingBoxIjk<caf::VecIjk0>, QString>
}
// Transform clamped coordinates to sector-relative coordinates
auto transformResult1 =
RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( clampedMin.x(), clampedMin.y(), clampedMin.z() ),
sectorMin,
sectorMax,
refinement,
false,
false );
auto transformResult2 =
RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( clampedMax.x(), clampedMax.y(), clampedMax.z() ),
sectorMin,
sectorMax,
refinement,
false,
true );
auto transformResult1 = RigGridExportAdapter::transformIjkToSectorCoordinates( clampedMin, sectorMin, sectorMax, refinement, false, false );
auto transformResult2 = RigGridExportAdapter::transformIjkToSectorCoordinates( clampedMax, sectorMin, sectorMax, refinement, false, true );
if ( !transformResult1 )
{
@@ -1547,3 +1542,253 @@ std::expected<void, QString> RigSimulationInputTool::addOperNumRegionAndOperater
return {};
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigSimulationInputTool::NNCConnection> RigSimulationInputTool::extractDeckEditNncConnections( RifOpmFlowDeckFile& deckFile,
const RigMainGrid& mainGrid )
{
std::vector<NNCConnection> connections;
// Find all EDITNNC keywords in deck (there may be multiple)
auto keywords = deckFile.findAllKeywordsWithIndices( "EDITNNC" );
for ( const auto& keywordPair : keywords )
{
const auto& keyword = keywordPair.second;
// Each keyword can have multiple records
for ( size_t recordIdx = 0; recordIdx < keyword.size(); ++recordIdx )
{
const auto& record = keyword.getRecord( recordIdx );
// Read IJK coordinates directly from deck (1-based) and convert to 0-based
caf::VecIjk1 ijk1( record.getItem( 0 ).get<int>( 0 ), record.getItem( 1 ).get<int>( 0 ), record.getItem( 2 ).get<int>( 0 ) );
caf::VecIjk1 ijk2( record.getItem( 3 ).get<int>( 0 ), record.getItem( 4 ).get<int>( 0 ), record.getItem( 5 ).get<int>( 0 ) );
double transMult = record.getItem( 6 ).get<double>( 0 );
// Convert IJK to global cell indices
size_t c1Idx = mainGrid.cellIndexFromIJK( ijk1.toZeroBased() );
size_t c2Idx = mainGrid.cellIndexFromIJK( ijk2.toZeroBased() );
// Validate indices
if ( c1Idx == cvf::UNDEFINED_SIZE_T || c2Idx == cvf::UNDEFINED_SIZE_T )
{
RiaLogging::warning( QString( "Invalid EDITNNC connection in deck: (%1)-(%2)" )
.arg( QString::fromStdString( ijk1.toString() ) )
.arg( QString::fromStdString( ijk2.toString() ) ) );
continue;
}
connections.push_back( { c1Idx, c2Idx, transMult } );
}
}
return connections;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigSimulationInputTool::NNCConnection>
RigSimulationInputTool::filterInternalSectorConnections( const std::vector<NNCConnection>& allConnections,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const caf::VecIjk0& max )
{
std::vector<NNCConnection> filteredConnections;
RigBoundingBoxIjk sectorBounds( min, max );
for ( const auto& conn : allConnections )
{
// Get IJK for both cells
auto ijk1 = mainGrid.ijkFromCellIndex( conn.c1GlobIdx );
auto ijk2 = mainGrid.ijkFromCellIndex( conn.c2GlobIdx );
// Only keep if BOTH cells are inside sector bounds
if ( ijk1 && ijk2 && sectorBounds.contains( *ijk1 ) && sectorBounds.contains( *ijk2 ) )
{
filteredConnections.push_back( conn );
}
}
return filteredConnections;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::VecIjk0 RigSimulationInputTool::transformToSectorCoordinates( const caf::VecIjk0& globalIjk,
const caf::VecIjk0& min,
const cvf::Vec3st& refinement )
{
// Transform global IJK to sector-relative coordinates with refinement
// Formula matches RigGridExportAdapter::transformIjkToSectorCoordinates for box minimum coordinates
// Returns 0-based sector coordinates
return caf::VecIjk0( ( globalIjk.i() - min.i() ) * refinement.x(),
( globalIjk.j() - min.j() ) * refinement.y(),
( globalIjk.k() - min.k() ) * refinement.z() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RigSimulationInputTool::TransformedNNCConnection, QString>
RigSimulationInputTool::transformNNCToSectorCoordinates( const NNCConnection& connection,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const cvf::Vec3st& refinement )
{
// Get global IJK for both cells
auto gijk1 = mainGrid.ijkFromCellIndex( connection.c1GlobIdx );
if ( !gijk1 )
{
return std::unexpected( QString( "Invalid cell index: %1" ).arg( connection.c1GlobIdx ) );
}
auto gijk2 = mainGrid.ijkFromCellIndex( connection.c2GlobIdx );
if ( !gijk2 )
{
return std::unexpected( QString( "Invalid cell index: %1" ).arg( connection.c2GlobIdx ) );
}
// Transform to sector coordinates using utility method
TransformedNNCConnection transformed;
transformed.cell1 = transformToSectorCoordinates( *gijk1, min, refinement );
transformed.cell2 = transformToSectorCoordinates( *gijk2, min, refinement );
transformed.transmissibility = connection.transmissibility;
return transformed;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigSimulationInputTool::TransformedNNCConnection> RigSimulationInputTool::refineEditNncConnection( const NNCConnection& connection,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const cvf::Vec3st& refinement )
{
// Get global IJK for both cells
auto gijk1 = mainGrid.ijkFromCellIndex( connection.c1GlobIdx );
auto gijk2 = mainGrid.ijkFromCellIndex( connection.c2GlobIdx );
if ( !gijk1 || !gijk2 )
{
return {}; // Return empty on error
}
// For EDITNNC: Connect subcells at same relative positions within coarse cells
// Example: refinement (2,2,1) creates 4 connections:
// (0,0,0) → (0,0,0)
// (0,1,0) → (0,1,0)
// (1,0,0) → (1,0,0)
// (1,1,0) → (1,1,0)
// Each connection gets the SAME TRANSMUL (no division)
// Get base sector coordinates for both cells (start of refined block)
caf::VecIjk0 baseCell1 = transformToSectorCoordinates( *gijk1, min, refinement );
caf::VecIjk0 baseCell2 = transformToSectorCoordinates( *gijk2, min, refinement );
std::vector<TransformedNNCConnection> refined;
for ( size_t subI = 0; subI < refinement.x(); ++subI )
{
for ( size_t subJ = 0; subJ < refinement.y(); ++subJ )
{
for ( size_t subK = 0; subK < refinement.z(); ++subK )
{
// Add subcell offset to base coordinates
TransformedNNCConnection conn;
conn.cell1 = caf::VecIjk0( baseCell1.i() + subI, baseCell1.j() + subJ, baseCell1.k() + subK );
// SAME relative position in cell2
conn.cell2 = caf::VecIjk0( baseCell2.i() + subI, baseCell2.j() + subJ, baseCell2.k() + subK );
conn.transmissibility = connection.transmissibility; // Same TRAN_MULT for all
refined.push_back( conn );
}
}
}
return refined;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<void, QString> RigSimulationInputTool::exportEditNncKeyword( RimEclipseCase* eclipseCase,
const RigSimulationInputSettings& settings,
RifOpmFlowDeckFile& deckFile )
{
RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid();
if ( !mainGrid ) return std::unexpected( "No main grid available" );
// Step 1: Extract EDITNNC from deck only
auto allConnections = extractDeckEditNncConnections( deckFile, *mainGrid );
if ( allConnections.empty() )
{
RiaLogging::info( "No EDITNNC keywords found in deck - skipping EDITNNC export" );
return {};
}
// Step 2: Filter for internal sector
auto sectorConnections = filterInternalSectorConnections( allConnections, *mainGrid, settings.min(), settings.max() );
if ( sectorConnections.empty() )
{
RiaLogging::info( "No internal EDITNNC connections in sector - skipping EDITNNC export" );
return {};
}
RiaLogging::info(
QString( "Found %1 internal EDITNNC connections in sector (out of %2 total)" ).arg( sectorConnections.size() ).arg( allConnections.size() ) );
// Step 3: Transform and refine
bool hasRefinement = ( settings.refinement().x() != 1 || settings.refinement().y() != 1 || settings.refinement().z() != 1 );
std::vector<TransformedNNCConnection> transformedConnections;
if ( hasRefinement )
{
// Refine to subcells with corresponding positions
for ( const auto& conn : sectorConnections )
{
auto refined = refineEditNncConnection( conn, *mainGrid, settings.min(), settings.refinement() );
transformedConnections.insert( transformedConnections.end(), refined.begin(), refined.end() );
}
}
else
{
// Simple coordinate transformation, no refinement
for ( const auto& conn : sectorConnections )
{
auto result = transformNNCToSectorCoordinates( conn, *mainGrid, settings.min(), settings.refinement() );
if ( result )
{
transformedConnections.push_back( *result );
}
else
{
RiaLogging::warning( result.error() );
}
}
}
if ( transformedConnections.empty() )
{
return std::unexpected( "Failed to transform any EDITNNC connections" );
}
RiaLogging::info( QString( "Exporting %1 EDITNNC connections to sector model" ).arg( transformedConnections.size() ) );
// Step 4: Create keyword
auto editnncKw = RimKeywordFactory::editnncKeyword( transformedConnections );
// Step 5: Replace/add to EDIT section (not GRID!)
deckFile.replaceKeyword( "EDIT", editnncKw );
return {};
}
@@ -37,6 +37,7 @@ class RigSimWellData;
template <typename Vec>
class RigBoundingBoxIjk;
class RigGridExportAdapter;
class RigMainGrid;
namespace Opm
{
@@ -52,6 +53,24 @@ class DeckKeyword;
class RigSimulationInputTool
{
public:
// Data structures for NNC export
/// Raw NNC connection with global cell indices
struct NNCConnection
{
size_t c1GlobIdx; // Global cell index for cell 1
size_t c2GlobIdx; // Global cell index for cell 2
double transmissibility; // Transmissibility value
};
/// Transformed NNC connection with sector-relative 0-based IJK coordinates
struct TransformedNNCConnection
{
caf::VecIjk0 cell1 = caf::VecIjk0::ZERO; // 0-based sector IJK for cell 1
caf::VecIjk0 cell2 = caf::VecIjk0::ZERO; // 0-based sector IJK for cell 2
double transmissibility = 0.0; // Transmissibility value
};
static std::expected<void, QString>
exportSimulationInput( RimEclipseCase& eclipseCase, const RigSimulationInputSettings& settings, cvf::UByteArray* visibility );
@@ -75,6 +94,21 @@ public:
static std::vector<int> createRefinedVisibility( const RigGridExportAdapter& gridAdapter );
// Public EDITNNC methods for testing
static std::vector<NNCConnection> extractDeckEditNncConnections( RifOpmFlowDeckFile& deckFile, const RigMainGrid& mainGrid );
static std::vector<NNCConnection> filterInternalSectorConnections( const std::vector<NNCConnection>& allConnections,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const caf::VecIjk0& max );
static std::expected<TransformedNNCConnection, QString> transformNNCToSectorCoordinates( const NNCConnection& connection,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const cvf::Vec3st& refinement );
static std::vector<TransformedNNCConnection> refineEditNncConnection( const NNCConnection& connection,
const RigMainGrid& mainGrid,
const caf::VecIjk0& min,
const cvf::Vec3st& refinement );
private:
static std::expected<void, QString> updateCornerPointGridInDeckFile( RimEclipseCase* eclipseCase,
const RigSimulationInputSettings& settings,
@@ -116,6 +150,12 @@ private:
static std::expected<void, QString>
filterAndUpdateWellKeywords( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile );
static std::expected<void, QString>
exportEditNncKeyword( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile );
// Utility: Transform global IJK to sector-relative coordinates with refinement
static caf::VecIjk0 transformToSectorCoordinates( const caf::VecIjk0& globalIjk, const caf::VecIjk0& min, const cvf::Vec3st& refinement );
static std::expected<void, QString> addOperNumRegionAndOperater( RifOpmFlowDeckFile& deckFile,
const RigGridExportAdapter& gridAdapter,
const std::vector<int>& operNumResult,
@@ -1150,3 +1150,159 @@ TEST( RigSimulationInputTool, ExportModel5WithRefinement_3_5_1 )
// K1: (1-0)*1 + 1 = 1, K2: (6-0)*1 + 1 = 6
EXPECT_TRUE( checkCompdatCoordinates( "B-2H", 1, 6 ) ) << "B-2H COMPDAT K coordinates not centered correctly";
}
//--------------------------------------------------------------------------------------------------
/// Helper to load model5 test case
//--------------------------------------------------------------------------------------------------
static cvf::ref<RigEclipseCaseData> loadModel5TestCase()
{
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
if ( !subFolderExists ) return nullptr;
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
if ( !QFile::exists( egridFilePath ) ) return nullptr;
std::unique_ptr<RimEclipseResultCase> resultCase( new RimEclipseResultCase );
cvf::ref<RigEclipseCaseData> caseData = new RigEclipseCaseData( resultCase.get() );
cvf::ref<RifReaderEclipseOutput> reader = new RifReaderEclipseOutput;
bool loadResult = reader->open( egridFilePath, caseData.p() );
if ( !loadResult ) return nullptr;
return caseData;
}
//--------------------------------------------------------------------------------------------------
/// Test filterInternalSectorConnections - both cells inside
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, FilterNNCConnections_BothInside )
{
// Load model5 test data (20x30x10 grid)
auto caseData = loadModel5TestCase();
ASSERT_TRUE( caseData.notNull() );
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
RigMainGrid* mainGrid = caseData->mainGrid();
// Create test connections using the real grid
std::vector<RigSimulationInputTool::NNCConnection> allConnections;
// Connection 1: (2,3,4) to (3,3,4) - both inside sector [1,5] x [1,5] x [1,5]
size_t c1Idx = mainGrid->cellIndexFromIJK( 2, 3, 4 );
size_t c2Idx = mainGrid->cellIndexFromIJK( 3, 3, 4 );
allConnections.push_back( { c1Idx, c2Idx, 1.5 } );
// Connection 2: (10,20,8) to (11,20,8) - both outside sector
c1Idx = mainGrid->cellIndexFromIJK( 10, 20, 8 );
c2Idx = mainGrid->cellIndexFromIJK( 11, 20, 8 );
allConnections.push_back( { c1Idx, c2Idx, 2.5 } );
// Connection 3: (1,2,3) to (15,15,8) - one inside, one outside
c1Idx = mainGrid->cellIndexFromIJK( 1, 2, 3 );
c2Idx = mainGrid->cellIndexFromIJK( 15, 15, 8 );
allConnections.push_back( { c1Idx, c2Idx, 3.5 } );
// Filter for sector [1,5] x [1,5] x [1,5]
caf::VecIjk0 sectorMin( 1, 1, 1 );
caf::VecIjk0 sectorMax( 5, 5, 5 );
auto filtered = RigSimulationInputTool::filterInternalSectorConnections( allConnections, *mainGrid, sectorMin, sectorMax );
// Only connection 1 should remain
ASSERT_EQ( 1u, filtered.size() );
EXPECT_EQ( 1.5, filtered[0].transmissibility );
}
//--------------------------------------------------------------------------------------------------
/// Test transformNNCToSectorCoordinates - no refinement
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, TransformNNCToSectorCoordinates_NoRefinement )
{
// Load model5 test data (20x30x10 grid)
auto caseData = loadModel5TestCase();
ASSERT_TRUE( caseData.notNull() );
RigMainGrid* mainGrid = caseData->mainGrid();
// Connection between (10,15,5) and (11,15,5) - I-face neighbors
size_t c1Idx = mainGrid->cellIndexFromIJK( 10, 15, 5 );
size_t c2Idx = mainGrid->cellIndexFromIJK( 11, 15, 5 );
RigSimulationInputTool::NNCConnection connection{ c1Idx, c2Idx, 4.5 };
// Sector starts at (5,10,2)
caf::VecIjk0 sectorMin( 5, 10, 2 );
cvf::Vec3st refinement( 1, 1, 1 );
auto result = RigSimulationInputTool::transformNNCToSectorCoordinates( connection, *mainGrid, sectorMin, refinement );
ASSERT_TRUE( result.has_value() );
// Cell 1: (10,15,5) - sector min (5,10,2) = (5,5,3) in 0-based sector coords
EXPECT_EQ( 5, result->cell1.i() );
EXPECT_EQ( 5, result->cell1.j() );
EXPECT_EQ( 3, result->cell1.k() );
// Cell 2: (11,15,5) - sector min (5,10,2) = (6,5,3) in 0-based sector coords
EXPECT_EQ( 6, result->cell2.i() );
EXPECT_EQ( 5, result->cell2.j() );
EXPECT_EQ( 3, result->cell2.k() );
EXPECT_EQ( 4.5, result->transmissibility );
}
//--------------------------------------------------------------------------------------------------
/// Test refineEDITNNCConnection - corresponding subcells with refinement (2,2,1)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, RefineEDITNNCConnection_CorrespondingSubcells )
{
// Load model5 test data (20x30x10 grid)
auto caseData = loadModel5TestCase();
ASSERT_TRUE( caseData.notNull() );
RigMainGrid* mainGrid = caseData->mainGrid();
// Connection between cells (3,4,5) and (6,8,7) - non-neighbors
size_t c1Idx = mainGrid->cellIndexFromIJK( 3, 4, 5 );
size_t c2Idx = mainGrid->cellIndexFromIJK( 6, 8, 7 );
RigSimulationInputTool::NNCConnection connection{ c1Idx, c2Idx, 2.5 }; // TRAN_MULT = 2.5
caf::VecIjk0 sectorMin( 0, 0, 0 );
cvf::Vec3st refinement( 2, 2, 1 ); // Refine by 2x2x1
auto refined = RigSimulationInputTool::refineEditNncConnection( connection, *mainGrid, sectorMin, refinement );
// For EDITNNC: creates connections between corresponding subcells
// Should create 2*2*1 = 4 connections (one per subcell pair at same relative position)
ASSERT_EQ( 4u, refined.size() );
// All connections should have same TRAN_MULT (no distribution)
for ( const auto& conn : refined )
{
EXPECT_DOUBLE_EQ( 2.5, conn.transmissibility );
}
// Verify corresponding subcell connections
// Cell1 starts at sector coordinates (3*2, 4*2, 5*1) = (6, 8, 5)
// Cell2 starts at sector coordinates (6*2, 8*2, 7*1) = (12, 16, 7)
// Connection 0: (0,0,0) -> (0,0,0): (6,8,5) -> (12,16,7)
EXPECT_EQ( caf::VecIjk0( 6, 8, 5 ), refined[0].cell1 );
EXPECT_EQ( caf::VecIjk0( 12, 16, 7 ), refined[0].cell2 );
// Connection 1: (0,1,0) -> (0,1,0): (6,9,5) -> (12,17,7)
EXPECT_EQ( caf::VecIjk0( 6, 9, 5 ), refined[1].cell1 );
EXPECT_EQ( caf::VecIjk0( 12, 17, 7 ), refined[1].cell2 );
// Connection 2: (1,0,0) -> (1,0,0): (7,8,5) -> (13,16,7)
EXPECT_EQ( caf::VecIjk0( 7, 8, 5 ), refined[2].cell1 );
EXPECT_EQ( caf::VecIjk0( 13, 16, 7 ), refined[2].cell2 );
// Connection 3: (1,1,0) -> (1,1,0): (7,9,5) -> (13,17,7)
EXPECT_EQ( caf::VecIjk0( 7, 9, 5 ), refined[3].cell1 );
EXPECT_EQ( caf::VecIjk0( 13, 17, 7 ), refined[3].cell2 );
}