diff --git a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp index addf652733..bbbc07706f 100644 --- a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp +++ b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp @@ -20,6 +20,8 @@ #include "RifOpmDeckTools.h" +#include "RigEclipseResultTools.h" + #include "opm/common/utility/TimeService.hpp" #include "opm/input/eclipse/Deck/Deck.hpp" #include "opm/input/eclipse/Deck/FileDeck.hpp" @@ -27,6 +29,7 @@ #include "opm/input/eclipse/Parser/InputErrorAction.hpp" #include "opm/input/eclipse/Parser/ParseContext.hpp" #include "opm/input/eclipse/Parser/Parser.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/B.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/C.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/D.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/E.hpp" @@ -37,6 +40,8 @@ #include "opm/input/eclipse/Parser/ParserKeywords/S.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/W.hpp" +#include "cvfStructGrid.h" + #include #include #include @@ -862,6 +867,77 @@ bool RifOpmFlowDeckFile::addOperaterKeyword( std::string section, return true; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RifOpmFlowDeckFile::addBcconKeyword( std::string section, const std::vector& borderCellFaces ) +{ + if ( m_fileDeck.get() == nullptr ) return false; + + if ( borderCellFaces.empty() ) return true; // Nothing to add + + // Find insertion point within the section + auto insertPos = internal::findSectionInsertionPoint( m_fileDeck, section ); + if ( !insertPos.has_value() ) + { + return false; // Section not found + } + + // Helper lambda to convert FaceType to string + auto faceTypeToString = []( cvf::StructGridInterface::FaceType faceType ) -> std::string + { + switch ( faceType ) + { + case cvf::StructGridInterface::POS_I: + return "X"; + case cvf::StructGridInterface::NEG_I: + return "X-"; + case cvf::StructGridInterface::POS_J: + return "Y"; + case cvf::StructGridInterface::NEG_J: + return "Y-"; + case cvf::StructGridInterface::POS_K: + return "Z"; + case cvf::StructGridInterface::NEG_K: + return "Z-"; + default: + return ""; + } + }; + + // Create the BCCON keyword using OPM's BCCON parser keyword + using B = Opm::ParserKeywords::BCCON; + + Opm::DeckKeyword bcconKw( ( Opm::ParserKeywords::BCCON() ) ); + + for ( const auto& borderFace : borderCellFaces ) + { + // Convert from 0-based to 1-based Eclipse indexing + int i1 = static_cast( borderFace.ijk[0] ) + 1; + int j1 = static_cast( borderFace.ijk[1] ) + 1; + int k1 = static_cast( borderFace.ijk[2] ) + 1; + + std::string faceStr = faceTypeToString( borderFace.faceType ); + + // Create items for the record using proper BCCON item names + std::vector recordItems; + + recordItems.push_back( item( B::INDEX::itemName, borderFace.boundaryCondition ) ); + recordItems.push_back( item( B::I1::itemName, i1 ) ); + recordItems.push_back( item( B::I2::itemName, i1 ) ); // Same as i1 for single cell + recordItems.push_back( item( B::J1::itemName, j1 ) ); + recordItems.push_back( item( B::J2::itemName, j1 ) ); // Same as j1 for single cell + recordItems.push_back( item( B::K1::itemName, k1 ) ); + recordItems.push_back( item( B::K2::itemName, k1 ) ); // Same as k1 for single cell + recordItems.push_back( item( B::DIRECTION::itemName, faceStr ) ); + + bcconKw.addRecord( Opm::DeckRecord{ std::move( recordItems ) } ); + } + + m_fileDeck->insert( insertPos.value(), bcconKw ); + return true; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h index 29630a5f49..6ec04001dd 100644 --- a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h +++ b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h @@ -32,6 +32,11 @@ class DeckKeyword; class ParseContext; } // namespace Opm +namespace RigEclipseResultTools +{ +struct BorderCellFace; +} + //================================================================================================== /// /// @@ -81,6 +86,8 @@ public: std::optional alpha, std::optional beta ); + bool addBcconKeyword( std::string section, const std::vector& borderCellFaces ); + private: void splitDatesIfNecessary(); diff --git a/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.cpp b/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.cpp index 2656f6f0a3..8927b5d764 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.cpp @@ -233,4 +233,85 @@ int findMaxOperNumValue( RimEclipseCase* eclipseCase ) return maxValue; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector generateBorderCellFaces( RimEclipseCase* eclipseCase ) +{ + if ( eclipseCase == nullptr ) return {}; + + auto resultsData = eclipseCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL ); + if ( !resultsData ) return {}; + + auto grid = eclipseCase->eclipseCaseData()->mainGrid(); + if ( !grid ) return {}; + + // Check if BORDNUM result exists + RigEclipseResultAddress bordNumAddr( RiaDefines::ResultCatType::GENERATED, RiaDefines::ResultDataType::INTEGER, RiaResultNames::bordnum() ); + if ( !resultsData->hasResultEntry( bordNumAddr ) ) return {}; // BORDNUM not generated yet + + resultsData->ensureKnownResultLoaded( bordNumAddr ); + auto bordNumValues = resultsData->cellScalarResults( bordNumAddr, 0 ); + if ( bordNumValues.empty() ) return {}; + + // Check if BCCON result exists to get boundary condition values + RigEclipseResultAddress bcconAddr( RiaDefines::ResultCatType::GENERATED, RiaDefines::ResultDataType::INTEGER, "BCCON" ); + if ( !resultsData->hasResultEntry( bcconAddr ) ) return {}; + + resultsData->ensureKnownResultLoaded( bcconAddr ); + std::vector bcconValues = resultsData->cellScalarResults( bcconAddr, 0 ); + + auto activeReservoirCellIdxs = + eclipseCase->eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL )->activeReservoirCellIndices(); + + std::vector borderCellFaces; + + // Iterate through all active cells + for ( size_t activeCellIdx : activeReservoirCellIdxs ) + { + // Check if this cell is a border cell + int borderValue = static_cast( bordNumValues[activeCellIdx] ); + if ( borderValue != BorderType::BORDER_CELL ) continue; + + // Get IJK indices for this cell + size_t i, j, k; + if ( !grid->ijkFromCellIndex( activeCellIdx, &i, &j, &k ) ) continue; + + // Check all 6 faces + std::vector faces = cvf::StructGridInterface::validFaceTypes(); + + for ( auto faceType : faces ) + { + // Get neighbor cell IJK + size_t ni, nj, nk; + cvf::StructGridInterface::neighborIJKAtCellFace( i, j, k, faceType, &ni, &nj, &nk ); + + // Check if neighbor is within bounds + if ( ni >= grid->cellCountI() || nj >= grid->cellCountJ() || nk >= grid->cellCountK() ) continue; + + // Get neighbor reservoir cell index + size_t neighborReservoirIdx = grid->cellIndexFromIJK( ni, nj, nk ); + + // Find active cell index for neighbor + auto it = std::find( activeReservoirCellIdxs.begin(), activeReservoirCellIdxs.end(), neighborReservoirIdx ); + if ( it == activeReservoirCellIdxs.end() ) continue; // Neighbor not active + + // Check if neighbor is an interior cell + int neighborBorderValue = static_cast( bordNumValues[neighborReservoirIdx] ); + if ( neighborBorderValue == BorderType::INTERIOR_CELL ) + { + // Get boundary condition value from BCCON grid property + int boundaryCondition = static_cast( bcconValues[activeCellIdx] ); + if ( boundaryCondition > 0 ) + { + // Add this face to the result + borderCellFaces.push_back( { cvf::Vec3st( i, j, k ), faceType, boundaryCondition } ); + } + } + } + } + + return borderCellFaces; +} + } // namespace RigEclipseResultTools diff --git a/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.h b/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.h index c87f621a9f..ae2b549fee 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.h +++ b/ApplicationLibCode/ReservoirDataModel/RigEclipseResultTools.h @@ -22,6 +22,7 @@ #include #include "cvfArray.h" +#include "cvfStructGrid.h" class RimEclipseCase; class RimEclipseView; @@ -35,6 +36,13 @@ enum BorderType : int INTERIOR_CELL = 2 }; +struct BorderCellFace +{ + cvf::Vec3st ijk; // Cell indices (0-based) + cvf::StructGridInterface::FaceType faceType; + int boundaryCondition; // BCCON grid value +}; + void createResultVector( RimEclipseCase& eclipseCase, const QString& resultName, const std::vector& intValues ); void generateBorderResult( RimEclipseCase* eclipseCase, cvf::ref customVisibility, const QString& resultName = "BORDER" ); @@ -43,4 +51,6 @@ void generateOperNumResult( RimEclipseCase* eclipseCase, int borderCellValue = - int findMaxOperNumValue( RimEclipseCase* eclipseCase ); +std::vector generateBorderCellFaces( RimEclipseCase* eclipseCase ); + } // namespace RigEclipseResultTools diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 5f48e491b1..a66cc3eda0 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -15,6 +15,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RifReaderEclipseSummary-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigActiveCellInfo-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigEclipseCaseDataTools-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigReservoir-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigResdataGridConverter-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigGridExportAdapter-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RigEclipseResultTools-Test.cpp b/ApplicationLibCode/UnitTests/RigEclipseResultTools-Test.cpp new file mode 100644 index 0000000000..f37a0fa50e --- /dev/null +++ b/ApplicationLibCode/UnitTests/RigEclipseResultTools-Test.cpp @@ -0,0 +1,203 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "RiaDefines.h" +#include "RiaTestDataDirectory.h" +#include "RifEclipseInputFileTools.h" +#include "RifOpmFlowDeckFile.h" +#include "RifReaderEclipseOutput.h" +#include "RigEclipseResultTools.h" + +#include "RigActiveCellInfo.h" +#include "RigCaseCellResultsData.h" +#include "RigEclipseCaseData.h" +#include "RigEclipseResultAddress.h" +#include "RigMainGrid.h" +#include "RimEclipseResultCase.h" + +#include +#include +#include +#include + +#include + +//-------------------------------------------------------------------------------------------------- +/// Test BCCON generation from border cells +/// +/// This test verifies that: +/// 1. We can generate BORDNUM result identifying border and interior cells +/// 2. We can extract border cell faces that connect to interior cells +/// 3. We can write BCCON keyword to an OPM deck file +/// +/// Test process: +/// 1. Load reek test model +/// 2. Create custom visibility for a subset of cells (a box region) +/// 3. Generate BORDNUM result +/// 4. Generate border cell faces +/// 5. Verify border cells and faces are correctly identified +/// 6. Create OPM deck and add BCCON keyword +/// 7. Save and verify deck file format +//-------------------------------------------------------------------------------------------------- +TEST( RigEclipseResultToolsTest, BorderCellBcconGeneration ) +{ + // Setup test data directory - use BRUGGE model like other tests + QDir baseFolder( TEST_MODEL_DIR ); + bool subFolderExists = baseFolder.cd( "Case_with_10_timesteps/Real0" ); + ASSERT_TRUE( subFolderExists ) << "Test model directory not found"; + + QString inputFilename( "BRUGGE_0000.EGRID" ); + QString inputFilePath = baseFolder.absoluteFilePath( inputFilename ); + ASSERT_TRUE( QFile::exists( inputFilePath ) ) << "Test model file not found: " << inputFilePath.toStdString(); + + // Step 1: Load original grid using RifReaderEclipseOutput (properly initializes everything) + std::unique_ptr testCase( new RimEclipseResultCase ); + cvf::ref caseData = new RigEclipseCaseData( testCase.get() ); + + cvf::ref readerInterfaceEcl = new RifReaderEclipseOutput; + bool success = readerInterfaceEcl->open( inputFilePath, caseData.p() ); + ASSERT_TRUE( success ) << "Failed to load grid"; + + testCase->setReservoirData( caseData.p() ); + + const RigMainGrid* grid = caseData->mainGrid(); + ASSERT_NE( grid, nullptr ) << "Grid is null"; + + // Step 2: Create custom visibility for a subset of cells (e.g., middle 50% of grid) + size_t cellCount = grid->cellCount(); + + cvf::ref customVisibility = new cvf::UByteArray( cellCount ); + customVisibility->setAll( 0 ); // Start with all invisible + + std::vector bcconValues( cellCount, 0 ); + + // Make a box in the middle visible + size_t startI = grid->cellCountI() / 4; + size_t endI = 3 * grid->cellCountI() / 4; + size_t startJ = grid->cellCountJ() / 4; + size_t endJ = 3 * grid->cellCountJ() / 4; + size_t startK = grid->cellCountK() / 4; + size_t endK = 3 * grid->cellCountK() / 4; + + for ( size_t i = startI; i < endI; ++i ) + { + for ( size_t j = startJ; j < endJ; ++j ) + { + for ( size_t k = startK; k < endK; ++k ) + { + size_t cellIndex = grid->cellIndexFromIJK( i, j, k ); + ( *customVisibility )[cellIndex] = 1; + + // Set bccon==1 for half of the cells + if ( i > grid->cellCountI() / 2 ) + { + bcconValues[cellIndex] = 1; + } + } + } + } + + // Step 3: Generate BORDNUM result + RigEclipseResultTools::generateBorderResult( testCase.get(), customVisibility, "BORDNUM" ); + + // Verify BORDNUM was created + auto resultsData = testCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL ); + ASSERT_NE( resultsData, nullptr ); + + RigEclipseResultAddress bordNumAddr( RiaDefines::ResultCatType::GENERATED, RiaDefines::ResultDataType::INTEGER, "BORDNUM" ); + ASSERT_TRUE( resultsData->hasResultEntry( bordNumAddr ) ) << "BORDNUM result not created"; + + RigEclipseResultTools::createResultVector( *testCase, "BCCON", bcconValues ); + + // Step 4: Generate border cell faces + auto borderCellFaces = RigEclipseResultTools::generateBorderCellFaces( testCase.get() ); + + // Step 5: Verify results + EXPECT_GT( borderCellFaces.size(), 0 ) << "No border cell faces generated"; + + // Verify that all faces are within the visible region boundaries + for ( const auto& face : borderCellFaces ) + { + EXPECT_GE( face.ijk[0], startI ); + EXPECT_LT( face.ijk[0], endI ); + EXPECT_GE( face.ijk[1], startJ ); + EXPECT_LT( face.ijk[1], endJ ); + EXPECT_GE( face.ijk[2], startK ); + EXPECT_LT( face.ijk[2], endK ); + + // Verify face type is valid + EXPECT_GE( static_cast( face.faceType ), static_cast( cvf::StructGridInterface::POS_I ) ); + EXPECT_LT( static_cast( face.faceType ), static_cast( cvf::StructGridInterface::NO_FACE ) ); + + // Verify boundary condition (should be 1 if no BCCON grid property exists) + EXPECT_EQ( face.boundaryCondition, 1 ); + } + + // Step 6: Create OPM deck file and add BCCON keyword + QTemporaryDir tempDir; + ASSERT_TRUE( tempDir.isValid() ) << "Failed to create temporary directory"; + + // Create a minimal deck file with GRID section + QString deckFilePath = tempDir.filePath( "test_bccon.DATA" ); + { + QFile deckFile( deckFilePath ); + ASSERT_TRUE( deckFile.open( QIODevice::WriteOnly | QIODevice::Text ) ); + QTextStream out( &deckFile ); + out << "RUNSPEC\n\n"; + out << "DIMENS\n"; + out << grid->cellCountI() << " " << grid->cellCountJ() << " " << grid->cellCountK() << " /\n\n"; + out << "GRID\n\n"; + out << "-- BCCON will be added here\n\n"; + out << "PROPS\n\n"; + out << "SCHEDULE\n\n"; + deckFile.close(); + } + + // Load the deck file + RifOpmFlowDeckFile deckFile; + bool deckLoaded = deckFile.loadDeck( deckFilePath.toStdString() ); + ASSERT_TRUE( deckLoaded ) << "Failed to load deck file"; + + // Add BCCON keyword + bool bcconAdded = deckFile.addBcconKeyword( "GRID", borderCellFaces ); + ASSERT_TRUE( bcconAdded ) << "Failed to add BCCON keyword"; + + // Step 7: Save deck and verify format + QString outputDeckPath = tempDir.filePath( "output_bccon.DATA" ); + bool deckSaved = deckFile.saveDeck( tempDir.path().toStdString(), "output_bccon.DATA" ); + ASSERT_TRUE( deckSaved ) << "Failed to save deck file"; + ASSERT_TRUE( QFile::exists( outputDeckPath ) ) << "Output deck file not created"; + + // Read and verify BCCON content + QFile outputFile( outputDeckPath ); + ASSERT_TRUE( outputFile.open( QIODevice::ReadOnly | QIODevice::Text ) ); + QString content = QTextStream( &outputFile ).readAll(); + outputFile.close(); + + // Verify BCCON keyword is present + EXPECT_TRUE( content.contains( "BCCON" ) ) << "BCCON keyword not found in output"; + + // Verify at least one entry exists + // BCCON format: counter i1 i2 j1 j2 k1 k2 face + // Should have face directions like X, X-, Y, Y-, Z, Z- + bool hasFaceDirection = content.contains( "X-" ) || content.contains( "Y-" ) || content.contains( "Z-" ) || content.contains( " X " ) || + content.contains( " Y " ) || content.contains( " Z " ); + EXPECT_TRUE( hasFaceDirection ) << "BCCON entries don't contain face directions"; +}