#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:
Magne Sjaastad
2026-04-28 19:18:12 +02:00
parent 5d7d6e85ee
commit 97ed7121ed
5 changed files with 252 additions and 1 deletions
@@ -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