Fix monotonicity of depth values in extendDepthTable and add tests

Only prepend/append padding rows to RSVD/RVVD/RTEMPVD/PBVD/PDVD tables
when the existing table does not already cover the padding depth range.
This prevents non-monotonic depth sequences that caused issues in OPM.

Also expose extendDepthTable as a public static method on RigPadModel
and add unit tests for all extension scenarios.

Co-authored-by: Vegard Kippe <vkip@equinor.com>
This commit is contained in:
Kristian Bendiksen
2026-04-10 11:49:45 +02:00
co-authored by Vegard Kippe
parent 52329d567d
commit 0aee9033ac
3 changed files with 219 additions and 7 deletions
@@ -788,10 +788,12 @@ void extendEQUIL( Opm::DeckKeyword& equil )
equil.addRecord( std::move( record ) );
}
} // namespace
//--------------------------------------------------------------------------------------------------
/// Extend RSVD/RVVD/RTEMPVD/PBVD/PDVD tables (padmodel lines 832-867)
//--------------------------------------------------------------------------------------------------
void extendDepthTable( const RigModelPaddingSettings& settings, Opm::DeckKeyword& propertyVD )
void RigPadModel::extendDepthTable( const RigModelPaddingSettings& settings, Opm::DeckKeyword& propertyVD )
{
assert( !propertyVD.empty() );
@@ -804,16 +806,25 @@ void extendDepthTable( const RigModelPaddingSettings& settings, Opm::DeckKeyword
const auto& values = table.getItem( 0 ).getData<double>();
i.front().push_back( settings.topUpper() );
i.front().push_back( values[0 + 1] );
// Need at least one depth-property pair for safe indexing
if ( values.size() < 2 ) continue;
if ( settings.topUpper() < values.front() )
{
i.front().push_back( settings.topUpper() );
i.front().push_back( values[0 + 1] );
}
for ( const auto& value : values )
{
i.front().push_back( value );
}
i.front().push_back( settings.bottomLower() );
i.front().push_back( values.back() );
if ( settings.bottomLower() > values[values.size() - 2] )
{
i.front().push_back( settings.bottomLower() );
i.front().push_back( values.back() );
}
newPropertyVD.addRecord( Opm::DeckRecord{ std::move( i ) } );
}
@@ -823,9 +834,12 @@ void extendDepthTable( const RigModelPaddingSettings& settings, Opm::DeckKeyword
newPropertyVD.addRecord( std::move( first ) );
}
propertyVD = newPropertyVD;
propertyVD = std::move( newPropertyVD );
}
namespace
{
//--------------------------------------------------------------------------------------------------
/// Extend EQUIL + depth tables in SOLUTION section (padmodel lines 869-886)
//--------------------------------------------------------------------------------------------------
@@ -852,7 +866,7 @@ void extendSolution( const RigModelPaddingSettings& settings, Opm::FileDeck& fil
else if ( keyword.name() == "RSVD" || keyword.name() == "RVVD" || keyword.name() == "RTEMPVD" || keyword.name() == "PBVD" ||
keyword.name() == "PDVD" )
{
extendDepthTable( settings, const_cast<Opm::DeckKeyword&>( keyword ) );
RigPadModel::extendDepthTable( settings, const_cast<Opm::DeckKeyword&>( keyword ) );
}
}
}
@@ -27,6 +27,7 @@
namespace Opm
{
class Deck;
class DeckKeyword;
class FileDeck;
} // namespace Opm
@@ -58,4 +59,6 @@ public:
int nzLower,
double upperDefault,
double lowerDefault );
static void extendDepthTable( const RigModelPaddingSettings& settings, Opm::DeckKeyword& propertyVD );
};
@@ -18,9 +18,11 @@
#include "gtest/gtest.h"
#include "RigModelPaddingSettings.h"
#include "RigPadModel.h"
#include "opm/input/eclipse/Deck/Deck.hpp"
#include "opm/input/eclipse/Deck/DeckKeyword.hpp"
#include "opm/input/eclipse/Parser/Parser.hpp"
#include <algorithm>
@@ -309,3 +311,196 @@ SCHEDULE
// Unknown keyword should return 0.0
EXPECT_DOUBLE_EQ( 0.0, RigPadModel::getPropsDefaultValue( deck, "UNKNOWN_KEYWORD" ) );
}
//--------------------------------------------------------------------------------------------------
// Helper: parse a minimal deck with RSVD and return a mutable copy of the keyword
//--------------------------------------------------------------------------------------------------
static Opm::DeckKeyword createRsvdKeyword( const std::string& rsvdData )
{
std::string deckString = R"(
EQLDIMS
1 /
SOLUTION
RSVD
)" + rsvdData;
auto deck = Opm::Parser{}.parseString( deckString );
return deck["RSVD"][0];
}
//--------------------------------------------------------------------------------------------------
// Helper: extract depth values (every other element starting at index 0) from RSVD record
//--------------------------------------------------------------------------------------------------
static std::vector<double> getDepths( const Opm::DeckKeyword& kw, size_t recordIndex = 0 )
{
const auto& values = kw.getRecord( recordIndex ).getItem( 0 ).getData<double>();
std::vector<double> depths;
for ( size_t i = 0; i < values.size(); i += 2 )
{
depths.push_back( values[i] );
}
return depths;
}
//--------------------------------------------------------------------------------------------------
// Helper: verify strict monotonicity of depth values in an RSVD record
//--------------------------------------------------------------------------------------------------
static void verifyMonotonicDepths( const Opm::DeckKeyword& kw, size_t recordIndex = 0 )
{
auto depths = getDepths( kw, recordIndex );
for ( size_t i = 1; i < depths.size(); ++i )
{
EXPECT_LT( depths[i - 1], depths[i] ) << "Depth values must be strictly monotonic at index " << i;
}
}
//--------------------------------------------------------------------------------------------------
/// extendDepthTable: both top and bottom rows are added when table doesn't cover padding range
//--------------------------------------------------------------------------------------------------
TEST( RigPadModel, ExtendDepthTable_BothExtended )
{
// RSVD table: depths 2000, 2500, 3000 with Rs values 120, 110, 100
auto rsvd = createRsvdKeyword( " 2000.0 120.0\n 2500.0 110.0\n 3000.0 100.0 /\n" );
RigModelPaddingSettings settings;
settings.setTopUpper( 1500.0 );
settings.setBottomLower( 3500.0 );
RigPadModel::extendDepthTable( settings, rsvd );
// The function duplicates the first record for the padding equilibrium region
ASSERT_EQ( 2u, rsvd.size() );
const auto& values = rsvd.getRecord( 0 ).getItem( 0 ).getData<double>();
// 5 depth-property pairs: prepended + 3 original + appended
ASSERT_EQ( 10u, values.size() );
// Prepended row: depth=1500, property copied from first entry (120)
EXPECT_DOUBLE_EQ( 1500.0, values[0] );
EXPECT_DOUBLE_EQ( 120.0, values[1] );
// Original data preserved
EXPECT_DOUBLE_EQ( 2000.0, values[2] );
EXPECT_DOUBLE_EQ( 120.0, values[3] );
EXPECT_DOUBLE_EQ( 2500.0, values[4] );
EXPECT_DOUBLE_EQ( 110.0, values[5] );
EXPECT_DOUBLE_EQ( 3000.0, values[6] );
EXPECT_DOUBLE_EQ( 100.0, values[7] );
// Appended row: depth=3500, property copied from last entry (100)
EXPECT_DOUBLE_EQ( 3500.0, values[8] );
EXPECT_DOUBLE_EQ( 100.0, values[9] );
verifyMonotonicDepths( rsvd );
}
//--------------------------------------------------------------------------------------------------
/// extendDepthTable: table already covers top, only bottom row added
//--------------------------------------------------------------------------------------------------
TEST( RigPadModel, ExtendDepthTable_TopAlreadyCovered )
{
auto rsvd = createRsvdKeyword( " 2000.0 120.0\n 2500.0 110.0\n 3000.0 100.0 /\n" );
RigModelPaddingSettings settings;
settings.setTopUpper( 2500.0 ); // >= first depth (2000), so no top row
settings.setBottomLower( 3500.0 );
RigPadModel::extendDepthTable( settings, rsvd );
const auto& values = rsvd.getRecord( 0 ).getItem( 0 ).getData<double>();
// 4 depth-property pairs: 3 original + 1 appended
ASSERT_EQ( 8u, values.size() );
// No prepended row - starts with original data
EXPECT_DOUBLE_EQ( 2000.0, values[0] );
EXPECT_DOUBLE_EQ( 120.0, values[1] );
// Appended row
EXPECT_DOUBLE_EQ( 3500.0, values[6] );
EXPECT_DOUBLE_EQ( 100.0, values[7] );
verifyMonotonicDepths( rsvd );
}
//--------------------------------------------------------------------------------------------------
/// extendDepthTable: table already covers bottom, only top row added
//--------------------------------------------------------------------------------------------------
TEST( RigPadModel, ExtendDepthTable_BottomAlreadyCovered )
{
auto rsvd = createRsvdKeyword( " 2000.0 120.0\n 2500.0 110.0\n 3000.0 100.0 /\n" );
RigModelPaddingSettings settings;
settings.setTopUpper( 1500.0 );
settings.setBottomLower( 2800.0 ); // < last depth (3000), so no bottom row
RigPadModel::extendDepthTable( settings, rsvd );
const auto& values = rsvd.getRecord( 0 ).getItem( 0 ).getData<double>();
// 4 depth-property pairs: 1 prepended + 3 original
ASSERT_EQ( 8u, values.size() );
// Prepended row
EXPECT_DOUBLE_EQ( 1500.0, values[0] );
EXPECT_DOUBLE_EQ( 120.0, values[1] );
// No appended row - ends with original data
EXPECT_DOUBLE_EQ( 3000.0, values[6] );
EXPECT_DOUBLE_EQ( 100.0, values[7] );
verifyMonotonicDepths( rsvd );
}
//--------------------------------------------------------------------------------------------------
/// extendDepthTable: table already covers both top and bottom, no rows added
//--------------------------------------------------------------------------------------------------
TEST( RigPadModel, ExtendDepthTable_BothAlreadyCovered )
{
auto rsvd = createRsvdKeyword( " 2000.0 120.0\n 2500.0 110.0\n 3000.0 100.0 /\n" );
RigModelPaddingSettings settings;
settings.setTopUpper( 2200.0 ); // > first depth (2000)
settings.setBottomLower( 2800.0 ); // < last depth (3000)
RigPadModel::extendDepthTable( settings, rsvd );
const auto& values = rsvd.getRecord( 0 ).getItem( 0 ).getData<double>();
// Unchanged: 3 original depth-property pairs
ASSERT_EQ( 6u, values.size() );
EXPECT_DOUBLE_EQ( 2000.0, values[0] );
EXPECT_DOUBLE_EQ( 120.0, values[1] );
EXPECT_DOUBLE_EQ( 2500.0, values[2] );
EXPECT_DOUBLE_EQ( 110.0, values[3] );
EXPECT_DOUBLE_EQ( 3000.0, values[4] );
EXPECT_DOUBLE_EQ( 100.0, values[5] );
verifyMonotonicDepths( rsvd );
}
//--------------------------------------------------------------------------------------------------
/// extendDepthTable: exact boundary match - strict inequalities prevent duplicate depths
//--------------------------------------------------------------------------------------------------
TEST( RigPadModel, ExtendDepthTable_ExactBoundaryMatch )
{
auto rsvd = createRsvdKeyword( " 2000.0 120.0\n 2500.0 110.0\n 3000.0 100.0 /\n" );
RigModelPaddingSettings settings;
settings.setTopUpper( 2000.0 ); // == first depth, strict < means no prepend
settings.setBottomLower( 3000.0 ); // == last depth, strict > means no append
RigPadModel::extendDepthTable( settings, rsvd );
const auto& values = rsvd.getRecord( 0 ).getItem( 0 ).getData<double>();
// Unchanged: 3 original depth-property pairs, no duplicates
ASSERT_EQ( 6u, values.size() );
EXPECT_DOUBLE_EQ( 2000.0, values[0] );
EXPECT_DOUBLE_EQ( 3000.0, values[4] );
verifyMonotonicDepths( rsvd );
}