Files
ResInsight/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp
T
Kristian Bendiksen 66c2cd47d3 Sector export: Assign all refined sub-cells to segments in COMPSEGS
Only END_IJK denotes a cell range, DIRECTION alone is a single cell and the MD
interval is split. A defaulted start MD is zero. Without an end MD the record is
copied to each sub-cell so all refined COMPDAT connections are attached to a segment.
2026-10-21 14:42:53 +02:00

3431 lines
167 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025- 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 "RiaTestDataDirectory.h"
#include "RifOpmDeckTools.h"
#include "RifOpmFlowDeckFile.h"
#include "RifReaderEclipseOutput.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseCaseDataTools.h"
#include "RigMainGrid.h"
#include "RigModelPaddingSettings.h"
#include "RigNoRefinement.h"
#include "RigNonUniformRefinement.h"
#include "RigSimulationInputSettings.h"
#include "RigSimulationInputTool.h"
#include "RigTransmissibilityEquations.h"
#include "RigUniformRefinement.h"
#include "RimEclipseResultCase.h"
#include "opm/input/eclipse/Deck/Deck.hpp"
#include "opm/input/eclipse/Deck/DeckItem.hpp"
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
#include "opm/input/eclipse/Parser/Parser.hpp"
#include <QDir>
#include <QFile>
#include <QRegularExpression>
#include <QString>
#include <QTemporaryDir>
#include <cmath>
#include <limits>
#include <set>
#include <vector>
static RigUniformRefinement makeRef( const caf::VecIjk0& sectorMin, const caf::VecIjk0& sectorMax, const cvf::Vec3st& refCounts )
{
cvf::Vec3st sectorSize( sectorMax.i() - sectorMin.i() + 1, sectorMax.j() - sectorMin.j() + 1, sectorMax.k() - sectorMin.k() + 1 );
return RigUniformRefinement( refCounts, sectorSize );
}
//--------------------------------------------------------------------------------------------------
/// Helper: set refinement on settings, computing sector size from min/max.
/// If refCounts is (1,1,1), creates RigNoRefinement; otherwise RigUniformRefinement.
//--------------------------------------------------------------------------------------------------
static void setSettingsRefinement( RigSimulationInputSettings& settings, const cvf::Vec3st& refCounts )
{
cvf::Vec3st sectorSize( settings.max().i() - settings.min().i() + 1,
settings.max().j() - settings.min().j() + 1,
settings.max().k() - settings.min().k() + 1 );
if ( refCounts.x() == 1 && refCounts.y() == 1 && refCounts.z() == 1 )
{
settings.setRefinement( std::make_unique<RigNoRefinement>( sectorSize ) );
}
else
{
settings.setRefinement( std::make_unique<RigUniformRefinement>( refCounts, sectorSize ) );
}
}
//--------------------------------------------------------------------------------------------------
/// Helper to create an EQUALS DeckRecord
/// EQUALS format: FIELD VALUE I1 I2 J1 J2 K1 K2 (1-based Eclipse coordinates)
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createEqualsRecord( const std::string& fieldName, int value, int i1, int i2, int j1, int j2, int k1, int k2 )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", fieldName ) );
items.push_back( RifOpmDeckTools::item( "VALUE", value ) );
items.push_back( RifOpmDeckTools::item( "I1", i1 ) );
items.push_back( RifOpmDeckTools::item( "I2", i2 ) );
items.push_back( RifOpmDeckTools::item( "J1", j1 ) );
items.push_back( RifOpmDeckTools::item( "J2", j2 ) );
items.push_back( RifOpmDeckTools::item( "K1", k1 ) );
items.push_back( RifOpmDeckTools::item( "K2", k2 ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record with no overlap - should return error
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_NoOverlap )
{
// Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9) (inclusive, cells [0-19, 15-29, 0-9])
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// EQUALS record: FIPNUM 1 1 20 1 14 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[0-13], K[0-9]
// Sector J is [15-29], so no overlap in J dimension
auto record = createEqualsRecord( "FIPNUM", 1, 1, 20, 1, 14, 1, 10 );
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record with partial overlap requiring clamping
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_PartialOverlapWithClamping )
{
// Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9) (inclusive, cells [0-19, 15-29, 0-9])
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// EQUALS record: FIPNUM 2 1 20 15 30 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[14-29], K[0-9]
// Sector: I[0-19], J[15-29], K[0-9]
// Intersection (clamped): I[0-19], J[15-29], K[0-9]
auto record = createEqualsRecord( "FIPNUM", 2, 1, 20, 15, 30, 1, 10 );
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Check transformed coordinates (sector-relative, 1-based)
// After clamping: I[0-19], J[15-29], K[0-9] (0-based)
// Transformed to sector-relative (min at 0,15,0):
// I: (0-0)*1+1 = 1 to (19-0)*1+1 = 20
// J: (15-15)*1+1 = 1 to (29-15)*1+1 = 15
// K: (0-0)*1+1 = 1 to (9-0)*1+1 = 10
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
// Check field name and value are preserved
EXPECT_EQ( "FIPNUM", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_EQ( 2, result->getItem( 1 ).get<int>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record completely inside sector (no clamping)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_CompletelyInside )
{
// Sector: min=(0, 0, 0), max=(19, 19, 9) (inclusive, cells [0-19, 0-19, 0-9])
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 19, 19, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// EQUALS record completely inside: FIPNUM 3 5 15 5 15 2 8 (1-based Eclipse)
// Converts to 0-based: I[4-14], J[4-14], K[1-7]
auto record = createEqualsRecord( "FIPNUM", 3, 5, 15, 5, 15, 2, 8 );
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// No clamping, direct transformation (sector min is 0,0,0)
// I: (4-0)*1+1 = 5 to (14-0)*1+1 = 15
// J: (4-0)*1+1 = 5 to (14-0)*1+1 = 15
// K: (1-0)*1+1 = 2 to (7-0)*1+1 = 8
EXPECT_EQ( 5, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 15, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 5, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 2, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 8, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record completely outside sector
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_CompletelyOutside )
{
// Sector: min=(0, 0, 0), max=(9, 9, 9) (inclusive, cells [0-9, 0-9, 0-9])
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 9, 9, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// EQUALS record outside: FIPNUM 4 20 30 20 30 1 10 (1-based Eclipse)
// Converts to 0-based: I[19-29], J[19-29], K[0-9]
// No overlap with sector
auto record = createEqualsRecord( "FIPNUM", 4, 20, 30, 20, 30, 1, 10 );
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test invalid EQUALS record (insufficient items)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_InvalidRecord )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 9, 9, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// Create a record with only 1 item (field name, missing value)
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", std::string( "FIPNUM" ) ) );
// Missing VALUE and box coordinates
Opm::DeckRecord record{ std::move( items ) };
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "insufficient items" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record without box definition (only field and value)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_NoBoxDefinition )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 9, 9, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// Create a record with only field name and value (no box coordinates)
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", std::string( "FIPNUM" ) ) );
items.push_back( RifOpmDeckTools::item( "VALUE", 1 ) );
Opm::DeckRecord record{ std::move( items ) };
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
EXPECT_TRUE( result.has_value() );
EXPECT_EQ( result->size(), 2U );
EXPECT_EQ( result->getItem( 0 ).get<std::string>( 0 ), "FIPNUM" );
EXPECT_EQ( result->getItem( 1 ).get<int>( 0 ), 1 );
}
//--------------------------------------------------------------------------------------------------
/// Test EQUALS record at sector boundary
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessEqualsRecord_AtBoundary )
{
// Sector: min=(5, 5, 5), max=(14, 14, 14) (inclusive, cells [5-14, 5-14, 5-14])
caf::VecIjk0 sectorMin( 5, 5, 5 );
caf::VecIjk0 sectorMax( 14, 14, 14 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// EQUALS record exactly matching sector: 6 15 6 15 6 15 (1-based Eclipse)
// Converts to 0-based: I[5-14], J[5-14], K[5-14]
auto record = createEqualsRecord( "FIPNUM", 5, 6, 15, 6, 15, 6, 15 );
auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should span entire sector in all dimensions
// Transformed: (5-5)*1+1 = 1 to (14-5)*1+1 = 10
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 10, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 10, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Helper to create an AQUCON DeckRecord
/// AQUCON format: ID I1 I2 J1 J2 K1 K2 CONNECT_FACE ... (1-based Eclipse coordinates)
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createAquconRecord( int aquiferId, int i1, int i2, int j1, int j2, int k1, int k2, const std::string& connectFace )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "ID", aquiferId ) );
items.push_back( RifOpmDeckTools::item( "I1", i1 ) );
items.push_back( RifOpmDeckTools::item( "I2", i2 ) );
items.push_back( RifOpmDeckTools::item( "J1", j1 ) );
items.push_back( RifOpmDeckTools::item( "J2", j2 ) );
items.push_back( RifOpmDeckTools::item( "K1", k1 ) );
items.push_back( RifOpmDeckTools::item( "K2", k2 ) );
items.push_back( RifOpmDeckTools::item( "CONNECT_FACE", connectFace ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Test AQUCON record with partial overlap (based on user-provided data)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAquconRecord_PartialOverlap )
{
// Sector: min=(40, 27, 21), max=(100, 72, 58) (inclusive, 0-based)
// This corresponds to a sector with cells [40-100, 27-72, 21-58]
caf::VecIjk0 sectorMin( 40, 27, 21 );
caf::VecIjk0 sectorMax( 100, 72, 58 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// AQUCON record from user data: 1 57 57 28 36 46 58 'I+' (1-based Eclipse)
// Converts to 0-based: I[56-56], J[27-35], K[45-57]
// Sector is I[40-100], J[27-72], K[21-58]
// Overlap: I[56-56], J[27-35], K[45-57] - all inside, no clamping needed
auto record = createAquconRecord( 1, 57, 57, 28, 36, 46, 58, "I+" );
auto result = RigSimulationInputTool::processAquconRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Check transformed coordinates (sector-relative, 1-based)
// I: (56-40)*1+1 = 17 to (56-40)*1+1 = 17
// J: (27-27)*1+1 = 1 to (35-27)*1+1 = 9
// K: (45-21)*1+1 = 25 to (57-21)*1+1 = 37
EXPECT_EQ( 17, result->getItem( 1 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 17, result->getItem( 2 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 3 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 9, result->getItem( 4 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 25, result->getItem( 5 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 37, result->getItem( 6 ).get<int>( 0 ) ); // K2
// Check aquifer ID and connect face are preserved
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) );
EXPECT_EQ( "I+", result->getItem( 7 ).get<std::string>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Test AQUCON record with partial K overlap requiring clamping
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAquconRecord_PartialOverlapWithClamping )
{
// Sector: min=(40, 27, 21), max=(100, 72, 58) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 40, 27, 21 );
caf::VecIjk0 sectorMax( 100, 72, 58 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// AQUCON record: 1 79 79 41 67 5 11 'I+' (1-based Eclipse)
// Converts to 0-based: I[78-78], J[40-66], K[4-10]
// Sector K is [21-58], so K[4-10] is outside and should be skipped
auto record = createAquconRecord( 1, 79, 79, 41, 67, 5, 11, "I+" );
auto result = RigSimulationInputTool::processAquconRecord( record, sectorMin, sectorMax, refinement );
// This should fail because K range [4-10] doesn't overlap with sector K range [21-58]
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test AQUCON record completely inside sector
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAquconRecord_CompletelyInside )
{
// Sector: min=(40, 27, 21), max=(100, 72, 58) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 40, 27, 21 );
caf::VecIjk0 sectorMax( 100, 72, 58 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// AQUCON record: 1 61 61 48 72 12 17 'I+' (1-based Eclipse)
// Converts to 0-based: I[60-60], J[47-71], K[11-16]
// However, K[11-16] is outside sector K[21-58], so this should fail
auto record = createAquconRecord( 1, 61, 61, 48, 72, 12, 17, "I+" );
auto result = RigSimulationInputTool::processAquconRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
}
//--------------------------------------------------------------------------------------------------
/// Test AQUCON record without box definition (only aquifer ID)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAquconRecord_NoBoxDefinition )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 9, 9, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// Create a record with only aquifer ID (no box coordinates)
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "ID", 1 ) );
Opm::DeckRecord record{ std::move( items ) };
auto result = RigSimulationInputTool::processAquconRecord( record, sectorMin, sectorMax, refinement );
EXPECT_TRUE( result.has_value() );
EXPECT_EQ( result->size(), 1U );
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5 )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model.DATA";
// Set up export settings for sector export
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive)
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Get all keywords from the deck
std::vector<std::string> allKeywords = deckFile.keywords( false );
// Verify key keywords exist in the file
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "DIMENS" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "SPECGRID" ) != allKeywords.end() )
<< "SPECGRID keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "COORD" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ZCORN" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ACTNUM" ) != allKeywords.end() );
// Verify OPERNUM and OPERATER keywords exist (for boundary conditions with default OPERNUM_OPERATER)
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "OPERNUM" ) != allKeywords.end() )
<< "OPERNUM keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "OPERATER" ) != allKeywords.end() )
<< "OPERATER keyword missing from exported file";
// Read the file content to verify dimensions
QFile file( exportFilePath );
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
QString fileContent = file.readAll();
file.close();
// Verify dimensions are correct for the sector (20x15x10)
// Max is (19,14,9) which means 20 cells in I, 15 cells in J, 10 cells in K
EXPECT_TRUE( fileContent.contains( " 20 15 10 /" ) ) << "File does not contain expected sector dimensions 20 15 10";
}
//--------------------------------------------------------------------------------------------------
/// Test that ACTNUM and FAULTS sourced from include files stay in their include files when
/// exporting, instead of being written inline into the main DATA file. See issue #14622.
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5_ActnumFaultsStayInIncludeFiles )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
// Deck with an inline ACTNUM overridden by ACTNUM in an include file, and FAULTS in an include file
QString dataFilename( "3_ACTNUM_FAULTS_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
ASSERT_TRUE( caseData->mainGrid() != nullptr );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model.DATA";
// Set up export settings for sector export
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive)
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// The exported deck must still parse
RifOpmFlowDeckFile deckFile;
ASSERT_TRUE( deckFile.loadDeck( exportFilePath.toStdString() ).has_value() ) << "Failed to load exported deck file";
// The duplicate ACTNUM must be collapsed to a single occurrence with sector-sized data (20x15x10)
auto actnumKeywords = deckFile.findAllKeywords( "ACTNUM" );
ASSERT_EQ( 1u, actnumKeywords.size() );
EXPECT_EQ( 20u * 15u * 10u, actnumKeywords.front().getRecord( 0 ).getItem( 0 ).data_size() );
auto readTextFile = []( const QString& filePath ) -> QString
{
QFile file( filePath );
EXPECT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) ) << "Failed to open " << filePath.toStdString();
return file.readAll();
};
// The main DATA file must reference the include files instead of containing the keywords inline
QString dataContent = readTextFile( exportFilePath );
const QRegularExpression actnumRegex( "^ACTNUM", QRegularExpression::MultilineOption );
const QRegularExpression faultsRegex( "^FAULTS", QRegularExpression::MultilineOption );
EXPECT_FALSE( dataContent.contains( actnumRegex ) ) << "ACTNUM must not be written inline in the main DATA file";
EXPECT_FALSE( dataContent.contains( faultsRegex ) ) << "FAULTS must not be written inline in the main DATA file";
EXPECT_TRUE( dataContent.contains( "include/actnum_model5.grdecl" ) );
EXPECT_TRUE( dataContent.contains( "include/faults_model5.inc" ) );
// The include files must contain the transformed sector-specific keywords
QString actnumContent = readTextFile( tempDir.path() + "/include/actnum_model5.grdecl" );
EXPECT_TRUE( actnumContent.contains( actnumRegex ) ) << "ACTNUM missing from exported include file";
QString faultsContent = readTextFile( tempDir.path() + "/include/faults_model5.inc" );
EXPECT_TRUE( faultsContent.contains( faultsRegex ) ) << "FAULTS missing from exported include file";
EXPECT_FALSE( faultsContent.contains( "F1" ) ) << "Original FAULTS data should have been replaced";
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data using BCCON_BCPROP boundary condition
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithBcconBcprop )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_bccon.DATA";
// Set up export settings for sector export with BCCON_BCPROP boundary condition
// Use a smaller sector (not starting at 0,0,0) to ensure border cells are created
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 7 ) ); // Sector (0-based inclusive) - 10x10x6
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement
settings.setBoundaryCondition( RiaModelExportDefines::BCCON_BCPROP );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Get all keywords from the deck
std::vector<std::string> allKeywords = deckFile.keywords( false );
// Verify key keywords exist in the file
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "DIMENS" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "SPECGRID" ) != allKeywords.end() )
<< "SPECGRID keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "COORD" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ZCORN" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ACTNUM" ) != allKeywords.end() );
// Verify BCCON and BCPROP keywords exist (for boundary conditions with BCCON_BCPROP)
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "BCCON" ) != allKeywords.end() )
<< "BCCON keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "BCPROP" ) != allKeywords.end() )
<< "BCPROP keyword missing from exported file";
// Verify BCPROP comes before any DATES keyword (i.e., first in SCHEDULE section)
auto bcpropIt = std::find( allKeywords.begin(), allKeywords.end(), "BCPROP" );
auto datesIt = std::find( allKeywords.begin(), allKeywords.end(), "DATES" );
if ( bcpropIt != allKeywords.end() && datesIt != allKeywords.end() )
{
EXPECT_LT( std::distance( allKeywords.begin(), bcpropIt ), std::distance( allKeywords.begin(), datesIt ) )
<< "BCPROP should appear before DATES in the SCHEDULE section";
}
// Verify OPERNUM and OPERATER keywords do NOT exist (should only be present with OPERNUM_OPERATER)
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "OPERNUM" ) == allKeywords.end() )
<< "OPERNUM keyword should not be present when using BCCON_BCPROP";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "OPERATER" ) == allKeywords.end() )
<< "OPERATER keyword should not be present when using BCCON_BCPROP";
// Read the file content to verify dimensions
QFile file( exportFilePath );
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
QString fileContent = file.readAll();
file.close();
// Verify dimensions are correct for the sector (10x10x6)
// Min (5,5,2) to Max (14,14,7) means 10 cells in I, 10 cells in J, 6 cells in K
EXPECT_TRUE( fileContent.contains( " 10 10 6 /" ) ) << "File does not contain expected sector dimensions 10 10 6";
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data verifying data keyword cropping
/// Ensures PORO, EQLNUM, SATNUM are cropped to sector cell count
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5_DataKeywordCropping )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_datakw.DATA";
// Set up export settings for a smaller sector to test cropping
// Sector: min=(5,5,2), max=(14,14,7) => 10x10x6 = 600 cells
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 7 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
const size_t expectedCellCount = 10 * 10 * 6; // 600 cells
// Verify PORO keyword is cropped to sector size with correct values
{
auto poroKw = deckFile.findKeyword( "PORO" );
ASSERT_TRUE( poroKw.has_value() ) << "PORO keyword missing from exported file";
const auto& poroData = poroKw->getRawDoubleData();
EXPECT_EQ( expectedCellCount, poroData.size() ) << "PORO should have " << expectedCellCount << " values";
// Original model5 has PORO = 6000*0.28, so all cropped values should be 0.28
for ( size_t i = 0; i < poroData.size(); ++i )
{
EXPECT_DOUBLE_EQ( 0.28, poroData[i] ) << "PORO value at index " << i << " should be 0.28";
}
}
// Verify EQLNUM keyword is cropped to sector size
{
auto eqlnumKw = deckFile.findKeyword( "EQLNUM" );
ASSERT_TRUE( eqlnumKw.has_value() ) << "EQLNUM keyword missing from exported file";
const auto& eqlnumData = eqlnumKw->getIntData();
EXPECT_EQ( expectedCellCount, eqlnumData.size() ) << "EQLNUM should have " << expectedCellCount << " values";
// Original model5 has EQLNUM = 6000*1
for ( size_t i = 0; i < eqlnumData.size(); ++i )
{
EXPECT_EQ( 1, eqlnumData[i] ) << "EQLNUM value at index " << i << " should be 1";
}
}
// Verify SATNUM keyword is cropped to sector size
{
auto satnumKw = deckFile.findKeyword( "SATNUM" );
ASSERT_TRUE( satnumKw.has_value() ) << "SATNUM keyword missing from exported file";
const auto& satnumData = satnumKw->getIntData();
EXPECT_EQ( expectedCellCount, satnumData.size() ) << "SATNUM should have " << expectedCellCount << " values";
// Original model5 has SATNUM = 6000*1
for ( size_t i = 0; i < satnumData.size(); ++i )
{
EXPECT_EQ( 1, satnumData[i] ) << "SATNUM value at index " << i << " should be 1";
}
}
}
TEST( RigSimulationInputTool, ExportModel5_DataKeywordCropping_4EqlnumRegions )
{
// Load model5 test data with 4 EQLNUM regions
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "1_4EQLNUM_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_4eqlnum.DATA";
// Set up export settings for a smaller sector to test cropping
// Sector: min=(5,5,2), max=(14,14,7) => 10x10x6 = 600 cells
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 7 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
const size_t expectedCellCount = 10 * 10 * 6; // 600 cells
// Verify PORO keyword is cropped to sector size with correct values
{
auto poroKw = deckFile.findKeyword( "PORO" );
ASSERT_TRUE( poroKw.has_value() ) << "PORO keyword missing from exported file";
const auto& poroData = poroKw->getRawDoubleData();
EXPECT_EQ( expectedCellCount, poroData.size() ) << "PORO should have " << expectedCellCount << " values";
for ( size_t i = 0; i < poroData.size(); ++i )
{
EXPECT_DOUBLE_EQ( 0.28, poroData[i] ) << "PORO value at index " << i << " should be 0.28";
}
}
// Verify EQLNUM keyword is cropped to sector size with correct region values
// Full grid EQLNUM: K=1-3 region 1, K=4-5 region 2, K=6-7 region 3, K=8-10 region 4
// Sector captures K=3-8 (0-based K=2-7), so:
// Sector K=0 (full K=3): region 1 -> 100 cells (indices 0-99)
// Sector K=1-2 (full K=4-5): region 2 -> 200 cells (indices 100-299)
// Sector K=3-4 (full K=6-7): region 3 -> 200 cells (indices 300-499)
// Sector K=5 (full K=8): region 4 -> 100 cells (indices 500-599)
{
auto eqlnumKw = deckFile.findKeyword( "EQLNUM" );
ASSERT_TRUE( eqlnumKw.has_value() ) << "EQLNUM keyword missing from exported file";
const auto& eqlnumData = eqlnumKw->getIntData();
EXPECT_EQ( expectedCellCount, eqlnumData.size() ) << "EQLNUM should have " << expectedCellCount << " values";
const size_t layerSize = 10 * 10; // 100 cells per layer
for ( size_t i = 0; i < eqlnumData.size(); ++i )
{
size_t sectorK = i / layerSize;
int expectedRegion = 0;
if ( sectorK == 0 )
expectedRegion = 1;
else if ( sectorK <= 2 )
expectedRegion = 2;
else if ( sectorK <= 4 )
expectedRegion = 3;
else
expectedRegion = 4;
EXPECT_EQ( expectedRegion, eqlnumData[i] )
<< "EQLNUM value at index " << i << " (sector K=" << sectorK << ") should be " << expectedRegion;
}
}
// Verify EQUIL has 4 records (one per EQLNUM region)
{
auto equilKw = deckFile.findKeyword( "EQUIL" );
ASSERT_TRUE( equilKw.has_value() ) << "EQUIL keyword missing from exported file";
EXPECT_EQ( 4u, equilKw->size() ) << "EQUIL should have 4 records (one per EQLNUM region)";
}
}
//--------------------------------------------------------------------------------------------------
/// Helper to create a BOX DeckRecord
/// BOX format: I1 I2 J1 J2 K1 K2 (1-based Eclipse coordinates)
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createBoxRecord( int i1, int i2, int j1, int j2, int k1, int k2 )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "I1", i1 ) );
items.push_back( RifOpmDeckTools::item( "I2", i2 ) );
items.push_back( RifOpmDeckTools::item( "J1", j1 ) );
items.push_back( RifOpmDeckTools::item( "J2", j2 ) );
items.push_back( RifOpmDeckTools::item( "K1", k1 ) );
items.push_back( RifOpmDeckTools::item( "K2", k2 ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record with user-provided dimensions
/// BOX 1 46 1 112 1 22
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_LargeBox )
{
// Sector: min=(0, 0, 0), max=(45, 111, 21) (inclusive, 0-based)
// This means the sector spans exactly the same range as the BOX keyword
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 45, 111, 21 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// BOX record: 1 46 1 112 1 22 (1-based Eclipse)
// Converts to 0-based: I[0-45], J[0-111], K[0-21]
auto record = createBoxRecord( 1, 46, 1, 112, 1, 22 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Check transformed coordinates (should span entire sector, 1-based)
// I: (0-0)*1+1 = 1 to (45-0)*1+1 = 46
// J: (0-0)*1+1 = 1 to (111-0)*1+1 = 112
// K: (0-0)*1+1 = 1 to (21-0)*1+1 = 22
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 46, result->getItem( 1 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 112, result->getItem( 3 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 22, result->getItem( 5 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record with refinement (3, 3, 1) and sector offset
/// BOX 1 46 1 112 1 22
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_LargeBoxWithRefinement )
{
// Sector: min=(19, 59, 0), max=(45, 111, 21) (inclusive, 0-based)
// BOX will be clamped to this sector and then refined
caf::VecIjk0 sectorMin( 19, 59, 0 );
caf::VecIjk0 sectorMax( 45, 111, 21 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 3, 3, 1 ) ); // Refinement: I=3, J=3, K=1
// BOX record: 1 46 1 112 1 22 (1-based Eclipse)
// Converts to 0-based: I[0-45], J[0-111], K[0-21]
// After clamping to sector: I[19-45], J[59-111], K[0-21]
auto record = createBoxRecord( 1, 46, 1, 112, 1, 22 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Check transformed coordinates with refinement (1-based)
// After clamping: I[19-45], J[59-111], K[0-21] (0-based)
// Relative to sector min (19, 59, 0): I[0-26], J[0-52], K[0-21]
// This gives us: 27 cells in I, 53 cells in J, 22 cells in K
//
// With refinement (3, 3, 1):
// - I: 27 cells * 3 = 81 refined cells, 0-based [0-80], 1-based [1-81]
// - J: 53 cells * 3 = 159 refined cells, 0-based [0-158], 1-based [1-159]
// - K: 22 cells * 1 = 22 refined cells, 0-based [0-21], 1-based [1-22]
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 81, result->getItem( 1 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 159, result->getItem( 3 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 22, result->getItem( 5 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record with partial overlap requiring clamping
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_PartialOverlapWithClamping )
{
// Sector: min=(10, 20, 5), max=(39, 99, 19) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 10, 20, 5 );
caf::VecIjk0 sectorMax( 39, 99, 19 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// BOX record: 1 46 1 112 1 22 (1-based Eclipse)
// Converts to 0-based: I[0-45], J[0-111], K[0-21]
// Sector: I[10-39], J[20-99], K[5-19]
// Intersection (clamped): I[10-39], J[20-99], K[5-19]
auto record = createBoxRecord( 1, 46, 1, 112, 1, 22 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Check transformed coordinates (sector-relative, 1-based)
// After clamping: I[10-39], J[20-99], K[5-19] (0-based)
// Transformed to sector-relative (min at 10,20,5):
// I: (10-10)*1+1 = 1 to (39-10)*1+1 = 30
// J: (20-20)*1+1 = 1 to (99-20)*1+1 = 80
// K: (5-5)*1+1 = 1 to (19-5)*1+1 = 15
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 30, result->getItem( 1 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 80, result->getItem( 3 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record completely inside sector (no clamping)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_CompletelyInside )
{
// Sector: min=(0, 0, 0), max=(49, 119, 29) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 49, 119, 29 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// BOX record: 5 20 10 50 5 15 (1-based Eclipse)
// Converts to 0-based: I[4-19], J[9-49], K[4-14]
// All inside sector, no clamping needed
auto record = createBoxRecord( 5, 20, 10, 50, 5, 15 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// No clamping, direct transformation (sector min is 0,0,0)
// I: (4-0)*1+1 = 5 to (19-0)*1+1 = 20
// J: (9-0)*1+1 = 10 to (49-0)*1+1 = 50
// K: (4-0)*1+1 = 5 to (14-0)*1+1 = 15
EXPECT_EQ( 5, result->getItem( 0 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 1 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 10, result->getItem( 2 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 50, result->getItem( 3 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 5, result->getItem( 4 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record with no overlap - should return error
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_NoOverlap )
{
// Sector: min=(0, 0, 0), max=(19, 29, 9) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// BOX record: 30 50 40 60 1 10 (1-based Eclipse)
// Converts to 0-based: I[29-49], J[39-59], K[0-9]
// No overlap in I and J dimensions with sector
auto record = createBoxRecord( 30, 50, 40, 60, 1, 10 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test invalid BOX record (insufficient items)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_InvalidRecord )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 9, 9, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// Create a record with only 3 items (missing J2, K1, K2)
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "I1", 1 ) );
items.push_back( RifOpmDeckTools::item( "I2", 5 ) );
items.push_back( RifOpmDeckTools::item( "J1", 1 ) );
Opm::DeckRecord record{ std::move( items ) };
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "insufficient items" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test BOX record at sector boundary
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessBoxRecord_AtBoundary )
{
// Sector: min=(5, 10, 3), max=(24, 39, 12) (inclusive, 0-based)
caf::VecIjk0 sectorMin( 5, 10, 3 );
caf::VecIjk0 sectorMax( 24, 39, 12 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// BOX record exactly matching sector: 6 25 11 40 4 13 (1-based Eclipse)
// Converts to 0-based: I[5-24], J[10-39], K[3-12]
auto record = createBoxRecord( 6, 25, 11, 40, 4, 13 );
auto result = RigSimulationInputTool::processBoxRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should span entire sector in all dimensions
// Transformed: (5-5)*1+1 = 1 to (24-5)*1+1 = 20
EXPECT_EQ( 1, result->getItem( 0 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 1 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 30, result->getItem( 3 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 5 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data and refinement (1, 3, 5)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithRefinement )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_refined.DATA";
// Set up export settings for sector export with refinement (1, 3, 5)
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive)
setSettingsRefinement( settings, cvf::Vec3st( 1, 3, 5 ) ); // Refinement: I=1 (no refinement), J=3, K=5
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Get all keywords from the deck
std::vector<std::string> allKeywords = deckFile.keywords( false );
// Verify key keywords exist in the file
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "DIMENS" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "SPECGRID" ) != allKeywords.end() )
<< "SPECGRID keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "COORD" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ZCORN" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ACTNUM" ) != allKeywords.end() );
// Read the file content to verify dimensions
QFile file( exportFilePath );
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
QString fileContent = file.readAll();
file.close();
// Verify dimensions are correct for the refined sector
// Original sector: 20x15x10 cells
// With refinement (1, 3, 5): 20*1 x 15*3 x 10*5 = 20 x 45 x 50
EXPECT_TRUE( fileContent.contains( " 20 45 50 /" ) ) << "File does not contain expected refined dimensions 20 45 50";
// Verify WELSPECS coordinates are centered in refined cells
auto welspecsKeywords = deckFile.findAllKeywords( "WELSPECS" );
ASSERT_FALSE( welspecsKeywords.empty() ) << "WELSPECS keyword not found in exported file";
std::cout << "\n=== WELSPECS Analysis ===" << std::endl;
for ( const auto& keyword : welspecsKeywords )
{
for ( size_t i = 0; i < keyword.size(); ++i )
{
const auto& record = keyword.getRecord( i );
std::string wellName = record.getItem( "WELL" ).get<std::string>( 0 );
int iHeel = record.getItem( "HEAD_I" ).get<int>( 0 );
int jHeel = record.getItem( "HEAD_J" ).get<int>( 0 );
std::cout << "Well: " << wellName << " - WELSPECS I=" << iHeel << ", J=" << jHeel << std::endl;
}
}
// Verify specific well coordinates are centered correctly
// For refinement (1, 3, 5):
// - I dimension: refinement=1, center offset = (1+1)/2 = 1
// - J dimension: refinement=3, center offset = (3+1)/2 = 2
// Formula: new_coord = (old_coord - sector_min) * refinement + center_offset
auto checkWelspecsCoordinates = [&welspecsKeywords]( const std::string& wellName, int expectedI, int expectedJ ) -> bool
{
return std::any_of( welspecsKeywords.begin(),
welspecsKeywords.end(),
[&]( const auto& kw )
{
for ( size_t i = 0; i < kw.size(); ++i )
{
const auto& rec = kw.getRecord( i );
if ( rec.getItem( "WELL" ).template get<std::string>( 0 ) == wellName )
{
return rec.getItem( "HEAD_I" ).template get<int>( 0 ) == expectedI &&
rec.getItem( "HEAD_J" ).template get<int>( 0 ) == expectedJ;
}
}
return false;
} );
};
//
// B-1H: original (11, 3), sector_min (0, 0)
// I: (11-0)*1 + 1 = 11, J: (3-0)*3 + 2 = 8
EXPECT_TRUE( checkWelspecsCoordinates( "B-1H", 11, 8 ) );
// B-2H: original (4, 7)
// I: (4-0)*1 + 1 = 4, J: (7-0)*3 + 2 = 20
EXPECT_TRUE( checkWelspecsCoordinates( "B-2H", 4, 20 ) );
// Verify COMPDAT coordinates are centered in refined cells
auto compdatKeywords = deckFile.findAllKeywords( "COMPDAT" );
ASSERT_FALSE( compdatKeywords.empty() ) << "COMPDAT keyword not found in exported file";
std::cout << "\n=== COMPDAT Analysis ===" << std::endl;
for ( const auto& keyword : compdatKeywords )
{
for ( size_t i = 0; i < keyword.size(); ++i )
{
const auto& record = keyword.getRecord( i );
std::string wellName = record.getItem( "WELL" ).get<std::string>( 0 );
int iComp = record.getItem( "I" ).get<int>( 0 );
int jComp = record.getItem( "J" ).get<int>( 0 );
int k1 = record.getItem( "K1" ).get<int>( 0 );
int k2 = record.getItem( "K2" ).get<int>( 0 );
std::cout << "Well: " << wellName << " - COMPDAT I=" << iComp << ", J=" << jComp << ", K1=" << k1 << ", K2=" << k2 << std::endl;
}
}
// Verify COMPDAT K ranges cover all refined sub-cells
// For refinement (1, 3, 5):
// - K dimension: refinement=5, original layer k maps to refined layers (k-1)*5+1 .. k*5
auto checkCompdatCoordinates = [&compdatKeywords]( const std::string& wellName, int expectedK1, int expectedK2 ) -> bool
{
return std::any_of( compdatKeywords.begin(),
compdatKeywords.end(),
[&]( const auto& kw )
{
for ( size_t i = 0; i < kw.size(); ++i )
{
const auto& rec = kw.getRecord( i );
if ( rec.getItem( "WELL" ).template get<std::string>( 0 ) == wellName )
{
return rec.getItem( "K1" ).template get<int>( 0 ) == expectedK1 &&
rec.getItem( "K2" ).template get<int>( 0 ) == expectedK2;
}
}
return false;
} );
};
// B-1H: original K1=1, K2=5 (defaulted CF), sector_min K=0
// All sub-cells of the original range: K1 = 1, K2 = 5*5 = 25
EXPECT_TRUE( checkCompdatCoordinates( "B-1H", 1, 25 ) ) << "B-1H COMPDAT K range does not cover all refined sub-cells";
// B-2H: original K1=1, K2=6
// K1 = 1, K2 = 6*5 = 30
EXPECT_TRUE( checkCompdatCoordinates( "B-2H", 1, 30 ) ) << "B-2H COMPDAT K range does not cover all refined sub-cells";
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data and refinement (3, 5, 1)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithRefinement_3_5_1 )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_refined_3_5_1.DATA";
// Set up export settings for sector export with refinement (3, 5, 1)
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive)
setSettingsRefinement( settings, cvf::Vec3st( 3, 5, 1 ) ); // Refinement: I=3, J=5, K=1 (no refinement in K)
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Get all keywords from the deck
std::vector<std::string> allKeywords = deckFile.keywords( false );
// Verify key keywords exist in the file
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "DIMENS" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "SPECGRID" ) != allKeywords.end() )
<< "SPECGRID keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "COORD" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ZCORN" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ACTNUM" ) != allKeywords.end() );
// Read the file content to verify dimensions
QFile file( exportFilePath );
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
QString fileContent = file.readAll();
file.close();
// Verify dimensions are correct for the refined sector
// Original sector: 20x15x10 cells
// With refinement (3, 5, 1): 20*3 x 15*5 x 10*1 = 60 x 75 x 10
EXPECT_TRUE( fileContent.contains( " 60 75 10 /" ) ) << "File does not contain expected refined dimensions 60 75 10";
// Verify WELSPECS coordinates are centered in refined cells
auto welspecsKeywords = deckFile.findAllKeywords( "WELSPECS" );
ASSERT_FALSE( welspecsKeywords.empty() ) << "WELSPECS keyword not found in exported file";
std::cout << "\n=== WELSPECS Analysis (3, 5, 1) ===" << std::endl;
for ( const auto& keyword : welspecsKeywords )
{
for ( size_t i = 0; i < keyword.size(); ++i )
{
const auto& record = keyword.getRecord( i );
std::string wellName = record.getItem( "WELL" ).get<std::string>( 0 );
int iHeel = record.getItem( "HEAD_I" ).get<int>( 0 );
int jHeel = record.getItem( "HEAD_J" ).get<int>( 0 );
std::cout << "Well: " << wellName << " - WELSPECS I=" << iHeel << ", J=" << jHeel << std::endl;
}
}
// Verify specific well coordinates are centered correctly
// For refinement (3, 5, 1):
// - I dimension: refinement=3, center offset = (3+1)/2 = 2
// - J dimension: refinement=5, center offset = (5+1)/2 = 3
// Formula: new_coord = (old_coord - sector_min) * refinement + center_offset
auto checkWelspecsCoordinates = [&welspecsKeywords]( const std::string& wellName, int expectedI, int expectedJ ) -> bool
{
return std::any_of( welspecsKeywords.begin(),
welspecsKeywords.end(),
[&]( const auto& kw )
{
for ( size_t i = 0; i < kw.size(); ++i )
{
const auto& rec = kw.getRecord( i );
if ( rec.getItem( "WELL" ).template get<std::string>( 0 ) == wellName )
{
return rec.getItem( "HEAD_I" ).template get<int>( 0 ) == expectedI &&
rec.getItem( "HEAD_J" ).template get<int>( 0 ) == expectedJ;
}
}
return false;
} );
};
// B-1H: original (11, 3) in 1-based Eclipse, converted to 0-based (10, 2), sector_min (0, 0)
// I: (10-0)*3 + 2 = 32, J: (2-0)*5 + 3 = 13
EXPECT_TRUE( checkWelspecsCoordinates( "B-1H", 32, 13 ) );
// B-2H: original (4, 7) in 1-based Eclipse, converted to 0-based (3, 6)
// I: (3-0)*3 + 2 = 11, J: (6-0)*5 + 3 = 33
EXPECT_TRUE( checkWelspecsCoordinates( "B-2H", 11, 33 ) );
// Verify COMPDAT coordinates are centered in refined cells
auto compdatKeywords = deckFile.findAllKeywords( "COMPDAT" );
ASSERT_FALSE( compdatKeywords.empty() ) << "COMPDAT keyword not found in exported file";
std::cout << "\n=== COMPDAT Analysis (3, 5, 1) ===" << std::endl;
for ( const auto& keyword : compdatKeywords )
{
for ( size_t i = 0; i < keyword.size(); ++i )
{
const auto& record = keyword.getRecord( i );
std::string wellName = record.getItem( "WELL" ).get<std::string>( 0 );
int iComp = record.getItem( "I" ).get<int>( 0 );
int jComp = record.getItem( "J" ).get<int>( 0 );
int k1 = record.getItem( "K1" ).get<int>( 0 );
int k2 = record.getItem( "K2" ).get<int>( 0 );
std::cout << "Well: " << wellName << " - COMPDAT I=" << iComp << ", J=" << jComp << ", K1=" << k1 << ", K2=" << k2 << std::endl;
}
}
// Verify specific COMPDAT coordinates are centered correctly
// For refinement (3, 5, 1):
// - K dimension: refinement=1, center offset = (1+1)/2 = 1
auto checkCompdatCoordinates = [&compdatKeywords]( const std::string& wellName, int expectedK1, int expectedK2 ) -> bool
{
return std::any_of( compdatKeywords.begin(),
compdatKeywords.end(),
[&]( const auto& kw )
{
for ( size_t i = 0; i < kw.size(); ++i )
{
const auto& rec = kw.getRecord( i );
if ( rec.getItem( "WELL" ).template get<std::string>( 0 ) == wellName )
{
return rec.getItem( "K1" ).template get<int>( 0 ) == expectedK1 &&
rec.getItem( "K2" ).template get<int>( 0 ) == expectedK2;
}
}
return false;
} );
};
// B-1H: original K1=1, K2=5, sector_min K=0
// K1: (1-0)*1 + 1 = 1, K2: (5-0)*1 + 1 = 5
EXPECT_TRUE( checkCompdatCoordinates( "B-1H", 1, 5 ) ) << "B-1H COMPDAT K coordinates not centered correctly";
// B-2H: original K1=1, K2=6
// 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), grid is 20x30x10
caf::VecIjk0 sectorMin( 5, 10, 2 );
auto refinement = RigUniformRefinement( cvf::Vec3st( 1, 1, 1 ), cvf::Vec3st( 20, 30, 10 ) );
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 );
auto refinement = RigUniformRefinement( cvf::Vec3st( 2, 2, 1 ), cvf::Vec3st( 20, 30, 10 ) ); // Refine by 2x2x1, grid is 20x30x10
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 );
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data and model padding
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithPadding )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_padded.DATA";
// Set up export settings for sector export with padding
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive) -> 20x15x10
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Configure padding: 2 upper, 3 lower
RigModelPaddingSettings paddingSettings;
paddingSettings.setEnabled( true );
paddingSettings.setNzUpper( 2 );
paddingSettings.setNzLower( 3 );
paddingSettings.setTopUpper( 1000.0 );
paddingSettings.setBottomLower( 3600.0 );
paddingSettings.setUpperPorosity( 0.1 );
paddingSettings.setMinLayerThickness( 10.0 );
paddingSettings.setVerticalPillars( true );
paddingSettings.setMonotonicZcorn( true );
paddingSettings.setFillGaps( true );
settings.setPaddingSettings( paddingSettings );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Get all keywords from the deck
std::vector<std::string> allKeywords = deckFile.keywords( false );
// Verify key keywords exist in the file
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "DIMENS" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "SPECGRID" ) != allKeywords.end() )
<< "SPECGRID keyword missing from exported file";
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "COORD" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ZCORN" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "ACTNUM" ) != allKeywords.end() );
EXPECT_TRUE( std::find( allKeywords.begin(), allKeywords.end(), "PORO" ) != allKeywords.end() );
// Read the file content to verify padded dimensions
QFile file( exportFilePath );
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
QString fileContent = file.readAll();
file.close();
// Verify dimensions are correct for the padded sector (20x15x15)
// Original sector: 20x15x10, with nzUpper=2, nzLower=3: 20x15x(10+2+3) = 20x15x15
EXPECT_TRUE( fileContent.contains( " 20 15 15 /" ) ) << "File does not contain expected padded dimensions 20 15 15";
// Verify SPECGRID/DIMENS show correct padded dimensions
auto specgridKw = deckFile.findKeyword( "SPECGRID" );
ASSERT_TRUE( specgridKw.has_value() );
EXPECT_EQ( 20, specgridKw->getRecord( 0 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_EQ( 15, specgridKw->getRecord( 0 ).getItem( 1 ).get<int>( 0 ) );
EXPECT_EQ( 15, specgridKw->getRecord( 0 ).getItem( 2 ).get<int>( 0 ) );
// Verify COORD array size: (NX+1)*(NY+1)*6 = 21*16*6 = 2016 (unchanged by padding)
auto coordKw = deckFile.findKeyword( "COORD" );
ASSERT_TRUE( coordKw.has_value() );
EXPECT_EQ( 2016u, coordKw->getRecord( 0 ).getItem( 0 ).data_size() );
// Verify ZCORN array size: NX*NY*NZ_new*8 = 20*15*15*8 = 36000
auto zcornKw = deckFile.findKeyword( "ZCORN" );
ASSERT_TRUE( zcornKw.has_value() );
EXPECT_EQ( 36000u, zcornKw->getRecord( 0 ).getItem( 0 ).data_size() );
// Verify ACTNUM array size: NX*NY*NZ_new = 20*15*15 = 4500
auto actnumKw = deckFile.findKeyword( "ACTNUM" );
ASSERT_TRUE( actnumKw.has_value() );
EXPECT_EQ( 4500u, actnumKw->getRecord( 0 ).getItem( 0 ).data_size() );
}
//--------------------------------------------------------------------------------------------------
/// Helper to create an ADD DeckRecord with box indices
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createAddRecord( const std::string& fieldName, double shift, int i1, int i2, int j1, int j2, int k1, int k2 )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", fieldName ) );
items.push_back( RifOpmDeckTools::item( "SHIFT", shift ) );
items.push_back( RifOpmDeckTools::item( "I1", i1 ) );
items.push_back( RifOpmDeckTools::item( "I2", i2 ) );
items.push_back( RifOpmDeckTools::item( "J1", j1 ) );
items.push_back( RifOpmDeckTools::item( "J2", j2 ) );
items.push_back( RifOpmDeckTools::item( "K1", k1 ) );
items.push_back( RifOpmDeckTools::item( "K2", k2 ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Helper to create an ADD DeckRecord without box indices
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createAddRecordNoBox( const std::string& fieldName, double shift )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", fieldName ) );
items.push_back( RifOpmDeckTools::item( "SHIFT", shift ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Helper to create a MULTIPLY DeckRecord with box indices
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createMultiplyRecord( const std::string& fieldName, double factor, int i1, int i2, int j1, int j2, int k1, int k2 )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", fieldName ) );
items.push_back( RifOpmDeckTools::item( "FACTOR", factor ) );
items.push_back( RifOpmDeckTools::item( "I1", i1 ) );
items.push_back( RifOpmDeckTools::item( "I2", i2 ) );
items.push_back( RifOpmDeckTools::item( "J1", j1 ) );
items.push_back( RifOpmDeckTools::item( "J2", j2 ) );
items.push_back( RifOpmDeckTools::item( "K1", k1 ) );
items.push_back( RifOpmDeckTools::item( "K2", k2 ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Helper to create a MULTIPLY DeckRecord without box indices
//--------------------------------------------------------------------------------------------------
static Opm::DeckRecord createMultiplyRecordNoBox( const std::string& fieldName, double factor )
{
std::vector<Opm::DeckItem> items;
items.push_back( RifOpmDeckTools::item( "FIELD", fieldName ) );
items.push_back( RifOpmDeckTools::item( "FACTOR", factor ) );
return Opm::DeckRecord{ std::move( items ) };
}
//--------------------------------------------------------------------------------------------------
/// Test ADD record without explicit box indices - should pass through with just field+shift
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAddRecord_NoBoxIndices )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
auto record = createAddRecordNoBox( "PORO", 0.1 );
auto result = RigSimulationInputTool::processAddRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should have only 2 items (field name and shift value, no box indices)
EXPECT_EQ( 2u, result->size() );
EXPECT_EQ( "PORO", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 0.1, result->getItem( 1 ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Test ADD record with explicit box indices - should transform coordinates
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAddRecord_WithBoxIndices )
{
// Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9)
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// ADD record with box fully inside sector (1-based Eclipse coords)
auto record = createAddRecord( "PORO", 0.05, 1, 20, 16, 30, 1, 10 );
auto result = RigSimulationInputTool::processAddRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should have 8 items with transformed coordinates
EXPECT_EQ( 8u, result->size() );
EXPECT_EQ( "PORO", result->getItem( 0 ).get<std::string>( 0 ) );
// Transformed: I stays same (1-20), J transforms from [16-30] to [1-15], K stays (1-10)
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY record without explicit box indices - should pass through with just field+factor
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessMultiplyRecord_NoBoxIndices )
{
caf::VecIjk0 sectorMin( 0, 0, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
auto record = createMultiplyRecordNoBox( "PERMX", 2.0 );
auto result = RigSimulationInputTool::processMultiplyRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should have only 2 items (field name and factor, no box indices)
EXPECT_EQ( 2u, result->size() );
EXPECT_EQ( "PERMX", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 2.0, result->getItem( 1 ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY record with explicit box indices - should transform coordinates
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessMultiplyRecord_WithBoxIndices )
{
// Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9)
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// MULTIPLY record with box fully inside sector
auto record = createMultiplyRecord( "PERMX", 3.0, 1, 20, 16, 30, 1, 10 );
auto result = RigSimulationInputTool::processMultiplyRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Should have 8 items with transformed coordinates
EXPECT_EQ( 8u, result->size() );
EXPECT_EQ( "PERMX", result->getItem( 0 ).get<std::string>( 0 ) );
// Transformed coordinates
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test transformKeywordsInDeckFile: EQUALS inside BOX/ENDBOX with no explicit indices
/// gets BOX indices injected and then transformed to sector-relative coordinates.
/// Using sector covering the whole grid so transformed coords = original coords.
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExpandBoxContext_EqualsInheritsBoxIndices )
{
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_box.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "BOX\n 2 8 3 7 1 5 /\n\n" );
file.write( "EQUALS\n 'PORO' 0.25 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Verify EQUALS has no explicit box indices before transformation
auto equalsBeforeVec = deckFile.findAllKeywordsWithIndices( "EQUALS" );
ASSERT_EQ( 1u, equalsBeforeVec.size() );
ASSERT_EQ( 1u, equalsBeforeVec[0].second.size() );
const auto& recBefore = equalsBeforeVec[0].second.getRecord( 0 );
bool hadExplicitBefore = recBefore.size() >= 8 && recBefore.getItem( 2 ).hasValue( 0 ) && recBefore.getItem( 3 ).hasValue( 0 ) &&
recBefore.getItem( 4 ).hasValue( 0 ) && recBefore.getItem( 5 ).hasValue( 0 ) &&
recBefore.getItem( 6 ).hasValue( 0 ) && recBefore.getItem( 7 ).hasValue( 0 );
EXPECT_FALSE( hadExplicitBefore ) << "Record should not have explicit box indices before transformation";
// Sector covers entire grid: sectorMin=(0,0,0), sectorMax=(9,9,4), refinement=(1,1,1)
// This makes coordinate transformation an identity operation
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 9, 9, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
// Verify EQUALS now has explicit box indices matching the BOX keyword
auto equalsAfterVec = deckFile.findAllKeywordsWithIndices( "EQUALS" );
ASSERT_EQ( 1u, equalsAfterVec.size() );
ASSERT_EQ( 1u, equalsAfterVec[0].second.size() );
const auto& recAfter = equalsAfterVec[0].second.getRecord( 0 );
ASSERT_GE( recAfter.size(), 8u );
EXPECT_TRUE( recAfter.getItem( 2 ).hasValue( 0 ) );
EXPECT_EQ( 2, recAfter.getItem( 2 ).get<int>( 0 ) ); // I1 from BOX
EXPECT_EQ( 8, recAfter.getItem( 3 ).get<int>( 0 ) ); // I2 from BOX
EXPECT_EQ( 3, recAfter.getItem( 4 ).get<int>( 0 ) ); // J1 from BOX
EXPECT_EQ( 7, recAfter.getItem( 5 ).get<int>( 0 ) ); // J2 from BOX
EXPECT_EQ( 1, recAfter.getItem( 6 ).get<int>( 0 ) ); // K1 from BOX
EXPECT_EQ( 5, recAfter.getItem( 7 ).get<int>( 0 ) ); // K2 from BOX
}
//--------------------------------------------------------------------------------------------------
/// Test transformKeywordsInDeckFile: records with explicit indices keep their own indices
/// (not overridden by BOX indices) and are transformed to sector-relative coordinates.
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExpandBoxContext_ExplicitIndicesPreserved )
{
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_box_explicit.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "BOX\n 2 8 3 7 1 5 /\n\n" );
file.write( "EQUALS\n 'PORO' 0.25 1 5 1 5 1 3 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Sector covers entire grid
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 9, 9, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
auto equalsVec = deckFile.findAllKeywordsWithIndices( "EQUALS" );
ASSERT_EQ( 1u, equalsVec.size() );
ASSERT_EQ( 1u, equalsVec[0].second.size() );
const auto& rec = equalsVec[0].second.getRecord( 0 );
ASSERT_GE( rec.size(), 8u );
// Should keep original explicit indices, not BOX indices
EXPECT_EQ( 1, rec.getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 5, rec.getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, rec.getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 5, rec.getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, rec.getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 3, rec.getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test transformKeywordsInDeckFile: EQUALS outside BOX context without explicit indices
/// passes through unchanged (no box indices injected).
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExpandBoxContext_OutsideBoxNotModified )
{
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_no_box.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "EQUALS\n 'PORO' 0.25 /\n/\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Sector covers entire grid
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 9, 9, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
auto equalsVec = deckFile.findAllKeywordsWithIndices( "EQUALS" );
ASSERT_EQ( 1u, equalsVec.size() );
ASSERT_EQ( 1u, equalsVec[0].second.size() );
const auto& rec = equalsVec[0].second.getRecord( 0 );
// Should still not have explicit box indices
bool hasExplicit = rec.size() >= 8 && rec.getItem( 2 ).hasValue( 0 ) && rec.getItem( 3 ).hasValue( 0 ) && rec.getItem( 4 ).hasValue( 0 ) &&
rec.getItem( 5 ).hasValue( 0 ) && rec.getItem( 6 ).hasValue( 0 ) && rec.getItem( 7 ).hasValue( 0 );
EXPECT_FALSE( hasExplicit );
}
//--------------------------------------------------------------------------------------------------
/// Test transformKeywordsInDeckFile: data keywords inside BOX/ENDBOX are cropped
/// to the intersection of the box with the sector
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, CropDataKeywordsInsideBoxContext )
{
// Grid: 10x10x5. BOX covers K=5 (1-based), i.e. K-index 4 (0-based).
// Sector: (2,2,1)-(7,7,4) => covers part of the box region.
// BOX: I[0-9], J[0-9], K[4-4] => 100 values
// Intersection: I[2-7], J[2-7], K[4-4] => 6x6x1 = 36 values
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_box_crop.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "REGIONS\n\n" );
file.write( "EQLNUM\n 500*1 /\n\n" );
file.write( "BOX\n 1 10 1 10 5 5 /\n\n" );
file.write( "EQLNUM\n 100*2 /\n\n" );
file.write( "ENDBOX\n\n" );
file.write( "SATNUM\n 500*1 /\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Before cropping: should find two EQLNUM keywords
auto eqlnumBefore = deckFile.findAllKeywordsWithIndices( "EQLNUM" );
EXPECT_EQ( 2u, eqlnumBefore.size() );
// Run transformation with sector (2,2,1)-(7,7,4), no refinement
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 1 ) );
settings.setMax( caf::VecIjk0( 7, 7, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
// After cropping: should still find two EQLNUM keywords
auto eqlnumAfter = deckFile.findAllKeywordsWithIndices( "EQLNUM" );
ASSERT_EQ( 2u, eqlnumAfter.size() );
// First EQLNUM (full grid) should be unchanged (500 values)
EXPECT_EQ( 500u, eqlnumAfter[0].second.getRecord( 0 ).getItem( 0 ).data_size() );
// Second EQLNUM (inside BOX) should be cropped to 6x6x1 = 36 values
const auto& croppedKw = eqlnumAfter[1].second;
EXPECT_EQ( 36u, croppedKw.getRecord( 0 ).getItem( 0 ).data_size() );
// All cropped values should still be 2
const auto& croppedData = croppedKw.getIntData();
for ( size_t i = 0; i < croppedData.size(); ++i )
{
EXPECT_EQ( 2, croppedData[i] ) << "Cropped EQLNUM value at index " << i << " should be 2";
}
// SATNUM outside BOX should still be present and unchanged
auto satnumAfter = deckFile.findAllKeywordsWithIndices( "SATNUM" );
EXPECT_EQ( 1u, satnumAfter.size() );
EXPECT_EQ( 500u, satnumAfter[0].second.getRecord( 0 ).getItem( 0 ).data_size() );
}
//--------------------------------------------------------------------------------------------------
/// Test transformKeywordsInDeckFile: box that doesn't intersect sector removes data keyword
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, CropDataKeywordsInsideBoxContext_NoIntersection )
{
// Grid: 10x10x5. BOX covers K=5 (1-based), i.e. K-index 4 (0-based).
// Sector: (2,2,0)-(7,7,2) => does NOT include K=4, so no intersection.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_box_nointersect.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "REGIONS\n\n" );
file.write( "EQLNUM\n 500*1 /\n\n" );
file.write( "BOX\n 1 10 1 10 5 5 /\n\n" );
file.write( "EQLNUM\n 100*2 /\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Sector does not include K=4
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 0 ) );
settings.setMax( caf::VecIjk0( 7, 7, 2 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
// Boxed EQLNUM should be removed since box doesn't intersect sector
auto eqlnumAfter = deckFile.findAllKeywordsWithIndices( "EQLNUM" );
ASSERT_EQ( 1u, eqlnumAfter.size() );
EXPECT_EQ( 500u, eqlnumAfter[0].second.getRecord( 0 ).getItem( 0 ).data_size() );
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 BOX data: EQLNUM inside BOX/ENDBOX
/// Verifies that the boxed EQLNUM is cropped to the sector/box intersection and the
/// BOX/ENDBOX structure is preserved in the exported deck.
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5_DataKeywordInsideBox )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "2_BOX_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_box_model.DATA";
// Set up export settings: sector includes K=10 to verify BOX override
// Sector: min=(5,5,2), max=(14,14,9) => 10x10x8 = 800 cells (includes K=10 which is K-index 9)
// BOX in input is: I[1-20], J[1-30], K[10-10] (1-based) = I[0-19], J[0-29], K[9-9] (0-based)
// Intersection with sector: I[5-14], J[5-14], K[9-9] => 10x10x1 = 100 cells
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 9 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
const size_t sectorNx = 10;
const size_t sectorNy = 10;
const size_t sectorNz = 8;
const size_t expectedCellCount = sectorNx * sectorNy * sectorNz; // 800 cells
// Both EQLNUM keywords should exist: the full-grid one (cropped to sector) and the boxed one (cropped to intersection)
auto eqlnumAll = deckFile.findAllKeywordsWithIndices( "EQLNUM" );
EXPECT_EQ( 2u, eqlnumAll.size() ) << "Should have two EQLNUM keywords (full-grid + cropped boxed)";
// First EQLNUM (full grid, cropped to sector): 10x10x8 = 800 values
{
const auto& firstKw = eqlnumAll[0].second;
const auto& firstData = firstKw.getIntData();
EXPECT_EQ( expectedCellCount, firstData.size() ) << "First EQLNUM should have " << expectedCellCount << " values";
}
// Second EQLNUM (boxed, cropped to sector/box intersection): 10x10x1 = 100 values
{
const size_t expectedBoxedCount = sectorNx * sectorNy * 1; // 100 cells
const auto& secondKw = eqlnumAll[1].second;
const auto& secondData = secondKw.getIntData();
EXPECT_EQ( expectedBoxedCount, secondData.size() ) << "Boxed EQLNUM should have " << expectedBoxedCount << " values";
// All boxed values should be 2 (the BOX override value)
for ( size_t i = 0; i < secondData.size(); ++i )
{
EXPECT_EQ( 2, secondData[i] ) << "Boxed EQLNUM value at index " << i << " should be 2";
}
}
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY record with no overlap - should return error
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessMultiplyRecord_NoOverlap )
{
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// MULTIPLY record: PERMX 2.0 1 20 1 14 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[0-13], K[0-9]
// Sector J is [15-29], so no overlap in J dimension
auto record = createMultiplyRecord( "PERMX", 2.0, 1, 20, 1, 14, 1, 10 );
auto result = RigSimulationInputTool::processMultiplyRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY record with partial overlap requiring clamping
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessMultiplyRecord_PartialOverlapWithClamping )
{
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// MULTIPLY record: PERMX 3.5 1 20 15 30 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[14-29], K[0-9]
// Clamped to sector: I[0-19], J[15-29], K[0-9]
auto record = createMultiplyRecord( "PERMX", 3.5, 1, 20, 15, 30, 1, 10 );
auto result = RigSimulationInputTool::processMultiplyRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Transformed to sector-relative (1-based):
// I: 1 to 20, J: 1 to 15, K: 1 to 10
EXPECT_EQ( "PERMX", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 3.5, result->getItem( 1 ).get<double>( 0 ) );
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY record completely inside sector (no clamping)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessMultiplyRecord_CompletelyInsideSector )
{
caf::VecIjk0 sectorMin( 2, 2, 1 );
caf::VecIjk0 sectorMax( 7, 7, 4 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// MULTIPLY record: PORV 0.5 4 6 4 6 2 4 (1-based Eclipse)
// Converts to 0-based: I[3-5], J[3-5], K[1-3] — inside sector
auto record = createMultiplyRecord( "PORV", 0.5, 4, 6, 4, 6, 2, 4 );
auto result = RigSimulationInputTool::processMultiplyRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Transformed to sector-relative (1-based):
// I: (3-2)+1=2 to (5-2)+1=4, J: (3-2)+1=2 to (5-2)+1=4, K: (1-1)+1=1 to (3-1)+1=3
EXPECT_EQ( "PORV", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 0.5, result->getItem( 1 ).get<double>( 0 ) );
EXPECT_EQ( 2, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 4, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 2, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 4, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 3, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test ADD record with no overlap - should return error
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAddRecord_NoOverlap )
{
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// ADD record: PRESSURE 100.0 1 20 1 14 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[0-13], K[0-9]
// Sector J is [15-29], so no overlap
auto record = createAddRecord( "PRESSURE", 100.0, 1, 20, 1, 14, 1, 10 );
auto result = RigSimulationInputTool::processAddRecord( record, sectorMin, sectorMax, refinement );
EXPECT_FALSE( result.has_value() );
EXPECT_TRUE( result.error().contains( "does not overlap" ) );
}
//--------------------------------------------------------------------------------------------------
/// Test ADD record with partial overlap requiring clamping
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAddRecord_PartialOverlapWithClamping )
{
caf::VecIjk0 sectorMin( 0, 15, 0 );
caf::VecIjk0 sectorMax( 19, 29, 9 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// ADD record: PRESSURE -50.0 1 20 15 30 1 10 (1-based Eclipse)
// Converts to 0-based: I[0-19], J[14-29], K[0-9]
// Clamped to sector: I[0-19], J[15-29], K[0-9]
auto record = createAddRecord( "PRESSURE", -50.0, 1, 20, 15, 30, 1, 10 );
auto result = RigSimulationInputTool::processAddRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Transformed to sector-relative (1-based):
// I: 1 to 20, J: 1 to 15, K: 1 to 10
EXPECT_EQ( "PRESSURE", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( -50.0, result->getItem( 1 ).get<double>( 0 ) );
EXPECT_EQ( 1, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 20, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 15, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 10, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test ADD record completely inside sector (no clamping)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessAddRecord_CompletelyInsideSector )
{
caf::VecIjk0 sectorMin( 2, 2, 1 );
caf::VecIjk0 sectorMax( 7, 7, 4 );
auto refinement = makeRef( sectorMin, sectorMax, cvf::Vec3st( 1, 1, 1 ) );
// ADD record: PORO 0.01 4 6 4 6 2 4 (1-based Eclipse)
// Converts to 0-based: I[3-5], J[3-5], K[1-3] — inside sector
auto record = createAddRecord( "PORO", 0.01, 4, 6, 4, 6, 2, 4 );
auto result = RigSimulationInputTool::processAddRecord( record, sectorMin, sectorMax, refinement );
ASSERT_TRUE( result.has_value() );
// Transformed to sector-relative (1-based):
// I: (3-2)+1=2 to (5-2)+1=4, J: (3-2)+1=2 to (5-2)+1=4, K: (1-1)+1=1 to (3-1)+1=3
EXPECT_EQ( "PORO", result->getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 0.01, result->getItem( 1 ).get<double>( 0 ) );
EXPECT_EQ( 2, result->getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 4, result->getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 2, result->getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 4, result->getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, result->getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 3, result->getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY inside BOX/ENDBOX context: coordinates are transformed correctly
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, MultiplyInsideBoxContext_NoIntersection )
{
// Grid: 10x10x5. BOX covers I[1-10], J[1-10], K[5-5] (1-based = K=4 0-based).
// Sector: (2,2,0)-(7,7,2) => K[0-2], does NOT include K=4.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_multiply_box.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "PROPS\n\n" );
file.write( "BOX\n 1 10 1 10 5 5 /\n\n" );
file.write( "MULTIPLY\n PERMX 2.0 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Sector (2,2,0)-(7,7,2) does NOT include K=4 (0-based)
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 0 ) );
settings.setMax( caf::VecIjk0( 7, 7, 2 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
// MULTIPLY record with box K=5 (0-based K=4) outside sector K[0-2] should be removed
auto multiplyAll = deckFile.findAllKeywordsWithIndices( "MULTIPLY" );
if ( !multiplyAll.empty() )
{
EXPECT_EQ( 0u, multiplyAll[0].second.size() );
}
}
//--------------------------------------------------------------------------------------------------
/// Test MULTIPLY inside BOX/ENDBOX with intersection: coordinates transformed correctly
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, MultiplyInsideBoxContext_WithIntersection )
{
// Grid: 10x10x5. BOX covers I[3-8], J[3-8], K[2-4] (1-based).
// Sector: (2,2,1)-(7,7,4) (0-based) => I[2-7], J[2-7], K[1-4]
// BOX 0-based: I[2-7], J[2-7], K[1-3]
// Intersection: I[2-7], J[2-7], K[1-3] => fully inside sector
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_multiply_box_intersect.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "PROPS\n\n" );
file.write( "BOX\n 3 8 3 8 2 4 /\n\n" );
file.write( "MULTIPLY\n PERMX 2.5 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 1 ) );
settings.setMax( caf::VecIjk0( 7, 7, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
auto multiplyAll = deckFile.findAllKeywordsWithIndices( "MULTIPLY" );
ASSERT_EQ( 1u, multiplyAll.size() );
const auto& kw = multiplyAll[0].second;
ASSERT_GE( kw.size(), 1u );
const auto& rec = kw.getRecord( 0 );
EXPECT_EQ( "PERMX", rec.getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 2.5, rec.getItem( 1 ).get<double>( 0 ) );
// BOX 0-based I[2-7], J[2-7], K[1-3], sector min (2,2,1)
// Sector-relative (1-based): I: 1-6, J: 1-6, K: 1-3
EXPECT_EQ( 1, rec.getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 6, rec.getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, rec.getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 6, rec.getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, rec.getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 3, rec.getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test ADD inside BOX/ENDBOX with intersection: coordinates transformed correctly
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, AddInsideBoxContext_WithIntersection )
{
// Grid: 10x10x5. BOX covers I[3-8], J[3-8], K[2-4] (1-based).
// Sector: (2,2,1)-(7,7,4) (0-based)
// BOX 0-based: I[2-7], J[2-7], K[1-3] — fully inside sector
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_add_box_intersect.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "PROPS\n\n" );
file.write( "BOX\n 3 8 3 8 2 4 /\n\n" );
file.write( "ADD\n PRESSURE 100.0 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 1 ) );
settings.setMax( caf::VecIjk0( 7, 7, 4 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
auto addAll = deckFile.findAllKeywordsWithIndices( "ADD" );
ASSERT_EQ( 1u, addAll.size() );
const auto& kw = addAll[0].second;
ASSERT_GE( kw.size(), 1u );
const auto& rec = kw.getRecord( 0 );
EXPECT_EQ( "PRESSURE", rec.getItem( 0 ).get<std::string>( 0 ) );
EXPECT_DOUBLE_EQ( 100.0, rec.getItem( 1 ).get<double>( 0 ) );
// Sector-relative (1-based): I: 1-6, J: 1-6, K: 1-3
EXPECT_EQ( 1, rec.getItem( 2 ).get<int>( 0 ) ); // I1
EXPECT_EQ( 6, rec.getItem( 3 ).get<int>( 0 ) ); // I2
EXPECT_EQ( 1, rec.getItem( 4 ).get<int>( 0 ) ); // J1
EXPECT_EQ( 6, rec.getItem( 5 ).get<int>( 0 ) ); // J2
EXPECT_EQ( 1, rec.getItem( 6 ).get<int>( 0 ) ); // K1
EXPECT_EQ( 3, rec.getItem( 7 ).get<int>( 0 ) ); // K2
}
//--------------------------------------------------------------------------------------------------
/// Test ADD inside BOX/ENDBOX with no intersection: record should be removed
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, AddInsideBoxContext_NoIntersection )
{
// Grid: 10x10x5. BOX covers K=5 (1-based), i.e. K-index 4 (0-based).
// Sector: (2,2,1)-(7,7,4) does NOT include K=4 (0-based is K[1-4], and K=4 is included)
// Actually sector max K=4 means K[1-4] inclusive (0-based), so K=4 IS included.
// Use sector (2,2,0)-(7,7,2) to NOT include K=4.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString dataFilePath = tempDir.path() + "/test_add_box_nointersect.DATA";
{
QFile file( dataFilePath );
ASSERT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( "RUNSPEC\n\n" );
file.write( "DIMENS\n 10 10 5 /\n\n" );
file.write( "GRID\n\n" );
file.write( "PROPS\n\n" );
file.write( "BOX\n 1 10 1 10 5 5 /\n\n" );
file.write( "ADD\n PRESSURE 100.0 /\n/\n\n" );
file.write( "ENDBOX\n\n" );
file.close();
}
RifOpmFlowDeckFile deckFile;
auto loadResult = deckFile.loadDeck( dataFilePath.toStdString() );
ASSERT_TRUE( loadResult.has_value() ) << loadResult.error();
// Sector (2,2,0)-(7,7,2) does NOT include K=4 (0-based)
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 2, 2, 0 ) );
settings.setMax( caf::VecIjk0( 7, 7, 2 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
EXPECT_TRUE( RigSimulationInputTool::transformKeywordsInDeckFile( nullptr, settings, deckFile ).has_value() );
// ADD record with box K=5 (0-based K=4) outside sector K[0-2] should be removed
auto addAll = deckFile.findAllKeywordsWithIndices( "ADD" );
if ( !addAll.empty() )
{
EXPECT_EQ( 0u, addAll[0].second.size() );
}
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data verifying FIP keyword cropping
/// Ensures custom FIPABC keyword in REGIONS section is cropped to sector cell count
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5_FipKeywordCropping )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_fipkw.DATA";
// Set up export settings for a smaller sector to test cropping
// Sector: min=(5,5,2), max=(14,14,7) => 10x10x6 = 600 cells
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 7 ) );
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
const size_t expectedCellCount = 10 * 10 * 6; // 600 cells
// Verify FIPABC keyword is cropped to sector size with correct values
// Original model5 has FIPABC = 2000*1 2000*2 2000*3 over a 20x30x10 grid (600 cells/layer)
// Sector (5,5,2)-(14,14,7) extracts 10x10 cells from layers k=2..7
{
auto fipabcKw = deckFile.findKeyword( "FIPABC" );
ASSERT_TRUE( fipabcKw.has_value() ) << "FIPABC keyword missing from exported file";
const auto& fipabcData = fipabcKw->getIntData();
EXPECT_EQ( expectedCellCount, fipabcData.size() ) << "FIPABC should have " << expectedCellCount << " values";
if ( fipabcData.size() == expectedCellCount )
{
// Verify expected values for each layer in the sector:
// k_s=0 (orig k=2): all value 1 (flat indices in [1200,1800), all in first 2000)
// k_s=1 (orig k=3): j_s=0..4 (orig j=5..9) -> value 1, j_s=5..9 (orig j=10..14) -> value 2
// k_s=2..4 (orig k=4..6): all value 2
// k_s=5 (orig k=7): all value 3
const size_t NI = 10;
const size_t NJ = 10;
// k_s=0: all value 1
for ( size_t j = 0; j < NJ; ++j )
for ( size_t i = 0; i < NI; ++i )
EXPECT_EQ( 1, fipabcData[i + j * NI + 0 * NI * NJ] ) << "FIPABC at k_s=0, j=" << j << ", i=" << i << " should be 1";
// k_s=1: j_s=0..4 -> value 1, j_s=5..9 -> value 2
for ( size_t j = 0; j < 5; ++j )
for ( size_t i = 0; i < NI; ++i )
EXPECT_EQ( 1, fipabcData[i + j * NI + 1 * NI * NJ] ) << "FIPABC at k_s=1, j=" << j << ", i=" << i << " should be 1";
for ( size_t j = 5; j < NJ; ++j )
for ( size_t i = 0; i < NI; ++i )
EXPECT_EQ( 2, fipabcData[i + j * NI + 1 * NI * NJ] ) << "FIPABC at k_s=1, j=" << j << ", i=" << i << " should be 2";
// k_s=2..4: all value 2
for ( size_t k = 2; k <= 4; ++k )
for ( size_t j = 0; j < NJ; ++j )
for ( size_t i = 0; i < NI; ++i )
EXPECT_EQ( 2, fipabcData[i + j * NI + k * NI * NJ] )
<< "FIPABC at k_s=" << k << ", j=" << j << ", i=" << i << " should be 2";
// k_s=5: all value 3
for ( size_t j = 0; j < NJ; ++j )
for ( size_t i = 0; i < NI; ++i )
EXPECT_EQ( 3, fipabcData[i + j * NI + 5 * NI * NJ] ) << "FIPABC at k_s=5, j=" << j << ", i=" << i << " should be 3";
}
}
}
//--------------------------------------------------------------------------------------------------
/// Test exportSimulationInput with model5 data and padding for FIP keyword
/// Ensures custom FIPABC keyword is padded with minimum value in pad layers
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithPadding_FipKeyword )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
// Verify grid dimensions (20x30x10)
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
// Create temporary directory for export
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_padded_fipkw.DATA";
// Set up export settings for sector export with padding
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive) -> 20x15x10
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Configure padding: 2 upper, 3 lower
RigModelPaddingSettings paddingSettings;
paddingSettings.setEnabled( true );
paddingSettings.setNzUpper( 2 );
paddingSettings.setNzLower( 3 );
paddingSettings.setTopUpper( 1000.0 );
paddingSettings.setBottomLower( 3600.0 );
paddingSettings.setUpperPorosity( 0.1 );
paddingSettings.setMinLayerThickness( 10.0 );
paddingSettings.setVerticalPillars( true );
paddingSettings.setMonotonicZcorn( true );
paddingSettings.setFillGaps( true );
settings.setPaddingSettings( paddingSettings );
// Create visibility from IJK bounds
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
// Export simulation input
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
// Verify exported file exists
ASSERT_TRUE( QFile::exists( exportFilePath ) );
// Load the exported deck file using RifOpmFlowDeckFile
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Padded dimensions: 20x15x15 (original 20x15x10 + 2 upper + 3 lower)
const size_t expectedCellCount = 20 * 15 * 15; // 4500 cells
// Verify FIPABC keyword exists and has correct padded size
{
auto fipabcKw = deckFile.findKeyword( "FIPABC" );
ASSERT_TRUE( fipabcKw.has_value() ) << "FIPABC keyword missing from padded exported file";
const auto& fipabcData = fipabcKw->getIntData();
EXPECT_EQ( expectedCellCount, fipabcData.size() ) << "FIPABC should have " << expectedCellCount << " values in padded model";
if ( fipabcData.size() == expectedCellCount )
{
const size_t NI = 20;
const size_t NJ = 15;
const size_t nzUpper = 2;
const size_t nzLower = 3;
const size_t layerSize = NI * NJ; // 300
// Upper pad layers (k=0,1) should use minimum value from FIPABC data (value 1)
for ( size_t k = 0; k < nzUpper; ++k )
for ( size_t idx = 0; idx < layerSize; ++idx )
EXPECT_EQ( 1, fipabcData[k * layerSize + idx] )
<< "FIPABC upper pad at k=" << k << ", idx=" << idx << " should be 1 (minimum)";
// Lower pad layers (k=12,13,14) should use minimum value from FIPABC data (value 1)
for ( size_t k = nzUpper + 10; k < nzUpper + 10 + nzLower; ++k )
for ( size_t idx = 0; idx < layerSize; ++idx )
EXPECT_EQ( 1, fipabcData[k * layerSize + idx] )
<< "FIPABC lower pad at k=" << k << ", idx=" << idx << " should be 1 (minimum)";
}
}
}
//--------------------------------------------------------------------------------------------------
/// Regression test for issue #13854: BCCON handling in sector export with model padding.
///
/// BCCON DIRECTION names the exterior face of the recorded cell using the OPM-Flow
/// convention: "X"/"Y"/"Z" for the positive axis direction and "X-"/"Y-"/"Z-" for the
/// negative direction. When model padding is applied, RigPadModel::extendBCCon must
/// recognise the positive-Z exterior face ("Z") and shift its K index by
/// nzUpper + nzLower so the boundary tracks the bottom of the padded grid, while the
/// negative-Z exterior face ("Z-") must remain at K=1 (the new top).
///
/// Sector 10x10x6 with 2 upper + 3 lower padding layers -> padded NZ = 11. The "Z"
/// face originally at K=6 must be shifted to K=11; the "Z-" face must stay at K=1.
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ExportModel5WithBcconBcpropAndPadding )
{
// Load model5 test data
QDir baseFolder( TEST_DATA_DIR );
bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" );
ASSERT_TRUE( subFolderExists );
QString egridFilename( "0_BASE_MODEL5.EGRID" );
QString egridFilePath = baseFolder.absoluteFilePath( egridFilename );
ASSERT_TRUE( QFile::exists( egridFilePath ) );
QString dataFilename( "0_BASE_MODEL5.DATA" );
QString dataFilePath = baseFolder.absoluteFilePath( dataFilename );
ASSERT_TRUE( QFile::exists( dataFilePath ) );
// Create Eclipse case and load grid
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() );
ASSERT_TRUE( loadResult );
ASSERT_TRUE( caseData->mainGrid() != nullptr );
EXPECT_EQ( 20u, caseData->mainGrid()->cellCountI() );
EXPECT_EQ( 30u, caseData->mainGrid()->cellCountJ() );
EXPECT_EQ( 10u, caseData->mainGrid()->cellCountK() );
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString exportFilePath = tempDir.path() + "/exported_model_bccon_padded.DATA";
// Use a sub-sector so there is a proper boundary of border cells on every side
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 5, 5, 2 ) );
settings.setMax( caf::VecIjk0( 14, 14, 7 ) ); // Sector (0-based inclusive) -> 10x10x6
setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) );
settings.setBoundaryCondition( RiaModelExportDefines::BCCON_BCPROP );
settings.setInputDeckFileName( dataFilePath );
settings.setOutputDeckFileName( exportFilePath );
// Configure padding: 2 upper, 3 lower -> padded sector NZ = 6 + 2 + 3 = 11
RigModelPaddingSettings paddingSettings;
paddingSettings.setEnabled( true );
paddingSettings.setNzUpper( 2 );
paddingSettings.setNzLower( 3 );
paddingSettings.setTopUpper( 1000.0 );
paddingSettings.setBottomLower( 3600.0 );
paddingSettings.setUpperPorosity( 0.1 );
paddingSettings.setMinLayerThickness( 10.0 );
paddingSettings.setVerticalPillars( true );
paddingSettings.setMonotonicZcorn( true );
paddingSettings.setFillGaps( true );
settings.setPaddingSettings( paddingSettings );
cvf::ref<cvf::UByteArray> visibility =
RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() );
resultCase->setReservoirData( caseData.p() );
auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() );
ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString();
ASSERT_TRUE( QFile::exists( exportFilePath ) );
RifOpmFlowDeckFile deckFile;
bool deckLoadResult = deckFile.loadDeck( exportFilePath.toStdString() ).has_value();
ASSERT_TRUE( deckLoadResult ) << "Failed to load exported deck file";
// Expected padded dimensions: 10x10x11
const int expectedPaddedNz = 6 + 2 + 3;
auto specgridKw = deckFile.findKeyword( "SPECGRID" );
ASSERT_TRUE( specgridKw.has_value() );
EXPECT_EQ( 10, specgridKw->getRecord( 0 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_EQ( 10, specgridKw->getRecord( 0 ).getItem( 1 ).get<int>( 0 ) );
EXPECT_EQ( expectedPaddedNz, specgridKw->getRecord( 0 ).getItem( 2 ).get<int>( 0 ) );
// BCCON must exist
auto bcconKw = deckFile.findKeyword( "BCCON" );
ASSERT_TRUE( bcconKw.has_value() ) << "BCCON keyword missing from exported file";
EXPECT_GT( bcconKw->size(), 0u ) << "BCCON should contain at least one record";
// Collect the set of DIRECTION values present in BCCON and the K range for each.
// BCCON DIRECTION names the exterior face of the recorded cell. Using OPM-Flow
// convention, "Z" is the +Z exterior face (bottom of grid) and "Z-" is the -Z
// exterior face (top of grid). Padding extends the grid in Z, so the +Z exterior
// face moves from K=NZ to K=NZ+nzUpper+nzLower while the -Z exterior face remains
// at K=1.
std::set<std::string> directions;
int minKForPosZ = std::numeric_limits<int>::max();
int maxKForPosZ = 0;
int minKForNegZ = std::numeric_limits<int>::max();
int maxKForNegZ = 0;
const int originalSectorNz = 6;
const int shift = 2 + 3; // nzUpper + nzLower
for ( size_t r = 0; r < bcconKw->size(); ++r )
{
const auto& rec = bcconKw->getRecord( r );
const int k1 = rec.getItem( "K1" ).get<int>( 0 );
const int k2 = rec.getItem( "K2" ).get<int>( 0 );
std::string direction = rec.getItem( "DIRECTION" ).getTrimmedString( 0 );
directions.insert( direction );
if ( direction == "Z" )
{
minKForPosZ = std::min( minKForPosZ, std::min( k1, k2 ) );
maxKForPosZ = std::max( maxKForPosZ, std::max( k1, k2 ) );
}
else if ( direction == "Z-" )
{
minKForNegZ = std::min( minKForNegZ, std::min( k1, k2 ) );
maxKForNegZ = std::max( maxKForNegZ, std::max( k1, k2 ) );
}
}
// OPM-Flow BCCON convention: axis-positive faces are named "X", "Y", "Z"
// (without a trailing '+'), and axis-negative faces are named "X-", "Y-", "Z-".
EXPECT_TRUE( directions.count( "X" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"X\"";
EXPECT_TRUE( directions.count( "X-" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"X-\"";
EXPECT_TRUE( directions.count( "Y" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"Y\"";
EXPECT_TRUE( directions.count( "Y-" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"Y-\"";
EXPECT_TRUE( directions.count( "Z" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"Z\"";
EXPECT_TRUE( directions.count( "Z-" ) > 0 ) << "Expected at least one BCCON record with DIRECTION=\"Z-\"";
EXPECT_EQ( 0u, directions.count( "Z+" ) ) << "BCCON must use \"Z\" (not \"Z+\") for the positive Z direction";
// Before padding, the +Z exterior face is at K = originalSectorNz. After padding the
// extension must shift it to K = originalSectorNz + nzUpper + nzLower = padded NZ.
ASSERT_LT( 0, maxKForPosZ ) << "Expected at least one Z-direction face in BCCON";
EXPECT_EQ( originalSectorNz + shift, minKForPosZ ) << "\"Z\" exterior face K must be shifted by nzUpper+nzLower to the padded bottom";
EXPECT_EQ( originalSectorNz + shift, maxKForPosZ ) << "\"Z\" exterior face K must be shifted by nzUpper+nzLower to the padded bottom";
// The -Z exterior face remains at K=1 (the new top of the padded grid is the new upper pad layer at K=1).
ASSERT_LT( 0, maxKForNegZ ) << "Expected at least one Z-negative direction face in BCCON";
EXPECT_EQ( 1, minKForNegZ ) << "\"Z-\" exterior face must remain at K=1 (top of padded grid)";
EXPECT_EQ( 1, maxKForNegZ ) << "\"Z-\" exterior face must remain at K=1 (top of padded grid)";
}
//--------------------------------------------------------------------------------------------------
/// Helper: parse a keyword from deck text and return all its records
//--------------------------------------------------------------------------------------------------
static std::vector<Opm::DeckRecord> parseKeywordRecords( const std::string& deckText, const std::string& keywordName )
{
Opm::Parser parser;
auto deck = parser.parseString( deckText );
const auto& keyword = deck[keywordName].back();
std::vector<Opm::DeckRecord> records;
for ( size_t i = 0; i < keyword.size(); ++i )
{
records.push_back( keyword.getRecord( i ) );
}
return records;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
static RigSimulationInputSettings createSectorSettings( const cvf::Vec3st& refCounts )
{
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 9, 9, 9 ) );
setSettingsRefinement( settings, refCounts );
return settings;
}
//--------------------------------------------------------------------------------------------------
/// A Z-direction connection in a K-refined layer must be completed in every sub-cell, with explicit
/// CF and KH split between the sub-cells (regression: only every other refined layer was completed)
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_ZDirectionSplitAcrossKSubcells )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 100.0 2* 'Z' /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
for ( size_t s = 0; s < 2; ++s )
{
const auto& rec = result->at( s );
EXPECT_EQ( 3, rec.getItem( "I" ).get<int>( 0 ) );
EXPECT_EQ( 4, rec.getItem( "J" ).get<int>( 0 ) );
EXPECT_EQ( 3 + static_cast<int>( s ), rec.getItem( "K1" ).get<int>( 0 ) );
EXPECT_EQ( 3 + static_cast<int>( s ), rec.getItem( "K2" ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 5.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 50.0, rec.getItem( "Kh" ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 0.216, rec.getItem( "DIAMETER" ).get<double>( 0 ) );
}
}
//--------------------------------------------------------------------------------------------------
/// With defaulted CF and KH the simulator computes the factors, so the full refined K range is kept in one record
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_DefaultedFactorsKeepsSingleRecord )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 3 'OPEN' 2* 0.216 /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
EXPECT_EQ( 3, result->at( 0 ).getItem( "K1" ).get<int>( 0 ) );
EXPECT_EQ( 6, result->at( 0 ).getItem( "K2" ).get<int>( 0 ) );
EXPECT_FALSE( result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).hasValue( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// An X-direction connection is split across the I sub-cells; J and K use the center sub-cell
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_XDirectionSplitAcrossISubcells )
{
auto settings = createSectorSettings( cvf::Vec3st( 3, 2, 2 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 2 1 1 1 'OPEN' 1* 9.0 0.216 90.0 2* 'X' /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 3u, result->size() );
for ( size_t s = 0; s < 3; ++s )
{
const auto& rec = result->at( s );
EXPECT_EQ( 4 + static_cast<int>( s ), rec.getItem( "I" ).get<int>( 0 ) );
EXPECT_EQ( 2, rec.getItem( "J" ).get<int>( 0 ) );
EXPECT_EQ( 2, rec.getItem( "K1" ).get<int>( 0 ) );
EXPECT_EQ( 2, rec.getItem( "K2" ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 3.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 30.0, rec.getItem( "Kh" ).get<double>( 0 ) );
}
}
//--------------------------------------------------------------------------------------------------
/// Non-uniform K refinement splits the CF by the sub-cell thickness fractions
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_NonUniformKRefinementScalesByFraction )
{
RigSimulationInputSettings settings;
settings.setMin( caf::VecIjk0( 0, 0, 0 ) );
settings.setMax( caf::VecIjk0( 9, 9, 9 ) );
auto refinement = std::make_unique<RigNonUniformRefinement>( cvf::Vec3st( 10, 10, 10 ) );
refinement->setCumulativeFractions( RigRefinement::DimK, 1, { 0.25, 1.0 } );
settings.setRefinement( std::move( refinement ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 8.0 0.216 /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_EQ( 2, result->at( 0 ).getItem( "K1" ).get<int>( 0 ) );
EXPECT_EQ( 3, result->at( 1 ).getItem( "K1" ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 2.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 6.0, result->at( 1 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
EXPECT_TRUE( result->at( 0 ).getItem( "Kh" ).defaultApplied( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Without refinement the record is passed through unchanged
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_NoRefinementUnchanged )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 1 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 5 'OPEN' 1* 10.0 0.216 100.0 2* 'Z' /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
EXPECT_EQ( 2, result->at( 0 ).getItem( "K1" ).get<int>( 0 ) );
EXPECT_EQ( 5, result->at( 0 ).getItem( "K2" ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 10.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// COMPSEGS cells are split along the connection direction with the MD interval divided between sub-cells
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_SplitAcrossKSubcells )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 /\n/\n"
"COMPSEGS\n 'W' /\n 3 4 2 1 100.0 120.0 /\n/\n" );
std::vector<Opm::DeckKeyword> compdatKeywords = { deck["COMPDAT"].back() };
auto directions = RigSimulationInputTool::connectionDirections( compdatKeywords );
const auto& compsegs = deck["COMPSEGS"].back();
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, directions );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_EQ( 3, result->at( 0 ).getItem( 2 ).get<int>( 0 ) );
EXPECT_EQ( 4, result->at( 1 ).getItem( 2 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 100.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 1 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 120.0, result->at( 1 ).getItem( 5 ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Refinement perpendicular to a Z well reduces the Peaceman radius given by PR, so CF increases
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_LateralRefinementCorrectsCFUsingPR )
{
auto settings = createSectorSettings( cvf::Vec3st( 10, 10, 1 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 100.0 1.0 1* 'Z' 20.0 /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
const auto& rec = result->at( 0 );
EXPECT_EQ( 26, rec.getItem( "I" ).get<int>( 0 ) );
EXPECT_EQ( 36, rec.getItem( "J" ).get<int>( 0 ) );
const double rw = 0.108;
const double expected = 10.0 * ( std::log( 20.0 / rw ) + 1.0 ) / ( std::log( 2.0 / rw ) + 1.0 );
EXPECT_NEAR( expected, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ), 1e-9 );
EXPECT_NEAR( 2.0, rec.getItem( "PR" ).get<double>( 0 ), 1e-9 );
EXPECT_DOUBLE_EQ( 100.0, rec.getItem( "Kh" ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Without PR the Peaceman radii are computed from the cell sizes and permeabilities, also for
/// refinement in only one of the perpendicular directions
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_LateralRefinementCorrectsCFUsingCellProperties )
{
auto settings = createSectorSettings( cvf::Vec3st( 4, 1, 1 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 /\n/\n", "COMPDAT" );
RigSimulationInputTool::ConnectionCellProperties properties{ cvf::Vec3d( 100.0, 50.0, 5.0 ), cvf::Vec3d( 200.0, 50.0, 10.0 ) };
auto provider = [&]( const caf::VecIjk0& ) -> std::optional<RigSimulationInputTool::ConnectionCellProperties> { return properties; };
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings, provider );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
const double rw = 0.108;
const double r0Coarse = RigTransmissibilityEquations::peacemanRadius( 200.0, 50.0, 100.0, 50.0 );
const double r0Fine = RigTransmissibilityEquations::peacemanRadius( 200.0, 50.0, 25.0, 50.0 );
const double expected = 10.0 * std::log( r0Coarse / rw ) / std::log( r0Fine / rw );
EXPECT_NEAR( expected, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ), 1e-9 );
}
//--------------------------------------------------------------------------------------------------
/// The correction is skipped when the refined Peaceman radius gets smaller than the well radius
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompdatRecord_LateralRefinementTooSmallRadiusKeepsCF )
{
auto settings = createSectorSettings( cvf::Vec3st( 10, 10, 1 ) );
auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 4* 0.5 /\n/\n", "COMPDAT" );
auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
EXPECT_DOUBLE_EQ( 10.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 0.5, result->at( 0 ).getItem( "PR" ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// A well travelling towards decreasing I assigns the MD interval from the highest to the lowest I sub-cell
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_ReversedTraversalAlongI )
{
auto settings = createSectorSettings( cvf::Vec3st( 2, 1, 1 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPDAT\n 'W' 4 2 1 1 'OPEN' 1* 10.0 0.216 3* 'X' /\n"
" 'W' 3 2 1 1 'OPEN' 1* 10.0 0.216 3* 'X' /\n/\n"
"COMPSEGS\n 'W' /\n 4 2 1 1 100.0 120.0 /\n 3 2 1 1 120.0 140.0 /\n/\n" );
std::vector<Opm::DeckKeyword> compdatKeywords = { deck["COMPDAT"].back() };
std::vector<Opm::DeckKeyword> compsegsKeywords = { deck["COMPSEGS"].back() };
auto directions = RigSimulationInputTool::connectionDirections( compdatKeywords );
auto reversedCells = RigSimulationInputTool::reversedSegmentCells( compsegsKeywords, directions );
EXPECT_TRUE( reversedCells.contains( { "W", caf::VecIjk0( 3, 1, 0 ) } ) );
EXPECT_TRUE( reversedCells.contains( { "W", caf::VecIjk0( 2, 1, 0 ) } ) );
const auto& compsegs = deck["COMPSEGS"].back();
// Original I=4 -> refined I sub-cells 7 and 8, the well enters at 8
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, directions, reversedCells );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_EQ( 8, result->at( 0 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 100.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
EXPECT_EQ( 7, result->at( 1 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 1 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 120.0, result->at( 1 ).getItem( 5 ).get<double>( 0 ) );
// Original I=3 is the last cell, direction taken from the previous cell
result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 2 ), "W", false, settings, directions, reversedCells );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_EQ( 6, result->at( 0 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 120.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_EQ( 5, result->at( 1 ).getItem( 0 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 140.0, result->at( 1 ).getItem( 5 ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// A well travelling towards increasing I keeps the sub-cell order
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ReversedSegmentCells_ForwardTraversalNotReversed )
{
Opm::Parser parser;
auto deck = parser.parseString( "COMPDAT\n 'W' 3 2 1 1 'OPEN' 1* 10.0 0.216 3* 'X' /\n"
" 'W' 4 2 1 1 'OPEN' 1* 10.0 0.216 3* 'X' /\n/\n"
"COMPSEGS\n 'W' /\n 3 2 1 1 100.0 120.0 /\n 4 2 1 1 120.0 140.0 /\n/\n" );
std::vector<Opm::DeckKeyword> compdatKeywords = { deck["COMPDAT"].back() };
std::vector<Opm::DeckKeyword> compsegsKeywords = { deck["COMPSEGS"].back() };
auto directions = RigSimulationInputTool::connectionDirections( compdatKeywords );
auto reversedCells = RigSimulationInputTool::reversedSegmentCells( compsegsKeywords, directions );
EXPECT_TRUE( reversedCells.empty() );
}
//--------------------------------------------------------------------------------------------------
/// DIRECTION without END_IJK is a single cell, so the MD interval is still split between the sub-cells
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_DirectionWithoutRangeIsSplit )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPSEGS\n 'W' /\n 3 4 2 1 100.0 120.0 'K' /\n/\n" );
const auto& compsegs = deck["COMPSEGS"].back();
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, {} );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_EQ( 3, result->at( 0 ).getItem( 2 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
EXPECT_EQ( 4, result->at( 1 ).getItem( 2 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 110.0, result->at( 1 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_EQ( "K", result->at( 1 ).getItem( 6 ).get<std::string>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// A defaulted start MD is zero
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_DefaultedStartMdIsZero )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPSEGS\n 'W' /\n 3 4 2 1 1* 20.0 /\n/\n" );
const auto& compsegs = deck["COMPSEGS"].back();
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, {} );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
EXPECT_DOUBLE_EQ( 0.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 10.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 10.0, result->at( 1 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 20.0, result->at( 1 ).getItem( 5 ).get<double>( 0 ) );
}
//--------------------------------------------------------------------------------------------------
/// Without an end MD every sub-cell still gets a segment record, with the MD items unchanged
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_NoEndMdCopiedToAllSubcells )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPSEGS\n 'W' /\n 3 4 2 1 100.0 1* 'K' /\n/\n" );
const auto& compsegs = deck["COMPSEGS"].back();
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, {} );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 2u, result->size() );
for ( size_t s = 0; s < 2; ++s )
{
EXPECT_EQ( 3 + static_cast<int>( s ), result->at( s ).getItem( 2 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 100.0, result->at( s ).getItem( 4 ).get<double>( 0 ) );
EXPECT_FALSE( result->at( s ).getItem( 5 ).hasValue( 0 ) );
}
}
//--------------------------------------------------------------------------------------------------
/// A cell range (END_IJK) is not split, only the center sub-cell is used
//--------------------------------------------------------------------------------------------------
TEST( RigSimulationInputTool, ProcessCompsegsRecord_CellRangeUsesCenterSubcell )
{
auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) );
Opm::Parser parser;
auto deck = parser.parseString( "COMPSEGS\n 'W' /\n 3 4 2 1 100.0 120.0 'K' 3 /\n/\n" );
const auto& compsegs = deck["COMPSEGS"].back();
auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, {} );
ASSERT_TRUE( result.has_value() ) << result.error().toStdString();
ASSERT_EQ( 1u, result->size() );
EXPECT_EQ( 4, result->at( 0 ).getItem( 2 ).get<int>( 0 ) );
EXPECT_DOUBLE_EQ( 100.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
EXPECT_DOUBLE_EQ( 120.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
}