Support generating border result based on current visibility

This commit is contained in:
Jon Jenssen
2025-09-19 15:22:17 +02:00
committed by Magne Sjaastad
parent 3ff9ac3336
commit 4c0d4b3d1d
9 changed files with 452 additions and 1 deletions
@@ -135,6 +135,8 @@ public:
void updateResultAddressCollection();
void setReservoirData( RigEclipseCaseData* eclipseCase );
protected:
void initAfterRead() override;
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
@@ -145,7 +147,6 @@ protected:
// Internal methods
protected:
void computeCachedData();
void setReservoirData( RigEclipseCaseData* eclipseCase );
std::vector<QString> additionalFiles() const;
RimEclipseViewCollection* globalViewCollection() const;
void addViewsFromViewCollection( std::vector<RimEclipseView*>& views, const RimEclipseViewCollection* viewColl ) const;
@@ -23,6 +23,7 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RigEclipseNativeVisibleCellsStatCalc.h
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultAddress.h
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultInfo.h
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultTools.h
${CMAKE_CURRENT_LIST_DIR}/RigElasticProperties.h
${CMAKE_CURRENT_LIST_DIR}/RigEnsembleFractureStatisticsCalculator.h
${CMAKE_CURRENT_LIST_DIR}/RigEnsembleParameter.h
@@ -109,6 +110,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RigEclipseNativeVisibleCellsStatCalc.cpp
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultAddress.cpp
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultInfo.cpp
${CMAKE_CURRENT_LIST_DIR}/RigEclipseResultTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RigElasticProperties.cpp
${CMAKE_CURRENT_LIST_DIR}/RigEnsembleFractureStatisticsCalculator.cpp
${CMAKE_CURRENT_LIST_DIR}/RigEnsembleParameter.cpp
@@ -25,6 +25,7 @@
#include "Well/RigSimulationWellCenterLineCalculator.h"
#include "cvfBoundingBox.h"
#include "cvfObject.h"
//--------------------------------------------------------------------------------------------------
///
@@ -199,3 +200,39 @@ std::pair<cvf::Vec3st, cvf::Vec3st> RigEclipseCaseDataTools::expandBoundingBoxIj
return { cvf::Vec3st( expandedMinI, expandedMinJ, expandedMinK ), cvf::Vec3st( expandedMaxI, expandedMaxJ, expandedMaxK ) };
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::UByteArray> RigEclipseCaseDataTools::createVisibilityFromIjkBounds( RigEclipseCaseData* eclipseCaseData,
const cvf::Vec3st& minIjk,
const cvf::Vec3st& maxIjk )
{
if ( !eclipseCaseData || minIjk.isUndefined() || maxIjk.isUndefined() ) return nullptr;
auto mainGrid = eclipseCaseData->mainGrid();
if ( !mainGrid ) return nullptr;
// Create visibility array sized for the total number of reservoir cells
size_t totalCellCount = mainGrid->cellCount();
cvf::ref<cvf::UByteArray> visibility = new cvf::UByteArray( totalCellCount );
visibility->setAll( false ); // Initialize all cells as invisible
// Mark cells within the IJK bounds as visible
for ( size_t i = minIjk.x(); i <= maxIjk.x() && i < mainGrid->cellCountI(); ++i )
{
for ( size_t j = minIjk.y(); j <= maxIjk.y() && j < mainGrid->cellCountJ(); ++j )
{
for ( size_t k = minIjk.z(); k <= maxIjk.z() && k < mainGrid->cellCountK(); ++k )
{
size_t cellIndex = mainGrid->cellIndexFromIJK( i, j, k );
if ( cellIndex < totalCellCount )
{
visibility->set( cellIndex, true );
}
}
}
}
return visibility;
}
@@ -21,6 +21,7 @@
#include "QString"
#include "cvfBoundingBox.h"
#include "cvfObject.h"
#include "cvfVector3.h"
#include <utility>
@@ -56,4 +57,7 @@ public:
static std::pair<cvf::Vec3st, cvf::Vec3st>
expandBoundingBoxIjk( RigEclipseCaseData* eclipseCaseData, const cvf::Vec3st& minIjk, const cvf::Vec3st& maxIjk, size_t numPadding );
static cvf::ref<cvf::UByteArray>
createVisibilityFromIjkBounds( RigEclipseCaseData* eclipseCaseData, const cvf::Vec3st& minIjk, const cvf::Vec3st& maxIjk );
};
@@ -0,0 +1,105 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RigEclipseResultTools.h"
#include "RiaDefines.h"
#include "RiaPorosityModel.h"
#include "RigActiveCellInfo.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h"
#include "RimEclipseCase.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseView.h"
namespace RigEclipseResultTools
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void createResultVector( RimEclipseCase& eclipseCase, const QString& resultName, const std::vector<int>& intValues )
{
RigEclipseResultAddress resultAddress( RiaDefines::ResultCatType::GENERATED, RiaDefines::ResultDataType::INTEGER, resultName );
auto resultsData = eclipseCase.results( RiaDefines::PorosityModelType::MATRIX_MODEL );
resultsData->addStaticScalarResult( RiaDefines::ResultCatType::GENERATED, resultName, false, intValues.size() );
std::vector<double>* resultVector = resultsData->modifiableCellScalarResult( resultAddress, 0 );
resultVector->resize( intValues.size() );
for ( size_t idx = 0; idx < intValues.size(); idx++ )
{
resultVector->at( idx ) = 1.0 * intValues[idx];
}
resultsData->recalculateStatistics( resultAddress );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void generateBorderResult( RimEclipseCase* eclipseCase, cvf::ref<cvf::UByteArray> customVisibility, const QString& resultName )
{
if ( eclipseCase == nullptr || customVisibility.isNull() ) return;
auto activeReservoirCellIdxs =
eclipseCase->eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL )->activeReservoirCellIndices();
int numActiveCells = static_cast<int>( activeReservoirCellIdxs.size() );
std::vector<int> result;
result.resize( numActiveCells, BorderType::INVISIBLE_CELL );
auto grid = eclipseCase->eclipseCaseData()->mainGrid();
// go through all cells, only check those visible
#pragma omp parallel for
for ( int i = 0; i < numActiveCells; i++ )
{
auto cellIdx = activeReservoirCellIdxs[i];
if ( customVisibility->val( cellIdx ) )
{
auto neighbors = grid->neighborCells( cellIdx, true /*ignore invalid k layers*/ );
int nVisibleNeighbors = 0;
for ( auto nIdx : neighbors )
{
if ( customVisibility->val( nIdx ) ) nVisibleNeighbors++;
}
if ( nVisibleNeighbors == 6 )
{
result[i] = BorderType::INTERIOR_CELL;
}
else
{
result[i] = BorderType::BORDER_CELL;
}
}
}
RigEclipseResultTools::createResultVector( *eclipseCase, resultName, result );
eclipseCase->updateConnectedEditors();
}
} // namespace RigEclipseResultTools
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <QString>
#include <vector>
#include "cvfArray.h"
class RimEclipseCase;
class RimEclipseView;
namespace RigEclipseResultTools
{
enum BorderType : int
{
INVISIBLE_CELL = 0,
BORDER_CELL = 1,
INTERIOR_CELL = 2
};
void createResultVector( RimEclipseCase& eclipseCase, const QString& resultName, const std::vector<int>& intValues );
void generateBorderResult( RimEclipseCase* eclipseCase, cvf::ref<cvf::UByteArray> customVisibility, const QString& resultName = "BORDER" );
} // namespace RigEclipseResultTools
@@ -504,6 +504,58 @@ cvf::BoundingBox RigGridBase::boundingBox()
return m_boundingBox;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<size_t> RigGridBase::neighborCells( size_t cellIndex, bool ignoreInvalidKLayers ) const
{
auto ijk = ijkFromCellIndex( cellIndex );
if ( !ijk.has_value() ) return {};
auto& ijkVec = ijk.value();
std::vector<size_t> neighbors;
for ( auto face : { FaceType::NEG_I, FaceType::NEG_J, FaceType::NEG_K, FaceType::POS_I, FaceType::POS_J, FaceType::POS_K } )
{
size_t ni, nj, nk;
neighborIJKAtCellFace( ijkVec.i(), ijkVec.j(), ijkVec.k(), face, &ni, &nj, &nk );
if ( nk > cellCountK() ) continue;
if ( isCellValid( ni, nj, nk ) )
{
auto neighborIdx = cellIndexFromIJKUnguarded( ni, nj, nk );
neighbors.push_back( neighborIdx );
}
else if ( face == FaceType::NEG_K )
{
while ( ignoreInvalidKLayers && ( nk > 1 ) )
{
nk--;
if ( isCellValid( ni, nj, nk ) )
{
auto neighborIdx = cellIndexFromIJKUnguarded( ni, nj, nk );
neighbors.push_back( neighborIdx );
break;
}
}
}
else if ( face == FaceType::POS_K )
{
while ( ignoreInvalidKLayers && ( nk < cellCountK() ) )
{
nk++;
if ( isCellValid( ni, nj, nk ) )
{
auto neighborIdx = cellIndexFromIJKUnguarded( ni, nj, nk );
neighbors.push_back( neighborIdx );
break;
}
}
}
}
return neighbors;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -114,6 +114,8 @@ public:
bool cellIJKNeighbor( size_t i, size_t j, size_t k, FaceType face, size_t* neighborCellIndex ) const override;
void cellIJKNeighborUnguarded( size_t i, size_t j, size_t k, FaceType face, size_t* neighborCellIndex ) const;
std::vector<size_t> neighborCells( size_t cellIndex, bool ignoreInvalidKLayers = false ) const;
protected:
size_t m_indexToStartOfCells; ///< Index into the global cell array stored in main-grid where this grids cells starts.
size_t m_cellCountIJK;
@@ -25,8 +25,11 @@
#include "RimEclipseResultCase.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseCaseDataTools.h"
#include "RigEclipseResultAddress.h"
#include "RigEclipseResultTools.h"
#include "RigMainGrid.h"
#include "Well/RigSimWellData.h"
@@ -290,3 +293,206 @@ TEST( RigEclipseCaseDataToolsTest, ExpandBoundingBoxIjkWithPadding )
ASSERT_LE( expandedMin3.y(), expandedMax3.y() ) << "Upper boundary expanded Min J should be <= Max J";
ASSERT_LE( expandedMin3.z(), expandedMax3.z() ) << "Upper boundary expanded Min K should be <= Max K";
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RigEclipseCaseDataToolsTest, CreateVisibilityFromIjkBounds )
{
// Load the BRUGGE test model using RifReaderEclipseOutput
QDir baseFolder( TEST_MODEL_DIR );
bool subFolderExists = baseFolder.cd( "Case_with_10_timesteps/Real0" );
ASSERT_TRUE( subFolderExists ) << "Could not find test model directory";
QString filename( "BRUGGE_0000.EGRID" );
QString filePath = baseFolder.absoluteFilePath( filename );
ASSERT_TRUE( QFile::exists( filePath ) ) << "BRUGGE test model file does not exist: " << filePath.toStdString();
std::unique_ptr<RimEclipseResultCase> resultCase( new RimEclipseResultCase );
cvf::ref<RigEclipseCaseData> eclipseCase = new RigEclipseCaseData( resultCase.get() );
cvf::ref<RifReaderEclipseOutput> readerInterfaceEcl = new RifReaderEclipseOutput;
bool success = readerInterfaceEcl->open( filePath, eclipseCase.p() );
ASSERT_TRUE( success ) << "Could not load BRUGGE test model";
ASSERT_NE( eclipseCase->mainGrid(), nullptr ) << "Main grid should not be null";
ASSERT_GT( eclipseCase->mainGrid()->cellCount(), 0 ) << "Grid should contain cells";
// Get grid dimensions for test validation
size_t gridCellCountI = eclipseCase->mainGrid()->cellCountI();
size_t gridCellCountJ = eclipseCase->mainGrid()->cellCountJ();
size_t gridCellCountK = eclipseCase->mainGrid()->cellCountK();
size_t totalCellCount = eclipseCase->mainGrid()->cellCount();
// Test case 1: Basic visibility generation with small bounds
cvf::Vec3st minIjk( 5, 5, 5 );
cvf::Vec3st maxIjk( 10, 10, 8 );
auto visibility = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), minIjk, maxIjk );
ASSERT_FALSE( visibility.isNull() ) << "Visibility array should not be null";
ASSERT_EQ( visibility->size(), totalCellCount ) << "Visibility array should match total cell count";
// Count visible cells and verify they match expected IJK range
size_t expectedVisibleCells = ( maxIjk.x() - minIjk.x() + 1 ) * ( maxIjk.y() - minIjk.y() + 1 ) * ( maxIjk.z() - minIjk.z() + 1 );
size_t actualVisibleCells = 0;
for ( size_t i = 0; i < totalCellCount; ++i )
{
if ( visibility->val( i ) )
{
actualVisibleCells++;
}
}
ASSERT_EQ( actualVisibleCells, expectedVisibleCells ) << "Number of visible cells should match IJK range volume";
// Test case 2: Verify specific cells within bounds are visible
for ( size_t i = minIjk.x(); i <= maxIjk.x(); ++i )
{
for ( size_t j = minIjk.y(); j <= maxIjk.y(); ++j )
{
for ( size_t k = minIjk.z(); k <= maxIjk.z(); ++k )
{
size_t cellIndex = eclipseCase->mainGrid()->cellIndexFromIJK( i, j, k );
ASSERT_TRUE( visibility->val( cellIndex ) ) << "Cell at IJK (" << i << "," << j << "," << k << ") should be visible";
}
}
}
// Test case 3: Verify cells outside bounds are invisible
for ( size_t i = 0; i < std::min( minIjk.x(), size_t( 3 ) ); ++i )
{
for ( size_t j = 0; j < std::min( minIjk.y(), size_t( 3 ) ); ++j )
{
for ( size_t k = 0; k < std::min( minIjk.z(), size_t( 3 ) ); ++k )
{
size_t cellIndex = eclipseCase->mainGrid()->cellIndexFromIJK( i, j, k );
ASSERT_FALSE( visibility->val( cellIndex ) ) << "Cell at IJK (" << i << "," << j << "," << k << ") should be invisible";
}
}
}
// Test case 4: Edge case - single cell bounds
cvf::Vec3st singleCellMin( 2, 2, 2 );
cvf::Vec3st singleCellMax( 2, 2, 2 );
auto singleCellVisibility = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), singleCellMin, singleCellMax );
ASSERT_FALSE( singleCellVisibility.isNull() ) << "Single cell visibility should not be null";
size_t singleCellVisibleCount = 0;
for ( size_t i = 0; i < totalCellCount; ++i )
{
if ( singleCellVisibility->val( i ) )
{
singleCellVisibleCount++;
}
}
ASSERT_EQ( singleCellVisibleCount, 1 ) << "Exactly one cell should be visible for single cell bounds";
// Verify the correct single cell is visible
size_t expectedSingleCellIndex = eclipseCase->mainGrid()->cellIndexFromIJK( 2, 2, 2 );
ASSERT_TRUE( singleCellVisibility->val( expectedSingleCellIndex ) ) << "The specified single cell should be visible";
// Test case 5: Error handling - invalid inputs
auto invalidVisibility1 = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( nullptr, minIjk, maxIjk );
ASSERT_TRUE( invalidVisibility1.isNull() ) << "Null case data should return null visibility";
auto invalidVisibility2 = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), cvf::Vec3st::UNDEFINED, maxIjk );
ASSERT_TRUE( invalidVisibility2.isNull() ) << "Undefined minIjk should return null visibility";
auto invalidVisibility3 = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), minIjk, cvf::Vec3st::UNDEFINED );
ASSERT_TRUE( invalidVisibility3.isNull() ) << "Undefined maxIjk should return null visibility";
// Test case 6: Boundary conditions - bounds at grid limits
cvf::Vec3st gridMinBounds( 0, 0, 0 );
cvf::Vec3st gridMaxBounds( gridCellCountI - 1, gridCellCountJ - 1, gridCellCountK - 1 );
auto fullGridVisibility = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), gridMinBounds, gridMaxBounds );
ASSERT_FALSE( fullGridVisibility.isNull() ) << "Full grid visibility should not be null";
size_t fullGridVisibleCount = 0;
for ( size_t i = 0; i < totalCellCount; ++i )
{
if ( fullGridVisibility->val( i ) )
{
fullGridVisibleCount++;
}
}
ASSERT_EQ( fullGridVisibleCount, totalCellCount ) << "All cells should be visible when using full grid bounds";
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RigEclipseCaseDataToolsTest, GenerateBorderResultFromIjkBounds )
{
// Load the BRUGGE test model using RifReaderEclipseOutput
QDir baseFolder( TEST_MODEL_DIR );
bool subFolderExists = baseFolder.cd( "Case_with_10_timesteps/Real0" );
ASSERT_TRUE( subFolderExists ) << "Could not find test model directory";
QString filename( "BRUGGE_0000.EGRID" );
QString filePath = baseFolder.absoluteFilePath( filename );
ASSERT_TRUE( QFile::exists( filePath ) ) << "BRUGGE test model file does not exist: " << filePath.toStdString();
std::unique_ptr<RimEclipseResultCase> resultCase( new RimEclipseResultCase );
cvf::ref<RigEclipseCaseData> eclipseCase = new RigEclipseCaseData( resultCase.get() );
cvf::ref<RifReaderEclipseOutput> readerInterfaceEcl = new RifReaderEclipseOutput;
bool success = readerInterfaceEcl->open( filePath, eclipseCase.p() );
ASSERT_TRUE( success ) << "Could not load BRUGGE test model";
resultCase->setReservoirData( eclipseCase.p() );
// Define IJK bounds for testing
cvf::Vec3st minIjk( 20, 20, 5 );
cvf::Vec3st maxIjk( 30, 30, 8 );
// Create visibility from IJK bounds
auto visibility = RigEclipseCaseDataTools::createVisibilityFromIjkBounds( eclipseCase.p(), minIjk, maxIjk );
ASSERT_FALSE( visibility.isNull() ) << "Visibility should be created successfully";
// Generate border result using the custom visibility
RigEclipseResultTools::generateBorderResult( resultCase.get(), visibility, "BORDNUM" );
// Verify that the result was created
auto resultsData = resultCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
ASSERT_NE( resultsData, nullptr ) << "Results data should not be null";
RigEclipseResultAddress resultAddress( RiaDefines::ResultCatType::GENERATED, RiaDefines::ResultDataType::INTEGER, "BORDNUM" );
ASSERT_TRUE( resultsData->hasResultEntry( resultAddress ) ) << "BORDNUM result should exist";
// Get the result data and verify it contains expected values
const std::vector<double>& resultVector = resultsData->cellScalarResults( resultAddress, 0 );
ASSERT_GT( resultVector.size(), 0 ) << "Result vector should not be empty";
// Count cells with different values
int invisibleCells = 0;
int borderCells = 0;
int interiorCells = 0;
for ( double value : resultVector )
{
int intValue = static_cast<int>( value );
if ( intValue == RigEclipseResultTools::BorderType::INVISIBLE_CELL )
invisibleCells++;
else if ( intValue == RigEclipseResultTools::BorderType::BORDER_CELL )
borderCells++;
else if ( intValue == RigEclipseResultTools::BorderType::INTERIOR_CELL )
interiorCells++;
}
// Verify that we have all three types of cells
ASSERT_GT( invisibleCells, 0 ) << "Should have invisible cells (value 0)";
ASSERT_GT( borderCells, 0 ) << "Should have border cells (value 1)";
ASSERT_GE( interiorCells, 0 ) << "May have interior cells (value 2)";
// The total should match the number of active cells
int totalCells = invisibleCells + borderCells + interiorCells;
ASSERT_EQ( totalCells, static_cast<int>( resultVector.size() ) ) << "Total should match result vector size";
}