Files
ResInsight/ApplicationLibCode/FileInterface/RifRoffFileTools.cpp
T
Magne Sjaastad 11d8b926bb #15110: Add node table API to RigMainGrid
Encapsulate the node table of RigMainGrid behind nodes() and modifiableNodes() returning spans, allocateNodes() and clearNodes(). Readers and LGR builders fill the node table in place through allocateNodes() instead of building a temporary vector, which reduces peak import memory by about 1.6 GiB for a 9.8M cell grid. The radial grid conversion and map axes transform update nodes in place.
2026-11-05 18:33:07 +01:00

977 lines
40 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2022- 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 "RifRoffFileTools.h"
#include "RiaApplication.h"
#include "RiaFractureDefines.h"
#include "RiaLogging.h"
#include "RiaQStringFormatter.h"
#include "RiaResultNames.h"
#include "RicFaciesPropertiesImportTools.h"
#include "RigActiveCellInfo.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h"
#include "RimColorLegendCollection.h"
#include "RimEclipseCase.h"
#include "RimProject.h"
#include "cafProgressInfo.h"
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <vector>
#include "Reader.hpp"
#ifdef USE_OPENMP
#include <omp.h>
#endif
using namespace std::chrono;
//--------------------------------------------------------------------------------------------------
/// NOTE: Moved from header file due to compile header when using RifRoffFileTools from
/// ApplicationLibCode/Commands
//--------------------------------------------------------------------------------------------------
template <typename IN, typename OUT>
static void convertToReservoirIndexOrder( int nx, int ny, int nz, const std::vector<IN>& in, std::vector<OUT>& out )
{
CAF_ASSERT( static_cast<size_t>( nx * ny * nz ) == in.size() );
out.resize( in.size(), -1 );
int outIdx = 0;
for ( int k = 0; k < nz; k++ )
{
for ( int j = 0; j < ny; j++ )
{
for ( int i = 0; i < nx; i++ )
{
int inIdx = i * ny * nz + j * nz + ( nz - k - 1 );
out[outIdx] = static_cast<OUT>( in[inIdx] );
outIdx++;
}
}
}
}
//--------------------------------------------------------------------------------------------------
/// Constructor
//--------------------------------------------------------------------------------------------------
RifRoffFileTools::RifRoffFileTools()
{
}
//--------------------------------------------------------------------------------------------------
/// Destructor
//--------------------------------------------------------------------------------------------------
RifRoffFileTools::~RifRoffFileTools()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RifRoffFileTools::openGridFile( const QString& fileName, RigEclipseCaseData* eclipseCase, QString* errorMessages )
{
RiaLogging::info( std::format( "Opening roff file: {}", fileName ) );
std::string filename = fileName.toStdString();
std::ifstream stream( filename, std::ios::binary );
if ( !stream.good() )
{
if ( errorMessages ) *errorMessages = QString( "Unable to open roff file" );
return false;
}
return openGridFile( stream, eclipseCase, errorMessages );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RifRoffFileTools::openGridFile( std::istream& stream, RigEclipseCaseData* eclipseCase, QString* errorMessages )
{
auto getInt = []( auto values, const std::string& name )
{
auto v = std::find_if( values.begin(), values.end(), [&name]( const auto& arg ) { return arg.first == name; } );
if ( v != values.end() )
return std::get<int>( v->second );
else
throw std::runtime_error( "Missing parameter (integer): " + name );
};
auto getFloat = []( auto values, const std::string& name )
{
auto v = std::find_if( values.begin(), values.end(), [&name]( const auto& arg ) { return arg.first == name; } );
if ( v != values.end() )
return std::get<float>( v->second );
else
throw std::runtime_error( "Missing parameter (float): " + name );
};
try
{
const auto totalStart = high_resolution_clock::now();
roff::Reader reader( stream );
reader.parse();
const auto tokenizeDone = high_resolution_clock::now();
std::vector<std::pair<std::string, roff::RoffScalar>> values = reader.scalarNamedValues();
std::vector<std::pair<std::string, roff::Token::Kind>> arrayTypes = reader.getNamedArrayTypes();
int nx = getInt( values, "dimensions.nX" );
int ny = getInt( values, "dimensions.nY" );
int nz = getInt( values, "dimensions.nZ" );
float xOffset = getFloat( values, "translate.xoffset" );
float yOffset = getFloat( values, "translate.yoffset" );
float zOffset = getFloat( values, "translate.zoffset" );
float xScale = getFloat( values, "scale.xscale" );
float yScale = getFloat( values, "scale.yscale" );
float zScale = getFloat( values, "scale.zscale" );
RiaLogging::debug( std::format( "Grid dimensions: {} {} {}", nx, ny, nz ) );
RiaLogging::debug( std::format( "Offset: {} {} {}", xOffset, yOffset, zOffset ) );
RiaLogging::debug( std::format( "Scale: {} {} {}", xScale, yScale, zScale ) );
std::vector<float> cornerLines = reader.getFloatArray( "cornerLines" + roff::Parser::postFixData() );
std::vector<float> zValues = reader.getFloatArray( "zvalues" + roff::Parser::postFixData() );
std::vector<char> splitEnz = reader.getByteArray( "zvalues.splitEnz" );
std::vector<char> active = reader.getByteArray( "active" + roff::Parser::postFixData() );
const auto parsingDone = high_resolution_clock::now();
unsigned int zCornerSize = static_cast<unsigned int>( ( nx + 1 ) * ( ny + 1 ) * ( nz + 1 ) * 4u );
std::vector<float> zCorners( zCornerSize, 0.0 );
interpretSplitenzData( static_cast<int>( nz ) + 1, zOffset, zScale, splitEnz, zValues, zCorners );
RigActiveCellInfo* activeCellInfo = eclipseCase->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
CAF_ASSERT( activeCellInfo );
RigActiveCellInfo* fractureActiveCellInfo = eclipseCase->activeCellInfo( RiaDefines::PorosityModelType::FRACTURE_MODEL );
CAF_ASSERT( fractureActiveCellInfo );
RigMainGrid* mainGrid = eclipseCase->mainGrid();
CAF_ASSERT( mainGrid );
mainGrid->setCellCounts( cvf::Vec3st( nx, ny, nz ) );
mainGrid->setGridName( "Main grid" );
size_t totalCellCount = nx * ny * nz;
activeCellInfo->setGridCount( 1 );
fractureActiveCellInfo->setGridCount( 1 );
activeCellInfo->setReservoirCellCount( totalCellCount );
fractureActiveCellInfo->setReservoirCellCount( totalCellCount );
// Reserve room for the cells and nodes and fill them with data
mainGrid->reservoirCells().reserve( totalCellCount );
int progTicks = 100;
caf::ProgressInfo progInfo( progTicks, "" );
int cellCount = static_cast<int>( totalCellCount );
size_t cellStartIndex = mainGrid->reservoirCells().size();
size_t nodeStartIndex = mainGrid->nodes().size();
RigCell defaultCell;
defaultCell.setHostGrid( mainGrid );
mainGrid->reservoirCells().resize( cellStartIndex + cellCount, defaultCell );
std::span<cvf::Vec3d> gridNodes = mainGrid->allocateNodes( static_cast<size_t>( cellCount ) * 8 );
// Swap i and j to get correct faces
const size_t cellMappingECLRi[8] = { 2, 3, 1, 0, 6, 7, 5, 4 };
cvf::Vec3d offset( xOffset, yOffset, zOffset );
cvf::Vec3d scale( xScale, yScale, zScale );
std::vector<int> activeCells;
convertToReservoirIndexOrder<char, int>( nx, ny, nz, active, activeCells );
// Precompute the active cell matrix index
size_t numActiveCells = computeActiveCellMatrixIndex( activeCells );
// Loop over cells and fill them with data
#pragma omp parallel for
for ( int gridLocalCellIndex = 0; gridLocalCellIndex < cellCount; ++gridLocalCellIndex )
{
RigCell& cell = mainGrid->cell( cellStartIndex + gridLocalCellIndex );
cell.setGridLocalCellIndex( gridLocalCellIndex );
// Active cell index
int matrixActiveIndex = activeCells[gridLocalCellIndex];
if ( matrixActiveIndex != -1 )
{
activeCellInfo->setCellResultIndex( ReservoirCellIndex( cellStartIndex + gridLocalCellIndex ),
ActiveCellIndex( matrixActiveIndex ) );
}
cell.setParentCellIndex( cvf::UNDEFINED_SIZE_T );
// Corner coordinates
for ( int cIdx = 0; cIdx < 8; ++cIdx )
{
double* point = gridNodes[(size_t)gridLocalCellIndex * 8 + cellMappingECLRi[cIdx]].ptr();
auto corner = getCorner( *mainGrid, cornerLines, zCorners, gridLocalCellIndex, cIdx, offset, scale );
point[0] = corner.x();
point[1] = corner.y();
point[2] = corner.z();
cell.cornerIndices()[cIdx] = nodeStartIndex + (size_t)gridLocalCellIndex * 8 + cIdx;
}
// Mark inactive long pyramid looking cells as invalid
cell.setInvalid( cell.isLongPyramidCell() );
}
activeCellInfo->setGridActiveCellCounts( 0, numActiveCells );
fractureActiveCellInfo->setGridActiveCellCounts( 0, 0 );
mainGrid->initAllSubGridsParentGridPointer();
activeCellInfo->computeDerivedData();
fractureActiveCellInfo->computeDerivedData();
auto gridConstructionDone = high_resolution_clock::now();
auto tokenizeDuration = duration_cast<milliseconds>( tokenizeDone - totalStart );
RiaLogging::debug( std::format( "Tokenizing: {} ms", tokenizeDuration.count() ) );
auto parsingDuration = duration_cast<milliseconds>( parsingDone - tokenizeDone );
RiaLogging::debug( std::format( "Parsing: {} ms", parsingDuration.count() ) );
auto gridConstructionDuration = duration_cast<milliseconds>( gridConstructionDone - parsingDone );
RiaLogging::debug( std::format( "Grid Construction: {} ms", gridConstructionDuration.count() ) );
auto totalDuration = duration_cast<milliseconds>( gridConstructionDone - totalStart );
RiaLogging::debug( std::format( "Total: {} ms", totalDuration.count() ) );
}
catch ( std::runtime_error& err )
{
RiaLogging::error( std::format( "Roff file import failed: {}", err.what() ) );
if ( errorMessages ) *errorMessages = QString::fromStdString( err.what() );
return false;
}
return true;
}
//--------------------------------------------------------------------------------------------------
// The indexing conventions for vertices in Roff file
//
// 1-------------0
// /| /|
// / | / | /j
// / | / | /
// 3-------------2 | *---i
// | | | | |
// | 5---------|---4 |
// | / | / |k
// | / | /
// |/ |/
// 7-------------6
// vertex indices
//
//
// The indexing conventions for vertices in ResInsight
//
// 7-------------6 |k
// /| /| | /j
// / | / | |/
// / | / | *---i
// 4-------------5 |
// | | | |
// | 3---------|---2
// | / | /
// | / | /
// |/ |/
// 0-------------1
// vertex indices
//
//--------------------------------------------------------------------------------------------------
cvf::Vec3d RifRoffFileTools::getCorner( const RigMainGrid& grid,
const std::vector<float>& cornerLines,
const std::vector<float>& zcorn,
size_t cellIdx,
int cornerIdx,
const cvf::Vec3d& offset,
const cvf::Vec3d& scale )
{
size_t iOffset = 0;
if ( cornerIdx != 1 && cornerIdx != 3 && cornerIdx != 5 && cornerIdx != 7 )
{
iOffset = 1;
}
size_t jOffset = 0;
if ( cornerIdx != 2 && cornerIdx != 3 && cornerIdx != 6 && cornerIdx != 7 )
{
jOffset = 1;
}
size_t i;
size_t j;
size_t k;
grid.ijkFromCellIndex( cellIdx, &i, &j, &k );
size_t ny = grid.cellCountJ();
size_t nz = grid.cellCountK();
size_t cOffset = ( ( i + iOffset ) * ( ny + 1 ) + ( j + jOffset ) ) * 6;
cvf::Vec3d top( cornerLines[cOffset], cornerLines[cOffset + 1], cornerLines[cOffset + 2] );
cvf::Vec3d bottom( cornerLines[cOffset + 3], cornerLines[cOffset + 4], cornerLines[cOffset + 5] );
int adjustedCornerIdx = cornerIdx;
if ( cornerIdx == 4 ) adjustedCornerIdx = 0;
if ( cornerIdx == 5 ) adjustedCornerIdx = 1;
if ( cornerIdx == 6 ) adjustedCornerIdx = 2;
if ( cornerIdx == 7 ) adjustedCornerIdx = 3;
// Find the correct offset k direction
size_t kOffset = 0;
if ( cornerIdx > 3 )
{
kOffset = 1;
}
size_t zOffset = ( ( i + iOffset ) * ( ny + 1 ) * ( nz + 1 ) + ( j + jOffset ) * ( nz + 1 ) + ( k + kOffset ) ) * 4 + adjustedCornerIdx;
double z = -zcorn[zOffset];
double x = interpolate( top, bottom, z, 0 ) + offset.x();
double y = interpolate( top, bottom, z, 1 ) + offset.y();
cvf::Vec3d result = cvf::Vec3d( x, y, z );
return result;
}
//--------------------------------------------------------------------------------------------------
/// Adapted from xtgeo
//--------------------------------------------------------------------------------------------------
double RifRoffFileTools::interpolate( const cvf::Vec3d& top, const cvf::Vec3d& bottom, double z, int idx )
{
if ( fabs( bottom[idx] - top[idx] ) > 0.01 )
{
return top[idx] - ( z - top.z() ) * ( top[idx] - bottom[idx] ) / ( bottom.z() - top.z() );
}
else
{
return top[idx];
}
}
//--------------------------------------------------------------------------------------------------
/// Adapted from xtgeo: https://github.com/equinor/xtgeo/blob/master/src/clib/xtg/grd3d_roff2xtgeo_splitenz.c
//--------------------------------------------------------------------------------------------------
void RifRoffFileTools::interpretSplitenzData( int nz,
float zoffset,
float zscale,
const std::vector<char>& splitenz,
const std::vector<float>& zdata,
std::vector<float>& zcornsv )
{
// We Read one corner line (pillar) from zdata at a time (one for each
// i,j), transform it according to zoffset and zscale, and put it in to
// zcornsv in reverse order.
// As i and j order and size is the same for both zcornsv and zdata, we can
// ignore it here and place one pillar at the time regardless of how many
// pillars there are.
size_t nzcorn = zcornsv.size();
size_t nsplitenz = splitenz.size();
size_t nzdata = zdata.size();
size_t num_row = 4 * static_cast<size_t>( nz );
if ( nzcorn % num_row != 0 ) throw std::runtime_error( "Incorrect size of zcorn." );
if ( nsplitenz != nzcorn / 4 ) throw std::runtime_error( "Incorrect size of splitenz." );
std::vector<float> pillar( num_row, 0.0 );
size_t it_zdata = 0, it_splitenz = 0, it_zcorn = 0;
while ( it_zcorn < nzcorn )
{
for ( size_t it_pillar = 0; it_pillar < num_row; )
{
char split = splitenz[it_splitenz++];
if ( split == 1 )
{
// There is one value for this corner which
// we must duplicate 4 times in zcornsv
if ( it_zdata >= nzdata ) throw std::runtime_error( "Incorrect size of zdata" );
float val = ( zdata[it_zdata++] + zoffset ) * zscale;
for ( int n = 0; n < 4; n++ )
{
pillar[it_pillar++] = val;
}
}
else if ( split == 4 )
{
// There are four value for this corner which
// we must duplicate 4 times in zcornsv
if ( it_zdata + 3 >= nzdata ) throw std::runtime_error( "Incorrect size of zdata" );
// As we place the pillar in reverse order into zcornsv,
// we must put zdata in reverse order into pillar to
// preserve n,s,w,e directions.
pillar[it_pillar + 3] = ( zdata[it_zdata++] + zoffset ) * zscale;
pillar[it_pillar + 2] = ( zdata[it_zdata++] + zoffset ) * zscale;
pillar[it_pillar + 1] = ( zdata[it_zdata++] + zoffset ) * zscale;
pillar[it_pillar] = ( zdata[it_zdata++] + zoffset ) * zscale;
it_pillar += 4;
}
else
{
throw std::runtime_error( "Unsupported split type" );
}
}
// Put the pillar into zcornsv in reverse order
for ( size_t it_pillar = num_row; it_pillar >= 1; )
{
zcornsv[it_zcorn++] = pillar[--it_pillar];
}
}
if ( it_splitenz != nsplitenz ) throw std::runtime_error( "Incorrect size of splitenz." );
if ( it_zdata != nzdata ) throw std::runtime_error( "Incorrect size of zdata" );
if ( it_zcorn != nzcorn ) throw std::runtime_error( "Incorrect size of zcorn." );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t RifRoffFileTools::computeActiveCellMatrixIndex( std::vector<int>& activeCells )
{
int activeMatrixIndex = 0;
int cellCount = static_cast<int>( activeCells.size() );
for ( int gridLocalCellIndex = 0; gridLocalCellIndex < cellCount; gridLocalCellIndex++ )
{
if ( activeCells[gridLocalCellIndex] != 0 )
{
activeCells[gridLocalCellIndex] = activeMatrixIndex;
activeMatrixIndex++;
}
else
{
activeCells[gridLocalCellIndex] = -1;
}
}
return activeMatrixIndex;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::pair<bool, std::map<QString, QString>> RifRoffFileTools::createInputProperties( const QString& fileName,
RigEclipseCaseData* eclipseCaseData )
{
RiaLogging::info( std::format( "Reading properties from roff file: {}", fileName ) );
std::ifstream stream( fileName.toStdString(), std::ios::binary );
if ( !stream.good() )
{
RiaLogging::error( "Unable to open roff file" );
return std::make_pair( false, std::map<QString, QString>{} );
}
return createInputProperties( stream, eclipseCaseData, fileName );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::pair<bool, std::map<QString, QString>> RifRoffFileTools::createInputProperties( std::istream& stream,
RigEclipseCaseData* eclipseCaseData,
const QString& sourceName,
RiaDefines::ResultCatType resultCategory )
{
std::map<QString, QString> keywordMapping;
auto codeNamesAndValuesForKeyword = []( const std::string& keyword, roff::Reader& reader ) -> std::map<int, QString>
{
const std::string codeNamesKeyword = keyword + roff::Parser::postFixCodeNames();
const std::string codeValuesKeyword = keyword + roff::Parser::postFixCodeValues();
if ( reader.getArrayLength( codeNamesKeyword ) > 0 &&
reader.getArrayLength( codeNamesKeyword ) == reader.getArrayLength( codeValuesKeyword ) )
{
const auto fileCodeNames = reader.getStringArray( codeNamesKeyword );
const auto fileCodeValues = reader.getIntArray( codeValuesKeyword );
std::map<int, QString> codeNamesAndValues;
for ( size_t i = 0; i < fileCodeNames.size(); i++ )
{
codeNamesAndValues[fileCodeValues[i]] = QString::fromStdString( fileCodeNames[i] ).trimmed();
}
return codeNamesAndValues;
};
return {};
};
try
{
roff::Reader reader( stream );
reader.parse();
std::vector<std::pair<std::string, roff::Token::Kind>> arrayTypes = reader.getNamedArrayTypes();
for ( auto [keyword, kind] : arrayTypes )
{
size_t keywordLength = reader.getArrayLength( keyword );
RiaLogging::debug(
std::format( "Array found: '{}'. Type: {} with size: {}.", keyword, roff::Token::kindToString( kind ), keywordLength ) );
QString keywordUpperCase = QString::fromStdString( keyword ).toUpper();
if ( keyword == "subgrids.nLayers" )
{
if ( !appendZoneIndexPropertyFromSubgrids( eclipseCaseData, reader, keywordMapping ) )
{
RiaLogging::warning( std::format( "Unable to import ROFF subgrids zonation from {}", sourceName ) );
}
}
else if ( eclipseCaseData->mainGrid()->cellCount() == keywordLength )
{
QString newResultName = eclipseCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )
->makeResultNameUnique( QString::fromStdString( keyword ) );
// Special handling for active.data ==> ACTNUM
if ( newResultName == QString( "active" ) + QString::fromStdString( roff::Parser::postFixData() ) )
{
newResultName = "ACTNUM";
}
if ( !appendNewInputPropertyResult( eclipseCaseData, newResultName, keyword, kind, reader, resultCategory ) )
{
RiaLogging::error( std::format( "Unable to import result '{}' from {}", keyword, sourceName ) );
return std::make_pair( false, keywordMapping );
}
const auto codeNames = codeNamesAndValuesForKeyword( keyword, reader );
bool anyValidCategoryName = false;
for ( const auto& codeName : codeNames )
{
if ( !codeName.second.isEmpty() )
{
anyValidCategoryName = true;
}
}
if ( anyValidCategoryName )
{
RimColorLegendCollection* colorLegendCollection = RimProject::current()->colorLegendCollection;
auto rimCase = eclipseCaseData->ownerCase();
// Update existing color legend in place, or create one, with values from file
if ( keywordUpperCase == RiaResultNames::facies() )
{
const auto colors = RicFaciesPropertiesImportTools::matchDefaultRockColors( codeNames );
colorLegendCollection->updateColorLegend( rimCase, newResultName, RiaDefines::faciesColorLegendName(), codeNames, colors );
}
else
{
colorLegendCollection->updateColorLegend( rimCase, newResultName, newResultName, codeNames );
}
colorLegendCollection->updateAllRequiredEditors();
}
keywordMapping[QString::fromStdString( keyword )] = newResultName;
}
else if ( keywordUpperCase == RiaResultNames::facies() )
{
// We have the definition of facies name and value, but we do not have values for cells. Create color legend and get values
// from other sources, i.e. Eclipse result files
const auto codeNames = codeNamesAndValuesForKeyword( keyword, reader );
RicFaciesPropertiesImportTools::createColorLegendMatchDefaultRockColors( codeNames );
}
else
{
// Skipped: typically grid metadata, but a property array whose length does not match the grid
// cell count is also skipped here. Log it so a size mismatch does not fail silently.
RiaLogging::debug( std::format( "Skipping roff array '{}' (length {}), grid cell count is {}.",
keyword,
keywordLength,
eclipseCaseData->mainGrid()->cellCount() ) );
}
}
}
catch ( std::runtime_error& err )
{
RiaLogging::error( std::format( "Roff property file import failed: {}", err.what() ) );
return std::make_pair( false, keywordMapping );
}
if ( keywordMapping.empty() )
{
RiaLogging::warning( std::format( "No grid properties matching the grid cell count ({}) were imported from {}.",
eclipseCaseData->mainGrid()->cellCount(),
sourceName.toStdString() ) );
}
return std::make_pair( true, keywordMapping );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RifRoffFileTools::hasGridData( const QString& filename )
{
// Check if arrayTypes contains grid info
// - Alt 1. Look for tag "cornerLines" and its data with keyword "cornerLines.data" - actual representation of grid data
// - Alt 2. Look for tag "filedata" and its filetype with keyword "filedata.filetype" - should be equal "grid" (not as robust, rather
// look for "cornerLines" which is grid representation)
std::ifstream stream( filename.toStdString(), std::ios::binary );
if ( !stream.good() )
{
RiaLogging::error( "Unable to open roff file" );
return false;
}
try
{
roff::Reader reader( stream );
reader.parse();
const std::vector<std::pair<std::string, roff::Token::Kind>> arrayTypes = reader.getNamedArrayTypes();
const std::string cornerLinesDataKeyword = "cornerLines" + roff::Parser::postFixData();
auto cornerLinesDataItr = std::find_if( arrayTypes.begin(),
arrayTypes.end(),
[&cornerLinesDataKeyword]( const auto& arrayType )
{ return arrayType.first == cornerLinesDataKeyword; } );
return cornerLinesDataItr != arrayTypes.end();
}
catch ( std::runtime_error& err )
{
RiaLogging::error( std::format( "Roff file parsing failed: {}", err.what() ) );
return false;
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double>
RifRoffFileTools::readAndConvertToDouble( int nx, int ny, int nz, const std::string& keyword, roff::Token::Kind kind, roff::Reader& reader )
{
std::vector<double> doubleVals;
if ( kind == roff::Token::Kind::FLOAT )
{
std::vector<float> values = reader.getFloatArray( keyword );
convertToReservoirIndexOrder<float, double>( nx, ny, nz, values, doubleVals );
}
else if ( kind == roff::Token::Kind::BOOL || kind == roff::Token::Kind::BYTE )
{
std::vector<char> values = reader.getByteArray( keyword );
convertToReservoirIndexOrder<char, double>( nx, ny, nz, values, doubleVals );
}
else if ( kind == roff::Token::Kind::INT )
{
std::vector<int> values = reader.getIntArray( keyword );
convertToReservoirIndexOrder<int, double>( nx, ny, nz, values, doubleVals );
}
else
{
RiaLogging::error( std::format( "Unsupported property type '{}' for keyword '{}'.", roff::Token::kindToString( kind ), keyword ) );
}
return doubleVals;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RifRoffFileTools::propertyValuesFromStream( std::istream& stream,
RigEclipseCaseData* eclipseCaseData,
const QString& propertyName,
std::vector<double>* values )
{
if ( !eclipseCaseData || !eclipseCaseData->mainGrid() || !values ) return false;
auto mainGrid = eclipseCaseData->mainGrid();
const int nx = static_cast<int>( mainGrid->cellCountI() );
const int ny = static_cast<int>( mainGrid->cellCountJ() );
const int nz = static_cast<int>( mainGrid->cellCountK() );
const size_t cellCount = mainGrid->cellCount();
try
{
roff::Reader reader( stream );
reader.parse();
// Collect the arrays whose length matches the grid cell count. A Sumo property blob may contain more
// than one such array, so pick the one whose keyword matches the requested property name rather than
// blindly taking the first match (which could decode a different property).
std::vector<std::pair<std::string, roff::Token::Kind>> candidates;
for ( const auto& [keyword, kind] : reader.getNamedArrayTypes() )
{
if ( reader.getArrayLength( keyword ) == cellCount ) candidates.push_back( { keyword, kind } );
}
if ( candidates.empty() ) return false;
std::string candidateNames;
for ( const auto& [keyword, kind] : candidates )
{
if ( !candidateNames.empty() ) candidateNames += ", ";
candidateNames += keyword;
}
auto selected = candidates.front();
for ( const auto& candidate : candidates )
{
if ( QString::fromStdString( candidate.first ).compare( propertyName, Qt::CaseInsensitive ) == 0 )
{
selected = candidate;
break;
}
}
RiaLogging::debug( std::format( "Roff property '{}': arrays matching cell count [{}], using '{}'.",
propertyName.toStdString(),
candidateNames,
selected.first ) );
std::vector<double> roffValues = readAndConvertToDouble( nx, ny, nz, selected.first, selected.second, reader );
if ( roffValues.size() != cellCount ) return false;
// Set better invalid value for inactive cells: roff file has -999.
auto activeCellInfo = eclipseCaseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
for ( size_t i = 0; i < cellCount; i++ )
{
if ( !activeCellInfo->isActive( ReservoirCellIndex( mainGrid->reservoirCellIndex( i ) ) ) )
{
roffValues[i] = HUGE_VAL;
}
}
*values = std::move( roffValues );
return true;
}
catch ( std::runtime_error& err )
{
RiaLogging::error( std::format( "Roff property parsing failed: {}", err.what() ) );
return false;
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RifRoffFileTools::appendNewInputPropertyResult( RigEclipseCaseData* caseData,
const QString& resultName,
const std::string& keyword,
roff::Token::Kind kind,
roff::Reader& reader,
RiaDefines::ResultCatType resultCategory )
{
CAF_ASSERT( caseData );
int nx = static_cast<int>( caseData->mainGrid()->cellCountI() );
int ny = static_cast<int>( caseData->mainGrid()->cellCountJ() );
int nz = static_cast<int>( caseData->mainGrid()->cellCountK() );
std::vector<double> values = readAndConvertToDouble( nx, ny, nz, keyword, kind, reader );
auto mainGrid = caseData->mainGrid();
if ( values.size() != mainGrid->cellCount() ) return false;
// Set better invalid value for inactive cells: roff file has -999
auto activeCellInfo = caseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
size_t cellCount = mainGrid->cellCount();
for ( size_t i = 0; i < cellCount; i++ )
{
if ( !activeCellInfo->isActive( ReservoirCellIndex( mainGrid->reservoirCellIndex( i ) ) ) )
{
values[i] = HUGE_VAL;
}
}
RigEclipseResultAddress resAddr( resultCategory, RifRoffFileTools::mapFromType( kind ), resultName );
caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->createResultEntry( resAddr, false );
auto newPropertyData = caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->modifiableCellScalarResultTimesteps( resAddr );
newPropertyData->push_back( values );
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 )
{
CAF_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(
std::format( "ROFF subgrids.nLayers could not be expanded to grid K dimension ({}). Skipping zonation import.", 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->updateColorLegend( rimCase, resultName, resultName, codeNames );
colorLegendCollection->updateAllRequiredEditors();
}
}
return true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaDefines::ResultDataType RifRoffFileTools::mapFromType( roff::Token::Kind kind )
{
switch ( kind )
{
case roff::Token::Kind::BOOL:
return RiaDefines::ResultDataType::INTEGER;
case roff::Token::Kind::BYTE:
return RiaDefines::ResultDataType::INTEGER;
case roff::Token::Kind::INT:
return RiaDefines::ResultDataType::INTEGER;
}
return RiaDefines::ResultDataType::FLOAT;
}