mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-27 05:37:21 -05:00
#13863 Roff: Load grid zonation (subgrids) as SUBGRIDS input property
Read the ROFF "subgrids.nLayers" array and expand it into a 1-based per-cell integer property (SUBGRIDS) so ROFF zonation becomes a regular grid property. If a companion "subgrids.names" array is present, zone names are surfaced as category code-names via the existing color-legend flow.
This commit is contained in:
@@ -550,7 +550,14 @@ std::pair<bool, std::map<QString, QString>> RifRoffFileTools::createInputPropert
|
||||
|
||||
QString keywordUpperCase = QString::fromStdString( keyword ).toUpper();
|
||||
|
||||
if ( eclipseCaseData->mainGrid()->cellCount() == keywordLength )
|
||||
if ( keyword == "subgrids.nLayers" )
|
||||
{
|
||||
if ( !appendZoneIndexPropertyFromSubgrids( eclipseCaseData, reader, keywordMapping ) )
|
||||
{
|
||||
RiaLogging::warning( QString( "Unable to import ROFF subgrids zonation from %1" ).arg( fileName ) );
|
||||
}
|
||||
}
|
||||
else if ( eclipseCaseData->mainGrid()->cellCount() == keywordLength )
|
||||
{
|
||||
QString newResultName = eclipseCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )
|
||||
->makeResultNameUnique( QString::fromStdString( keyword ) );
|
||||
@@ -725,6 +732,128 @@ bool RifRoffFileTools::appendNewInputPropertyResult( RigEclipseCaseData* caseDat
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<double> RifRoffFileTools::computeZoneValuesFromSubgrids( const std::vector<int>& nLayers, size_t nx, size_t ny, size_t nz )
|
||||
{
|
||||
if ( nLayers.empty() ) return {};
|
||||
|
||||
size_t layerSum = 0;
|
||||
for ( int n : nLayers )
|
||||
{
|
||||
if ( n <= 0 ) return {};
|
||||
layerSum += static_cast<size_t>( n );
|
||||
}
|
||||
if ( layerSum != nz ) return {};
|
||||
|
||||
// K→zone lookup in ROFF k-order (zone numbers are 1-based).
|
||||
std::vector<int> roffKToZone( nz );
|
||||
size_t kRoff = 0;
|
||||
for ( size_t z = 0; z < nLayers.size(); ++z )
|
||||
{
|
||||
const int zoneValue = static_cast<int>( z ) + 1;
|
||||
for ( int i = 0; i < nLayers[z]; ++i )
|
||||
{
|
||||
roffKToZone[kRoff++] = zoneValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-cell values in reservoir index order; apply the same K-flip as convertToReservoirIndexOrder.
|
||||
std::vector<double> values( nx * ny * nz );
|
||||
size_t outIdx = 0;
|
||||
for ( size_t k = 0; k < nz; ++k )
|
||||
{
|
||||
const int zone = roffKToZone[nz - k - 1];
|
||||
for ( size_t j = 0; j < ny; ++j )
|
||||
{
|
||||
for ( size_t i = 0; i < nx; ++i )
|
||||
{
|
||||
values[outIdx++] = static_cast<double>( zone );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RifRoffFileTools::appendZoneIndexPropertyFromSubgrids( RigEclipseCaseData* caseData,
|
||||
roff::Reader& reader,
|
||||
std::map<QString, QString>& keywordMapping )
|
||||
{
|
||||
CVF_ASSERT( caseData );
|
||||
|
||||
auto* mainGrid = caseData->mainGrid();
|
||||
if ( !mainGrid ) return false;
|
||||
|
||||
const size_t nx = mainGrid->cellCountI();
|
||||
const size_t ny = mainGrid->cellCountJ();
|
||||
const size_t nz = mainGrid->cellCountK();
|
||||
|
||||
const std::string nLayersKeyword = "subgrids.nLayers";
|
||||
if ( reader.getArrayLength( nLayersKeyword ) == 0 ) return false;
|
||||
|
||||
const std::vector<int> nLayers = reader.getIntArray( nLayersKeyword );
|
||||
|
||||
std::vector<double> values = computeZoneValuesFromSubgrids( nLayers, nx, ny, nz );
|
||||
if ( values.empty() )
|
||||
{
|
||||
RiaLogging::warning(
|
||||
QString( "ROFF subgrids.nLayers could not be expanded to grid K dimension (%1). Skipping zonation import." ).arg( nz ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* activeCellInfo = caseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
|
||||
const size_t cellCount = mainGrid->cellCount();
|
||||
for ( size_t cellIdx = 0; cellIdx < cellCount; ++cellIdx )
|
||||
{
|
||||
if ( !activeCellInfo->isActive( ReservoirCellIndex( mainGrid->reservoirCellIndex( cellIdx ) ) ) )
|
||||
{
|
||||
values[cellIdx] = HUGE_VAL;
|
||||
}
|
||||
}
|
||||
|
||||
const QString resultName = caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->makeResultNameUnique( QString( "SUBGRIDS" ) );
|
||||
|
||||
RigEclipseResultAddress resAddr( RiaDefines::ResultCatType::INPUT_PROPERTY, RiaDefines::ResultDataType::INTEGER, resultName );
|
||||
caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->createResultEntry( resAddr, false );
|
||||
|
||||
auto newPropertyData = caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->modifiableCellScalarResultTimesteps( resAddr );
|
||||
newPropertyData->push_back( values );
|
||||
|
||||
keywordMapping[QString::fromStdString( nLayersKeyword )] = resultName;
|
||||
|
||||
// Attach zone names as category code-names when the file provides a companion names array.
|
||||
const std::string namesKeyword = "subgrids.names";
|
||||
if ( reader.getArrayLength( namesKeyword ) == nLayers.size() )
|
||||
{
|
||||
// Skip legend creation when the application/project is not initialized (e.g. early-init or non-UI contexts).
|
||||
auto project = RiaApplication::instance() ? RimProject::current() : nullptr;
|
||||
auto colorLegendCollection = project ? project->colorLegendCollection() : nullptr;
|
||||
if ( colorLegendCollection )
|
||||
{
|
||||
const auto zoneNames = reader.getStringArray( namesKeyword );
|
||||
std::map<int, QString> codeNames;
|
||||
for ( size_t z = 0; z < zoneNames.size(); ++z )
|
||||
{
|
||||
codeNames[static_cast<int>( z ) + 1] = QString::fromStdString( zoneNames[z] ).trimmed();
|
||||
}
|
||||
|
||||
auto rimCase = caseData->ownerCase();
|
||||
|
||||
colorLegendCollection->deleteColorLegend( rimCase, resultName );
|
||||
RimColorLegend* colorLegend = colorLegendCollection->createColorLegend( resultName, codeNames );
|
||||
colorLegendCollection->setDefaultColorLegendForResult( rimCase, resultName, colorLegend );
|
||||
colorLegendCollection->updateAllRequiredEditors();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -58,6 +58,8 @@ public:
|
||||
|
||||
static size_t computeActiveCellMatrixIndex( std::vector<int>& activeCells );
|
||||
|
||||
static std::vector<double> computeZoneValuesFromSubgrids( const std::vector<int>& nLayers, size_t nx, size_t ny, size_t nz );
|
||||
|
||||
static cvf::Vec3d getCorner( const RigMainGrid& grid,
|
||||
const std::vector<float>& cornerLines,
|
||||
const std::vector<float>& zcorn,
|
||||
@@ -85,5 +87,8 @@ private:
|
||||
roff::Token::Kind token,
|
||||
roff::Reader& reader );
|
||||
|
||||
static bool
|
||||
appendZoneIndexPropertyFromSubgrids( RigEclipseCaseData* caseData, roff::Reader& reader, std::map<QString, QString>& keywordMapping );
|
||||
|
||||
static RiaDefines::ResultDataType mapFromType( roff::Token::Kind kind );
|
||||
};
|
||||
|
||||
@@ -80,6 +80,7 @@ set(SOURCE_UNITTEST_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifSurfaceImporter-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifFormationNamesReader-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifRoffReader-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifRoffFileTools-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifElasticPropertiesReader-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigStatisticsTools-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifStimPlanXmlReader-Test.cpp
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "RifRoffFileTools.h"
|
||||
|
||||
#include "RiaTestDataDirectory.h"
|
||||
|
||||
#include "Reader.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
TEST( RifRoffFileTools, ComputeZoneValuesFromSubgridsBasic )
|
||||
{
|
||||
// 3 zones spanning 6 K-layers total. ROFF k-order is top→bottom: zone 1 at top,
|
||||
// zone 3 at bottom. After the K-flip, reservoir K=0 is the top of the file.
|
||||
const std::vector<int> nLayers = { 2, 1, 3 };
|
||||
const size_t nx = 2;
|
||||
const size_t ny = 3;
|
||||
const size_t nz = 6;
|
||||
|
||||
const auto values = RifRoffFileTools::computeZoneValuesFromSubgrids( nLayers, nx, ny, nz );
|
||||
ASSERT_EQ( nx * ny * nz, values.size() );
|
||||
|
||||
// Expected zone per reservoir K (after flipping the ROFF top-down ordering):
|
||||
// ROFF k=0,1 → zone 1 → reservoir K=5,4
|
||||
// ROFF k=2 → zone 2 → reservoir K=3
|
||||
// ROFF k=3,4,5 → zone 3 → reservoir K=2,1,0
|
||||
const std::vector<int> expectedZoneByReservoirK = { 3, 3, 3, 2, 1, 1 };
|
||||
|
||||
for ( size_t k = 0; k < nz; ++k )
|
||||
{
|
||||
for ( size_t j = 0; j < ny; ++j )
|
||||
{
|
||||
for ( size_t i = 0; i < nx; ++i )
|
||||
{
|
||||
const size_t idx = i + nx * j + nx * ny * k;
|
||||
EXPECT_EQ( static_cast<double>( expectedZoneByReservoirK[k] ), values[idx] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST( RifRoffFileTools, ComputeZoneValuesFromSubgridsRejectsSumMismatch )
|
||||
{
|
||||
// Sum of nLayers (2+1+2 = 5) does not match nz (6) -> empty result.
|
||||
const auto values = RifRoffFileTools::computeZoneValuesFromSubgrids( { 2, 1, 2 }, 1, 1, 6 );
|
||||
EXPECT_TRUE( values.empty() );
|
||||
}
|
||||
|
||||
TEST( RifRoffFileTools, ComputeZoneValuesFromSubgridsRejectsNonPositiveLayer )
|
||||
{
|
||||
const auto values = RifRoffFileTools::computeZoneValuesFromSubgrids( { 3, 0, 3 }, 1, 1, 6 );
|
||||
EXPECT_TRUE( values.empty() );
|
||||
}
|
||||
|
||||
TEST( RifRoffFileTools, ComputeZoneValuesFromSubgridsRejectsEmptyInput )
|
||||
{
|
||||
const auto values = RifRoffFileTools::computeZoneValuesFromSubgrids( {}, 1, 1, 0 );
|
||||
EXPECT_TRUE( values.empty() );
|
||||
}
|
||||
|
||||
TEST( RifRoffFileTools, ReadSubgridsFromRoffFile )
|
||||
{
|
||||
QDir baseFolder( TEST_DATA_DIR );
|
||||
QString filePath = baseFolder.absoluteFilePath( "RifRoffReader/with_subgrids.roff" );
|
||||
ASSERT_TRUE( QFile::exists( filePath ) );
|
||||
|
||||
std::ifstream stream( filePath.toStdString(), std::ios::binary );
|
||||
ASSERT_TRUE( stream.good() );
|
||||
|
||||
roff::Reader reader( stream );
|
||||
reader.parse();
|
||||
|
||||
ASSERT_EQ( 3u, reader.getArrayLength( "subgrids.nLayers" ) );
|
||||
|
||||
const std::vector<int> nLayers = reader.getIntArray( "subgrids.nLayers" );
|
||||
ASSERT_EQ( 3u, nLayers.size() );
|
||||
EXPECT_EQ( 2, nLayers[0] );
|
||||
EXPECT_EQ( 1, nLayers[1] );
|
||||
EXPECT_EQ( 3, nLayers[2] );
|
||||
|
||||
ASSERT_EQ( 3u, reader.getArrayLength( "subgrids.names" ) );
|
||||
const std::vector<std::string> names = reader.getStringArray( "subgrids.names" );
|
||||
ASSERT_EQ( 3u, names.size() );
|
||||
EXPECT_EQ( "Top Zone", names[0] );
|
||||
EXPECT_EQ( "Middle Zone", names[1] );
|
||||
EXPECT_EQ( "Bottom Zone", names[2] );
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
roff-asc
|
||||
#ROFF file with subgrids/zonation information#
|
||||
tag filedata
|
||||
int byteswaptest 1
|
||||
char filetype "parameter"
|
||||
char creationDate "01/01/2026 00:00:00"
|
||||
endtag
|
||||
tag version
|
||||
int major 2
|
||||
int minor 0
|
||||
endtag
|
||||
tag dimensions
|
||||
int nX 2
|
||||
int nY 3
|
||||
int nZ 6
|
||||
endtag
|
||||
tag subgrids
|
||||
array int nLayers 3
|
||||
2 1 3
|
||||
array char names 3
|
||||
"Top Zone"
|
||||
"Middle Zone"
|
||||
"Bottom Zone"
|
||||
endtag
|
||||
tag eof
|
||||
endtag
|
||||
Reference in New Issue
Block a user