Files
ResInsight/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp
T
Kristian Bendiksen 65f16d04ef #14622 Sector export: Preserve separate include files for ACTNUM and FAULTS
Sector model export removed all ACTNUM and FAULTS keywords from the deck
and re-inserted the sector-specific replacements at the end of the GRID
section, which always lands in the main DATA file. Decks that keep these
keywords in separate include files lost their file organization, leaving
the original include files empty while the data was written inline.

Collapse duplicates to a single occurrence instead of removing all
occurrences, so the subsequent in-place replacement keeps the keyword in
its original file when the deck is saved with OutputMode::COPY. ACTNUM
keeps the last occurrence since a later ACTNUM overrides an earlier one;
FAULTS keeps the first so fault definitions do not move past keywords
referencing them.
2026-08-31 20:14:38 +02:00

3060 lines
146 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 "RigSimulationInputSettings.h"
#include "RigSimulationInputTool.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 <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 specific COMPDAT coordinates are centered correctly
// For refinement (1, 3, 5):
// - K dimension: refinement=5, center offset = (5+1)/2 = 3
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)*5 + 3 = 3, K2: (5-0)*5 + 3 = 23
EXPECT_TRUE( checkCompdatCoordinates( "B-1H", 3, 23 ) ) << "B-1H COMPDAT K coordinates not centered correctly";
// B-2H: original K1=1, K2=6
// K1: (1-0)*5 + 3 = 3, K2: (6-0)*5 + 3 = 28
EXPECT_TRUE( checkCompdatCoordinates( "B-2H", 3, 28 ) ) << "B-2H COMPDAT K coordinates not centered correctly";
}
//--------------------------------------------------------------------------------------------------
/// 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)";
}