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